graph TD
subgraph "标准 MHA"
A1["Hidden State h"] --> B1["W_Q → Q<br/>W_K → K<br/>W_V → V"]
B1 --> C1["Attention<br/>(缓存 K, V)"]
end
subgraph "GQA (LLaMA)"
A2["Hidden State h"] --> B2["W_Q → Q (full)<br/>共享 K, V (fewer heads)"]
B2 --> C2["Attention<br/>(缓存少量 K, V)"]
end
subgraph "MLA (DeepSeek)"
A3["Hidden State h"] --> B3["W_D → c<br/>(低秩潜在向量)"]
B3 --> C3["缓存 c (很小!)"]
C3 --> D3["W_U → K, V<br/>(推理时解码)"]
D3 --> E3["Attention"]
end
第27章 DeepSeek 系列训推实战
第27章 DeepSeek 系列训推实战
如果 2024 年开源 AI 领域有一个”年度团队”奖,它应该颁给 DeepSeek。这家来自中国的团队用两篇论文震撼了整个社区——MLA(Multi-head Latent Attention)重新定义了 Attention 的效率上限,FP8 训练证明了非 NVIDIA 生态也可以做大规模训练。DeepSeek-V3 不只是一个模型,它是一份工程宣言。
27.1 章节导入
2024 年 1 月,DeepSeek 发布了 V2 系列,引入了一个全新的 Attention 机制——MLA(Multi-head Latent Attention)。这个机制将 KV Cache 压缩到了之前 GQA 的 1/4,同时质量不降。
2024 年 12 月,DeepSeek-V3 发布。671B 参数(激活 37B),在多项基准上对标 LLaMA 3.1 405B 和 Claude 3.5 Sonnet,但训练成本只有 557 万美元——不到 LLaMA 3 的 1/10。
DeepSeek 的核心技术贡献有三个:
- MLA:通过低秩投影大幅压缩 KV Cache
- 细粒度 MoE:更多更小的专家 + 共享专家设计
- FP8 训练:在 H800 上实现了稳定的 FP8 大规模训练
本章将深入这三个技术,以及它们如何组合成一个完整的训推系统。
27.2 MLA:Multi-head Latent Attention
27.2.1 问题:KV Cache 太大了
在第 24 章我们讨论过,KV Cache 是长上下文推理的主要内存瓶颈。即使使用 GQA(如 LLaMA 3),一个 70B 模型在 128K 上下文下仍然需要数十 GB 的 KV Cache。
DeepSeek 的洞察是:与其用 GQA 减少 KV Head 的数量,不如用低秩投影压缩 KV 的维度。
27.2.2 MLA 的核心思想
MLA 将 Key 和 Value 投影到一个低秩的”潜在空间”中,只在需要时才解码回高维:
数学表达:
标准 Attention: \[ \mathbf{K} = \mathbf{h} \cdot W_K, \quad \mathbf{V} = \mathbf{h} \cdot W_V \]
MLA: \[ \mathbf{c} = \mathbf{h} \cdot W_D \quad (d_c \ll d \cdot n_{heads}) \] \[ \mathbf{K} = \mathbf{c} \cdot W_U^K, \quad \mathbf{V} = \mathbf{c} \cdot W_U^V \]
关键在于 只需要缓存 \(\mathbf{c}\),因为 \(W_U^K\) 和 \(W_U^V\) 是模型参数(固定的)。
27.2.3 MLA 的参数对比
以 DeepSeek-V2 23B(小型版本)为例:
标准 MHA:
KV Cache per token = 2 × n_layers × n_heads × head_dim
= 2 × 28 × 128 × 128 = 917,504 elements
≈ 1.75 MB (FP16, batch=1)
GQA (8 KV heads):
= 2 × 28 × 8 × 128 = 57,344 elements
≈ 0.11 MB
MLA (latent dim = 512):
= 28 × 512 = 14,336 elements (只需缓存 c!)
≈ 0.027 MB
MLA 的 KV Cache 比 GQA 小 4 倍,比标准 MHA 小 64 倍!
27.2.4 MLA 的工程实现
import torch
import torch.nn as nn
class MultiHeadLatentAttention(nn.Module):
def __init__(self, dim, n_heads, head_dim, latent_dim, rope_dim=None):
super().__init__()
self.n_heads = n_heads
self.head_dim = head_dim
self.latent_dim = latent_dim # 通常远小于 n_heads * head_dim
# 下投影:h → c (低秩)
self.W_D = nn.Linear(dim, latent_dim)
# 上投影:c → Q, K, V (只在计算时使用,不缓存)
self.W_UQ = nn.Linear(latent_dim, n_heads * head_dim) # Query
self.W_UKV = nn.Linear(latent_dim, 2 * n_heads * head_dim) # K, V
# RoPE(可选,对 Q 应用)
self.rope = RotaryEmbedding(rope_dim or head_dim)
def forward(self, h, past_kv=None):
batch_size, seq_len, _ = h.shape
# Step 1: 下投影到低秩空间
c = self.W_D(h) # [batch, seq, latent_dim]
# Step 2: 缓存 c(而不是 K, V!)
if past_kv is not None:
c_full = torch.cat([past_kv, c], dim=1)
else:
c_full = c
new_kv = c # 只缓存 c
# Step 3: 上投影回高维(推理时按需计算)
q = self.W_UQ(c_full) # [batch, seq, n_heads * head_dim]
kv = self.W_UKV(c_full)
k, v = kv.chunk(2, dim=-1)
# Reshape
q = q.view(batch_size, -1, self.n_heads, self.head_dim)
k = k.view(batch_size, -1, self.n_heads, self.head_dim)
v = v.view(batch_size, -1, self.n_heads, self.head_dim)
# RoPE
cos, sin = self.rope(q, seq_len=c_full.shape[1])
q, k = apply_rotary_pos_emb(q, k, cos, sin)
# Attention
attn_output = F.scaled_dot_product_attention(q, k, v, is_causal=True)
return attn_output, new_kvMLA 与 GQA 不是互斥的。DeepSeek-V3 的 MLA 在推理时也可以像 GQA 一样共享 KV heads,进一步压缩。MLA 的本质是”在维度上做低秩压缩”,而 GQA 是”在 head 数量上做分组”。两者可以组合使用。
27.3 DeepSeek MoE 架构创新
27.3.1 细粒度专家设计
DeepSeek 没有使用 Mixtral 式的”8 个大专家”,而是使用了更多更小的专家:
graph LR
subgraph "Mixtral 8x7B"
M_R["Router"] --> M_E1["Expert 1<br/>(FFN dim=14336)"]
M_R --> M_E2["Expert 2<br/>(FFN dim=14336)"]
M_R --> M_E8["... 8 experts"]
end
subgraph "DeepSeek-V3"
D_R["Router"] --> D_S["Shared Expert<br/>(always active)"]
D_R --> D_E1["Expert 1<br/>(small FFN)"]
D_R --> D_E2["Expert 2<br/>(small FFN)"]
D_R --> D_E256["... 256 experts"]
end
| 配置 | Mixtral 8x7B | DeepSeek-V3 |
|---|---|---|
| 专家总数 | 8 | 256 |
| 每层专家 | 8 | 256 |
| Top-K | 2 | 8 |
| 共享专家 | 无 | 1 |
| 总参数 | 46.7B | 671B |
| 激活参数 | 12.9B | 37B |
27.3.2 共享专家:关键创新
DeepSeek MoE 最重要的架构创新是Shared Expert(共享专家)——一个始终被激活的专家,参与每个 token 的计算:
\[ \text{output} = \text{SharedExpert}(x) + \sum_{i \in \text{TopK}} g_i \cdot E_i(x) \]
为什么需要共享专家?因为标准 MoE 存在一个问题:某些通用能力(如语法、基本逻辑)被分散到了各个专家中,导致冗余学习。共享专家负责处理这些”通用知识”,让路由专家专注于”特化知识”。
class DeepSeekMoELayer(nn.Module):
def __init__(self, dim, n_experts, n_shared_experts=1, top_k=8, expert_dim=None):
super().__init__()
self.top_k = top_k
# 共享专家(始终激活)
self.shared_experts = nn.ModuleList([
FeedForward(dim, expert_dim) for _ in range(n_shared_experts)
])
# 路由专家(动态选择)
self.router = nn.Linear(dim, n_experts, bias=False)
self.experts = nn.ModuleList([
FeedForward(dim, expert_dim // 4) # 更小的专家
for _ in range(n_experts)
])
def forward(self, x):
# 共享专家始终参与
shared_output = sum(expert(x) for expert in self.shared_experts)
# 路由专家
router_logits = self.router(x) # [batch, seq, n_experts]
weights, selected_experts = torch.topk(
F.softmax(router_logits, dim=-1), self.top_k
)
moe_output = torch.zeros_like(x)
for i in range(self.top_k):
expert_idx = selected_experts[..., i] # [batch, seq]
weight = weights[..., i:i+1] # [batch, seq, 1]
# 收集选中专家的输出
for b in range(x.shape[0]):
for s in range(x.shape[1]):
e = expert_idx[b, s]
moe_output[b, s] += weight[b, s] * self.experts[e](x[b, s])
return shared_output + moe_output27.3.3 为什么细粒度更好
想象你要训练一个”万能厨师”:
- Mixtral 方式:训练 8 个厨师,每个精通 2-3 个菜系
- DeepSeek 方式:训练 256 个厨师,每个精通 1 道菜 + 1 个主厨(共享专家)
DeepSeek 方式的优势: 1. 专业化程度更高:每个专家只需要学习一个窄领域,更容易学好 2. 组合灵活性更高:256 选 8 的组合数远大于 8 选 2 3. 负载均衡更容易:更多专家意味着更均匀的统计分布
27.4 FP8 训练的工程实现
27.4.1 为什么 FP8 如此重要
DeepSeek-V3 训练使用了 2048 张 NVIDIA H800 GPU,在 2 个月内完成了训练。训练成本 557 万美元——这个数字让整个行业震惊。
能实现如此低成本的关键之一是 FP8 训练——将训练精度从 BF16/FP16 降低到 FP8,理论上可以将计算量减半、通信量减半。
但 FP8 训练在 2024 年之前几乎没有大规模成功案例。主要挑战是数值稳定性——FP8 的动态范围很小(E4M3 格式只有 ±448),梯度很容易溢出或下溢。
27.4.2 DeepSeek 的 FP8 策略
DeepSeek 没有盲目地将所有计算都降到 FP8,而是采用了一个混合精度策略:
graph TD
subgraph "FP8 混合精度策略"
A["权重<br/>(FP8 存储)"] --> B["前向传播<br/>(FP8 GEMM)"]
B --> C["激活值<br/>(FP8 存储)"]
C --> D["梯度<br/>(BF16 反传播)"]
D --> E["权重更新<br/>(BF32 Master)"]
end
subgraph "保持高精度的部分"
F["Attention<br/>(BF16)"]
G["LayerNorm<br/>(BF16)"]
H["Embedding<br/>(BF16)"]
end
关键设计:
- FP8 GEMM(矩阵乘法):主要的线性层用 FP8 计算,利用 H800 的 FP8 Tensor Core
- BF16 累加:FP8 矩阵乘法的结果用 BF16 累加,避免精度损失
- 选择性 FP8:Attention、LayerNorm 等对精度敏感的操作保持 BF16
- 细粒度缩放:为每个矩阵/向量维护一个 FP32 的缩放因子
# FP8 线性层的简化实现
class FP8Linear(nn.Module):
def __init__(self, in_features, out_features):
super().__init__()
# Master 权重(BF16)
self.weight_bf16 = nn.Parameter(torch.randn(out_features, in_features, dtype=torch.bfloat16))
# FP8 缓存(推理/前向时使用)
self.register_buffer('weight_fp8', None)
# 缩放因子
self.weight_scale = 1.0
self.input_scale = 1.0
def _quantize_to_fp8(self, tensor):
"""将 BF16 tensor 量化为 FP8"""
E4M3_MAX = 448.0
# 计算缩放因子
scale = tensor.abs().max().clamp(min=1e-12) / E4M3_MAX
# 量化
scaled = tensor / scale
# 模拟 FP8(实际使用 torch.float8_e4m3fn)
quantized = scaled.to(torch.float8_e4m3fn)
return quantized, scale
def forward(self, x):
# 量化权重(如果还没有)
if self.weight_fp8 is None:
self.weight_fp8, self.weight_scale = self._quantize_to_fp8(self.weight_bf16)
# 量化输入
x_fp8, self.input_scale = self._quantize_to_fp8(x)
# FP8 GEMM(实际使用 transformer_engine 或 triton)
# 这里用 BF16 模拟
output = F.linear(x_fp8.to(torch.bfloat16),
self.weight_fp8.to(torch.bfloat16))
# 反缩放
output = output * (self.weight_scale * self.input_scale)
return output27.4.3 实际使用的工具链
DeepSeek 使用了 NVIDIA 的 Transformer Engine 库来实现 FP8 训练:
# 使用 Transformer Engine 的实际代码
import transformer_engine.pytorch as te
import transformer_engine.pytorch.te as te_utils
# TE 提供的 FP8 模块
model = te.TransformerLayer(
hidden_size=7168,
ffn_hidden_size=18432,
num_attention_heads=128,
num_gqa_groups=16,
layernorm_eps=1e-6,
fuse_qkv_params=True,
attention_type="self",
)
# FP8 自动管理
with te_utils.fp8_autocast(enabled=True, fp8_recipe=te_utils.DelayedScaling(
fp8_format=te_utils.Format.HYBRID,
amax_history_len=16,
amax_compute_algo="max",
)):
output = model(input_data)FP8 训练的陷阱: 1. 不是所有 GPU 都支持:FP8 Tensor Core 只在 H100/H800 (Hopper) 及以上架构上才有。A100 不支持。 2. 不是所有操作都能 FP8:LayerNorm、Softmax 等非线性操作需要 BF16,否则精度灾难性下降 3. 梯度容易爆炸:FP8 的动态范围小,需要仔细的梯度裁剪和损失缩放 4. 调试困难:FP8 的数值行为与 BF16 不同,很多 bug 只在特定数值范围触发
27.5 推理服务架构
27.5.1 DeepSeek 推理的挑战
DeepSeek-V3 的推理面临独特挑战——671B 参数(即使激活 37B)需要大量显存,而 MoE 的动态路由使得专家分布对性能影响很大。
27.5.2 预填充-解码分离
DeepSeek 的官方推理服务采用了预填充-解码分离(Prefill-Decode Disaggregation)架构:
graph LR
subgraph "传统(合一)"
A1["请求"] --> B1["GPU: Prefill + Decode"]
end
subgraph "DeepSeek(分离)"
A2["请求"] --> B2["Prefill 集群<br/>(处理长 prompt)"]
B2 --> C2["KV Cache"]
C2 --> D2["Decode 集群<br/>(生成 token)"]
end
为什么要分离?因为 Prefill 和 Decode 的计算模式完全不同:
| 特性 | Prefill | Decode |
|---|---|---|
| 计算密度 | 高(矩阵-矩阵乘) | 低(矩阵-向量乘) |
| Batch 友好度 | 好 | 差(需要 continuous batching) |
| GPU 利用率 | 高 | 低(memory-bound) |
| 最优 batch size | 大 | 小(但有 batching 技巧) |
27.5.3 使用 vLLM 部署 DeepSeek
# DeepSeek-V3 需要 8x H100/H800(FP8)
vllm serve deepseek-ai/DeepSeek-V3 \
--tensor-parallel-size 8 \
--max-model-len 65536 \
--quantization fp8 \
--enable-prefix-caching \
--gpu-memory-utilization 0.95
# DeepSeek-V2 Lite(更小的版本,2x GPU 即可)
vllm serve deepseek-ai/DeepSeek-V2-Lite \
--tensor-parallel-size 2 \
--max-model-len 32768 \
--enable-prefix-caching27.5.4 MLPerf 级别的性能参考
以下是基于社区测试的性能数据(DeepSeek-V2 23B Chat,单张 A100 80GB):
| 部署方式 | 精度 | 吞吐 (tok/s) | 首 token 延迟 | 显存占用 |
|---|---|---|---|---|
| vLLM FP16 | FP16 | 2,800 | 180ms | 72GB |
| vLLM FP8 | FP8 | 4,200 | 120ms | 48GB |
| vLLM INT8 | INT8 | 3,900 | 140ms | 40GB |
| TensorRT-LLM FP8 | FP8 | 5,100 | 90ms | 48GB |
27.6 对比分析
27.6.1 DeepSeek-V3 vs LLaMA 3.1 405B
| 维度 | DeepSeek-V3 | LLaMA 3.1 405B |
|---|---|---|
| 架构 | MoE + MLA | Dense + GQA |
| 总参数 | 671B | 405B |
| 激活参数 | 37B | 405B |
| 推理速度 | 快得多(37B 激活) | 慢(405B 全激活) |
| 推理显存 | ~600GB(FP8) | ~800GB(FP8) |
| 质量 | 相当 | 略优(在部分基准上) |
| 训练成本 | $5.57M | ~$50M+(估计) |
| 中文能力 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
DeepSeek 的效率优势核心来源: 1. MoE 架构:37B 激活 vs 405B(快 ~10x) 2. MLA:KV Cache 减少 4x 以上 3. FP8 训练:计算量减半 4. 这些技术不是孤立的——它们组合在一起产生了乘法效应
27.7 实践建议
何时选择 DeepSeek: 1. 中文为主的应用 → DeepSeek 在中文基准上全面领先 2. 推理延迟敏感 → MLA + MoE 的组合让 DeepSeek 成为同等质量下最快的开源模型之一 3. 成本敏感 → 激活参数少意味着更少的 GPU 需求 4. 需要推理能力 → DeepSeek-R1 系列提供开源推理增强模型
DeepSeek 部署的注意事项: 1. MoE 负载不均衡:某些专家可能被频繁选中,导致 GPU 利用率不均。需要监控专家命中率 2. 大词表的加载时间:DeepSeek 使用 100K+ 词表,首次加载需要较长时间 3. FP8 的兼容性:如果 GPU 不支持 FP8(如 A100),会自动回退到 BF16,性能下降明显 4. MoE 微调的专家坍缩:微调时部分专家可能完全不被激活,需要特殊的辅助损失
27.8 小结
DeepSeek 团队在 2024 年的贡献可以总结为一句话:他们证明了不需要 10 亿美元也能训练世界级大模型。
技术层面,三个核心创新值得记住:
- MLA:通过低秩投影压缩 KV Cache,是继 GQA 之后 Attention 效率的最大飞跃
- 细粒度 MoE + 共享专家:更多更小的专家 + 始终激活的共享专家,解决了 MoE 的冗余问题
- FP8 训练:在工程上证明了 FP8 可以稳定训练 600B+ 参数的模型
这三者的组合让 DeepSeek-V3 在质量对标 405B 稠密模型的同时,推理速度快了数倍、训练成本低了一个数量级。
27.9 延伸阅读
- DeepSeek-AI (2024). DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model. arXiv:2405.04434
- DeepSeek-AI (2024). DeepSeek-V3 Technical Report. arXiv:2412.19437 — 必读,工程细节极其丰富
- DeepSeek-AI (2024). DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning. arXiv:2501.12948
- NVIDIA (2024). Transformer Engine Documentation. FP8 训练的官方实现
- Peng et al. (2023). FP8 Quantization: The Power of the Exponent. FP8 理论基础
- Micikevicius et al. (2022). FP8 Formats for Deep Learning. NVIDIA/Arm/Intel 联合 FP8 白皮书