大模型知识体系 第 11 章 🎮 交互版(含动画演示与测验)
← 现代 LLM 架构:技术演化视角🏠 回到交互版首页Build Your Own Small LLM(上):组件篇 →
你正在浏览静态阅读版(无 JavaScript 也可阅读,便于搜索与 AI 抓取)。交互演示与答题功能请前往 交互版。
第 11 章 · 第二部分 · 构建 (Build)

PyTorch for LLM:动手之前的必备工具箱

shape 操作、广播、matmul/einsum、nn.Module、autograd、DataLoader、训练循环、checkpoint。

对应课件:工程前置(新增)

本章定位:第二部分「构建 (Build)」的起点。前面 10 章解决了「懂原理」,从本章开始解决「写得出来」。目标:读完本章,你能读懂并自己写出 small LLM 的全部代码。本章不引入新模型概念,只补齐动手所需的 PyTorch 工具箱。

📘 代码类型约定(Build / Systems 章节统一)
  • 【Runnable】可直接运行(关键逻辑已在本课程验证脚本中实测)
  • 【Skeleton】工程骨架:逻辑完整,需要自备数据/环境/权重
  • 【Pseudo-code】算法示意:用于解释思路,不保证可直接运行

11.1 为什么单独练 PyTorch

先懂直觉

LLM 的论文公式最终都要落成 PyTorch 代码。但初学者常见的困境是:公式看懂了,代码读不懂——transpose(-2,-1) 是转什么?keepdim=True 为什么必要?loss.backward() 到底写了什么?本章把这些问题一次性讲清,后面 12、13 章的 Lab 才能顺畅跟做。

结论先行——一个 LLM 工程师 90% 的时间只用这 12 个工具:

工具 一句话用途 本章小节
Tensor 的 shape/dtype/device 一切数据的载体 11.2
reshape / view / transpose / permute 形状变换(attention 的核心操作) 11.3
广播机制 不加循环做批量运算 11.4
matmul / @ / einsum 矩阵乘法与维度缩写 11.5
nn.Module / Parameter 组织模型与参数 11.6
autograd(backward) 自动求梯度 11.7
Dataset / DataLoader 喂数据 11.8
训练循环五件套 训练的标准骨架 11.9
no_grad / eval 推理模式 11.10
device / dtype / autocast GPU 与精度 11.11
state_dict / checkpoint 保存与续训 11.12
shape 断言与最小复现 调试 11.13
📘 「能读懂」的标准

看到任意一行 LLM 训练代码,你能说出:① 每个张量的 shape;② 这一行在公式上对应什么操作;③ 它跑在哪个 device、什么 dtype。做不到就先别急着读模型代码。

11.2 Tensor:shape / dtype / device

先懂直觉

Tensor 就是「带类型的多维数组 + 住在某块设备上」。LLM 里所有东西——输入、参数、激活、梯度——都是 Tensor。读代码第一件事永远是问:这个 tensor 的 shape、dtype、device 是什么?

import torch

x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])   # 从 Python 列表创建
print(x.shape)    # torch.Size([2, 2])
print(x.dtype)    # torch.float32
print(x.device)   # cpu

y = torch.randn(2, 3)                 # 标准正态
z = torch.zeros(2, 3)                 # 全 0
w = torch.arange(0, 10, 2)            # [0, 2, 4, 6, 8]
e = torch.empty(4, 4)                 # 未初始化(省时间,但值随机)

x_gpu = x.to("cuda")                  # 搬到 GPU
x_half = x.to(torch.bfloat16)         # 改精度
x_reshaped = x.view(1, 4)             # 改形状

三种最常打交道的 dtype

dtype 用途 显存/参数
torch.float32 (fp32) 优化器状态、master weights 4 bytes
torch.bfloat16 (bf16) 前向/反向计算(训练首选) 2 bytes
torch.float16 (fp16) 推理/部分训练(需 loss scaling) 2 bytes
torch.int64 (long) token id、label、索引 8 bytes
⚠️ 最常见的报错来源

device 不匹配:Expected all tensors to be on the same device —— 模型在 GPU、输入在 CPU(或反过来)。规则:输入和模型必须在同一 device,通常做法是 batch = {k: v.to(device) for k, v in batch.items()}。

11.3 形状变换四件套(LLM 代码的重灾区)

先懂直觉

