A high-throughput and memory-efficient inference and serving engine for LLMs
[Attention] Improve attention benchmarks: configs and profiling (#39336)
Signed-off-by: Matthew Bonanni <mbonanni@redhat.com>
Matthew Bonanni committed
Jun 12, 2026 at 16:24 UTC
cf567cbc71a467d8479411062917e9190ee11376
15 files changed
+573
-167
benchmarks/attention_benchmarks/README.md
+9
-13
@@ -108,7 +108,6 @@ python benchmark.py \
108
--backends flash triton flashinfer \
109
--batch-specs "q2k" "8q1s1k" "2q2k_32q1s1k" \
110
--num-layers 10 \
111
- --repeats 5 \
111
--output-csv results.csv
112
```
113
@@ -164,14 +163,17 @@ python benchmark.py \
163
# Model configuration
164
--num-layers N # Number of layers
165
--head-dim N # Head dimension
166
+--v-head-dim N # Value head dimension (defaults to --head-dim)
167
--num-q-heads N # Query heads
168
--num-kv-heads N # KV heads
169
--block-size N # Block size
170
+--kv-lora-rank N # MLA KV LoRA rank
171
+--qk-nope-head-dim N # MLA non-RoPE QK head dim
172
+--qk-rope-head-dim N # MLA RoPE QK head dim
173
174
# Benchmark settings
175
--device DEVICE # Device (default: cuda:0)
173
---repeats N # Repetitions
174
---warmup-iters N # Warmup iterations
176
+--warmup-ms N # Warmup window in ms for triton do_bench
177
--profile-memory # Profile memory usage
178
179
# Parameter sweeps
@@ -211,8 +213,6 @@ config = BenchmarkConfig(
213
num_kv_heads=1,
214
block_size=128,
215
device="cuda:0",
214
- repeats=5,
215
- warmup_iters=3,
216
)
217
218
# CUTLASS MLA with specific num_kv_splits
@@ -253,14 +253,10 @@ formatter.save_json(results, "output.json")
253
254
## Tips
255
256
-**1. Warmup matters** - Use `--warmup-iters 10` for stable results
256
+**1. Save results** - Always use `--output-csv` or `--output-json`
257
258
-**2. Multiple repeats** - Use `--repeats 20` for low variance
258
+**2. Test incrementally** - Start with `--num-layers 1`
259
260
-**3. Save results** - Always use `--output-csv` or `--output-json`
260
+**3. Extended grammar** - Leverage spec decode, chunked prefill patterns
261
262
-**4. Test incrementally** - Start with `--num-layers 1 --repeats 1`
263
-
264
-**5. Extended grammar** - Leverage spec decode, chunked prefill patterns
265
-
266
-**6. Parameter sweeps** - Use `--sweep-param` and `--sweep-values` to find optimal values
262
+**4. Parameter sweeps** - Use `--sweep-param` and `--sweep-values` to find optimal values
benchmarks/attention_benchmarks/benchmark.py
+179
-39
@@ -26,6 +26,9 @@ Examples:
26
"""
27
28
import argparse
29
+import os
30
+import shutil
31
+import subprocess
32
import sys
33
from dataclasses import replace
34
from pathlib import Path
@@ -83,13 +86,15 @@ def run_benchmark(config: BenchmarkConfig, **kwargs) -> BenchmarkResult:
86
else:
87
return run_standard_attention_benchmark(config)
88
except Exception as e:
89
+ error_msg = str(e) or repr(e)
90
return BenchmarkResult(
91
config=config,
92
mean_time=float("inf"),
93
+ median_time=float("inf"),
94
std_time=0,
95
min_time=float("inf"),
96
max_time=float("inf"),
92
- error=str(e),
97
+ error=error_msg,
98
)
99
100
@@ -115,9 +120,12 @@ def run_model_parameter_sweep(
120
"""
121
all_results = []
122
118
- console.print(
119
- f"[yellow]Model sweep mode: testing {sweep.param_name} = {sweep.values}[/]"
123
+ sweep_desc = (
124
+ f"{sweep.param_name} = {sweep.values}"
125
+ if sweep.param_name
126
+ else f"{len(sweep.values)} configurations"
127
)
128
+ console.print(f"[yellow]Model sweep mode: testing {sweep_desc}[/]")
129
130
total = len(backends) * len(batch_specs) * len(sweep.values)
131
@@ -125,9 +133,9 @@ def run_model_parameter_sweep(
133
for backend in backends:
134
for spec in batch_specs:
135
for value in sweep.values:
128
- # Create config with modified model parameter
136
+ # Create config with modified model parameter(s)
137
config_args = base_config_args.copy()
130
- config_args[sweep.param_name] = value
138
+ sweep.apply(config_args, value)
139
140
# Create config with original backend for running
141
clean_config = BenchmarkConfig(
@@ -144,13 +152,21 @@ def run_model_parameter_sweep(
152
all_results.append(result)
153
154
if not result.success:
155
+ err_label = (
156
+ f"{sweep.param_name}={value}"
157
+ if sweep.param_name
158
+ else f"{value}"
159
+ )
160
console.print(
148
- f"[red]Error {backend} {spec} {sweep.param_name}="
149
- f"{value}: {result.error}[/]"
161
+ f"[red]Error {backend} {spec} {err_label}"
162
+ f": {result.error}[/]"
163
)
164
165
pbar.update(1)
166
167
+ if base_config_args.get("ncu_profile"):
168
+ return all_results
169
+
170
# Display sweep results - create separate table for each parameter value
171
console.print("\n[bold green]Model Parameter Sweep Results:[/]")
172
formatter = ResultsFormatter(console)
@@ -184,7 +200,10 @@ def run_model_parameter_sweep(
200
)
201
202
for param_value in sorted_param_values:
187
- console.print(f"\n[bold cyan]{sweep.param_name} = {param_value}[/]")
203
+ label = (
204
+ f"{sweep.param_name} = {param_value}" if sweep.param_name else param_value
205
+ )
206
+ console.print(f"\n[bold cyan]{label}[/]")
207
param_results = by_param_value[param_value]
208
209
# Create modified results with original backend names
@@ -200,8 +219,9 @@ def run_model_parameter_sweep(
219
formatter.print_table(modified_results, backends, compare_to_fastest=True)
220
221
# Show optimal backend for each (param_value, batch_spec) combination
222
+ sweep_name = sweep.param_name or "config"
223
console.print(
204
- f"\n[bold cyan]Optimal backend for each ({sweep.param_name}, batch_spec):[/]"
224
+ f"\n[bold cyan]Optimal backend for each ({sweep_name}, batch_spec):[/]"
225
)
226
227
# Group by (param_value, batch_spec)
@@ -236,7 +256,10 @@ def run_model_parameter_sweep(
256
for param_value, spec in sorted_keys:
257
# Print header when param value changes
258
if param_value != current_param_value:
239
- console.print(f"\n [bold]{sweep.param_name}={param_value}:[/]")
259
+ header = (
260
+ f"{sweep.param_name}={param_value}" if sweep.param_name else param_value
261
+ )
262
+ console.print(f"\n [bold]{header}:[/]")
263
current_param_value = param_value
264
265
results = by_param_and_spec[(param_value, spec)]
@@ -322,6 +345,9 @@ def run_parameter_sweep(
345
346
pbar.update(1)
347
348
+ if base_config_args.get("ncu_profile"):
349
+ return all_results
350
+
351
# Display sweep results
352
console.print("\n[bold green]Sweep Results:[/]")
353
backend_labels = [sweep.get_label(b, v) for b in backends for v in sweep_values]
@@ -474,11 +500,35 @@ def main():
500
parser.add_argument("--num-q-heads", type=int, default=32, help="Query heads")
501
parser.add_argument("--num-kv-heads", type=int, default=8, help="KV heads")
502
parser.add_argument("--block-size", type=int, default=16, help="Block size")
503
+ parser.add_argument(
504
+ "--v-head-dim",
505
+ type=int,
506
+ default=None,
507
+ help="Value head dimension (defaults to --head-dim if unset)",
508
+ )
509
+
510
+ # MLA-specific model dimensions
511
+ parser.add_argument(
512
+ "--kv-lora-rank", type=int, default=None, help="MLA KV LoRA rank"
513
+ )
514
+ parser.add_argument(
515
+ "--qk-nope-head-dim", type=int, default=None, help="MLA non-RoPE QK head dim"
516
+ )
517
+ parser.add_argument(
518
+ "--qk-rope-head-dim", type=int, default=None, help="MLA RoPE QK head dim"
519
+ )
520
521
# Benchmark settings
522
parser.add_argument("--device", default="cuda:0", help="Device")
480
- parser.add_argument("--repeats", type=int, default=1, help="Repetitions")
481
- parser.add_argument("--warmup-iters", type=int, default=3, help="Warmup iterations")
523
+ parser.add_argument(
524
+ "--warmup-ms",
525
+ type=int,
526
+ default=None,
527
+ help=(
528
+ "Warmup window in ms for triton's do_bench (default: triton's own). "
529
+ "Has no effect with CUDA graphs; pass --no-cuda-graphs to use it."
530
+ ),
531
+ )
532
parser.add_argument("--profile-memory", action="store_true", help="Profile memory")
533
parser.add_argument(
534
"--kv-cache-dtype",
@@ -491,10 +541,33 @@ def main():
541
action=argparse.BooleanOptionalAction,
542
default=True,
543
help=(
494
- "Launch kernels with CUDA graphs to eliminate CPU overhead"
495
- "in measurements (default: True)"
544
+ "Use triton do_bench_cudagraph (True) or do_bench (False) "
545
+ "for timing. CUDA graphs eliminate CPU launch overhead "
546
+ "(default: True)"
547
+ ),
548
+ )
549
+ parser.add_argument(
550
+ "--num-splits",
551
+ type=int,
552
+ default=None,
553
+ help="FlashAttention split-K factor (0=auto heuristic, 1=disabled, >1=force N)",
554
+ )
555
+ parser.add_argument(
556
+ "--ncu-profile",
557
+ action="store_true",
558
+ default=False,
559
+ help=(
560
+ "Enable Nsight Compute profiling mode. Automatically wraps the "
561
+ "script with ncu, capturing a profile with source correlation. "
562
+ "Use --ncu-output to set the output file name."
563
),
564
)
565
+ parser.add_argument(
566
+ "--ncu-output",
567
+ type=str,
568
+ default="profile",
569
+ help="Output file name for ncu profile (default: 'profile').",
570
+ )
571
572
# Parameter sweep (use YAML config for advanced sweeps)
573
parser.add_argument(
@@ -576,23 +649,28 @@ def main():
649
model = yaml_config["model"]
650
args.num_layers = model.get("num_layers", args.num_layers)
651
args.head_dim = model.get("head_dim", args.head_dim)
652
+ args.v_head_dim = model.get("v_head_dim", args.v_head_dim)
653
args.num_q_heads = model.get("num_q_heads", args.num_q_heads)
654
args.num_kv_heads = model.get("num_kv_heads", args.num_kv_heads)
655
args.block_size = model.get("block_size", args.block_size)
656
+ # MLA-specific dimensions
657
+ args.kv_lora_rank = model.get("kv_lora_rank", args.kv_lora_rank)
658
+ args.qk_nope_head_dim = model.get("qk_nope_head_dim", args.qk_nope_head_dim)
659
+ args.qk_rope_head_dim = model.get("qk_rope_head_dim", args.qk_rope_head_dim)
660
661
# Benchmark settings (top-level keys)
662
if "device" in yaml_config:
663
args.device = yaml_config["device"]
586
- if "repeats" in yaml_config:
587
- args.repeats = yaml_config["repeats"]
588
- if "warmup_iters" in yaml_config:
589
- args.warmup_iters = yaml_config["warmup_iters"]
664
+ if "warmup_ms" in yaml_config:
665
+ args.warmup_ms = yaml_config["warmup_ms"]
666
if "profile_memory" in yaml_config:
667
args.profile_memory = yaml_config["profile_memory"]
668
if "kv_cache_dtype" in yaml_config:
669
args.kv_cache_dtype = yaml_config["kv_cache_dtype"]
670
if "cuda_graphs" in yaml_config:
671
args.cuda_graphs = yaml_config["cuda_graphs"]
672
+ if "ncu_profile" in yaml_config:
673
+ args.ncu_profile = yaml_config["ncu_profile"]
674
675
# Parameter sweep configuration
676
if "parameter_sweep" in yaml_config:
@@ -612,7 +690,7 @@ def main():
690
if "model_parameter_sweep" in yaml_config:
691
sweep_config = yaml_config["model_parameter_sweep"]
692
args.model_parameter_sweep = ModelParameterSweep(
615
- param_name=sweep_config["param_name"],
693
+ param_name=sweep_config.get("param_name"),
694
values=sweep_config["values"],
695
label_format=sweep_config.get(
696
"label_format", "{backend}_{param_name}_{value}"
@@ -631,6 +709,32 @@ def main():
709
710
console.print()
711
712
+ # Re-exec under ncu if --ncu-profile and not already inside ncu. This runs
713
+ # after YAML processing so ncu_profile set via config file is honored.
714
+ if args.ncu_profile and "_NCU_INNER" not in os.environ:
715
+ ncu = shutil.which("ncu")
716
+ if ncu is None:
717
+ print("Error: 'ncu' not found in PATH", file=sys.stderr)
718
+ sys.exit(1)
719
+ cmd = [
720
+ ncu,
721
+ "--profile-from-start",
722
+ "off",
723
+ "--set",
724
+ "full",
725
+ "--import-source",
726
+ "yes",
727
+ "-o",
728
+ args.ncu_output,
729
+ sys.executable,
730
+ *sys.argv,
731
+ ]
732
+ env = os.environ.copy()
733
+ env["CUTE_DSL_LINEINFO"] = "1"
734
+ env["_NCU_INNER"] = "1"
735
+ print(f"Launching: {' '.join(cmd)}")
736
+ sys.exit(subprocess.call(cmd, env=env))
737
+
738
# Handle CLI-based parameter sweep (if not from YAML)
739
if (
740
(not hasattr(args, "parameter_sweep") or args.parameter_sweep is None)
@@ -655,6 +759,18 @@ def main():
759
console.print(f"Batch specs: {', '.join(args.batch_specs)}")
760
console.print(f"KV cache dtype: {args.kv_cache_dtype}")
761
console.print(f"CUDA graphs: {args.cuda_graphs}")
762
+ if args.warmup_ms is not None and args.cuda_graphs:
763
+ console.print(
764
+ "[yellow]Warning: --warmup-ms is ignored with CUDA graphs "
765
+ "(do_bench_cudagraph warms up internally). Pass --no-cuda-graphs "
766
+ "to use it.[/]"
767
+ )
768
+ if args.num_splits == 0 and args.cuda_graphs:
769
+ console.print(
770
+ "[yellow]Warning: --num-splits 0 (FA3 heuristic) is not CUDA-graph "
771
+ "compatible and may fail or fall back. Pass --no-cuda-graphs or use "
772
+ "--num-splits >=1.[/]"
773
+ )
774
console.print()
775
776
init_workspace_manager(args.device)
@@ -662,6 +778,15 @@ def main():
778
# Run benchmarks
779
all_results = []
780
781
+ # Under ncu profiling the kernels run only to be captured by the profiler;
782
+ # timings are placeholder zeros, so the result tables and saved metrics are
783
+ # skipped. The Nsight Compute report (--ncu-output) holds the real data.
784
+ if args.ncu_profile:
785
+ console.print(
786
+ "[dim]ncu profiling enabled: result tables and saved metrics are "
787
+ "skipped (timings are placeholder zeros).[/]"
788
+ )
789
+
790
# Handle special mode: decode_vs_prefill comparison
791
if hasattr(args, "mode") and args.mode == "decode_vs_prefill":
792
console.print("[yellow]Mode: Decode vs Prefill pipeline comparison[/]")
@@ -708,11 +833,11 @@ def main():
833
num_kv_heads=args.num_kv_heads,
834
block_size=args.block_size,
835
device=args.device,
711
- repeats=args.repeats,
712
- warmup_iters=args.warmup_iters,
836
profile_memory=args.profile_memory,
837
kv_cache_dtype=args.kv_cache_dtype,
838
use_cuda_graphs=args.cuda_graphs,
839
+ ncu_profile=args.ncu_profile,
840
+ warmup_ms=args.warmup_ms,
841
)
842
843
# Add decode pipeline config
@@ -749,6 +874,7 @@ def main():
874
result = BenchmarkResult(
875
config=config,
876
mean_time=timing["mean"],
877
+ median_time=timing.get("median", timing["mean"]),
878
std_time=timing["std"],
879
min_time=timing["min"],
880
max_time=timing["max"],
@@ -770,6 +896,7 @@ def main():
896
result = BenchmarkResult(
897
config=config,
898
mean_time=float("inf"),
899
+ median_time=float("inf"),
900
std_time=0,
901
min_time=float("inf"),
902
max_time=float("inf"),
@@ -779,6 +906,9 @@ def main():
906
907
pbar.update(1)
908
909
+ if args.ncu_profile:
910
+ return
911
+
912
# Display decode vs prefill results
913
console.print("\n[bold green]Decode vs Prefill Results:[/]")
914
@@ -858,15 +988,20 @@ def main():
988
base_config_args = {
989
"num_layers": args.num_layers,
990
"head_dim": args.head_dim,
991
+ "v_head_dim": args.v_head_dim,
992
"num_q_heads": args.num_q_heads,
993
"num_kv_heads": args.num_kv_heads,
994
"block_size": args.block_size,
995
"device": args.device,
865
- "repeats": args.repeats,
866
- "warmup_iters": args.warmup_iters,
996
"profile_memory": args.profile_memory,
997
"kv_cache_dtype": args.kv_cache_dtype,
998
"use_cuda_graphs": args.cuda_graphs,
999
+ "ncu_profile": args.ncu_profile,
1000
+ "warmup_ms": args.warmup_ms,
1001
+ "num_splits": args.num_splits,
1002
+ "kv_lora_rank": args.kv_lora_rank,
1003
+ "qk_nope_head_dim": args.qk_nope_head_dim,
1004
+ "qk_rope_head_dim": args.qk_rope_head_dim,
1005
}
1006
all_results = run_model_parameter_sweep(
1007
backends,
@@ -882,15 +1017,17 @@ def main():
1017
base_config_args = {
1018
"num_layers": args.num_layers,
1019
"head_dim": args.head_dim,
1020
+ "v_head_dim": args.v_head_dim,
1021
"num_q_heads": args.num_q_heads,
1022
"num_kv_heads": args.num_kv_heads,
1023
"block_size": args.block_size,
1024
"device": args.device,
889
- "repeats": args.repeats,
890
- "warmup_iters": args.warmup_iters,
1025
"profile_memory": args.profile_memory,
1026
"kv_cache_dtype": args.kv_cache_dtype,
1027
"use_cuda_graphs": args.cuda_graphs,
1028
+ "ncu_profile": args.ncu_profile,
1029
+ "warmup_ms": args.warmup_ms,
1030
+ "num_splits": args.num_splits,
1031
}
1032
all_results = run_parameter_sweep(
1033
backends, args.batch_specs, base_config_args, args.parameter_sweep, console
@@ -914,15 +1051,17 @@ def main():
1051
batch_spec=spec,
1052
num_layers=args.num_layers,
1053
head_dim=args.head_dim,
1054
+ v_head_dim=getattr(args, "v_head_dim", None),
1055
num_q_heads=args.num_q_heads,
1056
num_kv_heads=args.num_kv_heads,
1057
block_size=args.block_size,
1058
device=args.device,
921
- repeats=args.repeats,
922
- warmup_iters=args.warmup_iters,
1059
profile_memory=args.profile_memory,
1060
kv_cache_dtype=args.kv_cache_dtype,
1061
use_cuda_graphs=args.cuda_graphs,
1062
+ ncu_profile=args.ncu_profile,
1063
+ warmup_ms=args.warmup_ms,
1064
+ num_splits=args.num_splits,
1065
)
1066
1067
result = run_benchmark(config)
@@ -935,9 +1074,10 @@ def main():
1074
1075
pbar.update(1)
1076
938
- console.print("\n[bold green]Results:[/]")
939
- formatter = ResultsFormatter(console)
940
- formatter.print_table(decode_results, backends)
1077
+ if not args.ncu_profile:
1078
+ console.print("\n[bold green]Results:[/]")
1079
+ formatter = ResultsFormatter(console)
1080
+ formatter.print_table(decode_results, backends)
1081
1082
# Run prefill backend comparison
1083
if prefill_backends:
@@ -962,9 +1102,8 @@ def main():
1102
num_kv_heads=args.num_kv_heads,
1103
block_size=args.block_size,
1104
device=args.device,
965
- repeats=args.repeats,
966
- warmup_iters=args.warmup_iters,
1105
profile_memory=args.profile_memory,
1106
+ warmup_ms=args.warmup_ms,
1107
prefill_backend=pb,
1108
)
1109
@@ -980,16 +1119,17 @@ def main():
1119
1120
pbar.update(1)
1121
983
- console.print("\n[bold green]Prefill Backend Results:[/]")
984
- formatter = ResultsFormatter(console)
985
- formatter.print_table(
986
- prefill_results, prefill_backends, compare_to_fastest=True
987
- )
1122
+ if not args.ncu_profile:
1123
+ console.print("\n[bold green]Prefill Backend Results:[/]")
1124
+ formatter = ResultsFormatter(console)
1125
+ formatter.print_table(
1126
+ prefill_results, prefill_backends, compare_to_fastest=True
1127
+ )
1128
1129
all_results = decode_results + prefill_results
1130
991
- # Save results
992
- if all_results:
1131
+ # Save results (skip ncu profiling runs: timings are placeholder zeros)
1132
+ if all_results and not args.ncu_profile:
1133
formatter = ResultsFormatter(console)
1134
if args.output_csv:
1135
formatter.save_csv(all_results, args.output_csv)
benchmarks/attention_benchmarks/common.py
+54
-6
@@ -15,6 +15,8 @@ from batch_spec import get_batch_type, parse_batch_spec
15
from rich.console import Console
16
from rich.table import Table
17
18
+from vllm.triton_utils import triton
19
+
20
21
def batch_spec_sort_key(spec: str) -> tuple[int, int, int]:
22
"""
@@ -34,6 +36,30 @@ def batch_spec_sort_key(spec: str) -> tuple[int, int, int]:
36
return (0, 0, 0)
37
38
39
+def run_do_bench(
40
+ benchmark_fn,
41
+ use_cuda_graphs: bool,
42
+ warmup_ms: int | None = None,
43
+) -> list[float]:
44
+ kwargs: dict[str, Any] = {"return_mode": "all"}
45
+ if use_cuda_graphs:
46
+ result = triton.testing.do_bench_cudagraph(benchmark_fn, **kwargs)
47
+ else:
48
+ if warmup_ms is not None:
49
+ kwargs["warmup"] = warmup_ms
50
+ result = triton.testing.do_bench(benchmark_fn, **kwargs)
51
+ return result
52
+
53
+
54
+def run_ncu_profile(benchmark_fn) -> None:
55
+ benchmark_fn()
56
+ torch.accelerator.synchronize()
57
+ torch.cuda.cudart().cudaProfilerStart()
58
+ benchmark_fn()
59
+ torch.accelerator.synchronize()
60
+ torch.cuda.cudart().cudaProfilerStop()
61
+
62
+
63
# Mock classes for vLLM attention infrastructure
64
65
@@ -182,18 +208,37 @@ class ParameterSweep:
208
209
@dataclass
210
class ModelParameterSweep:
185
- """Configuration for sweeping a model configuration parameter."""
211
+ """Configuration for sweeping model configuration parameter(s).
212
187
- param_name: str # Name of the model config parameter to sweep (e.g., "num_q_heads")
188
- values: list[Any] # List of values to test
189
- label_format: str = "{backend}_{param_name}_{value}" # Result label template
213
+ Supports two modes:
214
+ - Single param: param_name="head_dim", values=[128, 256, 512]
215
+ - Multi param: values=[{head_dim: 192, v_head_dim: 128}, {head_dim: 256}]
216
+ When values are dicts, each dict's keys are applied as config overrides.
217
+ """
218
+
219
+ param_name: str | None = None
220
+ values: list[Any] | None = None
221
+ label_format: str = "{backend}_{param_name}_{value}"
222
223
def get_label(self, backend: str, value: Any) -> str:
224
"""Generate a label for a specific parameter value."""
225
+ if isinstance(value, dict):
226
+ return self.label_format.format(
227
+ backend=backend, param_name=self.param_name, value=value, **value
228
+ )
229
return self.label_format.format(
230
backend=backend, param_name=self.param_name, value=value
231
)
232
233
+ def apply(self, config_args: dict, value: Any) -> None:
234
+ """Apply a sweep value to config args."""
235
+ if isinstance(value, dict):
236
+ config_args.update(value)
237
+ elif self.param_name is not None:
238
+ config_args[self.param_name] = value
239
+ else:
240
+ raise ValueError("param_name must be set if sweep values are not dicts")
241
+
242
243
@dataclass
244
class BenchmarkConfig:
@@ -208,10 +253,10 @@ class BenchmarkConfig:
253
block_size: int
254
device: str
255
dtype: torch.dtype = torch.float16
211
- repeats: int = 1
212
- warmup_iters: int = 3
256
profile_memory: bool = False
257
use_cuda_graphs: bool = False
258
+ ncu_profile: bool = False
259
+ warmup_ms: int | None = None
260
261
# "auto" or "fp8"
262
kv_cache_dtype: str = "auto"
@@ -226,6 +271,7 @@ class BenchmarkConfig:
271
# Backend-specific tuning
272
num_kv_splits: int | None = None # CUTLASS MLA
273
reorder_batch_threshold: int | None = None # FlashAttn MLA, FlashMLA
274
+ num_splits: int | None = None # FlashAttention split-K (0=auto, 1=disabled)
275
276
277
@dataclass
@@ -234,6 +280,7 @@ class BenchmarkResult:
280
281
config: BenchmarkConfig
282
mean_time: float # seconds
283
+ median_time: float # seconds
284
std_time: float # seconds
285
min_time: float # seconds
286
max_time: float # seconds
@@ -252,6 +299,7 @@ class BenchmarkResult:
299
return {
300
"config": asdict(self.config),
301
"mean_time": self.mean_time,
302
+ "median_time": self.median_time,
303
"std_time": self.std_time,
304
"min_time": self.min_time,
305
"max_time": self.max_time,
benchmarks/attention_benchmarks/configs/mla_decode.yaml
-2
@@ -56,8 +56,6 @@ backends:
56
- TOKENSPEED_MLA # Blackwell + R1 dims + FP8 KV (use --kv-cache-dtype fp8)
57
58
device: "cuda:0"
59
-repeats: 100
60
-warmup_iters: 10
59
profile_memory: true
60
61
# Backend-specific tuning
benchmarks/attention_benchmarks/configs/mla_mixed_batch.yaml
-2
@@ -51,8 +51,6 @@ backends:
51
- FLASHMLA # Hopper only
52
53
device: "cuda:0"
54
-repeats: 5
55
-warmup_iters: 3
54
profile_memory: true
55
56
# Analyze chunked prefill workspace size impact
benchmarks/attention_benchmarks/configs/mla_prefill.yaml
-2
@@ -124,5 +124,3 @@ prefill_backends:
124
- tokenspeed
125
126
device: "cuda:0"
127
-repeats: 20
128
-warmup_iters: 5
benchmarks/attention_benchmarks/configs/mla_sparse_decode.yaml
-2
@@ -53,6 +53,4 @@ backends:
53
- FLASHINFER_MLA_SPARSE
54
55
device: "cuda:0"
56
-repeats: 100
57
-warmup_iters: 10
56
profile_memory: true
benchmarks/attention_benchmarks/configs/mla_sparse_prefill.yaml
-2
@@ -57,6 +57,4 @@ backends:
57
- FLASHINFER_MLA_SPARSE
58
59
device: "cuda:0"
60
-repeats: 10
61
-warmup_iters: 3
60
profile_memory: true
benchmarks/attention_benchmarks/configs/reorder_threshold.yaml
-2
@@ -63,8 +63,6 @@ model:
63
64
# Benchmark settings
65
device: "cuda:0"
66
-repeats: 15 # More repeats for spec decode variance
67
-warmup_iters: 5
66
profile_memory: false
67
68
# Output
benchmarks/attention_benchmarks/configs/speculative_decode.yaml
-2
@@ -49,8 +49,6 @@ backends:
49
50
# Benchmark settings
51
device: "cuda:0"
52
-repeats: 10 # More repeats for statistical significance
53
-warmup_iters: 5
52
profile_memory: false
53
54
# Test these threshold values for optimization
benchmarks/attention_benchmarks/configs/standard_attention.yaml
-2
@@ -43,6 +43,4 @@ backends:
43
- FLASHINFER
44
45
device: "cuda:0"
46
-repeats: 5
47
-warmup_iters: 3
46
profile_memory: false
benchmarks/attention_benchmarks/configs/standard_decode.yaml
new
+142
@@ -0,0 +1,142 @@
1
+# Standard attention decode benchmark configuration
2
+# Sweeps num_q_heads and num_kv_heads to isolate effects of:
3
+# 1. GQA ratio (fixed num_q_heads=32, vary num_kv_heads)
4
+# 2. Absolute head count (fixed 4:1 ratio, vary scale)
5
+
6
+model:
7
+ num_layers: 32
8
+ num_q_heads: 32 # Base value, overridden by sweep
9
+ num_kv_heads: 8 # Base value, overridden by sweep
10
+ head_dim: 128
11
+ block_size: 16
12
+
13
+# Head count sweep: each entry overrides num_q_heads, num_kv_heads, and
14
+# head_dim where it differs from the base (128). Head counts are per-GPU
15
+# (i.e. after TP sharding).
16
+#
17
+# Group A — vary GQA ratio (fixed q=32, head_dim=128):
18
+# 32:32 (MHA), 32:8 (GQA 4:1), 32:4 (GQA 8:1), 32:1 (MQA)
19
+#
20
+# Groups B-E — real model configs at various TP degrees:
21
+# Model head_dim Full TP2 TP4 TP8
22
+# Llama 3 8B 128 32:8 16:4 8:2 4:1
23
+# Llama 3 70B 128 64:8 32:4 16:2 8:1
24
+# GPT-OSS 120B 64 64:8 32:4 16:2 8:1
25
+# Llama 3 405B 128 128:8 64:4 32:2 16:1
26
+model_parameter_sweep:
27
+ values:
28
+ # --- head_dim=128 (Llama 3 family) ---
29
+ - { num_q_heads: 32, num_kv_heads: 32, head_dim: 128 } # MHA 1:1
30
+ - { num_q_heads: 32, num_kv_heads: 1, head_dim: 128 } # MQA 32:1
31
+ - { num_q_heads: 4, num_kv_heads: 1, head_dim: 128 } # Llama 3 8B TP8
32
+ - { num_q_heads: 8, num_kv_heads: 2, head_dim: 128 } # Llama 3 8B TP4
33
+ - { num_q_heads: 16, num_kv_heads: 4, head_dim: 128 } # Llama 3 8B TP2
34
+ - { num_q_heads: 32, num_kv_heads: 8, head_dim: 128 } # Llama 3 8B TP1 / GQA 4:1
35
+ - { num_q_heads: 8, num_kv_heads: 1, head_dim: 128 } # Llama 3 70B TP8
36
+ - { num_q_heads: 16, num_kv_heads: 2, head_dim: 128 } # Llama 3 70B TP4
37
+ - { num_q_heads: 32, num_kv_heads: 4, head_dim: 128 } # Llama 3 70B TP2 / GQA 8:1
38
+ - { num_q_heads: 64, num_kv_heads: 8, head_dim: 128 } # Llama 3 70B TP1
39
+ - { num_q_heads: 16, num_kv_heads: 1, head_dim: 128 } # Llama 3 405B TP8
40
+ - { num_q_heads: 32, num_kv_heads: 2, head_dim: 128 } # Llama 3 405B TP4
41
+ - { num_q_heads: 64, num_kv_heads: 4, head_dim: 128 } # Llama 3 405B TP2
42
+ - { num_q_heads: 128, num_kv_heads: 8, head_dim: 128 } # Llama 3 405B TP1
43
+ # --- head_dim=64 (GPT-OSS 120B) ---
44
+ - { num_q_heads: 8, num_kv_heads: 1, head_dim: 64 } # GPT-OSS 120B TP8
45
+ - { num_q_heads: 16, num_kv_heads: 2, head_dim: 64 } # GPT-OSS 120B TP4
46
+ - { num_q_heads: 32, num_kv_heads: 4, head_dim: 64 } # GPT-OSS 120B TP2
47
+ - { num_q_heads: 64, num_kv_heads: 8, head_dim: 64 } # GPT-OSS 120B TP1
48
+ label_format: "{backend}_q{num_q_heads}kv{num_kv_heads}d{head_dim}"
49
+
50
+batch_specs:
51
+ # ---- batch_size x seq_len grid (decode: q_len=1) ----
52
+ # Small grid for quick iteration. Uncomment for full sweep.
53
+
54
+ # Batch size 1
55
+ - "q1s1k"
56
+ - "q1s512"
57
+ - "q1s2k"
58
+ - "q1s4k"
59
+ - "q1s8k"
60
+ - "q1s16k"
61
+ - "q1s32k"
62
+
63
+ # Batch size 2
64
+ - "2q1s512"
65
+ - "2q1s1k"
66
+ - "2q1s2k"
67
+ - "2q1s4k"
68
+ - "2q1s8k"
69
+ - "2q1s16k"
70
+ - "2q1s32k"
71
+
72
+ # Batch size 4
73
+ - "4q1s512"
74
+ - "4q1s1k"
75
+ - "4q1s2k"
76
+ - "4q1s4k"
77
+ - "4q1s8k"
78
+ - "4q1s16k"
79
+ - "4q1s32k"
80
+
81
+ # Batch size 8
82
+ - "8q1s1k"
83
+ - "8q1s512"
84
+ - "8q1s2k"
85
+ - "8q1s4k"
86
+ - "8q1s8k"
87
+ - "8q1s16k"
88
+ - "8q1s32k"
89
+
90
+ # Batch size 16
91
+ - "16q1s512"
92
+ - "16q1s1k"
93
+ - "16q1s2k"
94
+ - "16q1s4k"
95
+ - "16q1s8k"
96
+ - "16q1s16k"
97
+ - "16q1s32k"
98
+
99
+ # Batch size 32
100
+ - "32q1s512"
101
+ - "32q1s1k"
102
+ - "32q1s2k"
103
+ - "32q1s4k"
104
+ - "32q1s8k"
105
+ - "32q1s16k"
106
+ - "32q1s32k"
107
+
108
+ # Batch size 64
109
+ - "64q1s1k"
110
+ - "64q1s512"
111
+ - "64q1s2k"
112
+ - "64q1s4k"
113
+ - "64q1s8k"
114
+ - "64q1s16k"
115
+ - "64q1s32k"
116
+
117
+ # Batch size 128
118
+ - "128q1s512"
119
+ - "128q1s1k"
120
+ - "128q1s2k"
121
+ - "128q1s4k"
122
+ - "128q1s8k"
123
+ - "128q1s16k"
124
+ - "128q1s32k"
125
+
126
+ # Batch size 256
127
+ - "256q1s1k"
128
+ - "256q1s512"
129
+ - "256q1s2k"
130
+ - "256q1s4k"
131
+ - "256q1s8k"
132
+ - "256q1s16k"
133
+ - "256q1s32k"
134
+
135
+# Available backends: FLASH_ATTN, TRITON_ATTN, FLASHINFER
136
+backends:
137
+ - FLASH_ATTN
138
+ - TRITON_ATTN
139
+ - FLASHINFER
140
+
141
+device: "cuda:0"
142
+profile_memory: false
benchmarks/attention_benchmarks/configs/standard_prefill.yaml
new
+108
@@ -0,0 +1,108 @@
1
+# Standard attention prefill benchmark configuration
2
+# Sweeps num_q_heads and num_kv_heads to isolate effects of:
3
+# 1. GQA ratio (fixed num_q_heads=32, vary num_kv_heads)
4
+# 2. Absolute head count (fixed 4:1 ratio, vary scale)
5
+
6
+model:
7
+ num_layers: 32
8
+ num_q_heads: 32 # Base value, overridden by sweep
9
+ num_kv_heads: 8 # Base value, overridden by sweep
10
+ head_dim: 128
11
+ block_size: 16
12
+
13
+# Head count sweep: each entry overrides num_q_heads, num_kv_heads, and
14
+# head_dim where it differs from the base (128). Head counts are per-GPU
15
+# (i.e. after TP sharding).
16
+#
17
+# Group A — vary GQA ratio (fixed q=32, head_dim=128):
18
+# 32:32 (MHA), 32:8 (GQA 4:1), 32:4 (GQA 8:1), 32:1 (MQA)
19
+#
20
+# Groups B-E — real model configs at various TP degrees:
21
+# Model head_dim Full TP2 TP4 TP8
22
+# Llama 3 8B 128 32:8 16:4 8:2 4:1
23
+# Llama 3 70B 128 64:8 32:4 16:2 8:1
24
+# GPT-OSS 120B 64 64:8 32:4 16:2 8:1
25
+# Llama 3 405B 128 128:8 64:4 32:2 16:1
26
+model_parameter_sweep:
27
+ values:
28
+ # --- head_dim=128 (Llama 3 family) ---
29
+ - { num_q_heads: 32, num_kv_heads: 32, head_dim: 128 } # MHA 1:1
30
+ - { num_q_heads: 32, num_kv_heads: 1, head_dim: 128 } # MQA 32:1
31
+ - { num_q_heads: 4, num_kv_heads: 1, head_dim: 128 } # Llama 3 8B TP8
32
+ - { num_q_heads: 8, num_kv_heads: 2, head_dim: 128 } # Llama 3 8B TP4
33
+ - { num_q_heads: 16, num_kv_heads: 4, head_dim: 128 } # Llama 3 8B TP2
34
+ - { num_q_heads: 32, num_kv_heads: 8, head_dim: 128 } # Llama 3 8B TP1 / GQA 4:1
35
+ - { num_q_heads: 8, num_kv_heads: 1, head_dim: 128 } # Llama 3 70B TP8
36
+ - { num_q_heads: 16, num_kv_heads: 2, head_dim: 128 } # Llama 3 70B TP4
37
+ - { num_q_heads: 32, num_kv_heads: 4, head_dim: 128 } # Llama 3 70B TP2 / GQA 8:1
38
+ - { num_q_heads: 64, num_kv_heads: 8, head_dim: 128 } # Llama 3 70B TP1
39
+ - { num_q_heads: 16, num_kv_heads: 1, head_dim: 128 } # Llama 3 405B TP8
40
+ - { num_q_heads: 32, num_kv_heads: 2, head_dim: 128 } # Llama 3 405B TP4
41
+ - { num_q_heads: 64, num_kv_heads: 4, head_dim: 128 } # Llama 3 405B TP2
42
+ - { num_q_heads: 128, num_kv_heads: 8, head_dim: 128 } # Llama 3 405B TP1
43
+ # --- head_dim=64 (GPT-OSS 120B) ---
44
+ - { num_q_heads: 8, num_kv_heads: 1, head_dim: 64 } # GPT-OSS 120B TP8
45
+ - { num_q_heads: 16, num_kv_heads: 2, head_dim: 64 } # GPT-OSS 120B TP4
46
+ - { num_q_heads: 32, num_kv_heads: 4, head_dim: 64 } # GPT-OSS 120B TP2
47
+ - { num_q_heads: 64, num_kv_heads: 8, head_dim: 64 } # GPT-OSS 120B TP1
48
+ label_format: "{backend}_q{num_q_heads}kv{num_kv_heads}d{head_dim}"
49
+
50
+batch_specs:
51
+ # ---- batch_size x prefill_len grid (prefill: q_len == seq_len) ----
52
+ # Total tokens = batch_size * prefill_len, and prefill compute scales with
53
+ # prefill_len^2, so the largest cells are expensive. Trim batch sizes or
54
+ # lengths for quick iteration.
55
+
56
+ # Batch size 1
57
+ - "q512"
58
+ - "q1k"
59
+ - "q2k"
60
+ - "q4k"
61
+ - "q8k"
62
+ - "q16k"
63
+ - "q32k"
64
+
65
+ # Batch size 2
66
+ - "2q512"
67
+ - "2q1k"
68
+ - "2q2k"
69
+ - "2q4k"
70
+ - "2q8k"
71
+ - "2q16k"
72
+ - "2q32k"
73
+
74
+ # Batch size 4
75
+ - "4q512"
76
+ - "4q1k"
77
+ - "4q2k"
78
+ - "4q4k"
79
+ - "4q8k"
80
+ - "4q16k"
81
+ - "4q32k"
82
+
83
+ # Batch size 8
84
+ - "8q512"
85
+ - "8q1k"
86
+ - "8q2k"
87
+ - "8q4k"
88
+ - "8q8k"
89
+ - "8q16k"
90
+ - "8q32k"
91
+
92
+ # Batch size 16
93
+ - "16q512"
94
+ - "16q1k"
95
+ - "16q2k"
96
+ - "16q4k"
97
+ - "16q8k"
98
+ - "16q16k"
99
+ - "16q32k"
100
+
101
+# Available backends: FLASH_ATTN, TRITON_ATTN, FLASHINFER
102
+backends:
103
+ - FLASH_ATTN
104
+ - TRITON_ATTN
105
+ - FLASHINFER
106
+
107
+device: "cuda:0"
108
+profile_memory: false
benchmarks/attention_benchmarks/mla_runner.py
+26
-31
@@ -8,6 +8,8 @@ This module provides helpers for running MLA backends without
8
needing full VllmConfig integration.
9
"""
10
11
+import statistics
12
+
13
import numpy as np
14
import torch
15
from batch_spec import parse_batch_spec
@@ -17,6 +19,8 @@ from common import (
19
MockIndexer,
20
MockKVBProj,
21
MockLayer,
22
+ run_do_bench,
23
+ run_ncu_profile,
24
setup_mla_dims,
25
)
26
@@ -820,7 +824,7 @@ def _run_single_benchmark(
824
num_prefill, mla_dims, query_fmt, device, torch.bfloat16
825
)
826
823
- # Build forward function
827
+ # Build forward function (runs a single decode/prefill pass)
828
def forward_fn():
829
results = []
830
if has_decode:
@@ -839,44 +843,35 @@ def _run_single_benchmark(
843
)
844
return results[0] if len(results) == 1 else tuple(results)
845
842
- # Warmup
843
- for _ in range(config.warmup_iters):
844
- forward_fn()
845
- torch.accelerator.synchronize()
846
-
847
- # Optionally capture a CUDA graph after warmup.
848
- # Graph replay eliminates CPU launch overhead so timings reflect pure
849
- # kernel time.
850
- if config.use_cuda_graphs:
851
- graph = torch.cuda.CUDAGraph()
852
- with torch.cuda.graph(graph):
846
+ def benchmark_fn():
847
+ for _ in range(config.num_layers):
848
forward_fn()
854
- benchmark_fn = graph.replay
855
- else:
856
- benchmark_fn = forward_fn
849
858
- # Benchmark
859
- times = []
860
- for _ in range(config.repeats):
861
- start = torch.cuda.Event(enable_timing=True)
862
- end = torch.cuda.Event(enable_timing=True)
850
+ if config.ncu_profile:
851
+ run_ncu_profile(benchmark_fn)
852
+ return BenchmarkResult(
853
+ config=config,
854
+ mean_time=0.0,
855
+ median_time=0.0,
856
+ std_time=0.0,
857
+ min_time=0.0,
858
+ max_time=0.0,
859
+ throughput_tokens_per_sec=0.0,
860
+ )
861
864
- start.record()
865
- for _ in range(config.num_layers):
866
- benchmark_fn()
867
- end.record()
862
+ all_ms = run_do_bench(benchmark_fn, config.use_cuda_graphs, config.warmup_ms)
863
869
- torch.accelerator.synchronize()
870
- elapsed_ms = start.elapsed_time(end)
871
- times.append(elapsed_ms / 1000.0 / config.num_layers)
864
+ # Convert ms to seconds per layer
865
+ times = [t / 1000.0 / config.num_layers for t in all_ms]
866
+ mean_time = statistics.mean(times)
867
873
- mean_time = float(np.mean(times))
868
return BenchmarkResult(
869
config=config,
870
mean_time=mean_time,
877
- std_time=float(np.std(times)),
878
- min_time=float(np.min(times)),
879
- max_time=float(np.max(times)),
871
+ median_time=statistics.median(times),
872
+ std_time=statistics.stdev(times) if len(times) > 1 else 0.0,
873
+ min_time=min(times),
874
+ max_time=max(times),
875
throughput_tokens_per_sec=total_q / mean_time if mean_time > 0 else 0,
876
)
877
benchmarks/attention_benchmarks/runner.py
+55
-62
@@ -9,13 +9,20 @@ This module provides helpers for running standard attention backends
9
"""
10
11
import logging
12
+import statistics
13
import types
14
from contextlib import contextmanager
15
15
-import numpy as np
16
import torch
17
from batch_spec import parse_batch_spec, reorder_for_flashinfer
18
-from common import BenchmarkConfig, BenchmarkResult, MockLayer, get_attention_scale
18
+from common import (
19
+ BenchmarkConfig,
20
+ BenchmarkResult,
21
+ MockLayer,
22
+ get_attention_scale,
23
+ run_do_bench,
24
+ run_ncu_profile,
25
+)
26
27
from vllm.config import (
28
CacheConfig,
@@ -208,6 +215,13 @@ def _create_backend_impl(
215
216
scale = get_attention_scale(config.head_dim)
217
218
+ # Set v_head_dim for diff-headdim backends. Always reset (defaulting to
219
+ # head_dim) so a prior run's value doesn't leak into this one via the
220
+ # backend's class-level state.
221
+ if hasattr(backend_class, "set_head_size_v"):
222
+ v_dim = config.v_head_dim if config.v_head_dim is not None else config.head_dim
223
+ backend_class.set_head_size_v(v_dim)
224
+
225
impl = backend_class.get_impl_cls()(
226
num_heads=config.num_q_heads,
227
head_size=config.head_dim,
@@ -300,6 +314,7 @@ def _create_input_tensors(
314
from vllm.platforms import current_platform
315
316
q_dtype = current_platform.fp8_dtype()
317
+ v_dim = config.v_head_dim if config.v_head_dim is not None else config.head_dim
318
q_list = [
319
torch.randn(
320
total_q, config.num_q_heads, config.head_dim, device=device, dtype=dtype
@@ -313,9 +328,7 @@ def _create_input_tensors(
328
for _ in range(config.num_layers)
329
]
330
v_list = [
316
- torch.randn(
317
- total_q, config.num_kv_heads, config.head_dim, device=device, dtype=dtype
318
- )
331
+ torch.randn(total_q, config.num_kv_heads, v_dim, device=device, dtype=dtype)
332
for _ in range(config.num_layers)
333
]
334
return q_list, k_list, v_list
@@ -389,14 +402,17 @@ def _run_single_benchmark(
402
device: torch.device,
403
dtype: torch.dtype,
404
) -> tuple:
392
- """Run single benchmark iteration with warmup and timing loop."""
405
+ """Run single benchmark using triton's do_bench_cudagraph/do_bench.
406
+
407
+ Returns:
408
+ (timing_stats, mem_stats) where timing_stats is a dict with
409
+ mean/std/min/max in seconds per layer.
410
+ """
411
total_q = q_list[0].shape[0]
394
- out = torch.empty(
395
- total_q, config.num_q_heads, config.head_dim, device=device, dtype=dtype
396
- )
412
+ v_dim = config.v_head_dim if config.v_head_dim is not None else config.head_dim
413
+ out = torch.empty(total_q, config.num_q_heads, v_dim, device=device, dtype=dtype)
414
398
- # Warmup
399
- for _ in range(config.warmup_iters):
415
+ def benchmark_fn():
416
for i in range(config.num_layers):
417
impl.forward(
418
layer,
@@ -407,52 +423,22 @@ def _run_single_benchmark(
423
attn_metadata,
424
output=out,
425
)
410
- torch.accelerator.synchronize()
411
-
412
- # Optionally capture a CUDA graph after warmup.
413
- # Graph replay eliminates CPU launch overhead so timings reflect pure
414
- # kernel time.
415
- if config.use_cuda_graphs:
416
- graph = torch.cuda.CUDAGraph()
417
- with torch.cuda.graph(graph):
418
- for i in range(config.num_layers):
419
- impl.forward(
420
- layer,
421
- q_list[i],
422
- k_list[i],
423
- v_list[i],
424
- cache_list[i],
425
- attn_metadata,
426
- output=out,
427
- )
428
- benchmark_fn = graph.replay
429
- else:
430
-
431
- def benchmark_fn():
432
- for i in range(config.num_layers):
433
- impl.forward(
434
- layer,
435
- q_list[i],
436
- k_list[i],
437
- v_list[i],
438
- cache_list[i],
439
- attn_metadata,
440
- output=out,
441
- )
442
-
443
- # Benchmark
444
- times = []
445
- for _ in range(config.repeats):
446
- start = torch.cuda.Event(enable_timing=True)
447
- end = torch.cuda.Event(enable_timing=True)
426
449
- start.record()
450
- benchmark_fn()
451
- end.record()
452
-
453
- torch.accelerator.synchronize()
454
- elapsed_ms = start.elapsed_time(end)
455
- times.append(elapsed_ms / 1000.0 / config.num_layers) # seconds per layer
427
+ if config.ncu_profile:
428
+ run_ncu_profile(benchmark_fn)
429
+ timing_stats = dict.fromkeys(("mean", "median", "std", "min", "max"), 0.0)
430
+ else:
431
+ all_ms = run_do_bench(benchmark_fn, config.use_cuda_graphs, config.warmup_ms)
432
+
433
+ # Convert ms to seconds per layer
434
+ times = [t / 1000.0 / config.num_layers for t in all_ms]
435
+ timing_stats = {
436
+ "mean": statistics.mean(times),
437
+ "std": statistics.stdev(times) if len(times) > 1 else 0.0,
438
+ "min": min(times),
439
+ "max": max(times),
440
+ "median": statistics.median(times),
441
+ }
442
443
mem_stats = {}
444
if config.profile_memory:
@@ -461,7 +447,7 @@ def _run_single_benchmark(
447
"reserved_mb": torch.accelerator.memory_reserved(device) / 1024**2,
448
}
449
464
- return times, mem_stats
450
+ return timing_stats, mem_stats
451
452
453
# ============================================================================
@@ -541,6 +527,12 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
527
common_attn_metadata=common_metadata,
528
)
529
530
+ # Override num_splits for split-K testing (FlashAttention only)
531
+ if config.num_splits is not None and hasattr(
532
+ attn_metadata, "max_num_splits"
533
+ ):
534
+ attn_metadata.max_num_splits = config.num_splits
535
+
536
# Only quantize queries when the impl supports it
537
quantize_query = config.kv_cache_dtype.startswith("fp8") and getattr(
538
impl, "supports_quant_query_input", False
@@ -553,7 +545,7 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
545
config, max_num_blocks, backend_class, device, dtype
546
)
547
556
- times, mem_stats = _run_single_benchmark(
548
+ timing_stats, mem_stats = _run_single_benchmark(
549
config,
550
impl,
551
layer,
@@ -566,15 +558,16 @@ def run_attention_benchmark(config: BenchmarkConfig) -> BenchmarkResult:
558
dtype,
559
)
560
569
- mean_time = np.mean(times)
561
+ mean_time = timing_stats["mean"]
562
throughput = total_q / mean_time if mean_time > 0 else 0
563
564
return BenchmarkResult(
565
config=config,
566
mean_time=mean_time,
575
- std_time=np.std(times),
576
- min_time=np.min(times),
577
- max_time=np.max(times),
567
+ median_time=timing_stats["median"],
568
+ std_time=timing_stats["std"],
569
+ min_time=timing_stats["min"],
570
+ max_time=timing_stats["max"],
571
throughput_tokens_per_sec=throughput,
572
memory_allocated_mb=mem_stats.get("allocated_mb"),
573
memory_reserved_mb=mem_stats.get("reserved_mb"),