Current: Week 10

0%

Week 10

Week 10 - GPT Architecture:把最小语言模型组装成可训练的 Mini GPT

Key question怎样把 Weeks 6–9 的 token、Attention、Transformer 与 tokenizer 接口组装成一个可训练、可保存、可生成的 MiniGPT?

Learning objectives

  • 用唯一的 GPTConfig 解释 Vocabulary、context、width、head divisibility 与 depth 约束。
  • 沿 [2,2] → [2,2,4] → 两个 pre-norm Blocks → [2,2,5] 追踪完整 forward。
  • 认清每个 parameter、buffer 与 temporary activation 的 owner,并手算 canonical untied 模型的 520 个参数。
  • 实现并检查稳定的 forward、state-dict/checkpoint 与外部 generation 边界,为 Week 11 的训练循环做准备。

85 min estimated reading time

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

这周的目标是组装预测函数,不是先掌握全部 checkpoint 工具。保留五词输入,逐步把 embedding、Attention、FFN、残差和归一化接起来;最后仍返回 logits 和可选 loss。

Scroll horizontally to view all columns.

Course data table
学习单元本次解决的问题
一:最小模型先只有 token/position embedding 与输出层,定位输入、参数和输出。
二:逐项组装单头 → 多头 → 完整 Pre-Norm block → 多层;每阶段观察新增参数与 shape。
三:看清轴与参数用有编号的元素追踪 split/transpose/merge;检查 ModuleList 注册和参数数量。
四:交给训练循环模型只负责 forward;完整保存格式与权重共享是后面的工程选读。

运行 python week10_stages.py --stage embedding,依次换成 single、multi、block、full;完整默认模型与 mini_gpt_walkthrough.py 一致。520 是指定配置的参数量,不是所有 MiniGPT 的固定大小。

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

Week 10 核心目标:把零件接成一个稳定接口

mini-gpt-v1 按空格切分,ordered tokens 固定为 [我, 喜欢, AI, 学习, 猫],IDs 固定为 0..4;它没有 BOS、EOS、PAD 或 UNK。Week 9 的 V=11、T=4 教学 artifact 已经结束,不能把那里的整数直接送入本模型。

Scroll horizontally to view all columns.

mini-gpt-v1,V=5
IDordered token
0
1喜欢
2AI
3学习
4

本周固定 mini-gpt-v1:B=2 是 batch 行数,T=2 是当前 token 位置数,C=4 是每个位置的表示宽度,H=2 是 attention head 数,d_head=C/H=2 是每个 head 的宽度,V=5 是候选 token 数,n_layer=2 是 Transformer block 数。L 只保留给 token stream 或生成历史长度,绝不用作层数。

Scroll horizontally to view all columns.

idx=[[0,1],[4,1]],targets=[[1,2],[1,0]];两者都是 torch.long [B,T]=[2,2]
batch rowidx IDsinput tokenstarget IDs四道 teacher-forced 题
b=0[0,1][我, 喜欢][1,2][喜欢, AI]
b=1[4,1][猫, 喜欢][1,0][喜欢, 我]
mini_gpt_walkthrough.py
import torch


idx = torch.tensor([
    [0, 1],  # 我 喜欢
    [4, 1],  # 猫 喜欢
], dtype=torch.long)  # [B,T] = [2,2]

targets = torch.tensor([
    [1, 2],  # 我→喜欢,喜欢→AI
    [1, 0],  # 猫→喜欢,喜欢→我
], dtype=torch.long)  # [B,T] = [2,2]

Concept sequence
  1. mini-gpt-v1 IDs [B,T] = [2,2]
  2. token + position representations [B,T,C] = [2,2,4]
  3. pre-norm Block 1 [2,2,4]
  4. pre-norm Block 2 [2,2,4]
  5. final LayerNorm [2,2,4]
  6. bias-free LM head logits [B,T,V] = [2,2,5]
  7. optional reshape [4,5] with targets [4] → scalar mean cross-entropy

Scroll horizontally to view all columns.

Course data table
caller modeforward inputforward outputcaller consumes
training / evaluationidx [2,2] + targets [2,2]logits [2,2,5] + scalar lossall four aligned positions
generationcropped context, no targetslogits [B,T,5] + Noneonly logits[:,-1,:] for one append

Knowledge check

idx 是 [2,2] 时,logits 与 flattened targets 分别是什么 shape?

1. 模型配置:每个数字控制什么

Scroll horizontally to view all columns.

Course data table
读代码时遇到先这样理解
class GPTConfig列出要建什么规格
config = GPTConfig()得到一份具体规格
class MiniGPT(nn.Module)定义按规格建模型以及如何计算
model = MiniGPT(config)创建一组实际参数
self.xxx这个模型自己保存的对象
forward / model(idx)用当前参数进行一次计算

dataclass 帮助少写配置样板代码;frozen=True 固定配置字段,不会阻止神经网络学习。类型标注如 idx: torch.Tensor 是给读者和检查工具的提示,真正的输入检查仍由函数内代码执行。

Configuration 回答“要建什么”;nn.Parameter 回答“训练会改变哪些数”。frozen=True 只阻止意外改写 config fields,并不会冻结模型参数。本最小架构刻意没有 dropout field。

mini_gpt_walkthrough.py
import hashlib
import json
from dataclasses import dataclass

import torch
import torch.nn as nn
import torch.nn.functional as F


@dataclass(frozen=True)
class GPTConfig:
    vocab_size: int = 5
    block_size: int = 2
    n_embd: int = 4
    n_head: int = 2
    n_layer: int = 2

    def validate(self) -> None:
        if self.vocab_size <= 0:
            raise ValueError("vocab_size must be positive")
        if self.block_size <= 0:
            raise ValueError("block_size must be positive")
        if self.n_embd <= 0:
            raise ValueError("n_embd must be positive")
        if self.n_head <= 0:
            raise ValueError("n_head must be positive")
        if self.n_layer <= 0:
            raise ValueError("n_layer must be positive")
        if self.n_embd % self.n_head != 0:
            raise ValueError("n_embd must be divisible by n_head")

Scroll horizontally to view all columns.