LLM 代码里 80% 的烧脑行都在做 shape 变换:把 [B, S, d] 拆成多头、把 [B, H, S, D] 转成 [B, S, H, D] 再拼回来。四个函数搞定:reshape、view、transpose、permute。

对比表(背下来)

操作 作用 是否拷贝 典型 LLM 用途
reshape 按新形状重排(不满足 view 条件时自动拷贝) 视情况(必要时拷贝) 一般性形状重组
view 要求张量的 stride 与新形状兼容(见下方说明);不满足则报错 不拷贝 展平 logits:[B,S,V] → [B*S,V]
transpose(d1,d2) 只交换两个维度 视图(不拷贝,但变不连续) K.transpose(-2,-1)
permute(...) 任意重排所有维度 视图(不拷贝,但变不连续) [B,H,S,D] → [B,S,H,D]

最小数值例子

x = torch.arange(12).reshape(2, 3, 2)     # [2, 3, 2]
print(x.shape)                            # torch.Size([2, 3, 2])

a = x.transpose(1, 2)                     # 交换第 1、2 维 → [2, 2, 3]
b = x.permute(2, 0, 1)                    # 任意重排 → [2, 2, 3]
c = x.reshape(2, 6)                       # → [2, 6]
d = x.view(6, 2)                          # → [6, 2](x 连续,可 view)

print(a.shape, b.shape)                   # 都是 [2, 2, 3],但值不同!
🧮 view 的准确条件:stride 兼容,而非「绝对连续」

transpose/permute 只改「步长」(stride),不搬数据。之后调用 view 何时会失败?当新的形状无法用原 stride 表达时——转置后的张量通常如此:

x = torch.arange(12).reshape(2, 3, 2)
xt = x.transpose(1, 2)          # [2, 2, 3],stride 已改变
xt.view(2, 6)                   # ❌ RuntimeError: view size is not compatible ...
xt.reshape(2, 6)                # ✅ reshape 自动拷贝一份再变形
xt.contiguous().view(2, 6)      # ✅ 显式拷贝,然后 view 合法

严格来说,view 要求的是「stride 与新形状兼容」(例如「是否连续」的特殊情况),而不是简单的一条「必须 contiguous」;但实操中记「转置/换维之后不要直接 view,改用 reshape 或先 contiguous」就够用了。LLM 代码里 reshape 更常用,因为它会自动处理不满足条件的情况。

工程里怎么用(多头注意力的形状往返,第 7 章实现的完整版)
B, S, d_model, H = x.size(0), x.size(1), x.size(2), 8
D = d_model // H

# 1) 投影 → [B, S, d_model],拆成多头 → [B, S, H, D]
q = Wq(x).view(B, S, H, D)
# 2) 把 head 维换到前面 → [B, H, S, D],方便批量算注意力
q = q.transpose(1, 2)          # 或 permute(0, 2, 1, 3)
k = Wk(x).view(B, S, H, D).transpose(1, 2)
v = Wv(x).view(B, S, H, D).transpose(1, 2)

# 3) 注意力 → [B, H, S, S] @ [B, H, S, D] = [B, H, S, D]
attn = (q @ k.transpose(-2, -1)) / (D ** 0.5)
out = attn.softmax(-1) @ v

# 4) 拼回 → [B, S, d_model]
out = out.transpose(1, 2).contiguous().view(B, S, d_model)
out = Wo(out)
x[B, S, d]→view→ [B, S, H, D]→transpose(1,2)→ [B, H, S, D]
Q@Kᵀ[B, H, S, S]→softmax→@V[B, H, S, D]
→transpose(1,2)→ [B, S, H, D]→view→ [B, S, d]

11.4 广播:不加循环做批量运算

先懂直觉

广播让不同形状的 tensor 直接做逐元素运算:小形状会自动「复制扩展」到大形状。理解它,就能看懂 scores + mask、logits - logits.max(keepdim=True) 这类常见写法。

三条规则

  1. 从最后一维开始向左对齐;
  2. 两维相等,或其中一维是 1 → 兼容;
  3. 不兼容 → 报错。
a = torch.randn(3, 4)         # [3, 4]
b = torch.randn(4)            # [4]      → 广播成 [3, 4]
c = a + b                     # ✅

d = torch.randn(3, 1)         # [3, 1]   → 广播成 [3, 4]
e = a + d                     # ✅

f = torch.randn(3)            # [3]      → 与 [3, 4] 不兼容
# a + f  →  RuntimeError: The size of tensor a (4) ...

LLM 三个经典用法

