Current: Week 11

0%

Week 11

Week 11 — Training and inference: learn, evaluate, save, and generate with the same Mini GPT

Key questionHow do we organize training, validation, checkpoints and generation around the same Week 10 MiniGPT, and identify exactly which state each line changes?

Learning objectives

  • Trace the six fixed supervision signals through forward, loss, backward, clipping and AdamW in the correct state-change order.
  • Use ln(5)≈1.609 as a uniform reference, and token-weighted held-out loss to distinguish training performance from generalization measurement.
  • Recognize conflicting [you] contexts and derive the ideal fixed-batch mean-NLL infimum ln(2)/3≈0.231 rather than zero.
  • Reuse Week 10's exact model, tokenizer and checkpoint identity; apply τ, top-k, Softmax and multinomial to final-position logits.

100 min estimated reading time

Week progress: 0 of 23 sections (0%)Course progress: 0 of 321 sections (0%)

Week 10 computed one prediction. This week repeats that computation to learn and distinguishes training, validation and generation. Start with the minimal single-batch loop. Gradient accumulation and strict resume checks remain optional sections, not prerequisites for understanding one update.

Scroll horizontally to view all columns.

Course data table
Learning unitProblem to solve
1: Complete one updateDistinguish parameters, gradients and optimizer state; calculate two AdamW steps for one parameter.
2: Repeat and measureFirst learn fixed data, then validate on documents excluded from updates. State exactly when each log value was measured.
3: Save and loadSave model configuration, vocabulary and weights. Restoring inference is not the same as reproducing interrupted training exactly.
4: GenerateTake only the current final position's logits, adjust temperature, restrict candidates, sample, append and run forward again.

Run python week11_adamw_numbers.py to inspect the two-step calculation. Next week's complete project provides independent-document training commands. Do not call falling loss on three fixed sentences generalization. Compare probability tables before judging temperature from a single random continuation.

Alternate reading, hand calculation and code changes. Each unit can take several sessions. Section numbers remain stable for old links and references; follow the page from top to bottom rather than jumping around to restore numerical order.

Week 11 Goal: Four Jobs Around the Same MiniGPT

The model computes; Week 11's outer training/generation module decides how to use the result. Training uses labels and intentionally updates θ. Validation measures current θ against genuinely held-out labels. Checkpointing saves or restores compatible long-lived state. Inference has no targets and selects/appends tokens. English mini-gpt-v1 splits on whitespace, uses ordered tokens [you, like, AI, study, we] at IDs 0..4 and has no special, padding or unknown token. Its canonical GPTConfig is vocab_size=5, block_size=2, n_embd=4, n_head=2, n_layer=2, with untied token-embedding and output-head weights.

week11_training_and_generation.py
# week11_training_and_generation.py
# This Week 11 caller imports the frozen Week 10 implementation.
import math

import torch
import torch.nn.functional as F

from mini_gpt_walkthrough import (
    GPTConfig,
    MiniGPT,
    load_mini_gpt_for_inference,
    save_mini_gpt_training_checkpoint,
    validate_checkpoint_tokenizer_identity,
)


CANONICAL_ORDERED_TOKENS = ("you", "like", "AI", "study", "we")
CANONICAL_TOKENIZER_POLICY = (
    "whitespace-delimited;no-specials;no-pad;no-unk"
)
CANONICAL_TOKENIZER_VERSION = "mini-gpt-v1"

Scroll horizontally to view all columns.

Course data table
phaseInputs and purposeState that may changeExplicitly prohibited
traininginputs [3,2] + targets [3,2]; fit the training splitbackward changes .grad; step changes θ and AdamW stateDo not score only the final position
validationHeld-out inputs/targets; read and measure current θTemporarily change module mode, then restore it; no θ/optimizer changesNo backward or step
checkpointingSaving reads and persists parameters, optimizer, configuration, tokenizer and progress; loading validates before restoringmodel/optimizer load_state_dict explicitly replaces long-lived values; this is restoration, not learningDo not substitute matching shapes for identity checks
inferenceA prompt without targets; append one ID per roundThe caller's uncropped history growsNo loss, backward or step

Scroll horizontally to view all columns.

Course data table
stateownerWhen it changesWho reads or persists it?
parameters θmodeloptimizer.step() learns; model.load_state_dict() explicitly restoresTraining, validation and inference read it; checkpoint saving persists it
parameter.gradEach parameterbackward accumulates; zero_grad clearsoptimizer.step() reads it; the canonical checkpoint does not save transient .grad
AdamW moments / countersoptimizeroptimizer.step() learns; optimizer.load_state_dict() explicitly restoresTraining reads/updates it; checkpoints persist it for optimizer-state resumption
completed_updatestraining callerIncrement after each successful optimizer.step(); restore on checkpoint loadLogging/checkpoint saving; resumption also checks AdamW's step state
GPTConfig / tokenizer identitymodel / data callerFrozen within this run; loading validates canonical values before constructing matching objectsAll phases depend on it; checkpoints explicitly persist it
activations / loss value / training graphCurrent forward callForward creates values; with grad mode enabled, differentiable operations also build a graphCurrent training/validation computation; not persisted in the checkpoint

“Only optimizer.step() performs the learning update” still holds in this loop. load_state_dict() also replaces model/optimizer values, but that is explicit restoration, not learning from the current batch's error. Saving reads parameters, optimizer state, configuration, tokenizer and progress. Resumption validates identity before restoring the saved parameter, optimizer and progress values.

Concept sequence
  1. training:forward [3,2] → logits [3,2,5] + loss [] → backward → step
  2. Validation: held-out forward → token-loss sum / valid-token count; no update
  3. Checkpointing: validate and save/restore the same identity and long-lived state
  4. inference:prompt → last logits [B,5] → next_id [B,1] → append history
fθ:NB×TRB×T×5,B1,1T2f_{\theta}:\mathbb{N}^{B\times T}\to\mathbb{R}^{B\times T\times5},\qquad B\ge1,\quad1\le T\le2

The fixed Week 6/11 training batch has B=3,T=2, so one forward maps [3,2]→[3,2,5]: 30 raw scores, not an automatically generated sentence. Inference may use [1,2]→[1,2,5]; validation uses [B_i,T_i]→[B_i,T_i,5]. Both validation and inference should avoid gradient recording. Validation has held-out targets and aggregates loss; inference has no targets and uses only the current final-position distribution.

θAdamW(θ,θL)is the only learning update\theta\leftarrow\operatorname{AdamW}(\theta,\nabla_{\theta}\mathcal L)\quad\text{is the only learning update}

Knowledge check

Which of the four jobs may call optimizer.step() to learn?

1. One Batch Supplies Six Training Signals

A batch gives the model several independent sequences at once. Teacher forcing uses the real left-side tokens at each position, so one forward answers six next-token questions. Here B=3 batch rows, N=3 raw IDs per row before shifting, T=2 teacher-forced positions, C=4 representation channels per position and V=5 candidates per question.

Scroll horizontally to view all columns.

One vocabulary: mini-gpt-v1, V=5
IDtoken
0you
1like
2AI
3study
4we
week11_training_and_generation.py
raw_ids = torch.tensor([
    [0, 1, 2],  # you like AI
    [4, 1, 0],  # we like you
    [0, 3, 2],  # you study AI
], dtype=torch.long)  # [B,N] = [3,3]

inputs = raw_ids[:, :-1]   # [[0,1],[4,1],[0,3]], shape [3,2]
targets = raw_ids[:, 1:]   # [[1,2],[1,0],[3,2]], shape [3,2]

assert inputs.shape == targets.shape == (3, 2)
assert inputs.dtype == targets.dtype == torch.long

Scroll horizontally to view all columns.

Row-major flattening: both positions of b=0, then b=1, then b=2
PositionVisible causal contexttargetCorresponding five scores
b=0, t=0youlikelogits[0,0,:]
b=0, t=1you likeAIlogits[0,1,:]
b=1, t=0welikelogits[1,0,:]
b=1, t=1we likeyoulogits[1,1,:]
b=2, t=0youstudylogits[2,0,:]
b=2, t=1you studyAIlogits[2,1,:]

This lets hardware compute in parallel while one gradient estimate aggregates six supervision signals. logits[b,t,:] always contains five raw scores ordered [you, like, AI, study, we]. targets[b,t] is one long integer naming the correct class, not a one-hot vector.

Concept sequence
  1. raw IDs [B,N] = [3,3]
  2. Shift → inputs [3,2] and targets [3,2]
  3. MiniGPT token + position representations [3,2,4]
  4. two pre-norm Blocks + final_norm + lm_head → logits [3,2,5]
  5. Row-major reshape → logits [6,5] and targets [6]
  6. Mean of six per-position NLLs → scalar loss []
L=1BTb=1Bt=1Tlogpθ(yb,txb,t),BT=32=6\mathcal L=-\frac{1}{BT}\sum_{b=1}^{B}\sum_{t=1}^{T}\log p_{\theta}(y_{b,t}\mid x_{b,\le t}),\qquad B T=3\cdot2=6

inputs [B,T]=[3,2] → token/position representations [B,T,C]=[3,2,4] → logits [B,T,V]=[3,2,5] → logits.reshape(B×T,V)=[6,5] and targets.reshape(B×T)=[6] → scalar mean cross-entropy loss [].

Knowledge check

Why does this batch provide six target labels rather than three?

2. Step, Microbatch and Epoch: Name the Units of Progress

A microbatch is the data processed in one memory-sized forward/backward contribution. An optimizer step performs an actual parameter update. An epoch traverses the training loader once. If the three fixed sentences form one batch and accumulation_steps=1, an epoch happens to contain one step. That coincidence is not the definition.

Scroll horizontally to view all columns.

