一个 Token 的一生

Life Cycle of a Token
不泛讲 Transformer。拆一个真的 ——
跟着一个 token 走完全程,每走一步就把账算一遍。

走完之后,「为什么这个模型需要一个机房」就不是一句口号,而是一串你自己算出来的数

标的 DeepSeek V3 口径 128K · batch 1 · bf16 · 只算前向 参照 TPU v7 单 device 94.74 GiB 60 分钟

课程作者 Chris Yang·Google Cloud AI Infra 架构师

◉ 全页联动
128K
1
场景

开始之前,先猜一个数

DeepSeek V3。喂给它一篇 128K token 的长文档,做一次 prefill —— 把整篇读进去,还没开始往外吐字。
读完这一遍,有一样东西必须留在显存里: 后面每生成一个字,都要回头用到前面每一个字、在每一层算出来的 K 和 V。 这就是 KV cache
问的就是它:这一次 prefill,留下了多大一份 KV cache?
参照物 —— 一个 TPU v7 device 可用 HBM 约 94.74 GiB

答案是 8.58 GiB —— 一张卡的 10%
另外三个选项每一个也都是真数字,只是换了一种存法。 这四个数其实就是四代注意力:同一个模型、同一篇文档, 只因为「每个 token 每层存几个数」不一样,答案就跨了 128 倍第 2 步整节要讲的,全在这四个数的差距里。

8.58 GiB 是怎么来的 —— 三步,每步只乘一个数:

① 一个 token,一层

1.1 KiB

576 个数 × 2 B
576 = 512(压缩后的 KV)+ 64(RoPE 那一截)

② 一个 token,走完 61 层

68.6 KiB

× 61 每层各存各的,躲不掉

131,072 个 token

8.58 GiB

× 131,072  一张卡的 10%

为什么它非得乘层数 —— 这是 KV cache 跟别的东西最不一样的地方 中间结果算完就能扔:第 3 层的激活进了第 4 层就没用了,寿命只有一层, 所以不跟层数相乘
KV cache 不行。 生成第 n+1 个字的时候,前面 n 个字在每一层的 K 和 V 都还要再用一遍。 于是它寿命是整段对话跟 61 层相乘随长度线性长大且中途不释放
一句话判据:问「下一个 token 还要不要用它」。要 → KV cache;不要 → 算完就扔。

那四个选项,其实是四种存法 —— 同样 128K、61 层、bf16

选项存法每 token 每层 全长相对 MHA
约 500 GBMHA · 128 个头各存一份(最传统)32,768 488.00 GiB
约 30 GBGQA · 128 个头分 8 组,每 16 个共用一份(Llama 那一类)2,048 30.50 GiB16×
约 9 GB ✓MLA · 压成一个 512 维的潜向量(V3)576 8.58 GiB56.9×
约 4 GBMQA · 所有头共用一份(最省)256 3.81 GiB128×

MLA 落在 GQA 和 MQA 中间,但质量比这两个都好 —— MQA 更省,可它是靠让所有头共用一份省出来的,表达力是真丢了、找不回来; MLA 省得几乎一样多,却没让任何一个头做出牺牲这就是第 2 步的主线。 (GQA 那行按 Llama 系列常见的 8 组折算,不是 V3 的配置; 线性注意力和窗口注意力不在这张表里 —— 它们改的不是「每 token 存多少」而是「存不存」。)

56.9× 不是省了点显存,是换了一个量级 488 GiB 意味着光 KV cache 就要 5.5 张卡 —— 模型权重还一个字节都没放,而且这只是一个用户
8.58 GiB 则是一张卡的 10%
一个是「先想办法凑机器」,一个是「顺手就放下了」。

但重点不是这几个数字。重点是 —— 你能不能自己把它们算出来,以及算的过程中会撞见哪些反直觉的东西。 这一课就干这一件事:跟着一个 token 走完全程,每走一步就把账算一遍

为什么拆 DeepSeek V3

早就不是 GPT-3 175B 那个时代了。

它是黄金标准

后来的开源模型基本都从它出发再改。要讲清楚「现在的大模型长什么样」, 拆它一个,比泛泛讲十个有用。

三件事一次做成

MLA 把 KV cache 压下去、细粒度 MoE 把激活参数压下去、 FP8 把训练成本压下去。

每件都对着一个硬件瓶颈

显存、算力、带宽 —— 三个瓶颈,三个答案。 这正是这门课要建立的那条因果链。

⭐ 「它是标准」不是一句评价,是可以查证的事实 2026 年腾讯开源的混元 3(295B/21B),MoE 那部分是逐字段照着 V3 抄的 —— sigmoid 路由、无辅助损失的专家偏置、共享专家、MTP、 连「前 k 层保持 dense」这个字段的名字都一样。 它只把注意力从 MLA 换成了 GQA。

这就是为什么这一课拆 V3 而不是拆别的。 你把它拆明白了,再去看后面这一批开源模型,看到的是同一套零件的不同排列。

671B 是个什么概念

参数量这个数,单独看没有意义。摆到一起才有。

深色是总参数(显存要装的),浅色是激活参数(算力要花的)。 稠密模型两者相等;MoE 模型把它们掰开了 —— 这一掰,就是过去两年大模型能一路做大的根本原因。
⚠️ 最上面那条 Kimi K3 总参数第一(2.8T),但稀疏比不是第一2800 ÷ 104 = 26.9×,反而低于 V4 的 32.7×。 它把「更稀疏」花在了专家粒度上 —— 896 个专家里选 16, 只点亮 1.8%(V3 是 256 选 8,3.1%)。 「总参数」「稀疏比」「专家粒度」是三个数,谁第一要分开说。
看这张图要抓的一件事 V3 的总参数是 GPT-3 的 3.8 倍,但每个 token 实际的计算量只有它的 0.21 倍。 「更大」和「更贵」在 2024 年之后就不是一回事了 —— 这门课后面所有的账,都建立在这个分裂之上。

算账口径

后面每一步都按这一套算,中途不换。顶上那条控制台可以改,全页数字跟着变。

序列长度

128K

131,072 token

batch

1

先把单条算清楚,要乘再乘

精度

bf16

权重与激活都按 2 字节

方向

只算前向

反向与优化器状态留到专题四

为什么先只算前向 前向的账是事实,一步一步能推出来。反向要牵进 recompute 怎么选、优化器状态放哪、 梯度要不要累积 —— 那些是决策不是事实,混进来会把主线搅浑。

模型底牌

一张表,后面所有计算都从这里取数。全部来自官方 config.json,没有一个是估的。

骨架说明
hidden_size7168残差流宽度,记作 d
num_hidden_layers61
first_k_dense_replace3前 3 层 dense,后 58 层 MoE
vocab_size129,280
max_position_embeddings163,840YaRN 从 4K 外推来的
tie_word_embeddingsfalse⚠️ 进出口是两份独立矩阵
num_nextn_predict_layers1MTP
注意力 · MLA说明
num_attention_heads128
q_lora_rank1536Q 也走低秩
kv_lora_rank512⭐ KV 压到 512 维
qk_nope_head_dim128每头 QK 实际 192
qk_rope_head_dim64
v_head_dim128
MoE说明 MoE说明
n_routed_experts256 moe_intermediate_size2048⭐ 细粒度
n_shared_experts1每 token 都过 intermediate_size18,432只有前 3 层 dense 用
num_experts_per_tok8top-8 n_group / topk_group8 / 4分组限制路由
scoring_funcsigmoid不是 softmax topk_methodnoaux_tc无辅助损失均衡

路线

一个 token 从文字进来,从概率分布出去。中间七步。

残差流 · 宽度 d = 7168 · 一路贯穿 61 层 文字 "人工智能" 分词 切成 subword 查出 token id 不在模型里 · 无权重无 FLOPs ① 嵌入 129,280 × 7168 拿 id 查表 × 61 层 ② MLA 128 头 · 低秩 KV KV cache 在这 这一格二选一 ③ MLP 前 3 层 ④ MoE 后 58 层 ⑤ 残差回写 · norm ⑥ lm_head 7168 → 129,280 独立的第二份 概率分布 129,280 维 每一步问同样四个问题: ① 权重多大 ② 激活多大 ③ 多少 FLOPs ④ 有没有什么在这里爆炸 ←往往这个才是重点
七步路线。残差流是贯穿全程的那条总线 —— 每层从它读、往它写。
最左边那两个白盒子不在模型里 —— 分词把文字切成 subword、查出 token id,没有权重、不算 FLOPs,它是喂给 ① 之前的事(第 1 步里会讲这张词表为什么是 129,280)。
③ 和 ④ 共用一格,是二选一不是先后:前 3 层走 dense MLP,后 58 层走 MoE。
① 嵌入
② MLA
③ Dense
④ MoE
⑤ 层间
⑥ 出口
⑦ 合账
走到这里累计 权重 显存 算力
古卷悬于灵池之上,一个字离开纸面燃成一粒金色神识,水面荡开青色涟漪
第 1 步 · 旅程的起点 从文字到向量 一个字离开纸面,变成一串数。这一步什么都没算 —— 只是查了一次表,但显存已经掉了一块。
第 1 步

从文字到向量

这一步什么都没算,只是查了一次表。但显存已经掉了一块。

分词:129,280 是怎么来的

先看它落在哪 —— 把同代几个模型的词表放到同一根轴上,这个数一点都不特别。

词表往大做 —— 赚的(越往右越赚) · 同一句话切成更少的 token → 序列变短,attention 里那个平方项立刻跟着掉 · 罕见词不再被拆成碎片 → 专有名词、代码符号、生僻字能整块进模型 32K 64K 128K 256K 词表大小(对数刻度,每走一格是 2 倍) Llama 2 32,000 Llama 3 128,256 gpt-oss 201,088 Qwen3 151,936 Gemma 2 256,000 DeepSeek V3 129,280 词表往大做 —— 赔的(越往右越赔) · 嵌入表和出口表都跟着变大 → 但对 V3 这个体量几乎可以忽略,见下方展开 · 每个罕见词分到的训练样本变少 → 学不好,极端情况成为「一碰就出乱码」的死词 · 出口那一层要在整张词表上算一遍 softmax → 词表越大这笔越贵 ⭐ 两年多里主流词表涨了 4 到 8 倍 —— 129,280 就落在当下这一代的主流区间里,不是魔法数字
轴上六个数字全部取自各模型公开的 config.jsonvocab_sizeLlama 3 的 128,256 和 V3 的 129,280 几乎是同一个点 —— 图上那两个标记本来就叠在一起。
展开:那笔「两张表变大」的钱,到底有多少—— 为什么 V3 敢做 129K,小模型不敢

词表矩阵的大小只跟「词表 × d」有关,跟层数一点关系都没有。 所以模型越深越大,这笔钱占比越小。

// V3:嵌入表 + 出口表(两张表没有共享权重,tie_word_embeddings = false) 129,280 × 7,168 × 2 = 1.853 B → 占 671 B 的 0.28% // 反例:同一张 129,280 词表,塞进一个 d = 2,048、12 层的小模型 129,280 × 2,048 × 2 = 529 M // 两张表 12 × 12 × 2,048² = 604 M // 模型主体,按每层 12d² 估,只用来说明量级 → 词表占掉快一半

所以这张图上「往大做要赔钱」那一条,在 V3 身上几乎不构成阻力 —— 真正拉住它的是另外两条:罕见词学不好,以及出口 softmax 变贵

研究是怎么说的 有一篇专门做词表缩放律的工作(Scaling Laws with Vocabulary: Larger Models Deserve Larger Vocabularies, NeurIPS 2024, arXiv 2407.13623)给出的结论是 「大多数 LLM 的词表都偏小」:按它拟合的公式,Llama2-70B 的最优词表应该在 21.6 万 以上,而它实际只有 3.2 万;同样算力预算下把词表从 32K 提到 43K, ARC-Challenge 从 29.1 提到 32.0
⚠️ 但这条要留一个边界:那篇文章是在 33 M 到 3 B 的 dense 模型上拟合的, 直接外推到 671 B 的 MoE 上没有依据。它能说明「方向是往大走」, 不能用来论证 129,280 就是 V3 的最优解
至于「罕见词学成死词」,那类现象有个专门的名字叫 under-trained / glitch token(arXiv 2405.05417,就是著名的 SolidGoldMagikarp 那一类)。

为什么宽度是 7,168

记住这个 d = 7,168 —— 它是这个模型的「主干道宽度」,后面每一步都会用到。 一句话解释这个数:7,168 = 128 × 56 = 256 × 28两边都整除,在 TPU 和 GPU 上都不会因为补齐而白算。

展开:哪一半是硬约束,哪一半是没人能替 DeepSeek 回答的—— 这里很容易把两件事讲成一件

约束(硬的):它必须对齐硬件的矩阵单元。 TPU 的 MXU 是一块脉动阵列,v6e 和 TPU7x 上是 256 × 256,更早的世代是 128 × 128; NVIDIA 那边的对齐建议是 FP16 下维度取 8 的倍数,A100 上取 64 的倍数。 7,168 对 128 和 256 都整除,两边都不会有补齐浪费。 这条不管谁来定这个数都躲不掉。

取舍(软的):同样的参数预算,做宽一点还是做深一点。 这个在每个模型上都要重做一次,而 DeepSeek 从没公开说过 7,168 是怎么定的

别把约束讲成理由 「对齐」只解释了为什么不会是 7,000 或 7,200没有解释为什么不是 6,144 或 8,192 —— 那两个也一样整除。 讲台上把这两件事混成一句「所以他们选了 7168」,懂行的人会当场追问。

嵌入:一次查表 —— 至少默认是

拿 token id 去那张 129,280 × 7,168 的大表里 取出对应的那一行 —— 就这样。这条路径在算力账上是 0。 不过「一定是查表」这个直觉在真实框架里并不成立,展开看。

// 嵌入矩阵参数量 129,280 × 7,168 = 926.68 M 1.73 GiB // 查完表,那个张量多大 131,072 × 7,168 × 2 B = 1.75 GiB
展开:「查表不是矩阵乘」这句话要说多准—— 生产配置里它经常真的是矩阵乘

它在数学上等价于「one-hot 向量 × 嵌入矩阵」。直觉上没人会真去做那个乘法 —— 那是拿 129,280 次乘加去换 1 个数。但两条路在真实训练框架里都在跑,而且是一个显式开关。

// MaxText layers/embeddings.py — 两个分支都在源码里 if cfg.use_iota_embed: iota = lax.iota(jnp.int32, self.num_embeddings) one_hot = jnp.array(inputs[..., None] == iota, dtype=self.dtype) output = jnp.dot(one_hot, embedding) // ← 真的做那个矩阵乘 else: output = embedding.at[inputs].get() // ← gather,算力 0

base.yml 里这个开关默认是 false,也就是走 gather。 但仓库里 34 份实跑配置显式把它打开,没有一份显式关掉 —— 而且不只是 TPU, 连 GPU 那批模型配置(llama3、mixtral)也是打开的。默认值和实际用法是反的。

打开之后这一步值多少算力? 跟本页最后那张总表里的 lm_head 一模一样 —— 因为它们本就是同一个形状的矩阵, 一个 129,280 → 7,168,一个 7,168 → 129,280, 文档后面把这叫「镜像」。查表时这个镜像只在参数量上成立, 走 matmul 时它在算力上也成立了:
2 × 129,280 × 7,168 = 1.85 GFLOP / 每个 token // 全序列合计 = 242.92 TFLOP,占整步 52.50 PFLOP0.46%

所以准确的说法是:「算力是 0」只对 gather 那条路成立。 换条编译路径它就不是 0,只是分母够大,那 0.46% 淹没在噪声里。 至于为什么值得多花这笔算力 —— 它把一次不规则的稀疏取行变成了一个规整的、 能沿词表维切分的稠密矩阵乘,反向也随之变成矩阵乘。 但这只是最合理的解释,不是公开资料里写明的理由,别当结论用。

显存两条路一样:那张表本身要常驻,取出来的张量也要占地方 —— 见右边的图。

词表矩阵 129,280 × 7,168 token #8492 那一行 ↕ 129,280 行 ↔ 7,168 列 查表 残差流张量 1.75 GiB 128K token × batch ↔ 7,168 此后每一层都要读写它一次

⚠️ 一个容易想当然的地方:d 跟头数没有整除关系

在标准 MHA 里,hidden = 头数 × 每头维度必须成立的等式。 V3 不成立 —— 而且差得不是一点点。

① 标准 MHA —— 头数 × 每头维度 正好等于 d 头数 × 每头维度 = d —— 拼起来一个字节不多不少,「头」只是把残差流切开(这里画 8 个只是示意,画几个都成立) ② V3 的 MLA —— 128 个头 × 每头输出 128 维 = 16,384 比残差流宽出 1.29 倍 16,384 是注意力内部的宽度 —— 它跟 7,168 之间没有任何整除关系 o_proj 16,384 × 7,168 117.44 M 一层 MLA 权重里最大的一块(63%) ↙ 把多出来的那 1.29 倍收回去 回到残差流 7,168 ↑ d = 7,168 两行共用这条基准线,长度按真实比例画 ⭐ MLA 解除了「注意力内部宽度必须等于残差流宽度」这条老约束 —— 代价就是中间那块 117.44 M 的收口
两行共用同一条 d = 7,168 基准线,长度按真实比例画,没有压缩。 上面一行是老规矩:头拼起来正好填满残差流。下面一行是 V3: 注意力内部先撑到 16,384,再由 o_proj 收回 7,168
展开:这张图顺带解释了一件后面会反复撞到的事—— o_proj 为什么是一层里最大的一块

o_proj 是 MLA 一层里最大的那块权重(117.44 M,占一层的 63%) —— 不是它本身有什么特殊,纯粹是因为它跨着上图那道口子: 输入 16,384、输出 7,168,两头都很宽。

o_proj = 16,384 × 7,168 = 117.44 M 一层 MLA 权重合计 = 187.11 M → 117.44 / 187.11 = 62.76%

更要紧的是这道口子放开之后的后果:头数和 d 从此可以各按各的道理定, 不必再互相迁就 —— 这是后面第 2 步能把 128 个头全部保留下来的前提。

第一个让人坐直的数字 还没进第一层,光是把 token 变成向量,就已经吃掉 1.75 GiB。 而这个张量不是用完就扔 —— 它是残差流,61 层里每一层都要读它一次、写它一次。
① 嵌入
② MLA
③ Dense
④ MoE
⑤ 层间
⑥ 出口
⑦ 合账
走到这里累计 权重 显存 算力
翻涌的青色云海被吸进一枚玉简,一缕金线绕开漩涡径自飞走
第 2 步 · 全课最重的一段 MLA:注意力,以及 KV cache 的战争 把一整片云压成一颗珠子存起来,要用时再放回去 —— 而那缕带着位置信息的金线,必须绕开压缩单独走。
第 2 步 ⭐

MLA:注意力,以及 KV cache 的战争

整课的重头,也是 V3 最值得讲的地方。建议留 15 分钟给这一步。

先看死穴:KV cache 为什么会失控

注意力要让每个 token 看到它前面所有 token。为了不重复计算,前面那些 token 的 K 和 V 要存下来 —— 这就是 KV cache。它的大小是:

KV cache = 序列长度 × 层数 × 每 token 每层存多少个数 × 精度 ↑ 线性增长 ↑ 线性增长

两个线性相乘,就是灾难。 序列从 4K 涨到 128K 是 32 倍, 再乘 61 层 —— 这就是为什么长上下文一出现,注意力就必须改。

但「灾难」两个字太虚。把账一步一步堆出来看。

纵轴是对数刻度,每一格是 10 倍 —— 必须这样, 否则第一根柱子细到看不见。从最左边到最右边,跨了 800 万, 而中间只做了两件极普通的事:乘层数,乘 token 数。 右边那根蓝柱是同一件事换 MLA 来做。

① 一个 token,一层

64.0 KiB

32,768 个数 × 2 B 微不足道

② 一个 token,走完 61 层

3.81 MiB

× 61 还是不痛不痒

131,072 个 token

488.00 GiB

× 131,072 光这一项就占 5.5 张卡

最刺眼的一句话,可以直接背下来 照这个存法,一段 23,699 个 token 的对话, 光缓存就吃满一整张 TPU v7(94.74 GiB) —— 模型权重还一个字节都没放进去, 而且这只是一个用户
换成 MLA,同一张卡能撑到 1,348,190 个 token

喇叭口:为什么长上下文一来就绷不住

把序列长度当横轴,KV cache 当纵轴,两种存法各画一条线。两条都是直线, 但斜率差 56.9× —— 同一个原点出发,越走越张开。

横轴序列长度,纵轴 KV cache,两轴都是线性刻度,没有做任何视觉压缩。 红色区域就是「换一种存法能省下来的」—— 它不是一个固定的数,是一个随长度张开的口子。 拖动顶上的滑块,看这个口子怎么撑开。