# ① 因果掩码:scores [B, S, S] + mask [S, S]
scores = scores + causal_mask                # 广播自动处理 B 维

# ② padding mask:scores [B, S, S] + mask [B, 1, S]
scores = scores.masked_fill(pad_mask[:, None, :] == 0, float("-inf"))

# ③ softmax 数值稳定:每行减最大值
scores = scores - scores.max(dim=-1, keepdim=True).values
# keepdim=True 保留 [B, S, 1] 的形状,才能正确广播回 [B, S, S]
⚠️ 高频错误

keepdim=True 忘写:scores.max(dim=-1) 得到 [B, S],减去时会从最后一维对齐,变成减错维度(或直接报错)。凡是「按某维做归约、再减回原张量」的场景,几乎都要 keepdim=True。

🧮 为什么广播不会浪费显存

PyTorch 的广播是逻辑扩展(stride 设为 0),不会真的复制数据。[4] → [3,4] 不会分配 3 倍内存。只有结果张量会占用新内存。

11.5 矩阵乘法:matmul / @ / einsum

先懂直觉

LLM 就是一堆矩阵乘法。PyTorch 里表达矩阵乘法有三级工具:@ 最直观、matmul 支持批量、einsum 能把复杂维度变换写成一行「下标方程」,特别适合检查 shape 是否写对。

三种写法对照

A = torch.randn(2, 3, 4)      # [B=2, m=3, k=4]
B_ = torch.randn(2, 4, 5)     # [B=2, k=4, n=5]

C1 = A @ B_                   # [2, 3, 5](批量矩阵乘)
C2 = torch.matmul(A, B_)      # 同上
C3 = torch.einsum("bmk,bkn->bmn", A, B_)   # 同上,下标显式写出

# 只对最后两维做矩阵乘、前面维度广播:
A4 = torch.randn(2, 3, 4, 5)
B4 = torch.randn(4, 5, 6)
C4 = A4 @ B4                  # [2, 3, 4, 6]

用 einsum 重写 Attention(检查 shape 的神器)

# 第 7 章的公式:Attention(Q,K,V) = softmax(QKᵀ/√d)V
# Q,K,V: [B, H, S, D]

scores = torch.einsum("bhsd,bhtd->bhst", Q, K) / (D ** 0.5)   # [B,H,S,S]
attn = scores.softmax(dim=-1)
out = torch.einsum("bhst,bhtd->bhsd", attn, V)                 # [B,H,S,D]
📘 einsum 的读法

bhsd,bhtd->bhst:两个输入的下标里,d 出现在两个输入中但不在输出里 → 沿 d 求和(点积);s 和 t 分别保留 → 输出 [b,h,s,t]。出现即保留,消失即求和,这就是 einsum 的全部规则。

表格:LLM 里常见的矩阵乘

场景 形状变化 写法
线性投影 [B,S,d] @ [d,d'] → [B,S,d'] x @ W 或 nn.Linear
Attention 分数 [B,H,S,D] @ [B,H,D,S] → [B,H,S,S] q @ k.transpose(-2,-1)
加权求和 [B,H,S,S] @ [B,H,S,D] → [B,H,S,D] attn @ v
LM Head [B,S,d] @ [d,V] → [B,S,V] x @ W_lm
LoRA 低秩更新 [d,r] @ [r,k] → [d,k] B @ A
⚠️ 两个易错点
  • * 是逐元素乘,不是矩阵乘:A * B 要求形状可广播;矩阵乘必须 @ / matmul / einsum。
  • batch 维不参与矩阵乘:@ 只对最后两维做矩阵乘、前面的维度按广播对齐。所以 [B,H,S,D] @ [B,H,D,S] 是对的,而 [B,H,S,D] @ [D,S] 也能工作(B、H 维广播)。

11.6 nn.Module 与 Parameter:组织模型

先懂直觉

nn.Module 就是一个「会记账的层」:任何赋值为属性的 nn.Parameter / 子模块都会被自动登记,于是 model.parameters() 能一次拿到全部参数交给优化器。LLM 就是把几十个这样的层拼起来。

手写一个 Linear(理解内部)

import torch, torch.nn as nn
import math

class MyLinear(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()                                # 必须调用
        self.weight = nn.Parameter(torch.empty(out_features, in_features))
        self.bias = nn.Parameter(torch.zeros(out_features))
        nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))

    def forward(self, x):                                 # x: [..., in_features]
        return x @ self.weight.t() + self.bias            # [..., out_features]

