Current: Week 11

0%

Week 11

Week 11 - Training 与 Inference:让同一个 MiniGPT 正确学习、测量、保存与生成

Key question怎样围绕 Week 10 的同一个 MiniGPT 安排训练、验证、checkpoint 与生成,并准确说出每一行改变了什么 state?

Learning objectives

  • 沿固定六个监督信号解释 forward、loss、backward、gradient clipping 与 AdamW step 的状态变化和先后顺序。
  • 用 ln(5)≈1.609 建立均匀预测基线,并用 token-weighted held-out loss 区分训练效果与泛化测量。
  • 识别重复上下文 [我] 的冲突,推导 one-batch mean NLL 只能趋近 ln(2)/3≈0.231 而不能趋近 0。
  • 复用 Week 10 的精确模型、tokenizer 与 checkpoint identity,在最后位置 logits 上依次应用 τ、top-k、Softmax 与 multinomial。

100 min estimated reading time

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

Week 10 返回的是一次预测。本周让它重复学习,并区分训练、验证和生成。先读单 batch 的最小循环;梯度累积及严格恢复检查保留在选读小节,不是理解一次更新的门槛。

Scroll horizontally to view all columns.

Course data table
学习单元本次解决的问题
一:完成一次更新区分参数、梯度和 optimizer 状态;用一个参数算两步 AdamW。
二:重复与测量先学固定数据,再用不参与更新的文档验证;日志明确在什么时候测量。
三:保存和加载保存模型配置、词表和权重;能恢复推理不等于精确复现中断后的训练。
四:生成只取当前最后位置的 logits,调温度、限制候选、抽样、追加,再 forward。

运行 python week11_adamw_numbers.py 观察两步计算。独立文档训练命令在下一周完整项目;不要把固定三句的 Loss 下降称为泛化。数值比较先看概率表,不凭一条随机续写判断温度效果。

建议阅读、手算、改代码交替进行,每个单元可拆成几次完成。章节编号保留用于旧链接和回查;按页面从上到下的新顺序学习,不需要按旧编号来回跳转。

Week 11 核心目标:同一 MiniGPT 的四种工作

直白地说,模型只负责计算;外层 Week 11 training/generation module 决定怎样使用计算结果。Training 用 labels 并且是唯一会故意更新 θ 的 phase;validation 用真正 held-out labels 测量当前 θ;checkpointing 把兼容的长期 state 写出或读回;inference 没有 targets,只选择并追加 token。mini-gpt-v1 按空格切分,ordered tokens 固定为 [我, 喜欢, AI, 学习, 猫],IDs 固定为 0..4,没有 special、padding 或 unknown token;canonical GPTConfig 是 vocab_size=5、block_size=2、n_embd=4、n_head=2、n_layer=2,token embedding 与 LM head 保持 untied。

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 = ("我", "喜欢", "AI", "学习", "猫")
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
phase输入与用途允许改变的 state明确禁止
traininginputs [3,2] + targets [3,2];拟合训练 splitbackward 改 .grad;step 改 θ 与 AdamW state不能只评分最后位置
validationheld-out inputs/targets;读取 current θ 并测量暂时改变 module mode,再恢复;不改 θ/optimizer不能 backward 或 step
checkpointingsave 读取并持久化 parameters、optimizer、config、tokenizer、progress;load 验证后恢复model/optimizer load_state_dict 明确替换长期 values;不是 learning update不能用相同 shape 代替 identity validation
inferencetarget-free prompt;逐轮追加一个 IDcaller 的 uncropped history 增长不创建 loss、backward 或 step

Scroll horizontally to view all columns.

Course data table
stateowner何时改变谁读取或持久化
parameters θmodeloptimizer.step() 学习更新;model.load_state_dict() 显式恢复training、validation、inference 读取;checkpoint save 持久化
parameter.grad各 parameterbackward 累加;zero_grad 清除optimizer.step() 读取;canonical checkpoint 不保存 transient .grad
AdamW moments / countersoptimizeroptimizer.step() 学习更新;optimizer.load_state_dict() 显式恢复training step 读取/更新;checkpoint save 持久化供 faithful resume
completed_updatestraining caller每次成功 optimizer.step() 后加 1;checkpoint load 恢复logging/checkpoint save;resume 再与 AdamW step state 核对
GPTConfig / tokenizer identitymodel / data caller本 run 内冻结;load 先验证 canonical values,再构造 matching objects所有 phase 依赖;checkpoint save 明确持久化
activations / loss value / training graph当前 forwardforward 建立 values;只有 grad-enabled training 建 graphtraining/validation 当前计算;checkpoint 不持久化

“只有 optimizer.step() 执行 learning update”仍然成立:load_state_dict() 也会替换 model/optimizer values,但那是显式 restoration,不是从当前 batch error 学习。Checkpoint save 读取并持久化 parameters、optimizer、config、tokenizer 与 progress;faithful load 先验证 config/tokenizer identity,再用已保存值替换 parameters、optimizer state 与 progress。

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;没有 update
  3. checkpointing:验证并保存/恢复同一套 identity 与 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

