02 · 并行策略与 PP 切分
- GLM5.3 CyberSec W4A16权重地址
- 部署仓库地址
本篇讲并行策略怎么选:MLA 模型上
TP换不来 KV 容量、PP提升容量、PP_LAYER_PARTITION=41,37层切分的来源,以及DP/EP/DCP/PCP不使用的原因。
文章目录
- 02 · 并行策略与 PP 切分
- 0. 结论
- 1. 硬件与显存占用计算
- 2. TP 对 KV 容量贡献是 0
- 3. PP:按层切分,容量 ≈ ×PP
- 3.1 机制
- 3.2 基于VLLM v0.30定制镜像
- 4. `41,37`:PP 显存均衡的那笔账
- 4.1 `39,39` 下卡 0-3 只用 70.7 GiB
- 4.2 四档切分对照
- 4.3 实测结果(`41,37` 优于预测)
- 5. PP=2 的正确性缺陷与「方案 B」
- 5.1 机制:DSA 的跨层索引共享被切分边界打断
- 5.2 实测退化(同口径 `p5_identity`,sane 采样 `temp=0.8 / top_p=0.9`)
- 5.3 修复(方案 B)
- 5.4 修复后 PP=2 全面优于 PP=1
- 5.5 层切分与 gate 的耦合(必须 preflight)
- 6. `deploy.sh` 里的 PP 缺省与门禁
- 7. 其余维度:不使用原因
- 8. PP 层间通信与 NCCL
- 9. 部署配置选择
- 10. 复现命令
0. 结论
- MLA 的 KV 在 TP rank 间完全复制,
--tensor-parallel-size对 KV 容量的贡献是0,相关容量占用必须按单卡算。 PP是唯一的扩容手段:TP=8/PP=1338,688 →TP=4/PP=2791,744(×2.34)→TP=2/PP=41,193,600(×3.52);v0.30 定稿形态PP=1386,943 →PP=2 + 41,37729,280。41,37是把末级闲置显存换成 KV:池 597,760 →729,280(+22.0%),最紧余量 1,802 →2,479 MiB(+38%)(§4)。PP=2有一个真实的缺陷:DSA 跨层 top-k 索引共享在切分边界错位 ⇒19~21% 退化(PP=1 为 0/120);「方案 B」修复后0/200。PP>2被check_pp_dsa_gate()硬拦:PP=4 关掉投机仍有20%退化(开 MTP k=3 为45%)⇒1M 上下文今天不可用。- 其余并行维度都不采纳:DCP 启动即断言失败、PCP 被 MTP + full CUDA graph 阻断、DP 要 16 卡、EP−1.5%、
--block-size容量中性。
1. 硬件与显存占用计算
nvidia-smi topo -m实测4 对 NVLink(0↔1、2↔3、4↔5、6↔7,NV8)+跨对 PCIe/SYS,GPU 0-3 在 NUMA0、4-7 在 NUMA1。所以本机不是"PCIe-only",但跨对 all-reduce 很贵。kernel profile 给出的量级(TP=8 时代):NCCL all-reduce 占 39.1%的窗口时间(AllReduce 36.4% + AllGather 2.4%),每步156 次 all-reduce × 191 µs(78 层 × 2);其余是 DSA 注意力 23.1%、Marlin MoE 18.6%、bf16 GEMM 12.0%。这张表是后面所有并行取舍的地基:通信量由"哪些维度要跨卡同步"决定。
显存侧的三个基础事实:
每卡权重的口径是
390.58 GiB / (TP × PP)(Checkpoint size: 390.58 GiB,启动日志weight_utils.py:858)。TP×PP 恒为 8 时每卡 ≈48.8 GiB,所以 PP 换来的是纯 KV 收益,不是"省权重"。每 rank 的显存预算 =
gmu × 79.25 GiB,gmu 0.95⇒75.29 GiB,所有 rank 相同。TP×PP 必须 ≥ 8:否则每卡权重 > 80 GiB 装不下(TP=4/PP=1 是 97.5 GiB/卡)。8 卡上就只剩 (8,1)、(4,2)、(2,4) 三种分法。
KV 的每 token 记账(
fp8_ds_mla、TP=8/PP=1、block_size=64,实测反推、误差 0.22%):
主 attention 层:79层(78 主干 +1MTP)×656B(fp8_ds_mla)=51,824B DSA indexer cache:21层(indexer_types=="full") ×132B=2,772B 每 token/rank=54,596Bblock_size=64⇒ 每 block/rank3,494,144 B(3.332 MiB)338,688token ×54,596B=17.222GiB ≤17.26GiB ✅- 1 GiB 显存 ≈ 19,668 token 的 KV
- DSA indexer cache 与主 KV 在同一个池里
2. TP 对 KV 容量贡献是 0
- 这是整套并行策略的起点,值得把机制写清楚:
- MLA 只有 1 个 KV 头(
num_kv_heads=1,mla_attention.py:1160-1169),页大小公式是(head_size + head_size_v) × dtype_size或state_content_bytes,里面没有任何tensor_parallel_size项。 - 池子大小按"本 rank 的层数"算:
num_blocks = int(available_memory // page_size // num_layers),除的是num_layers而不是 TP。 - DSA indexer 的 k_cache 同样复制:
deepseek_v2.py:656的注释就是# no tensor parallel, just replicated。
⇒每个 TP rank 都分配一份同样大的完整 MLA KV 池。启动日志里那句Maximum concurrency for 327,680 tokens per request: 1.04x就是这么来的。343,232 / (327,680/64) ≈ 1.047。所以"加卡"对上下文长度一点贡献都没有;想扩 KV 只能换维度 :层维(PP)或序列维(DCP)。DP 也不行,每个 DP rank 各自一份完整模型与 KV 池,单卡容量不变。
3. PP:按层切分,容量 ≈ ×PP
3.1 机制
PP 把 78 层主干切成 PP 段,每 rank 只建自己那段。KV spec 只从本 rank 的
static_forward_context取。于是num_blocks公式里的num_layers变成L/PP,而每请求 block 数不变 ⇒max_concurrency = num_blocks / blocks_per_req≈ ×PP。细节:所有 rank 的 block 数取最小值对齐(“Change the num_blocks of each rank to the smallest among all ranks”)。这既解释了"为什么最紧的 stage 决定一切",也解释了 78 不能被 4 整除(19.5)时为什么要用
VLLM_PP_LAYER_PARTITION手动分。
3.2 基于VLLM v0.30定制镜像
| 形态 | GPU KV cache size | max_model_len | 单流(内容受控中位) | conc16 |
|---|---|---|---|---|
TP=8/PP=1+ MTP k=3 +fp8_ds_mla(单流形态) | 386,943 | 352,000 | ~68 | 245.8 |
TP=4/PP=2+41,37+ MTP k=3 +fp8_ds_mla(缺省) | 729,280 | 524,288 | 71.3 | 297~310 |
按两列相除是≈1.88×(386,943 → 729,280),比 v0.28 时代的 2.34× 低,是因为这一档同时带着 MTP—— 末 rank 多一个 MTP 层,而它的 DSA indexer cache 也在这个 KV 池里(v0.28 上同镜像 A/B 测过:PP=2 无投机791,744vs PP=2 + MTP623,168,MTP 吃掉约21%的容量收益)。这是"容量换单流"的取舍,不是配置失误。
PP解决的是"每 token 占多少显存"里的层数因子,另一个因子是每层每 token 的字节数:fp8_ds_mla(656 B)相对 bf16(1152 B)的容量是×1.71(343,232 / 200,704,低于格式上限 ×1.756,差额被两种配置下同样大的 DSA indexer cache 摊薄),而bf16 的硬上限是 236,224 token。
4.41,37:PP 显存均衡的那笔账
4.139,39下卡 0-3 只用 70.7 GiB
实测(TP=4/PP=2 + MTP k=3 +gmu 0.95;单位 GiB):
| 权重 + 非 torch | 峰值 activation | CUDAGraph | 可用 KV | 实际分配 KV | 合计 | nvidia-smi | |
|---|---|---|---|---|---|---|---|
| PP0 卡 0-3 | 50.98 | 2.36 | 0.76 | 21.95 | 15.56 | 69.66 | 72,430 MiB |
| PP1 卡 4-7 | 56.53 | 3.20 | 1.49 | 15.56← 瓶颈 | 15.56 | 76.78 | 80,118 MiB |
预算0.95 × 79.25 = 75.29 GiB两 rank 相同,加上 0.93 GiB CUDA context 后与nvidia-smi逐项闭合到 ~0.2 GiB。根因链条:
- PP0 = 层 0–38(含 3 个 dense 层);PP1 = 层 39–77+ MTP 层 + lm_head + final norm,外加 MTP 带来的更大 CUDA graph pool(0.76 →1.49 GiB)与更大峰值 activation。
- 权重体积按 checkpoint 头实测:MoE 层5,177.8 MiB/层(MTP 层 5,339.7),
TP=4⇒≈1.30 GiB/卡/层。反推 PP0 =36×1,332.6 + 3×191 + 454 = 49,000 MiB,与启动日志的 47.85 GiB 吻合。 num_blocks = min_r(可用KV_r / page_r)⇒ 池子被PP1 的 15.56 GiB卡死。卡 0-3 的 6.4 GiB(vLLM 口径)/ 8.7 GiB(nvidia-smi口径)在39,39下无法变成上下文。
4.2 四档切分对照
- 把 MoE 层从 PP1 挪到 PP0,就是在"把前一段的闲置显存换成瓶颈段的可用 KV":
| 切分 | 瓶颈可用 KV | KV 池预测 | 单流算力(层当量,MTP×3) | gate 翻层 |
|---|---|---|---|---|
39,39(旧缺省) | 15.56 GiB | 597,760(实测) | 39 vs42 | rank1: 39,40,41 |
40,38 | 16.86 GiB | ≈647,700 (+8.4%) | 40 vs 41 | rank1: 40,41 |
41,37★ | 18.16 GiB | ≈697,600 (+16.7%) | 41 vs 40 | rank1: 41 |
42,36 | 18.05 GiB | ≈693,300 (+16.0%) | 42 vs 39 | 无(零依赖 gate) |
- ★ = 同时"KV 最大"且"算力最均衡"。末级每步要替 MTP 跑3 次前向,所以现状
39 vs 42末级本来就是流水线的 straggler,挪走两层同时缓解了容量与算力两件事。
4.3 实测结果(41,37优于预测)
| 项 | 39,39 | 41,37 | 变化 |
|---|---|---|---|
GPU KV cache size | 597,760 | 729,280 | +22.0%(优于预测的 +16.7%) |
| 524,288 单请求并发 | 1.14× | 1.39× | +22% |
step_ms(与内容无关) | 34.15 | 33.70 | −1.3% |
| 单流内容受控中位 | 71.2 | 71.3 | 相等(噪声内) |
| agent 档退化 | 0/40 | 0/40(后加测 0/200) | 保持 |
| 质量探针 / Responses / 工具往返 | 12/12 | 12/12 / 10/10 / 20/20 | — |
nvidia-smi(卡 0-3 / 卡 4-7) | 72,430 / 80,118 MiB | 79,441 / 79,343 MiB | 最紧余量 1,802 →2,479 MiB(+38%) |
- 安全前置
scripts/v030_pp_rebalance.sh preflightPASS:rank1 区间[41,78)、自算层[42,46,…,74]、gate 需翻层=[41]、新 gate 残余不安全 = 0;反例45,33上旧式区间法漏判 1 层、新get_pp_indices()为 0 ⇒ 反例成立、不是恒真断言。
5. PP=2 的正确性缺陷与「方案 B」
5.1 机制:DSA 的跨层索引共享被切分边界打断
本模型 DSA 配置
index_topk_freq=4+index_skip_topk_offset=3⇒只有层 2, 6, 10, …, 38, 42, …, 74 自己计算 top-k 索引,其余层复用最近一次自算的共享缓冲。PP=2按39,39切:rank0 = 层 0–38,rank1 = 层 39–77,而rank1 的第一个自算层是 42⇒ rank1 上的39/40/41 三层复用了本 rank 从未写入过的缓冲(首轮未初始化,之后是上一轮遗留的陈旧索引)⇒ 3/39 层注意力错误。PP=1 免疫,因为共享链在同一个 rank 内是完整的。诊断性的根因验证开关:
HF_OVERRIDES='{"index_topk_freq":1}'把跨层共享整个关掉(每层自算),代价是 indexer 计算量 ×4。
5.2 实测退化(同口径p5_identity,sane 采样temp=0.8 / top_p=0.9)
| 栈 | 退化率 |
|---|---|
PP=1+ MRV1 + MTP | 0/40 = 0%✅ |
PP=1+ MRV2 + MTP | 0/120 = 0%✅ |
PP=2+ MRV2 + MTP | 21/100 =21%❌ |
PP=2+SPEC_METHOD=none(完全不投机) | 19/100 =19%❌ |
- 失败形态很好认:先答对再循环、从不发出 EOS,一路打满 3000 token;连正常样本的 token 中位都比 PP=1 长2.3×(84 vs 37)。
- 统计上 Fisher 单尾p = 7.4e-09。⇒缺陷是
PP>1本身,与投机解码、与 MRV2 都无关。
5.3 修复(方案 B)
patches/sm80_v030/pp_index_share_fix.pyfrag:用get_pp_indices()取真实层区间,强制"本 rank 内没有索引可复用的那一层"自算;PP=1 时判定恒为 False ⇒ 逐位兼容。修后p5_identity0/100,完整验收0/200。
5.4 修复后 PP=2 全面优于 PP=1
| 指标 | 旧定稿TP=8/PP=1 | TP=4/PP=2+ 方案 B |
|---|---|---|
GPU KV cache size | 386,943 | 597,760(×1.55) |
max_model_len(单请求上限) | 352,000 | 524,288(×1.49) |
| 单流纯解码 | 71~81 tok/s | 85.6 tok/s |
| conc4 / conc8 / conc16 聚合 | 168.1 / 144.8 / 245.8 | 179.3 / 190.7 / 312.3(+27%) |
p5_identity退化 | 0/120 | 0/100 |
| 5 prompt 轮转退化(N=200) | 0/200 | 0/200 |
| 长上下文寻针 | 300K 3/3 | 500K 3/3 |
| 就绪 | 654~670 s | 540 s |
- 能全面超过的原因有两条,缺一不可:上游 v0.30 在 MRV2 上真正实现了 PP + 投机解码(
PPHandler.broadcast_drafts,上游 #50514 / #46994 / #55745),以及方案 B 把 19~21% 的退化消掉。前者让 PP 不再牺牲单流,后者让 PP 的输出能用。
5.5 层切分与 gate 的耦合(必须 preflight)
- rank0 永远不需要翻层(层 0,1,2 本身就是自算层);错位只发生在rank1 的开头。
- 旧的手写
rank*n//w区间式在39/40/41/42这四档上恰好也安全(边界都落在两个自算层 38与42 之间),但它是脆的:45,33这类跨过自算层的切分会让层 45 去复用卡在 PP0 上的层 42 的索引 ⇒ 静默读未写入缓冲 ⇒ 退化复发。 - 所以方案 B 改用
get_pp_indices()。42,36是唯一的"零依赖 gate"档(rank1 首层 42 本身就是自算层),代价是算力 42 vs 39,不如41,37的 41 vs 40 均衡。
6.deploy.sh里的 PP 缺省与门禁
- 脚本自己的变量是
PP_LAYER_PARTITION/PP_SIZE/TP_SIZE,其中层切分最终以VLLM_PP_LAYER_PARTITION注入容器。
| 变量 | 缺省 | 形式 | 作用 |
|---|---|---|---|
TP_SIZE | 4 | --tensor-parallel-size | 张量并行度 |
PP_SIZE | 2 | --pipeline-parallel-size(仅 PP≠1 时加) | 流水线并行度 |
PP_LAYER_PARTITION | 41,37 | VLLM_PP_LAYER_PARTITION | 各 stage 层数 |
DISABLE_CUSTOM_ALL_REDUCE | 0 | --disable-custom-all-reduce | TP=2(含 PP=4)时必须开,否则 graph profiling 阶段custom_all_reduce.cuh:164 'invalid argument'崩掉 EngineCore |
ALLOW_UNVERIFIED_PP | 0 | — | PP>2 的逃生舱 |
FORCE_V2_RUNNER | 空(按场景自动) | VLLM_USE_V2_MODEL_RUNNER | PP>1 自动 =1 |
门禁(都在启动前跑,./deploy.sh cmd预览时也会跑):
check_pp_dsa_gate():PP_SIZE > 2直接die,打印"关掉投机仍 20%、开 MTP 45%"与三条替代路径;研究用途可ALLOW_UNVERIFIED_PP=1放行(放行时再警告一次,并提示用完跑./deploy.sh degen)。三条路径都已验证:缺省 exit 0 / PP=4 exit 1 / 逃生舱 exit 0 且带警告(不是恒真断言)。check_pp_layer_partition():段数必须 ==PP_SIZE、各段之和必须 ==78(num_hidden_layers)、只接受非负整数,否则die;PP_SIZE <= 1时只 warn(vLLM 会忽略它)。check_pp_mtp_compat()/check_pp_ngram_compat():按镜像版本判定(vllm_supports_pp_spec()只在 0.30 为真)。0.30 上 PP>1 + MTP 走 MRV2;SPEC_METHOD=ngram不在 MRV2 支持列表 ⇒ 直接die。resolve_defaults():自动选出的mtp在"PP>1 且镜像不支持 PP+spec"时降级为none;PP>1 时自动钉VLLM_USE_V2_MODEL_RUNNER=1,因为 MRV2 对不支持的方法只会静默回退 MRV1,而 MRV1 的 PP+投机正是坏路径。
缺省自证:./deploy.sh defaultcheck逐项比对 argv(含顺序)与脚本会注入的每一个-e KEY=VALUE,有漂移即exit 2。
7. 其余维度:不使用原因
| 维度 | flag | 容量乘数 | 不采纳的原因 |
|---|---|---|---|
| DCP(decode context parallel) | --decode-context-parallel-size | 配置层×DCP | 本后端启动即断言失败(TritonMLASparseImpl.can_return_lse_for_decode = False,v1/worker/cp_utils.py:47-54);即便跳过断言,mla_attention.py:953-976的assert lse is not None也过不去(overlay 的forward_mqa显式return attn_out, None);index 本地化函数triton_filter_and_convert_dcp_index存在但本后端没调用过(DCP 下会静默读错 KV);indexer 的 DCP 合并只有 CuteDSL 版、注释写明首发面向 Blackwell/Hopper。尽调结论是15~25 人日,且它与 fp8 补丁改同一个文件triton_mla_sparse_kernel.py(同一容器路径只能挂一次)⇒ 必须并进 fp8 的补丁链。详见05 |
| PCP(prefill context parallel) | --prefill-context-parallel-size | 0 | 官方注释就写明"不增加 KV 分片数"(config/parallel.py:126-127);还要额外占 GPU(本机 8 卡没余量),并被MTP + full CUDA graph 双重阻断(pcp_manager.py:125-162),而且只支持 MRV2 |
| DP / 两个 TP=4 实例 | --data-parallel-size | 单卡容量不变、只乘总吞吐 | 需要16 卡(8 卡做不到);TP=4 时每卡权重 97.5 GiB > 80 GiB装不下 |
| EP(专家并行) | --enable-expert-parallel | 与容量无关 | 实测−1.5%(无收益);且必须同时选对--all2all-backend(默认allgather_reducescatter不是真 all-to-all),镜像里 pplx 未装 |
--block-size加大 | — | ≈0(分子分母抵消) | 只影响尾部浪费/对齐;且必须是64 的倍数 |
--num-gpu-blocks-override | — | 不凭空造显存 | 填大了 OOM、填小了更少 |
--kv-cache-memory-bytes | — | 只是"钉住 KV 字节数" | 仍受物理显存上限 |
DCP 与 PP 理论上正交(一个切序列、一个切层,乘数会相乘),配置校验层也能过,但在拿到 LSE 补丁前不要尝试。
8. PP 层间通信与 NCCL
- PP 的通信是相邻 stage 之间的激活张量传递,再加一条容易出事的小路径:末 rank 把采样结果广播回其它 stage(
_pp_broadcast_prev_sampled_token_ids())—— v0.28 上PP>1 + MTP的第三层崩溃正是它:该广播只在 async 下走、且只支持无投机的形状[num_reqs, 1],而带 MTP 时是[num_reqs, num_spec+1]⇒ 直接断言失败。
本项目测量过的通信大头是TP 的 all-reduce:39.1%(AllReduce 36.4% + AllGather 2.4%)的窗口时间、每步 156 次 × 191 µs —— 这些都是TP=8/PP=1形态的数字。改成TP=4/PP=2后,all-reduce 的通信域缩到 stage 内的 4 卡、每 rank 层数减半,同时 stage 之间多出一条点对点路径;这两笔账的净量级我们没有单独 profile(--disable-custom-all-reduce退回 NCCL 的性能代价也未单独测量)。
唯一确定的是:NCCL 的三个旋钮(NCCL_ALGO=allreduce:tree/NCCL_PROTO=LL128/NCCL_MIN/MAX_NCHANNELS=1)在 TP=8 形态上全部负收益,默认RING_LL已是最优。
9. 部署配置选择
| 约束 | 布局 | KV 池 | 单流(内容受控中位) | conc16 | 上下文 | 正确性 |
|---|---|---|---|---|---|---|
| 综合最优 ← 缺省 | TP=4/PP=2+41,37+ MTP k=3 + fp8 | 729,280 | 71.3 | 297~310 | 512K | ✅ 0/200 · 12/12 · 工具往返 100/100 |
| 单流优先(旧缺省) | TP=8/PP=1+ MTP k=3 + fp8 | 386,943 | ~68 | 245.8 | 352K | ✅ 0/120(旧口径) |
| 吞吐优先(可选档) | TP=4/PP=2+41,37+bf16KV | 458,560 | 69.2 | 363 | 256K | ⚠️ 未做退化验收 |
| 超长单请求(1M) | TP=2/PP=4+20,20,20,18 | 1,199,872(无投机) | 64.0 | 214.4 | 1M | 🔴 20%(无投机)/ 45%(+MTP)退化,已被硬拦 |
- 128K 并发的容量账:池 729,280 ÷ (131,072 + 256) = 5.55 ⇒ KV 允许5 条常驻(实测 5/5、6/6 都被收下)。但"能收下 ≠ 更快":长 prefill 会被调度串行化,墙钟 ≈ 条数 × 50 s,prefill 聚合吞吐恒定 ≈2,600 tok/s。
- 一句话:PP 是"换容量"的重量手段;在 v0.30 上它恰好不再牺牲单流,因为 MTP 在 PP 下也能工作了。
10. 复现命令
# ① 缺省(= TP=4/PP=2 + 41,37 + 512K + fp8 + MTP k=3)与缺省自证./deploy.sh start ./deploy.shwait./deploy.sh cmd# 只看要拼出的命令行,不启动./deploy.sh defaultcheck# argv 逐项 + env 全部比对;有漂移 exit 2# ② 回退/对照PP_LAYER_PARTITION=39,39./deploy.sh start# 旧切分(KV 597,760)TP_SIZE=8PP_SIZE=1MAX_MODEL_LEN=352000./deploy.sh start# 单流形态(KV 386,943)KV_CACHE_DTYPE=bfloat16MAX_MODEL_LEN=262144./deploy.sh start# 吞吐档(conc16 ≈363)# ③ 层切分的 gate 安全性与完整 A/B(preflight 不动服务)scripts/v030_pp_rebalance.sh preflight scripts/v030_pp_rebalance.sh control# 在当前切分上采基线scripts/v030_pp_rebalance.sh boot41,37# stop + 换切分启动 + 启动门禁scripts/v030_pp_rebalance.sh verify# smoke / needle / degen / speed / qualityscripts/v030_pp_rebalance.sh verdict# 两支的显存·池·速度·退化对比表# ④ PP>2:默认被硬拦;确认拦截与"逃生舱"行为PP_SIZE=4TP_SIZE=2./deploy.sh start# 期望 exit 1(check_pp_dsa_gate)ALLOW_UNVERIFIED_PP=1TP_SIZE=2PP_SIZE=4PP_LAYER_PARTITION=20,20,20,18\MAX_MODEL_LEN=1048576DISABLE_CUSTOM_ALL_REDUCE=1./deploy.sh start# 研究用途,警告放行# ⑤ 正确性判据(PP 切分的真正验收线,不是容量)./deploy.sh degen# agent 档退化(有退化 exit 2)AGENT_PROBE_TRIALS=200./deploy.sh degen# 收窄上界# ⑥ 根因诊断开关(不是推荐配置):关掉 DSA 跨层索引共享,indexer 计算量 ×4HF_OVERRIDES='{"index_topk_freq":1}'./deploy.sh start