[Benchmark] Add Kimi-K3 PP16 prefill results
This commit is contained in:
parent
d790df39b2
commit
0433fcc3ee
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user