Course data table
field / value它防止的失败或歧义concrete owners
vocab_size=5输入 ID / 输出 class 没有合法范围token_embedding rows 与 lm_head outputs
block_size=2最大 context、position rows、mask 与 generation crop 不一致position_embedding、causal_mask、caller crop
n_embd=4组件的 representation width 不一致两种 embeddings、norms、attention、FFN、lm_head input
n_head=2multi-head 没有可执行的 width partitionqkv reshape 与 score head axis
n_layer=2block depth/order 含糊或重复模块未注册ModuleList construction 与 forward loop
dhead=nembdnhead=42=2d_{\mathrm{head}}=\frac{n_{\mathrm{embd}}}{n_{\mathrm{head}}}=\frac{4}{2}=2

这份合同要求 token_embedding.weight 为 [5,4]、position_embedding.weight 为 [2,4],并让 lm_head 把最后一维 4 映射为 5 个候选 scores。它也会原样进入 checkpoint compatibility metadata。

Knowledge check

哪个 config field 同时决定 position table 行数、causal mask 边长和 generation crop?

2. Configuration 的关键约束:让错误尽早发生

C=n_embd=4 是每个 block 的共同宽度,H=n_head=2 是 head 数,所以 4 mod 2=0 且 d_head=2。反例 n_embd=4、n_head=3 没有整数 head width,必须在 config.validate() 阶段拒绝,而不是等 view 失败。

CmodH=0,dhead=CH,1Tblock_sizeC\bmod H=0,\qquad d_{\mathrm{head}}=\frac{C}{H},\qquad 1\le T\le \mathrm{block\_size}

Scroll horizontally to view all columns.

Course data table
input / config结果尽早报告的原因
n_embd=4, n_head=2合法:d_head=2[B,T,4] 可重组为 [B,H,T,d_head]
n_embd=4, n_head=3ValueError四个 channels 无法均分成三个整数宽度 heads
idx shape [B,1] 或 [B,2]合法block_size 是上限,不要求每次填满
idx shape [B,0] 或 [B,3]ValueError空序列无最后位置;T=3 超出 position/mask capacity
float IDs 或 ID=5TypeError / ValueErrorembedding 只接受 torch.long 且地址范围为 0..4
python
config = GPTConfig()
config.validate()
assert config.n_embd // config.n_head == 2

try:
    GPTConfig(n_head=3).validate()
except ValueError as error:
    print(error)  # n_embd must be divisible by n_head

完整 MiniGPT.forward 会先检查 rank 与 dtype,再读取 B、T;在任何 min/max 之前先拒绝 empty tensor,随后检查 ID range。targets 也必须与 idx 同 shape、同为 torch.long 且 IDs 在 0..4。第 7 节会把这些条件写进 canonical class。

Knowledge check

为什么 [B,2,4] 可拆成两个 heads,却不能拆成三个等宽 heads?

3. GPT 数据流:先看输入输出,再看各层

MiniGPT.forward 总会计算 logits。training/evaluation caller 提供 aligned targets 时,它额外返回 scalar mean cross-entropy;generation caller 不提供 targets,只消费 logits。forward 是神经网络映射,不在内部改变 optimizer,也不挑选 next token。

Scroll horizontally to view all columns.

Course data table
ownerinputoutput / responsibility
calleridx [2,2],可选 targets [2,2]决定是 training/evaluation 还是 generation use
token_embeddingIDs 0..4identity representations [2,2,4]
position_embeddingpositions [2]position rows [2,4],沿 B broadcast
blocks[0] 与 blocks[1][2,2,4]依序产生 contextual [2,2,4]
final_norm + lm_head[2,2,4]每个位置的 logits [2,2,5]
loss branchlogits + aligned targets仅 targets 存在时得到 scalar mean CE
generation caller最后位置 logits [B,5]在 forward 外选择并 append next_id [B,1]
Concept sequence
  1. idx [2,2]
  2. token rows [2,2,4] + position rows [2,4] broadcast
  3. blocks[0] [2,2,4]
  4. blocks[1] [2,2,4]
  5. final_norm [2,2,4]
  6. lm_head logits [2,2,5]
  7. targets present? reshape logits [4,5] and targets [4] → mean loss []
logits=fθ(idx)RB×T×V\mathrm{logits}=f_{\theta}(\mathrm{idx})\in\mathbb{R}^{B\times T\times V}
L=CE ⁣(reshape(logits,[BT,V]),reshape(targets,[BT]))\mathcal{L}=\operatorname{CE}\!\left(\operatorname{reshape}(\mathrm{logits},[BT,V]),\operatorname{reshape}(\mathrm{targets},[BT])\right)

Knowledge check

training 和 generation 都一定得到哪个输出?哪个输出只在 targets 存在时得到?

4. Causal Self-Attention:用 Combined QKV 重实现同一操作

Q 表示当前 query 要找什么,K 表示每个可见位置如何被匹配,Value 表示匹配后带回什么。Week 7 为单个 head 展示三条独立 projections;这里用一个 bias-free qkv Linear 一次产生三份宽度 C 的 tensors,再显式增加 H 轴。数学工作相同,class organization 与 state-dict keys 不同。

mini_gpt_walkthrough.py
class CausalSelfAttention(nn.Module):
    def __init__(self, config: GPTConfig) -> None:
        super().__init__()
        self.n_head = config.n_head
        self.head_size = config.n_embd // config.n_head
        self.qkv = nn.Linear(
            config.n_embd,
            3 * config.n_embd,
            bias=False,
        )
        self.output_projection = nn.Linear(
            config.n_embd,
            config.n_embd,
        )
        mask = torch.tril(
            torch.ones(config.block_size, config.block_size)
        )
        self.register_buffer(
            "causal_mask",
            mask.view(1, 1, config.block_size, config.block_size),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B, T, C = x.shape
        q, k, value_states = self.qkv(x).chunk(3, dim=-1)
        q = q.view(B, T, self.n_head, self.head_size).transpose(1, 2)
        k = k.view(B, T, self.n_head, self.head_size).transpose(1, 2)
        value_states = value_states.view(
            B,
            T,
            self.n_head,
            self.head_size,
        ).transpose(1, 2)

        scores = (q @ k.transpose(-2, -1)) * (self.head_size ** -0.5)
        visible = self.causal_mask[:, :, :T, :T]
        scores = scores.masked_fill(visible == 0, float("-inf"))
        weights = F.softmax(scores, dim=-1)
        output = weights @ value_states
        output = output.transpose(1, 2).contiguous().view(B, T, C)
        return self.output_projection(output)

Concept sequence
  1. x [B,T,C] = [2,2,4]
  2. qkv(x) [2,2,12]
  3. chunk → q, k, value_states each [2,2,4]
  4. reshape + transpose → each [B,H,T,d_head] = [2,2,2,2]
  5. scores / weights [B,H,T,T] = [2,2,2,2]
  6. weighted Values [2,2,2,2]
  7. transpose + contiguous + view [2,2,4]
  8. biased output_projection [2,2,4]

Scroll horizontally to view all columns.

每个 batch row 与每个 head 共享 [[1,0],[1,1]];runtime T=1 时 slice 为 [1,1,1,1]
causal_mask row=query / column=keyj=0: first tokenj=1: second token
t=0: first query1 allow0 forbid
t=1: final 喜欢 query1 allow1 allow
A=softmax ⁣(QKdhead+M),Attention(X)=AValueStatesA=\operatorname{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_{\mathrm{head}}}}+M\right),\qquad \operatorname{Attention}(X)=A\,\mathrm{ValueStates}

