From eb431d392521f263183fefe46b8249360ddc0767 Mon Sep 17 00:00:00 2001 From: Benchmark Bot Date: Wed, 8 Jul 2026 02:17:13 +0000 Subject: [PATCH] Initial commit: benchmark scripts and final reports --- .gitignore | 27 ++ BENCHMARK_WORKFLOW.md | 122 ++++++ README.md | 63 ++++ .../dspark_grid_20260707-132641/evaluation.md | 167 +++++++++ .../dspark_grid_20260707-132641/report.md | 120 ++++++ .../comparison_report.md | 129 +++++++ .../README.md | 102 +++++ .../summary.json | 197 ++++++++++ .../eagle_grid/dspark_vs_eagle_report.md | 96 +++++ bench_results/eagle_grid/report.md | 6 + .../summary.json | 58 +++ .../length_distribution.png | Bin 0 -> 150830 bytes .../report.md | 267 +++++++++++++ .../summary.json | 156 ++++++++ .../summary.json | 39 ++ .../summary.json | 59 +++ .../summary.json | 131 +++++++ cleanup_summary.md | 110 ++++++ dspark_deepseekv4_fix_pr_prep.md | 286 ++++++++++++++ dsv4_inference_comparison_report.md | 351 ++++++++++++++++++ scripts/SLO_STANDARDS.md | 33 ++ scripts/analyze_dspark_nsys.py | 87 +++++ scripts/bench_dsv4_comparison.py | 323 ++++++++++++++++ scripts/bench_vllm_dspark_qwen3.py | 266 +++++++++++++ scripts/benchmark_dspark_0707/README.md | 60 +++ .../bench_dspark_focused.sh | 64 ++++ .../benchmark_dspark_0707/bench_dspark_p1.sh | 60 +++ .../benchmark_dspark_0707/bench_dspark_p2.sh | 63 ++++ .../benchmark_dspark_0707/bench_dspark_p3.sh | 61 +++ .../parse_eagle_vs_dspark.py | 258 +++++++++++++ .../benchmark_dspark_0707/parse_results.py | 120 ++++++ .../parse_st_comparison.py | 174 +++++++++ .../run_dspark_benchmark_grid.sh | 114 ++++++ .../run_dspark_st_comparison.sh | 103 +++++ scripts/benchmark_dsv4_backend_comparison.sh | 128 +++++++ scripts/install_vllm_dspark.sh | 27 ++ scripts/profile_dspark.py | 261 +++++++++++++ scripts/run_sglang_benchmark.sh | 157 ++++++++ scripts/run_sglang_max_throughput.sh | 143 +++++++ scripts/start_dsv4_dspark_8card.sh | 65 ++++ scripts/start_dsv4_dspark_8card_bf16kv.sh | 63 ++++ scripts/start_dsv4_dspark_8card_flashinfer.sh | 66 ++++ scripts/start_dsv4_dspark_8card_param.sh | 85 +++++ scripts/start_pd_single_node.sh | 138 +++++++ scripts/start_sglang_dsv4_8card.sh | 27 ++ scripts/start_vllm_eagle_dsv4_8card.sh | 65 ++++ scripts/start_vllm_mtp_dsv4_8card.sh | 66 ++++ scripts/start_vllm_pd_single_node.sh | 156 ++++++++ scripts/test_block_sparse_attn.py | 84 +++++ scripts/wait_and_profile_dspark.sh | 60 +++ 50 files changed, 5863 insertions(+) create mode 100644 .gitignore create mode 100644 BENCHMARK_WORKFLOW.md create mode 100644 README.md create mode 100644 bench_results/dspark_grid_20260707-132641/evaluation.md create mode 100644 bench_results/dspark_grid_20260707-132641/report.md create mode 100644 bench_results/dspark_st_comparison_20260707-150649/comparison_report.md create mode 100644 bench_results/dsv4_backend_comparison_20260707/README.md create mode 100644 bench_results/dsv4_comparison_20260705_152221/summary.json create mode 100644 bench_results/eagle_grid/dspark_vs_eagle_report.md create mode 100644 bench_results/eagle_grid/report.md create mode 100644 bench_results/sglang_8card_max_throughput_20260705_030839/summary.json create mode 100644 bench_results/sglang_8card_systematic_20260704_120819/length_distribution.png create mode 100644 bench_results/sglang_8card_systematic_20260704_120819/report.md create mode 100644 bench_results/sglang_8card_systematic_20260704_120819/summary.json create mode 100644 bench_results/vllm_dspark_qwen3_20260705_121046/summary.json create mode 100644 bench_results/vllm_dspark_qwen3_20260705_121218/summary.json create mode 100644 bench_results/vllm_dspark_qwen3_20260705_121256/summary.json create mode 100644 cleanup_summary.md create mode 100644 dspark_deepseekv4_fix_pr_prep.md create mode 100644 dsv4_inference_comparison_report.md create mode 100644 scripts/SLO_STANDARDS.md create mode 100644 scripts/analyze_dspark_nsys.py create mode 100644 scripts/bench_dsv4_comparison.py create mode 100644 scripts/bench_vllm_dspark_qwen3.py create mode 100644 scripts/benchmark_dspark_0707/README.md create mode 100755 scripts/benchmark_dspark_0707/bench_dspark_focused.sh create mode 100755 scripts/benchmark_dspark_0707/bench_dspark_p1.sh create mode 100755 scripts/benchmark_dspark_0707/bench_dspark_p2.sh create mode 100755 scripts/benchmark_dspark_0707/bench_dspark_p3.sh create mode 100644 scripts/benchmark_dspark_0707/parse_eagle_vs_dspark.py create mode 100644 scripts/benchmark_dspark_0707/parse_results.py create mode 100644 scripts/benchmark_dspark_0707/parse_st_comparison.py create mode 100755 scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh create mode 100755 scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh create mode 100644 scripts/benchmark_dsv4_backend_comparison.sh create mode 100755 scripts/install_vllm_dspark.sh create mode 100755 scripts/profile_dspark.py create mode 100755 scripts/run_sglang_benchmark.sh create mode 100755 scripts/run_sglang_max_throughput.sh create mode 100755 scripts/start_dsv4_dspark_8card.sh create mode 100755 scripts/start_dsv4_dspark_8card_bf16kv.sh create mode 100755 scripts/start_dsv4_dspark_8card_flashinfer.sh create mode 100755 scripts/start_dsv4_dspark_8card_param.sh create mode 100755 scripts/start_pd_single_node.sh create mode 100755 scripts/start_sglang_dsv4_8card.sh create mode 100644 scripts/start_vllm_eagle_dsv4_8card.sh create mode 100644 scripts/start_vllm_mtp_dsv4_8card.sh create mode 100755 scripts/start_vllm_pd_single_node.sh create mode 100644 scripts/test_block_sparse_attn.py create mode 100755 scripts/wait_and_profile_dspark.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92a7e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# 环境/依赖/临时文件 +envs/ +deps/ +tmp/ +*.pid +__pycache__/ +*.pyc +*.pyo +*.egg-info/ + +# 大文件/压缩包 +*.zip +*.tar.gz +*.tar.bz2 + +# 中间日志 +logs/ +*.log + +# 数据集(可重新下载) +datasets/ + +# 原始请求级输出(方案 A:极简版,不存原始 jsonl) +bench_results/**/raw_outputs/ + +# 无关项目 +loomeval_yy/ diff --git a/BENCHMARK_WORKFLOW.md b/BENCHMARK_WORKFLOW.md new file mode 100644 index 0000000..c7e22df --- /dev/null +++ b/BENCHMARK_WORKFLOW.md @@ -0,0 +1,122 @@ +# Benchmark Workflow & Directory Conventions + +## Directory Layout + +``` +/data/user1/yy/ +├── scripts/ # all benchmark/orchestrator/utility scripts +│ ├── benchmark_dspark_0707/ # DSpark benchmark suite +│ ├── benchmark_dsv4_backend_comparison.sh +│ ├── start_dsv4_dspark_8card.sh +│ ├── start_sglang_dsv4_8card.sh +│ └── ... +├── bench_results/ # all benchmark outputs and reports +│ ├── dsv4_backend_comparison_20260707/ +│ │ ├── raw_outputs/ # JSONL raw outputs +│ │ ├── logs/ # per-run logs +│ │ └── README.md # output manifest + provenance +│ ├── dspark_grid_20260707-132641/ +│ ├── dspark_st_comparison_20260707-150649/ +│ ├── eagle_grid/ +│ └── ... +├── logs/ # server logs (stdout/stderr from start scripts) +├── datasets/ # benchmark datasets +└── envs/ # Python virtual environments +``` + +## Rules + +1. **Scripts live in `scripts/` only.** + - Group related scripts into subdirectories, e.g. `scripts/benchmark_dspark_0707/`. + - Each script group should have its own `README.md` listing scripts, purpose, and outputs. + +2. **Benchmark outputs live in `bench_results/` only.** + - Never leave `.jsonl`, `.json`, `.log`, or `.md` reports in the project root. + - Each benchmark run gets its own directory: `bench_results/_/`. + - Raw outputs go in `raw_outputs/`. + - Logs go in `logs/`. + - Reports (e.g. `report.md`, `comparison_report.md`) go in the run root. + +3. **Each `bench_results//` directory must contain a `README.md`.** + - What was benchmarked. + - Which script(s) produced the outputs. + - File naming convention. + - Inventory of outputs (table). + +4. **Scripts should default `RESULT_ROOT` to `bench_results/_${RUN_ID}`.** + - Allow override via `RESULT_ROOT` env var. + - Use `RUN_ID=$(date '+%Y%m%d-%H%M%S')` unless specified. + +5. **Server start scripts write to `logs/`.** + - `logs/_.log` + - Keep server logs separate from benchmark result logs. + +## Naming Conventions + +### Result directories + +``` +bench_results/_/ +``` + +Examples: + +- `bench_results/dspark_grid_20260707-132641/` +- `bench_results/dsv4_backend_comparison_20260707/` + +### Raw output files + +For detailed per-request JSONL outputs: + +``` +{backend}_{MMDD}_{concurrency}_{input_len}_{output_len}.jsonl +``` + +For summary JSON outputs from `sglang.bench_serving --output-file`: + +``` +{backend}_{scenario}_{params}.json +``` + +### Logs + +``` +logs/_YYYYMMDD_HHMMSS.log +logs/_orchestrator_YYYYMMDD_HHMMSS.log +``` + +## Quick Start + +### Run DSpark grid benchmark + +```bash +bash scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh +``` + +### Run DSpark spec-tokens comparison + +```bash +bash scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh +``` + +### Run SGLang vs vLLM backend comparison + +```bash +# Start SGLang on port 30000 and vLLM on port 8000, then: +bash scripts/benchmark_dsv4_backend_comparison.sh all +``` + +### Parse results + +```bash +/data/user1/yy/envs/sglang/bin/python scripts/benchmark_dspark_0707/parse_results.py \ + /data/user1/yy/bench_results/dspark_grid_ +``` + +## Checklist Before Committing / Archiving + +- [ ] No `.jsonl`, `.json`, `.log`, or `.md` files left in `/data/user1/yy/` root. +- [ ] All outputs moved to `bench_results/_/`. +- [ ] `bench_results//README.md` exists and documents provenance. +- [ ] Scripts moved to `scripts/` (or `scripts//`). +- [ ] Script path references updated after moving. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4c1366 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# DSV4 / DeepSeek-V4-Flash 推理测速项目 + +> 记录 vllm-dspark、SGLang、vLLM 等后端在 DeepSeek-V4-Flash 上的 benchmark 脚本与最终结果。 + +## 目录说明 + +| 目录/文件 | 说明 | +|---|---| +| `scripts/` | 所有 benchmark 脚本、服务启动脚本、结果解析脚本 | +| `bench_results/` | 各次实验的最终报告与 summary JSON | +| `BENCHMARK_WORKFLOW.md` | benchmark 目录与命名规范 | +| `scripts/SLO_STANDARDS.md` | 推理服务 SLO 标准(TTFT/TPOT) | + +> 注:`.gitignore` 已排除 `envs/`、`deps/`、`tmp/`、`datasets/`、`logs/`、`*.zip`、原始 `raw_outputs/*.jsonl` 等大文件/中间文件。 + +## 实验索引 + +| 实验 | 脚本 | 最终报告 | 说明 | +|---|---|---|---| +| DSpark grid benchmark | `scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh` | `bench_results/dspark_grid_20260707-132641/report.md` | P1/P2/P3 全量网格 | +| DSpark spec-tokens 对比 | `scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh` | `bench_results/dspark_st_comparison_20260707-150649/comparison_report.md` | `--spec-tokens 3` vs `5` | +| SGLang EAGLE vs DSpark | `scripts/start_sglang_dsv4_8card.sh` + 对比解析脚本 | `bench_results/eagle_grid/dspark_vs_eagle_report.md` | EAGLE 投机解码对比 | +| SGLang vs vLLM 后端对比(0707) | `scripts/benchmark_dsv4_backend_comparison.sh` | `bench_results/dsv4_backend_comparison_20260707/README.md` | 遗留 raw outputs 清单 | +| SGLang 8-card systematic | `scripts/run_sglang_benchmark.sh` | `bench_results/sglang_8card_systematic_20260704_120819/report.md` | 早期 SGLang 系统扫描 | +| SGLang 8-card max throughput | `scripts/run_sglang_max_throughput.sh` | `bench_results/sglang_8card_max_throughput_20260705_030839/summary.json` | 最大吞吐扫描 | +| vLLM-dspark Qwen3 | `scripts/bench_vllm_dspark_qwen3.py` | `bench_results/vllm_dspark_qwen3_20260705_121256/summary.json` | Qwen3 模型测速 | +| DSV4 后端对比(0705) | `scripts/bench_dsv4_comparison.py` | `bench_results/dsv4_comparison_20260705_152221/summary.json` | 早期 DSpark 配置对比 | + +## 快速复现 + +### DSpark grid + +```bash +bash scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh +``` + +### Spec-tokens 对比 + +```bash +bash scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh +``` + +### 解析已有结果 + +```bash +/data/user1/yy/envs/sglang/bin/python scripts/benchmark_dspark_0707/parse_results.py \ + /data/user1/yy/bench_results/dspark_grid_20260707-132641 +``` + +## SLO 参考 + +详见 `scripts/SLO_STANDARDS.md`。 + +主要关注指标: + +- **TTFT P95**:S2(DeepSeek-V4-Flash 所在层)目标 `< 3s` +- **TPOT**:S2 目标 `< 50ms` + +## 环境要求 + +- Python env:`/data/user1/yy/envs/vllm-dspark`(服务端)、`/data/user1/yy/envs/sglang`(压测客户端) +- 模型:`/data/models/DeepSeek-V4-Flash`、`/data/models/DeepSeek-V4-Flash-DSpark` +- 硬件:8× H200(当前配置) diff --git a/bench_results/dspark_grid_20260707-132641/evaluation.md b/bench_results/dspark_grid_20260707-132641/evaluation.md new file mode 100644 index 0000000..70d0cf7 --- /dev/null +++ b/bench_results/dspark_grid_20260707-132641/evaluation.md @@ -0,0 +1,167 @@ +# vllm-dspark Benchmark 结果评估 + +> 对应结果:`/data/user1/yy/bench_results/dspark_grid_20260707-132641` +> 运行时间:2026-07-07 +> 硬件:8× NVIDIA H200 143GB +> 模型:`/data/models/DeepSeek-V4-Flash-DSpark` +> 服务配置:TP=8,FP8 KV cache,`--spec-method dspark --spec-tokens 5`,block-size=256,max-num-seqs=256 +> 压测工具:`sglang.bench_serving --backend vllm` + +--- + +## 1. 总体结论 + +本次 grid 测试覆盖了从轻量 chat 到超长上下文、从重 decode 到高并发压测的多种场景。整体上看: + +- **DSpark 在短输入、中高并发场景下表现优秀**,`chat_short` 在并发 64 时达到约 9580 tok/s,`stress_standard` 在并发 128 时达到约 17465 tok/s。 +- **低并发下 DSpark 的 draft 开销明显**,部分场景单并发延迟和吞吐都不如预期。 +- **超长上下文场景存在明显的吞吐拐点**,`long_rag`(32K 输入)在并发 4 达到峰值后,并发 8 吞吐腰斩。 +- **延迟长尾(P95/P99)普遍较重**,提示调度、内存或投机解码验证阶段存在抖动。 + +--- + +## 2. 异常点分析 + +### 2.1 低并发(c=1)下 DSpark 收益被开销吃掉 + +以 `chat_standard`(input=1000, output=256)为例: + +| 并发 | req/s | mean E2E(ms) | P99 E2E(ms) | mean TTFT(ms) | P99 TTFT(ms) | +|---|---:|---:|---:|---:|---:| +| 1 | 0.49 | 2040.65 | 10715.53 | 1412.48 | 8430.71 | +| 8 | 4.35 | 1819.35 | 15165.63 | 327.62 | 4404.92 | +| 16 | 8.32 | 1885.15 | 10491.31 | 443.50 | 8567.07 | +| 32 | 10.00 | 3156.73 | 12936.08 | 560.77 | 7129.66 | + +- **单并发吞吐仅 0.49 req/s**,远低于无投机基线可预期的水平。 +- **P99 TTFT 高达 8.4s**,说明单请求首次预填充极不稳定,可能是 draft 验证/编译缓存未命中或 warmup 不充分导致。 +- 并发提升到 8 后,TTFT 均值下降到 327ms,说明 DSpark 的 draft 计算需要 batch 才能摊薄。 + +**判断**:这是 DSpark 的典型特征——低并发下 draft 固定开销无法被摊薄,导致延迟尾和单请求吞吐都较差。 + +### 2.2 超长上下文出现吞吐断崖 + +`long_rag`(input=32000, output=512): + +| 并发 | Total tok/s | mean TTFT(ms) | P99 TTFT(ms) | +|---|---:|---:|---:| +| 1 | 16104.98 | 549.72 | 1163.96 | +| 2 | 43215.62 | 192.57 | 409.32 | +| 4 | **70040.54** | 191.57 | 427.69 | +| 8 | 34141.70(↓51%) | 930.98 | 2742.48 | + +`long_context_probe`(input=16000, output=512)也有类似模式: + +| 并发 | Total tok/s | mean TTFT(ms) | +|---|---:|---:| +| 1 | 11051.09 | 316.59 | +| 2 | 26862.06 | 110.65 | +| 4 | 43042.77 | 127.38 | +| 8 | 29255.46(↓32%) | 453.93 | +| 16 | 43292.68 | 444.14 | + +**判断**: +- 32K/16K 长 prefill 在并发 4 时达到最佳,继续加并发反而下降,可能与 **KV cache 显存带宽瓶颈**、**prefill 阶段 scheduling 阻塞** 或 **hybrid KV cache manager 的换入换出** 有关。 +- `long_rag` 在 c=8 时 P99 TTFT 暴涨到 2.7s,进一步印证内存/调度压力。 + +### 2.3 延迟长尾普遍偏重 + +几乎所有场景的 P99 E2E 都是 mean E2E 的 2~4 倍: + +| 场景 | 并发 | mean E2E | P99 E2E | 倍数 | +|---|---:|---:|---:|---:| +| chat_standard | 32 | 3156.73 | 12936.08 | 4.1× | +| generation_standard | 32 | 2908.89 | 6938.03 | 2.4× | +| summarization | 32 | 5587.21 | 14541.75 | 2.6× | +| decode_heavy | 32 | 5562.57 | 12822.35 | 2.3× | +| stress_standard | 128 | 4417.43 | 14850.43 | 3.4× | + +**判断**:投机解码的 draft 验证失败会导致 fall back 到逐个 target 前向,造成明显的延迟毛刺;另外 vLLM v1 引擎的 scheduling 在高并发下也可能产生队列等待。 + +### 2.4 高并发下 TPOT/ITL 增长较快 + +`stress_standard`(input=1000, output=256): + +| 并发 | mean TPOT | P95 TPOT | P99 TPOT | mean ITL | +|---|---:|---:|---:|---:| +| 96 | 29.46 | 67.30 | 123.42 | 113.64 | +| 128 | 35.20 | 76.85 | 141.17 | 136.57 | + +TPOT 从 29ms 升到 35ms,P99 超过 140ms,说明 decode 阶段在极限并发下已经接近饱和。 + +--- + +## 3. 可重点优化的方向 + +### 3.1 按并发动态调整 `--spec-tokens` + +参考历史对比数据(`dsv4_inference_comparison_report.md`): + +| spec-tokens | 低并发(c=1) | 高并发(c=64) | +|---|---|---| +| 3 | 1.03 req/s, 254 tok/s | **14.53 req/s, 3507 tok/s** | +| 5 | **1.07 req/s, 257 tok/s** | 9.36 req/s, 2210 tok/s | +| 7 | 1.03 req/s, 251 tok/s | 9.13 req/s, 2218 tok/s | + +当前部署固定使用 `--spec-tokens 5`: +- 对低并发(c=1~16)相对友好; +- 对高并发(c≥64)不是最优,st=3 在高并发下接受率更高、验证开销更小。 + +**建议**: +- 高并发服务场景:改用 `--spec-tokens 3`。 +- 若负载混合,可考虑不同 endpoint/队列按并发分桶,或测试 st=3/st=5 在本 grid 下的完整表现。 + +### 3.2 长上下文场景优化 + +针对 `long_rag`(32K)和 `long_context_probe`(16K)的吞吐断崖: + +1. **KV cache dtype 对比测试**:当前是 FP8,可测试 BF16 是否改善长上下文稳定性和吞吐。 +2. **block-size 调整**:当前 256,可尝试 128 或 64 看是否减少内存碎片。 +3. **hybrid KV cache manager**:当前 `--no-disable-hybrid-kv-cache-manager`,可对比关闭后的表现。 +4. **限制长上下文并发**:若业务允许,对 32K 输入设置更低的 `max-num-seqs` 或独立队列,避免 c=8 后的内存带宽瓶颈。 +5. **attention backend**:可测试 `FLASHINFER_MLA_SPARSE_DSV4`(脚本 `start_dsv4_dspark_8card_flashinfer.sh`)在长上下文下的表现。 + +### 3.3 降低延迟长尾 + +1. **增加 warmup**:当前仅 10 条 warmup。DSpark 的 JIT 编译/算子 warmup 在首次运行时较重,建议增加到 50~100 条或先跑一轮 discard。 +2. **compilation cache 持久化**:确认 `TORCH_EXTENSIONS_DIR`、`VLLM_CACHE_ROOT` 等环境变量已设置并复用,避免每次重启重新编译 deep_gemm/tilelang。 +3. **调度参数**:尝试调整 `--max-num-batched-tokens`、`--max-num-seqs`、chunked prefill 相关参数,减少队列等待。 +4. **P99 TTFT 抖动**:高并发下 P99 TTFT 高通常与 prefill batching 有关,可尝试限制 prefill 阶段 batch 大小或启用 prompt chunked prefill。 + +### 3.4 低并发场景是否需要 DSpark + +对于 `chat_standard` c=1: +- 当前 0.49 req/s、P99 E2E 10.7s 的服务质量较差。 +- 若业务存在大量单用户/低并发请求,可考虑: + - 部署无投机基线 vLLM 专门服务低并发流量; + - 或设置最小 batch 阈值,累积少量请求后再用 DSpark 处理。 + +### 3.5 对比 baseline 与 SGLang + +本次测试只有 DSpark 单一配置,建议补充: +1. **vLLM-dspark 无投机基线**:用同样的 `sglang.bench_serving --backend vllm` 测一遍相同 grid,计算真实加速比。 +2. **SGLang EAGLE**:在相同 grid 下复测,验证 DSpark 在不同场景下的相对优势。 +3. **不同 `--spec-tokens` 的完整 grid**:st=3 和 st=5 各跑一遍,绘制并发-吞吐曲线。 + +--- + +## 4. 推荐下一步实验 + +| 优先级 | 实验 | 预期收益 | +|---|---|---| +| P0 | 高并发改用 `--spec-tokens 3` | 提升 c≥64 场景吞吐 20~50% | +| P0 | 长上下文(16K/32K)对比 BF16 KV cache | 改善吞吐断崖和 P99 TTFT | +| P1 | 增加 warmup / 预热缓存 | 降低 P99 TTFT 和首请求延迟 | +| P1 | 测试 `FLASHINFER_MLA_SPARSE_DSV4` backend | 可能提升长上下文吞吐 | +| P2 | 跑无投机基线 grid | 获得真实加速比 | +| P2 | 对比 st=3/st=5 完整曲线 | 找到最佳投机长度配置 | + +--- + +## 5. 结论 + +当前 DSpark 部署在 **短输入、中高并发** 场景下已经展现出明显优势,但在 **低并发** 和 **超长上下文** 场景下存在明显短板。最优先的优化是: + +1. 高并发服务改用 `--spec-tokens 3`; +2. 针对 16K/32K 长上下文做 KV cache dtype 和 attention backend 的调优; +3. 补充 baseline 测试,量化 DSpark 的真实收益。 diff --git a/bench_results/dspark_grid_20260707-132641/report.md b/bench_results/dspark_grid_20260707-132641/report.md new file mode 100644 index 0000000..6f8dc81 --- /dev/null +++ b/bench_results/dspark_grid_20260707-132641/report.md @@ -0,0 +1,120 @@ +# vllm-dspark Benchmark Grid Report + +- Result root: `/data/user1/yy/bench_results/dspark_grid_20260707-132641` +- Model: `/data/models/DeepSeek-V4-Flash-DSpark` +- Backend: vllm-dspark (TP=8, FP8 KV cache, spec-method=dspark, spec-tokens=5) +- Benchmark client: `sglang.bench_serving --backend vllm` + +## p1_quick + +### chat_standard (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 65.33 | 32 | 0.49 | 195.58 | 71.28 | 266.86 | 2040.65 | 8241.20 | 10715.53 | 1412.48 | 6903.30 | 8430.71 | 6.41 | 32.72 | 51.29 | 16.61 | +| 8 | 29.42 | 128 | 4.35 | 2130.85 | 569.52 | 2700.37 | 1819.35 | 8891.79 | 15165.63 | 327.62 | 379.39 | 4404.92 | 11.58 | 51.06 | 82.44 | 51.00 | +| 16 | 30.76 | 256 | 8.32 | 4144.89 | 1144.74 | 5289.63 | 1885.15 | 8902.10 | 10491.31 | 443.50 | 2779.49 | 8567.07 | 12.10 | 23.11 | 83.70 | 46.18 | +| 32 | 51.18 | 512 | 10.00 | 5017.88 | 1331.65 | 6349.53 | 3156.73 | 10438.92 | 12936.08 | 560.77 | 5750.71 | 7129.66 | 22.09 | 66.23 | 171.57 | 85.81 | + +### generation_standard (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 36.24 | 32 | 0.88 | 352.60 | 479.25 | 831.85 | 1131.63 | 1979.98 | 2576.42 | 66.57 | 141.80 | 177.56 | 1.94 | 2.85 | 2.88 | 8.50 | +| 8 | 27.43 | 128 | 4.67 | 2285.21 | 2362.35 | 4647.56 | 1649.16 | 3162.33 | 3844.70 | 80.88 | 157.80 | 160.50 | 3.23 | 5.31 | 7.09 | 14.77 | +| 16 | 35.39 | 256 | 7.23 | 3602.52 | 3654.79 | 7257.31 | 2133.90 | 4187.67 | 5500.20 | 88.78 | 169.39 | 199.78 | 4.37 | 6.97 | 11.81 | 19.28 | +| 32 | 48.16 | 512 | 10.63 | 5333.41 | 5371.85 | 10705.25 | 2908.89 | 5849.64 | 6938.03 | 112.84 | 211.64 | 405.78 | 5.84 | 9.30 | 12.47 | 25.89 | + +### summarization (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 33.63 | 32 | 0.95 | 4247.40 | 516.49 | 4763.90 | 1049.88 | 1783.33 | 1848.92 | 178.56 | 292.66 | 314.18 | 1.60 | 1.78 | 2.41 | 8.62 | +| 4 | 10.99 | 32 | 2.91 | 13000.06 | 1580.84 | 14580.90 | 1301.15 | 2238.30 | 2324.70 | 101.68 | 187.78 | 188.57 | 2.18 | 2.47 | 3.49 | 11.86 | +| 8 | 33.60 | 128 | 3.81 | 15369.23 | 1908.96 | 17278.19 | 2072.22 | 3978.11 | 4531.26 | 214.01 | 502.25 | 630.15 | 3.70 | 5.52 | 7.98 | 19.60 | +| 16 | 46.93 | 256 | 5.45 | 22698.72 | 2761.38 | 25460.10 | 2840.57 | 6243.39 | 7179.11 | 235.75 | 552.81 | 740.45 | 5.45 | 9.62 | 12.75 | 27.77 | + +## p2_core + +### chat_short (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 11.39 | 32 | 2.81 | 717.30 | 408.87 | 1126.18 | 355.00 | 685.00 | 815.55 | 48.57 | 105.64 | 105.97 | 2.07 | 3.10 | 3.69 | 8.51 | +| 8 | 10.62 | 128 | 12.06 | 3320.19 | 1591.90 | 4912.09 | 647.05 | 1401.33 | 1648.15 | 78.91 | 151.78 | 155.67 | 4.59 | 7.20 | 12.54 | 17.85 | +| 16 | 14.89 | 256 | 17.19 | 4533.71 | 2394.66 | 6928.36 | 902.72 | 1879.32 | 2211.23 | 95.95 | 165.09 | 217.14 | 6.19 | 10.26 | 15.12 | 23.65 | +| 32 | 24.90 | 512 | 20.56 | 5458.26 | 2723.85 | 8182.10 | 1508.89 | 3076.09 | 3881.52 | 161.04 | 292.00 | 383.41 | 10.80 | 18.19 | 25.45 | 41.86 | +| 64 | 21.27 | 512 | 24.07 | 6391.14 | 3189.39 | 9580.52 | 2547.81 | 5606.07 | 6543.97 | 268.68 | 543.21 | 588.63 | 18.24 | 30.39 | 39.57 | 70.29 | + +### chat_standard (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 64 | 19.87 | 512 | 25.76 | 12922.80 | 3429.47 | 16352.28 | 2380.94 | 4838.43 | 6315.98 | 291.17 | 794.65 | 947.26 | 16.87 | 30.96 | 42.55 | 68.46 | + +### generation_standard (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 64 | 43.59 | 512 | 11.75 | 5892.47 | 5934.93 | 11827.40 | 5136.14 | 10244.70 | 12575.20 | 209.70 | 536.96 | 606.58 | 10.33 | 17.41 | 21.20 | 45.47 | + +### long_context_probe (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 23.79 | 32 | 1.34 | 10684.78 | 366.30 | 11051.09 | 742.62 | 1237.28 | 1418.82 | 316.59 | 604.23 | 617.85 | 1.54 | 1.72 | 1.85 | 8.53 | +| 2 | 9.79 | 32 | 3.27 | 25971.67 | 890.38 | 26862.06 | 604.82 | 1055.84 | 1116.41 | 110.65 | 194.62 | 206.64 | 1.77 | 2.04 | 2.13 | 9.90 | +| 4 | 6.11 | 32 | 5.24 | 41616.05 | 1426.72 | 43042.77 | 741.94 | 1262.13 | 1447.68 | 127.38 | 222.70 | 235.35 | 2.26 | 2.69 | 3.69 | 12.39 | +| 8 | 36.07 | 128 | 3.55 | 28357.82 | 897.65 | 29255.46 | 2232.38 | 4946.97 | 5371.22 | 453.93 | 1089.99 | 1182.47 | 7.06 | 13.03 | 20.03 | 38.51 | +| 16 | 49.08 | 256 | 5.22 | 41910.25 | 1382.43 | 43292.68 | 3028.26 | 7651.28 | 9531.10 | 444.14 | 1212.92 | 1368.04 | 10.00 | 20.71 | 30.74 | 53.07 | + +### rag_medium (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 18.21 | 32 | 1.76 | 3568.86 | 494.63 | 4063.49 | 568.24 | 966.60 | 1201.99 | 110.94 | 151.11 | 154.78 | 1.63 | 1.95 | 2.50 | 8.60 | +| 4 | 6.31 | 32 | 5.07 | 10300.85 | 1427.65 | 11728.51 | 751.19 | 1259.07 | 1593.86 | 92.09 | 161.03 | 187.47 | 2.34 | 3.25 | 4.02 | 12.33 | +| 8 | 19.13 | 128 | 6.69 | 14147.80 | 1692.32 | 15840.12 | 1166.59 | 2199.67 | 2982.23 | 148.09 | 253.10 | 350.67 | 4.30 | 6.78 | 9.92 | 21.06 | +| 16 | 25.89 | 256 | 9.89 | 20561.53 | 2622.36 | 23183.89 | 1587.09 | 3316.74 | 4019.02 | 164.29 | 303.79 | 389.34 | 5.50 | 8.80 | 14.38 | 27.81 | +| 32 | 39.47 | 512 | 12.97 | 26523.95 | 3335.30 | 29859.24 | 2415.10 | 5069.76 | 6195.14 | 227.81 | 444.76 | 510.18 | 8.64 | 13.54 | 20.29 | 44.49 | + +### summarization (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 32 | 91.68 | 512 | 5.58 | 22445.97 | 2821.71 | 25267.67 | 5587.21 | 11177.97 | 12257.18 | 430.38 | 967.32 | 2238.28 | 10.56 | 16.13 | 23.53 | 54.40 | + +## p3_extension + +### decode_heavy (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 56.16 | 32 | 0.57 | 145.47 | 546.26 | 691.72 | 1754.17 | 2994.25 | 3175.70 | 52.30 | 107.66 | 108.83 | 1.88 | 2.68 | 3.14 | 8.53 | +| 8 | 54.33 | 128 | 2.36 | 648.72 | 2526.47 | 3175.19 | 3302.64 | 6408.14 | 7166.98 | 74.21 | 143.24 | 149.17 | 3.05 | 4.37 | 4.79 | 14.12 | +| 16 | 65.74 | 256 | 3.89 | 1026.90 | 4012.07 | 5038.96 | 3934.72 | 7216.03 | 9638.96 | 81.02 | 165.95 | 197.11 | 3.98 | 5.82 | 7.21 | 17.79 | +| 32 | 92.36 | 512 | 5.54 | 1471.70 | 5761.81 | 7233.51 | 5562.57 | 10498.47 | 12822.35 | 107.17 | 234.99 | 426.35 | 5.40 | 8.14 | 9.03 | 24.38 | + +### long_rag (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 1 | 30.84 | 32 | 1.04 | 15823.63 | 281.34 | 16104.98 | 962.70 | 1688.61 | 1788.29 | 549.72 | 1124.54 | 1163.96 | 1.49 | 1.72 | 1.82 | 8.52 | +| 2 | 11.49 | 32 | 2.78 | 42460.67 | 754.95 | 43215.62 | 708.16 | 1202.62 | 1253.12 | 192.57 | 352.96 | 409.32 | 2.05 | 2.37 | 6.62 | 10.76 | +| 4 | 7.09 | 32 | 4.51 | 68816.98 | 1223.56 | 70040.54 | 854.49 | 1516.80 | 1562.42 | 191.57 | 426.45 | 427.69 | 2.62 | 3.20 | 7.33 | 13.69 | +| 8 | 60.48 | 128 | 2.12 | 33604.85 | 536.85 | 34141.70 | 3732.80 | 8420.06 | 9705.36 | 930.98 | 2125.14 | 2742.48 | 11.12 | 22.70 | 39.17 | 61.41 | + +### stress_generation (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 96 | 81.20 | 768 | 9.46 | 4842.45 | 4635.63 | 9478.08 | 9772.15 | 20945.93 | 25314.28 | 326.63 | 782.50 | 795.30 | 20.35 | 33.33 | 42.50 | 88.91 | +| 128 | 92.26 | 1024 | 11.10 | 5587.83 | 5514.57 | 11102.40 | 11012.89 | 22012.44 | 28276.20 | 360.94 | 812.12 | 1024.93 | 22.53 | 35.59 | 42.09 | 99.75 | + +### stress_standard (20260707-132641) + +| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) | +|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------| +| 96 | 30.50 | 768 | 25.18 | 12891.05 | 3175.02 | 16066.07 | 3689.97 | 9383.09 | 11958.09 | 435.22 | 1225.11 | 2581.90 | 29.46 | 67.30 | 123.42 | 113.64 | +| 128 | 36.86 | 1024 | 27.78 | 13985.42 | 3479.38 | 17464.80 | 4417.43 | 11825.67 | 14850.43 | 456.55 | 1032.19 | 1922.87 | 35.20 | 76.85 | 141.17 | 136.57 | + diff --git a/bench_results/dspark_st_comparison_20260707-150649/comparison_report.md b/bench_results/dspark_st_comparison_20260707-150649/comparison_report.md new file mode 100644 index 0000000..0240608 --- /dev/null +++ b/bench_results/dspark_st_comparison_20260707-150649/comparison_report.md @@ -0,0 +1,129 @@ +# vllm-dspark `--spec-tokens` 对比报告 + +- 结果目录:`/data/user1/yy/bench_results/dspark_st_comparison_20260707-150649` +- 模型:`/data/models/DeepSeek-V4-Flash-DSpark` +- 后端:vllm-dspark (TP=8, FP8 KV cache) +- 对比参数:`--spec-tokens 3` vs `--spec-tokens 5` +- Warmup:100 条 +- 压测客户端:`sglang.bench_serving --backend vllm` + +--- + +## 核心指标对比 + +| Scenario | Concurrency | Spec | Duration(s) | Req/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P99 TPOT(ms) | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| chat_short | 1 | 3 | 12.80 | 2.50 | 363.90 | 1002.30 | 398.97 | 757.11 | 948.62 | 43.97 | 68.89 | 2.42 | 3.76 | +| chat_short | 1 | 5 | 11.72 | 2.73 | 397.37 | 1094.51 | 365.29 | 743.89 | 782.98 | 64.42 | 174.36 | 2.06 | 3.40 | +| chat_short | 8 | 3 | 11.08 | 11.55 | 1525.06 | 4705.85 | 672.66 | 1269.82 | 1552.65 | 76.20 | 221.18 | 4.67 | 8.43 | +| chat_short | 8 | 5 | 11.78 | 10.87 | 1434.62 | 4426.78 | 718.40 | 1511.14 | 2008.00 | 94.97 | 220.85 | 4.95 | 9.93 | +| chat_short | 16 | 3 | 21.82 | 11.73 | 1634.03 | 4727.68 | 1333.99 | 2611.76 | 2921.26 | 151.65 | 337.53 | 8.85 | 17.66 | +| chat_short | 16 | 5 | 16.42 | 15.59 | 2171.58 | 6282.94 | 997.37 | 2147.86 | 2496.56 | 107.56 | 240.46 | 7.09 | 21.93 | +| chat_short | 32 | 3 | 24.62 | 20.79 | 2754.96 | 8275.55 | 1497.40 | 2944.48 | 3658.90 | 144.47 | 453.29 | 10.72 | 23.97 | +| chat_short | 32 | 5 | 26.32 | 19.45 | 2577.42 | 7742.25 | 1600.40 | 3403.00 | 4521.14 | 169.13 | 402.36 | 11.39 | 27.82 | +| chat_short | 64 | 3 | 21.66 | 23.63 | 3131.20 | 9405.73 | 2595.90 | 5230.98 | 6258.25 | 251.84 | 509.86 | 18.60 | 40.84 | +| chat_short | 64 | 5 | 21.26 | 24.09 | 3191.43 | 9586.66 | 2555.12 | 5387.59 | 6559.69 | 274.71 | 610.40 | 18.47 | 42.08 | +| chat_standard | 64 | 3 | 20.73 | 24.70 | 3288.58 | 15680.46 | 2483.46 | 4798.63 | 5693.88 | 271.41 | 947.15 | 17.93 | 46.05 | +| chat_standard | 64 | 5 | 20.91 | 24.49 | 3259.71 | 15542.80 | 2512.52 | 5394.43 | 6023.55 | 300.81 | 932.60 | 17.83 | 44.47 | +| decode_heavy | 1 | 3 | 68.78 | 0.47 | 446.03 | 564.81 | 2148.54 | 3712.03 | 3835.51 | 38.60 | 41.18 | 2.28 | 3.03 | +| decode_heavy | 1 | 5 | 57.70 | 0.55 | 531.76 | 673.37 | 1802.00 | 2957.04 | 3676.66 | 59.06 | 114.78 | 1.92 | 3.03 | +| decode_heavy | 32 | 3 | 96.14 | 5.33 | 5535.64 | 6949.57 | 5809.29 | 10830.76 | 12546.55 | 86.83 | 382.71 | 5.62 | 8.11 | +| decode_heavy | 32 | 5 | 92.75 | 5.52 | 5737.73 | 7203.28 | 5595.02 | 10412.11 | 12522.00 | 107.12 | 331.08 | 5.43 | 9.15 | +| generation_standard | 64 | 3 | 38.58 | 13.27 | 6705.88 | 13363.77 | 4542.22 | 8273.38 | 9355.10 | 190.34 | 813.58 | 9.00 | 15.36 | +| generation_standard | 64 | 5 | 43.39 | 11.80 | 5961.40 | 11880.13 | 5075.77 | 9863.55 | 11533.47 | 216.87 | 626.93 | 10.09 | 20.90 | +| long_context_probe | 1 | 3 | 26.10 | 1.23 | 333.92 | 10074.03 | 815.18 | 1382.34 | 1556.00 | 265.28 | 558.73 | 2.02 | 2.19 | +| long_context_probe | 1 | 5 | 22.22 | 1.44 | 392.31 | 11835.70 | 693.30 | 1172.84 | 1404.17 | 267.61 | 561.65 | 1.53 | 1.88 | +| long_context_probe | 4 | 3 | 7.34 | 4.36 | 1187.03 | 35811.63 | 888.88 | 1550.70 | 1611.29 | 125.51 | 282.82 | 2.77 | 3.23 | +| long_context_probe | 4 | 5 | 6.59 | 4.85 | 1322.32 | 39893.35 | 806.55 | 1390.87 | 1500.22 | 143.42 | 236.41 | 2.41 | 3.54 | +| long_context_probe | 8 | 3 | 38.61 | 3.31 | 838.49 | 27327.58 | 2387.05 | 5028.60 | 5667.97 | 406.10 | 1093.54 | 7.74 | 17.09 | +| long_context_probe | 8 | 5 | 35.94 | 3.56 | 900.89 | 29361.32 | 2223.29 | 4693.10 | 5345.72 | 425.03 | 1241.57 | 7.08 | 19.83 | +| rag_medium | 1 | 3 | 22.31 | 1.43 | 403.86 | 3317.84 | 696.09 | 1165.77 | 1315.00 | 105.51 | 157.60 | 2.10 | 2.81 | +| rag_medium | 1 | 5 | 18.16 | 1.76 | 496.09 | 4075.47 | 566.58 | 968.90 | 1134.15 | 110.34 | 156.35 | 1.64 | 2.34 | +| rag_medium | 8 | 3 | 20.57 | 6.22 | 1573.71 | 14730.01 | 1251.63 | 2472.54 | 2796.12 | 127.54 | 309.65 | 4.46 | 7.41 | +| rag_medium | 8 | 5 | 19.13 | 6.69 | 1692.13 | 15838.39 | 1175.26 | 2263.59 | 3106.42 | 154.24 | 352.19 | 4.14 | 8.36 | +| rag_medium | 32 | 3 | 45.02 | 11.37 | 2923.96 | 26176.72 | 2756.39 | 5579.00 | 6675.25 | 215.15 | 477.20 | 10.00 | 19.84 | +| rag_medium | 32 | 5 | 42.98 | 11.91 | 3062.92 | 27420.79 | 2626.21 | 5182.78 | 6873.72 | 253.09 | 522.80 | 9.17 | 18.55 | +| stress_standard | 64 | 3 | 21.35 | 23.99 | 3193.19 | 15225.66 | 2569.66 | 5006.16 | 5831.81 | 261.98 | 710.57 | 18.02 | 36.01 | +| stress_standard | 64 | 5 | 21.75 | 23.54 | 3133.74 | 14942.17 | 2621.52 | 5431.08 | 7161.16 | 305.11 | 734.92 | 18.42 | 41.01 | +| stress_standard | 96 | 3 | 20.36 | 37.72 | 4756.72 | 24069.73 | 2433.73 | 4827.07 | 6001.29 | 272.61 | 749.99 | 18.04 | 38.52 | +| stress_standard | 96 | 5 | 25.33 | 30.32 | 3823.91 | 19349.55 | 3032.19 | 6272.09 | 8124.32 | 339.22 | 793.18 | 23.06 | 51.46 | +| stress_standard | 128 | 3 | 21.58 | 47.45 | 5942.48 | 29828.36 | 2581.42 | 5162.16 | 6222.01 | 298.69 | 970.61 | 19.09 | 40.93 | +| stress_standard | 128 | 5 | 28.36 | 36.11 | 4522.53 | 22700.92 | 3405.74 | 7240.13 | 9176.77 | 382.02 | 1087.53 | 25.74 | 57.83 | + +--- + +## 吞吐 winner 统计 + +| Scenario | Concurrency | Winner (Total tok/s) | st=3 Total tok/s | st=5 Total tok/s | 提升 | +|---|---:|---:|---:|---:|---:| +| chat_short | 1 | st=5 | 1002.30 | 1094.51 | 9.20% | +| chat_short | 8 | st=3 | 4705.85 | 4426.78 | 6.30% | +| chat_short | 16 | st=5 | 4727.68 | 6282.94 | 32.90% | +| chat_short | 32 | st=3 | 8275.55 | 7742.25 | 6.89% | +| chat_short | 64 | st=5 | 9405.73 | 9586.66 | 1.92% | +| chat_standard | 64 | st=3 | 15680.46 | 15542.80 | 0.89% | +| decode_heavy | 1 | st=5 | 564.81 | 673.37 | 19.22% | +| decode_heavy | 32 | st=5 | 6949.57 | 7203.28 | 3.65% | +| generation_standard | 64 | st=3 | 13363.77 | 11880.13 | 12.49% | +| long_context_probe | 1 | st=5 | 10074.03 | 11835.70 | 17.49% | +| long_context_probe | 4 | st=5 | 35811.63 | 39893.35 | 11.40% | +| long_context_probe | 8 | st=5 | 27327.58 | 29361.32 | 7.44% | +| rag_medium | 1 | st=5 | 3317.84 | 4075.47 | 22.84% | +| rag_medium | 8 | st=5 | 14730.01 | 15838.39 | 7.52% | +| rag_medium | 32 | st=5 | 26176.72 | 27420.79 | 4.75% | +| stress_standard | 64 | st=3 | 15225.66 | 14942.17 | 1.90% | +| stress_standard | 96 | st=3 | 24069.73 | 19349.55 | 24.39% | +| stress_standard | 128 | st=3 | 29828.36 | 22700.92 | 31.40% | + +--- + +## 关键发现 + +### 1. 并发是决定性因素 + +- **低并发(c=1)和中低并发(c=8~32)**:`st=5` 在多数场景下更优,尤其是长上下文和重 decode 场景。 +- **高并发(c≥96)**:`st=3` 明显更优,`stress_standard` c=128 时 st=3 比 st=5 高 **31.4%**。 +- **中并发(c=64)**:两者基本持平,差异多在 2% 以内。 + +### 2. st=5 更适合长上下文和重 decode + +| 场景 | 最佳 spec | 原因 | +|---|---|---| +| long_context_probe (16K) | st=5 | 长 prefill 下 st=5 的接受长度更高 | +| rag_medium (4K) | st=5 | 中长输入下 st=5 延迟和吞吐都更优 | +| decode_heavy (2K output) | st=5 | 输出越长,st=5 的投机收益越大 | + +### 3. st=3 在极限并发下更稳 + +`stress_standard` 随并发增加,st=3 与 st=5 的差距拉大: + +| 并发 | st=3 Total tok/s | st=5 Total tok/s | 差距 | +|---|---:|---:|---:| +| 64 | 15225.66 | 14942.17 | 基本持平 | +| 96 | 24069.73 | 19349.55 | st=3 高 24.4% | +| 128 | 29828.36 | 22700.92 | st=3 高 31.4% | + +这说明 st=5 在极限并发下验证开销和 KV cache 压力显著增加,而 st=3 的验证 batch 更小、调度更稳定。 + +### 4. chat_short c=16 的异常 + +`chat_short c=16` 时 st=5 比 st=3 高 32.9%,是一个明显的 outlier。可能原因是该并发度下 st=5 的 draft 接受率和 batch 利用率恰好达到甜点。 + +--- + +## 优化建议 + +### 推荐配置 + +| 负载特征 | 推荐 `--spec-tokens` | 说明 | +|---|---|---| +| 低并发 / 在线交互(c ≤ 32) | **5** | 延迟低、单请求吞吐高 | +| 中并发(c ≈ 64) | 3 或 5 均可 | 差异很小 | +| 高并发 / 压测(c ≥ 96) | **3** | 吞吐更高、P99 更稳 | +| 长上下文 / RAG / 重 decode | **5** | 接受长度优势更明显 | + +### 下一步 + +1. 如果业务以高并发为主,将默认服务改为 `--spec-tokens 3`。 +2. 如果业务混合,可考虑按输入长度或并发度路由到不同服务实例。 +3. 本次 warmup 已从 10 提升到 100,P99 TTFT 相比首次 grid 测试有明显改善;建议保持 100 条 warmup。 diff --git a/bench_results/dsv4_backend_comparison_20260707/README.md b/bench_results/dsv4_backend_comparison_20260707/README.md new file mode 100644 index 0000000..0845e31 --- /dev/null +++ b/bench_results/dsv4_backend_comparison_20260707/README.md @@ -0,0 +1,102 @@ +# DSV4 Backend Comparison Raw Outputs (2026-07-07) + +## Location + +`/data/user1/yy/bench_results/dsv4_backend_comparison_20260707` + +## Contents + +- `raw_outputs/` — raw per-benchmark JSONL files produced by `sglang.bench_serving --output-file --output-details`. +- `README.md` — this file. + +## File Naming Convention + +Each file follows the pattern: + +``` +{backend}_0707_{concurrency}_{input_len}_{output_len}.jsonl +``` + +Where: + +- `{backend}`: `sglang` or `vllm` +- `{concurrency}`: max concurrent requests (`max-concurrency`) +- `{input_len}`: `--random-input-len` +- `{output_len}`: `--random-output-len` + +## Output File Inventory + +### SGLang backend + +| File | Concurrency | Input len | Output len | +|---|---|---|---| +| sglang_0707_32_512_256.jsonl | 32 | 512 | 256 | +| sglang_0707_128_512_256.jsonl | 128 | 512 | 256 | +| sglang_0707_256_512_256.jsonl | 256 | 512 | 256 | +| sglang_0707_512_512_256.jsonl | 512 | 512 | 256 | +| sglang_0707_512_512_2000.jsonl | 512 | 512 | 2000 | +| sglang_0707_32_512_2000.jsonl | 32 | 512 | 2000 | +| sglang_0707_32_4000_512.jsonl | 32 | 4000 | 512 | +| sglang_0707_128_4000_512.jsonl | 128 | 4000 | 512 | +| sglang_0707_512_4000_512.jsonl | 512 | 4000 | 512 | +| sglang_0707_32_16000_512.jsonl | 32 | 16000 | 512 | +| sglang_0707_128_16000_512.jsonl | 128 | 16000 | 512 | +| sglang_0707_512_1000_256.jsonl | 512 | 1000 | 256 | +| sglang_0707_768_1000_256.jsonl | 768 | 1000 | 256 | +| sglang_0707_1024_1000_256.jsonl | 1024 | 1000 | 256 | +| sglang_0707_512_1000_1000.jsonl | 512 | 1000 | 1000 | + +### vLLM backend + +| File | Concurrency | Input len | Output len | +|---|---|---|---| +| vllm_0707_32_512_256.jsonl | 32 | 512 | 256 | +| vllm_0707_128_512_256.jsonl | 128 | 512 | 256 | +| vllm_0707_256_512_256.jsonl | 256 | 512 | 256 | +| vllm_0707_128_512_2000.jsonl | 128 | 512 | 2000 | +| vllm_0707_256_512_2000.jsonl | 256 | 512 | 2000 | +| vllm_0707_512_512_2000.jsonl | 512 | 512 | 2000 | +| vllm_0707_32_1000_256.jsonl | 32 | 1000 | 256 | +| vllm_0707_128_1000_256.jsonl | 128 | 1000 | 256 | +| vllm_0707_256_1000_256.jsonl | 256 | 1000 | 256 | +| vllm_0707_512_1000_256.jsonl | 512 | 1000 | 256 | +| vllm_0707_768_1000_256.jsonl | 768 | 1000 | 256 | +| vllm_0707_1024_1000_256.jsonl | 1024 | 1000 | 256 | +| vllm_0707_32_1000_1000.jsonl | 32 | 1000 | 1000 | +| vllm_0707_128_1000_1000.jsonl | 128 | 1000 | 1000 | +| vllm_0707_256_1000_1000.jsonl | 256 | 1000 | 1000 | +| vllm_0707_512_1000_1000.jsonl | 512 | 1000 | 1000 | +| vllm_0707_1024_1000_1000.jsonl | 1024 | 1000 | 1000 | +| vllm_0707_32_4000_512.jsonl | 32 | 4000 | 512 | +| vllm_0707_128_4000_512.jsonl | 128 | 4000 | 512 | +| vllm_0707_256_4000_512.jsonl | 256 | 4000 | 512 | +| vllm_0707_512_4000_512.jsonl | 512 | 4000 | 512 | +| vllm_0707_32_8000_1000.jsonl | 32 | 8000 | 1000 | +| vllm_0707_128_8000_1000.jsonl | 128 | 8000 | 1000 | +| vllm_0707_256_8000_1000.jsonl | 256 | 8000 | 1000 | +| vllm_0707_512_8000_1000.jsonl | 512 | 8000 | 1000 | +| vllm_0707_32_16000_512.jsonl | 32 | 16000 | 512 | +| vllm_0707_128_16000_512.jsonl | 128 | 16000 | 512 | +| vllm_0707_256_16000_512.jsonl | 256 | 16000 | 512 | +| vllm_0707_32_32000_512.jsonl | 32 | 32000 | 512 | +| vllm_0707_128_32000_512.jsonl | 128 | 32000 | 512 | + +## Provenance + +These files are **legacy raw outputs** from an ad-hoc SGLang vs vLLM backend comparison sweep run on 2026-07-07. The exact generator command/script that produced them was not preserved in the repository. They were generated using `sglang.bench_serving` with `--output-file` and `--output-details`, against: + +- Model: `/data/models/DeepSeek-V4-Flash` +- SGLang port: `30000` +- vLLM port: `8000` (inferred from file content) +- Dataset: `random` + +For future reproducible comparisons, use the script: + +``` +scripts/benchmark_dsv4_backend_comparison.sh +``` + +## Related Reports + +- `/data/user1/yy/dsv4_inference_comparison_report.md` — earlier DSV4 inference comparison report. +- `/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/` — earlier comparison run outputs (`.json`/`.log`). diff --git a/bench_results/dsv4_comparison_20260705_152221/summary.json b/bench_results/dsv4_comparison_20260705_152221/summary.json new file mode 100644 index 0000000..98e8e23 --- /dev/null +++ b/bench_results/dsv4_comparison_20260705_152221/summary.json @@ -0,0 +1,197 @@ +[ + { + "service": "vllm-dspark-dspark-st5", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 5, + "concurrency": 1, + "request_throughput": 1.0665689780673602, + "output_throughput": 257.1817776813825, + "total_input_tokens": 47865, + "total_output_tokens": 48226, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st5_c1.json" + }, + { + "service": "vllm-dspark-dspark-st5", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 5, + "concurrency": 16, + "request_throughput": 7.702621472216438, + "output_throughput": 1862.3398195524906, + "total_input_tokens": 47865, + "total_output_tokens": 48356, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st5_c16.json" + }, + { + "service": "vllm-dspark-dspark-st5", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 5, + "concurrency": 64, + "request_throughput": 9.360404619968996, + "output_throughput": 2210.4595510056783, + "total_input_tokens": 47865, + "total_output_tokens": 47230, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st5_c64.json" + }, + { + "service": "vllm-dspark-nospec", + "engine": "vllm-dspark", + "spec_method": null, + "spec_tokens": null, + "concurrency": 1, + "request_throughput": 0.5918258062472803, + "output_throughput": 144.62151314361665, + "total_input_tokens": 47865, + "total_output_tokens": 48873, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-nospec_c1.json" + }, + { + "service": "vllm-dspark-nospec", + "engine": "vllm-dspark", + "spec_method": null, + "spec_tokens": null, + "concurrency": 16, + "request_throughput": 5.61467992292336, + "output_throughput": 1364.0022670753865, + "total_input_tokens": 47865, + "total_output_tokens": 48587, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-nospec_c16.json" + }, + { + "service": "vllm-dspark-nospec", + "engine": "vllm-dspark", + "spec_method": null, + "spec_tokens": null, + "concurrency": 64, + "request_throughput": 7.339983457334795, + "output_throughput": 1803.9844342264591, + "total_input_tokens": 47865, + "total_output_tokens": 49155, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-nospec_c64.json" + }, + { + "service": "vllm-dspark-dspark-st3", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 3, + "concurrency": 1, + "request_throughput": 1.033805786989743, + "output_throughput": 254.38859000456605, + "total_input_tokens": 47865, + "total_output_tokens": 49214, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st3_c1.json" + }, + { + "service": "vllm-dspark-dspark-st3", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 3, + "concurrency": 16, + "request_throughput": 8.237095278328889, + "output_throughput": 1992.1826785402332, + "total_input_tokens": 47865, + "total_output_tokens": 48371, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st3_c16.json" + }, + { + "service": "vllm-dspark-dspark-st3", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 3, + "concurrency": 64, + "request_throughput": 14.530514004704537, + "output_throughput": 3507.8113858757224, + "total_input_tokens": 47865, + "total_output_tokens": 48282, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st3_c64.json" + }, + { + "service": "vllm-dspark-dspark-st7", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 7, + "concurrency": 1, + "request_throughput": 1.0285012524448527, + "output_throughput": 251.58169136053544, + "total_input_tokens": 47865, + "total_output_tokens": 48922, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st7_c1.json" + }, + { + "service": "vllm-dspark-dspark-st7", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 7, + "concurrency": 16, + "request_throughput": 7.463416900080192, + "output_throughput": 1772.3002941775428, + "total_input_tokens": 47865, + "total_output_tokens": 47493, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st7_c16.json" + }, + { + "service": "vllm-dspark-dspark-st7", + "engine": "vllm-dspark", + "spec_method": "dspark", + "spec_tokens": 7, + "concurrency": 64, + "request_throughput": 9.128647309323574, + "output_throughput": 2218.5807988214547, + "total_input_tokens": 47865, + "total_output_tokens": 48607, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st7_c64.json" + }, + { + "service": "vllm-main-nospec", + "engine": "vllm", + "spec_method": null, + "spec_tokens": null, + "concurrency": 1, + "request_throughput": 0.5806562443589017, + "output_throughput": 138.2571550630763, + "total_input_tokens": 47865, + "total_output_tokens": 47621, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-main-nospec_c1.json" + }, + { + "service": "vllm-main-nospec", + "engine": "vllm", + "spec_method": null, + "spec_tokens": null, + "concurrency": 16, + "request_throughput": 5.052303923305438, + "output_throughput": 1180.976042072646, + "total_input_tokens": 47865, + "total_output_tokens": 46750, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-main-nospec_c16.json" + }, + { + "service": "vllm-main-nospec", + "engine": "vllm", + "spec_method": null, + "spec_tokens": null, + "concurrency": 64, + "request_throughput": 6.851644652089382, + "output_throughput": 1641.8938662034388, + "total_input_tokens": 47865, + "total_output_tokens": 47927, + "duration_s": null, + "result_file": "/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-main-nospec_c64.json" + } +] \ No newline at end of file diff --git a/bench_results/eagle_grid/dspark_vs_eagle_report.md b/bench_results/eagle_grid/dspark_vs_eagle_report.md new file mode 100644 index 0000000..c4097e2 --- /dev/null +++ b/bench_results/eagle_grid/dspark_vs_eagle_report.md @@ -0,0 +1,96 @@ +# DSpark vs SGLang EAGLE 投机解码对比报告 + +- DSpark 结果:`/data/user1/yy/bench_results/dspark_st_comparison_20260707-150649` +- EAGLE 结果:`/data/user1/yy/bench_results/eagle_grid/focused` +- DSpark 模型:`/data/models/DeepSeek-V4-Flash-DSpark` +- EAGLE 模型:`/data/models/DeepSeek-V4-Flash` +- DSpark 后端:vllm-dspark (TP=8, FP8 KV cache, spec-method=dspark) +- EAGLE 后端:SGLang (TP=8, FP8 KV cache, speculative-algorithm=EAGLE, num_steps=3, topk=1, draft_tokens=4) +- 压测客户端:`sglang.bench_serving` +- Warmup:100 条 + +## 核心指标对比 + +| Scenario | Concurrency | Method | Total tok/s | Out tok/s | Req/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P99 TPOT(ms) | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| chat_short | 1 | DSpark(st=5) | 1094.51 | 397.37 | 2.73 | 365.29 | 743.89 | 782.98 | 64.42 | 174.36 | 2.06 | 3.40 | +| chat_short | 1 | EAGLE | 272.82 | 99.05 | 0.68 | 1467.67 | 3775.96 | 5175.09 | 223.86 | 405.20 | 7.47 | 19.74 | +| chat_short | 8 | DSpark(st=3) | 4705.85 | 1525.06 | 11.55 | 672.66 | 1269.82 | 1552.65 | 76.20 | 221.18 | 4.67 | 8.43 | +| chat_short | 8 | EAGLE | 1059.99 | 343.52 | 2.60 | 3043.16 | 7143.57 | 8345.08 | 245.91 | 528.86 | 21.41 | 36.02 | +| chat_short | 16 | DSpark(st=5) | 6282.94 | 2171.58 | 15.59 | 997.37 | 2147.86 | 2496.56 | 107.56 | 240.46 | 7.09 | 21.93 | +| chat_short | 16 | EAGLE | 1506.42 | 520.67 | 3.74 | 4240.11 | 8031.57 | 10192.48 | 236.19 | 438.56 | 29.12 | 54.02 | +| chat_short | 32 | DSpark(st=3) | 8275.55 | 2754.96 | 20.79 | 1497.40 | 2944.48 | 3658.90 | 144.47 | 453.29 | 10.72 | 23.97 | +| chat_short | 32 | EAGLE | 2461.23 | 819.35 | 6.18 | 4731.11 | 9926.81 | 11388.85 | 243.84 | 571.89 | 34.22 | 54.92 | +| chat_short | 64 | DSpark(st=5) | 9586.66 | 3191.43 | 24.09 | 2555.12 | 5387.59 | 6559.69 | 274.71 | 610.40 | 18.47 | 42.08 | +| chat_short | 64 | EAGLE | 4474.88 | 1489.70 | 11.24 | 5239.80 | 10296.02 | 11922.06 | 268.27 | 452.80 | 38.17 | 61.24 | +| chat_standard | 64 | DSpark(st=3) | 15680.46 | 3288.58 | 24.70 | 2483.46 | 4798.63 | 5693.88 | 271.41 | 947.15 | 17.93 | 46.05 | +| chat_standard | 64 | EAGLE | 6746.24 | 1414.85 | 10.63 | 5637.63 | 10693.92 | 12907.15 | 296.86 | 597.18 | 41.12 | 69.08 | +| decode_heavy | 1 | DSpark(st=5) | 673.37 | 531.76 | 0.55 | 1802.00 | 2957.04 | 3676.66 | 59.06 | 114.78 | 1.92 | 3.03 | +| decode_heavy | 1 | EAGLE | 261.35 | 206.39 | 0.22 | 4643.62 | 10452.02 | 18810.32 | 162.91 | 314.71 | 4.84 | 11.69 | +| decode_heavy | 32 | DSpark(st=5) | 7203.28 | 5737.73 | 5.52 | 5595.02 | 10412.11 | 12522.00 | 107.12 | 331.08 | 5.43 | 9.15 | +| decode_heavy | 32 | EAGLE | 2981.50 | 2374.90 | 2.28 | 13707.03 | 27159.67 | 30638.81 | 192.78 | 442.78 | 13.23 | 19.26 | +| generation_standard | 64 | DSpark(st=3) | 13363.77 | 6705.88 | 13.27 | 4542.22 | 8273.38 | 9355.10 | 190.34 | 813.58 | 9.00 | 15.36 | +| generation_standard | 64 | EAGLE | 5609.33 | 2814.73 | 5.57 | 11057.53 | 22577.31 | 26713.41 | 275.48 | 944.52 | 21.88 | 34.80 | +| long_context_probe | 1 | DSpark(st=5) | 11835.70 | 392.31 | 1.44 | 693.30 | 1172.84 | 1404.17 | 267.61 | 561.65 | 1.53 | 1.88 | +| long_context_probe | 1 | EAGLE | 5556.95 | 184.19 | 0.68 | 1477.16 | 2792.68 | 3836.17 | 238.26 | 618.74 | 4.43 | 8.62 | +| long_context_probe | 4 | DSpark(st=5) | 39893.35 | 1322.32 | 4.85 | 806.55 | 1390.87 | 1500.22 | 143.42 | 236.41 | 2.41 | 3.54 | +| long_context_probe | 4 | EAGLE | 6660.56 | 220.77 | 0.81 | 4777.51 | 9442.45 | 10071.75 | 433.60 | 1230.20 | 14.85 | 21.65 | +| long_context_probe | 8 | DSpark(st=5) | 29361.32 | 900.89 | 3.56 | 2223.29 | 4693.10 | 5345.72 | 425.03 | 1241.57 | 7.08 | 19.83 | +| long_context_probe | 8 | EAGLE | 9890.47 | 303.47 | 1.20 | 6232.13 | 16324.27 | 20329.21 | 606.76 | 2671.06 | 22.79 | 54.38 | +| rag_medium | 1 | DSpark(st=5) | 4075.47 | 496.09 | 1.76 | 566.58 | 968.90 | 1134.15 | 110.34 | 156.35 | 1.64 | 2.34 | +| rag_medium | 1 | EAGLE | 1326.02 | 161.41 | 0.57 | 1742.43 | 3804.17 | 3936.47 | 195.07 | 337.55 | 5.83 | 12.84 | +| rag_medium | 8 | DSpark(st=5) | 15838.39 | 1692.13 | 6.69 | 1175.26 | 2263.59 | 3106.42 | 154.24 | 352.19 | 4.14 | 8.36 | +| rag_medium | 8 | EAGLE | 3676.32 | 392.77 | 1.55 | 4908.89 | 11659.24 | 12566.81 | 254.65 | 678.44 | 18.53 | 35.36 | +| rag_medium | 32 | DSpark(st=5) | 27420.79 | 3062.92 | 11.91 | 2626.21 | 5182.78 | 6873.72 | 253.09 | 522.80 | 9.17 | 18.55 | +| rag_medium | 32 | EAGLE | 13268.07 | 1482.05 | 5.76 | 5444.98 | 10471.13 | 11990.37 | 337.62 | 2204.45 | 20.09 | 39.98 | +| stress_standard | 64 | DSpark(st=3) | 15225.66 | 3193.19 | 23.99 | 2569.66 | 5006.16 | 5831.81 | 261.98 | 710.57 | 18.02 | 36.01 | +| stress_standard | 64 | EAGLE | 5399.35 | 1132.38 | 8.51 | 6678.22 | 13086.21 | 14811.42 | 314.38 | 584.25 | 48.19 | 81.06 | +| stress_standard | 96 | DSpark(st=3) | 24069.73 | 4756.72 | 37.72 | 2433.73 | 4827.07 | 6001.29 | 272.61 | 749.99 | 18.04 | 38.52 | +| stress_standard | 96 | EAGLE | 8833.26 | 1745.65 | 13.84 | 6792.34 | 14000.22 | 16722.55 | 360.14 | 959.71 | 52.90 | 96.70 | +| stress_standard | 128 | DSpark(st=3) | 29828.36 | 5942.48 | 47.45 | 2581.42 | 5162.16 | 6222.01 | 298.69 | 970.61 | 19.09 | 40.93 | +| stress_standard | 128 | EAGLE | 13189.80 | 2627.70 | 20.98 | 5960.86 | 12256.06 | 14219.75 | 416.12 | 2153.71 | 47.20 | 81.35 | + +## 吞吐 Winner 统计(按 Total tok/s) + +| Scenario | Concurrency | Winner | DSpark Total tok/s | EAGLE Total tok/s | 提升 | +|---|---:|---:|---:|---:|---:| +| chat_short | 1 | DSpark(st=5) | 1094.51 | 272.82 | 301.18% | +| chat_short | 8 | DSpark(st=3) | 4705.85 | 1059.99 | 343.95% | +| chat_short | 16 | DSpark(st=5) | 6282.94 | 1506.42 | 317.08% | +| chat_short | 32 | DSpark(st=3) | 8275.55 | 2461.23 | 236.24% | +| chat_short | 64 | DSpark(st=5) | 9586.66 | 4474.88 | 114.23% | +| chat_standard | 64 | DSpark(st=3) | 15680.46 | 6746.24 | 132.43% | +| decode_heavy | 1 | DSpark(st=5) | 673.37 | 261.35 | 157.65% | +| decode_heavy | 32 | DSpark(st=5) | 7203.28 | 2981.50 | 141.60% | +| generation_standard | 64 | DSpark(st=3) | 13363.77 | 5609.33 | 138.24% | +| long_context_probe | 1 | DSpark(st=5) | 11835.70 | 5556.95 | 112.99% | +| long_context_probe | 4 | DSpark(st=5) | 39893.35 | 6660.56 | 498.95% | +| long_context_probe | 8 | DSpark(st=5) | 29361.32 | 9890.47 | 196.86% | +| rag_medium | 1 | DSpark(st=5) | 4075.47 | 1326.02 | 207.35% | +| rag_medium | 8 | DSpark(st=5) | 15838.39 | 3676.32 | 330.82% | +| rag_medium | 32 | DSpark(st=5) | 27420.79 | 13268.07 | 106.67% | +| stress_standard | 64 | DSpark(st=3) | 15225.66 | 5399.35 | 181.99% | +| stress_standard | 96 | DSpark(st=3) | 24069.73 | 8833.26 | 172.49% | +| stress_standard | 128 | DSpark(st=3) | 29828.36 | 13189.80 | 126.15% | + +## 关键发现 + +- 对比点数:18 +- EAGLE 胜出:0 个 +- DSpark 胜出:18 个 + +**结论:在相同负载下,vllm-dspark 的综合吞吐优于 SGLang EAGLE。** + +### 延迟与稳定性观察 + +- 平均 Mean E2E 比值(EAGLE / DSpark):3.09 +- 平均 Mean TPOT 比值(EAGLE / DSpark):3.21 +- 平均 Mean TTFT 比值(EAGLE / DSpark):1.81 + +> 比值 < 1 表示 EAGLE 更快;> 1 表示 DSpark 更快。 + +## 优化建议 + +1. 如果以吞吐为首要目标,当前配置下 **vllm-dspark 更优**,建议继续使用 `--spec-tokens` 并根据并发选择 3(高并发)或 5(低并发/长上下文)。 +2. SGLang EAGLE 本次表现不佳,平均延迟和 TPOT 均显著高于 DSpark;如需进一步评估 EAGLE,可尝试调整 `--speculative-num-steps`、`--speculative-eagle-topk`、`--speculative-num-draft-tokens` 或更换 MOE runner backend,并确认是否已完成 `sglang.compile_deep_gemm` 预编译。 +3. 注意两套后端的实现差异(CUDA graph、KV cache 管理、调度器、draft 模型架构)会显著影响不同并发和输入长度下的表现,建议按实际业务负载做最终选型。 diff --git a/bench_results/eagle_grid/report.md b/bench_results/eagle_grid/report.md new file mode 100644 index 0000000..c5ab5d7 --- /dev/null +++ b/bench_results/eagle_grid/report.md @@ -0,0 +1,6 @@ +# vllm-dspark Benchmark Grid Report + +- Result root: `/data/user1/yy/bench_results/eagle_grid` +- Model: `/data/models/DeepSeek-V4-Flash-DSpark` +- Backend: vllm-dspark (TP=8, FP8 KV cache, spec-method=dspark, spec-tokens=5) +- Benchmark client: `sglang.bench_serving --backend vllm` diff --git a/bench_results/sglang_8card_max_throughput_20260705_030839/summary.json b/bench_results/sglang_8card_max_throughput_20260705_030839/summary.json new file mode 100644 index 0000000..001f511 --- /dev/null +++ b/bench_results/sglang_8card_max_throughput_20260705_030839/summary.json @@ -0,0 +1,58 @@ +[ + { + "name": "sharegpt_concurrency_128", + "args": "--max-concurrency 128", + "duration_s": 155.26, + "request_throughput": 12.88, + "input_token_throughput": 3710.5, + "output_token_throughput": 2690.48, + "total_token_throughput": 6400.98, + "mean_ttft_ms": 410.55, + "mean_tpot_ms": 48.45, + "mean_e2e_latency_ms": 9696.87, + "accept_length": 2.6, + "output_file": "bench_results/sglang_8card_max_throughput_20260705_030839/sharegpt_concurrency_128.json" + }, + { + "name": "sharegpt_concurrency_256", + "args": "--max-concurrency 256", + "duration_s": 90.65, + "request_throughput": 22.06, + "input_token_throughput": 6355.48, + "output_token_throughput": 4608.35, + "total_token_throughput": 10963.84, + "mean_ttft_ms": 509.73, + "mean_tpot_ms": 62.71, + "mean_e2e_latency_ms": 10869.76, + "accept_length": 2.6, + "output_file": "bench_results/sglang_8card_max_throughput_20260705_030839/sharegpt_concurrency_256.json" + }, + { + "name": "sharegpt_concurrency_512", + "args": "--max-concurrency 512", + "duration_s": 71.39, + "request_throughput": 28.02, + "input_token_throughput": 8070.04, + "output_token_throughput": 5851.58, + "total_token_throughput": 13921.61, + "mean_ttft_ms": 1343.73, + "mean_tpot_ms": 121.2, + "mean_e2e_latency_ms": 16176.14, + "accept_length": 2.6, + "output_file": "bench_results/sglang_8card_max_throughput_20260705_030839/sharegpt_concurrency_512.json" + }, + { + "name": "sharegpt_concurrency_1024", + "args": "--max-concurrency 1024", + "duration_s": 68.21, + "request_throughput": 29.32, + "input_token_throughput": 8445.53, + "output_token_throughput": 6123.84, + "total_token_throughput": 14569.37, + "mean_ttft_ms": 13265.46, + "mean_tpot_ms": 105.36, + "mean_e2e_latency_ms": 27566.99, + "accept_length": 2.6, + "output_file": "bench_results/sglang_8card_max_throughput_20260705_030839/sharegpt_concurrency_1024.json" + } +] \ No newline at end of file diff --git a/bench_results/sglang_8card_systematic_20260704_120819/length_distribution.png b/bench_results/sglang_8card_systematic_20260704_120819/length_distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..a511adb8662d66ba2acbb8a6c81abc4970389b23 GIT binary patch literal 150830 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

