第三章 Llama 与 DeepSeek:现代解码器的关键改造¶
Llama、Qwen 和 DeepSeek 并没有抛弃 Transformer,而是在归一化、位置编码、注意力、FFN、数据与训练工程上持续改进。本章先拆解一个 Llama 风格的 Decoder Block,再解释 DeepSeek-V3/R1 中与工程最相关的 MoE、MLA、MTP 和强化学习路线。阅读模型报告时,要区分“架构创新”“训练策略”“数据策略”和“系统优化”,不要把所有提升都归因于某一个模块。

图 3-1 典型 Llama Block:Pre-RMSNorm、RoPE 注意力、残差和 SwiGLU FFN。
3.1 RMSNorm:只控制尺度¶
先建立直觉。 RMSNorm 只用均方根控制向量尺度,不减去均值。它保留方向信息、计算更简单,现代解码器常把它放在注意力和 FFN 之前。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 沿最后一维计算平方均值。
- 加 epsilon 后开方。
- 输入除以均方根。
- 乘可学习缩放参数。
最小例子。 向量整体乘 10 后,RMSNorm 输出尺度近似不变;但所有元素同时加常数时,它与 LayerNorm 的响应不同。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 epsilon 放在根号内外会改变数值;归一化维度必须是隐藏维。RMSNorm 不是没有参数,也不等同 L2 normalize。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:实现 RMSNorm,与 torch RMSNorm 或参考实现比较前向和梯度。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
LayerNorm 会减均值再除标准差;RMSNorm 只根据均方根缩放:
$$ \mathrm{RMSNorm}(x)=\gamma\odot\frac{x}{\sqrt{\frac{1}{d}\sum_i x_i^2+\epsilon}}. $$
import torch
from torch import nn
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x):
dtype = x.dtype
x32 = x.float()
scale = torch.rsqrt(x32.pow(2).mean(dim=-1, keepdim=True) + self.eps)
return (x32 * scale).to(dtype) * self.weight
升到 FP32 计算均方根是常见的稳定性处理。RMSNorm 不是“没有参数”,仍有可学习缩放 weight;也不是 BatchNorm,它不依赖批次统计。
3.2 RoPE:让相对位置信息进入点积¶
从问题出发。 RoPE 把偶数维与奇数维配成二维平面,并按位置旋转。旋转后的 Q/K 点积依赖位置角度差,从而让相对位置信息进入注意力。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 为每对维度生成频率。
- 位置乘频率得到角度。
- 分别旋转 Q 与 K。
- 在点积中形成相对相位。
用小数据走一遍。 二维向量在位置 m、n 分别旋转 mθ、nθ,二者点积等价于只旋转 (m-n)θ 后的点积。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 RoPE 通常作用于 Q/K 而非 V;维度配对、position_ids、缓存偏移和长上下文缩放必须一致。只改 max_position_embeddings 不会自然获得外推能力。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:对同一内容向量放在不同位置,验证点积随相对距离而不是绝对起点变化。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Rotary Position Embedding 把隐藏维两两配对,并按位置旋转。对二维向量 \((x_{2i},x_{2i+1})\),位置 \(m\) 的旋转为
$$ \begin{bmatrix}x'{2i}\x' = \begin{bmatrix}\cos(m\theta_i)&-\sin(m\theta_i)\ \sin(m\theta_i)&\cos(m\theta_i) \end{bmatrix} \begin{bmatrix}x_{2i}\x_{2i+1}\end{bmatrix}. $$ 同样旋转 }\end{bmatrix\(Q\) 和 \(K\) 后,二者点积自然包含相对位置差。RoPE 不直接作用于 \(V\)。下面是便于理解的实数实现:
def rotate_half(x):
x1, x2 = x[..., 0::2], x[..., 1::2]
return torch.stack((-x2, x1), dim=-1).flatten(-2)
def rope(q, k, positions, base=10000.0):
# q/k: (B,H,T,D),D 必须为偶数;positions: (T,)
d = q.size(-1)
inv_freq = base ** (-torch.arange(0, d, 2, device=q.device).float() / d)
angles = torch.outer(positions.float(), inv_freq)
cos = torch.repeat_interleave(angles.cos(), 2, dim=-1)[None, None]
sin = torch.repeat_interleave(angles.sin(), 2, dim=-1)[None, None]
return q * cos + rotate_half(q) * sin, k * cos + rotate_half(k) * sin
上下文扩展不是简单把 max_position_embeddings 改大。位置外推可能降低高频分量的有效性,实际方案会采用频率缩放、分段策略或继续训练,并用长文本检索、困惑度和下游任务验证。
3.3 MHA、MQA、GQA 与 KV Cache¶

图 03-2 位置旋转、KV 头共享和历史缓存共同决定现代解码器的生成路径。 先看它解决什么。 MHA 为每个查询头保留独立 K/V 头;MQA 让所有查询头共享一组 K/V;GQA 介于两者之间。减少 KV 头能显著降低生成阶段缓存与带宽。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 按层保存历史 K/V。
- 新 token 只计算自身 Q/K/V。
- 将 KV 头扩展匹配查询头。
- 与历史缓存做注意力。
一个可以手算的例子。 层数 L、batch B、长度 T、KV 头 Hkv、头维 Dh、字节数 s 时,K+V 缓存约为 2LBTHkvDh·s。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 缓存减少不等于计算完全按同倍数减少;repeat_kv 应是视图式扩展或 kernel 支持,物理复制会浪费内存。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为一个假想 32 层模型分别计算 MHA 与 GQA 在 8k 上下文的 KV Cache。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
标准 MHA 中每个查询头都有自己的 K/V 头。Multi-Query Attention 让所有查询头共享一组 K/V;Grouped-Query Attention 把查询头分组,每组共享 K/V,在质量与 KV Cache 大小之间折中。
若层数为 \(L\),批量为 \(B\),序列长度为 \(T\),K/V 头数为 \(H_{kv}\),头维为 \(d_h\),元素字节数为 \(s\),KV Cache 粗略占用为
$$ 2LBTH_{kv}d_hs. $$ 前面的 2 对应 K 和 V。GQA 减少 \(H_{kv}\),因此长上下文和高并发下收益明显。缓存只避免重复计算历史 K/V,并不能让新 token 之间并行产生。
def repeat_kv(x, n_rep: int):
# x: (B, H_kv, T, D) -> (B, H_q, T, D)
if n_rep == 1:
return x
b, h, t, d = x.shape
return x[:, :, None].expand(b, h, n_rep, t, d).reshape(b, h * n_rep, t, d)
教学实现可物理重复 K/V,生产内核通常通过广播或专用 kernel 避免额外拷贝。
3.4 SwiGLU:门控前馈网络¶
抓住这一节的主线。 SwiGLU 用一条分支产生候选值,另一条分支经 SiLU 产生门,再逐元素相乘。门控让网络按输入选择信息通过程度。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- x 分别投影为 gate 与 value。
- gate 经过 SiLU。
- 两支逐元素相乘。
- down projection 回到隐藏维。
先做最小实验。 若 gate 的某维为大负数,SiLU 后接近零,该维 value 即使很大也被抑制。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 SwiGLU 通常需要三组矩阵;把 SiLU 放错分支、忘记逐元素乘或沿错误维度切分,会得到另一种网络。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:实现 SwiGLU,并与普通 GELU FFN 在参数量近似相等时比较输出形状和参数数。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
SwiGLU 常写作
$$ \mathrm{SwiGLU}(x)=W_2\left(\mathrm{SiLU}(W_gx)\odot(W_ux)\right). $$
class SwiGLU(nn.Module):
def __init__(self, dim: int, hidden_dim: int):
super().__init__()
self.gate = nn.Linear(dim, hidden_dim, bias=False)
self.up = nn.Linear(dim, hidden_dim, bias=False)
self.down = nn.Linear(hidden_dim, dim, bias=False)
def forward(self, x):
return self.down(torch.nn.functional.silu(self.gate(x)) * self.up(x))
两条投影分别产生门和值,再逐元素相乘。由于有三组矩阵,比较 FFN 参数量时不能只看中间维度;许多配置会选择约为传统 \(4d\) 的某个折算维度并对齐硬件倍数。
3.5 组装一个 Llama 风格 Block¶
先把概念落到可观察对象上。 组装 Block 时最重要的是接口:输入、mask、position_ids、past_key_values 和返回缓存必须一致。组件单独正确,组合时仍可能因形状和残差次序出错。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- Pre-Norm 后进入注意力。
- 注意力输出与残差相加。
- 再次归一化进入 SwiGLU。
- FFN 输出与第二条残差相加。
把它缩小到能逐项检查。 输入 (B,T,D) 经 GQA 后仍必须是 (B,T,D),否则无法做残差。缓存只含 K/V,不应把 Q 跨步保存。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 用 Identity 冒充注意力只能测试外壳,不能证明模型正确;还要验证因果性、缓存增量结果与全序列结果一致。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:用长度 4 的序列比较一次性前向与逐 token 带缓存前向的最后一步 logits。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
class LlamaStyleBlock(nn.Module):
def __init__(self, dim, hidden_dim, attention: nn.Module):
super().__init__()
self.attn_norm = RMSNorm(dim)
self.ffn_norm = RMSNorm(dim)
# attention 由外部传入,便于分别测试 MHA、GQA 或带 KV Cache 的实现
self.attn = attention
self.ffn = SwiGLU(dim, hidden_dim)
def forward(self, x):
x = x + self.attn(self.attn_norm(x))
x = x + self.ffn(self.ffn_norm(x))
return x
这段代码只负责组织 Block,注意力由外部注入。这样可以分别测试 MHA、GQA 或带 KV Cache 的实现,也避免把过度简化的占位模块伪装成完整 Llama。配套示例 examples/03_llama_components.py 给出可执行的 RMSNorm、RoPE、GQA 形状测试和 SwiGLU。
3.6 Llama 3 应该怎样读¶
先建立直觉。 读模型技术报告要分清架构、数据、训练配方、评估和安全五层证据。公开参数并不等于公开训练数据或完整生产系统。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 先列出模型族与上下文配置。
- 再定位 tokenizer 和架构改动。
- 核对预训练与后训练数据说明。
- 比较同设置评估。
最小例子。 看到某个 benchmark 分数时,同时记录提示模板、shot 数、解码设置、评测脚本版本和是否使用工具。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 把模型卡营销语当因果结论、跨版本混比分数、忽略许可证与安全限制,会造成错误选型。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为 Llama 3 报告做一页证据表:已公开事实、合理推断、未公开信息。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
Llama 3 技术报告同时讨论模型架构、数据、训练基础设施、后训练、安全和多模态实验。其最大公开模型是 405B 的 Dense Transformer,支持长上下文,并发布预训练与后训练版本。阅读时建议按以下问题做笔记:
- tokenizer 与词表怎样影响多语言和代码效率?
- 上下文长度如何训练和评估,而不是只修改配置?
- 预训练数据经历了哪些过滤、去重和质量控制?
- 后训练如何组合 SFT、偏好优化和安全数据?
- 训练稳定性、并行策略和故障恢复如何保证长周期运行?
参数规模只是结果的一部分。一个模型是否适合你的任务,还取决于许可证、语言覆盖、上下文、推理成本、工具调用能力和安全边界。
3.7 DeepSeek-V3:MoE、MLA、MTP 与系统协同¶
从问题出发。 DeepSeek-V3 的关键不只是 MoE,而是模型结构、训练稳定性、通信与推理成本的协同。MLA 压缩需要缓存的表示,MTP 在训练中增加未来 token 预测信号。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 区分总参数与激活参数。
- 理解专家路由与共享专家。
- 理解 MLA 的低秩潜变量。
- 把系统优化与算法指标分开。
用小数据走一遍。 若 KV 不直接逐头缓存,而是缓存可恢复 K/V 的低维潜变量,内存占用可下降,但解码 kernel 必须配合重构。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 不要把 MLA 说成普通 GQA,也不要把 MTP 等同推理时一次永久输出多个 token;训练目标和服务策略是不同层次。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:根据技术报告画出一层的数据流,标注哪些张量跨 token 缓存、哪些只在当前步使用。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
DeepSeek-V3 报告给出的模型总参数为 671B,每个 token 激活约 37B 参数。其 DeepSeekMoE 通过更细粒度专家与共享专家增强专业化;路由需要兼顾专家分工和设备负载。
Multi-head Latent Attention(MLA)的核心目标之一是压缩推理时需要缓存的 K/V 表示。它先把隐藏状态投影到低维潜在表示,再恢复注意力所需分量,从而减少 KV Cache。理解 MLA 时要区分“训练时表达”“推理时可吸收的矩阵变换”和“实际缓存内容”,不能把它简单说成普通低秩压缩。
Multi-Token Prediction(MTP)在训练时增加对多个未来 token 的预测目标,为模型提供更密集的学习信号;推理系统还可利用相关预测进行投机式加速,但训练目标与部署收益不是自动等价的。DualPipe 则是面向大规模训练的流水线并行设计,重点在计算与通信重叠,属于系统层创新。
3.8 DeepSeek-R1:推理能力来自后训练路线¶
先看它解决什么。 DeepSeek-R1 说明推理行为可通过可验证奖励、强化学习与后续数据整理增强。推理能力不是简单由长答案产生,关键是奖励、探索、数据与基础模型能力。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 准备具有可判定结果的任务。
- 采样多条推理轨迹。
- 依据规则或模型计算奖励。
- 更新策略并做蒸馏/对齐。
一个可以手算的例子。 数学题可用最终答案验证器提供相对客观奖励,但证明质量、格式和安全仍需额外检查。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 把所有后训练都称为 GRPO、把思维长度当正确性、公开展示敏感内部推理或忽略 reward hacking,都会带来问题。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:设计一个字符串运算任务的奖励函数,分别列出正确性、格式、长度和作弊路径。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
DeepSeek-R1 与 R1-Zero 基于 DeepSeek-V3-Base。R1-Zero 展示了在没有先做 SFT 的条件下,通过大规模强化学习激励自验证、反思和长推理轨迹的可能,但出现重复、可读性差和语言混杂等问题。R1 加入冷启动数据,并组合多个 SFT/RL 阶段以改善可用性与偏好对齐。其蒸馏模型把大模型产生的推理数据迁移到 Qwen/Llama 系列的较小 Dense 模型。
因此要避免两个误解:GRPO 不是“DPO 的泛化形式”,它是面向组内相对奖励的策略优化方法;R1 的推理表现也不能归结为单一损失,数据、可验证奖励、采样、训练稳定性与基础模型能力共同作用。
3.9 练习与资料¶
抓住这一节的主线。 练习现代解码器要强调等价性测试:组件实现正确、缓存路径与全序列路径一致、混合精度有限、模型配置能完整序列化。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 先测单组件。
- 再测 Block。
- 再测多层与缓存。
- 最后测生成停止条件。
先做最小实验。 对同一随机种子,full forward 第 t 位 logits 应与增量缓存第 t 步 logits 接近;不接近时优先检查 position_ids 与 mask。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 只验证形状不验证数值、只跑 CPU 不测半精度、忽略 batch 中不同长度,都会漏掉真实错误。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:建立 Llama 组件测试矩阵,并把失败样例保存成最小回归用例。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
练习:验证 RoPE 旋转前后向量范数不变;比较 MHA 与 GQA 的 KV Cache 理论大小;实现带缓存的单层 GQA,并验证逐 token 结果与整段前向在数值误差内一致。
本章配套代码¶
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
examples/03_llama_components.py:RMSNorm、RoPE、SwiGLU。examples/03_rope_gqa_cache.py:RoPE、GQA 与 KV Cache。
本章端到端实验:把知识变成可复现证据¶
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤¶
- 实现 RMSNorm,与 torch RMSNorm 或参考实现比较前向和梯度。
- 对同一内容向量放在不同位置,验证点积随相对距离而不是绝对起点变化。
- 为一个假想 32 层模型分别计算 MHA 与 GQA 在 8k 上下文的 KV Cache。
- 实现 SwiGLU,并与普通 GELU FFN 在参数量近似相等时比较输出形状和参数数。
- 用长度 4 的序列比较一次性前向与逐 token 带缓存前向的最后一步 logits。
- 为 Llama 3 报告做一页证据表:已公开事实、合理推断、未公开信息。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准¶
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测¶
- 不看正文,用自己的话解释“RMSNorm 只用均方根控制向量尺度,不减去均值”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“RoPE 把偶数维与奇数维配成二维平面,并按位置旋转”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“MHA 为每个查询头保留独立 K/V 头”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“SwiGLU 用一条分支产生候选值,另一条分支经 SiLU 产生门,再逐元素相乘”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“组装 Block 时最重要的是接口:输入、mask、position_ids、past_key_values 和返回缓存必须一致”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“读模型技术报告要分清架构、数据、训练配方、评估和安全五层证据”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。