sskj/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search

Kimi K3 推理优化 - 优化 Prefill - Deep PP

1. 工作背景

Kimi-K3 在 4 台 RTX 6000D、共 32 张 GPU 上部署时,最直接的方式是 PP1 / TP32 / EP4:完整模型只有一个 Pipeline StageTensor Parallel 覆盖全部 32 张卡MoE Expert Parallel 使用 4 张卡。

这套配置可以正确运行,但 16K 长输入的 Prefill 延迟很高。既有 Nsight Systems Profile 显示,在 16K -> 1、C=8、chunk=8K 场景中:

  • 稳定 GPU 的 Prefill 窗口中NCCL 暴露时间占比中位数为 57.27%
  • 单个 Profile 窗口约有 557 次 AllReduce
  • AllReduce p50 约为 7.10 ms
  • 通信与计算重叠率接近 0%
  • 其中 MoE 相关 AllReduce 约占已分类 AllReduce 时间的 58.73%

这说明 PP1/TP32 并不只是“32 张卡一起算得更快”。Kimi-K3 每层中频繁出现 的 TP Collective 会跨越 4 台服务器,在没有 NVLink 的 RTX 6000D 上通过 RoCE 执行;等待通信的时间已经超过实际计算时间。

2. 为什么尝试 Deep PP

本实验所说的 Deep PP是把一个模型划分为更多 Pipeline Stage同时相应 缩小每个 Stage 内的 TP Group

总 GPU 数 = PP × TP = 32

在固定 32 张 GPU 的前提下,本实验考察:

配置 每个 Stage 使用的 GPU Stage 与节点的关系
PP1 / TP32 32 一个 Stage 跨 4 个节点
PP2 / TP16 16 一个 Stage 跨 2 个节点
PP4 / TP8 8 一个 Stage 完整落在 1 个节点
PP8 / TP4 4 每个节点放置 2 个 Stage
PP16 / TP2 2 每个节点放置 4 个 Stage

PP 加深后会新增 Pipeline Activation 的 Stage-to-Stage 传输,也会产生流水线 填充和排空造成的 bubble因此 PP 并不是越大越快。它可能带来收益的原因是:

  1. 每层高频 TP AllReduce 的通信域从 32 卡逐步缩小到 16、8、4 卡;
  2. PP4 开始TP Group 完整落在单机内,不再让每次 TP AllReduce 都跨节点;
  3. PP8 中 TP4/EP4 都位于单机,跨节点通信主要集中在少数 Pipeline 边界;
  4. 只要 C=8/16 提供了足够多的请求Scheduler 就可能用不同请求交错填充 Pipeline抵消一部分 bubble。

因此这一阶段要回答的不是“PP 能不能启动”,而是:在 Kimi-K3、6000D 和固定 Prefill 压力下,缩小 TP 通信域的收益能否超过 Pipeline 开销,以及 拐点出现在 PP2、PP4、PP8 还是 PP16。

3. 实现方式

3.1 唯一实验入口

实验只保留一个入口脚本:

run_pp_baseline_search.sh

脚本负责:

  1. 检查 601-604 的模型、镜像与 /dev/infiniband
  2. 依次启动 PP2/TP16、PP4/TP8、PP8/TP4、PP16/TP2 服务;
  3. 通过 /get_server_info 校验实际 PP、TP、EP 和 MoE backend
  4. 每个配置执行 C=8/16各重复 3 次;
  5. 校验每轮是否完成 40/40 请求以及是否存在请求错误;
  6. 保存完整 Docker 命令、服务日志、benchmark 日志、原始 JSONL 和 GPU 快照;
  7. 生成逐轮 results.csv 和中位数 summary.csv
  8. 无论成功或失败,最终清理 4 台服务器上的实验容器。

3.2 SGLang PP 路径

本轮使用镜像:

local/sglang:kimi-k3-sm120-flashinfer-mxfp4-phase5

服务直接通过 SGLang 官方参数启动:

python3 -m sglang.launch_server \
  --model-path /data/hf_models/Kimi-K3 \
  --tp-size "${TP_SIZE}" \
  --pp-size "${PP_SIZE}" \
  --ep-size "${EP_SIZE}" \
  --nnodes 4 \
  --node-rank "${NODE_RANK}" \
  --dist-init-addr 174.1.60.1:20000 \
  --moe-runner-backend flashinfer_mxfp4 \
  --chunked-prefill-size 8192

