跳到主要内容
推理优化

6.3 流水线并行推理

分析推理场景下 PP 段间传递的内容与通信量,解释 Decode 的步间依赖为什么需要多个 Batch 同时在飞,讨论层划分不均、单请求延迟与 KV 容量,以及 TP×PP 的组合选择

流水线并行 Pipeline Bubble TP×PP 多节点 vLLM

TP 让每张卡算每一层的一部分,PP 换了个方向:每张卡算完整的几层,算完把结果交给下一张卡。段与段之间只传一次激活,通信次数比 TP 少一到两个数量级,所以它是跨节点扩展的主要手段。

代价是”流水线”本身。只有一个 Batch 时,任意时刻只有一段在工作,其余都在等。训练用切 Micro-batch 的办法填满流水线,推理的 Decode 每一步依赖上一步的采样结果,填法不同。本节讲清这件事,再讨论层怎么分、延迟和容量怎么算,以及 TP 和 PP 怎么组合。

⏰ 信息时效:原理部分与框架无关;vLLM 相关描述基于 v0.32.0(2026-10-18 发布)。

📑 目录


1. 按层切:段之间传什么

1.1 每段是一组连续的层

PP 把 LL 层按顺序分成 pp 段,每段(Stage)放在一张卡或一个 TP 组上。第一段还要负责 Embedding,最后一段负责最后的 Norm、LM Head 和采样。

一个 Batch 的前向就是依次走过各段:Stage 0 算完前几层,把输出发给 Stage 1,Stage 1 接着算,直到最后一段得到 Logits。

1.2 只传当前步的激活

段之间传的是当前这一步的隐藏状态,形状是 n×hn\times h,nn 是本步的 Token 数。vLLM 的 Llama 实现里,非末段的输出打包成 IntermediateTensors,包含 hidden_states 和 residual 两个张量(残差连接要用到)。

KV Cache 不需要传。每段只为自己负责的那些层存 KV,Attention 在本段内就能完成。

以 Llama-3.1-70B、Decode 64 个请求为例,每个段边界每步传:

2×64×8192×2 B=2 MiB2\times64\times8192\times2\ \text{B}=2\ \text{MiB}

PP=2 时每步只有 1 次这样的传输,对比 TP 的 160 次 AllReduce,对网络的要求低得多。即使走 50 GB/s 的 InfiniBand,传输时间也只有约 40 µs。

📌 关键点:PP 的通信是点对点的单向发送,不需要所有卡同步。下一段只等上一段,和其他段无关。


2. 单个 Batch 填不满流水线

接着用第 6.1 节的后厨流水线:备料、炒菜、装盘各一个人。只有一桌客人时,炒菜的师傅要等备料完成,装盘的要等炒完。每个人只有三分之一的时间在干活。

推理的 Decode 比这还麻烦:第 k+1k+1 步的输入是第 kk 步采样出来的 Token。一个 Batch 必须走完全部 pp 段、采样出新 Token,才能开始下一步。只有一个 Batch 时,任意时刻只有一段在工作,每段的利用率是 1/p1/p。

训练也有气泡,处理方法是把一个大 Batch 切成 mm 个 Micro-batch,一个接一个塞进流水线。推理能借用这个思路,但有一条限制:同一个请求的相邻两步有严格的先后关系,所以同时在流水线里的必须是不同请求组成的 Batch。同一个请求的第 k+1k+1 步,无论怎么切都不能在第 kk 步走完之前开始。


3. 推理怎么填:多个 Batch 交错

3.1 让 pp 个 Batch 同时在飞

办法是把当前正在运行的请求分成 pp 组,组成 pp 个互相独立的 Batch,轮流送进流水线。Batch A 在 Stage 1 时,Batch B 进入 Stage 0;A 到 Stage 2 时,B 到 Stage 1,C 进入 Stage 0……填满以后,每一段在每个时刻都在处理某个 Batch。

下图对比 PP=4 时只有 1 个 Batch 和 4 个 Batch 交错的情况,四种颜色是 Batch A–D,格子里的数字是该 Batch 第几步 Decode:

PP=4 的 Decode 时间线:只有 1 个 Batch 时,每段只有四分之一的时间在工作,A2 要等 A1 走完 4 段才能开始;4 个 Batch A 到 D 轮流进入后,流水线填满,每段每个时刻都在处理不同的 Batch

