"""
Benchmark: Fused Gate+Cumsum vs Separate Gate + Cumsum.

Compares two paths:
  - Separate: torch gate activation -> chunk_local_cumsum (2 steps)
  - Fused:    kda_gate_chunk_cumsum (single kernel)

Both produce the same output: cumsum of gate-activated g.

Usage:
    python bench_fused_gate_cumsum.py
    python bench_fused_gate_cumsum.py --batch-sizes 4 16 64 128
    python bench_fused_gate_cumsum.py --seq-lens 64 128 256 512 1024
"""

import argparse
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python"))

import torch
import triton

from sglang.kernels.ops.attention.fla.cumsum import chunk_local_cumsum
from sglang.kernels.ops.attention.fla.index import prepare_chunk_indices
from sglang.kernels.ops.attention.fla.kda import kda_gate_chunk_cumsum

CHUNK_SIZE = 64


def make_inputs(
    B: int,
    T_per_seq: int,
    H: int,
    K: int,
    device: str,
    dtype: torch.dtype,
    seed: int = 42,
):
    T = B * T_per_seq
    torch.manual_seed(seed)

    # Raw gate: [1, T_total, H, K] (varlen format, before activation)
    raw_g = torch.randn(1, T, H, K, dtype=dtype, device=device)

    # A_log: [H] (per-head log-scale parameter)
    A_log = torch.randn(H, dtype=torch.float32, device=device) * 0.5

    # dt_bias: [H*K] (per-head bias, flat)
    dt_bias = torch.randn(H * K, dtype=torch.float32, device=device) * 0.1

    # cu_seqlens for varlen mode
    cu_seqlens = torch.arange(
        0, (B + 1) * T_per_seq, T_per_seq, dtype=torch.long, device=device
    )

    return dict(
        raw_g=raw_g,
        A_log=A_log,
        dt_bias=dt_bias,
        cu_seqlens=cu_seqlens,
        B=B,
        T=T,
        T_per_seq=T_per_seq,
        H=H,
        K=K,
    )


def run_ref(inp):
    """Separate path: torch gate activation -> chunk_local_cumsum."""
    raw_g = inp["raw_g"]  # [1, T, H, K]
    A_log = inp["A_log"]  # [H]
    dt_bias = inp["dt_bias"]  # [H*K]
    cu_seqlens = inp["cu_seqlens"]
    H, K = inp["H"], inp["K"]

    # Step 1: gate activation using torch ops
    g_float = raw_g.float()
    if dt_bias is not None:
        g_float = g_float + dt_bias.float().view(1, 1, H, K)
    g_activated = -torch.exp(
        A_log.float().view(1, 1, H, 1)
    ) * torch.nn.functional.softplus(g_float)

    # Step 2: chunk-local cumsum
    chunk_indices = prepare_chunk_indices(cu_seqlens, CHUNK_SIZE)
    g_cumsum = chunk_local_cumsum(
        g_activated,
        chunk_size=CHUNK_SIZE,
        cu_seqlens=cu_seqlens,
        chunk_indices=chunk_indices,
    )
    return g_cumsum


def run_fused(inp):
    """Fused path: kda_gate_chunk_cumsum (single kernel)."""
    raw_g = inp["raw_g"]
    A_log = inp["A_log"]
    dt_bias = inp["dt_bias"]
    cu_seqlens = inp["cu_seqlens"]

    chunk_indices = prepare_chunk_indices(cu_seqlens, CHUNK_SIZE)
    g_cumsum = kda_gate_chunk_cumsum(
        raw_g,
        A_log=A_log,
        chunk_size=CHUNK_SIZE,
        dt_bias=dt_bias,
        cu_seqlens=cu_seqlens,
        chunk_indices=chunk_indices,
    )
    return g_cumsum


def verify_correctness(inp):
    """Verify fused and separate paths produce the same output."""
    out_separate = run_ref(inp)
    out_fused = run_fused(inp)

    max_diff = (out_separate - out_fused).abs().max().item()
    rel_diff = max_diff / (out_separate.abs().mean().item() + 1e-8)
    return max_diff, rel_diff


def bench_shape(B, H, T_per_seq, K, device, dtype):
    T = B * T_per_seq
    inp = make_inputs(B, T_per_seq, H, K, device, dtype)

    # Warmup (includes triton compilation)
    for _ in range(5):
        run_ref(inp)
        run_fused(inp)
    torch.cuda.synchronize()

    ms_sep, ms_sep_lo, ms_sep_hi = triton.testing.do_bench(
        lambda: run_ref(inp), quantiles=[0.5, 0.2, 0.8], warmup=50, rep=200
    )
    ms_fused, ms_fused_lo, ms_fused_hi = triton.testing.do_bench(
        lambda: run_fused(inp), quantiles=[0.5, 0.2, 0.8], warmup=50, rep=200
    )

    speedup = ms_sep / ms_fused if ms_fused > 0 else 0
    saved_us = (ms_sep - ms_fused) * 1000  # microseconds

    print(
        f"  {B:>5}  {H:>3}  {T_per_seq:>6}  {T:>7} | "
        f"{ms_sep:>8.3f}  {ms_fused:>8.3f} | "
        f"{speedup:>6.2f}x  {saved_us:>+8.1f}us"
    )


def main():
    parser = argparse.ArgumentParser(
        description="Benchmark: Fused vs Separate Gate+Cumsum"
    )
    parser.add_argument("--dtype", choices=["bfloat16", "float16"], default="bfloat16")
    parser.add_argument("--head-size-k", type=int, default=128)
    parser.add_argument("--num-heads", type=int, nargs="+", default=[16])
    parser.add_argument(
        "--batch-sizes", type=int, nargs="+", default=[4, 8, 16, 32, 64, 128]
    )
    parser.add_argument(
        "--seq-lens", type=int, nargs="+", default=[64, 128, 256, 512, 1024]
    )
    args = parser.parse_args()

    device = "cuda"
    dtype = getattr(torch, args.dtype)
    K = args.head_size_k

    cap = torch.cuda.get_device_capability()
    dev_name = torch.cuda.get_device_name()
    print(f"Device: {dev_name}  (SM {cap[0]}{cap[1]})")
    print()

    # Correctness check
    print("=" * 80)
    print("Correctness verification")
    print("=" * 80)
    for H in args.num_heads:
        inp = make_inputs(16, 256, H, K, device, dtype)
        max_diff, rel_diff = verify_correctness(inp)
        print(
            f"  H={H:>3}, B=16, T/seq=256: "
            f"max_diff={max_diff:.2e}, rel_diff={rel_diff:.2e}  "
            f"{'PASS' if max_diff < 1e-3 else 'FAIL'}"
        )
    print()

    # Performance benchmark
    print("=" * 80)
    print("Performance: Separate (gate+cumsum) vs Fused (single kernel)")
    print("=" * 80)
    print(f"  Config: K={K}, chunk_size={CHUNK_SIZE}, dtype={dtype}")
    print(
        f"  {'B':>5}  {'H':>3}  {'T/seq':>6}  {'T_tot':>7} | "
        f"{'sep(ms)':>8}  {'fuse(ms)':>8} | "
        f"{'speedup':>6}  {'saved':>9}"
    )
    print("  " + "-" * 73)

    for H in args.num_heads:
        for B in args.batch_sizes:
            for T_per_seq in args.seq_lens:
                bench_shape(B, H, T_per_seq, K, device, dtype)
        if len(args.num_heads) > 1:
            print()


if __name__ == "__main__":
    sys.exit(main())