三个旋钮,四代方案

所有注意力变体,拧的都是同样几个旋钮。这一代代下来,改的是「每个 token 存多少」。 把上面那个公式里的「每 token 每层存多少个数」再拆开一层,旋钮就露出来了:

每 token 每层 = 2 × KV 头数 × 每头维度 // 2 = K 和 V 各一份,动不了 // V3 如果用最传统的存法:2 × 128 × 128 = 32,768
旋钮拧下去会怎样谁在拧
① 存几份
KV 头数
128 → 8 → 1,直接除下去,三个里最猛的。代价是多个 Q 头被迫共用同一份 K/V, 表达力是真丢了,而且找不回来 MQA、GQA
② 每份多宽
每头维度
几乎没人动,业界基本钉在 128。它决定单个头能装多少信息,砍窄了每个头都变笨; 而且 kernel 只对几个固定宽度优化过,太奇怪的宽度省了显存却跑不快 ——
③ 存几层
层数
让相邻几层共用同一份 KV,或者只让部分层看全长(滑动窗口)。 它改的是「存不存」,不是「存多少」 —— 所以不在四代那张表里 跨层共享
窗口注意力
⭐ 先把结论放在这儿:MLA 一个旋钮都没拧 128 个头还是 128 个头,每头还是 128 维,61 层一层不少。 它换的是「存什么」 —— 不存展开后的 K 和 V, 改存一个 512 维的压缩向量,外加 64 维单独走的位置信息,一共 576 个数。
所以四代对照里,MLA 的 576 卡在 GQA 的 2,048 和 MQA 的 256 中间,质量却比两个都好。 在同一根轴上这不可能 —— 它做得到,正是因为它根本不在那根轴上。
MHA · 每头一份 KV 2017 · Transformer 原版 128 个 Q 头 128 个 Q 头 ↔ 128 份 KV 32,768 数 488.00 GiB / 128K 上下文 质量最好 cache 最大 MQA · 所有头共用一份 2019 · Shazeer 128 个 Q 头 128 个 Q 头 ↔ 1 份 KV 256 数 ÷128 约 3.81 GiB cache 骤降 质量掉得明显 GQA · 分组共享 2023 · Ainslie 等 128 个 Q 头 128 个 Q 头 ↔ 8 份 KV(每 16 个共用) 2,048 数 ÷16 30.50 GiB 折中,成了业界默认 MLA · 压成一个隐向量 ⭐ 2024 · DeepSeek 128 个 Q 头 压缩向量 128 个 Q 头 ↔ 1 个压缩向量 576 数 ÷57 8.58 GiB = 512 压缩 + 64 位置 cache 小 57 倍,质量还更好 越往右,每个 token 要存的东西越少 → 四栏一律按 128 个头 × 每头 128 维 × K 和 V 两份 = 32,768 起算,跟本节开头那张表同一套口径 前三代都在「共享」上做文章,MLA 换了个思路:压缩
四代注意力。前三代的思路都是「让多个头共用同一份 KV」—— 省了显存但丢了表达力。MLA 换了个思路:不共享,改压缩
⭐ 这一段真正的落点:换掉的不是数字,是「拿什么去换」 前三代拿质量换显存 —— 共用一份 KV,丢掉的表达力找不回来。
MLA 拿算力换显存 —— 存的时候压成 576,用的时候现场展开成 128 个头。 信息没丢,只是换了个存法,代价是每次用都要多算一遍。
这个形状后面还会再见一次:训练里的「重计算」也是不存中间结果、要用时重算一遍。 某种意义上,MLA 就是对 KV cache 做重计算。
⚠️ 但「MLA 省显存」这句话,只在推理侧成立 因为训练里根本没有 KV cache。缓存是解码才有的东西 —— 一次只吐一个字,前面每个 token 的 K、V 得留着给下一步复用,所以要「缓存」。 训练是整条序列一次前向,没有「下一步」要复用,无从缓存
训练侧对应的是另一笔账:激活显存。而到了这个量级, 重计算是开到顶的 —— 一层里只留层入口那条残差流, 展开成 128 个头的 K/V 是重算出来、用完就扔的临时量,根本不驻留。 于是 MHA 和 MLA 存下来的是同一条残差流:这笔账上两者打平,不是 MLA 赢。
所以别让人带走「MLA 让训练也省显存」这个印象; 但也别记成「MLA 对训练没用」—— 它在训练侧真正吃得到的是另外两处: 重计算松一档时,可以只存那个 512 维隐向量而不是展开后的 32,768 个数; 以及上下文并行要把 K/V 传一圈时,传的是隐向量 —— 后者是通信的账, 不是显存的账。都留到专题四。

MLA 到底怎么做的

存的时候压,用的时候升。 一个 token 的 KV 信息,不按 128 个头分别存, 而是先压成一个 512 维的隐向量存起来;真要算注意力了, 再用一个矩阵把它升回 128 个头。

先说清楚 RoPE 是什么 注意力本身看不见顺序 —— 把句子里的词打乱,算出来是一样的。 所以必须额外告诉它「谁在前谁在后」。RoPE 的做法很巧: 不给向量加东西,而是按位置把它「转一个角度」 —— 第 1 个 token 转 1 度,第 100 个转 100 度。 两个向量做点积时,结果自然就带上了它们的角度差,也就是相对距离

位置信息为什么必须单独走一路。麻烦就出在这个「转角度」上 ——  转多少度,取决于这个 token 排在第几位。 先转角度、再压进隐向量,隐向量里就掺进了位置; 而这个旋转夹在两个矩阵中间挪不走 —— 矩阵乘法不满足交换律。

这里有一句要说准:坏掉的不是正确性 —— 那样做模型照样能训、照样能跑。 坏掉的是推理时一个很值钱的省法,代价是每生成一个字,都得把前面所有 token 的 K 重算一遍是哪个省法,留到下面的 shape 变换链那一节讲透。

所以 V3 把每个头拆成两半:128 维不带位置(可以压), 64 维带位置(不压,直接存)。

576 = 512 + 64 这就是 MLA 每 token 每层存的全部东西。跟头数无关 —— 128 个头也好,256 个头也好,存的都是这 576 个数。
输入 h · 7168 维 kv_a_proj 单独一路 c_KV · 512 维 · 存进 cache k_rope · 64 ↓ 以上 576 个数就是全部要存的 ↓ 用的时候现场升回来(不进 cache) kv_b_proj 128 头 × (K 128 + V 128) 128 头共用这 64 维位置 每头 QK 实际维度 = 128 不带位置 + 64 带位置 = 192 存 576 个数 ←→ 算的时候展开成 128 头 · 用算力换显存

⭐ 一个容易想当然的地方:7,168 和 16,384

几乎所有人第一次看这份配置都会做一个心算:7,168 ÷ 128 个头 = 56。 这个除法背后其实藏着两个误解,而且第二个比第一个更根本

误解一:以为 V3 每个头是 56 维 不是。V3 每个头是 128 维,128 个头拼起来是 16,384是残差流宽度的 2.29 倍 —— 这两个数在 V3 上根本不相等。
误解二:以为「切成 128 份,每份 56」—— 这个更要命 输入从来没有被切开。 128 个头各自都读入完整的 7,168 维, 每个头有自己的一个 7,168 × 每头维度 的投影矩阵, 把这完整的 7,168 压到自己的工作空间里。
「切」发生在输出侧,不在输入侧 —— 是 128 个头各自的输出拼在一起, 拼出来的总宽才是那个 7,168 或 16,384。
所以哪怕在最标准的模型里,那个除法也只是在说 「拼起来正好等于输入宽度」,不是在说「输入被分掉了」。

这一点是整节的地基 —— 看懂了它,「为什么可以撑宽」根本不需要解释: 既然每个头的矩阵是 7,168 × d,那个 d 从来就是自由的。

注意左边那把扇子 —— 每个头都从完整的 7,168 里取信息, 没有谁只拿到一段。上下两条的差别不在输入,在每个头输出多宽: 老取值让 128 个头拼起来正好 7,168,V3 让它们拼出 16,384, 最后靠 o_proj 收回残差流。右边是各自要为此付的 KV cache。

那条约束从哪来的

它来自 2017 年 Transformer 原论文的一个选择,不是一条定义。 原文第 3.2.2 节把输出投影写成 W^O ∈ R^(h·d_v × d_model) ——  形状上这两个数从一开始就不必相等。

紧接着那句是:「In this work we employ h = 8 … we use d_k = d_v = d_model/h = 64」。「In this work」四个字说明它是个取值,不是个规律。 而给出的理由也写得很直白:

原文给的理由(Vaswani et al., 2017 · arXiv 1706.03762)Due to the reduced dimension of each head, the total computational cost is similar to that of single-head attention with full dimensionality.
—— 理由是算力预算,不是数学必要。

八年下来,这个取值被抄成了直觉。但它从来只是个惯例。

V3 为什么敢把它撑开

因为在标准注意力里,把头做宽是要付 cache 的: 每个头的 K 和 V 都要各存一份,2 × 头数 × 每头维度 个数 —— 头一宽,每份就变大,cache 跟着线性涨。这两件事是锁在一起的。

MLA 把这把锁剪断了。MLA 论文的 Table 1 直接把四代的 cache 写成公式, MLA 那一行是 (d_c + d_h^R) × 层数 ——  这个式子里没有头数,也没有每头维度。

于是出现了一个新的自由度 注意力想做多宽就做多宽,KV cache 一个字节都不涨。 表达力和缓存成本,第一次不再是同一个旋钮。

佐证:MLA 首次出现的 DeepSeek-V2, 残差流只有 5,120,注意力内部同样是 128 × 128 = 16,384 —— 3.2 倍,比 V3 还激进。(DeepSeek 没有明说这是刻意的因果, 但两代都这么配,很难当成巧合。)

代价:撑宽要付三笔钱,V3 只付了两笔

只讲好处不叫讲清楚。把头撑宽,账面上要多付三笔 ——  有意思的是第三笔被另一个设计抵消掉了

以「等宽做法」为 1.00 倍,看撑宽之后各付了多少 ① 注意力算力 2.86× QK 走 24,576、AV 走 16,384 —— 这笔跑不掉 ② o_proj 权重 2.29× 51.38 M → 117.44 M —— 这笔也跑不掉 ③ Q 投影 · 一步投到位 3.43× 51.38 M → 176.16 M 撑宽 2.29× × RoPE 分离 1.5× ③ Q 投影 · 实际 0.95× 48.76 M 走 q_lora:7,168 → 1,536 → 24,576 比等宽还小 5.1% ↑ 等宽做法 = 1.00× ⭐ 三笔里前两笔真付了,第三笔被 q_lora 那个 1,536 的窄腰整个吃掉 —— 宽度撑了 2.29 倍,Q 的参数量反而降了 这就是 q_lora_rank 这个字段存在的理由之一:它省的不是 KV cache(Q 根本不进 cache),是权重
三根条对着同一条 1.00 倍基准线,长度按倍数线性画。 黄色那根是虚线 —— 它是「还是 V3 这个形状,但不掐 q_lora 那个窄腰」不是「换成别的注意力」。真正落地的是下面那根绿的。
黄条中间那道竖线要看清:3.43 倍是两个独立决策乘出来的 —— 左边 2.29 倍是撑宽的账,右边那 1.5 倍(192 ÷ 128) 是 MLA 把 RoPE 单独拎出 64 维造成的。
⚠️ 别把两个「如果」搞混:右边那一截只有 MLA 才有。 如果 V3 压根不用 MLA、就用一个普通 MHA,RoPE 直接转在完整的 128 维上, 根本不会出现 192 这个数 —— Q 投影就是 7,168 × (128×128) = 117.44 M,跟 o_proj 一样是 2.29 倍, 黄条走到竖线那儿就该结束了。
展开:这三笔账的完整算式—— 图上四个倍数怎么来的
代价等宽取值撑宽之后 结果
① 注意力算力
QK 和 AV 两段宽度不同,要分开算
QK 56 + AV 56
= 112
QK 192 + AV 128
= 320
2.86×
真的多付了
o_proj 权重
要跨 16,384 → 7,168 这道口子
7,168 × 7,168
51.38 M
16,384 × 7,168
117.44 M
2.29×
占一层的 63%
③ Q 投影权重
128 个头各要一个 7,168 × d 的矩阵
7,168 × (128×56)
51.38 M
如果只是撑宽(普通 MHA)
7,168 × (128×128)
117.44 M —— RoPE 就转在这 128 维里, 没有 192 这个数
2.29×
纯撑宽的账
③ 续:但 V3 用的是 MLA —— RoPE 不转在那 128 维里,另开 64 维走 V3 的真实形状,一步投到位
7,168 × (128×192)
176.16 M
再 ×1.5
合计本该 3.43×
③ 的实际做法:走 q_lora 绕一下
7,168 → 1,536 → 128×192,中间掐一个窄腰
11.01 M + 37.75 M
= 48.76 M
比一步投到位省 3.61×
比等宽还小 5.1%

验算:7168×7168 = 51,380,224(注意 128×56 = 7,168, 所以等宽时 Q 投影和 o_proj 一样大);7168×16384 = 117,440,512117.44 ÷ 51.38 = 2.297168×24576 = 176,160,768176.16 ÷ 51.38 = 3.4311.01 + 37.75 = 48.76176.16 ÷ 48.76 = 3.6148.76 ÷ 51.38 = 0.949
③ 的两截怎么乘起来:2.29 × 1.5 = 3.43,其中 1.5 = 192 ÷ 128 —— 这一截只有 MLA 才有。
① 为什么是 2.86 而不是 2.29:注意力的两段矩阵乘宽度不同, QKTqk_head_dim = 192AVv_head_dim = 128,所以 (192+128) ÷ (56+56) = 320 ÷ 112 = 2.857本页所有 FLOP 数字都是按这个分段口径算的, 拿单一的 16,384 去估会低报注意力算力。

于是 q_lora_rank 这个字段的意思就清楚了 V3 把注意力撑宽了 2.29 倍,而 Q 投影的参数量反而比等宽做法还少一点。 撑宽的账,被那个 1,536 的窄腰吃掉了。
这也点明了两个 LoRA 的分工完全不同:
kv_lora_rank = 512 省的是 KV cache(因为存的就是它);
q_lora_rank = 1536 省的不是 cache —— Q 根本不进 cache
下面那张五行表里 q_a_proj 那行写着「省参数,不省 cache」,说的就是这件事。
⚠️ 但这里要分清出处。上面那笔「省权重」的账是这张表自己算出来的,论文没这么讲论文给的理由是另一个:「为了降低训练时的激活显存」(V2 论文 2.1.2 节末尾), 而且特意补了一句 —— 即便它并不能减少 KV cache两个说法都成立,但别把我们的推导安到论文头上。
但第 ① 笔是真的,跑不掉 注意力的计算量确实跟着涨了 —— 原论文当年设 d_model/h, 图的就是省这笔算力。V3 是明确地拿算力换了表达力,不是免费午餐。
② 也是真的:o_proj 117.44 M,一层 MLA 权重里最大的一块下面那张五行表里它为什么最大,答案就在这儿。

⭐ 完整的 shape 变换链:从 [128K, 7168] 走到 Q / K / V

上面讲清了「为什么可以撑宽」。下面是这一层实际发生的每一步 ——  进去和出来都是 [128K, 7168], 中间拆成 Q、K、V 三条路,而这三条走得完全不同。

每个方块 = 一次变换,方块里三行分别是做什么 / 权重形状 / 出来是什么 shape。 金色虚线框住的两块就是推理时唯一被缓存的东西512 + 64 = 576 个数 / token / 层。 注意 k_pe 只有 1 个头 —— 128 个头共用同一份,所以它只贡献 64 而不是 128×64。
两点画法说明:① kv_a_proj 的输出实际顺序是 [c_KV 512 | k_pe 64],图里把 k_pe 画在左边, 只是为了让它和 k_nope 相邻、拼接那一步不用交叉连线; ② 权重按 [输入维, 输出维] 写,跟代码里 nn.Linear(in, out) 一致。

① Q 路:降下去,再升回来

Q 走的是「两级投影 + 中间掐一个窄腰」。 7,168 先被 q_a_proj 压到 1,536 —— 这就是 Q 的潜空间; 过一层 RMSNorm 之后,q_b_proj 把它升到 24,576view 一下就是 128 个头 × 192

一个容易记错的数:这里是 24,576,不是 12,288 每个头是 192 维,不是 96 也不是 128 —— 192 = 128(不转的)+ 64(要转的)
所以 q_b_proj 的形状是 [1536, 128×192] = [1536, 24576]那个 128×128 = 16,384输出侧的宽度 (v_head_dim × 头数),它出现在 o_proj 的入口,不在这里。

接着 每个头的 192 维被切成两段:前 128 维(q_nope)原样不动, 后 64 维(q_pe)拿去转 RoPE。转完再拼回 192 维, query_states 定型:[128K, 128, 192]

Q 一个字节都不进 cache —— 它压缩纯粹是为了省权重和训练时的激活值,这一点上面那张代价表已经算过。

② KV 路:只存 576,其余全是现算的

K 和 V 共用一次下投影。 kv_a_proj_with_mqa 一把把 7,168 压成 576,然后当场切开: 前 512 是 c_KV(K 和 V 共享的压缩表示), 后 64 是 k_pe(专门用来扛位置信息的那一小条)。

⭐ 全篇最关键的一句 存进 KV cache 的,就只有这两块 —— 512 + 64 = 576。 再往下的 k_nopevaluekey_states 全部是用的时候现算出来的,一个都不存。
MLA 论文把这件事写成了一个式子:cache 的元素个数是 (d_c + d_h^R) × 层数 —— 里面没有头数,也没有每头维度。

c_KV 过 RMSNorm 后由 kv_b_proj 升到 32,768view128 个头 × 256,再切成两半: 前 128 是 k_nope,后 128 直接就是 V —— value_states 定型:[128K, 128, 128], 它全程不碰 RoPE

k_pe 转完 RoPE 之后,同一份广播给全部 128 个头, 跟每个头自己的 k_nope 拼在一起: key_states 定型:[128K, 128, 192], 正好和 Q 对齐。

③ RoPE 为什么非得单独切出一条 64 维的支路

这是整个 MLA 里最绕、也最容易被跳过的一处。 直觉上位置编码应该均匀地作用在整个向量上,为什么要单开一小条? 因为如果不这么做,前面省下来的 cache 会当场还回去。

如果整个 K 都带 RoPE,会发生什么 推理时本来有一个漂亮的省法:把「K 的升维矩阵」提前乘进「Q 的矩阵」里, 这样连 K 都不用算出来,直接拿 Q 去和缓存里那个 512 维的 c_KV 打分就行。
但 RoPE 是跟位置有关的旋转 —— 它会夹在这两个矩阵中间。 矩阵乘法不满足交换律,夹在中间的东西挪不走,这个合并就做不成了。
后果很直接:每生成一个字,都得把前面所有 token 的 K 重新算一遍。
展开:论文原话—— DeepSeek-V2 · 2.1.3 节
RoPE is incompatible with low-rank KV compression … W^UK cannot be absorbed into W^Q any more during inference, since a RoPE matrix related to the currently generating token will lie between W^Q and W^UK and matrix multiplication does not obey a commutative law. As a result, we must recompute the keys for all the prefix tokens during inference.

注意最后那句 we must recompute the keys —— 论文说的是「必须重算」,不是「会算错」。 坏掉的是效率,不是正确性。 这个区别在讲台上很值钱: 说成「算出来会错」,懂行的人会当场纠正你。

于是他们把位置信息「隔离」到一条小路上

做法是把每个头的向量劈成两半,各管一件事

那一段宽度转 RoPE 吗负责
q_nope / k_nope128 ❌ 不转内容 —— 保住「矩阵能合并」这个性质
q_pe / k_pe64 ✅ 转位置 —— 位置信息全塞在这 64 维里
拼起来打分192 分数 = 内容项 + 位置项,一次点积同时算完

点积天然可加[a;b]·[c;d] = a·c + b·d。 所以拼起来做一次 192 维点积,等价于 「内容相似度」和「位置相关性」两项分别算完再相加 —— 互不干扰,各自那一半的数学性质都保住了。

