#!/usr/bin/env python3
"""SCRATCH EXPERIMENT (not production; do not commit).

Question: does charging the *primitive* decode kernel for per-HW-tile
(16x16x16) CPU dispatch flip the "composite gives no decode-latency
benefit" conclusion?

We monkeypatch TLContext.dot so that, in the primitive kernel, every
tl.dot whose (M,K,N) exceeds the HW GEMM tile (mac_m/mac_k/mac_n) is
split by the CPU into ceil(M/mac_m)*ceil(K/mac_k)*ceil(N/mac_n)
HW-tile-sized GemmCmds.  Each tile GemmCmd is emitted through the normal
_emit() path, so it (a) charges PE_CPU dispatch overhead via
_charge_dispatch (PeCpuOverheadCmd), and (b) blocks like a normal
single-op GemmCmd on PE_GEMM at the cycle-accurate ceil-product latency.

We also inject mac_m/mac_k/mac_n into every pe_gemm topology node so BOTH
the primitive-tiled and the composite variants run on the *same*
cycle-accurate engine (fair comparison).  Composite is left untouched:
the CPU emits ONE CompositeCmd, and PE_SCHEDULER tiles internally (no
per-HW-tile CPU dispatch).

Data correctness:
  Inputs are ctx.zeros (q/k/v), so every matmul result is zeros and the
  DataExecutor replay is trivial.  To keep replay numerically correct
  regardless, exactly ONE emitted tile per dot carries the *real* full
  operands+output handles (so the DataExecutor computes the true (M,N)
  result via the recorded handle shapes), while its timing fields
  (m,k,n) are the HW-tile size so the engine charges exactly one tile of
  cycle time.  The remaining n_tiles-1 emitted tiles are timing-only
  GemmCmds (16x16x16) writing to throwaway scratch.  Net: n_tiles tiles
  of engine time + n_tiles dispatch charges, and a correct final output.

Engine mode: enable_data=True (same as the production sweep's
_engine_latency_ns), op_log end-to-end latency.
"""
from __future__ import annotations

import json
from math import ceil
from pathlib import Path

# --- HW GEMM tile under test -------------------------------------------------
MAC_M = 16
MAC_K = 16
MAC_N = 16
# Legacy alt to also try: (8, 16, 32)

S_KV_LATENCY = (8192, 32_768, 65_536, 131_072)

ROOT = Path(__file__).resolve().parent
SWEEP_JSON = (
    ROOT / "src" / "kernbench" / "benches" / "1H_milestone_output"
    / "gqa" / "long_ctx" / "sweep_decode_composite.json"
)


# --- mac-dim topology override -----------------------------------------------
def _topo_with_mac(mac_m: int, mac_k: int, mac_n: int):
    """Compiled topology with mac dims injected into every pe_gemm node."""
    from kernbench.topology.builder import resolve_topology

    handle = resolve_topology("topology.yaml")
    g = handle.topology_obj
    n = 0
    for node in g.nodes.values():
        if node.kind == "pe_gemm":
            node.attrs["mac_m"] = mac_m
            node.attrs["mac_k"] = mac_k
            node.attrs["mac_n"] = mac_n
            n += 1
    print(f"    injected mac=({mac_m},{mac_k},{mac_n}) into {n} pe_gemm nodes")
    return handle


# --- tiling monkeypatch for TLContext.dot ------------------------------------
def _make_tiled_dot(orig_dot, mac_m: int, mac_k: int, mac_n: int):
    from kernbench.common.pe_commands import GemmCmd

    def tiled_dot(self, a, b):
        if len(a.shape) < 2 or len(b.shape) < 2:
            return orig_dot(self, a, b)
        m, k = a.shape[-2], a.shape[-1]
        k2, n = b.shape[-2], b.shape[-1]
        if k != k2:
            raise ValueError(f"dot shape mismatch: a.K={k} != b.K={k2}")

        n_tiles = ceil(m / mac_m) * ceil(k / mac_k) * ceil(n / mac_n)

        out_shape = (*a.shape[:-2], m, n)
        out = self._make_compute_out(shape=out_shape, dtype=a.dtype)
        self._await_pending(a, b)

        if n_tiles <= 1:
            self._emit(GemmCmd(a=a, b=b, out=out, m=m, k=k, n=n))
            return out

        # One real-data tile: full handles (so DataExecutor computes the
        # true result), but timing fields = HW tile (one tile of cycles).
        self._emit(GemmCmd(a=a, b=b, out=out, m=mac_m, k=mac_k, n=mac_n))
        # Remaining timing-only tiles: throwaway scratch, 16x16x16.
        scratch = self._make_compute_out(shape=(mac_m, mac_n), dtype=a.dtype)
        for _ in range(n_tiles - 1):
            self._emit(GemmCmd(a=a, b=b, out=scratch,
                               m=mac_m, k=mac_k, n=mac_n))
        return out

    return tiled_dot