要点在下半张图的对角线:A1 走完 Stage 3、采样出 Token,正好轮到 Stage 0 空出来处理 A2。只要在飞的 Batch 数等于段数,并且各段耗时相同,这个节奏就能一直保持。

3.2 vLLM 的做法

vLLM V1 的 EngineCore 在 PP>1 时启用一个 Batch 队列,队列长度取自 max_concurrent_batches。VllmConfig.max_concurrent_batches 的源码注释写得很直接:“PP requires PP-size concurrent batches to fill the pipeline”,默认就是 pp;开启异步调度并使用 V2 Model Runner 时是 p+1p+1,多出的一个用来让调度和执行重叠。

调度器每次产出一个新 Batch 送进 Stage 0,不必等上一个 Batch 的结果回来。某个请求已经在一个在飞的 Batch 里时,它要等这一步走完整条流水线、采样出 Token 后才能进入下一步,这正是第 2 节说的步间依赖。

由此可以推出:PP 要发挥作用,并发请求数至少要能分成 pp 组。

  • 只有 1 个请求时,不管 PP 是多少,流水线里永远只有 1 个 Batch,利用率是 1/p1/p。
  • 并发 64、PP=4 时,每个 Batch 约 16 个请求。和不开 PP 时 64 个请求一起算相比,每次前向的 Batch 变小了,但 4 段同时在工作。

3.3 不均匀会产生新的气泡

图里假设每格耗时相同。实际中几种情况会打破这个假设:

  • Batch 之间工作量不同:一个 Batch 里有长上下文请求,Attention 更慢,后面的 Batch 就要等它。
  • Prefill 和 Decode 混在一起:一个 Batch 里塞了几千个 Token 的 Prefill,这一格会比其他 Batch 长很多。
  • 请求结束或新请求到达:组的大小在不断变化。

任何一格变长,后续所有 Batch 在那一段都要排队,空出来的时间就是气泡。所以 PP 下的吞吐对调度的均衡性更敏感。


4. Prefill 与 Chunked Prefill

Prefill 一次处理一整段 Prompt,没有步间依赖,只要有多个请求在排队就能交错进入流水线。

单个超长 Prompt 可以切成多个 Chunk。Chunk 2 在 Stage 0 只依赖 Chunk 1 在 Stage 0 写下的 KV,而这部分 KV 在 Chunk 1 离开 Stage 0 时就已经写好。所以从原理上讲,同一个请求的不同 Chunk 也可以像流水线一样一前一后走过各段,降低长 Prompt 的 TTFT。引擎是否这样调度取决于具体实现,以所用版本的行为为准。

和第 2 章 2.4 节讲过的一样,Chunked Prefill 的块大小还影响 Decode 的稳定性:块越大,含 Prefill 的那一格越长,上文 3.3 小节说的不均匀就越明显。


5. 层怎么分:最慢的一段决定速度

流水线的速度由最慢的一段决定。层的划分有几个容易忽略的地方:

  • 层数不一定整除:Llama-3.1-405B 有 126 层,PP=4 时分不成整数,只能有的段 32 层、有的段 31 层。
  • 首尾段有额外的工作:第一段有 Embedding,最后一段有最后的 Norm、LM Head 和采样。LM Head 要把隐藏状态乘到 128256 维的词表上,计算量和显存都不小。
  • MoE 模型的层不一样重:DeepSeek-V3 的前 3 层是 Dense FFN,其余 58 层是 MoE。

vLLM 的默认划分(vllm/distributed/utils.py 的 get_pp_indices)已经考虑了首尾段:层数除不尽时,余下的层优先分给中间段,不分给首尾段;中间段不够分时,再分给除最后一段以外的段。所以 405B 在 PP=4 下默认就是 31,32,32,31,启动日志会打印实际的划分。

默认划分仍然只按层数算。实测发现某一段明显更慢时,可以用环境变量 VLLM_PP_LAYER_PARTITION 手动指定每段的层数,数字个数必须等于 PP 大小,总和必须等于模型层数,否则启动时报错。例如 Llama-3.1-70B 在 PP=2 下默认是 40,40,如果最后一段因为 LM Head 偏慢,可以试 VLLM_PP_LAYER_PARTITION="41,39"。

💡 提示:调整划分前先测:用 Nsight Systems 或日志看各段每步的耗时,找出最慢的那段,再决定挪动哪几层。