代价小到几乎白送 位置那条只有 64 维、而且全部 128 个头共用同一份 (所以图里它的 shape 是 [128K, 1, 64])。
cache 上多付 64 个数,换回「K 不用重算」 —— 这是 576 里的那个 64 的全部来历。
顺带一提,如果它不共享、每个头一份,就要多存 128 × 64 = 8,192, cache 直接从 576 涨到 8,704,MLA 就白做了。
展开:那个自然的追问 —— Q 那 64 维为什么不也共用一份—— 不是数学要求的,是成本决定的
位置是 token 的属性,跟哪个头无关 —— 旋转角度确实每个头都一样。 那为什么 K 那边共用一份,Q 那边却是 128 份(128 × 64 = 8,192)?
因为那 64 维不是纯位置。 论文式 (14) 写的是 RoPE(W^QR · c_Q) —— 先从内容投出一个向量,再去转它。 128 个头取的是这个投影矩阵的不同行,所以角度一样,被转的东西不一样
把位置那一项的点积展开就清楚了(旋转矩阵正交,R_t 转置 × R_s = R_(s−t)):
// 第 i 个头,query 在位置 t,key 在位置 s (R_t · a_i) · (R_s · b) = a_i 转置 × R_(s−t) × b // 中间只跟相对距离有关
中间那块只跟相对距离有关,两边是内容。a_i 每个头不一样, 于是每个头拿到一条不同的「距离 → 分数」曲线 —— 有的头可以只盯紧邻几个字,有的头可以到远处找呼应。 共用一份,128 个头的位置项就完全相同了。
⭐ 但真正拍板的不是表达力,是账: Q 一个字节都不进 cache,共用省不到任何东西 —— 白丢表达力,没理由做; K 要进 cache,共用能把 8,192 压成 64 —— 那笔损失才值得认。
所以这个不对称不是数学要求的,是成本决定的:哪一侧花钱,哪一侧才做妥协。
一句诚实的补充:上面那个「合并矩阵」是推理优化,不是模型定义 公开的参考实现(HuggingFace 那份 modeling_deepseek.py是老老实实把 K 和 V 都算出来再做注意力的 —— 图里画的就是这条路径,因为它才是模型在数学上的定义。
「把升维矩阵吸收进 Q / 吸收进 o_proj」是高性能推理引擎才会做的一步等价变形。 但正因为要给这一步留出可能性,RoPE 才必须被隔离出去 —— 架构在设计阶段就为它让了路。
论文自己也是这么说的(V2 附录 C 末尾):「due to the associative law of matrix multiplication, we can absorb W^UK into W^UQ, and W^UV into W^O. Therefore, we do not need to compute keys and values out for each query.
展开:这「128 个头」是真的存在,还是只是个记账方式—— 答案是「头有生命周期」

看完上面这条链子,很容易生出一个疑问:从头到尾权重都是几个大矩阵, view 一个数都没动过 —— 那 128 个头是不是只是个记账方式?

在「投影」这一段:是的,纯粹是记账 q_b_proj一整块 1,536 × 24,576, kv_b_proj一整块 512 × 32,768, o_proj一整块 16,384 × 7,168。
没有 128 个小矩阵,就是三个大的。 view / reshape 只是换了个读法,零数据移动、零计算
所谓「第 i 个头的投影矩阵」,就是那块大矩阵里的第 i 段列。
在「打分」这一段:头是物理的,而且合并不了 假如真的不分头,拿完整的 24,576 维 Q 和 K 做一次点积 —— 每一对 token 得到一个分数。
分成 128 个头,每一对 token 得到 128 个分数, 然后各自 softmax、各自加权自己那份 V
这两件事结果完全不同,而且后者写不成一次大矩阵乘 —— 因为 softmax 是非线性的,它在每个头内部单独做。 线性的东西怎么摆都能合并,非线性一旦按组切开,就再也拼不回去。

所以准确的说法是:「头」有生命周期。 它在 view 那一刻只是拿到一个名字; 在打分和 softmax 那一段真正活着; 在 o_proj 那一刻死掉 —— 那块 16,384 × 7,168 的大矩阵 是 128 个头唯一一次互相交换信息的地方前后两头都是一整块,只有中间那一小段是真的。

硬件上也看得出来:投影那几步是又大又规整的矩阵乘,加速器最喜欢;打分那一步是 batch × 128 个小矩阵乘批在一起,算术强度天然低一截 —— 这也是注意力长期是性能瓶颈的原因之一。

⭐ 凭什么能压这么狠:那个 32,768 从来就不存在

56.9 倍听起来像魔术,而魔术总有障眼法。这一节把它拆开 ——  拆完你会发现,其中一大半根本不是压缩,是「别犯傻」。

条长按元素个数线性画。 最下面那根只有 12 像素 —— 不是画错了,就是这个比例。
⚠️ 这个「两刀」拆法是本课的推导,论文没有这么写。 但两个因子都能自己验算:32,768 ÷ 7,168 = 4.577,168 ÷ 576 = 12.444.57 × 12.44 = 56.9

第一刀:那 32,768 个数里,只有 7,168 个自由度

标准 MHA 每 token 每层要存 2 × 128 头 × 128 维 = 32,768 个数。 但这 32,768 个数是从哪儿来的?

全都是那 7,168 维的输入乘出来的。 K 是一个 7,168 × 16,384 的矩阵作用在 h 上,V 也一样。 而一个 7,168 × 16,384 的矩阵,秩最多只能是 7,168。

这句话是可以证明的,不是感觉 那个 32,768 维的向量,永远只在一个不超过 7,168 维的子空间里打转
所以你只要把那 7,168 个数(也就是输入本身)存下来, 就能把 K 和 V 一个 bit 不差地还原出来。
也就是说:标准存法里,有 4.57 倍是纯冗余 —— 砍掉它不叫压缩,叫别犯傻。
那为什么以前没人只存输入? 因为要重算。 只存 h 的话, 每生成一个字,都得把前面所有 token 的 K 和 V 重新乘一遍 —— 省了显存,赔了算力。
MLA 真正的贡献,是让这个「重算」几乎不用真的发生(靠矩阵吸收), 于是这 4.57 倍才第一次变成可以拿走的。

第二刀:把「秩」限制到 576 —— 这才是真压缩

剩下的 7,168 → 57612.44 倍,这一刀是真的、是有损的。 但它压的不是「数」,是「秩」

c_KV 想成一条信息总线。 128 个头都挂在这条 512 宽的总线上取数据 —— 每个头用自己的矩阵去取,取出来的东西各不相同(头之间的差异性一点没丢), 但总线上没有的东西,谁也拿不到
注意那条金色虚线:k_pe 那 64 维是从 h 直接旁路出去的, 不经过总线 —— 论文式 (15) 写的就是 k^R = RoPE(W^KR · h)位置信息不能被压进这条共享总线,原因就是前面那一节讲的矩阵吸收。

所以 MLA 押的赌注可以写成一句话: 128 个头真正需要的信息通道,加起来不超过 512 种。

⭐ 论文凭什么这么说?—— 它没说,它只给了实验,而实验赢得很干净 同规模、同架构,只换注意力机制(V2 论文附录 D,250 B 的大号 MoE):
KV cache 从 860.2 K 掉到 34.6 K —— 只有 MHA 的 4%; BBH、MMLU、C-Eval、CMMLU 四项全赢
而它越过的 MHA,正是「不省 cache、质量最好」的那个基线。
展开:那两张消融表的完整数据,以及「压了 57 倍质量反而更好」怎么解释—— 有一格是输的,要老实讲
先把话说清楚:这一点上论文是「先做后说」的 DeepSeek-V2 论文 2.1.2 节从头到尾没有给任何理论证明 —— 没有秩分析,没有信息论论证,就是「我们这么做」。 上面那套「两刀 / 总线」的解释是本课的推导,不要当成论文的说法。
论文给的是硬碰硬的消融实验,在附录 D。那份数据才是真正的答案。
Benchmark 小号 MoE
MHA
小号 MoE
MLA
大号 MoE
MHA
大号 MoE
MLA
总参数15.8 B15.7 B 250.8 B247.4 B
KV cache / token
元素个数
110.6 K15.6 K 860.2 K34.6 K
= MHA 的 4%
BBH37.939.0 46.650.7
MMLU48.750.0 57.559.0
C-Eval51.650.9 57.959.2
CMMLU52.353.4 60.762.5

数据出处:DeepSeek-V2 论文 Table 9。 同规模、同架构,只换注意力机制。小号在 1.33T token 上训,大号在 420B token 上训。 大号四项全赢,小号三胜一负(C-Eval 51.6 → 50.9,这一格要老实讲出来)。

而且它打败的是最强的那个基线,不是软柿子 同一篇论文的 Table 8 先做了另一组对照:7B 稠密模型,MHA vs GQA(8 组) vs MQA
MMLU:MHA 45.2 > GQA 41.2 > MQA 37.9; C-Eval:MHA 42.9 > GQA 37.7 > MQA 30.0。
结论:省 cache 的传统办法(GQA / MQA)是真的会掉质量的。 MLA 越过的,是那个「不省 cache、质量最好」的 MHA。

最难受的一问:压了 57 倍,质量为什么反而更好?

论文没有解释。以下两条是推测,请按推测听 推测一 · 低秩瓶颈本身是一种正则化。 强迫所有头把信息挤过一个窄口,可能反而逼出更干净、更共享的表示。 这个现象在别处也见过,但这里没有对照实验支持。
推测二 · 省下的预算被花在了别处,所以对比并不是「压缩 vs 不压缩」。 MLA 省出来的 cache,让他们敢把注意力内部撑到 128 × 128 = 16,384 —— 残差流的 2.29 倍所以真实的对比更像是 「窄瓶颈 + 超宽头」对「无瓶颈 + 常规头」。
不要替 DeepSeek 声称他们没说过的因果。 能确定的只有一件事:在他们自己的对照实验里,这笔交易是赚的。
最后把 512 这个数字的三重身份收一下 它同时决定了三件事,而且是同一个旋钮
① cache 多大 —— 存的就是它;
② 模型能表达多少 —— 从 h 到 K/V 的秩上限就是它;
③ 「不用真算 K」那个优化划不划算 —— 总线越窄,吸收出来的合并矩阵就越小
而 RoPE 那 64 维之所以必须待在总线外面,正是为了保住第 ③ 条。

⭐ Q、K、V 都到手了:这一仗到底怎么打

前面几节讲的全是怎么把三样东西准备出来。 这一节讲它们凑齐之后发生了什么 —— 注意力真正干活的那几步。

上半是五步流水线,下半是那张分数矩阵的缩略图。 对角线以上被遮掉,剩下的每一行 softmax 之后加起来正好是 1 —— 一行就是一个概率分布
⚠️ 方阵里的深浅和右边那几个百分比是示意值, 用来说明「一行加起来是 1」这件事,不是真实模型的输出。

先讲个直觉。你读到一句话的第五个字,得回头看看前面四个字, 决定哪几个字对理解现在这个字最重要。打分就是在算这个 —— 而且 128 个头 各自有各自的一套判断标准,同时在算。

① 点积:谁该看谁 拿当前位置的 Q,去跟每一个位置的 K 做点积。方向接近,点积就大, 意思是「这个位置我该多看一眼」;不相干甚至相反,就是小的或者负的。
每个头独立做一遍,所以这一步产出的是 128 张 128K × 128K 的方阵。
② 除以 √192:不是玄学,是防饱和 192 个维度累加,点积的数值会随维度一起长大。直接送进 softmax 会饱和 —— 几乎全部权重压给一个位置,其余的被压成 0,梯度也跟着消失
除以 √d 把幅度拉回可用区间。注意分母是 192 不是 128 —— Q 和 K 每个头是「128 不转 + 64 转过」拼起来的。
展开:关于「除以 √192」的两个细节—— 讲的时候可以跳,但不能讲错
两个值得较真的细节 ① 原始论文对这一步的措辞是「我们怀疑」,不是「我们证明」。 Attention Is All You Need §3.2.1 的原话是 “We suspect that for large values of d_k, the dot products grow large in magnitude, pushing the softmax function into regions where it has extremely small gradients”, 脚注里给的是一个直觉论证:假设各分量独立、均值 0 方差 1,点积的方差就是 d_k, 所以除以 √d_k 把标准差拉回 1。这是个很好用的直觉,但它是猜想不是定理。
② V3 实际代码里这个系数不等于 1/√192。 因为开了 YaRN 做长度外推(original_max_position_embeddings = 4096factor = 404,096 × 40 = 163,840), 缩放系数还要再乘一个 mscale²
mscale = 0.1 × ln(40) + 1 = 1.36891/√192 × 1.3689² = 0.1352 —— 而不是 0.0722,差了 1.87 倍。 原理完全不变,只是这个常数跟长上下文外推是绑在一起的。
出处是参考实现 modeling_deepseek.pysoftmax_scale = q_head_dim^(-0.5) 之后那个 * mscale * mscale,以及 yarn_get_mscale() 的定义。 另外注意 163,840 才是配置里的位置上限,128K 是对外宣称的可用长度,两个数不一样。
③ 遮住未来:这一步是正确性,不是优化 第 5 个位置不许看第 6 个。做法是把方阵的上三角设成负无穷, softmax 之后自然就是 0。
不遮的话模型就是在抄答案 —— 训练时看着收敛得很漂亮, 推理时后面的 token 还不存在,立刻垮掉。
④ softmax:把分数变成「取多少」 按行归一化,做完每一行加起来正好是 1。这一行就是一个概率分布: 「我这个位置的信息,30% 从第 1 个位置取,47% 从第 5 个位置取」。
也正因为 softmax 是逐行、逐头做的非线性运算, 128 个头到这一步就再也合并不回去了(上一节讲的就是这件事)。

⑤ 加权求和,⑥ 拼起来出去。 拿第 ④ 步那套权重去对 V 加权求和,每个头吐出一个 128 维的向量。 128 个头拼成 16,384 维,过一次 o_proj 压回 7,168,汇回残差流。 o_proj 是一层里最大的一块矩阵(117.44 M,占了 MLA 一层的六成), 也是 128 个头之间唯一交换信息的地方 —— 在那之前它们各算各的,谁也不看谁。

这一步跟它前后的计算,根本不是一类东西 投影(Q/K/V 那几步、o_proj)是 [128K, 7168] × [7168, N] 这种又大又规整的矩阵乘, 形状静态、算术强度高,加速器最喜欢。
打分是 128 个小矩阵乘批在一起,中间还夹着 softmax 这种逐行归约; 而且它的规模跟序列长度是平方关系 —— 序列翻倍,这一段翻四倍。
下一节那个把人吓一跳的数,就是从这里长出来的。

五个投影矩阵,一层多大

一层 MLA 只有五块权重。五块里有一块自己就占了六成 —— 看条长就够了,数字在下面展开里。

条长按参数量线性画 · 一层合计 187.11 M o_proj 117.44 M  62.8% —— 一块顶其余四块加起来还多 16,384 × 7,168 q_b_proj 37.75 M 20.2% 1,536 × 24,576 kv_b_proj 16.78 M 9.0% 512 × 32,768 q_a_proj 11.01 M 5.9% 7,168 × 1,536 kv_a_proj 4.13 M 2.2% ⭐ 唯一决定 KV cache 的那块,却是最小的一块 7,168 × 576 ⭐ o_proj 大,是因为它跨着 16,384 → 7,168 那道口子,两头都宽 ⭐ kv_a_proj 小,是因为它只往 576 里写 —— 而全部 KV cache 就是这 576
一层 MLA 的五块权重,条长按参数量线性画最大的那块和最小的那块差 28 倍,而决定显存账的恰恰是最小的那块× 61 层 = 11.41 B,这就是 671 B 里注意力占的全部。
展开:五个矩阵各自的形状和分工—— 图上那五个数字的出处
矩阵形状参数量干什么
q_a_proj7168 × 153611.01 MQ 降到低秩 —— 省参数,不省 cache
q_b_proj1536 × (128×192)37.75 MQ 升回 128 头
kv_a_proj7168 × 5764.13 M⭐ 压成 512 + 单出 64 维 RoPE
kv_b_proj512 × (128×256)16.78 M⭐ 升回 128 头的 K 和 V
o_proj(128×128) × 7168117.44 M输出投影。一层里最大的一块 —— 因为它要把 128 头拼出来的 16,384 维压回 7168
一层合计187.11 M× 61 层 = 11.41 B

验算:16384×7168 = 117,440,5121536×24576 = 37,748,736512×32768 = 16,777,2167168×1536 = 11,010,0487168×576 = 4,128,768。 合计 187,105,280 = 187.11 M117.44 / 187.11 = 62.76%

算账:MLA 到底省了多少

MLA 的 KV cache

8.58 GiB

131,072 token × 61 层 × 576 × 2 B

同配置换标准 MHA

488.00 GiB

每 token 每层要存 32,768 个数

差距

56.9×

这个差值就是 MLA 存在的理由。

单 device 94.74 GiB
同样的序列长度,四种方案的 KV cache。红色虚线是单个 TPU v7 device 的 HBM —— 标准 MHA 的 KV cache 一家就要好几张卡才放得下,而模型权重还一个字节都没算进去。

⚠️ 真正的爆点:注意力分数矩阵

上面说的都是「存下来的」。还有一个算的时候临时产生的东西,比 KV cache 吓人得多。

注意力要算每个 token 对每个 token 的分数,这是一个 序列长度 × 序列长度 的方阵,而且每个头一份

128 头 × 131,072 × 131,072 × 2 B = 4.00 TiB ← 每层,一次
这就是 FlashAttention 必须存在的原因 注意它不是为了快,是为了根本放得下。 FlashAttention 的做法是永远不把这个矩阵整个物化出来 —— 分块算、边算边累加、算完就扔。省下的不是时间,是这 4 TiB
每个小方块 = 一个 TPU v7 device(94.74 GiB)。 这还只是一层、一次、batch 1。

FLOPs:平方项什么时候开始吃人

注意力有两处的计算量是序列长度的平方:算分数(QKᵀ)和加权求和(AV)。 其他所有部分 —— 投影、MLP、MoE —— 都只是线性的。

短序列时平方项微不足道,长序列时它会反过来吃掉一切。拐点在哪,可以算出来。

横轴序列长度(对数),纵轴 attention 平方项占前向总算力的比例。 竖线是当前选定的序列长度 —— 拖动顶上的滑块看它怎么移动
128K 下,attention 的平方项吃掉 81.8% 的前向算力。 换句话说:这个规模上,「模型有多少参数」已经不是算力的主要矛盾了 —— 序列有多长才是。 把顶上滑块推到 1M 看看,那个数字会变成 97%。
所以后面发生了什么 这条曲线一旦被算出来,接下来该做什么就很明显了:如果 attention 吃掉九成算力, 那就别让每个 query 都看全部 token。 V3.2 的做法叫 DSA:给每个 query 先挑出最相关的 2,048 个 token 再算注意力(index_topk = 2048它写在 V3.2 自己的 config 里)—— 128K 下这是 1.6%, 平方项当场变成线性。
再往后的 V4 是一个混合架构,两种注意力配着用Compressed Sparse Attention(压缩 + 稀疏挑选)和 Heavily Compressed Attention(压得更狠,但不挑选,看全局)。 结果是 100 万 token 上下文下,V4-Pro 的单 token 推理算力只要 V3.2 的 27%, KV cache 只要 10%。 (DeepSeek-V4 论文摘要,arXiv 2606.19348。 注意是两种,不是三种 —— 这一处本课之前写错过。)

这些方案长什么样、各自在换什么,是专题三的事。 这一课只负责把「为什么非改不可」算出来 —— 就是上面这条曲线。
① 嵌入
② MLA
③ Dense
④ MoE
⑤ 层间
⑥ 出口
⑦ 合账
走到这里累计 权重 显存 算力
第 3 步

Dense MLP:先把「不稀疏」讲清楚

first_k_dense_replace = 3 —— V3 的前 3 层是普通 MLP。 先讲这 3 层,因为不理解 dense 就不知道 MoE 在省什么。

结构:三个矩阵 + 一个门

撑宽、开一个阀门、再压回来。 三个矩阵,每个 7,168 × 18,432 —— 一层 396.36 M 参数,三层合计 1.19 B

这一步的形状是完全固定的, 跟输入内容、跟序列多长都没有关系。下面那四条好处,全都是从这一句长出来的。

展开:四行公式—— 图上那条金色的乘号,写出来是什么
gate = W_gate · x // 7168 → 18432 up = W_up · x // 7168 → 18432 h = SiLU(gate) ⊙ up // 逐元素相乘 out = W_down · h // 18432 → 7168

是逐元素相乘,不是矩阵乘 —— 两个 18,432 维的向量对位相乘,出来还是 18,432 维。 SiLU(z) = z · σ(z)config 里那个字段就叫 hidden_act: "silu"

dense 的四条好性质 —— 它对硬件到底有多友好

每个 token 走完全相同的一条路→ 形状静态可预测
就是一个大矩阵乘→ 算力利用率天然高,MXU 最爱
不需要任何通信来决定谁算什么→ 没有调度开销
编译器能提前把一切排好→ 无运行时不确定性

这四条后面会被一条一条拿回来说 —— 因为 MoE 一条都保不住。 ①②③④ 到第 4 步分别变成:形状要等路由跑完才知道(dropless 那一节就是在抢它)、 一个大矩阵碎成 257 个小的、多出两趟 all-to-all、编译器只能按最坏情况留位置。

SwiGLU · 门控 = 两条路 + 一个乘法 输入 x 7168 工作空间 18,432 撑宽 2.57× 输出 7168 gate_proj 7168 → 18,432 SiLU 阀门:每个通道开多大 up_proj 7168 → 18,432 水流:不过非线性,原样的值 逐元素相乘 down_proj 18,432 → 7168 三根灰条的高度是按真实宽度画的 —— 撑宽 2.57 倍,再原样压回来 每个矩阵 7168 × 18,432 · 一层 396.36 M 参数 · 前 3 层合计 1.19 B
三个矩阵的分工:两个撑宽,一个压回。 上面那条过 SiLU 变成阀门,下面那条原样当水流,两者逐元素相乘。 经典 MLP 只有 up 和 down —— 多出来的那个矩阵,买到的是 「阀门开多大,由这个 token 自己决定」

为什么是三个矩阵?两个不是就够了吗

这是这一节最常被打断的地方,而且问得对 —— 经典 MLP 确实只有两个: 撑宽、过一个固定的非线性、压回。问题在于那个非线性是死的, 不管什么输入、哪个通道,都按同一个规则压一遍。

SwiGLU 把「撑宽」这一步做了两份:一份过 SiLU 当阀门,一份原样当水流, 再逐元素相乘。于是「哪些通道该放行、放多少」变成了 学出来的、而且跟着输入变的东西。

关键不在 SiLU,在那个乘号。 同一篇论文里还有一个叫 Bilinear 的变体 —— 两条路都不过任何激活函数,直接相乘, 这时候输出就是 x 的严格二次型。 而它照样赢过所有「只换激活函数」的写法。 说明买到表达力的是「两个线性变换相乘」,不是某个激活函数更聪明。

这句话有实验撑着,而且是同一张表里读出来的。 八个写法,全部对齐参数量和算力,训一样多的步数 —— 看它们分成了哪两堆。

条长 = 比 ReLU 基线好多少(heldout log-perplexity,越小越好) · 八个写法全部对齐参数量与算力 只换激活函数 还是两个矩阵 —— 这一组一个都没赢 GELU 1.679 比基线还差一点 Swish 1.683 差得更多 加一个乘号 三个矩阵,中间宽度按 2/3 缩过 —— 这一组全都赢 GLU 1.663 ⭐ Bilinear 连激活函数都没有,纯粹两条线性路相乘 1.648 ReGLU 1.645 SwiGLU V3 用的就是这个 1.636 GEGLU 1.633 全场最好 ↑ ReLU 基线 1.677 ⭐ 两组之间的差距,比组内任何两个的差距都大 —— 分界线是「有没有那个乘号」,不是「用了哪个激活函数」 四个 GLU 变体的区别只在门用什么函数:GLU 用 sigmoid,ReGLU 用 ReLU,GEGLU 用 GELU,SwiGLU 用 SiLU。Bilinear 干脆不装门
出处:GLU Variants Improve Transformer,arXiv 2002.05202 的 Table 1。 T5 规模的 span-filling 任务,八个写法全部对齐参数量与算力,训练 524,288 步。
读法只有一句:换激活函数(上面两根)没用甚至更差,加乘号(下面五根)全都有用。 而 SwiGLU 1.636 和 GEGLU 1.633 基本打平 —— V3 选 SwiGLU,不是因为它是最好的那个。
这里必须诚实:连提出 SwiGLU 的人都说不出为什么它好 这篇论文的结论段原话是:“We offer no explanation as to why these architectures seem to work; we attribute their success, as all else, to divine benevolence.”
翻成人话:我们没法解释它为什么行,就当是老天赏饭。 所以上面那句「乘法带来交互项」是一个说得通的直觉,不是被证明的因果。 这张表能支撑的只有一件事:门控这一组确实赢,而且赢的不是激活函数。
展开:那张表的完整数据,以及它能不能推广到 671 B—— 图上只画了一列,还有一列
写法矩阵数 65,536 步524,288 步
FFNReLU 基线2 1.9971.677
FFNGELU2 1.9831.679
FFNSwish2 1.9941.683
FFNGLU3 1.9821.663
FFNBilinear ⭐ 无激活函数3 1.9601.648
FFNReGLU3 1.9531.645
FFNSwiGLU V3 用的3 1.9441.636
FFNGEGLU3 1.9421.633

heldout log-perplexity,越小越好。 两列分别是训练 65,536 步和 524,288 步。两列的分组结论完全一致 —— 不是只在某个训练量上成立。

这张表的适用范围,要说清楚 它跑在 T5 规模的 span-filling 任务上,不是 671 B 的因果语言模型。 严格说,它证明的是「在那个设置下,门控这一组赢」。
能支持我们的是另一件事:翻开今天主流模型的 config,全都在 GLU 家族里。 这是很强的共识信号,但它是共识,不是这张表的直接外推。
模型hidden_act写法 中间宽度 ÷ 残差流
Llama 3 8BsiluSwiGLU3.50×
Mistral 7BsiluSwiGLU3.50×
Qwen3 8BsiluSwiGLU3.00×
Gemma 2 9Bgelu_pytorch_tanhGEGLU4.00×
DeepSeek V3(前 3 层)siluSwiGLU 2.57×

都是从各自 config.json 里读的。 顺带看出一件本来不明显的事:那条「乘 2/3」的规则(4× → 2.67×)今天并没有被普遍遵守 —— Llama 3 和 Mistral 是 3.5×,Gemma 2 干脆 4×,都比规则宽得多。 反倒是 V3 的 2.57× 最接近原始规则,也是这几个里最省的。 所以「V3 照着参数持平反推 18,432」是一个说得通的解读,但那是我们的解读 —— 同样遵循这条规则的模型,今天已经是少数。

多一个矩阵,为什么没多花钱

第二个必被问到的问题。答案是中间宽度被缩过 —— 而且缩多少,是那篇论文里写死的一条规则。

每一小格 = 一个权重矩阵,格子长度 = 它的参数量 · 三行总长直接可比 经典 MLP 2 个矩阵 · 宽 28,672(4×) up 205.52 M down 205.52 M 411.04 M 论文的 2/3 规则 3 个矩阵 · 宽 19,115 gate 137.01 M up 137.01 M down 137.01 M 411.04 M V3 实际 3 个矩阵 · 宽 18,432(2.57×) gate 132.12 M up 132.12 M down 132.12 M 396.36 M 参数持平线 V3 少 3.57% ↘ ⭐ 2/3 是论文里写死的规则:三矩阵写法把中间宽度乘 2/3,参数量和算力就跟两矩阵写法完全持平。V3 又比它低一点(0.643 而不是 0.667)
三行总长直接可比。多一个矩阵不等于多花钱 —— 中间宽度缩了,总长反而更短。 门控那点表达力,在参数账上是白送的,V3 甚至还省了 3.57%。
「乘 2/3」不是我们凑的,是论文里明写的 “All of these layers have three weight matrices, as opposed to two for the original FFN. To keep the number of parameters and the amount of computation constant, we reduce the number of hidden units dff … by a factor of 2/3 when comparing these layers to the original two-matrix version.”
—— GLU Variants Improve Transformer,arXiv 2002.05202 第 2 节。 上面那张消融表也是在这条规则下跑的,所以那八个写法才真的可比。
展开:18,432 这个数同时卡在三条线上—— 哪一条是真正的理由,论文没说

这个数字很干净,而且它同时满足三件事。三件都能验算,但没有一件被论文认领过。

约束验算吻合程度
① 正好是 9 个专家
= 8 个路由 + 1 个共享
9 × 2,048 = 18,432 完全相等
一层 dense = 一层 MoE 每个 token 真正激活的量
② 对齐加速器的矩阵单元 18,432 ÷ 256 = 72 整除
但 2/3 规则给的 19,114.67 连整数都不是
③ 论文的 2/3 规则 28,672 × 2/3 = 19,114.67 接近但没踩上
V3 是 0.643 倍,规则是 0.667 倍

① 那条最值得注意,因为它不是巧合能解释的量级。 它意味着:V3 前三层每个 token 走的 MLP,和后面 58 层里每个 token 激活的那 9 个专家, 参数量与算力逐个矩阵地相等 整个模型从头到尾,每个 token 走的 MLP 算力几乎是一条平线。

「几乎」这两个字要留着:MoE 层还多一个路由器, 7,168 × 256 = 1.84 M相对 396.36 M 是 0.46%所以准确的说法是「专家那部分严格相等,加上路由多 0.46%」,不是「一个都不多」。

但请注意措辞。「相等」是能验算的事实; 「所以他们是照着这个定的」是解读 —— V3 论文只写了 intermediate_sizemoe_intermediate_size 这两个数,没有写它们之间的关系是有意的。

这个结构后面还会原样出现一次 MoE 的每个专家用的就是一模一样的三矩阵 SwiGLU,只是中间宽度从 18,432 缩到 2,048moe_intermediate_size)。 下一步不用重新学结构 —— 只要盯着宽度和数量怎么变。
这一条要说透,它是后面所有 MoE 麻烦的对照组 别把 MoE 理解成「白捡的便宜」。 本步开头列过 dense 的四条好性质,MoE 一条都没有 —— 这里再摆一遍,因为第 4 步的每一个麻烦都能对回其中一条:
形状静态可预测 → 要等路由跑完才知道谁去哪,而且下一批就变
一个大矩阵乘、MXU 最爱 → 碎成 257 个小矩阵,单个都喂不饱 MXU
不需要通信来决定谁算什么 → 每层多两趟 all-to-all,送去再送回
编译器能提前把一切排好 → 只能按最坏情况预留,留少了就得丢 token
MoE 省下来的算力,买单的就是这四条。

