DeepSeek V4 源码剖析

第1章 V4 之路:从 V2 / V3 / V3.2 到 V4 的架构演进

作者 杨艺韬 · 11,761 字

第1章 V4 之路:从 V2 / V3 / V3.2 到 V4 的架构演进

"现代神经网络架构的每一次跃迁,本质上都是一场对'空间 / 时间 / 精度'三角的再平衡。" —— Tri Dao

你看到的 V4,不是一次发布。是 28 个月。


1.1 引子:为什么必须先讲 V2 / V3 / V3.2

互联网上关于 V4 的解读,大多数从 2026-04-24 那次发布讲起——这种讲法读起来很爽,但会错过 V4 真正的灵魂

DeepSeek 在 V4 上做的几乎每一处架构改动,都是对前三代某个已知工程伤疤的回应:

把 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 大小是:

KV size=2×L×H×dh×ntok\text{KV size} = 2 \times L \times H \times d_h \times n_\text{tok}

其中 LL 是层数、HH 是 head 数、dhd_h 是 head 维度、ntokn_\text{tok} 是序列长度。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 提出两个改造:

  1. 细粒度专家:把 expert 数量加到 64(V2)/ 160(V2.5)/ 256(V3)/ 384(V4),并相应减小每个 expert 的 intermediate_size
  2. 共享专家(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 负载均衡:

V4 完全继承了这套机制,并在 topk_method="noaux_tc" 这个配置项中显式标注。

1.3.2 FP8 训练 + ue8m0 scale

V3 是世界上第一个在 671B 级别成功跑通 FP8 全栈训练的开源 LLM。关键工程包括:

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。它带来三个好处:

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 的 embedhead 直接复用主模型的 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)

V3.2-Exp 跑了几个月,得到了三个关键经验:

  1. 稀疏 + dense 双路径会增加训练复杂度——V4 直接砍掉 dense 路径,纯稀疏
  2. 滑动窗口 + 稀疏 KV 选择的组合在 1M context 下数学上能稳——V4 直接采用
  3. 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 的实战教训——这些是可以验证的,不需要任何内部资料:

  1. Indexer 拥有独立的 Compressorinference/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 在源码里把它写死。

  2. 每层独立的 compress_ratioconfig.jsoncompress_ratios 是一个长度 62 的数组——主模型 61 层 + MTP 层 1 个)

    V3.2-Exp 的 DSA 是"全层均匀稀疏"。V4 改成 per-layer 的非均匀配置——[128, 128, 4, 128, 4, ...]。这个配置只能从工程实验里得来,不是凭直觉拍出来的。

  3. 滑动窗口 + 稀疏 KV 的串联Attention.forwardtopk_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.forwardinference/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%

差异性质三个标签:

V4 真正的工程量集中在那 6 个"重构"和 4 个"新增"上。


1.6·补 V4 的三种推理模式:Non-Think / Think High / Think Max

V4 在 README 中显式列出了三种推理模式,这一点是 V3 / V3.2 都没有的。这三种模式不是后处理选项,而是chat template 与 system prompt 的差别——在 encoding/encoding_dsv4.pyencode_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 的工程意图就完全暴露了:

  1. KV LoRA 被抹掉——V4 不再相信"潜在低秩 KV"是最优方案。它选了"head_dim=512 的全维 KV + 极强压缩 + 滑窗 + 稀疏选取"
  2. O 投影变 grouped low-rank——和 LoRA 微调里的 grouped LoRA 异曲同工,省参省算
  3. 稀疏化是层级可配的——compress_ratios[layer_id] 决定本层是 4 倍压缩或 128 倍压缩(dense / ratio=0 仅 MTP 层 1 处)
  4. 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 的几处架构决策里:

  1. 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 数差不多要翻倍。
  2. Hyper-Connections 让每层有效"路径"翻 4 倍——HC 通过 hc_mult 维持 4 路 hidden state,意味着模型的有效参数容量比"乘以 4"更接近真相。更大容量需要更多 token 才能填充。
  3. 从 BF16 训练降到 FP4 expert 训练,每一次梯度更新的有效精度更低——更低的精度需要更长的训练曲线来补偿,这是 FP4 训练栈的一个隐性成本。

