前面几节讨论的并行策略,几乎都是围绕"如何把一次训练迭代拆到多个设备上"展开的。推理看似是训练的子集——只做前向传播,似乎更简单——但真正做过部署的人都知道,推理的并行设计有一套完全不同的约束和目标。本节先说清楚这种差异的根源,再分别讨论张量并行、上下文并行和专家并行在推理场景下的具体优化手段。
训练一步的计算量大致是前向的 3 倍。以参数量为 N 的稠密模型为例,每个 token 的计算量可以粗略估算如下:
| 阶段 | 每 token FLOPs(近似) | 说明 |
|---|---|---|
| 前向 | 2N | 每个参数一次乘、一次加 |
| 反向 | 4N | 需要同时算 ∂L/∂x 和 ∂L/∂W |
| 合计(训练) | 6N | 加上重计算还会更多 |
| 推理 | 2N | 只有前向 |
去掉反向带来三个直接后果:
不需要保存激活值:训练必须把前向的中间激活留到反向用,激活显存往往和参数显存同量级,这也是重计算(activation checkpointing)、序列并行等技术存在的主要动机。推理算完一层就能释放,激活显存几乎可以忽略。
没有梯度同步:训练里的数据并行需要在每步结束做梯度 all-reduce,通信量正比于参数量;ZeRO/FSDP 还要切分和收集优化器状态。推理的"数据并行"退化成了纯粹的多副本(replica)部署,设备之间零通信,只需要前面挂一个负载均衡器。这是推理最廉价、扩展性最好的一维并行。
没有优化器状态:训练中 Adam 的一阶、二阶动量加上 FP32 主权重,通常要占参数量的 12~16 倍字节数;推理只需要权重本身,而且可以激进量化到 FP8/INT4。
这是推理独有的、也是最容易被忽视的一点。一次生成请求分为 prefill 和 decode 两个阶段,二者的性能特征几乎相反。
Prefill(预填充 / 提示处理)
一次性处理整个输入 prompt,序列长度 S 通常是几百到几十万。所有 token 可以并行计算,矩阵乘法是"胖"的 GEMM,算术强度(arithmetic intensity)高,是计算受限(compute-bound)的。它决定了 TTFT(Time To First Token,首 token 延迟)。
Decode(解码 / 自回归生成)
每步只处理 1 个新 token。此时 M 维退化为 batch size,GEMM 变成瘦长的 GEMV。以 batch=1 为例,读一遍 70B 模型的 FP16 权重要搬运 140 GB,而计算量只有 0.14 TFLOPs——在 H100 上访存需要约 42 ms,计算只需 0.14 ms,访存受限(memory-bound)程度高达两个数量级。它决定了 TPOT(Time Per Output Token)/ ITL(Inter-Token Latency)。
Prefill: [t1 t2 t3 ... tS] ──→ 一次并行算完,GEMM 胖而高效
算术强度高 → compute-bound
关键指标:TTFT
Decode: [t_{S+1}] ──→ [t_{S+2}] ──→ [t_{S+3}] ──→ ...
每步只有 1 个 token,GEMM 退化为 GEMV
算术强度极低 → memory-bound
关键指标:TPOT / ITL
理解这一点极其重要,因为两个阶段的并行诉求是相反的:
Prefill 阶段的并行策略应该按"训练思路"设计——追求计算效率,可以用上下文并行切长序列。
Decode 阶段的并行策略要按"降低访存与延迟"设计——增大 batch 摊薄权重读取成本,通信要优化小消息延迟而非带宽。
诉求相反,于是催生了训练中完全不存在的一种并行形态:PD 分离(Prefill-Decode Disaggregation)——把 prefill 和 decode 部署在不同的设备池上,各自用最合适的并行配置和 batch 策略,中间通过高速网络传递 KV Cache。
训练时用 teacher forcing 一次算完整个序列,不需要缓存。推理为了避免每生成一个 token 就重算一遍全部历史,必须缓存每层的 Key 和 Value。其大小为:
举个具体例子,Llama-3-70B(80 层,GQA 8 个 KV 头,head_dim 128,FP16):
如果没有 GQA(比如早期 MHA 模型有 64 个 KV 头),这个数字还要再乘 8。KV Cache 给并行设计带来三条硬约束:
它是"每请求私有"的状态,不是可复制的权重。权重可以随便复制,KV Cache 复制就是纯浪费显存。所以任何并行方案都必须回答"KV Cache 怎么切"。
张量并行的切分被 KV 头数卡住。TP 按注意力头切分时,KV Cache 天然按 H_kv 切开。但 Llama-3-70B 只有 8 个 KV 头,TP=8 时刚好每卡一个;要做 TP=16,就只能让两张卡各存一份相同的 KV 头(显存冗余翻倍),或者改用其他维度切分。这是 GQA/MQA 时代 TP 扩展的真实上限。
它随时间增长,且长度不可预知。训练的张量形状在编译期就固定了;推理中每个请求的 KV Cache 边生成边变长,还要支持请求随时加入和退出(continuous batching)。这意味着并行策略必须处理动态形状,每步的通信量都在变化,也很难像训练那样做静态的通信-计算调度编排。
| 维度 | 训练 | 推理 |
|---|---|---|
| 优化目标 | 吞吐(单一) | TTFT / TPOT / 吞吐 / 成本(多目标且冲突) |
| 约束形式 | 总时长预算 | 逐请求的 SLO(如 P99 TTFT < 500 ms) |
| 负载特征 | 静态、同构、可预知 | 动态到达、长度异构、有突发 |
| 批大小 | 固定,通常很大 | 动态,decode 阶段常常很小 |
| 数值精度 | 受限于收敛性,一般 BF16/FP8 | 可激进量化到 FP8/INT4 |
| 失败代价 | 回滚到 checkpoint | 单请求失败,需在线容错 |
训练几乎总是选吞吐最优的并行配置;推理则要在 延迟-吞吐的 Pareto 前沿 上选点。同一个模型,做低延迟对话服务可能选 TP=8、小 batch;做离线批量总结可能选 TP=2 + 4 副本、超大 batch。二者的硬件利用率和成本可以差好几倍。
训练中 PP 用 1F1B 调度,把 micro-batch 塞满流水线,气泡率大致是 (P-1)/(M+P-1),其中 P 为 stage 数、M 为 micro-batch 数。
推理的 decode 阶段每步只有"一个 token",天然没有 micro-batch 可以填充流水线。如果朴素地做 PP,P 个 stage 里只有 1 个在工作,气泡率高达 (P-1)/P。可行的做法是靠 continuous batching 制造流水:把并发请求拆成多个 micro-batch,让不同请求占据不同 stage。代价是单请求的 ITL 变长(要串行穿过所有 stage)。
DP(多副本):零通信,首选的扩容手段。只要单实例装得下模型,优先加副本。
TP:用于单卡装不下模型,或需要降低单请求延迟。必须在 NVLink 域内,规模受 KV 头数限制。
PP:用于跨节点装下超大模型,吞吐友好、延迟不友好。
CP/SP(上下文并行):用于超长上下文,是唯一能线性扩展 KV Cache 容量的维度。
EP(专家并行):MoE 模型专用,核心难题是负载均衡。
Megatron 式张量并行的标准切法是:MLP 的第一个权重按列切、第二个按行切,注意力按头切,每个 Transformer 层在前向中产生 2 次 all-reduce(一次在注意力后,一次在 MLP 后)。训练时前向反向合计 4 次,推理只有 2 次。
小消息 all-reduce 的耗时由 延迟 主导而非带宽。NVLink 上一次 NCCL all-reduce 的固定开销大约 10~30 μs,80 层 × 2 次 = 160 次,光通信固定开销就是 2~5 ms;而 70B 模型在 8 卡 TP 下一步 decode 的计算时间也就 5~10 ms。通信占比轻松到 30%~50%。
最基础也最有效的一条:让 TP 组落在同一个 NVLink/NVSwitch 域内。
TP size ≤ 单节点 GPU 数(通常是 8)。
跨节点的维度留给通信量小的 PP,或零通信的 DP。
在 NVL72 这类超节点上,NVLink 域扩展到 72 卡,TP 的可行规模才有可能突破 8。
具体到进程编号,要保证 TP rank 相邻(即 TP 是最内层维度),避免 rank 映射把一个 TP 组横跨两个 NUMA 或两台机器。
NCCL 的 Ring 算法对大消息是带宽最优的(每卡收发 2S(P-1)/P 字节),但对小消息,2(P-1) 步的串行跳数带来的延迟远大于传输时间。推理框架(vLLM、TensorRT-LLM、SGLang)因此都实现了自己的 custom all-reduce:
| 策略 | 做法 | 通信量 | 同步次数 | 适用消息大小 |
|---|---|---|---|---|
| One-shot | 每卡把自己的数据经 NVLink P2P 直写到其他所有卡,各卡本地求和 | 每卡发 (P-1)S | 1 | 极小(< 1 MB) |
| Two-shot | reduce-scatter + all-gather | 每卡 2S(P-1)/P | 2 | 中等(1~8 MB) |
| NCCL Ring/Tree | 标准集合通信库 | 带宽最优 | 多 | 大消息 |
实测在 8 卡 NVLink、消息小于 512 KB 时,one-shot 比 NCCL 快 2~3 倍。框架通常按消息大小自动选择策略。另有两个易被忽略但收益巨大的点:
CUDA Graph:decode 阶段的 kernel 又多又小,CPU 侧的 launch 开销(每个 kernel 5~10 μs)会成为瓶颈。把整个 decode step 用 CUDA Graph 捕获成一张图,一次提交,能消掉绝大部分 CPU 开销。注意 custom all-reduce 必须是 graph-capturable 的。
Kernel 融合:把 all-reduce 后紧跟的 residual add + RMSNorm 融进一个 kernel,减少一次全局显存往返。
序列并行(Sequence Parallel, SP) 把 LayerNorm、Dropout 等"沿 hidden 维不可切但沿序列维可切"的算子也切开。此时 TP 层的一次 all-reduce 被拆成 reduce-scatter + all-gather 两个半程:
[SP 区: 序列切分] --all-gather--> [TP 区: 头/列切分] --reduce-scatter--> [SP 区]
总通信量不变(all-reduce = RS + AG),但拆开后可以和 GEMM 重叠:
AllGather + GEMM 重叠:
收到第 k 块 → 立刻启动第 k 块的 GEMM
GEMM + ReduceScatter 重叠:
GEMM 沿输出 M 维分块,算完一块就发出去做 reduce-scatter
代表实现有 Megatron-LM 的 async tensor parallel、TransformerEngine 的 userbuffers(用 CUDA multicast 和 P2P 显存直写,绕开 NCCL),以及 FLUX 这类把通信直接融进 GEMM kernel 的工作。
既然 decode 是访存/通信受限,那把通信本身的数据量压下来就是直接收益:FP8 通信可将通信量减半,INT8/INT4 更激进,但通常需要 per-token 或 per-channel 的 scale。
关键工程细节是:求和必须在高精度下做。常见做法是"低精度传输 + 高精度累加"——reduce-scatter 阶段传 FP8,本地反量化成 FP32 累加,再量化成 FP8 做 all-gather。
前面提到 GQA 模型的 H_kv 可能小于 TP size。几种处理方式:
| 方案 | 做法 | 代价 |
|---|---|---|
| KV 头复制 | 多张卡持有相同的 KV 头 | KV Cache 显存冗余 ×(P/H_kv) |
| 按 head_dim 切 | 在头内部切分 d_head | 需要额外通信合并注意力结果 |
| 降低 TP,配合 CP | TP 只到 H_kv,剩余并行度给上下文并行 | 实现复杂度上升 |
| DP Attention | 注意力部分用数据并行(各卡持有不同请求的完整 KV),MLP 部分用 TP | 两部分之间需 all-gather / reduce-scatter,但 KV Cache 零冗余 |
最后一种(DeepSeek 系模型和 SGLang 采用的 DP Attention)在 MLA/GQA 且 KV 头很少的模型上尤其有效,值得重点关注。
随着 TP size 增大,单卡计算量按 1/P 下降,但通信的 固定延迟不随 P 下降(甚至因跳数增加而上升)。于是 decode 延迟大致是:
第二项是常数,导致 P 增大到一定程度后曲线变平,再往上甚至因为 kernel 效率下降(GEMM 的 N 维太小)而反弹。实践中稠密模型的 TP 甜点通常在 4~8,超过之后应该考虑加副本(DP)而不是继续加 TP。
当上下文长度从 8K 涨到 128K 甚至 1M 时,两个问题同时爆发:
KV Cache 爆炸:前面算过,Llama-3-70B 在 8K 上下文时单请求 KV Cache 是 2.68 GB;到 128K 就是 43 GB,一张 H100(80 GB)扣掉权重后连一个请求都放不下。
注意力计算量爆炸:Attention 的 FLOPs 是 O(S²),而 FFN 是 O(S)。在 8K 时注意力只占总计算的百分之十几;到 128K 时注意力反过来占了绝大部分。
TP 无法解决这个问题:它的切分粒度是注意力头,而头数是固定的(GQA 下只有 8 个)。唯一能随序列长度线性扩展的维度,是序列维本身。这就是上下文并行(Context Parallel, CP)。
设有 P 个设备、序列长度 S。把序列切成 P 段,设备 i 持有 Q_i、K_i、V_i(各 S/P 个 token)。问题在于注意力要求每个 Query 看到 所有 的 Key/Value,而 KV 分散在各设备上。Ring Attention 的解法是让 KV 块 在环上转圈:
┌─────→ Dev0 ─────→ Dev1 ─────→ Dev2 ─────→ Dev3 ─────┐
└───────────────────────────────────────────────────────┘
(KV 块环形传递)
第 0 步: 设备 i 用 (Q_i, K_i, V_i) 算局部注意力,
同时把 K_i, V_i 发给设备 i+1
第 1 步: 设备 i 用 (Q_i, K_{i-1}, V_{i-1}) 算,同时转发 KV
...
第 P-1 步: 每个设备的 Q_i 已经见过全部 P 份 KV
结果的正确性靠 online softmax(FlashAttention 的核心技巧)保证:每个设备维护三个 running 状态——行最大值 m、指数和 ℓ、加权输出累加 O。收到新的 KV 块并算出局部结果 (m', ℓ', O') 后按下式合并:
这样就 无需在任何时刻物化完整的 S × S 注意力矩阵,也无需任何设备持有全量 KV。
Ring Attention 的精妙之处在于:第 t 步的 KV 传输,可以和第 t-1 步的注意力计算重叠(用双缓冲 + 异步 P2P send/recv)。
所以 序列越长,计算越容易掩盖通信。化简后可以得到一个对 S/P 的下界:每个设备分到的 token 数不能太少。这条约束在实践中意味着 CP 的并行度不是越大越好——切得太碎,单块计算时间盖不住 KV 传输。
这是 Ring Attention 最重要的工程陷阱。因果掩码下,持有序列后段的设备要 attend 前面所有 token,持有前段的设备只需要 attend 自己那一小段。
方案一:Zigzag / 对折切分(Megatron-LM 的 CP、ring-flash-attention 采用)
把序列切成 2P 块,设备 i 拿第 i 块和第 (2P-1-i) 块,让靠前的"轻"块和靠后的"重"块配对:
P = 4,序列切成 8 块(块号越大,因果计算量越大): 设备 0: 块 0 + 块 7 ← 最轻 + 最重 设备 1: 块 1 + 块 6 设备 2: 块 2 + 块 5 设备 3: 块 3 + 块 4 → 每个设备的总计算量近似相等
实现简单、均衡效果好,是目前最常用的做法。
方案二:Striped Attention(条纹切分)
按 stride 交错分配:设备 i 拿 token i, i+P, i+2P, …。这样每个设备持有的 token 在序列中均匀分布,因果计算量天然均衡。代价是块内的因果掩码不再是标准的下三角,需要修改 kernel 处理"偏移一格"的掩码。
Prefill 阶段:query 有 S 个,KV 也有 S 个。Ring Attention 完全适用,把长 prompt 切开并行处理,能显著降低 TTFT。这是 CP 在推理中的主战场。
Decode 阶段:query 只有 1 个 token,而 KV Cache 有 S 个且分散在 P 个设备上。此时如果还转 KV,就等于把几十 GB 的 cache 在环上搬一圈——荒谬。正确的做法是 方向反过来:
Decode 阶段的正确做法(Flash-Decoding 的跨设备版本):
1. 把这 1 个 token 的 Q 广播给所有设备
(数据量极小:B × H_q × d_head)
2. 每个设备用全局 Q 对【本地那 1/P 份 KV Cache】算局部注意力,
输出 (O_i, m_i, ℓ_i)
3. 一次 all-gather / all-reduce,
用 online softmax 合并公式归约出最终结果
→ 通信量 O(B × H_q × d_head),与序列长度 S 无关
| Prefill | Decode | |
|---|---|---|
| 移动的对象 | KV 块(环形传递) | Q 和局部结果 |
| 通信量 | O(S/P) 每步,共 P-1 步 | O(1),与 S 无关 |
| 掩盖方式 | 与本块注意力计算重叠 | 通信量本身就极小 |
| 主要收益 | 降低 TTFT、装下超长 prompt | 装下超大 KV Cache、加速长上下文 decode |
CP 与 TP 正交:可以先 TP 切头,再 CP 切序列,总并行度 P_TP × P_CP。一个 128K 上下文的 70B 模型典型配置可能是 TP=8 × CP=2,共 16 卡。组合时需要注意:
CP 组内的 P2P 通信量随序列长度增长,优先放在 NVLink 域内;若必须跨节点,要确保 InfiniBand/RoCE 带宽充足,并核算前面那条掩盖条件。
CP 让每卡只存 1/P 的 KV Cache,和 PagedAttention 的分页管理是互补的:前者解决"跨设备的容量扩展",后者解决"单设备内的碎片与共享"。
CP 与 chunked prefill 结合时,要注意 chunk 边界与 CP 切分边界的对齐,否则会产生额外的重排通信。
MoE 层里有 E 个专家,router 为每个 token 选出 top-k 个。专家并行(Expert Parallel, EP)把 E 个专家分散到 P 个设备,每设备持有 E/P 个:
Router 打分
↓
All-to-All (dispatch) ← 把 token 发到目标专家所在设备
↓
本地专家 GEMM
↓
All-to-All (combine) ← 把结果送回原设备
↓
按 gate 权重加权求和
特性一:"总参数大、激活参数小"的错配
以 DeepSeek-V3 这类模型为例,总参数量数千亿但每 token 只激活其中几十分之一。Decode 阶段是访存受限,理论上访存量只对应激活参数——但前提是权重能放得下。放得下就必须把总权重铺开到很多卡,于是 EP 度往往被"显存容量"而非"计算需求"推到几十上百。
特性二:单专家的 batch 太小,GEMM 效率崩塌
所以 MoE 推理天然需要大 batch,这也是为什么 MoE 服务通常要配合更激进的 continuous batching 和更大的并发目标。
训练时有辅助负载均衡损失(auxiliary loss)或 loss-free 的 bias 调整机制,持续把 router 分布往均匀推。推理时 没有任何训练信号,不均衡的来源有三:
分布漂移:线上流量分布(语言、领域、格式)与训练语料不同,某些专家会系统性地热门。比如代码请求和中文对话激活的专家集合可能差异很大。
小数定律:即使期望是均匀的,decode 阶段每步只有几十上百个 token,采样方差极大。T·k = 512 个 token 分给 256 个专家,即使完全随机,最热专家的负载也会远超均值。
All-to-All 是同步屏障:所有设备必须等最慢的那个算完才能进入 combine。整层耗时由最大负载决定,而非平均负载。
(1)冗余专家 / 专家副本(Redundant Experts)
把统计上最热的若干专家在多个设备上各放一份,路由时在副本之间按当前负载分流。DeepSeek 公开的部署方案里就用了这一手,配套的 EPLB(Expert Parallel Load Balancer) 会周期性地根据统计热度重新计算专家到设备的放置方案。优点是直接有效,代价是额外显存以及需要在线负载统计。
(2)专家重排 / 分组放置
不复制,只是 换个放法。根据统计的专家热度,把"热专家"和"冷专家"搭配放在同一设备上,让各设备的期望负载和方差都尽量小。这本质是一个装箱问题;如果进一步考虑专家的 共激活关系(哪些专家常被同一 token 同时选中),还可以建模成图划分问题,顺带减少跨节点流量。
(3)动态迁移
流量特征会随时间变化(白天 vs 夜间、不同业务接入)。可以用滑动窗口统计热度,当不均衡度超过阈值时触发专家权重的重新放置。因为搬运权重的代价在百毫秒到秒级,这个动作必须 低频执行(分钟级),并且要做好迁移期间的请求路由过渡。
(4)不丢 token(Dropless)
训练中常用 capacity factor 限制每个专家的 token 上限,超出的直接丢弃,好处是张量形状固定。推理不能这么做——丢 token 会直接损害生成质量,而且用户能感知。因此推理必须走 dropless 路线,用变长的 Grouped GEMM / 块稀疏 GEMM(如 MegaBlocks 的实现)处理不等长的专家输入。这也是为什么 MoE 推理 kernel 比训练更难写。
(5)双批次重叠(Two-Batch Overlap)
把一个 batch 拆成两个 micro-batch 交错执行:
时间 →
micro-batch A: [计算]──[All-to-All]──[计算]──[All-to-All]
micro-batch B: [A2A]───[计算]───────[A2A]───[计算]
↑ 一个通信时另一个计算,All-to-All 被隐藏
这是 DeepSeek DualPipe 思想在推理侧的对应物,SGLang 等框架已有实现。收益在跨节点 EP 场景下尤其明显。
(6)限制路由范围(Node-Limited Routing)
从 模型设计层面 约束通信:限制每个 token 最多被路由到 M 个节点(而不是任意 E 个专家所在的任意节点)。这样 all-to-all 的跨节点流量有了硬上界,可以按"节点内 NVLink + 节点间 IB"的带宽比来设定 M。DeepSeek-V3 采用了这一设计,是"算法-系统协同设计"的典型例子。
MoE 层既可以用 EP 切,也可以用 TP 切(切每个专家内部的权重矩阵):
| 维度 | EP(切专家) | TP(切专家内部) |
|---|---|---|
| 通信原语 | All-to-All ×2 | All-Reduce ×1 |
| 通信量 | ∝ T · k · d_model | ∝ T · d_model |
| 权重分布 | 每卡持有完整的若干专家 | 每卡持有所有专家的一部分 |
| 单卡 GEMM 形状 | 瘦(token 少),但 N 维完整 | N 维被切小,token 全量 |
| 负载均衡 | 是核心难题 | 天然均衡 |
| 对拓扑敏感度 | All-to-All 对跨节点极敏感 | All-Reduce 相对友好 |
实践上常见的组合是:节点内用 TP、节点间用 EP,或者在超大模型上直接铺开大规模 EP(数十到数百路),把 attention 部分用 DP Attention 处理。
回到 7.4.1 的主线:两个阶段的诉求不同,在 MoE 上体现得尤其明显。
| 维度 | Prefill | Decode |
|---|---|---|
| 每步 token 数 | 很多(整个 prompt) | 很少(等于并发数) |
| 天然均衡度 | 好(大样本,方差小) | 差(小样本,方差大) |
| 主要瓶颈 | 专家 GEMM 的计算量 | 权重访存 + All-to-All 延迟 + 等待最慢专家 |
| 建议 EP 度 | 相对小,够放下权重即可 | 更大,配合冗余专家和大 batch |
每设备的专家 token 数分布:计算 max/mean 不均衡度,做时序告警
All-to-All 的实际耗时占比,以及各 rank 的等待时间方差
Top-N 热点专家列表 及其随时间的漂移(用于触发重排 / 迁移)
有效 batch:每专家平均 token 数 t̄,低于阈值说明并发不足,GEMM 在空转
推理并行与训练并行的分野,根源在三件事:没有反向传播(去掉了梯度同步和激活存储)、多了 KV Cache(引入了随请求增长的私有状态)、分裂成计算受限的 prefill 和访存受限的 decode(两个阶段诉求相反)。
由此推出的工程结论是:
DP 在推理里是零通信的,优先用它扩容
TP 受 NVLink 域和 KV 头数双重约束,甜点在 4~8,核心优化是小消息延迟(custom all-reduce + CUDA Graph)而非带宽
CP 是长上下文的唯一线性扩展维度,prefill 转 KV(Ring)、decode 转 Q(Flash-Decoding 式归约),方向相反,且必须处理因果掩码的负载不均
EP 的核心矛盾是负载均衡,手段是冗余专家、重排、大 batch 和通信重叠,并且 prefill / decode 应当分别配置