Course data table
UnitWhat happens?How Week 11 counts it
microbatchOne forward + backward contributionNot automatically an update
optimizer stepReads accumulated gradients and updates stateIncrement completed_updates by 1
epochOne full traversal of training batchesMay contain many updates
generation iterationSample and append one IDNot a training step
Nupdates/epoch=NmicrobatchesNaccumulation=1204=30N_{\mathrm{updates/epoch}}=\frac{N_{\mathrm{microbatches}}}{N_{\mathrm{accumulation}}}=\frac{120}{4}=30

Each fixed microbatch still follows [3,2]→[3,2,4]→[3,2,5]→[6,5]+[6]→[]. An epoch changes how often the computation repeats, not tensor rank. A checkpoint's completed_updates counts optimizer.step() calls already completed, not a zero-based loop index or the next step's number.

Knowledge check

With 120 microbatches and one update per four complete contributions, how many optimizer steps are in an epoch?

3. AdamW: Turn Gradient History into This Parameter Update

AdamW uses more than current gradient g_t. For each parameter coordinate, it keeps a moving average m_t of gradients and v_t of squared gradients, then uses them to scale the update. Weight decay separately shrinks weights. This is useful for noisy gradients with differing scales, but the learning rate still needs a deliberate choice.

week11_training_and_generation.py
device = torch.device(
    "cuda" if torch.cuda.is_available() else "cpu"
)
model = MiniGPT(GPTConfig()).to(device)
optimizer = torch.optim.AdamW(
    model.parameters(),
    lr=1e-3,
    weight_decay=1e-2,
)

Move the model to its device before passing its parameter objects to AdamW. Construction creates parameter groups; the first optimizer.step() with a gradient creates and updates that parameter's moment tensors and counter. optimizer.zero_grad() affects parameter.grad, not m, v or step history.

mt=β1mt1+(1β1)gtm_t=\beta_1m_{t-1}+(1-\beta_1)g_t
vt=β2vt1+(1β2)gt2v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2
θt=(1ηλ)θt1ηm^tv^t+ε\theta_t=(1-\eta\lambda)\theta_{t-1}-\eta\frac{\widehat m_t}{\sqrt{\widehat v_t}+\varepsilon}

Scroll horizontally to view all columns.

For a given weight tensor, its gradient and AdamW moment tensors have the same elementwise shape.
ObjectExample shapeWho changes it?
token_embedding.weight[5,4]optimizer.step()
token_embedding.weight.grad[5,4]backward accumulates; zero_grad clears
Corresponding AdamW m and vEach [5,4]optimizer.step()
Current logits / loss[3,2,5] / []Recomputed by forward

Knowledge check

Why restore AdamW state when continuing its history?

4. A Standard Training Step: Follow State Line by Line

week11_minimal_loop.py
# Run in the English course_examples directory: python week11_minimal_loop.py
import torch
from mini_gpt_walkthrough import GPTConfig, MiniGPT
from course_data import DEMO_DOCUMENTS, FIVE_WORD_TOKENIZER, make_windows, configure_console

configure_console()
torch.set_num_threads(1)
torch.manual_seed(7)
x, y = make_windows(DEMO_DOCUMENTS, FIVE_WORD_TOKENIZER, block_size=2)
inputs = torch.tensor(x, dtype=torch.long)
targets = torch.tensor(y, dtype=torch.long)
model = MiniGPT(GPTConfig())
optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)

for step in range(1, 101):
    model.train()
    optimizer.zero_grad(set_to_none=True)
    logits, loss = model(inputs, targets)
    if loss is None or not torch.isfinite(loss):
        raise ValueError("Non-finite loss; no parameter update performed")
    loss.backward()
    optimizer.step()
    if step == 1 or step % 20 == 0:
        print("completed_update=", step, "loss_before_this_update=", loss.item())

model.eval()
with torch.no_grad():
    _, final_loss = model(inputs, targets)
print("same_batch_loss_after_100_updates=", final_loss.item())
print("This demonstrates the pipeline on three fixed sentences, not independent validation or generalization.")

The logged loss comes from the forward before that update. Only final_loss is recomputed after all 100 updates. optimizer.step does not rewrite the old loss variable. Predict what happens if you remove step or recreate the model each round, then inspect the reusable diagnostic function below.

A training step turns a batch's supervision error into one parameter update. This function belongs to week11_training_and_generation.py and uses the imported canonical MiniGPT. It does not redefine configuration, attention, blocks, initialization or state-dict names.

week11_training_and_generation.py
def train_mini_gpt_step(
    model: MiniGPT,
    optimizer: torch.optim.AdamW,
    inputs: torch.Tensor,
    targets: torch.Tensor,
    device: torch.device,
    max_grad_norm: float = 1.0,
    completed_updates: int = 0,
) -> tuple[torch.Tensor, torch.Tensor, int]:
    if not math.isfinite(max_grad_norm) or max_grad_norm <= 0:
        raise ValueError("max_grad_norm must be positive")
    if type(completed_updates) is not int or completed_updates < 0:
        raise ValueError("completed_updates must be a non-negative integer")
    validate_mini_gpt_adamw_completed_updates(
        model,
        optimizer,
        completed_updates,
    )

    model.train()
    inputs = inputs.to(device)
    targets = targets.to(device)
    optimizer.zero_grad(set_to_none=True)
    logits, loss = model(inputs, targets)
    assert logits.shape == (*inputs.shape, model.config.vocab_size)
    assert loss is not None
    if not torch.isfinite(loss):
        raise ValueError("Non-finite training loss; stop before this update")
    loss.backward()
    grad_norm = torch.nn.utils.clip_grad_norm_(
        model.parameters(),
        max_norm=max_grad_norm,
        error_if_nonfinite=True,
    )
    optimizer.step()
    completed_updates += 1  # only after optimizer.step succeeds
    return loss.detach(), grad_norm.detach(), completed_updates

Scroll horizontally to view all columns.

Course data table
lineWhat changes or is created immediately?Why here?
model.train()Recursively sets module training flagsSelects training behavior before forward; does not update weights
inputs/targets.to(device)Creates or returns batch tensors on the requested deviceA CPU batch cannot directly multiply CUDA parameters
optimizer.zero_grad(set_to_none=True)Clears old .grad referencesPrevents unintended addition of gradients from the previous update window
model(inputs, targets)Creates activations/graph, logits [3,2,5] and loss []Parameters and AdamW state are still unchanged
loss.backward()Accumulates ∂L/∂θ in parameter.gradThe optimizer step needs current gradients first
clip_grad_norm_If necessary, scales all gradient tensors in placeLimits the norm after all intended backward contributions and before step
optimizer.step()Changes parameter values and AdamW moments/counters; increment the caller's count after successThis is the line that performs the learning update
loss.detach()Returns a logging tensor detached from the graphAvoids retaining the completed graph through logs
Concept sequence
  1. inputs/targets [3,2] on model device
  2. forward → representations [3,2,4] → raw logits [3,2,5]
  3. reshape [6,5] + [6] → scalar loss []
  4. backward → one .grad tensor per participating parameter
  5. clip complete gradient set → optimizer.step()
  6. same parameter shapes, new parameter values and AdamW state
L=CE(logits.reshape(6,5),targets.reshape(6))R\mathcal L=\operatorname{CE}(\mathrm{logits.reshape}(6,5),\mathrm{targets.reshape}(6))\in\mathbb{R}

Conceptually, the parameter update is θ←θ+Δθ. It does not retroactively change the previous forward's inputs, targets or logits. The next forward uses the new θ to calculate new logits.

The executable version checks that loss is finite before backward and requires finite gradients during clipping. NaN/Inf stops the step. Detect the numerical problem before updating parameters, not afterward in a log.

Knowledge check

After loss.backward() but before optimizer.step(), which state is new?

6. Initial Loss Reference: Derive ln(5) from Uniform Five-Way Prediction

Cross entropy measures the probability assigned to the true target. If each question has logits [0,0,0,0,0], Softmax gives [0.2,0.2,0.2,0.2,0.2] in candidate order [you, like, AI, study, we]. Whether the target is like or AI, its probability is only 0.2.

puniform(y)=1V=15=0.2p_{\mathrm{uniform}}(y)=\frac{1}{V}=\frac{1}{5}=0.2
Luniform=ln ⁣(1V)=ln(V)=ln(5)1.609\mathcal L_{\mathrm{uniform}}=-\ln\!\left(\frac{1}{V}\right)=\ln(V)=\ln(5)\approx1.609
i=15qiln ⁣(15)=ln(5)-\sum_{i=1}^{5}q_i\ln\!\left(\frac{1}{5}\right)=\ln(5)

Scroll horizontally to view all columns.

Course data table
QuantityMeaning in the fixed batchshape / value
raw equal logitsSix questions, each with a row [0,0,0,0,0][6,5]
uniform probabilitiesAll five entries in every row are 0.2[6,5]
per-position NLLSix values, each approximately 1.609[6]
mean CEMean of the six valuesScalar [], approximately 1.609

This provides a scale for reading the loss curve, not a pass/fail threshold. Randomly initialized logits need not be zero, so initial loss may be somewhat above or below 1.609. The path remains inputs [3,2] → representations [3,2,4] → logits [3,2,5] → flattened logits [6,5] and targets [6] → scalar mean CE [].

Knowledge check

With five tokens, what is per-position CE when the correct target has probability 0.2?

7. Why Validate? Measure Data That Did Not Participate in Updates

The training split supplies gradient signals; validation observes current parameters. Both use the same tokenizer, input/target alignment and CE definition, but different data sources. Validation may inform checkpoint selection, stopping, learning rate or capacity choices; do not backpropagate its batches.

Scroll horizontally to view all columns.