为什么最前面几层偏偏不做 MoE

这个问题有一个流传很广的答案:「靠近输入的层做的是通用活儿,没什么可分工的。」 听起来很顺,但那不是 DeepSeek 给的理由 —— 真实情况是一条链,而且最后一环是断的。

同一个设计,三篇论文各说了什么 · 按时间从上往下 DeepSeekMoE · 2024 前 1 层保持 dense ✅ 给了理由: 第一层的负载均衡收敛得特别慢 DeepSeek-V2 · 2024 前 1 层保持 dense ↩ 只说「沿用上一篇」 (原文:Following Dai et al. 2024) DeepSeek-V3 · 2024 前 3 层保持 dense ❓ 只陈述事实,没有给理由 为什么从 1 变成 3 —— 论文没写 能说的 「保留前几层 dense」这个做法,最初的公开理由是训练稳定性(负载均衡收敛慢),不是「通用 vs 专用」 不能说的 「V3 选 3 层是因为前三层最通用」——这是我们的猜测,论文里没有任何依据 顺带一提 V3 的部署方案里,浅层这几个 dense MLP 是单独按 1-way 张量并行摆的 —— 为了省掉那部分通信 这一条反过来印证了第 ③ 条好性质(dense 不需要通信来决定谁算什么):dense 的好,是能在部署上兑现成真钱的
这是一条能查证的链,也是一个能查证的缺口。 把「负载均衡收敛慢」讲成「前几层比较通用」,是把一个训练现象换成了一个语义故事 —— 两者都可能对,但只有前者写在论文里。
展开:那句「唯一的理由」,原文长什么样—— 一共就一句话

整条链上只有 DeepSeekMoE 那一篇给了理由,而且就是一句话(原文):

// DeepSeekMoE (arXiv:2401.06066), §5.1 Experimental Setup We substitute all FFNs except for the first layer with MoE layers, since we observe that the load balance status converges especially slower for the first layer.

请注意措辞是 we observe —— 这是一条实验观察,不是理论推导。 论文没有解释「为什么偏偏是第一层」,后面两代也再没展开过。

同一篇里 145B 那版也是同样处理,但没有重复给理由; DeepSeek-V2 的原文是 Following Dai et al. (2024), we substitute all FFNs except for the first layerDeepSeek-V3 的原文是 We substitute all FFNs except for the first three layers —— 1 变成 3 这件事,前后没有一个字的说明。

横着看一眼:这远不是共识

上面那条链只是 DeepSeek 一家的谱系。把同期几个主流 MoE 的 config.json 拉下来对一眼,分歧大得出乎意料 —— 有人留 3 层,有人留 1 层,还有一大批干脆一层都不留。

模型层数 前面留几层 dense依据
DeepSeekMoE 16B281 论文原文 —— 唯一给了理由的那篇
DeepSeek-V21 论文原文「沿用上一篇」
DeepSeek-V2-Lite271 first_k_dense_replace: 1
DeepSeek-V3613 first_k_dense_replace: 3 —— 无解释
GLM-4.5923 first_k_dense_replace: 3 —— 跟 V3 一致
Kimi K261 1 first_k_dense_replace: 1 —— 架构照抄 V3,唯独把这个改回去了
Qwen3-235B-A22B940 mlp_only_layers: [] —— 94 层全是 MoE
Mixtral 8x7B320 压根没有这个字段
MiniMax-M2620 没有 dense 前缀,shared_intermediate_size: 0 连共享专家都不要了

除 DeepSeekMoE 16B 与 DeepSeek-V2 两行取自论文正文外, 其余每一行都是从各自 Hugging Face 仓库的 config.json 直接读的。 V2 本体没读到 config,层数留空,不猜。

Kimi K2 那一行值得单独看三十秒 它的 architectures 字段写的就是 DeepseekV3ForCausalLM, 并且 hidden_size 7168intermediate_size 18432moe_intermediate_size 2048q_lora_rank 1536kv_lora_rank 512qk_nope 128qk_rope 64num_hidden_layers 61 —— 本页讲过的形状它一个没改
偏偏把 first_k_dense_replace 从 3 改回了 1。 一个抄到这个程度的团队专门动了这个参数, 说明它不是架构上的必然,而是各家自己试出来的经验值。
展开:机制上为什么会是第一层—— 这段是推测,我标清楚

先说清楚:下面这套解释论文里没有,是我们的推断。

router 说到底就是拿 hidden state 做一次线性投影再取 top-k —— hidden state 像的 token,必然被分到同一批专家。 而第一层的 hidden state 还没被 attention 混合过,它基本就是 embedding 加个位置信息:同一个词不管出现在什么上下文里,长得几乎一样。

于是 router 在第一层会退化成「按词典 ID 分配」。 语言里高频词占绝对多数 —— 逗号、the、「的」—— 这些词会成批涌向同一个专家。负载自然平不了,而且它平不了的原因是数据分布本身, aux loss 要跟这个分布对着拧,收敛慢就不奇怪。

⚠️ 但有反证,所以别把它当定律 也有实测工作报告的是相反方向:早期层路由还算均衡, 反而是深层出现专家坍缩、少数专家吃掉大部分 token。
两边都是实测,说明「哪一层难平衡」不是普遍规律, 跟模型、数据、aux loss 系数都有关系。 这也正好解释了为什么各家留的层数从 0 到 3 都有 —— 大家试出来的结果本来就不一样。
给这一节收个尾:这个设计到底是为了啥 唯一的公开起点是一条实验观察 ——「第一层负载均衡收敛特别慢」,出自 DeepSeekMoE。
机制为什么如此,没人给过解释;V3 从 1 加到 3,也没给过解释
远不是共识:同期至少三个主流模型一层 dense 都不留。
但它有一个白捡的好处 —— 浅层 dense 不需要 all-to-all, 部署时能单独按 1-way 张量并行摆,省通信。
所以最诚实的说法是:这是一个有实证起点、缺机制解释、且尚未收敛成共识的工程选择。 留 0 层到 3 层之间怎么选,目前只能靠试。
① 嵌入
② MLA
③ Dense
④ MoE
⑤ 层间
⑥ 出口
⑦ 合账
走到这里累计 权重 显存 算力
洞天石壁上凿着数百间闭关石室,只有九间透出金光,一粒神识正沿光路飘去
第 4 步 · 全课参数量的 97.8% MoE:671B 的主体 256 扇门都在那儿,每一扇都占着显存。 但每个 token 走过时,只有 9 扇会为它亮起。
第 4 步 ⭐

MoE:671B 的主体

整个模型 97.8% 的参数都在这 58 层里。

护士分诊,专家看病。 不是每个医生都给每个病人看一遍 —— 先分诊,再送到对口的少数几个专家那儿。 总的诊疗量因此降下来了。

4a · 原理:稀疏在哪

Dense MLP · 前 3 层 18,432 宽 396.36 M · 100% 参与计算 每 token 都走完整一遍 形状静态 · 无通信 · MXU 满载 MoE · 后 58 层 256 个小专家,每个 token 只走 8 个 + 1 个共享 门控 · 分诊台 7168 × 256 打分 ■ 被选中的 8 个(每个 3×7168×2048 = 44.04 M) ■ 共享专家 □ 没被激活的 248 个 —— 参数还在显存里 一层全部参数 11.32 B 一个 token 实际用到 396.36 M · 稀疏比 28.6×
左边是 dense,一个大而宽的 MLP;右边是 MoE,256 个窄专家。 两边一个 token 实际做的乘法一样多 —— 巧的是右边那 9 个小专家加起来正好也是 396.36 M。
MoE 唯一的、也是全部的价值 参数量和计算量解耦了。 参数可以往上堆到 671B(模型知道得更多), 而每个 token 的计算量还停在 dense 那个水平(算得起)。
⚠️ 「专家」这个名字,是整个 MoE 里最容易误导人的地方 「专家」这个词太容易让人想到分科:一个管数学、一个管代码、一个管医学。 这件事被真的量过,答案是:不这么分。

所以「护士分诊、专家看病」这个比喻,用来理解「为什么总计算量降下来了」是对的, 但别顺着它推出「所以有个专家懂医学」—— 那一步是错的,而且是有实测反驳的。
有人把训好的 MoE 拆开数过路由 —— 两个结果,一个否定一个肯定 ✗ 不是按学科分的 把不同领域的文本喂进去,看专家分布 —— 数学论文、生物摘要、哲学论文,三条分布几乎重合 ArXiv PubMed 哲学 ← 形状 一样 原文:“we do not observe obvious patterns in the assignment of experts based on the topic” ✓ 是按局部语法结构分的 判据:相邻两个 token 选中同一个专家的比例 随机 12.5% 14.1% 第 0 层 ≈ 随机 26.4% 第 15 层 是随机的 2 倍 22.7% 第 31 层 论文举的例子很具体:Python 里的 self、英文里的 Question 总是走同一个专家,哪怕它们是好几个 token; 代码的缩进 token「总是」被分给同一批专家 ⭐ 而这件事直接变成系统问题 —— 这是这门课最关心的一步 原文点破了两面:局部性高 → 做专家并行时更容易把某几个专家挤爆(over-subscription); 但反过来,局部性也可以拿来做缓存 同一个性质,既是负载均衡的敌人,又是缓存的朋友 —— 取决于你站在哪一侧看它
出处:Mixtral of Experts,arXiv 2401.04088 第 5 节 Routing analysis 与 Table 5。 右边三根条是 8 个领域的平均值(第 0 层 13.6–14.9%, 第 15 层 23.6–28.4%,第 31 层 19.7–26.3%)。
适用范围要说清楚:Mixtral 是 8 专家 top-2,V3 是 256 专家 top-8,两者不能直接换算。 它能支撑的是那个否定结论 ——「专家按学科分工」这个直觉在实测里没有出现; 至于 V3 的 256 个专家具体怎么分,公开资料里我没有查到同等力度的分析。

⭐ 一个 token 的路径,是它自己的

这才是「一个 token 的一生」这个说法真正的意思。58 个 MoE 层,每一层都重新分诊一次 —— 于是每个 token 都走出一条只属于它自己的路径。

示意:三个 token 穿过若干 MoE 层。每一层各自分诊,路径互不相同。 注意下面那条黄带 —— 共享专家不参与路由,每个 token 每一层都过它, 所以它是图里唯一一条直线;上面三条锯齿才是 top-8 选出来的。
比例是缩过的:背景 16 行代表 256 个路由专家,每层点亮 3 格代表选中的 8 个。 真实情况是 58 层、每层从 256 个里选 8 个,外加 1 个共享 —— 可能的路径数是个天文数字,而模型的容量就藏在这些组合里。

为什么被稀疏化的是 MLP,不是 attention

两个原因。一,参数大头在 MLP —— 上面那张表已经看到,MLA 一层 187 M, MLP 一层 396 M,MoE 一层 11.32 B。二,MLP 天然就是「一堆独立的问题」 —— 它对每个 token 单独作用,token 之间不交互,所以可以放心地让不同 token 走不同的路。 attention 的全部意义恰恰是让 token 互相看,切不开。

细粒度:为什么专家要做小做多

dense 中间层

18,432

前 3 层用的宽度

每个专家

2,048

只有 dense 的 1/9

能选出的组合数

C(256,8)

≈ 4×10¹⁴ 种分工方式

同样的激活预算,专家越小越多,能表达的分工组合就越多。 如果只有 8 个大专家选 1 个,那就只有 8 种可能;256 个小专家选 8 个, 组合数是个天文数字。这是细粒度 MoE 的全部道理。

共享专家:省的是冗余