脚本默认取 EP_SIZE=min(4, TP_SIZE)。因此 PP2/4/8 使用 EP4PP16 因 TP2 必须使用 EP2满足 SGLang 的 TP % EP == 0 约束。

实验没有复制同事旧代码,也没有注入 patch_pp_group.py。准备阶段曾验证过 一版运行时 PP Group patch但它会让 Python Object 通信和 Tensor/P2P 通信 复用错误的 Gloo Group出现消息大小不匹配。回到当前 SGLang 上游实现后, PP2、PP4 和 PP8 均能正常启动,因此该 patch 被完全移除,没有进入正式数据。

3.3 网络配置

4 个节点都向容器映射 /dev/infiniband,并使用:

NCCL_SOCKET_IFNAME=bond0
GLOO_SOCKET_IFNAME=bond0
NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3
NCCL_IB_GID_INDEX=3

bond0 承担 Bootstrap/控制连接,mlx5_0~3 是 NCCL 的 RoCE HCA。正式运行 中没有出现 NCCL、Gloo、RDMA、connection refused 或 communicator failure。

4. 实验设计

为了让结果只反映 PP/TP 变化,其他主要变量保持固定:

项目 固定值
模型 Kimi-K3
GPU 601-60432 × RTX 6000D
MoE runner FlashInfer MXFP4
A2A backend none
EP PP1/2/4/8 为 4PP16 为 2
输入/输出 16,384 / 1 token
Chunked Prefill 8,192 token
并发 C=8、C=16
每轮请求数 40
重复次数 3
数据集 random-ids输入长度比例固定为 1.0
Prefix Cache --disable-radix-cache
显存比例 0.88

输出长度固定为 1是为了让 TTFT 和 Input TPS 主要反映 Prefill而不是被长 Decode 混入。每个点重复 3 次,最终使用中位数,避免首轮 JIT、缓存状态或系统 瞬时抖动主导结果。

PP1 没有重复跑。它来自前一阶段已经完成的 FlashInfer MXFP4、EP4、chunk8K 实验。经逐项核对PP1 与本轮使用相同镜像、模型、MoE backend、EP、输入输出、 并发、请求数、显存比例和网络参数,区别只有 PP1/TP32,因此可以直接并入。

5. 一个关键实现差异PP Micro Batch

同事旧 PP4 配置显式设置过:

--pp-max-micro-batch-size 1

当前 SGLang 在该参数未设置时,会在 Scheduler 初始化后自动计算:

pp_max_micro_batch_size = max(max_running_requests // pp_size, 1)

接纳新请求时又会执行:

allocatable_requests = pp_max_micro_batch_size - running_batch_size

因此这里的 1 不只是一个普通的 Kernel batch 参数,它会把 PP Scheduler 允许同时运行的请求数压到 1。对于 C=8/16这会让请求大量排队Pipeline 难以通过请求交错保持各 Stage 忙碌bubble、P2P 和调度固定成本都会被放大。

本实验不传这个参数,使用 SGLang 当前的自动值。旧实验还混合了 Marlin、 chunk16K、EP1/EP8、PP16、旧镜像和运行时 PP patch因此旧数据不能作为 “PP 天生更慢”的单变量证据。

6. 实验过程与正确性

正式 Run ID

kimi3-pp-upstream-20260821-120208
kimi3-pp16-20260821-161721

服务启动时间分别为:

配置 健康检查通过时间
PP2 / TP16 / EP4 417 s
PP4 / TP8 / EP4 405 s
PP8 / TP4 / EP4 338 s
PP16 / TP2 / EP2 538 s

PP2、PP4、PP8 共形成 18 个正式样本;补充的 PP16 形成 6 个正式样本。 每个样本均完成 40/40 请求,原始结果中的 errors 为空。服务和 benchmark 日志没有发现:

  • CUDA OOM / OutOfMemory
  • Traceback / Exception
  • NCCL / Gloo / RDMA failure
  • Engine failure
  • 请求连接失败或提前终止。

PP8 在 C=8 和 C=16 下三次 Input TPS 的极差都小于 0.7%,说明结果不是某 一次运行的偶然峰值。

PP16 首次启动在 decode CUDA Graph 捕获时触发 custom all-reduce IPC buffer 注册错误;关闭 PP16 的 decode graph 后16-stage warmup 又超过默认 300 秒 watchdog。正式 PP16 使用 --disable-decode-cuda-graph --watchdog-timeout 3000000538 秒后健康。该差异不改变本轮 Prefill 路径:输出仅 1 token 没有迭代 Decode但 PP16 的 Decode 性能不能由本实验推断。PP16 三次 Input TPS 极差不超过 0.7%,结果同样稳定。

7. 性能结果

下表均为 3 次正式运行的中位数:

PP / TP / EP C Input TPS TTFT p50 TTFT p95 Input TPS 相对 PP1 TTFT p50 相对 PP1
1 / 32 / 4 8 3,257.96 39.19 s 41.62 s 基线 基线
2 / 16 / 4 8 5,015.95 25.22 s 26.69 s +53.96% -35.65%
4 / 8 / 4 8 7,155.20 17.28 s 18.62 s +119.62% -55.90%
8 / 4 / 4 8 8,809.50 13.47 s 16.45 s +170.40% -65.64%
16 / 2 / 2 8 6,701.04 17.94 s 21.39 s +105.68% -54.22%
1 / 32 / 4 16 3,260.14 78.39 s 80.88 s 基线 基线
2 / 16 / 4 16 5,018.38 50.70 s 52.14 s +53.93% -35.32%
4 / 8 / 4 16 7,152.58 34.84 s 35.95 s +119.39% -55.56%
8 / 4 / 4 16 8,824.12 27.31 s 29.77 s +170.67% -65.16%
16 / 2 / 2 16 6,700.96 35.88 s 39.49 s +105.54% -54.23%

从 PP1 到 PP8Input TPS 沿着 PP2、PP4、PP8 单调上升TTFT 也同步下降; PP16 则出现反转。相对 PP8PP16 在 C=8/16 的 Input TPS 分别下降 23.93% 和 24.06%TTFT p50 分别增加 33.18% 和 31.37%。这说明继续把 TP4 缩到 TP2 的收益已无法抵消 16-stage bubble、更多 P2P 边界和调度开销。

PP16 同时把 EP4 改为 EP2因此它不是严格的纯 PP 单变量对照。但它准确代表 32 卡上可实际部署的 PP16/TP2 组合,足以判定该组合不应替代 PP8 基线。

C=16 的 TTFT 大致是 C=8 的两倍,而同一 PP 下 Input TPS 基本不变。这表明 这批 Prefill 工作已接近该配置的稳定吞吐区:增加并发主要增加排队等待,而不是 继续提高输入吞吐。PP 改善的是服务处理这批 token 的速率,因此两个并发点都 出现了接近一致的 TPS 增益。

8. 显存结果

下表取每个配置 benchmark 完成后4 个节点 32 张卡中的最高显存:

PP / TP / EP 峰值显存 相对 85,651 MiB 的剩余空间
1 / 32 / 4 81,355 MiB 4,296 MiB
2 / 16 / 4 80,335 MiB 5,316 MiB
4 / 8 / 4 82,417 MiB 3,234 MiB
8 / 4 / 4 79,407 MiB 6,244 MiB
16 / 2 / 2 79,989 MiB 5,662 MiB

PP 的 Stage 不会获得完全相同的层、输出头和状态,因此各 PP 配置的显存不再 像 PP1 那样均匀。PP4 的最重 Stage 已使用约 82.4 GiBPP8 的最重 Stage 约 79.4 GiB在本轮反而保留了最多的最坏 Rank 余量。

这一点对下一阶段 A2A 很重要,因为 DeepEP 或 FlashInfer A2A 都可能申请额外 dispatch/combine buffer。只看 TPS 而忽略最重 Rank可能出现基线可跑、打开 A2A 后启动或 Prefill OOM 的情况。

9. 通信结果如何解释

9.1 累计收益变大,但每次增加 PP 的边际收益在下降

第 7 节相对 PP1 的累计数字会让人感觉“PP 越大,收益增长越快”。逐级比较后, 实际规律相反。以 C=8 为例:

变化 Input TPS 边际变化 TTFT p50 边际变化
PP1 -> PP2 +53.96% -35.65%
PP2 -> PP4 +42.65% -31.48%
PP4 -> PP8 +23.12% -22.05%
PP8 -> PP16 -23.93% +33.18%

所以 PP1 到 PP8 的累计收益持续扩大,但每次把 PP 翻倍带来的新增收益已经逐步 减小PP16 则越过最优点。C=16 也呈现相同趋势。

9.2 节点内 PCIe 与节点间 RoCE 的实测

在 601--604 上使用 NCCL 2.28.9 + CUDA 13 的 all_reduce_perf,分别测量与 PP Stage 对应的 TP4、TP8、TP16 和 TP32 通信域。所有测试均为 0 个错误值:

通信域 实际拓扑 128 MiB 延迟 128 MiB busbw 128/256 MiB 平均 busbw
TP4 单节点 GPU0--3同一 PCIe/NUMA 岛 4.93 ms 40.85 GB/s 41.00 GB/s
TP8 单节点 8 卡,跨两个 NUMA 岛 6.00 ms 39.13 GB/s 38.89 GB/s
TP16 两节点 6.24 ms 40.31 GB/s 40.60 GB/s
TP32 四节点 6.59 ms 39.47 GB/s 40.08 GB/s

大消息带宽都在约 39--41 GB/s说明 PP 收益不是因为节点间 400G RoCE 的 持续带宽远低于节点内 PCIe。真正变化更明显的是 collective 的参与 Rank 数、 固定延迟和同步范围128 MiB AllReduce 从 TP32 的 6.59 ms 降到 TP4 的 4.93 ms。

两个拓扑边界尤其重要:

  • PP2 -> PP4 把每个 Stage 的 TP 域从两节点 TP16 缩到单节点 TP8逐层 AllReduce 不再跨节点;
  • PP4 -> PP8 把 TP8 缩到 TP4每个 Stage 可落在同一个 PCIe/NUMA 岛,避免 单节点内跨 CPU Root Complex同时每卡得到更大的 GEMM 分片,减轻 TP32 过度切分造成的小矩阵低效率。

9.3 PP1 与 PP8 的 Nsight 对照

为避免把 Profiler 开销混入第 7 节性能结果,另起同场景短窗口 Nsight 16K -> 1, C=8, chunk=8K。以下是稳定 Rank 的中位数:

每个活跃 Rank/Stage 的指标 PP1 / TP32 PP8 / TP4 变化
普通 AllReduce 调用数 557 66 -88.2%
普通 AllReduce 累计时间 4,160.6 ms 392.3 ms -90.6%
单次 AllReduce p50 7.10 ms 5.83 ms -17.9%
计算与 NCCL 时间线重叠 约 0 约 0 未形成有效重叠

这里的 557 -> 66 是单个 Rank/Stage的 Profile 窗口,不是一个请求在整个 模型上的 collective 总数。PP8 把模型层分给 8 个 Stage一个请求仍要依次经过 全部 Stage它减少的是每个活跃 Rank 在一个 Stage 内负责的层数和同步工作, 并让不同请求的 Stage 可以流水并行,而不是凭空删除模型全部通信。

PP8 Trace 还出现约 3.43 s 的非 AllReduce NCCL 区间,主要对应 Pipeline Send/Recv 及 Stage 等待。该值包含异步 Stage 等待,不能直接当作 3.43 s 的 网络传输时间,也不能把 PP8 的 NCCL wall 百分比与 PP1 做等口径比较。可靠的 对照是上表中的调用数、普通 AllReduce 累计时间和单次延迟。

9.4 为什么 PP8 最优PP16 反而退化

PP1 -> PP8 的收益由四项共同产生:

  1. TP 通信域由 32 Rank 缩到 4 Rank单次 collective 延迟降低;
  2. PP4/PP8 的 TP collective 保持在节点内PP8 进一步保持在同一 PCIe/NUMA 岛;
  3. 每个 Rank 只执行所在 Stage 的层C=8/16 的不同请求能在多个 Stage 并行, 将通信和计算流水化;
  4. TP32 过度切分的 GEMM 在 TP4 下变成更大的本地矩阵Kernel 效率更高。

收益没有继续延伸到 PP16C=8 时请求数甚至少于 16 个 Stage流水线无法填满 C=16 也只有一波请求。更多 Stage 边界、P2P Send/Recv、调度和 bubble 超过了 TP4 -> TP2 的剩余收益,而且 PP16 还因整除约束从 EP4 改成 EP2。因此当前证据 支持的是“PP8 在这个 Prefill 压力点达到通信、计算粒度与流水线开销的平衡”, 而不是“PP 越大越快”。

10. 最终判断与后续使用

在本次固定场景中,PP8 / TP4 / EP4 同时得到:

  • 最高的 Input TPS
  • 最低的 TTFT p50/p95
  • 三次重复中稳定的结果;
  • 比 PP1 和 PP4 更充足的最坏 Rank 显存余量;
  • 单节点内的 TP4/EP4 通信域。

因此,下一阶段 MoE A2A 的首个兼容性和显存 Smoke 应固定使用:

PP8 / TP4 / EP4
16K -> 1
C=8/16
chunked_prefill_size=8192
FlashInfer MXFP4

PP4 / TP8 / EP4 保留为回退对照:如果某个 A2A backend 对 PP8/TP4 有当前 代码未覆盖的约束,可以快速判断问题来自 backend 兼容性,还是 A2A 本身。

PP16 / TP2 / EP2 不进入下一阶段默认矩阵。它相对 PP1 仍有明显收益,但比 PP8 慢约 24%,并引入 decode graph 兼容性、超长启动 warmup 和 EP2 变量。

这项结果不能外推到低并发 C=1、长 Decode、在线混合流量或更多节点。当前 TTFT 即使在 PP8 下仍为 13.47 sC8和 27.31 sC16距离 5 s 目标仍有 明显差距。Deep PP 解决了一个主要通信瓶颈,但不是 Prefill 优化的终点。

11. 复现命令与证据

在 601 执行:

cd /data/hzy/sskj/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search
RUN_ID=kimi3-pp-$(date +%Y%m%d-%H%M%S) \
  SUDO_PASSWORD_FILE=/path/to/password-file \
  bash run_pp_baseline_search.sh run

PP2/4/8 正式结果:

/data/hzy/sskj/experiments/pro6000/
  kimi3_pro6000_sglang_pp_baseline_search/
  results/kimi3-pp-upstream-20260821-120208/

其中:

summary.csv       6 个 PP×C 中位数
results.csv       18 个逐轮结果
metadata/         实际服务配置与 Run manifest
service/          四节点 Docker 命令和完整服务日志
bench/            benchmark 文本输出
raw/              benchmark JSONL
gpu/              四节点启动前、健康、完成和清理后的显存快照

PP1 原始结果:

/data/hzy/sskj/experiments/pro6000/
  kimi3_pro6000_sglang_tp32ep32_moe_backend_prefill/
  results/kimi3-ep4-moe-full-20260818-151349/

PP16 正式结果:

/data/hzy/sskj/experiments/pro6000/
  kimi3_pro6000_sglang_pp_baseline_search/
  results/kimi3-pp16-20260821-161721/

PP16 启动兼容性证据(未产生 benchmark 数据):

results/kimi3-pp16-20260821-155734/  # custom AR decode graph IPC 错误
results/kimi3-pp16-20260821-160555/  # 默认 300s watchdog 超时

PP1 Nsight 通信归因:

/data/hzy/sskj/experiments/pro6000/
  kimi3_pro6000_sglang_prefill_communication_profile/
  results/kimi3-prefill-comm-20260820-143749/nsys_analysis.json

PP8 Nsight 通信归因与四组 NCCL 微基准:

/data/hzy/sskj/experiments/pro6000/
  kimi3_pro6000_sglang_prefill_communication_profile/
  results/kimi3-prefill-comm-pp8-20260821-172717/

nsys_analysis.json          # PP8 各 Rank CUDA/NCCL 汇总
nsys_device_metrics.csv     # PP8 各设备时间线指标
nsys/node0/prefill_node0.nsys-rep  # 601 原始 Nsight Trace
nsys/node1/prefill_node1.nsys-rep  # 602 原始 Nsight Trace
nsys/node2/prefill_node2.nsys-rep  # 603 原始 Nsight Trace
nsys/node3/prefill_node3.nsys-rep  # 604 原始 Nsight Trace
nccl_tp4_intra_node.log     # 单节点同 PCIe/NUMA 岛
nccl_tp8_intra_node.log     # 单节点跨 NUMA
nccl_tp16_2node.log         # 两节点
nccl_tp32_4node.log         # 四节点