Course data table
train lossheld-out validation lossReasonable interpretationNext direct check
Falls while staying close to validationAlso fallsHeld-out performance on this split is improvingContinue inspecting samples and checkpoints
Keeps fallingKeeps risingPossible overfitting to training dataConsider an earlier checkpoint, more data, regularization or a smaller model
Stays near ln(5)Also near ln(5)Underfitting or a broken signal/update pathone-batch diagnostic、targets、gradients、LR
Large fluctuations or non-finite valuesAlso unstabledata/numerical/update instabilityLocate the first non-finite value; inspect learning rate and gradient norm
Ltrain=iDtrainiNtrain,Lval=jDvaljNval\mathcal L_{\mathrm{train}}=\frac{\sum_{i\in\mathcal D_{\mathrm{train}}}\ell_i}{N_{\mathrm{train}}},\qquad \mathcal L_{\mathrm{val}}=\frac{\sum_{j\in\mathcal D_{\mathrm{val}}}\ell_j}{N_{\mathrm{val}}}

Each validation batch maps inputs [B_i,T_i] to logits [B_i,T_i,5] and computes target losses. B_i may vary; T_i must be within 1..block_size=2. Across batches, sum token losses and divide by the total valid-target count. Do not equally average means from differently sized batches.

Knowledge check

What is the immediate concern if training loss keeps falling while held-out validation loss keeps rising?

8. Correct Validation: Mode, No-Grad and Token Weighting

eval() controls layer behavior; no_grad() controls backward-graph recording. Token-weighted aggregation gives a batch with six valid targets three times the weight of one with two targets—not half each.

week11_training_and_generation.py
def evaluate_mini_gpt_loss(
    model: MiniGPT,
    validation_batches,
    device: torch.device,
    ignore_index: int = -100,
) -> float:
    if 0 <= ignore_index < model.config.vocab_size:
        raise ValueError("ignore_index must not be a vocabulary ID")

    was_training = model.training
    batch_count = 0
    valid_target_count = 0
    loss_sum = 0.0
    model.eval()
    try:
        with torch.no_grad():
            for inputs, targets in validation_batches:
                batch_count += 1
                if targets.shape != inputs.shape:
                    raise ValueError("targets must match inputs shape")
                if targets.dtype != torch.long:
                    raise TypeError("targets must have dtype torch.long")

                inputs = inputs.to(device)
                targets = targets.to(device)
                valid_mask = targets.ne(ignore_index)
                batch_valid_count = int(valid_mask.sum().item())
                if batch_valid_count == 0:
                    continue

                valid_targets = targets[valid_mask]
                if (
                    int(valid_targets.min().item()) < 0
                    or int(valid_targets.max().item())
                    >= model.config.vocab_size
                ):
                    raise ValueError("valid target IDs are outside vocabulary")

                # Do not pass -100 targets into canonical MiniGPT.forward.
                logits, no_loss = model(inputs)
                assert no_loss is None
                batch_loss_sum = F.cross_entropy(
                    logits.reshape(-1, model.config.vocab_size),
                    targets.reshape(-1),
                    ignore_index=ignore_index,
                    reduction="sum",
                )
                loss_sum += batch_loss_sum.item()
                valid_target_count += batch_valid_count
    finally:
        model.train(was_training)

    if batch_count == 0:
        raise ValueError("validation_batches must not be empty")
    if valid_target_count == 0:
        raise ValueError("validation has no valid target tokens")
    return loss_sum / valid_target_count

Canonical MiniGPT rejects target ID -100, so masked validation must not call model(inputs, targets). Call model(inputs) for raw logits, then compute summed CE externally with ignore_index. Unpadded batches use the same token-weighted path. try/finally restores the previous train/eval mode even if iteration or shape validation raises an error.

Lval=m=1MiVmm,im=1MVm\mathcal L_{\mathrm{val}}=\frac{\sum_{m=1}^{M}\sum_{i\in\mathcal V_m}\ell_{m,i}}{\sum_{m=1}^{M}|\mathcal V_m|}

Scroll horizontally to view all columns.

Equal weighting of batch means is wrong when valid-token counts differ.
batchValid targetsloss sumIncorrect weight for the batch meanCorrect token weight
A66.01/26/8
B26.01/22/8
Aggregate812.0(1.0+3.0)/2=2.012.0/8=1.5
Concept sequence
  1. remember was_training
  2. model.eval() + torch.no_grad()
  3. inputs [B_i,T_i] → logits [B_i,T_i,5]; do not pass ignored targets into the model
  4. external CE reduction=sum over valid target IDs
  5. accumulate loss_sum and valid_target_count
  6. finally restore exact prior mode
  7. guard empty/all-masked → return token-weighted Python float

Knowledge check

What prevents validation forward from recording a backward graph? Can eval() replace it?

9. Gradient Clipping: Limit the Complete Gradient Before Updating

Read the L2 norm as the length of a vector: [3,4] has length √(3²+4²)=5. With a cap of 1, multiply both entries by 1/5 to obtain [0.6,0.8], length 1. Their directional ratio remains 3:4.

Clipping directly bounds gradient length. For plain SGD, update length scales with η times that length. AdamW also uses moment history and weight decay, so a gradient cap of 1 is not a guarantee that parameter-update length is at most 1.

Imagine concatenating all parameter gradients into one long vector g. If its L2 norm exceeds cap c, multiply every component by a common scale to preserve direction while limiting magnitude. This helps guard against unusually large gradients; the logged norm still matters for diagnosing their cause.

g=concat(g1,,gn),g=gmin ⁣(1,cg2+ε)g=\operatorname{concat}(g_1,\ldots,g_n),\qquad g'=g\min\!\left(1,\frac{c}{\lVert g\rVert_2+\varepsilon}\right)

Scroll horizontally to view all columns.

Course data table
raw global normcap cShared scaleResult
5.01.0Approximately 1/5Norm falls to approximately 1.0; direction is unchanged
0.61.01Gradients unchanged

In the fixed model, token_embedding.weight.grad remains [5,4] and each qkv.weight.grad remains [12,4]. Clipping is not an operation on [B,T] activations. clip_grad_norm_ returns the pre-clip norm for detached logging alongside loss. With accumulation, wait until all contributions in the window have reached .grad, then clip once.

week11_training_and_generation.py
def backward_and_clip_mini_gpt(
    model: MiniGPT,
    loss: torch.Tensor,
    max_grad_norm: float = 1.0,
) -> torch.Tensor:
    if loss.ndim != 0:
        raise ValueError("loss must be a scalar tensor")
    if not math.isfinite(max_grad_norm) or max_grad_norm <= 0:
        raise ValueError("max_grad_norm must be finite and positive")

    loss.backward()
    grad_norm = torch.nn.utils.clip_grad_norm_(
        model.parameters(),
        max_norm=max_grad_norm,
    )
    return grad_norm.detach()


# Caller order inside one update window:
# optimizer.zero_grad(...) -> forward creates loss -> helper above
# -> optimizer.step() -> increment completed_updates

Knowledge check

When should clipping occur with gradient accumulation?

10. Learning Rate: AdamW Still Needs a Global Step Scale

lr controls the scale of optimizer.step(). It does not directly change current logits [3,2,5]; it changes θ, which affects a subsequent forward. Record lr, completed_updates, loss and gradient norm, and change one experimental factor at a time.

θu+1=θu+Δθu(η,mu,vu,gu)\theta_{u+1}=\theta_u+\Delta\theta_u(\eta,m_u,v_u,g_u)

Scroll horizontally to view all columns.

Course data table
controlled-run symptomPossible learning-rate interpretationCheck first
Loss stays near ln(5) over many updates1e-6 may be too smallConfirm aligned labels, non-None gradients and actual optimizer steps
Loss falls smoothly1e-3 is a possible starting point for this tiny runKeep comparing held-out loss and samples
loss spike / oscillationMay be too largeInspect the first bad update and pre-clip gradient norm
Non-finite loss or gradientsUpdates may be unstableLocate the first NaN/Inf before adjusting learning rate

The fixed teaching run uses AdamW(..., lr=1e-3, weight_decay=1e-2), not a universal magic setting. When comparing 1e-6 with 1e-3, keep tokenizer, split, batch/accumulation policy, seed and update count fixed so the comparison isolates learning rate.

Knowledge check

What observation might suggest an excessively small learning rate in a controlled run?

11. Overfit One Batch: Identify Irreducible Label Conflicts First

Read inf as a lower bound that can be approached arbitrarily closely in the ideal distribution calculation. Identical prefixes must use one distribution but are asked for different next tokens. Splitting probability equally is optimal; both cannot receive probability 1. The 0.231 reference follows from context visibility and conflicting data, not a guarantee that this finite network and optimizer will attain it.

This diagnostic asks whether the supervision path can learn substantially, not whether the model generalizes. Repeating inputs [[0,1],[4,1],[0,3]] and targets [[1,2],[1,0],[3,2]] should produce a clear reduction from the approximate ln(5) random reference in a working, suitably configured run. Do not require deterministic causal argmax to classify all six labels correctly.

week11_training_and_generation.py
def overfit_mini_gpt_one_batch(
    model: MiniGPT,
    optimizer: torch.optim.AdamW,
    inputs: torch.Tensor,
    targets: torch.Tensor,
    device: torch.device,
    updates: int = 200,
    completed_updates: int = 0,
) -> tuple[list[float], int]:
    if type(updates) is not int or updates < 1:
        raise ValueError("updates must be a positive integer")
    if type(completed_updates) is not int or completed_updates < 0:
        raise ValueError("completed_updates must be a non-negative integer")
    validate_mini_gpt_adamw_completed_updates(
        model,
        optimizer,
        completed_updates,
    )

    model.train()
    inputs = inputs.to(device)
    targets = targets.to(device)
    history: list[float] = []
    for _ in range(updates):
        optimizer.zero_grad(set_to_none=True)
        logits, loss = model(inputs, targets)
        assert logits.shape == (3, 2, 5)
        assert loss is not None and torch.isfinite(loss)
        loss.backward()
        optimizer.step()
        completed_updates += 1  # only after optimizer.step succeeds
        history.append(loss.detach().item())
    return history, completed_updates