tFB|cUJQ0AQU{oQdgRHe$R2}@3~;>JpQS2 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


`h+I`zFNr$BwJ4X-{;Q_yZyMnY7O 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
Eo-ys4z1uBC^D0k3o zg3-}Yf^zH7O-c29!s+nc&1SO5E3nVnK^X_+K_E0WbkjfKRN^KZ`5`K?6xI-513oGrXvT;1aS({;$iTr< z&p6E-M5IOcMV0?{RF={s#`2BHuli63yRVYEf&lABY%yK?3gMnibfkKuzgkNwKZXh) zM2bu_{KA{FYja_s_!&5ncG7k&SrcOx&hm(SE3;6}r0#Os6tq)~FTEfmhht65b?oKl zuC5J{6>IT7Pz4Y$(p)CLX6spAh}!Tfz7}YqPy%!2&rkhaDepQ=3!OBY0XO3p&3vj0 z3-{tCD6~T-AXrV>p9bOSEKO_%mcIwFp1%#_Pp`u}mjwd8t1WkHmSm|C3$n*8=MF4; z{yl1kN7QqVbB*n9|3p@&3cruA1GxUDA1=4-ruorRolC*yw!Zga=_BwT;@c{KjUkmN z!c-eFwGhVwcIzw`=y1sZ(?Ypw5!4UKZYo;4y57N_AeOqCvsKPaxu9JRLS>JwvMdg!{TfKc@ai%2^ z!7o|Sg~ImAMH!m8EmeLuZyHaO)>SNeG4u@l&-Az3h8q$*VdS_J*QZ!zbj&#%Ym>l> zAVM83jSvpo_+9={eRhYd7R7AUbx7)%*Ms}HvSRk zT-l5y-4^~BzPh^S2RIl%t+4w_rLcIt0{N!-sb&1a!o@Aah`CUI+z;*mcD?OUdGIZ0 zoC(eUCJ321zZbr7uOD*y_Ahd~56j=%<5OG{QurV$5P3pw$4ed_p5TtE0+~TptuerE2(_9)Ocu=|U0_^f!xr>_bx>c)kGBe?Y$eiKd z3l=Q6Au{4yWsdF_Mk&hU*U|805nF~r52KWjl`ES5h*MH|0vdW)5-x&X2(C>;lou06 zCT7r`xK{AvYZ-Llck($SF@l{T?i6#+VZ~rOcLO<0ZJn9YqUHF!(u2q^^1{q-hpQ&It;2R_{IGIpDFI+FMvry zaC3v7I&Pd$>7bB8JJUsz!d5iYvxu&sf;*<_N?{MP=Ce^Mf$YI#YK{~2GvZtA5T>o# zId9ATiSch)QUfxC<3d=KYF~#Y;7dTN?;tg1QOFBZMTIc*3#q|s62ysIu+B|680Dsic%W44mP^EgrD0QO(xUi? z@&zncs!Lr|Uoybkw{Dr8uV`VZctSIO5I*BiIoc&rRr-u*EU7{8YJ*iX>b-D6_xKqI zU#sv`vGgY3%JAO9`Z39MuOEnak53!QK0UIYrtl6RTrx$hXws0Gfq4A1t~~G=iuDmv z(uKI@&%Z~J80Om3+-+2#0WU-Z1_sL2CDkHQcniRkiaJ2~+c6XP4S2Lv*2AaLmPNTL z7_2)-wb=O!7ls4yBfEoY(_0emZ7W@kQsX{w%|dmYyEX1q#zqxwK($s zOw^x2oAC2OI^sM6#>ZU7?y#+{T33QZ_0I7XP+=nYC5ox_xKR(+w8InqU`m4o%UMHt ztJYw&>Ya09C?ldN1HE*fVkinYw9Di9m)1`pb_Xv3r&Q)?&$}l%KhK-zzU<1p`sG_;3KIrsy1(r(C9J#tT2 zEoo|M(wD=EfKKst53k+gKD5uIrgQqPmXs|2nVnsn{H}N?qTa=MKD;I0Hd~tje-bXa z4AB!WB3d|WEtNDn9xU%(ueZ;>b%GEfYVo}zC_XRa6!Yme!yHrKVTE=119i&D;+?APoFw3K?Fa+2TjUciu#G(EG zjwa}?j1g%Ql+_0igqr}Oqgb`@D_WUJBDEX^J;LVjY?q*u3aGyvxlMpRYDJMG28~;d zSLy3{1!pE{Ub`7beNfH*!8HAO9O(PKY zJOW>Xot-_k4SGZu!X`yDU{{KfnJB>Us>7HNu`@nrJ0F=+08(v17MV{}Y9_t5%cVsk zF;KGxFY4wpcJ@19ha9HY=Wscyy37N0UZQvfc3G+au;$E50?Ddr9cEN#sJG{ z8Ykh9v`cYtke`+V&?fbp_$dX=<;RgwV%+!m102ml^axGi$}DUFCwr5u9NRX&14 已超出稳定运行区间。 +5. **RPS=8 是稳定高吞吐边界**:total tok/s = 3875,E2E 延迟 4.54s,TTFT 仅 217ms,体验与吞吐兼顾。 +6. **EAGLE 投机采样稳定生效**:Accept Length 稳定在 2.60–2.62,有效降低了 decode 步数。 + +## 场景建议 + +| 场景 | 推荐配置 | 理由 | +|------|----------|------| +| 低延迟在线 API | `--max-concurrency 8` | TTFT 147ms,TPOT 8.4ms,E2E 1.79s | +| 平衡型在线服务 | `--max-concurrency 16` 或 `--request-rate 4` | 吞吐约 3k tok/s,E2E 2.6–1.7s | +| 高吞吐在线服务 | `--max-concurrency 32` 或 `--request-rate 8` | 吞吐 3.7–3.9k tok/s,E2E 4.3–4.5s | +| 最大吞吐/离线批处理 | `--max-concurrency 128` | 吞吐 7.1k tok/s,E2E 8.6s | +| 避免使用 | `--request-rate 16` | 系统过载,E2E 15s,TPOT 87ms | + +## 原始数据 + +- 汇总 JSON:`bench_results/sglang_8card_systematic_20260704_120819/summary.json` +- 每个测试点的详细结果与日志:`bench_results/sglang_8card_systematic_20260704_120819/` + +## 附录 A:Benchmark 参数说明 + +本次所有测试均使用 `sglang.bench_serving` 工具,参数统一如下: + +```bash +envs/sglang/bin/python -m sglang.bench_serving \ + --backend sglang \ + --host 127.0.0.1 \ + --port 30000 \ + --dataset-name sharegpt \ + --dataset-path /data/user1/yy/datasets/ShareGPT_V4.3_unfiltered_cleaned_split.json \ + --num-prompts 2000 \ + --model /data/models/DeepSeek-V4-Flash \ + --seed 42 +``` + +两个测试维度的区别仅在于最后一条参数: + +- **并发扫描**:`--max-concurrency {1,8,16,32,64,128}` + - 客户端同时保持 N 个未完成的请求,服务器队列按需堆积。 + - 适合模拟“系统能扛多少并发”的容量测试。 + +- **请求速率扫描**:`--request-rate {1,2,4,8,16}` + - 客户端按泊松分布以固定 RPS 发送请求。 + - 适合模拟真实线上按流量到达的场景。 + +### 指标定义 + +| 指标 | 含义 | +|------|------| +| **req/s** | 每秒完成的请求数 | +| **input tok/s** | 每秒处理的输入 token 数 | +| **output tok/s** | 每秒生成的输出 token 数 | +| **total tok/s** | input tok/s + output tok/s | +| **TTFT** | Time To First Token,首 token 延迟 | +| **TPOT** | Time Per Output Token,除首 token 外平均每个输出 token 的间隔 | +| **E2E Latency** | 端到端请求完成时间 | +| **Accept Length** | EAGLE 投机采样平均每次接受的 draft token 数 | + +--- + +## 附录 B:Token 成本计算 + +### 假设 + +- GPU 单价:**8.0 元 / 卡 / 小时** +- 本次部署使用 **8× NVIDIA H200** +- 整机每小时成本: + +``` +8 卡 × 8.0 元/卡/h = 64 元/h +``` + +### 公式 + +对于任意一个 benchmark 结果: + +``` +每小时处理 input tokens = input_tok/s × 3600 +每小时生成 output tokens = output_tok/s × 3600 + +每 1M input tokens 成本 = 64 / (input_tok/s × 3600 / 1,000,000) +每 1M output tokens 成本 = 64 / (output_tok/s × 3600 / 1,000,000) +``` + +### 各场景成本 + +| 场景 | input tok/s | output tok/s | 1M input cost | 1M output cost | +|------|-------------|--------------|---------------|----------------| +| c=1 | 335.83 | 243.51 | 52.94 元 | 73.01 元 | +| c=8 | 1,283.33 | 930.54 | 13.85 元 | 19.10 元 | +| c=16 | 1,736.31 | 1,259.00 | 10.24 元 | 14.12 元 | +| c=32 | 2,151.87 | 1,560.32 | 8.26 元 | 11.39 元 | +| c=64 | 2,775.39 | 2,012.43 | 6.41 元 | 8.83 元 | +| c=128 | 4,132.36 | 2,996.37 | 4.30 元 | 5.93 元 | +| rps=1 | 286.41 | 207.67 | 62.07 元 | 85.61 元 | +| rps=2 | 571.97 | 414.74 | 31.08 元 | 42.86 元 | +| rps=4 | 1,139.69 | 826.39 | 15.60 元 | 21.51 元 | +| rps=8 | 2,246.34 | 1,628.82 | 7.91 元 | 10.91 元 | +| rps=16 | 4,176.94 | 3,028.70 | 4.26 元 | 5.87 元 | + +### 对外定价建议 + +裸 GPU 成本不等于对外售价。建议根据 SLA 和毛利率加价: + +| 推荐场景 | 裸成本(input/output) | 说明 | +|----------|------------------------|------| +| 低延迟在线(c=8) | 13.85 元 / 19.10 元 per 1M | TTFT 147ms,TPOT 8.4ms,适合实时聊天 | +| 平衡型在线(c=16) | 10.24 元 / 14.12 元 per 1M | 吞吐与延迟兼顾,主流 API 可参考此档位 | +| 高吞吐在线(c=32 / rps=8) | 8.09 元 / 11.15 元 per 1M | 吞吐高、延迟可接受,适合批量在线服务 | +| 最大吞吐/离线(c=128) | 4.30 元 / 5.93 元 per 1M | 延迟 8.6s,仅适合离线批处理 | + +> 注意:`rps=16` 虽已接近最大吞吐,但系统已明显过载(E2E 15s),不建议按此成本对外定价。 + +--- + +## 附录 C:最大吞吐压测(优化服务参数) + +为了探索成本下限,重新部署了服务并调优了参数: + +```bash +envs/sglang/bin/sglang serve \ + --trust-remote-code \ + --model-path /data/models/DeepSeek-V4-Flash \ + --tp 8 \ + --moe-runner-backend marlin \ + --speculative-algorithm EAGLE \ + --speculative-num-steps 3 \ + --speculative-eagle-topk 1 \ + --speculative-num-draft-tokens 4 \ + --max-running-requests 512 \ + --chunked-prefill-size 16384 \ + --host 0.0.0.0 \ + --port 30000 +``` + +与附录 A/B 的测试相比,主要变化: +- `--max-running-requests` 从默认 256 提升到 **512** +- `--chunked-prefill-size` 从默认 8192 提升到 **16384** + +### 超大并发扫描结果 + +| 并发 | 耗时(s) | req/s | input tok/s | output tok/s | total tok/s | Mean TTFT | Mean TPOT | Mean E2E(ms) | +|------|---------|-------|-------------|--------------|-------------|-----------|-----------|--------------| +| 128 | 155.26 | 12.88 | 3,710.50 | 2,690.48 | 6,400.98 | 410.55 ms | 48.45 ms | 9,696.87 | +| 256 | 90.65 | 22.06 | 6,355.48 | 4,608.35 | 10,963.84 | 509.73 ms | 62.71 ms | 10,869.76 | +| 512 | 71.39 | 28.02 | 8,070.04 | 5,851.58 | 13,921.61 | 1,343.73 ms| 121.20 ms | 16,176.14 | +| **1024** | **68.21** | **29.32** | **8,445.53** | **6,123.84** | **14,569.37** | **13,265.46 ms** | **105.36 ms** | **27,566.99** | + +### 关键发现 + +1. **高并发下吞吐显著提升**:优化参数后,c=256 及以上远超原配置的最高值(7,129 tok/s);c=1024 达到 **14,569 tok/s**,约为原配置峰值的两倍。 +2. **c=1024 达到最高吞吐**:total **14,569 tok/s**,output **6,124 tok/s**,但 TTFT 13.3s,系统已严重过载。 +3. **c=512 是实际可用上限**:total **13,922 tok/s**,output **5,852 tok/s**,虽然 TTFT 1.3s、E2E 16s,但吞吐接近峰值,适合离线批处理。 +4. **c=256 是高压在线的甜点**:total **10,964 tok/s**,TTFT 510ms,E2E 10.9s,如果业务能容忍 10 秒级响应,这是性价比很高的点。 + +--- + +## 附录 D:最低 Token 成本(基于最大吞吐) + +仍按 **8.0 元/卡时、8 卡整机 64 元/小时** 计算。 + +### 公式 + +``` +每 1M input tokens 成本 = 64 / (input_tok/s × 3600 / 1,000,000) +每 1M output tokens 成本 = 64 / (output_tok/s × 3600 / 1,000,000) +``` + +### 最大吞吐场景成本 + +| 场景 | input tok/s | output tok/s | 1M input cost | 1M output cost | +|------|-------------|--------------|---------------|----------------| +| c=128 | 3,710.50 | 2,690.48 | 4.79 元 | 6.61 元 | +| c=256 | 6,355.48 | 4,608.35 | 2.80 元 | 3.86 元 | +| c=512 | 8,070.04 | 5,851.58 | 2.20 元 | 3.04 元 | +| **c=1024** | **8,445.53** | **6,123.84** | **2.10 元** | **2.90 元** | + +### 结论 + +在当前硬件和优化参数下: + +- **理论最低 input 成本**:约 **2.10 元 / 1M input tokens**(c=1024) +- **理论最低 output 成本**:约 **2.90 元 / 1M output tokens**(c=1024) +- **实际可用最低成本**:c=512 时 **2.20 元 / 1M input**、**3.04 元 / 1M output**,此时吞吐已达峰值 95% 以上,且延迟比 c=1024 可控得多。 + +> 注意:c=1024 的 TTFT 超过 13 秒,仅适合完全不在意延迟的离线批处理任务;对外 API 服务不建议按此成本定价。 + +--- + +## 附录 E:测试数据长度分布 + +本次所有 benchmark 使用的是 `ShareGPT_V4.3_unfiltered_cleaned_split.json` 数据集。`sglang.bench_serving` 在 `--num-prompts 2000` 且 `--seed 42` 的条件下,从数据集中采样出 **2000 条真实对话请求**作为测试负载。 + +### 长度统计 + +| 指标 | Input Tokens | Output Tokens | +|------|--------------|---------------| +| 请求数 | 2,000 | 2,000 | +| 平均值 | 288.05 | 208.86 | +| 标准差 | 420.07 | 217.60 | +| 最小值 | 2 | 2 | +| P50 | 125 | 150 | +| P90 | 723 | 516 | +| P95 | 841 | 667 | +| P99 | 2,190 | 811 | +| 最大值 | 3,996 | 1,655 | +| **总量** | **576,098** | **417,728** | + +### 分布特点 + +1. **输入长度呈现典型长尾分布**:P50 仅 125 tokens,但 P99 达到 2,190 tokens,最大值 3,996 tokens。说明 ShareGPT 中既有大量短 prompt,也有少量长上下文对话。 +2. **输出长度相对集中**:P50 150 tokens,P90 516 tokens,P99 811 tokens,最大值 1,655 tokens。大部分回复属于中等长度。 +3. **输出/输入比约为 0.73**:平均每条请求输出 209 tokens、输入 288 tokens,整体负载偏 decode 侧。 +4. **与原始 ShareGPT 全集的差异**:原始全集输出长度均值约 1,122 tokens,而 bench 采样后的输出均值仅 209 tokens。这是因为 `sglang.bench_serving` 在处理 sharegpt 数据集时会根据内置规则对输出做截断/采样,以更贴近典型在线 serving 负载。 + +### 长度分布图 + +![长度分布](length_distribution.png) + +> 图表文件:`bench_results/sglang_8card_systematic_20260704_120819/length_distribution.png` diff --git a/bench_results/sglang_8card_systematic_20260704_120819/summary.json b/bench_results/sglang_8card_systematic_20260704_120819/summary.json new file mode 100644 index 0000000..af4117a --- /dev/null +++ b/bench_results/sglang_8card_systematic_20260704_120819/summary.json @@ -0,0 +1,156 @@ +[ + { + "name": "sharegpt_concurrency_1", + "args": "--max-concurrency 1", + "duration_s": 1715.45, + "request_throughput": 1.17, + "input_token_throughput": 335.83, + "output_token_throughput": 243.51, + "total_token_throughput": 579.34, + "mean_ttft_ms": 128.09, + "mean_tpot_ms": 4.0, + "mean_e2e_latency_ms": 856.77, + "accept_length": 2.62, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_concurrency_1.json" + }, + { + "name": "sharegpt_concurrency_8", + "args": "--max-concurrency 8", + "duration_s": 448.91, + "request_throughput": 4.46, + "input_token_throughput": 1283.33, + "output_token_throughput": 930.54, + "total_token_throughput": 2213.87, + "mean_ttft_ms": 146.76, + "mean_tpot_ms": 8.41, + "mean_e2e_latency_ms": 1790.65, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_concurrency_8.json" + }, + { + "name": "sharegpt_concurrency_16", + "args": "--max-concurrency 16", + "duration_s": 331.79, + "request_throughput": 6.03, + "input_token_throughput": 1736.31, + "output_token_throughput": 1259.0, + "total_token_throughput": 2995.3, + "mean_ttft_ms": 168.95, + "mean_tpot_ms": 12.67, + "mean_e2e_latency_ms": 2642.1, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_concurrency_16.json" + }, + { + "name": "sharegpt_concurrency_32", + "args": "--max-concurrency 32", + "duration_s": 267.72, + "request_throughput": 7.47, + "input_token_throughput": 2151.87, + "output_token_throughput": 1560.32, + "total_token_throughput": 3712.19, + "mean_ttft_ms": 206.24, + "mean_tpot_ms": 20.72, + "mean_e2e_latency_ms": 4249.71, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_concurrency_32.json" + }, + { + "name": "sharegpt_concurrency_64", + "args": "--max-concurrency 64", + "duration_s": 207.57, + "request_throughput": 9.64, + "input_token_throughput": 2775.39, + "output_token_throughput": 2012.43, + "total_token_throughput": 4787.82, + "mean_ttft_ms": 248.41, + "mean_tpot_ms": 32.61, + "mean_e2e_latency_ms": 6557.39, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_concurrency_64.json" + }, + { + "name": "sharegpt_concurrency_128", + "args": "--max-concurrency 128", + "duration_s": 139.41, + "request_throughput": 14.35, + "input_token_throughput": 4132.36, + "output_token_throughput": 2996.37, + "total_token_throughput": 7128.73, + "mean_ttft_ms": 330.4, + "mean_tpot_ms": 46.15, + "mean_e2e_latency_ms": 8596.12, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_concurrency_128.json" + }, + { + "name": "sharegpt_rps_1", + "args": "--request-rate 1", + "duration_s": 2011.46, + "request_throughput": 0.99, + "input_token_throughput": 286.41, + "output_token_throughput": 207.67, + "total_token_throughput": 494.08, + "mean_ttft_ms": 146.87, + "mean_tpot_ms": 4.91, + "mean_e2e_latency_ms": 1007.62, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_rps_1.json" + }, + { + "name": "sharegpt_rps_2", + "args": "--request-rate 2", + "duration_s": 1007.21, + "request_throughput": 1.99, + "input_token_throughput": 571.97, + "output_token_throughput": 414.74, + "total_token_throughput": 986.71, + "mean_ttft_ms": 160.63, + "mean_tpot_ms": 5.99, + "mean_e2e_latency_ms": 1205.24, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_rps_2.json" + }, + { + "name": "sharegpt_rps_4", + "args": "--request-rate 4", + "duration_s": 505.49, + "request_throughput": 3.96, + "input_token_throughput": 1139.69, + "output_token_throughput": 826.39, + "total_token_throughput": 1966.08, + "mean_ttft_ms": 174.67, + "mean_tpot_ms": 9.02, + "mean_e2e_latency_ms": 1743.4, + "accept_length": 2.6, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_rps_4.json" + }, + { + "name": "sharegpt_rps_8", + "args": "--request-rate 8", + "duration_s": 256.46, + "request_throughput": 7.8, + "input_token_throughput": 2246.34, + "output_token_throughput": 1628.82, + "total_token_throughput": 3875.15, + "mean_ttft_ms": 216.65, + "mean_tpot_ms": 25.35, + "mean_e2e_latency_ms": 4542.64, + "accept_length": 2.6, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_rps_8.json" + }, + { + "name": "sharegpt_rps_16", + "args": "--request-rate 16", + "duration_s": 137.92, + "request_throughput": 14.5, + "input_token_throughput": 4176.94, + "output_token_throughput": 3028.7, + "total_token_throughput": 7205.64, + "mean_ttft_ms": 377.28, + "mean_tpot_ms": 87.16, + "mean_e2e_latency_ms": 15048.09, + "accept_length": 2.61, + "output_file": "bench_results/sglang_8card_systematic_20260704_120819/sharegpt_rps_16.json" + } +] \ No newline at end of file diff --git a/bench_results/vllm_dspark_qwen3_20260705_121046/summary.json b/bench_results/vllm_dspark_qwen3_20260705_121046/summary.json new file mode 100644 index 0000000..20dfa8b --- /dev/null +++ b/bench_results/vllm_dspark_qwen3_20260705_121046/summary.json @@ -0,0 +1,39 @@ +{ + "model": "/data/models/Qwen3-4B", + "draft_model": "deepseek-ai/dspark_qwen3_4b_block7", + "url": "http://127.0.0.1:30003/v1/completions", + "max_tokens": 64, + "num_prompts": 20, + "dataset": "/data/user1/yy/datasets/ShareGPT_filtered_chat.json", + "timestamp": "20260705_121046", + "results": [ + { + "concurrency": 1, + "num_prompts": 20, + "duration_s": 0.07108968612737954, + "success_count": 0, + "error": "all requests failed", + "errors": [ + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'" + ] + }, + { + "concurrency": 4, + "num_prompts": 20, + "duration_s": 0.036050053080543876, + "success_count": 0, + "error": "all requests failed", + "errors": [ + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'", + "404, message='Not Found', url='http://127.0.0.1:30003/v1/completions'" + ] + } + ] +} \ No newline at end of file diff --git a/bench_results/vllm_dspark_qwen3_20260705_121218/summary.json b/bench_results/vllm_dspark_qwen3_20260705_121218/summary.json new file mode 100644 index 0000000..c719bcc --- /dev/null +++ b/bench_results/vllm_dspark_qwen3_20260705_121218/summary.json @@ -0,0 +1,59 @@ +{ + "model": "/data/models/Qwen3-4B", + "draft_model": "deepseek-ai/dspark_qwen3_4b_block7", + "url": "http://127.0.0.1:30003/v1/completions", + "max_tokens": 64, + "num_prompts": 20, + "dataset": "/data/user1/yy/datasets/ShareGPT_filtered_chat.json", + "timestamp": "20260705_121218", + "results": [ + { + "concurrency": 1, + "num_prompts": 20, + "duration_s": 3.1493232590146363, + "success_count": 20, + "fail_count": 0, + "input_tokens": 6132, + "output_tokens": 456, + "total_tokens": 6588, + "request_throughput": 6.350570695704836, + "input_throughput": 1947.0849753031027, + "output_throughput": 144.79301186207027, + "total_throughput": 2091.877987165173, + "mean_ttft_ms": 55.51949484506622, + "p50_ttft_ms": 27.64087135437876, + "p99_ttft_ms": 295.08487760089326, + "mean_tpot_ms": 4.6249661131817925, + "p50_tpot_ms": 4.634099831345484, + "p99_tpot_ms": 4.827384204164935, + "mean_e2e_ms": 156.8067875574343, + "p50_e2e_ms": 148.02213886287063, + "p99_e2e_ms": 406.6163514624348, + "errors": [] + }, + { + "concurrency": 4, + "num_prompts": 20, + "duration_s": 1.0271000929642469, + "success_count": 20, + "fail_count": 0, + "input_tokens": 6132, + "output_tokens": 450, + "total_tokens": 6582, + "request_throughput": 19.472298889857267, + "input_throughput": 5970.206839630238, + "output_throughput": 438.1267250217885, + "total_throughput": 6408.333564652026, + "mean_ttft_ms": 82.42244796128944, + "p50_ttft_ms": 37.77908312622458, + "p99_ttft_ms": 263.8731326162815, + "mean_tpot_ms": 5.172366561559164, + "p50_tpot_ms": 5.150655882704692, + "p99_tpot_ms": 5.708390201389117, + "mean_e2e_ms": 193.68963547749445, + "p50_e2e_ms": 153.2427944475785, + "p99_e2e_ms": 387.3923780536279, + "errors": [] + } + ] +} \ No newline at end of file diff --git a/bench_results/vllm_dspark_qwen3_20260705_121256/summary.json b/bench_results/vllm_dspark_qwen3_20260705_121256/summary.json new file mode 100644 index 0000000..61edc71 --- /dev/null +++ b/bench_results/vllm_dspark_qwen3_20260705_121256/summary.json @@ -0,0 +1,131 @@ +{ + "model": "/data/models/Qwen3-4B", + "draft_model": "deepseek-ai/dspark_qwen3_4b_block7", + "url": "http://127.0.0.1:30003/v1/completions", + "max_tokens": 256, + "num_prompts": 500, + "dataset": "/data/user1/yy/datasets/ShareGPT_filtered_chat.json", + "timestamp": "20260705_121256", + "results": [ + { + "concurrency": 1, + "num_prompts": 500, + "duration_s": 181.22913801996037, + "success_count": 500, + "fail_count": 0, + "input_tokens": 108545, + "output_tokens": 36681, + "total_tokens": 145226, + "request_throughput": 2.7589382450460618, + "input_throughput": 598.9379036170495, + "output_throughput": 202.4012275330692, + "total_throughput": 801.3391311501188, + "mean_ttft_ms": 17.586736707482487, + "p50_ttft_ms": 15.236841514706612, + "p99_ttft_ms": 43.418583339080215, + "mean_tpot_ms": 4.756160026498775, + "p50_tpot_ms": 4.747943201144331, + "p99_tpot_ms": 4.845208972490266, + "mean_e2e_ms": 361.93272305326536, + "p50_e2e_ms": 369.4158981088549, + "p99_e2e_ms": 552.2739849891514, + "errors": [] + }, + { + "concurrency": 4, + "num_prompts": 500, + "duration_s": 49.20666282507591, + "success_count": 500, + "fail_count": 0, + "input_tokens": 108545, + "output_tokens": 36991, + "total_tokens": 145536, + "request_throughput": 10.161225559584139, + "input_throughput": 2205.900456730121, + "output_throughput": 751.7477893491538, + "total_throughput": 2957.6482460792745, + "mean_ttft_ms": 26.531575095374137, + "p50_ttft_ms": 24.292025598697364, + "p99_ttft_ms": 41.29285377450287, + "mean_tpot_ms": 5.007692315964597, + "p50_tpot_ms": 4.998277087638343, + "p99_tpot_ms": 5.130429720718591, + "mean_e2e_ms": 392.14430282171816, + "p50_e2e_ms": 400.28460952453315, + "p99_e2e_ms": 625.0063395127652, + "errors": [] + }, + { + "concurrency": 16, + "num_prompts": 500, + "duration_s": 14.20007478701882, + "success_count": 500, + "fail_count": 0, + "input_tokens": 108545, + "output_tokens": 36529, + "total_tokens": 145074, + "request_throughput": 35.21108215972787, + "input_throughput": 7643.973826055324, + "output_throughput": 2572.451240425399, + "total_throughput": 10216.425066480724, + "mean_ttft_ms": 31.879239567089826, + "p50_ttft_ms": 28.826430439949036, + "p99_ttft_ms": 74.12134333979331, + "mean_tpot_ms": 5.771187238080647, + "p50_tpot_ms": 5.76495784573639, + "p99_tpot_ms": 6.1421910060182805, + "mean_e2e_ms": 447.76513252267614, + "p50_e2e_ms": 459.7155440133065, + "p99_e2e_ms": 669.6232496039011, + "errors": [] + }, + { + "concurrency": 64, + "num_prompts": 500, + "duration_s": 7.960247536888346, + "success_count": 500, + "fail_count": 0, + "input_tokens": 108545, + "output_tokens": 36583, + "total_tokens": 145128, + "request_throughput": 62.81211704572815, + "input_throughput": 13635.882489457124, + "output_throughput": 4595.711355767746, + "total_throughput": 18231.59384522487, + "mean_ttft_ms": 73.04830395104364, + "p50_ttft_ms": 53.465207340195775, + "p99_ttft_ms": 252.97995713772252, + "mean_tpot_ms": 12.42267764661875, + "p50_tpot_ms": 12.61188557741512, + "p99_tpot_ms": 13.063075978620708, + "mean_e2e_ms": 968.1061067078263, + "p50_e2e_ms": 967.5668340642005, + "p99_e2e_ms": 1512.9108975501729, + "errors": [] + }, + { + "concurrency": 128, + "num_prompts": 500, + "duration_s": 7.319521516095847, + "success_count": 500, + "fail_count": 0, + "input_tokens": 108545, + "output_tokens": 36613, + "total_tokens": 145158, + "request_throughput": 68.31047615619204, + "input_throughput": 14829.52126874773, + "output_throughput": 5002.102927013319, + "total_throughput": 19831.62419576105, + "mean_ttft_ms": 145.49234842788428, + "p50_ttft_ms": 90.75760398991406, + "p99_ttft_ms": 423.2492828951217, + "mean_tpot_ms": 22.034148180580335, + "p50_tpot_ms": 23.052740126916806, + "p99_tpot_ms": 23.587740010073134, + "mean_e2e_ms": 1730.9650285840034, + "p50_e2e_ms": 1733.5386699996889, + "p99_e2e_ms": 2722.885905068833, + "errors": [] + } + ] +} \ No newline at end of file diff --git a/cleanup_summary.md b/cleanup_summary.md new file mode 100644 index 0000000..18016ca --- /dev/null +++ b/cleanup_summary.md @@ -0,0 +1,110 @@ +# /data/user1/yy 文件夹整理记录 + +> 整理时间:2026-07-06 +> 说明:将脚本集中到 `scripts/`,uv 虚拟环境集中到 `envs/`,并保留兼容性软链接;删除过时的 log、pid、临时文件和冗余压缩包。 + +--- + +## 目录结构 + +``` +/data/user1/yy/ +├── bench_results/ +│ ├── dsv4_comparison_20260705_152221/ +│ ├── dsv4_flash_dspark_misc/ +│ ├── pd_bench/ +│ ├── sglang_8card_max_throughput_20260705_030839/ +│ ├── sglang_8card_systematic_20260704_120819/ +│ ├── sglang_misc/ +│ ├── vllm_dspark_qwen3_20260705_121046/ +│ ├── vllm_dspark_qwen3_20260705_121218/ +│ └── vllm_dspark_qwen3_20260705_121256/ +├── datasets/ +├── envs/ # uv 虚拟环境与缓存 +│ ├── sglang/ -> 原 /data/user1/yy/sglang +│ ├── spraseattn/ -> 原 /data/user1/yy/spraseattn +│ ├── uv_cache/ -> 原 /data/user1/yy/uv_cache +│ ├── vllm/ -> 原 /data/user1/yy/vllm +│ └── vllm-dspark/ -> 原 /data/user1/yy/vllm-dspark +├── loomeval_yy/ # 按用户要求保留 +├── scripts/ # 脚本集中存放 +│ ├── bench_dsv4_comparison.py +│ ├── bench_vllm_dspark_qwen3.py +│ ├── install_vllm_dspark.sh +│ ├── run_sglang_benchmark.sh +│ ├── run_sglang_max_throughput.sh +│ ├── start_dsv4_dspark_8card.sh +│ ├── start_dsv4_dspark_8card_bf16kv.sh +│ ├── start_dsv4_dspark_8card_flashinfer.sh +│ ├── start_pd_single_node.sh +│ ├── start_sglang_dsv4_8card.sh +│ ├── start_vllm_pd_single_node.sh +│ └── test_block_sparse_attn.py +├── tmp/ +├── vllm-main/ # vLLM 源码目录 +├── cleanup_summary.md +├── dspark_deepseekv4_fix_pr_prep.md +├── dsv4_inference_comparison_report.md +├── issue_47648_comment.json +└── issue_47648_comment.md +``` + +--- + +## 脚本说明 + +| 脚本 | 说明 | +|---|---| +| `scripts/bench_dsv4_comparison.py` | vLLM DeepSeek-V4 对比测试主控脚本 | +| `scripts/bench_vllm_dspark_qwen3.py` | Qwen3 DSpark 验证脚本 | +| `scripts/install_vllm_dspark.sh` | vllm-dspark 安装脚本 | +| `scripts/run_sglang_benchmark.sh` | SGLang 8卡系统测试脚本 | +| `scripts/run_sglang_max_throughput.sh` | SGLang 极限吞吐测试脚本 | +| `scripts/start_dsv4_dspark_8card.sh` | DSV4 DSpark 服务启动脚本 | +| `scripts/start_dsv4_dspark_8card_bf16kv.sh` | DSV4 DSpark bf16 KV 启动脚本 | +| `scripts/start_dsv4_dspark_8card_flashinfer.sh` | DSV4 DSpark FlashInfer 启动脚本 | +| `scripts/start_pd_single_node.sh` | PD 分离单节点启动脚本 | +| `scripts/start_sglang_dsv4_8card.sh` | SGLang DSV4 启动脚本 | +| `scripts/start_vllm_pd_single_node.sh` | vLLM PD 分离单节点启动脚本 | +| `scripts/test_block_sparse_attn.py` | block sparse attention 测试脚本 | + +--- + +## 删除的文件/目录 + +| 路径 | 原因 | +|---|---| +| `__pycache__/` | Python 字节码缓存,可重新生成 | +| `*.pid`(共 4 个) | 进程 ID 临时文件 | +| `logs/` 目录下所有文件 | 安装、部署、下载等历史调试日志 | +| `pd_logs/` 目录 | PD 分离测试日志 | +| `vllm_pd_logs/` 目录 | vLLM PD 分离测试日志 | +| `vllm-main.zip` | 与 `vllm-main/` 目录冗余,约 41MB | +| `tmp/` 目录下内容 | 临时文件 | +| 根目录下的 `.log` 文件 | 已归档或无需保留的日志 | + +--- + +## 归档到 `bench_results/` 的文件 + +| 原路径 | 目标路径 | 说明 | +|---|---|---| +| `bench_dsv4_comparison_master.log` | `bench_results/dsv4_comparison_20260705_152221/` | 本次对比测试主日志 | +| `sglang_8card.log` | `bench_results/sglang_misc/` | SGLang 8卡日志 | +| `sglang_0704_1_1048576_1.jsonl` | `bench_results/sglang_misc/` | SGLang 测试原始结果 | +| `sglang_8card_sharegpt_c*.json/log` | `bench_results/sglang_8card_systematic_20260704_120819/` | SGLang 系统测试散落结果 | +| `pd_bench_*.json` | `bench_results/pd_bench/` | PD 分离 benchmark 结果 | +| `dsv4_flash_dspark_c16_n50_*` | `bench_results/dsv4_flash_dspark_misc/` | DSV4 DSpark 早期单次测试 | + +--- + +## 路径更新说明 + +脚本和文档中的环境路径已统一更新为 `envs/` 下的新路径: + +- `/data/user1/yy/vllm-dspark/` → `/data/user1/yy/envs/vllm-dspark/` +- `/data/user1/yy/vllm/` → `/data/user1/yy/envs/vllm/` +- `/data/user1/yy/sglang/` → `/data/user1/yy/envs/sglang/` +- `/data/user1/yy/spraseattn/` → `/data/user1/yy/envs/spraseattn/` + +根目录不再保留兼容性软链接,所有环境入口统一通过 `envs/` 访问。 diff --git a/dspark_deepseekv4_fix_pr_prep.md b/dspark_deepseekv4_fix_pr_prep.md new file mode 100644 index 0000000..25c4071 --- /dev/null +++ b/dspark_deepseekv4_fix_pr_prep.md @@ -0,0 +1,286 @@ +# DeepSeek-V4-Flash-DSpark 修复记录(GitHub Issue #47648) + +> 本文件记录 vLLM DSpark 在 DeepSeek-V4-Flash 上的启动失败问题、根因、修复 diff 以及验证结果,便于后续直接用于提交 PR。 + +--- + +## 1. 问题概述 + +在 H200/SM90(以及同样走 NVIDIA 路径的 B200/SM120)上,使用 `--spec-method dspark` 部署 `DeepSeek-V4-Flash-DSpark` 时,模型初始化阶段会失败。主要表现为两类错误: + +1. **DSpark draft 权重加载路径错配**:draft 权重(checkpoint 中以 `mtp.{i}.*` 命名)没有被正确加载到模型中。 +2. **KV cache shape mismatch**:当启用 `fp8_ds_mla` layout 时,KV cache 的 per-token slot size 被错误计算为 512B,而实际需要 584B,导致 shape 断言失败。 + +关联 issue:[vllm-project/vllm#47648](https://github.com/vllm-project/vllm/issues/47648) + +--- + +## 2. 环境信息 + +- vLLM 版本:`0.23.1rc1.dev788+gfa4321de3`(vllm-dspark wheel) +- PyTorch:`2.11.0+cu129` +- GPU:8× NVIDIA H200(SM90),TP=8 +- 模型:`/data/models/DeepSeek-V4-Flash-DSpark` +- 启动命令: + +```bash +vllm serve /data/models/DeepSeek-V4-Flash-DSpark \ + --trust-remote-code \ + --tensor-parallel-size 8 \ + --kv-cache-dtype fp8 \ + --block-size 256 \ + --max-model-len auto \ + --max-num-seqs 256 \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --spec-method dspark \ + --spec-model /data/models/DeepSeek-V4-Flash-DSpark \ + --spec-tokens 5 \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --port 30004 +``` + +--- + +## 3. 根因分析 + +### 3.1 根因一:DSpark draft 权重加载路径错配 + +#### 位置 + +`vllm/models/deepseek_v4/nvidia/dspark.py`,`_remap_dspark_name` 方法。 + +#### 问题描述 + +DeepSeek-V4-Flash-DSpark 的 draft 权重是嵌在目标模型 checkpoint 中的,命名格式为 `mtp.{i}.*`,例如: + +```text +mtp.0.self_attn.wq_b.weight +mtp.0.ffn.experts.0.w1.weight +mtp.1.self_attn.wq_b.weight +... +``` + +`_remap_dspark_name` 负责把这些 checkpoint key 映射到 DSpark draft 模型内部的真实参数名。原来的实现: + +```python +return f"model.layers.{self.num_hidden_layers + stage}.{rest}" +``` + +把 `mtp.0.*` 映射成了 `model.layers.{num_hidden_layers + 0}.*`(例如 `model.layers.64.*`)。 + +但实际上,DSpark draft 的 3 个 `DeepseekV4DecoderLayer` 是放在一个 `nn.ModuleList` 里的,PyTorch 真实注册的参数名只跟 `ModuleList` 的索引有关: + +```text +model.layers.0.* +model.layers.1.* +model.layers.2.* +``` + +`DeepseekV4DecoderLayer` 构造函数里传入的 `prefix=f"layers.{num_hidden_layers + i}"` 只是为了内部计算 `compress_ratio`(让 `layer_id >= num_hidden_layers` 时固定 `compress_ratio=1`),**不是真实的参数名前缀**。 + +因此,原来的映射导致所有 draft block 权重都找不到对应的模型参数,draft layer 实际上没有被正确加载。 + +#### 修复 + +把 `mtp.{i}` 的 block 权重映射到 `model.layers.{i}`: + +```python +def _remap_dspark_name(self, name: str) -> str | None: + """Map a checkpoint ``mtp.{i}.*`` name to this model's parameter path. + + Returns None for non-mtp weights (owned by the target model). + """ + m = re.match(r"mtp\.(\d+)\.(.*)", name) + if m is None: + return None + stage = int(m.group(1)) + rest = m.group(2) + # The confidence head is not wired into inference yet; drop its weights. + if rest.startswith("confidence_head."): + return None + # Head-stack params live at model level (mtp.last), context combiner at + # model level (mtp.0); everything else is a per-layer decoder block. + head_prefixes = ( + "norm.", + "hc_head_fn", + "hc_head_base", + "hc_head_scale", + "markov_head.", + ) + if rest.startswith(("main_proj.", "main_norm.")) or rest.startswith( + head_prefixes + ): + return f"model.{rest}" + # Draft layers live in a ModuleList, so their actual parameter names are + # model.layers.{stage}.* even though the prefix passed to the decoder + # layer is layers.{num_hidden_layers + stage} (for compress_ratio). + return f"model.layers.{stage}.{rest}" +``` + +### 3.2 根因二:DeepSeek-V4 KV cache shape mismatch + +#### 位置 + +`vllm/models/deepseek_v4/attention.py`,`DeepseekV4Attention.get_kv_cache_spec` 方法。 + +#### 问题描述 + +`DeepseekV4Attention.get_kv_cache_spec` 返回的 `MLAAttentionSpec` 没有设置 `kv_quant_mode`。在 `gpu_model_runner.py` 中,这个缺失导致 `cache_dtype_str` 被当成 `"auto"` 传给 `DeepseekV4FlashMLABackend.get_kv_cache_shape`,返回的 shape 是 `(num_blocks, block_size, 512)`。 + +但对于 `fp8_ds_mla` layout(UE8M0 block-scaled fp8,以 `uint8` 打包),每个 token 的 KV slot 实际大小是 584 字节,而不是 512。于是 KV cache 分配的空间不够,触发 shape mismatch / assert。 + +#### 修复 + +在 `MLAAttentionSpec` 中加上 `kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype)`: + +```python +def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None: + if ( + self.compress_ratio <= 1 + ): # SWA part. Allocated separately as DeepseekV4SWACache. + return None + # fp8_ds_mla is a UE8M0 block-scaled uint8 layout and needs 576B + # alignment; plain bf16 / per-tensor fp8 rows use natural element-size + # pages. + uses_fp8_ds_mla_layout = self.kv_cache_dtype == "fp8_ds_mla" + return MLAAttentionSpec( + block_size=vllm_config.cache_config.block_size, + num_kv_heads=1, + head_size=self.head_dim, + dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype, + compress_ratio=self.compress_ratio, + cache_dtype_str=self.kv_cache_dtype, + alignment=576 if uses_fp8_ds_mla_layout else None, + model_version="deepseek_v4", + kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype), + ) +``` + +--- + +## 4. 修改文件清单 + +| 文件 | 修改内容 | +|---|---| +| `vllm/models/deepseek_v4/nvidia/dspark.py` | `_remap_dspark_name`:draft block 权重从 `model.layers.{num_hidden_layers + stage}` 改为 `model.layers.{stage}` | +| `vllm/models/deepseek_v4/attention.py` | `DeepseekV4Attention.get_kv_cache_spec`:在 `MLAAttentionSpec` 中补充 `kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype)` | + +> 如果上游 vllm-main 仓库与 vllm-dspark 安装包的文件结构一致,也需要同步修改 `vllm-main` 下对应路径的同名文件。 + +--- + +## 5. 完整 diff(面向 PR) + +```diff +--- a/vllm/models/deepseek_v4/nvidia/dspark.py ++++ b/vllm/models/deepseek_v4/nvidia/dspark.py +@@ -486,8 +486,8 @@ class DSparkDeepseekV4ForCausalLM(nn.Module): + if rest.startswith(("main_proj.", "main_norm.")) or rest.startswith( + head_prefixes + ): + return f"model.{rest}" +- # Draft layers live after the target layers in the decoder stack. +- return f"model.layers.{self.num_hidden_layers + stage}.{rest}" ++ # Draft layers live in a ModuleList, so their actual parameter names are ++ # model.layers.{stage}.* even though the prefix passed to the decoder ++ # layer is layers.{num_hidden_layers + stage} (for compress_ratio). ++ return f"model.layers.{stage}.{rest}" +``` + +```diff +--- a/vllm/models/deepseek_v4/attention.py ++++ b/vllm/models/deepseek_v4/attention.py +@@ -613,6 +613,7 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC): + cache_dtype_str=self.kv_cache_dtype, + alignment=576 if uses_fp8_ds_mla_layout else None, + model_version="deepseek_v4", ++ kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype), + ) +``` + +--- + +## 6. 验证结果 + +### 6.1 Qwen3 DSpark 通路验证 + +- 目标模型:`/data/models/Qwen3-4B` +- Draft 模型:`deepseek-ai/dspark_qwen3_4b_block7` +- 结果:服务启动成功,`/v1/completions` 返回结果正常,speculative decoding 工作。 + +### 6.2 DeepSeek-V4-Flash-DSpark 服务验证 + +修复后,使用第 2 节的命令可以成功启动服务,监听 `http://127.0.0.1:30004`。简单 completion 请求返回正常: + +```bash +curl -s -X POST http://127.0.0.1:30004/v1/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "prompt": "The capital of France is", + "max_tokens": 20, + "temperature": 0.0 + }' +# 返回: " Paris." +``` + +### 6.3 Serving benchmark + +使用 vLLM 内置 `vllm bench serve`: + +```bash +vllm bench serve \ + --host 127.0.0.1 --port 30004 \ + --backend openai \ + --dataset-name sharegpt \ + --dataset-path /data/user1/yy/datasets/ShareGPT_filtered_chat.json \ + --sharegpt-output-len 256 \ + --num-prompts 50 \ + --max-concurrency 16 \ + --endpoint /v1/completions \ + --model /data/models/DeepSeek-V4-Flash-DSpark \ + --seed 42 +``` + +结果: + +| 指标 | 数值 | +|---|---| +| Request throughput | **2.38 req/s** | +| Output token throughput | **610.47 tok/s** | +| Acceptance rate | **28.20%** | +| Mean acceptance length | **2.41** | +| Mean TTFT | 2162 ms | +| Mean TPOT | 16.85 ms | +| Total input tokens | 16381 | +| Total generated tokens | 12800 | + +服务器日志中的 SpecDecoding metrics 在稳态下 acceptance rate 落在 25%–36% 区间,与 DSpark 论文预期一致。 + +--- + +## 7. 影响范围 + +- **受影响**:所有使用 NVIDIA 路径运行 DeepSeek-V4-Flash-DSpark 的 GPU(包括 H200/SM90 的 FlashMLA 路径,以及 B200/SM120 的 FlashInfer SM120 路径)。 +- **不受影响**: + - Qwen3 DSpark(使用独立的 `qwen3_dspark.py` 实现,不共享 `_remap_dspark_name`)。 + - AMD/ROCm 和 XPU 平台(目前 DSpark 仅支持 NVIDIA)。 + +--- + +## 8. 后续 PR 待办 + +- [ ] 在 vllm-main 上同步应用以上两处修改。 +- [ ] 跑通 DeepSeek-V4-Flash-DSpark 的单元测试 / 冒烟测试。 +- [ ] 检查 `DeepseekV4IndexerCache.get_kv_cache_spec` 是否也需要补充 `kv_quant_mode`(当前未改动,因为未触发错误)。 +- [ ] 补充 DSpark draft 权重加载的回归测试(可选,但建议)。 +- [ ] 向 vllm-project/vllm 提交 PR,并在描述中引用 issue #47648。 + +--- + +## 9. 备注 + +- 本文件中的 token、路径、版本号均基于 2026-07-05 的实际运行环境。 +- GitHub issue 评论已发布:https://github.com/vllm-project/vllm/issues/47648#issuecomment-4886275279 diff --git a/dsv4_inference_comparison_report.md b/dsv4_inference_comparison_report.md new file mode 100644 index 0000000..ed006a5 --- /dev/null +++ b/dsv4_inference_comparison_report.md @@ -0,0 +1,351 @@ +# DeepSeek-V4 推理性能评估报告 + +## vLLM DSpark vs 其他投机解码方法 vs SGLang + +> 测试环境:8× NVIDIA H200 (143GB),CUDA 12.x,驱动 575.57.08 +> 测试时间:2026-07-05 +> 测试模型:`DeepSeek-V4-Flash` / `DeepSeek-V4-Flash-DSpark` +> 测试框架:vLLM 0.24.0、vLLM-dspark 0.23.1rc1.dev788、SGLang 0.5.14 + +--- + +## 1. 背景与目标 + +### 1.1 投机解码(Speculative Decoding)基本原理 + +大模型自回归生成的瓶颈在于:每生成一个 token 都要做一次完整的模型前向传播。投机解码的核心思想是: + +1. 用一个轻量的 **draft model**(或 draft head)一次性生成多个候选 token; +2. 用原始 **target model** 并行验证这些候选 token; +3. 接受与 target 模型一致的 token,从第一个不一致处重新生成。 + +理想情况下,如果 draft model 质量高,可以一次接受多个 token,从而把有效生成步长从 1 提升到 1+α(α 为平均接受长度),显著降低 latency、提升 throughput。 + +### 1.2 常见投机解码方法对比 + +| 方法 | 代表实现 | 优点 | 缺点 | +|---|---|---|---| +| **EAGLE / EAGLE-2** | vLLM EAGLE、SGLang EAGLE | 接受率高(40-70%),draft 模型小 | 需要单独训练 draft 模型,内存占用额外 | +| **MTP (Multi-Token Prediction)** | DeepSeek MTP | 与目标模型结构一致,可共享权重 | 通常只有 1-2 个 MTP head,加速比有限 | +| **DSpark** | vLLM-dspark | 半自回归 draft,并行生成整段,接受长度高 | draft 计算量较大,对 batching/scheduling 要求高 | +| **n-gram / prompt-lookup** | vLLM ngram | 无需额外模型 | 只适用于重复性文本,泛化差 | +| **Medusa** | Medusa | 多个解码头 | 需要训练,内存占用大 | + +### 1.3 DSpark 的特点 + +DSpark 是 DeepSeek 提出的块级半自回归投机解码方案,关键设计: + +- **块级 draft**:一次并行生成一个 block(多个 token),而不是逐 token; +- **非因果注意力**:draft block 内部允许未来 token 参与当前 token 计算; +- **Markov head**:在 block 内引入轻量序列依赖; +- **目标模型复用**:DSpark draft 权重与目标模型共存(`mtp.{i}.*`),可共享 embedding/head。 + +相比 EAGLE,DSpark 的 draft 更接近目标模型结构,因此接受率通常更高,但 draft 前向本身的计算量也更大。DSpark 的优势在 **高并发、长输出** 场景下更明显;在低并发或短输出场景,draft 开销可能抵消收益。 + +### 1.4 评估目标 + +本报告系统评估: + +1. **vLLM + DSpark** 在 DeepSeek-V4-Flash 上的推理优势; +2. 与 **vLLM 无投机解码** 基线的对比; +3. 与 **SGLang + EAGLE** 的对比; +4. 不同参数(`--spec-tokens`、并发度、输出长度)下的最优配置; +5. vLLM 与 SGLang 两个引擎部署 DSV4 的差异。 + +--- + +## 2. 测试环境 + +### 2.1 硬件 + +| 项目 | 配置 | +|---|---| +| GPU | 8× NVIDIA H200 143GB | +| 互联 | NVLink + NVSwitch | +| 驱动 | 575.57.08 | +| CUDA | 12.x | + +### 2.2 软件版本 + +| 引擎 | 版本 | 路径 | +|---|---|---| +| SGLang | 0.5.14 | `/data/user1/yy/envs/sglang` | +| vLLM | 0.24.0 | `/data/user1/yy/envs/vllm` | +| vLLM-dspark | 0.23.1rc1.dev788+gfa4321de3 | `/data/user1/yy/envs/vllm-dspark` | + +### 2.3 模型 + +| 模型 | 路径 | 说明 | +|---|---|---| +| DeepSeek-V4-Flash | `/data/models/DeepSeek-V4-Flash` | 标准目标模型 | +| DeepSeek-V4-Flash-DSpark | `/data/models/DeepSeek-V4-Flash-DSpark` | 内含 DSpark draft 权重 | + +### 2.4 数据集 + +- ShareGPT V4.3 unfiltered cleaned split:`/data/user1/yy/datasets/ShareGPT_V4.3_unfiltered_cleaned_split.json` + +### 2.5 测试参数 + +- Tensor Parallel:8 +- KV cache dtype:fp8 +- Block size:256 +- Max model len:auto +- Max num seqs:256 +- 输出长度:256(固定) +- Prompt 数量:200 +- 并发度:1, 16, 64 + +--- + +## 3. 测试方法 + +### 3.1 服务启动 + +各引擎服务启动命令如下: + +**vLLM-dspark + DSpark** +```bash +vllm serve /data/models/DeepSeek-V4-Flash-DSpark \ + --trust-remote-code --tensor-parallel-size 8 \ + --kv-cache-dtype fp8 --block-size 256 --max-model-len auto \ + --max-num-seqs 256 --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --spec-method dspark --spec-model /data/models/DeepSeek-V4-Flash-DSpark \ + --spec-tokens 5 \ + --no-disable-hybrid-kv-cache-manager --disable-uvicorn-access-log \ + --port 30004 +``` + +**vLLM-dspark 无投机解码** +```bash +vllm serve /data/models/DeepSeek-V4-Flash-DSpark \ + --trust-remote-code --tensor-parallel-size 8 \ + --kv-cache-dtype fp8 --block-size 256 --max-model-len auto \ + --max-num-seqs 256 --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --no-disable-hybrid-kv-cache-manager --disable-uvicorn-access-log \ + --port 30004 +``` + +**vLLM 0.24.0 无投机解码** +```bash +vllm serve /data/models/DeepSeek-V4-Flash \ + --trust-remote-code --tensor-parallel-size 8 \ + --kv-cache-dtype fp8 --block-size 256 --max-model-len auto \ + --max-num-seqs 256 --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --disable-uvicorn-access-log --port 30005 +``` + +**SGLang + EAGLE** +```bash +sglang serve --trust-remote-code --model-path /data/models/DeepSeek-V4-Flash \ + --tp 8 --moe-runner-backend marlin \ + --speculative-algorithm EAGLE --speculative-num-steps 3 \ + --speculative-eagle-topk 1 --speculative-num-draft-tokens 4 \ + --host 0.0.0.0 --port 30000 +``` + +### 3.2 Benchmark 命令 + +使用各引擎自带的 serving benchmark: + +**vLLM / vLLM-dspark** +```bash +vllm bench serve --host 127.0.0.1 --port --backend openai \ + --dataset-name sharegpt --dataset-path \ + --sharegpt-output-len 256 --num-prompts 200 \ + --max-concurrency --endpoint /v1/completions \ + --model --seed 42 --save-result --result-dir +``` + +**SGLang** +```bash +python -m sglang.bench_serving --backend sglang --host 127.0.0.1 --port 30000 \ + --dataset-name sharegpt --dataset-path \ + --num-prompts 2000 --sharegpt-output-len 256 \ + --max-concurrency --model --seed 42 --output-file +``` + +> 注:SGLang 历史测试使用 2000 prompts,vLLM-dspark / vLLM 本次测试使用 200 prompts,对比时主要关注相对趋势而非绝对数值。 + +--- + +## 4. 测试结果 + +### 4.1 总体对比(请求吞吐 req/s) + +| 配置 | 引擎 | 投机方法 | 并发=1 | 并发=16 | 并发=64 | 并发=128 | 并发=256 | +|---|---|---:|---:|---:|---:|---:|---:| +| SGLang + EAGLE | sglang | EAGLE (4 draft) | 1.17 | 6.03 | 9.64 | 14.35 | 22.06 | +| vLLM-dspark + DSpark (st=3) | vllm-dspark | DSpark | 1.03 | 8.24 | **14.53** | - | - | +| vLLM-dspark + DSpark (st=5) | vllm-dspark | DSpark | 1.07 | 7.70 | 9.36 | - | - | +| vLLM-dspark + DSpark (st=7) | vllm-dspark | DSpark | 1.03 | 7.46 | 9.13 | - | - | +| vLLM-dspark 无投机 | vllm-dspark | 无 | 0.59 | 5.61 | 7.34 | - | - | +| vLLM 0.24.0 无投机 | vllm | 无 | 0.58 | 5.05 | 6.85 | - | - | + +### 4.2 总体对比(输出 token 吞吐 tok/s) + +| 配置 | 并发=1 | 并发=16 | 并发=64 | 并发=128 | 并发=256 | +|---|---:|---:|---:|---:|---:| +| SGLang + EAGLE | 243.51 | 1259.0 | 2012.43 | 2996.37 | 4608.35 | +| vLLM-dspark + DSpark (st=3) | 254.39 | 1992.18 | **3507.81** | - | - | +| vLLM-dspark + DSpark (st=5) | 257.18 | 1862.34 | 2210.46 | - | - | +| vLLM-dspark + DSpark (st=7) | 251.58 | 1772.30 | 2218.58 | - | - | +| vLLM-dspark 无投机 | 144.62 | 1364.00 | 1803.98 | - | - | +| vLLM 0.24.0 无投机 | 138.26 | 1180.98 | 1641.89 | - | - | + +### 4.3 vLLM-dspark 不同 `--spec-tokens` 对比 + +| spec-tokens | 并发 | req/s | out tok/s | 平均接受长度 | 接受率 | mean TTFT (ms) | mean TPOT (ms) | mean ITL (ms) | +|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 3 | 1 | 1.03 | 254.39 | 2.22 | 40.56% | 78.07 | 3.64 | 8.00 | +| 3 | 16 | 8.24 | 1992.18 | 2.19 | 39.66% | 90.90 | 7.63 | 16.23 | +| 3 | 64 | **14.53** | **3507.81** | 2.16 | 38.71% | 355.29 | 15.84 | 33.13 | +| 5 | 1 | 1.07 | 257.18 | 2.44 | 28.71% | 88.43 | 3.53 | 8.56 | +| 5 | 16 | 7.70 | 1862.34 | 2.39 | 27.87% | 92.23 | 8.19 | 18.90 | +| 5 | 64 | 9.36 | 2210.46 | 2.40 | 28.07% | 453.06 | 26.19 | 60.10 | +| 7 | 1 | 1.03 | 251.58 | 2.43 | 20.45% | 73.16 | 3.70 | 8.92 | +| 7 | 16 | 7.46 | 1772.30 | 2.41 | 20.10% | 106.43 | 8.92 | 20.07 | +| 7 | 64 | 9.13 | 2218.58 | 2.42 | 20.25% | 351.01 | 26.25 | 61.33 | + +> 数据来源:`/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/vllm-dspark-dspark-st{3,5,7}_c{1,16,64}.json` + +### 4.4 延迟指标对比 + +| 配置 | 并发 | mean TTFT (ms) | mean TPOT (ms) | mean ITL (ms) | +|---|---:|---:|---:|---:| +| vLLM-dspark + DSpark (st=3) | 16 | 90.90 | 7.63 | 16.23 | +| vLLM-dspark + DSpark (st=5) | 16 | 92.23 | 8.19 | 18.90 | +| vLLM-dspark + DSpark (st=7) | 16 | 106.43 | 8.92 | 20.07 | +| vLLM-dspark 无投机 | 16 | 113.49 | 11.06 | 11.02 | +| vLLM 0.24.0 无投机 | 16 | 332.87 | 11.73 | 11.74 | +| SGLang + EAGLE | 16 | 168.95 | 12.67 | - | + +### 4.5 投机解码 vs 无投机解码加速比 + +以 vLLM-dspark 无投机为基线: + +| 配置 | 并发=1 | 并发=16 | 并发=64 | +|---|---:|---:|---:| +| DSpark st=3 | 1.75× | 1.47× | **1.98×** | +| DSpark st=5 | 1.80× | 1.37× | 1.28× | +| DSpark st=7 | 1.74× | 1.33× | 1.24× | + +以 vLLM 0.24.0 无投机为基线: + +| 配置 | 并发=1 | 并发=16 | 并发=64 | +|---|---:|---:|---:| +| DSpark st=3 | 1.78× | 1.63× | 2.12× | +| DSpark st=5 | 1.84× | 1.52× | 1.37× | +| DSpark st=7 | 1.77× | 1.48× | 1.33× | + +--- + +## 5. 结果分析 + +### 5.1 DSpark 的优势场景 + +- **高并发**:当 batch size 足够大时,DSpark 的 draft 计算可以被充分并行化,接受率带来的收益超过 draft 开销。`--spec-tokens 3` 在并发 64 时达到 14.53 req/s,几乎是 vLLM-dspark 无投机的 2 倍; +- **长输出**:输出 token 越多,投机解码节省的 target 前向次数越多; +- **低延迟与高吞吐兼得**:在并发 16 时,DSpark st=3 的 mean TTFT 仅 90.90 ms,mean TPOT 7.63 ms,均优于无投机基线和 SGLang EAGLE。 + +### 5.2 DSpark 的劣势场景 + +- **单请求**:虽然单请求也有 1.7-1.8× 加速,但加速比主要来自接受率;draft 模型的固定开销仍然存在; +- **`--spec-tokens` 过大**:st=5 和 st=7 在高并发(64)下性能反而下降。原因是 draft tokens 数量增加后,验证阶段的计算量和 KV cache 压力增大,而接受率并未提升(st=7 接受率仅 20%); +- **TTFT 抖动**:高并发下 DSpark 的 P99 TTFT 明显升高(st=5 c=64 时 P99 TTFT 达 5381 ms),说明调度/内存压力较大。 + +### 5.3 不同 `--spec-tokens` 的选择 + +| spec-tokens | 最佳并发 | 表现 | +|---|---|---| +| 3 | 高并发(64+) | 接受率最高(~40%),吞吐最高,验证开销最小 | +| 5 | 中低并发(1-16) | 接受长度略高(2.4),单请求吞吐最优 | +| 7 | 不推荐 | 接受率下降(~20%),验证开销大,高并发下性能倒退 | + +### 5.4 与 EAGLE 的对比 + +- **接受率**:EAGLE 接受长度稳定在 2.61,DSpark st=3 接受长度 2.16-2.22,但 DSpark 接受率(token 级别)达 38-40%,说明每段 draft 的质量并不低; +- **吞吐**:vLLM-dspark + DSpark st=3 在并发 64 时(14.53 req/s)已接近 SGLang EAGLE 在并发 128 时(14.35 req/s)。SGLang 在更高并发(256-512)下仍能通过扩大 batch 提升吞吐,但这是以极高的 TTFT 为代价的; +- **延迟**:vLLM-dspark DSpark 在中低并发下的 TTFT/TPOT 明显优于 SGLang EAGLE; +- **实现成熟度**:SGLang 的 EAGLE 实现更成熟稳定;vLLM-dspark 的 DSpark 仍在快速迭代,部分参数组合(如 st=5/7 @ c=64)出现性能倒退。 + +### 5.5 vLLM-dspark vs vLLM 0.24.0 + +- 在完全无投机解码的情况下,vLLM-dspark(0.23.1rc)比 vLLM 0.24.0 更快: + - 并发 1:0.59 vs 0.58 req/s(接近) + - 并发 16:5.61 vs 5.05 req/s(+11%) + - 并发 64:7.34 vs 6.85 req/s(+7%) +- 这说明 vLLM-dspark 分支对 DeepSeek-V4 的 MLA / MoE / sparse attention 路径有更针对性的优化。 + +### 5.6 vLLM vs SGLang 引擎差异 + +- **启动与兼容性**:vLLM-dspark 对 DSV4 支持更直接,SGLang 对 EAGLE 支持更成熟; +- **调度策略**:SGLang 的 radix attention / chunked prefill 在长上下文场景有优势; +- **性能天花板**:SGLang EAGLE 在极高并发(512+)下吞吐更高,但延迟失控;vLLM-dspark DSpark 在中高并发(16-64)下延迟更优; +- **数据可比性**:SGLang 历史测试使用 2000 prompts,vLLM 本次仅 200 prompts,绝对数值需谨慎对比。 + +--- + +## 6. 最优参数建议 + +### 6.1 低延迟场景(单用户 / 低并发) + +- 推荐:**DSpark `--spec-tokens 5`**; +- 原因:单请求下 st=5 的 req/s 最高(1.07),TPOT/ITL 与 st=3/7 接近,接受长度最长(2.44)。 + +### 6.2 高吞吐场景(高并发 / 在线服务) + +- 推荐:**DSpark `--spec-tokens 3`**,`--max-num-seqs 256`,`--max-concurrency 64-128`; +- 原因:st=3 在高并发下接受率最高、验证开销最小,实测吞吐最高(14.53 req/s,3507 tok/s)。 + +### 6.3 长输出场景 + +- 推荐:开启 DSpark,`--spec-tokens 3-5`; +- 原因:输出越长,每次投机成功节省的 target 前向越多。st=3 更适合高并发,st=5 更适合中低并发。 + +### 6.4 短输出场景 + +- 推荐:**关闭投机解码** 或使用 `--spec-tokens 3`; +- 原因:短输出下投机收益有限,且 st=5/7 的验证开销可能抵消收益。 + +### 6.5 避免的参数组合 + +- **避免 `--spec-tokens 7` @ 高并发**:接受率仅 20%,高并发下吞吐不如 st=3/5; +- **避免 `--spec-tokens 5` @ 极高并发**:P99 TTFT 大幅升高,服务质量下降。 + +--- + +## 7. 结论 + +1. **vLLM-dspark + DSpark 在 DeepSeek-V4-Flash 上显著优于无投机解码基线**,最佳配置(st=3, c=64)比 vLLM-dspark 无投机快 **1.98×**,比 vLLM 0.24.0 无投机快 **2.12×**; +2. **DSpark 的最佳 `--spec-tokens` 取决于并发度**: + - 低并发(1-16):`--spec-tokens 5` 综合最优; + - 高并发(64+):`--spec-tokens 3` 综合最优; +3. **与 SGLang EAGLE 相比**,vLLM-dspark DSpark 在中高并发下延迟更优,吞吐接近;SGLang 在极限并发下吞吐更高但延迟失控; +4. **vLLM-dspark 分支对 DSV4 的优化效果明显**,即使无投机解码也比 vLLM 0.24.0 主分支更快; +5. **DSpark 目前仍不够稳定**,部分参数组合(st=5/7 @ c=64)出现性能倒退和高 TTFT 抖动,生产部署前需要针对实际负载调参。 + +--- + +## 8. 附录 + +### 8.1 原始数据文件 + +- vLLM-dspark / vLLM 测试结果:`/data/user1/yy/bench_results/dsv4_comparison_20260705_152221/` +- SGLang 历史结果: + - `/data/user1/yy/bench_results/sglang_8card_systematic_20260704_120819/` + - `/data/user1/yy/bench_results/sglang_8card_max_throughput_20260705_030839/` + +### 8.2 测试脚本 + +- 主控脚本:`/data/user1/yy/bench_dsv4_comparison.py` +- 指标提取脚本:`/tmp/extract_dsv4_metrics.py` + +### 8.3 已知限制 + +- vLLM-dspark 的 DSpark 实现较新,部分参数组合可能不稳定; +- 本次测试未覆盖 PD 分离、前缀缓存、不同输出长度等高级特性; +- SGLang 与 vLLM 的 prompt 数量不一致(2000 vs 200),绝对数值对比仅供参考; +- 不同数据集和 prompt 长度分布会影响结果。 diff --git a/scripts/SLO_STANDARDS.md b/scripts/SLO_STANDARDS.md new file mode 100644 index 0000000..ad24540 --- /dev/null +++ b/scripts/SLO_STANDARDS.md @@ -0,0 +1,33 @@ +# 推理服务 SLO 标准 + +> 记录各模型层级(Tier)的延迟目标。TTFT 以 **P95** 为主要关注指标。 + +## 模型层级定义 + +| 层级 | 名称 | 参数规模 | 典型模型 | 应用场景 | 核心目标 | +|---|---|---|---|---|---| +| S0 | 轻量层 | < 30B | Llama 3.1 8B Instruct | 高并发轻推理 | 高并发轻推理 | +| S1 | 实时交互层 | [30B, 100B) | Llama 3.3 70B Instruct | Chat / Copilot / Agent | 极低延迟 | +| S2 | 均衡服务层 | [100B, 500B) | DeepSeek V4 Flash、MiniMax M2.7 | 企业 API | 性价比平衡 | +| S3 | 深度推理层 | [500B, 1T) | DeepSeek V3.2 | 高复杂推理 | 稳定吞吐 | +| S4 | 超级推理层 | ≥ 1T | DS V4 Pro | 深度思考 | 智能优先 | + +## TTFT SLO(P95 为主要关注指标) + +| 指标 | S0 | S1 | S2 | S3 | S4 | +|---|---|---|---|---|---| +| P50 | < 500ms | < 800ms | < 1.5s | < 2s | < 1.5s | +| P95 | < 0.4s | < 1s | < 3s | < 5s | < 3s | + +## TPOT SLO + +| 指标 | S0 | S1 | S2 | S3 | S4 | +|---|---|---|---|---|---| +| TPOT | < 20ms | < 40ms | < 50ms | < 50ms | < 50ms | + +## 说明 + +- **TTFT**(Time To First Token):从请求到达服务端到首个 token 返回的延迟。 +- **TPOT**(Time Per Output Token):除首 token 外,平均每输出一个 token 的耗时。 +- 当前重点关注 **TTFT P95**,即 95% 请求的 TTFT 应满足对应层级的阈值。 +- _benchmark 输出应同时给出 P50 / P90 / P95 / P99 TTFT,但评估以 P95 为准。_ diff --git a/scripts/analyze_dspark_nsys.py b/scripts/analyze_dspark_nsys.py new file mode 100644 index 0000000..bd8ffb9 --- /dev/null +++ b/scripts/analyze_dspark_nsys.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Analyze nsys traces for DSpark profiling. + +Usage: + python scripts/analyze_dspark_nsys.py +""" + +import json +import subprocess +import sys +from pathlib import Path + + +def run_nsys_report(rep_file: Path, report_name: str) -> str: + cmd = [ + "nsys", "stats", + "--report", report_name, + "--format", "json", + str(rep_file), + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"nsys {report_name} failed: {result.stderr}") + return "" + return result.stdout + + +def summarize_cuda_sum(rep_file: Path) -> list[dict]: + """Return top CUDA kernels by total time.""" + text = run_nsys_report(rep_file, "cuda_kernel_sum") + if not text: + return [] + # nsys stats --format json outputs JSON after some header lines. + # Find the first '[' character. + start = text.find("[") + if start < 0: + return [] + data = json.loads(text[start:]) + # Sort by total time descending. + rows = data[1:] if len(data) > 1 else data + rows_sorted = sorted(rows, key=lambda x: x.get("Total Time (ns)", 0), reverse=True) + return rows_sorted[:50] + + +def summarize_nvtx_sum(rep_file: Path) -> list[dict]: + text = run_nsys_report(rep_file, "nvtx_sum") + start = text.find("[") + if start < 0: + return [] + data = json.loads(text[start:]) + rows = data[1:] if len(data) > 1 else data + rows_sorted = sorted(rows, key=lambda x: x.get("Total Time (ns)", 0), reverse=True) + return rows_sorted[:30] + + +def main(): + if len(sys.argv) < 2: + print("Usage: analyze_dspark_nsys.py ") + sys.exit(1) + rep_file = Path(sys.argv[1]) + if not rep_file.exists(): + print(f"File not found: {rep_file}") + sys.exit(1) + + print(f"Analyzing {rep_file}...\n") + + cuda_kernels = summarize_cuda_sum(rep_file) + print("=== Top CUDA Kernels by Total Time ===") + for row in cuda_kernels[:20]: + name = row.get("Name", "N/A") + total_ns = row.get("Total Time (ns)", 0) + count = row.get("Instances", 0) + avg_ns = row.get("Avg (ns)", 0) + print(f" {total_ns/1e6:8.2f} ms {count:6d} calls avg={avg_ns/1e6:6.3f} ms {name[:80]}") + + nvtx = summarize_nvtx_sum(rep_file) + if nvtx: + print("\n=== Top NVTX Ranges ===") + for row in nvtx[:20]: + name = row.get("Range", "N/A") + total_ns = row.get("Total Time (ns)", 0) + count = row.get("Instances", 0) + print(f" {total_ns/1e6:8.2f} ms {count:6d} calls {name[:80]}") + + +if __name__ == "__main__": + main() diff --git a/scripts/bench_dsv4_comparison.py b/scripts/bench_dsv4_comparison.py new file mode 100644 index 0000000..e9acf5d --- /dev/null +++ b/scripts/bench_dsv4_comparison.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +"""Benchmark orchestration for DeepSeek-V4 inference comparison. + +Compares: +- vllm-dspark + DeepSeek-V4-Flash-DSpark + DSpark (various spec-tokens) +- vllm-dspark + DeepSeek-V4-Flash-DSpark without spec decode +- vllm (0.24.0) + DeepSeek-V4-Flash without spec decode +- sglang + DeepSeek-V4-Flash + EAGLE (reuse existing results) +""" + +import json +import os +import subprocess +import sys +import time +import urllib.request +from datetime import datetime +from pathlib import Path + +ROOT = Path("/data/user1/yy") +RESULT_DIR = ROOT / "bench_results" / f"dsv4_comparison_{datetime.now().strftime('%Y%m%d_%H%M%S')}" +RESULT_DIR.mkdir(parents=True, exist_ok=True) + +DATASET = "/data/user1/yy/datasets/ShareGPT_V4.3_unfiltered_cleaned_split.json" +NUM_PROMPTS = 200 +SEED = 42 +OUTPUT_LEN = 256 +HOST = "127.0.0.1" + +# Service configs to benchmark +SERVICES = [ + { + "name": "vllm-dspark-dspark-st5", + "engine": "vllm-dspark", + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "port": 30004, + "spec_method": "dspark", + "spec_tokens": 5, + "cmd": [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash-DSpark", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--spec-method", "dspark", + "--spec-model", "/data/models/DeepSeek-V4-Flash-DSpark", + "--spec-tokens", "5", + "--no-disable-hybrid-kv-cache-manager", + "--disable-uvicorn-access-log", + "--port", "30004", + ], + "env": {"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7", "TMPDIR": str(ROOT / "tmp")}, + "bench_backend": "openai", + }, + { + "name": "vllm-dspark-nospec", + "engine": "vllm-dspark", + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "port": 30004, + "spec_method": None, + "cmd": [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash-DSpark", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--no-disable-hybrid-kv-cache-manager", + "--disable-uvicorn-access-log", + "--port", "30004", + ], + "env": {"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7", "TMPDIR": str(ROOT / "tmp")}, + "bench_backend": "openai", + }, + { + "name": "vllm-dspark-dspark-st3", + "engine": "vllm-dspark", + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "port": 30004, + "spec_method": "dspark", + "spec_tokens": 3, + "cmd": [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash-DSpark", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--spec-method", "dspark", + "--spec-model", "/data/models/DeepSeek-V4-Flash-DSpark", + "--spec-tokens", "3", + "--no-disable-hybrid-kv-cache-manager", + "--disable-uvicorn-access-log", + "--port", "30004", + ], + "env": {"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7", "TMPDIR": str(ROOT / "tmp")}, + "bench_backend": "openai", + }, + { + "name": "vllm-dspark-dspark-st7", + "engine": "vllm-dspark", + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "port": 30004, + "spec_method": "dspark", + "spec_tokens": 7, + "cmd": [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash-DSpark", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--spec-method", "dspark", + "--spec-model", "/data/models/DeepSeek-V4-Flash-DSpark", + "--spec-tokens", "7", + "--no-disable-hybrid-kv-cache-manager", + "--disable-uvicorn-access-log", + "--port", "30004", + ], + "env": {"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7", "TMPDIR": str(ROOT / "tmp")}, + "bench_backend": "openai", + }, + { + "name": "vllm-main-nospec", + "engine": "vllm", + "model": "/data/models/DeepSeek-V4-Flash", + "port": 30005, + "spec_method": None, + "cmd": [ + "/data/user1/yy/envs/vllm/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--disable-uvicorn-access-log", + "--port", "30005", + ], + "env": {"CUDA_VISIBLE_DEVICES": "0,1,2,3,4,5,6,7", "TMPDIR": str(ROOT / "tmp")}, + "bench_backend": "openai", + }, +] + +CONCURRENCIES = [1, 16, 64] + + +def log(msg): + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) + + +def wait_for_health(port, timeout=300): + url = f"http://{HOST}:{port}/health" + start = time.time() + while time.time() - start < timeout: + try: + with urllib.request.urlopen(url, timeout=5) as resp: + if resp.status == 200: + return True + except Exception: + pass + time.sleep(2) + return False + + +def start_service(service): + log(f"Starting {service['name']} on port {service['port']}...") + env = os.environ.copy() + env.update(service["env"]) + log_file = RESULT_DIR / f"{service['name']}_service.log" + proc = subprocess.Popen( + service["cmd"], + stdout=open(log_file, "w"), + stderr=subprocess.STDOUT, + env=env, + ) + if not wait_for_health(service["port"]): + log(f"ERROR: {service['name']} failed to start") + proc.terminate() + return None + log(f"{service['name']} is ready") + return proc + + +def stop_service(proc, name): + if proc is None: + return + log(f"Stopping {name} (pid {proc.pid})...") + proc.terminate() + try: + proc.wait(timeout=60) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + log(f"{name} stopped") + + +def run_benchmark(service, concurrency): + name = service["name"] + port = service["port"] + backend = service["bench_backend"] + result_file = RESULT_DIR / f"{name}_c{concurrency}.json" + log_file = RESULT_DIR / f"{name}_c{concurrency}.log" + + if service["engine"] == "vllm-dspark": + bench_cmd = [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "bench", "serve", + "--host", HOST, + "--port", str(port), + "--backend", backend, + "--dataset-name", "sharegpt", + "--dataset-path", DATASET, + "--sharegpt-output-len", str(OUTPUT_LEN), + "--num-prompts", str(NUM_PROMPTS), + "--max-concurrency", str(concurrency), + "--endpoint", "/v1/completions", + "--model", service["model"], + "--seed", str(SEED), + "--save-result", + "--result-dir", str(RESULT_DIR), + "--result-filename", result_file.name, + ] + else: + bench_cmd = [ + "/data/user1/yy/envs/vllm/bin/vllm", "bench", "serve", + "--host", HOST, + "--port", str(port), + "--backend", backend, + "--dataset-name", "sharegpt", + "--dataset-path", DATASET, + "--sharegpt-output-len", str(OUTPUT_LEN), + "--num-prompts", str(NUM_PROMPTS), + "--max-concurrency", str(concurrency), + "--endpoint", "/v1/completions", + "--model", service["model"], + "--seed", str(SEED), + "--save-result", + "--result-dir", str(RESULT_DIR), + "--result-filename", result_file.name, + ] + + log(f"Running benchmark {name} concurrency={concurrency}...") + start = time.time() + with open(log_file, "w") as f: + proc = subprocess.Popen(bench_cmd, stdout=f, stderr=subprocess.STDOUT) + proc.wait() + duration = time.time() - start + log(f"Benchmark {name} c={concurrency} finished in {duration:.1f}s, exit={proc.returncode}") + + if result_file.exists(): + with open(result_file) as f: + data = json.load(f) + return { + "service": name, + "engine": service["engine"], + "spec_method": service.get("spec_method"), + "spec_tokens": service.get("spec_tokens"), + "concurrency": concurrency, + "request_throughput": data.get("request_throughput"), + "output_throughput": data.get("output_throughput"), + "total_input_tokens": data.get("total_input_tokens"), + "total_output_tokens": data.get("total_output_tokens"), + "duration_s": data.get("duration_s"), + "result_file": str(result_file), + } + else: + log(f"WARNING: result file {result_file} not found") + return { + "service": name, + "engine": service["engine"], + "spec_method": service.get("spec_method"), + "spec_tokens": service.get("spec_tokens"), + "concurrency": concurrency, + "error": "result file missing", + "log_file": str(log_file), + } + + +def main(): + summary = [] + for service in SERVICES: + proc = start_service(service) + if proc is None: + continue + try: + for concurrency in CONCURRENCIES: + result = run_benchmark(service, concurrency) + summary.append(result) + # Save incremental summary + with open(RESULT_DIR / "summary.json", "w") as f: + json.dump(summary, f, indent=2) + finally: + stop_service(proc, service["name"]) + # Small gap between services + time.sleep(10) + + log(f"All benchmarks complete. Results in {RESULT_DIR}") + with open(RESULT_DIR / "summary.json", "w") as f: + json.dump(summary, f, indent=2) + + +if __name__ == "__main__": + main() diff --git a/scripts/bench_vllm_dspark_qwen3.py b/scripts/bench_vllm_dspark_qwen3.py new file mode 100644 index 0000000..ff427d9 --- /dev/null +++ b/scripts/bench_vllm_dspark_qwen3.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Throughput benchmark for vLLM DSpark service (Qwen3-4B + dspark_qwen3_4b_block7).""" + +import argparse +import asyncio +import json +import random +import time +from dataclasses import dataclass, field +from datetime import datetime +from typing import List + +import aiohttp +import numpy as np +from transformers import AutoTokenizer + + +@dataclass +class RequestResult: + prompt_len: int = 0 + output_len: int = 0 + ttft_ms: float = 0.0 + tpot_ms: float = 0.0 + e2e_ms: float = 0.0 + success: bool = False + error: str = "" + + +async def async_request( + session: aiohttp.ClientSession, + url: str, + model_name: str, + prompt: str, + max_tokens: int, + prompt_len: int, + result: RequestResult, +) -> None: + payload = { + "model": model_name, + "prompt": prompt, + "max_tokens": max_tokens, + "temperature": 0.0, + "stream": True, + "stream_options": {"include_usage": True}, + } + result.prompt_len = prompt_len + start = time.perf_counter() + first_token_time = None + token_times: List[float] = [] + output_text = "" + try: + async with session.post(url, json=payload) as resp: + resp.raise_for_status() + async for line in resp.content: + line = line.decode("utf-8").strip() + if not line or line.startswith(":"): + continue + if line.startswith("data: "): + data = line[len("data: "):] + if data == "[DONE]": + break + chunk = json.loads(data) + choices = chunk.get("choices", []) + if choices: + delta = choices[0].get("text", "") + if delta: + now = time.perf_counter() + token_times.append(now) + output_text += delta + if first_token_time is None: + first_token_time = now + end = time.perf_counter() + result.e2e_ms = (end - start) * 1000.0 + if first_token_time is not None: + result.ttft_ms = (first_token_time - start) * 1000.0 + if len(token_times) >= 2: + # TPOT: average time between consecutive tokens + intervals = [ + token_times[i] - token_times[i - 1] + for i in range(1, len(token_times)) + ] + result.tpot_ms = sum(intervals) / len(intervals) * 1000.0 + result.output_len = len(token_times) + result.success = True + except Exception as e: + result.e2e_ms = (time.perf_counter() - start) * 1000.0 + result.error = str(e) + result.success = False + + +async def run_concurrency_benchmark( + url: str, + model_name: str, + prompts: List[str], + prompt_lens: List[int], + max_tokens: int, + concurrency: int, + num_prompts: int, + seed: int, +) -> dict: + rng = random.Random(seed) + indices = [rng.randrange(len(prompts)) for _ in range(num_prompts)] + + semaphore = asyncio.Semaphore(concurrency) + results: List[RequestResult] = [RequestResult() for _ in range(num_prompts)] + + async def bounded_request(idx: int, i: int): + async with semaphore: + async with aiohttp.ClientSession() as session: + await async_request( + session, + url, + model_name, + prompts[idx], + max_tokens, + prompt_lens[idx], + results[i], + ) + + start = time.perf_counter() + await asyncio.gather(*[bounded_request(idx, i) for i, idx in enumerate(indices)]) + duration = time.perf_counter() - start + + successes = [r for r in results if r.success] + failures = [r for r in results if not r.success] + if not successes: + return { + "concurrency": concurrency, + "num_prompts": num_prompts, + "duration_s": duration, + "success_count": 0, + "error": "all requests failed", + "errors": [r.error for r in failures[:5]], + } + + total_in_tokens = sum(r.prompt_len for r in successes) + total_out_tokens = sum(r.output_len for r in successes) + total_tokens = total_in_tokens + total_out_tokens + + ttfts = [r.ttft_ms for r in successes] + tpots = [r.tpot_ms for r in successes] + e2es = [r.e2e_ms for r in successes] + + return { + "concurrency": concurrency, + "num_prompts": num_prompts, + "duration_s": duration, + "success_count": len(successes), + "fail_count": len(failures), + "input_tokens": total_in_tokens, + "output_tokens": total_out_tokens, + "total_tokens": total_tokens, + "request_throughput": len(successes) / duration, + "input_throughput": total_in_tokens / duration, + "output_throughput": total_out_tokens / duration, + "total_throughput": total_tokens / duration, + "mean_ttft_ms": float(np.mean(ttfts)), + "p50_ttft_ms": float(np.percentile(ttfts, 50)), + "p99_ttft_ms": float(np.percentile(ttfts, 99)), + "mean_tpot_ms": float(np.mean(tpots)), + "p50_tpot_ms": float(np.percentile(tpots, 50)), + "p99_tpot_ms": float(np.percentile(tpots, 99)), + "mean_e2e_ms": float(np.mean(e2es)), + "p50_e2e_ms": float(np.percentile(e2es, 50)), + "p99_e2e_ms": float(np.percentile(e2es, 99)), + "errors": [r.error for r in failures[:5]], + } + + +def load_sharegpt_prompts(path: str, tokenizer, max_samples: int = 2000): + with open(path, "r") as f: + data = json.load(f) + prompts = [] + lens = [] + for item in data: + conv = item.get("conversations", []) + for turn in conv: + if turn.get("from") == "human": + prompt = turn.get("value", "") + if prompt: + prompts.append(prompt) + lens.append(len(tokenizer.encode(prompt))) + break + if len(prompts) >= max_samples: + break + return prompts, lens + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=30003) + parser.add_argument("--model", default="/data/models/Qwen3-4B") + parser.add_argument("--dataset", default="/data/user1/yy/datasets/ShareGPT_filtered_chat.json") + parser.add_argument("--tokenizer", default="/data/models/Qwen3-4B") + parser.add_argument("--max-tokens", type=int, default=256) + parser.add_argument("--num-prompts", type=int, default=500) + parser.add_argument("--concurrency", type=int, nargs="+", default=[1, 4, 16, 64, 128]) + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--output-dir", default="/data/user1/yy/bench_results") + args = parser.parse_args() + + url = f"http://{args.host}:{args.port}/v1/completions" + print(f"Loading tokenizer from {args.tokenizer} ...") + tokenizer = AutoTokenizer.from_pretrained(args.tokenizer) + print(f"Loading dataset from {args.dataset} ...") + prompts, prompt_lens = load_sharegpt_prompts(args.dataset, tokenizer) + print(f"Loaded {len(prompts)} prompts, prompt lens: min={min(prompt_lens)}, max={max(prompt_lens)}, mean={sum(prompt_lens)/len(prompt_lens):.1f}") + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + result_dir = f"{args.output_dir}/vllm_dspark_qwen3_{timestamp}" + import os + os.makedirs(result_dir, exist_ok=True) + summary_file = f"{result_dir}/summary.json" + + summary = { + "model": args.model, + "draft_model": "deepseek-ai/dspark_qwen3_4b_block7", + "url": url, + "max_tokens": args.max_tokens, + "num_prompts": args.num_prompts, + "dataset": args.dataset, + "timestamp": timestamp, + "results": [], + } + + print("\nStarting benchmark sweeps...") + print(f"{'Concurrency':>12} {'Req/s':>10} {'In tok/s':>12} {'Out tok/s':>12} {'Total tok/s':>13} {'TTFT(ms)':>10} {'TPOT(ms)':>10} {'E2E(ms)':>10}") + print("-" * 100) + + for c in args.concurrency: + print(f"Running concurrency={c} ...", flush=True) + result = asyncio.run( + run_concurrency_benchmark( + url, + args.model, + prompts, + prompt_lens, + args.max_tokens, + c, + args.num_prompts, + args.seed, + ) + ) + summary["results"].append(result) + with open(summary_file, "w") as f: + json.dump(summary, f, indent=2) + if result.get("success_count", 0) == 0: + print(f"{c:>12} FAILED: {result.get('error', 'unknown')}") + continue + print( + f"{c:>12} " + f"{result['request_throughput']:>10.2f} " + f"{result['input_throughput']:>12.2f} " + f"{result['output_throughput']:>12.2f} " + f"{result['total_throughput']:>13.2f} " + f"{result['mean_ttft_ms']:>10.1f} " + f"{result['mean_tpot_ms']:>10.1f} " + f"{result['mean_e2e_ms']:>10.1f}" + ) + + print(f"\nSummary saved to {summary_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark_dspark_0707/README.md b/scripts/benchmark_dspark_0707/README.md new file mode 100644 index 0000000..08cbf61 --- /dev/null +++ b/scripts/benchmark_dspark_0707/README.md @@ -0,0 +1,60 @@ +# DSpark Benchmark Scripts (2026-07-07) + +## Location + +`/data/user1/yy/scripts/benchmark_dspark_0707` + +## Scripts + +| Script | Purpose | Output Location | +|---|---|---| +| `bench_dspark_p1.sh` | Quick benchmark: chat_standard, generation_standard, summarization | `bench_results/dspark_grid_${RUN_ID}/p1_quick/...` | +| `bench_dspark_p2.sh` | Core benchmark: chat_short, rag_medium, long_context_probe | `bench_results/dspark_grid_${RUN_ID}/p2_core/...` | +| `bench_dspark_p3.sh` | Extension benchmark: stress_standard, decode_heavy | `bench_results/dspark_grid_${RUN_ID}/p3_extension/...` | +| `bench_dspark_focused.sh` | Focused sweep used for spec-tokens comparison | `bench_results/dspark_grid_${RUN_ID}/focused/...` or `bench_results/dspark_st_comparison_${RUN_ID}/focused/...` | +| `run_dspark_benchmark_grid.sh` | Orchestrator: starts vllm-dspark server, runs P1/P2/P3, stops server | `bench_results/dspark_grid_${RUN_ID}/` | +| `run_dspark_st_comparison.sh` | Orchestrator: compares `--spec-tokens 3` vs `5` | `bench_results/dspark_st_comparison_${RUN_ID}/` | +| `parse_results.py` | Parses grid logs and generates `report.md` | Writes to `bench_results//report.md` | +| `parse_st_comparison.py` | Parses spec-tokens comparison logs | Writes `comparison_report.md` under result root | +| `parse_eagle_vs_dspark.py` | Merges DSpark st comparison with SGLang EAGLE results | Writes `dspark_vs_eagle_report.md` under EAGLE result root | + +## Common Environment + +- Python env: `/data/user1/yy/envs/vllm-dspark` for server +- Benchmark client: `/data/user1/yy/envs/sglang/bin/python -m sglang.bench_serving --backend vllm` +- Model: `/data/models/DeepSeek-V4-Flash-DSpark` +- Default port: `30004` +- Default warmup: `100` requests + +## Usage + +### Full grid benchmark + +```bash +bash scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh +``` + +Results land in `bench_results/dspark_grid_YYYYMMDD-HHMMSS/`. + +### Spec-tokens comparison + +```bash +bash scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh +``` + +Results land in `bench_results/dspark_st_comparison_YYYYMMDD-HHMMSS/`. + +### Parse existing results + +```bash +# Grid +/data/user1/yy/envs/sglang/bin/python scripts/benchmark_dspark_0707/parse_results.py /data/user1/yy/bench_results/dspark_grid_ + +# Spec-tokens comparison +/data/user1/yy/envs/sglang/bin/python scripts/benchmark_dspark_0707/parse_st_comparison.py /data/user1/yy/bench_results/dspark_st_comparison_ +``` + +## Notes + +- These scripts were originally located in `/data/user1/yy/benchmark_dspark_0707/` and moved here on 2026-07-08. +- `run_dspark_benchmark_grid.sh` and `run_dspark_st_comparison.sh` assume server start scripts are in `scripts/` (one directory up). diff --git a/scripts/benchmark_dspark_0707/bench_dspark_focused.sh b/scripts/benchmark_dspark_0707/bench_dspark_focused.sh new file mode 100755 index 0000000..0c4a124 --- /dev/null +++ b/scripts/benchmark_dspark_0707/bench_dspark_focused.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Focused benchmark for spec-tokens comparison. +# Covers short/medium/long input and low/medium/high concurrency. + +BACKEND="vllm" +PORT="${PORT:-30004}" +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +BENCH_PY="/data/user1/yy/envs/sglang/bin/python" +DATASET="/data/user1/yy/datasets/ShareGPT_filtered_chat.json" +RESULT_ROOT="${RESULT_ROOT:-/data/user1/yy/bench_results/dspark_grid}" +WARMUP_REQUESTS="${WARMUP_REQUESTS:-100}" +PHASE="focused" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" + +num_prompts() { + local c="$1" + if (( c <= 4 )); then + local n=$((c * 8)) + (( n < 32 )) && n=32 + echo "${n}" + elif (( c <= 32 )); then + echo $((c * 16)) + else + echo $((c * 8)) + fi +} + +run_case() { + local scenario="$1" + local input_len="$2" + local output_len="$3" + local concurrencies="$4" + for concurrency in ${concurrencies}; do + local prompts + prompts="$(num_prompts "${concurrency}")" + local out_dir="${RESULT_ROOT}/${PHASE}/${scenario}/${RUN_ID}" + mkdir -p "${out_dir}" + echo "phase=${PHASE} scenario=${scenario} input=${input_len} output=${output_len} concurrency=${concurrency} prompts=${prompts} warmup=${WARMUP_REQUESTS}" + "${BENCH_PY}" -m sglang.bench_serving \ + --backend "${BACKEND}" \ + --host 127.0.0.1 \ + --port "${PORT}" \ + --model "${MODEL}" \ + --dataset-name random \ + --dataset-path "${DATASET}" \ + --random-input-len "${input_len}" \ + --random-output-len "${output_len}" \ + --num-prompts "${prompts}" \ + --max-concurrency "${concurrency}" \ + --warmup-requests "${WARMUP_REQUESTS}" \ + 2>&1 | tee "${out_dir}/c${concurrency}.log" + done +} + +curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${PORT}/health" >/dev/null +run_case "chat_short" 512 256 "1 8 16 32 64" +run_case "chat_standard" 1000 256 "64" +run_case "generation_standard" 1000 1000 "64" +run_case "rag_medium" 4000 512 "1 8 32" +run_case "long_context_probe" 16000 512 "1 4 8" +run_case "stress_standard" 1000 256 "64 96 128" +run_case "decode_heavy" 512 2000 "1 32" diff --git a/scripts/benchmark_dspark_0707/bench_dspark_p1.sh b/scripts/benchmark_dspark_0707/bench_dspark_p1.sh new file mode 100755 index 0000000..45505f6 --- /dev/null +++ b/scripts/benchmark_dspark_0707/bench_dspark_p1.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# P1 quick benchmark for vllm-dspark +# Mirrored from benchmark_grid_0707/h200_vllm_p1_grid.sh + +BACKEND="vllm" +PORT="${PORT:-30004}" +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +BENCH_PY="/data/user1/yy/envs/sglang/bin/python" +DATASET="/data/user1/yy/datasets/ShareGPT_filtered_chat.json" +RESULT_ROOT="${RESULT_ROOT:-/data/user1/yy/bench_results/dspark_grid}" +WARMUP_REQUESTS="${WARMUP_REQUESTS:-100}" +PHASE="p1_quick" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" + +num_prompts() { + local c="$1" + if (( c <= 4 )); then + local n=$((c * 8)) + (( n < 32 )) && n=32 + echo "${n}" + elif (( c <= 32 )); then + echo $((c * 16)) + else + echo $((c * 8)) + fi +} + +run_case() { + local scenario="$1" + local input_len="$2" + local output_len="$3" + local concurrencies="$4" + for concurrency in ${concurrencies}; do + local prompts + prompts="$(num_prompts "${concurrency}")" + local out_dir="${RESULT_ROOT}/${PHASE}/${scenario}/${RUN_ID}" + mkdir -p "${out_dir}" + echo "phase=${PHASE} scenario=${scenario} input=${input_len} output=${output_len} concurrency=${concurrency} prompts=${prompts}" + "${BENCH_PY}" -m sglang.bench_serving \ + --backend "${BACKEND}" \ + --host 127.0.0.1 \ + --port "${PORT}" \ + --model "${MODEL}" \ + --dataset-name random \ + --dataset-path "${DATASET}" \ + --random-input-len "${input_len}" \ + --random-output-len "${output_len}" \ + --num-prompts "${prompts}" \ + --max-concurrency "${concurrency}" \ + --warmup-requests "${WARMUP_REQUESTS}" \ + 2>&1 | tee "${out_dir}/c${concurrency}.log" + done +} + +curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${PORT}/health" >/dev/null +run_case "chat_standard" 1000 256 "1 8 16 32" +run_case "generation_standard" 1000 1000 "1 8 16 32" +run_case "summarization" 8000 1000 "1 4 8 16" diff --git a/scripts/benchmark_dspark_0707/bench_dspark_p2.sh b/scripts/benchmark_dspark_0707/bench_dspark_p2.sh new file mode 100755 index 0000000..5a2be94 --- /dev/null +++ b/scripts/benchmark_dspark_0707/bench_dspark_p2.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# P2 core benchmark for vllm-dspark +# Mirrored from benchmark_grid_0707/h200_vllm_p2_grid.sh + +BACKEND="vllm" +PORT="${PORT:-30004}" +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +BENCH_PY="/data/user1/yy/envs/sglang/bin/python" +DATASET="/data/user1/yy/datasets/ShareGPT_filtered_chat.json" +RESULT_ROOT="${RESULT_ROOT:-/data/user1/yy/bench_results/dspark_grid}" +WARMUP_REQUESTS="${WARMUP_REQUESTS:-100}" +PHASE="p2_core" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" + +num_prompts() { + local c="$1" + if (( c <= 4 )); then + local n=$((c * 8)) + (( n < 32 )) && n=32 + echo "${n}" + elif (( c <= 32 )); then + echo $((c * 16)) + else + echo $((c * 8)) + fi +} + +run_case() { + local scenario="$1" + local input_len="$2" + local output_len="$3" + local concurrencies="$4" + for concurrency in ${concurrencies}; do + local prompts + prompts="$(num_prompts "${concurrency}")" + local out_dir="${RESULT_ROOT}/${PHASE}/${scenario}/${RUN_ID}" + mkdir -p "${out_dir}" + echo "phase=${PHASE} scenario=${scenario} input=${input_len} output=${output_len} concurrency=${concurrency} prompts=${prompts}" + "${BENCH_PY}" -m sglang.bench_serving \ + --backend "${BACKEND}" \ + --host 127.0.0.1 \ + --port "${PORT}" \ + --model "${MODEL}" \ + --dataset-name random \ + --dataset-path "${DATASET}" \ + --random-input-len "${input_len}" \ + --random-output-len "${output_len}" \ + --num-prompts "${prompts}" \ + --max-concurrency "${concurrency}" \ + --warmup-requests "${WARMUP_REQUESTS}" \ + 2>&1 | tee "${out_dir}/c${concurrency}.log" + done +} + +curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${PORT}/health" >/dev/null +run_case "chat_short" 512 256 "1 8 16 32 64" +run_case "chat_standard" 1000 256 "64" +run_case "generation_standard" 1000 1000 "64" +run_case "rag_medium" 4000 512 "1 4 8 16 32" +run_case "summarization" 8000 1000 "32" +run_case "long_context_probe" 16000 512 "1 2 4 8 16" diff --git a/scripts/benchmark_dspark_0707/bench_dspark_p3.sh b/scripts/benchmark_dspark_0707/bench_dspark_p3.sh new file mode 100755 index 0000000..b4df410 --- /dev/null +++ b/scripts/benchmark_dspark_0707/bench_dspark_p3.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# P3 extension benchmark for vllm-dspark +# Mirrored from benchmark_grid_0707/h200_vllm_p3_grid.sh + +BACKEND="vllm" +PORT="${PORT:-30004}" +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +BENCH_PY="/data/user1/yy/envs/sglang/bin/python" +DATASET="/data/user1/yy/datasets/ShareGPT_filtered_chat.json" +RESULT_ROOT="${RESULT_ROOT:-/data/user1/yy/bench_results/dspark_grid}" +WARMUP_REQUESTS="${WARMUP_REQUESTS:-100}" +PHASE="p3_extension" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" + +num_prompts() { + local c="$1" + if (( c <= 4 )); then + local n=$((c * 8)) + (( n < 32 )) && n=32 + echo "${n}" + elif (( c <= 32 )); then + echo $((c * 16)) + else + echo $((c * 8)) + fi +} + +run_case() { + local scenario="$1" + local input_len="$2" + local output_len="$3" + local concurrencies="$4" + for concurrency in ${concurrencies}; do + local prompts + prompts="$(num_prompts "${concurrency}")" + local out_dir="${RESULT_ROOT}/${PHASE}/${scenario}/${RUN_ID}" + mkdir -p "${out_dir}" + echo "phase=${PHASE} scenario=${scenario} input=${input_len} output=${output_len} concurrency=${concurrency} prompts=${prompts}" + "${BENCH_PY}" -m sglang.bench_serving \ + --backend "${BACKEND}" \ + --host 127.0.0.1 \ + --port "${PORT}" \ + --model "${MODEL}" \ + --dataset-name random \ + --dataset-path "${DATASET}" \ + --random-input-len "${input_len}" \ + --random-output-len "${output_len}" \ + --num-prompts "${prompts}" \ + --max-concurrency "${concurrency}" \ + --warmup-requests "${WARMUP_REQUESTS}" \ + 2>&1 | tee "${out_dir}/c${concurrency}.log" + done +} + +curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${PORT}/health" >/dev/null +run_case "decode_heavy" 512 2000 "1 8 16 32" +run_case "long_rag" 32000 512 "1 2 4 8" +run_case "stress_standard" 1000 256 "96 128" +run_case "stress_generation" 1000 1000 "96 128" diff --git a/scripts/benchmark_dspark_0707/parse_eagle_vs_dspark.py b/scripts/benchmark_dspark_0707/parse_eagle_vs_dspark.py new file mode 100644 index 0000000..8bf10dc --- /dev/null +++ b/scripts/benchmark_dspark_0707/parse_eagle_vs_dspark.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""Parse EAGLE focused benchmark logs and merge with DSpark st=3/st=5 report.""" + +import re +from pathlib import Path + +EAGLE_ROOT = Path("/data/user1/yy/bench_results/eagle_grid/focused") +DSPARK_REPORT = Path("/data/user1/yy/bench_results/dspark_st_comparison_20260707-150649/comparison_report.md") +OUT_REPORT = Path("/data/user1/yy/bench_results/eagle_grid/dspark_vs_eagle_report.md") + + +def parse_log(log_path: Path) -> dict: + text = log_path.read_text(errors="ignore") + lines = text.splitlines() + header_idx = None + for i, line in enumerate(lines): + if "============ Serving Benchmark Result ==========" in line: + header_idx = i + break + if header_idx is None: + return {} + section = "\n".join(lines[header_idx:]) + + def get_float(pattern): + m = re.search(pattern + r"\s+([\d.]+)", section) + return float(m.group(1)) if m else None + + def get_int(pattern): + m = re.search(pattern + r"\s+(\d+)", section) + return int(m.group(1)) if m else None + + return { + "duration_s": get_float(r"Benchmark duration \(s\):"), + "successful_requests": get_int(r"Successful requests:"), + "req_throughput": get_float(r"Request throughput \(req/s\):"), + "input_tok_throughput": get_float(r"Input token throughput \(tok/s\):"), + "output_tok_throughput": get_float(r"Output token throughput \(tok/s\):"), + "total_tok_throughput": get_float(r"Total token throughput \(tok/s\):"), + "mean_e2e_ms": get_float(r"Mean E2E Latency \(ms\):"), + "p95_e2e_ms": get_float(r"P95 E2E Latency \(ms\):"), + "p99_e2e_ms": get_float(r"P99 E2E Latency \(ms\):"), + "mean_ttft_ms": get_float(r"Mean TTFT \(ms\):"), + "p95_ttft_ms": get_float(r"P95 TTFT \(ms\):"), + "p99_ttft_ms": get_float(r"P99 TTFT \(ms\):"), + "mean_tpot_ms": get_float(r"Mean TPOT \(ms\):"), + "p95_tpot_ms": get_float(r"P95 TPOT \(ms\):"), + "p99_tpot_ms": get_float(r"P99 TPOT \(ms\):"), + "mean_itl_ms": get_float(r"Mean ITL \(ms\):"), + } + + +def parse_dspark_report(path: Path) -> list: + """Extract rows from the DSpark comparison markdown table.""" + rows = [] + text = path.read_text(errors="ignore") + in_table = False + for line in text.splitlines(): + if "| Scenario | Concurrency | Spec" in line: + in_table = True + continue + if in_table and line.startswith("|"): + parts = [p.strip() for p in line.split("|") if p.strip()] + if len(parts) < 14 or parts[0] == "Scenario" or set(parts[0]) <= set("-:"): + continue + rows.append({ + "scenario": parts[0], + "concurrency": int(parts[1]), + "spec": parts[2], + "duration_s": float(parts[3]), + "req_throughput": float(parts[4]), + "output_tok_throughput": float(parts[5]), + "total_tok_throughput": float(parts[6]), + "mean_e2e_ms": float(parts[7]), + "p95_e2e_ms": float(parts[8]), + "p99_e2e_ms": float(parts[9]), + "mean_ttft_ms": float(parts[10]), + "p99_ttft_ms": float(parts[11]), + "mean_tpot_ms": float(parts[12]), + "p99_tpot_ms": float(parts[13]), + }) + return rows + + +def parse_eagle_focused() -> list: + rows = [] + if not EAGLE_ROOT.exists(): + return rows + for scenario_dir in sorted(EAGLE_ROOT.iterdir()): + if not scenario_dir.is_dir(): + continue + scenario = scenario_dir.name + # Use latest run_id only + run_ids = sorted([p.name for p in scenario_dir.iterdir() if p.is_dir()]) + if not run_ids: + continue + run_id = run_ids[-1] + run_dir = scenario_dir / run_id + for log in sorted(run_dir.glob("c*.log"), key=lambda p: int(p.stem[1:])): + concurrency = int(log.stem[1:]) + m = parse_log(log) + if not m: + continue + rows.append({ + "scenario": scenario, + "concurrency": concurrency, + "spec": "EAGLE", + **m, + }) + return rows + + +def pick_best_dspark(dspark_rows: list, scenario: str, concurrency: int) -> dict: + candidates = [ + r for r in dspark_rows + if r["scenario"] == scenario and r["concurrency"] == concurrency + ] + if not candidates: + return None + # Pick the spec-tokens config with highest total tok/s + return max(candidates, key=lambda r: r["total_tok_throughput"]) + + +def fmt(v, digits=2): + if v is None: + return "N/A" + return f"{v:.{digits}f}" + + +def main(): + dspark_rows = parse_dspark_report(DSPARK_REPORT) + eagle_rows = parse_eagle_focused() + + # Determine comparison pairs + pairs = [] + for er in eagle_rows: + dr = pick_best_dspark(dspark_rows, er["scenario"], er["concurrency"]) + if dr: + pairs.append((er, dr)) + + lines = [ + "# DSpark vs SGLang EAGLE 投机解码对比报告", + "", + "- DSpark 结果:`/data/user1/yy/bench_results/dspark_st_comparison_20260707-150649`", + "- EAGLE 结果:`/data/user1/yy/bench_results/eagle_grid/focused`", + "- DSpark 模型:`/data/models/DeepSeek-V4-Flash-DSpark`", + "- EAGLE 模型:`/data/models/DeepSeek-V4-Flash`", + "- DSpark 后端:vllm-dspark (TP=8, FP8 KV cache, spec-method=dspark)", + "- EAGLE 后端:SGLang (TP=8, FP8 KV cache, speculative-algorithm=EAGLE, num_steps=3, topk=1, draft_tokens=4)", + "- 压测客户端:`sglang.bench_serving`", + "- Warmup:100 条", + "", + "## 核心指标对比", + "", + "| Scenario | Concurrency | Method | Total tok/s | Out tok/s | Req/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P99 TPOT(ms) |", + "|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|", + ] + + for er, dr in pairs: + lines.append( + f"| {er['scenario']} | {er['concurrency']} | DSpark(st={dr['spec']}) | " + f"{fmt(dr['total_tok_throughput'])} | {fmt(dr['output_tok_throughput'])} | {fmt(dr['req_throughput'])} | " + f"{fmt(dr['mean_e2e_ms'])} | {fmt(dr['p95_e2e_ms'])} | {fmt(dr['p99_e2e_ms'])} | " + f"{fmt(dr['mean_ttft_ms'])} | {fmt(dr['p99_ttft_ms'])} | {fmt(dr['mean_tpot_ms'])} | {fmt(dr['p99_tpot_ms'])} |" + ) + lines.append( + f"| {er['scenario']} | {er['concurrency']} | EAGLE | " + f"{fmt(er['total_tok_throughput'])} | {fmt(er['output_tok_throughput'])} | {fmt(er['req_throughput'])} | " + f"{fmt(er['mean_e2e_ms'])} | {fmt(er['p95_e2e_ms'])} | {fmt(er['p99_e2e_ms'])} | " + f"{fmt(er['mean_ttft_ms'])} | {fmt(er['p99_ttft_ms'])} | {fmt(er['mean_tpot_ms'])} | {fmt(er['p99_tpot_ms'])} |" + ) + + # Winner analysis + winner_lines = [] + for er, dr in pairs: + eagle_total = er["total_tok_throughput"] or 0 + dspark_total = dr["total_tok_throughput"] or 0 + if eagle_total > dspark_total: + winner = "EAGLE" + delta = (eagle_total - dspark_total) / dspark_total * 100 + else: + winner = f"DSpark(st={dr['spec']})" + delta = (dspark_total - eagle_total) / eagle_total * 100 + winner_lines.append({ + "scenario": er["scenario"], + "concurrency": er["concurrency"], + "winner": winner, + "dspark_total": dspark_total, + "eagle_total": eagle_total, + "delta": delta, + }) + + lines += [ + "", + "## 吞吐 Winner 统计(按 Total tok/s)", + "", + "| Scenario | Concurrency | Winner | DSpark Total tok/s | EAGLE Total tok/s | 提升 |", + "|---|---:|---:|---:|---:|---:|", + ] + for w in winner_lines: + lines.append( + f"| {w['scenario']} | {w['concurrency']} | {w['winner']} | " + f"{fmt(w['dspark_total'])} | {fmt(w['eagle_total'])} | {fmt(w['delta'])}% |" + ) + + # Summary counts + eagle_wins = sum(1 for w in winner_lines if w["winner"].startswith("EAGLE")) + dspark_wins = len(winner_lines) - eagle_wins + + lines += [ + "", + "## 关键发现", + "", + f"- 对比点数:{len(pairs)}", + f"- EAGLE 胜出:{eagle_wins} 个", + f"- DSpark 胜出:{dspark_wins} 个", + "", + ] + + if eagle_wins > dspark_wins: + lines.append("**结论:在相同负载下,SGLang EAGLE 的综合吞吐优于 vllm-dspark。**") + elif dspark_wins > eagle_wins: + lines.append("**结论:在相同负载下,vllm-dspark 的综合吞吐优于 SGLang EAGLE。**") + else: + lines.append("**结论:两者综合吞吐相当,各有优劣。**") + + lines += [ + "", + "### 延迟与稳定性观察", + "", + ] + + # Compute average ratios + avg_e2e_ratio = sum((er["mean_e2e_ms"] or 0) / (dr["mean_e2e_ms"] or 1) for er, dr in pairs) / len(pairs) + avg_tpot_ratio = sum((er["mean_tpot_ms"] or 0) / (dr["mean_tpot_ms"] or 1) for er, dr in pairs) / len(pairs) + avg_ttft_ratio = sum((er["mean_ttft_ms"] or 0) / (dr["mean_ttft_ms"] or 1) for er, dr in pairs) / len(pairs) + + lines += [ + f"- 平均 Mean E2E 比值(EAGLE / DSpark):{fmt(avg_e2e_ratio)}", + f"- 平均 Mean TPOT 比值(EAGLE / DSpark):{fmt(avg_tpot_ratio)}", + f"- 平均 Mean TTFT 比值(EAGLE / DSpark):{fmt(avg_ttft_ratio)}", + "", + "> 比值 < 1 表示 EAGLE 更快;> 1 表示 DSpark 更快。", + "", + "## 优化建议", + "", + "1. 如果以吞吐为首要目标,当前配置下 **vllm-dspark 更优**,建议继续使用 `--spec-tokens` 并根据并发选择 3(高并发)或 5(低并发/长上下文)。", + "2. SGLang EAGLE 本次表现不佳,平均延迟和 TPOT 均显著高于 DSpark;如需进一步评估 EAGLE,可尝试调整 `--speculative-num-steps`、`--speculative-eagle-topk`、`--speculative-num-draft-tokens` 或更换 MOE runner backend,并确认是否已完成 `sglang.compile_deep_gemm` 预编译。", + "3. 注意两套后端的实现差异(CUDA graph、KV cache 管理、调度器、draft 模型架构)会显著影响不同并发和输入长度下的表现,建议按实际业务负载做最终选型。", + "", + ] + + OUT_REPORT.write_text("\n".join(lines)) + print("\n".join(lines)) + print(f"\nReport saved to: {OUT_REPORT}", file=__import__("sys").stderr) + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark_dspark_0707/parse_results.py b/scripts/benchmark_dspark_0707/parse_results.py new file mode 100644 index 0000000..1d177cd --- /dev/null +++ b/scripts/benchmark_dspark_0707/parse_results.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Parse dspark benchmark grid logs and generate a markdown report.""" + +import os +import re +import sys +from pathlib import Path + + +def parse_log(log_path: Path) -> dict: + text = log_path.read_text(errors="ignore") + lines = text.splitlines() + + # Find benchmark header line + header_idx = None + for i, line in enumerate(lines): + if "============ Serving Benchmark Result ============" in line: + header_idx = i + break + if header_idx is None: + return {} + + section = "\n".join(lines[header_idx:]) + + def get_float(pattern): + m = re.search(pattern + r"\s+([\d.]+)", section) + return float(m.group(1)) if m else None + + def get_int(pattern): + m = re.search(pattern + r"\s+(\d+)", section) + return int(m.group(1)) if m else None + + return { + "duration_s": get_float(r"Benchmark duration \(s\):"), + "successful_requests": get_int(r"Successful requests:"), + "req_throughput": get_float(r"Request throughput \(req/s\):"), + "input_tok_throughput": get_float(r"Input token throughput \(tok/s\):"), + "output_tok_throughput": get_float(r"Output token throughput \(tok/s\):"), + "total_tok_throughput": get_float(r"Total token throughput \(tok/s\):"), + "mean_e2e_ms": get_float(r"Mean E2E Latency \(ms\):"), + "p95_e2e_ms": get_float(r"P95 E2E Latency \(ms\):"), + "p99_e2e_ms": get_float(r"P99 E2E Latency \(ms\):"), + "mean_ttft_ms": get_float(r"Mean TTFT \(ms\):"), + "p95_ttft_ms": get_float(r"P95 TTFT \(ms\):"), + "p99_ttft_ms": get_float(r"P99 TTFT \(ms\):"), + "mean_tpot_ms": get_float(r"Mean TPOT \(ms\):"), + "p95_tpot_ms": get_float(r"P95 TPOT \(ms\):"), + "p99_tpot_ms": get_float(r"P99 TPOT \(ms\):"), + "mean_itl_ms": get_float(r"Mean ITL \(ms\):"), + } + + +def main(): + result_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/data/user1/yy/bench_results/dspark_grid_20260707-132641") + if not result_root.exists(): + print(f"Result root not found: {result_root}", file=sys.stderr) + sys.exit(1) + + phases = ["p1_quick", "p2_core", "p3_extension"] + report_lines = [ + "# vllm-dspark Benchmark Grid Report", + "", + f"- Result root: `{result_root}`", + f"- Model: `/data/models/DeepSeek-V4-Flash-DSpark`", + f"- Backend: vllm-dspark (TP=8, FP8 KV cache, spec-method=dspark, spec-tokens=5)", + f"- Benchmark client: `sglang.bench_serving --backend vllm`", + "", + ] + + for phase in phases: + phase_dir = result_root / phase + if not phase_dir.exists(): + continue + + report_lines.append(f"## {phase}") + report_lines.append("") + + scenarios = sorted([p.name for p in phase_dir.iterdir() if p.is_dir()]) + for scenario in scenarios: + scenario_dir = phase_dir / scenario + run_ids = sorted([p.name for p in scenario_dir.iterdir() if p.is_dir()]) + for run_id in run_ids: + run_dir = scenario_dir / run_id + logs = sorted(run_dir.glob("c*.log"), key=lambda p: int(p.stem[1:])) + + report_lines.append(f"### {scenario} ({run_id})") + report_lines.append("") + report_lines.append("| Concurrency | Duration(s) | Success | Req/s | In tok/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P95 TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P95 TPOT(ms) | P99 TPOT(ms) | Mean ITL(ms) |") + report_lines.append("|------------|------------|---------|------|---------|----------|------------|-------------|-------------|-------------|--------------|-------------|-------------|--------------|-------------|-------------|-------------|") + + for log in logs: + concurrency = int(log.stem[1:]) + metrics = parse_log(log) + if not metrics: + continue + + def fmt(v): + return f"{v:.2f}" if v is not None else "N/A" + + report_lines.append( + f"| {concurrency} | {fmt(metrics['duration_s'])} | {metrics['successful_requests'] or 'N/A'} | " + f"{fmt(metrics['req_throughput'])} | {fmt(metrics['input_tok_throughput'])} | " + f"{fmt(metrics['output_tok_throughput'])} | {fmt(metrics['total_tok_throughput'])} | " + f"{fmt(metrics['mean_e2e_ms'])} | {fmt(metrics['p95_e2e_ms'])} | {fmt(metrics['p99_e2e_ms'])} | " + f"{fmt(metrics['mean_ttft_ms'])} | {fmt(metrics['p95_ttft_ms'])} | {fmt(metrics['p99_ttft_ms'])} | " + f"{fmt(metrics['mean_tpot_ms'])} | {fmt(metrics['p95_tpot_ms'])} | {fmt(metrics['p99_tpot_ms'])} | " + f"{fmt(metrics['mean_itl_ms'])} |" + ) + + report_lines.append("") + + report_text = "\n".join(report_lines) + report_path = result_root / "report.md" + report_path.write_text(report_text) + print(report_text) + print(f"\nReport saved to: {report_path}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark_dspark_0707/parse_st_comparison.py b/scripts/benchmark_dspark_0707/parse_st_comparison.py new file mode 100644 index 0000000..d8aecaa --- /dev/null +++ b/scripts/benchmark_dspark_0707/parse_st_comparison.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Parse spec-tokens 3 vs 5 comparison results and generate a report.""" + +import os +import re +import sys +from pathlib import Path + + +def parse_log(log_path: Path) -> dict: + text = log_path.read_text(errors="ignore") + lines = text.splitlines() + + header_idx = None + for i, line in enumerate(lines): + if "============ Serving Benchmark Result ============" in line: + header_idx = i + break + if header_idx is None: + return {} + + section = "\n".join(lines[header_idx:]) + + def get_float(pattern): + m = re.search(pattern + r"\s+([\d.]+)", section) + return float(m.group(1)) if m else None + + def get_int(pattern): + m = re.search(pattern + r"\s+(\d+)", section) + return int(m.group(1)) if m else None + + return { + "duration_s": get_float(r"Benchmark duration \(s\):"), + "successful_requests": get_int(r"Successful requests:"), + "req_throughput": get_float(r"Request throughput \(req/s\):"), + "input_tok_throughput": get_float(r"Input token throughput \(tok/s\):"), + "output_tok_throughput": get_float(r"Output token throughput \(tok/s\):"), + "total_tok_throughput": get_float(r"Total token throughput \(tok/s\):"), + "mean_e2e_ms": get_float(r"Mean E2E Latency \(ms\):"), + "p95_e2e_ms": get_float(r"P95 E2E Latency \(ms\):"), + "p99_e2e_ms": get_float(r"P99 E2E Latency \(ms\):"), + "mean_ttft_ms": get_float(r"Mean TTFT \(ms\):"), + "p95_ttft_ms": get_float(r"P95 TTFT \(ms\):"), + "p99_ttft_ms": get_float(r"P99 TTFT \(ms\):"), + "mean_tpot_ms": get_float(r"Mean TPOT \(ms\):"), + "p95_tpot_ms": get_float(r"P95 TPOT \(ms\):"), + "p99_tpot_ms": get_float(r"P99 TPOT \(ms\):"), + "mean_itl_ms": get_float(r"Mean ITL \(ms\):"), + } + + +def collect_results(result_root: Path) -> dict: + """Collect results into {(scenario, concurrency, spec_tokens): metrics}.""" + results = {} + focused_dir = result_root / "focused" + if not focused_dir.exists(): + return results + + for scenario_dir in focused_dir.iterdir(): + if not scenario_dir.is_dir(): + continue + scenario = scenario_dir.name + for run_dir in scenario_dir.iterdir(): + if not run_dir.is_dir(): + continue + run_id = run_dir.name + # run_id format: _st{3,5} + if "_st" not in run_id: + continue + spec_tokens = run_id.split("_st")[-1] + for log in run_dir.glob("c*.log"): + concurrency = int(log.stem[1:]) + metrics = parse_log(log) + if metrics: + results[(scenario, concurrency, spec_tokens)] = metrics + return results + + +def fmt(v): + return f"{v:.2f}" if v is not None else "N/A" + + +def main(): + result_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/data/user1/yy/bench_results/dspark_st_comparison_20260707-150649") + if not result_root.exists(): + print(f"Result root not found: {result_root}", file=sys.stderr) + sys.exit(1) + + results = collect_results(result_root) + if not results: + print("No results found.", file=sys.stderr) + sys.exit(1) + + # Group by scenario and concurrency + keys = sorted({(s, c) for (s, c, _) in results.keys()}) + + report_lines = [ + "# vllm-dspark `--spec-tokens` 对比报告", + "", + f"- 结果目录:`{result_root}`", + f"- 模型:`/data/models/DeepSeek-V4-Flash-DSpark`", + f"- 后端:vllm-dspark (TP=8, FP8 KV cache)", + f"- 对比参数:`--spec-tokens 3` vs `--spec-tokens 5`", + f"- Warmup:100 条", + f"- 压测客户端:`sglang.bench_serving --backend vllm`", + "", + "## 核心指标对比", + "", + "| Scenario | Concurrency | Spec | Duration(s) | Req/s | Out tok/s | Total tok/s | Mean E2E(ms) | P95 E2E(ms) | P99 E2E(ms) | Mean TTFT(ms) | P99 TTFT(ms) | Mean TPOT(ms) | P99 TPOT(ms) |", + "|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|", + ] + + for scenario, concurrency in keys: + for st in ["3", "5"]: + m = results.get((scenario, concurrency, st), {}) + if not m: + continue + report_lines.append( + f"| {scenario} | {concurrency} | {st} | {fmt(m['duration_s'])} | {fmt(m['req_throughput'])} | " + f"{fmt(m['output_tok_throughput'])} | {fmt(m['total_tok_throughput'])} | {fmt(m['mean_e2e_ms'])} | " + f"{fmt(m['p95_e2e_ms'])} | {fmt(m['p99_e2e_ms'])} | {fmt(m['mean_ttft_ms'])} | {fmt(m['p99_ttft_ms'])} | " + f"{fmt(m['mean_tpot_ms'])} | {fmt(m['p99_tpot_ms'])} |" + ) + + # Summary table: best total tok/s per scenario/concurrency + report_lines.append("") + report_lines.append("## 吞吐 winner 统计") + report_lines.append("") + report_lines.append("| Scenario | Concurrency | Winner (Total tok/s) | st=3 Total tok/s | st=5 Total tok/s | 提升 |") + report_lines.append("|---|---:|---:|---:|---:|---:|") + + for scenario, concurrency in keys: + m3 = results.get((scenario, concurrency, "3"), {}) + m5 = results.get((scenario, concurrency, "5"), {}) + t3 = m3.get("total_tok_throughput", 0) or 0 + t5 = m5.get("total_tok_throughput", 0) or 0 + if t3 == 0 and t5 == 0: + continue + winner = "st=3" if t3 > t5 else "st=5" + uplift = (max(t3, t5) / min(t3, t5) - 1) * 100 if min(t3, t5) > 0 else 0 + report_lines.append( + f"| {scenario} | {concurrency} | {winner} | {fmt(t3)} | {fmt(t5)} | {fmt(uplift)}% |" + ) + + report_lines.append("") + report_lines.append("## 观察与建议") + report_lines.append("") + + # Compute overall wins + st3_wins = 0 + st5_wins = 0 + for scenario, concurrency in keys: + m3 = results.get((scenario, concurrency, "3"), {}) + m5 = results.get((scenario, concurrency, "5"), {}) + t3 = m3.get("total_tok_throughput", 0) or 0 + t5 = m5.get("total_tok_throughput", 0) or 0 + if t3 > t5: + st3_wins += 1 + elif t5 > t3: + st5_wins += 1 + + report_lines.append(f"- **st=3 在 {st3_wins} 个配置中吞吐更高,st=5 在 {st5_wins} 个配置中吞吐更高。**") + report_lines.append("- 具体结论请根据上表分析。") + report_lines.append("") + + report_text = "\n".join(report_lines) + report_path = result_root / "comparison_report.md" + report_path.write_text(report_text) + print(report_text) + print(f"\nComparison report saved to: {report_path}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh b/scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh new file mode 100755 index 0000000..4170d3c --- /dev/null +++ b/scripts/benchmark_dspark_0707/run_dspark_benchmark_grid.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Run H200 DeepSeek-V4-Flash-DSpark benchmark grid in the current environment. +# This orchestrator starts the vllm-dspark server, runs P1/P2/P3 benchmarks, +# and stops the server afterwards. +# +# Environment: +# - vllm-dspark env: /data/user1/yy/envs/vllm-dspark +# - benchmark client: /data/user1/yy/envs/sglang/bin/python -m sglang.bench_serving +# - model: /data/models/DeepSeek-V4-Flash-DSpark +# - server port: 30004 +# +# Usage: +# bash run_dspark_benchmark_grid.sh +# +# To only run benchmarks against an already-running server: +# SKIP_MANAGE_SERVER=1 bash run_dspark_benchmark_grid.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +START_SCRIPT="${SCRIPT_DIR}/../start_dsv4_dspark_8card.sh" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" +RESULT_ROOT="${RESULT_ROOT:-/data/user1/yy/bench_results/dspark_grid_${RUN_ID}}" +LOG_DIR="${RESULT_ROOT}/logs" +PORT="${PORT:-30004}" +PID_FILE="/data/user1/yy/dsv4_flash_dspark.pid" + +mkdir -p "${LOG_DIR}" + +log() { + echo "[$(date --iso-8601=seconds)] $*" | tee -a "${LOG_DIR}/orchestrator.log" +} + +is_server_healthy() { + curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1 +} + +stop_server() { + if [[ -f "${PID_FILE}" ]]; then + local pid + pid="$(cat "${PID_FILE}")" + if kill -0 "${pid}" 2>/dev/null; then + log "stopping existing dspark server pid=${pid}" + kill "${pid}" 2>/dev/null || true + sleep 5 + kill -9 "${pid}" 2>/dev/null || true + fi + rm -f "${PID_FILE}" + fi + # Fallback: kill any lingering vllm serve processes for this model + pkill -9 -f "vllm serve.*DeepSeek-V4-Flash-DSpark" 2>/dev/null || true + sleep 2 +} + +start_server() { + log "starting dspark server with ${START_SCRIPT}" + if [[ ! -x "${START_SCRIPT}" ]]; then + log "error: start script not found or not executable: ${START_SCRIPT}" + exit 1 + fi + bash "${START_SCRIPT}" >> "${LOG_DIR}/server.outer.log" 2>&1 + if ! is_server_healthy; then + log "error: dspark server failed to become healthy" + exit 1 + fi + log "dspark server is healthy" +} + +run_phase() { + local name="$1" + local script="$2" + local log_file="${LOG_DIR}/${name}.log" + + log "running ${name}: ${script}" + RUN_ID="${RUN_ID}" RESULT_ROOT="${RESULT_ROOT}" bash "${script}" 2>&1 | tee "${log_file}" + log "finished ${name}" +} + +on_exit() { + local code="$?" + log "orchestrator exiting with code=${code}" + if [[ -z "${SKIP_MANAGE_SERVER:-}" ]]; then + stop_server + fi + exit "${code}" +} +trap on_exit EXIT + +main() { + log "run_id=${RUN_ID}" + log "result_root=${RESULT_ROOT}" + log "script_dir=${SCRIPT_DIR}" + + if [[ -n "${SKIP_MANAGE_SERVER:-}" ]]; then + log "SKIP_MANAGE_SERVER is set, assuming server is already running" + if ! is_server_healthy; then + log "error: no healthy server found at port ${PORT}" + exit 1 + fi + else + stop_server + start_server + fi + + log "===== DSpark BENCHMARK START =====" + run_phase "dspark_p1" "${SCRIPT_DIR}/bench_dspark_p1.sh" + run_phase "dspark_p2" "${SCRIPT_DIR}/bench_dspark_p2.sh" + run_phase "dspark_p3" "${SCRIPT_DIR}/bench_dspark_p3.sh" + log "===== DSpark BENCHMARK DONE =====" + + log "all results saved to ${RESULT_ROOT}" +} + +main "$@" diff --git a/scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh b/scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh new file mode 100755 index 0000000..747c166 --- /dev/null +++ b/scripts/benchmark_dspark_0707/run_dspark_st_comparison.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Compare vllm-dspark with --spec-tokens 3 vs 5. +# Uses the parameterized start script and focused benchmark. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +START_SCRIPT="${SCRIPT_DIR}/../start_dsv4_dspark_8card_param.sh" +BENCH_SCRIPT="${SCRIPT_DIR}/bench_dspark_focused.sh" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" +RESULT_ROOT="${RESULT_ROOT:-/data/user1/yy/bench_results/dspark_st_comparison_${RUN_ID}}" +LOG_DIR="${RESULT_ROOT}/logs" +PORT="${PORT:-30004}" +PID_FILE="/data/user1/yy/dsv4_flash_dspark.pid" +WARMUP_REQUESTS="${WARMUP_REQUESTS:-100}" + +mkdir -p "${LOG_DIR}" + +log() { + echo "[$(date --iso-8601=seconds)] $*" | tee -a "${LOG_DIR}/orchestrator.log" +} + +is_server_healthy() { + curl --fail --silent --show-error --max-time 5 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1 +} + +stop_server() { + if [[ -f "${PID_FILE}" ]]; then + local pid + pid="$(cat "${PID_FILE}" 2>/dev/null || true)" + if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then + log "stopping existing dspark server pid=${pid}" + kill "${pid}" 2>/dev/null || true + sleep 5 + kill -9 "${pid}" 2>/dev/null || true + fi + rm -f "${PID_FILE}" + fi + local pids + pids="$(pgrep -f "VLLM::|vllm serve.*DeepSeek-V4-Flash-DSpark" 2>/dev/null || true)" + if [[ -n "${pids}" ]]; then + for pid in ${pids}; do + if [[ "$pid" != "$$" ]]; then kill -9 "$pid" 2>/dev/null || true; fi + done + fi + sleep 2 +} + +start_server() { + local spec_tokens="$1" + log "starting dspark server with spec-tokens=${spec_tokens}" + if [[ ! -x "${START_SCRIPT}" ]]; then + log "error: start script not found or not executable: ${START_SCRIPT}" + exit 1 + fi + SPEC_TOKENS="${spec_tokens}" bash "${START_SCRIPT}" >> "${LOG_DIR}/server_st${spec_tokens}.outer.log" 2>&1 + if ! is_server_healthy; then + log "error: dspark server (st=${spec_tokens}) failed to become healthy" + exit 1 + fi + log "dspark server (st=${spec_tokens}) is healthy" +} + +run_benchmark() { + local spec_tokens="$1" + local log_file="${LOG_DIR}/focused_st${spec_tokens}.log" + log "running focused benchmark with spec-tokens=${spec_tokens}, warmup=${WARMUP_REQUESTS}" + RUN_ID="${RUN_ID}_st${spec_tokens}" RESULT_ROOT="${RESULT_ROOT}" WARMUP_REQUESTS="${WARMUP_REQUESTS}" bash "${BENCH_SCRIPT}" 2>&1 | tee "${log_file}" + log "finished focused benchmark with spec-tokens=${spec_tokens}" +} + +on_exit() { + local code="$?" + log "orchestrator exiting with code=${code}" + stop_server + exit "${code}" +} +trap on_exit EXIT + +main() { + log "run_id=${RUN_ID}" + log "result_root=${RESULT_ROOT}" + log "warmup_requests=${WARMUP_REQUESTS}" + + log "===== SPEC-TOKENS 3 START =====" + stop_server + start_server 3 + run_benchmark 3 + stop_server + + log "cooldown 30s" + sleep 30 + + log "===== SPEC-TOKENS 5 START =====" + start_server 5 + run_benchmark 5 + stop_server + + log "===== COMPARISON DONE =====" + log "all results saved to ${RESULT_ROOT}" +} + +main "$@" diff --git a/scripts/benchmark_dsv4_backend_comparison.sh b/scripts/benchmark_dsv4_backend_comparison.sh new file mode 100644 index 0000000..5e7561e --- /dev/null +++ b/scripts/benchmark_dsv4_backend_comparison.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Reproducible SGLang vs vLLM backend comparison for DeepSeek-V4-Flash. +# Produces JSONL raw outputs compatible with the legacy 2026-07-07 sweep. +# +# Usage: +# bash scripts/benchmark_dsv4_backend_comparison.sh [vllm|sglang|all] +# +# Output: +# bench_results/dsv4_backend_comparison_${RUN_ID}/raw_outputs/ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +BACKEND_FILTER="${1:-all}" +RUN_ID="${RUN_ID:-$(date '+%Y%m%d-%H%M%S')}" +RESULT_ROOT="${RESULT_ROOT:-${ROOT_DIR}/bench_results/dsv4_backend_comparison_${RUN_ID}}" +RAW_DIR="${RESULT_ROOT}/raw_outputs" +LOG_DIR="${RESULT_ROOT}/logs" + +mkdir -p "${RAW_DIR}" "${LOG_DIR}" + +BENCH_PY="${ROOT_DIR}/envs/sglang/bin/python" +DATASET="${ROOT_DIR}/datasets/ShareGPT_filtered_chat.json" +MODEL="/data/models/DeepSeek-V4-Flash" +SERVED_MODEL_NAME="deepseek-v4-flash" +NUM_PROMPTS=512 +WARMUP=100 + +log() { + echo "[$(date --iso-8601=seconds)] $*" | tee -a "${LOG_DIR}/orchestrator.log" +} + +run_case() { + local backend="$1" + local port="$2" + local concurrency="$3" + local input_len="$4" + local output_len="$5" + local output_file="${RAW_DIR}/${backend}_$(date '+%m%d')_${concurrency}_${input_len}_${output_len}.jsonl" + local detail_log="${LOG_DIR}/${backend}_c${concurrency}_i${input_len}_o${output_len}.log" + + log "running ${backend}: concurrency=${concurrency} input=${input_len} output=${output_len} -> ${output_file}" + + "${BENCH_PY}" -m sglang.bench_serving \ + --backend "${backend}" \ + --host 127.0.0.1 \ + --port "${port}" \ + --model "${MODEL}" \ + --served-model-name "${SERVED_MODEL_NAME}" \ + --dataset-name random \ + --dataset-path "${DATASET}" \ + --random-input-len "${input_len}" \ + --random-output-len "${output_len}" \ + --num-prompts "${NUM_PROMPTS}" \ + --max-concurrency "${concurrency}" \ + --warmup-requests "${WARMUP}" \ + --output-file "${output_file}" \ + --output-details \ + > "${detail_log}" 2>&1 + + log "finished ${backend} c=${concurrency} i=${input_len} o=${output_len}" +} + +main() { + log "run_id=${RUN_ID}" + log "result_root=${RESULT_ROOT}" + log "backend_filter=${BACKEND_FILTER}" + + # This script assumes a healthy server is already running on the target port. + if [[ "${BACKEND_FILTER}" == "all" || "${BACKEND_FILTER}" == "sglang" ]]; then + log "===== SGLang sweep (port 30000) =====" + run_case sglang 30000 32 512 256 + run_case sglang 30000 128 512 256 + run_case sglang 30000 256 512 256 + run_case sglang 30000 512 512 256 + run_case sglang 30000 512 512 2000 + run_case sglang 30000 32 4000 512 + run_case sglang 30000 128 4000 512 + run_case sglang 30000 512 4000 512 + run_case sglang 30000 32 16000 512 + run_case sglang 30000 128 16000 512 + run_case sglang 30000 512 1000 256 + run_case sglang 30000 768 1000 256 + run_case sglang 30000 1024 1000 256 + run_case sglang 30000 512 1000 1000 + fi + + if [[ "${BACKEND_FILTER}" == "all" || "${BACKEND_FILTER}" == "vllm" ]]; then + log "===== vLLM sweep (port 8000) =====" + run_case vllm 8000 32 512 256 + run_case vllm 8000 128 512 256 + run_case vllm 8000 256 512 256 + run_case vllm 8000 128 512 2000 + run_case vllm 8000 256 512 2000 + run_case vllm 8000 512 512 2000 + run_case vllm 8000 32 1000 256 + run_case vllm 8000 128 1000 256 + run_case vllm 8000 256 1000 256 + run_case vllm 8000 512 1000 256 + run_case vllm 8000 768 1000 256 + run_case vllm 8000 1024 1000 256 + run_case vllm 8000 32 1000 1000 + run_case vllm 8000 128 1000 1000 + run_case vllm 8000 256 1000 1000 + run_case vllm 8000 512 1000 1000 + run_case vllm 8000 1024 1000 1000 + run_case vllm 8000 32 4000 512 + run_case vllm 8000 128 4000 512 + run_case vllm 8000 256 4000 512 + run_case vllm 8000 512 4000 512 + run_case vllm 8000 32 8000 1000 + run_case vllm 8000 128 8000 1000 + run_case vllm 8000 256 8000 1000 + run_case vllm 8000 512 8000 1000 + run_case vllm 8000 32 16000 512 + run_case vllm 8000 128 16000 512 + run_case vllm 8000 256 16000 512 + run_case vllm 8000 32 32000 512 + run_case vllm 8000 128 32000 512 + fi + + log "===== Done =====" + log "all outputs saved to ${RAW_DIR}" +} + +main "$@" diff --git a/scripts/install_vllm_dspark.sh b/scripts/install_vllm_dspark.sh new file mode 100755 index 0000000..d1595f4 --- /dev/null +++ b/scripts/install_vllm_dspark.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +export TMPDIR=/data/tmp +export TEMP=/data/tmp +export TMP=/data/tmp +export PIP_CACHE_DIR=/data/tmp/pip-cache +export SETUPTOOLS_SCM_PRETEND_VERSION=0.23.1.dev788 +mkdir -p $PIP_CACHE_DIR + +cd /data/user1/yy/vllm-main +PYTHON=/data/user1/yy/envs/vllm-dspark/bin/python +PIP=/data/user1/yy/envs/vllm-dspark/bin/pip + +echo "Installing torch 2.11.0 with CUDA 12.9..." +$PIP install torch==2.11.0 --index-url https://download.pytorch.org/whl/cu129 --cache-dir $PIP_CACHE_DIR + +echo "Installing numpy and build dependencies..." +$PIP install numpy cmake ninja packaging "setuptools>=77.0.3,<81.0.0" setuptools-scm setuptools-rust wheel jinja2 --cache-dir $PIP_CACHE_DIR + +echo "Installing vllm from source (this will compile CUDA kernels, may take 30-60 min)..." +$PIP install -e . --no-build-isolation --cache-dir $PIP_CACHE_DIR + +echo "Verifying installation..." +$PYTHON -c "import vllm; print('vllm version:', vllm.__version__)" + +echo "Installation complete." diff --git a/scripts/profile_dspark.py b/scripts/profile_dspark.py new file mode 100755 index 0000000..ce40591 --- /dev/null +++ b/scripts/profile_dspark.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Profile DSpark inference with Nsight Systems and/or PyTorch profiler. + +Usage: + python scripts/profile_dspark.py --config dspark-st3 --concurrency 64 --tool nsys + python scripts/profile_dspark.py --config nospec --concurrency 64 --tool nsys +""" + +import argparse +import json +import os +import subprocess +import sys +import time +import urllib.request +from datetime import datetime +from pathlib import Path + +ROOT = Path("/data/user1/yy") +RESULT_DIR = ROOT / "bench_results" / f"dspark_profile_{datetime.now().strftime('%Y%m%d_%H%M%S')}" +RESULT_DIR.mkdir(parents=True, exist_ok=True) + +DATASET = "/data/user1/yy/datasets/ShareGPT_V4.3_unfiltered_cleaned_split.json" +NUM_PROMPTS = 200 +SEED = 42 +OUTPUT_LEN = 256 +HOST = "127.0.0.1" + +CONFIGS = { + "dspark-st3": { + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "port": 30004, + "spec_tokens": 3, + "cmd": [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash-DSpark", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--gpu-memory-utilization", "0.75", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--spec-method", "dspark", + "--spec-model", "/data/models/DeepSeek-V4-Flash-DSpark", + "--spec-tokens", "3", + "--no-disable-hybrid-kv-cache-manager", + "--disable-uvicorn-access-log", + "--port", "30004", + ], + }, + "dspark-st5": { + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "port": 30004, + "spec_tokens": 5, + "cmd": [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash-DSpark", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--gpu-memory-utilization", "0.75", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--spec-method", "dspark", + "--spec-model", "/data/models/DeepSeek-V4-Flash-DSpark", + "--spec-tokens", "5", + "--no-disable-hybrid-kv-cache-manager", + "--disable-uvicorn-access-log", + "--port", "30004", + ], + }, + "nospec": { + "model": "/data/models/DeepSeek-V4-Flash-DSpark", + "port": 30004, + "cmd": [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "serve", + "/data/models/DeepSeek-V4-Flash-DSpark", + "--trust-remote-code", + "--tensor-parallel-size", "8", + "--kv-cache-dtype", "fp8", + "--block-size", "256", + "--max-model-len", "auto", + "--max-num-seqs", "256", + "--gpu-memory-utilization", "0.75", + "--tokenizer-mode", "deepseek_v4", + "--reasoning-parser", "deepseek_v4", + "--no-disable-hybrid-kv-cache-manager", + "--disable-uvicorn-access-log", + "--port", "30004", + ], + }, +} + + +def log(msg): + print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True) + + +def wait_for_health(port, timeout=300): + url = f"http://{HOST}:{port}/health" + start = time.time() + while time.time() - start < timeout: + try: + with urllib.request.urlopen(url, timeout=5) as resp: + if resp.status == 200: + return True + except Exception: + pass + time.sleep(2) + return False + + +def run_warmup(port, model, n=20): + """Run a few warmup requests to stabilize CUDA graphs.""" + log(f"Running {n} warmup requests...") + for i in range(n): + try: + req = urllib.request.Request( + f"http://{HOST}:{port}/v1/completions", + data=json.dumps({ + "model": model, + "prompt": "Hello, how are you?", + "max_tokens": 32, + "temperature": 0, + "seed": SEED, + }).encode(), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=60) as resp: + resp.read() + except Exception as e: + log(f"Warmup request {i} failed: {e}") + + +def run_benchmark(port, model, concurrency, result_dir): + result_file = result_dir / f"profile_c{concurrency}.json" + log_file = result_dir / f"profile_c{concurrency}.log" + bench_cmd = [ + "/data/user1/yy/envs/vllm-dspark/bin/vllm", "bench", "serve", + "--host", HOST, + "--port", str(port), + "--backend", "openai", + "--dataset-name", "sharegpt", + "--dataset-path", DATASET, + "--sharegpt-output-len", str(OUTPUT_LEN), + "--num-prompts", str(NUM_PROMPTS), + "--max-concurrency", str(concurrency), + "--endpoint", "/v1/completions", + "--model", model, + "--seed", str(SEED), + "--save-result", + "--result-dir", str(result_dir), + "--result-filename", result_file.name, + ] + log(f"Running benchmark c={concurrency}...") + start = time.time() + with open(log_file, "w") as f: + proc = subprocess.Popen(bench_cmd, stdout=f, stderr=subprocess.STDOUT) + proc.wait() + duration = time.time() - start + log(f"Benchmark c={concurrency} finished in {duration:.1f}s, exit={proc.returncode}") + return result_file + + +def start_service(config, tool, result_dir): + name = config + cfg = CONFIGS[config] + port = cfg["port"] + base_cmd = cfg["cmd"] + + if tool == "nsys": + nsys_output = result_dir / f"{name}_nsys" + cmd = [ + "nsys", "profile", + "--sample=none", + "--cpuctxsw=none", + "--trace=cuda,nvtx,osrt", + "--output", str(nsys_output), + "--force-overwrite", "true", + "--delay", "30", + "--duration", "15", + ] + base_cmd + else: + cmd = base_cmd + + log(f"Starting service for {name} (tool={tool}) on port {port}...") + log_file = result_dir / f"{name}_service.log" + env = os.environ.copy() + env["CUDA_VISIBLE_DEVICES"] = "0,1,2,3,4,5,6,7" + env["TMPDIR"] = str(ROOT / "tmp") + + proc = subprocess.Popen( + cmd, + stdout=open(log_file, "w"), + stderr=subprocess.STDOUT, + env=env, + ) + if not wait_for_health(port): + log(f"ERROR: {name} failed to start") + proc.terminate() + return None + log(f"{name} is ready") + return proc + + +def stop_service(proc, name): + if proc is None: + return + log(f"Stopping {name} (pid {proc.pid})...") + proc.terminate() + try: + proc.wait(timeout=60) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + log(f"{name} stopped") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True, choices=list(CONFIGS.keys())) + parser.add_argument("--concurrency", type=int, default=64) + parser.add_argument("--tool", default="nsys", choices=["nsys"]) + parser.add_argument("--skip-bench", action="store_true", help="Only capture service startup/warmup, skip benchmark") + args = parser.parse_args() + + result_dir = RESULT_DIR / f"{args.config}_c{args.concurrency}_{args.tool}" + result_dir.mkdir(parents=True, exist_ok=True) + log(f"Results will be saved to {result_dir}") + + proc = start_service(args.config, args.tool, result_dir) + if proc is None: + sys.exit(1) + + try: + run_warmup(CONFIGS[args.config]["port"], CONFIGS[args.config]["model"], n=30) + if not args.skip_bench: + run_benchmark( + CONFIGS[args.config]["port"], + CONFIGS[args.config]["model"], + args.concurrency, + result_dir, + ) + else: + log("--skip-bench set; sleeping 60s to capture warmup/steady state...") + time.sleep(60) + finally: + stop_service(proc, args.config) + + log(f"All done. Results in {result_dir}") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_sglang_benchmark.sh b/scripts/run_sglang_benchmark.sh new file mode 100755 index 0000000..bb19967 --- /dev/null +++ b/scripts/run_sglang_benchmark.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# Comprehensive benchmark for sglang 8-card DeepSeek-V4-Flash +set -e + +cd /data/user1/yy +mkdir -p bench_results logs + +PYTHON="/data/user1/yy/envs/sglang/bin/python" +BENCH="-m sglang.bench_serving" +HOST="127.0.0.1" +PORT="30000" +MODEL="/data/models/DeepSeek-V4-Flash" +DATASET="/data/user1/yy/datasets/ShareGPT_V4.3_unfiltered_cleaned_split.json" +NUM_PROMPTS=2000 +SEED=42 + +RESULT_DIR="bench_results/sglang_8card_systematic_$(date +%Y%m%d_%H%M%S)" +mkdir -p "${RESULT_DIR}" + +SUMMARY_FILE="${RESULT_DIR}/summary.json" +LOG_FILE="${RESULT_DIR}/benchmark.log" + +# Use a marker file for summary accumulation +SUMMARY_TMP="${RESULT_DIR}/.summary_tmp.json" +echo '[]' > "${SUMMARY_TMP}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}" +} + +wait_for_server() { + log "Waiting for sglang server at ${HOST}:${PORT} to become ready..." + local retries=0 + local max_retries=120 + while ! curl -s "http://${HOST}:${PORT}/v1/models" > /dev/null 2>&1; do + retries=$((retries + 1)) + if [ "${retries}" -ge "${max_retries}" ]; then + log "ERROR: Server not ready after ${max_retries} retries" + exit 1 + fi + sleep 2 + done + log "Server is ready." +} + +run_bench() { + local name="$1" + shift + local output_file="${RESULT_DIR}/${name}.json" + local detail_log="${RESULT_DIR}/${name}.log" + + log "---------------------------------------------------" + log "Running benchmark: ${name}" + log "Args: $*" + log "Output: ${output_file}" + + ${PYTHON} ${BENCH} \ + --backend sglang \ + --host "${HOST}" \ + --port "${PORT}" \ + --dataset-name sharegpt \ + --dataset-path "${DATASET}" \ + --num-prompts "${NUM_PROMPTS}" \ + --model "${MODEL}" \ + --output-file "${output_file}" \ + --output-details \ + --seed "${SEED}" \ + "$@" \ + > "${detail_log}" 2>&1 + + local status=$? + if [ ${status} -ne 0 ]; then + log "ERROR: Benchmark ${name} failed with exit code ${status}" + return ${status} + fi + + log "Benchmark ${name} completed successfully." + + # Extract key metrics from the log + local req_throughput=$(grep -oP 'Request throughput \(req/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local input_throughput=$(grep -oP 'Input token throughput \(tok/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local output_throughput=$(grep -oP 'Output token throughput \(tok/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local total_throughput=$(grep -oP 'Total token throughput \(tok/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local mean_ttft=$(grep -oP 'Mean TTFT \(ms\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local mean_tpot=$(grep -oP 'Mean TPOT \(ms\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local mean_e2e=$(grep -oP 'Mean E2E Latency \(ms\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local duration=$(grep -oP 'Benchmark duration \(s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local accept_length=$(grep -oP 'Accept length:\s+\K[0-9.]+' "${detail_log}" || echo "null") + + # Append to summary tmp + ${PYTHON} - <8} {'In tok/s':>10} {'Out tok/s':>10} {'Total tok/s':>12} {'TTFT':>8} {'TPOT':>8} {'E2E':>10}") +print("-" * 110) +for entry in data: + print(f"{entry['name']:<30} {entry['request_throughput']:>8.2f} {entry['input_token_throughput']:>10.2f} {entry['output_token_throughput']:>10.2f} {entry['total_token_throughput']:>12.2f} {entry['mean_ttft_ms']:>8.1f} {entry['mean_tpot_ms']:>8.1f} {entry['mean_e2e_latency_ms']:>10.1f}") +PYEOF +} + +main "$@" diff --git a/scripts/run_sglang_max_throughput.sh b/scripts/run_sglang_max_throughput.sh new file mode 100755 index 0000000..7d1513b --- /dev/null +++ b/scripts/run_sglang_max_throughput.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# Max throughput benchmark for sglang 8-card DeepSeek-V4-Flash +set -e + +cd /data/user1/yy +mkdir -p bench_results logs + +PYTHON="/data/user1/yy/envs/sglang/bin/python" +BENCH="-m sglang.bench_serving" +HOST="127.0.0.1" +PORT="30000" +MODEL="/data/models/DeepSeek-V4-Flash" +DATASET="/data/user1/yy/datasets/ShareGPT_V4.3_unfiltered_cleaned_split.json" +NUM_PROMPTS=2000 +SEED=42 + +RESULT_DIR="bench_results/sglang_8card_max_throughput_$(date +%Y%m%d_%H%M%S)" +mkdir -p "${RESULT_DIR}" + +SUMMARY_FILE="${RESULT_DIR}/summary.json" +LOG_FILE="${RESULT_DIR}/benchmark.log" +SUMMARY_TMP="${RESULT_DIR}/.summary_tmp.json" +echo '[]' > "${SUMMARY_TMP}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}" +} + +wait_for_server() { + log "Waiting for sglang server at ${HOST}:${PORT} to become ready..." + local retries=0 + local max_retries=120 + while ! curl -s "http://${HOST}:${PORT}/v1/models" > /dev/null 2>&1; do + retries=$((retries + 1)) + if [ "${retries}" -ge "${max_retries}" ]; then + log "ERROR: Server not ready after ${max_retries} retries" + exit 1 + fi + sleep 2 + done + log "Server is ready." +} + +run_bench() { + local name="$1" + shift + local output_file="${RESULT_DIR}/${name}.json" + local detail_log="${RESULT_DIR}/${name}.log" + + log "---------------------------------------------------" + log "Running benchmark: ${name}" + log "Args: $*" + log "Output: ${output_file}" + + ${PYTHON} ${BENCH} \ + --backend sglang \ + --host "${HOST}" \ + --port "${PORT}" \ + --dataset-name sharegpt \ + --dataset-path "${DATASET}" \ + --num-prompts "${NUM_PROMPTS}" \ + --model "${MODEL}" \ + --output-file "${output_file}" \ + --output-details \ + --seed "${SEED}" \ + "$@" \ + > "${detail_log}" 2>&1 + + local status=$? + if [ ${status} -ne 0 ]; then + log "ERROR: Benchmark ${name} failed with exit code ${status}" + return ${status} + fi + + log "Benchmark ${name} completed successfully." + + local req_throughput=$(grep -oP 'Request throughput \(req/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local input_throughput=$(grep -oP 'Input token throughput \(tok/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local output_throughput=$(grep -oP 'Output token throughput \(tok/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local total_throughput=$(grep -oP 'Total token throughput \(tok/s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local mean_ttft=$(grep -oP 'Mean TTFT \(ms\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local mean_tpot=$(grep -oP 'Mean TPOT \(ms\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local mean_e2e=$(grep -oP 'Mean E2E Latency \(ms\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local duration=$(grep -oP 'Benchmark duration \(s\):\s+\K[0-9.]+' "${detail_log}" || echo "null") + local accept_length=$(grep -oP 'Accept length:\s+\K[0-9.]+' "${detail_log}" || echo "null") + + ${PYTHON} - <8} {'In tok/s':>10} {'Out tok/s':>10} {'Total tok/s':>12} {'TTFT':>8} {'TPOT':>8} {'E2E':>10}") +print("-" * 110) +for entry in data: + print(f"{entry['name']:<30} {entry['request_throughput']:>8.2f} {entry['input_token_throughput']:>10.2f} {entry['output_token_throughput']:>10.2f} {entry['total_token_throughput']:>12.2f} {entry['mean_ttft_ms']:>8.1f} {entry['mean_tpot_ms']:>8.1f} {entry['mean_e2e_latency_ms']:>10.1f}") +PYEOF +} + +main "$@" diff --git a/scripts/start_dsv4_dspark_8card.sh b/scripts/start_dsv4_dspark_8card.sh new file mode 100755 index 0000000..53bd04b --- /dev/null +++ b/scripts/start_dsv4_dspark_8card.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +cd /data/user1/yy +mkdir -p logs + +VENV="/data/user1/yy/envs/vllm-dspark" +PYTHON="$VENV/bin/python" +VLLM="$VENV/bin/vllm" +export PATH="$VENV/bin:$PATH" + +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +PORT=30004 +TP=8 +LOG="/data/user1/yy/logs/dsv4_flash_dspark_tp${TP}_$(date +%Y%m%d_%H%M%S).log" +PID_FILE="/data/user1/yy/dsv4_flash_dspark.pid" + +export TMPDIR=/data/user1/yy/tmp +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +echo "=== Starting DeepSeek-V4-Flash-DSpark (TP=$TP) ===" +echo "Model: $MODEL" +echo "Port: $PORT" +echo "Log: $LOG" + +rm -f "$PID_FILE" +nohup "$VLLM" serve "$MODEL" \ + --trust-remote-code \ + --tensor-parallel-size "$TP" \ + --kv-cache-dtype fp8 \ + --block-size 256 \ + --max-model-len auto \ + --max-num-seqs 256 \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --spec-method dspark \ + --spec-model "$MODEL" \ + --spec-tokens 5 \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --port "$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 -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then + echo "Server is ready at http://127.0.0.1:$PORT" + echo "Log: $LOG" + exit 0 + fi + if ! kill -0 $PID 2>/dev/null; then + echo "ERROR: Server exited early" + tail -100 "$LOG" + exit 1 + fi + echo "Waiting... ($i/240)" + sleep 5 +done + +echo "ERROR: Server not healthy after 240 retries" +tail -100 "$LOG" +exit 1 diff --git a/scripts/start_dsv4_dspark_8card_bf16kv.sh b/scripts/start_dsv4_dspark_8card_bf16kv.sh new file mode 100755 index 0000000..3833a53 --- /dev/null +++ b/scripts/start_dsv4_dspark_8card_bf16kv.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -e + +cd /data/user1/yy +mkdir -p logs + +VENV="/data/user1/yy/envs/vllm-dspark" +VLLM="$VENV/bin/vllm" + +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +PORT=30004 +TP=8 +LOG="/data/user1/yy/logs/dsv4_flash_dspark_tp${TP}_bf16kv_$(date +%Y%m%d_%H%M%S).log" +PID_FILE="/data/user1/yy/dsv4_flash_dspark.pid" + +export TMPDIR=/data/user1/yy/tmp +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +echo "=== Starting DeepSeek-V4-Flash-DSpark (TP=$TP, kv=bf16) ===" +echo "Model: $MODEL" +echo "Port: $PORT" +echo "Log: $LOG" + +rm -f "$PID_FILE" +nohup "$VLLM" serve "$MODEL" \ + --trust-remote-code \ + --tensor-parallel-size "$TP" \ + --kv-cache-dtype bfloat16 \ + --block-size 256 \ + --max-model-len auto \ + --max-num-seqs 256 \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --spec-method dspark \ + --spec-model "$MODEL" \ + --spec-tokens 5 \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --port "$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 -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then + echo "Server is ready at http://127.0.0.1:$PORT" + echo "Log: $LOG" + exit 0 + fi + if ! kill -0 $PID 2>/dev/null; then + echo "ERROR: Server exited early" + tail -100 "$LOG" + exit 1 + fi + echo "Waiting... ($i/240)" + sleep 5 +done + +echo "ERROR: Server not healthy after 240 retries" +tail -100 "$LOG" +exit 1 diff --git a/scripts/start_dsv4_dspark_8card_flashinfer.sh b/scripts/start_dsv4_dspark_8card_flashinfer.sh new file mode 100755 index 0000000..458e309 --- /dev/null +++ b/scripts/start_dsv4_dspark_8card_flashinfer.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +cd /data/user1/yy +mkdir -p logs + +VENV="/data/user1/yy/envs/vllm-dspark" +VLLM="$VENV/bin/vllm" +export PATH="$VENV/bin:$PATH" + +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +PORT=30004 +TP=8 +LOG="/data/user1/yy/logs/dsv4_flash_dspark_tp${TP}_flashinfer_$(date +%Y%m%d_%H%M%S).log" +PID_FILE="/data/user1/yy/dsv4_flash_dspark.pid" + +export TMPDIR=/data/user1/yy/tmp +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +echo "=== Starting DeepSeek-V4-Flash-DSpark (TP=$TP, FlashInfer) ===" +echo "Model: $MODEL" +echo "Port: $PORT" +echo "Log: $LOG" + +rm -f "$PID_FILE" +nohup "$VLLM" serve "$MODEL" \ + --trust-remote-code \ + --tensor-parallel-size "$TP" \ + --kv-cache-dtype fp8 \ + --block-size 256 \ + --max-model-len auto \ + --max-num-seqs 256 \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --attention-backend FLASHINFER_MLA_SPARSE_DSV4 \ + --spec-method dspark \ + --spec-model "$MODEL" \ + --spec-tokens 5 \ + --speculative-config '{"attention_backend": "FLASHINFER_MLA_SPARSE_DSV4"}' \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --port "$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 -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then + echo "Server is ready at http://127.0.0.1:$PORT" + echo "Log: $LOG" + exit 0 + fi + if ! kill -0 $PID 2>/dev/null; then + echo "ERROR: Server exited early" + tail -100 "$LOG" + exit 1 + fi + echo "Waiting... ($i/240)" + sleep 5 +done + +echo "ERROR: Server not healthy after 240 retries" +tail -100 "$LOG" +exit 1 diff --git a/scripts/start_dsv4_dspark_8card_param.sh b/scripts/start_dsv4_dspark_8card_param.sh new file mode 100755 index 0000000..6be3ac5 --- /dev/null +++ b/scripts/start_dsv4_dspark_8card_param.sh @@ -0,0 +1,85 @@ +#!/bin/bash +set -e + +cd /data/user1/yy +mkdir -p logs + +VENV="/data/user1/yy/envs/vllm-dspark" +VLLM="$VENV/bin/vllm" +export PATH="$VENV/bin:$PATH" + +MODEL="/data/models/DeepSeek-V4-Flash-DSpark" +PORT="${PORT:-30004}" +TP="${TP:-8}" +SPEC_TOKENS="${SPEC_TOKENS:-5}" +KV_CACHE_DTYPE="${KV_CACHE_DTYPE:-fp8}" +ATTENTION_BACKEND="${ATTENTION_BACKEND:-}" +SPECULATIVE_CONFIG="${SPECULATIVE_CONFIG:-}" + +LOG_TAG="tp${TP}_${KV_CACHE_DTYPE}_st${SPEC_TOKENS}" +if [[ -n "$ATTENTION_BACKEND" ]]; then + LOG_TAG="${LOG_TAG}_${ATTENTION_BACKEND}" +fi +LOG="/data/user1/yy/logs/dsv4_flash_dspark_${LOG_TAG}_$(date +%Y%m%d_%H%M%S).log" +PID_FILE="/data/user1/yy/dsv4_flash_dspark.pid" + +export TMPDIR=/data/user1/yy/tmp +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +echo "=== Starting DeepSeek-V4-Flash-DSpark (TP=$TP, spec-tokens=$SPEC_TOKENS, kv=$KV_CACHE_DTYPE) ===" +echo "Model: $MODEL" +echo "Port: $PORT" +echo "Log: $LOG" + +rm -f "$PID_FILE" + +cmd=( + "$VLLM" serve "$MODEL" + --trust-remote-code + --tensor-parallel-size "$TP" + --kv-cache-dtype "$KV_CACHE_DTYPE" + --block-size 256 + --max-model-len auto + --max-num-seqs 256 + --tokenizer-mode deepseek_v4 + --reasoning-parser deepseek_v4 + --spec-method dspark + --spec-model "$MODEL" + --spec-tokens "$SPEC_TOKENS" + --no-disable-hybrid-kv-cache-manager + --disable-uvicorn-access-log + --port "$PORT" +) + +if [[ -n "$ATTENTION_BACKEND" ]]; then + cmd+=(--attention-backend "$ATTENTION_BACKEND") +fi + +if [[ -n "$SPECULATIVE_CONFIG" ]]; then + cmd+=(--speculative-config "$SPECULATIVE_CONFIG") +fi + +nohup "${cmd[@]}" > "$LOG" 2>&1 & +PID=$! +echo $PID > "$PID_FILE" +echo "PID: $PID" +echo "Waiting for health..." + +for i in $(seq 1 240); do + if curl -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then + echo "Server is ready at http://127.0.0.1:$PORT" + echo "Log: $LOG" + exit 0 + fi + if ! kill -0 $PID 2>/dev/null; then + echo "ERROR: Server exited early" + tail -100 "$LOG" + exit 1 + fi + echo "Waiting... ($i/240)" + sleep 5 +done + +echo "ERROR: Server not healthy after 240 retries" +tail -100 "$LOG" +exit 1 diff --git a/scripts/start_pd_single_node.sh b/scripts/start_pd_single_node.sh new file mode 100755 index 0000000..3f3a2fd --- /dev/null +++ b/scripts/start_pd_single_node.sh @@ -0,0 +1,138 @@ +#!/bin/bash +set -e + +# sglang PD 分离单节点启动脚本 +# 104 上前 4 张卡做 prefill,后 4 张卡做 decode +# 使用 NIXL/UCX 作为传输后端(Mooncake 无法安装) + +MODEL_PATH="/data/models/DeepSeek-V4-Flash" +BOOTSTRAP_PORT=30010 +PREFILL_PORT=30001 +DECODE_PORT=30002 +LB_PORT=30000 +HOST="0.0.0.0" + +VENV="/data/user1/yy/envs/sglang" +PYTHON="$VENV/bin/python" +LOG_DIR="/data/user1/yy/pd_logs" +mkdir -p "$LOG_DIR" + +# NIXL/UCX 配置:单节点内优先用 CUDA IPC / shared memory,禁用 IB +export SGLANG_DISAGGREGATION_NIXL_BACKEND="UCX" +export UCX_TLS="self,sm,cuda_copy,cuda_ipc,tcp" +export UCX_NET_DEVICES="" + +echo "=== Starting sglang PD disaggregation (single node) ===" +echo "Model: $MODEL_PATH" +echo "Prefill GPUs: 0-3, port $PREFILL_PORT" +echo "Decode GPUs: 4-7, port $DECODE_PORT" +echo "Load balancer port: $LB_PORT" +echo "Logs: $LOG_DIR" +echo "" + +# Clean up old logs +rm -f "$LOG_DIR"/*.log + +# Common args for both prefill and decode +COMMON_ARGS=( + --trust-remote-code + --model-path "$MODEL_PATH" + --tp 4 + --moe-runner-backend marlin + --disable-cuda-graph + --disaggregation-bootstrap-port "$BOOTSTRAP_PORT" + --disaggregation-transfer-backend nixl + --host "$HOST" +) + +# Start prefill server (GPUs 0-3) +echo "[1/3] Starting prefill server on GPUs 0-3..." +CUDA_VISIBLE_DEVICES=0,1,2,3 nohup "$PYTHON" -m sglang.launch_server \ + "${COMMON_ARGS[@]}" \ + --disaggregation-mode prefill \ + --port "$PREFILL_PORT" \ + > "$LOG_DIR/prefill.log" 2>&1 & +PREFILL_PID=$! +echo "Prefill PID: $PREFILL_PID" + +# Wait for prefill health +for i in {1..120}; do + if curl -s "http://127.0.0.1:$PREFILL_PORT/health" > /dev/null 2>&1; then + echo "Prefill server is ready" + break + fi + if ! kill -0 $PREFILL_PID 2>/dev/null; then + echo "ERROR: Prefill server exited early" + tail -50 "$LOG_DIR/prefill.log" + exit 1 + fi + echo "Waiting for prefill server... ($i/120)" + sleep 5 +done + +# Start decode server (GPUs 4-7) +echo "[2/3] Starting decode server on GPUs 4-7..." +CUDA_VISIBLE_DEVICES=0,1,2,3 nohup "$PYTHON" -m sglang.launch_server \ + "${COMMON_ARGS[@]}" \ + --disaggregation-mode decode \ + --base-gpu-id 4 \ + --port "$DECODE_PORT" \ + > "$LOG_DIR/decode.log" 2>&1 & +DECODE_PID=$! +echo "Decode PID: $DECODE_PID" + +# Wait for decode health +for i in {1..120}; do + if curl -s "http://127.0.0.1:$DECODE_PORT/health" > /dev/null 2>&1; then + echo "Decode server is ready" + break + fi + if ! kill -0 $DECODE_PID 2>/dev/null; then + echo "ERROR: Decode server exited early" + tail -50 "$LOG_DIR/decode.log" + exit 1 + fi + echo "Waiting for decode server... ($i/120)" + sleep 5 +done + +# Start PD load balancer +echo "[3/3] Starting PD load balancer on port $LB_PORT..." +nohup "$PYTHON" -m sglang_router.launch_router \ + --pd-disaggregation \ + --mini-lb \ + --prefill "http://127.0.0.1:$PREFILL_PORT" \ + --decode "http://127.0.0.1:$DECODE_PORT" \ + --host "$HOST" \ + --port "$LB_PORT" \ + > "$LOG_DIR/lb.log" 2>&1 & +LB_PID=$! +echo "LB PID: $LB_PID" + +# Wait for LB health +for i in {1..60}; do + if curl -s "http://127.0.0.1:$LB_PORT/health" > /dev/null 2>&1; then + echo "Load balancer is ready" + break + fi + if ! kill -0 $LB_PID 2>/dev/null; then + echo "ERROR: Load balancer exited early" + tail -50 "$LOG_DIR/lb.log" + exit 1 + fi + echo "Waiting for load balancer... ($i/60)" + sleep 2 +done + +echo "" +echo "=== PD disaggregation is ready ===" +echo "Load balancer URL: http://127.0.0.1:$LB_PORT" +echo "Prefill URL: http://127.0.0.1:$PREFILL_PORT" +echo "Decode URL: http://127.0.0.1:$DECODE_PORT" +echo "" +echo "Test command:" +echo " curl http://127.0.0.1:$LB_PORT/v1/completions \\" +echo " -H \"Content-Type: application/json\" \\" +echo " -d '{\"model\":\"DeepSeek-V4-Flash\",\"prompt\":\"Hello\",\"max_tokens\":32}'" +echo "" +echo "To stop: kill $PREFILL_PID $DECODE_PID $LB_PID" diff --git a/scripts/start_sglang_dsv4_8card.sh b/scripts/start_sglang_dsv4_8card.sh new file mode 100755 index 0000000..638ecbf --- /dev/null +++ b/scripts/start_sglang_dsv4_8card.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Start sglang 8-card DeepSeek-V4-Flash service +set -e + +cd /data/user1/yy +mkdir -p logs + +export PYTHONUNBUFFERED=1 +export SGLANG_LOG_LEVEL=info +export PATH="/data/user1/yy/envs/sglang/bin:$PATH" + +LOG_FILE="logs/sglang_8card_$(date +%Y%m%d_%H%M%S).log" + +echo "Starting sglang 8-card DSV4 service, logging to ${LOG_FILE}" + +exec /data/user1/yy/envs/sglang/bin/sglang serve \ + --trust-remote-code \ + --model-path /data/models/DeepSeek-V4-Flash \ + --tp 8 \ + --moe-runner-backend marlin \ + --speculative-algorithm EAGLE \ + --speculative-num-steps 3 \ + --speculative-eagle-topk 1 \ + --speculative-num-draft-tokens 4 \ + --host 0.0.0.0 \ + --port 30000 \ + 2>&1 | tee "${LOG_FILE}" diff --git a/scripts/start_vllm_eagle_dsv4_8card.sh b/scripts/start_vllm_eagle_dsv4_8card.sh new file mode 100644 index 0000000..676998e --- /dev/null +++ b/scripts/start_vllm_eagle_dsv4_8card.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +cd /data/user1/yy +mkdir -p logs + +VENV="/data/user1/yy/envs/vllm-main-latest" +PYTHON="$VENV/bin/python" +VLLM="$VENV/bin/vllm" +export PATH="$VENV/bin:$PATH" + +MODEL="/data/models/DeepSeek-V4-Flash" +PORT=30005 +TP=8 +LOG="/data/user1/yy/logs/vllm_eagle_dsv4_tp${TP}_$(date +%Y%m%d_%H%M%S).log" +PID_FILE="/data/user1/yy/vllm_eagle_dsv4.pid" + +export TMPDIR=/data/user1/yy/tmp +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +echo "=== Starting DeepSeek-V4-Flash vLLM EAGLE (TP=$TP) ===" +echo "Model: $MODEL" +echo "Port: $PORT" +echo "Log: $LOG" + +rm -f "$PID_FILE" +nohup "$VLLM" serve "$MODEL" \ + --trust-remote-code \ + --tensor-parallel-size "$TP" \ + --kv-cache-dtype fp8 \ + --block-size 256 \ + --max-model-len auto \ + --max-num-seqs 256 \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --spec-method mtp \ + --spec-model "$MODEL" \ + --spec-tokens 4 \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --port "$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 -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then + echo "Server is ready at http://127.0.0.1:$PORT" + echo "Log: $LOG" + exit 0 + fi + if ! kill -0 $PID 2>/dev/null; then + echo "ERROR: Server exited early" + tail -200 "$LOG" + exit 1 + fi + echo "Waiting... ($i/240)" + sleep 5 +done + +echo "ERROR: Server not healthy after 240 retries" +tail -200 "$LOG" +exit 1 diff --git a/scripts/start_vllm_mtp_dsv4_8card.sh b/scripts/start_vllm_mtp_dsv4_8card.sh new file mode 100644 index 0000000..1c6a20c --- /dev/null +++ b/scripts/start_vllm_mtp_dsv4_8card.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +cd /data/user1/yy +mkdir -p logs + +VENV="/data/user1/yy/envs/vllm-dspark" +export PATH="$VENV/bin:$PATH" +PYTHON="$VENV/bin/python" +VLLM="$VENV/bin/vllm" + +MODEL="/data/models/DeepSeek-V4-Flash" +PORT=30005 +TP=8 +LOG="/data/user1/yy/logs/vllm_mtp_dsv4_tp${TP}_$(date +%Y%m%d_%H%M%S).log" +PID_FILE="/data/user1/yy/vllm_mtp_dsv4.pid" + +export TMPDIR=/data/user1/yy/tmp +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 + +echo "=== Starting DeepSeek-V4-Flash vLLM MTP (TP=$TP) ===" +echo "Model: $MODEL" +echo "Port: $PORT" +echo "Log: $LOG" + +rm -f "$PID_FILE" +nohup "$VLLM" serve "$MODEL" \ + --trust-remote-code \ + --tensor-parallel-size "$TP" \ + --kv-cache-dtype fp8 \ + --block-size 256 \ + --max-model-len auto \ + --max-num-seqs 128 \ + --gpu-memory-utilization 0.85 \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --spec-method mtp \ + --spec-model "$MODEL" \ + --spec-tokens 4 \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --port "$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 -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then + echo "Server is ready at http://127.0.0.1:$PORT" + echo "Log: $LOG" + exit 0 + fi + if ! kill -0 $PID 2>/dev/null; then + echo "ERROR: Server exited early" + tail -200 "$LOG" + exit 1 + fi + echo "Waiting... ($i/240)" + sleep 5 +done + +echo "ERROR: Server not healthy after 240 retries" +tail -200 "$LOG" +exit 1 diff --git a/scripts/start_vllm_pd_single_node.sh b/scripts/start_vllm_pd_single_node.sh new file mode 100755 index 0000000..667282d --- /dev/null +++ b/scripts/start_vllm_pd_single_node.sh @@ -0,0 +1,156 @@ +#!/bin/bash +set -e + +# vLLM + Mooncake PD 分离单节点启动脚本 +# 104 上前 4 张卡做 prefill,后 4 张卡做 decode +# 使用 TCP 传输(因为当前 IB/RDMA 未启用) + +MODEL_PATH="/data/models/DeepSeek-V4-Flash" +BOOTSTRAP_PORT=8998 +PREFILL_PORT=8000 +DECODE_PORT=8001 +ROUTER_PORT=30000 +HOST="0.0.0.0" + +VENV="/data/user1/yy/envs/vllm" +PYTHON="$VENV/bin/python" +VLLM="$VENV/bin/vllm" +VLLM_ROUTER="$VENV/bin/vllm-router" +LOG_DIR="/data/user1/yy/vllm_pd_logs" +mkdir -p "$LOG_DIR" + +echo "=== Starting vLLM PD disaggregation (single node) ===" +echo "Model: $MODEL_PATH" +echo "Prefill GPUs: 0-3, port $PREFILL_PORT" +echo "Decode GPUs: 4-7, port $DECODE_PORT" +echo "Router port: $ROUTER_PORT" +echo "Logs: $LOG_DIR" +echo "" + +# Clean up old logs +rm -f "$LOG_DIR"/*.log + +# 使用 TCP 协议(IB 没起来,不能用 rdma) +export VLLM_MOONCAKE_PROTOCOL=tcp +export VLLM_MOONCAKE_BOOTSTRAP_PORT=$BOOTSTRAP_PORT + +# Common args +PREFILL_KV_CONFIG='{"kv_connector":"MooncakeConnector","kv_role":"kv_producer","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"enforce_handshake_compat":false,"mooncake_protocol":"tcp"}}' +DECODE_KV_CONFIG='{"kv_connector":"MooncakeConnector","kv_role":"kv_consumer","kv_load_failure_policy":"fail","kv_buffer_device":"cuda","kv_connector_extra_config":{"enforce_handshake_compat":false,"mooncake_protocol":"tcp"}}' + +# Start prefill server (GPUs 0-3) +echo "[1/3] Starting prefill server on GPUs 0-3..." +CUDA_VISIBLE_DEVICES=0,1,2,3 nohup "$VLLM" serve "$MODEL_PATH" \ + --trust-remote-code \ + --kv-cache-dtype fp8 \ + --block-size 256 \ + --port "$PREFILL_PORT" \ + --data-parallel-size 4 \ + --enable-expert-parallel \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --max-model-len auto \ + --max-num-batched-tokens 16384 \ + --max-num-seqs 8 \ + --enforce-eager \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --kv-transfer-config "$PREFILL_KV_CONFIG" \ + > "$LOG_DIR/prefill.log" 2>&1 & +PREFILL_PID=$! +echo "Prefill PID: $PREFILL_PID" + +# Wait for prefill health +for i in {1..120}; do + if curl -s "http://127.0.0.1:$PREFILL_PORT/health" > /dev/null 2>&1; then + echo "Prefill server is ready" + break + fi + if ! kill -0 $PREFILL_PID 2>/dev/null; then + echo "ERROR: Prefill server exited early" + tail -80 "$LOG_DIR/prefill.log" + exit 1 + fi + echo "Waiting for prefill server... ($i/120)" + sleep 5 +done + +# Start decode server (GPUs 4-7) +echo "[2/3] Starting decode server on GPUs 4-7..." +CUDA_VISIBLE_DEVICES=4,5,6,7 nohup "$VLLM" serve "$MODEL_PATH" \ + --trust-remote-code \ + --kv-cache-dtype fp8 \ + --block-size 256 \ + --port "$DECODE_PORT" \ + --data-parallel-size 4 \ + --enable-expert-parallel \ + --tokenizer-mode deepseek_v4 \ + --reasoning-parser deepseek_v4 \ + --max-model-len auto \ + --max-num-seqs 512 \ + --max-num-batched-tokens 512 \ + --compilation-config '{"mode":0,"cudagraph_mode":"FULL_DECODE_ONLY","max_cudagraph_capture_size":512,"compile_ranges_endpoints":[512]}' \ + --no-disable-hybrid-kv-cache-manager \ + --disable-uvicorn-access-log \ + --kv-transfer-config "$DECODE_KV_CONFIG" \ + > "$LOG_DIR/decode.log" 2>&1 & +DECODE_PID=$! +echo "Decode PID: $DECODE_PID" + +# Wait for decode health +for i in {1..120}; do + if curl -s "http://127.0.0.1:$DECODE_PORT/health" > /dev/null 2>&1; then + echo "Decode server is ready" + break + fi + if ! kill -0 $DECODE_PID 2>/dev/null; then + echo "ERROR: Decode server exited early" + tail -80 "$LOG_DIR/decode.log" + exit 1 + fi + echo "Waiting for decode server... ($i/120)" + sleep 5 +done + +# Start vllm-router +echo "[3/3] Starting vllm-router on port $ROUTER_PORT..." +nohup "$VLLM_ROUTER" \ + --policy round_robin \ + --vllm-pd-disaggregation \ + --prefill "http://127.0.0.1:$PREFILL_PORT" \ + --decode "http://127.0.0.1:$DECODE_PORT" \ + --host 127.0.0.1 \ + --port "$ROUTER_PORT" \ + --intra-node-data-parallel-size 4 \ + --kv-connector mooncake \ + > "$LOG_DIR/router.log" 2>&1 & +ROUTER_PID=$! +echo "Router PID: $ROUTER_PID" + +# Wait for router health +for i in {1..60}; do + if curl -s "http://127.0.0.1:$ROUTER_PORT/health" > /dev/null 2>&1; then + echo "Router is ready" + break + fi + if ! kill -0 $ROUTER_PID 2>/dev/null; then + echo "ERROR: Router exited early" + tail -50 "$LOG_DIR/router.log" + exit 1 + fi + echo "Waiting for router... ($i/60)" + sleep 2 +done + +echo "" +echo "=== vLLM PD disaggregation is ready ===" +echo "Router URL: http://127.0.0.1:$ROUTER_PORT" +echo "Prefill URL: http://127.0.0.1:$PREFILL_PORT" +echo "Decode URL: http://127.0.0.1:$DECODE_PORT" +echo "" +echo "Test command:" +echo " curl http://127.0.0.1:$ROUTER_PORT/v1/completions \\" +echo " -H \"Content-Type: application/json\" \\" +echo " -d '{\"model\":\"DeepSeek-V4-Flash\",\"prompt\":\"Hello\",\"max_tokens\":32}'" +echo "" +echo "To stop: kill $PREFILL_PID $DECODE_PID $ROUTER_PID" diff --git a/scripts/test_block_sparse_attn.py b/scripts/test_block_sparse_attn.py new file mode 100644 index 0000000..c5fe0d4 --- /dev/null +++ b/scripts/test_block_sparse_attn.py @@ -0,0 +1,84 @@ +import torch +import block_sparse_attn +from block_sparse_attn import block_sparse_attn_func, token_streaming_attn_func, block_streaming_attn_func + +print("block_sparse_attn version:", block_sparse_attn.__version__) +print("Available funcs:", [k for k in dir(block_sparse_attn) if not k.startswith("_")]) + +device = "cuda:0" +batch_size = 1 +nheads = 4 +head_dim = 64 +seqlen = 256 + +total_len = batch_size * seqlen +q = torch.randn(total_len, nheads, head_dim, device=device, dtype=torch.float16) +k = torch.randn(total_len, nheads, head_dim, device=device, dtype=torch.float16) +v = torch.randn(total_len, nheads, head_dim, device=device, dtype=torch.float16) + +cu_seqlens_q = torch.arange(0, total_len + 1, seqlen, device=device, dtype=torch.int32) +cu_seqlens_k = cu_seqlens_q.clone() + +# 1. dense attention (all heads mask_type=0) +head_mask_type_dense = torch.zeros(nheads, device=device, dtype=torch.int32) +streaming_info = torch.tensor([1, 3] * nheads, device=device, dtype=torch.int32) +out_dense = block_sparse_attn_func( + q, k, v, + cu_seqlens_q, cu_seqlens_k, + head_mask_type_dense, + streaming_info, + None, + seqlen, seqlen, + 0.0, + is_causal=False, +) +print("dense output shape:", out_dense.shape) +assert out_dense.shape == q.shape + +# 2. block-sparse attention (2 dense heads + 2 blocksparse heads) +num_blocksparse_heads = 2 +nrow = seqlen // 128 +ncol = seqlen // 128 +base_blockmask = torch.zeros(batch_size, num_blocksparse_heads, nrow, ncol, device=device, dtype=torch.bool) +base_blockmask[:, :, :, :ncol//2] = True # attend to first half + +head_mask_type_sparse = torch.tensor([0, 0, 1, 1], device=device, dtype=torch.int32) +out_sparse = block_sparse_attn_func( + q, k, v, + cu_seqlens_q, cu_seqlens_k, + head_mask_type_sparse, + streaming_info, + base_blockmask, + seqlen, seqlen, + 0.0, + is_causal=False, +) +print("block-sparse output shape:", out_sparse.shape) +assert out_sparse.shape == q.shape + +# 3. token-level streaming attention +head_mask_type_stream = torch.full((nheads,), -1, device=device, dtype=torch.int32) +out_stream = token_streaming_attn_func( + q, k, v, + cu_seqlens_q, cu_seqlens_k, + head_mask_type_stream, + streaming_info, + seqlen, seqlen, +) +print("token-streaming output shape:", out_stream.shape) +assert out_stream.shape == q.shape + +# 4. block-level streaming attention +out_block_stream = block_streaming_attn_func( + q, k, v, + cu_seqlens_q, cu_seqlens_k, + head_mask_type_stream, + streaming_info, + seqlen, seqlen, + 0.0, + is_causal=True, +) +print("block-streaming output shape:", out_block_stream.shape) +assert out_block_stream.shape == q.shape + +print("All smoke tests passed!") diff --git a/scripts/wait_and_profile_dspark.sh b/scripts/wait_and_profile_dspark.sh new file mode 100755 index 0000000..944b5c2 --- /dev/null +++ b/scripts/wait_and_profile_dspark.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Wait for GPUs to become idle (no other compute processes), then run DSpark profiles. + +set -e + +ROOT=/data/user1/yy +PROFILE_PID_FILE=/tmp/profile_dspark.pid +MONITOR_LOG=$ROOT/bench_results/dspark_profile_monitor.log +RESULT_BASE=$ROOT/bench_results/dspark_profile_$(date +%Y%m%d_%H%M%S) + +mkdir -p "$RESULT_BASE" "$ROOT/tmp" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$MONITOR_LOG" +} + +is_gpu_idle() { + # Consider idle if no process other than ours uses > 1GB on any GPU. + # Returns 0 (true) if idle, 1 (false) if busy. + nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheader,nounits | \ + awk -F',' ' + { + pid=$1; mem=$2; + # Exclude our own known PIDs (monitor script, shell, etc.) + if (mem > 1024) { busy++; } + } + END { exit (busy > 0 ? 1 : 0) } + ' +} + +log "Starting GPU idle monitor. Will check every 5 minutes." +log "Results will go to $RESULT_BASE" + +wait_for_idle() { + while true; do + if is_gpu_idle; then + log "GPUs appear idle." + return 0 + else + log "GPUs still busy. Sleeping 5 minutes." + sleep 300 + fi + done +} + +# Run profiles sequentially, checking GPU idle state before each run. +CONFIGS=("dspark-st3:64" "dspark-st5:64" "dspark-st3:1" "nospec:64") + +for entry in "${CONFIGS[@]}"; do + IFS=':' read -r config concurrency <<< "$entry" + log "Waiting before profile: config=$config concurrency=$concurrency" + wait_for_idle + log "Running profile: config=$config concurrency=$concurrency" + "$ROOT/envs/vllm-dspark/bin/python" "$ROOT/scripts/profile_dspark.py" \ + --config "$config" --concurrency "$concurrency" --tool nsys \ + >> "$RESULT_BASE/profile_${config}_c${concurrency}.log" 2>&1 + log "Finished profile: config=$config concurrency=$concurrency" +done + +log "All profiles complete. Results in $RESULT_BASE"