语法、常识这类东西每个 token 都要用。如果不设共享专家, 那 256 个路由专家里每一个都得把这些基本功再学一遍 —— 同一份知识存了 256 份。 V3 的做法是拎出 1 个共享专家专门装这些,所有 token 都过它; 路由专家于是可以专心做分工。

⭐ 一个恰好相等:MoE 那一层,每个 token 的计算量跟 dense 一模一样

Dense MLP 前 3 层 中间宽度 18,432 MoE 一层 这个 token 实际走过的 8 个路由专家 · 每个 2,048 共享 · 2,048 同一层里还躺着另外 248 个路由专家 —— 这个 token 一个都没碰 9 / 257 = 3.50% 8 × 2,048 + 1 × 2,048 = 18,432  两根条一样长,不是画得像,是相等
九段拼起来,正好是上面那根 dense 条。 中间宽度决定了三个矩阵的大小,所以宽度相等 ⇒ 参数相等 ⇒ 计算量相等 —— MoE 那一层,每个 token 在专家上走过的 FLOPs 跟一层普通 dense MLP 严格相等。
唯一的小尾巴:MoE 还得先跑一遍门控矩阵, 7,168 × 256 = 1.84 M,相对 396.36 M 是 0.46%所以是「专家那部分一个都不多,外加千分之五的分诊费」。
把它写成两行,自己验一遍
// 一层 dense(前 3 层) 3 × 7,168 × 18,432 = 396,361,728 // 一层 MoE,这个 token 实际激活的 8 + 1 个专家 9 × 3 × 7,168 × 2,048 = 396,361,728
不是接近,是同一个数。 因为 9 × 2,048 = 18,432, 而中间宽度是唯一的变量 —— 三个矩阵的形状全由它定。
同一层里躺着 11.32 B 参数,是 dense 层的 28.6 倍; 而每个 token 掏出来算的,是同样的 396 M,外加千分之五的分诊费这就是「参数量和计算量解耦」这句话的字面意思。 模型可以一直变大,而每个字的账单不动 —— 代价是那多出来的 28 倍必须全程躺在显存里,这一点在 4d 会算清楚。
这个相等是怎么来的,说清楚边界 可以确定的9 × 2,048 = 18,432 是恒等式, 上面两行算式都来自 V3 公开的 config(intermediate_sizemoe_intermediate_sizenum_experts_per_tokn_shared_experts),谁都能自己对一遍。
不能确定的:作者是不是为了让它相等才这样挑参数。 公开资料里没有这句话。所以讲的时候说「这两个数相等」,别说「他们特意设计成相等」。

但相等这件事本身很有用 —— 它给了一把现成的尺子: 凡是 MoE 比 dense 多付出的,都不在 FLOPs 上。 路由、通信、负载不均、显存,后面每一条麻烦都能对回这句话。

4b · 路由:怎么选那 8 个

残差流上的这个 token · 7,168 维 gate 矩阵 7,168 × 256 184 万参数 —— 只占这一层 11.32 B 的 0.016% sigmoid(不是 softmax) 256 个独立分数 s,彼此不竞争、加起来也不等于 1 原始分数 s 原封不动 带到最后 ↓ 偏置只在这个框里起作用 ↓ 偏置 b 每个专家一个 按负载直接调,不学 竞选分 = s + b 吃太饱的调低,一直没人选的调高 256 个分成 8 组,每组 32 个 —— 只准挑 4 组 组分 = 组内前 2 名之和 · 落选的 4 组整组出局,候选只剩 128 个 在 128 个候选里取 top-8 → 只得到 8 个编号 偏置到此为止,不往下走 按这 8 个编号,去取没加过偏置的 s 归一化到和为 1,再乘 2.5 —— 这才是加权用的权重
两条路径在图上完全不相交。 蓝色那条带着偏置,但它走到「8 个编号」就被剪断了; 绿色那条从 sigmoid 直接旁路到最后,一路没碰过偏置。 所以偏置能把负载掰匀,却一个字节都没改变模型的输出。
⭐ 这一处值得单独记:调度归调度,不污染输出 负载均衡是一个「调度」问题,模型质量是一个「学习」问题。 V3 把它们放在两条互不相交的路径上。

后果有三条,都很实在: 偏置可以任意大力地掰 —— 掰得再狠,输出也不失真; 它不参与反向传播,因此不跟主目标抢梯度; 它是训练循环外面的一个动作,想改策略不用动模型。

这不用从源码推,论文里有一句现成的“Note that the bias term is only used for routing. The gating value, which will be multiplied with the FFN output, is still derived from the original affinity score.” —— arXiv 2412.19437 第 2.1.2 节。两份分数,两条路。
展开:sigmoid「加起来不等于 1」,可最后那 8 个权重又归一化到 1 —— 这不矛盾吗—— 会被问到的一个点

不矛盾,因为这是两个时刻的事。 「不等于 1」说的是打分那一刻:256 个分数各算各的,谁高谁低不影响别人。 「归一化到 1」说的是选完之后:只在选中的那 8 个之间重新分配比例。

// ① 打分:256 个独立的 sigmoid,互不相干,总和是多少都行 s_i = sigmoid(u · e_i) i = 1 … 256 // ② 选:加偏置只为排序,选出 8 个编号(偏置到此为止) // ③ 定权重:只在这 8 个里归一化,再乘 routed_scaling_factor g_i = s_i / Σ(选中的 8 个 s) × 2.5

那 sigmoid 换成 softmax 到底差在哪? 差在第 ① 步。softmax 的分母是全部 256 个,所以抬高任何一个专家的分, 就等于压低其余 255 个 —— 专家之间被迫零和竞争。 sigmoid 没有这个分母,每个专家可以独立地说「这个 token 我很在行」。
换句话说:竞争关系被推迟到了「排序取前 8」这一步,而不是烤进打分函数里。 V2 用的是 softmax,V3 换成了 sigmoid —— 论文里明写了这处改动。

那个 2.5 是哪来的:config 里的 routed_scaling_factor归一化会把 8 个权重的总和摁成 1,而共享专家那一路的系数是 1, 两边加起来相对残差流就偏小了;乘一个大于 1 的常数把整支路的幅度抬回去。 这是我们的读法 —— 论文的公式里没有出现这个系数,它只在开源 config 和实现里。

设计取值为什么这么定
打分函数sigmoid 不是 softmax。softmax 逼着 256 个专家的分数互相竞争、加起来等于 1; sigmoid 让每个专家独立打分,专家之间不必零和
分组限制n_group=8
topk_group=4
纯粹是硬件逼出来的。 论文管它叫 node-limited routing, 一句话原文:「我们保证每个 token 最多被送到 M 个节点。」 这里 M = 4组就是节点 —— 限制一个 token 最多要跨几台机器去取专家
选几个top-8 8 个路由 + 1 个共享 = 每 token 过 9 个专家
负载均衡noaux_tc 主力机制是给每个专家挂一个动态偏置: 最近接活太多的调低一点,太闲的调高一点。
这个偏置只参与「选谁」,不参与「选完之后按多大权重算」 —— 所以它能把负载掰匀,却不改变模型的输出。 而且它不是梯度学出来的,是按实际负载直接调的。
但「完全不用辅助损失」是说过头了 —— V3 另外还留了一个极小的序列级均衡损失兜底,见下方那个框。
⭐ 全课主线最锋利的一个例证 n_group=8, topk_group=4 这两个数字,跟模型质量毫无关系。 它们存在的唯一理由是:让一个 token 要去的专家尽量集中在少数几台机器上, 好让 all-to-all 通信不要跨太多机。 通信约束被直接写进了模型结构里。
展开:这条规则在论文里长什么样,以及「组内前 2 名」这个 2 是从哪来的—— 一个能自己算出来的数
论文原话 “Like the device-limited routing used by DeepSeek-V2, DeepSeek-V3 also uses a restricted routing mechanism to limit communication costs during training. In short, we ensure that each token will be sent to at most M nodes, which are selected according to the sum of the highest Kr/M affinity scores of the experts distributed on each node.”
—— DeepSeek-V3 Technical Report,arXiv 2412.19437 第 2.1.2 节。 注意它给的理由是一个字都不含糊的 “to limit communication costs”。

那个「前 2 名」不是拍的,是代进去算出来的。 论文写的是 Kr/M —— Kr 是激活的路由专家数,M 是最多能跨的节点数:

// K_r = num_experts_per_tok = 8  M = topk_group = 4 K_r / M = 8 / 4 = 2 ← 所以是「每组取前 2 名求和」当组分

这条规则的形状很讲道理如果一个 token 最多只能去 4 个节点、总共要选 8 个专家, 那么平均每个节点得贡献 2 个。 用「组内前 2 名之和」给组打分,等于在问「假如我选了这个节点,它最好的那 2 个能给我多少」 —— 正好对上后面要从它这儿拿走的份额。

顺带一个很能说明问题的数:这套通信设计其实还有富余 论文原话:「虽然 DeepSeek-V3 实际只选 8 个路由专家, 但在通信代价不变的前提下,这个数可以一路加到最多 13 个 (4 个节点 × 平均每节点 3.2 个专家)。」 同一段还提到,这套 IB / NVLink 重叠的方案只需要占用 20 个 SM
读法:先按机器的形状定下「最多跨 4 个节点」,再在这个盒子里看能塞多少专家 —— 最后填了 8,盒子其实能装 13。 这就是「硬件先划线、模型再在线内选参数」最直白的一个样子。
为什么这个设计值得单独讲 老办法是在损失函数里加一项「分得匀不匀」的惩罚,逼模型均衡。 问题是你在让模型同时优化两件不相干的事 ——「答得对」和「分得匀」—— 这两个目标会互相拉扯,最后两头都打折。 论文自己就是这么说的「辅助损失太大会损害模型性能。」

V3 的做法是把「分得匀」这件事的主要负担搬出损失函数。 它变成了训练循环外面的一个调度动作,跟模型学什么无关。 这是一个很好的工程直觉的例子:一个约束如果不属于目标,就尽量别塞进目标里。
⚠️ 「auxiliary-loss-free」这个名字,比实情干净 课件之前写的是「不靠辅助损失」。翻回论文,这句话说过头了。 原文是:「虽然 DeepSeek-V3 主要依赖 auxiliary-loss-free 策略来做负载均衡, 为了防止任何单条序列内出现极端失衡,我们同时还使用了一个补充的序列级均衡损失。」 论文接着说,这个损失的系数 α 「会被赋一个极小的值」
所以准确的说法是:主力是偏置,另有一个极小的辅助损失兜底。 不是「一点都没有」。
V3 其实有两套均衡机制,管的是两个不同尺度的问题 主力 · 动态偏置 b 尺度:整个 batch,跨步累积 每步结束看一遍负载,超载的减 γ,欠载的加 γ ✓ 不进损失函数,不抢梯度 ✓ 只影响「选谁」,不影响输出 解决的是:某几个专家长期吃太饱 兜底 · 序列级均衡损失(α 极小) 尺度:一条序列内部 同一条序列里,别让 token 全挤到少数专家 ✗ 它确实进了损失函数 但系数被论文明说「取一个极小的值」 解决的是:单条序列内的极端失衡 ⭐ 为什么偏置管不了右边那个:偏置是每步结束才调一次的全局量, 它对「这一条序列碰巧全挤在一起」无能为力 —— 那是发生在一步内部的事 读法:左边是策略,右边是保险丝。名字里的「free」说的是左边那件事是主力,不是右边不存在
两套机制不是重复,是尺度不同。 偏置在「步与步之间」把长期负载掰匀;那个极小的辅助损失在「一步之内」防单条序列崩掉。 这也解释了为什么它可以取极小值 —— 它不需要驱动均衡,只需要在偏置够不着的地方兜一下。
边界:「两套机制、各管一个尺度」是论文原文直接说的; 「所以 α 才能取那么小」是一个合理的读法,论文没有这样解释过。)

⭐ 负载不均会怎样:一张卡忙,全场陪着等

专家分散在不同卡上。如果某几个专家特别热门,它们所在的卡就要算更多 token, 而其他卡只能干等 —— 这一层什么时候结束,由最慢的那张卡说了算。

同一个 MoE 层,四张卡,百分比是这张卡分到的 token 占比。条长 = 它要算多久 失衡时 整层在这里才结束 = 最忙那张卡的时间 卡 0 · 40% 一直在算 卡 1 · 20% 干等 卡 2 · 25% 干等 卡 3 · 15% 干等 斜线部分是买了但没用上的算力 —— 这里占了全部四张卡的 37.5% 掰匀之后 提前这么多结束 同样的总工作量,整层快 1.6 倍 四张卡各 25% 示意图,比例是编的 —— 40/20/25/15 只是为了让「谁在等」看得见。真实失衡程度取决于数据和路由,不是一个固定的数
这不是「效率损失几个百分点」,是整个集群按最慢那张卡的速度走。 而且它每一层都重来一次 —— 58 层,每层都要等齐。
回头看 4b 那个偏置:它做的事就是把上面那张图掰成下面那张图。 「让卡别闲着」听起来像个运维指标,其实它直接乘在整个训练的时间和账单上。

4c · 分布式:挪 token,还是挪权重

这一小节是整课的系统高光。 专家分散在不同卡上, 那么一个 token 要用第 137 号专家,怎么办?两条路,真实的二选一

方案 A · Dispatch / Combine —— 把 token 送到专家那儿 卡 0 专家 0-63 卡 1 专家 64-127 卡 2 卡 3 all-to-all ×2(送去 + 送回) 通信量 ∝ token 数 × hidden ✓ token 少的时候便宜(推理 decode) ✗ token 多的时候贵(训练大 batch) 方案 B · EP = 1 + FSDP —— 把专家权重取过来,在本地算 卡 0 卡 1 卡 2 卡 3 all-gather 权重 → 本地算 token 一步都不动 通信量 ∝ 专家权重大小(跟 token 数无关) ✓ token 多的时候摊薄了 ✗ token 少的时候纯浪费
没有哪个方案永远对。token 多、权重相对小 → 挪权重划算; token 少(比如推理 decode 一次就几个 token)→ 挪 token 划算。 还要看互联拓扑 —— 跨机带宽比机内小得多,前面那个分组限制正是为了让 all-to-all 尽量留在机内。
方案 B 图里只画了前向。反向还要再 all-gather 一次权重, 并且把梯度 reduce-scatter 回各自的分片 —— 那是 FSDP 的标准动作,不在这张图里。
⚠️ 一个特别容易叫错的名字:方案 B 不叫 AG-RS 「把专家权重 all-gather 到每张卡」这件事真实存在、而且在 TPU 上是对的选择 —— 但它的名字是「EP = 1 + FSDP」,或者干脆叫「不用专家并行」。
MoE 语境里的 AG-RS 指的是另一件事:它是 all-to-all 的替代品, 搬的同样是 token —— 把全部 token 复制给每张卡,本地路由一遍, 算完再 reduce-scatter 回去。三个互相独立的实现口径一致: Megatron-Core 的开关叫 --moe-token-dispatcher-type,两个取值 allgather / alltoall 都是 token dispatcher; MaxText 里这条路叫 ring of experts

所以「挪 token 还是挪权重」这个二分是对的,只是第二条的标签之前写错了。 三条路的机制、通信量、以及为什么 TPU 反而选字节最多的那条, 在附录里有一整节:§14 token 是怎么见到它的专家的 →

算一下通信量

// Dispatch + Combine,一层 2 × 131,072 token × 4 份 × 7,168 × 2 B = 14.00 GiB // × 58 层 = 812.00 GiB ← 一次前向要搬这么多
⭐ 那个「4 份」最容易写错成「8 份」—— 搬运的单位是目的地,不是专家 直觉上很顺:选了 8 个专家,那就送 8 份。但这是错的。
一个 token 要被拷贝几份,取决于它要落到几个地方同一个节点上的两个中选专家,共享同一份拷贝 —— token 送到那个节点之后,节点内部再分发,那一步走的是机内互联,不是跨机。
目的地的数量是被钉死的:前面讲过的分组路由(topk_group = 4) 保证一个 token 最多落 4 个节点所以倍数是 4,不是 8。
判据一句话:这个数跟 top-k 无关。 就算把 top-k 从 8 改成 16,只要还是最多落 4 个节点,跨机通信量一个字节都不会多。
论文自己把这件事说死了—— 8 加到 13,通信代价不变

“…we limit each token to be dispatched to at most 4 nodes, thereby reducing IB traffic. For each token, when its routing decision is made, it will first be transmitted via IB to the GPUs with the same in-node index on its target nodes. Once it reaches the target nodes, we will endeavor to ensure that it is instantaneously forwarded via NVLink to specific GPUs that host their target experts… This implies that, although DeepSeek-V3 selects only 8 routed experts in practice, it can scale up this number to a maximum of 13 experts (4 nodes × 3.2 experts/node) while preserving the same communication cost.
—— DeepSeek-V3 Technical Report,arXiv 2412.19437 第 3.2.2 节。

最后那句是决定性的:8 → 13 免费。 如果通信量正比于专家个数,多选 5 个不可能不要钱。 它只能正比于节点数 —— 而节点数被 M = 4 焊死了。

同一节还给了布局:256 个路由专家均匀摊在 64 张卡、8 个节点上 (每卡 4 个、每节点 32 个),M = 4。所以「8 组」就是 8 个节点, 一个 token 最多去其中 4 个。

还能再抠一点:那 4 个目的地里,可能有一个正好就是 token 所在的节点, 那一份不用过网。8 个节点里选 4 个,本节点被选中的概率是 1/2所以跨机的期望份数是 3.5 而不是 4。 上面的账保守取 4,这样它是个上界,不会把量算小。

把它跟这一层的计算量放一起,就得到通信/计算比 —— 这个比值决定了通信能不能被计算盖住。它是专题七性能工程那一课的地基。

⭐ 为什么这是真正的二选一:两条斜率不同的线

110 1001K 10K100K 1M10M 一个 step / 一次前向里的 token 数(对数) 1 MiB1 GiB1 TiB 一层的通信量(对数) 方案 B · 挪权重 21.08 GiB —— 水平线,跟 token 数完全无关 方案 A · 挪 token 每个 token 112 KiB —— 线性增长 拐点 ≈ 197,376 token 21.08 GiB ÷ 112 KiB decode 一步(几十个 token) → 远在拐点左边,挪 token 一次 128K prefill = 131,072 token 14.00 GiB —— 还没到拐点,挪 token 仍占优 训练一个 step(百万级 token) → 远在右边,挪权重 专家摊在更多卡上 → A 被分摊 → 拐点往右移 粗口径:只比「每层要搬多少字节」,没算 all-to-all 和 all-gather 的效率差异
两条线的斜率不一样(1 和 0),所以它们一定相交 —— 「要看场景」这句话,是从几何上看出来的,不是一句和稀泥。 拐点两边分别住着推理 decode 和训练,这就是为什么这两件事在这道题上 给出了相反的答案。
拐点那个数是怎么来的,两步就能自己验
// A:每个 token 每层要搬的字节(送去 + 送回) // 4 = 最多落几个节点(topk_group),不是 8 个专家 —— 见上面那个红框 2 × 4 份 × 7,168 × 2 B = 112 KiB // B:一层专家权重(256 路由 + 1 共享,三矩阵 SwiGLU) 257 × 3 × 7,168 × 2,048 × 2 B = 21.08 GiB // 相等时的 token 数 21.08 GiB ÷ 112 KiB = 197,376
要留意的边界:这是把两者都简化成「每层要搬多少字节」来比的 粗口径真实系统里 all-to-all 和 all-gather 的效率不一样, 还要看专家摊在几张卡上 —— 摊得越开,A 的量被分摊得越薄,拐点越往右。
所以不要把 197,376 当成一个工程门槛去用。 它的价值在于告诉你「这两个方案在同一个数量级上」—— 这才是「二选一」成立的前提。

⭐ dropless:静态形状是怎么丢的,又是怎么抢回来的

📄 这一节有一篇专门的附录 Dropless 与分组矩阵乘 → 站在一张卡上、用真实量级的数字(一个专家一步收 16,384 行) 把两种做法一步一步走完:为什么非要形状整齐、MegaBlocks 改了什么、 这个能力该叫 block-sparse 还是 grouped GEMM、 补齐 128 跟 PagedAttention 的 page 是什么关系、 以及 V3 那句「不丢 token」的证据链到哪儿为止。
后半篇往下钻了三层:MXU 是怎么被喂饱的(HBM → VMEM → VREG 那条流水)、 token 是怎么见到它的专家的(all-to-all / AG-RS / 干脆不用 EP, 以及为什么 TPU 反而选通信量最大的那条)、 还有「行数 × 8」在显存上到底要多少 —— 那一笔平时很少有人算。

回到 dense MLP 的第一条好性质:形状是静态的。 编译器在开跑之前就知道每个矩阵多大、每块内存放在哪,才能把整张图排好。