register_buffer 让 causal_mask 随 model.to(device)、state_dict 与 module traversal 一起管理,却不会被 optimizer 更新。最终 喜欢 的 query 可读取 position 0:第一行读到 我,第二行读到 猫,因此 contextual outputs 可能不同;随机 weights 并不保证某个具体预测。

Knowledge check

query position 0 的哪些 key columns 可以有非零 weight?为什么?

5. 拆分 Heads 时的 Shape:数字相同也要读 Axis

本周固定 mini-gpt-v1:B=2 是 batch 行数,T=2 是当前 token 位置数,C=4 是每个位置的表示宽度,H=2 是 attention head 数,d_head=C/H=2 是每个 head 的宽度,V=5 是候选 token 数,n_layer=2 是 Transformer block 数。L 只保留给 token stream 或生成历史长度,绝不用作层数。

Scroll horizontally to view all columns.

数值 shape 多次相同,语义 axis order 不同
stageshape with named axesoperation meaning
q after chunk[B,T,C]=[2,2,4]每个 token row 有四个 q channels
q.view[B,T,H,d_head]=[2,2,2,2]把 C=4 分组为 2×2,不改变元素数
q.transpose(1,2)[B,H,T,d_head]=[2,2,2,2]每个 head 获得自己的 T×d_head matrix
q @ kᵀ[B,H,T,T]=[2,2,2,2]第三轴为 query t,第四轴为 key j
weights @ Values[B,H,T,d_head]=[2,2,2,2]每个 query 得到两个 head features
transpose back[B,T,H,d_head]=[2,2,2,2]把 token position 放回 head 之前
contiguous().view[B,T,C]=[2,2,4]在 feature axis 合并 H×d_head
Qb,hRT×dhead,Qb,hKb,hRT×TQ_{b,h}\in\mathbb{R}^{T\times d_{\mathrm{head}}},\qquad Q_{b,h}K_{b,h}^{\top}\in\mathbb{R}^{T\times T}
Hdhead=22=C=4H\,d_{\mathrm{head}}=2\cdot2=C=4

transpose 改变 stride/layout,不复制数学元素;随后若用 view 假定 features 在内存中相邻,就先调用 contiguous()。reshape 有时会自行复制,但这里显式 contiguous().view 让 rejoin 意图可见。

调试时另建 GPTConfig(block_size=3),保持 C=4、H=2,输入 B=1,T=3。应经过 [1,3,4]→[1,3,2,2]→[1,2,3,2],score 为 [1,2,3,3]。不要拿 T=3 直接喂给本章默认 block_size=2 的模型;额外检查使用独立实例,不保存成 canonical checkpoint。

Knowledge check

计算 scores 前 q 的语义 axis order 是什么?

6. FeedForward 与 Block:通信之后逐位置计算

Attention 是 cross-position communication:喜欢@1 可以读取 我@0 或 猫@0。FFN 则对每个 x[b,t,:] 单独应用同一函数,不直接读取另一行。Pre-Norm 先规范 branch input,再把 branch update 加回未经该 norm 替换的 residual state。

mini_gpt_walkthrough.py
class FeedForward(nn.Module):
    def __init__(self, config: GPTConfig) -> None:
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(config.n_embd, 4 * config.n_embd),
            nn.GELU(),
            nn.Linear(4 * config.n_embd, config.n_embd),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.net(x)


class TransformerBlock(nn.Module):
    def __init__(self, config: GPTConfig) -> None:
        super().__init__()
        self.ln1 = nn.LayerNorm(config.n_embd)
        self.attention = CausalSelfAttention(config)
        self.ln2 = nn.LayerNorm(config.n_embd)
        self.feed_forward = FeedForward(config)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = x + self.attention(self.ln1(x))
        x = x + self.feed_forward(self.ln2(x))
        return x

Concept sequence
  1. x0 [2,2,4]
  2. ln1(x0) [2,2,4] → attention [2,2,4]
  3. x1 = x0 + attention update [2,2,4]
  4. ln2(x1) [2,2,4] → FFN [2,2,4] through 4→16→4
  5. x2 = x1 + FFN update [2,2,4]
  6. 第二个独立 TransformerBlock 重复同一 shape contract