固定 Week 6/11 training batch 才取 B=3、T=2,因此其一次 forward 是 [3,2]→[3,2,5],也就是 30 个 raw scores,而不是一句自动生成的文字。Inference 可以是 [1,2]→[1,2,5],validation 则是 [B_i,T_i]→[B_i,T_i,5]。Validation 与 inference 都应使用 no-grad,但前者有 held-out targets 并聚合 loss,后者没有 targets 且只消费当前最后位置的分布。

θ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

四种工作里,哪一种可以调用 optimizer.step() 来学习?

1. 一个 Batch 提供六个训练信号

直白地说,一个 batch 把多行 independent sequences 同时交给模型;teacher forcing 在每个位置使用真实左侧 token,所以一次 forward 同时回答六道 next-token 题。这里 B=3 是 batch rows,N=3 是 shift 前每行 raw IDs 数,T=2 是每行 teacher-forced positions,C=4 是每个位置的 representation width,V=5 是每道题的 candidate 数。

Scroll horizontally to view all columns.

唯一 vocabulary:mini-gpt-v1,V=5
IDtoken
0
1喜欢
2AI
3学习
4
week11_training_and_generation.py
raw_ids = torch.tensor([
    [0, 1, 2],  # 我 喜欢 AI
    [4, 1, 0],  # 猫 喜欢 我
    [0, 3, 2],  # 我 学习 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 flatten 顺序:先 b=0 的两个位置,再 b=1,最后 b=2
位置可见 causal contexttarget对应五个 scores
b=0, t=0喜欢logits[0,0,:]
b=0, t=1我 喜欢AIlogits[0,1,:]
b=1, t=0喜欢logits[1,0,:]
b=1, t=1猫 喜欢logits[1,1,:]
b=2, t=0学习logits[2,0,:]
b=2, t=1我 学习AIlogits[2,1,:]

实际用途是让 accelerator 并行计算,并让一次 gradient estimate 汇总六个监督信号。logits[b,t,:] 始终按 [我, 喜欢, AI, 学习, 猫] 排列五个 raw scores;targets[b,t] 是其中正确 class 的一个 long ID,不是 one-hot vector。

Concept sequence
  1. raw IDs [B,N] = [3,3]
  2. shift → inputs [3,2] 与 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] 与 targets [6]
  6. 六个 per-position NLL 的 mean → 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 representation [B,T,C]=[3,2,4] → logits [B,T,V]=[3,2,5] → logits.reshape(B×T,V)=[6,5] 与 targets.reshape(B×T)=[6] → scalar mean cross-entropy loss []。

Knowledge check

这个 batch 为什么提供六个 target labels,而不是三个?

2. Step、Microbatch 与 Epoch:先给训练进度单位

直白地说,microbatch 是一次装得进 memory 的数据切片;optimizer step 是一次实际参数更新;epoch 是完整走过 training loader 一遍。本周固定三句语料若作为一个 batch 且 accumulation_steps=1,一轮 epoch 恰好有一个 step,但这只是极小示例,不是定义。

Scroll horizontally to view all columns.

Course data table
单位发生什么Week 11 如何计数
microbatch一次 forward + backward contribution不自动等于 update
optimizer step读取当前 accumulated gradients 并更新 statecompleted_updates 加 1
epochtraining batches 完整遍历一次可能含许多 updates
generation iterationsample 并 append 一个 ID不是 training step
Nupdates/epoch=NmicrobatchesNaccumulation=1204=30N_{\mathrm{updates/epoch}}=\frac{N_{\mathrm{microbatches}}}{N_{\mathrm{accumulation}}}=\frac{120}{4}=30

Shape 仍是每个固定 microbatch 的 [3,2]→[3,2,4]→[3,2,5]→[6,5]+[6]→[];epoch 只改变这条计算被重复多少次,不改变 tensor rank。Checkpoint 的 completed_updates 记录已经执行完的 optimizer.step() 次数,而不是零起始 loop index 或下一步编号。

Knowledge check

120 个 microbatches 以每四个完整累积窗口更新一次,一轮 epoch 有多少 optimizer steps?

3. AdamW:把历史梯度变成这一步的参数改变量

直白地说,AdamW 不只看当前 gradient g_t;它为每个 parameter coordinate 维护一阶 moving average m_t 与平方 gradient 的 moving average v_t,再用它们调节 update scale。weight decay 另行轻微收缩 weights。它适合 noisy、scale 差异大的 language-model gradients,但仍需要选择 learning rate。

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,
)

先把 model 移到目标 device,再把这些 parameter objects 交给 AdamW。构造 optimizer 时 param groups 已存在;第一次有 gradient 的 optimizer.step() 才建立并更新相应 moment tensors 与 counters。optimizer.zero_grad() 只处理 parameter.grad,不会清空 m、v 或 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.

parameter、gradient 与 AdamW moments 对同一 weight tensor 具有相同 element-wise shape
对象示例 shape谁改变它
token_embedding.weight[5,4]optimizer.step()
token_embedding.weight.grad[5,4]backward 累加;zero_grad 清除
对应 AdamW m 与 v各 [5,4]optimizer.step()
当前 logits / loss[3,2,5] / []forward 重新计算

Knowledge check

为什么 faithful resume 必须恢复 AdamW state?

4. 一个标准 Training Step:逐行追踪 State