lin = MyLinear(768, 256)
print(sum(p.numel() for p in lin.parameters()))           # 768*256 + 256 = 196,864

nn.Module 的三条规则

规则 说明
__init__ 里定义参数和子模块 用 self.x = ... 赋值即可自动注册
计算写在 forward 调用时用 model(x)(自动走 hooks),不要直接 .forward(x)
参数用 nn.Parameter 普通 tensor 不会被登记,也就不会被优化器更新
工程里怎么用(一个最小 Transformer Block 的骨架)
class Block(nn.Module):
    def __init__(self, d, h):
        super().__init__()
        self.norm1 = nn.LayerNorm(d)
        self.attn = nn.MultiheadAttention(d, h, batch_first=True)
        self.norm2 = nn.LayerNorm(d)
        self.ffn = nn.Sequential(
            nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d)
        )

    def forward(self, x):
        h = x + self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0]
        out = h + self.ffn(self.norm2(h))
        return out

model = nn.Sequential(*[Block(256, 8) for _ in range(4)])
print(sum(p.numel() for p in model.parameters()))

11.7 autograd:把第 1 章的手算交给框架

先懂直觉

第 1 章你手算过 ∂L/∂w=−12\partial L/\partial w = -12。PyTorch 的做法是:给参数设 requires_grad=True,前向时框架默默记录计算图,调用 .backward() 时按链式法则自动算所有梯度,结果存在 .grad 里。

import torch

x = torch.tensor(2.0)
y = torch.tensor(5.0)
w = torch.tensor(1.0, requires_grad=True)   # 需要求导

z = w * x            # 前向(自动记录计算图)
L = (z - y) ** 2
L.backward()         # 反向:自动微分

print(w.grad)        # tensor(-12.)   ← 与第 1 章手算完全一致

手动对照:计算图长这样

w ──×── z = wx ──(z−y)²── L
∂L/∂w ← ∂L/∂w = ∂L/∂z · ∂z/∂w
     = 2(z−y) · x = (−6)·2 = −12
🎮 交互:前向/反向/更新三步走(第 1 章实现)交互演示 · 静态阅读版

拖动 x、w、y,点「前向传播 → 反向传播 → 更新参数」,每一步显示具体数字——和 autograd 的结果一一对应。

在交互版中打开这个演示 →

三条必须记住的规则

规则 说明
梯度会累积 backward() 是 +=,所以每步前要 optimizer.zero_grad()
只有叶子节点默认有 .grad w.grad 有值;中间张量要看 retain_grad()
想断开梯度 .detach() 或 with torch.no_grad():(推理、生成标签、RL 采样都要用)
⚠️ 训练里最常见的 3 个 autograd 事故
  1. 忘记 zero_grad() → 梯度越滚越大,loss 爆炸;
  2. 在 backward() 后又对同一图调用一次 → RuntimeError: Trying to backward through the graph a second time(需要 retain_graph=True 或重建图);
  3. 在 no_grad 区域里构建了需要梯度的计算 → 训练不更新(比如把模型输出存成了好几步之前的旧值)。

11.8 Dataset / DataLoader:喂数据

先懂直觉

LLM 的「数据集」其实很简单:一大串 token id。训练时随机切一段长度为 S 的窗口,喂给模型。(更复杂的文档边界、packing 在数据管线章节讲。)

from torch.utils.data import Dataset, DataLoader

class TokenDataset(Dataset):
    """data.bin:把整个语料存成一串 uint16 token id"""
    def __init__(self, path, seq_len):
        import numpy as np
        self.data = np.memmap(path, dtype=np.uint16, mode="r")
        self.seq_len = seq_len

    def __len__(self):
        # 合法起点 i 满足 i+1+seq_len <= N,即 i ∈ [0, N-seq_len-1],共 N-seq_len 个样本
        return len(self.data) - self.seq_len

    def __getitem__(self, i):
        x = torch.from_numpy(self.data[i:i + self.seq_len].astype("int64"))
        y = torch.from_numpy(self.data[i + 1:i + 1 + self.seq_len].astype("int64"))
        return x, y                      # 输入与「右移一位」的标签

# 自测:N=10、S=4 时应恰好 6 个样本,最后一个样本是 x=data[5:9], y=data[6:10]
import numpy as np
arr = np.arange(10, dtype=np.uint16)
arr.tofile("/tmp/tiny.bin")
tiny = TokenDataset("/tmp/tiny.bin", seq_len=4)
assert len(tiny) == 6, len(tiny)
x_last, y_last = tiny[5]
assert x_last.tolist() == [5, 6, 7, 8] and y_last.tolist() == [6, 7, 8, 9]

