From a58b931cc834da8bb94a59cf24f393e78385e1f6 Mon Sep 17 00:00:00 2001 From: Zhiyi Hong <2497491955@qq.com> Date: Fri, 21 Aug 2026 18:05:42 +0800 Subject: [PATCH] [Profiling] Explain Kimi-K3 Deep PP scaling --- .../README.md | 107 +++++++++++++++--- .../run_prefill_communication_profile.sh | 17 ++- 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/README.md b/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/README.md index f96e41c..9052065 100644 --- a/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/README.md +++ b/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/README.md @@ -263,22 +263,84 @@ A2A 后启动或 Prefill OOM 的情况。 ## 9. 通信结果如何解释 -本轮 PP 搜索没有为 PP2/4/8/16 再录制 Nsight,也没有在每个 benchmark 前后采集 -独立 HCA Counter,因此不能把 PP1 的 57.27% NCCL 占比直接写成 PP8 的实测 -通信占比。 +### 9.1 累计收益变大,但每次增加 PP 的边际收益在下降 -能够被当前证据直接证明的是: +第 7 节相对 PP1 的累计数字会让人感觉“PP 越大,收益增长越快”。逐级比较后, +实际规律相反。以 C=8 为例: -1. PP1 的同口径 Nsight 确认 TP32 Prefill 以暴露的 AllReduce 为主; -2. PP2/4/8/16 的 `/get_server_info` 证明实际 PP/TP/EP 与计划一致; -3. Rank 布局使 PP4 的 TP8、PP8 的 TP4/EP4 都限制在单节点内; -4. 所有 PP 配置的 NCCL P2P communicator 成功建立并完成全部请求; -5. Input TPS 按 PP1 < PP2 < PP4 < PP8 提升,随后 PP16 回落,表明通信域 - 缩小确有收益,但 Deep PP 开销在 PP8 之后成为更大的限制。 +| 变化 | 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% | -因此当前结果支持通信域重构这一解释,但没有伪造一个未经采样的“PP8 NCCL -占比”。如需进一步拆解 PP8 内部剩余时间,应单独执行短窗口 Nsight,而不是 -把 Profile 开销混进这组性能基准。 +所以 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 效率更高。 + +收益没有继续延伸到 PP16:C=8 时请求数甚至少于 16 个 Stage,流水线无法填满; +C=16 也只有一波请求。更多 Stage 边界、P2P Send/Recv、调度和 bubble 超过了 +TP4 -> TP2 的剩余收益,而且 PP16 还因整除约束从 EP4 改成 EP2。因此当前证据 +支持的是“PP8 在这个 Prefill 压力点达到通信、计算粒度与流水线开销的平衡”, +而不是“PP 越大越快”。 ## 10. 最终判断与后续使用 @@ -371,3 +433,22 @@ PP1 Nsight 通信归因: kimi3_pro6000_sglang_prefill_communication_profile/ results/kimi3-prefill-comm-20260820-143749/nsys_analysis.json ``` + +PP8 Nsight 通信归因与四组 NCCL 微基准: + +```text +/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 # 四节点 +``` diff --git a/experiments/pro6000/kimi3_pro6000_sglang_prefill_communication_profile/run_prefill_communication_profile.sh b/experiments/pro6000/kimi3_pro6000_sglang_prefill_communication_profile/run_prefill_communication_profile.sh index 4d2b5c7..3268568 100755 --- a/experiments/pro6000/kimi3_pro6000_sglang_prefill_communication_profile/run_prefill_communication_profile.sh +++ b/experiments/pro6000/kimi3_pro6000_sglang_prefill_communication_profile/run_prefill_communication_profile.sh @@ -17,6 +17,9 @@ DIST_PORT="${DIST_PORT:-20000}" PROFILE_STEPS="${PROFILE_STEPS:-3}" CONCURRENCY="${CONCURRENCY:-8}" NUM_PROMPTS="${NUM_PROMPTS:-40}" +PP_SIZE="${PP_SIZE:-1}" +TP_SIZE="${TP_SIZE:-32}" +EP_SIZE="${EP_SIZE:-4}" CONTAINER_PREFIX="kimi3_prefill_comm_profile" SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10) @@ -89,7 +92,7 @@ start_node() { host="${NODE_HOSTS[$rank]}" name="$(container_name "$rank")" output="${RESULT_ROOT}/nsys/node${rank}/prefill_node${rank}" - launch="export SGLANG_HOST_IP=174.1.60.$((rank + 1)); exec nsys profile --trace=cuda,nvtx,nccl --nccl-trace=api,group,gpu,coll,kernel-launch --sample=none --cpuctxsw=none --capture-range=cudaProfilerApi --capture-range-end=stop --cuda-graph-trace=node --force-overwrite=true --stats=true --output ${output} python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name kimi-k3 --tp-size 32 --ep-size 4 --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend flashinfer_mxfp4 --chunked-prefill-size 8192 --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 --mamba-radix-cache-strategy extra_buffer_lazy --disable-radix-cache --dist-timeout 3600 --mamba-full-memory-ratio 0.36 --host 0.0.0.0 --port ${PORT}" + launch="export SGLANG_HOST_IP=174.1.60.$((rank + 1)); exec nsys profile --trace=cuda,nvtx,nccl --nccl-trace=api,group,gpu,coll,kernel-launch --sample=none --cpuctxsw=none --capture-range=cudaProfilerApi --capture-range-end=stop --cuda-graph-trace=node --force-overwrite=true --stats=true --output ${output} python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name kimi-k3 --tp-size ${TP_SIZE} --pp-size ${PP_SIZE} --ep-size ${EP_SIZE} --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend flashinfer_mxfp4 --chunked-prefill-size 8192 --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 --mamba-radix-cache-strategy extra_buffer_lazy --disable-radix-cache --dist-timeout 3600 --mamba-full-memory-ratio 0.36 --host 0.0.0.0 --port ${PORT}" local -a command=( docker run -d --name "$name" @@ -191,7 +194,7 @@ write_manifest() { printf 'run_id=%s\n' "$RUN_ID" printf 'image=%s\n' "$IMAGE" printf 'model=%s\n' "$MODEL_PATH" - printf 'tp=32\nep=4\ndp=1\n' + printf 'pp=%s\ntp=%s\nep=%s\ndp=1\n' "$PP_SIZE" "$TP_SIZE" "$EP_SIZE" printf 'moe_runner_backend=flashinfer_mxfp4\nmoe_a2a_backend=none\n' printf 'input_len=16384\noutput_len=1\nconcurrency=%s\n' "$CONCURRENCY" printf 'chunked_prefill_size=8192\nprofile_steps=%s\n' "$PROFILE_STEPS" @@ -200,11 +203,19 @@ write_manifest() { run_all() { require_password + (( PP_SIZE * TP_SIZE == 32 )) || { + echo "ERROR: PP_SIZE * TP_SIZE must equal 32" >&2 + return 2 + } + (( TP_SIZE % EP_SIZE == 0 )) || { + echo "ERROR: TP_SIZE must be divisible by EP_SIZE" >&2 + return 2 + } prepare_nodes write_manifest stop_service collect_gpu before - log "starting current TP32/EP4/FlashInfer service under Nsight" + log "starting PP${PP_SIZE}/TP${TP_SIZE}/EP${EP_SIZE}/FlashInfer service under Nsight" start_node 1 start_node 2 start_node 3