x=x+Attention(LN1(x)),xout=x+FFN(LN2(x))x' = x+\operatorname{Attention}(\operatorname{LN}_1(x)),\qquad x_{\mathrm{out}}=x'+\operatorname{FFN}(\operatorname{LN}_2(x'))
FFN(u)=GELU(uW1+b1)W2+b2\operatorname{FFN}(u)=\operatorname{GELU}(uW_1^{\top}+b_1)W_2^{\top}+b_2

Scroll horizontally to view all columns.

Course data table
sublayermixes positions?input → internal → output
causal attention是,只读 j≤t[2,2,4] → scores [2,2,2,2] → [2,2,4]
feed_forward否,每个 [b,t] 独立[2,2,4] → [2,2,16] → [2,2,4]
residual add否,逐元素相加[2,2,4] + [2,2,4] → [2,2,4]

Knowledge check

哪个 sublayer 能让 喜欢@1 使用 position 0,哪个只处理 喜欢@1 已有的 row?

7. 完整 MiniGPT:把已经学过的计算接起来

以下代码继续使用前面已定义的 imports、GPTConfig、CausalSelfAttention、FeedForward 与 TransformerBlock。__init__ 注册持久架构;forward 执行一次计算。canonical mini-gpt-v1 是两个 pre-norm blocks、untied token embedding/LM head,且没有 dropout。

mini_gpt_walkthrough.py
class MiniGPT(nn.Module):
    def __init__(self, config: GPTConfig) -> None:
        super().__init__()
        config.validate()
        self.config = config
        self.token_embedding = nn.Embedding(
            config.vocab_size,
            config.n_embd,
        )
        self.position_embedding = nn.Embedding(
            config.block_size,
            config.n_embd,
        )
        self.blocks = nn.ModuleList(
            [TransformerBlock(config) for _ in range(config.n_layer)]
        )
        self.final_norm = nn.LayerNorm(config.n_embd)
        self.lm_head = nn.Linear(
            config.n_embd,
            config.vocab_size,
            bias=False,
        )
        self.apply(self._init_weights)

    @staticmethod
    def _init_weights(module: nn.Module) -> None:
        if isinstance(module, nn.Linear):
            nn.init.normal_(module.weight, mean=0.0, std=0.02)
            if module.bias is not None:
                nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            nn.init.normal_(module.weight, mean=0.0, std=0.02)

    @staticmethod
    def _validate_token_ids(
        token_ids: torch.Tensor,
        *,
        name: str,
        vocab_size: int,
    ) -> None:
        if token_ids.dtype != torch.long:
            raise TypeError(f"{name} must have dtype torch.long")
        if token_ids.numel() == 0:
            raise ValueError(f"{name} must contain at least one token ID")
        minimum_id = int(token_ids.min().item())
        maximum_id = int(token_ids.max().item())
        if minimum_id < 0 or maximum_id >= vocab_size:
            raise ValueError(
                f"{name} IDs must be in [0, {vocab_size - 1}]"
            )

    def forward(
        self,
        idx: torch.Tensor,
        targets: torch.Tensor | None = None,
    ) -> tuple[torch.Tensor, torch.Tensor | None]:
        if idx.ndim != 2:
            raise ValueError("idx must have rank 2 with shape [B,T]")
        if idx.dtype != torch.long:
            raise TypeError("idx must have dtype torch.long")

        B, T = idx.shape
        if T < 1 or T > self.config.block_size:
            raise ValueError(
                f"sequence length must be in [1, {self.config.block_size}]"
            )
        self._validate_token_ids(
            idx,
            name="idx",
            vocab_size=self.config.vocab_size,
        )

        if targets is not None:
            if targets.shape != idx.shape:
                raise ValueError("targets must have the same shape as idx")
            if targets.dtype != torch.long:
                raise TypeError("targets must have dtype torch.long")
            self._validate_token_ids(
                targets,
                name="targets",
                vocab_size=self.config.vocab_size,
            )

        positions = torch.arange(T, device=idx.device)  # [T]
        token_rows = self.token_embedding(idx)  # [B,T,C]
        position_rows = self.position_embedding(positions)  # [T,C]
        x = token_rows + position_rows  # broadcast to [B,T,C]
        for block in self.blocks:
            x = block(x)  # [B,T,C]
        x = self.final_norm(x)  # [B,T,C]
        logits = self.lm_head(x)  # [B,T,V]

        loss = None
        if targets is not None:
            loss = F.cross_entropy(
                logits.reshape(B * T, self.config.vocab_size),
                targets.reshape(B * T),
            )
        return logits, loss

Scroll horizontally to view all columns.

这些 names 从 Week 10 到 Week 12 保持不变,state-dict keys 才能稳定
stable ownercanonical shape / children
token_embedding.weight[V,C]=[5,4]
position_embedding.weight[block_size,C]=[2,4]
blocksModuleList 长度 n_layer=2;每项含 ln1, attention, ln2, feed_forward
attentionqkv, output_projection, causal_mask
final_normweight [4] + bias [4]
lm_head.weight[V,C]=[5,4],bias-free 且与 token_embedding untied
xb,t=Etoken[idxb,t]+Eposition[t]RCx_{b,t}=E_{\mathrm{token}}[\mathrm{idx}_{b,t}]+E_{\mathrm{position}}[t]\in\mathbb{R}^{C}
Concept sequence
  1. idx [2,2] passes rank / long / nonempty / T / ID checks
  2. positions [2]
  3. token_rows [2,2,4] + position_rows [2,4] → x [2,2,4]
  4. blocks[0] [2,2,4] → blocks[1] [2,2,4]
  5. final_norm [2,2,4] → lm_head logits [2,2,5]
  6. targets [2,2] → logits [4,5] plus targets [4] → scalar mean CE

Knowledge check

为什么 positions 是 [T],而 idx 是 [B,T]?

8. ModuleList 为什么重要:能执行不等于被 Model 拥有

把 nn.Module 赋给另一个 nn.Module 的 attribute 会注册它;可变数量的 children 应放进 nn.ModuleList。于是 blocks.0.attention.qkv.weight 与 blocks.1.attention.qkv.weight 成为独立、可命名的 parameters,两个 causal_mask 也成为 buffers。

python
class IncorrectStack(nn.Module):
    def __init__(self, config: GPTConfig) -> None:
        super().__init__()
        self.blocks = [
            TransformerBlock(config) for _ in range(config.n_layer)
        ]


class RegisteredStack(nn.Module):
    def __init__(self, config: GPTConfig) -> None:
        super().__init__()
        self.blocks = nn.ModuleList(
            [TransformerBlock(config) for _ in range(config.n_layer)]
        )


config = GPTConfig()
incorrect = IncorrectStack(config)
registered = RegisteredStack(config)

assert len(list(incorrect.parameters())) == 0
assert len(list(registered.parameters())) > 0
assert "blocks.0.attention.qkv.weight" in registered.state_dict()

Scroll horizontally to view all columns.

Course data table
operationplain list childrenModuleList children
forward 手动 loop可以可以
model.parameters() / optimizer遗漏包含
state_dict()遗漏 child state包含 named parameters 与 persistent buffers
model.to(device)不递归移动递归移动
train() / eval()不递归切换递归切换
Blocki:RB×T×CRB×T×C,i{0,1}\operatorname{Block}_i:\mathbb{R}^{B\times T\times C}\to\mathbb{R}^{B\times T\times C},\qquad i\in\{0,1\}

Knowledge check

普通 list 最少会造成哪两类实际遗漏?

9. Parameter 到底在哪里:把长期 State 与临时 Tensor 分开

Parameter 是注册且通常 requires_grad=True 的持久 Tensor;backward 把 gradient 写到 parameter.grad,optimizer 再更新 parameter。Buffer 会随 model 移动和保存,却不由 optimizer 学习。idx、positions、scores、weights、logits 与 loss 只是当前计算的 inputs/activations。

Scroll horizontally to view all columns.

Course data table
owner / objectshape in canonical modelkind
token_embedding.weight[5,4]learned parameter
position_embedding.weight[2,4]learned parameter
blocks.i.attention.qkv.weight[12,4],无 biaslearned parameter
blocks.i.attention.output_projectionweight [4,4] + bias [4]learned parameters
blocks.i.ln1 / ln2各 weight [4] + bias [4]learned parameters
blocks.i.feed_forward.net.0weight [16,4] + bias [16]learned parameters
blocks.i.feed_forward.net.2weight [4,16] + bias [4]learned parameters
blocks.i.attention.causal_mask[1,1,2,2]registered buffer,not trained
final_normweight [4] + bias [4]learned parameters
lm_head.weight[5,4],无 bias 且 untiedlearned parameter
idx / positions / scores / logits / loss随 call 改变inputs or temporary activations
θ={θi}i=1m,θiθiηLθi\theta=\{\theta_i\}_{i=1}^{m},\qquad \theta_i\leftarrow\theta_i-\eta\frac{\partial\mathcal{L}}{\partial\theta_i}

Knowledge check

causal_mask 会训练吗?它会随模型保存和移动吗?

10. 手算参数量:默认不共享权重时为 520

这里 V=5、block_size=2、C=4、n_layer=2。QKV 与 LM head 无 bias;attention output_projection 和两个 FFN linears 有 bias;每个 block 有两个带 scale/bias 的 LayerNorm;token_embedding 与 lm_head 是两份独立 parameters。

Scroll horizontally to view all columns.

nn.Linear(in_features,out_features) stores weight [out_features,in_features]
ownercalculationparameters
token embeddingV×C = 5×420
position embeddingblock_size×C = 2×48
one attentionqkv 3C×C = 12×4;output C×C+C = 4×4+448+20=68
one FFN(4C×C+4C) + (C×4C+C)80+68=148
two block LayerNorms2×(C+C)16
one whole block68+148+16232
two independent blocksn_layer×232 = 2×232464
final LayerNormC+C8
independent bias-free LM headV×C = 5×420
canonical untied total20+8+464+8+20520
Nparams=VC+block_sizeC+nlayer(12C2+10C)+2C+VC=520N_{\mathrm{params}}=VC+\mathrm{block\_size}\,C+n_{\mathrm{layer}}(12C^2+10C)+2C+VC=520
python
canonical_model = MiniGPT(GPTConfig())
parameter_count = sum(
    parameter.numel() for parameter in canonical_model.parameters()
)
assert parameter_count == 520

causal_mask [1,1,2,2] 是 buffer,所以贡献零个 trainable parameters。两个 blocks 只共享 class definition 与 shapes,不共享 tensor storage,因此必须算两次。

Knowledge check

为什么一个 block 的两个 LayerNorm 一共有 16 个 parameters?

12. 正确性首先检查 Shape 与 API Boundary

固定 idx 与 targets 经过同一个 canonical model:两行、每行两个 token positions、每个位置五个 logits;四个 target IDs 与四个 logit rows 对齐。loss 的 shape [] 表示 rank-0 scalar,不是长度一 vector。

python
config = GPTConfig()
model = MiniGPT(config)
logits, loss = model(idx, targets)

assert logits.shape == (2, 2, 5)
assert loss is not None and loss.ndim == 0
assert torch.isfinite(loss)

logits_without_targets, no_loss = model(idx)
assert logits_without_targets.shape == (2, 2, 5)
assert no_loss is None

# Boundary failures are deliberate and readable.
invalid_cases = [
    torch.tensor([0, 1], dtype=torch.long),  # rank 1
    torch.empty((2, 0), dtype=torch.long),  # T=0
    torch.tensor([[0, 1, 2]], dtype=torch.long),  # T=3
    torch.tensor([[0.0, 1.0]]),  # wrong dtype
    torch.tensor([[0, 5]], dtype=torch.long),  # ID out of 0..4
]
for invalid_idx in invalid_cases:
    try:
        model(invalid_idx)
    except (TypeError, ValueError):
        pass
    else:
        raise AssertionError("invalid idx was accepted")

invalid_target_cases = [
    torch.tensor([[1, 2]], dtype=torch.long),  # shape mismatch
    torch.tensor([[1.0, 2.0], [1.0, 0.0]]),  # wrong dtype
    torch.tensor([[1, 5], [1, 0]], dtype=torch.long),  # ID out of 0..4
]
for invalid_targets in invalid_target_cases:
    try:
        model(idx, invalid_targets)
    except (TypeError, ValueError):
        pass
    else:
        raise AssertionError("invalid targets were accepted")

Concept sequence
  1. idx [2,2]
  2. embeddings [2,2,4]
  3. block 1 [2,2,4]
  4. block 2 [2,2,4]
  5. logits [2,2,5]
  6. reshape logits [4,5] + targets [4]
  7. mean cross-entropy loss []
[2,2][2,2,4][2,2,4][2,2,4][2,2,5][4,5]+[4][][2,2]\to[2,2,4]\to[2,2,4]\to[2,2,4]\to[2,2,5]\to[4,5]+[4]\to[]

Scroll horizontally to view all columns.

Course data table
successful assertion它证明什么它还没证明什么
logits.shape==(2,2,5)top-level output interfacecausal mask 方向或 labels 语义
loss.ndim==0 and finitemean CE 返回可用 scalar模型已经学会语料
no targets → no_loss is Noneinference branch contractgeneration append 正确
invalid inputs raiseAPI boundary 拒绝已知坏数据所有可能错误都被覆盖

Knowledge check

成功完成 shape/API assertions 后,还需哪项检查才能确认 future token 没有泄漏?

13. 必须检查 Causal 性:比较可观察行为

两行都以 我(ID 0)开始;第二个 token 分别是 喜欢(ID 1)与 猫(ID 4)。query position 0 只允许读取 key column 0,所以它的五个 vocabulary logits 必须一致。position 1 可以变化,因为它允许读取自己。

python
model.eval()
past_same_future_changed = torch.tensor([
    [0, 1],  # 我 喜欢
    [0, 4],  # 我 猫
], dtype=torch.long)

with torch.no_grad():
    causal_logits, _ = model(past_same_future_changed)

assert causal_logits.shape == (2, 2, 5)
assert torch.allclose(
    causal_logits[0, 0, :],
    causal_logits[1, 0, :],
)

Scroll horizontally to view all columns.

Course data table
slicemeaningexpected comparison
causal_logits[0,0,:]row 0,earlier query t=0,五个 candidate logits与 row 1 的 t=0 相同
causal_logits[1,0,:]row 1,same 我 prefix at t=0与 row 0 的 t=0 相同
causal_logits[:,1,:]两行 final positions允许不同,不用于 future-leak assertion
logitst(xt,x>t)=logitst(xt,x>t)\mathrm{logits}_{t}(x_{\le t},x_{>t})=\mathrm{logits}_{t}(x_{\le t},x'_{>t})
Mt,j=forj>tM_{t,j}=-\infty\quad\text{for}\quad j>t

本 canonical model 没有 Dropout,所以同一次 eval forward 的比较是确定的;model.eval() 仍是正确习惯,因为将来的 Dropout 或 BatchNorm model 会让 mode 改变行为。

Knowledge check

这里若 causal_logits[0,0,:] 与 causal_logits[1,0,:] 不同,最可能是哪类错误?

15. 从 Week 2 的公式到 GPT:仍是 Affine Maps 与 Gradient Updates

PyTorch nn.Linear(in_features,out_features) 保存 weight [out_features,in_features],对 row-vector input 计算 XWᵀ+b。Combined qkv 把每个四维 row 变成十二维并切成三份;LM head 再把最终四维 row 变成五个 categorical next-token scores。

Scroll horizontally to view all columns.

Course data table
componentinput / stored weightoutputbias policy
combined qkvX[...,4],W_qkv [12,4]QKV[...,12]无 bias
attention output_projectionX[...,4],W_o [4,4]X[...,4]bias [4]
FFN first / second[...,4]→[...,16]→[...,4]per-token nonlinear updatebias [16] 与 [4]
bias-free lm_headH[...,4],W_head [5,4]logits[...,5]无 bias
Y=XW+b,X:[,4],W:[12,4],Y:[,12]Y=XW^{\top}+b,\qquad X:[\ldots,4],\quad W:[12,4],\quad Y:[\ldots,12]
Z=HWhead,H:[B,T,4],Whead:[5,4],Z:[B,T,5]Z=H\,W_{\mathrm{head}}^{\top},\qquad H:[B,T,4],\quad W_{\mathrm{head}}:[5,4],\quad Z:[B,T,5]
L=1BTb=1Bt=1Tlogpθ(yb,txb,t)\mathcal{L}=-\frac{1}{BT}\sum_{b=1}^{B}\sum_{t=1}^{T}\log p_{\theta}(y_{b,t}\mid x_{b,\le t})

Backward 仍按 chain rule 把 loss gradient 传到每个参与的 parameter,optimizer 再更新 theta。Attention 让 earlier positions 进入 current representation,GELU 提供非线性,两条 residual paths 与 norms 支持组合深度。

Knowledge check

LM head 在一个 position 输出的五个数表示什么?

16. Week 10 最应该理解的 7 件事

  1. Config 是 shared shape/checkpoint contract;C=4 可被 H=2 整除,所以 d_head=2。
  2. mini-gpt-v1 的 token identity [B,T] 加 position rows [T] 后得到 [B,T,4];它与 Week 9 的 V=11 artifact 不兼容。
  3. 两个独立 pre-norm blocks 都保持 [B,T,4]:attention 因果混合可见 positions,feed_forward 逐位置处理 channels。
  4. final_norm 与 bias-free lm_head 把 [B,T,4] 变为 [B,T,5] raw logits,而不是 probabilities。
  5. 有 targets 时,[2,2,5]→[4,5] 与 [2,2]→[4] 得 scalar mean CE;没有 targets 时 loss=None。
  6. registered modules 持有的 parameters 会被 model.parameters() 枚举,并在 caller 把它们传给 optimizer 后成为可优化对象;registered parameters 与 persistent registered buffers 会进入 state_dict 并随 module 移动;train()/eval() 的 mode 递归作用于 modules;buffers 不是 optimizer parameters。canonical token table/head untied,总参数 520。
  7. 可用 checkpoint 需要 matching code、exact config、untied policy 与 tokenizer identity;generation 只 crop forward context、只读 last logits,却保留完整 history。
Concept sequence
  1. [我,喜欢] / [猫,喜欢] → idx [2,2]
  2. token + position [2,2,4]
  3. two causal pre-norm blocks [2,2,4]
  4. final norm + head → logits [2,2,5]
  5. generation context = history[:,-2:]
  6. next_logits = logits[:,-1,:] [B,5]
  7. next_id [B,1] append to uncropped history

这里把生成总记录的长度称为 L_history;它可以大于 2。每轮真正进入 model 的 T_context 至多为 block_size=2,避免把 L 错当 layer count。

Tcontext=min(Lhistory,block_size),block_size=2T_{\mathrm{context}}=\min(L_{\mathrm{history}},\mathrm{block\_size}),\qquad \mathrm{block\_size}=2

Knowledge check

为何两个 prompt 都以“喜欢”结尾,最后 logits 仍可能不同?

11. 选读:输入表与输出层怎样共享权重

Canonical MiniGPT 保持 token_embedding.weight 与 lm_head.weight 独立,所以是 520 parameters。可选实验把 output weight 指向 input table;lookup 与 output scoring 随后从两条路径向同一 tensor 累积 gradients。它是另一个明确标注的 architecture variant,不是默认修补。

python
# Optional teaching variant only; canonical mini-gpt-v1 stays untied.
tied_variant = MiniGPT(GPTConfig())
tied_variant.lm_head.weight = tied_variant.token_embedding.weight

assert tied_variant.lm_head.weight is tied_variant.token_embedding.weight
assert sum(p.numel() for p in tied_variant.parameters()) == 500

Wout=EtokenRV×C,[B,T,C]Wout[B,T,V]W_{\mathrm{out}}=E_{\mathrm{token}}\in\mathbb{R}^{V\times C},\qquad [B,T,C]\,W_{\mathrm{out}}^{\top}\to[B,T,V]

Scroll horizontally to view all columns.

Course data table
policytwo [5,4] names contributewhole-model totalcheckpoint identity
canonical untied20+20 distinct parameters520weight_policy=untied
optional tied variant20 unique parameters500必须明确记录 tied policy 并在 construction 时重建 alias

Knowledge check

可选 tied variant 中,两张 [5,4] names 合计贡献多少 unique parameters?

14. 工程选读:保存格式与词表一致性

mini-gpt-v1 的 tokenizer artifact 只含 version=mini-gpt-v1、ordered tokens [我,喜欢,AI,学习,猫] 与 policy=whitespace-delimited;no-specials;no-pad;no-unk。canonical bytes 用 JSON 的 sorted keys、无多余空格 separators、原样 Unicode 和 UTF-8 编码得到,再计算 SHA-256;因此同一 artifact 在不同进程中产生同一 digest。相同 V=5 绝不等于相同 ID semantics。

mini_gpt_walkthrough.py
def make_tokenizer_artifact(
    *,
    version: str,
    ordered_tokens: tuple[str, ...],
    policy: str,
) -> dict[str, object]:
    return {
        "version": version,
        "ordered_tokens": list(ordered_tokens),
        "policy": policy,
    }


def tokenizer_artifact_sha256(artifact: dict[str, object]) -> str:
    canonical_bytes = json.dumps(
        artifact,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return hashlib.sha256(canonical_bytes).hexdigest()


def canonical_mini_gpt_tokenizer_artifact() -> dict[str, object]:
    return make_tokenizer_artifact(
        version="mini-gpt-v1",
        ordered_tokens=("我", "喜欢", "AI", "学习", "猫"),
        policy="whitespace-delimited;no-specials;no-pad;no-unk",
    )


def validate_checkpoint_tokenizer_identity(
    checkpoint: dict[str, object],
    *,
    expected_ordered_tokens: tuple[str, ...],
    expected_tokenizer_policy: str,
    expected_tokenizer_version: str,
) -> None:
    stored_tokenizer = checkpoint.get("tokenizer")
    if not isinstance(stored_tokenizer, dict):
        raise ValueError("checkpoint tokenizer metadata is missing")
    required_keys = {"version", "ordered_tokens", "policy", "sha256"}
    if set(stored_tokenizer) != required_keys:
        raise ValueError("checkpoint tokenizer metadata has unexpected keys")

    stored_artifact = {
        "version": stored_tokenizer["version"],
        "ordered_tokens": stored_tokenizer["ordered_tokens"],
        "policy": stored_tokenizer["policy"],
    }
    stored_digest = stored_tokenizer["sha256"]
    if not isinstance(stored_digest, str):
        raise ValueError("checkpoint tokenizer SHA-256 must be text")
    try:
        recomputed_digest = tokenizer_artifact_sha256(stored_artifact)
    except (TypeError, ValueError) as error:
        raise ValueError(
            "checkpoint tokenizer artifact is not canonical JSON data"
        ) from error
    if recomputed_digest != stored_digest:
        raise ValueError("checkpoint tokenizer artifact failed SHA-256 check")

    expected_artifact = make_tokenizer_artifact(
        version=expected_tokenizer_version,
        ordered_tokens=expected_ordered_tokens,
        policy=expected_tokenizer_policy,
    )
    canonical_artifact = canonical_mini_gpt_tokenizer_artifact()
    if expected_artifact != canonical_artifact:
        raise ValueError("caller tokenizer identity is not mini-gpt-v1")
    expected_digest = tokenizer_artifact_sha256(expected_artifact)
    if stored_artifact != expected_artifact:
        raise ValueError("stored tokenizer artifact does not match caller")
    if stored_digest != expected_digest:
        raise ValueError("stored tokenizer digest does not match caller")


def save_mini_gpt_training_checkpoint(
    path: str,
    *,
    model: MiniGPT,
    optimizer: torch.optim.Optimizer,
    completed_updates: int,
    ordered_tokens: tuple[str, ...],
    tokenizer_policy: str,
    tokenizer_version: str,
) -> None:
    if type(completed_updates) is not int or completed_updates < 0:
        raise ValueError("completed_updates must be a non-negative integer")
    tokenizer_artifact = make_tokenizer_artifact(
        version=tokenizer_version,
        ordered_tokens=ordered_tokens,
        policy=tokenizer_policy,
    )
    if tokenizer_artifact != canonical_mini_gpt_tokenizer_artifact():
        raise ValueError("tokenizer artifact does not match mini-gpt-v1")
    tokenizer_digest = tokenizer_artifact_sha256(tokenizer_artifact)
    if model.config != GPTConfig():
        raise ValueError("model config is not the canonical GPTConfig")
    if model.lm_head.weight is model.token_embedding.weight:
        raise ValueError("canonical checkpoint requires untied weights")

    checkpoint = {
        "schema": {
            "name": "mini-gpt-training-checkpoint",
            "version": 1,
        },
        "tokenizer": {
            **tokenizer_artifact,
            "sha256": tokenizer_digest,
        },
        "config": {
            "vocab_size": model.config.vocab_size,
            "block_size": model.config.block_size,
            "n_embd": model.config.n_embd,
            "n_head": model.config.n_head,
            "n_layer": model.config.n_layer,
        },
        "weight_policy": {
            "token_embedding_lm_head": "untied",
        },
        "model_state": model.state_dict(),
        "optimizer": {
            "class": (
                f"{optimizer.__class__.__module__}."
                f"{optimizer.__class__.__qualname__}"
            ),
            "state": optimizer.state_dict(),
        },
        "completed_updates": completed_updates,
    }
    torch.save(checkpoint, path)


def load_mini_gpt_for_inference(
    path: str,
    *,
    expected_ordered_tokens: tuple[str, ...],
    expected_tokenizer_policy: str,
    expected_tokenizer_version: str,
    map_location: str | torch.device,
) -> MiniGPT:
    checkpoint = torch.load(
        path,
        map_location=map_location,
        weights_only=False,
    )
    if not isinstance(checkpoint, dict):
        raise ValueError("checkpoint must be a dictionary")
    if checkpoint.get("schema") != {
        "name": "mini-gpt-training-checkpoint",
        "version": 1,
    }:
        raise ValueError("checkpoint schema/version mismatch")

    validate_checkpoint_tokenizer_identity(
        checkpoint,
        expected_ordered_tokens=expected_ordered_tokens,
        expected_tokenizer_policy=expected_tokenizer_policy,
        expected_tokenizer_version=expected_tokenizer_version,
    )

    expected_config = GPTConfig()
    expected_config_fields = {
        "vocab_size": expected_config.vocab_size,
        "block_size": expected_config.block_size,
        "n_embd": expected_config.n_embd,
        "n_head": expected_config.n_head,
        "n_layer": expected_config.n_layer,
    }
    if checkpoint.get("config") != expected_config_fields:
        raise ValueError("checkpoint config mismatch")
    if checkpoint.get("weight_policy") != {
        "token_embedding_lm_head": "untied",
    }:
        raise ValueError("checkpoint weight policy mismatch")

    model = MiniGPT(expected_config).to(map_location)
    model.load_state_dict(checkpoint["model_state"], strict=True)
    return model

Scroll horizontally to view all columns.

sort_keys=True、separators=(",", ":")、ensure_ascii=False,再以 UTF-8 编码
canonical identity representationexact value
UTF-8 JSON text{"ordered_tokens":["我","喜欢","AI","学习","猫"],"policy":"whitespace-delimited;no-specials;no-pad;no-unk","version":"mini-gpt-v1"}
SHA-256 hex digest38d630f4c589664c9bef567457d48764cbe2307734777e80f7d5d5c63ac88dd6

保存端不再接受任意 hash 字符串:它从 exact artifact 直接计算 digest。加载端先从 stored fields 重建同样的 canonical bytes,验证 stored digest,再把 stored artifact 与 digest 同 caller 明确提供的 expected identity 比较;这些检查全部发生在构造和使用模型之前。SHA-256 能绑定这里记录的 bytes 并发现意外损坏或 identity mismatch,但它不是签名:它不证明来源可信,也不证明未记录的 tokenizer code、Unicode normalization 或 split behavior 等实现细节相同。

Scroll horizontally to view all columns.

Course data table
restore goalrequired fieldswhat may be omitted
inference onlyschema/version、tokenizer identity、exact config、untied policy、model_stateoptimizer state 与 completed_updates
faithful optimizer resumeinference fields + optimizer class/state + unambiguous completed_updates不能省略 optimizer moments 或把 update index 猜成 count
bit-for-bit resume claim还需相同 data order 与所用 CPU/CUDA/Python RNG state本最小函数不宣称保存这些额外状态

Scroll horizontally to view all columns.

Course data table
saved key examplecompatibility consequence
tokenizer.sha256先对 stored version/tokens/policy 重新 canonicalize 并验 digest,再与 caller expected identity 比较
token_embedding.weight [5,4]无法 strict-load 到 [6,4] 或 [5,8]
blocks.1.attention.qkv.weight [12,4]一层 model 没有 blocks.1;renamed member 也会 key mismatch
blocks.i.attention.causal_mask [1,1,2,2]registered buffer 也在 state management 中
weight_policy=untied不能静默恢复为 tied alias
loadstrict:{nameshape}saved={nameshape}constructed\operatorname{load}_{\mathrm{strict}}:\{\mathrm{name}\mapsto\mathrm{shape}\}_{\mathrm{saved}}=\{\mathrm{name}\mapsto\mathrm{shape}\}_{\mathrm{constructed}}

Knowledge check

为什么两个 tokenizer 都是 V=5,checkpoint 仍可能不可用?

17. Week 10 → Week 11:预测函数怎样接入训练循环

所有 interacting tensors 必须与 model 在同一 device。model.train() 与 model.eval() 标记 mode;本 Week 10 架构没有 Dropout 或 BatchNorm,所以两种 mode 的数值 forward 相同,但未来含这些 modules 时会改变行为。eval() 本身不关闭 gradients,仍要配合 torch.no_grad()。

mini_gpt_walkthrough.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)

idx_on_device = idx.to(device)
targets_on_device = targets.to(device)

model.train()
logits, loss = model(idx_on_device, targets_on_device)
assert logits.shape == (2, 2, 5)
assert loss is not None
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()  # exactly one completed update

model.eval()
with torch.no_grad():
    evaluation_logits, no_loss = model(idx_on_device)
assert evaluation_logits.shape == (2, 2, 5)
assert no_loss is None

Scroll horizontally to view all columns.

Course data table
line / phasestate change or observation
model.train()递归设置 module training flags;本 no-dropout model 数值不变
forward with targets构建 logits 与 scalar mean-loss computation graph
zero_grad(set_to_none=True)清除上一次 parameter.grad storage
loss.backward()计算并累积 gradients,不直接改 parameter values
optimizer.step()使用 gradients/moments 改变 parameters;completed_updates 增加 1
eval() + no_grad()设置 evaluation mode 并避免构建 gradient graph
mini_gpt_walkthrough.py
@torch.no_grad()
def generate_mini_gpt(
    model: MiniGPT,
    history: torch.Tensor,
    max_new_tokens: int,
) -> torch.Tensor:
    if max_new_tokens < 0:
        raise ValueError("max_new_tokens cannot be negative")

    was_training = model.training
    model.eval()
    for _ in range(max_new_tokens):
        context = history[:, -model.config.block_size:]
        logits, _ = model(context)
        next_logits = logits[:, -1, :]
        next_id = torch.argmax(
            next_logits,
            dim=-1,
            keepdim=True,
        )
        history = torch.cat((history, next_id), dim=1)

    if was_training:
        model.train()
    return history

Concept sequence
  1. uncropped history [B,L_history]
  2. context = history[:,-block_size:] → [B,min(L_history,2)]
  3. forward(context) → logits [B,T_context,5]
  4. next_logits = logits[:,-1,:] → [B,5]
  5. choose next_id outside forward → [B,1]
  6. append to uncropped history → [B,L_history+1]
E[Luniform]=ln(V)=ln(5)1.609\mathbb{E}[\mathcal{L}_{\mathrm{uniform}}]=\ln(V)=\ln(5)\approx1.609
[B,Lhistory][B,min(Lhistory,2)][B,5][B,1][B,Lhistory+1][B,L_{\mathrm{history}}]\to[B,\min(L_{\mathrm{history}},2)]\to[B,5]\to[B,1]\to[B,L_{\mathrm{history}}+1]

Knowledge check

为什么 generation 只 crop forward input,却把 next_id append 到完整 history?