ds = TokenDataset("data.bin", seq_len=512)
loader = DataLoader(ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True)

x, y = next(iter(loader))
print(x.shape, y.shape)                  # [32, 512] [32, 512]
📘 训练时标签是怎么来的

y 不是另存一份数据,而是 x 右移一位:位置 t 的输入预测位置 t+1 的 token。这就是第 9 章讲的 shift 对齐。

11.9 训练循环:标准骨架

先懂直觉

所有 LLM 训练(从 0.1B 玩具到 GPT-4)都是同一个五步循环,多出来的只是分布式和调度细节。

model.train()
for step, (x, y) in enumerate(loader):
    x, y = x.to(device), y.to(device)

    logits = model(x)                          # ① 前向:  [B,S,V]
    loss = F.cross_entropy(                       # ② 算损失
        logits.view(-1, logits.size(-1)),
        y.view(-1),
    )

    optimizer.zero_grad(set_to_none=True)      # ③ 清梯度
    loss.backward()                            # ④ 反向:算梯度
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)  # 梯度裁剪
    optimizer.step()                           # ⑤ 更新参数

    if step % 50 == 0:
        print(f"step {step}  loss {loss.item():.4f}")

五个零件各自的角色

零件 作用 相关章节
model(x) 前向传播 第 9 章
F.cross_entropy next token 的损失 第 1 章
zero_grad 清空上一步梯度(autograd 是累加的) 第 2 章
backward 自动微分 第 1 章
step 优化器更新(AdamW) 第 2 章
clip_grad_norm_ 防梯度爆炸 第 4 章
工程里怎么用(加学习率调度与梯度累积)

scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)  # warmup+cosine

accum_steps = 8
for step, (x, y) in enumerate(loader):
    logits = model(x)
    loss = F.cross_entropy(logits.view(-1, V), y.view(-1)) / accum_steps
    loss.backward()                              # 梯度在 .grad 里累积

    if (step + 1) % accum_steps == 0:            # 攒够 8 个 micro-batch 才更新
        clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad(set_to_none=True)
梯度累积 = 用小显存模拟大 batch(见第 17 章)。

11.10 no_grad / eval / inference_mode

先懂直觉

训练要梯度(占显存),推理不要。两行代码的区别能让推理显存降一半、速度快一截。

用法 作用 什么时候用
model.train() 开启 dropout 等训练行为 训练前
model.eval() 关闭 dropout、BN 用 running stats 验证/推理前
torch.no_grad() 不建计算图、不存中间激活 验证/推理/生成标签
torch.inference_mode() 更强版本(连版本计数都不记) 纯推理
model.eval()
with torch.no_grad():
    logits = model(x_val)
    val_loss = F.cross_entropy(logits.view(-1, V), y_val.view(-1))
⚠️ eval() 不等于 no_grad()

eval() 只改层的行为(dropout/BN),不省显存;no_grad() 才是不建图。验证循环两个都要。

11.11 device / dtype / autocast:进入 GPU 世界

device = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(device)

# 混合精度:前向/反向用 bf16,主权重仍是 fp32(第 17 章细讲)
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
    logits = model(x)
    loss = F.cross_entropy(logits.view(-1, V), y.view(-1))
loss.backward()

# 查看显存
print(torch.cuda.memory_allocated() / 1e9, "GB")
print(torch.cuda.max_memory_allocated() / 1e9, "GB")
📘 三个 dtype 何时出现
  • 数据/参数:训练默认 fp32(老代码)或 bf16(新代码);
  • 计算:autocast 区域内自动用 bf16/fp16 做矩阵乘;
  • token id / label:必须是整数(int64),永远不要转成浮点。

11.12 state_dict / checkpoint:保存与续训

先懂直觉

state_dict 是一个字典:参数名 → 张量。保存它就是保存模型;加载它就是恢复模型。断点续训还要额外保存优化器状态和步数。

# 只保存模型权重(推理用)
torch.save(model.state_dict(), "model.pt")
model.load_state_dict(torch.load("model.pt", map_location="cpu"))

# 断点续训 checkpoint(训练用)
ckpt = {
    "model": model.state_dict(),
    "optimizer": optimizer.state_dict(),
    "scheduler": scheduler.state_dict(),
    "step": step,
}
torch.save(ckpt, f"ckpt_{step}.pt")