week11_minimal_loop.py
# 在 course_examples 目录运行: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("这是三句固定数据的流程演示,不是独立验证或泛化证明。")

日志中的 loss 是本次更新之前那次 forward 的值;最后的 final_loss 才是 100 次更新后重新计算的结果。optimizer.step 不会改写旧 loss 变量。请先预测删除 step 或每轮重建 model 会发生什么,再看下面带诊断保护的可复用函数。

直白地说,一个 training step 把一个 batch 的监督误差变成一次 parameter update。以下函数属于 week11_training_and_generation.py,并直接使用导入的 canonical MiniGPT;它没有重定义 config、attention、blocks、initializer 或 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("训练 loss 非有限,停止本次更新")
    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
line立即改变或创建什么为什么必须在这里
model.train()递归设置 module training flags在 forward 前选择 training behaviour;不更新 weights
inputs/targets.to(device)创建或返回共置的 batch tensorsCPU batch 不能直接乘 CUDA parameters
optimizer.zero_grad(set_to_none=True)清除旧 .grad references防止本 step 与上个 window 的 gradients 意外相加
model(inputs, targets)创建 activations/graph、logits [3,2,5]、loss []parameters 与 AdamW state 仍未改变
loss.backward()把 ∂L/∂θ 累加到 parameter.gradstep 必须先拥有当前 gradients
clip_grad_norm_必要时原地缩放全部 gradient tensors在所有 intended backward 后、step 前限制 norm
optimizer.step()改变 parameter values、AdamW moments/counters;成功返回后 caller count 加 1这是唯一真正学习的一行
loss.detach()返回与 graph 分离的 logging tensor避免日志留住已经用完的 graph
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}

参数更新可概念化为 θ←θ+Δθ,但这不会 retroactively 改写刚才 forward 的 inputs、targets 或 logits。下一次 forward 才会使用新 θ 计算新 logits。

可执行版本在 backward 前检查 loss 是否有限,裁剪时也要求梯度有限;遇到 NaN/Inf 就停止本次 step。不能等参数已经更新后,才从日志发现本应停止的数值问题。

Knowledge check

loss.backward() 已执行而 optimizer.step() 尚未执行时,什么 state 是新的?

6. 初始 Loss 的参照:从均匀五选一推导 ln(5)

直白地说,cross-entropy 衡量模型给真实 target 留了多少 probability。若每道五分类题的 logits 都是 [0,0,0,0,0],Softmax 在候选顺序 [我, 喜欢, AI, 学习, 猫] 上就是 [0.2,0.2,0.2,0.2,0.2];无论 target 是 喜欢 还是 AI,正确项都只拿到 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
固定 batch 中的含义shape / value
raw equal logits六道题各一行 [0,0,0,0,0][6,5]
uniform probabilities每行五项都是 0.2[6,5]
per-position NLL六项各约 1.609[6]
mean CE六项 meanscalar [],约 1.609

实际用途是为 loss 曲线提供数量级参照,而不是 pass/fail threshold。随机 initialization 的 logits 不会恰好全零,所以第一次 loss 可略高或略低于 1.609。inputs [B,T]=[3,2] → token/position representation [B,T,C]=[3,2,4] → logits [B,T,V]=[3,2,5] → logits.reshape(B×T,V)=[6,5] 与 targets.reshape(B×T)=[6] → scalar mean cross-entropy loss []。

Knowledge check

五 token vocabulary 中,模型给真实 target 概率 0.2 时 per-position CE 是多少?

7. Validation 为什么必要:测量没有参与 Update 的数据

直白地说,training split 用来选择 gradients,validation split 只用来观察当前 parameters。它们使用相同 tokenizer、input/target alignment 与 CE 定义,但数据来源不同;validation 结果可帮助选择 checkpoint、停止点、learning rate 或容量,却绝不能对这个 batch backward。

Scroll horizontally to view all columns.

Course data table
train lossheld-out validation loss较合理的解读下一项直接检查
下降且两者保持接近也下降该 split 上的 generalization 在改善继续看 samples 与 checkpoints
持续下降持续上升开始过拟合 training data较早 checkpoint、更多数据/regularization 或较小模型
长期近 ln(5)也近 ln(5)underfit 或 signal/update brokenone-batch diagnostic、targets、gradients、LR
剧烈波动或非有限同样不稳data/numerical/update instability定位第一处 non-finite,检查 LR 与 grad 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}}}

每个 validation batch 仍从 inputs [B_i,T_i] 产生 logits [B_i,T_i,5],再对有效 targets 求 loss;B_i 可以不同,T_i 必须在 1..block_size=2。跨 batch 汇总时要把 token-level loss sums 相加后除以有效 target 总数,不能把大小不同的 batch means 等权平均。

Knowledge check

Training loss 继续下降而 held-out validation loss 持续上升,最直接的担忧是什么?

8. 正确的 Validation:Mode、No-Grad 与 Token Weighting

