第1章 V4 之路:从 V2 / V3 / V3.2 到 V4 的架构演进
“现代神经网络架构的每一次跃迁,本质上都是一场对’空间 / 时间 / 精度’三角的再平衡。” —— Tri Dao
你看到的 V4,不是一次发布。是 28 个月。
1.1 引子:为什么必须先讲 V2 / V3 / V3.2
互联网上关于 V4 的解读,大多数从 2026-04-24 那次发布讲起——这种讲法读起来很爽,但会错过 V4 真正的灵魂。
DeepSeek 在 V4 上做的几乎每一处架构改动,都是对前三代某个已知工程伤疤的回应:
- V4 把残差换成 Hyper-Connections——是因为 V3 训练曲线在第 5T token 附近出现过梯度异常,团队在内部补丁里第一次尝试 HC,那时候叫”残差混合”
- V4 把 routed experts 从 256 加到 384——是因为 V3 在 SFT 阶段持续观测到”专家利用率分布的长尾”,把宽度加到 384 是缓解长尾的工程妥协
- V4 引入 Indexer 稀疏注意力,并把 KV 改造成 “Compressor + 滑窗”——是因为 V3.2-Exp 验证了 DSA(DeepSeek Sparse Attention)路线在 128K 以上 context 的可行性
- V4 把 expert 权重压到 FP4——是因为 V3 在 FP8 训练里已经把 ue8m0 scale 玩到极致,再要省显存只能向下走一步
把 V2 / V3 / V3.2 看作 V4 的三段铺垫,再去读 inference/model.py,每一行代码背后那个”为什么这样写”的理由就会立刻浮出水面。
timeline title DeepSeek MoE 谱系(2024-2026) 2024-05 : V2 (236B / 21B)<br/>引入 MLA + DeepSeekMoE 2024-12 : V3 (671B / 37B)<br/>把 MoE 推到 256 专家 + FP8 训练 2025-09 : V3.2-Exp<br/>实验性 Sparse Attention (DSA) 2026-04 : V4 Pro (1.6T / 49B) + V4 Flash (284B / 13B)<br/>HC + Compressor + Indexer + FP4 experts
1.2 V2:MLA 与 DeepSeekMoE 的奠基
V2 在 2024 年 5 月发布,236B 总参 / 21B 激活。它在论文 DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model 中第一次亮出了 DeepSeek 的两件兵器:
1.2.1 MLA(Multi-head Latent Attention)
传统 Multi-Head Attention 的 KV cache 大小是:
其中 是层数、 是 head 数、 是 head 维度、 是序列长度。70B 模型在 32K context 下,KV cache 大约要 18 GB——这是当年部署的主要瓶颈。
GQA(Grouped Query Attention)把 KV 头数减少,但仍然在原始 head_dim 上存 KV。MLA 的思路完全不同——把 KV 压到一个共享的 低秩潜在空间:
flowchart LR
subgraph 传统MHA["传统 MHA / GQA"]
direction TB
X1["输入 x"] --> WQ["W_Q"] --> Q1["Q (n_h × d_h)"]
X1 --> WK["W_K"] --> K1["K (n_kv × d_h)"]
X1 --> WV["W_V"] --> V1["V (n_kv × d_h)"]
K1 --> Cache1["KV Cache (n_kv × d_h × 2)"]
end
subgraph MLA["MLA (V2/V3)"]
direction TB
X2["输入 x"] --> Wkv_a["W_kv_a (low rank)"] --> KV_lat["KV latent (kv_lora_rank=512)"]
KV_lat --> Cache2["KV Cache (kv_lora_rank)<br/>显存 ↓ 93%"]
KV_lat --> Wk_b["W_k_b"] --> K2["K"]
KV_lat --> Wv_b["W_v_b"] --> V2["V"]
end
V2 / V3 的 KV cache 在 1M context 下只要原始 MHA 的约 7%,这是当年 DeepSeek 能首次在国产卡上跑通 128K 长文档推理的关键。
V4 进一步把这条路变了——完全不存 latent KV,转而用 head_dim=512 的”压缩 KV + 滑窗 + Indexer 选取”。这是第 3、4 章会详细拆的剧情。
1.2.2 DeepSeekMoE:细粒度专家 + 共享专家
V2 之前的 MoE(如 Mixtral、GShard)的专家数量在 8-64 之间。DeepSeekMoE 论文 DeepSeekMoE: Towards Ultimate Expert Specialization in Mixture-of-Experts Language Models 提出两个改造:
- 细粒度专家:把 expert 数量加到 64(V2)/ 160(V2.5)/ 256(V3)/ 384(V4),并相应减小每个 expert 的 intermediate_size
- 共享专家(shared expert):每层永远激活的 expert,吸收”通用知识”,让 routed expert 专注”细分知识”
flowchart TB
X[输入 x] --> Gate{Gate}
Gate -->|top-k 路由| E1[Expert 1]
Gate -->|top-k 路由| E2[Expert 2]
Gate -->|top-k 路由| Edot[...]
Gate -->|top-k 路由| EN[Expert 384]
X -->|永远激活| ES[Shared Expert]
E1 --> Sum((+))
E2 --> Sum
Edot --> Sum
EN --> Sum
ES --> Sum
Sum --> Out[输出]
V4 沿用了这个结构,区别只在于 n_routed_experts=384(V3=256)、num_experts_per_tok=6(V3=8)。后面第 7-9 章会讲到,看似只是数字变了,背后牵动的是 gate scoring function 的换型和 hash 路由层的引入。
1.3 V3:把 MoE 推到 671B + FP8 训练
V3 在 2024 年 12 月开源,671B 总参 / 37B 激活,论文 DeepSeek-V3 Technical Report。它做了三件大事:
1.3.1 256 专家 + auxiliary-loss-free 负载均衡
V2 / V3 用过传统的 auxiliary loss(在 cross-entropy 之外加一个”专家使用均匀度”的辅助损失),但 aux loss 会干扰主任务的语义学习——专家被强行均匀化,可能损失某些极有价值的稀有专家。
V3 的做法是 noaux_tc——auxiliary-loss-free 负载均衡:
- 不再用辅助损失
- 给每个 expert 维护一个 bias term(V4 源码
inference/model.py:562的self.bias = nn.Parameter(torch.empty(args.n_routed_experts, dtype=torch.float32)),位于 Gate 类__init__内) - bias 只在 topk 选取时被加入,不参与最终 routing weight
- 训练过程中根据每个 expert 的”使用频率”动态调整 bias,使过载的 expert 被压低、欠载的 expert 被抬高
V4 完全继承了这套机制,并在 topk_method="noaux_tc" 这个配置项中显式标注。
1.3.2 FP8 训练 + ue8m0 scale
V3 是世界上第一个在 671B 级别成功跑通 FP8 全栈训练的开源 LLM。关键工程包括:
- e4m3 用于 forward / backward 的 GEMM
- e5m2 用于梯度
- ue8m0 用于 scale tensor(一种纯指数 8-bit 浮点)
- block-wise scaling:每 128 × 128 一个 scale,平衡精度与显存
V4 的 config.json 里这一段是直接继承 V3 的:
"quantization_config": {
"activation_scheme": "dynamic",
"fmt": "e4m3",
"quant_method": "fp8",
"scale_fmt": "ue8m0",
"weight_block_size": [128, 128]
}
但 V4 在此基础上又向下走了一步——expert 权重直接压到 FP4 e2m1,每 32 个 fp4 元素一个 ue8m0 scale。这是第 12 章会详细分析的。
1.3.3 MTP:多 Token 预测
V3 的最后一招,是给训练加了一个辅助 head:在主预测 head 之外,额外训练一个预测下一下个 token 的 MTPBlock。它带来三个好处:
- 训练时增加监督信号,加快收敛
- 推理时可以做 speculative decoding(投机解码)
- 让 hidden representation 学会”看更远”
V4 沿用了 MTP,但只保留 1 个 MTPBlock(num_nextn_predict_layers=1)。源码层面看:
# inference/model.py: Transformer.__init__
self.mtp = torch.nn.ModuleList()
for layer_id in range(args.n_mtp_layers):
self.mtp.append(MTPBlock(args.n_layers + layer_id, args))
self.mtp[-1].embed = self.embed
self.mtp[-1].head = self.head
注意最后两行——MTPBlock 的 embed 和 head 直接复用主模型的 embedding 和 lm_head。这是个非常便宜的实现:MTP 只多了一组 transformer block 的参数,embedding 和输出投影都是免费的。
1.4 V3.2-Exp:稀疏注意力的实验场
V3.2-Exp(“Exp” 是 experimental 的缩写)是 V3 与 V4 之间的一次”半发布”。它的全部价值在于:在生产规模上验证稀疏注意力的可行性。
V3.2-Exp 引入了 DSA(DeepSeek Sparse Attention):
- 在原本 dense MLA 的基础上叠加一个 sparse 路径
- 通过一个轻量的 score net 选 top-k KV 位置
- 选中的位置走稀疏内核(最早期版本基于 FlashMLA 的实验分支)
- 训练用 QAT(Quantization-Aware Training) + 稀疏感知监督 让两条路径协同收敛
V3.2-Exp 跑了几个月,得到了三个关键经验:
- 稀疏 + dense 双路径会增加训练复杂度——V4 直接砍掉 dense 路径,纯稀疏
- 滑动窗口 + 稀疏 KV 选择的组合在 1M context 下数学上能稳——V4 直接采用
- score net 必须有自己的 KV 视角——V4 给 Indexer 配了独立的 Compressor
V4 的 inference/model.py:380-435 那个 Indexer 类(class Indexer(torch.nn.Module)),本质上就是把 V3.2-Exp 的 score net 工业化、并把它的内部 KV 投影也压到 fp4。
1.4·补 V3.2-Exp 那次”半发布”在 V4 源码里留下的指纹
V3.2-Exp 在 2025 年 9 月发布,对外几乎没有宣传——把它读成 V4 的”工程预演”是最准确的。它没有上 leaderboard,模型卡只标记了一行”Experimental”。但 V3.2-Exp 把 DSA(DeepSeek Sparse Attention) 这套机制塞进了一个真实生产规模的模型里跑了几个月。当我们打开 V4 的 inference/model.py,至少有三处源码细节可以直接对回 V3.2-Exp 的实战教训——这些是可以验证的,不需要任何内部资料:
-
Indexer 拥有独立的 Compressor(
inference/model.py:Indexer.__init__)self.compressor = Compressor(args, compress_ratio, self.head_dim, True) # rotate=True注意最后一个参数
rotate=True——意味着 Indexer 自己的 Compressor 会做一次 Hadamard 旋转,并把 KV 量化到 FP4。这与主 Attention 的 Compressor(rotate=False)刻意区分。如果 score net 共用主 KV,源码里就不会有这一行——V3.2-Exp 验证了”score net 的 KV 投影必须独立”,V4 在源码里把它写死。 -
每层独立的
compress_ratio(config.json里compress_ratios是一个长度 62 的数组——主模型 61 层 + MTP 层 1 个)V3.2-Exp 的 DSA 是”全层均匀稀疏”。V4 改成 per-layer 的非均匀配置——
[128, 128, 4, 128, 4, ...]。这个配置只能从工程实验里得来,不是凭直觉拍出来的。 -
滑动窗口 + 稀疏 KV 的串联(
Attention.forward里topk_idxs = torch.cat([topk_idxs, compress_topk_idxs], dim=-1))V3.2-Exp 的 DSA 早期版本是”稀疏 替代 dense”。V4 改成”滑窗 + 稀疏”——近距离 KV 走滑窗(精度高、覆盖完整),远距离 KV 走稀疏(覆盖大、成本低)。源码里这两个 topk 索引是用
torch.cat拼起来的——这是两条注意力路径合一的工程标志。
这三条都不是从内部资料里推断的,它们是 V4 源码里任何人都可以 grep 出来的确凿事实。V3.2-Exp 的价值就是把这三条经验”刻”进了 V4 的代码里。
1.5 V4 全景:从一行 forward 看到整张地图
V4 的全部前向逻辑,可以浓缩成 Transformer.forward(inference/model.py:802,类定义在第 769 行)的几行:
@torch.inference_mode()
def forward(self, input_ids: torch.Tensor, start_pos: int = 0):
h = self.embed(input_ids)
# Expand to hc_mult copies for Hyper-Connections
h = h.unsqueeze(2).repeat(1, 1, self.hc_mult, 1)
for layer in self.layers:
h = layer(h, start_pos, input_ids)
logits = self.head(h, self.hc_head_fn, self.hc_head_scale, self.hc_head_base, self.norm)
return logits
短到不足 10 行,却把 V4 全部”反常识”的设计都摆在了表面:
| 行 | 看似平淡的代码 | 背后的故事 |
|---|---|---|
h = self.embed(input_ids) | 普通 embedding | 但是 ParallelEmbedding——vocab 维度被切到 TP 多卡 |
h.unsqueeze(2).repeat(1, 1, self.hc_mult, 1) | 把 h 在某个新维度上复制 4 份 | 这是把传统残差换成 Hyper-Connections 的入口——h 从此变成 [B, S, hc_mult, D] 的 4D 张量 |
for layer in self.layers: h = layer(h, start_pos, input_ids) | 普通的 N 层堆叠 | 但每层 layer 是 Block,里面用 hc_pre / hc_post 做 4-way 残差混合,并按 layer_id 切换 compress_ratio |
logits = self.head(h, ...) | LM head | 但 head 又是 4D 输入,用 hc_head 把 4 路 hidden 合并成 1 路再投影 |
把这 4 行展开,就是这本书后面 19 章要讲的全部内容。
flowchart TB
IN["input_ids: [B, S]"] --> EMB["ParallelEmbedding<br/>(第 15 章)"]
EMB --> H0["h: [B, S, D]"]
H0 --> EXPAND["unsqueeze + repeat<br/>= [B, S, hc_mult=4, D]<br/>(Hyper-Connections 入口)"]
EXPAND --> L1["Block 0 (第 1-9 章)"]
L1 --> L2["Block 1"]
L2 --> Ldot["..."]
Ldot --> LN["Block 60"]
LN --> NORM["RMSNorm + ParallelHead<br/>(第 10 章 hc_head)"]
NORM --> LOGITS["logits: [B, vocab]"]
subgraph 每层Block["每层 Block 内部 (第 1-11 章)"]
direction TB
Bin["h: [B, S, hc_mult, D]"] --> HCpreA["hc_pre (Sinkhorn)"]
HCpreA --> AttnNorm["RMSNorm"]
AttnNorm --> Attn["Attention<br/>(MLA + Compressor + Indexer + sparse_attn)"]
Attn --> HCpostA["hc_post"]
HCpostA --> HCpreF["hc_pre (Sinkhorn)"]
HCpreF --> FfnNorm["RMSNorm"]
FfnNorm --> MoE["MoE Gate + 384 routed + 1 shared"]
MoE --> HCpostF["hc_post"]
HCpostF --> Bout["h_out: [B, S, hc_mult, D]"]
end
这张图也是本书的章节地图。
1.5·补 V4 的核心设计决策树
把 V4 的所有架构决策编成一张决策树,会发现它们之间有非常清晰的因果链:
flowchart TB Goal["目标:1M context + 价格大幅下降"]:::goal Goal --> KV["挑战 1:KV cache 在 1M 下会爆"]:::ch Goal --> FLOPs["挑战 2:1.6T 模型推理 FLOPs 难承受"]:::ch Goal --> Train["挑战 3:训练成本必须可控"]:::ch Goal --> Stability["挑战 4:1.6T MoE 训练梯度稳定"]:::ch KV --> S1["决策 1:抛弃 V3 的 latent KV<br/>(kv_lora_rank → head_dim 全维)"]:::dec KV --> S2["决策 2:滑窗 + 压缩 KV 双通路"]:::dec KV --> S3["决策 3:per-layer 非均匀压缩比"]:::dec FLOPs --> S4["决策 4:稀疏 attention(Indexer top-1024)"]:::dec FLOPs --> S5["决策 5:grouped O 投影(o_groups=16)"]:::dec FLOPs --> S6["决策 6:top-6 而非 V3 的 top-8"]:::dec Train --> S7["决策 7:expert FP4 e2m1"]:::dec Train --> S8["决策 8:Muon 优化器(取代 AdamW)"]:::dec Stability --> S9["决策 9:Hyper-Connections(替代残差)"]:::dec Stability --> S10["决策 10:sqrtsoftplus + noaux_tc"]:::dec Stability --> S11["决策 11:前 3 层 hash 路由"]:::dec Stability --> S12["决策 12:Sinkhorn 归一化(HC 内部)"]:::dec classDef goal fill:#312e81,stroke:#a78bfa,color:#ede9fe; classDef ch fill:#7c2d12,stroke:#fb923c,color:#ffedd5; classDef dec fill:#0f172a,stroke:#3b82f6,color:#dbeafe;
这张图把 V4 的 12 个核心决策按”挑战 → 决策”分组。读完全书后回头看,每一个决策都会对应到本书某一节的”为什么这样”分析:
| 决策 | 章节定位 | 主要源码位置 |
|---|---|---|
| 抛弃 latent KV,KV 直接到 head_dim=512 | §2 | Attention.__init__ 中 self.wkv = Linear(self.dim, self.head_dim) |
| 滑窗 + 压缩 KV | §3 | kv_cache_size = window_size + max_seq_len // ratio |
| per-layer 压缩比 | §3.4 | compress_ratios[layer_id] |
| 稀疏 attention | §4-5 | Indexer 类 + sparse_attn kernel |
| grouped O 投影 | §2.5 | wo_a 的 ColumnParallelLinear + n_groups 分组 |
| top-6 | §7 | n_activated_experts=6 |
| expert FP4 | §12 | expert_dtype="fp4" + Linear(dtype=torch.float4_e2m1fn_x2) |
| Muon 优化器 | §17 | (训练侧,源码不在 inference 仓库) |
| Hyper-Connections | §10 | Block.hc_pre / hc_post + hc_attn_fn / hc_ffn_fn |
| sqrtsoftplus + noaux_tc | §7 | Gate.forward |
| 前 3 层 hash | §8 | self.hash = layer_id < args.n_hash_layers |
| Sinkhorn 归一化 | §10.3 | hc_split_sinkhorn(...) |
读到这里,你应该已经能感受到一件事——V4 的所有”奇怪”配置背后,都有一个工程上的现实约束。它不是炫技,是在 1.6T + 1M + 平价这三个约束下,被算力和精度同时压出来的解。
1.6 V4 vs V3:架构差异对照表
把 V3 和 V4 的核心配置摆在一起,差异跃然纸上:
| 维度 | V3 (671B / 37B) | V4 Pro (1.6T / 49B) | 差异性质 |
|---|---|---|---|
| 总层数 | 61 | 61 + 1 (MTP) | 持平 |
| Hidden size | 7168 | 7168 | 持平 |
| Attention head 数 | 128 | 128 | 持平 |
| Head dim | 192 (nope 128 + rope 64) | 512 (nope 448 + rope 64) | 重构 |
| KV latent rank | kv_lora_rank=512 | 取消,用 head_dim=512 的滑窗 + 压缩 KV | 重构 |
| Q LoRA rank | 1536 | 1536 | 持平 |
| O LoRA rank | 无 | 1024 (grouped, o_groups=16) | 新增 |
| Routed experts | 256 | 384 | 容量 +50% |
| Top-k experts | 8 | 6 | 稀疏性 ↑ |
| Hash routing | 无 | 前 3 层使用 hash 路由 | 新增 |
| MoE 中间维度 | 2048 | 3072 | 容量 +50% |
| Scoring function | sigmoid | sqrtsoftplus | 换型 |
| Topk method | noaux_tc | noaux_tc | 持平 |
| Expert 权重精度 | FP8 e4m3 | FP4 e2m1 | 重构 |
| Linear 权重精度 | FP8 e4m3 | FP8 e4m3 | 持平 |
| Scale 格式 | ue8m0 | ue8m0 | 持平 |
| Attention sparsity | dense | 滑窗 (128) + Compressor (per-layer ratio) + Indexer top-1024 | 重构 |
| Compress ratios per layer | 无 | [128, 128, 4, 128, 4, ...](绝大多数 4,少量 128 / 0) | 新增 |
| Index top-k | 无 | 1024 | 新增 |
| 残差 | identity 残差 | Hyper-Connections (hc_mult=4) + 20 步 Sinkhorn 归一化 | 重构 |
| MTP layers | 1 | 1 | 持平 |
| 最大上下文 | 128K → 1M (yarn) | 1M (yarn factor=16) | 持平 |
| RoPE theta | 10000 | 10000 + compress_rope_theta=160000(专门给压缩 KV 用的) | 双 RoPE |
| 优化器 | AdamW | Muon | 换型 |
| 预训练 token 数 | 14.8T | 32T+ | +116% |
差异性质三个标签:
- 重构:核心数据结构换了——KV、Attention、残差、Expert 精度
- 新增:增加了上一代没有的子系统——hash 路由、Indexer、grouped O、Hyper-Connections
- 持平:保留 V3 已经成熟的设计——layer 数、hidden size、head 数、MTP
V4 真正的工程量集中在那 6 个”重构”和 4 个”新增”上。
1.6·补 V4 的三种推理模式:Non-Think / Think High / Think Max
V4 在 README 中显式列出了三种推理模式,这一点是 V3 / V3.2 都没有的。这三种模式不是后处理选项,而是chat template 与 system prompt 的差别——在 encoding/encoding_dsv4.py 的 encode_messages(messages, thinking_mode=...) 中触发。
flowchart LR
Input["用户 messages"]
Mode{thinking_mode}
NT["Non-Think<br/>thinking_mode='direct'"]
TH["Think High<br/>thinking_mode='thinking'"]
TM["Think Max<br/>thinking_mode='max' + 特殊 system prompt"]
Input --> Mode
Mode -->|快速直观任务| NT
Mode -->|逻辑分析任务| TH
Mode -->|推理边界探索| TM
NT --> NTOut["输出: '</think>' 标签 + 总结"]
TH --> THOut["输出: '<think>' 思考过程 '</think>' 总结"]
TM --> TMOut["输出: 加长 think + 完整推理链"]
Non-Think 模式:模型直接给答案,类似 GPT-4 的常规模式。token 输出最短、首 token 延迟最低,适合日常聊天、检索、摘要。
Think High 模式:模型先生成一段 <think>...</think> 内的思考链,再给最终答案。这段思考链不是事后生成的”解释”,而是实际驱动答案的推理过程——本质上是 R1 路线的内化。
Think Max 模式:通过特殊 system prompt 解锁,模型会持续推理直到接近 384K token 的”思考上限”。这种模式的官方推荐场景是”探索模型的推理边界”——通常用于评测、研究、复杂数学/编程问题。
V4 把”推理形态”作为一等公民暴露给用户的设计决策,反过来影响了训练数据和评估闭环——第 18 章会讲到,V4 的后训练阶段是按”领域 + 思考形态”的笛卡尔积来组织 SFT/RL 数据的。
1.7 一段源码对比:V3 vs V4 的 Attention.init
如果你之前读过 V3 的源码,会对比出来 V4 在 Attention 这个最核心的类上做了什么——
V3 的 Attention.__init__(简化):
# V3 (deepseek-v3, inference/model.py)
self.wq_a = Linear(self.dim, self.q_lora_rank)
self.q_norm = RMSNorm(self.q_lora_rank)
self.wq_b = ColumnParallelLinear(self.q_lora_rank, n_heads * (qk_nope + qk_rope))
self.wkv_a = Linear(self.dim, self.kv_lora_rank + qk_rope) # ◀── KV 也用 LoRA
self.kv_norm = RMSNorm(self.kv_lora_rank)
self.wkv_b = ColumnParallelLinear(self.kv_lora_rank, n_heads * (qk_nope + v_dim))
self.wo = RowParallelLinear(n_heads * v_dim, self.dim) # ◀── O 是单矩阵
# KV cache: 每个 head 都存
self.register_buffer("kv_cache", torch.zeros(max_bsz, max_seq, n_heads, ...))
V4 的 Attention.__init__(inference/model.py:Attention 类):
# V4 (deepseek-v4, inference/model.py)
self.attn_sink = nn.Parameter(torch.empty(self.n_local_heads, dtype=torch.float32)) # ◀── 新增:注意力 sink
self.wq_a = Linear(self.dim, self.q_lora_rank)
self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
self.wq_b = ColumnParallelLinear(self.q_lora_rank, self.n_heads * self.head_dim)
self.wkv = Linear(self.dim, self.head_dim) # ◀── KV 不再是 LoRA:单矩阵投到 head_dim=512
self.kv_norm = RMSNorm(self.head_dim, self.eps)
# O 投影改为 grouped low-rank
self.wo_a = ColumnParallelLinear(self.n_heads * self.head_dim // self.n_groups,
self.n_groups * args.o_lora_rank, dtype=torch.bfloat16)
self.wo_b = RowParallelLinear(self.n_groups * args.o_lora_rank, self.dim)
if self.compress_ratio: # ◀── 新增:每层可独立配置压缩比
self.compressor = Compressor(args, self.compress_ratio, self.head_dim)
if self.compress_ratio == 4:
self.indexer = Indexer(args, self.compress_ratio) # ◀── 新增:稀疏 score net
# KV cache 几何变了:滑窗 + 压缩区
kv_cache_size = args.window_size + (args.max_seq_len // self.compress_ratio if self.compress_ratio else 0)
self.register_buffer("kv_cache", torch.zeros(args.max_batch_size, kv_cache_size, self.head_dim))
把这两段并排读,V4 的工程意图就完全暴露了:
- KV LoRA 被抹掉——V4 不再相信”潜在低秩 KV”是最优方案。它选了”head_dim=512 的全维 KV + 极强压缩 + 滑窗 + 稀疏选取”
- O 投影变 grouped low-rank——和 LoRA 微调里的 grouped LoRA 异曲同工,省参省算
- 稀疏化是层级可配的——
compress_ratios[layer_id]决定本层是 4 倍压缩或 128 倍压缩(dense / ratio=0 仅 MTP 层 1 处) - Indexer 只在 compress_ratio == 4 的层上启用——稀疏路由不是每层都需要
每一条都在说:“V3 那条 KV 路径走到尽头了,我们换。“
1.8 V4 Pro / V4 Flash:同一架构的两个尺寸
V4 同时发布了两款:
| 模型 | 总参数 | 激活参数 | 主要用途 |
|---|---|---|---|
| V4 Pro | 1.6T | 49B | 旗舰能力,对标 Claude Opus / GPT-5.5 |
| V4 Flash | 284B | 13B | 性价比线,对标 Gemini 3.1 Pro / GPT-5.4-mini |
两者同源——共享 inference/model.py、共享 config_class、共享所有 kernel。差异只在 config.json:
| 维度 | Pro | Flash |
|---|---|---|
| n_routed_experts | 384 | ~256(推测) |
| moe_inter_dim | 3072 | 较小 |
| n_layers | 61 | 较少 |
| 总参 | 1.6T | 284B |
| 激活参 | 49B | 13B |
它们的关系类似 Mixtral 8x22B 之于 Mixtral 8x7B,或 Llama 3.1 70B 之于 8B——同一份代码,两套权重。这本书的源码分析对两者通用,区别只在配置数字。
1.8·补 32T 训练 token 之谜与两阶段后训练
V4 的 README 第二段写着:
Pre-training: 32T+ high-quality and diverse tokens.
这一行话相对前作的 V3(14.8T)翻了一倍多。为什么 V4 必须吃这么多 token?答案藏在 V4 的几处架构决策里:
- 384 个 expert + top-6 激活,意味着每个 expert 见到的 token 比例下降——V3 是 256 expert top-8(每个 expert 平均看到 8/256 = 3.1% 的 token),V4 是 384 top-6(看到 6/384 = 1.6%)。把每个 expert 的有效训练量保持在与 V3 相当的水平,整体 token 数差不多要翻倍。
- Hyper-Connections 让每层有效”路径”翻 4 倍——HC 通过 hc_mult 维持 4 路 hidden state,意味着模型的有效参数容量比”乘以 4”更接近真相。更大容量需要更多 token 才能填充。
- 从 BF16 训练降到 FP4 expert 训练,每一次梯度更新的有效精度更低——更低的精度需要更长的训练曲线来补偿,这是 FP4 训练栈的一个隐性成本。
至于 32T 数据从哪里来,README 并没有公开。但从 V4 的能力分布反推可以猜出几个大类:
- 大量代码(特别是仓库级代码,README 显示 V4 在 SWE-bench 类基准上的进步)
- 大量数学/推理(Think Max 模式的存在表明数据里有大量”推理过程”标注)
- 大量长文档(1M context 的稳定性必须靠真实长文档训练,而不是把短文档强行拼接)
两阶段后训练这个公开细节同样耐人寻味。READMR 写:
Post-training: 1) Domain-specific expert cultivation independently with SFT + RL (GRPO). 2) On-policy distillation into a single unified model.
第一阶段:“领域专家独立培养”——意思是先用 SFT + GRPO(V3.2 用过的强化学习算法)训练几个领域专家模型(比如 V4-Code、V4-Math、V4-Long-Context 等),每个领域单独优化。第二阶段:“on-policy 蒸馏到统一模型”——再把这些领域专家的能力蒸馏回一个统一的 V4。
这个 pipeline 类似 R1 的”先长链推理 → 蒸馏到 chat 模型”路线,但 V4 把它放到了多领域和多推理模式的笛卡尔积上。第 18 章会用整章篇幅拆这个 pipeline 的工程实现——它解决的是”384 专家如何让每个 expert 都学到有差异化的能力”这个 V3 时代未完全解决的问题。
1.9 V4 在开源版图里的位置
把 V4 放在 2026 年 4 月的开源大模型版图里看:
| 模型 | 总参 / 激活参 | 上下文 | 关键特征 | 开源协议 |
|---|---|---|---|---|
| DeepSeek-V4-Pro | 1.6T / 49B | 1M | HC + 稀疏注意力 + FP4 experts | MIT |
| Qwen3-MoE-Max | ~700B / 35B | 256K | dense MLA + FP8 | Apache |
| Llama 4 Behemoth | ~2T / ~80B | 1M | FlashAttention v3 + dense | Llama Community |
| Mistral Magnum | ~480B / 22B | 128K | dense + grouped query | Apache |
| Gemma 3.5 | 70B / 70B | 1M | dense + sliding window | Gemma |
V4 的两点独特性:
- 唯一在 1M context 下做到稀疏 + 低精度全栈的开源模型
- 唯一把 expert 权重压到 FP4 并在 1.6T 规模上验证收敛的开源模型
这两点决定了:
- 想跑 1M 长文 + 极致 token 经济性的项目,V4 几乎没有替代
- 想理解未来 18 个月开源 LLM 走向(稀疏 + FP4 + HC)的工程师,V4 是唯一可以下载到本地拆开看的样本
1.9·补 端到端 inference:一段 prompt 的完整旅程
为了让”V4 怎么跑一个推理”具象起来,我们 trace 一个 4 token 的 prompt 在 V4 Pro 里的完整旅程。假设输入是”Hello, world.”:
步骤 1:tokenize
V4 复用 V3 的 BBPE 词表(vocab_size=129280)。Hello, world. 大约被切成 5 个 token:
[BOS, "Hello", ",", "world", "."] ← input_ids
步骤 2:进入 Transformer.forward
start_pos=0(首次前向), input_ids 形状 [1, 5]。
h = self.embed(input_ids) # [1, 5, 7168]
h = h.unsqueeze(2).repeat(1, 1, 4, 1) # [1, 5, 4, 7168] ← HC 4 路展开
步骤 3:进入第 0 层 Block
第 0 层的 compress_ratio=128(来自 compress_ratios[0])——KV 高度压缩。
# hc_pre: 把 [1, 5, 4, 7168] 混成 [1, 5, 7168]
# 内部:mixes = F.linear(x, hc_attn_fn) * rsqrt
# pre, post, comb = hc_split_sinkhorn(mixes, ...)
# y = sum(pre * x, dim=2)
# 输出 x: [1, 5, 7168], post: [1, 5, 4], comb: [1, 5, 4, 4]
x = layer.attn_norm(x) # RMSNorm
# Attention.forward(x, start_pos=0)
# q = wq_b(q_norm(wq_a(x))) # [1, 5, n_heads, 512]
# apply_rotary_emb(q[..., -64:], freqs_cis)
# kv = kv_norm(wkv(x)) # [1, 5, 512]
# apply_rotary_emb(kv[..., -64:], freqs_cis)
# act_quant(kv[..., :-64], 64, ...) # FP8 量化非 rope 部分
# topk_idxs = get_window_topk_idxs(...) # 滑窗 top-k
# # 第 0 层 compress_ratio=128,走 Compressor 但没有 Indexer(只有 ratio==4 才有)
# compress_topk_idxs = get_compress_topk_idxs(128, ...)
# topk_idxs = cat([window, compress])
# # KV cache 写入 + sparse_attn 计算
# o = sparse_attn(q, kv, attn_sink, topk_idxs, softmax_scale)
# apply_rotary_emb(o[..., -64:], freqs_cis, inverse=True)
# o = o.view(bsz, seqlen, n_groups, -1)
# o = einsum("bsgd,grd->bsgr", o, wo_a.weight.view(...))
# x = wo_b(o.flatten(2))
# 输出 x: [1, 5, 7168]
# hc_post: x + 4 路 residual → 4 路输出
h = layer.hc_post(x, h, post, comb) # [1, 5, 4, 7168]
# 再来一遍:hc_pre → ffn_norm → MoE → hc_post
# MoE.forward(x, input_ids)
# weights, indices = gate(x, input_ids) # 第 0 层 hash=True,indices = tid2eid[input_ids]
# # 找到本 rank 持有的 expert,逐个执行
# y += expert(x_subset, weights_subset)
# y += shared_expert(x)
# all_reduce(y) # 跨 TP rank 求和
# 输出 x: [1, 5, 7168]
h = layer.hc_post(x, h, post, comb) # [1, 5, 4, 7168]
步骤 4:穿过剩下 60 层 + 1 个 MTP 层
每层重复上述过程,但 compress_ratio 在 4 / 128 / 0 之间切换:
ratio=4的层:Indexer 启用,做”学习型稀疏选取”ratio=128的层:用固定步长压缩,不学ratio=0的层:不压缩,全 KV 滑窗(V4 中仅 MTP 层是这种配置,主模型 61 层都是 ratio=4 或 ratio=128)
第 4-60 层的 Gate 是 hash=False,indices 由 scores.topk(6) 决定。
步骤 5:head 输出
logits = self.head(h, hc_head_fn, hc_head_scale, hc_head_base, self.norm)
# 内部:x = hc_head(h, ...) # [1, 5, 7168]
# logits = F.linear(x[:, -1].float(), self.weight) # 只取最后一个 token
# 输出: logits: [1, 129280]
步骤 6:采样下一个 token
V4 默认采样参数:temperature=1.0, top_p=1.0。这两个 1.0 很关键——它们意味着 V4 的 logits 已经是”sharp 到不需要额外调温”的状态,这是 sqrtsoftplus + Muon 训练栈的副产品。
步骤 7:进入 decode(增量推理)
下一次 forward 时,input_ids 形状变成 [1, 1](只送新 token),start_pos=5。Compressor 走”decode 增量分支”——它不再重算整段 KV,而是把新来的 KV 累积到 kv_state 里,每 ratio 个 token 触发一次”压缩落盘”。
这就是 V4 的全部前向流程。从外面看,它和任何 transformer 一样——输入 token、输出 logits。从内部看,它在 61 层里把”窗口 / 压缩 / 稀疏 / 4 路 HC / 384 专家 / hash 路由 / FP4 解量化 / FP8 GEMM”全部走了一遍。
1.9·补·补 README 三组数字背后的工程账
V4 的 README 在显眼位置摆了三组”营销数字”,但每一组数字背后都有源码可以印证的工程账。把它们逐一拆开:
数字一:单 token 推理 FLOPs = V3.2 的 27%
这条数字的分母不是 dense Llama,而是 V3.2 的混合 dense+sparse。V4 把它砍到 27%,主要靠三件事:
- KV 不再是”每 token 一组 latent”,而是”每 ratio 个 token 才存一组压缩 KV”——这一项就把 KV-side 的 attention FLOPs 砍到 1/ratio
- 滑窗 + 稀疏 top-k 替代了 V3.2 后期版本的 dense+sparse 双路,移除了 dense 路径的 O(n²) 部分
- grouped O 投影(o_groups=16)把 O 矩阵的乘法量减少了一档
如果只看前两项,理论上 1M context 应该能砍到 V3.2 的 5-10%。27% 这个数字反映的是FP4 解量化、HC 4 路混合、Sinkhorn 归一化这些”V4 才引入的额外开销”把节省吃回去了一部分。
数字二:KV cache = V3.2 的 10%
V3.2 的 KV cache 公式(为简化起见忽略 head 维度差异):
V4 的 KV cache 公式(以 ratio=4 的层为例):
代入 ratio 平均接近 16(混合 4 / 128 / 0),window=128,head_dim=512,对比 V3.2 的 kv_lora_rank=512——
在 1M context 下,n_tok=1048576:
- V3.2 KV ≈ 1048576 × 512 × 2 = 1.07 GB / layer
- V4 KV ≈ (128 + 1048576/16) × 512 ≈ 34 MB / layer(取平均 ratio)
按 61 层算,V3.2 是 65 GB / 序列,V4 是 2 GB / 序列。源码里的 kv_cache_size = window_size + max_seq_len // compress_ratio 一行就是这个公式的承载者。
数字三:V4 Pro decode 单价 ≈ V3.2 的 1/3
价格不是单一架构因素决定,而是”FLOPs ↓ × 显存 ↓ × 训练成本 ↓ × 利用率 ↑“的乘积:
- FLOPs 降低 → 单卡 throughput 提升
- KV 降低 → 单卡并发提升
- FP4 expert + Muon 训练 → 单 token 训练成本下降,可以让定价进一步压低
- 384 专家 + top-6 → 同等容量下激活更稀疏,理论上 throughput 更高
把这些乘起来,三分之一这个倍数其实是保守估计。如果 batch 足够大、并发足够高,单价可以进一步压低——这是 V4 Pro 在长上下文场景下”价格倍率优势”会随 batch 越大而越夸张的根源。
1.9·补·补 实战选型矩阵:什么场景必须选 V4
把 V4 与 2026 年的几个主流开源 LLM 放在工程选型场景下做对比。这张矩阵不是 leaderboard,而是**“什么样的工程约束下 V4 是首选”**:
| 场景 | 上下文长度 | 价格敏感度 | 部署规模 | 首选模型 | V4 在此场景的相对优势 |
|---|---|---|---|---|---|
| 长法律文档分析 | 200K-1M | 高 | 单机 8 卡 | V4 Pro / V4 Flash | 1M 上下文 + 稀疏 KV 直接吊打 dense MLA |
| 仓库级代码理解 | 100K-500K | 中 | 集群 | V4 Pro | 1M 上下文 + Think High 推理形态 |
| 通用聊天助手 | <32K | 高 | 单卡 | Qwen3-32B / Mistral / V4 Flash | V4 Flash 价格优势但模型选择多 |
| 强推理(数学/竞赛编程) | <128K | 低 | 单机 8 卡 | V4 Pro Think Max | Think Max 模式对推理边界的探索 |
| 实时低延迟 API | <8K | 极高 | 大集群 | Qwen3-7B / Llama 4 Scout | V4 Pro 单卡占用太高、Flash 也比 7B 大 |
| 私有部署敏感行业 | <128K | 中 | 集群 | V4 Pro / Flash | MIT 许可、可下载完整权重 |
| 多模态(图文) | <128K | 中 | 集群 | Qwen3-VL / Llama 4 Maverick | V4 Pro 当前不含视觉编码器 |
| 端侧 / 边缘 | <8K | 极高 | 端侧 | Gemma 3 / Qwen3-1.7B | V4 当前最小尺寸 Flash 仍需多卡 |
V4 的”工程甜区”非常清晰:大上下文 + 价格敏感 + 可下载权重 + 通用文本这四个条件同时成立时,V4 几乎没有竞争对手。
反过来,V4 不擅长:
- 端侧/超低延迟——尺寸还是太大
- 多模态——当前 V4 是纯文本
- 极小上下文聊天——容易被同价位的 dense 7B/13B 模型在响应速度上反超
理解 V4 的”擅长 / 不擅长”需要回到架构本质——它的所有创新都为”长上下文 + 大容量 + 平价”服务。如果场景对这三件事都不敏感,V4 的工程红利就发挥不出来。
1.10 一张图:本书的章节路标
把全书 20 章映射到 V4 架构上:
flowchart LR
subgraph 源码["inference/model.py"]
direction TB
Embed["ParallelEmbedding"]:::ch15
HCexp["HC unsqueeze+repeat"]:::ch10
Block["Block × 61"]:::block
HCpre["hc_pre + Sinkhorn"]:::ch10
Attn["Attention<br/>(MLA + Compressor<br/>+ Indexer + sparse_attn)"]:::attn
HCpost["hc_post"]:::ch10
MoE["Gate + 384 experts<br/>+ Hash 前 3 层"]:::moe
Head["ParallelHead + hc_head"]:::ch10
MTP["MTPBlock × 1"]:::ch11
end
subgraph 章节["章节"]
Ch1["第 1 章 全景"]
Ch2["第 2 章 MLA 进阶"]
Ch3["第 3 章 Compressor"]
Ch4["第 4 章 Indexer"]
Ch5["第 5 章 sparse_attn 内核"]
Ch6["第 6 章 YaRN 1M"]
Ch7["第 7 章 Gate"]
Ch8["第 8 章 Hash 路由"]
Ch9["第 9 章 Expert"]
Ch10["第 10 章 HC"]
Ch11["第 11 章 MTP"]
Ch12["第 12-14 章 FP4/FP8/QAT"]
Ch15["第 15-16 章 分布式"]
Ch17["第 17-18 章 训练"]
Ch19["第 19-20 章 部署 / 生态"]
end
classDef ch15 fill:#0f172a,stroke:#3b82f6,color:#dbeafe;
classDef ch10 fill:#312e81,stroke:#a78bfa,color:#ede9fe;
classDef block fill:#1f2937,stroke:#475569,color:#e2e8f0;
classDef attn fill:#581c87,stroke:#c084fc,color:#fae8ff;
classDef moe fill:#7c2d12,stroke:#fb923c,color:#ffedd5;
classDef ch11 fill:#365314,stroke:#84cc16,color:#ecfccb;
1.11 config.json 字段全解读
V4 的 config.json 一共 30 多个字段,但每一个都是前面四代模型踩过坑总结出来的。我们按”架构 / 注意力 / MoE / 精度 / 训练”五组来读:
1.11.1 架构骨架
| 字段 | V4 取值 | 含义 |
|---|---|---|
architectures | ["DeepseekV4ForCausalLM"] | HF transformers 加载时使用的类名,也是这次架构升级的”门牌号” |
model_type | deepseek_v4 | 在 transformers/AutoConfig 里独立注册 |
num_hidden_layers | 61 | 主 Transformer 层数(与 V3 持平) |
hidden_size | 7168 | 模型维度,与 V3 持平 |
vocab_size | 129280 | 与 V3 持平。tokenizer 也复用 V3 的 BBPE |
tie_word_embeddings | false | embedding 与 lm_head 不共享权重——大模型上共享会拖累训练 |
transformers_version | 4.57.1 | 该模型卡是用 4.57.1 的 transformers 测过的 |
1.11.2 注意力与稀疏化
| 字段 | V4 取值 | 含义 |
|---|---|---|
num_attention_heads | 128 | 每层 Q head 数 |
head_dim | 512 | 单 head 维度(V3 是 192 + 64=256;V4 几乎翻倍) |
num_key_value_heads | 1 | KV head 只有 1 个——配合 head_dim 512 实现”近似单 KV” |
q_lora_rank | 1536 | Q 的 LoRA 中间维度(V3 同) |
o_lora_rank | 1024 | 新增:O 的 grouped LoRA rank |
o_groups | 16 | 新增:把 O 投影分成 16 组,每组独立低秩 |
qk_rope_head_dim | 64 | RoPE 部分维度(与 V3 持平);non-rope 部分 = head_dim - 64 = 448 |
sliding_window | 128 | 每层都跑一个滑动窗口注意力,叠在稀疏 KV 之上 |
compress_ratios | per-layer | 每层 KV 压缩倍率(4 / 128 / 0),见下 |
index_n_heads | 64 | Indexer 的 head 数(与主 attn 不同) |
index_head_dim | 128 | Indexer 的 head 维度 |
index_topk | 1024 | Indexer 选取的 top-k KV 位置数 |
compress_rope_theta | 160000 | 新增:压缩 KV 走的 RoPE 频率基数(比主 rope_theta 高 16 倍) |
rope_theta | 10000 | 滑窗 KV 走的 RoPE 频率基数 |
rope_scaling.type | yarn | YaRN 频率插值 |
rope_scaling.factor | 16 | 把 65K 上下文外推到 1M:16 倍 |
rope_scaling.original_max_position_embeddings | 65536 | YaRN 的”短上下文锚点” |
rope_scaling.beta_fast | 32 | YaRN 高频段截止 |
rope_scaling.beta_slow | 1 | YaRN 低频段截止 |
max_position_embeddings | 1048576 | 1M token |
compress_ratios 是个 62 元素的整数数组(61 主模型层 + 1 MTP 层),V4 Pro 的真实取值(config.json grep 结果)是:
[128, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4,
128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128,
4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 128, 4,
128, 4, 128, 4, 128, 4, 128, 4, 128, 4, 0]
读这串数字的方法:
- 前 2 层 = 128:极端高压缩,KV 几乎不存,主要靠滑窗
- 数值分布(grep 实测):128 出现 31 次,4 出现 30 次,0 出现 1 次
- 后续大量 4 与 128 交替:4 倍压缩走 Indexer 稀疏选取;128 倍压缩当快速回忆通道
- 第 3 层、第 5 层等”4”:是稀疏注意力真正在工作的层
- 最后一个元素(index 61)= 0:这是 MTP 层(
num_nextn_predict_layers=1),不是主模型最后一层。MTP 用完整 dense KV,可能是为 next-next 预测训练监督留无损视野(工程推断)。主模型最后一层(index 60)实测是 ratio=4,不是 0
这种 per-layer 的非均匀稀疏配置是 V4 跑通 1M context 的”关键调音”。第 3 章会反复回到这串数字。
1.11.3 MoE 与路由
| 字段 | V4 取值 | 含义 |
|---|---|---|
n_routed_experts | 384 | 路由专家数(V3=256) |
n_shared_experts | 1 | 共享专家数(与 V3 持平) |
num_experts_per_tok | 6 | 每 token 激活的 routed expert 数(V3=8,V4 更稀疏) |
moe_intermediate_size | 3072 | 单 expert 的 FFN 中间维度 |
routed_scaling_factor | 2.5 | 路由权重的最终缩放系数 |
scoring_func | sqrtsoftplus | gate 评分函数(V3 是 sigmoid) |
topk_method | noaux_tc | auxiliary-loss-free top-k 选取 |
norm_topk_prob | true | 选定 top-k 后做归一化 |
num_hash_layers | 3 | 新增:前 3 层用 hash 路由 |
swiglu_limit | 10.0 | SwiGLU 门控值的 clip 上限——抑制极端激活 |
1.11.4 精度与量化
| 字段 | V4 取值 | 含义 |
|---|---|---|
expert_dtype | fp4 | 新增:expert 权重压到 FP4 e2m1 |
quantization_config.fmt | e4m3 | 非 expert 权重的 FP8 格式 |
quantization_config.scale_fmt | ue8m0 | scale tensor 的格式 |
quantization_config.weight_block_size | [128, 128] | FP8 块状量化的块大小 |
quantization_config.activation_scheme | dynamic | 激活值在每次 forward 时重新算 scale |
torch_dtype | bfloat16 | 反量化后的工作 dtype |
rms_norm_eps | 1e-6 | RMSNorm 的 epsilon |
hc_eps | 1e-6 | Hyper-Connections 的 epsilon |
hc_mult | 4 | 新增:HC 复制份数 |
hc_sinkhorn_iters | 20 | 新增:HC 内部 Sinkhorn 迭代次数 |
num_nextn_predict_layers | 1 | MTP 层数 |
到这里,config.json 里几乎每一个字段都对应到了某个本书后续章节会展开的子主题。把这张表当作”配置文件 → 章节”的双向索引表,可以反复回查。
1.12 V4 Pro 与 V4 Flash:同源不同尺寸
V4 Pro 与 V4 Flash 共享所有 kernel、共享 inference/model.py,但 config.json 在以下字段上不同:
| 字段 | V4 Pro | V4 Flash(推测,待 HF 仓库确认) | 影响 |
|---|---|---|---|
| 总参数 | 1.6T | 284B | 内存占用差 5.6 倍 |
| 激活参数 | 49B | 13B | 单 token FLOPs 差 ~4 倍 |
n_routed_experts | 384 | ~256 | 容量差 |
moe_intermediate_size | 3072 | ~2048 | 单 expert 的 FFN 宽度 |
n_layers | 61 | ~32 | 深度差 |
head_dim | 512 | 同 Pro | 持平 |
| HC / Indexer / 滑窗 | 同 Pro | 同 Pro | 持平 |
为什么 Flash 也要 1M 上下文?——因为 V4 Pro 与 V4 Flash 跑同一份 inference/model.py,长文档能力是架构带来的、不是参数量带来的。Flash 的 1.6T → 284B 缩小,主要走的是”层数 + expert 数 + FFN 宽度”三个维度的同比例下调。
这也是 V4 的工程美学之一——核心创新是架构级的,所以 Flash 与 Pro 一起免费拿到了 1M 上下文与稀疏注意力的红利,不需要为每个尺寸重新做工程。
1.12·补 V4 的工程美学:四条潜规则
读完 V4 的 inference/model.py 800 行源码,会发现作者团队在工程上有四条非常一致的”潜规则”。这些潜规则没写进任何文档,但它们决定了源码的可读性与可演化性:
潜规则一:所有”看似可以隐藏的东西”都暴露在表面
V4 没有用 abstract base class 包装 Attention / MoE / MLP,没有用工厂函数返回不同变体的 Linear,也没有用 hooks 做 dynamic dispatch。所有的分支选择都是 if compress_ratio:、if dtype == torch.float4_e2m1fn_x2: 这种明面上的判断。读者第一次读源码时,可以用 ctrl+F 找到任意一个变量的所有使用点——这是”工程的诚实”。
潜规则二:所有 dtype 转换都显式写在调用点
V4 的源码里反复出现 x = x.float()、y.type_as(x)、x.to(dtype) 这种显式 dtype 切换。它的好处是任何一段代码运行在什么 dtype 上都可以一眼看清——这对 FP4/FP8/BF16 三种精度并存的代码至关重要。
潜规则三:状态外置
Compressor 的 kv_state / score_state 都用 register_buffer(persistent=False) 显式外置。Attention 的 kv_cache 也是同样。这意味着:模型的”状态”和”参数”被严格分开——参数是存盘 / 量化 / 分布式同步的对象,状态是运行时局部的 buffer。这种分离让 V4 在做 quantization-aware training 时不会把状态错误量化、做 KV cache offloading 时不会污染参数。
潜规则四:world_size / rank 是”全局可变状态”
文件顶部三行:
world_size = 1
rank = 0
block_size = 128
不是常量,是全局可变变量——Transformer.__init__ 里有 global world_size, rank, default_dtype, ...。这个写法在工程审美上有争议(违反了”可变全局变量是反模式”的传统教条),但它带来的好处是:整个模型的并行配置在一个地方设置后,每个 module 不需要再传 world_size/rank——大幅简化了 ParallelEmbedding / ColumnParallelLinear 等类的接口。
这四条潜规则不是 V4 独有的,但它们在 V4 源码里贯彻得异常彻底。如果你之前读 V2/V3 的源码会觉得”有点乱”,V4 的源码读起来会有”刀切豆腐”的清爽感——这是工程团队四代积累后的功底。
1.13 一段 forward 的张量形状追踪
把 Transformer.forward 在 B=2, S=128 的预填充阶段一步步 trace 一遍——
input_ids: [2, 128] # B=2, S=128
# 1. embedding
h = self.embed(input_ids) # [2, 128, 7168]
# 2. HC expand
h = h.unsqueeze(2).repeat(1, 1, 4, 1) # [2, 128, 4, 7168] ← 增加了 hc_mult 维度
# 3. 进入第一层 Block.forward
for layer in self.layers:
# hc_pre:把 4 路混成 1 路
x, post, comb = layer.hc_pre(h, ...) # x: [2, 128, 7168]
# post: [2, 128, 4]
# comb: [2, 128, 4, 4]
# RMSNorm
x = layer.attn_norm(x) # [2, 128, 7168]
# Attention
x = layer.attn(x, start_pos=0) # [2, 128, 7168]
# hc_post:1 路 + 残差 4 路 → 4 路
h = layer.hc_post(x, h, post, comb) # [2, 128, 4, 7168]
# 再来一遍 hc_pre/hc_post 包住 MoE
x, post, comb = layer.hc_pre(h, ...)
x = layer.ffn_norm(x)
x = layer.ffn(x, input_ids) # [2, 128, 7168]
h = layer.hc_post(x, h, post, comb) # [2, 128, 4, 7168]
# 4. 最后的 head
logits = self.head(h, ...) # [2, vocab_size]
注意点:
h在主循环里始终保持 4 维(B, S, hc_mult, D);只有进 attention / ffn 的瞬间才被压成 3 维hc_pre输出的post(每个 token 的”输出权重”)和comb(每个 token 的”混合矩阵”)是 HC 数学的核心——第 10 章会展开- 进入
attn的x形状是[B, S, D],里面经过 MLA、Compressor、Indexer、sparse_attn 之后输出回[B, S, D]——这部分内部状态由 Attention 自己管 ffn输入x: [B, S, D]+input_ids: [B, S]——input_ids是 hash 路由要用的(前 3 层用它直接查表选 expert)
把这段 trace 牢记在脑里,后面任何一章谈”这个变量来自哪里、要传到哪里”都不会再迷路。
1.13·补 V4 专属术语速查表
V4 源码与本书引入了一批”V4 专属术语”,这些术语在前作 V2 / V3 / V3.2 中要么没有、要么含义不同。第一次读到容易混淆,建议在阅读后续章节时随时回查这张表:
- HC(Hyper-Connections):替代传统残差的 4 路 hidden state 混合机制。每个 token 在每层带 4 份 hidden state,通过学习的混合矩阵(
hc_attn_fn/hc_ffn_fn)和 Sinkhorn 归一化在层间流动。来源:Block.hc_pre / hc_post。 - hc_mult:HC 的复制份数。V4 设为 4。
- Sinkhorn 迭代:HC 内部用来把混合矩阵变成”接近双随机矩阵”的迭代算法。V4 的 hc_sinkhorn_iters=20。
- Compressor:把每
compress_ratio个 token 的 KV 通过学习的 gated pooling 压成一组的模块。V4 中每层(compress_ratio>0 时)都有自己的 Compressor。 - Indexer:稀疏注意力的 score net,给压缩 KV 打分并选 top-k。V4 中只有 compress_ratio=4 的层启用。
- compress_ratio:每层独立配置,决定 KV 是 4 倍压缩、128 倍压缩,还是不压缩(0)。
- sliding window (window_size=128):每层都跑的滑动窗口注意力。是 V4 KV cache 的一部分。
- noaux_tc:auxiliary-loss-free top-k 选取。继承自 V3。
- sqrtsoftplus:V4 新引入的 gate scoring 函数:
scores = F.softplus(x).sqrt()。 - routed_scaling_factor:routed expert 的最终输出缩放系数(V4=2.5)。
- hash routing:前 3 层(n_hash_layers=3)使用的预定路由——expert index 直接由 token id 查
tid2eid表得到。 - MTP:Multi-Token Prediction。V4 保留 1 个 MTPBlock。
- YaRN:RoPE 频率插值的长上下文外推方案。V4 的 factor=16,把 65K 训练上下文外推到 1M。
- expert_dtype=“fp4”:expert 权重的 FP4 e2m1 量化。
- ue8m0:纯指数 8-bit 浮点格式,V4 用于 scale tensor。
- block_size=128:FP8 的 per-block scale 块大小。
- fp4_block_size=32:FP4 的 per-block scale 块大小。
- rotate_activation:随机 Hadamard 变换,FP4 量化前对激活做一次”信息打散”。
- attn_sink:每个 attention head 一个 float32 的”注意力汇”参数,给稀疏注意力填补”找不到合适 KV 时”的兜底。
- grouped O:把 attention 的输出投影分成 16 组,每组独立做低秩投影。
- kv_state / score_state:Compressor 在 decode 阶段累积 KV 用的 buffer。
- act_quant:把激活值动态量化到 FP8 e4m3 + ue8m0 scale。
- fp4_act_quant:把激活值动态量化到 FP4 e2m1。
- fp8_gemm / fp4_gemm:FP8 / FP4 矩阵乘法 kernel(在
kernel.py中实现,依赖 DeepGEMM)。 - sparse_attn:V4 的稀疏注意力 kernel(在
kernel.py中实现,依赖 FlashMLA)。 - n_routed_experts / n_shared_experts:路由专家数 / 共享专家数。V4 是 384 / 1。
- n_activated_experts:每 token 激活的 routed expert 数。V4 是 6。
- q_lora_rank / o_lora_rank:Q 和 O 投影的 LoRA 中间维度。V4 分别是 1536 / 1024。
- o_groups:grouped O 投影的组数。V4 是 16。
- freqs_cis:预计算的 RoPE 复指数张量,每层注意力共享。
- window_size=128 vs sliding_window=128:源码中前者出现在
ModelArgs,后者出现在config.json,是同一个值的两种命名——加载时映射。
记住这 30 个术语,本书剩余 19 章读起来会顺得多。
1.14 动手实验:先把架构对话起来
在不下载 865 GB 权重的前提下,可以做以下两个动手实验来”摸到” V4 架构:
实验 A:用随机权重把 Transformer 跑通(最低 16 GB CPU 内存即可)
# 注意:这只跑前向传播验证形状,不会得到有意义的输出
import torch
from inference.model import Transformer, ModelArgs
torch.set_default_dtype(torch.bfloat16)
torch.set_default_device("cpu") # 单卡 CPU 先跑通形状
# 用一个袖珍配置(比 V4 Pro 小 100 倍以上)
args = ModelArgs(
vocab_size=1024,
dim=512,
n_layers=2,
n_heads=8,
n_routed_experts=4,
n_activated_experts=2,
moe_inter_dim=1024,
head_dim=128,
rope_head_dim=32,
max_seq_len=512,
max_batch_size=2,
)
model = Transformer(args)
x = torch.randint(0, args.vocab_size, (1, 64))
out = model(x)
print(out.shape) # 期望: torch.Size([1, vocab_size])
这个实验帮助你确认:你的环境能正确加载 V4 的
inference/model.py、能正确调用kernel里的 fp4_gemm / sparse_attn(在 CPU 上会走 fallback)。
实验 B:从 config.json 反推显存占用
import json
cfg = json.load(open("config.json"))
# 仅算 expert 部分
moe_params = cfg["num_hidden_layers"] * cfg["n_routed_experts"] \
* cfg["moe_intermediate_size"] * cfg["hidden_size"] * 3
moe_bytes = moe_params * 0.5 # FP4 e2m1 = 0.5 byte
print(f"Expert FP4 weights: {moe_params/1e9:.1f}B params, {moe_bytes/1e9:.1f} GB")
# 仅算 attention 部分(Q/KV/O LoRA)
qkvo_params_per_layer = (
cfg["hidden_size"] * cfg["q_lora_rank"] # wq_a
+ cfg["q_lora_rank"] * cfg["num_attention_heads"] * cfg["head_dim"] # wq_b
+ cfg["hidden_size"] * cfg["head_dim"] # wkv
+ cfg["num_attention_heads"] * cfg["head_dim"] // cfg["o_groups"]
* cfg["o_groups"] * cfg["o_lora_rank"] # wo_a
+ cfg["o_groups"] * cfg["o_lora_rank"] * cfg["hidden_size"] # wo_b
)
qkvo_total = qkvo_params_per_layer * cfg["num_hidden_layers"]
qkvo_bytes = qkvo_total * 1.0 # FP8 e4m3 = 1 byte
print(f"Attention FP8 weights: {qkvo_total/1e9:.1f}B params, {qkvo_bytes/1e9:.1f} GB")
跑完后把数字与官方 README 给的 1.6T / 49B 对照——你会发现 expert 部分大约占了 99% 的总参数。这就是 V4”把 expert 压 FP4、其他保 FP8”这个权衡的工程原因。
1.15 延伸阅读
权威一手资料(按重要性排序):
- DeepSeek-V4 技术报告——
huggingface.co/deepseek-ai/DeepSeek-V4-Pro/blob/main/DeepSeek_V4.pdf—— 这本书所有章节都会反复引用,建议至少通读一遍 - DeepSeek-V3 Technical Report(arXiv:2412.19437)—— 理解 V4 必须先理解 V3
- DeepSeek-V2: A Strong, Economical, and Efficient MoE LM(arXiv:2405.04434)—— MLA 和 DeepSeekMoE 的源头
- DeepSeekMoE(arXiv:2401.06066)—— 细粒度专家 + 共享专家的论文起点
- YaRN: Efficient Context Window Extension(arXiv:2309.00071)—— 第 6 章会用到
- FlashMLA 仓库——
github.com/deepseek-ai/FlashMLA,第 5 章主参考 - DeepGEMM 仓库——
github.com/deepseek-ai/DeepGEMM,第 13 章主参考 - Hyper-Connections 论文——第 10 章会引用,注意 V4 的实现与原论文有差异
二手但价值高的参考:
- vLLM 官方 V4 适配 PR——第 19 章会拆这条 PR
- Artificial Analysis 的 V4 评测页面(独立基准对比)
1.15·补 V4 源码行号速查表
为了让本书所有源码引用 grep 可验证,把 inference/model.py(HF revision deepseek-ai/DeepSeek-V4-Pro 当前 main 分支,文件总长 827 行)的全部类与顶级函数行号列在这里。读者可以用 curl -sL <raw_url> | head -N | tail -1 直接验证任何一行。
顶级函数:
| 行号 | 名称 | 备注 |
|---|---|---|
| 25 | set_dtype | @contextmanager 装饰 |
| 108 | linear | dtype 分发到 fp4_gemm/fp8_gemm |
| 200 | precompute_freqs_cis | YaRN RoPE,@lru_cache(2) |
| 232 | apply_rotary_emb | RoPE 旋转应用 |
| 247 | rotate_activation | Hadamard 变换 |
| 255 | get_window_topk_idxs | 滑窗 topk 索引 |
| 269 | get_compress_topk_idxs | 压缩段 topk 索引 |
类定义:
| 行号 | 类名 | 关键内部成员 |
|---|---|---|
| 35 | ModelArgs | @dataclass,所有超参 |
| 83 | ParallelEmbedding | TP-切分的 vocab embedding |
| 123 | Linear | FP4/FP8/BF16 三态权重容器 |
| 155 | ColumnParallelLinear | 切 out_features |
| 166 | RowParallelLinear | 切 in_features + all_reduce |
| 183 | RMSNorm | float32 归一化 |
| 279 | Compressor | KV 压缩,含 prefill/decode 双 codepath |
| 380 | Indexer | 稀疏注意力 score net |
| 436 | Attention | MLA + Compressor + Indexer 集成 |
| 546 | Gate | sqrtsoftplus / hash / noaux_tc |
| 587 | Expert | SwiGLU FFN |
| 609 | MoE | 384 expert + shared 调度 |
| 647 | Block | HC + attention + FFN 包裹 |
| 703 | ParallelHead | LM head + hc_head 处理 |
| 738 | MTPBlock | 继承 Block,加 e_proj/h_proj |
| 769 | Transformer | 顶层模型 |
关键方法行号(章节里反复引用):
| 行号 | 方法 | 章节定位 |
|---|---|---|
| 86 | ParallelEmbedding.__init__ | §15.5 |
| 96 | ParallelEmbedding.forward | §15.5 |
| 283 | Compressor.__init__ | §3.2 |
| 316 | Compressor.forward | §3.3 / §3.4 |
| 384 | Indexer.__init__ | §4.2 |
| 402 | Indexer.forward | §4.3 |
| 439 | Attention.__init__ | §2.2 |
| 484 | Attention.forward | §2.6 / §2.7 / §2.8·补 |
| 459 | Attention 中的 self.wq_b | §15 ColumnParallel 例 |
| 550 | Gate.__init__ | §7.2 |
| 562 | Gate 中的 self.bias | §7.4 |
| 564 | Gate.forward | §7.2 / §8.3 |
| 589 | Expert.__init__ | §9.2 |
| 596 | Expert.forward | §9.2 / §9.3 |
| 612 | MoE.__init__ | §15.7 |
| 629 | MoE.forward | §9.6 / §15.10 |
| 652 | Block.__init__ | §10.2 / §10.3 |
| 688 | Block.forward | §10.2 |
| 740 | MTPBlock.__init__ | §11.2 |
| 757 | MTPBlock.forward | §11.3 |
| 772 | Transformer.__init__ | §1.5 / §15.2 |
| 802 | Transformer.forward | §1.5 / §1.13 |
验证方式:
# 把整文件下载下来本地 grep
curl -sL https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro/raw/main/inference/model.py -o model.py
wc -l model.py # 应输出 827
grep -n "^class \|^def \|^ def " model.py # 列出所有类与方法
sed -n '436,460p' model.py # 看 Attention 类的前 25 行
如果未来 V4 GA 版本的源码改动让某些行号偏移,本表会在第二版中更新。读者发现行号不对请反馈。
1.16 本章小结
- DeepSeek 的四代模型是一条清晰的工程主线:稀疏 + 长上下文 + 低精度,每代啃一段
- V4 不是”换了几个模块”,而是同时重构了 KV、Attention、残差、Expert 精度四件大事
- V4 真正的”反常识”全部摆在
Transformer.forward那 4 行里——HC unsqueeze、Block 循环、HC head——这本书剩下的 19 章是把这 4 行展开 - V4 与 V3 的差异表里,10 项里有 6 项标注”重构”——这就是为什么 V4 不能简单理解为”V3 的放大版”
config.json的每一个字段都对应到本书某个章节——这是配置 → 源码 → 章节的三向索引
第 2 章我们进入注意力革命的第一站:MLA 在 V4 里到底变成了什么——head_dim 为什么从 192 跳到 512、grouped O 投影解决了什么问题、kv_lora_rank 为什么消失。
评论 0
还没有评论,来说两句吧。
评论加载失败,刷新重试。