MoE 一来,这条就断了。 这一批里有多少个 token 落到第 37 号专家头上,只有把路由跑完才知道, 而且下一批就变了。 可硬件要的是「开跑前就定死」。

老办法很直接:给每个专家发一个固定大小的盒子, 形状就又静态了。代价是盒子装不下的要扔掉、没装满的要拿零去填。

① 老办法:给每个专家一个固定大小的盒子 形状静态了,代价是两头都在赔 容量上限 丢掉这 2 个 它们这一层的输出被置零,只剩残差穿过去 这 3 格是零 照样占算力和显存 专家 A · 来了 6 个 专家 B · 来了 4 个 专家 C · 来了 1 个 ② dropless:不要盒子,来多少算多少 把整层重写成 block-sparse 矩阵乘,形状不齐也能一次算完 没有 ✕,也没有斜线格 不丢 token,也不白算 6 个4 个1 个 容量系数就是「盒子开多大」的那个旋钮 —— 开小了丢 token,开大了白算。它是一个必须手调的超参数。 dropless 的价值在于:这个旋钮直接不存在了
左边这张图里,同一个 MoE 层同时在两头赔钱: 热门专家那边丢掉真数据,冷门专家那边拿零去凑算力。 而丢与不丢,取决于这一批数据碰巧怎么分 —— 这也是为什么 4b 那个负载均衡偏置 不只是「让卡别闲着」,它还在防止 token 凭空消失。
三个来自 MegaBlocks 论文的数字(arXiv 2211.15841) ① 丢 token 是真的伤质量。 同一组对照实验里,容量系数取 1(会丢)的 MoE 只把验证损失降了 0.15; 完全不丢的那版降了 0.26 —— 是前者的 1.73 倍
② 靠「把盒子开大」来不丢,很贵。 原文:为了不丢 token,MoE 层的数学运算量涨了两倍多
③ 而且你不知道该开多大。 论文引 Tutel 的观察:有的模型容量系数要开到 11 才不丢, 还有的模型这个值会在训练中途毫无征兆地飙上去
所以这不是「调个参数就行」的事 —— 它是一个你既不知道该设多少、设错了两个方向都赔的超参数。 dropless 把这个参数整个删掉了。
展开:这三个数是在什么设置下量的—— 用之前先知道它的适用范围

不是 671B,也不是 top-8。 那组实验跑的是一个 Transformer-Small 规模的 decoder-only 模型, 每个 FFN 换成 64 个专家、top-1 路由,在 The Pile 上训 100 亿 token,单张 A100。

维度MegaBlocks 那组实验DeepSeek V3
专家数64256 + 1 共享
每 token 选几个top-1top-8
模型规模Transformer-Small 量级671 B
均衡机制常规辅助损失动态偏置 + 极小辅助损失

差别最大的是 top-1 这一条。 top-1 意味着每个 token 只押一个专家,一旦这个专家满了就整个丢掉; top-8 天然摊得开一些,同样的失衡程度下丢得没那么惨。
所以这三个数字能证明的是「容量系数这个旋钮本身是个麻烦」, 不能直接换算成「V3 如果丢 token 会掉多少分」。 课上就按前者讲。

顺带记两个它的正面结果(同样来自摘要,同一套设置): 相比用 padding 硬凑的做法,端到端训练快 1.38× / 2.0× / 4.35×(三个规模); 相比同质量的稠密 Transformer 快 1.8×–2.4×。

DeepSeek V3 在这件事上是怎么说的 —— 训练和推理,两句话 技术报告里那一小段标题就叫 “No Token-Dropping”,两句话: 「由于负载均衡策略有效,DeepSeek-V3 在整个训练过程中都保持了良好的负载, 因此训练时不丢弃任何 token。此外,我们还实现了专门的部署策略来保证推理时的负载均衡, 所以 DeepSeek-V3 推理时也不丢 token。」
它给出的理由是「负载被掰得足够匀」 —— 这正好回接到 4b:那套偏置机制不只是让卡别闲着,它还在防止 token 凭空消失。
「推理时也不丢」靠的是另一套东西:冗余专家 训练时靠偏置把负载掰匀;上线之后没有训练循环了,靠的是搬专家。 报告写的是:把观测到的高负载专家复制一份额外部署 (prefill 阶段设 32 个冗余专家,每张卡除了原本的 8 个再多带 1 个), 热点是根据线上统计检测出来的,而且每隔一段时间(比如 10 分钟)重新调整一次
值得记的是这个动作的性质:它是在节点内部重排,明确避免增加跨节点 all-to-all 的开销。 又一次 —— 先认拓扑,再谈优化。
这里有一句不要讲 「V3 训练不丢 token」可以讲,「V3 用的是 dropless 算子」不要讲。 报告说明的是结果(不丢)和它归因的原因(负载均衡好)。 至于它的训练算子内部还留不留「容量」这个概念,我没有在公开资料里查到。
这两件事差别很大:「靠均衡好到没触发容量上限」和 「压根没有容量上限」,在系统实现上是两回事,在台上却很容易被说成同一句。)

另外两个名字顺带一提,不展开:DeepEP(专门优化这个 all-to-all 的通信库)、 EPLB(专家负载再平衡)。

4d · 算账:671B 是怎么来的

层级算式参数量占比
单个专家3 × 7168 × 204844.04 M
一层(256 路由 + 1 共享 + 门控)257 × 44.04 M + 1.84 M11.32 B
58 层 MoE58 × 11.32 B656.57 B97.8%
61 层 MLA61 × 187.11 M11.41 B1.7%
3 层 dense MLP3 × 396.36 M1.19 B0.2%
嵌入 + lm_head2 × 926.68 M1.85 B0.3%
总计671.03 B官方口径 671B ✓

⭐ 表里最扎眼的一行:整个注意力 = 一层 MoE

全部 61 层 MLA 加起来 11.41 B ↑ 61 片,一片是一层 其中随便哪一层 MoE 11.32 B ↑ 1 块,就是一层 两条一样长,差 0.8% —— 61 : 1
我们花了整整一步讲注意力,它占全模型参数的 1.7%。 而第 2 步那条曲线说:在 128K 长度上,它吃掉 81.8% 的算力。 参数量和计算量是两本账,连排序都不一样。
展开:顺带一个反直觉的小发现 —— MLA 里最大的矩阵不是压缩用的那几个—— 第 2 步那笔账的另一面
187.11 M 是怎么分的
q 降维 7168 × 1536 = 11.01 M q 升维 1536 × (128 × 192) = 37.75 M kv 降维 7168 × (512 + 64) = 4.13 M kv 升维 512 × (128 × 256) = 16.78 M // 输出投影:把 128 个头拼回残差流 o_proj (128 × 128) × 7168 = 117.44 M ← 占 62.8% // 合计 = 187.11 M(对上表格 ✓)
压了半天 KV 缓存,参数的大头其实在出口那一侧。 这不矛盾 —— MLA 压的是每个 token 要存多少(那是显存账), 不是这一层有多少参数(这是参数账)。又一次:不同的账,不同的答案。
全部参数 · 671.03 B · 显存要装下这些 MoE 656.57 B(97.8%)· MLA 11.41 B · dense 1.19 B · 进出口 1.85 B 一个 token 实际用到 · 36.62 B · 算力只花这些 37B 这 634 B 参数这一步没参与计算 —— 但它们一个字节都不能从显存里拿走 ⚠️ MoE 省的是算力(18.3× ),不省显存(0×)
展开:「37B」这个数,边界在哪—— 官方口径和逐项加出来的数对不上,差在哪
两个都能算出来的数,官方那个落在中间 按上面那张表逐项加,一个 token 真正参与计算的是 36.62 B (58 层各 9 个专家 + 全部 61 层 MLA + 3 层 dense + 出口那个大矩阵)。 如果把嵌入表也算成「激活」,是 37.55 B。
官方口径的 37B 落在这两个数中间 —— 具体怎么归的,报告里没有细说,我也没有查到。 所以这门课统一用 36.62 B,需要跟官方对齐时就说「约 37B」。 差的这不到 1 B 不影响任何结论:18 倍还是 18 倍。

把这一节压成一句话

那座山是显存要扛的,那条溪才是算力要付的。

671B 的参数像一整座山压在显存上,一块都搬不走; 而每个 token 真正淌过的,只有山脚下那条细得几乎看不见的溪流 —— 37B。

MoE 做的事,就是把山和溪分开。 在它之前,你想要多大的山,就得付多大的水。

一整座灵矿巨山,山脚下只淌出一线极细的青色灵气
这个反直觉必须点破 显存要装 671B,算力只花 37B。 很多人听到「激活 37B」就以为 MoE 模型跟 37B 的稠密模型一样好养 —— 完全不是。 你需要的显存跟 671B 稠密模型一模一样,省下来的只有算力。 MoE 换的是「同样算力下能装更多知识」,不是「同样显存下跑更大模型」。

4e · 稀疏是每个 token 的事,稠密是这一批的事

上面那句「算力只花 37B」,还得再拧一道。 因为它是每个 token 的账 —— 而机器一次不处理一个 token。

两句话都对,但它们说的是两件事 单个 token:只过 9 个专家,不是 257 个 —— 这是稀疏
一批 token:批量够大时,256 个专家全都会被点亮 —— 这是稠密
不是某个 token 激活了所有专家,是这一批 token 合起来激活了所有专家。
所以「MoE 其实也是全激活」这个直觉 —— 在「权重字节要不要读」这根轴上完全正确,在「做了多少次乘法」这根轴上完全错误。 671B 的权重一个字节都少不了地要过 HBM;而 FLOPs 确实只有 9/257。

把「稠密」变成一个能算的数

稠密到什么程度,不是形容词,是一个除法。每个专家分到多少行, 决定了它那次矩阵乘长什么样:

// 一个专家在这一步收到多少行 每专家行数 = 参与这一步的 token 总数 × 8 ÷ 256 = token 总数 ÷ 32 // 而「token 总数」由什么撑起来 训练 / prefill = batch × sequence length ← 两个都算数 decode = batch × 1 ← 序列长度这条腿没了
32 这个数,是这套配置的「稀释倍数」 256 个专家、每个 token 挑 8 个 —— 于是每个专家只看得到全部 token 的 1/32。 你喂进去多少 token,专家实际拿到的只有三十二分之一。 专家越多、top-k 越小,这个稀释越狠: 换成 384 选 8,稀释倍数就变成 48。

硬件那头的门槛:128 行

矩阵乘不是按行做的,是按块做的。TPU 上那个 grouped matmul kernel, bf16 情况下每块的高度是 128 行 —— 这是公开源码里写死的常数:

// JAX / MaxText 的 megablox Pallas kernel,_get_tile_sizes() bf16_bf16_tile_m = 128 // 注释原话:「128 is the largest tile_m value that is safe to use // for most scenarios」(低位宽会往上调)

于是「每专家 16,384 行」和「每专家 8 行」,对硬件是两个完全不同的世界 —— 前者是 128 个满块,后者连一个块都填不满。

场景这一步的 token 每专家行数 相当于几块状态
训练 · 64 卡 · 每卡 8,192 token524,288 16,384128 块 ✅ 喂饱
推理 prefill · 一个 8K chunk8,192 2562 块 ⚠️ 勉强
推理 decode · 256 个并发请求256 8填不满 1 块 ❌ 塌了
decode 那一行是 MoE 推理全部痛苦的来源 要让 decode 也拿到训练那样的 16,384 行,需要 16,384 × 32 = 524,288 个并发请求同时在解码。这不是工程能凑出来的数。
序列长度在 decode 阶段是彻底失效的 —— 每个请求每步只吐一个 token, 你有再长的上下文也不多贡献一行。唯一的杠杆只剩并发数,而它远远不够。
所以同一个模型,训练时算力受限,decode 时带宽受限 —— 换了一种物理约束。

回头看 4c:EP 换的不是通信,是段长

这条账还解释了一件 4c 里没讲透的事。把两种方案的每卡工作量摆出来 —— 会发现行数一模一样

// A · 用 EP:每卡 4 个专家,token 从 64 张卡 all-to-all 汇过来 每卡行数 = 16,384 × 4 = 65,536 分成 4 段,每段 16,384 行 // B · 不用 EP(EP=1 + FSDP):每卡拿全部 256 个专家,只算自己那 8,192 个 token 每卡行数 = 8,192 × 8 = 65,536 分成 256 段,每段 256 行 // 完全相同的 65,536 行 —— 差的只有「切成几段」
同一条 M 轴 · 65,536 行 = 512 个 128 行的块 A · 用 EP —— 4 段 3 条内部缝 → 最多多跑 3 个块 → +0.59% B · 不用 EP —— 256 段 255 条内部缝 → 最多多跑 255 个块 → 上界 +49.8% ⭐ 同样 65,536 行、同样的 FLOPs —— 贵的不是活,是缝 下面那条的缝为示意,实际 255 条
为什么「缝」要花钱:TPU 这条路不补零,它重复访问 段边界很少正好落在 128 的整数倍上。GPU 那条路的解法是补零把段撑到块边界; TPU 的 megablox 选了另一条 —— 让跨界的那个块被执行两次, 一次用左边专家的权重、一次用右边的,累加进同一个输出。
所以代价的形式变了:不是「每段补 < 127 行零」,是「总共多跑 < 段数 − 1 个块」。 源码注释直接给了上界。 机制细节见附录 §11 · TPU 那边不补齐
// 上界:tiles_m ≤ 实际执行块数 ≤ tiles_m + 段数 − 1 tiles_m = 65,536 ÷ 128 = 512 A · EP ≤ 512 + 3 = 515 块 → +0.59% B · FSDP ≤ 512 + 255 = 767 块 → +49.8% ← 上界
这个 49.8% 是上界,不是必然 —— 但 dropless 的前提就是它躲不掉 如果 256 段每段都正好 256 行,边界全落在 128 的倍数上,额外开销是 0可是「每段正好相等」恰恰是 dropless 放弃的那个假设 —— 段长本来就参差不齐,绝大多数边界都会错开。 所以实际值靠近上界,而不是靠近 0。
这一节要带走的那句话 EP 的第一价值不是省通信,是把段拼长。 它把散在 64 张卡上、属于同一个专家的 token 重新聚成一堆, 让每段从 256 行长回 16,384 行 —— 64 倍的段长
(注意:64 倍的是段长,不是效率。效率差是 0.59% 对上界 49.8%, 大约 1.5 倍 —— 段长的改善远大于效率的改善,因为块内浪费本来就有上界。)
而 FSDP 那条路真正致命的代价在别处:每卡要凑齐全部 256 个专家的权重, 一层就是 22.5 GB(256 × 44.04 M × 2 B),EP 那边只要 352 MB。
① 嵌入
② MLA
③ Dense
④ MoE
⑤ 层间
⑥ 出口
⑦ 合账
走到这里累计 权重 显存 算力
第 5 步

层间:残差流这条总线

前面讲的都是「一层里面」。这一步讲 61 层是怎么串起来的。

✗ 常见的错误图像:一根管道 数据从第 1 层流到第 2 层,再流到第 3 层 嵌入 第 1 层 第 2 层 第 61 层 每一层只看得见上一层的输出 第 30 层一旦坏了,后面 31 层全废 ✓ 实际的样子:一条总线 一个 7,168 维的向量一路贯穿,每层从它读、往它加 残差流 · d = 7,168 第 1 层 norm → MLA + 第 2 层 norm → MoE + 第 61 层 norm → MoE + 加回去 每一层读到的是「到此为止的全部」 某一层输出 0 → 只是少一份贡献,别人照跑
差别不在画法,在「加」这个字。 管道是替换 —— 这一层的输出取代上一层的;总线是累加 —— 这一层只往公共的那个向量上添一笔。所以「什么都不做」是免费的默认状态, 而这正是能把网络堆到 61 层的前提。
先解决一个卡壳:中间那条明明是主干,凭什么叫「残差」流 直觉是对的 —— 拐一圈加回来的那一小份才该叫残差,中间那条应该另有名字。 按最早的定义,这个直觉完全正确。 ResNet 那篇(He 等 2015)说的是「把每一层重新表述成学习相对于层输入的残差函数」—— residual 指的是支路,也就是 H(x) 减掉 x 的那个差量; 主干那条线,论文里叫的是 shortcut connection(捷径 / 恒等路径)。
「残差流」是另一个传统的词,来自可解释性那一派。它的原始定义是: 「这条流就是之前所有层输出的总和,加上最初的嵌入」
所以它不是「残差 = 主干」,而是「residual 们汇成的那条 stream」—— 一个被压掉的所有格。主干是残差的累加,不是残差本身: x = 嵌入 + 第 1 层的残差 + 第 2 层的残差 + …… + 第 61 层的残差。 这条流里装的全是残差,所以叫残差流 —— 命名其实是精确的,只是省了两个字。
中文这边还多吃了一记翻译的亏 「残差」看着像「残留、剩下的」,但 residual 在这里的意思是「相对恒等映射的差量」 —— 是增量,不是余料把它在心里读成「增量流」,这一整节立刻就顺了: 每一层往公共账本上记一笔增量,账本本身就是所有增量的和。
展开:两个出处的原话,以及主干其实还有另一个名字—— 「残差流」是被特意挑出来的,它当年的竞争对手叫「embedding」

① 残差 = 支路。He 等 2015,Deep Residual Learning for Image Recognition(arXiv 1512.03385)摘要原话:

"We explicitly reformulate the layers as learning residual functions with reference to the layer inputs, instead of learning unreferenced functions." // 正文里主干叫什么: "The formulation of F(x)+x can be realized by feedforward neural networks with shortcut connections."

② 残差流 = 残差的累加。Elhage 等 2021, A Mathematical Framework for Transformer Circuits

"each layer adds its results into what we call the residual stream." "The residual stream is simply the sum of the output of all the previous layers and the original embedding."
彩蛋:主干的另一个名字就是「embedding」,而他们特意没用它 同一篇里还有这么一句 —— 「In transformers, the residual stream vectors are often called the "embedding". We prefer the residual stream terminology, both because it emphasizes the residual nature ... and also because we believe the residual stream often dedicates subspaces to tokens other than the present token, breaking the intuitions the embedding terminology suggests.」
所以「主干该不该有自己的名字」这个直觉是对的,它本来就有一个 —— 叫 embedding。 是这一派刻意换掉了它:一来要强调「这里面装的是残差」, 二来因为这条流里常常有子空间是留给别的 token 的, 叫 embedding 会误导人以为它只描述当前这个 token。

顺带一句血统:同一篇还提到,「用一条残差流来搭模型」这个做法 可以追到 Schmidhuber 那一系的早期工作 —— highway network 和 LSTM, 后来在 ResNet 上取得了现代意义上的成功。

残差流是一条总线,不是一根管道

常见的误解是把网络想成流水线:数据从第 1 层流到第 2 层再到第 3 层。 实际上更像一条总线 —— 有一个宽度 7168 的向量一路贯穿全程, 每一层从它一份、算出点东西、再回去。

x = x + MLA(norm(x)) // 加回去,不是替换 x = x + MoE(norm(x)) // ↑ 以上两行 × 61

为什么必须是「加」。 两个理由。梯度上,加法让反向传播有一条直达的高速路, 不会在 61 层里衰减殆尽。语义上,它让「什么都不做」成为默认状态 —— 一层如果暂时没学到有用的东西,输出接近 0,加上去等于没动,不会破坏已有的信息。

RMSNorm 摆在哪 —— 一个字的位置,决定了要不要 warm-up

V3 用 pre-norm:先 norm 再进子层,子层输出直接加回主干。 另一种摆法是 post-norm:先加,再对加完的结果做 norm —— 那样 norm 就横在总线上,上图那条「直达的高速路」被切断了。

这不是审美问题,是有人证明过的 —— 而且证据的形式比「训不稳」这个说法具体得多。

展开:「post-norm 训不稳」这句话,可验证的版本长什么样—— 结论是「要不要 warm-up」,不是「能不能收敛」

出处是 Xiong 等 2020 年那篇 On Layer Normalization in the Transformer Architecture(arXiv 2002.04745)。 他们做的事是分析两种摆法在「初始化那一刻」的梯度,然后得到两条并列的结论:

post-norm初始化时靠近输出层的梯度很大。 论文的说法是:这解释了「why the learning rate warm-up stage is essential in training the Post-LN Transformer」 —— warm-up 不是调参技巧,是必需品
pre-norm梯度是良态的。论文自称「the first to show that the learning-rate warm-up stage can be removed for the Pre-LN Transformer」 —— 可以把 warm-up 整个删掉

注意这个结论的形状:它说的不是「post-norm 训不出来」,是「post-norm 多了一个必须调对的东西」。 一个必须调对、调不对就炸、而且随深度越来越难调的东西 —— 在几十上百层的规模上, 这跟「训不稳」在实践中是一回事。但转述的时候,说清楚是哪一回事更好。