直白地说,eval() 决定 layers 怎样运行,no_grad() 决定是否记录 backward graph;两者解决不同问题。Token-weighted aggregation 则确保含 6 个 targets 的 batch 对总 loss 的贡献是含 2 个 targets batch 的三倍,而不是两者各占一半。

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 会拒绝 target ID -100,因此 masked validation 不能调用 model(inputs, targets)。这里先调用 model(inputs) 取得 raw logits,再在外部用 ignore_index 计算 summed CE。没有 padding 的普通 batch 也走同一 token-weighted path。try/finally 保证函数原来处于 train mode 就恢复 train,原来处于 eval mode 就保持 eval,即使 iterator 或 shape validation 抛错也不遗留 mode change。

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.

batch means 等权平均会在有效 token 数不同时产生偏差
batch有效 targetsloss sum错误的 batch mean 权重正确 token 权重
A66.01/26/8
B26.01/22/8
聚合812.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],不传 masked targets 给 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

哪一步防止 validation forward 建立 backward graph?eval() 是否能替代它?

9. Gradient Clipping:在 Update 前限制完整 Gradient

L2 norm 可以先读成一组数的总长度:梯度 [3,4] 的长度是 √(3²+4²)=5。若上限为 1,就同乘 1/5,得到 [0.6,0.8],长度为 1;方向比例仍是 3:4。

裁剪直接限制的是梯度长度。对普通 SGD,更新长度随 η×梯度长度变化;AdamW 还要使用历史均值、平方均值与 weight decay,所以不能把“梯度上限 1”解释成“参数更新长度严格不超过 1”。

直白地说,把所有 parameter gradients 想成一个很长向量 g;若它的 L2 norm 超过 cap c,就把所有分量乘同一个比例,保留 direction 而限制 magnitude。实际用途是给异常大 update 加一道 guardrail,同时保留 grad_norm 日志帮助定位根因。

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 c共同 scale结果
5.01.0约 1/5norm 降到约 1.0,direction 不变
0.61.01gradients 不变

固定模型中 token_embedding.weight.grad 仍是 [5,4],每个 block 的 qkv.weight.grad 仍是 [12,4];clipping 没有 [B,T] activation shape。train_mini_gpt_step 中 clip_grad_norm_ 返回 pre-clip norm,可与 scalar loss 一起 detached logging。Accumulation 时必须等完整 window 的 contributions 都进入 .grad 后再 clip 一次。

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

Gradient accumulation 时 clipping 应在哪一刻执行?

10. Learning Rate:AdamW 仍需要全局步幅

直白地说,lr 决定每次 optimizer.step() 大致走多远。它不直接修改当前 logits [3,2,5],而是改变 θ,因而只在下一次 forward 间接改变 logits values。实际使用时应记录当前 lr、completed_updates、loss 与 grad norm,并一次只改变一个实验变量。

θ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 symptom可能的 LR reading先做的检查
loss 多次 update 后仍近 ln(5)1e-6 可能太小确认 labels 对齐、grads 非 None、step 真执行
loss 下降平滑1e-3 在该 tiny run 可作为起点继续比较 held-out loss 与 samples
loss spike / oscillation可能太大查看 first bad update 与 pre-clip grad norm
loss 或 gradients 非有限update 可能失稳先定位第一个 NaN/Inf,再调整 LR

固定教学 run 使用 AdamW(..., lr=1e-3, weight_decay=1e-2),但这不是所有模型的 magic value。比较 1e-6 与 1e-3 时必须固定 tokenizer、split、batch/accumulation policy、seed 与 update 数,否则差异不能归因给 LR。

Knowledge check

Controlled run 中什么现象可能提示 LR 太小?

11. Overfit One Batch:先识别不可消除的标签冲突

下面 inf 读作“能无限接近的下界”。两个相同前缀必须使用同一分布,却被要求回答两个不同词,因此最好各给一半概率,不能同时给两个词概率 1。0.231 是仅按可见上下文和数据冲突推导的理想参照;有限大小的网络、训练步数与优化器不保证达到它。

直白地说,这个 diagnostic 问的是“完整监督路径能否明显学习”,而不是“模型是否 generalize”。反复使用 inputs [[0,1],[4,1],[0,3]] 与 targets [[1,2],[1,0],[3,2]] 后,loss 应从 random reference 约 ln(5) 明显下降,并朝该 batch 的 empirical conditional optimum 走;但不能要求 deterministic causal model argmax 正确预测全部六个 labels。

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.

重复上下文 [我] 在同一 position embedding t=0 下必须产生同一 distribution
position(s)causal contextobserved target(s)empirical optimum
b=0,t=0 与 b=2,t=0[我]喜欢、学习P(喜欢|我)=0.5,P(学习|我)=0.5
b=0,t=1[我,喜欢]AI该 row 可趋近给 AI probability 1
b=1,t=0[猫]喜欢可趋近给 喜欢 probability 1
b=1,t=1[猫,喜欢]该 row 可趋近给 我 probability 1
b=2,t=1[我,学习]AI该 row 可趋近给 AI probability 1
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. same [我] prefix at t=0 → exactly one shared five-token distribution
  2. two conflicting labels → empirical mass 0.5 on 喜欢 and 0.5 on 学习
  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. inspect [我,喜欢] versus [猫,喜欢] final logits to confirm context can create different predictions