至于 32T 数据从哪里来,README 并没有公开。但从 V4 的能力分布反推可以猜出几个大类:

两阶段后训练这个公开细节同样耐人寻味。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 的两点独特性:

  1. 唯一在 1M context 下做到稀疏 + 低精度全栈的开源模型
  2. 唯一把 expert 权重压到 FP4 并在 1.6T 规模上验证收敛的开源模型

这两点决定了:


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 之间切换:

第 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%,主要靠三件事:

  1. KV 不再是"每 token 一组 latent",而是"每 ratio 个 token 才存一组压缩 KV"——这一项就把 KV-side 的 attention FLOPs 砍到 1/ratio
  2. 滑窗 + 稀疏 top-k 替代了 V3.2 后期版本的 dense+sparse 双路,移除了 dense 路径的 O(n²) 部分
  3. 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 维度差异):

KVV3.2=L×ntok×dkv_lora×2(dense MLA)\text{KV}_{V3.2} = L \times n_\text{tok} \times d_\text{kv\_lora} \times 2 \quad\text{(dense MLA)}

V4 的 KV cache 公式(以 ratio=4 的层为例):

KVV4=L×(window_size+ntokratio)×dhead\text{KV}_{V4} = L \times \left(\text{window\_size} + \frac{n_\text{tok}}{\text{ratio}}\right) \times d_\text{head}

代入 ratio 平均接近 16(混合 4 / 128 / 0),window=128,head_dim=512,对比 V3.2 的 kv_lora_rank=512——

在 1M context 下,n_tok=1048576:

按 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 ↓ × 显存 ↓ × 训练成本 ↓ × 利用率 ↑"的乘积:

把这些乘起来,三分之一这个倍数其实是保守估计。如果 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 的"擅长 / 不擅长"需要回到架构本质——它的所有创新都为"长上下文 + 大容量 + 平价"服务。如果场景对这三件事都不敏感,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]

读这串数字的方法:

这种 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 三种精度并存的代码至关重要。

潜规则三:状态外置

Compressorkv_state / score_state 都用 register_buffer(persistent=False) 显式外置。Attentionkv_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.forwardB=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]

注意点:

把这段 trace 牢记在脑里,后面任何一章谈"这个变量来自哪里、要传到哪里"都不会再迷路。


1.13·补 V4 专属术语速查表

V4 源码与本书引入了一批"V4 专属术语",这些术语在前作 V2 / V3 / V3.2 中要么没有、要么含义不同。第一次读到容易混淆,建议在阅读后续章节时随时回查这张表:

记住这 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 延伸阅读

权威一手资料(按重要性排序):

  1. DeepSeek-V4 技术报告——huggingface.co/deepseek-ai/DeepSeek-V4-Pro/blob/main/DeepSeek_V4.pdf —— 这本书所有章节都会反复引用,建议至少通读一遍
  2. DeepSeek-V3 Technical ReportarXiv:2412.19437)—— 理解 V4 必须先理解 V3
  3. DeepSeek-V2: A Strong, Economical, and Efficient MoE LMarXiv:2405.04434)—— MLA 和 DeepSeekMoE 的源头
  4. DeepSeekMoEarXiv:2401.06066)—— 细粒度专家 + 共享专家的论文起点
  5. YaRN: Efficient Context Window ExtensionarXiv:2309.00071)—— 第 6 章会用到
  6. FlashMLA 仓库——github.com/deepseek-ai/FlashMLA,第 5 章主参考
  7. DeepGEMM 仓库——github.com/deepseek-ai/DeepGEMM,第 13 章主参考
  8. Hyper-Connections 论文——第 10 章会引用,注意 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 本章小结

第 2 章我们进入注意力革命的第一站:MLA 在 V4 里到底变成了什么——head_dim 为什么从 192 跳到 512、grouped O 投影解决了什么问题、kv_lora_rank 为什么消失。