第十六周是大模型结构演进——从 2017 年原版 Transformer 讲到 2026 年的四大旗舰(DeepSeek-V4、GLM-5.2、Kimi-K3、Qwen3.6)。
作业是挑两个开源模型做结构解读。我选了 MiniMax-M2.7 和 小米 MiMo-V2.5,方法是直接读官方发布的 config.json 和 modeling_*.py——网上关于新模型的介绍大多滞后或含糊,结论以源码为准。
一、演进地图:四大件七年后各自变成了什么
2017 年的标准答案是四大件:MHA 注意力 + Dense FFN + LayerNorm/残差 + 绝对位置编码。这套结构统治了 2017~2022。
七年后:
MHA 注意力 → MLA / 稀疏选择 / 线性注意力 / 压缩注意力
Dense FFN → MoE:数百至近千个专家,每 token 只激活几个
LayerNorm+残差 → RMSNorm / QK-Norm / 超连接 mHC / AttnRes
绝对位置编码 → RoPE → partial RoPE / NoPE
共同驱动力:模型更大(T 级参数)、上下文更长(1M token)、成本更低(FP4/FP8)。
为什么必须改
注意力是长上下文的天敌:
计算量随长度平方增长——128K 上下文的注意力开销是 4K 的 1024 倍。KV cache 随长度线性膨胀,1M 上下文轻松占几十 GB 显存。
FFN 的容量与成本绑死:
FFN 占模型参数约 2/3,想扩容量就得按比例增加每 token 的计算量。知识需要容量,推理需要便宜,dense 架构下两者不可兼得。
MoE 的答案就是把两者解耦。2026 旗舰的激活参数只占总参数 3%~9%——Kimi-K3 总参数 2.8T,每 token 只算 104B。
二、注意力的瘦身之路:三条路线
第一步 少存 KV:MHA → MQA → GQA → MLA
MHA(基线) 每个头各存一份 K/V
MQA(2019) 所有头共享 1 份,cache 暴降但质量略损
GQA(2023) 折中,每 4~8 头共享 1 份
MLA(2024) 不存 K/V,改存 576 维压缩 latent,用的时候再重建
MLA 的关键转折:从「减少 KV 的头数」变成「把 KV 本身压成低秩向量」——压缩的对象变了。
而且它不是近似:重建矩阵可以吸收进 Q/O 投影,数学上等价于一种参数化的低秩注意力。
第二步 少看:DSA 稀疏注意力
思想是长上下文中 90% 的注意力是冗余的——先用一个极便宜的索引器给所有历史 token 打分,只选 top-2048 个进入真正的注意力计算。
主注意力从 O(L²) 降为 O(L·k),k=2048 固定。
GLM-5.2 的 IndexShare 又发现了下一个瓶颈:索引器本身也是 O(L²),200K 上下文时它占 prefill 延迟的 81%。关键观察是相邻层选出的 top-k token 有 70%~100% 重叠,所以 4 层共享一次索引。
第三步 换范式:线性注意力 + 混合
维护固定大小的记忆状态,每来一个新 token「先遗忘一点,再写入一点」,计算量 O(L)。
代价是记忆是有损压缩——精确检索(「第 317 页那句话」)弱于全量注意力。
共同答案是混合:Kimi-K3 与 Qwen3.6 都选 3:1。GLM-5 的消融数据最诚实:
naive 滑窗交替在 128K 检索任务上暴跌 30 分——全量层不是装饰,是必需品。
三条路线的对比
| 稀疏选择 DSA | 线性混合 | 压缩 CSA/HCA | |
|---|---|---|---|
| 一句话 | 选 top-k 重要的看 | 固定状态递推 + 全量层兜底 | 相邻 token 池化成摘要 |
| 长文本精确检索 | 好(选中即原文) | 靠 1/4 全量层补救 | 中等(远端只有摘要) |
| 训练成本 | 可后期改造(20B tokens) | 需原生预训练 | 需原生预训练 |
没有银弹:三条路线都在「保精确检索」与「省成本」之间找各自的平衡点。
三、几个我觉得最有意思的机制
无辅助损失的负载均衡
MoE 路由不均时,少数专家被挤爆、多数闲置。老办法是加负载均衡辅助损失,但它与主损失打架。
DS-V3 的巧思:给每个专家加一个偏置 bias,它只影响「选谁」,不影响「权重多少」。
类比:挂号时给冷门科室开「优先通道」,但诊断水平不受影响。
Muon 优化器,以及它对架构的反作用
Muon 相对 AdamW 的三处改动:砍掉二阶动量、把更新矩阵正交化(奇异值全部拉向 1)、只对 2D 权重用。
Adam 是逐坐标「除 RMS」,Muon 是对整个更新矩阵「白化」——强弱方向不再挤占同一个学习率。
但真正让我记住这一节的是它的反作用:
GLM 报告的坑:MLA 配 Muon 会输给 GQA-8,解法是把上投影矩阵按头拆开分别正交化。
所以「MLA 打不过 GQA」这个结论,在 AdamW 和 Muon 下答案是不同的。
架构、优化器、归一化是一个耦合系统——评价任何一个改动都要说明「在什么训练配置下」。
有界激活是量化的前提
SwiGLU 的两个因子都无界,激活值会出现极端离群点。FP8/FP4 下,少数离群点会吃掉整个动态范围。
低精度训练要求激活「没有惊喜」——有界激活是量化的前提条件。
四、作业:先撞到一个坑
★ 读源码前先撞到一个方法论上的坑,值得先记:config 里有某个字段,不代表这个字段在起作用。
MiniMax-M2.7 的 config 里有个 attn_type_list,名字看着像逐层指定注意力类型,很容易当成「混合注意力」的证据。但:
- 62 项取值全是 1,没有任何区分
- 在
modeling_minimax_m2.py里grep attn_type一次都搜不到
判断一个特性到底有没有启用,得同时看取值分布和代码里有没有引用,光看字段名会看错。
五、结构总览
MiniMax-M2.7 MiMo-V2.5
层数 62 48
hidden_size 3072 4096
稠密 FFN 中间层 1536 16384
head_dim (Q/K) 128 192
v_head_dim 同 head_dim 128
★逐层混合注意力 否(全为 1) 是(9 层全注意力 + 39 层滑窗)
滑动窗口 无 128
★partial RoPE 是 64/128 是 64/192
rope_theta 5,000,000 10,000,000
上下文 204,800 1,048,576
专家数 / 激活 256 / 8 256 / 8
★共享专家 无(显式设 0) 无
MTP 模块 3 无
多模态 无 vision、audio
两者的取向差得很明显:MiMo 在注意力上做文章,MiniMax 不做。
六、MiniMax-M2.7:深而窄,容量全靠专家堆
62 层 × hidden 3072
intermediate_size 1536 ← 稠密 FFN 的中间层比 hidden 还窄
62 层全部是 MoE 层,没有 dense 层
intermediate_size 1536 值得停一下:常见的 Transformer 里 FFN 中间层是 hidden 的 2.7~4 倍,这里只有 hidden 的一半。 稠密路径被压得很薄,计算量都挪进了稀疏专家。
Partial RoPE:一半的通道不带位置信息
head_dim=128 但 rotary_dim=64:
q_rot, q_pass = q[..., :64], q[..., 64:] # 切成前 64 / 后 64
q_embed = (q_rot * cos) + (rotate_half(q_rot) * sin) # 只有前 64 维参与旋转
q_embed = torch.cat([q_embed, q_pass], dim=-1) # 后 64 维原样拼回后 64 维完全没有位置信息。 等于把每个注意力头的通道分成两半:一半负责「谁在哪个位置」,另一半负责「谁是什么内容」,两种匹配互不干扰。
为什么说得通:全部维度都上 RoPE,等于强迫内容相似度也随距离摆动。
QK-Norm:代码注释自己标了这是主要差异
if self.use_qk_norm: # main diff from Llama
query_states = self.q_norm(query_states)
key_states = self.k_norm(key_states)注意力分数进 softmax 前会乘 1/√d,但如果 Q、K 本身的模长在训练中漂移,缩放因子救不回来,softmax 会饱和到接近 one-hot(梯度消失)。QK-Norm 从源头把模长钉住。
MoE 路由:一个只管「选」不管「配」的 bias
routing_weights = torch.sigmoid(router_logits.float())
scores_for_choice = routing_weights + self.e_score_correction_bias # 加 bias 的分数
_, top_k_index = torch.topk(scores_for_choice, self.top_k, ...)
top_k_weights = routing_weights.gather(1, top_k_index) # 权重取**原始** sigmoid
top_k_weights /= top_k_weights.sum(dim=-1, keepdim=True) # 选完之后才归一化四行代码里有三个设计决定:
① sigmoid 而不是 softmax —— softmax 会让 256 个专家互相抢概率;sigmoid 让每个专家独立给出「我适不适合处理这个 token」的分数。
② bias 只影响选谁,不影响权重 —— bias 加在 scores_for_choice 上参与 topk,但真正的加权系数是从未加 bias 的 routing_weights 里 gather 的。
负载均衡和输出质量被解耦了。 这正是课上讲的 aux-loss-free 方案——代码里它是
register_buffer而不是Parameter,印证了「不进梯度」。
③ 归一化在 top-k 之后 —— 是 8 个入选专家内部归一到和为 1,所以每个 token 拿到的仍是完整的一份输出。
没有共享专家
"shared_intermediate_size": 0代码支持这个机制但主动关掉了,是个选择而不是没实现。 意味着它认为 256 个路由专家里自然会涌现出「通用型」专家。
七、MiMo-V2.5:用层级上的取舍换长上下文
逐层混合注意力
hybrid_layer_pattern: 011110111110111110111110111110111110111110111110
全注意力层:[0, 5, 11, 17, 23, 29, 35, 41, 47] 共 9 层
滑窗层: 其余 39 层,窗口 128
排布是第 0 层全注意力,之后每 6 层插一个,最后一层也是全注意力——首尾都留全注意力,中间固定间隔穿插。
这是它敢上 1M 上下文的直接原因:
KV cache 从「48 层 × 1M」降到「9 层 × 1M + 39 层 × 128」
└─ 这一项基本可以忽略
相当于只有 9 层在为长上下文付显存。
两种注意力用两套超参,但只有两项真的分岔
config 里 SWA 层的参数全部单独配了一遍(swa_ 前缀),但摆在一起会发现5 项里只有 2 项取值不同:
swa_num_key_value_heads 8 ← 全注意力层: 4 ★不同
swa_rope_theta 10000 ← 全注意力层: 10000000 ★不同
(head_dim / num_heads / v_head_dim 三项完全相同)
这两项分岔的方向都反直觉但说得通:
KV 头数:全注意力 4 个,滑窗 8 个(滑窗反而更多)。
KV cache 开销 = 层数 × 序列长度 × KV 头数。全注意力层序列长度是 1M,是显存大头,所以把头数压到 4;滑窗层序列长度只有 128,cache 本来就微不足道,多给几个头换表达力很划算。
约束在哪,就在哪省。
RoPE theta:全注意力 1e7,滑窗 1e4,差 1000 倍。
theta 越大能区分的位置跨度越大,但相邻位置的角度差越小(分辨率越低)。全注意力层要区分百万量级的位置差,必须用大 theta;滑窗层视野只有 128,用 1e7 的话 128 个位置挤在一个极窄的角度范围里,几乎分辨不出先后——所以给它 1e4,把分辨率还给近距离。
★ 这是我认为整个调研里最讲究的设计:「混合注意力」不是把两种层交替摆就完事,两种层面对的问题不同,该分开调的超参就得分开调。
而且光看 config 有没有 swa_* 字段还不够,得把取值和对应项比一遍,才知道哪些是真分岔、哪些只是复制了一份。
Attention Sink:只装在滑窗层
"add_full_attention_sink_bias": false,
"add_swa_attention_sink_bias": true实现是在 softmax 之前给注意力分数拼上一列可学习的标量(每个头一个),算完 softmax 再把这列丢掉。
为什么只有滑窗层需要:
softmax 的输出必然和为 1——注意力权重是必须分配出去的,没有「不分配」这个选项。滑窗层每次只能看到 128 个 token,如果这 128 个里根本没有相关的,模型也只能把权重硬摊给它们,等于被迫读入噪声。
拼上的这一列相当于一个「哪儿都不是」的去处。softmax 之后这列被丢弃,实际效果是其余 128 个位置的权重之和小于 1——获得了「这一层我不太需要看什么」的表达能力。
全注意力层视野完整,总能找到该看的位置,不缺这个出口,所以关掉。
Q/K 和 V 的维度不对称
head_dim: 192 ← Q 和 K 是 192 维
v_head_dim: 128 ← V 只有 128 维
这两个维度承担的是不同的工作,本来就没理由相等:
- Q/K 的维度决定注意力分数算得准不准——维度越高,能表达的相似模式越细
- V 的维度决定每个位置能携带多少信息,而且直接决定 KV cache 里 V 那一半的大小
所以把 Q/K 抬到 192 换精度、把 V 压到 128 省显存,各按各的需求定。
多模态:ViT 内部也是混合注意力
MiMoVisionTransformer 是 28 层 ViT,fullatt_block_indexes: [0, 9, 18, 27]——只有这 4 层是全注意力,其余是窗口注意力。
和主干 LLM 用的是同一个思路。
八、两个模型的小结
MiniMax-M2.7 的效率来源不在注意力(标准 GQA,没有滑窗、没有稀疏化,上下文也只要 204K),而在模型形状和稀疏度:把宽度压到 3072、稠密 FFN 压到 1536,用 62 层深度和 256 专家换容量。
MiMo-V2.5 的核心是用层级上的取舍换长上下文:81% 的层只看 128 个 token,只有 9 层承担全局视野。围绕这个主结构配了三处适配——滑窗层单独调 KV 头数和 RoPE theta、给滑窗层加 attention sink、Q/K 与 V 维度分开定。
这几处放在一起看,能看出「混合注意力」不是一个开关,是一整套配套改动。
这周的收获
1. 直接读源码,比读二手介绍靠谱得多。
attn_type_list 那个坑就是例子——光看 config 的字段名会得出完全错误的结论。而 # main diff from Llama 这种作者自己写的注释,是任何二手文章都不会有的信息。
2. 所有的效率优化都是「在约束处省」。
MiMo 给全注意力层 4 个 KV 头、给滑窗层 8 个,看起来反直觉,但把「层数 × 序列长度 × KV 头数」这个乘式写出来就明白了。优化的前提是先算清楚成本是怎么构成的。
3. 评价一个架构改动,必须带上训练配置。
「MLA 打不过 GQA」在 AdamW 和 Muon 下答案不同。这条让我对所有「A 比 B 好」的说法都多了一层警惕——得先问在什么条件下。
4. 有些设计的道理,是从「不这么做会怎样」看出来的。
attention sink 那一节最典型:softmax 强制归一化 → 滑窗层视野受限时被迫读入噪声 → 所以需要一个「倒进下水道」的出口。理解一个机制,就是理解它在解决哪个具体的难受。
下周是强化学习与分布式训练。