Files
xserv/docs/18-pipeline-parallelism.md
Gahow Wang 11e0154e4d docs: Phase 18 pipeline parallelism — design + benchmark results
docs/18-pipeline-parallelism.md: PP design (layer split, NCCL P2P,
per-stage KV, engine/threading model).
docs/benchmarks/pp-sweep.md: measured on dash5 (8x RTX 5090, Qwen3-8B
BF16) — single-stream latency + per-GPU VRAM (~1/N), byte-exact
correctness (single x2 vs pp4 x2 control), and the full AIME-30 +
GSM8K-30 quality matrix (xserv & llama.cpp PP=1/2/4): GSM8K 29/30 in
every cell, TPOT flat across PP.
README: multi-card (TP/PP) section + roadmap to Phase 18.
gitignore: /.claude/ runtime state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 18:57:09 +08:00

9.7 KiB
Raw Blame History

Phase 18: Pipeline Parallelism (PP)

目标:在单机多卡上做 流水线并行,把 Qwen3-8B 的 切成 Pstage 每张卡只持有连续的一段层(+ stage0 的 embed_tokens、最后一段的 norm/lm_head 激活hidden state在相邻 stage 之间用 NCCL P2P send/recv 传递。 与 TP按 head / 中间维切,每层 2 次 AllReduce互补PP 通信量小(每 token 仅 P-1 次点对点传 [tokens, hidden]KV 与权重按 降到约 1/P。 先做 PP=2 / 4组内,正确性优先。

1. 硬件约束dash5

  • 8× RTX 509032GBSM120无 NVLink,纯 PCIe Gen5。
  • 拓扑GPU 03 一组、47 一组,组内 PHB(同 host bridge可 P2P跨组 NODE
  • PP 同样建议在组内03 或 47虽然 PP 的通信量远小于 TP但 P2P 仍走 PCIe 跨组延迟更高。PP=2/4 用 01 / 03。
  • 相比 TPTP 每 token 2·layers = 72 次 AllReduce延迟主导PP 每 token 仅 P-1 次 send/recv每次 [tokens, hidden] BF16decode batch=1 时 8KBPP 对慢互联PCIe / 无 NVLink更友好,这是在 dash5 上做 PP 的主要动机之一。

2. 切分方案layer-wise

Qwen3-8Bhidden=4096num_heads=32num_kv_heads=8head_dim=128intermediate=12288layers=36vocab=15193636 能被 2/4 整除PP=3/6 需处理余数, 本阶段先要求 layers % P == 0)。

设 stage 数 P,本 stage = s,每段 L = layers / P 层,本段持有全局层 [s·L, (s+1)·L)

组件 持有者 说明
embed_tokens [vocab, hidden] 仅 stage 0 token → hidden
transformer block i 的全部权重 持有 i 的那个 stage 不切 head / 中间维(与 TP 正交)
该层 KV cache 持有 i 的那个 stage 每卡 KV 降到约 1/P
最终 norm [hidden] 仅最后一段
lm_head [vocab, hidden] 仅最后一段 hidden → logits
  • 注意力 / MLP 的层内计算 完全不变(不需要 AllReduce每个 stage 用它自己那几层 的完整权重、完整 head 做 forward。PP 与 TP 正交,可叠加(本阶段不实现 TP×PP
  • RoPE 用全局绝对 position每个 stage 的 RopeCache 完全相同(按 position 索引), 各 stage 独立做,无需通信。
  • 每个 stage 一个独立的 PagedKVCache,层数 = 本段层数 L(不是 36。forward 时 按「本段内的局部层号 0..L」索引 cache —— 与单卡代码完全一致,只是 self.layers 只装了本段的层。实现技巧:给 cache 传一个 num_hidden_layers 改写成 L 的 config 克隆, 无需改 PagedKVCache

通信点

  • prefillstage s 算完本段层,得到 [S, hidden]send 给 s+1s+1 recv 后接着算。
  • decode同理传 [B, hidden]batch=1 时 [1, hidden])。
  • 每 token 共 P-1 次 send/recv最后一段算出 logits 并采样。
  • 采样得到的 token id一个 u32)由 最后一段经线程内 channel 回传给 stage0 (同进程多线程,无需走 NCCL

3. 进程 / 线程模型

沿用 TP 的 单进程、多线程:每个 stage 一个 OS 线程,线程启动时 cudaSetDevice(stage)

  • stage 0 = 协调者coordinator,跑在调用线程上:持有 scheduler、tokenizer、HTTP response sender、停止判定eos / max_tokens与「下一步输入 token」。
  • stage 1..P-1 = worker 线程:从控制 channel 收命令Register/Prefill/Decode/Free/Shutdown 每步 recv 上游 hidden → 跑本段层 → send 给下游;最后一段 head+采样 → 把 token 回传 stage0。
  • 控制信息命令、采样参数、token idmpsc(极小);重活hidden 张量)走 NCCL P2PGPU↔GPU

v1 串行语义:一次处理一个请求、一次一个 token流水线每步「灌满又排空」 stage0 decode 第 t+1 步依赖最后一段第 t 步采出的 token。这保证 正确性 并拿到 TTFT/TPOT 与每卡显存;throughput 的真正收益来自 microbatch/请求级流水线 重叠1F1B,列为后续工作(见 §7

执行流(每请求):

coordinator                     worker s (1..P-1)            last stage (P-1)
─────────────                   ─────────────────            ────────────────
broadcast Register(slot)        cache.register(slot)         cache.register(slot)
broadcast Prefill{n,slot,samp}
 x=embed(prompt)
 x=layers_prefill(x,slot)
 send x → stage1                recv x ← s-1
                                x=layers_prefill(x,slot)
                                send x → s+1 ───────────────► recv x ← P-2
                                                             x=layers_prefill(x,slot)
                                                             logits=head(x); next=sample
 next ◄────────────── token channel ◄──────────────────────  token_tx.send(next)
 stream(next); loop Decode{slot} 直到 eos/length
broadcast Free(slot)            cache.free(slot)             cache.free(slot)

4. 通信库NCCL P2P

复用 xserv-distributed(已有 NCCL FFI + TpContext/AllReduce新增

  • FFIncclSend(sendbuff, count, dtype, peer, comm, stream)ncclRecv(recvbuff, count, dtype, peer, comm, stream)
  • PpContext:与 TpContext 同样的 ncclCommInitRank(一个 comm 跨 P 个 stage 外加 send_bf16_ptr(ptr, count, peer) / recv_bf16_ptr(ptr, count, peer),在 null stream 上发起(与模型 kernel 同流,天然有序)。
  • 线性流水线无死锁stage0 只 send、最后一段只 recv、中间段「先 recv 上游、再 send 下游」, 依赖链无环,从头解锁。每个 stage 在 send/recv + 本段计算后 synchronize() 确保 NCCL 读完发送缓冲再复用/释放v1 串行下成本可接受)。

决策点:和 TP 一样collective/P2P 先用 NCCL 把 PP 跑通拿正确性与基线; 手写 P2PPCIe 上的 cudaMemcpyPeer作为后续学习项。

5. 权重分片加载

Qwen3::from_weights_pp(config, weights, stage, num_stages, device)

  • 只把全局层 [s·L, (s+1)·L) 搬到本 stage 的 GPU其余层的权重直接 drop不占显存
  • embed_tokens:仅 stage 0 加载;其余 stage 放一个 1×1 占位张量forward 用 is_first_stage 守卫,永不触碰)。
  • norm/lm_head:仅最后一段加载;其余放占位。
  • head 不切(不做 TP所以 local_num_heads = num_headslocal_num_kv_heads = num_kv_heads

每卡显存 ≈ 权重(transformer 1/P) + KV(1/P) + (stage0: embed) + (last: norm+lm_head)。 对 Qwen3-8Btransformer 层约 14GBPP=2 每卡约 7GB 层权重 + embed 或 lm_head各 ~1.2GB)。

6. 实现步骤(逐步可验证)

  1. P18.1 — xserv-distributed P2PncclSend/Recv FFI + PpContext。 验收2 卡rank0 send 已知向量、rank1 recv校验一致tests/sendrecv.rs)。
  2. P18.2 — 分段权重加载from_weights_pp,每 stage 只持有本段层 + 该有的 embed/head。 验收:各 stage 层数 = L、显存约 1/P+ embed/head
  3. P18.3 — stage forwardembed / forward_layers_prefill / forward_layers_decode / head,每段独立 KV cache。 验收:PP=1 与单卡 forward_*_paged 逐 token 一致(同一条代码路径退化)。
  4. P18.4 — PP engine + --pp N:多线程 stage workers + NCCL 传递 + stage0 协调。 验收:--pp 2/4 端到端可服务;greedy 输出与单卡PP=1逐 token 一致 用现有 llama.cpp bench 跑正确性GSM8K/AIME测 PP=1/2/4 的 TTFT/TPOT/每卡显存。

7. 预期与风险

  • 显存:每卡 transformer 权重 + KV ≈ 1/P这是 PP 的主要收益(可上更大模型 / 更长 context
  • 单流吞吐v1 串行无 stage 重叠 → 单流 tok/s 不会超过单卡(多一份 P2P + sync 开销, 可能略低)。这是 PP 的本质:没有 microbatch 重叠就没有加速。诚实记录实测,并与 llama.cpp 的 --split-mode layer(同样是层切流水线、单序列也串行跨卡)对比 —— 两者单流 都应≈单卡。
  • 真正的 throughput 收益(后续):请求级 / microbatch 流水线1F1B让 stage 间重叠: stage1 算 microbatch A 时 stage0 算 B。需要把 scheduler 改成跨 stage 连续批处理。
  • 风险NCCL 多线程 init 同步send 缓冲生命周期(必须 sync 后再复用); layers % P != 0 的余数分配(本阶段先约束整除);与 CUDA Graph decode 的结合(先走非 graph 路径)。
  • 正确性优先:先 PP=1 等价(逐 token 对齐PP=2/4 与单卡对齐,再谈性能。

8. 与 llama.cpp 的对比口径

  • xserv--pp NCUDA_VISIBLE_DEVICES=0..N-1
  • llama.cpp-sm layer(默认即层切流水线)+ --tensor-split 均分层,CUDA_VISIBLE_DEVICES=0..N-1。 (对照 TP 用的是 -sm row。)
  • 指标正确性GSM8K / AIME exact-match、单流 TTFT/TPOT、并发吞吐、每卡 VRAM。
  • 复用 tools/bench/runner.pyrun_pp_parallel.sh(仿 run_tp_parallel.sh)。

9. 不在本阶段范围

  • TP×PP 混合2D 并行)、跨组 / 多节点。
  • microbatch / 1F1B 流水线重叠throughput 收益,后续)。
  • vocab-parallel embedding / lm_head。
  • layers % P != 0 的非均匀切分;与 CUDA Graph decode 结合。