"""
Benchmark & Correctness: Triton GDN vs CuTeDSL GDN (prefill, SM100 Blackwell).

Compares:
  - Triton:  sglang's chunk_gated_delta_rule (FLA chunkwise, fp32 state, K-contig pool)
  - CuteDSL: ported vLLM #43273 chunk_gated_delta_rule_cutedsl (SM100 only)

The two kernels share the same math and the same g/beta convention (log-space
g, post-sigmoid beta). The CuteDSL kernel needs pre-allocated chunk metadata
from prepare_metadata_cutedsl, and l2norm is done outside the kernel.

Reports correctness (output & state matching) and performance (ms, TFLOPS, TB/s).

Usage:
    python bench_gdn_prefill_cutedsl.py                       # default sweep
    python bench_gdn_prefill_cutedsl.py --mode bench          # benchmark only
    python bench_gdn_prefill_cutedsl.py --mode correctness    # correctness only
    python bench_gdn_prefill_cutedsl.py --preset qwen3-next   # Qwen3-Next config
"""

import argparse
import os
import sys

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

import torch

from sglang.kernels.ops.attention.fla.chunk import (
    chunk_gated_delta_rule as triton_chunk_gated_delta_rule,
)
from sglang.kernels.ops.attention.fla.l2norm import l2norm_fwd
from sglang.kernels.ops.attention.linear.gdn_blackwell import (
    chunk_gated_delta_rule_cutedsl,
    prepare_metadata_cutedsl,
)

# ---------------------------------------------------------------------------
# Helpers (shared shape: pool layout [N, H, K, V] with K-last stride)
# ---------------------------------------------------------------------------


def make_k_contiguous(t: torch.Tensor) -> torch.Tensor:
    """K-last view of a logical [..., K, V] tensor (physically [..., V, K])."""
    return t.transpose(-2, -1).contiguous().transpose(-2, -1)


def gdn_flops(total_seq_len, num_heads, head_size_k, head_size_v):
    """Per-token-per-head: k@v^T outer (2*K*V) + q@state output (2*K*V)."""
    return 4 * total_seq_len * num_heads * head_size_k * head_size_v


def gdn_bytes(
    total_seq_len, num_q_heads, num_v_heads, head_size_k, head_size_v, num_seqs, dtype
):
    num_o_heads = max(num_q_heads, num_v_heads)
    elem = dtype.itemsize
    q_b = total_seq_len * num_q_heads * head_size_k * elem
    k_b = total_seq_len * num_v_heads * head_size_k * elem
    v_b = total_seq_len * num_v_heads * head_size_v * elem
    o_b = total_seq_len * num_o_heads * head_size_v * elem
    state_b = 2 * num_seqs * num_o_heads * head_size_k * head_size_v * 4  # fp32 r/w
    g_b = total_seq_len * num_o_heads * 4
    beta_b = total_seq_len * num_o_heads * 4
    return q_b + k_b + v_b + o_b + state_b + g_b + beta_b


# ---------------------------------------------------------------------------
# Input factory
# ---------------------------------------------------------------------------


def make_inputs(
    B, T_per_seq, H, K, V, pool_size, device, dtype, sequential_indices=False, seed=42
):
    T = B * T_per_seq
    torch.manual_seed(seed)

    if sequential_indices:
        cache_indices = torch.arange(B, dtype=torch.int32, device=device)
    else:
        perm = torch.randperm(pool_size, device=device)[:B]
        cache_indices = perm.to(torch.int32)

    pool_init = torch.randn(pool_size, H, K, V, dtype=dtype, device=device) * 0.1
    cu_seqlens = torch.arange(
        0, (B + 1) * T_per_seq, T_per_seq, dtype=torch.long, device=device
    )

    q = torch.randn(1, T, H, K, dtype=dtype, device=device)
    k = torch.randn(1, T, H, K, dtype=dtype, device=device)
    v = torch.randn(1, T, H, V, dtype=dtype, device=device)

    g_raw = torch.randn(1, T, H, dtype=dtype, device=device)
    g_triton = torch.nn.functional.logsigmoid(g_raw)
    beta_triton = torch.sigmoid(torch.randn(1, T, H, dtype=dtype, device=device))

    return dict(
        B=B,
        T=T,
        T_per_seq=T_per_seq,
        H=H,
        K=K,
        V=V,
        pool_size=pool_size,
        cache_indices=cache_indices,
        pool_init=pool_init,
        cu_seqlens=cu_seqlens,
        q=q,
        k=k,
        v=v,
        g_triton=g_triton,
        beta_triton=beta_triton,
    )