实际诊断应记录 first/last loss、finite values 与 gradients,并比较 logits.argmax(dim=-1) [3,2] 时承认冲突位置最多选中其中一个。Helper 接受已有 completed_updates,只在每次 optimizer.step() 成功返回后加 1,并把 cumulative count 与 history 一起交还 caller;它不会用 loop 上限冒充已完成进度。更有信息量的 context check 是让 [我,喜欢] 与 [猫,喜欢] 在 t=1 产生可不同 distributions;它们的 visible prefixes 确实不同。

Knowledge check

为什么这个 one-batch diagnostic 不能要求六个 positions 全部 argmax 正确?

13. Inference、Training 与 Validation:都 Forward,但目的不同

直白地说,training 用目标答案计算 loss 并把误差反传;validation 也有答案,但只用来计分;inference 没有答案,必须把当前最后位置分布转换成一个 ID 并循环追加。MiniGPT.forward 保持不变,差异由 Week 11 caller 建立。

Scroll horizontally to view all columns.

Course data table
phasemodel callgraph / mode之后做什么
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;不更新
inferencemodel(cropped context),无 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 我 喜欢

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() 不会删除之前残留的 .grad;它只改变 module mode。no_grad() 防止本 forward 建 graph,却也不清除旧 .grad。真正保证 inference 不更新的是调用链中完全没有 loss.backward() 与 optimizer.step()。

Knowledge check

Training 与 inference 都一定能得到哪个 output?loss 何时才存在?

14. 为什么生成只取最后一个位置

直白地说,prompt [我, 喜欢] 的 position 0 logits 回答“我后面是什么”,position 1 logits 回答“我 喜欢后面是什么”。生成当前轮只会 append 后一个问题的答案;早期 logits 在 teacher-forced training 中有监督价值,在本 generation iteration 却不是 prompt 末尾之后的预测。

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.

每个五维 row 的 candidate order 都是 [我, 喜欢, AI, 学习, 猫]
tensor sliceshapelabelled meaning
logits[0,0,:][5]after 我:五个候选 scores
logits[0,1,:][5]after 我 喜欢:五个候选 scores
logits[:,-1,:][1,5]每个 batch prompt 的当前最后位置 distribution source
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,:})

若输入三条 prompts 的 logits 是 [3,2,5],同一 indexing 得到 [3,5],每行独立决定一个 next ID。这里 -1 表示当前 sequence axis 的最后位置,不是 token ID -1。

Concept sequence
  1. training:logits [3,2,5] 全部 reshape 为 [6,5] 并评分六个 positions
  2. generation:logits [B,T_context,5] 先选择 [:,-1,:]
  3. next logits [B,5] 再经过 τ / top-k / Softmax / sampling

Knowledge check

logits shape 是 [3,2,5] 时,logits[:,-1,:] 是什么 shape?

15. Temperature τ:只改变 Sampling 分布的尖锐程度

直白地说,τ<1 会放大 logit 差距,让最高分候选更集中;τ>1 会缩小差距,让分布更平;τ=1 保持普通 Softmax。实际计算先让每行减去自己的 maximum,使最大值变成 0,再除以 τ;所有 candidates 同减一个常数不会改变 ranking 或 Softmax probabilities,却避免正方向的 exponential overflow。它是 inference-time sampling control,不改变 model parameters、AdamW state 或训练目标。T 在本周始终保留给 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
τ settingscaled logits 的相对间距sampling effect
τ=0.5原差距乘 2更尖,最高 logit probability 上升
τ=1不变普通 Softmax
τ=2原差距减半更平,低分候选 probability 上升
Concept sequence
  1. finite floating last logits z [B,5]
  2. promote float16/bfloat16 至至少 float32,并 validate τ>0
  3. row-center z−max(z) [B,5],再 scale /τ [B,5]
  4. reject non-finite centered/scaled values with a clear numeric-domain error
  5. ranking 不变;relative gaps 改变
  6. 之后才做 optional top-k 与 Softmax

Positive τ 除法保持 candidate ranking;它只改变相对概率差距。τ=0 会除零,τ<0 会翻转 ranking,都不是本 sampling API 允许的设置。即使 raw logits 都 finite,极小 τ 也可能让负的 centered gaps 在 working dtype 中溢出为 -∞;helper 会在 Softmax 前用明确 ValueError 拒绝,而不是把 NaN 交给 multinomial。

Knowledge check

把 τ 从 1 降到 0.5 会改变 checkpoint 中的 model weights 吗?

16. 手算 Temperature τ:同一组五个带标签概率

直白地说,手算同一组 scores 能把“更尖、更平”变成可核对的 probability changes;实际使用时据此选择 sampling diversity。仍用 candidate order [我, 喜欢, AI, 学习, 猫] 与 z=[0,2,1,-1,-0.5]。Softmax 只在 τ scaling 之后发生;表中值为四舍五入,所以每列显示值可能不严格相加为 1。

Scroll horizontally to view all columns.

同一 last-position scores;只改变正的 sampling temperature τ
tokenraw zp(τ=1)p(τ=0.5)p(τ=2)
0.00.0830.0160.148
喜欢2.00.6120.8600.403
AI1.00.2250.1160.244
学习-1.00.0300.0020.090
-0.50.0500.0060.115

