"""Steps 1/2/4 - profiler kernel table, sequence scaling, isolated CE timing.

Loads the real model exactly as erp_sft_harness.runtime does (same
from_pretrained args, same PEFT config, same gradient checkpointing, same
chunked-CE compute_loss) and measures:

  A. sequence scaling  2x2048 / 2x8192 / 2x16384 fwd+bwd
       linear-dominated    -> time falls ~8x from 16384 to 2048
       launch-bound        -> time barely falls
       quadratic-dominated -> time falls ~64x
  B. isolated CE wall clock (CUDA events around the chunked-CE block)
  C. torch.profiler kernel table, sorted by self CUDA time
  D. expert-GEMM launch counts (settles grouped_mm without kernel-name
     archaeology: 128 sequential launches per layer = no-op, 1 = grouped)

Runs on GPU0, which is reserved and idle. Nothing else touches it.
"""
import json
import sys
import time

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model

sys.path.insert(0, "/tank/erp-tune/eitri-smithy")
from erp_sft_harness.core import IGNORE_INDEX, discover_target_modules

MODEL = "/tank/aimodels/gemma4-26b-a4b-it-heretic-bf16"
CHUNK = 1024
MB = 2

print("=" * 72)
print("loading model")
print("=" * 72, flush=True)
t0 = time.time()
model = AutoModelForCausalLM.from_pretrained(
    MODEL, dtype=torch.bfloat16, device_map={"": 0}, attn_implementation="sdpa",
)
targets = discover_target_modules(model)
model = get_peft_model(model, LoraConfig(
    r=64, lora_alpha=128, lora_dropout=0.05, target_modules=targets,
    bias="none", task_type="CAUSAL_LM",
))
model.enable_input_require_grads()
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
model.train()
print("loaded in %.1fs  targets=%d" % (time.time() - t0, len(targets)), flush=True)

base = model.base_model.model if hasattr(model, "base_model") else model
body = base.model
lm_head = base.get_output_embeddings()
softcap = getattr(model.config.get_text_config(), "final_logit_softcapping", None)
print("final_logit_softcapping = %s" % softcap)
print("attn_implementation     = %s" % model.config.get_text_config()._attn_implementation)
print(flush=True)

ce_ms = {"fwd": 0.0}


def compute_loss(input_ids, attention_mask, labels, time_ce=False):
    """Byte-for-byte the harness's compute_loss, with optional CE timing."""
    hidden = body(input_ids=input_ids, attention_mask=attention_mask,
                  use_cache=False).last_hidden_state
    flat_hidden = hidden[:, :-1, :].reshape(-1, hidden.size(-1))
    flat_labels = labels[:, 1:].reshape(-1)
    keep = flat_labels != IGNORE_INDEX
    kept_hidden = flat_hidden[keep]
    kept_labels = flat_labels[keep]
    kept = int(kept_labels.numel())

    def chunk_loss(chunk_hidden, chunk_labels):
        logits = lm_head(chunk_hidden).float()
        if softcap is not None:
            logits = torch.tanh(logits / softcap) * softcap
        return torch.nn.functional.cross_entropy(logits, chunk_labels, reduction="sum")

    if time_ce:
        s, e = torch.cuda.Event(True), torch.cuda.Event(True)
        torch.cuda.synchronize()
        s.record()
    total = torch.zeros((), device=kept_hidden.device, dtype=torch.float32)
    for start in range(0, kept, CHUNK):
        total = total + torch.utils.checkpoint.checkpoint(
            chunk_loss, kept_hidden[start:start + CHUNK],
            kept_labels[start:start + CHUNK], use_reentrant=False,
        )
    if time_ce:
        e.record()
        torch.cuda.synchronize()
        ce_ms["fwd"] = s.elapsed_time(e)
    return total / kept, kept


def make_batch(n, pad_frac=0.0):
    """Synthetic batch. pad_frac trims the SECOND row and right-pads it,
    mimicking collate_mixed on a heterogeneous pair."""
    ids = torch.randint(100, 200000, (MB, n), device="cuda")
    am = torch.ones(MB, n, dtype=torch.long, device="cuda")
    labels = ids.clone()
    if pad_frac > 0:
        keep = int(n * (1 - pad_frac))
        am[1, keep:] = 0
        labels[1, keep:] = IGNORE_INDEX
    # ~40% of real tokens carry loss (measured mean 2188/2752 is higher, but
    # rp-dialogue assistant-only masking pulls the mix down); use the measured
    # global ratio 57.7M ctx -> 45.9M targets = 0.795
    m = torch.rand(labels.shape, device="cuda") > 0.795
    labels[m] = IGNORE_INDEX
    return ids, am, labels