还有一个常被忽略的连带好处:pre-norm 把 norm 挪出主干之后, 残差流上就是一路纯加法,反向的梯度可以原样直达第 1 层。 上面那张「总线 vs 管道」的图,画的其实就是这条通路 —— post-norm 会在每一段总线上插一个 norm,通路就不纯了。

残差流 · d = 7168 norm → MLA + norm → MoE + 第 1 层 第 2 层 ⋮ ×61 层 ⋮ 第 61 层 单层激活 1.75 GiB × 61 层全留着 = 106.75 GiB
一个立住问题、只给半个答案的数字 在每一层的边界上把残差流存一份。61 个边界存满,光这一项就是 106.75 GiB —— 已经超过单张卡,而这还没算层内部的任何中间量。 于是自然冒出一个问题:凭什么要留着?
半个答案就在 V3 论文里,而且它选择的是「不留」。 原文:「We recompute all RMSNorm operations and MLA up-projections during back-propagation, thereby eliminating the need to persistently store their output activations.」 存不下就别存,反向的时候拿算力换回来 —— 这是拿时间换空间,不是免费的。 另外半个答案(为什么反向非要这些中间量、这笔交换怎么定价)在 专题四这里要留下的是这个反射:看到一个「装不下」的激活数字,先问的不是「怎么加卡」,是「这份非存不可吗」。

题外一眼:mHC —— V4 把这条总线加宽了

这一整节讲的是「一条 7,168 维的总线」。那条总线本身也是可以改的设计 ——  DeepSeek 在 V4 上换成了 mHC(Manifold-Constrained Hyper-Connections)。 唯一一处越过 V3 的地方,放这儿是因为要先讲清总线才看得懂它在改什么。

V3 · 一条残差流 xℓ+1 = x + F(norm(x)) x 7,168 norm → F(MLA / MoE) + xℓ+1 7,168 V4 · mHC,nhc = 4 Xℓ+1 = BX + CF(AX) X · 4 × 7,168 B · 4×4 双随机 A · 1×4 norm → F 还是 7,168 维 C · 4×1 + Xℓ+1 ⭐ 变宽的是总线,不是车间 —— AX 仍然是 7,168 维,所以 MLA、MoE 内部一个字节都不用改 论文原话:「the expanded residual width does not influence the design of the inner layers」。 mHC 的贡献不是 B 这个矩阵,是把它约束在双随机矩阵上
加宽残差流这件事不是 DeepSeek 起的头 —— Hyper-Connections(Zhu 等,2025)先做的,把残差流从 d 拓宽成 n×d, 确实有收益,但它破坏了残差连接原本的恒等映射性质,堆深了就训不稳mHC 的那一下,是把上图里的 B 约束到双随机矩阵(Birkhoff 多胞形)上: 行和、列和都是 1,谱范数因此被压在 1 以内,信号既不放大也不消失。 所以 mHC 不是「更宽」,是「让更宽这件事变得能训」。

① 它到底改了什么

残差状态从 Rd 变成 R4×d但层的输入输出仍然是 d 维 —— 上图那句「还是 7,168 维」是重点。 宽度这条轴,被从 hidden size 里解耦出来了,成了一个新的可调旋钮。

② 「双随机」是干嘛的

行和列都加起来等于 1,意味着混合是一次凸组合 —— 特征均值守恒、范数受控。而且双随机矩阵乘起来还是双随机, 这条性质正是「堆 61 层也不炸」的依据。用 Sinkhorn-Knopp 迭代把矩阵投影上去,V4 迭 20 次。

③ ⚠️ 代价:不在你以为的地方

直觉会说「4 倍宽 → 激活显存 4 倍」,那是没读工程章节的算法。 V4 论文明说 mHC 同时抬高了激活显存和流水线级间通信, 于是他们上了 fused kernel、选择性重算、改 DualPipe 重叠 ——  最后把墙钟开销压到 6.7%

展开:这一段每个数字的出处,以及为什么 V4-Pro 的账能跟这一页直接对—— 61 层、7,168 维,跟 V3 一模一样

先说一个巧合,它让这一节的数字直接可比。 V4 有两个型号,V4-Pro 是 61 层、hidden 7,168 —— 跟 V3 逐字相同V4-Flash 是 43 层、hidden 4,096。两个型号的 nhc 都是 4,Sinkhorn-Knopp 都迭 20 次。 所以上面那条 1.75 GiB 的总线,在 V4-Pro 上就是同一条。

// 出处:DeepSeek-V4 技术报告(arXiv 2606.19348)§4.2.1 模型配置 V4-Pro layers = 61 d = 7168 n_hc = 4 s_max = 20 1.6 T 总 / 49 B 激活 V4-Flash layers = 43 d = 4096 n_hc = 4 s_max = 20 284 B 总 / 13 B 激活 // 出处:mHC 论文(arXiv 2512.24880)实验设置 HC 与 mHC 的 expansion rate n 均设为 4,主实验模型 27 B

那「4 倍宽」到底涨了多少显存?这门课不给这个数,因为给不出来。 naive 的算法是 1.75 GiB × 4 = 7.00 GiB 每层 —— 但那是假设四条流的每层激活全部原样留着。V4 论文对这一点的原话是: 「we recompute most hidden states between layers and all normalized layer inputs, while avoiding recomputation of compute-intensive operations」。 也就是说,层间的大部分 hidden state 根本不存,反向时重算。 存多少、重算多少,是一个工程档位,不是一个由架构决定的常数 —— 所以真实增量取决于他们怎么调这个档位,论文没有给出显存的绝对数。

论文唯一给了绝对数的代价是时间: 「Collectively, these optimizations constrain the wall-time overhead of mHC to only 6.7% of the overlapped 1F1B pipeline stage.」 注意这个 6.7% 的分母是重叠后的 1F1B 流水线级,不是整个训练时间 —— 引用这个数的时候要连分母一起引,否则就是断章取义。

⚠️ 这一条以前是这么讲的,现在改了 课件先前写的是「4 倍宽 → 1.75 GiB 变 7.00 GiB 每层」。 那句话孤立看没错,但它把一个被工程手段大幅抵消掉的上界,讲成了实际付出的代价。 真实增量取决于重算档位,论文没给绝对数;给了绝对数的是那个 6.7% 的墙钟开销。
① 嵌入
② MLA
③ Dense
④ MoE
⑤ 层间
⑥ 出口
⑦ 合账
走到这里累计 权重 显存 算力
一粒金色神识在洞口炸开成漫天飞散的符文
第 6 步 · 旅程的终点 出口:从向量回到文字 走了 61 层,最后这一下要把一个向量摊开成 129,280 个可能 —— 而摊开的那一瞬间,是全程最占地方的时刻。
第 6 步

出口:从向量回到文字

走完 61 层,手里是一个 7168 维的向量。要把它变回「下一个字是什么」。

反向嵌入

最后一次 RMSNorm,然后一个 7168 → 129,280 的矩阵, 给词表里每一个词打一个分。这跟第 1 步的嵌入矩阵是镜像关系 —— 而且一旦第 1 步也编译成 matmul(见那一节的展开),两边连 FLOP 都一样。

tie_word_embeddings = false —— 但另有一处是真共享的 很多模型让进出口共享同一份权重(省一份参数,而且「表示一个词」 和「预测一个词」直觉上是一回事)。V3 没有共享 —— 它认为读和写是两个不同的任务,各给一份权重。代价是多 926 M 参数。
容易混的是:V3 里确实有一处「共享 output head」,但共享的对象不是嵌入表,是 MTP 模块。 论文把最浅的几层(含嵌入)和最深的几层(含 output head)摆在同一个流水线 rank 上, 让 MTP 模块和主模型物理上共用这两份参数和梯度。 所以下面 MTP 那一段,不要再往总账里加一份 926 M。

MTP:为训练造的东西,被推理捡了便宜

num_nextn_predict_layers = 1 —— 深度 1,就是额外多预测一个 (下下一个)。不是两个 —— 这个数就是 depth。

为什么造它
(训练)
额外的监督信号。只预测下一个字,模型容易学成「顺嘴溜」; 逼它同时预测下下个字,等于要求它对更远的地方也有规划V3 论文把这条写成 MTP 的主要目的:「we utilize MTP to improve training」
推理时它可以
直接不要
「during inference, we can directly discard the MTP modules and the main model can function independently and normally」 —— 它是训练期的脚手架,拆掉不影响主模型。消融实验也正是这么做的, 所以对比双方的推理成本完全一样
但拆掉太可惜
(投机解码)
留着当草稿模型:先便宜地猜出下下个字, 主模型一次性验证两个位置。猜对白赚一个字,猜错也不亏。
V3 实测:第二个字的接受率 85%–90%,端到端吐字速度 1.8× TPS

投机解码为什么划算,得回到专题六那条结论: 吐字那一段是被带宽卡住的,不是被算力卡住的。 既然读一遍权重的钱已经付了,顺手多验证一个字几乎是免费的。 85%–90% 这个接受率之所以能这么高,是因为草稿不是另一个小模型 —— 它读的是主模型自己的隐状态。