τ=0.5 时,原始等价手算仍是 z/τ=[0,4,2,-2,-1],其 exponentials 总和约 63.490。稳定实现先减 m=2,再除 τ,得到 [-4,0,-2,-6,-5];这只是把前一向量所有项同减 4,所以 probabilities 完全相同,而最大 exponential 现在只是 exp(0)=1。最高 logit“喜欢”仍得到约 0.860。τ=2 时差距缩小,centering 同样不改变结果。

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 我, 喜欢, AI, 学习, 猫
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) 也输出 [1,1],但总选最高 probability;torch.multinomial 会按概率随机抽样,所以可能选择非 argmax token。二者都是 forward 之后的 choice policy,不是另一次模型计算。

若某词概率为 0.6,可以想象多次从同一分布独立抽样,大约六成会选到它;只抽一次可能选不到。实际生成每追加一个 token,前缀会变化,下一轮分布也通常变化,所以不能把整段续写当成反复从同一张表抽签。降低温度只是改变本轮选择的集中程度,不会让知识变多,也不保证事实更正确。

Knowledge check

本例 τ 从 1 降到 0.5 时,哪个 token 的 probability 增加最多?为什么?

17. Top-k:在 τ Scaling 后限制候选集

直白地说,top-k 在 sampling 前缩小“本轮允许抽中的词表”。正 τ 不改变 ranking,因此先做 τ scaling,再取最高 k 项;被过滤项设为 -∞,Softmax 后精确变成 0,保 survivors 会重新归一化。它不更新 model,也不是 training regularizer。

Scroll horizontally to view all columns.

保留 喜欢、AI、我;denominator=exp(2)+exp(1)+exp(0)=11.107
tokenscaled logit at τ=1after k=3 filterfinal probability
0.00.00.090
喜欢2.02.00.665
AI1.01.00.245
学习-1.0-∞0.000
-0.5-∞0.000

表格保留前一节的 uncentered τ=1 scores,方便逐 token 核对 11.107 与 probabilities。实际 helper 对每行同减 maximum 2,使用 [-2,0,-1,-3,-2.5];top-3 survivors、ranking 与最后 probabilities 完全相同,只把 survivor denominator 等价地缩放为 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 与 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 使剩余 distribution 在唯一最高项上确定;k=V=5 不过滤任何 finite candidate。Cutoff 处相同 logits 的 tie selection 可依实现而定,不能承诺相等分数中固定保留哪一个。

Knowledge check

一个 candidate 被 top-k 设为 -∞ 后,其 Softmax probability 是多少?

18. 完整 Generation Loop:Crop、Last、Sample、Append

直白地说,每轮只把完整 history 的最后至多两个 IDs 送进 MiniGPT;模型返回每个 visible position 的 logits;caller 只拿最后一行,抽一个 [B,1] integer ID,再把它接到未裁剪 history 上。整个过程 target-free 且 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 我 喜欢valueshape
full history[[0,1]] = [我,喜欢][1,2]
cropped context[[0,1]],仍在 block_size=2 内[1,2]
model logits两个 visible positions,各五个 scores[1,2,5]
last_logitsafter 我 喜欢[1,5]
sampled example next_id[[2]] = AI[1,1]
new full history[[0,1,2]] = [我,喜欢,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]

第二轮 full history 已是 [0,1,2],但进入 model 的 context 是尾部 [1,2]。若需要 sampled demo 可复现,应在 caller 明确设置 torch RNG seed;这不把 sampling 变成 model state update。

Knowledge check

Append 前哪个 tensor 的 shape 必须是 [B,1]?

19. 为什么只截断 Forward Context,不截断 History

直白地说,history 是要交给用户的完整结果,context 是本轮模型能看的尾部窗口。Caller 保留前者,只有在调用 forward 前才计算后者。这样 output 不会丢词,但模型的决定确实只依赖最近至多两个 token。

Scroll horizontally to view all columns.

Course data table
对象IDs / tokensshapeowner
full history[0,1,2] = 我 喜欢 AI[1,3]generation caller
tail context[1,2] = 喜欢 AI[1,2]本轮 forward input
model logitscontext 两个 positions × 五 candidates[1,2,5]MiniGPT forward
last logitsafter 喜欢 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,
)  # 我 喜欢 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

实际用途是遵守 position_embedding.weight [2,4]、每个 causal_mask [1,1,2,2] 和 forward 的 1≤T≤2 guard。Crop 不是 causal mask:mask 限制当前窗口内 query 能看哪些 key;crop 直接让更早 tokens 不进入计算,因此模型无法“暗中记得”我@history position 0。

Knowledge check

Full history 是 [0,1,2] 且 block_size=2 时,下一次 forward 收到哪些 IDs?

20. 常见 Bug Checklist:按症状检查 Phase 边界

直白地说,先确认你正在做哪一个 phase,再检查该 phase 允许读取和改变什么。以下表应在“加层、换 optimizer、增数据”之前使用;每一行都指向一个最小观察点。

Scroll horizontally to view all columns.

