zhijianliu commited on
Commit
2923bf7
·
verified ·
1 Parent(s): 872bcc4

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ pipeline_tag: text-generation
3
+ library_name: transformers
4
+ base_model:
5
+ - Qwen/Qwen3.5-397B-A17B
6
+ license: apache-2.0
7
+ inference: false
8
+ tags:
9
+ - dflash
10
+ - speculative-decoding
11
+ - speculative-decoding-draft
12
+ - block-diffusion
13
+ - draft-model
14
+ - diffusion-language-model
15
+ - efficiency
16
+ - qwen
17
+ - qwen3
18
+ - qwen3.5
19
+ - sglang
20
+ ---
21
+
22
+ # Qwen3.5-397B-A17B-DFlash
23
+
24
+ [Paper](https://arxiv.org/abs/2602.06036) | [Github](https://github.com/z-lab/dflash) | [Blog](https://z-lab.ai/projects/dflash)
25
+
26
+ This is a joint release collaboration between [Z-Lab](https://z-lab.ai), [Modal](https://modal.com), and [SGLang](https://github.com/sgl-project/sglang). The draft model is mirrored across the following Hugging Face repositories:
27
+
28
+ - [`z-lab/Qwen3.5-397B-A17B-DFlash`](https://huggingface.co/z-lab/Qwen3.5-397B-A17B-DFlash)
29
+ - [`modal-labs/Qwen3.5-397B-A17B-DFlash`](https://huggingface.co/modal-labs/Qwen3.5-397B-A17B-DFlash)
30
+ - [`lmsys/Qwen3.5-397B-A17B-DFlash`](https://huggingface.co/lmsys/Qwen3.5-397B-A17B-DFlash)
31
+
32
+ This repository contains a DFlash draft model for `Qwen/Qwen3.5-397B-A17B`. It is not a standalone language model. It is intended to be paired with the target model in a speculative decoding server.
33
+
34
+ DFlash uses a lightweight block diffusion draft model to propose multiple tokens in parallel. The target model verifies those proposals, improving serving throughput while preserving the target model's output distribution.
35
+
36
+ ## Quick Start
37
+
38
+ This model should be used with an inference server that supports DFlash speculative decoding. An example SGLang deployment is:
39
+
40
+ ```bash
41
+ export SGLANG_ENABLE_OVERLAP_PLAN_STREAM=1
42
+
43
+ python -m sglang.launch_server \
44
+ --model-path Qwen/Qwen3.5-397B-A17B \
45
+ --trust-remote-code \
46
+ --speculative-algorithm DFLASH \
47
+ --speculative-draft-model-path z-lab/Qwen3.5-397B-A17B-DFlash \
48
+ --speculative-dflash-block-size 8 \
49
+ --speculative-draft-attention-backend fa4 \
50
+ --attention-backend trtllm_mha \
51
+ --linear-attn-prefill-backend triton \
52
+ --linear-attn-decode-backend flashinfer \
53
+ --mamba-scheduler-strategy extra_buffer \
54
+ --tp-size 8 \
55
+ --max-running-requests 32 \
56
+ --cuda-graph-max-bs-decode 32 \
57
+ --cuda-graph-backend-prefill tc_piecewise \
58
+ --enable-flashinfer-allreduce-fusion \
59
+ --mem-fraction-static 0.8 \
60
+ --host 0.0.0.0 \
61
+ --port 30000
62
+ ```
63
+
64
+ Block size `8` is the recommended default for higher-concurrency serving. Block size `16` gives longer accept lengths and the best concurrency-1 throughput in most workloads.
65
+
66
+ ## Benchmark Results
67
+
68
+ We benchmarked DFlash against the autoregressive baseline and Qwen3.5's built-in MTP draft path. DFlash reaches up to `4.31x` speedup at concurrency 1 and `2.77x` at concurrency 32. Across the benchmark suite, DFlash delivers higher throughput than MTP at every tested setting.
69
+
70
+ ### Setup
71
+
72
+ - Runtime: SGLang on 8x NVIDIA B200 GPUs, tensor parallel size 8, `bfloat16`
73
+ - Backends: `trtllm_mha` target attention, `fa4` DFlash draft attention, `flashinfer` linear attention
74
+ - Workloads: GSM8K, MATH500, HumanEval, MBPP, and MT-Bench with the Qwen chat template
75
+ - Decoding: greedy, thinking enabled, max output length 4096 tokens
76
+ - Measurement: 5 independent runs per configuration at concurrency 1 and 32 with continuous batching
77
+ - Throughput: generated output tokens / wall-clock benchmark time, including prefill and scheduling
78
+ - Accept length: `completion_tokens / spec_verify_ct` per generation turn, averaged across generation turns
79
+
80
+ For reproduction, the evaluation scripts, Modal runner, runtime patches, and raw benchmark outputs are included under [`benchmark/`](https://huggingface.co/modal-labs/Qwen3.5-397B-A17B-DFlash/tree/main/benchmark).
81
+
82
+ ### Throughput and Speedup
83
+
84
+ Each cell is `output tok/s (speedup)`. Bold marks the fastest speculative configuration in each row.
85
+
86
+ #### Concurrency 1
87
+
88
+ | Workload | Baseline | MTP steps=3 | DFlash block=4 | MTP steps=7 | DFlash block=8 | MTP steps=15 | DFlash block=16 |
89
+ | --- | --- | --- | --- | --- | --- | --- | --- |
90
+ | gsm8k | 204.6 (1.00x) | 484.5 (2.37x) | 530.0 (2.59x) | 536.7 (2.62x) | 689.0 (3.37x) | 439.2 (2.15x) | **711.3 (3.48x)** |
91
+ | math500 | 204.4 (1.00x) | 505.2 (2.47x) | 551.3 (2.70x) | 589.4 (2.88x) | 762.6 (3.73x) | 499.8 (2.44x) | **831.9 (4.07x)** |
92
+ | humaneval | 202.9 (1.00x) | 483.0 (2.38x) | 543.6 (2.68x) | 557.9 (2.75x) | 752.5 (3.71x) | 480.5 (2.37x) | **874.6 (4.31x)** |
93
+ | mbpp | 204.6 (1.00x) | 487.4 (2.38x) | 550.3 (2.69x) | 543.7 (2.66x) | 751.2 (3.67x) | 445.6 (2.18x) | **807.8 (3.95x)** |
94
+ | mt-bench | 202.6 (1.00x) | 442.9 (2.19x) | 473.6 (2.34x) | 441.5 (2.18x) | **545.9 (2.69x)** | 338.0 (1.67x) | 515.3 (2.54x) |
95
+
96
+ #### Concurrency 32
97
+
98
+ | Workload | Baseline | MTP steps=3 | DFlash block=4 | MTP steps=7 | DFlash block=8 | MTP steps=15 | DFlash block=16 |
99
+ | --- | --- | --- | --- | --- | --- | --- | --- |
100
+ | gsm8k | 2597.7 (1.00x) | 4462.3 (1.72x) | 5078.0 (1.95x) | 4902.9 (1.89x) | **6158.9 (2.37x)** | 4079.5 (1.57x) | 5648.8 (2.17x) |
101
+ | math500 | 2615.5 (1.00x) | 4838.6 (1.85x) | 5318.6 (2.03x) | 5601.6 (2.14x) | **6910.5 (2.64x)** | 4855.0 (1.86x) | 6857.8 (2.62x) |
102
+ | humaneval | 2452.7 (1.00x) | 3838.1 (1.56x) | 5081.4 (2.07x) | 4347.4 (1.77x) | 6666.0 (2.72x) | 3917.2 (1.60x) | **6783.7 (2.77x)** |
103
+ | mbpp | 2582.3 (1.00x) | 4129.8 (1.60x) | 5265.7 (2.04x) | 4461.0 (1.73x) | **6681.4 (2.59x)** | 3783.7 (1.47x) | 6344.1 (2.46x) |
104
+ | mt-bench | 2537.3 (1.00x) | 4024.6 (1.59x) | 4425.3 (1.74x) | 3991.7 (1.57x) | **4763.2 (1.88x)** | 3100.3 (1.22x) | 4016.0 (1.58x) |
105
+
106
+ ### Accept Length
107
+
108
+ Mean accept length at concurrency 1. Bold marks the higher value in each matched MTP/DFlash pair.
109
+
110
+ | Workload | MTP steps=3 | DFlash block=4 | MTP steps=7 | DFlash block=8 | MTP steps=15 | DFlash block=16 |
111
+ | --- | --- | --- | --- | --- | --- | --- |
112
+ | gsm8k | **3.578** | 3.548 | 5.612 | **5.738** | 7.006 | **7.696** |
113
+ | math500 | **3.636** | 3.622 | 5.810 | **6.003** | 7.294 | **8.212** |
114
+ | humaneval | 3.643 | **3.697** | 5.893 | **6.292** | 7.541 | **9.344** |
115
+ | mbpp | 3.532 | **3.588** | 5.357 | **5.860** | 6.398 | **7.801** |
116
+ | mt-bench | **3.244** | 3.188 | 4.567 | **4.584** | 5.256 | **5.523** |
117
+
118
+ ## Citation
119
+
120
+ If you find DFlash useful, please cite the original paper:
121
+
122
+ ```bibtex
123
+ @article{chen2026dflash,
124
+ title = {{DFlash: Block Diffusion for Flash Speculative Decoding}},
125
+ author = {Chen, Jian and Liang, Yesheng and Liu, Zhijian},
126
+ journal = {arXiv preprint arXiv:2602.06036},
127
+ year = {2026}
128
+ }
129
+ ```
benchmark/README.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Benchmark Reproduction
2
+
3
+ This folder contains the benchmark harness, Modal runner, runtime patch, and raw outputs used for the model card tables.
4
+
5
+ These scripts are intended to reproduce the release benchmark. They are not a general-purpose benchmark framework, and they should not be treated as a reference for Modal application structure or production Modal best practices.
6
+
7
+ ## Full Sweep
8
+
9
+ The full sweep runs baseline, MTP, and DFlash across all workloads at concurrency 1 and 32, with 5 independent runs per configuration. It uses up to 32 Modal containers, each with 8 B200 GPUs.
10
+
11
+ This is a large benchmark and can take a long time to complete.
12
+
13
+ ```bash
14
+ modal run run_modal_benchmark.py \
15
+ --workloads all \
16
+ --concurrencies 1,32 \
17
+ --spec-modes mtp,dflash \
18
+ --mtp-num-steps 3,7,15 \
19
+ --dflash-draft-model modal-labs/Qwen3.5-397B-A17B-DFlash \
20
+ --dflash-block-sizes 4,8,16 \
21
+ --runs-per-config 5 \
22
+ --raw-output results/full-sweep-r5.raw.jsonl \
23
+ --csv-output results/full-sweep-r5.csv
24
+ ```
25
+
26
+ The generated CSV and raw JSONL are written under `results/`. The raw JSONL contains per-run records; the CSV contains aggregated rows used by the README tables.
27
+
28
+ ## Files
29
+
30
+ - `run_benchmark.py`: local benchmark harness and result aggregation.
31
+ - `run_modal_benchmark.py`: Modal entrypoint that dispatches one benchmark job per configuration.
32
+ - `patches/`: runtime patch helper and FlashInfer patch used by the reproduced environment.
33
+ - `results/full-sweep-r5.csv`: aggregate benchmark results.
34
+ - `results/full-sweep-r5.raw.jsonl`: raw per-run benchmark records.
benchmark/patches/apply_patches.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ PATCH_DIR = Path(os.environ.get("MODAL_PATCH_DIR", "/root/patches"))
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PatchSpec:
16
+ name: str
17
+ module: str
18
+ patch_file: str
19
+ strip: int
20
+ includes: tuple[str, ...] = ()
21
+
22
+
23
+ PATCHES = (
24
+ PatchSpec(
25
+ name="flashinfer-pr-3312",
26
+ module="flashinfer",
27
+ patch_file="flashinfer-pr-3312.patch",
28
+ strip=1,
29
+ ),
30
+ )
31
+
32
+
33
+ def _package_parent(module_name: str) -> Path:
34
+ spec = importlib.util.find_spec(module_name)
35
+ if spec is None or spec.submodule_search_locations is None:
36
+ raise RuntimeError(f"Could not find installed package {module_name!r}.")
37
+ locations = list(spec.submodule_search_locations)
38
+ if not locations:
39
+ raise RuntimeError(f"Installed package {module_name!r} has no package path.")
40
+ return Path(locations[0]).resolve().parent
41
+
42
+
43
+ def _git_apply_command(spec: PatchSpec, patch_path: Path) -> list[str]:
44
+ cmd = ["git", "apply", f"-p{spec.strip}"]
45
+ for include in spec.includes:
46
+ cmd.append(f"--include={include}")
47
+ cmd.append(str(patch_path))
48
+ return cmd
49
+
50
+
51
+ def _check(cmd: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
52
+ return subprocess.run(
53
+ cmd,
54
+ cwd=cwd,
55
+ text=True,
56
+ stdout=subprocess.PIPE,
57
+ stderr=subprocess.STDOUT,
58
+ )
59
+
60
+
61
+ def _apply_patch(spec: PatchSpec) -> None:
62
+ patch_path = PATCH_DIR / spec.patch_file
63
+ if not patch_path.exists():
64
+ raise RuntimeError(f"Missing patch file: {patch_path}")
65
+
66
+ cwd = _package_parent(spec.module)
67
+ base_cmd = _git_apply_command(spec, patch_path)
68
+ reverse_cmd = [*base_cmd[:2], "--reverse", "--check", *base_cmd[2:]]
69
+ check_cmd = [*base_cmd[:2], "--check", *base_cmd[2:]]
70
+
71
+ reverse = _check(reverse_cmd, cwd=cwd)
72
+ if reverse.returncode == 0:
73
+ print(f"[patch] {spec.name} already applied under {cwd}")
74
+ return
75
+
76
+ check = _check(check_cmd, cwd=cwd)
77
+ if check.returncode != 0:
78
+ print(check.stdout, file=sys.stderr)
79
+ raise RuntimeError(f"Patch {spec.name} does not apply under {cwd}.")
80
+
81
+ print(f"[patch] applying {spec.name} under {cwd}")
82
+ subprocess.run(base_cmd, cwd=cwd, check=True)
83
+
84
+
85
+ def main() -> None:
86
+ for patch in PATCHES:
87
+ _apply_patch(patch)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
benchmark/patches/flashinfer-pr-3312.patch ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ From 8c8f152595c11316bc72166929819c56bd3acdb8 Mon Sep 17 00:00:00 2001
2
+ From: William Hu <willhu@modal.com>
3
+ Date: Wed, 13 May 2026 15:19:48 +0000
4
+ Subject: [PATCH] Fix Blackwell GDN chunked kernel compilation
5
+
6
+ ---
7
+ .../blackwell/gated_delta_net_chunked.py | 18 +++++++++---------
8
+ 1 file changed, 9 insertions(+), 9 deletions(-)
9
+
10
+ diff --git a/flashinfer/gdn_kernels/blackwell/gated_delta_net_chunked.py b/flashinfer/gdn_kernels/blackwell/gated_delta_net_chunked.py
11
+ index 53fe44ce55..2c22c8e120 100644
12
+ --- a/flashinfer/gdn_kernels/blackwell/gated_delta_net_chunked.py
13
+ +++ b/flashinfer/gdn_kernels/blackwell/gated_delta_net_chunked.py
14
+ @@ -2403,7 +2403,7 @@ def compute_group_0(
15
+
16
+ tKKrKK_out = cute.make_rmem_tensor_like(tKKrKK, self.io_dtype)
17
+ tCrAI = tiled_ainv_r2s.retile(tKKrKK_out)
18
+ - for sub in cutlass.range(tKKrKK.shape[2]):
19
+ + for sub in cutlass.range_constexpr(tKKrKK.shape[2]):
20
+ cute.copy(
21
+ tiled_shared_t2r,
22
+ tTR_tStS[None, 0, sub, kk_handle.index],
23
+ @@ -2443,7 +2443,7 @@ def compute_group_0(
24
+ # Convert fp32 tQKrQK -> fp16 and store to sQk, one subtile at a time.
25
+ tQKrQK_out = cute.make_rmem_tensor_like(tQKrQK, self.io_dtype)
26
+ tCrQK = tiled_qk_r2s.retile(tQKrQK_out)
27
+ - for sub in cutlass.range(tQKrQK.shape[2]):
28
+ + for sub in cutlass.range_constexpr(tQKrQK.shape[2]):
29
+ cute.copy(
30
+ tiled_shared_t2r,
31
+ tTR_tStS[None, 0, sub, qk_handle.index],
32
+ @@ -2979,7 +2979,7 @@ def _load_initial_state(
33
+ )[None, None, 0, 0]
34
+ tGR_tCgState = thr_state_r2t.partition_S(gS_init)
35
+ kv_acc_handle = kv_acc_producer.acquire_and_advance()
36
+ - for sub in cutlass.range(tRT_tCrState.shape[2]):
37
+ + for sub in cutlass.range_constexpr(tRT_tCrState.shape[2]):
38
+ # 1. Load S_init fp32 GMEM -> fp32 registers
39
+ cute.autovec_copy(
40
+ tGR_tCgState[None, 0, sub],
41
+ @@ -3063,7 +3063,7 @@ def _store_final_state(
42
+ # Wait for last GEMM-7 to finish
43
+ kv_acc_handle = kv_acc_consumer.wait_and_advance()
44
+
45
+ - for sub in cutlass.range(tTR_rState.shape[2]):
46
+ + for sub in cutlass.range_constexpr(tTR_rState.shape[2]):
47
+ # Read state TMEM -> fp32 registers
48
+ cute.copy(
49
+ tiled_state_t2r,
50
+ @@ -3344,7 +3344,7 @@ def compute_group_1(
51
+ kv_handle = kv_acc_consumer.wait_and_advance()
52
+
53
+ state_inp_ready_handle = state_inp_ready_producer.acquire_and_advance()
54
+ - for sub in cutlass.range(tRT_rState_inp.shape[2]):
55
+ + for sub in cutlass.range_constexpr(tRT_rState_inp.shape[2]):
56
+ cute.copy(
57
+ tiled_state_t2r,
58
+ tTR_tCtState[None, 0, sub, kv_handle.index],
59
+ @@ -3386,7 +3386,7 @@ def compute_group_1(
60
+ state_inp_ready_handle.commit()
61
+
62
+ # Load S_prev -> scale by Phi -> write Phi*S_prev back to same TMEM slot.
63
+ - for sub in cutlass.range(tTR_rState.shape[2]):
64
+ + for sub in cutlass.range_constexpr(tTR_rState.shape[2]):
65
+ for k in cutlass.range(sub_tile_size, vectorize=True):
66
+ tTR_rState[k, 0, sub] = tTR_rState[k, 0, sub] * cumprod_total
67
+ cute.copy(
68
+ @@ -3471,7 +3471,7 @@ def compute_group_1(
69
+ # Write scaled result back to same q_state TMEM slot so GEMM 6 accumulates on top.
70
+ if cutlass.const_expr(valid_state):
71
+ qs_handle = q_state_acc_consumer.wait_and_advance()
72
+ - for sub in cutlass.range(tTR_rQS.shape[1]):
73
+ + for sub in cutlass.range_constexpr(tTR_rQS.shape[1]):
74
+ cute.copy(
75
+ tiled_qs_t2r,
76
+ tTR_tCtQS[None, sub, 0, qs_handle.index],
77
+ @@ -3499,7 +3499,7 @@ def compute_group_1(
78
+
79
+ tTR_rNv = cute.make_rmem_tensor_like(tTR_tCcShared, self.acc_dtype)
80
+ tTR_rNv_inp = cute.make_rmem_tensor_like(tTR_rNv, self.io_dtype)
81
+ - for sub in cutlass.range(tTR_rNv.shape[1]):
82
+ + for sub in cutlass.range_constexpr(tTR_rNv.shape[1]):
83
+ cute.copy(
84
+ tiled_shared_t2r,
85
+ tTR_tCtShared[None, sub, 0, nv_handle.index],
86
+ @@ -3523,7 +3523,7 @@ def compute_group_1(
87
+ decay_v_handle = shared_inp_ready_producer.acquire_and_advance()
88
+ tTR_rDv = tTR_rNv
89
+ tRT_rDv_inp = cute.make_rmem_tensor_like(tTR_rDv, self.io_dtype)
90
+ - for sub in cutlass.range(tTR_rDv.shape[1]):
91
+ + for sub in cutlass.range_constexpr(tTR_rDv.shape[1]):
92
+ for k in cutlass.range(sub_tile_size, vectorize=True):
93
+ tTR_rDv[k, sub, 0] = tTR_rDv[k, sub, 0] * tGrDecayScale[k, sub, 0]
94
+ tRT_rDv_inp[None, sub, 0].store(
benchmark/results/full-sweep-r5.csv ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ workload,backend,tp,mode,mtp_num_steps,dflash_block_size,concurrency,source_sample_count,source_generation_turn_count,runs_per_config,successful_runs,failed_runs,status,successful_run_numbers,failed_run_numbers,failure_messages,measured_sample_count,measured_generation_turn_count,output_toks_per_s,output_toks_per_s_std,latency_s,latency_s_std,output_tokens,speedup_vs_baseline,accept_length_mean_from_conc1,accept_length_mean_this_conc,accept_length_mean_this_conc_std,spec_verify_ct_sum
2
+ gsm8k,trtllm_mha,8,baseline,,,1,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,204.555526,2.145455,12635.580474,132.149628,2584451,,,,,0
3
+ gsm8k,trtllm_mha,8,baseline,,,32,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,2597.659928,4.218969,1000.151140,1.480044,2598050,,,,,0
4
+ math500,trtllm_mha,8,baseline,,,1,500,500,5,5,0,ok,"1,2,3,4,5",,,500,500,204.419625,2.159273,8028.813504,85.127007,1641100,,,,,0
5
+ math500,trtllm_mha,8,baseline,,,32,500,500,5,5,0,ok,"1,2,3,4,5",,,1500,1500,2615.492879,4.644475,1877.009292,4.043241,4909295,,,,,0
6
+ humaneval,trtllm_mha,8,baseline,,,1,164,164,5,5,0,ok,"1,2,3,4,5",,,164,164,202.857238,2.560204,708.993392,9.004063,143806,,,,,0
7
+ humaneval,trtllm_mha,8,baseline,,,32,164,164,5,5,0,ok,"1,2,3,4,5",,,1148,1148,2452.728100,6.909290,444.353322,2.704221,1089875,,,,,0
8
+ mbpp,trtllm_mha,8,baseline,,,1,257,257,5,5,0,ok,"1,2,3,4,5",,,257,257,204.627794,2.490696,1431.877214,17.502916,292967,,,,,0
9
+ mbpp,trtllm_mha,8,baseline,,,32,257,257,5,5,0,ok,"1,2,3,4,5",,,1028,1028,2582.331962,16.758001,457.544569,3.271579,1181489,,,,,0
10
+ mt-bench,trtllm_mha,8,baseline,,,1,80,160,5,5,0,ok,"1,2,3,4,5",,,80,160,202.627315,2.064800,1838.911437,18.577677,372583,,,,,0
11
+ mt-bench,trtllm_mha,8,baseline,,,32,80,160,5,5,0,ok,"1,2,3,4,5",,,560,1120,2537.280069,5.056329,1035.679885,6.266022,2627822,,,,,0
12
+ gsm8k,trtllm_mha,8,mtp_s3,3,,1,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,484.515466,1.092627,5333.599683,12.056415,2584201,2.368626,3.578206,3.578206,0.000000,741348
13
+ gsm8k,trtllm_mha,8,mtp_s3,3,,32,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,4462.277886,15.165587,582.052122,4.740478,2597311,1.717807,3.578206,3.575976,0.002168,745479
14
+ math500,trtllm_mha,8,mtp_s3,3,,1,500,500,5,5,0,ok,"1,2,3,4,5",,,500,500,505.220064,0.998658,3243.616450,6.397895,1638735,2.471485,3.636082,3.636082,0.000000,452103
15
+ math500,trtllm_mha,8,mtp_s3,3,,32,500,500,5,5,0,ok,"1,2,3,4,5",,,1500,1500,4838.626174,12.753263,1026.170872,6.286706,4965232,1.849986,3.636082,3.625503,0.002359,1374541
16
+ humaneval,trtllm_mha,8,mtp_s3,3,,1,164,164,5,5,0,ok,"1,2,3,4,5",,,164,164,482.971992,0.848283,291.487600,0.511024,140780,2.380847,3.642722,3.642722,0.000000,39031
17
+ humaneval,trtllm_mha,8,mtp_s3,3,,32,164,164,5,5,0,ok,"1,2,3,4,5",,,1148,1148,3838.116080,27.625345,274.271199,4.884137,1052625,1.564836,3.642722,3.639766,0.004242,291133
18
+ mbpp,trtllm_mha,8,mtp_s3,3,,1,257,257,5,5,0,ok,"1,2,3,4,5",,,257,257,487.387355,1.119903,590.364547,1.356427,287735,2.381824,3.532388,3.532388,0.000000,81640
19
+ mbpp,trtllm_mha,8,mtp_s3,3,,32,257,257,5,5,0,ok,"1,2,3,4,5",,,1028,1028,4129.797287,36.155174,286.132210,3.022273,1181583,1.599251,3.532388,3.529203,0.000768,335234
20
+ mt-bench,trtllm_mha,8,mtp_s3,3,,1,80,160,5,5,0,ok,"1,2,3,4,5",,,80,160,442.878488,0.540885,795.239395,0.971336,352194,2.185680,3.243687,3.243687,0.000000,110843
21
+ mt-bench,trtllm_mha,8,mtp_s3,3,,32,80,160,5,5,0,ok,"1,2,3,4,5",,,560,1120,4024.576377,25.738403,646.195740,3.841449,2600641,1.586177,3.243687,3.246441,0.002884,817461
22
+ gsm8k,trtllm_mha,8,mtp_s7,7,,1,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,536.722583,0.679534,4919.137870,6.225032,2640209,2.623848,5.612091,5.612091,0.000000,503221
23
+ gsm8k,trtllm_mha,8,mtp_s7,7,,32,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,4902.882902,12.827666,538.479681,5.171311,2640066,1.887423,5.612091,5.626037,0.006586,501991
24
+ math500,trtllm_mha,8,mtp_s7,7,,1,500,500,5,5,0,ok,"1,2,3,4,5",,,500,500,589.358698,1.263260,2788.186599,5.961230,1643236,2.883083,5.810442,5.810442,0.000000,285934
25
+ math500,trtllm_mha,8,mtp_s7,7,,32,500,500,5,5,0,ok,"1,2,3,4,5",,,1500,1500,5601.600738,16.440313,889.261794,2.935187,4981255,2.141700,5.810442,5.803398,0.002263,867913
26
+ humaneval,trtllm_mha,8,mtp_s7,7,,1,164,164,5,5,0,ok,"1,2,3,4,5",,,164,164,557.878026,1.239829,255.434983,0.568559,142501,2.750102,5.892962,5.892962,0.000000,24985
27
+ humaneval,trtllm_mha,8,mtp_s7,7,,32,164,164,5,5,0,ok,"1,2,3,4,5",,,1148,1148,4347.409011,39.265600,241.632788,1.833643,1050421,1.772479,5.892962,5.879749,0.015840,181992
28
+ mbpp,trtllm_mha,8,mtp_s7,7,,1,257,257,5,5,0,ok,"1,2,3,4,5",,,257,257,543.663939,1.714676,547.506183,1.726410,297657,2.656843,5.357283,5.357283,0.000000,55728
29
+ mbpp,trtllm_mha,8,mtp_s7,7,,32,257,257,5,5,0,ok,"1,2,3,4,5",,,1028,1028,4461.002797,39.420307,263.953747,2.570069,1177424,1.727509,5.357283,5.345784,0.004161,221558
30
+ mt-bench,trtllm_mha,8,mtp_s7,7,,1,80,160,5,5,0,ok,"1,2,3,4,5",,,80,160,441.534700,1.301296,825.671311,2.434773,364560,2.179048,4.566832,4.566832,0.000000,85056
31
+ mt-bench,trtllm_mha,8,mtp_s7,7,,32,80,160,5,5,0,ok,"1,2,3,4,5",,,560,1120,3991.741300,32.936341,647.364770,5.132053,2584055,1.573236,4.566832,4.541887,0.004682,602291
32
+ gsm8k,trtllm_mha,8,mtp_s15,15,,1,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,439.221305,1.229396,5995.457146,16.788503,2633316,2.147198,7.006249,7.006249,0.000000,420009
33
+ gsm8k,trtllm_mha,8,mtp_s15,15,,32,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,4079.495401,11.679675,633.391910,2.207605,2583916,1.570450,7.006249,7.028783,0.011465,409272
34
+ math500,trtllm_mha,8,mtp_s15,15,,1,500,500,5,5,0,ok,"1,2,3,4,5",,,500,500,499.795245,0.970217,3292.170089,6.386190,1645406,2.444947,7.293583,7.293583,0.000000,231130
35
+ math500,trtllm_mha,8,mtp_s15,15,,32,500,500,5,5,0,ok,"1,2,3,4,5",,,1500,1500,4855.008946,8.652228,1010.792887,4.438424,4907387,1.856250,7.293583,7.311887,0.008057,686655
36
+ humaneval,trtllm_mha,8,mtp_s15,15,,1,164,164,5,5,0,ok,"1,2,3,4,5",,,164,164,480.467613,1.472814,294.038677,0.900927,141275,2.368501,7.541116,7.541116,0.000000,19796
37
+ humaneval,trtllm_mha,8,mtp_s15,15,,32,164,164,5,5,0,ok,"1,2,3,4,5",,,1148,1148,3917.161422,42.698655,255.505991,5.499563,1000688,1.597063,7.541116,7.621674,0.023832,136276
38
+ mbpp,trtllm_mha,8,mtp_s15,15,,1,257,257,5,5,0,ok,"1,2,3,4,5",,,257,257,445.583765,0.958029,657.470281,1.413204,292957,2.177533,6.397939,6.397939,0.000000,46043
39
+ mbpp,trtllm_mha,8,mtp_s15,15,,32,257,257,5,5,0,ok,"1,2,3,4,5",,,1028,1028,3783.733878,62.936735,308.658226,5.474796,1167609,1.465239,6.397939,6.397951,0.005553,184152
40
+ mt-bench,trtllm_mha,8,mtp_s15,15,,1,80,160,5,5,0,ok,"1,2,3,4,5",,,80,160,337.978110,1.422539,1118.839806,4.712557,378138,1.667979,5.256258,5.256258,0.000000,79620
41
+ mt-bench,trtllm_mha,8,mtp_s15,15,,32,80,160,5,5,0,ok,"1,2,3,4,5",,,560,1120,3100.276145,10.251785,825.233091,5.046098,2558455,1.221890,5.256258,5.243602,0.026024,539673
42
+ gsm8k,trtllm_mha,8,dflash_b4,,4,1,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,529.973579,2.750086,4982.874443,25.817938,2640735,2.590854,3.548232,3.548232,0.000000,764746
43
+ gsm8k,trtllm_mha,8,dflash_b4,,4,32,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,5077.980519,16.739932,511.322717,5.595165,2596508,1.954829,3.548232,3.552208,0.002646,751403
44
+ math500,trtllm_mha,8,dflash_b4,,4,1,500,500,5,5,0,ok,"1,2,3,4,5",,,500,500,551.321433,1.244735,2992.718261,6.748220,1649943,2.697008,3.621767,3.621767,0.000000,457367
45
+ math500,trtllm_mha,8,dflash_b4,,4,32,500,500,5,5,0,ok,"1,2,3,4,5",,,1500,1500,5318.551035,10.917699,929.319749,3.097557,4942635,2.033479,3.621767,3.615034,0.000278,1372522
46
+ humaneval,trtllm_mha,8,dflash_b4,,4,1,164,164,5,5,0,ok,"1,2,3,4,5",,,164,164,543.640692,2.479882,257.803241,1.175757,140150,2.679918,3.696950,3.696950,0.000000,38313
47
+ humaneval,trtllm_mha,8,dflash_b4,,4,32,164,164,5,5,0,ok,"1,2,3,4,5",,,1148,1148,5081.410529,29.000740,208.624984,1.507118,1060084,2.071738,3.696950,3.675101,0.002088,290297
48
+ mbpp,trtllm_mha,8,dflash_b4,,4,1,257,257,5,5,0,ok,"1,2,3,4,5",,,257,257,550.264179,1.811608,534.984026,1.756697,294380,2.689098,3.587588,3.587588,0.000000,82300
49
+ mbpp,trtllm_mha,8,dflash_b4,,4,32,257,257,5,5,0,ok,"1,2,3,4,5",,,1028,1028,5265.730230,28.737314,224.127865,1.861627,1180158,2.039138,3.587588,3.592031,0.001113,329893
50
+ mt-bench,trtllm_mha,8,dflash_b4,,4,1,80,160,5,5,0,ok,"1,2,3,4,5",,,80,160,473.614100,2.823570,774.775195,4.629198,366934,2.337366,3.187625,3.187625,0.000000,118198
51
+ mt-bench,trtllm_mha,8,dflash_b4,,4,32,80,160,5,5,0,ok,"1,2,3,4,5",,,560,1120,4425.336211,24.979476,591.383061,3.704361,2617052,1.744126,3.187625,3.193840,0.002660,839845
52
+ gsm8k,trtllm_mha,8,dflash_b8,,8,1,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,688.975247,0.392468,3765.307512,2.145629,2594203,3.368158,5.737918,5.737918,0.000000,486689
53
+ gsm8k,trtllm_mha,8,dflash_b8,,8,32,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,6158.902318,9.870529,425.115125,4.125581,2618218,2.370942,5.737918,5.736067,0.006153,491935
54
+ math500,trtllm_mha,8,dflash_b8,,8,1,500,500,5,5,0,ok,"1,2,3,4,5",,,500,500,762.557640,0.697669,2156.116490,1.973454,1644162,3.730354,6.002648,6.002648,0.000000,277367
55
+ math500,trtllm_mha,8,dflash_b8,,8,32,500,500,5,5,0,ok,"1,2,3,4,5",,,1500,1500,6910.547547,20.426974,711.452192,2.596787,4916500,2.642159,6.002648,5.997982,0.005109,830565
56
+ humaneval,trtllm_mha,8,dflash_b8,,8,1,164,164,5,5,0,ok,"1,2,3,4,5",,,164,164,752.541701,1.714420,186.686775,0.425482,140489,3.709711,6.292280,6.292280,0.000000,23074
57
+ humaneval,trtllm_mha,8,dflash_b8,,8,32,164,164,5,5,0,ok,"1,2,3,4,5",,,1148,1148,6665.997002,79.178765,159.330876,1.022338,1062057,2.717789,6.292280,6.261022,0.010858,173340
58
+ mbpp,trtllm_mha,8,dflash_b8,,8,1,257,257,5,5,0,ok,"1,2,3,4,5",,,257,257,751.207961,1.162745,401.422233,0.620335,301551,3.671094,5.859880,5.859880,0.000000,52097
59
+ mbpp,trtllm_mha,8,dflash_b8,,8,32,257,257,5,5,0,ok,"1,2,3,4,5",,,1028,1028,6681.357693,82.794608,175.347448,2.882539,1171380,2.587335,5.859880,5.841399,0.004678,203168
60
+ mt-bench,trtllm_mha,8,dflash_b8,,8,1,80,160,5,5,0,ok,"1,2,3,4,5",,,80,160,545.919718,1.078610,673.313558,1.330103,367574,2.694206,4.583530,4.583530,0.000000,86934
61
+ mt-bench,trtllm_mha,8,dflash_b8,,8,32,80,160,5,5,0,ok,"1,2,3,4,5",,,560,1120,4763.179599,22.935557,545.122784,1.479916,2596518,1.877278,4.583530,4.562539,0.003416,614050
62
+ gsm8k,trtllm_mha,8,dflash_b16,,16,1,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,711.312214,0.629131,3647.057609,3.224631,2594195,3.477355,7.695530,7.695530,0.000000,385387
63
+ gsm8k,trtllm_mha,8,dflash_b16,,16,32,1319,1319,5,5,0,ok,"1,2,3,4,5",,,1319,1319,5648.751541,15.871959,465.626848,7.499576,2630146,2.174554,7.695530,7.669634,0.022668,393802
64
+ math500,trtllm_mha,8,dflash_b16,,16,1,500,500,5,5,0,ok,"1,2,3,4,5",,,500,500,831.942883,0.807027,1987.593474,1.928107,1653563,4.069780,8.212200,8.212200,0.000000,208019
65
+ math500,trtllm_mha,8,dflash_b16,,16,32,500,500,5,5,0,ok,"1,2,3,4,5",,,1500,1500,6857.774343,11.793806,720.006772,1.546469,4937653,2.621982,8.212200,8.250632,0.014168,618127
66
+ humaneval,trtllm_mha,8,dflash_b16,,16,1,164,164,5,5,0,ok,"1,2,3,4,5",,,164,164,874.628457,1.713351,168.090176,0.329217,147016,4.311547,9.344168,9.344168,0.000000,16775
67
+ humaneval,trtllm_mha,8,dflash_b16,,16,32,164,164,5,5,0,ok,"1,2,3,4,5",,,1148,1148,6783.744877,39.371020,152.562043,2.393255,1034873,2.765796,9.344168,9.106922,0.022708,119005
68
+ mbpp,trtllm_mha,8,dflash_b16,,16,1,257,257,5,5,0,ok,"1,2,3,4,5",,,257,257,807.812087,1.041081,361.183488,0.465354,291768,3.947714,7.801495,7.801495,0.000000,38256
69
+ mbpp,trtllm_mha,8,dflash_b16,,16,32,257,257,5,5,0,ok,"1,2,3,4,5",,,1028,1028,6344.112863,52.433245,185.962377,2.091507,1179682,2.456738,7.801495,7.820849,0.012002,155067
70
+ mt-bench,trtllm_mha,8,dflash_b16,,16,1,80,160,5,5,0,ok,"1,2,3,4,5",,,80,160,515.340841,0.331810,715.456826,0.460634,368704,2.543294,5.523387,5.523387,0.000000,76747
71
+ mt-bench,trtllm_mha,8,dflash_b16,,16,32,80,160,5,5,0,ok,"1,2,3,4,5",,,560,1120,4015.994589,28.511157,650.353476,6.109586,2611696,1.582795,5.523387,5.534502,0.019500,541217
benchmark/results/full-sweep-r5.raw.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
benchmark/run_benchmark.py ADDED
@@ -0,0 +1,2406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import csv
5
+ import io
6
+ import json
7
+ import math
8
+ import os
9
+ import statistics
10
+ import sys
11
+ import time
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
13
+ from dataclasses import dataclass, replace
14
+ from pathlib import Path
15
+ from typing import Any, Optional
16
+ from urllib.request import urlretrieve
17
+
18
+
19
+ CACHE_DIR = Path(__file__).resolve().parent / "cache"
20
+ SUPPORTED_WORKLOADS = ("gsm8k", "math500", "humaneval", "mbpp", "mt-bench")
21
+ DEFAULT_WORKLOADS = "gsm8k"
22
+ DEFAULT_TIMEOUT_S = 3600
23
+ DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH = 600
24
+ SERVER_SHUTDOWN_DRAIN_TIMEOUT_S = 30.0
25
+ SERVER_SHUTDOWN_TIMEOUT_S = 120.0
26
+ GSM8K_TEST_URL = "https://raw.githubusercontent.com/openai/grade-school-math/master/grade_school_math/data/test.jsonl"
27
+ MT_BENCH_QUESTION_URL = (
28
+ "https://raw.githubusercontent.com/lm-sys/FastChat/main/"
29
+ "fastchat/llm_judge/data/mt_bench/question.jsonl"
30
+ )
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class SharedServerConfig:
35
+ tp_size: int = 8
36
+ attention_backend: str = "trtllm_mha"
37
+ dtype: str = "bfloat16"
38
+ max_running_requests: int = 32
39
+ cuda_graph_max_bs: int = 32
40
+ mem_fraction_static: Optional[float] = 0.8
41
+ page_size: Optional[int] = None
42
+ mamba_scheduler_strategy: str = "extra_buffer"
43
+ mamba_ssm_dtype: str = "bfloat16"
44
+ linear_attn_backend: str = "flashinfer"
45
+ enable_piecewise_cuda_graph: bool = True
46
+ enable_flashinfer_allreduce_fusion: bool = True
47
+
48
+ def to_args(self) -> list[str]:
49
+ args = [
50
+ "--trust-remote-code",
51
+ "--attention-backend",
52
+ self.attention_backend,
53
+ "--tp-size",
54
+ str(self.tp_size),
55
+ "--dtype",
56
+ self.dtype,
57
+ "--max-running-requests",
58
+ str(self.max_running_requests),
59
+ "--cuda-graph-max-bs-decode",
60
+ str(self.cuda_graph_max_bs),
61
+ "--mamba-scheduler-strategy",
62
+ self.mamba_scheduler_strategy,
63
+ "--mamba-ssm-dtype",
64
+ self.mamba_ssm_dtype,
65
+ "--linear-attn-backend",
66
+ self.linear_attn_backend,
67
+ "--cuda-graph-backend-prefill",
68
+ "tc_piecewise" if self.enable_piecewise_cuda_graph else "disabled",
69
+ ]
70
+ if self.enable_flashinfer_allreduce_fusion:
71
+ args.append("--enable-flashinfer-allreduce-fusion")
72
+ if self.mem_fraction_static is not None:
73
+ args.extend(["--mem-fraction-static", str(self.mem_fraction_static)])
74
+ if self.page_size is not None:
75
+ args.extend(["--page-size", str(int(self.page_size))])
76
+ return args
77
+
78
+ def summary_label(self) -> str:
79
+ return (
80
+ f"tp:{self.tp_size},attention:{self.attention_backend},"
81
+ f"dtype:{self.dtype},max_running:{self.max_running_requests},"
82
+ f"cuda_graph_max_bs_decode:{self.cuda_graph_max_bs},"
83
+ f"mem_fraction:{self.mem_fraction_static},page_size:{self.page_size}"
84
+ )
85
+
86
+
87
+ BASE_SHARED_SERVER_CONFIG = SharedServerConfig()
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class MTPConfig:
92
+ num_steps: int
93
+ eagle_topk: int = 1
94
+
95
+ @property
96
+ def mode_key(self) -> str:
97
+ return f"mtp_s{self.num_steps}"
98
+
99
+ @property
100
+ def display_name(self) -> str:
101
+ return f"MTP steps={self.num_steps}"
102
+
103
+ @property
104
+ def expect_spec(self) -> bool:
105
+ return True
106
+
107
+ @property
108
+ def num_draft_tokens(self) -> int:
109
+ return self.num_steps + 1
110
+
111
+ def to_args(self) -> list[str]:
112
+ return [
113
+ "--speculative-algorithm",
114
+ "EAGLE",
115
+ "--speculative-num-steps",
116
+ str(self.num_steps),
117
+ "--speculative-eagle-topk",
118
+ str(self.eagle_topk),
119
+ "--speculative-num-draft-tokens",
120
+ str(self.num_draft_tokens),
121
+ ]
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class DFlashConfig:
126
+ draft_model: str
127
+ block_size: Optional[int] = None
128
+ draft_attention_backend: str = "fa4"
129
+
130
+ @property
131
+ def mode_key(self) -> str:
132
+ if self.block_size is None:
133
+ return "dflash"
134
+ return f"dflash_b{self.block_size}"
135
+
136
+ @property
137
+ def display_name(self) -> str:
138
+ if self.block_size is None:
139
+ return "DFLASH"
140
+ return f"DFLASH block={self.block_size}"
141
+
142
+ @property
143
+ def expect_spec(self) -> bool:
144
+ return True
145
+
146
+ def to_args(self) -> list[str]:
147
+ args = [
148
+ "--speculative-algorithm",
149
+ "DFLASH",
150
+ "--speculative-draft-model-path",
151
+ self.draft_model,
152
+ "--speculative-draft-attention-backend",
153
+ self.draft_attention_backend,
154
+ ]
155
+ if self.block_size is not None:
156
+ args.extend(["--speculative-dflash-block-size", str(int(self.block_size))])
157
+ return args
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class BaselineConfig:
162
+ @property
163
+ def mode_key(self) -> str:
164
+ return "baseline"
165
+
166
+ @property
167
+ def display_name(self) -> str:
168
+ return "Baseline"
169
+
170
+ @property
171
+ def expect_spec(self) -> bool:
172
+ return False
173
+
174
+ def to_args(self) -> list[str]:
175
+ return []
176
+
177
+
178
+ @dataclass(frozen=True)
179
+ class ServerDeployment:
180
+ shared_config: SharedServerConfig
181
+ mode_config: BaselineConfig | MTPConfig | DFlashConfig
182
+
183
+ @property
184
+ def mode_key(self) -> str:
185
+ return self.mode_config.mode_key
186
+
187
+ @property
188
+ def display_name(self) -> str:
189
+ return self.mode_config.display_name
190
+
191
+ @property
192
+ def expect_spec(self) -> bool:
193
+ return self.mode_config.expect_spec
194
+
195
+ @property
196
+ def server_args(self) -> list[str]:
197
+ return [*self.shared_config.to_args(), *self.mode_config.to_args()]
198
+
199
+ @property
200
+ def mtp_num_steps(self) -> Optional[int]:
201
+ if isinstance(self.mode_config, MTPConfig):
202
+ return self.mode_config.num_steps
203
+ return None
204
+
205
+ @property
206
+ def dflash_block_size(self) -> Optional[int]:
207
+ if isinstance(self.mode_config, DFlashConfig):
208
+ return self.mode_config.block_size
209
+ return None
210
+
211
+ @property
212
+ def enable_overlap_plan_stream(self) -> bool:
213
+ # MTP on Qwen3.5 uses CUDA HybridLinearAttnBackend, which does not
214
+ # implement update_verify_buffers_to_fill_after_draft for overlap plan
215
+ # streams. DFlash has its own compatible planning path.
216
+ return isinstance(self.mode_config, DFlashConfig)
217
+
218
+
219
+ @dataclass(frozen=True)
220
+ class DeploymentSweep:
221
+ include_baseline: bool
222
+ spec_modes: tuple[str, ...]
223
+ mtp_num_steps: tuple[int, ...]
224
+ dflash_draft_model: Optional[str]
225
+ dflash_block_sizes: tuple[Optional[int], ...]
226
+
227
+ @property
228
+ def mode_keys(self) -> list[str]:
229
+ mode_keys: list[str] = []
230
+ for spec_mode in self.spec_modes:
231
+ if spec_mode == "mtp":
232
+ mode_keys.extend(f"mtp_s{int(steps)}" for steps in self.mtp_num_steps)
233
+ elif spec_mode == "dflash":
234
+ for block_size in self.dflash_block_sizes:
235
+ mode_keys.append(
236
+ DFlashConfig("", block_size=block_size).mode_key
237
+ )
238
+ else:
239
+ mode_keys.append(spec_mode)
240
+ return mode_keys
241
+
242
+
243
+ @dataclass(frozen=True)
244
+ class SamplingConfig:
245
+ enable_thinking: bool
246
+ max_new_tokens: int
247
+ temperature: float
248
+ top_p: float
249
+ top_k: int
250
+
251
+
252
+ @dataclass(frozen=True)
253
+ class BenchmarkMethodologyConfig:
254
+ num_samples: Optional[int]
255
+ min_generation_turns_per_config: int
256
+ min_warmup_generation_turns: int
257
+ runs_per_config: int
258
+ timeout_s: int = DEFAULT_TIMEOUT_S
259
+ server_shutdown_drain_timeout_s: float = SERVER_SHUTDOWN_DRAIN_TIMEOUT_S
260
+ server_shutdown_timeout_s: float = SERVER_SHUTDOWN_TIMEOUT_S
261
+
262
+
263
+ @dataclass(frozen=True)
264
+ class SweepConfig:
265
+ target_model: str
266
+ dflash_draft_model: Optional[str]
267
+ workloads: tuple[str, ...]
268
+ concurrencies: tuple[int, ...]
269
+ sampling: SamplingConfig
270
+ methodology: BenchmarkMethodologyConfig
271
+ deployment_sweep: DeploymentSweep
272
+ csv_output: Optional[str]
273
+
274
+
275
+ @dataclass(frozen=True)
276
+ class RunKey:
277
+ workload: str
278
+ backend: str
279
+ tp: int
280
+ concurrency: int
281
+ mode: str
282
+
283
+ def metric_key(self) -> tuple[str, int, int, str]:
284
+ return (self.backend, self.tp, self.concurrency, self.mode)
285
+
286
+
287
+ @dataclass(frozen=True)
288
+ class BenchmarkPlan:
289
+ measured_samples: list[list[str]]
290
+ warmup_samples: list[list[str]]
291
+ warmdown_samples: list[list[str]]
292
+
293
+ @property
294
+ def measured_sample_count(self) -> int:
295
+ return len(self.measured_samples)
296
+
297
+ @property
298
+ def measured_generation_turn_count(self) -> int:
299
+ return _generation_turn_count(self.measured_samples)
300
+
301
+ @property
302
+ def warmup_generation_turn_count(self) -> int:
303
+ return _generation_turn_count(self.warmup_samples)
304
+
305
+ @property
306
+ def warmdown_generation_turn_count(self) -> int:
307
+ return _generation_turn_count(self.warmdown_samples)
308
+
309
+
310
+ @dataclass(frozen=True)
311
+ class BenchmarkJob:
312
+ target_model: str
313
+ workload: str
314
+ deployment: ServerDeployment
315
+ concurrency: int
316
+ run_index: int
317
+ sampling: SamplingConfig
318
+ methodology: BenchmarkMethodologyConfig
319
+
320
+ @property
321
+ def key(self) -> RunKey:
322
+ shared_config = self.deployment.shared_config
323
+ return RunKey(
324
+ workload=self.workload,
325
+ backend=shared_config.attention_backend,
326
+ tp=shared_config.tp_size,
327
+ concurrency=self.concurrency,
328
+ mode=self.deployment.mode_key,
329
+ )
330
+
331
+ @property
332
+ def label(self) -> str:
333
+ key = self.key
334
+ return (
335
+ f"workload={key.workload} backend={key.backend} tp={key.tp} "
336
+ f"conc={key.concurrency} ({self.deployment.display_name})"
337
+ )
338
+
339
+ @property
340
+ def run_label(self) -> str:
341
+ return f"run={self.run_index + 1}/{self.methodology.runs_per_config}"
342
+
343
+
344
+ def _parse_int_csv(value: str) -> list[int]:
345
+ return [int(x) for x in value.split(",") if x.strip()]
346
+
347
+
348
+ def _parse_optional_int_csv(value: str) -> list[Optional[int]]:
349
+ values: list[Optional[int]] = []
350
+ for raw in value.split(","):
351
+ item = raw.strip().lower()
352
+ if not item:
353
+ continue
354
+ if item in ("default", "none"):
355
+ values.append(None)
356
+ else:
357
+ values.append(int(item))
358
+ return values or [None]
359
+
360
+
361
+ def _parse_str_csv(value: str) -> list[str]:
362
+ return [x.strip().lower() for x in value.split(",") if x.strip()]
363
+
364
+
365
+ def _duplicate_values(values: list[str]) -> list[str]:
366
+ seen: set[str] = set()
367
+ duplicates: list[str] = []
368
+ for value in values:
369
+ if value in seen and value not in duplicates:
370
+ duplicates.append(value)
371
+ seen.add(value)
372
+ return duplicates
373
+
374
+
375
+ def _shared_server_config_to_payload(config: SharedServerConfig) -> dict[str, Any]:
376
+ return {
377
+ "tp_size": config.tp_size,
378
+ "attention_backend": config.attention_backend,
379
+ "dtype": config.dtype,
380
+ "max_running_requests": config.max_running_requests,
381
+ "cuda_graph_max_bs": config.cuda_graph_max_bs,
382
+ "mem_fraction_static": config.mem_fraction_static,
383
+ "page_size": config.page_size,
384
+ "mamba_scheduler_strategy": config.mamba_scheduler_strategy,
385
+ "mamba_ssm_dtype": config.mamba_ssm_dtype,
386
+ "linear_attn_backend": config.linear_attn_backend,
387
+ "enable_piecewise_cuda_graph": config.enable_piecewise_cuda_graph,
388
+ "enable_flashinfer_allreduce_fusion": (
389
+ config.enable_flashinfer_allreduce_fusion
390
+ ),
391
+ }
392
+
393
+
394
+ def _shared_server_config_from_payload(payload: dict[str, Any]) -> SharedServerConfig:
395
+ return SharedServerConfig(
396
+ tp_size=int(payload["tp_size"]),
397
+ attention_backend=str(payload["attention_backend"]),
398
+ dtype=str(payload["dtype"]),
399
+ max_running_requests=int(payload["max_running_requests"]),
400
+ cuda_graph_max_bs=int(payload["cuda_graph_max_bs"]),
401
+ mem_fraction_static=payload.get("mem_fraction_static"),
402
+ page_size=payload.get("page_size"),
403
+ mamba_scheduler_strategy=str(payload["mamba_scheduler_strategy"]),
404
+ mamba_ssm_dtype=str(payload["mamba_ssm_dtype"]),
405
+ linear_attn_backend=str(payload["linear_attn_backend"]),
406
+ enable_piecewise_cuda_graph=bool(payload["enable_piecewise_cuda_graph"]),
407
+ enable_flashinfer_allreduce_fusion=bool(
408
+ payload["enable_flashinfer_allreduce_fusion"]
409
+ ),
410
+ )
411
+
412
+
413
+ def _mode_config_to_payload(
414
+ config: BaselineConfig | MTPConfig | DFlashConfig,
415
+ ) -> dict[str, Any]:
416
+ if isinstance(config, BaselineConfig):
417
+ return {"kind": "baseline"}
418
+ if isinstance(config, MTPConfig):
419
+ return {
420
+ "kind": "mtp",
421
+ "num_steps": config.num_steps,
422
+ "eagle_topk": config.eagle_topk,
423
+ }
424
+ if isinstance(config, DFlashConfig):
425
+ return {
426
+ "kind": "dflash",
427
+ "draft_model": config.draft_model,
428
+ "block_size": config.block_size,
429
+ "draft_attention_backend": config.draft_attention_backend,
430
+ }
431
+ raise TypeError(f"Unsupported mode config type: {type(config).__name__}")
432
+
433
+
434
+ def _mode_config_from_payload(
435
+ payload: dict[str, Any],
436
+ ) -> BaselineConfig | MTPConfig | DFlashConfig:
437
+ kind = payload["kind"]
438
+ if kind == "baseline":
439
+ return BaselineConfig()
440
+ if kind == "mtp":
441
+ return MTPConfig(
442
+ num_steps=int(payload["num_steps"]),
443
+ eagle_topk=int(payload.get("eagle_topk", 1)),
444
+ )
445
+ if kind == "dflash":
446
+ return DFlashConfig(
447
+ draft_model=str(payload["draft_model"]),
448
+ block_size=payload.get("block_size"),
449
+ draft_attention_backend=str(
450
+ payload.get("draft_attention_backend", "fa4")
451
+ ),
452
+ )
453
+ raise ValueError(f"Unsupported mode config kind: {kind}")
454
+
455
+
456
+ def _deployment_to_payload(deployment: ServerDeployment) -> dict[str, Any]:
457
+ return {
458
+ "shared_config": _shared_server_config_to_payload(deployment.shared_config),
459
+ "mode_config": _mode_config_to_payload(deployment.mode_config),
460
+ }
461
+
462
+
463
+ def _deployment_from_payload(payload: dict[str, Any]) -> ServerDeployment:
464
+ return ServerDeployment(
465
+ shared_config=_shared_server_config_from_payload(payload["shared_config"]),
466
+ mode_config=_mode_config_from_payload(payload["mode_config"]),
467
+ )
468
+
469
+
470
+ def _sampling_config_to_payload(config: SamplingConfig) -> dict[str, Any]:
471
+ return {
472
+ "enable_thinking": config.enable_thinking,
473
+ "max_new_tokens": config.max_new_tokens,
474
+ "temperature": config.temperature,
475
+ "top_p": config.top_p,
476
+ "top_k": config.top_k,
477
+ }
478
+
479
+
480
+ def _sampling_config_from_payload(payload: dict[str, Any]) -> SamplingConfig:
481
+ return SamplingConfig(
482
+ enable_thinking=bool(payload["enable_thinking"]),
483
+ max_new_tokens=int(payload["max_new_tokens"]),
484
+ temperature=float(payload["temperature"]),
485
+ top_p=float(payload["top_p"]),
486
+ top_k=int(payload["top_k"]),
487
+ )
488
+
489
+
490
+ def _methodology_to_payload(config: BenchmarkMethodologyConfig) -> dict[str, Any]:
491
+ return {
492
+ "num_samples": config.num_samples,
493
+ "min_generation_turns_per_config": (
494
+ config.min_generation_turns_per_config
495
+ ),
496
+ "min_warmup_generation_turns": config.min_warmup_generation_turns,
497
+ "runs_per_config": config.runs_per_config,
498
+ "timeout_s": config.timeout_s,
499
+ "server_shutdown_drain_timeout_s": (
500
+ config.server_shutdown_drain_timeout_s
501
+ ),
502
+ "server_shutdown_timeout_s": config.server_shutdown_timeout_s,
503
+ }
504
+
505
+
506
+ def _methodology_from_payload(
507
+ payload: dict[str, Any],
508
+ ) -> BenchmarkMethodologyConfig:
509
+ return BenchmarkMethodologyConfig(
510
+ num_samples=payload.get("num_samples"),
511
+ min_generation_turns_per_config=int(
512
+ payload["min_generation_turns_per_config"]
513
+ ),
514
+ min_warmup_generation_turns=int(payload["min_warmup_generation_turns"]),
515
+ runs_per_config=int(payload["runs_per_config"]),
516
+ timeout_s=int(payload["timeout_s"]),
517
+ server_shutdown_drain_timeout_s=float(
518
+ payload["server_shutdown_drain_timeout_s"]
519
+ ),
520
+ server_shutdown_timeout_s=float(payload["server_shutdown_timeout_s"]),
521
+ )
522
+
523
+
524
+ def benchmark_job_to_payload(job: BenchmarkJob) -> dict[str, Any]:
525
+ return {
526
+ "target_model": job.target_model,
527
+ "workload": job.workload,
528
+ "deployment": _deployment_to_payload(job.deployment),
529
+ "concurrency": job.concurrency,
530
+ "run_index": job.run_index,
531
+ "sampling": _sampling_config_to_payload(job.sampling),
532
+ "methodology": _methodology_to_payload(job.methodology),
533
+ }
534
+
535
+
536
+ def benchmark_job_from_payload(payload: dict[str, Any]) -> BenchmarkJob:
537
+ return BenchmarkJob(
538
+ target_model=str(payload["target_model"]),
539
+ workload=str(payload["workload"]),
540
+ deployment=_deployment_from_payload(payload["deployment"]),
541
+ concurrency=int(payload["concurrency"]),
542
+ run_index=int(payload["run_index"]),
543
+ sampling=_sampling_config_from_payload(payload["sampling"]),
544
+ methodology=_methodology_from_payload(payload["methodology"]),
545
+ )
546
+
547
+
548
+ def _parse_workload_selection(value: str) -> list[str]:
549
+ values = _parse_str_csv(value)
550
+ if values == ["all"]:
551
+ return list(SUPPORTED_WORKLOADS)
552
+ unknown = sorted(set(values) - set(SUPPORTED_WORKLOADS))
553
+ if unknown:
554
+ raise ValueError(
555
+ f"Unknown workloads: {','.join(unknown)}. Supported: "
556
+ f"{','.join(SUPPORTED_WORKLOADS)} or all."
557
+ )
558
+ if not values:
559
+ raise ValueError("--workloads must include at least one workload.")
560
+ duplicates = _duplicate_values(values)
561
+ if duplicates:
562
+ raise ValueError(f"Duplicate workloads: {','.join(duplicates)}.")
563
+ return values
564
+
565
+
566
+ def _filter_attention_backends(backends: list[str], *, device_sm: int) -> list[str]:
567
+ if not (80 <= device_sm <= 90):
568
+ backends = [b for b in backends if b != "fa3"]
569
+ if device_sm < 100:
570
+ backends = [b for b in backends if b not in ("fa4", "trtllm_mha")]
571
+ return backends or ["flashinfer"]
572
+
573
+
574
+ def _read_jsonl(path: Path) -> list[dict]:
575
+ with open(path) as f:
576
+ return [json.loads(line) for line in f]
577
+
578
+
579
+ def _download_to_cache(url: str, filename: str) -> Path:
580
+ CACHE_DIR.mkdir(exist_ok=True)
581
+ out_path = CACHE_DIR / filename
582
+ if out_path.exists():
583
+ return out_path
584
+
585
+ tmp_path = out_path.with_name(f"{out_path.name}.{os.getpid()}.tmp")
586
+ print(f"[download] {url}")
587
+ urlretrieve(url, tmp_path)
588
+ os.replace(tmp_path, out_path)
589
+ return out_path
590
+
591
+
592
+ def _load_gsm8k_user_prompts() -> list[str]:
593
+ path = _download_to_cache(GSM8K_TEST_URL, "gsm8k_test.jsonl")
594
+ if not path.is_file():
595
+ raise RuntimeError(f"GSM8K data file does not exist: {path}")
596
+
597
+ prompts: list[str] = []
598
+ for row in _read_jsonl(path):
599
+ prompts.append(
600
+ row["question"]
601
+ + "\nPlease reason step by step, and put your final answer within \\boxed{}."
602
+ )
603
+ return prompts
604
+
605
+
606
+ def _load_math500_user_prompts() -> list[str]:
607
+ rows = _load_hf_dataset_rows("HuggingFaceH4/MATH-500", split="test")
608
+
609
+ prompts: list[str] = []
610
+ for row in rows:
611
+ prompts.append(
612
+ row["problem"]
613
+ + "\nPlease reason step by step, and put your final answer within \\boxed{}."
614
+ )
615
+ return prompts
616
+
617
+
618
+ def _load_hf_dataset_rows(*load_args, **load_kwargs) -> list[dict]:
619
+ from datasets import load_dataset
620
+
621
+ return list(load_dataset(*load_args, **load_kwargs))
622
+
623
+
624
+ def _load_humaneval_user_prompts() -> list[str]:
625
+ rows = _load_hf_dataset_rows("openai/openai_humaneval", split="test")
626
+
627
+ return [row["prompt"] for row in rows]
628
+
629
+
630
+ def _load_mbpp_user_prompts() -> list[str]:
631
+ rows = _load_hf_dataset_rows(
632
+ "google-research-datasets/mbpp", "sanitized", split="test"
633
+ )
634
+
635
+ return [row["prompt"] for row in rows]
636
+
637
+
638
+ def _load_mt_bench_user_turns() -> list[list[str]]:
639
+ path = _download_to_cache(MT_BENCH_QUESTION_URL, "mt_bench_question.jsonl")
640
+ if not path.is_file():
641
+ raise RuntimeError(f"MT-bench data file does not exist: {path}")
642
+ rows = _read_jsonl(path)
643
+
644
+ prompts: list[list[str]] = []
645
+ for row in rows:
646
+ turns = row.get("turns", row.get("prompt"))
647
+ if not isinstance(turns, list):
648
+ raise RuntimeError(
649
+ "MT-bench rows must contain a list-valued `turns` or `prompt` field."
650
+ )
651
+ turns = [str(turn) for turn in turns[:2]]
652
+ if len(turns) != 2:
653
+ raise RuntimeError(
654
+ f"MT-bench rows must contain exactly two turns; got {len(turns)}."
655
+ )
656
+ prompts.append(turns)
657
+ return prompts
658
+
659
+
660
+ def _load_user_turns(workload: str) -> list[list[str]]:
661
+ if workload == "gsm8k":
662
+ return [[prompt] for prompt in _load_gsm8k_user_prompts()]
663
+ if workload == "math500":
664
+ return [[prompt] for prompt in _load_math500_user_prompts()]
665
+ if workload == "humaneval":
666
+ return [[prompt] for prompt in _load_humaneval_user_prompts()]
667
+ if workload == "mbpp":
668
+ return [[prompt] for prompt in _load_mbpp_user_prompts()]
669
+ if workload == "mt-bench":
670
+ return _load_mt_bench_user_turns()
671
+ raise ValueError(f"Unknown workload: {workload}")
672
+
673
+
674
+ def _flush_cache(
675
+ base_url: str, timeout_s: float = SERVER_SHUTDOWN_DRAIN_TIMEOUT_S
676
+ ) -> None:
677
+ import requests
678
+
679
+ try:
680
+ requests.get(
681
+ base_url + "/flush_cache",
682
+ params={"timeout": float(timeout_s)},
683
+ timeout=max(float(timeout_s) + 5.0, 10.0),
684
+ ).raise_for_status()
685
+ except Exception as exc:
686
+ raise RuntimeError(
687
+ "Failed to flush cache before the next benchmark phase; "
688
+ "SGLang still had pending requests after waiting for drain."
689
+ ) from exc
690
+
691
+
692
+ def _flush_cache_best_effort(base_url: str, timeout_s: float) -> None:
693
+ import requests
694
+
695
+ try:
696
+ requests.get(
697
+ base_url + "/flush_cache",
698
+ params={"timeout": float(timeout_s)},
699
+ timeout=max(float(timeout_s) + 5.0, 10.0),
700
+ ).raise_for_status()
701
+ except Exception as exc:
702
+ print(f"[shutdown] /flush_cache failed before server shutdown: {exc}")
703
+
704
+
705
+ def _shutdown_server(proc, base_url: str, *, drain_timeout_s: float, kill_timeout_s: float) -> None:
706
+ from sglang.srt.utils import kill_process_tree
707
+
708
+ if proc.poll() is not None:
709
+ return
710
+
711
+ _flush_cache_best_effort(base_url, drain_timeout_s)
712
+
713
+ if proc.poll() is not None:
714
+ return
715
+
716
+ print(f"[shutdown] sending SIGTERM to server pid={proc.pid}")
717
+ proc.terminate()
718
+ try:
719
+ proc.wait(timeout=float(kill_timeout_s))
720
+ return
721
+ except Exception:
722
+ print(
723
+ f"[shutdown] server pid={proc.pid} did not exit within "
724
+ f"{kill_timeout_s}s; falling back to kill_process_tree."
725
+ )
726
+
727
+ kill_process_tree(proc.pid, wait_timeout=30)
728
+
729
+
730
+ def _send_generate(
731
+ base_url: str,
732
+ text: str,
733
+ *,
734
+ max_new_tokens: int,
735
+ temperature: float,
736
+ top_p: float,
737
+ top_k: int,
738
+ timeout_s: int,
739
+ ) -> dict:
740
+ import requests
741
+
742
+ sampling_params: dict = {
743
+ "temperature": float(temperature),
744
+ "top_p": float(top_p),
745
+ "top_k": int(top_k),
746
+ "max_new_tokens": int(max_new_tokens),
747
+ }
748
+ resp = requests.post(
749
+ base_url + "/generate",
750
+ json={
751
+ "text": text,
752
+ "sampling_params": sampling_params,
753
+ },
754
+ timeout=int(timeout_s),
755
+ )
756
+ resp.raise_for_status()
757
+ out = resp.json()
758
+ if isinstance(out, list):
759
+ raise RuntimeError(
760
+ "Expected an object response for single /generate, but got "
761
+ f"type={type(out).__name__}."
762
+ )
763
+ return out
764
+
765
+
766
+ @dataclass(frozen=True)
767
+ class BenchMetrics:
768
+ sample_count: int
769
+ generation_turn_count: int
770
+ latency_s: float
771
+ output_tokens: int
772
+ output_toks_per_s: float
773
+ spec_accept_length: Optional[float]
774
+ spec_verify_ct_sum: int
775
+
776
+
777
+ @dataclass(frozen=True)
778
+ class JobResult:
779
+ key: RunKey
780
+ deployment: ServerDeployment
781
+ source_sample_count: int
782
+ source_generation_turn_count: int
783
+ warmup_generation_turn_count: int
784
+ warmdown_generation_turn_count: int
785
+ run_index: int
786
+ metrics: BenchMetrics
787
+
788
+
789
+ @dataclass(frozen=True)
790
+ class JobFailure:
791
+ key: RunKey
792
+ deployment: ServerDeployment
793
+ run_index: int
794
+ error_type: str
795
+ error_message: str
796
+
797
+
798
+ @dataclass(frozen=True)
799
+ class ConfigResult:
800
+ key: RunKey
801
+ deployment: ServerDeployment
802
+ source_sample_count: Optional[int]
803
+ source_generation_turn_count: Optional[int]
804
+ warmup_generation_turn_count: Optional[int]
805
+ warmdown_generation_turn_count: Optional[int]
806
+ metrics: Optional[BenchMetrics]
807
+ repeat_metrics: tuple[BenchMetrics, ...]
808
+ successful_run_indices: tuple[int, ...]
809
+ failures: tuple[JobFailure, ...]
810
+
811
+ @property
812
+ def run_count(self) -> int:
813
+ return self.successful_run_count + self.failed_run_count
814
+
815
+ @property
816
+ def successful_run_count(self) -> int:
817
+ return len(self.repeat_metrics)
818
+
819
+ @property
820
+ def failed_run_count(self) -> int:
821
+ return len(self.failures)
822
+
823
+ @property
824
+ def status(self) -> str:
825
+ if self.failed_run_count == 0:
826
+ return "ok"
827
+ if self.successful_run_count == 0:
828
+ return "failed"
829
+ return "partial_failed"
830
+
831
+
832
+ def _run_key_to_payload(key: RunKey) -> dict[str, Any]:
833
+ return {
834
+ "workload": key.workload,
835
+ "backend": key.backend,
836
+ "tp": key.tp,
837
+ "concurrency": key.concurrency,
838
+ "mode": key.mode,
839
+ }
840
+
841
+
842
+ def _run_key_from_payload(payload: dict[str, Any]) -> RunKey:
843
+ return RunKey(
844
+ workload=str(payload["workload"]),
845
+ backend=str(payload["backend"]),
846
+ tp=int(payload["tp"]),
847
+ concurrency=int(payload["concurrency"]),
848
+ mode=str(payload["mode"]),
849
+ )
850
+
851
+
852
+ def _bench_metrics_to_payload(metrics: BenchMetrics) -> dict[str, Any]:
853
+ return {
854
+ "sample_count": metrics.sample_count,
855
+ "generation_turn_count": metrics.generation_turn_count,
856
+ "latency_s": metrics.latency_s,
857
+ "output_tokens": metrics.output_tokens,
858
+ "output_toks_per_s": metrics.output_toks_per_s,
859
+ "spec_accept_length": metrics.spec_accept_length,
860
+ "spec_verify_ct_sum": metrics.spec_verify_ct_sum,
861
+ }
862
+
863
+
864
+ def _bench_metrics_from_payload(payload: dict[str, Any]) -> BenchMetrics:
865
+ return BenchMetrics(
866
+ sample_count=int(payload["sample_count"]),
867
+ generation_turn_count=int(payload["generation_turn_count"]),
868
+ latency_s=float(payload["latency_s"]),
869
+ output_tokens=int(payload["output_tokens"]),
870
+ output_toks_per_s=float(payload["output_toks_per_s"]),
871
+ spec_accept_length=payload.get("spec_accept_length"),
872
+ spec_verify_ct_sum=int(payload["spec_verify_ct_sum"]),
873
+ )
874
+
875
+
876
+ def job_outcome_to_payload(result: JobResult | JobFailure) -> dict[str, Any]:
877
+ if isinstance(result, JobFailure):
878
+ return {
879
+ "kind": "failure",
880
+ "key": _run_key_to_payload(result.key),
881
+ "deployment": _deployment_to_payload(result.deployment),
882
+ "run_index": result.run_index,
883
+ "error_type": result.error_type,
884
+ "error_message": result.error_message,
885
+ }
886
+ return {
887
+ "kind": "result",
888
+ "key": _run_key_to_payload(result.key),
889
+ "deployment": _deployment_to_payload(result.deployment),
890
+ "source_sample_count": result.source_sample_count,
891
+ "source_generation_turn_count": result.source_generation_turn_count,
892
+ "warmup_generation_turn_count": result.warmup_generation_turn_count,
893
+ "warmdown_generation_turn_count": result.warmdown_generation_turn_count,
894
+ "run_index": result.run_index,
895
+ "metrics": _bench_metrics_to_payload(result.metrics),
896
+ }
897
+
898
+
899
+ def job_outcome_from_payload(payload: dict[str, Any]) -> JobResult | JobFailure:
900
+ kind = payload["kind"]
901
+ if kind == "failure":
902
+ return JobFailure(
903
+ key=_run_key_from_payload(payload["key"]),
904
+ deployment=_deployment_from_payload(payload["deployment"]),
905
+ run_index=int(payload["run_index"]),
906
+ error_type=str(payload["error_type"]),
907
+ error_message=str(payload["error_message"]),
908
+ )
909
+ if kind == "result":
910
+ return JobResult(
911
+ key=_run_key_from_payload(payload["key"]),
912
+ deployment=_deployment_from_payload(payload["deployment"]),
913
+ source_sample_count=int(payload["source_sample_count"]),
914
+ source_generation_turn_count=int(
915
+ payload["source_generation_turn_count"]
916
+ ),
917
+ warmup_generation_turn_count=int(
918
+ payload["warmup_generation_turn_count"]
919
+ ),
920
+ warmdown_generation_turn_count=int(
921
+ payload["warmdown_generation_turn_count"]
922
+ ),
923
+ run_index=int(payload["run_index"]),
924
+ metrics=_bench_metrics_from_payload(payload["metrics"]),
925
+ )
926
+ raise ValueError(f"Unsupported job outcome kind: {kind}")
927
+
928
+
929
+ @dataclass(frozen=True)
930
+ class SampleMetrics:
931
+ generation_turn_count: int
932
+ output_tokens: int
933
+ spec_verify_ct_sum: int
934
+ spec_accept_lengths: tuple[float, ...]
935
+
936
+
937
+ def _extract_generated_text(out: dict) -> str:
938
+ text = out.get("text")
939
+ if isinstance(text, str):
940
+ return text
941
+ if isinstance(text, list) and len(text) == 1 and isinstance(text[0], str):
942
+ return text[0]
943
+ raise RuntimeError(
944
+ "Expected /generate response to include generated text in `text`; "
945
+ f"got keys={sorted(out.keys())}."
946
+ )
947
+
948
+
949
+ def _extract_generate_stats(out: dict) -> tuple[int, int, Optional[float]]:
950
+ meta = out.get("meta_info", {}) or {}
951
+ output_tokens = int(meta.get("completion_tokens", 0))
952
+ spec_verify_ct = int(meta.get("spec_verify_ct", 0))
953
+ spec_accept_length = None
954
+ if "spec_accept_length" in meta:
955
+ try:
956
+ spec_accept_length = float(meta["spec_accept_length"])
957
+ except (TypeError, ValueError):
958
+ pass
959
+ return output_tokens, spec_verify_ct, spec_accept_length
960
+
961
+
962
+ def _run_sample(
963
+ base_url: str,
964
+ *,
965
+ turns: list[str],
966
+ tokenizer,
967
+ sampling: SamplingConfig,
968
+ timeout_s: int,
969
+ ) -> SampleMetrics:
970
+ messages: list[dict[str, str]] = []
971
+ total_tokens = 0
972
+ spec_verify_ct_sum = 0
973
+ turn_accept_lengths: list[float] = []
974
+
975
+ for turn_idx, user_content in enumerate(turns):
976
+ messages.append({"role": "user", "content": user_content})
977
+ prompt = tokenizer.apply_chat_template(
978
+ messages,
979
+ tokenize=False,
980
+ add_generation_prompt=True,
981
+ enable_thinking=bool(sampling.enable_thinking),
982
+ )
983
+ out = _send_generate(
984
+ base_url=base_url,
985
+ text=prompt,
986
+ max_new_tokens=sampling.max_new_tokens,
987
+ temperature=sampling.temperature,
988
+ top_p=sampling.top_p,
989
+ top_k=sampling.top_k,
990
+ timeout_s=timeout_s,
991
+ )
992
+ output_tokens, spec_verify_ct, turn_accept_length = _extract_generate_stats(out)
993
+ total_tokens += output_tokens
994
+ spec_verify_ct_sum += spec_verify_ct
995
+ if spec_verify_ct > 0:
996
+ turn_accept_lengths.append(float(output_tokens) / float(spec_verify_ct))
997
+ elif turn_accept_length is not None:
998
+ turn_accept_lengths.append(turn_accept_length)
999
+
1000
+ if turn_idx + 1 < len(turns):
1001
+ messages.append({"role": "assistant", "content": _extract_generated_text(out)})
1002
+
1003
+ return SampleMetrics(
1004
+ generation_turn_count=len(turns),
1005
+ output_tokens=int(total_tokens),
1006
+ spec_verify_ct_sum=int(spec_verify_ct_sum),
1007
+ spec_accept_lengths=tuple(turn_accept_lengths),
1008
+ )
1009
+
1010
+
1011
+ def _run_unmeasured_requests(
1012
+ base_url: str,
1013
+ *,
1014
+ samples: list[list[str]],
1015
+ tokenizer,
1016
+ sampling: SamplingConfig,
1017
+ concurrency: int,
1018
+ timeout_s: int,
1019
+ ) -> None:
1020
+ if not samples:
1021
+ return
1022
+
1023
+ with ThreadPoolExecutor(max_workers=int(concurrency)) as pool:
1024
+ futures = [
1025
+ pool.submit(
1026
+ _run_sample,
1027
+ base_url=base_url,
1028
+ turns=turns,
1029
+ tokenizer=tokenizer,
1030
+ sampling=sampling,
1031
+ timeout_s=timeout_s,
1032
+ )
1033
+ for turns in samples
1034
+ ]
1035
+ for fut in as_completed(futures):
1036
+ fut.result()
1037
+
1038
+
1039
+ def _take_samples(samples: list[list[str]], *, start: int, count: int) -> list[list[str]]:
1040
+ if count <= 0:
1041
+ return []
1042
+ if not samples:
1043
+ raise RuntimeError("Cannot take benchmark samples from an empty workload.")
1044
+ return [samples[(start + i) % len(samples)] for i in range(count)]
1045
+
1046
+
1047
+ def _generation_turn_count(samples: list[list[str]]) -> int:
1048
+ return sum(len(turns) for turns in samples)
1049
+
1050
+
1051
+ def _take_samples_for_min_generation_turns(
1052
+ samples: list[list[str]], *, start: int, min_generation_turns: int
1053
+ ) -> list[list[str]]:
1054
+ if min_generation_turns <= 0:
1055
+ return []
1056
+ if not samples:
1057
+ raise RuntimeError("Cannot take benchmark samples from an empty workload.")
1058
+
1059
+ out: list[list[str]] = []
1060
+ generation_turns = 0
1061
+ idx = 0
1062
+ while generation_turns < int(min_generation_turns):
1063
+ sample = samples[(start + idx) % len(samples)]
1064
+ out.append(sample)
1065
+ generation_turns += len(sample)
1066
+ idx += 1
1067
+ return out
1068
+
1069
+
1070
+ def _build_measured_samples(
1071
+ samples: list[list[str]], *, num_samples: Optional[int], min_generation_turns: int
1072
+ ) -> list[list[str]]:
1073
+ if not samples:
1074
+ raise RuntimeError("Cannot build measured samples from an empty workload.")
1075
+ if num_samples is not None:
1076
+ if num_samples <= 0:
1077
+ raise RuntimeError(f"--num-samples must be > 0, got {num_samples}.")
1078
+ return _take_samples(samples, start=0, count=int(num_samples))
1079
+ if min_generation_turns < 0:
1080
+ raise RuntimeError(
1081
+ "--min-generation-turns-per-config must be >= 0, "
1082
+ f"got {min_generation_turns}."
1083
+ )
1084
+ source_generation_turns = _generation_turn_count(samples)
1085
+ if source_generation_turns <= 0:
1086
+ raise RuntimeError("Cannot build measured samples with zero generation turns.")
1087
+ repeats = max(1, math.ceil(int(min_generation_turns) / source_generation_turns))
1088
+ return samples * repeats
1089
+
1090
+
1091
+ def _build_measured_samples_for_concurrency(
1092
+ samples: list[list[str]],
1093
+ *,
1094
+ num_samples: Optional[int],
1095
+ min_generation_turns: int,
1096
+ concurrency: int,
1097
+ ) -> list[list[str]]:
1098
+ # Concurrency 1 is the stable accept-length pass; use one full workload by
1099
+ # default instead of cache-favorable repeated copies.
1100
+ if num_samples is None and int(concurrency) == 1:
1101
+ return samples
1102
+ return _build_measured_samples(
1103
+ samples,
1104
+ num_samples=num_samples,
1105
+ min_generation_turns=min_generation_turns,
1106
+ )
1107
+
1108
+
1109
+ def _build_benchmark_plan(
1110
+ samples: list[list[str]],
1111
+ *,
1112
+ concurrency: int,
1113
+ methodology: BenchmarkMethodologyConfig,
1114
+ ) -> BenchmarkPlan:
1115
+ measured_samples = _build_measured_samples_for_concurrency(
1116
+ samples,
1117
+ num_samples=methodology.num_samples,
1118
+ min_generation_turns=int(methodology.min_generation_turns_per_config),
1119
+ concurrency=int(concurrency),
1120
+ )
1121
+ warmup_min_generation_turns = max(
1122
+ int(methodology.min_warmup_generation_turns), 2 * int(concurrency)
1123
+ )
1124
+ warmup_samples = _take_samples_for_min_generation_turns(
1125
+ measured_samples,
1126
+ start=0,
1127
+ min_generation_turns=warmup_min_generation_turns,
1128
+ )
1129
+ warmdown_samples = _take_samples(
1130
+ measured_samples,
1131
+ start=len(warmup_samples),
1132
+ count=int(concurrency),
1133
+ )
1134
+ return BenchmarkPlan(
1135
+ measured_samples=measured_samples,
1136
+ warmup_samples=warmup_samples,
1137
+ warmdown_samples=warmdown_samples,
1138
+ )
1139
+
1140
+
1141
+ def _run_requests(
1142
+ base_url: str,
1143
+ *,
1144
+ samples: list[list[str]],
1145
+ warmdown_samples: list[list[str]],
1146
+ tokenizer,
1147
+ sampling: SamplingConfig,
1148
+ concurrency: int,
1149
+ timeout_s: int,
1150
+ expect_spec: bool,
1151
+ ) -> BenchMetrics:
1152
+ start = time.perf_counter()
1153
+ total_tokens = 0
1154
+ spec_verify_ct_sum = 0
1155
+ generation_turn_count = 0
1156
+ turn_accept_lengths: list[float] = []
1157
+ measured_completed = 0
1158
+ latency: Optional[float] = None
1159
+
1160
+ with ThreadPoolExecutor(max_workers=int(concurrency)) as pool:
1161
+ measured_futures = [
1162
+ pool.submit(
1163
+ _run_sample,
1164
+ base_url=base_url,
1165
+ turns=turns,
1166
+ tokenizer=tokenizer,
1167
+ sampling=sampling,
1168
+ timeout_s=timeout_s,
1169
+ )
1170
+ for turns in samples
1171
+ ]
1172
+ measured_future_set = set(measured_futures)
1173
+ # Queue warmdown behind measured work so the server does not immediately
1174
+ # drain to idle as the measured tail completes. These futures are waited
1175
+ # on for correctness, but excluded from timing and metrics.
1176
+ warmdown_futures = [
1177
+ pool.submit(
1178
+ _run_sample,
1179
+ base_url=base_url,
1180
+ turns=turns,
1181
+ tokenizer=tokenizer,
1182
+ sampling=sampling,
1183
+ timeout_s=timeout_s,
1184
+ )
1185
+ for turns in warmdown_samples
1186
+ ]
1187
+ consumed_warmdown_futures = set()
1188
+
1189
+ for fut in as_completed([*measured_futures, *warmdown_futures]):
1190
+ if fut in measured_future_set:
1191
+ sample_metrics = fut.result()
1192
+ total_tokens += sample_metrics.output_tokens
1193
+ spec_verify_ct_sum += sample_metrics.spec_verify_ct_sum
1194
+ generation_turn_count += sample_metrics.generation_turn_count
1195
+ turn_accept_lengths.extend(sample_metrics.spec_accept_lengths)
1196
+ measured_completed += 1
1197
+ if measured_completed == len(measured_futures):
1198
+ latency = time.perf_counter() - start
1199
+ break
1200
+ else:
1201
+ consumed_warmdown_futures.add(fut)
1202
+ fut.result()
1203
+
1204
+ for fut in warmdown_futures:
1205
+ if fut not in consumed_warmdown_futures:
1206
+ fut.result()
1207
+
1208
+ if latency is None:
1209
+ latency = time.perf_counter() - start
1210
+ toks_per_s = total_tokens / max(latency, 1e-6)
1211
+
1212
+ if expect_spec and spec_verify_ct_sum <= 0:
1213
+ raise RuntimeError(
1214
+ "Speculative decoding sanity check failed: did not observe any "
1215
+ "`spec_verify_ct` in responses (speculative decoding may not have been enabled)."
1216
+ )
1217
+
1218
+ spec_accept_length = (
1219
+ float(statistics.mean(turn_accept_lengths))
1220
+ if turn_accept_lengths
1221
+ else None
1222
+ )
1223
+
1224
+ return BenchMetrics(
1225
+ sample_count=len(samples),
1226
+ generation_turn_count=int(generation_turn_count),
1227
+ latency_s=float(latency),
1228
+ output_tokens=int(total_tokens),
1229
+ output_toks_per_s=float(toks_per_s),
1230
+ spec_accept_length=spec_accept_length,
1231
+ spec_verify_ct_sum=int(spec_verify_ct_sum),
1232
+ )
1233
+
1234
+
1235
+ def _format_table(
1236
+ *,
1237
+ tp_sizes: list[int],
1238
+ concurrencies: list[int],
1239
+ values: dict[tuple[int, int], Optional[float]],
1240
+ float_fmt: str,
1241
+ ) -> str:
1242
+ header = ["tp\\conc"] + [str(c) for c in concurrencies]
1243
+ rows: list[list[str]] = [header]
1244
+ for tp in tp_sizes:
1245
+ row = [str(tp)]
1246
+ for c in concurrencies:
1247
+ v = values.get((tp, c), None)
1248
+ row.append("N/A" if v is None else format(v, float_fmt))
1249
+ rows.append(row)
1250
+
1251
+ col_widths = [
1252
+ max(len(row[col_idx]) for row in rows) for col_idx in range(len(rows[0]))
1253
+ ]
1254
+
1255
+ lines: list[str] = []
1256
+ lines.append(" ".join(cell.rjust(col_widths[i]) for i, cell in enumerate(rows[0])))
1257
+ lines.append(" ".join("-" * w for w in col_widths))
1258
+ for row in rows[1:]:
1259
+ lines.append(" ".join(cell.rjust(col_widths[i]) for i, cell in enumerate(row)))
1260
+ return "\n".join(lines)
1261
+
1262
+
1263
+ def _build_shared_server_configs(
1264
+ *, device_sm: int, visible_gpus: int, max_concurrency: int
1265
+ ) -> list[SharedServerConfig]:
1266
+ attention_backends = _filter_attention_backends(
1267
+ [BASE_SHARED_SERVER_CONFIG.attention_backend], device_sm=device_sm
1268
+ )
1269
+ scheduler_capacity = max(
1270
+ BASE_SHARED_SERVER_CONFIG.max_running_requests,
1271
+ int(max_concurrency),
1272
+ )
1273
+ configs = [
1274
+ replace(
1275
+ BASE_SHARED_SERVER_CONFIG,
1276
+ attention_backend=backend,
1277
+ max_running_requests=scheduler_capacity,
1278
+ cuda_graph_max_bs=max(
1279
+ BASE_SHARED_SERVER_CONFIG.cuda_graph_max_bs,
1280
+ scheduler_capacity,
1281
+ ),
1282
+ )
1283
+ for backend in attention_backends
1284
+ ]
1285
+ runnable_configs = [
1286
+ config for config in configs if 1 <= config.tp_size <= visible_gpus
1287
+ ]
1288
+ if not runnable_configs:
1289
+ raise RuntimeError(
1290
+ f"No shared server configs are runnable with visible_gpus={visible_gpus}. "
1291
+ "Set CUDA_VISIBLE_DEVICES accordingly."
1292
+ )
1293
+ return runnable_configs
1294
+
1295
+
1296
+ def _build_deployments(
1297
+ shared_config: SharedServerConfig, sweep: DeploymentSweep
1298
+ ) -> list[ServerDeployment]:
1299
+ deployments: list[ServerDeployment] = []
1300
+ if sweep.include_baseline:
1301
+ deployments.append(
1302
+ ServerDeployment(
1303
+ shared_config=shared_config,
1304
+ mode_config=BaselineConfig(),
1305
+ )
1306
+ )
1307
+
1308
+ for spec_mode in sweep.spec_modes:
1309
+ if spec_mode == "mtp":
1310
+ for mtp_num_steps in sweep.mtp_num_steps:
1311
+ mtp_config = MTPConfig(num_steps=int(mtp_num_steps))
1312
+ deployments.append(
1313
+ ServerDeployment(
1314
+ shared_config=shared_config,
1315
+ mode_config=mtp_config,
1316
+ )
1317
+ )
1318
+ elif spec_mode == "dflash":
1319
+ if sweep.dflash_draft_model is None:
1320
+ raise RuntimeError("DFlash deployment requires a draft model.")
1321
+ for block_size in sweep.dflash_block_sizes:
1322
+ dflash_config = DFlashConfig(
1323
+ draft_model=sweep.dflash_draft_model,
1324
+ block_size=block_size,
1325
+ )
1326
+ deployments.append(
1327
+ ServerDeployment(
1328
+ shared_config=shared_config,
1329
+ mode_config=dflash_config,
1330
+ )
1331
+ )
1332
+ else:
1333
+ raise ValueError(f"Unknown speculative mode: {spec_mode}")
1334
+ return deployments
1335
+
1336
+
1337
+ def _build_benchmark_jobs(
1338
+ config: SweepConfig, shared_configs: list[SharedServerConfig]
1339
+ ) -> list[BenchmarkJob]:
1340
+ jobs: list[BenchmarkJob] = []
1341
+ for shared_config in shared_configs:
1342
+ deployments = _build_deployments(shared_config, config.deployment_sweep)
1343
+ for deployment in deployments:
1344
+ for workload in config.workloads:
1345
+ for concurrency in config.concurrencies:
1346
+ concurrency_deployment = replace(
1347
+ deployment,
1348
+ shared_config=replace(
1349
+ deployment.shared_config,
1350
+ max_running_requests=int(concurrency),
1351
+ cuda_graph_max_bs=int(concurrency),
1352
+ ),
1353
+ )
1354
+ for run_index in range(config.methodology.runs_per_config):
1355
+ jobs.append(
1356
+ BenchmarkJob(
1357
+ target_model=config.target_model,
1358
+ workload=workload,
1359
+ deployment=concurrency_deployment,
1360
+ concurrency=concurrency,
1361
+ run_index=run_index,
1362
+ sampling=config.sampling,
1363
+ methodology=config.methodology,
1364
+ )
1365
+ )
1366
+ return jobs
1367
+
1368
+
1369
+ def _mode_display_name(mode: str) -> str:
1370
+ if mode.startswith("mtp_s"):
1371
+ return f"MTP steps={mode.removeprefix('mtp_s')}"
1372
+ if mode.startswith("dflash_b"):
1373
+ return f"DFLASH block={mode.removeprefix('dflash_b')}"
1374
+ return {
1375
+ "baseline": "Baseline",
1376
+ "dflash": "DFLASH",
1377
+ }.get(mode, mode)
1378
+
1379
+
1380
+ def _collect_metric(
1381
+ *,
1382
+ results: dict[tuple[str, int, int, str], BenchMetrics],
1383
+ backend: str,
1384
+ tp_sizes: list[int],
1385
+ concurrencies: list[int],
1386
+ mode: str,
1387
+ field: str,
1388
+ ) -> dict[tuple[int, int], Optional[float]]:
1389
+ out: dict[tuple[int, int], Optional[float]] = {}
1390
+ for tp in tp_sizes:
1391
+ for conc in concurrencies:
1392
+ metrics = results.get((backend, tp, conc, mode), None)
1393
+ out[(tp, conc)] = None if metrics is None else getattr(metrics, field)
1394
+ return out
1395
+
1396
+
1397
+ def _compute_speedup(
1398
+ baseline: dict[tuple[int, int], Optional[float]],
1399
+ speculative: dict[tuple[int, int], Optional[float]],
1400
+ ) -> dict[tuple[int, int], Optional[float]]:
1401
+ return {
1402
+ key: None if (b is None or d is None or b <= 0) else (d / b)
1403
+ for key, b in baseline.items()
1404
+ for d in [speculative.get(key, None)]
1405
+ }
1406
+
1407
+
1408
+ def _metric_map_from_config_results(
1409
+ config_results: list[ConfigResult],
1410
+ ) -> dict[tuple[str, int, int, str], BenchMetrics]:
1411
+ return {
1412
+ result.key.metric_key(): result.metrics
1413
+ for result in config_results
1414
+ if result.metrics is not None
1415
+ }
1416
+
1417
+
1418
+ def _print_kv_lines(items: list[tuple[str, object]]) -> None:
1419
+ for key, value in items:
1420
+ print(f"{key}={value}")
1421
+
1422
+
1423
+ def _print_failure_summary(config_results: list[ConfigResult]) -> None:
1424
+ failed_results = [
1425
+ result for result in config_results if result.failed_run_count > 0
1426
+ ]
1427
+ if not failed_results:
1428
+ return
1429
+
1430
+ print("\n=== Failed/Partial Runs ===")
1431
+ for result in failed_results:
1432
+ key = result.key
1433
+ print(
1434
+ f"workload={key.workload} backend={key.backend} tp={key.tp} "
1435
+ f"mode={key.mode} conc={key.concurrency} status={result.status} "
1436
+ f"successful_runs={result.successful_run_count} "
1437
+ f"failed_runs={result.failed_run_count} "
1438
+ f"successful_run_numbers={_format_successful_run_numbers(result)} "
1439
+ f"failed_run_numbers={_format_failed_run_numbers(result.failures)} "
1440
+ f"errors={_format_failure_messages(result.failures)}"
1441
+ )
1442
+
1443
+
1444
+ def _server_env_for_job(job: BenchmarkJob) -> dict[str, str]:
1445
+ return {
1446
+ "SGLANG_ENABLE_OVERLAP_PLAN_STREAM": (
1447
+ "1" if job.deployment.enable_overlap_plan_stream else "0"
1448
+ ),
1449
+ "SGLANG_PYSPY_DUMP_BEFORE_CRASH": "0",
1450
+ "SGLANG_CUDA_COREDUMP_BEFORE_CRASH": "0",
1451
+ }
1452
+
1453
+
1454
+ def _run_benchmark_job(job: BenchmarkJob) -> JobResult:
1455
+ from sglang.test.test_utils import (
1456
+ DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH as SGLANG_DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
1457
+ find_available_port,
1458
+ popen_launch_server,
1459
+ )
1460
+ from transformers import AutoTokenizer
1461
+
1462
+ key = job.key
1463
+ print(f"\n=== {job.label} {job.run_label} ===")
1464
+ samples = _load_user_turns(job.workload)
1465
+ if not samples:
1466
+ raise RuntimeError(f"Workload '{job.workload}' did not produce any prompts.")
1467
+
1468
+ source_sample_count = len(samples)
1469
+ source_generation_turn_count = _generation_turn_count(samples)
1470
+ plan = _build_benchmark_plan(
1471
+ samples,
1472
+ concurrency=job.concurrency,
1473
+ methodology=job.methodology,
1474
+ )
1475
+ if plan.measured_sample_count > source_sample_count:
1476
+ print(
1477
+ "[config] measured sample count exceeds workload size; "
1478
+ "repeating whole workload copies with radix cache enabled."
1479
+ )
1480
+
1481
+ base_url = f"http://127.0.0.1:{find_available_port(20000)}"
1482
+ tokenizer = AutoTokenizer.from_pretrained(job.target_model)
1483
+ server_start_timeout_s = int(
1484
+ max(SGLANG_DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, job.methodology.timeout_s)
1485
+ )
1486
+ server_env = _server_env_for_job(job)
1487
+ print(
1488
+ "server_env="
1489
+ + ",".join(f"{key}:{value}" for key, value in sorted(server_env.items()))
1490
+ )
1491
+ proc = popen_launch_server(
1492
+ job.target_model,
1493
+ base_url,
1494
+ timeout=server_start_timeout_s,
1495
+ other_args=job.deployment.server_args,
1496
+ env=server_env,
1497
+ )
1498
+ try:
1499
+ _send_generate(
1500
+ base_url,
1501
+ "Hello",
1502
+ max_new_tokens=8,
1503
+ temperature=job.sampling.temperature,
1504
+ top_p=job.sampling.top_p,
1505
+ top_k=job.sampling.top_k,
1506
+ timeout_s=min(job.methodology.timeout_s, 300),
1507
+ )
1508
+
1509
+ _flush_cache(base_url)
1510
+ print(
1511
+ f"[warmup {job.run_label}] run {len(plan.warmup_samples)} samples / "
1512
+ f"{plan.warmup_generation_turn_count} generation turns after "
1513
+ "/flush_cache; excluded from metrics."
1514
+ )
1515
+ _run_unmeasured_requests(
1516
+ base_url,
1517
+ samples=plan.warmup_samples,
1518
+ tokenizer=tokenizer,
1519
+ sampling=job.sampling,
1520
+ concurrency=job.concurrency,
1521
+ timeout_s=job.methodology.timeout_s,
1522
+ )
1523
+ _flush_cache(base_url)
1524
+ print(
1525
+ f"[warmup {job.run_label}] flushed cache after warmup; "
1526
+ "starting measured workload."
1527
+ )
1528
+ metrics = _run_requests(
1529
+ base_url,
1530
+ samples=plan.measured_samples,
1531
+ warmdown_samples=plan.warmdown_samples,
1532
+ tokenizer=tokenizer,
1533
+ sampling=job.sampling,
1534
+ concurrency=job.concurrency,
1535
+ timeout_s=job.methodology.timeout_s,
1536
+ expect_spec=job.deployment.expect_spec,
1537
+ )
1538
+ line = (
1539
+ f"[{job.label} {job.run_label}] samples={plan.measured_sample_count:<4} "
1540
+ f"turns={plan.measured_generation_turn_count:<4} "
1541
+ f"toks/s={metrics.output_toks_per_s:,.2f} "
1542
+ f"latency={metrics.latency_s:.1f}s "
1543
+ f"warmup_turns={plan.warmup_generation_turn_count} "
1544
+ f"warmdown_turns={plan.warmdown_generation_turn_count}"
1545
+ )
1546
+ if job.deployment.expect_spec:
1547
+ accept_len = (
1548
+ "N/A"
1549
+ if metrics.spec_accept_length is None
1550
+ else f"{metrics.spec_accept_length:.3f}"
1551
+ )
1552
+ line += (
1553
+ f" accept_len_mean={accept_len} "
1554
+ f"spec_verify_ct_sum={metrics.spec_verify_ct_sum}"
1555
+ )
1556
+ print(line)
1557
+ return JobResult(
1558
+ key=key,
1559
+ deployment=job.deployment,
1560
+ source_sample_count=source_sample_count,
1561
+ source_generation_turn_count=source_generation_turn_count,
1562
+ warmup_generation_turn_count=plan.warmup_generation_turn_count,
1563
+ warmdown_generation_turn_count=plan.warmdown_generation_turn_count,
1564
+ run_index=job.run_index,
1565
+ metrics=metrics,
1566
+ )
1567
+ finally:
1568
+ _shutdown_server(
1569
+ proc,
1570
+ base_url,
1571
+ drain_timeout_s=job.methodology.server_shutdown_drain_timeout_s,
1572
+ kill_timeout_s=job.methodology.server_shutdown_timeout_s,
1573
+ )
1574
+
1575
+
1576
+ def _run_benchmark_job_gracefully(job: BenchmarkJob) -> JobResult | JobFailure:
1577
+ try:
1578
+ return _run_benchmark_job(job)
1579
+ except Exception as exc:
1580
+ error_type = type(exc).__name__
1581
+ error_message = _one_line(str(exc) or repr(exc))
1582
+ print(
1583
+ f"[failed {job.label} {job.run_label}] "
1584
+ f"{error_type}: {error_message}"
1585
+ )
1586
+ return JobFailure(
1587
+ key=job.key,
1588
+ deployment=job.deployment,
1589
+ run_index=job.run_index,
1590
+ error_type=error_type,
1591
+ error_message=error_message,
1592
+ )
1593
+
1594
+
1595
+ def _print_summary(
1596
+ *,
1597
+ config: SweepConfig,
1598
+ workload: str,
1599
+ config_results: list[ConfigResult],
1600
+ shared_configs: list[SharedServerConfig],
1601
+ attention_backends: list[str],
1602
+ tp_sizes: list[int],
1603
+ concurrencies: list[int],
1604
+ device_sm: int,
1605
+ mode_keys: list[str],
1606
+ source_sample_count: Optional[int],
1607
+ source_generation_turn_count: Optional[int],
1608
+ results: dict[tuple[str, int, int, str], BenchMetrics],
1609
+ ) -> None:
1610
+ print("\n=== Speculative Benchmark Sweep Summary ===")
1611
+ _print_kv_lines(
1612
+ [
1613
+ ("workload", workload),
1614
+ ("source_sample_count", source_sample_count),
1615
+ ("source_generation_turn_count", source_generation_turn_count),
1616
+ ("target_model", config.target_model),
1617
+ ("dflash_draft_model", config.dflash_draft_model),
1618
+ ("spec_modes", ",".join(mode_keys)),
1619
+ (
1620
+ "mtp_num_steps",
1621
+ ",".join(str(x) for x in config.deployment_sweep.mtp_num_steps),
1622
+ ),
1623
+ (
1624
+ "mtp_num_draft_tokens",
1625
+ ",".join(
1626
+ str(int(x) + 1)
1627
+ for x in config.deployment_sweep.mtp_num_steps
1628
+ ),
1629
+ ),
1630
+ ("mtp_eagle_topk", 1),
1631
+ ("max_new_tokens", config.sampling.max_new_tokens),
1632
+ ("enable_thinking", bool(config.sampling.enable_thinking)),
1633
+ ("timeout_s", config.methodology.timeout_s),
1634
+ (
1635
+ "server_shutdown_drain_timeout_s",
1636
+ config.methodology.server_shutdown_drain_timeout_s,
1637
+ ),
1638
+ (
1639
+ "server_shutdown_timeout_s",
1640
+ config.methodology.server_shutdown_timeout_s,
1641
+ ),
1642
+ (
1643
+ "shared_server_configs",
1644
+ ";".join(
1645
+ server_config.summary_label()
1646
+ for server_config in shared_configs
1647
+ ),
1648
+ ),
1649
+ (
1650
+ "sampling",
1651
+ f"temperature:{config.sampling.temperature}, "
1652
+ f"top_p:{config.sampling.top_p}, top_k:{config.sampling.top_k}",
1653
+ ),
1654
+ ("attention_backends", ",".join(attention_backends)),
1655
+ (
1656
+ "dflash_block_sizes",
1657
+ ",".join(
1658
+ "default" if x is None else str(x)
1659
+ for x in config.deployment_sweep.dflash_block_sizes
1660
+ ),
1661
+ ),
1662
+ ("tp_sizes", ",".join(str(x) for x in tp_sizes)),
1663
+ ("concurrencies", ",".join(str(x) for x in concurrencies)),
1664
+ ("num_samples", config.methodology.num_samples),
1665
+ ("runs_per_config", config.methodology.runs_per_config),
1666
+ (
1667
+ "min_generation_turns_per_config",
1668
+ config.methodology.min_generation_turns_per_config,
1669
+ ),
1670
+ (
1671
+ "min_warmup_generation_turns",
1672
+ config.methodology.min_warmup_generation_turns,
1673
+ ),
1674
+ ("disable_radix_cache", False),
1675
+ ("device_sm", device_sm),
1676
+ ("skip_baseline", not config.deployment_sweep.include_baseline),
1677
+ ]
1678
+ )
1679
+ _print_failure_summary(config_results)
1680
+
1681
+ for backend in attention_backends:
1682
+ print(f"\n=== Backend: {backend} ===")
1683
+ baseline_output_tps = _collect_metric(
1684
+ results=results,
1685
+ backend=backend,
1686
+ tp_sizes=tp_sizes,
1687
+ concurrencies=concurrencies,
1688
+ mode="baseline",
1689
+ field="output_toks_per_s",
1690
+ )
1691
+ sections: list[tuple[str, dict[tuple[int, int], Optional[float]], str]] = [
1692
+ ("Baseline output tok/s", baseline_output_tps, ",.2f")
1693
+ ]
1694
+
1695
+ for spec_mode in mode_keys:
1696
+ display_name = _mode_display_name(spec_mode)
1697
+ spec_output_tps = _collect_metric(
1698
+ results=results,
1699
+ backend=backend,
1700
+ tp_sizes=tp_sizes,
1701
+ concurrencies=concurrencies,
1702
+ mode=spec_mode,
1703
+ field="output_toks_per_s",
1704
+ )
1705
+ spec_accept_length = _collect_metric(
1706
+ results=results,
1707
+ backend=backend,
1708
+ tp_sizes=tp_sizes,
1709
+ concurrencies=concurrencies,
1710
+ mode=spec_mode,
1711
+ field="spec_accept_length",
1712
+ )
1713
+ sections.extend(
1714
+ [
1715
+ (f"{display_name} output tok/s", spec_output_tps, ",.2f"),
1716
+ (
1717
+ f"Speedup ({display_name} / baseline)",
1718
+ _compute_speedup(baseline_output_tps, spec_output_tps),
1719
+ ".3f",
1720
+ ),
1721
+ (
1722
+ f"{display_name} acceptance length (mean per generation turn)",
1723
+ spec_accept_length,
1724
+ ".3f",
1725
+ ),
1726
+ ]
1727
+ )
1728
+
1729
+ for title, values, fmt in sections:
1730
+ print(f"\n{title}")
1731
+ print(
1732
+ _format_table(
1733
+ tp_sizes=tp_sizes,
1734
+ concurrencies=concurrencies,
1735
+ values=values,
1736
+ float_fmt=fmt,
1737
+ )
1738
+ )
1739
+
1740
+
1741
+ CSV_FIELDS = [
1742
+ "workload",
1743
+ "backend",
1744
+ "tp",
1745
+ "mode",
1746
+ "mtp_num_steps",
1747
+ "dflash_block_size",
1748
+ "concurrency",
1749
+ "source_sample_count",
1750
+ "source_generation_turn_count",
1751
+ "runs_per_config",
1752
+ "successful_runs",
1753
+ "failed_runs",
1754
+ "status",
1755
+ "successful_run_numbers",
1756
+ "failed_run_numbers",
1757
+ "failure_messages",
1758
+ "measured_sample_count",
1759
+ "measured_generation_turn_count",
1760
+ "output_toks_per_s",
1761
+ "output_toks_per_s_std",
1762
+ "latency_s",
1763
+ "latency_s_std",
1764
+ "output_tokens",
1765
+ "speedup_vs_baseline",
1766
+ "accept_length_mean_from_conc1",
1767
+ "accept_length_mean_this_conc",
1768
+ "accept_length_mean_this_conc_std",
1769
+ "spec_verify_ct_sum",
1770
+ ]
1771
+
1772
+
1773
+ def _one_line(value: str) -> str:
1774
+ return " ".join(str(value).split())
1775
+
1776
+
1777
+ def _fmt_optional_int(value: Optional[int]) -> str:
1778
+ if value is None:
1779
+ return ""
1780
+ return str(value)
1781
+
1782
+
1783
+ def _fmt_csv_value(value: Optional[float]) -> str:
1784
+ if value is None:
1785
+ return ""
1786
+ return f"{value:.6f}"
1787
+
1788
+
1789
+ def _format_failed_run_numbers(failures: tuple[JobFailure, ...]) -> str:
1790
+ return ",".join(str(failure.run_index + 1) for failure in failures)
1791
+
1792
+
1793
+ def _format_successful_run_numbers(result: ConfigResult) -> str:
1794
+ return ",".join(
1795
+ str(run_index + 1) for run_index in result.successful_run_indices
1796
+ )
1797
+
1798
+
1799
+ def _format_failure_messages(failures: tuple[JobFailure, ...]) -> str:
1800
+ return " | ".join(
1801
+ (
1802
+ f"run={failure.run_index + 1} "
1803
+ f"{failure.error_type}: {failure.error_message}"
1804
+ )
1805
+ for failure in failures
1806
+ )
1807
+
1808
+
1809
+ def _mean_optional(values: list[Optional[float]]) -> Optional[float]:
1810
+ present_values = [value for value in values if value is not None]
1811
+ if not present_values:
1812
+ return None
1813
+ return float(statistics.mean(present_values))
1814
+
1815
+
1816
+ def _stdev_optional(values: list[Optional[float]]) -> Optional[float]:
1817
+ present_values = [value for value in values if value is not None]
1818
+ if len(present_values) < 2:
1819
+ return None
1820
+ return float(statistics.stdev(present_values))
1821
+
1822
+
1823
+ def _metric_stdev(
1824
+ metrics: tuple[BenchMetrics, ...], field: str
1825
+ ) -> Optional[float]:
1826
+ return _stdev_optional([getattr(metric, field) for metric in metrics])
1827
+
1828
+
1829
+ def _aggregate_bench_metrics(metrics: list[BenchMetrics]) -> BenchMetrics:
1830
+ if not metrics:
1831
+ raise RuntimeError("Cannot aggregate an empty metrics list.")
1832
+ first = metrics[0]
1833
+ return BenchMetrics(
1834
+ sample_count=first.sample_count,
1835
+ generation_turn_count=first.generation_turn_count,
1836
+ latency_s=float(statistics.mean(metric.latency_s for metric in metrics)),
1837
+ output_tokens=int(
1838
+ round(statistics.mean(metric.output_tokens for metric in metrics))
1839
+ ),
1840
+ output_toks_per_s=float(
1841
+ statistics.mean(metric.output_toks_per_s for metric in metrics)
1842
+ ),
1843
+ spec_accept_length=_mean_optional(
1844
+ [metric.spec_accept_length for metric in metrics]
1845
+ ),
1846
+ spec_verify_ct_sum=int(
1847
+ round(statistics.mean(metric.spec_verify_ct_sum for metric in metrics))
1848
+ ),
1849
+ )
1850
+
1851
+
1852
+ def _aggregate_job_results(
1853
+ job_results: list[JobResult | JobFailure],
1854
+ ) -> list[ConfigResult]:
1855
+ grouped_results: dict[RunKey, list[JobResult | JobFailure]] = {}
1856
+ ordered_keys: list[RunKey] = []
1857
+ for result in job_results:
1858
+ if result.key not in grouped_results:
1859
+ grouped_results[result.key] = []
1860
+ ordered_keys.append(result.key)
1861
+ grouped_results[result.key].append(result)
1862
+
1863
+ config_results: list[ConfigResult] = []
1864
+ for key in ordered_keys:
1865
+ results = sorted(grouped_results[key], key=lambda result: result.run_index)
1866
+ successful_results = [
1867
+ result for result in results if isinstance(result, JobResult)
1868
+ ]
1869
+ failures = tuple(
1870
+ result for result in results if isinstance(result, JobFailure)
1871
+ )
1872
+ first = results[0]
1873
+ first_success = successful_results[0] if successful_results else None
1874
+ repeat_metrics = tuple(result.metrics for result in successful_results)
1875
+ successful_run_indices = tuple(
1876
+ result.run_index for result in successful_results
1877
+ )
1878
+ metrics = (
1879
+ _aggregate_bench_metrics(list(repeat_metrics))
1880
+ if repeat_metrics
1881
+ else None
1882
+ )
1883
+ config_results.append(
1884
+ ConfigResult(
1885
+ key=key,
1886
+ deployment=first.deployment,
1887
+ source_sample_count=(
1888
+ None if first_success is None else first_success.source_sample_count
1889
+ ),
1890
+ source_generation_turn_count=(
1891
+ None
1892
+ if first_success is None
1893
+ else first_success.source_generation_turn_count
1894
+ ),
1895
+ warmup_generation_turn_count=(
1896
+ None
1897
+ if first_success is None
1898
+ else first_success.warmup_generation_turn_count
1899
+ ),
1900
+ warmdown_generation_turn_count=(
1901
+ None
1902
+ if first_success is None
1903
+ else first_success.warmdown_generation_turn_count
1904
+ ),
1905
+ metrics=metrics,
1906
+ repeat_metrics=repeat_metrics,
1907
+ successful_run_indices=successful_run_indices,
1908
+ failures=failures,
1909
+ )
1910
+ )
1911
+ return config_results
1912
+
1913
+
1914
+ def _build_csv_rows(
1915
+ *,
1916
+ config_results: list[ConfigResult],
1917
+ ) -> list[dict[str, object]]:
1918
+ rows: list[dict[str, object]] = []
1919
+ results_by_key = {result.key: result for result in config_results}
1920
+
1921
+ for result in config_results:
1922
+ key = result.key
1923
+ metrics = result.metrics
1924
+ baseline_result = results_by_key.get(
1925
+ RunKey(
1926
+ workload=key.workload,
1927
+ backend=key.backend,
1928
+ tp=key.tp,
1929
+ concurrency=key.concurrency,
1930
+ mode="baseline",
1931
+ )
1932
+ )
1933
+ speedup = None
1934
+ if (
1935
+ metrics is not None
1936
+ and key.mode != "baseline"
1937
+ and baseline_result is not None
1938
+ and baseline_result.metrics is not None
1939
+ and baseline_result.metrics.output_toks_per_s > 0
1940
+ ):
1941
+ speedup = (
1942
+ metrics.output_toks_per_s
1943
+ / baseline_result.metrics.output_toks_per_s
1944
+ )
1945
+
1946
+ accept_source_result = results_by_key.get(
1947
+ RunKey(
1948
+ workload=key.workload,
1949
+ backend=key.backend,
1950
+ tp=key.tp,
1951
+ concurrency=1,
1952
+ mode=key.mode,
1953
+ )
1954
+ )
1955
+ accept_length_from_conc1 = (
1956
+ None
1957
+ if accept_source_result is None or accept_source_result.metrics is None
1958
+ else accept_source_result.metrics.spec_accept_length
1959
+ )
1960
+
1961
+ rows.append(
1962
+ {
1963
+ "workload": key.workload,
1964
+ "backend": key.backend,
1965
+ "tp": key.tp,
1966
+ "mode": key.mode,
1967
+ "mtp_num_steps": result.deployment.mtp_num_steps or "",
1968
+ "dflash_block_size": result.deployment.dflash_block_size or "",
1969
+ "concurrency": key.concurrency,
1970
+ "source_sample_count": _fmt_optional_int(result.source_sample_count),
1971
+ "source_generation_turn_count": _fmt_optional_int(
1972
+ result.source_generation_turn_count
1973
+ ),
1974
+ "runs_per_config": result.run_count,
1975
+ "successful_runs": result.successful_run_count,
1976
+ "failed_runs": result.failed_run_count,
1977
+ "status": result.status,
1978
+ "successful_run_numbers": _format_successful_run_numbers(result),
1979
+ "failed_run_numbers": _format_failed_run_numbers(result.failures),
1980
+ "failure_messages": _format_failure_messages(result.failures),
1981
+ "measured_sample_count": (
1982
+ "" if metrics is None else metrics.sample_count
1983
+ ),
1984
+ "measured_generation_turn_count": (
1985
+ "" if metrics is None else metrics.generation_turn_count
1986
+ ),
1987
+ "output_toks_per_s": _fmt_csv_value(
1988
+ None if metrics is None else metrics.output_toks_per_s
1989
+ ),
1990
+ "output_toks_per_s_std": _fmt_csv_value(
1991
+ _metric_stdev(result.repeat_metrics, "output_toks_per_s")
1992
+ ),
1993
+ "latency_s": _fmt_csv_value(
1994
+ None if metrics is None else metrics.latency_s
1995
+ ),
1996
+ "latency_s_std": _fmt_csv_value(
1997
+ _metric_stdev(result.repeat_metrics, "latency_s")
1998
+ ),
1999
+ "output_tokens": "" if metrics is None else metrics.output_tokens,
2000
+ "speedup_vs_baseline": _fmt_csv_value(speedup),
2001
+ "accept_length_mean_from_conc1": _fmt_csv_value(
2002
+ accept_length_from_conc1
2003
+ ),
2004
+ "accept_length_mean_this_conc": _fmt_csv_value(
2005
+ None if metrics is None else metrics.spec_accept_length
2006
+ ),
2007
+ "accept_length_mean_this_conc_std": _fmt_csv_value(
2008
+ _metric_stdev(result.repeat_metrics, "spec_accept_length")
2009
+ ),
2010
+ "spec_verify_ct_sum": (
2011
+ "" if metrics is None else metrics.spec_verify_ct_sum
2012
+ ),
2013
+ }
2014
+ )
2015
+ return rows
2016
+
2017
+
2018
+ def _print_csv_summary(rows: list[dict[str, object]]) -> None:
2019
+ buffer = io.StringIO()
2020
+ writer = csv.DictWriter(buffer, fieldnames=CSV_FIELDS)
2021
+ writer.writeheader()
2022
+ writer.writerows(rows)
2023
+ print("\n=== CSV Summary ===")
2024
+ print(buffer.getvalue(), end="", flush=True)
2025
+
2026
+
2027
+ def _write_csv_summary(path: str, rows: list[dict[str, object]]) -> None:
2028
+ out_path = Path(path)
2029
+ out_path.parent.mkdir(parents=True, exist_ok=True)
2030
+ with open(out_path, "w", newline="") as f:
2031
+ writer = csv.DictWriter(f, fieldnames=CSV_FIELDS)
2032
+ writer.writeheader()
2033
+ writer.writerows(rows)
2034
+ print(f"[csv] wrote {len(rows)} rows to {out_path}", flush=True)
2035
+
2036
+
2037
+ def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
2038
+ parser = argparse.ArgumentParser()
2039
+ parser.add_argument(
2040
+ "--workloads",
2041
+ dest="workloads",
2042
+ default=DEFAULT_WORKLOADS,
2043
+ help=(
2044
+ "Comma-separated workloads to run, or `all`."
2045
+ ),
2046
+ )
2047
+ parser.add_argument(
2048
+ "--csv-output",
2049
+ default=None,
2050
+ help="Optional path to write the final CSV summary.",
2051
+ )
2052
+ parser.add_argument("--target-model", default="Qwen/Qwen3.5-397B-A17B")
2053
+ parser.add_argument(
2054
+ "--dflash-draft-model",
2055
+ dest="dflash_draft_model",
2056
+ default=None,
2057
+ help="Required when --spec-modes includes dflash.",
2058
+ )
2059
+ parser.add_argument(
2060
+ "--spec-modes",
2061
+ default="mtp",
2062
+ help="Comma-separated speculative modes to benchmark. Supported: mtp,dflash.",
2063
+ )
2064
+ parser.add_argument(
2065
+ "--mtp-num-steps",
2066
+ default="3",
2067
+ help=(
2068
+ "Comma-separated MTP/EAGLE speculative num steps. num draft tokens "
2069
+ "is always num_steps + 1."
2070
+ ),
2071
+ )
2072
+ parser.add_argument(
2073
+ "--skip-baseline",
2074
+ action="store_true",
2075
+ help="Skip running the baseline (target-only) sweep; only run speculative modes and report N/A for baseline/speedup.",
2076
+ )
2077
+ thinking_group = parser.add_mutually_exclusive_group()
2078
+ thinking_group.add_argument(
2079
+ "--enable-thinking",
2080
+ dest="enable_thinking",
2081
+ action="store_true",
2082
+ default=True,
2083
+ help="Pass enable_thinking=True when applying the model chat template (default).",
2084
+ )
2085
+ thinking_group.add_argument(
2086
+ "--disable-thinking",
2087
+ dest="enable_thinking",
2088
+ action="store_false",
2089
+ help="Pass enable_thinking=False when applying the model chat template.",
2090
+ )
2091
+ parser.add_argument("--max-new-tokens", type=int, default=4096)
2092
+ parser.add_argument("--temperature", type=float, default=0.0)
2093
+ parser.add_argument("--top-p", type=float, default=1.0)
2094
+ parser.add_argument("--top-k", type=int, default=1)
2095
+ parser.add_argument("--concurrencies", default="1,32")
2096
+ parser.add_argument(
2097
+ "--num-samples",
2098
+ dest="num_samples",
2099
+ type=int,
2100
+ default=None,
2101
+ help=(
2102
+ "Exact number of measured samples per config. Repeats the selected "
2103
+ "workload if this exceeds the workload size. Default: unset."
2104
+ ),
2105
+ )
2106
+ parser.add_argument(
2107
+ "--runs-per-config",
2108
+ type=int,
2109
+ default=1,
2110
+ help=(
2111
+ "Number of repeated measured runs per benchmark configuration. "
2112
+ "The final reported metrics are averaged across these runs."
2113
+ ),
2114
+ )
2115
+ parser.add_argument(
2116
+ "--min-generation-turns-per-config",
2117
+ dest="min_generation_turns_per_config",
2118
+ type=int,
2119
+ default=1024,
2120
+ help=(
2121
+ "When --num-samples is unset and concurrency > 1, repeat whole workload "
2122
+ "copies until each config measures at least this many generation turns. "
2123
+ "Use 0 for one full workload copy."
2124
+ ),
2125
+ )
2126
+ parser.add_argument(
2127
+ "--min-warmup-generation-turns",
2128
+ type=int,
2129
+ default=8,
2130
+ help=(
2131
+ "Minimum generation turns to run after /flush_cache before measured "
2132
+ "timing. Effective warmup is max(this value, 2 * concurrency)."
2133
+ ),
2134
+ )
2135
+ parser.add_argument(
2136
+ "--dflash-block-sizes",
2137
+ default="default",
2138
+ help=(
2139
+ "Comma-separated DFlash block-size sweep. Use `default` to omit "
2140
+ "--speculative-dflash-block-size and let the server choose."
2141
+ ),
2142
+ )
2143
+ args = parser.parse_args(argv)
2144
+ try:
2145
+ workloads = _parse_workload_selection(args.workloads)
2146
+ except ValueError as exc:
2147
+ parser.error(str(exc))
2148
+
2149
+ spec_modes = _parse_str_csv(args.spec_modes)
2150
+ supported_spec_modes = {"mtp", "dflash"}
2151
+ unknown_spec_modes = sorted(set(spec_modes) - supported_spec_modes)
2152
+ if unknown_spec_modes:
2153
+ parser.error(
2154
+ "--spec-modes contains unsupported values: "
2155
+ + ",".join(unknown_spec_modes)
2156
+ )
2157
+ if not spec_modes:
2158
+ parser.error("--spec-modes must include at least one mode: mtp or dflash")
2159
+ if "dflash" in spec_modes and not args.dflash_draft_model:
2160
+ parser.error(
2161
+ "--dflash-draft-model is required when --spec-modes includes dflash"
2162
+ )
2163
+ try:
2164
+ dflash_block_sizes = _parse_optional_int_csv(str(args.dflash_block_sizes))
2165
+ except ValueError as exc:
2166
+ parser.error(f"--dflash-block-sizes must be integers/default: {exc}")
2167
+ if any(x is not None and x <= 0 for x in dflash_block_sizes):
2168
+ parser.error(
2169
+ "--dflash-block-sizes values must be > 0, "
2170
+ f"got {args.dflash_block_sizes}"
2171
+ )
2172
+ mtp_num_steps = _parse_int_csv(str(args.mtp_num_steps))
2173
+ if not mtp_num_steps:
2174
+ parser.error("--mtp-num-steps must include at least one positive integer")
2175
+ if any(x <= 0 for x in mtp_num_steps):
2176
+ parser.error(f"--mtp-num-steps values must be > 0, got {args.mtp_num_steps}")
2177
+ mode_keys = DeploymentSweep(
2178
+ include_baseline=not args.skip_baseline,
2179
+ spec_modes=tuple(spec_modes),
2180
+ mtp_num_steps=tuple(mtp_num_steps),
2181
+ dflash_draft_model=args.dflash_draft_model,
2182
+ dflash_block_sizes=tuple(dflash_block_sizes),
2183
+ ).mode_keys
2184
+ duplicate_mode_keys = _duplicate_values(mode_keys)
2185
+ if duplicate_mode_keys:
2186
+ parser.error(
2187
+ "Duplicate deployment modes from sweep flags: "
2188
+ + ",".join(duplicate_mode_keys)
2189
+ )
2190
+ args.workloads = workloads
2191
+ args.spec_modes = spec_modes
2192
+ args.mtp_num_steps = mtp_num_steps
2193
+ args.dflash_block_sizes = dflash_block_sizes
2194
+ return args
2195
+
2196
+
2197
+ def build_sweep_config_from_args(args: argparse.Namespace) -> SweepConfig:
2198
+ sampling = SamplingConfig(
2199
+ enable_thinking=bool(args.enable_thinking),
2200
+ max_new_tokens=int(args.max_new_tokens),
2201
+ temperature=float(args.temperature),
2202
+ top_p=float(args.top_p),
2203
+ top_k=int(args.top_k),
2204
+ )
2205
+ methodology = BenchmarkMethodologyConfig(
2206
+ num_samples=args.num_samples,
2207
+ min_generation_turns_per_config=int(args.min_generation_turns_per_config),
2208
+ min_warmup_generation_turns=int(args.min_warmup_generation_turns),
2209
+ runs_per_config=int(args.runs_per_config),
2210
+ )
2211
+ deployment_sweep = DeploymentSweep(
2212
+ include_baseline=not args.skip_baseline,
2213
+ spec_modes=tuple(args.spec_modes),
2214
+ mtp_num_steps=tuple(args.mtp_num_steps),
2215
+ dflash_draft_model=args.dflash_draft_model,
2216
+ dflash_block_sizes=tuple(args.dflash_block_sizes),
2217
+ )
2218
+
2219
+ if sampling.temperature < 0.0:
2220
+ raise RuntimeError(f"--temperature must be >= 0, got {sampling.temperature}.")
2221
+ if not (0.0 < sampling.top_p <= 1.0):
2222
+ raise RuntimeError(f"--top-p must be in (0, 1], got {sampling.top_p}.")
2223
+ if sampling.top_k == 0 or sampling.top_k < -1:
2224
+ raise RuntimeError(
2225
+ f"--top-k must be -1 (all vocab) or >= 1, got {sampling.top_k}."
2226
+ )
2227
+ if methodology.num_samples is not None and methodology.num_samples <= 0:
2228
+ raise RuntimeError(f"--num-samples must be > 0, got {methodology.num_samples}.")
2229
+ if methodology.runs_per_config <= 0:
2230
+ raise RuntimeError(
2231
+ f"--runs-per-config must be > 0, got {methodology.runs_per_config}."
2232
+ )
2233
+ if (
2234
+ methodology.min_generation_turns_per_config < 0
2235
+ or methodology.min_warmup_generation_turns < 0
2236
+ ):
2237
+ raise RuntimeError(
2238
+ "--min-generation-turns-per-config and "
2239
+ "--min-warmup-generation-turns must be >= 0."
2240
+ )
2241
+
2242
+ try:
2243
+ concurrencies = _parse_int_csv(args.concurrencies)
2244
+ except ValueError as exc:
2245
+ raise RuntimeError("--concurrencies must be comma-separated integers.") from exc
2246
+ if not concurrencies:
2247
+ raise RuntimeError("No concurrencies specified.")
2248
+ if any(c < 1 for c in concurrencies):
2249
+ raise RuntimeError(
2250
+ f"--concurrencies values must be >= 1, got {concurrencies}."
2251
+ )
2252
+ duplicate_concurrencies = _duplicate_values([str(c) for c in concurrencies])
2253
+ if duplicate_concurrencies:
2254
+ raise RuntimeError(
2255
+ "Duplicate concurrencies: " + ",".join(duplicate_concurrencies)
2256
+ )
2257
+
2258
+ return SweepConfig(
2259
+ target_model=args.target_model,
2260
+ dflash_draft_model=args.dflash_draft_model,
2261
+ workloads=tuple(args.workloads),
2262
+ concurrencies=tuple(concurrencies),
2263
+ sampling=sampling,
2264
+ methodology=methodology,
2265
+ deployment_sweep=deployment_sweep,
2266
+ csv_output=args.csv_output,
2267
+ )
2268
+
2269
+
2270
+ def _get_current_cuda_runtime() -> tuple[int, int]:
2271
+ import torch
2272
+ from sglang.srt.utils import get_device_sm
2273
+
2274
+ if not torch.cuda.is_available():
2275
+ raise RuntimeError("CUDA is required for this sweep.")
2276
+ return int(torch.cuda.device_count()), int(get_device_sm())
2277
+
2278
+
2279
+ def build_shared_configs_for_runtime(
2280
+ sweep_config: SweepConfig,
2281
+ ) -> tuple[list[SharedServerConfig], int]:
2282
+ visible_gpus, device_sm = _get_current_cuda_runtime()
2283
+ shared_configs = _build_shared_server_configs(
2284
+ device_sm=device_sm,
2285
+ visible_gpus=visible_gpus,
2286
+ max_concurrency=max(sweep_config.concurrencies),
2287
+ )
2288
+ return shared_configs, device_sm
2289
+
2290
+
2291
+ def build_shared_configs_for_modal(
2292
+ sweep_config: SweepConfig,
2293
+ *,
2294
+ device_sm: int,
2295
+ visible_gpus: int,
2296
+ ) -> list[SharedServerConfig]:
2297
+ return _build_shared_server_configs(
2298
+ device_sm=int(device_sm),
2299
+ visible_gpus=int(visible_gpus),
2300
+ max_concurrency=max(sweep_config.concurrencies),
2301
+ )
2302
+
2303
+
2304
+ def build_benchmark_jobs(
2305
+ sweep_config: SweepConfig, shared_configs: list[SharedServerConfig]
2306
+ ) -> list[BenchmarkJob]:
2307
+ return _build_benchmark_jobs(sweep_config, shared_configs)
2308
+
2309
+
2310
+ def run_benchmark_job_payload(payload: dict[str, Any]) -> dict[str, Any]:
2311
+ job = benchmark_job_from_payload(payload)
2312
+ return job_outcome_to_payload(_run_benchmark_job_gracefully(job))
2313
+
2314
+
2315
+ def aggregate_job_outcomes(
2316
+ outcomes: list[JobResult | JobFailure],
2317
+ ) -> list[ConfigResult]:
2318
+ return _aggregate_job_results(outcomes)
2319
+
2320
+
2321
+ def render_results(
2322
+ *,
2323
+ sweep_config: SweepConfig,
2324
+ shared_configs: list[SharedServerConfig],
2325
+ device_sm: int,
2326
+ config_results: list[ConfigResult],
2327
+ ) -> list[dict[str, object]]:
2328
+ mode_keys = sweep_config.deployment_sweep.mode_keys
2329
+ for workload in sweep_config.workloads:
2330
+ workload_results = [
2331
+ result for result in config_results if result.key.workload == workload
2332
+ ]
2333
+ if not workload_results:
2334
+ continue
2335
+ workload_shared_configs: list[SharedServerConfig] = []
2336
+ seen_shared_configs: set[SharedServerConfig] = set()
2337
+ for result in workload_results:
2338
+ shared_config = result.deployment.shared_config
2339
+ if shared_config not in seen_shared_configs:
2340
+ seen_shared_configs.add(shared_config)
2341
+ workload_shared_configs.append(shared_config)
2342
+ attention_backends = sorted(
2343
+ {config.attention_backend for config in workload_shared_configs}
2344
+ )
2345
+ tp_sizes = sorted({config.tp_size for config in workload_shared_configs})
2346
+ source_sample_count = next(
2347
+ (
2348
+ result.source_sample_count
2349
+ for result in workload_results
2350
+ if result.source_sample_count is not None
2351
+ ),
2352
+ None,
2353
+ )
2354
+ source_generation_turn_count = next(
2355
+ (
2356
+ result.source_generation_turn_count
2357
+ for result in workload_results
2358
+ if result.source_generation_turn_count is not None
2359
+ ),
2360
+ None,
2361
+ )
2362
+ print(f"\n\n##### Workload Summary: {workload} #####")
2363
+ _print_summary(
2364
+ config=sweep_config,
2365
+ workload=workload,
2366
+ config_results=workload_results,
2367
+ shared_configs=workload_shared_configs,
2368
+ attention_backends=attention_backends,
2369
+ tp_sizes=tp_sizes,
2370
+ concurrencies=list(sweep_config.concurrencies),
2371
+ device_sm=device_sm,
2372
+ mode_keys=mode_keys,
2373
+ source_sample_count=source_sample_count,
2374
+ source_generation_turn_count=source_generation_turn_count,
2375
+ results=_metric_map_from_config_results(workload_results),
2376
+ )
2377
+
2378
+ csv_rows = _build_csv_rows(config_results=config_results)
2379
+ _print_csv_summary(csv_rows)
2380
+ if sweep_config.csv_output is not None:
2381
+ _write_csv_summary(sweep_config.csv_output, csv_rows)
2382
+ return csv_rows
2383
+
2384
+
2385
+ def run_local_sweep(sweep_config: SweepConfig) -> list[ConfigResult]:
2386
+ shared_configs, device_sm = build_shared_configs_for_runtime(sweep_config)
2387
+ jobs = build_benchmark_jobs(sweep_config, shared_configs)
2388
+ job_results = [_run_benchmark_job_gracefully(job) for job in jobs]
2389
+ config_results = aggregate_job_outcomes(job_results)
2390
+ render_results(
2391
+ sweep_config=sweep_config,
2392
+ shared_configs=shared_configs,
2393
+ device_sm=device_sm,
2394
+ config_results=config_results,
2395
+ )
2396
+ return config_results
2397
+
2398
+
2399
+ def main() -> None:
2400
+ args = parse_args()
2401
+ sweep_config = build_sweep_config_from_args(args)
2402
+ run_local_sweep(sweep_config)
2403
+
2404
+
2405
+ if __name__ == "__main__":
2406
+ main()
benchmark/run_modal_benchmark.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Modal entrypoint for running benchmark jobs in parallel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import modal
12
+
13
+
14
+ MINUTES = 60
15
+ HOURS = 60 * MINUTES
16
+
17
+ APP_NAME = "qwen35-397b-a17b-dflash-eval"
18
+ SGLANG_IMAGE = "lmsysorg/sglang:v0.5.13-cu130"
19
+ SGLANG_GIT_SHA = "ec36dde58083aca8f26c3740332498a11a06debf"
20
+
21
+ GPU_TYPE = "B200"
22
+ N_GPUS = 8
23
+ GPU = f"{GPU_TYPE}:{N_GPUS}"
24
+ CLOUD: str | None = "csc"
25
+ MAX_CONTAINERS = 32
26
+ JOB_TIMEOUT_S = 24 * HOURS
27
+ STARTUP_TIMEOUT_S = 30 * MINUTES
28
+
29
+ MODAL_VISIBLE_GPUS = N_GPUS
30
+ MODAL_DEVICE_SM = 100
31
+
32
+ HF_CACHE_PATH = "/root/.cache/huggingface"
33
+ FLASHINFER_CACHE_PATH = "/root/.cache/flashinfer"
34
+ EVAL_CACHE_PATH = "/cache/eval"
35
+ REMOTE_BENCHMARK_PATH = "/root/run_benchmark.py"
36
+ REMOTE_PATCH_DIR = "/root/patches"
37
+ REMOTE_PATCH_SCRIPT_PATH = "/root/apply_modal_patches.py"
38
+
39
+ LOCAL_BENCHMARK_PATH = Path(__file__).resolve().parent / "run_benchmark.py"
40
+ LOCAL_PATCH_DIR = Path(__file__).resolve().parent / "patches"
41
+ LOCAL_PATCH_SCRIPT_PATH = LOCAL_PATCH_DIR / "apply_patches.py"
42
+ FLASHINFER_PATCH_PATH = LOCAL_PATCH_DIR / "flashinfer-pr-3312.patch"
43
+
44
+ HF_CACHE_VOL = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
45
+ FLASHINFER_CACHE_VOL = modal.Volume.from_name(
46
+ "flashinfer-cache", create_if_missing=True
47
+ )
48
+ EVAL_CACHE_VOL = modal.Volume.from_name(
49
+ "qwen35-dflash-eval-cache", create_if_missing=True
50
+ )
51
+ HF_SECRET = modal.Secret.from_name("huggingface-secret")
52
+
53
+ RUNTIME_ENV = {
54
+ "HF_HOME": HF_CACHE_PATH,
55
+ "HF_HUB_CACHE": HF_CACHE_PATH,
56
+ "HF_DATASETS_CACHE": f"{HF_CACHE_PATH}/datasets",
57
+ "TRANSFORMERS_CACHE": HF_CACHE_PATH,
58
+ "XDG_CACHE_HOME": f"{HF_CACHE_PATH}/xdg",
59
+ "HF_XET_HIGH_PERFORMANCE": "1",
60
+ "PYTHONPATH": "/root",
61
+ "SGLANG_PYSPY_DUMP_BEFORE_CRASH": "0",
62
+ "SGLANG_CUDA_COREDUMP_BEFORE_CRASH": "0",
63
+ }
64
+
65
+ sglang_image = (
66
+ modal.Image.from_registry(SGLANG_IMAGE)
67
+ .entrypoint([])
68
+ .add_local_file(
69
+ FLASHINFER_PATCH_PATH,
70
+ remote_path=f"{REMOTE_PATCH_DIR}/flashinfer-pr-3312.patch",
71
+ copy=True,
72
+ )
73
+ .add_local_file(
74
+ LOCAL_PATCH_SCRIPT_PATH,
75
+ remote_path=REMOTE_PATCH_SCRIPT_PATH,
76
+ copy=True,
77
+ )
78
+ .env(
79
+ {
80
+ "PYTHONPATH": "/root",
81
+ }
82
+ )
83
+ .uv_pip_install(
84
+ f"git+https://github.com/sgl-project/sglang.git@{SGLANG_GIT_SHA}#subdirectory=python",
85
+ "datasets",
86
+ "hf_xet",
87
+ "requests",
88
+ "transformers",
89
+ )
90
+ .run_commands(f"python {REMOTE_PATCH_SCRIPT_PATH}")
91
+ .run_commands(f"rm -rf {HF_CACHE_PATH}")
92
+ .add_local_file(LOCAL_BENCHMARK_PATH, remote_path=REMOTE_BENCHMARK_PATH)
93
+ )
94
+
95
+ app = modal.App(name=APP_NAME)
96
+
97
+
98
+ def _load_benchmark_module():
99
+ return importlib.import_module("run_benchmark")
100
+
101
+
102
+ @app.function(
103
+ image=sglang_image,
104
+ gpu=GPU,
105
+ env=RUNTIME_ENV,
106
+ secrets=[HF_SECRET],
107
+ volumes={
108
+ HF_CACHE_PATH: HF_CACHE_VOL,
109
+ FLASHINFER_CACHE_PATH: FLASHINFER_CACHE_VOL,
110
+ EVAL_CACHE_PATH: EVAL_CACHE_VOL,
111
+ },
112
+ cloud=CLOUD,
113
+ retries=0,
114
+ timeout=JOB_TIMEOUT_S,
115
+ startup_timeout=STARTUP_TIMEOUT_S,
116
+ max_containers=MAX_CONTAINERS,
117
+ single_use_containers=True,
118
+ )
119
+ def run_benchmark_job_modal(job_payload: dict[str, Any]) -> dict[str, Any]:
120
+ sys.path.insert(0, str(Path(REMOTE_BENCHMARK_PATH).parent))
121
+ benchmark = _load_benchmark_module()
122
+ benchmark.CACHE_DIR = Path(EVAL_CACHE_PATH)
123
+ return benchmark.run_benchmark_job_payload(job_payload)
124
+
125
+
126
+ def _platform_failure_payload(
127
+ job_payload: dict[str, Any],
128
+ exc: BaseException | str,
129
+ ) -> dict[str, Any]:
130
+ benchmark = _load_benchmark_module()
131
+ job = benchmark.benchmark_job_from_payload(job_payload)
132
+ if isinstance(exc, BaseException):
133
+ error_type = type(exc).__name__
134
+ error_message = f"Modal platform failure: {exc!r}"
135
+ else:
136
+ error_type = "UnexpectedModalResult"
137
+ error_message = exc
138
+ return benchmark.job_outcome_to_payload(
139
+ benchmark.JobFailure(
140
+ key=job.key,
141
+ deployment=job.deployment,
142
+ run_index=job.run_index,
143
+ error_type=error_type,
144
+ error_message=error_message,
145
+ )
146
+ )
147
+
148
+
149
+ def _write_raw_output(path: str, outcome_payloads: list[dict[str, Any]]) -> None:
150
+ output_path = Path(path)
151
+ output_path.parent.mkdir(parents=True, exist_ok=True)
152
+ with open(output_path, "w") as f:
153
+ for payload in outcome_payloads:
154
+ f.write(json.dumps(payload, sort_keys=True) + "\n")
155
+ print(
156
+ f"[modal] wrote {len(outcome_payloads)} raw job outcomes to {output_path}",
157
+ flush=True,
158
+ )
159
+
160
+
161
+ def _build_eval_argv(
162
+ *,
163
+ workloads: str,
164
+ csv_output: str,
165
+ target_model: str,
166
+ dflash_draft_model: str,
167
+ spec_modes: str,
168
+ mtp_num_steps: str,
169
+ skip_baseline: bool,
170
+ enable_thinking: bool,
171
+ max_new_tokens: int,
172
+ temperature: float,
173
+ top_p: float,
174
+ top_k: int,
175
+ concurrencies: str,
176
+ num_samples: int,
177
+ runs_per_config: int,
178
+ min_generation_turns_per_config: int,
179
+ min_warmup_generation_turns: int,
180
+ dflash_block_sizes: str,
181
+ ) -> list[str]:
182
+ argv = [
183
+ "--workloads",
184
+ workloads,
185
+ "--target-model",
186
+ target_model,
187
+ "--spec-modes",
188
+ spec_modes,
189
+ "--mtp-num-steps",
190
+ mtp_num_steps,
191
+ "--max-new-tokens",
192
+ str(int(max_new_tokens)),
193
+ "--temperature",
194
+ str(float(temperature)),
195
+ "--top-p",
196
+ str(float(top_p)),
197
+ "--top-k",
198
+ str(int(top_k)),
199
+ "--concurrencies",
200
+ concurrencies,
201
+ "--runs-per-config",
202
+ str(int(runs_per_config)),
203
+ "--min-generation-turns-per-config",
204
+ str(int(min_generation_turns_per_config)),
205
+ "--min-warmup-generation-turns",
206
+ str(int(min_warmup_generation_turns)),
207
+ "--dflash-block-sizes",
208
+ dflash_block_sizes,
209
+ ]
210
+ if csv_output:
211
+ argv.extend(["--csv-output", csv_output])
212
+ if dflash_draft_model:
213
+ argv.extend(["--dflash-draft-model", dflash_draft_model])
214
+ if skip_baseline:
215
+ argv.append("--skip-baseline")
216
+ if enable_thinking:
217
+ argv.append("--enable-thinking")
218
+ else:
219
+ argv.append("--disable-thinking")
220
+ if num_samples > 0:
221
+ argv.extend(["--num-samples", str(int(num_samples))])
222
+ return argv
223
+
224
+
225
+ @app.local_entrypoint()
226
+ def main(
227
+ workloads: str = "gsm8k",
228
+ csv_output: str = "",
229
+ target_model: str = "Qwen/Qwen3.5-397B-A17B",
230
+ dflash_draft_model: str = "",
231
+ spec_modes: str = "mtp",
232
+ mtp_num_steps: str = "3",
233
+ skip_baseline: bool = False,
234
+ enable_thinking: bool = True,
235
+ max_new_tokens: int = 4096,
236
+ temperature: float = 0.0,
237
+ top_p: float = 1.0,
238
+ top_k: int = 1,
239
+ concurrencies: str = "1,32",
240
+ num_samples: int = 0,
241
+ runs_per_config: int = 1,
242
+ min_generation_turns_per_config: int = 1024,
243
+ min_warmup_generation_turns: int = 8,
244
+ dflash_block_sizes: str = "default",
245
+ raw_output: str = "",
246
+ device_sm: int = MODAL_DEVICE_SM,
247
+ visible_gpus: int = MODAL_VISIBLE_GPUS,
248
+ ) -> None:
249
+ benchmark = _load_benchmark_module()
250
+ args = benchmark.parse_args(
251
+ _build_eval_argv(
252
+ workloads=workloads,
253
+ csv_output=csv_output,
254
+ target_model=target_model,
255
+ dflash_draft_model=dflash_draft_model,
256
+ spec_modes=spec_modes,
257
+ mtp_num_steps=mtp_num_steps,
258
+ skip_baseline=skip_baseline,
259
+ enable_thinking=enable_thinking,
260
+ max_new_tokens=max_new_tokens,
261
+ temperature=temperature,
262
+ top_p=top_p,
263
+ top_k=top_k,
264
+ concurrencies=concurrencies,
265
+ num_samples=num_samples,
266
+ runs_per_config=runs_per_config,
267
+ min_generation_turns_per_config=min_generation_turns_per_config,
268
+ min_warmup_generation_turns=min_warmup_generation_turns,
269
+ dflash_block_sizes=dflash_block_sizes,
270
+ )
271
+ )
272
+ sweep_config = benchmark.build_sweep_config_from_args(args)
273
+ shared_configs = benchmark.build_shared_configs_for_modal(
274
+ sweep_config,
275
+ device_sm=device_sm,
276
+ visible_gpus=visible_gpus,
277
+ )
278
+ jobs = benchmark.build_benchmark_jobs(sweep_config, shared_configs)
279
+ job_payloads = [benchmark.benchmark_job_to_payload(job) for job in jobs]
280
+
281
+ print(
282
+ f"[modal] dispatching {len(job_payloads)} benchmark jobs "
283
+ f"across up to {MAX_CONTAINERS} containers"
284
+ )
285
+ mapped_outputs = list(
286
+ run_benchmark_job_modal.map(
287
+ job_payloads,
288
+ order_outputs=True,
289
+ return_exceptions=True,
290
+ )
291
+ )
292
+
293
+ outcome_payloads: list[dict[str, Any]] = []
294
+ for job_payload, output in zip(job_payloads, mapped_outputs):
295
+ if isinstance(output, BaseException):
296
+ outcome_payloads.append(_platform_failure_payload(job_payload, output))
297
+ elif not isinstance(output, dict):
298
+ outcome_payloads.append(
299
+ _platform_failure_payload(
300
+ job_payload,
301
+ f"Modal returned {type(output).__name__}, expected dict payload.",
302
+ )
303
+ )
304
+ else:
305
+ outcome_payloads.append(output)
306
+
307
+ if raw_output:
308
+ _write_raw_output(raw_output, outcome_payloads)
309
+
310
+ outcomes = [
311
+ benchmark.job_outcome_from_payload(payload) for payload in outcome_payloads
312
+ ]
313
+ config_results = benchmark.aggregate_job_outcomes(outcomes)
314
+ benchmark.render_results(
315
+ sweep_config=sweep_config,
316
+ shared_configs=shared_configs,
317
+ device_sm=device_sm,
318
+ config_results=config_results,
319
+ )
320
+ sys.stdout.flush()
321
+ sys.stderr.flush()
config.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DFlashDraftModel"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "auto_map": {
8
+ "AutoModel": "dflash.DFlashDraftModel"
9
+ },
10
+ "bos_token_id": null,
11
+ "dflash_config": {
12
+ "block_size": 16,
13
+ "mask_token_id": 248077,
14
+ "target_layer_ids": [
15
+ 1,
16
+ 9,
17
+ 17,
18
+ 25,
19
+ 33,
20
+ 41,
21
+ 49,
22
+ 57
23
+ ]
24
+ },
25
+ "dtype": "bfloat16",
26
+ "eos_token_id": 248044,
27
+ "head_dim": 128,
28
+ "hidden_act": "silu",
29
+ "hidden_size": 4096,
30
+ "initializer_range": 0.02,
31
+ "intermediate_size": 12288,
32
+ "layer_types": [
33
+ "sliding_attention",
34
+ "sliding_attention",
35
+ "sliding_attention",
36
+ "sliding_attention",
37
+ "sliding_attention",
38
+ "full_attention"
39
+ ],
40
+ "max_position_embeddings": 262144,
41
+ "max_window_layers": 6,
42
+ "model_type": "qwen3",
43
+ "num_attention_heads": 32,
44
+ "num_hidden_layers": 6,
45
+ "num_key_value_heads": 8,
46
+ "num_target_layers": 60,
47
+ "pad_token_id": null,
48
+ "rms_norm_eps": 1e-06,
49
+ "rope_parameters": {
50
+ "rope_theta": 10000000,
51
+ "rope_type": "default"
52
+ },
53
+ "sliding_window": 4096,
54
+ "tie_word_embeddings": false,
55
+ "transformers_version": "5.7.0",
56
+ "use_cache": true,
57
+ "use_sliding_window": true,
58
+ "vocab_size": 248320
59
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:63e59f09a49d0c6101305a8ce8b4f29435941c9ff76186fdfea047cbb40c5a37
3
+ size 6652291584