Scroll horizontally to view all columns.

The repeated [you] context at position t=0 must produce the same distribution.
position(s)causal contextobserved target(s)empirical optimum
b=0,t=0 and b=2,t=0[you]like, studyP(like|you)=0.5, P(study|you)=0.5
b=0,t=1[you,like]AIThis row can ideally approach probability 1 for AI
b=1,t=0[we]likeCan ideally approach probability 1 for like
b=1,t=1[we,like]youThis row can ideally approach probability 1 for you
b=2,t=1[you,study]AIThis row can ideally approach probability 1 for AI
minp+q=1[lnplnq]=2ln2atp=q=12\min_{p+q=1}\bigl[-\ln p-\ln q\bigr]=2\ln2\quad\text{at}\quad p=q=\frac12
infLbatch=2ln2+0+0+0+06=ln230.231\inf\mathcal L_{\mathrm{batch}}=\frac{2\ln2+0+0+0+0}{6}=\frac{\ln2}{3}\approx0.231
Concept sequence
  1. The same [you] prefix at t=0 → one shared five-token distribution
  2. Two conflicting labels → empirical mass 0.5 on like and 0.5 on study
  3. four distinguishable contexts → correct-label probability can approach 1
  4. mean loss falls materially below ln(5) toward, but not to, ln(2)/3
  5. Compare final logits for [you,like] and [we,like] to inspect whether context changes predictions.

Record first/last loss, finite values and gradients. When inspecting logits.argmax(dim=-1) [3,2], acknowledge that the conflicting contexts can select only one of their labels. The helper accepts an existing completed_updates count, increments only after a successful optimizer.step(), and returns cumulative progress with history. It does not pretend the requested loop length equals completed work. Compare final distributions for [you,like] and [we,like] as an additional context check: those prefixes really differ.

Knowledge check

Why can this diagnostic not require correct argmax at all six positions?

13. Inference, Training and Validation: All Use Forward, for Different Purposes

Training uses answers to compute loss and backpropagate error. Validation also has answers, but only scores them. Inference has no answer: it converts the current final-position distribution into an ID, appends it and repeats. MiniGPT.forward stays unchanged; the Week 11 caller defines the workflow.

Scroll horizontally to view all columns.

Course data table
phasemodel callgraph / modeWhat happens next?
trainingmodel(inputs [3,2], targets [3,2])train mode + Autograd graphall-position loss → backward → step
validationmodel(held_out_inputs),external summed CEeval mode + no_gradToken-weighted report; no update
inferencemodel(cropped context), without targetseval mode + no_gradlast logits → sample [B,1] → append
week11_training_and_generation.py
prompt = torch.tensor([[0, 1]], dtype=torch.long, device=device)
was_training = model.training
model.eval()
try:
    with torch.no_grad():
        prompt_logits, no_loss = model(prompt)
finally:
    model.train(was_training)

assert prompt_logits.shape == (1, 2, 5)
assert no_loss is None
next_logits = prompt_logits[:, -1, :]  # [1,5] after you like

Concept sequence
  1. training:([3,2],[3,2]) → (logits [3,2,5], loss []) → gradients → updated θ
  2. validation:held-out ([B,T],[B,T]) → logits → external loss sum/count → Python float
  3. inference:[B,T_context] → logits [B,T_context,5] → last [B,5] → next_id [B,1]
training: θθ,validation: θheld-out metric,inference: θsampled history\mathrm{training}:\ \theta\mapsto\theta',\qquad \mathrm{validation}:\ \theta\mapsto\text{held-out metric},\qquad \mathrm{inference}:\ \theta\mapsto\text{sampled history}

eval() does not remove existing .grad values; it changes module mode. no_grad() prevents this forward from recording a gradient graph, but does not clear old gradients either. Inference avoids updates because its call path contains neither loss.backward() nor optimizer.step().

Knowledge check

Which output do training and inference both receive, and when does loss exist?

14. Why Generation Uses Only the Final Position

For prompt [you, like], position 0 asks “what follows you?” Position 1 asks “what follows you like?” This generation round appends the answer to the latter. Earlier logits are useful supervised predictions during teacher-forced training, but they do not predict beyond the current prompt's end.

week11_training_and_generation.py
prompt = torch.tensor([[0, 1]], dtype=torch.long, device=device)
was_training = model.training
model.eval()
try:
    with torch.no_grad():
        logits, no_loss = model(prompt)  # targets intentionally absent
finally:
    model.train(was_training)

assert logits.shape == (1, 2, 5)
assert no_loss is None
next_logits = logits[:, -1, :]
assert next_logits.shape == (1, 5)

Scroll horizontally to view all columns.

Every five-dimensional row uses candidate order [you, like, AI, study, we].
tensor sliceshapelabelled meaning
logits[0,0,:][5]After you: five candidate scores
logits[0,1,:][5]After you like: five candidate scores
logits[:,-1,:][1,5]Source of the next-token distribution for each prompt's current final position
znext=logits:,Tcontext1,:RB×Vz_{\mathrm{next}}=\mathrm{logits}_{:,T_{\mathrm{context}}-1,:}\in\mathbb{R}^{B\times V}
pθ(xT+1xT)=softmax(logits:,1,:)p_{\theta}(x_{T+1}\mid x_{\le T})=\operatorname{softmax}(\mathrm{logits}_{:,-1,:})

For three prompts with logits [3,2,5], the same indexing gives [3,5], one independent next-ID decision per row. Here -1 indexes the final sequence position; it is not token ID -1.

Concept sequence
  1. Training: reshape all logits [3,2,5] to [6,5] and score six positions
  2. Generation: first select [:,-1,:] from logits [B,T_context,5]
  3. Next logits [B,5] then pass through τ / top-k / Softmax / sampling

Knowledge check

If logits has shape [3,2,5], what shape does logits[:,-1,:] have?

15. Temperature τ: Change the Sampling Distribution's Concentration

τ<1 enlarges logit gaps and concentrates mass on higher scores; τ>1 reduces gaps and flattens the distribution; τ=1 gives ordinary Softmax. Subtract each row's maximum first, making its largest value zero, then divide by τ. Subtracting a common constant preserves ranking and Softmax probabilities while avoiding positive exponential overflow. This is an inference-time control, not a change to parameters, AdamW state or the training objective. Reserve T for the sequence/time axis.

pi(τ)=exp((zim)/τ)j=1Vexp((zjm)/τ)=exp(zi/τ)j=1Vexp(zj/τ),m=maxjzj,τ>0p_i(\tau)=\frac{\exp((z_i-m)/\tau)}{\sum_{j=1}^{V}\exp((z_j-m)/\tau)}=\frac{\exp(z_i/\tau)}{\sum_{j=1}^{V}\exp(z_j/\tau)},\qquad m=\max_j z_j,\quad \tau>0

Scroll horizontally to view all columns.

Course data table
τ settingRelative gaps between scaled logitssampling effect
τ=0.5Original gaps doubledSharper; probability concentrates on the highest logit
τ=1UnchangedOrdinary Softmax
τ=2Original gaps halvedFlatter; lower-scoring candidates gain probability
Concept sequence
  1. finite floating last logits z [B,5]
  2. Promote float16/bfloat16 to at least float32 and validate τ>0
  3. Center z−max(z) [B,5], then divide by τ [B,5]
  4. reject non-finite centered/scaled values with a clear numeric-domain error
  5. Ranking unchanged; relative gaps change
  6. Then apply optional top-k and Softmax

Positive τ preserves ranking while changing relative probabilities. τ=0 divides by zero and τ<0 reverses ranking; neither is allowed by this API. Even with finite raw logits, an extremely small τ can overflow negative centered gaps to -∞ in the working dtype. The helper explicitly rejects this before Softmax instead of sending NaN probabilities to multinomial.

Knowledge check

Does lowering τ from 1 to 0.5 change model weights in the checkpoint?

16. Calculate Temperature: The Same Five Labeled Probabilities

Calculating the same scores makes “sharper” and “flatter” checkable probability changes. Keep [you, like, AI, study, we] and z=[0,2,1,-1,-0.5]. Apply Softmax after temperature scaling. Table entries are rounded, so displayed columns may not sum to exactly 1.

Scroll horizontally to view all columns.

Same final-position scores; only positive temperature τ changes
tokenraw zp(τ=1)p(τ=0.5)p(τ=2)
you0.00.0830.0160.148
like2.00.6120.8600.403
AI1.00.2250.1160.244
study-1.00.0300.0020.090
we-0.50.0500.0060.115

At τ=0.5, uncentered hand calculation uses z/τ=[0,4,2,-2,-1], with exponential sum approximately 63.490. Stable computation centers by m=2 first, then divides, giving [-4,0,-2,-6,-5]. This subtracts 4 from every uncentered scaled score, preserving probabilities while making the largest exponential exp(0)=1. The highest-logit token, like, still gets approximately 0.860. At τ=2 the gaps shrink; centering again preserves the result.

softmax([0,4,2,2,1])=softmax([4,0,2,6,5])[0.016,0.860,0.116,0.002,0.006]\operatorname{softmax}([0,4,2,-2,-1])=\operatorname{softmax}([-4,0,-2,-6,-5])\approx[0.016,0.860,0.116,0.002,0.006]
week11_training_and_generation.py
next_logits = torch.tensor(
    [[0.0, 2.0, 1.0, -1.0, -0.5]],
    device=device,
)  # [1,5] ordered as you, like, AI, study, we
temperature = 0.5
if not math.isfinite(temperature) or temperature <= 0:
    raise ValueError("temperature must be positive")