# --- latency runner (replicates sweep's _engine_latency_ns) ------------------
def _engine_latency_ns(variant: str, S_kv: int, topo) -> float:
    from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_composite import (  # noqa: E501
        _end_to_end_ns, _run_panel_fn,
    )
    from kernbench.runtime_api.bench_runner import run_bench
    from kernbench.runtime_api.types import resolve_device
    from kernbench.sim_engine.engine import GraphEngine

    result = run_bench(
        topology=topo, bench_fn=_run_panel_fn(variant, S_kv),
        device=resolve_device(None),
        engine_factory=lambda t, d: GraphEngine(
            getattr(t, "topology_obj", t), enable_data=True,
        ),
    )
    if not result.completion.ok:
        raise RuntimeError(
            f"{variant}@{S_kv} failed: {result.completion}"
        )
    return _end_to_end_ns(result.engine.op_log)


def _emit_dispatch(variant: str, S_kv: int) -> int:
    """PE_CPU command count at the center rank (cube 6, pe 0)."""
    from kernbench.benches.gqa_helpers.long_ctx.gqa_decode_long_ctx_composite import (  # noqa: E501
        _emit_dispatch as prod_emit,
    )
    return prod_emit(variant, S_kv)[0]


def main() -> None:
    import kernbench.triton_emu.tl_context as tlc

    # Baseline (A): primitive UNTILED latencies from the production sweep
    # (mac=0 / TFLOPS model). Read straight off the committed sweep JSON.
    sweep = json.loads(SWEEP_JSON.read_text())
    base_A = {}
    for r in sweep["rows"]:
        if r["variant"] == "primitive" and r["latency_ns"] is not None:
            base_A[r["S_kv"]] = r["latency_ns"]

    print(f"== mac tile = ({MAC_M},{MAC_K},{MAC_N}) ==")

    # --- command-count sanity (emit-time, mac-independent) ---------------
    orig_dot = tlc.TLContext.dot
    print("\n[dispatch counts @ S_kv=131072]")
    n_prim_untiled = _emit_dispatch("primitive", 131072)
    tlc.TLContext.dot = _make_tiled_dot(orig_dot, MAC_M, MAC_K, MAC_N)
    try:
        n_prim_tiled = _emit_dispatch("primitive", 131072)
        n_comp = None
    finally:
        tlc.TLContext.dot = orig_dot
    n_comp = _emit_dispatch("composite", 131072)
    print(f"  primitive UNTILED PE_CPU cmds : {n_prim_untiled}")
    print(f"  primitive TILED   PE_CPU cmds : {n_prim_tiled}  "
          f"(x{n_prim_tiled / max(n_prim_untiled,1):.0f})")
    print(f"  composite         PE_CPU cmds : {n_comp}")

    # --- latency sweep ----------------------------------------------------
    rows = []
    topo = _topo_with_mac(MAC_M, MAC_K, MAC_N)

    for S_kv in S_KV_LATENCY:
        A = base_A.get(S_kv)

        # (C) composite on the mac engine (untouched dot path)
        C = _engine_latency_ns("composite", S_kv, topo)

        # (B) primitive TILED on the mac engine
        tlc.TLContext.dot = _make_tiled_dot(orig_dot, MAC_M, MAC_K, MAC_N)
        try:
            B = _engine_latency_ns("primitive", S_kv, topo)
        finally:
            tlc.TLContext.dot = orig_dot

        gap_pct = (B - C) / C * 100.0 if C else float("nan")
        rows.append((S_kv, A, B, C, gap_pct))
        print(f"  S_kv={S_kv:>7}: A(untiled)={A!s:>12}  "
              f"B(tiled)={B:12.2f}  C(comp)={C:12.2f}  (B-C)/C={gap_pct:+6.1f}%")

    # --- final table ------------------------------------------------------
    print("\n==================== RESULT TABLE ====================")
    print(f"{'S_kv':>8} | {'A untiled(ns)':>14} | {'B tiled(ns)':>14} | "
          f"{'C comp(ns)':>14} | {'(B-C)/C':>9}")
    print("-" * 72)
    for S_kv, A, B, C, gap in rows:
        a_s = f"{A:.2f}" if A is not None else "n/a"
        print(f"{S_kv:>8} | {a_s:>14} | {B:>14.2f} | {C:>14.2f} | "
              f"{gap:>+8.1f}%")


if __name__ == "__main__":
    main()
