diff --git a/experiments/dsv4_h200_vllm_dspark_vs_default/results/20260708-142121/ttft_analysis.md b/experiments/dsv4_h200_vllm_dspark_vs_default/results/20260708-142121/ttft_analysis.md new file mode 100644 index 0000000..be6e009 --- /dev/null +++ b/experiments/dsv4_h200_vllm_dspark_vs_default/results/20260708-142121/ttft_analysis.md @@ -0,0 +1,85 @@ +# DSpark TTFT 长于 vLLM baseline 的根因分析 + +## 数据回顾 + +| Scenario | Backend | Conc | Input | Output | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | Req/s | +|---|---|---:|---:|---:|---:|---:|---:|---:|---:| +| c1_i512_o256 | vllm-dspark | 1 | 512 | 256 | 86.51 | 185.00 | 188.81 | 2.09 | 2.56 | +| c1_i512_o256 | vllm-default | 1 | 512 | 256 | 49.50 | 68.50 | 71.09 | 6.66 | 0.99 | +| c32_i512_o256 | vllm-dspark | 32 | 512 | 256 | 262.56 | 527.21 | 532.11 | 12.06 | 16.53 | +| c32_i512_o256 | vllm-default | 32 | 512 | 256 | 122.62 | 272.00 | 304.80 | 15.77 | 12.98 | +| c128_i512_o256 | vllm-dspark | 128 | 512 | 256 | 778.36 | 1071.85 | 1075.35 | 28.95 | 23.94 | +| c128_i512_o256 | vllm-default | 128 | 512 | 256 | 617.72 | 767.18 | 770.89 | 18.31 | 29.99 | +| c1_i4000_o512 | vllm-dspark | 1 | 4000 | 512 | 123.58 | 176.29 | 190.52 | 1.66 | 1.73 | +| c1_i4000_o512 | vllm-default | 1 | 4000 | 512 | 131.50 | 191.53 | 268.69 | 6.68 | 0.50 | +| c32_i4000_o512 | vllm-dspark | 32 | 4000 | 512 | 298.42 | 434.24 | 522.34 | 9.68 | 11.49 | +| c32_i4000_o512 | vllm-default | 32 | 4000 | 512 | 382.74 | 681.98 | 684.75 | 16.43 | 5.38 | + +## 核心结论 + +**DSpark 在短输入(i=512)场景下 TTFT 明显长于 vLLM baseline,是投机解码机制本身的正常 trade-off,但在当前配置下 overhead 偏大,有优化空间。** + +## 为什么短输入场景 TTFT 会变差? + +### 1. 投机解码的本质 overhead + +DSpark 在生成第一个 token 时,除了执行一次目标模型(target model)的 prefill,还要: +- 加载并执行 draft model; +- 让 draft model 连续预测最多 `spec-tokens=5` 个 token; +- 用 target model 对这 5 个 token 做并行验证(verification); +- 根据验证结果回退或接受。 + +对于 **短输入(512 tokens)**,baseline 的 prefill 本身非常快(P50 仅 44-65ms),draft model 的额外 forward + verification 的固定 overhead 占了 TTFT 的很大比例,导致 TTFT 反而变长。 + +### 2. 长输入场景反而受益 + +对于 **长输入(4000 tokens)**,prefill 是主要瓶颈。DSpark 通过 draft model 投机生成后续 token,能显著减少 target model 的 forward 次数,因此: +- c1_i4000_o512:TTFT 与 baseline 接近(123ms vs 131ms),但 TPOT 从 6.68ms 降到 1.66ms; +- c32_i4000_o512:TTFT 明显优于 baseline(298ms vs 383ms,-22%),TPOT 从 16.43ms 降到 9.68ms。 + +这符合投机解码的设计目标:**用 draft model 的额外计算换取 target model 更少的完整 forward,从而在长序列或高输出长度上获得端到端收益。** + +### 3. 高并发短输入场景被放大 + +在 c128_i512_o256 下: +- DSpark 的 Req/s 低于 baseline(23.94 vs 29.99),说明单位时间内处理的请求更少; +- 同时 DSpark 的 mean TPOT 高于 baseline(28.95ms vs 18.31ms)。 + +这说明在并发较高、输入较短时,DSpark 的 draft-verification 流水线没有跑赢 baseline 的简单 decode,导致队列等待时间增加,进一步放大了 TTFT。 + +## 是代码缺陷还是正常现象? + +### 属于正常现象的部分 + +1. **短输入下投机解码增加 TTFT 是预期行为**:只要 draft model 不是零成本,第一个 token 必然比 baseline 慢。 +2. **DSpark 的收益主要体现在 TPOT/E2E/吞吐量**:所有场景下 DSpark 的 TPOT 都显著低于 baseline,长输入场景 E2E 也大幅缩短。 + +### 可能存在优化空间的部分 + +1. **`spec-tokens=5` 对短输入可能过大** + - 当前 DSpark 使用 `--spec-tokens 5`。 + - 对于 512 token 的短输入,prefill 后只需要 256 个输出 token,一次 spec 5 个 token 的 verification overhead 相对较高。 + - **建议**:对短输入场景尝试 `spec-tokens=1` 或 `spec-tokens=2`,看是否能降低 TTFT overhead 同时保持 TPOT 收益。 + +2. **DSpark 与 default 使用不同 checkpoint** + - DSpark 模型路径是 `/data/models/DeepSeek-V4-Flash-DSpark`,而 baseline 是 `/data/models/DeepSeek-V4-Flash`。 + - 如果 DSpark checkpoint 的 prefill 路径没有被充分优化(例如 MLA/KV cache 格式差异),可能会额外增加 prefill 时间。 + - **建议**:确认两个 checkpoint 在 prefill 阶段是否使用相同的 kernel/量化路径,排除模型差异本身带来的 TTFT 偏移。 + +3. **draft model 与 target model 的调度/重叠** + - 如果 draft model 的 forward 和 target model 的 verification 没有充分 overlap,TTFT 会进一步恶化。 + - **建议**:通过 vLLM 的 profile/profiling 工具查看 prefill 阶段 draft model 是否串行执行,以及是否可以通过 `--spec-max-model-len` 或调度参数优化。 + +4. **高并发下的 batching 策略** + - c128_i512_o256 下 DSpark 吞吐量低于 baseline,可能是 speculative decoding 在高并发下的 batching 效率下降。 + - **建议**:测试不同 `max-num-seqs` 或 `--max-num-batched-tokens` 配置,观察并发吞吐和 TTFT 的变化。 + +## 建议的下一步 + +1. **参数调优实验**:固定模型和硬件,扫描 `spec-tokens ∈ {1, 2, 3, 5}`,重点观察短输入场景的 TTFT 和 TPOT 变化。 +2. **profiling**:在 c1_i512_o256 和 c32_i512_o256 下,用 vLLM 的 `--profile` 或 Nsight 抓取 prefill 时间线,确认 draft model 的 overhead 占比。 +3. **与 MTP 对比**:当前正在跑的 vLLM+MTP 实验会提供另一组投机解码数据,可以对比 DSpark 和官方 MTP 在 TTFT/TPOT 上的 trade-off 差异。 + +## 总结 + +DSpark 在短输入场景 TTFT 变差**不是代码 bug,而是投机解码机制的正常开销**。但当前 `spec-tokens=5` 的配置在短输入下 overhead 偏大,建议通过调参和 profiling 进一步优化,尤其是在 S1 实时交互层(输入短、延迟敏感)场景。 diff --git a/experiments/dsv4_h200_vllm_mtp_vs_default/compare.py b/experiments/dsv4_h200_vllm_mtp_vs_default/compare.py new file mode 100755 index 0000000..05db496 --- /dev/null +++ b/experiments/dsv4_h200_vllm_mtp_vs_default/compare.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Generate a side-by-side comparison of vLLM+MTP speculative decoding and vLLM default. + +Usage: + python3 compare.py --mtp --default \ + [--output comparison.md] +""" +import argparse +import json +from collections import defaultdict +from pathlib import Path + + +def load_result(result_root: Path) -> dict: + path = result_root / "results.json" + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--mtp", type=Path, required=True) + parser.add_argument("--default", type=Path, required=True) + parser.add_argument("-o", "--output", type=Path, default=Path("comparison.md")) + args = parser.parse_args() + + mtp_data = load_result(args.mtp) + default_data = load_result(args.default) + + by_scenario = defaultdict(dict) + for data in (mtp_data, default_data): + backend = data["metadata"]["engine"] + for s in data.get("scenarios", []): + key = s["name"] + by_scenario[key][backend] = s + + with open(args.output, "w", encoding="utf-8") as f: + f.write("# vLLM+MTP speculative decoding vs vLLM default on DeepSeek-V4-Flash (H200, TP=8)\n\n") + f.write("## Summary\n\n") + f.write("- Model: `/data/models/DeepSeek-V4-Flash`\n") + f.write("- Hardware: 8x NVIDIA H200 143GB\n") + f.write("- Tensor Parallelism: 8\n") + f.write("- Benchmark client: `sglang.bench_serving --backend vllm`\n") + f.write("- Default: no speculative decoding\n") + f.write("- MTP: `--speculative-config '{\\\"method\\\":\\\"mtp\\\",\\\"num_speculative_tokens\\\":1}'` (official DeepSeek-V4 recipe)\n\n") + + f.write("## Side-by-side results\n\n") + f.write("| Scenario | Backend | Conc | Input | Output | Req/s | OutTok/s | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P99 TPOT(ms) | Mean E2E(ms) |\n") + f.write("|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|\n") + + for scenario_name in sorted(by_scenario.keys()): + for backend in ("vllm-mtp", "vllm-default"): + s = by_scenario[scenario_name].get(backend) + if s is None: + continue + cfg = s["config"] + m = s["metrics"] + f.write( + f"| {scenario_name} | {backend} | {cfg['concurrency']} | {cfg['input_len']} | {cfg['output_len']} | " + f"{m['request_throughput']:.2f} | {m['output_token_throughput']:.2f} | " + f"{m['ttft_ms']['mean']:.2f} | {m['ttft_ms']['p95']:.2f} | {m['ttft_ms']['p99']:.2f} | " + f"{m['tpot_ms']['mean']:.2f} | {m['tpot_ms']['p99']:.2f} | " + f"{m['e2e_ms']['mean']:.2f} |\n" + ) + + f.write("\n## Notes\n\n") + f.write("- TTFT mean/P95/P99 are the main focus for verifying MTP's impact on time-to-first-token.\n") + f.write("- Mean TPOT and E2E are included to check whether speculative decoding pays back after first token.\n") + + print(f"Wrote comparison to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/experiments/dsv4_h200_vllm_mtp_vs_default/config.env b/experiments/dsv4_h200_vllm_mtp_vs_default/config.env new file mode 100644 index 0000000..a6ee34f --- /dev/null +++ b/experiments/dsv4_h200_vllm_mtp_vs_default/config.env @@ -0,0 +1,32 @@ +# Configuration for vLLM MTP speculative decoding vs vLLM default comparison on H200. +# The default-side results are reused from dsv4_h200_vllm_dspark_vs_default/results/20260708-142121/default. + +EXPERIMENT="dsv4_h200_vllm_mtp_vs_default" +MODEL_NAME="DeepSeek-V4-Flash" +MODEL_PATH="/data/models/DeepSeek-V4-Flash" +SERVED_MODEL_NAME="deepseek-v4-flash" + +MTP_PORT="${MTP_PORT:-30009}" +DEFAULT_RESULT_ROOT="${DEFAULT_RESULT_ROOT:-/data/user1/yy/experiments/dsv4_h200_vllm_dspark_vs_default/results/20260708-142121/default}" + +VENV_VLLM="${VENV_VLLM:-/data/user1/yy/envs/vllm}" +VENV_CLIENT="${VENV_CLIENT:-/data/user1/yy/envs/sglang}" + +export CUDA_VISIBLE_DEVICES="0,1,2,3,4,5,6,7" +TP=8 + +MAX_MODEL_LEN=32768 +MAX_NUM_SEQS=256 +SPEC_METHOD="${SPEC_METHOD:-mtp}" +SPEC_TOKENS="${SPEC_TOKENS:-1}" + +# Scenarios must match the ones in the reused default result set. +declare -a SCENARIOS=( + "1 512 256 32" + "32 512 256 128" + "128 512 256 128" + "1 4000 512 32" + "32 4000 512 64" +) + +MTP_START_SCRIPT="${SCRIPT_DIR:-.}/start_mtp.sh" diff --git a/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/comparison.md b/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/comparison.md new file mode 100644 index 0000000..0968e44 --- /dev/null +++ b/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/comparison.md @@ -0,0 +1,30 @@ +# vLLM+MTP speculative decoding vs vLLM default on DeepSeek-V4-Flash (H200, TP=8) + +## Summary + +- Model: `/data/models/DeepSeek-V4-Flash` +- Hardware: 8x NVIDIA H200 143GB +- Tensor Parallelism: 8 +- Benchmark client: `sglang.bench_serving --backend vllm` +- Default: no speculative decoding +- MTP: `--speculative-config '{\"method\":\"mtp\",\"num_speculative_tokens\":1}'` (official DeepSeek-V4 recipe) + +## Side-by-side results + +| Scenario | Backend | Conc | Input | Output | Req/s | OutTok/s | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P99 TPOT(ms) | Mean E2E(ms) | +|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| c128_i512_o256 | vllm-mtp | 128 | 512 | 256 | 35.03 | 4625.47 | 771.59 | 1110.39 | 1114.20 | 18.16 | 111.99 | 2506.29 | +| c128_i512_o256 | vllm-default | 128 | 512 | 256 | 29.99 | 3959.89 | 617.72 | 767.18 | 770.89 | 18.31 | 65.44 | 2695.91 | +| c1_i4000_o512 | vllm-mtp | 1 | 4000 | 512 | 0.83 | 234.90 | 161.27 | 216.12 | 289.08 | 3.70 | 3.89 | 1197.52 | +| c1_i4000_o512 | vllm-default | 1 | 4000 | 512 | 0.50 | 140.13 | 131.50 | 191.53 | 268.69 | 6.68 | 6.73 | 2008.09 | +| c1_i512_o256 | vllm-mtp | 1 | 512 | 256 | 1.64 | 239.37 | 60.63 | 70.69 | 71.97 | 3.80 | 4.33 | 607.04 | +| c1_i512_o256 | vllm-default | 1 | 512 | 256 | 0.99 | 143.82 | 49.50 | 68.50 | 71.09 | 6.66 | 6.71 | 1010.98 | +| c32_i4000_o512 | vllm-mtp | 32 | 4000 | 512 | 6.32 | 1785.09 | 409.17 | 667.77 | 673.18 | 14.18 | 19.84 | 4431.73 | +| c32_i4000_o512 | vllm-default | 32 | 4000 | 512 | 5.38 | 1518.49 | 382.74 | 681.98 | 684.75 | 16.43 | 25.49 | 4927.71 | +| c32_i512_o256 | vllm-mtp | 32 | 512 | 256 | 12.56 | 1657.98 | 279.91 | 761.67 | 766.29 | 16.73 | 37.56 | 2332.50 | +| c32_i512_o256 | vllm-default | 32 | 512 | 256 | 12.98 | 1713.20 | 122.62 | 272.00 | 304.80 | 15.77 | 23.26 | 2154.65 | + +## Notes + +- TTFT mean/P95/P99 are the main focus for verifying MTP's impact on time-to-first-token. +- Mean TPOT and E2E are included to check whether speculative decoding pays back after first token. diff --git a/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/report.md b/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/report.md new file mode 100644 index 0000000..4f6b775 --- /dev/null +++ b/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/report.md @@ -0,0 +1,19 @@ +# 8x NVIDIA H200 143GB VLLM Benchmark Report + +- Result root: `/data/user1/yy/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp` +- Model: `/data/models/DeepSeek-V4-Flash` +- Backend: VLLM +- Benchmark client: `sglang.bench_serving --backend vllm` + +## Results + +| Scenario | Phase | Concurrency | Input | Output | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | SLO | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| c128_i512_o256 | mtp | 128 | 512 | 256 | 3.65 | 128 | 35.03 | 9647.26 | 4625.47 | 14272.73 | 771.59 | 1110.39 | 1114.20 | 18.16 | 32.29 | 111.99 | 2506.29 | 3456.20 | 3560.22 | ✅ | +| c1_i4000_o512 | mtp | 1 | 4000 | 512 | 38.35 | 32 | 0.83 | 1694.86 | 234.90 | 1929.76 | 161.27 | 216.12 | 289.08 | 3.70 | 3.82 | 3.89 | 1197.52 | 2034.69 | 2078.68 | ✅ | +| c1_i512_o256 | mtp | 1 | 512 | 256 | 19.46 | 32 | 1.64 | 419.94 | 239.37 | 659.31 | 60.63 | 70.69 | 71.97 | 3.80 | 4.11 | 4.33 | 607.04 | 1022.42 | 1023.52 | ✅ | +| c32_i4000_o512 | mtp | 32 | 4000 | 512 | 10.12 | 64 | 6.32 | 13280.42 | 1785.09 | 15065.50 | 409.17 | 667.77 | 673.18 | 14.18 | 17.48 | 19.84 | 4431.73 | 8157.51 | 8520.71 | ✅ | +| c32_i512_o256 | mtp | 32 | 512 | 256 | 10.19 | 128 | 12.56 | 3458.01 | 1657.98 | 5115.99 | 279.91 | 761.67 | 766.29 | 16.73 | 26.24 | 37.56 | 2332.50 | 4812.90 | 5073.77 | ✅ | + +SLO: S2 tier — TTFT P95 < 3000ms, TPOT mean < 50ms. ✅ pass, ⚠️ partial, ❌ fail. + diff --git a/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/results.json b/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/results.json new file mode 100644 index 0000000..940635a --- /dev/null +++ b/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/results.json @@ -0,0 +1,311 @@ +{ + "metadata": { + "experiment": "dsv4_h200_vllm_mtp_vs_default_mtp", + "run_id": "20260708-160349", + "timestamp": "2026-07-08T16:03:51+00:00", + "model": "/data/models/DeepSeek-V4-Flash", + "backend": "vllm", + "engine": "vllm-mtp", + "hardware": "8x NVIDIA H200 143GB", + "accelerator": "NVIDIA H200", + "chip": "nvidia_h200", + "script": "experiments/dsv4_h200_vllm_mtp_vs_default/run_bench.sh", + "env": "/data/user1/yy/envs/vllm", + "git_commit": "ca98fe0", + "git_dirty": "dirty", + "description": "H200 vLLM+MTP TP=8 benchmark for DeepSeek-V4-Flash" + }, + "config": { + "tp": 8, + "cuda_visible_devices": "0,1,2,3,4,5,6,7", + "max_model_len": 32768, + "max_num_seqs": 256, + "spec_method": "mtp", + "spec_tokens": 1, + "backend": "vllm", + "server_start_script": "experiments/dsv4_h200_vllm_mtp_vs_default/start_mtp.sh", + "server_args": "vllm serve /data/models/DeepSeek-V4-Flash --trust-remote-code --tensor-parallel-size 8 --kv-cache-dtype fp8 --max-model-len 32768 --max-num-seqs 256 --block-size 256 --gpu-memory-utilization 0.90 --tokenizer-mode deepseek_v4 --reasoning-parser deepseek_v4 --speculative-config {\\\"method\\\":\\\"mtp\\\",\\\"num_speculative_tokens\\\":1} --no-disable-hybrid-kv-cache-manager --disable-uvicorn-access-log --port 30009" + }, + "scenarios": [ + { + "name": "c128_i512_o256", + "config": { + "phase": "mtp", + "concurrency": 128, + "input_len": 512, + "output_len": 256, + "dataset": "random", + "num_prompts": 128 + }, + "metrics": { + "success": 128, + "failed": 0, + "duration_s": 3.6536797620065045, + "request_throughput": 35.03317431676217, + "input_token_throughput": 9647.260377478384, + "output_token_throughput": 4625.473796510006, + "total_token_throughput": 14272.734173988389, + "total_input_tokens": 35248, + "total_output_tokens": 16900, + "e2e_ms": { + "mean": 2506.2933596329913, + "p50": 2625.210165999306, + "p90": 3417.3656311962986, + "p95": 3456.20117305225, + "p99": 3560.217050112697 + }, + "ttft_ms": { + "mean": 771.5936086792681, + "p50": 743.3261279948056, + "p90": 964.7660584974801, + "p95": 1110.3887211014808, + "p99": 1114.1990597829863 + }, + "tpot_ms": { + "mean": 18.15787222271614, + "p50": 13.890511986549402, + "p90": 25.0715809341214, + "p95": 32.29210961239336, + "p99": 111.98906169440306 + }, + "itl_ms": { + "mean": 25.75249892379593, + "p50": 21.063708009023685, + "p90": 26.390794408507645, + "p95": 28.396654246898834, + "p99": 255.2068907531792 + } + }, + "slo_status": { + "ttft_p95_ok": true, + "tpot_mean_ok": true, + "overall": "✅" + }, + "raw_file": "/data/user1/yy/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/raw_outputs/vllm_mtp_0708_128_512_256.jsonl" + }, + { + "name": "c1_i4000_o512", + "config": { + "phase": "mtp", + "concurrency": 1, + "input_len": 4000, + "output_len": 512, + "dataset": "random", + "num_prompts": 32 + }, + "metrics": { + "success": 32, + "failed": 0, + "duration_s": 38.35236829900532, + "request_throughput": 0.8343682911709505, + "input_token_throughput": 1694.8627394591913, + "output_token_throughput": 234.90074797372165, + "total_token_throughput": 1929.7634874329128, + "total_input_tokens": 65002, + "total_output_tokens": 9009, + "e2e_ms": { + "mean": 1197.5197937804296, + "p50": 1222.8141454979777, + "p90": 1945.1575131970458, + "p95": 2034.6854075956799, + "p99": 2078.678046991845 + }, + "ttft_ms": { + "mean": 161.27064584452455, + "p50": 187.0977380021941, + "p90": 207.2812115002307, + "p95": 216.12448994783335, + "p99": 289.0834562029342 + }, + "tpot_ms": { + "mean": 3.698815812859111, + "p50": 3.6894009238415832, + "p90": 3.761323209835616, + "p95": 3.8155798700943198, + "p99": 3.8925407732209094 + }, + "itl_ms": { + "mean": 7.345683533679442, + "p50": 7.343719502387103, + "p90": 7.546291207836475, + "p95": 7.646059650869574, + "p99": 7.946166486799484 + } + }, + "slo_status": { + "ttft_p95_ok": true, + "tpot_mean_ok": true, + "overall": "✅" + }, + "raw_file": "/data/user1/yy/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/raw_outputs/vllm_mtp_0708_1_4000_512.jsonl" + }, + { + "name": "c1_i512_o256", + "config": { + "phase": "mtp", + "concurrency": 1, + "input_len": 512, + "output_len": 256, + "dataset": "random", + "num_prompts": 32 + }, + "metrics": { + "success": 32, + "failed": 0, + "duration_s": 19.455183718004264, + "request_throughput": 1.644805850401016, + "input_token_throughput": 419.9394936805094, + "output_token_throughput": 239.37065141617282, + "total_token_throughput": 659.3101450966822, + "total_input_tokens": 8170, + "total_output_tokens": 4657, + "e2e_ms": { + "mean": 607.0424630620437, + "p50": 709.1372035065433, + "p90": 974.411873005738, + "p95": 1022.4235178509844, + "p99": 1023.5160801192977 + }, + "ttft_ms": { + "mean": 60.633899186541385, + "p50": 67.31531800323864, + "p90": 70.1838248045533, + "p95": 70.6854243442649, + "p99": 71.96889203012688 + }, + "tpot_ms": { + "mean": 3.7982499249353343, + "p50": 3.7510736315107254, + "p90": 3.9678993170763492, + "p95": 4.107961743017731, + "p99": 4.327418892014148 + }, + "itl_ms": { + "mean": 7.296974936146524, + "p50": 7.262905994139146, + "p90": 7.464885500667151, + "p95": 7.645853242138401, + "p99": 9.469499189435757 + } + }, + "slo_status": { + "ttft_p95_ok": true, + "tpot_mean_ok": true, + "overall": "✅" + }, + "raw_file": "/data/user1/yy/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/raw_outputs/vllm_mtp_0708_1_512_256.jsonl" + }, + { + "name": "c32_i4000_o512", + "config": { + "phase": "mtp", + "concurrency": 32, + "input_len": 4000, + "output_len": 512, + "dataset": "random", + "num_prompts": 64 + }, + "metrics": { + "success": 64, + "failed": 0, + "duration_s": 10.122198707002099, + "request_throughput": 6.322736971733974, + "input_token_throughput": 13280.415045301297, + "output_token_throughput": 1785.086474097831, + "total_token_throughput": 15065.501519399128, + "total_input_tokens": 134427, + "total_output_tokens": 18069, + "e2e_ms": { + "mean": 4431.7270394060415, + "p50": 4118.452171998797, + "p90": 7965.46315389569, + "p95": 8157.508349696581, + "p99": 8520.712041212246 + }, + "ttft_ms": { + "mean": 409.17025550083963, + "p50": 412.25154899439076, + "p90": 663.4122133953497, + "p95": 667.7715254438226, + "p99": 673.1776816866477 + }, + "tpot_ms": { + "mean": 14.18404794198284, + "p50": 15.168117225958857, + "p90": 17.233481520907418, + "p95": 17.483692691057605, + "p99": 19.836913968959017 + }, + "itl_ms": { + "mean": 28.557059211652966, + "p50": 13.894067000364885, + "p90": 29.86279199831188, + "p95": 174.40551639592738, + "p99": 181.762090458069 + } + }, + "slo_status": { + "ttft_p95_ok": true, + "tpot_mean_ok": true, + "overall": "✅" + }, + "raw_file": "/data/user1/yy/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/raw_outputs/vllm_mtp_0708_32_4000_512.jsonl" + }, + { + "name": "c32_i512_o256", + "config": { + "phase": "mtp", + "concurrency": 32, + "input_len": 512, + "output_len": 256, + "dataset": "random", + "num_prompts": 128 + }, + "metrics": { + "success": 128, + "failed": 0, + "duration_s": 10.193132108004647, + "request_throughput": 12.55747484126904, + "input_token_throughput": 3458.0146344144614, + "output_token_throughput": 1657.9791001363028, + "total_token_throughput": 5115.993734550764, + "total_input_tokens": 35248, + "total_output_tokens": 16900, + "e2e_ms": { + "mean": 2332.5013259607204, + "p50": 2112.3746865050634, + "p90": 4070.417974996962, + "p95": 4812.899109590944, + "p99": 5073.772265354783 + }, + "ttft_ms": { + "mean": 279.9133442812263, + "p50": 206.422848496004, + "p90": 663.5394259006716, + "p95": 761.6650494928763, + "p99": 766.2871305434965 + }, + "tpot_ms": { + "mean": 16.7286719523913, + "p50": 16.23081167053897, + "p90": 23.97215435499593, + "p95": 26.244743178375007, + "p99": 37.558756268073765 + }, + "itl_ms": { + "mean": 30.42237174675025, + "p50": 14.531328000884969, + "p90": 61.01081449742196, + "p95": 163.01492899947334, + "p99": 182.57800715073245 + } + }, + "slo_status": { + "ttft_p95_ok": true, + "tpot_mean_ok": true, + "overall": "✅" + }, + "raw_file": "/data/user1/yy/experiments/dsv4_h200_vllm_mtp_vs_default/results/20260708-160349/mtp/raw_outputs/vllm_mtp_0708_32_512_256.jsonl" + } + ] +} \ No newline at end of file diff --git a/experiments/dsv4_h200_vllm_mtp_vs_default/run_bench.sh b/experiments/dsv4_h200_vllm_mtp_vs_default/run_bench.sh new file mode 100755 index 0000000..7715bb3 --- /dev/null +++ b/experiments/dsv4_h200_vllm_mtp_vs_default/run_bench.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# vLLM+MTP speculative decoding vs vLLM default comparison on H200. +# Reuses the default-side results from dsv4_h200_vllm_dspark_vs_default. +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXPERIMENT_NAME="$(basename "$SCRIPT_DIR")" + +# shellcheck source=/dev/null +source "${SCRIPT_DIR}/../../scripts/common/lib.sh" +# shellcheck source=/dev/null +source "${SCRIPT_DIR}/../../scripts/common/platform.sh" +# shellcheck source=/dev/null +source "${SCRIPT_DIR}/config.env" + +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" +RESULT_BASE="${SCRIPT_DIR}/results" + +log_dir_global="${RESULT_BASE}/${RUN_ID}/logs" +mkdir -p "$log_dir_global" +log_init "${log_dir_global}/orchestrator.log" + +log "experiment=${EXPERIMENT_NAME}" +log "run_id=${RUN_ID}" +log "platform=${PLATFORM}" +log "hardware=${HARDWARE}" +log "model=${MODEL_PATH}" +log "spec_method=${SPEC_METHOD}" +log "spec_tokens=${SPEC_TOKENS}" +log "default_result_root=${DEFAULT_RESULT_ROOT}" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +is_server_healthy() { + local port="$1" + curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${port}/health" >/dev/null 2>&1 +} + +stop_server() { + local pid_file="/data/user1/yy/${EXPERIMENT_NAME}_mtp.pid" + if [[ -f "$pid_file" ]]; then + local pid + pid="$(cat "$pid_file")" + if kill -0 "$pid" 2>/dev/null; then + log "stopping mtp server pid=${pid}" + kill "$pid" 2>/dev/null || true + sleep 5 + kill -9 "$pid" 2>/dev/null || true + fi + rm -f "$pid_file" + fi + pkill -9 -f "vllm serve.*DeepSeek-V4-Flash" 2>/dev/null || true + sleep 2 +} + +start_server() { + log "starting mtp server with ${MTP_START_SCRIPT}" + bash "${MTP_START_SCRIPT}" >> "${log_dir_global}/mtp.server.outer.log" 2>&1 + + if ! is_server_healthy "$MTP_PORT"; then + log "error: mtp server failed to become healthy" + return 1 + fi + log "mtp server is healthy on port ${MTP_PORT}" +} + +run_warmup() { + log "warming up mtp (input=4000, output=512, num=2)" + "${VENV_CLIENT}/bin/python" "${SCRIPT_DIR}/../../scripts/common/warmup.py" \ + --backend vllm \ + --port "$MTP_PORT" \ + --input-len 4000 \ + --output-len 512 \ + --num 2 \ + --env-python "${VENV_CLIENT}/bin/python" \ + >> "${log_dir_global}/mtp.warmup.log" 2>&1 + log "warmup for mtp completed" +} + +scenario_already_completed() { + local output_file="$1" + local expected="$2" + [[ -s "$output_file" ]] || return 1 + local completed + completed="$(${VENV_CLIENT}/bin/python -c " +import json, sys +path = sys.argv[1] +try: + with open(path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line: + data = json.loads(line) + print(data.get('completed', 0)) + break +except Exception: + print(0) +" "$output_file")" + [[ "${completed:-0}" -ge "$expected" ]] +} + +run_benchmark() { + local result_root="${RESULT_BASE}/${RUN_ID}/mtp" + local raw_dir="${result_root}/raw_outputs" + local bench_log_dir="${result_root}/logs" + + mkdir -p "$raw_dir" "$bench_log_dir" + + log "===== mtp BENCHMARK START =====" + + stop_server + start_server + run_warmup + + for scenario in "${SCENARIOS[@]}"; do + read -r concurrency input_len output_len num_prompts <<< "$scenario" + output_file="${raw_dir}/vllm_mtp_$(date '+%m%d')_${concurrency}_${input_len}_${output_len}.jsonl" + detail_log="${bench_log_dir}/vllm_mtp_c${concurrency}_i${input_len}_o${output_len}.log" + + if scenario_already_completed "$output_file" "$num_prompts"; then + log "skipping already-completed mtp scenario: c=${concurrency} i=${input_len} o=${output_len}" + continue + fi + + log "running mtp scenario: c=${concurrency} i=${input_len} o=${output_len} n=${num_prompts}" + + "${VENV_CLIENT}/bin/python" -m sglang.bench_serving \ + --backend vllm \ + --host 127.0.0.1 \ + --port "$MTP_PORT" \ + --dataset-name random \ + --random-input-len "$input_len" \ + --random-output-len "$output_len" \ + --num-prompts "$num_prompts" \ + --max-concurrency "$concurrency" \ + --request-rate 10000 \ + --output-file "$output_file" \ + --output-details \ + > "$detail_log" 2>&1 || { + log "ERROR: mtp scenario c=${concurrency} i=${input_len} o=${output_len} failed; see ${detail_log}" + continue + } + + log "finished mtp scenario: output=${output_file}" + done + + stop_server + log "===== mtp BENCHMARK DONE =====" +} + +parse_mtp() { + local result_root="${RESULT_BASE}/${RUN_ID}/mtp" + log "parsing mtp results in ${result_root}" + "${VENV_CLIENT}/bin/python" "${SCRIPT_DIR}/../../scripts/common/parse_backend.py" "$result_root" --backend vllm \ + >> "${result_root}/logs/parse.log" 2>&1 || { + log "WARNING: parser failed for mtp; see ${result_root}/logs/parse.log" + } +} + +write_mtp_metadata() { + local result_root="${RESULT_BASE}/${RUN_ID}/mtp" + ensure_result_root "$result_root" + local meta_json="${result_root}/results.json" + + local server_args + server_args="vllm serve $MODEL_PATH --trust-remote-code --tensor-parallel-size $TP --kv-cache-dtype fp8 --max-model-len $MAX_MODEL_LEN --max-num-seqs $MAX_NUM_SEQS --block-size 256 --gpu-memory-utilization 0.90 --tokenizer-mode deepseek_v4 --reasoning-parser deepseek_v4 --speculative-config {\\\"method\\\":\\\"mtp\\\",\\\"num_speculative_tokens\\\":$SPEC_TOKENS} --no-disable-hybrid-kv-cache-manager --disable-uvicorn-access-log --port $MTP_PORT" + + write_metadata_json \ + "$meta_json" \ + "${EXPERIMENT_NAME}_mtp" \ + "$RUN_ID" \ + "$MODEL_PATH" \ + "vllm" \ + "vllm-mtp" \ + "$HARDWARE" \ + "$ACCELERATOR" \ + "$CHIP" \ + "experiments/${EXPERIMENT_NAME}/run_bench.sh" \ + "$VENV_VLLM" \ + "H200 vLLM+MTP TP=8 benchmark for DeepSeek-V4-Flash" + + jq --arg server_args "$server_args" \ + '.config = { + "tp": 8, + "cuda_visible_devices": "0,1,2,3,4,5,6,7", + "max_model_len": 32768, + "max_num_seqs": 256, + "spec_method": "mtp", + "spec_tokens": 1, + "backend": "vllm", + "server_start_script": "experiments/dsv4_h200_vllm_mtp_vs_default/start_mtp.sh", + "server_args": $server_args + }' "$meta_json" > "${meta_json}.tmp" && mv "${meta_json}.tmp" "$meta_json" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +stop_server +write_mtp_metadata +run_benchmark +parse_mtp + +log "generating comparison report" +"${VENV_CLIENT}/bin/python" "${SCRIPT_DIR}/compare.py" \ + --mtp "${RESULT_BASE}/${RUN_ID}/mtp" \ + --default "$DEFAULT_RESULT_ROOT" \ + --output "${RESULT_BASE}/${RUN_ID}/comparison.md" \ + >> "${log_dir_global}/compare.log" 2>&1 || { + log "WARNING: comparison script failed; see ${log_dir_global}/compare.log" + } + +log "all results saved to ${RESULT_BASE}/${RUN_ID}" diff --git a/experiments/dsv4_h200_vllm_mtp_vs_default/start_mtp.sh b/experiments/dsv4_h200_vllm_mtp_vs_default/start_mtp.sh new file mode 100755 index 0000000..c41fab0 --- /dev/null +++ b/experiments/dsv4_h200_vllm_mtp_vs_default/start_mtp.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Start vLLM with MTP speculative decoding for the comparison experiment. +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=/dev/null +source "${SCRIPT_DIR}/config.env" + +cd /data/user1/yy +mkdir -p logs tmp + +VENV="${VENV_VLLM}" +export PATH="$VENV/bin:$PATH" +export PYTHONUNBUFFERED=1 +export TMPDIR=/data/user1/yy/tmp +export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES}" + +LOG="/data/user1/yy/logs/dsv4_h200_vllm_mtp_vs_default_mtp_$(date +%Y%m%d_%H%M%S).log" +PID_FILE="/data/user1/yy/dsv4_h200_vllm_mtp_vs_default_mtp.pid" + +rm -f "$PID_FILE" + +echo "=== Starting vLLM+MTP server (TP=$TP, spec_method=$SPEC_METHOD, spec_tokens=$SPEC_TOKENS) ===" +echo "Model: $MODEL_PATH" +echo "Port: $MTP_PORT" +echo "Log: $LOG" + +nohup vllm serve "$MODEL_PATH" \ + --trust-remote-code \ + --tensor-parallel-size "$TP" \ + --kv-cache-dtype fp8 \ + --max-model-len "$MAX_MODEL_LEN" \ + --max-num-seqs "$MAX_NUM_SEQS" \ + --block-size 256 \ + --gpu-memory-utilization 0.90 \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --speculative-config "{\"method\":\"mtp\",\"num_speculative_tokens\":$SPEC_TOKENS}" \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --port "$MTP_PORT" \ + > "$LOG" 2>&1 & + +PID=$! +echo $PID > "$PID_FILE" +echo "PID: $PID" +echo "Waiting for health..." + +for i in $(seq 1 240); do + if curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${MTP_PORT}/health" >/dev/null 2>&1; then + echo "vLLM+MTP server is ready at http://127.0.0.1:${MTP_PORT}" + echo "Log: $LOG" + exit 0 + fi + if ! kill -0 $PID 2>/dev/null; then + echo "ERROR: vLLM+MTP server exited early" + tail -200 "$LOG" + exit 1 + fi + echo "Waiting... ($i/240)" + sleep 5 +done + +echo "ERROR: vLLM+MTP server not healthy after 240 retries" +tail -200 "$LOG" +exit 1