def timed(n, pad_frac=0.0, reps=2, time_ce=False, label=""):
    ids, am, labels = make_batch(n, pad_frac)
    for _ in range(1):  # warmup
        loss, kept = compute_loss(ids, am, labels)
        loss.backward()
        model.zero_grad(set_to_none=True)
    torch.cuda.synchronize()
    best = None
    for _ in range(reps):
        torch.cuda.reset_peak_memory_stats()
        t = time.perf_counter()
        loss, kept = compute_loss(ids, am, labels, time_ce=time_ce)
        loss.backward()
        torch.cuda.synchronize()
        dt = time.perf_counter() - t
        best = dt if best is None else min(best, dt)
        model.zero_grad(set_to_none=True)
    peak = torch.cuda.max_memory_allocated() / 2**30
    print("  %-34s %7.3f s   kept=%-6d peak=%5.1f GiB%s" % (
        label or ("2x%d pad=%.0f%%" % (n, pad_frac * 100)),
        best, kept, peak,
        ("   CE=%.0f ms (%.1f%%)" % (ce_ms["fwd"], 100 * ce_ms["fwd"] / 1000 / best)) if time_ce else ""))
    return best


print("=" * 72)
print("A. SEQUENCE SCALING  (no padding - isolates n)")
print("=" * 72, flush=True)
t2048 = timed(2048, 0.0, label="2 x  2,048")
t8192 = timed(8192, 0.0, label="2 x  8,192")
t16384 = timed(16384, 0.0, label="2 x 16,384")
print()
print("  16384 -> 2048 ratio        %.2fx    (linear ~8x, launch-bound ~1x, quadratic ~64x)"
      % (t16384 / t2048))
print("  16384 -> 8192 ratio        %.2fx    (linear ~2x, quadratic ~4x)"
      % (t16384 / t8192))
print(flush=True)

print("=" * 72)
print("B. PADDING PENALTY  (same real tokens, with vs without pad)")
print("=" * 72, flush=True)
timed(16384, 0.0, label="2 x 16,384  no padding")
timed(16384, 0.5, label="2 x 16,384  50% pad on row 1")
print(flush=True)

print("=" * 72)
print("C. ISOLATED CE WALL CLOCK")
print("=" * 72, flush=True)
timed(16384, 0.0, reps=2, time_ce=True, label="2 x 16,384 (CE timed)")
timed(4096, 0.0, reps=2, time_ce=True, label="2 x  4,096 (CE timed)")
print(flush=True)

print("=" * 72)
print("D. KERNEL TABLE - one fwd+bwd at 2 x 16,384")
print("=" * 72, flush=True)
ids, am, labels = make_batch(16384, 0.0)
loss, _ = compute_loss(ids, am, labels)
loss.backward()
model.zero_grad(set_to_none=True)
torch.cuda.synchronize()

with torch.profiler.profile(
    activities=[torch.profiler.ProfilerActivity.CPU,
                torch.profiler.ProfilerActivity.CUDA],
    record_shapes=False, with_stack=False,
) as prof:
    loss, _ = compute_loss(ids, am, labels)
    loss.backward()
    torch.cuda.synchronize()
model.zero_grad(set_to_none=True)

print(prof.key_averages().table(sort_by="self_cuda_time_total", row_limit=45))

print()
print("=" * 72)
print("E. LAUNCH COUNTS  (grouped_mm: 128/layer sequential = no-op, 1 = grouped)")
print("=" * 72)
rows = []
for ev in prof.key_averages():
    if ev.self_device_time_total <= 0:
        continue
    rows.append((ev.key, ev.count, ev.self_device_time_total / 1000.0))
rows.sort(key=lambda r: -r[2])
print("  %-58s %8s %10s" % ("kernel", "count", "self ms"))
for k, c, ms in rows[:30]:
    print("  %-58s %8d %10.1f" % (k[:58], c, ms))

total_ms = sum(r[2] for r in rows)
print()
print("  total self CUDA time  %.1f ms" % total_ms)
gemm = sum(ms for k, c, ms in rows if any(t in k.lower() for t in
           ("gemm", "cutlass", "sm90", "sm100", "sm120", "nvjet", "ampere", "tensor")))
print("  GEMM-ish kernels      %.1f ms  (%.1f%%)" % (gemm, 100 * gemm / total_ms))
print("  non-GEMM              %.1f ms  (%.1f%%)" % (total_ms - gemm, 100 * (total_ms - gemm) / total_ms))