# ---------------------------------------------------------------------------
# Runner wrappers
# ---------------------------------------------------------------------------


def run_triton(inp):
    """Triton path: K-contiguous pool, pool-indexed, [1,T,H,D] tensors."""
    pool = make_k_contiguous(inp["pool_init"].clone())
    o, _, h = triton_chunk_gated_delta_rule(
        q=inp["q"],
        k=inp["k"],
        v=inp["v"],
        g=inp["g_triton"],
        beta=inp["beta_triton"],
        initial_state=pool,
        initial_state_indices=inp["cache_indices"],
        cu_seqlens=inp["cu_seqlens"],
        head_first=False,
        use_qk_l2norm_in_kernel=True,
    )
    return o, pool, h


def run_cutedsl(inp):
    """CuteDSL path: matches CuteDSLGDNKernel.extend() exactly."""
    pool = make_k_contiguous(inp["pool_init"].clone())
    cache_indices = inp["cache_indices"]
    cu_seqlens = inp["cu_seqlens"].to(torch.int32)

    q_in = l2norm_fwd(inp["q"][0].contiguous()).unsqueeze(0)
    k_in = l2norm_fwd(inp["k"][0].contiguous()).unsqueeze(0)
    v_in = inp["v"][0].contiguous().unsqueeze(0)
    g_in = inp["g_triton"][0].to(torch.float32).unsqueeze(0)
    beta_in = inp["beta_triton"][0].to(torch.float32).unsqueeze(0)

    initial_state = pool[cache_indices.to(torch.long)].contiguous()
    chunk_indices, chunk_offsets = prepare_metadata_cutedsl(
        cu_seqlens, inp["T"], chunk_size=64
    )

    o, final_state = chunk_gated_delta_rule_cutedsl(
        q=q_in,
        k=k_in,
        v=v_in,
        g=g_in,
        beta=beta_in,
        initial_state=initial_state,
        cu_seqlens=cu_seqlens,
        chunk_indices=chunk_indices,
        chunk_offsets=chunk_offsets,
    )

    pool.index_copy_(0, cache_indices.to(torch.long), final_state.to(pool.dtype))
    return o, pool, final_state


# ---------------------------------------------------------------------------
# Correctness check
# ---------------------------------------------------------------------------


def check_shape(
    B, T_per_seq, H, K, V, pool_size, device, dtype, sequential_indices=False, seed=42
):
    tag = (
        f"B={B:>3} T/seq={T_per_seq:>4} H={H:>2} K={K:>3} V={V:>3} pool={pool_size:>4}"
    )
    idx_tag = " (seq)" if sequential_indices else ""

    # The ported CuteDSL kernel hard-codes K == V == 128.
    if K != 128 or V != 128:
        print(f"  [SKIP] {tag}{idx_tag}  (CuteDSL requires K=V=128)")
        return True

    inp = make_inputs(
        B,
        T_per_seq,
        H,
        K,
        V,
        pool_size,
        device,
        dtype,
        sequential_indices=sequential_indices,
        seed=seed,
    )

    o_triton, pool_triton, _ = run_triton(inp)

    try:
        o_cutedsl, pool_cutedsl, _ = run_cutedsl(inp)
        torch.cuda.synchronize()
    except Exception as e:
        try:
            torch.cuda.synchronize()
        except Exception:
            pass
        print(f"  [SKIP] {tag}{idx_tag}  (CuteDSL error: {e})")
        return True

    # Output comparison. Both kernels are bf16 with L2norm + chunked accumulation,
    # tolerances mirror bench_gdn_prefill.py.
    try:
        torch.testing.assert_close(o_triton, o_cutedsl, atol=5e-2, rtol=1e-2)
        out_ok = True
    except AssertionError as e:
        out_ok = False
        out_err = str(e).splitlines()[0]

    status = "PASS" if out_ok else "FAIL"
    extra = "" if out_ok else f"  ({out_err})"
    print(f"  [{status}] {tag}{idx_tag}{extra}")
    return out_ok


