From 0433fcc3ee6ee4a5a9d0427965926a97221f62a2 Mon Sep 17 00:00:00 2001 From: Zhiyi Hong <2497491955@qq.com> Date: Fri, 21 Aug 2026 17:08:26 +0800 Subject: [PATCH] [Benchmark] Add Kimi-K3 PP16 prefill results --- .../README.md | 69 +++++++++++++++---- .../run_pp_baseline_search.sh | 68 ++++++++++++------ 2 files changed, 102 insertions(+), 35 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 8a35ab7..f96e41c 100644 --- a/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/README.md +++ b/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/README.md @@ -36,6 +36,7 @@ RoCE 执行;等待通信的时间已经超过实际计算时间。 | 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 并不是越大越快。它可能带来收益的原因是: @@ -48,7 +49,7 @@ PP 加深后会新增 Pipeline Activation 的 Stage-to-Stage 传输,也会产 因此,这一阶段要回答的不是“PP 能不能启动”,而是:在 Kimi-K3、6000D 和固定 Prefill 压力下,缩小 TP 通信域的收益能否超过 Pipeline 开销,以及 -拐点出现在 PP2、PP4 还是 PP8。 +拐点出现在 PP2、PP4、PP8 还是 PP16。 ## 3. 实现方式 @@ -63,7 +64,7 @@ run_pp_baseline_search.sh 脚本负责: 1. 检查 601-604 的模型、镜像与 `/dev/infiniband`; -2. 依次启动 PP2/TP16、PP4/TP8、PP8/TP4 服务; +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 请求以及是否存在请求错误; @@ -86,7 +87,7 @@ python3 -m sglang.launch_server \ --model-path /data/hf_models/Kimi-K3 \ --tp-size "${TP_SIZE}" \ --pp-size "${PP_SIZE}" \ - --ep-size 4 \ + --ep-size "${EP_SIZE}" \ --nnodes 4 \ --node-rank "${NODE_RANK}" \ --dist-init-addr 174.1.60.1:20000 \ @@ -94,6 +95,9 @@ python3 -m sglang.launch_server \ --chunked-prefill-size 8192 ``` +脚本默认取 `EP_SIZE=min(4, TP_SIZE)`。因此 PP2/4/8 使用 EP4,PP16 因 TP2 +必须使用 EP2,满足 SGLang 的 `TP % EP == 0` 约束。 + 实验没有复制同事旧代码,也没有注入 `patch_pp_group.py`。准备阶段曾验证过 一版运行时 PP Group patch,但它会让 Python Object 通信和 Tensor/P2P 通信 复用错误的 Gloo Group,出现消息大小不匹配。回到当前 SGLang 上游实现后, @@ -123,7 +127,7 @@ NCCL_IB_GID_INDEX=3 | GPU | 601-604,32 × RTX 6000D | | MoE runner | FlashInfer MXFP4 | | A2A backend | none | -| EP | 4 | +| EP | PP1/2/4/8 为 4;PP16 为 2 | | 输入/输出 | 16,384 / 1 token | | Chunked Prefill | 8,192 token | | 并发 | C=8、C=16 | @@ -175,6 +179,7 @@ chunk16K、EP1/EP8、PP16、旧镜像和运行时 PP patch,因此旧数据不 ```text kimi3-pp-upstream-20260821-120208 +kimi3-pp16-20260821-161721 ``` 服务启动时间分别为: @@ -184,9 +189,11 @@ kimi3-pp-upstream-20260821-120208 | PP2 / TP16 / EP4 | 417 s | | PP4 / TP8 / EP4 | 405 s | | PP8 / TP4 / EP4 | 338 s | +| PP16 / TP2 / EP2 | 538 s | -PP2、PP4、PP8 共形成 18 个正式样本。每个样本均完成 40/40 请求,原始结果 -中的 errors 为空。服务和 benchmark 日志没有发现: +PP2、PP4、PP8 共形成 18 个正式样本;补充的 PP16 形成 6 个正式样本。 +每个样本均完成 40/40 请求,原始结果中的 errors 为空。服务和 benchmark +日志没有发现: - CUDA OOM / OutOfMemory; - Traceback / Exception; @@ -197,6 +204,13 @@ PP2、PP4、PP8 共形成 18 个正式样本。每个样本均完成 40/40 请 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 +3000000`,538 秒后健康。该差异不改变本轮 Prefill 路径:输出仅 1 token, +没有迭代 Decode;但 PP16 的 Decode 性能不能由本实验推断。PP16 三次 Input +TPS 极差不超过 0.7%,结果同样稳定。 + ## 7. 性能结果 下表均为 3 次正式运行的中位数: @@ -207,14 +221,20 @@ PP8 在 C=8 和 C=16 下三次 Input TPS 的极差都小于 0.7%,说明结果 | 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 到 PP8,Input TPS 沿着 PP2、PP4、PP8 单调上升,TTFT 也同步下降。 -这说明在当前 C=8/16 压力下,流水线仍有足够工作可以填充;在测试范围内, -缩小 TP 通信域的收益尚未被更深 PP 的 bubble 抵消。 +从 PP1 到 PP8,Input TPS 沿着 PP2、PP4、PP8 单调上升,TTFT 也同步下降; +PP16 则出现反转。相对 PP8,PP16 在 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 工作已接近该配置的稳定吞吐区:增加并发主要增加排队等待,而不是 @@ -231,8 +251,9 @@ C=16 的 TTFT 大致是 C=8 的两倍,而同一 PP 下 Input TPS 基本不变 | 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 不会获得完全相同的层、输出头和状态,因此 PP2/4/8 的显存不再 +PP 的 Stage 不会获得完全相同的层、输出头和状态,因此各 PP 配置的显存不再 像 PP1 那样均匀。PP4 的最重 Stage 已使用约 82.4 GiB;PP8 的最重 Stage 约 79.4 GiB,在本轮反而保留了最多的最坏 Rank 余量。 @@ -242,18 +263,18 @@ A2A 后启动或 Prefill OOM 的情况。 ## 9. 通信结果如何解释 -本轮 PP 搜索没有为 PP2/4/8 再录制 Nsight,也没有在每个 benchmark 前后采集 +本轮 PP 搜索没有为 PP2/4/8/16 再录制 Nsight,也没有在每个 benchmark 前后采集 独立 HCA Counter,因此不能把 PP1 的 57.27% NCCL 占比直接写成 PP8 的实测 通信占比。 能够被当前证据直接证明的是: 1. PP1 的同口径 Nsight 确认 TP32 Prefill 以暴露的 AllReduce 为主; -2. PP2/4/8 的 `/get_server_info` 证明实际 PP/TP/EP 与计划一致; +2. PP2/4/8/16 的 `/get_server_info` 证明实际 PP/TP/EP 与计划一致; 3. Rank 布局使 PP4 的 TP8、PP8 的 TP4/EP4 都限制在单节点内; -4. PP2/4/8 的 NCCL P2P communicator 成功建立并完成全部请求; -5. 随 TP 通信域缩小,端到端 Input TPS 按 PP1 < PP2 < PP4 < PP8 单调提升, - 与“跨节点 TP AllReduce 是主要瓶颈”的 Profile 结论一致。 +4. 所有 PP 配置的 NCCL P2P communicator 成功建立并完成全部请求; +5. Input TPS 按 PP1 < PP2 < PP4 < PP8 提升,随后 PP16 回落,表明通信域 + 缩小确有收益,但 Deep PP 开销在 PP8 之后成为更大的限制。 因此当前结果支持通信域重构这一解释,但没有伪造一个未经采样的“PP8 NCCL 占比”。如需进一步拆解 PP8 内部剩余时间,应单独执行短窗口 Nsight,而不是 @@ -282,6 +303,9 @@ 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 s(C8)和 27.31 s(C16),距离 5 s 目标仍有 明显差距。Deep PP 解决了一个主要通信瓶颈,但不是 Prefill 优化的终点。 @@ -325,6 +349,21 @@ PP1 原始结果: results/kimi3-ep4-moe-full-20260818-151349/ ``` +PP16 正式结果: + +```text +/data/hzy/sskj/experiments/pro6000/ + kimi3_pro6000_sglang_pp_baseline_search/ + results/kimi3-pp16-20260821-161721/ +``` + +PP16 启动兼容性证据(未产生 benchmark 数据): + +```text +results/kimi3-pp16-20260821-155734/ # custom AR decode graph IPC 错误 +results/kimi3-pp16-20260821-160555/ # 默认 300s watchdog 超时 +``` + PP1 Nsight 通信归因: ```text diff --git a/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/run_pp_baseline_search.sh b/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/run_pp_baseline_search.sh index 0ed19ed..1e1874a 100755 --- a/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/run_pp_baseline_search.sh +++ b/experiments/pro6000/kimi3_pro6000_sglang_pp_baseline_search/run_pp_baseline_search.sh @@ -22,8 +22,8 @@ NODE_HOSTS=(174.1.60.1 174.1.60.2 174.1.60.3 174.1.60.4) SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=10) DIST_PORT="${DIST_PORT:-20000}" -EP_SIZE=4 -PP_SIZES=(2 4 8) +MAX_EP_SIZE=4 +read -r -a PP_SIZES <<< "${PP_SIZES_OVERRIDE:-2 4 8 16}" CONCURRENCIES=(8 16) INPUT_LEN=16384 OUTPUT_LEN=1 @@ -83,6 +83,19 @@ tp_for_pp() { echo $((32 / pp)) } +ep_for_tp() { + local tp="$1" + local ep="$MAX_EP_SIZE" + if (( tp < ep )); then + ep="$tp" + fi + (( tp % ep == 0 )) || { + echo "ERROR: TP=$tp must be divisible by EP=$ep" >&2 + return 1 + } + echo "$ep" +} + preflight() { require_password [[ -d "$MODEL_PATH" ]] || { echo "ERROR: missing model $MODEL_PATH" >&2; exit 2; } @@ -96,8 +109,12 @@ preflight() { echo "image=$DOCKER_IMAGE" echo "model=$MODEL_PATH" echo "pp_sizes=${PP_SIZES[*]}" - echo "tp_sizes=16 8 4" - echo "ep_size=$EP_SIZE" + local pp tp ep + for pp in "${PP_SIZES[@]}"; do + tp="$(tp_for_pp "$pp")" + ep="$(ep_for_tp "$tp")" + echo "layout=pp${pp}_tp${tp}_ep${ep}" + done echo "shape=${INPUT_LEN}->${OUTPUT_LEN}" echo "chunked_prefill_size=$CHUNKED_PREFILL_SIZE" echo "concurrencies=${CONCURRENCIES[*]}" @@ -137,10 +154,17 @@ collect_gpu_snapshot() { } start_node() { - local rank="$1" pp="$2" tp="$3" - local host="${NODE_HOSTS[$rank]}" name bootstrap + local rank="$1" pp="$2" tp="$3" ep="$4" + local host="${NODE_HOSTS[$rank]}" name bootstrap pp16_args="" name="$(container_name "$rank")" - bootstrap="export SGLANG_HOST_IP=174.1.60.$((rank + 1)) && exec python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name ${SERVED_MODEL_NAME} --tp-size ${tp} --pp-size ${pp} --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 ${CHUNKED_PREFILL_SIZE} --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}" + # PP16/TP2 currently fails while registering custom-all-reduce IPC buffers + # for decode graph capture. This prefill-only OSL=1 experiment does not use + # iterative decode. Its 16-stage startup also exceeds the default 300-second + # watchdog while lazily creating adjacent-stage P2P communicators. + if (( pp == 16 )); then + pp16_args="--disable-decode-cuda-graph --watchdog-timeout 3000000" + fi + bootstrap="export SGLANG_HOST_IP=174.1.60.$((rank + 1)) && exec python3 -m sglang.launch_server --model-path ${MODEL_PATH} --served-model-name ${SERVED_MODEL_NAME} --tp-size ${tp} --pp-size ${pp} --ep-size ${ep} --nnodes 4 --node-rank ${rank} --dist-init-addr ${HEAD_HOST}:${DIST_PORT} --trust-remote-code --moe-runner-backend flashinfer_mxfp4 --chunked-prefill-size ${CHUNKED_PREFILL_SIZE} --mem-fraction-static 0.88 --cuda-graph-max-bs-decode 16 ${pp16_args} --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 cmd=( docker run -d --name "$name" --gpus all --network host --ipc=host --ulimit memlock=-1 @@ -182,9 +206,9 @@ wait_health() { } verify_server() { - local pp="$1" tp="$2" info="${RESULT_ROOT}/metadata/pp${pp}_server_info.json" + local pp="$1" tp="$2" ep="$3" info="${RESULT_ROOT}/metadata/pp${pp}_server_info.json" curl --fail --silent "http://${HEAD_HOST}:${PORT}/get_server_info" >"$info" - python3 - "$info" "$pp" "$tp" "$EP_SIZE" <<'PY' + python3 - "$info" "$pp" "$tp" "$ep" <<'PY' import json, sys path, pp, tp, ep = sys.argv[1], *map(int, sys.argv[2:]) @@ -200,21 +224,22 @@ PY } start_service() { - local pp="$1" tp + local pp="$1" tp ep tp="$(tp_for_pp "$pp")" + ep="$(ep_for_tp "$tp")" stop_service - log "starting PP=${pp} TP=${tp} EP=${EP_SIZE} on 601-604" - start_node 1 "$pp" "$tp" - start_node 2 "$pp" "$tp" - start_node 3 "$pp" "$tp" + log "starting PP=${pp} TP=${tp} EP=${ep} on 601-604" + start_node 1 "$pp" "$tp" "$ep" + start_node 2 "$pp" "$tp" "$ep" + start_node 3 "$pp" "$tp" "$ep" sleep 5 - start_node 0 "$pp" "$tp" + start_node 0 "$pp" "$tp" "$ep" if ! wait_health "$pp"; then collect_service_logs "pp${pp}_startup_failed" stop_service return 1 fi - verify_server "$pp" "$tp" + verify_server "$pp" "$tp" "$ep" collect_service_logs "pp${pp}_healthy" collect_gpu_snapshot "pp${pp}_healthy" } @@ -269,7 +294,8 @@ for path in sorted((root / "raw").glob("*.jsonl")): continue data = next(json.loads(line) for line in path.open(encoding="utf-8") if line.strip()) pp, concurrency, repeat = map(int, match.groups()) - row = {"pp_size": pp, "tp_size": 32 // pp, "ep_size": 4, + tp = 32 // pp + row = {"pp_size": pp, "tp_size": tp, "ep_size": min(4, tp), "concurrency": concurrency, "repeat": repeat, "completed": data.get("completed")} row.update({name: data.get(name) for name in metrics}) @@ -287,7 +313,8 @@ for row in rows: groups.setdefault((row["pp_size"], row["concurrency"]), []).append(row) summary = [] for (pp, concurrency), group in sorted(groups.items()): - item = {"pp_size": pp, "tp_size": 32 // pp, "ep_size": 4, + tp = 32 // pp + item = {"pp_size": pp, "tp_size": tp, "ep_size": min(4, tp), "concurrency": concurrency, "repeats": len(group)} for name in metrics: values = [float(row[name]) for row in group if row.get(name) is not None] @@ -328,10 +355,11 @@ run_matrix() { } dry_run() { - local pp tp + local pp tp ep for pp in "${PP_SIZES[@]}"; do tp="$(tp_for_pp "$pp")" - echo "PP=${pp} TP=${tp} EP=${EP_SIZE} ${INPUT_LEN}->${OUTPUT_LEN} C=${CONCURRENCIES[*]} chunk=${CHUNKED_PREFILL_SIZE}" + ep="$(ep_for_tp "$tp")" + echo "PP=${pp} TP=${tp} EP=${ep} ${INPUT_LEN}->${OUTPUT_LEN} C=${CONCURRENCIES[*]} chunk=${CHUNKED_PREFILL_SIZE}" done bash -n "$0" }