残差流最后一个向量 7,168 lm_head · 926.68 M logits 张量 —— 出口这一下 31.56 GiB 128K token × 129,280 词 × 2 B 对照:整个 MLA 权重(61 层) 21.26 GiB —— 比不过出口这一个临时张量(1.48×
又一个容易漏掉的爆点 logits 张量 31.56 GiB —— 比中间任何一层都吓人,而且它是纯临时的。 所以真实实现里 logits 必须分块算、loss 要 fuse 进去, 绝不能整个物化出来。这跟 FlashAttention 是同一类手法: 不要把中间结果落地。

⭐ 三个让它更难堪的细节

角度为什么值得说
算力小得离谱 0.46% lm_head 只花 242.92 TFLOP不到整个前向 52.50 PFLOP千分之五全程算力最少的一步,产出了全程最大的张量
loss 还要 fp32 63.13 GiB cross-entropy 在 bf16 下数值不稳,logits 通常要提到 fp32 再算。 于是这个纯临时的张量吃掉单卡 72%
⚠️ 这一格曾经把单位讲反了,留在这里当反面教材。 旧版写的是:「63.13 GiB 对 94.74 GB,直接相除得 67% 是错的, 换算成 67.78 GB ÷ 94.74 = 72% 才对。」 实际上 94.74 本来就是 GiB(编译器报错串 95.38G − 94.74G = 656.93M 只有按 1024 才对得上),所以两个数本来就同进制, 63.13 ÷ 94.74 = 67% 才是对的,被「纠正」成的 72% 反而是错的
这个错误的形状值得记:它不是不知道有单位陷阱,恰恰是知道、然后往错的方向修了一次 —— 先认定分母是十进制,再把分子换过去。检查单位的时候,第一件事是确认分母到底是哪一进制, 而不是急着换算分子。
页面上的百分比一律按字节算,不按显示出来的那两个数算。
训练 vs 推理 252.5 KiB 训练每个位置都要算 loss,所以要全部; 推理 prefill 只要最后一个位置。 同一个算子,两边差 131,072
训练 · 每个位置都要算 loss 131,072 个位置,一个都不能少 31.56 GiB 131,072 × 129,280 × 2 B bf16 就这么大;提到 fp32 算 loss 还要翻倍 推理 prefill · 只要最后一个位置 前面 131,071 个位置的 logits,一个都不需要 (虚线是「如果照训练那样全算」的体积) ↑ 就这一条 · 252.5 KiB 这条线是骗你的 —— 真实比例只有虚线框的 1/131,072 高, 合 0.0013 像素。为了画得出来,它放大了约 1,000 倍 同一个算子、同一份 926.68 M 权重、同一行代码 —— 两边差 131,072 倍。 「训练贵」和「推理省」在这里说的是同一个 lm_head
这张图有一半是画不出来的,而画不出来本身就是结论。 lm_head 是全程算力最少的一步,却在训练侧产出全程最大的张量; 换到推理侧,同一步瞬间退化成一条几乎不占地方的细线。 所以「这个算子贵不贵」这个问题,脱离了「训练还是推理」根本没法回答 —— 这也是为什么专题六要单独讲推理,而不是当成前向的一个特例带过。

⭐ 三个几乎人人都卡在这儿的问题

① 训练的 logits 凭什么大十三万倍 —— 大的是「位置数」,不是词表 词表两边一模一样,都是 129,280。差的是「要为多少个位置各算一份」。
训练是 teacher forcing:整条序列一次喂进去, 每一个位置同时都是一个预测点 —— 第 5 个位置的向量预测第 6 个字, 第 6 个预测第 7 个,全部并行,一次前向算出 131,072 个 loss。一个都不能省。
推理只在最后一个位置采样,前面 131,071 个位置的 logits 一个都不需要。 131,072 : 1 —— 差的就是这个。
顺手一个特别好记的比例:logits 就是「18 层残差流叠在一起」
// 出口这一下,宽度突然从 7,168 变成 129,280 129,280 ÷ 7,168 = 18.04 // 于是 单层残差流 1.75 GiB × 18.04 = 31.56 GiB ← 正好是上面那个数
模型里所有别的东西都是「位置数 × 7,168」,只有出口这一步是「位置数 × 129,280」。 同样的位置数,18 倍的宽度 —— 这就是它显得那么突兀的全部原因。
② 那推理常用的 top-k 呢 —— 反正小的那些也选不上 观察是对的,但有个先后顺序:你必须先把 129,280 个分全算出来,才知道哪 64 个最大。 top-k 是算完之后的挑选,不是省掉计算的手段。
推理这边本来也不需要省 —— 一个位置的完整 logits 才 252.5 KiB
真正有压力的是训练侧,而训练侧不能用 top-k —— cross-entropy 的分母是全词表求和,少算一项分母就错了。 (历史上有 sampled softmax / hierarchical softmax 这类近似,代价是梯度有偏。)
现代做法是分块:一次只算一小段位置的 logits,算完 loss 立刻扔。 这是精确的,不是近似的 —— 省的是显存峰值,不是计算量。

换一个真实配置算一遍 —— 顺便看清为什么非分块不可

上面那个 31.56 GiB 用的是课程口径的 128K。换成一个更常见的训练形状 ——序列 4,096、每 device batch 12

// 这一步要算 logits 的位置数 4,096 × 12 = 49,152 // logits 张量本身 bf16 49,152 × 129,280 × 2 B = 11.84 GiB fp32 49,152 × 129,280 × 4 B = 23.67 GiB ← cross-entropy 数值不稳,通常要提到 fp32 // 对照:同样位置数的一层残差流 49,152 × 7,168 × 2 B = 0.66 GiB ← 又是那个 18 倍
放到一张卡上看:这一个纯临时的张量吃掉四分之一显存 以单 device 94.74 GiB 的 HBM 算 —— bf16 的 logits 占 12.5%,提到 fp32 算 loss 就是 25.0%而它在参数表上不存在,在算力账上只占 0.46%, 并且下一步就被丢掉。
分块为什么行得通:各个位置之间没有任何耦合 loss 是各位置独立求和的 —— 位置 i 的 logits 跟位置 j 一点关系都没有。 所以可以流式过一遍:算一小段的 logits → 立刻算完 loss → 扔掉 → 下一段。
切成 1,024 一段 → 峰值 1,024 × 129,280 × 2 B = 252 MiB // 11.84 GiB → 252 MiB,降到 1/48
而且省的是双份。反向要用的那个梯度张量跟 logits 一样大 (梯度就是 p − y,逐元素,形状完全相同)—— 融合之后它也不落地:在这一小段里当场算出来,当场收缩回 「1,024 × 7,168」的隐状态梯度。
代价几乎为零 —— lm_head 只占全程算力的 0.46%,需要时重算一遍根本不心疼。

「下一个字」到底是从哪来的 —— 一个几乎所有人都问过的问题

第 0 层的时候,那个向量确实就是当前这个 token 的嵌入。 那它凭什么走完 61 层,就变成了下一个 token? 这个「往后错一位」的规则,是写在 lm_head 的参数里吗?

不在。它在损失函数里 —— 架构里没有任何一处在做位移 lm_head 做的事朴素得可怜:它就是一张表,每个词一行, 每行是 7,168 维空间里的一个方向。打分就是做点积。
lm_head 的第 w 行 = 词 w 在 7,168 维空间里的一个方向 第 i 个位置给词 w 的分 = 顶层向量ᵢ · 第 w 行 // 就是个点积 ↑ 谁的方向最贴近,谁分高
所以 lm_head 里存的只是「每个词长什么样」。 它对「下一个」这三个字一无所知。
猜测发生在下面那 61 层 —— 注意力让第 i 个位置看得到前面所有位置, 于是走到顶上,那个向量已经不是「第 i 个字长什么样」, 而是「读完前 i 个字之后,接下来该是什么」
而「接下来」这三个字,是训练时把标签往后挪了一格挪出来的。
// 语料:今 天 天 气 不 错 位置 1 2 3 4 5 输入 今 天 天 气 不 ← 永远来自语料 ↓ ↓ ↓ ↓ ↓ 61 层 + lm_head,每个位置各打一份分 ↓ ↓ ↓ ↓ ↓ 标签 天 天 气 不 错 ← 同一行,往左错一格
最好的证据:MTP 把标签挪两格,什么代码都没改 上面讲的 MTP 模块,预测的是下下个字。 同一套结构、同一种打分方式 —— 唯一的区别是标签往后挪了两格。 如果「错一位」真的长在架构里,这件事不可能这么便宜。
③ 那训练时,中间每个 token 是取概率最高的那个来算 loss 吗 不是 —— 训练时既不采样,也不取 argmax。它根本不「选」一个 token。
它拿语料里真实的下一个字,看模型给这个字打了多少概率, loss 就是 −log(那个概率)。模型自己觉得哪个字最像,完全不影响 loss 怎么算。
更关键的是:模型自己的预测,一次都不会被喂回去。 第 i+1 个位置的输入是语料里真实的第 i+1 个字, 不管第 i 个位置猜成了什么 —— 这就是 teacher forcing 这个名字的由来: 老师在每一步都把你掰回正轨。
为什么非这么做不可不这么做会怎样
并行性 —— 131,072 个位置能在一次前向里全算完 要是把自己的预测喂回去,位置 i+1 就得等位置 i, 训练退化成串行,慢 131,072 倍
可微性 —— cross-entropy 对整个分布光滑 argmax 不可导,反向传播根本过不去
梯度覆盖全词表 —— 把真实那个词的分往上推, 把其余 129,279 个往下压 只看 argmax 的话,分母就不需要了 —— 这正好回答了上面 ② 为什么不能 top-k
代价:exposure bias 训练时模型看到的前缀永远是正确的,推理时看到的是自己刚吐出来的(可能是错的)。 这个落差有个名字叫 exposure bias。 历史上有 scheduled sampling 之类的办法去缓解; 现在的大模型主要靠后训练阶段(模型真的自己 rollout)来补这一课。

④ 那个 0 / 1 的标签,跟真实的概率分布差太远了吧

「今天天气」后面,「不错」「很好」「真好」都说得通。 凭什么标签说其中一个的概率是 1,另外两个是 0这个质疑完全成立 —— 单看一条样本,标签确实是错的。

关键一句:标签不是「对分布的断言」,是「从分布里抽的一个样本」 抛硬币。每一次抛出来都是 0 或 1,从来不会是 0.5。 但你抛一万次取平均,它就收敛到 0.5。
语料里「今天天气」这个前缀出现很多次 —— 有些次后面跟「不错」,有些次跟「很好」,有些次跟「真好」。 每一次各推各的,梯度平均下来就是经验频率。 单条标签是硬的,但它们的和是软的。
// 为什么这个目标在期望上是对的 最小化 E(x,y)~数据 [ −log pθ(y|x) ] ≡ 最小化 KL( P数据(·|x) ‖ pθ(·|x) ) + H( P数据(·|x) ) ↑ 常数,跟 θ 无关 // 所以最优解正好落在 pθ = P数据 ← 真实的条件分布,不是 one-hot
推论一:模型永远到不了 1,而且是梯度下降主动拦着它 假设模型真的在「今天天气」之后把「不错」输出成概率 1。 那么在同一前缀后面跟「很好」的那些样本上,它的 loss 是 −log(0) = ∞。
所以「压到 0 / 推到 1」这个动作,被同一个前缀的其他出现互相拽住了。 平衡点不在 one-hot,就在经验分布上。
推论二:训练 loss 的地板不是 0,是语言本身的熵 上面那个式子里,KL 项最好能降到 0,但 H(P数据) 那一项降不掉 —— 它跟参数无关。 所以 loss 曲线停在一个大于零的地方,不是训练失败, 是「下一个词本来就有多种合理答案」这件事的量化值。
反过来说:如果 loss 真的趋近 0,那说明模型在背语料,不是在学语言。

那如果我手上真有更好的软标签呢?有两条路,而且它们的存在本身 就承认了 one-hot 丢信息这件事:

办法做什么代价 / 现状
label smoothing 把 1 换成 1−ε,ε 摊到其余词上,人为把标签变软 Transformer 原论文用 ε=0.1。现代 LLM 预训练一般不用 —— 它损害的正是生成时要靠的那个概率校准
知识蒸馏 直接拿老师模型输出的完整分布当标签 —— 真正意义上的软标签 要先有一个更强的老师。蒸馏之所以数据效率高, 正是因为把 one-hot 丢掉的那部分补回来了
原论文自己就说了这笔交易换的是什么 「During training, we employed label smoothing of value εls = 0.1. This hurts perplexity, as the model learns to be more unsure, but improves accuracy and BLEU score.」 —— Vaswani 等 2017,Attention Is All You Need §5.4
「让模型变得更不确定」这句话,正好说反了你担心的那件事: one-hot 的问题不是把模型教得太糊涂,而是把它教得太自信。 label smoothing 是在往回拉。
一句话收尾 one-hot 每一条只告诉模型「这次是这个词」。 老师的软分布还额外告诉它「猫有点像狗,完全不像汽车」。
前者靠海量样本平均出后者 —— 慢,但不需要老师。 这就是预训练和蒸馏的分工。

⑤ 展开讲讲蒸馏 —— 它凭什么比 one-hot 快,以及快多少

上面说了 one-hot 在期望上是对的。它唯一的毛病是慢。 蒸馏做的事只有一件:把那个硬标签换成老师模型输出的完整分布。 就这一下,换来的东西超乎直觉 —— 下面的数字全部出自 Hinton / Vinyals / Dean 2015,Distilling the Knowledge in a Neural Network (arXiv:1503.02531)。

第一层:老师真正教的,是那些「小到没意义」的概率之间的比值 「one version of a 2 may be given a probability of 10−6 of being a 3 and 10−9 of being a 7 whereas for another version it may be the other way around. This is valuable information that defines a rich similarity structure over the data.」
百万分之一和十亿分之一都小到不值一提,但它们的比值有意义 —— 它说的是「这个 2 写得偏 3,不偏 7」。 这就是后来大家说的 dark knowledge:老师在答案之外, 还顺带交出了一张「谁像谁」的关系图。
one-hot 把这张图整个扔了。
// 同一个位置,两种标签下的梯度 one-hot ∂L/∂logiti = pi − yi y 是 0/1 → 那 129,279 个错词的目标全是 0, 而模型本来就给它们 ~0 ⇒ 这一步它们几乎没有梯度 软标签 ∂L/∂logiti = pi − qi q 是老师给的值 ⇒ 每个跟老师不一致的词都拿到推力 有的往上推、有的往下压,不是只有一个赢家
第二层:one-hot 是抽样,所以它抖 —— 老师的分布不抖 「When the soft targets have high entropy, they provide much more information per training case than hard targets and much less variance in the gradient between training cases, so the small model can often be trained on much less data than the original cumbersome model and using a much higher learning rate.」
回到上一节那个硬币比喻:one-hot 是每次抛一次硬币,同一个前缀 这次抽到「不错」、下次抽到「很好」,梯度方向来回晃,只能靠海量样本平均掉。 老师直接把那枚硬币的偏向报给你 —— 每次都一样,没有噪声要平均。
论文里那句「可以用高得多的学习率」,就是这么来的。
顺带算一笔信息账(这段是我按词表大小推的,不是论文原文)
one-hot 每个位置最多携带 log2(129,280) ≈ 17 bit // 「是这 12.9 万个词里的哪一个」 软分布 每个位置携带 整条 129,280 维 的形状 // 每一维都是一条约束
同样跑一次 forward、同样付那 31.56 GiB 的 logits,拿回来的监督信号完全不是一个量级。 算力一分没多花,信息多了几个数量级 —— 这就是「快」的来源。

那到底快多少倍 —— 三组实测

实验硬标签软标签(蒸馏)结论
语音识别 · 只给 3% 数据
论文 Table 5
测试帧准确率 44.5%
严重过拟合,只能早停
测试帧准确率 57.0%
没early stop,自己收敛
全量数据的基线是 58.9% ——
1/33 的数据,只差 1.9 个点
MNIST
大网 1200 单元 / 小网 800 单元
小网自己训 146 个错
大网 67 个错
小网学软标签(T=20)
74 个错
差距 146−67 = 79 个,
蒸馏补回 146−74 = 72 个
= 91% (这一步是我算的)
10 个模型的 ensemble
语音,帧分类准确率
把 ensemble 的收益蒸进单个模型 原文:「More than 80% of the improvement … is transferred」
最硬的那个数字:33 倍的数据效率 百分之三的数据 = 三十三分之一。 同样的模型、同样的 3% 数据,换标签这一件事把测试准确率从 44.5% 抬到 57.0%,逼近拿了三十三倍数据的那条基线(58.9%)。 注意训练准确率反而是硬标签那组更高(67.3% vs 65.4%)—— 它不是学不动,是在背那 3% 的数据。
所以蒸馏省的不是算力,是数据;换来的不是拟合,是泛化。
🪄 最反直觉的一个实验:让它学一个从没见过的类 把训练集里的数字 3 全部删掉 —— 对蒸馏出来的那个模型来说, 3 是一个传说中的数字,一次都没见过。
直接测 206 个错,其中 133 个错在那 1,010 个 3 上 // 错因是 3 这一类的 bias 学得太低(它从没被正例推高过) bias +3.5 109 个错,其中 只有 14 个错在 3 上 → 98.6% 的 3 认对了,而它从没见过一个 3
老师从来没有直接教过 3。但它在教 8、教 5 的时候, 每一次都顺带说了一句「这个有点像 3」——那张关系图里, 3 的位置是被周围所有类挤出来的。
⚠️ 温度必须调高,否则等于没蒸 老师自己也是拿 one-hot 训出来的,所以它在 T=1 时的输出同样接近 one-hot —— 那些 10−6 和 10−9 全被压在地板上,你什么都学不到。 升温就是把地板上的东西照亮。
// 高温极限下,蒸馏退化成直接对齐 logits(论文 Eq. 4,要求 logits 逐样本零均值) ∂C/∂zi ≈ (1 / N·T²) · (zi − vi) // 推论:梯度按 1/T² 衰减 ⇒ 软硬两个损失混用时,软的那项要乘回 T² 才不会被压没
有多要紧?论文里提到另一组人也做蒸馏,但用的是 T=1 —— 他们只补回了大小模型差距的 28%,而升温之后是 80% 以上同一个方法,温度选错,收益掉到三分之一。
但别忘了蒸馏的前提:要先有一个老师 软标签的信息不是凭空出现的,是老师当年用海量 one-hot 一点点平均出来的。 蒸馏做的是把那份已经平均好的结果直接交出去,省掉重走一遍的成本。
所以预训练仍然是唯一能「无中生有」的那一步 —— 它慢,但它不需要老师。 这也解释了为什么 V3 这一级的模型自己是拿 one-hot 硬训出来的, 而它的小尺寸版本可以蒸。
顺带:lm_head 里的 LM 是什么 LM = Language Model,语言模型。head(头)是骨干网络上面接的 任务专用输出层 —— 换个头就能干别的活:分类头、打分头、奖励模型头。 lm_head 就是干「预测下一个词」这份活的那个头。
这已经是第三次撞上同一件事 第 2 步是 KV cache,第 5 步是激活,现在是 logits。 三次的形状一模一样:一个在参数表上根本不存在的东西, 体积超过了参数本身。
所以这一步真正的收获不是 31.56 这个数,是那句 ——「算显存只算权重,一定会算错」。
① 嵌入
② MLA
③ Dense
④ MoE
⑤ 层间
⑥ 出口
⑦ 合账
走到这里累计 权重 显存 算力
第 7 步 ⭐

把账合起来

七步走完。现在回答三个问题。

部件权重 显存前向 FLOPs备注
嵌入926.68 M1.73 GiB 查表则为 0;编译成 matmul 则同 lm_head
MLA × 6111.41 B21.26 GiB 45.61 PFLOP81.8% 的平方项
KV cache8.58 GiB 随序列线性增长
Dense MLP × 31.19 B2.21 GiB 0.31 PFLOP
MoE × 58656.57 B1.19 TiB 6.03 PFLOP+ all-to-all 812.00 GiB
lm_head926.68 M1.73 GiB 242.92 TFLOP⚠️ logits 31.56 GiB
合计671.03 B 1.23 TiB 52.50 PFLOP激活参数仅 36.62 B
常驻显存的构成。每一小格是一个 TPU v7 device 的 94.74 GiB —— 数一数要几个。而这还只是前向、batch=1、不含激活和临时缓冲。

① 一次前向多少算力

52.50 PFLOP

其中 attention 平方项占 81.8%

② 常驻显存多少

1.23 TiB

权重 1.22 TiB + KV 8.58 GiB

③ 要多少个 device

14.3

对着 94.74 GiB 一除。而且这只是下限。

⚠️「14.3 张卡」是最乐观的可能值,不是答案 这个数建立在五个假设上,而每一个在真实训练里都不成立: 只算前向、batch = 1、不含激活、不含临时缓冲、还假设权重能完美打包。
把峰值激活 41.89 GiB 也加进去, 14.3 变成 14.7 —— 这一步还好。但换成训练就不是一个量级了: 每参数 16 字节(权重 + 梯度 + fp32 主权重 + 两个动量), 671 B 就是 9.77 TiB约 105 张卡 —— 比这里多将近八倍这笔账在专题四
所以正确的读法是:连最省的口径都要 14 张,而真实训练要八倍于此。

41.89 GiB 峰值激活,是怎么凑出来的

开场那一问只算了 KV cache,因为那笔账是确定的 —— 存什么、存多久,一步不含糊。 激活这笔账得等一个 token 真的走完全程才有得算,现在走完了,可以补上。

先把两个总被混着说的词分清楚 KV cache = 要留下来的。 生成第 n+1 个字时还要用前面 n 个字的 K 和 V, 寿命是整段对话跟层数相乘,随长度线性长大、中途不释放。
激活 = 算完就扔的。 第 3 层的中间结果进了第 4 层就没用了,寿命只有一层之内所以它不跟层数相乘 —— 只看最胖的那一层有多胖。 这就是为什么 61 层的模型,激活峰值只有几十 GB。
(训练时不一样:激活要留着做反向,于是反过来由激活主导显存。 这门课只算前向,所以能把它们干净地分开。)

激活不累加,取三个候选峰值里最大的那个:

峰值出现在哪一步里面有什么合计
MLA 那一层KV 8.58 GiB + Q/K 各 6.00 GiB + V/输出各 4.00 GiB + 残差流 ×2 32.08 GiB
MoE 那一层KV + dispatch/combine 缓冲 14.00 GiB + 残差流 ×2 40.08 GiB
出口那一下 ← 最大KV + logits 31.56 GiB + 残差流 41.89 GiB

最吓人的一项不在注意力,在出口 —— 那个跟 12.9 万词表相乘出来的 logits,比整段 KV cache 还大三倍多。 (这一栏是按「算全部位置的 logits」估的;纯生成时只需要最后一个位置,可以忽略。 激活这一侧跟实现关系很大,这里给的是量级,不是精确值。)

表里那个「×2」出现了三次,是三件不同的事 Q/K 各一份 —— 两个一样大的张量,都是 128 头 × 192。
V/输出各一份 —— 也是两个一样大的,都是 128 头 × 128。
残差流 ×2 就不一样了。 一层开头先把进来的 x 原样存一份, 因为这一层的最后一步是「把它加回去」,扔不得; 然后对 x 做一次 layer norm,norm 出来的是另一块内存。 于是从 norm 开始到加法做完,两份同时活着,各 1.75 GiB
MoE 那层是同一个结构再来一遍,所以也 ×2; 出口那一下没有「加回去」,所以只有一份。
顺带:那为什么预训练一般用 4K,不用 128K—— 省的不是 logits,是注意力

省的不是 logits。logits 只看 token 总数(=序列长度 × batch),跟序列本身多长无关 —— 128K × batch 14K × batch 32 都是 131,072 个 token, logits 两边都是 31.56 GiB,一模一样。

短序列真正省的是注意力:它的代价是 batch × 序列长度的平方。 同样这 131,072 个 token,128K 那种切法要付 4K 切法的 32 倍

V3 的配置文件里 original_max_position_embeddings 是 4096、 YaRN 的 factor 是 40,4096 × 40 = 163840 —— 它本体确实是按 4K 训的, 128K 是后面扩出来的。另外训练时 logits 通常还要转 fp32 算 cross entropy,再翻一倍, 所以现在普遍用分块的 cross entropy:算一段扔一段,不把整张表物化出来。

同一个模型,换一个口径,就换一个数量级。条长 = 要几个 TPU v7 device ① 前向 · batch=1 · 只算权重 14.3 ② 再加上峰值激活 41.89 GiB 14.7 ← 跟①几乎看不出区别 ③ 换成训练 · 每参数 16 字节 2 B 2 B 4 B 4 B 4 B 约 105 个 bf16 权重 梯度 fp32 主权重 动量 m 动量 v 671.03 B × 16 B = 9.77 TiB ÷ 94.74 GiB/device (两边都是二进制,可以直接除) ⭐ 这张图真正要说的不是「105」,是前两条几乎一样长、第三条长了八倍这个形状 橙紫红那三段(fp32 主权重 + 两个动量,共 12 B)全是优化器的 —— 模型本身只占最左边那 2 B。这三段怎么砍,就是专题四的全部内容
①②③ 的区别不在精度,在问的是什么问题 ① 问「权重放得下吗」,② 问「跑一次前向放得下吗」,③ 问「能不能训」。 三个问题的答案差了一个数量级,而它们常常被同一句「这个模型要几张卡」混在一起问。 下次听到这句话,先反问一句是哪个口径。
⚠️ 顺带把单位钉死:本页说的「个」「张」一律指 device,因为 94.74 GiB 是每 device 的容量。 TPU v7 一颗芯片里有两个 device —— 所以 105 个 device 换算成芯片是 53 颗, 两个数都对,但说的不是一回事。这个 1:2 在后面几门课里会反复咬人。

再换个感觉:这些算力是多长时间

PFLOP 是个没有体感的单位。除以硬件的算力,就变成秒了 —— 单个 TPU v7 device 的 bf16 峰值大约 1153 TFLOP/s —— 官方给的是每颗芯片 2307 BF16 TFLOPS,而一颗芯片里有两个 device,除以二得来。

理论极限(100% 利用率)

45.5 秒

一个 device 跑完这一次前向。现实里达不到。

现实一点(35% 利用率)

130 秒

大模型训练的实际利用率通常就在这个量级

摊到装得下它的那些卡上

8.7 秒

15 个 device(14.3 向上取整,卡不能切一半)、35% 利用率、且假设并行完美无损 —— 最后那半句是这门课剩下的全部内容。

「假设并行完美无损」这七个字,是整门课的入口 把活分给 14 张卡,绝不等于快 14 倍 —— 它们要互相传数据、要等最慢的那个、 要为了省显存重算一部分。那道差距有多大、从哪来、怎么缩小, 就是专题五和专题七要回答的。
到这里,「为什么需要并行」不用讲了 —— 它是算出来的结论。 怎么并行、沿哪一维切、通信怎么藏,是专题五的事。 这一课的任务只是把「装不下」这个事实出来,而不是出来。
第 7.5 步

算出来的数,跟机器上量出来的数

前面七步都是推导。这一节只说一件事:这些推导能不能信?

能验证,而且不用占一张卡 编译器本身就是一个静态分析器:把模型和并行配置喂给它提前编译一次 (这条路子一般叫 AOT),它会在不申请任何加速卡的前提下报出这份配置要多少显存 —— 排队等卡之前就能拿到答案。对着上面那张表交叉验证,这是最省钱的一步。
下面三条不是 AOT 的使用说明 —— 这门课不讲怎么跑它。 下面三条讲的是:一个「不用跑就能告诉你答案」的工具,该信到什么程度。 这个问题跟工具无关,你换成任何一个性能模型、任何一份容量估算表,结论都一样。

但真跑过之后会知道三件事。这三条是我们自己踩出来的,不是书上的。

① 编译过了 ≠ 装得下

编译成功只说明这张图合法。我们见过编译一路绿灯、 但显存需求远超单卡上限的配置。

更麻烦的是报错会误导人 —— 真正的编译失败常常被报成「装不下」,于是人去调小 batch, 方向完全错了。这类「错误的错误信息」比没有信息更危险。

② 报出来的数不是全部

它只是临时缓冲,参数和优化器状态是另外一笔 (见专题四)。

而且它不随并行度缩小 —— 切得再宽,临时缓冲该多大还多大。这一点最容易算错。

③ ⭐ 贴着上限那一带会失灵

远离上限时它非常可靠。但逼近上限的那个窄带里,它判不准 ——  我们判错过,而且错的都落在这一带。

不要把静态分析当神谕。 它帮你排除掉大部分明显不行的配置,不替你做最后那个决定。

横轴 = 这份配置要的显存 ÷ 单卡上限。颜色 = 静态分析在这一段可不可信 单卡上限 远离上限 —— 判得准 明显超了 —— 也判得准 ⚠️ 判不准 0 0.5× 1.0× 1.3× 而真正值得一试的配置,几乎全挤在这一带 —— 谁会花时间去试一个明显装不下、或者明显浪费的配置? ⭐ 所以:静态分析最不可靠的那一段,恰好就是你最需要它的那一段。 这不是工具的 bug,是它的性质 —— 换个工具也一样
红带的宽度是示意的,位置不是。 它确实贴着上限、确实很窄,但「窄到几个百分点」这种话我们给不出可复现的数 —— 那取决于模型、实现和编译器版本。 能负责任说的只有形状:两侧可信,中间不可信,而中间正是决策发生的地方。
顺带一个反直觉 同一个模型、同样的并行配置,只是换了某个算子的实现,显存需求就会变 —— 而且方向不固定,有的变大有的变小。
显存不是模型的属性,是「模型 × 实现 × 编译器」三者的属性。 这一条会在专题七里反复出现。

这一课带走这几条

数字会忘,这几个反直觉的结论要记住 —— 后面每一课都会用到。

01 · 关于 MoE

它省的是算力,不省显存。

听到「激活只有 37B」就以为跟 37B 的稠密模型一样好养 —— 完全不是。 显存要装的还是 671B,一个字节都少不了。

02 · 关于长上下文

序列一长,瓶颈就从「参数多少」换成「序列多长」。

4K 时 attention 只占一成算力,128K 时占八成,1M 时占九成七。 同一个模型,换个输入长度,优化的方向完全不同。

03 · 关于「爆炸」

最吓人的往往不是权重,是临时张量。

注意力分数矩阵、出口的 logits —— 它们连参数都不是, 用完就扔,但峰值比任何一层权重都大。算显存只算权重,一定会算错。

04 · 关于结构设计

模型结构里有些数字,跟质量无关,是硬件逼出来的。

那个「先选 4 个组、再组内选专家」的规则,唯一的目的就是别让通信跨太多台机器。 看模型结构时要能分出:哪些是为了聪明,哪些是为了跑得动。

05 · 关于「专家」

目前唯一被公开测过的模型里,它不按领域分工。

Mixtral 的作者试着按学科去找规律,结论是「没看出明显模式」; 反倒是同一个 token 连着两次选中同一个专家的比例,中层能到 26.4%, 远高于随机的 12.5%。看起来更贴语法和 token 本身,不是学科。
⚠️ 但那是 8 选 2 的 Mixtral,V3 是 256 选 8 —— 不能直接搬。 能确定的只有一条:每个 token 在 58 层里走的,是一条只属于它自己的路径。

06 · 关于算账这件事本身

显存不是模型的属性,是「模型 × 实现 × 编译器」三者的属性。

同样的配置,换个算子实现,显存需求就变,而且方向不固定。 静态分析能帮你排除掉大部分明显不行的方案,但不替你做最后那个决定。

下一课
专题二 · TPU 与 GPU
这一整课,我们反复拿一个数当尺子量东西:94.74 GiB
装不下、要 14 张卡、logits 占了 0.33 张 —— 全都是拿它除出来的。

但那张卡到底长什么样?为什么偏偏是这个数?
为什么有的卡叫 GPU、有的叫 TPU,它们看同一个模型的眼光有什么不同?

第一课算出了「要什么」。第二课讲「机器能给什么」。
(专题二的网页版已经写完了,四十张图;旁边那个链接是它的大纲)
去上专题二 → 看它的大纲 回课程总纲

这个专题明确不讲

写清楚是为了让边界清楚,不是遗漏。

反向传播与优化器状态

前向的账是事实,反向的账掺着策略,混讲会讲不清 → 专题四

并行策略

本专题只负责把「装不下」这个事实算出来 → 专题五

FP8 训练

V3 的一大贡献,但那是精度与数值的话题 → 专题八

推理

prefill / decode 的差异、连续批处理 —— 全程按训练的前向算 → 专题六