# ---------------------------------------------------------------------------
# Benchmark
# ---------------------------------------------------------------------------


def bench_shape(B, H, T_per_seq, K, V, pool_size, device, dtype):
    import triton.testing

    if K != 128 or V != 128:
        print(f"  [SKIP] B={B} H={H} T={T_per_seq} K={K} V={V} (CuteDSL K=V=128 only)")
        return

    T = B * T_per_seq
    inp = make_inputs(B, T_per_seq, H, K, V, pool_size, device, dtype)

    q, k_t, v = inp["q"], inp["k"], inp["v"]
    g_triton, beta_triton = inp["g_triton"], inp["beta_triton"]
    cu_seqlens = inp["cu_seqlens"]
    cache_indices = inp["cache_indices"]
    pool_v = inp["pool_init"]
    T_total = inp["T"]

    def fn_triton():
        pool = make_k_contiguous(pool_v.clone())
        triton_chunk_gated_delta_rule(
            q=q,
            k=k_t,
            v=v,
            g=g_triton,
            beta=beta_triton,
            initial_state=pool,
            initial_state_indices=cache_indices,
            cu_seqlens=cu_seqlens,
            head_first=False,
            use_qk_l2norm_in_kernel=True,
        )

    cu_int32 = cu_seqlens.to(torch.int32)

    def fn_cutedsl():
        q_in = l2norm_fwd(q[0].contiguous()).unsqueeze(0)
        k_in = l2norm_fwd(k_t[0].contiguous()).unsqueeze(0)
        v_in = v[0].contiguous().unsqueeze(0)
        g_in = g_triton[0].to(torch.float32).unsqueeze(0)
        beta_in = beta_triton[0].to(torch.float32).unsqueeze(0)

        pool = make_k_contiguous(pool_v.clone())
        initial_state = pool[cache_indices.to(torch.long)].contiguous()
        chunk_indices, chunk_offsets = prepare_metadata_cutedsl(
            cu_int32, T_total, chunk_size=64
        )
        chunk_gated_delta_rule_cutedsl(
            q=q_in,
            k=k_in,
            v=v_in,
            g=g_in,
            beta=beta_in,
            initial_state=initial_state,
            cu_seqlens=cu_int32,
            chunk_indices=chunk_indices,
            chunk_offsets=chunk_offsets,
        )

    quantiles = [0.5, 0.2, 0.8]

    fn_triton()
    fn_cutedsl()
    torch.cuda.synchronize()

    ms_triton, _, _ = triton.testing.do_bench_cudagraph(fn_triton, quantiles=quantiles)
    ms_cutedsl, _, _ = triton.testing.do_bench_cudagraph(
        fn_cutedsl, quantiles=quantiles
    )

    flops = gdn_flops(T, H, K, V)
    mem_bytes = gdn_bytes(T, H, H, K, V, B, dtype)

    tflops_triton = flops / ms_triton / 1e9
    tflops_cutedsl = flops / ms_cutedsl / 1e9
    tb_s_triton = mem_bytes / ms_triton / 1e9
    tb_s_cutedsl = mem_bytes / ms_cutedsl / 1e9
    speedup = ms_triton / ms_cutedsl if ms_cutedsl > 0 else float("inf")

    print(
        f"  {B:>5}  {H:>3}  {T_per_seq:>6}  {T:>7} | "
        f"{ms_triton:>8.3f}  {tflops_triton:>7.2f}  {tb_s_triton:>7.2f} | "
        f"{ms_cutedsl:>8.3f}  {tflops_cutedsl:>7.2f}  {tb_s_cutedsl:>7.2f} | "
        f"{speedup:>7.2f}x"
    )


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------


