main
py 778 lines 25.6 KB
Raw
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: Apache-2.0
3 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4 """Benchmark and tuning script for the Mamba selective_state_update kernel.
5
6 Mirrors the fused MoE tuning workflow: sweeps (BLOCK_SIZE_M, num_warps) across
7 an effective_batch grid for a given (headdim, dstate, ngroups, cache_dtype) and
8 saves the best config per effective_batch to JSON. Generated configs are picked
9 up by selective_state_update at runtime.
10
11 Usage:
12 python -m benchmarks.kernels.benchmark_selective_state_update \
13 --all-dstates --save-configs --compare
14 """
15
16 import argparse
17 import json
18 import os
19 import sys
20 from io import StringIO
21 from itertools import product
22 from typing import Any
23
24 import torch
25
26 from tests.kernels.mamba.utils import selective_state_update_ref
27 from vllm.model_executor.layers.mamba.ops.mamba_ssm import (
28 _CONFIGS_DIR,
29 _canonical_cache_dtype,
30 _get_default_ssm_launch_config,
31 get_ssm_config_file_name,
32 get_ssm_device_name,
33 override_ssm_config,
34 selective_state_update,
35 )
36 from vllm.platforms import current_platform
37 from vllm.triton_utils import triton
38
39 # bf16 shares configs with fp16 - same bit width.
40 _SSM_CACHE_DTYPE_MAP: dict[str, torch.dtype] = {
41 "float32": torch.float32,
42 "float16": torch.float16,
43 "bfloat16": torch.float16,
44 }
45
46 _RESULTS_DIR = os.path.dirname(os.path.realpath(__file__))
47
48 # ---------------------------------------------------------------------------
49 # Tuning search space
50 # ---------------------------------------------------------------------------
51
52 _BSM_CHOICES_ALL = [4, 8, 16, 32, 64, 128, 256]
53
54 NUM_WARPS_CHOICES = [1, 2, 4, 8]
55
56
57 def _block_size_m_choices(headdim: int) -> list[int]:
58 """BLOCK_SIZE_M candidates worth sweeping for a given headdim.
59
60 BLOCK_SIZE_M > next_pow2(headdim) wastes >=50% of each tile via masking
61 (offs_m >= dim rows are zeroed out), so we cap the sweep there.
62 """
63 ceiling = 1
64 while ceiling < headdim:
65 ceiling <<= 1
66 return [b for b in _BSM_CHOICES_ALL if b <= ceiling]
67
68
69 # Default deployment shapes. effective_batch = batch * nheads scales the
70 # kernel grid, so configs transfer across (model, TP) combos sharing
71 # (headdim, dstate, cache_dtype).
72 DEFAULT_BATCH_SIZES = [1, 8, 16, 32, 64, 128, 256, 512, 1024, 1536, 2048]
73 DEFAULT_NHEADS = [128, 256]
74
75 ALL_DSTATES = [16, 32, 64, 128, 256]
76
77 # Default tuning shape — matches Nemotron-3-Super and Nemotron-3-Nano Mamba layers.
78 # Override with CLI flags for other architectures.
79 DEFAULT_HEADDIM = 64
80 DEFAULT_NGROUPS = 8
81
82
83 # ---------------------------------------------------------------------------
84 # Benchmark helper
85 # ---------------------------------------------------------------------------
86
87
88 def _make_inputs(
89 batch: int,
90 nheads: int,
91 dim: int,
92 dstate: int,
93 ngroups: int,
94 dtype: torch.dtype,
95 state_dtype: torch.dtype | None = None,
96 ):
97 if state_dtype is None:
98 state_dtype = dtype
99 device = current_platform.device_type
100 state = torch.randn(batch, nheads, dim, dstate, dtype=state_dtype, device=device)
101 x = torch.randn(batch, nheads, dim, dtype=dtype, device=device)
102 dt = torch.randn(batch, nheads, dim, dtype=dtype, device=device)
103 A = -torch.rand(nheads, dim, dstate, dtype=torch.float32, device=device)
104 B = torch.randn(batch, ngroups, dstate, dtype=dtype, device=device)
105 C = torch.randn(batch, ngroups, dstate, dtype=dtype, device=device)
106 D = torch.randn(nheads, dim, dtype=dtype, device=device)
107 dt_bias = torch.randn(nheads, dim, dtype=dtype, device=device)
108 out = torch.zeros(batch, nheads, dim, dtype=dtype, device=device)
109 return state, x, dt, A, B, C, D, dt_bias, out
110
111
112 def benchmark_config(
113 batch: int,
114 nheads: int,
115 dim: int,
116 dstate: int,
117 ngroups: int,
118 block_size_m: int,
119 num_warps_val: int,
120 dtype: torch.dtype,
121 state_dtype: torch.dtype | None = None,
122 num_iters: int = 100,
123 num_warmup: int = 20,
124 graph_batch_size: int = 10,
125 ) -> float | None:
126 """Time one (BLOCK_SIZE_M, num_warps) config for selective_state_update.
127 Returns elapsed time in microseconds, or None on error.
128
129 Uses accelerator graph capture-and-replay to isolate kernel time from
130 Python eager-mode dispatch / kwarg-resolution overhead, mirroring the
131 timing methodology in benchmarks/kernels/benchmark_moe.py.
132 """
133 state, x, dt, A, B, C, D, dt_bias, out = _make_inputs(
134 batch, nheads, dim, dstate, ngroups, dtype, state_dtype=state_dtype
135 )
136
137 def _call_kernel() -> None:
138 selective_state_update(
139 state,
140 x,
141 dt,
142 A,
143 B,
144 C,
145 D=D,
146 z=None,
147 dt_bias=dt_bias,
148 dt_softplus=True,
149 out=out,
150 )
151
152 try:
153 with override_ssm_config((block_size_m, num_warps_val)):
154 # Eager-mode warmup: triggers Triton autotune / JIT, primes caches.
155 for _ in range(num_warmup):
156 _call_kernel()
157 torch.accelerator.synchronize()
158
159 # Capture graph_batch_size invocations into a device graph so the
160 # timed region runs without Python dispatch overhead per call.
161 # Capture via graph(), not the graph object: CUDA needs a side stream.
162 graph = (
163 torch.cuda.CUDAGraph()
164 if current_platform.is_cuda_alike()
165 else torch.xpu.XPUGraph()
166 )
167 with current_platform.graph(graph):
168 for _ in range(graph_batch_size):
169 _call_kernel()
170 torch.accelerator.synchronize()
171
172 # Warmup graph replays (let the runtime stabilize).
173 for _ in range(5):
174 graph.replay()
175 torch.accelerator.synchronize()
176
177 start = torch.Event(enable_timing=True)
178 end = torch.Event(enable_timing=True)
179 latencies: list[float] = []
180 for _ in range(num_iters):
181 start.record()
182 graph.replay()
183 end.record()
184 end.synchronize()
185 latencies.append(start.elapsed_time(end))
186 graph.reset()
187 # elapsed_time returns ms; each replay runs graph_batch_size kernels,
188 # so divide by (num_iters * graph_batch_size) and convert ms -> us.
189 return sum(latencies) / (num_iters * graph_batch_size) * 1000
190 except Exception as e:
191 if "OutOfResources" not in str(e):
192 print(
193 f" Warning: config M={block_size_m},w={num_warps_val} "
194 f"raised {type(e).__name__}: {e}"
195 )
196 return None
197
198
199 # ---------------------------------------------------------------------------
200 # Tuning loop
201 # ---------------------------------------------------------------------------
202
203
204 # CUDA grid Y/Z dim limit — both `batch` and `nheads` must fit individually.
205 _CUDA_MAX_GRID_DIM = 65535
206
207 # Above this, kernel state-offset arithmetic (batch * nheads * headdim * dstate)
208 # overflows int32 and the launch raises cudaErrorIllegalAddress.
209 # 262144 covers Nemotron Super TP1 BS=2048.
210 _MAX_EFFECTIVE_BATCH = 262144
211
212
213 def expand_batch_x_nheads(
214 batch_sizes: list[int],
215 nheads_list: list[int],
216 ngroups: int,
217 ) -> list[tuple[int, int, int]]:
218 """Cross-product batch_sizes × nheads_list → sorted [(effective_batch,
219 batch, nheads)], deduped by effective_batch. Filters pairs that exceed
220 the CUDA grid dim limit, the effective_batch ceiling, or where nheads is
221 not a positive multiple of ngroups.
222 """
223 seen: dict[int, tuple[int, int]] = {}
224 skipped_grid: list[tuple[int, int]] = []
225 skipped_ngroups: list[tuple[int, int]] = []
226 skipped_eb: list[tuple[int, int]] = []
227 for b, n in product(batch_sizes, nheads_list):
228 if b <= 0 or n <= 0:
229 continue
230 if b > _CUDA_MAX_GRID_DIM or n > _CUDA_MAX_GRID_DIM:
231 skipped_grid.append((b, n))
232 continue
233 if n % ngroups != 0:
234 skipped_ngroups.append((b, n))
235 continue
236 if b * n > _MAX_EFFECTIVE_BATCH:
237 skipped_eb.append((b, n))
238 continue
239 seen.setdefault(b * n, (b, n))
240 if skipped_grid:
241 print(
242 f" Note: skipping (batch, nheads) pairs exceeding CUDA grid dim "
243 f"{_CUDA_MAX_GRID_DIM}: {skipped_grid}"
244 )
245 if skipped_ngroups:
246 print(
247 f" Note: skipping (batch, nheads) pairs where nheads % ngroups != 0 "
248 f"for ngroups={ngroups}: {skipped_ngroups}"
249 )
250 if skipped_eb:
251 print(
252 f" Note: skipping (batch, nheads) pairs whose effective_batch "
253 f"exceeds {_MAX_EFFECTIVE_BATCH}: {skipped_eb}"
254 )
255 return sorted((eb, b, n) for eb, (b, n) in seen.items())
256
257
258 def tune_dstate(
259 dstate: int,
260 headdim: int,
261 ngroups: int,
262 dtype: torch.dtype,
263 num_iters: int,
264 verbose: bool,
265 active: list[tuple[int, int, int]],
266 state_dtype: torch.dtype | None = None,
267 ) -> tuple[dict[int, dict], dict[int, dict[tuple[int, int], float]]]:
268 """For each (effective_batch, batch, nheads) in *active*, sweep
269 (BLOCK_SIZE_M, num_warps) and return
270 ({effective_batch: best_config}, {effective_batch: {(bsm, nw): us}}).
271 The second map is the full timing grid, used downstream so we don't
272 re-measure the same config in the comparison phase.
273 """
274 best_per_eb: dict[int, dict] = {}
275 timings: dict[int, dict[tuple[int, int], float]] = {}
276
277 print(f"\n{'=' * 74}")
278 effective_state_dtype = state_dtype if state_dtype is not None else dtype
279 print(
280 f"Tuning headdim={headdim} dstate={dstate} ngroups={ngroups} "
281 f"dtype={dtype} ssm_cache_dtype={effective_state_dtype}"
282 )
283 print(f"{'=' * 74}")
284
285 bsm_choices = _block_size_m_choices(headdim)
286 print(f"BSM candidates (capped at next_pow2(headdim={headdim})): {bsm_choices}")
287
288 hdr = f"{'EffBatch':>8} | {'BLOCK_M':>7} | {'warps':>5} | {'us':>10} | note"
289 print(hdr)
290 print("-" * 52)
291
292 for eb, batch, nheads in active:
293 best_time = float("inf")
294 best_cfg: dict = {}
295 eb_timings: dict[tuple[int, int], float] = {}
296
297 for bsm, nw in product(bsm_choices, NUM_WARPS_CHOICES):
298 t = benchmark_config(
299 batch=batch,
300 nheads=nheads,
301 dim=headdim,
302 dstate=dstate,
303 ngroups=ngroups,
304 block_size_m=bsm,
305 num_warps_val=nw,
306 dtype=dtype,
307 state_dtype=state_dtype,
308 num_iters=num_iters,
309 )
310 if t is None:
311 continue
312 eb_timings[(bsm, nw)] = t
313 is_best = t < best_time
314 if is_best:
315 best_time = t
316 best_cfg = {"BLOCK_SIZE_M": bsm, "num_warps": nw}
317 if verbose:
318 marker = " <-- best" if is_best else ""
319 print(f"{eb:>8} | {bsm:>7} | {nw:>5} | {t:>10.2f} |{marker}")
320
321 timings[eb] = eb_timings
322
323 if not best_cfg:
324 print(
325 f"{eb:>8} | {'-':>7} | {'-':>5} | {'-':>10} | "
326 f"no working config (skipped)"
327 )
328 continue
329
330 if not verbose:
331 print(
332 f"{eb:>8} | {best_cfg['BLOCK_SIZE_M']:>7} | "
333 f"{best_cfg['num_warps']:>5} | {best_time:>10.2f} | best"
334 )
335
336 best_per_eb[eb] = best_cfg
337
338 return best_per_eb, timings
339
340
341 # ---------------------------------------------------------------------------
342 # Correctness validation
343 # ---------------------------------------------------------------------------
344
345
346 def validate_configs(
347 dstate: int,
348 headdim: int,
349 ngroups: int,
350 tuned: dict[int, dict],
351 active: list[tuple[int, int, int]],
352 dtype: torch.dtype,
353 atol: float = 1e-2,
354 rtol: float = 1e-2,
355 state_dtype: torch.dtype | None = None,
356 ) -> dict[int, bool]:
357 """For every (effective_batch, batch, nheads) in *active* that has a tuned
358 config, run the kernel with that config and compare against the reference.
359 Returns {effective_batch: passed}.
360 """
361 # Disable TF32 so the reference's matmul matches the Triton kernel's
362 # fp32 accumulation; otherwise large ebs show bf16 rounding mismatches.
363 torch.set_float32_matmul_precision("highest")
364
365 print(f"\n{'=' * 74}")
366 effective_state_dtype = state_dtype if state_dtype is not None else dtype
367 print(
368 f"Validation headdim={headdim} dstate={dstate} ngroups={ngroups} "
369 f"dtype={dtype} ssm_cache_dtype={effective_state_dtype} atol={atol}"
370 )
371 print(f"{'=' * 74}")
372 print(f"{'EffBatch':>8} | {'MaxAbsErr':>12} | {'Status':>8}")
373 print("-" * 36)
374
375 results: dict[int, bool] = {}
376
377 for eb, batch, nheads in active:
378 cfg = tuned.get(eb)
379 if cfg is None:
380 continue
381 state, x, dt, A, B, C, D, dt_bias, out = _make_inputs(
382 batch=batch,
383 nheads=nheads,
384 dim=headdim,
385 dstate=dstate,
386 ngroups=ngroups,
387 dtype=dtype,
388 state_dtype=state_dtype,
389 )
390 # Clone state before GPU kernel modifies it in-place
391 state_ref = state.clone()
392
393 with override_ssm_config((cfg["BLOCK_SIZE_M"], cfg["num_warps"])):
394 selective_state_update(
395 state,
396 x,
397 dt,
398 A,
399 B,
400 C,
401 D=D,
402 z=None,
403 dt_bias=dt_bias,
404 dt_softplus=True,
405 out=out,
406 )
407 torch.accelerator.synchronize()
408 gpu_out = out.detach().cpu()
409
410 # Reference uses the original (unmodified) state
411 # Upcast to fp32 so the reference sums in fp32 (matches the Triton
412 # kernel); summing in bf16 over `dstate` blows up the error.
413 ref_out = (
414 selective_state_update_ref(
415 state_ref.float(),
416 x.float(),
417 dt.float(),
418 A.float(),
419 B.float(),
420 C.float(),
421 D=D.float(),
422 dt_bias=dt_bias.float(),
423 dt_softplus=True,
424 )
425 .to(out.dtype)
426 .cpu()
427 )
428
429 passed = torch.allclose(gpu_out.float(), ref_out.float(), atol=atol, rtol=rtol)
430 max_err = (gpu_out.float() - ref_out.float()).abs().max().item()
431 status = "PASS" if passed else "FAIL"
432 results[eb] = passed
433 print(f"{eb:>8} | {max_err:>12.6f} | {status:>8}")
434
435 n_pass = sum(results.values())
436 n_total = len(results)
437 print(f"\n {n_pass}/{n_total} configs passed validation for dstate={dstate}")
438 return results
439
440
441 # ---------------------------------------------------------------------------
442 # Save configs
443 # ---------------------------------------------------------------------------
444
445
446 def save_configs(
447 headdim: int,
448 dstate: int,
449 cache_dtype: str,
450 configs: dict[int, dict],
451 save_dir: str | None = None,
452 ) -> str:
453 # bf16 shares configs with fp16, use common filename for both
454 cache_dtype = _canonical_cache_dtype(cache_dtype)
455
456 base_dir = save_dir if save_dir else _CONFIGS_DIR
457 os.makedirs(base_dir, exist_ok=True)
458 file_path = os.path.join(
459 base_dir,
460 get_ssm_config_file_name(headdim, dstate, cache_dtype, get_ssm_device_name()),
461 )
462 # triton_version is informational only, the loader ignores it
463 payload: dict[str, Any] = {
464 "triton_version": triton.__version__,
465 **{str(k): v for k, v in sorted(configs.items())},
466 }
467 with open(file_path, "w") as f:
468 json.dump(payload, f, indent=4)
469 return file_path
470
471
472 # ---------------------------------------------------------------------------
473 # Comparison table
474 # ---------------------------------------------------------------------------
475
476
477 def current_heuristic(dstate: int, is_blackwell: bool = False) -> dict:
478 """Return the current hard-coded BLOCK_SIZE_M / num_warps for dstate."""
479 bsm, nw = _get_default_ssm_launch_config(dstate, is_blackwell)
480 return {"BLOCK_SIZE_M": bsm, "num_warps": nw}
481
482
483 def compare_heuristic_vs_tuned(
484 dstate: int,
485 headdim: int,
486 ngroups: int,
487 tuned: dict[int, dict],
488 timings: dict[int, dict[tuple[int, int], float]],
489 active: list[tuple[int, int, int]],
490 dtype: torch.dtype,
491 num_iters: int,
492 is_blackwell: bool,
493 state_dtype: torch.dtype | None = None,
494 ):
495 heur_cfg = current_heuristic(dstate, is_blackwell)
496 heur_key = (heur_cfg["BLOCK_SIZE_M"], heur_cfg["num_warps"])
497
498 print(f"\n{'=' * 74}")
499 print(
500 f"Comparison headdim={headdim} dstate={dstate} "
501 f"ngroups={ngroups} — heuristic vs tuned"
502 )
503 print(
504 f"Heuristic: BLOCK_SIZE_M={heur_cfg['BLOCK_SIZE_M']}, "
505 f"num_warps={heur_cfg['num_warps']}"
506 )
507 print(f"{'=' * 74}")
508 hdr = (
509 f"{'EffBatch':>8} | {'Heur(us)':>10} | {'Tuned(us)':>10} | "
510 f"{'Speedup':>8} | Best config"
511 )
512 print(hdr)
513 print("-" * len(hdr))
514
515 for eb, batch, nheads in active:
516 eb_timings = timings.get(eb, {})
517
518 # Heuristic timing: reuse the tuning measurement if the heuristic
519 # config was in the swept grid; otherwise measure it once.
520 t_h = eb_timings.get(heur_key)
521 if t_h is None:
522 t_h = benchmark_config(
523 batch=batch,
524 nheads=nheads,
525 dim=headdim,
526 dstate=dstate,
527 ngroups=ngroups,
528 block_size_m=heur_cfg["BLOCK_SIZE_M"],
529 num_warps_val=heur_cfg["num_warps"],
530 dtype=dtype,
531 state_dtype=state_dtype,
532 num_iters=num_iters,
533 )
534
535 # `tuned[eb]` may be missing if all configs failed in tune_dstate;
536 # in that case fall back to the heuristic so the table still prints.
537 best = tuned.get(eb) or heur_cfg
538 t_t = eb_timings.get((best["BLOCK_SIZE_M"], best["num_warps"]))
539
540 if t_h is None or t_t is None:
541 print(f"{eb:>8} | {'N/A':>10} | {'N/A':>10} | {'N/A':>8} |")
542 continue
543 speedup = t_h / t_t
544 marker = " <--" if speedup > 1.05 else ""
545 print(
546 f"{eb:>8} | {t_h:>10.2f} | {t_t:>10.2f} | "
547 f"{speedup:>7.2f}x | "
548 f"M={best['BLOCK_SIZE_M']},w={best['num_warps']}{marker}"
549 )
550
551
552 # ---------------------------------------------------------------------------
553 # CLI
554 # ---------------------------------------------------------------------------
555
556
557 def save_results(device_name: str, output: str, results_file: str | None = None) -> str:
558 """Save the full benchmark output to a results text file."""
559 if results_file is None:
560 results_file = os.path.join(
561 _RESULTS_DIR, f"ssm_benchmark_results_{device_name}.txt"
562 )
563 with open(results_file, "w") as f:
564 f.write(output)
565 return results_file
566
567
568 def main():
569 parser = argparse.ArgumentParser(
570 description="Tune selective_state_update kernel for Mamba SSM"
571 )
572 parser.add_argument(
573 "--dstate",
574 type=int,
575 default=128,
576 help="SSM state size to tune for (default: 128)",
577 )
578 parser.add_argument(
579 "--all-dstates",
580 action="store_true",
581 help="Tune all common dstate values: " + str(ALL_DSTATES),
582 )
583 parser.add_argument(
584 "--dtype",
585 type=str,
586 default="bfloat16",
587 choices=["float16", "bfloat16"],
588 help="Activation / input data type (default: bfloat16)",
589 )
590 parser.add_argument(
591 "--mamba-ssm-cache-dtype",
592 type=str,
593 default="float32",
594 choices=list(_SSM_CACHE_DTYPE_MAP.keys()),
595 help="SSM state cache dtype (default: float32)",
596 )
597 parser.add_argument(
598 "--num-iters",
599 type=int,
600 default=100,
601 help="Number of timing iterations (default: 100)",
602 )
603 parser.add_argument(
604 "--save-configs",
605 action="store_true",
606 help=f"Save best configs to JSON in {_CONFIGS_DIR}",
607 )
608 parser.add_argument(
609 "--compare",
610 action="store_true",
611 help="Show comparison table: heuristic vs tuned",
612 )
613 parser.add_argument(
614 "--verbose",
615 action="store_true",
616 help="Print every (BLOCK_SIZE_M, num_warps) result, not just best",
617 )
618 parser.add_argument(
619 "--results-file",
620 type=str,
621 default=None,
622 help="Path to save the benchmark results text file "
623 "(default: ssm_benchmark_results_<device>.txt alongside this script)",
624 )
625 parser.add_argument(
626 "--save-dir",
627 type=str,
628 default=None,
629 help=f"Directory to save JSON configs (default: {_CONFIGS_DIR})",
630 )
631 parser.add_argument(
632 "--headdim",
633 type=int,
634 default=DEFAULT_HEADDIM,
635 help=f"Per-head feature dim (default: {DEFAULT_HEADDIM})",
636 )
637 parser.add_argument(
638 "--ngroups",
639 type=int,
640 default=DEFAULT_NGROUPS,
641 help=f"Number of B/C groups (default: {DEFAULT_NGROUPS})",
642 )
643 parser.add_argument(
644 "--batch-sizes",
645 type=int,
646 nargs="+",
647 default=DEFAULT_BATCH_SIZES,
648 metavar="B",
649 help=f"Decoder batch sizes to sweep (default: {DEFAULT_BATCH_SIZES})",
650 )
651 parser.add_argument(
652 "--nheads",
653 type=int,
654 nargs="+",
655 default=DEFAULT_NHEADS,
656 metavar="N",
657 help=f"Number of heads per rank to sweep (default: {DEFAULT_NHEADS}). "
658 "effective_batch = batch * nheads; cross-product is deduped by eb.",
659 )
660 parser.add_argument(
661 "--validate",
662 action="store_true",
663 help="After tuning, verify each best config against a CPU reference "
664 "implementation. Configs that fail are flagged in the output.",
665 )
666 parser.add_argument(
667 "--atol",
668 type=float,
669 default=1e-2,
670 help="Absolute tolerance for --validate (default: 1e-2)",
671 )
672 args = parser.parse_args()
673
674 dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
675 state_dtype = _SSM_CACHE_DTYPE_MAP[args.mamba_ssm_cache_dtype]
676 device_name = get_ssm_device_name()
677 cap = current_platform.get_device_capability()
678 is_blackwell = cap is not None and cap[0] >= 10
679
680 # Mirror all output to a results file (like Unix tee).
681 buf = StringIO()
682
683 class _Tee:
684 """Writes to both the original stdout and an in-memory buffer."""
685
686 def write(self, s):
687 buf.write(s)
688 sys.__stdout__.write(s)
689
690 def flush(self):
691 sys.__stdout__.flush()
692
693 sys.stdout = _Tee() # type: ignore[assignment]
694
695 try:
696 cap_str = f"sm_{cap[0]}{cap[1]}" if cap is not None else "n/a"
697 print(f"Device : {device_name} ({cap_str})")
698 print(f"Blackwell: {is_blackwell}")
699 print(f"dtype : {args.dtype}")
700 print(f"ssm_cache_dtype: {args.mamba_ssm_cache_dtype}")
701 print(f"headdim: {args.headdim}")
702 print(f"ngroups: {args.ngroups}")
703 print(f"triton : {triton.__version__}")
704
705 dstates = ALL_DSTATES if args.all_dstates else [args.dstate]
706 active = expand_batch_x_nheads(args.batch_sizes, args.nheads, args.ngroups)
707
708 for dstate in dstates:
709 tuned, timings = tune_dstate(
710 dstate=dstate,
711 headdim=args.headdim,
712 ngroups=args.ngroups,
713 dtype=dtype,
714 num_iters=args.num_iters,
715 verbose=args.verbose,
716 active=active,
717 state_dtype=state_dtype,
718 )
719
720 if args.compare:
721 compare_heuristic_vs_tuned(
722 dstate=dstate,
723 headdim=args.headdim,
724 ngroups=args.ngroups,
725 tuned=tuned,
726 timings=timings,
727 active=active,
728 dtype=dtype,
729 num_iters=args.num_iters,
730 is_blackwell=is_blackwell,
731 state_dtype=state_dtype,
732 )
733
734 if args.validate:
735 validity = validate_configs(
736 dstate=dstate,
737 headdim=args.headdim,
738 ngroups=args.ngroups,
739 tuned=tuned,
740 active=active,
741 dtype=dtype,
742 atol=args.atol,
743 state_dtype=state_dtype,
744 )
745 # Filter out any configs that failed correctness check
746 failed = [eb for eb, ok in validity.items() if not ok]
747 if failed:
748 print(
749 f"\n WARNING: {len(failed)} config(s) failed validation "
750 f"for dstate={dstate}: effective_batches {failed}"
751 )
752 print(" These will NOT be saved even with --save-configs.")
753 tuned = {
754 eb: cfg for eb, cfg in tuned.items() if validity.get(eb, True)
755 }
756
757 if args.save_configs:
758 path = save_configs(
759 headdim=args.headdim,
760 dstate=dstate,
761 cache_dtype=args.mamba_ssm_cache_dtype,
762 configs=tuned,
763 save_dir=args.save_dir,
764 )
765 print(f"\nSaved: {path}")
766 else:
767 print(f"\nBest configs for dstate={dstate}:")
768 for eb, cfg in sorted(tuned.items()):
769 print(f" effective_batch={eb:>6}: {cfg}")
770 print("\n(Re-run with --save-configs to persist to JSON)")
771 finally:
772 sys.stdout = sys.__stdout__
773 results_path = save_results(device_name, buf.getvalue(), args.results_file)
774 print(f"\nResults saved to: {results_path}")
775
776
777 if __name__ == "__main__":
778 main()