sampling_logits = (
    next_logits.float()
    if next_logits.dtype in (torch.float16, torch.bfloat16)
    else next_logits
)
centered_logits = sampling_logits - sampling_logits.amax(
    dim=-1,
    keepdim=True,
)
scaled_logits = centered_logits / temperature  # [-4,0,-2,-6,-5]
if not bool(torch.isfinite(scaled_logits).all()):
    raise ValueError("temperature is too small for stable scaling")
probabilities = F.softmax(scaled_logits, dim=-1)  # [1,5]
if not bool(torch.isfinite(probabilities).all()):
    raise ValueError("sampling probabilities must be finite")
next_id = torch.multinomial(
    probabilities,
    num_samples=1,
)  # torch.long [1,1]

z [1,5]zmax(z) [1,5](zmax(z))/τ [1,5]p [1,5]next_id [1,1]z\ [1,5]\to z-\max(z)\ [1,5]\to (z-\max(z))/\tau\ [1,5]\to p\ [1,5]\to\mathrm{next\_id}\ [1,1]

torch.argmax(probabilities, dim=-1, keepdim=True) also returns [1,1], but always selects a highest-probability candidate. torch.multinomial samples by probability and can select another token. Both are policies applied after forward, not a second model computation.

If a token has probability 0.6, repeated independent draws from that fixed distribution select it about 60% of the time in the long run; a single draw may not. During generation, every appended token changes the prefix, so the next distribution usually changes too. A whole continuation is not repeated drawing from one fixed table. Lowering temperature changes concentration, not knowledge, and does not guarantee factual correctness.

Knowledge check

Which token gains the most probability when τ drops from 1 to 0.5 here, and why?

17. Top-k: Restrict Candidates After Temperature Scaling

Top-k narrows the set of tokens eligible for this draw. Positive τ preserves ranking, so scale first and retain the highest k entries. Rejected entries become -∞, receive probability zero after Softmax, and surviving entries renormalize. This does not update the model and is not a training regularizer.

Scroll horizontally to view all columns.

Retain like, AI and you; denominator=exp(2)+exp(1)+exp(0)=11.107
tokenscaled logit at τ=1after k=3 filterfinal probability
you0.00.00.090
like2.02.00.665
AI1.01.00.245
study-1.0-∞0.000
we-0.5-∞0.000

The table uses uncentered τ=1 scores so the denominator 11.107 is easy to verify. The helper subtracts maximum 2, using [-2,0,-1,-3,-2.5]. The top-three survivors, ranking and probabilities are identical; their equivalent centered denominator is exp(-2)+exp(0)+exp(-1).

week11_training_and_generation.py
def sample_mini_gpt_next_id(
    last_logits: torch.Tensor,
    temperature: float = 1.0,
    top_k: int | None = None,
) -> torch.Tensor:
    if not isinstance(last_logits, torch.Tensor):
        raise TypeError("last_logits must be a tensor")
    if last_logits.ndim != 2:
        raise ValueError("last_logits must have shape [B,V]")
    if last_logits.size(0) < 1:
        raise ValueError("last_logits must contain at least one batch row")
    if last_logits.size(-1) != 5:
        raise ValueError("mini-gpt-v1 requires V=5")
    if not torch.is_floating_point(last_logits):
        raise TypeError("last_logits must use a floating dtype")
    if not bool(torch.isfinite(last_logits).all()):
        raise ValueError("last_logits must be finite")
    if type(temperature) not in (int, float):
        raise TypeError("temperature must be a real number")
    temperature = float(temperature)
    if not math.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be positive")

    vocabulary_size = last_logits.size(-1)
    if top_k is not None:
        if type(top_k) is not int or not 1 <= top_k <= vocabulary_size:
            raise ValueError("top_k must be an integer in [1,V]")

    # Softmax support and numeric headroom are safer than half precision.
    working_dtype = (
        torch.float64
        if last_logits.dtype == torch.float64
        else torch.float32
    )
    sampling_logits = last_logits.to(dtype=working_dtype)
    dtype_limits = torch.finfo(working_dtype)
    if temperature < dtype_limits.tiny:
        raise ValueError("temperature is too small for the sampling dtype")
    if temperature > dtype_limits.max:
        raise ValueError("temperature is too large for the sampling dtype")

    row_max = sampling_logits.amax(dim=-1, keepdim=True)
    centered_logits = sampling_logits - row_max
    if not bool(torch.isfinite(centered_logits).all()):
        raise ValueError("last_logits range is too wide after centering")

    scaled_logits = centered_logits / temperature
    if not bool(torch.isfinite(scaled_logits).all()):
        raise ValueError(
            "temperature is too small for this logit range and sampling dtype"
        )

    # Top-k stays after temperature scaling and before Softmax.
    filtered_logits = scaled_logits
    if top_k is not None:
        top_values, top_indices = torch.topk(
            scaled_logits,
            k=top_k,
            dim=-1,
        )
        filtered_logits = torch.full_like(
            scaled_logits,
            float("-inf"),
        )
        filtered_logits.scatter_(
            dim=-1,
            index=top_indices,
            src=top_values,
        )

    probabilities = F.softmax(filtered_logits, dim=-1)
    probability_sums = probabilities.sum(dim=-1)
    if (
        not bool(torch.isfinite(probabilities).all())
        or bool((probabilities < 0).any())
        or not bool(torch.isfinite(probability_sums).all())
        or bool((probability_sums <= 0).any())
    ):
        raise ValueError("temperature/top_k produced unusable probabilities")

    next_id = torch.multinomial(probabilities, num_samples=1)
    assert next_id.dtype == torch.long
    return next_id  # [B,1]

Concept sequence
  1. last_logits z [B,V]=[B,5]
  2. Validate floating/finite z, τ>0 and optional integer 1≤k≤V
  3. promote low precision → row-center → finite scale (z−max(z))/τ [B,5]
  4. retain top k logits; others become -∞ [B,5]
  5. Softmax renormalizes survivors;verify finite nonnegative row mass [B,5]
  6. multinomial samples next_id torch.long [B,1]