def run_correctness(device, dtype):
    print("=" * 78)
    print("Correctness sweep: Triton vs CuTeDSL")
    print("=" * 78)

    shapes = [
        # (B, T_per_seq, H,  K,   V,   pool_size)
        (4, 64, 16, 128, 128, 32),
        (4, 256, 16, 128, 128, 32),
        (1, 128, 16, 128, 128, 32),
        (8, 128, 16, 128, 128, 64),
        (16, 64, 16, 128, 128, 128),
        (32, 32, 16, 128, 128, 256),
        (4, 128, 4, 128, 128, 32),
        (4, 128, 8, 128, 128, 32),
        (4, 128, 32, 128, 128, 32),
        (4, 128, 64, 128, 128, 32),
        (4, 1, 16, 128, 128, 32),
        (4, 7, 16, 128, 128, 32),
        (4, 16, 16, 128, 128, 32),
        (4, 128, 16, 128, 128, 512),
        (32, 128, 32, 128, 128, 256),
    ]

    shapes_seq = [
        (8, 128, 16, 128, 128, 8),
        (4, 128, 32, 128, 128, 4),
        (4, 128, 64, 128, 128, 4),
        (32, 128, 32, 128, 128, 32),
    ]

    all_pass = True
    for cfg in shapes:
        if not check_shape(*cfg, device, dtype):
            all_pass = False

    print("\nSequential-index variants:")
    for cfg in shapes_seq:
        if not check_shape(*cfg, device, dtype, sequential_indices=True):
            all_pass = False

    print()
    print("ALL PASSED." if all_pass else "SOME FAILED.")
    return all_pass


def run_benchmark(device, dtype, args):
    print()
    print("=" * 105)
    print("Benchmark: Triton GDN vs CuTeDSL GDN  (do_bench_cudagraph)")
    print("=" * 105)

    K = args.head_size_k
    V = args.head_size_v
    pool_size = args.pool_size

    if args.preset == "qwen3-next":
        bench_configs = [
            (4, 16, 256),
            (4, 32, 256),
            (16, 16, 256),
            (16, 32, 256),
            (32, 16, 256),
            (32, 32, 256),
            (64, 16, 256),
            (64, 32, 256),
            (128, 16, 256),
            (128, 32, 256),
            (4, 16, 1024),
            (4, 32, 1024),
            (32, 16, 1024),
            (32, 32, 1024),
        ]
    else:
        bench_configs = [
            (B, H, T)
            for B in args.batch_sizes
            for H in args.num_heads
            for T in args.seq_lens
        ]

    print(f"  Config: K={K}, V={V}, pool_size={pool_size}, dtype={dtype}")
    print(
        f"  {'B':>5}  {'H':>3}  {'T/seq':>6}  {'T_tot':>7} | "
        f"{'tri(ms)':>8}  {'TFLOPS':>7}  {'TB/s':>7} | "
        f"{'cute(ms)':>8}  {'TFLOPS':>7}  {'TB/s':>7} | "
        f"{'speedup':>8}"
    )
    print("  " + "-" * 98)

    for B, H, T_per_seq in bench_configs:
        actual_pool = max(pool_size, B)
        bench_shape(B, H, T_per_seq, K, V, actual_pool, device, dtype)


def main():
    parser = argparse.ArgumentParser(
        description="Benchmark & Correctness: Triton GDN vs CuTeDSL GDN (SM100)"
    )
    parser.add_argument(
        "--mode", choices=["all", "correctness", "bench"], default="all"
    )
    parser.add_argument(
        "--preset", choices=["qwen3-next", "custom"], default="qwen3-next"
    )
    parser.add_argument("--dtype", choices=["float16", "bfloat16"], default="bfloat16")
    parser.add_argument("--head-size-k", type=int, default=128)
    parser.add_argument("--head-size-v", type=int, default=128)
    parser.add_argument("--pool-size", type=int, default=256)
    parser.add_argument(
        "--batch-sizes", type=int, nargs="+", default=[4, 16, 32, 64, 128]
    )
    parser.add_argument("--num-heads", type=int, nargs="+", default=[16, 32])
    parser.add_argument(
        "--seq-lens", type=int, nargs="+", default=[128, 256, 512, 1024]
    )
    args = parser.parse_args()

    if args.preset == "qwen3-next":
        args.head_size_k = 128
        args.head_size_v = 128

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

    cap = torch.cuda.get_device_capability()
    dev_name = torch.cuda.get_device_name()
    print(f"Device: {dev_name}  (SM {cap[0]}{cap[1]})")
    if cap[0] < 10:
        print("ERROR: CuTeDSL GDN prefill requires SM100+ (Blackwell). Exiting.")
        return 1

    if args.mode in ("all", "correctness"):
        all_pass = run_correctness(device, dtype)
        if not all_pass and args.mode == "all":
            print("\nSkipping benchmark due to correctness failures.")
            return 1

    if args.mode in ("all", "bench"):
        run_benchmark(device, dtype, args)

    return 0


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