#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Benchmark and tuning script for the Mamba selective_state_update kernel.

Mirrors the fused MoE tuning workflow: sweeps (BLOCK_SIZE_M, num_warps) across
an effective_batch grid for a given (headdim, dstate, ngroups, cache_dtype) and
saves the best config per effective_batch to JSON. Generated configs are picked
up by selective_state_update at runtime.

Usage:
    python -m benchmarks.kernels.benchmark_selective_state_update \
        --all-dstates --save-configs --compare
"""

import argparse
import json
import os
import sys
from io import StringIO
from itertools import product
from typing import Any

import torch

from tests.kernels.mamba.utils import selective_state_update_ref
from vllm.model_executor.layers.mamba.ops.mamba_ssm import (
    _CONFIGS_DIR,
    _canonical_cache_dtype,
    _get_default_ssm_launch_config,
    get_ssm_config_file_name,
    get_ssm_device_name,
    override_ssm_config,
    selective_state_update,
)
from vllm.triton_utils import triton

# bf16 shares configs with fp16 - same bit width.
_SSM_CACHE_DTYPE_MAP: dict[str, torch.dtype] = {
    "float32": torch.float32,
    "float16": torch.float16,
    "bfloat16": torch.float16,
}

_RESULTS_DIR = os.path.dirname(os.path.realpath(__file__))

# ---------------------------------------------------------------------------
# Tuning search space
# ---------------------------------------------------------------------------

_BSM_CHOICES_ALL = [4, 8, 16, 32, 64, 128, 256]

NUM_WARPS_CHOICES = [1, 2, 4, 8]


def _block_size_m_choices(headdim: int) -> list[int]:
    """BLOCK_SIZE_M candidates worth sweeping for a given headdim.

    BLOCK_SIZE_M > next_pow2(headdim) wastes >=50% of each tile via masking
    (offs_m >= dim rows are zeroed out), so we cap the sweep there.
    """
    ceiling = 1
    while ceiling < headdim:
        ceiling <<= 1
    return [b for b in _BSM_CHOICES_ALL if b <= ceiling]


# Default deployment shapes. effective_batch = batch * nheads scales the
# kernel grid, so configs transfer across (model, TP) combos sharing
# (headdim, dstate, cache_dtype).
DEFAULT_BATCH_SIZES = [1, 8, 16, 32, 64, 128, 256, 512, 1024, 1536, 2048]
DEFAULT_NHEADS = [128, 256]

ALL_DSTATES = [16, 32, 64, 128, 256]

# Default tuning shape — matches Nemotron-3-Super and Nemotron-3-Nano Mamba layers.
# Override with CLI flags for other architectures.
DEFAULT_HEADDIM = 64
DEFAULT_NGROUPS = 8


# ---------------------------------------------------------------------------
# Benchmark helper
# ---------------------------------------------------------------------------


def _make_inputs(
    batch: int,
    nheads: int,
    dim: int,
    dstate: int,
    ngroups: int,
    dtype: torch.dtype,
    state_dtype: torch.dtype | None = None,
    device: str = "cuda",
):
    if state_dtype is None:
        state_dtype = dtype
    state = torch.randn(batch, nheads, dim, dstate, dtype=state_dtype, device=device)
    x = torch.randn(batch, nheads, dim, dtype=dtype, device=device)
    dt = torch.randn(batch, nheads, dim, dtype=dtype, device=device)
    A = -torch.rand(nheads, dim, dstate, dtype=torch.float32, device=device)
    B = torch.randn(batch, ngroups, dstate, dtype=dtype, device=device)
    C = torch.randn(batch, ngroups, dstate, dtype=dtype, device=device)
    D = torch.randn(nheads, dim, dtype=dtype, device=device)
    dt_bias = torch.randn(nheads, dim, dtype=dtype, device=device)
    out = torch.zeros(batch, nheads, dim, dtype=dtype, device=device)
    return state, x, dt, A, B, C, D, dt_bias, out


def benchmark_config(
    batch: int,
    nheads: int,
    dim: int,
    dstate: int,
    ngroups: int,
    block_size_m: int,
    num_warps_val: int,
    dtype: torch.dtype,
    state_dtype: torch.dtype | None = None,
    num_iters: int = 100,
    num_warmup: int = 20,
    graph_batch_size: int = 10,
) -> float | None:
    """
    Time one (BLOCK_SIZE_M, num_warps) config for selective_state_update.
    Returns elapsed time in microseconds, or None on error.

    Uses CUDA graph capture-and-replay to isolate kernel time from Python
    eager-mode dispatch / kwarg-resolution overhead, mirroring the timing
    methodology in benchmarks/kernels/benchmark_moe.py.
    """
    state, x, dt, A, B, C, D, dt_bias, out = _make_inputs(
        batch, nheads, dim, dstate, ngroups, dtype, state_dtype=state_dtype
    )

    def _call_kernel() -> None:
        selective_state_update(
            state,
            x,
            dt,
            A,
            B,
            C,
            D=D,
            z=None,
            dt_bias=dt_bias,
            dt_softplus=True,
            out=out,
        )

    try:
        with override_ssm_config((block_size_m, num_warps_val)):
            # Eager-mode warmup: triggers Triton autotune / JIT, primes caches.
            for _ in range(num_warmup):
                _call_kernel()
            torch.accelerator.synchronize()

            # Capture graph_batch_size invocations into a CUDA graph so the
            # timed region runs without Python dispatch overhead per call.
            graph = torch.cuda.CUDAGraph()
            with torch.cuda.graph(graph):
                for _ in range(graph_batch_size):
                    _call_kernel()
            torch.accelerator.synchronize()

            # Warmup graph replays (let the runtime stabilize).
            for _ in range(5):
                graph.replay()
            torch.accelerator.synchronize()

            start = torch.cuda.Event(enable_timing=True)
            end = torch.cuda.Event(enable_timing=True)
            latencies: list[float] = []
            for _ in range(num_iters):
                start.record()
                graph.replay()
                end.record()
                end.synchronize()
                latencies.append(start.elapsed_time(end))
            graph.reset()
        # elapsed_time returns ms; each replay runs graph_batch_size kernels,
        # so divide by (num_iters * graph_batch_size) and convert ms -> us.
        return sum(latencies) / (num_iters * graph_batch_size) * 1000
    except Exception as e:
        if "OutOfResources" not in str(e):
            print(
                f"    Warning: config M={block_size_m},w={num_warps_val} "
                f"raised {type(e).__name__}: {e}"
            )
        return None


# ---------------------------------------------------------------------------
# Tuning loop
# ---------------------------------------------------------------------------


# CUDA grid Y/Z dim limit — both `batch` and `nheads` must fit individually.
_CUDA_MAX_GRID_DIM = 65535

# Above this, kernel state-offset arithmetic (batch * nheads * headdim * dstate)
# overflows int32 and the launch raises cudaErrorIllegalAddress.
# 262144 covers Nemotron Super TP1 BS=2048.
_MAX_EFFECTIVE_BATCH = 262144


def expand_batch_x_nheads(
    batch_sizes: list[int],
    nheads_list: list[int],
    ngroups: int,
) -> list[tuple[int, int, int]]:
    """Cross-product batch_sizes × nheads_list → sorted [(effective_batch,
    batch, nheads)], deduped by effective_batch. Filters pairs that exceed
    the CUDA grid dim limit, the effective_batch ceiling, or where nheads is
    not a positive multiple of ngroups.
    """
    seen: dict[int, tuple[int, int]] = {}
    skipped_grid: list[tuple[int, int]] = []
    skipped_ngroups: list[tuple[int, int]] = []
    skipped_eb: list[tuple[int, int]] = []
    for b, n in product(batch_sizes, nheads_list):
        if b <= 0 or n <= 0:
            continue
        if b > _CUDA_MAX_GRID_DIM or n > _CUDA_MAX_GRID_DIM:
            skipped_grid.append((b, n))
            continue
        if n % ngroups != 0:
            skipped_ngroups.append((b, n))
            continue
        if b * n > _MAX_EFFECTIVE_BATCH:
            skipped_eb.append((b, n))
            continue
        seen.setdefault(b * n, (b, n))
    if skipped_grid:
        print(
            f"  Note: skipping (batch, nheads) pairs exceeding CUDA grid dim "
            f"{_CUDA_MAX_GRID_DIM}: {skipped_grid}"
        )
    if skipped_ngroups:
        print(
            f"  Note: skipping (batch, nheads) pairs where nheads % ngroups != 0 "
            f"for ngroups={ngroups}: {skipped_ngroups}"
        )
    if skipped_eb:
        print(
            f"  Note: skipping (batch, nheads) pairs whose effective_batch "
            f"exceeds {_MAX_EFFECTIVE_BATCH}: {skipped_eb}"
        )
    return sorted((eb, b, n) for eb, (b, n) in seen.items())


def tune_dstate(
    dstate: int,
    headdim: int,
    ngroups: int,
    dtype: torch.dtype,
    num_iters: int,
    verbose: bool,
    active: list[tuple[int, int, int]],
    state_dtype: torch.dtype | None = None,
) -> tuple[dict[int, dict], dict[int, dict[tuple[int, int], float]]]:
    """For each (effective_batch, batch, nheads) in *active*, sweep
    (BLOCK_SIZE_M, num_warps) and return
    ({effective_batch: best_config}, {effective_batch: {(bsm, nw): us}}).
    The second map is the full timing grid, used downstream so we don't
    re-measure the same config in the comparison phase.
    """
    best_per_eb: dict[int, dict] = {}
    timings: dict[int, dict[tuple[int, int], float]] = {}

    print(f"\n{'=' * 74}")
    effective_state_dtype = state_dtype if state_dtype is not None else dtype
    print(
        f"Tuning  headdim={headdim}  dstate={dstate}  ngroups={ngroups}  "
        f"dtype={dtype}  ssm_cache_dtype={effective_state_dtype}"
    )
    print(f"{'=' * 74}")

    bsm_choices = _block_size_m_choices(headdim)
    print(f"BSM candidates (capped at next_pow2(headdim={headdim})): {bsm_choices}")

    hdr = f"{'EffBatch':>8} | {'BLOCK_M':>7} | {'warps':>5} | {'us':>10} | note"
    print(hdr)
    print("-" * 52)

    for eb, batch, nheads in active:
        best_time = float("inf")
        best_cfg: dict = {}
        eb_timings: dict[tuple[int, int], float] = {}

        for bsm, nw in product(bsm_choices, NUM_WARPS_CHOICES):
            t = benchmark_config(
                batch=batch,
                nheads=nheads,
                dim=headdim,
                dstate=dstate,
                ngroups=ngroups,
                block_size_m=bsm,
                num_warps_val=nw,
                dtype=dtype,
                state_dtype=state_dtype,
                num_iters=num_iters,
            )
            if t is None:
                continue
            eb_timings[(bsm, nw)] = t
            is_best = t < best_time
            if is_best:
                best_time = t
                best_cfg = {"BLOCK_SIZE_M": bsm, "num_warps": nw}
            if verbose:
                marker = " <-- best" if is_best else ""
                print(f"{eb:>8} | {bsm:>7} | {nw:>5} | {t:>10.2f} |{marker}")

        timings[eb] = eb_timings

        if not best_cfg:
            print(
                f"{eb:>8} | {'-':>7} | {'-':>5} | {'-':>10} | "
                f"no working config (skipped)"
            )
            continue

        if not verbose:
            print(
                f"{eb:>8} | {best_cfg['BLOCK_SIZE_M']:>7} | "
                f"{best_cfg['num_warps']:>5} | {best_time:>10.2f} | best"
            )

        best_per_eb[eb] = best_cfg

    return best_per_eb, timings


# ---------------------------------------------------------------------------
# Correctness validation
# ---------------------------------------------------------------------------


def validate_configs(
    dstate: int,
    headdim: int,
    ngroups: int,
    tuned: dict[int, dict],
    active: list[tuple[int, int, int]],
    dtype: torch.dtype,
    atol: float = 1e-2,
    rtol: float = 1e-2,
    state_dtype: torch.dtype | None = None,
) -> dict[int, bool]:
    """
    For every (effective_batch, batch, nheads) in *active* that has a tuned
    config, run the kernel with that config and compare against the reference.
    Returns {effective_batch: passed}.
    """
    # Disable TF32 so the reference's matmul matches the Triton kernel's
    # fp32 accumulation; otherwise large ebs show bf16 rounding mismatches.
    torch.set_float32_matmul_precision("highest")

    print(f"\n{'=' * 74}")
    effective_state_dtype = state_dtype if state_dtype is not None else dtype
    print(
        f"Validation  headdim={headdim}  dstate={dstate}  ngroups={ngroups}  "
        f"dtype={dtype}  ssm_cache_dtype={effective_state_dtype}  atol={atol}"
    )
    print(f"{'=' * 74}")
    print(f"{'EffBatch':>8} | {'MaxAbsErr':>12} | {'Status':>8}")
    print("-" * 36)

    results: dict[int, bool] = {}

    for eb, batch, nheads in active:
        cfg = tuned.get(eb)
        if cfg is None:
            continue
        state, x, dt, A, B, C, D, dt_bias, out = _make_inputs(
            batch=batch,
            nheads=nheads,
            dim=headdim,
            dstate=dstate,
            ngroups=ngroups,
            dtype=dtype,
            state_dtype=state_dtype,
        )
        # Clone state before GPU kernel modifies it in-place
        state_ref = state.clone()

        with override_ssm_config((cfg["BLOCK_SIZE_M"], cfg["num_warps"])):
            selective_state_update(
                state,
                x,
                dt,
                A,
                B,
                C,
                D=D,
                z=None,
                dt_bias=dt_bias,
                dt_softplus=True,
                out=out,
            )
        torch.accelerator.synchronize()
        gpu_out = out.detach().cpu()

        # Reference uses the original (unmodified) state
        # Upcast to fp32 so the reference sums in fp32 (matches the Triton
        # kernel); summing in bf16 over `dstate` blows up the error.
        ref_out = (
            selective_state_update_ref(
                state_ref.float(),
                x.float(),
                dt.float(),
                A.float(),
                B.float(),
                C.float(),
                D=D.float(),
                dt_bias=dt_bias.float(),
                dt_softplus=True,
            )
            .to(out.dtype)
            .cpu()
        )

        passed = torch.allclose(gpu_out.float(), ref_out.float(), atol=atol, rtol=rtol)
        max_err = (gpu_out.float() - ref_out.float()).abs().max().item()
        status = "PASS" if passed else "FAIL"
        results[eb] = passed
        print(f"{eb:>8} | {max_err:>12.6f} | {status:>8}")

    n_pass = sum(results.values())
    n_total = len(results)
    print(f"\n  {n_pass}/{n_total} configs passed validation for dstate={dstate}")
    return results


# ---------------------------------------------------------------------------
# Save configs
# ---------------------------------------------------------------------------


def save_configs(
    headdim: int,
    dstate: int,
    cache_dtype: str,
    configs: dict[int, dict],
    save_dir: str | None = None,
) -> str:
    # bf16 shares configs with fp16, use common filename for both
    cache_dtype = _canonical_cache_dtype(cache_dtype)

    base_dir = save_dir if save_dir else _CONFIGS_DIR
    os.makedirs(base_dir, exist_ok=True)
    file_path = os.path.join(
        base_dir,
        get_ssm_config_file_name(headdim, dstate, cache_dtype, get_ssm_device_name()),
    )
    # triton_version is informational only, the loader ignores it
    payload: dict[str, Any] = {
        "triton_version": triton.__version__,
        **{str(k): v for k, v in sorted(configs.items())},
    }
    with open(file_path, "w") as f:
        json.dump(payload, f, indent=4)
    return file_path


# ---------------------------------------------------------------------------
# Comparison table
# ---------------------------------------------------------------------------


def current_heuristic(dstate: int, is_blackwell: bool = False) -> dict:
    """Return the current hard-coded BLOCK_SIZE_M / num_warps for dstate."""
    bsm, nw = _get_default_ssm_launch_config(dstate, is_blackwell)
    return {"BLOCK_SIZE_M": bsm, "num_warps": nw}


def compare_heuristic_vs_tuned(
    dstate: int,
    headdim: int,
    ngroups: int,
    tuned: dict[int, dict],
    timings: dict[int, dict[tuple[int, int], float]],
    active: list[tuple[int, int, int]],
    dtype: torch.dtype,
    num_iters: int,
    is_blackwell: bool,
    state_dtype: torch.dtype | None = None,
):
    heur_cfg = current_heuristic(dstate, is_blackwell)
    heur_key = (heur_cfg["BLOCK_SIZE_M"], heur_cfg["num_warps"])

    print(f"\n{'=' * 74}")
    print(
        f"Comparison  headdim={headdim}  dstate={dstate}  "
        f"ngroups={ngroups}  —  heuristic vs tuned"
    )
    print(
        f"Heuristic: BLOCK_SIZE_M={heur_cfg['BLOCK_SIZE_M']}, "
        f"num_warps={heur_cfg['num_warps']}"
    )
    print(f"{'=' * 74}")
    hdr = (
        f"{'EffBatch':>8} | {'Heur(us)':>10} | {'Tuned(us)':>10} | "
        f"{'Speedup':>8} | Best config"
    )
    print(hdr)
    print("-" * len(hdr))

    for eb, batch, nheads in active:
        eb_timings = timings.get(eb, {})

        # Heuristic timing: reuse the tuning measurement if the heuristic
        # config was in the swept grid; otherwise measure it once.
        t_h = eb_timings.get(heur_key)
        if t_h is None:
            t_h = benchmark_config(
                batch=batch,
                nheads=nheads,
                dim=headdim,
                dstate=dstate,
                ngroups=ngroups,
                block_size_m=heur_cfg["BLOCK_SIZE_M"],
                num_warps_val=heur_cfg["num_warps"],
                dtype=dtype,
                state_dtype=state_dtype,
                num_iters=num_iters,
            )

        # `tuned[eb]` may be missing if all configs failed in tune_dstate;
        # in that case fall back to the heuristic so the table still prints.
        best = tuned.get(eb) or heur_cfg
        t_t = eb_timings.get((best["BLOCK_SIZE_M"], best["num_warps"]))

        if t_h is None or t_t is None:
            print(f"{eb:>8} | {'N/A':>10} | {'N/A':>10} | {'N/A':>8} |")
            continue
        speedup = t_h / t_t
        marker = " <--" if speedup > 1.05 else ""
        print(
            f"{eb:>8} | {t_h:>10.2f} | {t_t:>10.2f} | "
            f"{speedup:>7.2f}x | "
            f"M={best['BLOCK_SIZE_M']},w={best['num_warps']}{marker}"
        )


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------


def save_results(device_name: str, output: str, results_file: str | None = None) -> str:
    """Save the full benchmark output to a results text file."""
    if results_file is None:
        results_file = os.path.join(
            _RESULTS_DIR, f"ssm_benchmark_results_{device_name}.txt"
        )
    with open(results_file, "w") as f:
        f.write(output)
    return results_file


def main():
    parser = argparse.ArgumentParser(
        description="Tune selective_state_update kernel for Mamba SSM"
    )
    parser.add_argument(
        "--dstate",
        type=int,
        default=128,
        help="SSM state size to tune for (default: 128)",
    )
    parser.add_argument(
        "--all-dstates",
        action="store_true",
        help="Tune all common dstate values: " + str(ALL_DSTATES),
    )
    parser.add_argument(
        "--dtype",
        type=str,
        default="bfloat16",
        choices=["float16", "bfloat16"],
        help="Activation / input data type (default: bfloat16)",
    )
    parser.add_argument(
        "--mamba-ssm-cache-dtype",
        type=str,
        default="float32",
        choices=list(_SSM_CACHE_DTYPE_MAP.keys()),
        help="SSM state cache dtype (default: float32)",
    )
    parser.add_argument(
        "--num-iters",
        type=int,
        default=100,
        help="Number of timing iterations (default: 100)",
    )
    parser.add_argument(
        "--save-configs",
        action="store_true",
        help=f"Save best configs to JSON in {_CONFIGS_DIR}",
    )
    parser.add_argument(
        "--compare",
        action="store_true",
        help="Show comparison table: heuristic vs tuned",
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Print every (BLOCK_SIZE_M, num_warps) result, not just best",
    )
    parser.add_argument(
        "--results-file",
        type=str,
        default=None,
        help="Path to save the benchmark results text file "
        "(default: ssm_benchmark_results_<device>.txt alongside this script)",
    )
    parser.add_argument(
        "--save-dir",
        type=str,
        default=None,
        help=f"Directory to save JSON configs (default: {_CONFIGS_DIR})",
    )
    parser.add_argument(
        "--headdim",
        type=int,
        default=DEFAULT_HEADDIM,
        help=f"Per-head feature dim (default: {DEFAULT_HEADDIM})",
    )
    parser.add_argument(
        "--ngroups",
        type=int,
        default=DEFAULT_NGROUPS,
        help=f"Number of B/C groups (default: {DEFAULT_NGROUPS})",
    )
    parser.add_argument(
        "--batch-sizes",
        type=int,
        nargs="+",
        default=DEFAULT_BATCH_SIZES,
        metavar="B",
        help=f"Decoder batch sizes to sweep (default: {DEFAULT_BATCH_SIZES})",
    )
    parser.add_argument(
        "--nheads",
        type=int,
        nargs="+",
        default=DEFAULT_NHEADS,
        metavar="N",
        help=f"Number of heads per rank to sweep (default: {DEFAULT_NHEADS}). "
        "effective_batch = batch * nheads; cross-product is deduped by eb.",
    )
    parser.add_argument(
        "--validate",
        action="store_true",
        help="After tuning, verify each best config against a CPU reference "
        "implementation. Configs that fail are flagged in the output.",
    )
    parser.add_argument(
        "--atol",
        type=float,
        default=1e-2,
        help="Absolute tolerance for --validate (default: 1e-2)",
    )
    args = parser.parse_args()

    dtype = torch.bfloat16 if args.dtype == "bfloat16" else torch.float16
    state_dtype = _SSM_CACHE_DTYPE_MAP[args.mamba_ssm_cache_dtype]
    device_name = get_ssm_device_name()
    cap = torch.cuda.get_device_capability()
    is_blackwell = cap[0] >= 10

    # Mirror all output to a results file (like Unix tee).
    buf = StringIO()

    class _Tee:
        """Writes to both the original stdout and an in-memory buffer."""

        def write(self, s):
            buf.write(s)
            sys.__stdout__.write(s)

        def flush(self):
            sys.__stdout__.flush()

    sys.stdout = _Tee()  # type: ignore[assignment]

    try:
        print(f"Device : {device_name}  (sm_{cap[0]}{cap[1]})")
        print(f"Blackwell: {is_blackwell}")
        print(f"dtype  : {args.dtype}")
        print(f"ssm_cache_dtype: {args.mamba_ssm_cache_dtype}")
        print(f"headdim: {args.headdim}")
        print(f"ngroups: {args.ngroups}")
        print(f"triton : {triton.__version__}")

        dstates = ALL_DSTATES if args.all_dstates else [args.dstate]
        active = expand_batch_x_nheads(args.batch_sizes, args.nheads, args.ngroups)

        for dstate in dstates:
            tuned, timings = tune_dstate(
                dstate=dstate,
                headdim=args.headdim,
                ngroups=args.ngroups,
                dtype=dtype,
                num_iters=args.num_iters,
                verbose=args.verbose,
                active=active,
                state_dtype=state_dtype,
            )

            if args.compare:
                compare_heuristic_vs_tuned(
                    dstate=dstate,
                    headdim=args.headdim,
                    ngroups=args.ngroups,
                    tuned=tuned,
                    timings=timings,
                    active=active,
                    dtype=dtype,
                    num_iters=args.num_iters,
                    is_blackwell=is_blackwell,
                    state_dtype=state_dtype,
                )

            if args.validate:
                validity = validate_configs(
                    dstate=dstate,
                    headdim=args.headdim,
                    ngroups=args.ngroups,
                    tuned=tuned,
                    active=active,
                    dtype=dtype,
                    atol=args.atol,
                    state_dtype=state_dtype,
                )
                # Filter out any configs that failed correctness check
                failed = [eb for eb, ok in validity.items() if not ok]
                if failed:
                    print(
                        f"\n  WARNING: {len(failed)} config(s) failed validation "
                        f"for dstate={dstate}: effective_batches {failed}"
                    )
                    print("  These will NOT be saved even with --save-configs.")
                    tuned = {
                        eb: cfg for eb, cfg in tuned.items() if validity.get(eb, True)
                    }

            if args.save_configs:
                path = save_configs(
                    headdim=args.headdim,
                    dstate=dstate,
                    cache_dtype=args.mamba_ssm_cache_dtype,
                    configs=tuned,
                    save_dir=args.save_dir,
                )
                print(f"\nSaved: {path}")
            else:
                print(f"\nBest configs for dstate={dstate}:")
                for eb, cfg in sorted(tuned.items()):
                    print(f"  effective_batch={eb:>6}: {cfg}")
                print("\n(Re-run with --save-configs to persist to JSON)")
    finally:
        sys.stdout = sys.__stdout__
        results_path = save_results(device_name, buf.getvalue(), args.results_file)
        print(f"\nResults saved to: {results_path}")


if __name__ == "__main__":
    main()