ci=zimaxjzjτ,c~i={ci,iTopK(c),otherwise,pi=softmax(c~)ic_i=\frac{z_i-\max_j z_j}{\tau},\qquad \widetilde c_i=\begin{cases}c_i,&i\in\operatorname{TopK}(c)\\-\infty,&\text{otherwise}\end{cases},\qquad p_i=\operatorname{softmax}(\widetilde c)_i

k=1 leaves one selected maximum with probability 1; k=V=5 filters no candidate. If scores tie at the cutoff, the particular retained indices may depend on the implementation. Do not promise a specific tie outcome.

Knowledge check

What is a candidate's Softmax probability after top-k sets its logit to -∞?

18. Complete Generation Loop: Crop, Last, Sample, Append

Each round passes at most the last two IDs of full history into MiniGPT. The model returns logits for every visible position; the caller takes the final row, samples one integer ID per batch row [B,1], and appends it to uncropped history. No targets are supplied and model parameters are read-only.

week11_training_and_generation.py
@torch.no_grad()
def generate_mini_gpt_sampled(
    model: MiniGPT,
    history: torch.Tensor,
    max_new_tokens: int,
    temperature: float = 1.0,
    top_k: int | None = None,
) -> torch.Tensor:
    if type(max_new_tokens) is not int or max_new_tokens < 0:
        raise ValueError("max_new_tokens must be a non-negative integer")
    if history.ndim != 2:
        raise ValueError("history must have shape [B,L_history]")
    if history.dtype != torch.long:
        raise TypeError("history must have dtype torch.long")
    if history.numel() == 0 or history.size(1) < 1:
        raise ValueError("history must contain at least one token per row")
    if int(history.min().item()) < 0 or int(history.max().item()) >= 5:
        raise ValueError("history IDs must be in [0,4]")
    if history.device != next(model.parameters()).device:
        raise ValueError("history and model must be on the same device")
    if not math.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be positive")
    if top_k is not None:
        if type(top_k) is not int or not 1 <= top_k <= 5:
            raise ValueError("top_k must be an integer in [1,5]")

    was_training = model.training
    model.eval()
    try:
        for _ in range(max_new_tokens):
            context = history[:, -model.config.block_size :]
            logits, no_loss = model(context)  # no targets in generation
            assert no_loss is None
            last_logits = logits[:, -1, :]
            next_id = sample_mini_gpt_next_id(
                last_logits,
                temperature=temperature,
                top_k=top_k,
            )
            assert next_id.shape == (history.size(0), 1)
            history = torch.cat((history, next_id), dim=1)
    finally:
        model.train(was_training)
    return history

Scroll horizontally to view all columns.

Course data table
First iteration from you likevalueshape
full history[[0,1]] = [you,like][1,2]
cropped context[[0,1]], still within block_size=2[1,2]
model logitsTwo visible positions, five scores each[1,2,5]
last_logitsAfter you like[1,5]
sampled example next_id[[2]] = AI[1,1]
new full history[[0,1,2]] = [you,like,AI][1,3]
Concept sequence
  1. uncropped history [B,L_history]
  2. crop only forward context → [B,min(L_history,2)]
  3. MiniGPT(context), no targets → [B,T_context,5]
  4. logits[:,-1,:] → [B,5]
  5. promote + row-center + τ scale → optional top-k → checked Softmax → multinomial
  6. next_id torch.long [B,1]
  7. append to uncropped history → [B,L_history+1]
[B,Lhistory][B,min(Lhistory,2)][B,Tcontext,5][B,5][B,1][B,Lhistory+1][B,L_{\mathrm{history}}]\to[B,\min(L_{\mathrm{history}},2)]\to[B,T_{\mathrm{context}},5]\to[B,5]\to[B,1]\to[B,L_{\mathrm{history}}+1]

In round 2, full history is [0,1,2] but model context is its tail [1,2]. For a reproducible sampling demonstration, explicitly set the caller's PyTorch RNG seed. Sampling changes RNG/history, not model parameters.

Knowledge check

Which tensor must have shape [B,1] before appending?

19. Crop the Forward Context, Not the History

History is the complete result for the user. Context is the tail window the model sees this round. Retain history and calculate context just before forward. Returned text loses no tokens, but each decision genuinely depends on at most the latest two.

Scroll horizontally to view all columns.

Course data table
ObjectIDs / tokensshapeowner
full history[0,1,2] = you like AI[1,3]generation caller
tail context[1,2] = like AI[1,2]Current forward input
model logitsTwo context positions × five candidates[1,2,5]MiniGPT forward
last logitsAfter like AI[1,5]caller sampling path
Tcontext=min(Lhistory,block_size),block_size=2T_{\mathrm{context}}=\min(L_{\mathrm{history}},\mathrm{block\_size}),\qquad \mathrm{block\_size}=2
context=history:,2:NB×Tcontext\mathrm{context}=\mathrm{history}_{:,-2:}\in\mathbb{N}^{B\times T_{\mathrm{context}}}
week11_training_and_generation.py
history = torch.tensor(
    [[0, 1, 2]],
    dtype=torch.long,
    device=device,
)  # you like AI, shape [1,3]
context = history[:, -model.config.block_size :]
assert context.tolist() == [[1, 2]]
assert context.shape == (1, 2)

was_training = model.training
model.eval()
try:
    with torch.no_grad():
        logits, no_loss = model(context)  # targets intentionally absent
finally:
    model.train(was_training)
assert logits.shape == (1, 2, 5)
assert no_loss is None

This respects position_embedding.weight [2,4], causal_mask [1,1,2,2] and the forward guard 1≤T≤2. Cropping is not causal masking: the mask restricts query/key visibility inside the window; cropping excludes earlier tokens from computation altogether. The model cannot secretly retain you at history position 0.

Knowledge check

With full history [0,1,2] and block_size=2, which IDs enter the next forward?

20. Common Bug Checklist: Inspect Phase Boundaries by Symptom

First identify the phase and what it may read or change. Use this checklist before adding layers, changing optimizers or collecting more data. Each row points to a small observable check.

Scroll horizontally to view all columns.

Course data table
symptomLikely boundary mistakefirst direct check
Fixed-batch loss never fallsMisaligned targets, unregistered/unoptimized parameters, missing backward/step or unsuitable learning ratePrint the six pairs; inspect gradients and completed_updates
Loss/gradients become NaN or InfExcessive update scale, or a non-finite input/intermediate valueFind the first non-finite value; record pre-clip gradient norm
Expected zero loss, but it approaches about 0.231Ignored conflicting labels for two identical [you] contextsInspect whether P(like|you) and P(study|you) approach 0.5/0.5
Accumulation behaves like one microbatchClearing gradients inside the window or stepping too earlyCount backward contributions before each step
Earlier logits change with a future tokenAnswer leakage through mask/slice/axis mistakesFix the prefix, change only the right-side token and compare t=0 logits
Validation consumes excess memory or fluctuates randomlyMissing no_grad/eval, or too little validation dataUse both controls and restore mode; inspect held-out sample size
Validation changes when batch packing changesEqual averaging of batch meansAccumulate reduction=sum losses and valid-target counts
Checkpoint loading gives incorrect text or errorsTokenizer/configuration/tying/member-key mismatch, or different AdamW parameter groups/orderValidate identity first, then canonical optimizer IDs/state shapes before optimizer loading
device errorModel, inputs and targets are on incompatible devicesPrint devices; use map_location and .to(device) appropriately
Generation fails after history exceeds two tokensForgot to crop forward contextassert context.size(1)≤block_size
Wrong shape/type for the next valueUsed all positions or treated a probability vector as an IDassert last [B,5];next_id long [B,1]
τ/top-k produces an invalid distributionτ≤0 or too small, low-precision overflow, invalid k or incorrect filtering orderPromote and center; check scaled values before Softmax and probabilities before sampling
week11_training_and_generation.py
assert inputs.shape == (3, 2)
assert targets.shape == (3, 2)
logits, loss = model(inputs.to(device), targets.to(device))
assert logits.shape == (3, 2, 5)
assert loss is not None and loss.ndim == 0

last_logits = logits[:, -1, :]
assert last_logits.shape == (3, 5)
# Shape assertions locate axes; they do not prove targets or causality.

Concept sequence
  1. training contract:inputs [3,2] + targets [3,2]
  2. MiniGPT representations [3,2,4] → logits [3,2,5]
  3. reshape [6,5] + [6] → mean loss [] → backward → step
  4. generation contract:context [B,T_context] → logits [B,T_context,5]
  5. select last [B,5] → safe center/τ/top-k/Softmax → next_id [B,1]
training: [3,2][3,2,4][3,2,5][6,5]+[6][]\mathrm{training}:\ [3,2]\to[3,2,4]\to[3,2,5]\to[6,5]+[6]\to[]
generation: [B,Tcontext][B,Tcontext,5][B,5][B,1]\mathrm{generation}:\ [B,T_{\mathrm{context}}]\to[B,T_{\mathrm{context}},5]\to[B,5]\to[B,1]

One successful sample does not prove model quality. One shape assertion does not prove target meaning, causality or held-out integrity. Inspect actual state along data→forward→loss→gradient→update or prompt→crop→last→sample→append.

Knowledge check

After generation receives logits [B,T,V], what indexing step comes first?

21. Seven Things to Understand from Week 11

Use this route when reading a small language-model run: identify the data signal, then the state change, measurement, persistence and target-free generation. Diagnose failures in that order too.

  1. Fixed inputs/targets both have shape [3,2]. One forward returns logits [3,2,5]; reshape to [6,5]+[6] and score all six next-token signals.
  2. Uniform five-way prediction has mean NLL ln(5)≈1.609. Random initial loss may be nearby; this is not a training-completion target.
  3. backward() accumulates parameter.grad and zero_grad() defines the accumulation window. In the training loop, optimizer.step() updates parameters and AdamW moments/counters.
  4. A step is one parameter update; an epoch traverses training batches once. Accumulation can combine several forward/backward contributions into one completed update.
  5. Held-out validation uses eval() and no_grad(), restores the prior mode, and aggregates summed loss divided by valid-target count. Same-corpus scoring is not held-out validation.
  6. Identical [you] contexts have targets like and study. Their empirical optimum is 0.5/0.5, giving an ideal batch-NLL infimum ln(2)/3≈0.231, not zero; finite weights need not attain it. Also inspect distinguishable contexts such as [you,like] and [we,like].
  7. Compatible checkpoints preserve Week 10's schema, exact tokenizer SHA-256, configuration and untied state keys, and validate AdamW's parameter group/object/order binding. Inference has no targets: crop context, take logits[:,-1,:], promote/center, apply τ and optional top-k, validate Softmax probabilities, sample [B,1] with multinomial and append to full history.

Scroll horizontally to view all columns.

Course data table
training tracegeneration trace
inputs [3,2]History shape [1,2], IDs [[0,1]] = [you,like]
representations [3,2,4]cropped context [1,2]
logits [3,2,5]logits [1,2,5] → last [1,5]
loss [] → gradients → updated θsample next_id [1,1] → history [1,3]
[3,2][3,2,4][3,2,5][]θθsupervised training[1,2][1,2,5][1,5][1,1][1,3]target-free generation\underbrace{[3,2]\to[3,2,4]\to[3,2,5]\to[]\to\nabla_{\theta}\to\theta'}_{\mathrm{supervised\ training}}\qquad\underbrace{[1,2]\to[1,2,5]\to[1,5]\to[1,1]\to[1,3]}_{\mathrm{target\text{-}free\ generation}}

Place symptoms at the right boundary: absent learning suggests the supervision path; strange validation suggests measurement; load failures suggest identity; generation failures suggest crop, final-position selection or sampling transforms.

Knowledge check

Distinguish zero_grad() and optimizer.step() in one sentence.

5. Engineering Extension: Combine Small Batches into One Update

On the first pass, use train_mini_gpt_step with accumulation_steps=1 and understand one forward/backward per update. The epoch helper below adds prevalidation of equally sized microbatches before combining them. This is a second-pass extension; infinite streams, variable-size batches and accumulation need not all be learned at once.

backward adds new contributions to existing .grad. This supports both a parameter used multiple times in one graph and several microbatches forming one effective batch. Accumulation is intentional only when loss scaling, clearing and step boundaries agree.

week11_training_and_generation.py
def train_mini_gpt_epoch(
    model: MiniGPT,
    optimizer: torch.optim.AdamW,
    train_batches,
    device: torch.device,
    accumulation_steps: int = 1,
    completed_updates: int = 0,
) -> tuple[int, float]:
    if type(accumulation_steps) is not int or accumulation_steps < 1:
        raise ValueError("accumulation_steps must be a positive integer")
    if type(completed_updates) is not int or completed_updates < 0:
        raise ValueError("completed_updates must be a non-negative integer")
    validate_mini_gpt_adamw_completed_updates(
        model,
        optimizer,
        completed_updates,
    )

    # Validate all batches before touching model mode, gradients, or optimizer.
    batches = list(train_batches)
    if not batches:
        raise ValueError("train_batches must not be empty")
    if len(batches) % accumulation_steps != 0:
        raise ValueError(
            "train_batches must form complete accumulation windows"
        )

    reference_shape = None
    reference_target_count = None
    for batch_number, batch in enumerate(batches, start=1):
        if not isinstance(batch, (tuple, list)) or len(batch) != 2:
            raise TypeError(f"batch {batch_number} must be (inputs, targets)")
        inputs, targets = batch
        if not isinstance(inputs, torch.Tensor) or not isinstance(
            targets,
            torch.Tensor,
        ):
            raise TypeError(f"batch {batch_number} values must be tensors")
        if inputs.ndim != 2 or targets.shape != inputs.shape:
            raise ValueError(
                f"batch {batch_number} inputs/targets must share [B,T]"
            )
        if inputs.dtype != torch.long or targets.dtype != torch.long:
            raise TypeError(
                f"batch {batch_number} inputs/targets must be torch.long"
            )
        target_count = targets.numel()
        if target_count <= 0:
            raise ValueError(f"batch {batch_number} has no target tokens")
        if not 1 <= inputs.size(1) <= model.config.block_size:
            raise ValueError(f"batch {batch_number} has invalid T")
        for name, token_ids in (("inputs", inputs), ("targets", targets)):
            if (
                int(token_ids.min().item()) < 0
                or int(token_ids.max().item()) >= model.config.vocab_size
            ):
                raise ValueError(
                    f"batch {batch_number} {name} IDs are outside vocabulary"
                )

        if reference_shape is None:
            reference_shape = inputs.shape
            reference_target_count = target_count
        elif (
            inputs.shape != reference_shape
            or target_count != reference_target_count
        ):
            raise ValueError(
                "this teaching helper requires equal-token microbatches"
            )

    model.train()
    optimizer.zero_grad(set_to_none=True)
    detached_loss_sum = 0.0

    for window_start in range(0, len(batches), accumulation_steps):
        window = batches[
            window_start : window_start + accumulation_steps
        ]
        for inputs, targets in window:
            inputs = inputs.to(device)
            targets = targets.to(device)
            logits, loss = model(inputs, targets)
            assert logits.shape[-1] == model.config.vocab_size
            assert loss is not None
            (loss / accumulation_steps).backward()
            detached_loss_sum += loss.detach().item()

        torch.nn.utils.clip_grad_norm_(
            model.parameters(),
            max_norm=1.0,
        )
        optimizer.step()
        completed_updates += 1  # only after optimizer.step succeeds
        optimizer.zero_grad(set_to_none=True)

    mean_microbatch_loss = detached_loss_sum / len(batches)
    return completed_updates, mean_microbatch_loss

This teaching helper supports equal-token microbatches only. It materializes and validates all inputs before model.train() or zero_grad(): nonempty data, complete windows, tensor input/target pairs, matching shapes, a shared [B,T] shape across batches, torch.long dtype, equal positive target counts, valid T and valid IDs. Then loss/accumulation_steps and mean_microbatch_loss are correctly token-weighted.

An empty iterator, seven microbatches with accumulation_steps=4, or a smaller/shorter batch is rejected before model, mode, gradient or optimizer mutation. No partial gradient window remains. This helper is unsuitable for large, infinite or variable-token streams. Those need window-level summed token loss divided by valid-token count, or a deliberately equal-size sampler/drop_last policy.

gwindow=i=14θ ⁣(Li4)=14i=14θLig_{\mathrm{window}}=\sum_{i=1}^{4}\nabla_{\theta}\!\left(\frac{\mathcal L_i}{4}\right)=\frac{1}{4}\sum_{i=1}^{4}\nabla_{\theta}\mathcal L_i

Scroll horizontally to view all columns.

Course data table
Moment in the window.gradparameters / AdamW state
After zero_gradNoneUnchanged
After backward contributions 1–3Accumulates a partial windowUnchanged
After backward 4 + clippingComplete and possibly rescaledStill unchanged
After optimizer.stepStill present until clearedBoth update once
After step returns successfullyThe complete window has been consumedOnly now increment completed_updates
After the following zero_gradNone; the next window starts cleanRetain the newly updated long-lived state

Knowledge check

Why does the accumulation loop clear gradients after optimizer.step()?

12. Engineering Extension: What State Does Resuming Training Need?

A checkpoint is a compatibility contract, not an arbitrary bag of tensors. English mini-gpt-v1 uses whitespace-delimited [you, like, AI, study, we] at IDs 0..4, with no special/padding/unknown tokens. Configuration remains vocab_size=5, block_size=2, n_embd=4, n_head=2, n_layer=2 and untied embeddings/head. Week 10's stable members—token_embedding, position_embedding, blocks[i].ln1/attention/ln2/feed_forward, final_norm, lm_head—and _init_weights stay unchanged. Week 11 imports and drives them.

Scroll horizontally to view all columns.

Course data table
checkpoint keyexact meaning / value
schema{name: mini-gpt-training-checkpoint, version: 1}
tokenizerversion + ordered_tokens + policy + sha256
configvocab_size=5, block_size=2, n_embd=4, n_head=2, n_layer=2
weight_policytoken_embedding_lm_head=untied
model_stateWeek 10 exact state-dict names and tensors
optimizerexact AdamW class + one canonical ordered model-parameter group + state_dict
completed_updatesNonnegative integer count of completed optimizer.step() calls

Scroll horizontally to view all columns.

Same as Week 10: sort_keys=True, separators=(",", ":"), ensure_ascii=False, then UTF-8 encoding
canonical tokenizer identityexact value
compact sorted-key UTF-8 JSON{"ordered_tokens":["you","like","AI","study","we"],"policy":"whitespace-delimited;no-specials;no-pad;no-unk","version":"mini-gpt-v1"}
SHA-25638d630f4c589664c9bef567457d48764cbe2307734777e80f7d5d5c63ac88dd6
week11_training_and_generation.py
def validate_mini_gpt_adamw_model_binding(
    model: MiniGPT,
    optimizer: torch.optim.AdamW,
) -> list[torch.nn.Parameter]:
    if type(model) is not MiniGPT:
        raise TypeError("faithful training requires exactly MiniGPT")
    if type(optimizer) is not torch.optim.AdamW:
        raise TypeError("faithful training requires exactly torch.optim.AdamW")

    model_parameters = list(model.parameters())
    if not model_parameters:
        raise ValueError("MiniGPT must have parameters")
    if len(optimizer.param_groups) != 1:
        raise ValueError("AdamW must have exactly one canonical param group")

    live_group = optimizer.param_groups[0]
    live_parameters = live_group.get("params")
    if not isinstance(live_parameters, list):
        raise ValueError("AdamW live params must be a list")
    if len(live_parameters) != len(model_parameters):
        raise ValueError("AdamW must own every MiniGPT parameter exactly once")
    if any(
        actual is not expected
        for actual, expected in zip(live_parameters, model_parameters)
    ):
        raise ValueError("AdamW parameters must match MiniGPT identity and order")
    return model_parameters


def validate_mini_gpt_adamw_state_dict(
    model: MiniGPT,
    optimizer_state: object,
    completed_updates: int,
) -> None:
    if type(completed_updates) is not int or completed_updates < 0:
        raise ValueError("completed_updates must be a non-negative integer")
    if not isinstance(optimizer_state, dict):
        raise ValueError("AdamW state_dict must be a dictionary")
    if set(optimizer_state) != {"state", "param_groups"}:
        raise ValueError("AdamW state_dict keys mismatch")

    state = optimizer_state["state"]
    param_groups = optimizer_state["param_groups"]
    if not isinstance(state, dict) or not isinstance(param_groups, list):
        raise ValueError("malformed AdamW state_dict")
    if len(param_groups) != 1 or not isinstance(param_groups[0], dict):
        raise ValueError("serialized AdamW must have one canonical param group")

    model_parameters = list(model.parameters())
    expected_ids = list(range(len(model_parameters)))
    stored_ids = param_groups[0].get("params")
    if not isinstance(stored_ids, list) or not all(
        type(parameter_id) is int for parameter_id in stored_ids
    ):
        raise ValueError("serialized AdamW parameter IDs must be integers")
    if stored_ids != expected_ids:
        raise ValueError("serialized AdamW parameter IDs/order are not canonical")

    # AdamW creates per-parameter state lazily on its first successful step.
    if completed_updates == 0:
        if state:
            raise ValueError("zero completed updates require empty AdamW state")
        return

    if not all(type(parameter_id) is int for parameter_id in state):
        raise ValueError("AdamW state keys must be integer parameter IDs")
    if set(state) != set(expected_ids):
        raise ValueError("nonzero progress requires state for every parameter")

    amsgrad = param_groups[0].get("amsgrad")
    if type(amsgrad) is not bool:
        raise ValueError("AdamW amsgrad metadata must be boolean")
    expected_state_keys = {"step", "exp_avg", "exp_avg_sq"}
    if amsgrad:
        expected_state_keys.add("max_exp_avg_sq")

    for parameter_id, parameter in enumerate(model_parameters):
        parameter_state = state[parameter_id]
        if not isinstance(parameter_state, dict):
            raise ValueError("each AdamW parameter state must be a dictionary")
        if set(parameter_state) != expected_state_keys:
            raise ValueError("AdamW per-parameter state keys mismatch")

        raw_step = parameter_state["step"]
        if torch.is_tensor(raw_step):
            if raw_step.numel() != 1:
                raise ValueError("AdamW step must be scalar")
            raw_step = raw_step.detach().cpu().item()
        if isinstance(raw_step, bool) or not isinstance(raw_step, (int, float)):
            raise ValueError("AdamW step must be a finite integer")
        numeric_step = float(raw_step)
        if not math.isfinite(numeric_step) or not numeric_step.is_integer():
            raise ValueError("AdamW step must be a finite integer")
        if int(numeric_step) != completed_updates:
            raise ValueError("completed_updates disagrees with AdamW step state")

        moment_names = ["exp_avg", "exp_avg_sq"]
        if amsgrad:
            moment_names.append("max_exp_avg_sq")
        for moment_name in moment_names:
            moment = parameter_state[moment_name]
            if not torch.is_tensor(moment) or moment.shape != parameter.shape:
                raise ValueError(
                    f"AdamW {moment_name} shape mismatches parameter order"
                )


def validate_mini_gpt_adamw_completed_updates(
    model: MiniGPT,
    optimizer: torch.optim.AdamW,
    completed_updates: int,
) -> None:
    validate_mini_gpt_adamw_model_binding(model, optimizer)
    validate_mini_gpt_adamw_state_dict(
        model,
        optimizer.state_dict(),
        completed_updates,
    )


def train_and_save_week11_one_batch(
    path: str,
    *,
    model: MiniGPT,
    optimizer: torch.optim.AdamW,
    inputs: torch.Tensor,
    targets: torch.Tensor,
    device: torch.device,
    requested_updates: int = 200,
    completed_updates: int = 0,
) -> tuple[list[float], int]:
    # Check resume progress before training, then derive new progress from
    # successful optimizer.step calls rather than from requested_updates.
    validate_mini_gpt_adamw_completed_updates(
        model,
        optimizer,
        completed_updates,
    )
    history, completed_updates = overfit_mini_gpt_one_batch(
        model,
        optimizer,
        inputs,
        targets,
        device,
        updates=requested_updates,
        completed_updates=completed_updates,
    )
    validate_mini_gpt_adamw_completed_updates(
        model,
        optimizer,
        completed_updates,
    )

    # Save through the canonical Week 10 API; do not invent new keys.
    save_mini_gpt_training_checkpoint(
        path,
        model=model,
        optimizer=optimizer,
        completed_updates=completed_updates,
        ordered_tokens=CANONICAL_ORDERED_TOKENS,
        tokenizer_policy=CANONICAL_TOKENIZER_POLICY,
        tokenizer_version=CANONICAL_TOKENIZER_VERSION,
    )
    return history, completed_updates

The live check does more than count parameters. AdamW must have exactly one group, whose params match list(model.parameters()) in length, order and object identity via is. Subsets, reordering, duplicates or multiple groups are rejected before training/saving. Serialized IDs are PyTorch's positional bookkeeping, not Python id(parameter). This canonical one-group layout requires exactly 0..n−1, and checks groups/order, state keys, moment shapes and every step before optimizer.load_state_dict.

week11_training_and_generation.py
def week11_config_fields(config: GPTConfig) -> dict[str, int]:
    return {
        "vocab_size": config.vocab_size,
        "block_size": config.block_size,
        "n_embd": config.n_embd,
        "n_head": config.n_head,
        "n_layer": config.n_layer,
    }


def load_mini_gpt_training_resume(
    path: str,
    *,
    device: torch.device,
) -> tuple[MiniGPT, torch.optim.AdamW, int]:
    checkpoint = torch.load(
        path,
        map_location=device,
        weights_only=False,
    )
    if not isinstance(checkpoint, dict):
        raise ValueError("checkpoint must be a dictionary")
    required_keys = {
        "schema",
        "tokenizer",
        "config",
        "weight_policy",
        "model_state",
        "optimizer",
        "completed_updates",
    }
    if set(checkpoint) != required_keys:
        raise ValueError("training checkpoint keys mismatch")
    if checkpoint["schema"] != {
        "name": "mini-gpt-training-checkpoint",
        "version": 1,
    }:
        raise ValueError("checkpoint schema/version mismatch")

    validate_checkpoint_tokenizer_identity(
        checkpoint,
        expected_ordered_tokens=CANONICAL_ORDERED_TOKENS,
        expected_tokenizer_policy=CANONICAL_TOKENIZER_POLICY,
        expected_tokenizer_version=CANONICAL_TOKENIZER_VERSION,
    )
    expected_config = GPTConfig()
    if checkpoint["config"] != week11_config_fields(expected_config):
        raise ValueError("checkpoint config mismatch")
    if checkpoint["weight_policy"] != {
        "token_embedding_lm_head": "untied",
    }:
        raise ValueError("checkpoint weight policy mismatch")

    completed_updates = checkpoint["completed_updates"]
    if type(completed_updates) is not int or completed_updates < 0:
        raise ValueError("completed_updates must be a non-negative integer")
    optimizer_payload = checkpoint["optimizer"]
    if not isinstance(optimizer_payload, dict):
        raise ValueError("optimizer checkpoint must be a dictionary")
    if set(optimizer_payload) != {"class", "state"}:
        raise ValueError("optimizer checkpoint keys mismatch")
    expected_optimizer_class = (
        f"{torch.optim.AdamW.__module__}."
        f"{torch.optim.AdamW.__qualname__}"
    )
    if optimizer_payload["class"] != expected_optimizer_class:
        raise ValueError("optimizer class mismatch")
    serialized_optimizer_state = optimizer_payload["state"]
    if not isinstance(serialized_optimizer_state, dict):
        raise ValueError("optimizer state must be a dictionary")

    # Construct and use state only after every identity check above passes.
    model = MiniGPT(expected_config).to(device)
    model.load_state_dict(checkpoint["model_state"], strict=True)
    if model.lm_head.weight is model.token_embedding.weight:
        raise ValueError("restored model must keep canonical untied weights")
    optimizer = torch.optim.AdamW(
        model.parameters(),
        lr=1e-3,
        weight_decay=1e-2,
    )
    validate_mini_gpt_adamw_model_binding(model, optimizer)
    validate_mini_gpt_adamw_state_dict(
        model,
        serialized_optimizer_state,
        completed_updates,
    )
    optimizer.load_state_dict(serialized_optimizer_state)
    validate_mini_gpt_adamw_completed_updates(
        model,
        optimizer,
        completed_updates,
    )
    return model, optimizer, completed_updates

week11_training_and_generation.py
def load_week11_mini_gpt_for_inference(
    path: str,
    *,
    device: torch.device,
) -> MiniGPT:
    # Inference-only restore delegates to the frozen Week 10 loader.
    inference_model = load_mini_gpt_for_inference(
        path,
        expected_ordered_tokens=CANONICAL_ORDERED_TOKENS,
        expected_tokenizer_policy=CANONICAL_TOKENIZER_POLICY,
        expected_tokenizer_version=CANONICAL_TOKENIZER_VERSION,
        map_location=device,
    )
    inference_model.eval()
    return inference_model

Scroll horizontally to view all columns.

Course data table
restore goalRequiredMay omit
inference onlyschema/tokenizer/config/untied policy/model_stateOptimizer and completed_updates
faithful optimizer resumeinference fields + exact optimizer class/state + completed_updatesCannot omit optimizer moments/history
bit-for-bit continuationAlso needs matching data order and relevant CPU/CUDA/Python RNG stateThis teaching checkpoint does not claim that guarantee

map_location maps serialized tensors to the requested CPU/CUDA/MPS device; model and later batches must still be compatible. A fresh optimizer has completed_updates=0, one complete canonical group and empty per-parameter state. Nonzero progress requires moments of the right shapes for every parameter, with all step counters equal to completed_updates. Resume checks serialized layout before loading and live object binding/state afterward. The schema does not separately save parameter names, so IDs alone cannot authenticate two maliciously swapped same-shape moment payloads. Load trusted files only. SHA-256 binds the tokenizer bytes to detect mismatch/corruption, not authenticate provenance.

Knowledge check

How do inference restoration and faithful optimizer-state resumption differ in checkpoint requirements?

22. Week 11 → Week 12: Connect the Lifecycle to the Full Pipeline

Week 9 determines how text becomes model-bound IDs. Week 10's fixed MiniGPT defines one forward from IDs to logits. Week 11 determines when labels, gradients, updates, measurement, persistence and sampling are used. Week 12 traces the same code and state contract end to end rather than inventing new classes or silently changing IDs.

Concept sequence
  1. Raw corpus: you like AI / we like you / you study AI
  2. mini-gpt-v1 IDs [B,N]=[3,3]
  3. shift → inputs/targets [3,2]
  4. token + position representations [3,2,4]
  5. Week 10 two-block MiniGPT → logits [3,2,5]
  6. reshape [6,5]+[6] → loss [] → backward → optimizer.step()
  7. token-weighted held-out validation and/or canonical checkpoint
  8. target-free prompt history → crop → last logits [B,5] → sampled ID [B,1] → append
raw text[3,3][3,2][3,2,4][3,2,5][6,5]+[6]Lθθcheckpoint\mathrm{raw\ text}\to[3,3]\to[3,2]\to[3,2,4]\to[3,2,5]\to[6,5]+[6]\to\mathcal L\to\nabla_{\theta}\to\theta'\to\mathrm{checkpoint}
[B,Lhistory][B,min(Lhistory,2)][B,5]τ,top-k,softmax[B,5][B,1][B,L_{\mathrm{history}}]\to[B,\min(L_{\mathrm{history}},2)]\to[B,5]\xrightarrow{\tau,\,\mathrm{top}\text{-}k,\,\mathrm{softmax}}[B,5]\to[B,1]

Scroll horizontally to view all columns.

Course data table
handoff pointBeforeAfter
after optimizer stepSupervised training; may update θMay validate, save or continue training
after checkpoint identity validation/loadserialized compatible stateRestored model/optimizer or inference model
target-free eval/no-grad forwardprompt context IDsSelect final-position logits and begin autoregressive appending

The switch occurs when the caller runs an eval/no-grad forward on a prompt without targets and samples/appends from logits[:,-1,:]. A checkpoint may sit between training and inference, but is not itself a learning update.

Knowledge check

Where does the end-to-end trace switch from supervised training to autoregressive inference?