6. 延迟、容量与 TP×PP 组合

6.1 单请求延迟不会变短

一个 Token 要依次走过 pp 段,每段读 1/p1/p 的权重,加起来还是读了一整份,只是分在不同的卡上先后进行。再加上 p−1p-1 次段间传输,单请求的 TPOT 比同样每段配置的单副本略长。

所以 PP 加卡换来的是容量和吞吐,不是延迟。要降低单请求延迟,只能让每段内部更快,也就是加大每段的 TP。

6.2 KV 容量随段数线性增长

每段只存自己那些层的 KV,每个 Token 在每张卡上的 KV 按 1/p1/p 缩小。和 TP 不同,PP 没有”KV 头不够分”的问题,不会出现复制。

6.1 节的估算表里,16 张卡开 TP=16 只能放下约 43 个 32K 请求,TP=8 × PP=2 能放下 87 个。原因就在这里:TP=16 时 8 个 KV 头已经被复制了一遍,而 PP=2 让每张卡只负责 40 层的 KV。

6.3 什么时候选 PP

🎯 情况选择原因
单节点装得下优先 TP,不用 PP同样卡数,TP 能降延迟,PP 不能
单节点装不下TP = 每节点卡数,PP = 节点数只有段边界的少量流量跨过网卡
卡数不能整除头数(如 6 张卡)PP,或 TP × PP 组合PP 按层切,各段层数可以不同
GQA 模型想用超过 KV 头数的卡TP 取 KV 头数,其余用 PP避开 KV 头复制
机内没有 NVLink实测 PP 与小 TP 的组合PP 的同步次数少得多,对互联要求低

⚠️ 注意:PP 需要足够的并发才能填满。低并发的交互式服务开 PP,大部分卡在等;离线批处理、高并发在线服务才能吃到 PP 的吞吐。


7. 动手算:PP 与 TP 的同卡数对比

沿用 6.2 节的假设成本模型:只算读权重、读 KV、TP 的 AllReduce 和 PP 的段间传输,忽略计算和调度。PP 时把并发请求平均分成 pp 个 Batch 轮流在飞。机内、跨机的固定开销 10 µs、30 µs 都是假设值。

# 假设成本模型:比较同卡数下 TP 与 TP×PP 的 Decode
# 只算读权重、读 KV、TP AllReduce 和 PP 段间传输,忽略计算、Kernel 启动与调度
HBM = 3.35e12                     # H100 SXM 显存带宽,B/s
NVLINK, IB = 450e9, 50e9          # 机内 NVLink 4 单向、机间 400 Gb/s 网卡,B/s
A_IN, A_X = 10e-6, 30e-6          # 机内、跨机每次通信的固定开销,假设值
LAYERS, HIDDEN = 80, 8192
WEIGHT_BYTES = 70.6e9 * 2
KV_PER_TOKEN = 320 * 1024         # 整个模型每 Token 的 KV,B
CTX = 4096                        # 每个请求的平均上下文长度