Course data table
symptom最可能的 boundary mistakefirst direct check
one-batch loss 完全不降targets 错位、parameters 未注册/未进 optimizer、缺 backward/step、LR 不合适打印六个 pairs;检查 non-None grads 与 completed_updates
loss/gradients 变成 NaN 或 InfLR/update 过大、输入或中间值先非有限定位 first non-finite value;记录 pre-clip grad norm
期待 loss→0 却停在约 0.231忽略两个 [我] contexts 的 conflicting labels核对 P(喜欢|我) 与 P(学习|我) 是否趋近 0.5/0.5
accumulation 像单 microbatchwindow 内 zero_grad 或过早 step数每个 step 前有几次 backward
earlier logits 随 future token 改变causal mask/slice/axis 泄漏答案固定 prefix,只改变右侧 token,比较 t=0 logits
validation 占 memory 或随机波动缺 no_grad、缺 eval,或 split/样本太小同时使用两者并恢复 mode;检查 held-out 数据量
validation 值受 batch packing 改变平均了 batch means累加 reduction=sum 与 valid target count
checkpoint load 后乱码/异常tokenizer/config/tie policy/member keys 或 AdamW group/order 不匹配先核 identity,再在 optimizer load 前核 canonical IDs/state shapes
device errormodel、inputs、targets 不共置打印各 tensor/parameter device;用 map_location 和 .to(device)
generation 超过两 tokens 失败忘记 crop forward contextassert context.size(1)≤block_size
next value shape/type 不对使用了所有 positions 或把 probability vector 当 IDassert last [B,5];next_id long [B,1]
τ/top-k 产生 invalid distributionτ≤0/过小、low-precision overflow、k 越界或 filter 顺序错误promote、row-center、核 scaled/probabilities finite,再 Softmax/sample
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]

一个 successful sample 不能证明 model quality;一个 passing shape assertion 也不能证明 target semantics、causality 或 held-out integrity。Diagnosis 要沿 data→forward→loss→gradient→update 或 prompt→crop→last→sample→append 顺序观察实际 state。

Knowledge check

Generation 收到 [B,T,V] logits 后,第一项必要 indexing 是什么?

21. Week 11 最应该理解的 7 件事

直白地说,下面七条是阅读任何 small language-model run 的最短路线,而不是孤立术语:先认 data signal,再认 state change,随后认 measurement、persistence 与 target-free generation;实际排错时也按这个顺序定位边界。

  1. 固定 corpus 的 inputs/targets 都是 [3,2];一次 forward 产生 logits [3,2,5],reshape 为 [6,5]+[6] 后评分全部六个 next-token signals。
  2. Uniform five-way prediction 的 mean NLL reference 是 ln(5)≈1.609;random first loss 可在其附近,而它绝不是训练完成标准。
  3. backward() 把 gradients 累加到 parameter.grad,zero_grad() 定义 accumulation window;只有 optimizer.step() 改 parameter values 与 AdamW moments/counters。
  4. Step 是一次 parameter update,epoch 是走完 training batches 一遍;accumulation 可让多次 forward/backward 只产生一次 completed update。
  5. Held-out validation 必须同时用 eval() 与 no_grad()、恢复先前 mode,并按 reduction=sum / valid target count 做 token-weighted 聚合;same-corpus score 不是 validation。
  6. 两个相同 [我] causal contexts 分别标为 喜欢 与 学习,所以 empirical optimum 是 0.5/0.5,batch mean NLL 只会趋近而不会以 finite weights 达到 ln(2)/3≈0.231;检查 [我,喜欢] 与 [猫,喜欢] 等可区分 contexts 是否能分化。
  7. 兼容 checkpoint 复用 Week 10 exact schema/tokenizer SHA-256/config/untied state keys,并校验 AdamW 对 canonical parameters 的 group/object/order 绑定;inference 则 target-free 地 crop context、取 logits[:,-1,:]、promote/center 后按 τ 和 optional top-k 处理、检查 Softmax probabilities,再 multinomial 得 [B,1] 并 append full history。

Scroll horizontally to view all columns.

Course data table
training tracegeneration trace
inputs [3,2]history [1,2] = [我,喜欢]
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}}

实际用途是把每个 symptom 放回所属 boundary:没有下降先查 supervised path;validation 异常先查 measurement;load 异常先查 identity;generation 异常先查 crop、last-position 与 sampling transforms。

Knowledge check

用一句话区分 zero_grad() 与 optimizer.step()。

5. 工程选读:多个小 Batch 怎样合成一次更新

第一遍先使用上一节 train_mini_gpt_step,accumulation_steps=1,确认一次 forward/backward 对应一次更新。下面的 epoch helper 多了“先检查一组等大小小批次,再合成一次更新”的工程边界;这是第二遍扩展,不需要在第一次看到循环时同时掌握无限数据流、变长 batch 和梯度累积。

直白地说,backward 会把新 gradient contribution 加进现有 .grad;这既支持一个 parameter 在 graph 中多次贡献,也允许若干 microbatches 共同形成一个 effective batch。只有当 loss scaling、zeroing 与 step boundary 全部配套时,这种累加才是故意的。

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

这个教学 helper 明确只支持 equal-token microbatches。它先 materialize 全部输入,并在 model.train() 或 zero_grad() 之前验证:非空、完整 accumulation windows、每项确实是 input/target tensors、两者 shape 相同且所有 batches 共享同一 [B,T] shape、dtype 都是 torch.long、target count 相同且大于零、T 与 IDs 合法。于是 loss/accumulation_steps 的 gradient 与 mean_microbatch_loss 都恰好是按 tokens 等权的结果。

