DeepSeek 工程深读¶
更新日期:2026-04-26
DeepSeek 是公开技术诚意最高的实验室之一。从 V2 (MLA) → V3 (FP8 + DualPipe + 671B MoE) → V3.1 (混合 thinking) → V3.2 (Sparse Attention) → V4 (1.6T + 1M context) 一年走完三代架构突破。2025-02 Open Infra Week 一次性开源 7 个底层基础设施(FlashMLA、DeepEP、DeepGEMM、3FS、EPLB、profile-data、smallpond、DualPipe),把工程栈展开了。
本文按"读 paper 同时想能不能复现"的角度过一遍:架构 → 底层 infra → 训练 → 推理。
主要参考:
- DeepSeek-V2 Technical Report(MLA 起源)
- DeepSeek-V3 Technical Report
- DeepSeek-R1: Incentivizing Reasoning Capability via RL
- DeepSeekMath: GRPO
- DeepSeek Open Infra Index(2025-02)
- DeepSeek-V3.2-Exp Sparse Attention(2025-12)
一、模型版本对照(V1 → V4)¶
每代关键架构数字。每 cell 一行,详细配置和 cite 见表下方。
| 模型 | 发布 | 总参数 | 激活 | 层 | Hidden | Attention | Experts | Shared | Top-K | Vocab | Context |
|---|---|---|---|---|---|---|---|---|---|---|---|
| LLM-67B | 2024-01 | 67B | 67B | 95 | 8192 | GQA-8 | — | — | — | 102k | 4k |
| MoE-16B | 2024-01 | 16.4B | 2.8B | 28 | 2048 | MHA | 64 | 2 | 6 | 102k | 4k |
| V2 | 2024-05 | 236B | 21B | 60 | 5120 | MLA | 160 | 2 | 6 | 102k | 128k |
| V2.5 | 2024-09 | 236B | 21B | 60 | 5120 | MLA | 160 | 2 | 6 | 102k | 128k |
| Coder-V2 | 2024-07 | 236B | 21B | 60 | 5120 | MLA | 160 | 2 | 6 | 102k | 128k |
| V3 | 2024-12 | 671B | 37B | 61 | 7168 | MLA | 256 | 1 | 8 | 128k | 128k |
| R1 | 2025-01 | 671B | 37B | 61 | 7168 | MLA | 256 | 1 | 8 | 128k | 128k |
| V3.1 | 2025-08 | 671B | 37B | 61 | 7168 | MLA | 256 | 1 | 8 | 128k | 128k |
| V3.2 | 2025-12 | 685B | ~37B | 61 | 7168 | DSA | 256 | 1 | 8 | 128k | 128k |
| V4 Pro | 2026-04 | 1.6T | 49B | 61 | n/a | CSA+HCA | n/a | n/a | n/a | n/a | 1M |
| V4 Flash | 2026-04 | 284B | 13B | n/a | n/a | CSA+HCA | n/a | n/a | n/a | n/a | 1M |
源:V2 paper · V3 paper · R1 paper · V3.2 HF card · V4-Pro HF card · V4 tech report PDF(58 页)
关键配置详解(每列 < 80 字塞不下的内容放这里):
- MLA(V2+):\(d_c=512\)(latent dim),\(d_h=128\)(per-head),V3 用 \(n_h=128\) heads;V2 用 \(n_h=128\) 但 latent KV head 数不同
- DSA(V3.2):DeepSeek Sparse Attention,fine-grained sparse 选择,长 context 显著提速;保留 MLA 底层
- CSA + HCA hybrid(V4):双引擎 attention
- CSA:Compressed Sparse Attention,小窗口 \(m=4\),sparse top-k 选择
- HCA:Heavily Compressed Attention,大块 \(m'=128\),dense attention
- 效果:1M context 下 KV cache 仅为 BF16 GQA8 baseline 的 2%,FLOPs 仅为 V3.2 的 27%
- V4 训练:32T tokens,Muon optimizer(Newton-Schulz orthogonalize),FP4 master quant + FP8 compute 原生低精度训练
- V4 layer 数:61(与 V3 同),其他细节(hidden / expert 数 / top-k)官方 tech report 还在分析中
- V3 训练:14.8T tokens,AdamW + ZeRO-1,FP8 + DualPipe
几个 pattern:
- MoE 经济化:V2 → V3,总参数 ×2.8 但激活 ×1.7,expert 数 ×1.6
- Attention 演化:MHA(67B) → GQA(67B) → MLA(V2-V3.1) → DSA(V3.2) → CSA+HCA(V4)
- Context 增长:4k(V1) → 128k(V2 YaRN) → 1M(V4 hybrid attention)
- 训练 token:V2 8.1T → V3 14.8T → V4 32T+
- 训练精度:BF16(V2) → FP8 fine-grained(V3) → FP4 master + FP8 compute(V4)
flowchart LR
in["Token IDs<br/>[B, S]"] --> emb["Embedding<br/>[B, S, 7168]"]
emb --> dense["Dense Block × 3"]
dense --> moe["MoE Block × 58<br/>(MLA + Routed-MoE + Shared-Expert)"]
moe --> norm[RMSNorm]
norm --> head["LM Head + MTP heads<br/>(deep n=1 lookahead)"]
classDef stage fill:#fff,stroke:#cc785c,color:#1a1a1a;
classDef io fill:#f5f3eb,stroke:#bdb9ab,color:#1a1a1a;
class in,emb,norm io
class dense,moe,head stage
为什么前 3 层 dense:MoE 早期路由不稳定,dense warm-up 让 token rep 先聚焦再被分到 expert。这跟 Mixtral 的"全 MoE"策略不同,是 V3 的工程经验取舍。
二、MLA — Multi-Head Latent Attention¶
MLA 是 V3 在 KV Cache 维度上的核心创新。从 V2 开始引入,V3 沿用并扩展。
2.1 出发点¶
标准 MHA 推理时 KV Cache 大小:\(2 \cdot n_{kv} \cdot d_h \cdot S \cdot \text{layers}\)。70B 模型 128k context 单 sequence KV 占几十 GB,是 long-context 推理的最大瓶颈。
GQA / MQA 通过减少 \(n_{kv}\)(共享 K/V 头)来压缩,但表达能力下降。MLA 走另一路:把 K/V 投到一个低维 latent,再 up-project 出多头,等价于把 KV Cache 压成 latent 向量。
MHA / GQA / MLA 三种 attention 的 KV cache 结构对比:
┌─────────────────────────────────────────────────────────────────┐
│ MHA: 每个 head 独立 K/V │
│ │
│ h ──┬─→ W_K^1 ──→ k¹ ┐ │
│ ├─→ W_V^1 ──→ v¹ │ │
│ │ │ │
│ ├─→ W_K^2 ──→ k² │ cache 全部 n_h 个 │
│ ├─→ W_V^2 ──→ v² ├─→ KV size = 2 · n_h · d_h │
│ │ │ = 2 × 128 × 128 = 32768/token │
│ ├─→ W_K^h ──→ kʰ │ │
│ └─→ W_V^h ──→ vʰ ┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ GQA: n_kv 个 head 共享 K/V │
│ │
│ h ──┬─→ W_K^g ──→ k_g (n_kv 组) │
│ │ W_V^g ──→ v_g │
│ │ │
│ └─→ broadcast 到 n_h 个 query head │
│ │
│ cache size = 2 · n_kv · d_h │
│ = 2 × 8 × 128 = 2048/token (GQA-8) │
│ │
├─────────────────────────────────────────────────────────────────┤
│ MLA: down-project to latent, up-project on-the-fly │
│ │
│ h ──→ W_DKV ──→ c_KV (d_c=512) ← cache 这个 │
│ │ │
│ ├──→ W_UK ──→ k¹..kʰ (推理时实时算) │
│ └──→ W_UV ──→ v¹..vʰ │
│ │
│ cache size = d_c + d_R = 512 + 64 = 576/token (V3) │
│ │
│ 压缩比 vs MHA: 576/32768 = 1.7% (32×) │
└─────────────────────────────────────────────────────────────────┘
直观区别: - MHA:每个 head 自己一份 K/V → 全量缓存,但表达力最强 - GQA:n_kv 组共享 → 缓存压缩,但表达力损失 - MLA:缓存的是 latent,K/V 推理时再展开 → 缓存极小,且 K/V 展开是矩阵乘可融合到 Q 路径(§2.5),等价无开销
2.2 数学结构¶
记 hidden state \(h \in \mathbb{R}^{D}\),要拿到 \(n_h\) 个 head 的 K/V:
Down-projection(共享 latent):
其中 \(d_c \ll n_h \cdot d_h\)。V3 取 \(d_c = 512\),对比标准 MHA 的 \(128 \times 128 = 16384\),KV 维度压缩 32×。
Up-projection(per-head K/V):
注意:缓存里只存 \(c^{KV}\)(512 维),不存 \(k_i^C, v_i\)。推理时实时 up-project。
2.3 RoPE 的处理¶
RoPE 是位置依赖的旋转矩阵 \(R(m)\) 应用在 \(q\) 和 \(k\) 上。如果 K 是从 latent up-project 出来的,那 RoPE 只能加到 up-project 之后;但缓存的是 latent,意味着每次 attention 都要重算 RoPE(contradicting 缓存的初衷)。
V3 的解法 — decoupled RoPE:
其中 \(k_i^R, q_i^R\) 是专门承载位置信息的"位置头",从 \(h\) 单独投影出来 + 应用 RoPE:
\(k^R\) 在所有 head 间共享(类似 MQA),所以缓存 \(k^R\) 的代价小。注意力分数:
第一项可以用预乘技巧(吸收 \(W^{UK}\) 到 \(W^Q\) 里)做到与缓存 \(c^{KV}\) 同级别效率,第二项用缓存的 \(k^R\)。
MLA 完整数据流(含 decoupled RoPE):
flowchart LR
h["hidden h<br/>[B, S, D=7168]"]
h --> Wq["W^Q<br/>(已吸收 W^UK)"]
h --> Wdkv["W^DKV"]
h --> Wkr["W^KR<br/>(共享位置头)"]
h --> Wqr["W^QR"]
Wdkv --> ckv["c^KV<br/>[B, S, d_c=512]<br/><b>★ 缓存这个</b>"]
Wkr --> kr_pre["k^R pre-rotate<br/>[B, S, d_R=64]"]
kr_pre --> rope1["RoPE"]
rope1 --> kr["k^R<br/><b>★ 也缓存</b>"]
Wq --> qC["q^C<br/>[B, S, n_h, d_c]"]
Wqr --> qr_pre["q^R pre"]
qr_pre --> rope2["RoPE"]
rope2 --> qR["q^R"]
ckv --> dot1["q^C · c^KV<br/>(content score)"]
qC --> dot1
kr --> dot2["q^R · k^R<br/>(position score)"]
qR --> dot2
dot1 --> sum["+"]
dot2 --> sum
sum --> sm["softmax"]
sm --> attn["attn weights"]
ckv --> Wuv["W^UV"]
Wuv --> v["v"]
attn --> out["attn · v"]
v --> out
out --> Wo["W^O"]
Wo --> y["output"]
classDef cache fill:#fff5e8,stroke:#cc785c,color:#1a1a1a;
classDef op fill:#fff,stroke:#bdb9ab,color:#1a1a1a;
class ckv,kr cache
class h,Wq,Wdkv,Wkr,Wqr,kr_pre,qr_pre,rope1,rope2,qC,qR,dot1,dot2,sum,sm,attn,Wuv,v,out,Wo,y op
橙色框是唯一缓存的两个张量(\(c^{KV}\) 和 \(k^R\)),其它都是 on-the-fly 计算。
关键工程窍门:左下角 q^C · c^KV —— 朴素实现是 q 先 up-project 成 k,再点积;但因为 \(W^{UK}\) 是线性的,可以吸收到 W^Q 里:
原: score = (W^Q h)^T · (W^UK c^KV)
等价: score = ((W^UK)^T W^Q h)^T · c^KV
让 W^Q' = (W^UK)^T W^Q —— 离线预乘一次
此时 attention 等价于直接在 \(d_c\) 维点积,跟正常 attention 算 \(d_h\) 维点积是一回事,没多余 cost。这是 MLA"省 cache 不费 compute"的核心。
2.4 实际 KV 占用对比¶
128k context、70B 模型、bf16:
| 方法 | KV per token per layer | 70B/61 layers/128k |
|---|---|---|
| MHA | \(2 \cdot 128 \cdot 128 \cdot 2\) B = 64 KB | ~500 GB |
| GQA(8) | \(2 \cdot 8 \cdot 128 \cdot 2\) B = 4 KB | ~31 GB |
| MLA(V3) | \((512 + 64) \cdot 2\) B = 1.15 KB | ~9 GB |
KV 缓存数字是单序列估算;batched serving 时 prefill / decode 分离架构(PD 分离)会进一步降低有效占用。
2.5 实现陷阱¶
- Up-projection 矩阵融合:朴素实现每次 attention 都要做 \(c^{KV} \to k_i^C, v_i\) 的乘法,一头一矩阵。V3 把 \(W^{UK}_i\) 吸收到 \(W^Q_i\) 里:\(W^{Q,\text{new}}_i = (W^{UK}_i)^\top W^Q_i\)。这样 attention 等价为 \(q\) 直接和 \(c^{KV}\) 做点积(dim \(d_c\)),prefill 后只算 \(d_c\) 维内积,不再 up-project。
- 量化兼容性:吸收后的权重矩阵更大,per-channel quantization 的"channel"定义需重审视。现存 INT4/FP8 算子库(vLLM、TensorRT-LLM 早期版本)需要打补丁。
- 训练梯度路径:MLA 的 down-projection 是 bottleneck,如果 \(d_c\) 太小 accumulation 不够会把信息压坏。V3 选 \(d_c=512\) 是经验上限。
2.6 PyTorch 参考实现¶
下面是 MLA 的最小可运行 PyTorch 实现(教学用,不含 decoupled RoPE 优化)。改编自 rasbt/LLMs-from-scratch ch04/05_mla:
import torch
import torch.nn as nn
import math
class MultiHeadLatentAttention(nn.Module):
"""简化版 MLA:down-project to latent, up-project per-head K/V.
Production 版要加 decoupled RoPE + W^UK 吸收到 W^Q(见 §2.5)。"""
def __init__(self, d_in, d_out, num_heads, latent_dim=None):
super().__init__()
assert d_out % num_heads == 0
self.num_heads = num_heads
self.head_dim = d_out // num_heads
# latent_dim = d_c; V3 实测 d_c=512 是经验上限
self.latent_dim = latent_dim if latent_dim else max(16, d_out // 8)
self.W_query = nn.Linear(d_in, d_out) # Q 不压缩
self.W_DKV = nn.Linear(d_in, self.latent_dim) # 共享 down-project
self.W_UK = nn.Linear(self.latent_dim, d_out) # K up-project (per-head)
self.W_UV = nn.Linear(self.latent_dim, d_out) # V up-project (per-head)
self.out_proj = nn.Linear(d_out, d_out)
self.register_buffer("cache_c_kv", None, persistent=False)
self.cache_pos = 0
def reset_cache(self):
self.cache_c_kv = None
self.cache_pos = 0
def forward(self, x, use_cache=False):
B, S, _ = x.shape
Q = self.W_query(x) # (B, S, d_out)
c_kv = self.W_DKV(x) # (B, S, d_c) ← 缓存这个
if use_cache:
if self.cache_c_kv is None:
c_total = c_kv
else:
c_total = torch.cat([self.cache_c_kv, c_kv], dim=1)
self.cache_c_kv = c_total
else:
c_total = c_kv
K = self.W_UK(c_total) # 推理时实时 up-project
V = self.W_UV(c_total)
Q = Q.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
K = K.view(B, -1, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(B, -1, self.num_heads, self.head_dim).transpose(1, 2)
scores = (Q @ K.transpose(-2, -1)) / math.sqrt(self.head_dim)
# causal mask
s_q, s_k = Q.shape[-2], K.shape[-2]
q_pos = torch.arange(self.cache_pos, self.cache_pos + s_q, device=x.device)
k_pos = torch.arange(s_k, device=x.device)
scores = scores.masked_fill(q_pos[:, None] < k_pos[None, :], float('-inf'))
if use_cache:
self.cache_pos += s_q
attn = torch.softmax(scores, dim=-1)
out = (attn @ V).transpose(1, 2).contiguous().view(B, S, -1)
return self.out_proj(out)
KV cache 大小:朴素 MHA 缓存 (n_h × head_dim × 2),MLA 只缓存 d_c → 压缩比 ≈ d_c / (n_h × head_dim × 2)。V3 配置:512 / (128×128×2) = 1.5%,对应 §2.4 的 64 KB → 1.15 KB per token per layer。
Production-grade MLA:
- 上面 reference 是教学版,真 production 用 FlashMLA kernel:H800 上 BF16 580 TFLOPS / 3000 GB/s 内存 bound
- vLLM 0.6+ / SGLang 0.3+ / TensorRT-LLM 都已合并 MLA 支持
- V3 实际跑的 attention 等价于 \(q\) 与 \(c^{KV}\) 直接做内积(吸收 \(W^{UK}\) 到 \(W^Q\) 后),decoupled RoPE 走单独 head
三、MoE 架构¶
flowchart LR
h["token h<br/>[B, S, 7168]"]
gate["Gate W_r<br/>logits [256]"]
bias["+ bias b_i<br/>(load balance)"]
topk["Top-8 routed<br/>3.1% 稀疏率"]
shared["Shared Expert<br/>always on"]
e1["Routed E_1"]
e2["Routed E_2"]
edot["…"]
e8["Routed E_8"]
sum["加权求和<br/>(softmax score × FFN out)"]
out["输出 [B, S, 7168]"]
h --> gate --> bias --> topk
h --> shared --> sum
topk --> e1 --> sum
topk --> e2 --> sum
topk --> edot --> sum
topk --> e8 --> sum
sum --> out
classDef stage fill:#fff,stroke:#cc785c,color:#1a1a1a;
class h,gate,bias,topk,shared,e1,e2,edot,e8,sum,out stage
3.1 256 expert × top-8 + 1 shared¶
V3 MoE 设计:
- 每层 256 个 routed expert + 1 个 shared expert
- 每个 token 选 top-8 routed + always-on shared = 9 个 expert 算
- 路由 score:\(g_i = \text{softmax}(W_r h)_i\),取 top-8 indices
- 稀疏率:8/256 = 3.1%(V3 设计哲学是"足够稀疏 + 足够多 expert")
跟其他实验室对比:
| 模型 | 总 expert | top-k | shared | 稀疏率 | 总/激活 |
|---|---|---|---|---|---|
| Mixtral 8x7B | 8 | 2 | 0 | 25% | 47B / 13B |
| Mixtral 8x22B | 8 | 2 | 0 | 25% | 141B / 39B |
| DeepSeek-V3 | 256 | 8 | 1 | 3.1% | 671B / 37B |
| Qwen3-MoE 235B | 128 | 8 | 0 | 6.3% | 235B / 22B |
| Kimi K2 | 384 | 8 | 1 | 2.1% | 1.04T / 32B |
DeepSeek 在 V2 已经验证了细粒度路由(更多更小的 expert)泛化更好;V3 拉到 256 是该思想的 scale-up。
3.2 Auxiliary-loss-free load balancing¶
经典 MoE 训练用 auxiliary loss 强制路由均匀(鼓励所有 expert 被调用)。这个 loss 跟主 loss 之间需要权重调整,难调还会损害模型容量。V3 用bias term update:
\(b_i\) 是每个 expert 的 bias,按以下规则更新(不参与梯度,纯 EMA 控制)。完整 reference 实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class DeepSeekV3MoEGate(nn.Module):
"""V3 的 auxiliary-loss-free routing gate.
关键: bias b_i 不进 backward,每 step 后按 expert load 累计更新.
"""
def __init__(self, hidden_dim, n_experts=256, top_k=8, bias_lr=1e-3):
super().__init__()
self.n_experts = n_experts
self.top_k = top_k
self.bias_lr = bias_lr # γ in paper
# 路由权重 (这个进 backward)
self.router = nn.Linear(hidden_dim, n_experts, bias=False)
# Expert bias (不进 backward)
self.register_buffer("expert_bias", torch.zeros(n_experts))
# Tracking expert 调用频率(用于 update bias)
self.register_buffer("expert_load", torch.zeros(n_experts))
self.register_buffer("step_count", torch.zeros(1))
def forward(self, x): # x: [B, S, D]
# 1. compute routing logits
logits = self.router(x) # [B, S, n_experts]
# 2. add bias for routing (但 bias 不进 grad)
biased_logits = logits + self.expert_bias.detach()
# 3. top-k select
top_k_logits, top_k_idx = biased_logits.topk(self.top_k, dim=-1)
# softmax 只在 top-k 上做(关键:用 unbiased logits 算 weight)
unbiased_top_k = logits.gather(-1, top_k_idx)
gates = F.softmax(unbiased_top_k, dim=-1) # [B, S, top_k]
# 4. 累计 expert 调用次数(推理时不更新)
if self.training:
with torch.no_grad():
# 统计每个 expert 被选了多少次
flat_idx = top_k_idx.flatten()
load = torch.bincount(flat_idx, minlength=self.n_experts).float()
self.expert_load += load
self.step_count += 1
return gates, top_k_idx # [B, S, top_k] each
@torch.no_grad()
def update_bias(self):
"""每 N step 后调一次。让"火"的 expert bias 降低,"冷"的升。"""
avg_load = self.expert_load.mean()
# 每个 expert: 若高于均值 → bias 降;低于均值 → bias 升
delta = torch.where(
self.expert_load > avg_load,
-self.bias_lr,
self.bias_lr,
)
self.expert_bias += delta
# reset load tracker
self.expert_load.zero_()
self.step_count.zero_()
为什么这样比 aux loss 好:
| 维度 | Aux loss(GShard / Switch) | V3 bias-only |
|---|---|---|
| Backward 干扰 | ★★★ 有,与主 loss 权重打架 | 无(bias 不进 grad) |
| Hyperparameter | 1-2(loss 权重 + capacity factor) | 1(bias_lr γ) |
| Capacity 损失 | 强制均匀 → 难学专家专精 | 软推动 → 允许 expert 适度专精 |
| Cold-start | 需要 warm-up 阶段 | bias=0 初始即可 |
陷阱 / 已知问题:
- 推理 deploy 时仍然用 biased logits(
router_logits + bias),不是 raw logits —— 因为 expert 已经按 biased 分布学专精 - domain shift 时 bias 收敛慢(数千 step 才稳定),跨 domain CPT 时需重置 bias
- \(\gamma\)(bias_lr)取太大震荡,太小不收敛 —— V3 paper Section 4.2 给的是
1e-3(每 step)
3.3 通信成本¶
MoE 训练的瓶颈往往是 all-to-all:每个 token 选 top-8 expert,分散到 8 张不同 GPU 跑,结果再汇总。V3 用 EP(Expert Parallel)+ DualPipe(见 §五)把通信掩盖在计算后面。
通信量估算(per token):
- Forward dispatch:\(2 \cdot d_c \cdot \text{topk}\) bytes(用 fp16)
- Backward gather:同上
671B / 37B activation 实际 forward + backward all-to-all 单步通信量约 1-2 MB/token(待核实,跟具体 batch shape 有关)。
四、FP8 训练 stack¶
V3 是首个把 FP8 训练验证到 671B 规模的开源工作。FP8 有两种格式:E4M3(高精度,小动态范围)、E5M2(大范围,低精度)。V3 选 E4M3 全场景(训练 forward / backward / 权重 / 激活)。
4.1 为什么不用 E5M2¶
NVIDIA Transformer Engine 默认 forward 用 E4M3、backward 用 E5M2(FP8 paper 推荐)。V3 全 E4M3 的理由:
- fine-grained scaling(见 §4.2)让动态范围问题被 tile-level scale 吸收
- E4M3 的精度(mantissa=3)比 E5M2(mantissa=2)多一倍有效 bit
- 反向传播的 gradient 量级用 tile-level scaling 控制,不需要 E5M2 的大范围
4.2 Per-tile fine-grained scaling¶
朴素 FP8:每个 tensor 一个 scale factor。问题:tensor 内部不同位置的数值范围可能差几个数量级,单一 scale 必然牺牲 outlier 或 small value。
V3:
- 激活 (act):每 1×128 tile 一个 scale
- 权重 (W):每 128×128 tile 一个 scale
flowchart LR
subgraph act["激活 act [S, H]"]
a1["1×128 tile<br/>scale_a1"]
a2["1×128 tile<br/>scale_a2"]
a3["… 沿 hidden 切"]
end
subgraph wt["权重 W [H, H']"]
w1["128×128<br/>scale_w1"]
w2["128×128<br/>scale_w2"]
w3["… 块状切"]
end
matmul["FP8 matmul<br/>FP8 × FP8"]
accum["BF16 / FP32<br/>accumulator"]
out["BF16 输出"]
act --> matmul
wt --> matmul
matmul --> accum --> out
classDef stage fill:#fff,stroke:#cc785c,color:#1a1a1a;
class a1,a2,a3,w1,w2,w3,matmul,accum,out stage
# 概念伪代码(V3 paper Section 3.3)
def fp8_quantize(x, tile_h, tile_w):
"""x: [H, W] tensor"""
scales = []
quantized = torch.zeros_like(x, dtype=torch.float8_e4m3fn)
for i in range(0, x.shape[0], tile_h):
for j in range(0, x.shape[1], tile_w):
tile = x[i:i+tile_h, j:j+tile_w]
# tile-level absmax → scale
scale = tile.abs().amax() / FP8_MAX # FP8_MAX = 448 for E4M3
quantized[i:i+tile_h, j:j+tile_w] = (tile / scale).to(torch.float8_e4m3fn)
scales.append(scale)
return quantized, scales
关键点:1×128 vs 128×128 不对称是有理由的。激活是 sequence × hidden,sequence 维度上的位置往往有长程相关,用 1×128 在 hidden 维度切;权重是各向同性,128×128 块状切更省 scale 数量。
4.3 高精度累加¶
FP8 矩阵乘 → BF16 / FP32 累加器。这是 NVIDIA Hopper Tensor Core 原生支持:
V3 在 TileLang / 自研 kernel 里强制累加用 BF16(FP32 太慢),关键 reduction(如 softmax 分母)用 FP32。
4.4 Outlier 控制¶
V3 没有 SmoothQuant 那种"激活 → 权重难度转移",因为 fine-grained scaling 已经把 outlier 局部化到单个 tile。但报告里提到一些经验:
- Embedding / LM head 不量化(保持 BF16)
- 训练初期 1k step 用 BF16 warm-up,避免 FP8 噪音破坏初始化
- Loss spike 检测时回退到 BF16 重训该 batch
4.5 复现度评估¶
| 组件 | 公开度 | 是否能直接用 |
|---|---|---|
| FP8 算子 | 部分(FlashMLA 公开,但端到端 stack 没全开) | 需要拼 |
| Per-tile scaling | 描述充分 | 可实现,需写 CUDA / Triton |
| 训练 hyperparam | 给了 LR schedule, batch | 可复现规模化 |
| Outlier 检测策略 | 没细节 | 需自己摸 |
五、DualPipe — 双向 pipeline 并行¶
5.1 为什么需要新 schedule¶
Megatron 1F1B (one-forward-one-backward) 在 P stages 上 bubble = \((P-1)/(\text{micro-batches})\)。256 expert MoE 训练时 micro-batch 受限于 expert 显存,bubble 占总时间 5-10%。V3 用 DualPipe 把这个 bubble 压到接近 0。
5.2 核心思想¶
把 forward 和 backward 看成两条数据流(一条往前,一条往后),让它们在 pipeline 里对穿:
flowchart LR
subgraph t1["时刻 t"]
g0t1["GPU0<br/>F4 + B1"]
g1t1["GPU1<br/>F3 + B2"]
g2t1["GPU2<br/>F2 + B3"]
g3t1["GPU3<br/>F1 + B4"]
end
subgraph t2["时刻 t+1"]
g0t2["GPU0<br/>F5 + B2"]
g1t2["GPU1<br/>F4 + B3"]
g2t2["GPU2<br/>F3 + B4"]
g3t2["GPU3<br/>F2 + B5"]
end
g0t1 -.send act.-> g1t1
g1t1 -.send act.-> g2t1
g2t1 -.send act.-> g3t1
g3t1 -.send grad.-> g2t1
g2t1 -.send grad.-> g1t1
g1t1 -.send grad.-> g0t1
t1 ==> t2
classDef stage fill:#fff,stroke:#cc785c,color:#1a1a1a;
class g0t1,g1t1,g2t1,g3t1,g0t2,g1t2,g2t2,g3t2 stage
每个 GPU 同时承担 forward 的某 micro-batch 和 backward 的另一 micro-batch,bubble ≈ 0。
每个 GPU 在任意时刻同时跑 forward 的某个 micro-batch 和 backward 的另一个 micro-batch,计算 + 通信通过精心安排时序完全重叠。
5.3 关键工程点¶
- 流水线 stage 数 = expert 通信周期的整数倍 —— 让 dispatch / gather 完美对齐
- 激活内存 :DualPipe 需要存的 activation 比 1F1B 多(两条流),但 V3 把 last layer 激活重计算(recompute)省回来
- PTX 手工 kernel:通信 + 计算重叠用普通 cuBLAS / NCCL 做不到精细时序,V3 直接写 PTX 控制 SM 调度(见 V3 paper Acknowledgements 提及)
5.4 vs Megatron interleaved 1F1B¶
| 维度 | 1F1B | Interleaved 1F1B | DualPipe |
|---|---|---|---|
| Bubble | \((P-1)/M\) | \((P-1)/(M \cdot V)\) | ~0 |
| 激活内存 | 1× | \(V \times\) | 2× |
| 通信复杂度 | 简单 | 中 | 高 |
| 适用规模 | 小到中 | 大 | 极大(千卡 +) |
\(V\) = virtual stage 数;\(M\) = micro-batch 数;\(P\) = pipeline stages。
六、跨节点通信优化¶
6.1 All-to-All 是瓶颈¶
MoE 训练最大的通信开销是expert 路由的 all-to-all:每个 token 要发到 8 个不同 expert(在不同 GPU 上)跑 FFN,然后把结果收回。standard NCCL all-to-all 在 IB 网络上 latency 高、吞吐低。
flowchart LR
subgraph node1["Node 1 (NVLink 域)"]
g0["GPU0<br/>tokens A,B,C"]
g1["GPU1<br/>tokens D,E,F"]
end
subgraph node2["Node 2 (NVLink 域)"]
g2["GPU2<br/>experts 0-63"]
g3["GPU3<br/>experts 64-127"]
end
g0 -.NVLink<br/>900GB/s.-> g1
g0 ==IB 400Gb/s==> g2
g0 ==IB 400Gb/s==> g3
g1 ==IB==> g2
g1 ==IB==> g3
classDef stage fill:#fff,stroke:#cc785c,color:#1a1a1a;
class g0,g1,g2,g3 stage
V3 优化点:尽量让热门 expert 留在同一 NVLink 域;dispatch 用 FP8、gather 用 BF16;用 PTX 让 all-to-all 与 attention 重叠(部分 SM 跑通信,剩余 SM 跑算)。
6.2 V3 的优化¶
手写 PTX kernel:直接控制 SM 切片,让 all-to-all 通信占用一部分 SM,剩余 SM 同时跑 attention 计算。绕开 cuBLAS / NCCL 调度开销。
topology-aware routing:尽量把热门 expert 放在同一 NVLink 域内,跨 IB 流量减半(待核实,paper 提及但细节不足)。
FP8 通信:dispatch 数据用 FP8 传,gather 结果用 BF16 传(梯度精度需要)。
6.3 集群拓扑假设¶
V3 训练集群(公开声明):
- 2048 H800 GPU(H100 中国阉割版,NVLink 受限)
- NVLink 8-way 节点内
- IB 200/400 GbE 节点间
H800 vs H100 的 NVLink 减半(300 GB/s vs 600 GB/s),DualPipe + 手写 PTX 的核心动机就是把这个瓶颈干掉。
七、Multi-Token Prediction (MTP)¶
V3 在每层 LM head 之外加了一个 MTP head:
flowchart LR
backbone["Backbone<br/>h_t (主隐藏态)"]
norm1["RMSNorm(h_t)"]
norm2["RMSNorm(Emb(x_{t+1}))"]
cat["concat"]
trm["TRM⁽¹⁾<br/>1 层 transformer block"]
h1["h_t⁽¹⁾"]
main_head["LM Head<br/>→ token_{t+1}"]
mtp_head["MTP Head⁽¹⁾<br/>→ token_{t+2}"]
loss["L = L_main + λ·L_MTP<br/>λ=0.3"]
backbone --> main_head
backbone --> norm1
norm1 --> cat
norm2 --> cat
cat --> trm --> h1 --> mtp_head
main_head --> loss
mtp_head --> loss
classDef stage fill:#fff,stroke:#cc785c,color:#1a1a1a;
class backbone,norm1,norm2,cat,trm,h1,main_head,mtp_head,loss stage
训练时同时预测 \(t+1\) 和 \(t+2\),loss 加权求和。两个动机:
- 稠密化训练信号 —— 每个 token 提供 2 个预测目标,等效 batch 翻倍
- 天然 speculative decoding 信号 —— 推理时 MTP head 的输出可以做 1-step 投机(类似 EAGLE-style),加速 decode
V3 报告里 MTP 让 token efficiency 提升约 1.8×(上下文相关,待核实具体数)。
7.1 MTP 数学结构¶
V3 的 MTP 不是独立预测每个未来位置,而是串行依赖 —— 第 \(k\) 个 MTP head 的输入是第 \(k-1\) 个 head 的隐藏状态 + 当前 token embedding:
其中 TRM 是单层小 transformer block(自带 attention + FFN),\(\Vert\) 是 concat。
V3 用 D=1(只看 t+1 一个未来位置)。Loss:
\(\lambda\) 是 MTP loss 权重(V3 用 0.3,比 main loss 小)。
7.2 MTP PyTorch 参考实现¶
import torch
import torch.nn as nn
class MTPHead(nn.Module):
"""V3 single-depth MTP head.
Input:
h_main: [B, S, D] 主 backbone 输出
next_embed: [B, S, D] 下一个 token 的 embedding (训练时已知)
Output:
logits: [B, S, V] 对 t+1 位置的预测
"""
def __init__(self, hidden_dim, vocab_size, lm_head=None):
super().__init__()
# RMSNorm 前置
self.norm_h = nn.RMSNorm(hidden_dim)
self.norm_e = nn.RMSNorm(hidden_dim)
# 拼接后 dim = 2D,用 W_combined 压回 D
self.W_combined = nn.Linear(2 * hidden_dim, hidden_dim)
# 单层 transformer block (attention + FFN)
self.block = TransformerBlock(hidden_dim) # 复用主网络的 block
# LM head 通常和主网络共享(weight tying,省 V*D 参数)
self.lm_head = lm_head if lm_head else nn.Linear(hidden_dim, vocab_size, bias=False)
def forward(self, h_main, next_embed):
# 1. norm + concat
x = torch.cat([self.norm_h(h_main), self.norm_e(next_embed)], dim=-1)
# 2. project back to D
x = self.W_combined(x)
# 3. 走一层 transformer
x = self.block(x)
# 4. LM head
return self.lm_head(x)
class TransformerWithMTP(nn.Module):
def __init__(self, base_model, mtp_depth=1, mtp_loss_weight=0.3):
super().__init__()
self.base = base_model
self.mtp_depth = mtp_depth
self.lambda_mtp = mtp_loss_weight
# 共享 lm_head + embed
self.mtp_heads = nn.ModuleList([
MTPHead(base_model.hidden_dim, base_model.vocab_size, base_model.lm_head)
for _ in range(mtp_depth)
])
def forward(self, ids):
# base forward
h = self.base.embed(ids)
h = self.base.transformer(h) # [B, S, D]
main_logits = self.base.lm_head(h) # [B, S, V]
# MTP forward (只在训练时)
mtp_logits_list = []
if self.training:
h_prev = h
for k, head in enumerate(self.mtp_heads):
# 第 k 个 head 看 t+k+1 位置的 embed
shift = k + 1
next_emb = self.base.embed(ids[:, shift:]) # [B, S-shift, D]
# h_prev 也要对齐
h_aligned = h_prev[:, :-shift] if shift > 0 else h_prev
logits_k = head(h_aligned, next_emb)
mtp_logits_list.append(logits_k)
h_prev = h_aligned # 串行:下一 head 用这个的输入(粗略)
return main_logits, mtp_logits_list
def compute_loss(self, ids, main_logits, mtp_logits_list):
# main loss: predict t+1
main_loss = F.cross_entropy(
main_logits[:, :-1].reshape(-1, main_logits.size(-1)),
ids[:, 1:].reshape(-1),
)
# MTP loss: predict t+k+1 for each head
mtp_loss = 0
for k, mtp_logits in enumerate(mtp_logits_list):
shift = k + 2 # t+k+1, prediction shift = k+2
mtp_loss += F.cross_entropy(
mtp_logits.reshape(-1, mtp_logits.size(-1)),
ids[:, shift:].reshape(-1),
)
mtp_loss = mtp_loss / len(mtp_logits_list)
return main_loss + self.lambda_mtp * mtp_loss
7.3 推理时如何用 MTP head 做 speculative decoding¶
# Decode 阶段:主模型生成 t+1,MTP head 同时给 t+2 候选
def speculative_decode_step(model, prompt_ids, n_speculate=1):
# 1. 主模型 forward 一步,拿到 h_t 和 logits_t
h, main_logits = model.base.forward_step(prompt_ids)
next_token = main_logits.argmax(-1)
# 2. MTP head 用 h_t + next_token 的 embed 预测 t+2
next_embed = model.base.embed(next_token)
mtp_logits = model.mtp_heads[0](h, next_embed)
speculated = mtp_logits.argmax(-1) # 候选 t+2
# 3. 用主模型 verify:拿 [next_token, speculated] 跑 forward,
# 看主模型对 t+2 位置的预测是不是 speculated
candidate_ids = torch.cat([prompt_ids, next_token.unsqueeze(0), speculated.unsqueeze(0)], -1)
h_v, main_v = model.base.forward(candidate_ids)
verified = main_v[:, -1].argmax(-1)
if verified == speculated:
return [next_token, speculated] # 一步输出 2 token
else:
return [next_token, verified] # MTP 错了,用主模型预测
V3 实测:MTP head 接受率 ~85-90%(V3 paper Section 4.1),等效 1.8× decode 加速。
八、Inference engineering¶
8.1 PD 分离¶
V3 推理时 prefill / decode 分离部署:
- Prefill workers:H200 / 计算密集型集群,专跑长 prompt 的 attention
- Decode workers:MoE 专用,bandwidth 敏感,跑 routed expert
- KV transfer:prefill 产出的 KV cache 经 RDMA 传给 decode worker
8.2 Decode 优化¶
256 expert × top-8 在 decode 时是高度稀疏的:每 batch 不同 user 选不同 expert,expert utilization 可能只有 30-50%。V3 在 inference 阶段:
- expert 静态调度:根据 prompt 类别(代码 / 数学 / 普通对话)pre-load 高频 expert
- MoE batch packing:把同 expert 的 token 打包跑(类似 vLLM PagedAttention 但 expert 维度)
8.3 速度数字¶
公开 benchmark(V3, FP8, 128k context):
| 阶段 | 吞吐 | 备注 |
|---|---|---|
| Prefill | ~10k tokens/s/GPU | H100 |
| Decode (single user) | ~60-80 tokens/s | H100 |
| Decode (batched) | ~3000 tokens/s 总 | 待核实 |
九、Open Infra Index(2025-02 开源周)¶
DeepSeek 在 V3 / R1 之后,2025-02 用一周时间开源了整套训推 infra,是公开技术诚意最高的一批 release。Index 在 github.com/deepseek-ai/open-infra-index。
flowchart LR
subgraph storage["存储层"]
fs["3FS<br/>6.6 TiB/s"]
sp["Smallpond<br/>数据处理"]
end
subgraph compute["计算层"]
gemm["DeepGEMM<br/>FP8 1350+ TFLOPS"]
mla["FlashMLA<br/>3000 GB/s"]
end
subgraph comm["通信层"]
eep["DeepEP<br/>FP8 dispatch"]
eplb["EPLB<br/>负载均衡"]
end
subgraph orch["编排层"]
dual["DualPipe<br/>bubble≈0"]
prof["Profile-data<br/>真实 trace"]
end
storage --> compute --> comm --> orch
classDef stage fill:#fff,stroke:#cc785c,color:#1a1a1a;
class fs,sp,gemm,mla,eep,eplb,dual,prof stage
按层次叠起来就是一套可复现的 671B-MoE 训推栈,几乎没有"黑盒"——除了真实训练数据本身。
9.1 全清单¶
| 项目 | 类型 | 性能 | 用途 |
|---|---|---|---|
| FlashMLA | Hopper attention kernel | 3000 GB/s mem / 580 TFLOPS BF16 | MLA 推理加速(取代 vLLM 默认) |
| DeepEP | EP 通信库 | NVLink + RDMA 双 path,FP8 dispatch | MoE all-to-all(解 V3 §六瓶颈) |
| DeepGEMM | FP8 GEMM 库 | 1350+ TFLOPS,core ~300 行 | V3/R1 训推全用 |
| DualPipe | Pipeline schedule | 双向 + 通信掩盖 | V3 § 五完整开源 |
| EPLB | Expert Parallel Load Balancer | 减 EP 长尾 | 跟 §3.2 配合使用 |
| Profile-Data | Profiling 数据集 | 真实 V3/R1 trace | 复现优化的参考 |
| 3FS | 分布式文件系统 | 6.6 TiB/s read,180 节点 | 训练数据 + KVCache 共享存储 |
| Smallpond | 数据处理框架 | 基于 3FS | DataLoader / dedup / mix |
生产数据(V3/R1 在线服务):
- 每 H800 节点:input 73.7K tokens/s,output 14.8K tokens/s
- cost-profit margin: 545%(按理论最大 token throughput 计费 vs 实际成本)
9.2 FlashMLA(Day 1,attention kernel)¶
V3 inference 的关键瓶颈是 MLA 在 Hopper 上的 fused kernel。FlashMLA 是 DeepSeek 自家版的 FlashAttention 等价物,专门为 MLA 优化:
- Variable-length sequence:batch 内不同 seq 长度不需要 padding,省 KV cache
- Paged KV cache:block size = 64,跟 vLLM PagedAttention 类似
- BF16 / FP8 双精度
- H800 实测:3000 GB/s 内存带宽 bound(≈ HBM3 理论上限 95%);580 TFLOPS BF16 计算 bound
工程上:vLLM / SGLang 在 0.6+ / 0.4+ 都已经 vendor 进 FlashMLA,是当前 V3 部署的事实标准。
9.3 DeepEP(Day 2,MoE 通信)¶
MoE 训推的 all-to-all 是最大瓶颈。DeepEP 是第一个开源的 EP 专用通信库:
- 训练版 kernel:高吞吐,掩盖在 backward 计算下
- 推理版 kernel:低延迟,专为 decode 优化
- NVLink + RDMA dual-path:单机内走 NVLink,跨机走 RDMA,不用 NCCL 通用 path
- Native FP8 dispatch:dispatch 阶段 token activations 直接 FP8,省 2× 带宽
- Flexible GPU resource control:可以指定多少 SM 给通信,不与计算抢 SM
实测(DeepSeek 自报):相比 NCCL 默认 all-to-all,DeepEP MoE 通信延迟降 30-50%。
9.4 DeepGEMM(Day 3,FP8 矩阵乘)¶
V3 训练的 90% 算力在 GEMM 上。DeepGEMM 是 V3/R1 production 用的 FP8 GEMM 库:
- Core 仅 ~300 行 CUDA 代码 —— 极简
- JIT 编译:根据矩阵 shape 自动生成 kernel
- Dense + MoE GEMM 双支持(MoE GEMM 是不规则 shape,单独优化)
- 1350+ TFLOPS on H800(接近 NVIDIA 自家 cuBLAS 上限)
为什么短:DeepSeek 的策略是只在 V3 实际用到的 shape 上极致优化,不做通用库。300 行覆盖 ~95% 训练时间。
参考实现:github.com/deepseek-ai/DeepGEMM。读 source code 是工程艺术品。
9.5 EPLB(Day 4,专家负载均衡)¶
§3.2 讲的 auxiliary-loss-free 是算法层面的 LB;EPLB 是通信层面的:
- Hierarchical balancing:跨节点 vs 节点内不同策略
- Replicated experts:高频 expert 复制到多 GPU,降低单 GPU 拥塞
- Dynamic placement:根据 traffic profile 重新分布 expert 到 GPU
实操上 EPLB 跟 DeepEP 配合用:DeepEP 负责 token dispatch,EPLB 负责 expert placement。
9.6 3FS(Day 5,存储层)¶
训练超大模型的隐性瓶颈:数据 IO + checkpoint IO + KV cache 共享。3FS 把这三个场景统一:
- 聚合 read 6.6 TiB/s(180 节点)—— 数据 loader 不再阻塞
- GraySort 3.66 TiB/min —— 数据预处理加速
- 40+ GiB/s per-client KVCache lookup —— inference 时跨实例共享 KV cache
- Disaggregated 架构:compute / storage 解耦,扩展性好
为啥不用现成的 Lustre / GPFS:训推混合 workload + RDMA full-utilization 需要专设计。3FS 跟 NVIDIA Magnum IO / Storage Fabric 是同类工具但更贴 LLM 场景。
9.7 Smallpond(Day 5,数据处理)¶
基于 3FS 的数据处理框架,做 LLM pretrain data 的 dedup / mix / shuffle / shard。比 Spark 更窄但更快。
9.8 复现优先级建议¶
不必全套 adopt。最有价值的 3 个:
- FlashMLA:直接 vendor 进 vLLM/SGLang,零成本免费提速
- DeepGEMM:H100/H800 训练栈插入,自家 cuBLAS / TE 替换
- DeepEP:MoE 训练 > 100B 才有显著收益
TileKernels(疑似命名):DeepSeek 没单独 release 一个叫 "TileKernels" 的项目,社区有时这样称呼可能是指 TileLang(Microsoft)—— DeepGEMM 部分思路与 TileLang 类似(tile-level scaling + JIT)。可以读 DeepGEMM source code 学。
十、跟 Kimi K2 / Qwen-3 / LLaMA 的横向比较¶
V3 是 2024.12 发布,2025 一年内开源 MoE 生态的新参考:
| 设计选择 | DeepSeek-V3 | Kimi K2 | Qwen3-MoE 235B | 设计取舍 |
|---|---|---|---|---|
| 总参数 | 671B | 1.04T | 235B | 越大越能 scale,但 serve 成本 |
| 激活参数 | 37B | 32B | 22B | 推理成本主因 |
| Attention | MLA | MLA | GQA | MLA 长 context 优势,GQA 简单 |
| 路由 | 256 ex × top-8 | 384 × top-8 | 128 × top-8 | 越细粒度越泛化 |
| Optimizer | AdamW + ZeRO-1 | MuonClip | AdamW | MuonClip 是 K2 创新 |
| FP8 训练 | ✅ 全栈 | ✅ | 部分(Hybrid) | V3 验证可行性后通用 |
| Pipeline | DualPipe | 待核实 | Megatron 1F1B | DualPipe H800 优化 |
| 数据 | 14.8T | 公开声明 15.5T | 36T | 数据量级正在指数增 |
K2 用 MLA + 更激进的 384 expert + MuonClip 优化器 —— 在 V3 设计基础上每个维度都加重;Qwen3 选保守但够用的 GQA + 1F1B,赌训练稳定。
十一、复现度自评("是否可实现")¶
| 组件 | 公开材料 | 复现难度 | 替代方案 |
|---|---|---|---|
| MLA | 论文 + FlashMLA | 中(需写 kernel) | GQA 已经够用 |
| Aux-loss-free LB | 描述充分 | 易(10 行 EMA) | aux loss 老办法 |
| FP8 + per-tile scaling | 描述够 | 中(需自写 Triton) | TorchAO FP8 / TE 现成 |
| DualPipe | 高层描述 | 难(需 PTX 调度) | Megatron 1F1B 退而求其次 |
| MTP | 描述充分 | 易 | 不做也能训 |
| 跨节点 all-to-all PTX | 框架描述 | 极难 | NCCL + 接受 bubble |
实操建议(如果要 mini-DeepSeek):
- 先复现 MLA + aux-loss-free LB(这两个改动小、收益大)
- FP8 用 TorchAO 起步,per-tile scaling 第二阶段做
- DualPipe / PTX kernel 是规模化优化,small-scale 训练 1F1B 够了
- MTP 几行代码加上,免费提速 1.5-2×
参考文献¶
- DeepSeek-AI. DeepSeek-V3 Technical Report. 2024. arXiv:2412.19437
- DeepSeek-AI. DeepSeek-V2: A Strong, Economical, and Efficient MoE. 2024. arXiv:2405.04434
- DeepSeek-AI. DeepSeek-R1. 2025. arXiv:2501.12948
- Shao et al. DeepSeekMath: GRPO. 2024. arXiv:2402.03300
- NVIDIA. Transformer Engine Documentation(FP8 训练参考实现)
- FlashMLA — DeepSeek 公开的 MLA Hopper kernel
- Micikevicius et al. FP8 Formats for Deep Learning. 2022. arXiv:2209.05433
↑ 上级 · DeepSeek