def stage_s(tp, pp, n, cross_node_tp):
    """一段处理 n 个请求、走一步 Decode 的时间(秒)"""
    alpha, bw = (A_X, IB) if cross_node_tp else (A_IN, NVLINK)
    weight = WEIGHT_BYTES / (tp * pp) / HBM
    kv = n * CTX * KV_PER_TOKEN / (min(tp, 8) * pp) / HBM    # TP>8 后 KV 不再缩小
    msg = n * HIDDEN * 2
    comm = 0 if tp == 1 else 2 * (LAYERS // pp) * (alpha + 2 * (tp - 1) / tp * msg / bw)
    return weight + kv + comm


def decode(tp, pp, conc, cross_node_tp=False, pp_bw=NVLINK, pp_alpha=A_IN):
    n = max(conc / pp, 1)                                     # 请求分成 pp 组轮流在飞
    p2p = pp_alpha + 2 * n * HIDDEN * 2 / pp_bw               # hidden_states + residual
    tpot = pp * stage_s(tp, pp, n, cross_node_tp) + (pp - 1) * p2p
    return tpot * 1e3, conc / tpot


CASES = [                        # 前三种在一台 8 卡机内;后两种横跨两台 8 卡机
    dict(tp=8, pp=1),
    dict(tp=4, pp=2),
    dict(tp=2, pp=4),
    dict(tp=16, pp=1, cross_node_tp=True),
    dict(tp=8, pp=2, pp_bw=IB, pp_alpha=A_X),
]
print(f"{'TP×PP':>6} {'卡数':>4} {'并发':>4} {'TPOT ms':>8} {'Token/s':>8} {'Token/s/卡':>10}")
for conc in [1, 64]:
    for kw in CASES:
        t, thr = decode(conc=conc, **kw)
        gpus = kw["tp"] * kw["pp"]
        print(f"{kw['tp']:>3}×{kw['pp']:<2} {gpus:>5} {conc:>5} {t:>8.2f} {thr:>8.0f} {thr / gpus:>11.0f}")

输出:

 TP×PP   卡数   并发  TPOT ms  Token/s  Token/s/卡
  8×1      8     1     6.93      144          18
  4×2      8     1    12.26       82          10
  2×4      8     1    22.91       44           5
 16×1     16     1     7.58      132           8
  8×2     16     1     6.96      144           9
  8×1      8    64    10.73     5967         746
  4×2      8    64    15.63     4094         512
  2×4      8    64    26.01     2461         308
 16×1     16    64    16.93     3780         236
  8×2     16    64     8.85     7233         452

在这组假设下:

  • 一台机器内,PP 越大越慢。8 张卡、并发 64 时,TP=8 的吞吐是 5967 Token/s,4×2 降到 4094,2×4 只有 2461。原因是 PP 把 64 个请求拆成了 pp 个小 Batch,每个小 Batch 都要把本段权重完整读一遍,权重读取没有被整个并发摊薄。
  • 跨两台机器时,PP 的优势出来了。TP=16 每层两次 AllReduce 都过网卡,并发 64 时 TPOT 16.93 ms;TP=8 × PP=2 只有段边界跨机,TPOT 8.85 ms,吞吐高出约 90%。
  • 单请求时,8×2 的 TPOT 和单机 TP=8 几乎一样(6.96 ms 对 6.93 ms)。多出来的 8 张卡没有让单请求变快,换来的是更大的 KV 容量:按 6.1 节的估算表,8×2 能放下的 Token 数约是单机 TP=8 的 2.4 倍。
  • 按卡数归一化,16 卡的 8×2(452 Token/s/卡)不如单机 TP=8(746)。如果一台机器已经装得下模型、KV 也够用,两台机器各开一个 TP=8 副本做 DP,总吞吐更高。PP 适合”一台装不下”的情况。

真实系统里还有计算时间、调度开销和上文 3.3 小节说的各种不均匀,PP 的气泡只会比这个模型更多。这些结论只说明趋势,部署时以实测为准。


📝 总结

  • PP 按层把模型切成 pp 段,段间只传当前步的 hidden_states 和 residual,每步 p−1p-1 次点对点发送,KV Cache 不跨段。
  • Decode 每一步依赖上一步的采样结果,单个 Batch 下每段利用率只有 1/p1/p。填满流水线要靠 pp 个由不同请求组成的 Batch 交错在飞,vLLM V1 用 Batch 队列实现。
  • 任何一格变长都会让后面的 Batch 排队,长上下文、混入的 Prefill、请求进出都会制造气泡。
  • 最慢的一段决定整条流水线的速度,首段的 Embedding、末段的 LM Head 和 MoE 的 Dense 层都会造成不均衡,vLLM 可用 VLLM_PP_LAYER_PARTITION 手动调整。
  • PP 不降低单请求延迟,换来的是 KV 容量随段数线性增长,且没有 KV 头复制。
  • 单节点内优先 TP;单节点装不下时 TP 取每节点卡数、PP 取节点数。

🎯 自我检验清单

  • 能说出 vLLM 中 PP 段间传递的张量,并算出给定 Batch 下每个段边界的传输量
  • 能解释推理 Decode 为什么不能用”切 Micro-batch”填满流水线,以及多个 Batch 交错的条件
  • 能画出 PP=4、4 个 Batch 交错的时间线,指出稳态下每段的利用率
  • 能列举至少三种造成 PP 气泡的不均匀来源
  • 能为一个层数不能整除段数的模型写出 VLLM_PP_LAYER_PARTITION
  • 能解释 PP 为什么不降低单请求 TPOT,以及它在 KV 容量上相对 TP 的优势
  • 能用本节的成本模型比较同卡数下 TP 与 TP×PP 的吞吐,并说明结论只在什么条件下成立

📚 参考资料