空 iterator 会得到清晰错误;7 个 microbatches 配 accumulation_steps=4,或混入较短/较小 batch,也会在任何 model/mode/gradient/optimizer mutation 前拒绝,不会残留 partial gradients。大型、无限或 variable-token stream 不适合这个教学 helper;production code 应按 window 的 token-loss sum 除以 valid-token 总数,或使用已知长度且等大小的 sampler/drop_last。

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
window 时刻.gradparameters / AdamW state
zero_grad 后None不变
第 1–3 次 backward 后逐次累加 partial window不变
第 4 次 backward + clip 后完整且可能已缩放仍不变
optimizer.step 后仍存在,直到下一行清除两者更新一次
step 成功返回后同一完整 window 已消费completed_updates 才加 1
随后 zero_grad 后None,下一窗口干净开始保留刚更新的长期 state

Knowledge check

为什么 accumulation loop 在 optimizer.step() 后才 zero_grad?

12. 工程选读:继续训练需要保存哪些状态

直白地说,checkpoint 是一份 compatibility contract,而不是“任意 tensors 的袋子”。mini-gpt-v1 按空格切分,ordered tokens 固定为 [我, 喜欢, AI, 学习, 猫],IDs 固定为 0..4,没有 special、padding 或 unknown token;canonical GPTConfig 是 vocab_size=5、block_size=2、n_embd=4、n_head=2、n_layer=2,token embedding 与 LM head 保持 untied。 Week 10 的 stable members(token_embedding、position_embedding、blocks[i].ln1/attention/ln2/feed_forward、final_norm、lm_head)及 _init_weights 原样保留;Week 11 只 import 并驱动它们。

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_updates已完成 optimizer.step() 的非负整数次数

Scroll horizontally to view all columns.

与 Week 10 相同:sort_keys=True、separators=(",", ":")、ensure_ascii=False,再 UTF-8 encode
canonical tokenizer identityexact value
compact sorted-key UTF-8 JSON{"ordered_tokens":["我","喜欢","AI","学习","猫"],"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

Live check 不只数 parameters:它要求 AdamW 恰有一个 group,且 group.params 与 list(model.parameters()) 等长、同序,并对每一项用 is 验证是同一个 Parameter object;因此 subset、reorder、duplicate 或 multiple groups 都会在 training/save 前被拒绝。Serialized IDs 则是 PyTorch 的 positional bookkeeping,不是 Python id(parameter):本 canonical one-group layout 必须精确为 0..n−1,且在 optimizer.load_state_dict 前就检查 group/order、state keys、moment shapes 与每个 step。

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 goal必须有可以省略
inference onlyschema/tokenizer/config/untied policy/model_stateoptimizer 与 completed_updates
faithful optimizer resumeinference fields + exact optimizer class/state + completed_updates不能省 optimizer moments/history
bit-for-bit continuation还需相同 data order 及相关 CPU/CUDA/Python RNG state本教学 checkpoint 不宣称做到

map_location 决定 serialized tensors 映射到哪个 CPU/CUDA/MPS device;model、后续 inputs 与 targets 仍必须共置。Fresh optimizer 的合法起点是 completed_updates=0、一个完整 canonical param group 且 per-parameter state 为空;非零 progress 则要求同一组每个 parameter 都有匹配 shape 的 AdamW moments,且所有 step 恰好等于 completed_updates。Resume 在 load_state_dict 前验证 serialized layout,load 后再验证 live object binding 与 state。由于 frozen schema 不另存 parameter names,恶意交换两个同 shape moment payload 无法仅凭 IDs 证明来源;所以仍只 load trusted files。SHA-256 绑定 tokenizer canonical bytes、用于发现 identity mismatch 或损坏,但不是来源签名。

Knowledge check

Meaningful inference 与 faithful training resume 的 checkpoint 要求差在哪里?

22. Week 11 → Week 12:把 Lifecycle 接回完整 Pipeline

直白地说,Week 9 决定 text 怎样成为 model-bound IDs;Week 10 的 frozen MiniGPT 决定一次 forward 怎样产生 logits;Week 11 决定 labels、gradients、updates、measurement、persistence 与 sampling 何时发生。Week 12 将把同一代码和同一 state contract 放进一个完整 project trace,而不是再造另一套类或 ID space。

Concept sequence
  1. raw corpus:我 喜欢 AI / 猫 喜欢 我 / 我 学习 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 point之前之后
after optimizer stepsupervised training,可改变 θ可验证、保存或继续 training
after checkpoint identity validation/loadserialized compatible staterestored model/optimizer 或 inference model
target-free eval/no-grad forwardprompt context IDs只取 final-position logits 并开始 autoregressive append

从 supervised training 切到 autoregressive inference 的精确时刻,是 caller 对没有 targets 的 prompt 做 eval/no-grad forward,并从 logits[:, -1, :] 开始 sampling 与 append。Checkpoint 可以夹在两者之间,但它不构成学习 update。

Knowledge check

端到端 trace 在哪一步从 supervised training 切换为 autoregressive inference?