# 恢复
ckpt = torch.load("ckpt_1000.pt", map_location=device)
model.load_state_dict(ckpt["model"])
optimizer.load_state_dict(ckpt["optimizer"])
scheduler.load_state_dict(ckpt["scheduler"])
start_step = ckpt["step"]
⚠️ 常见坑
  • map_location:在 GPU 上存的权重,想在 CPU 上加载时必须写 map_location="cpu";
  • 保存 state_dict() 而不是整个 model 对象(后者依赖类定义,跨项目会加载失败);
  • 恢复后别忘 model.train() 和把 start_step 接上——否则调度器会从头开始。

11.13 调试清单:shape 断言与最小复现

先懂直觉

LLM 代码的 bug 90% 是 shape 错、device 错、dtype 错。学会「三行断言 + 最小复现」,比会写代码更重要。

# ① shape 断言:把期望写出来,让错误尽早暴露
assert x.shape == (B, S), f"expected [B,S], got {x.shape}"
assert q.shape == (B, H, S, D)

# ② device / dtype 断言
assert x.device == model.device or x.device.type == "cuda"
assert y.dtype == torch.long

# ③ 最小复现:造 2 个样本、3 个 token、8 维的小输入
x = torch.randint(0, 100, (2, 3)).to(device)

常见报错对照表(收藏)

报错 原因 修法
Expected all tensors on same device 输入没 .to(device) 统一 device
view size is not compatible with stride 转置后直接 view 先 .contiguous() 或改 reshape
Trying to backward through the graph a second time 重复 backward 重建前向或 retain_graph=True
CUDA out of memory batch 太大 降 batch / 梯度累积 / 看第 17 章
Expected Long but got Float label 是浮点 y.long()
RuntimeError: mat1 and mat2 shapes cannot be multiplied 矩阵乘维度不匹配 打印 shape 逐个核对
🔑 本章必须记住
工具 一句话
shape/dtype/device 读代码第一问
view/reshape/transpose/permute 多头注意力全靠它们
广播 + keepdim=True 归约后减回原张量
einsum 下标规则 出现即保留,消失即求和
nn.Module 参数自动登记、forward 写计算
zero_grad/backward/step 训练五件套中的三件
no_grad + eval 验证/推理的标配
state_dict 保存模型 = 保存参数字典
📝 小测验
一行代码 out = out.transpose(1, 2).contiguous().view(B, S, d) 中,contiguous() 的作用是?
A加快矩阵乘法
B把转置后的数据真正重排为连续内存,使 view 合法
C自动复制 batch 维
D转为半精度
显示答案与解析

正确答案:B

transpose/permute 只改 stride 不搬数据,之后直接 view 会因内存不连续而报错;contiguous() 做一次真实拷贝使内存连续。
📝 小测验
scores.max(dim=-1, keepdim=True) 中 keepdim=True 的作用是?
A节省显存
B保留被归约的维度([B,S,1]),使其能正确广播回 [B,S,S]
C只取每个 batch 的最大值
D加速计算
显示答案与解析

正确答案:B

归约后若不保留维度得到 [B,S],与 [B,S,S] 相减时会从最后一维对齐导致算错或报错;keepdim 保留为 [B,S,1] 才能按行广播。
📝 小测验
关于 optimizer.zero_grad(),说法正确的是?
A可选操作,可省略
B必须每步调用,因为 PyTorch 的梯度是累加的
C只在第一个 step 需要
D它会重置模型参数
显示答案与解析

正确答案:B

backward() 是梯度累加(+=),不清零会导致梯度滚雪球。梯度累积场景下是每 accum_steps 步清一次,但本质仍是要清零。
📝 小测验
为什么验证循环要同时用 model.eval() 和 torch.no_grad()?
A两者功能相同,用哪个都行
Beval() 改变层行为(关 dropout),no_grad() 不建计算图省显存,两者作用不同
Ceval() 更快
Dno_grad() 会关闭 dropout
显示答案与解析

正确答案:B

eval() 只改行为(dropout/BN),不影响显存;no_grad() 不构建计算图、不存中间激活,才真正省显存。验证时两个都要。
← 现代 LLM 架构:技术演化视角🏠 回到交互版首页Build Your Own Small LLM(上):组件篇 →
本章完。原始 Markdown:content/11-pytorch.md