#!/usr/bin/env python3
"""Summarize graph-on vLLM Torch traces by decode component."""

from __future__ import annotations

import argparse
import gzip
import json
import math
import statistics
from collections import Counter, defaultdict
from pathlib import Path


COMPONENT_NAMES = (
    "attention",
    "linear_norm_rope",
    "router",
    "moe",
    "collective",
    "output_head",
    "other",
)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--trace-root", type=Path, required=True)
    parser.add_argument("--label", required=True)
    parser.add_argument("--output", type=Path, required=True)
    return parser.parse_args()


def classify(name: str, occurrences: int, steps: int) -> str:
    lower = name.lower()
    if any(token in lower for token in ("nccl", "allreduce", "all_reduce")):
        return "collective"
    if "topkgating" in lower or "fused_topk" in lower:
        return "router"
    if any(
        token in lower
        for token in (
            "fused_moe",
            "moefcgemm",
            "tensorrt_llm::kernels::cutlass_kernels",
            "groupproblemshape",
            "memcpy32_post",
        )
    ):
        return "moe"
    if any(
        token in lower
        for token in (
            "flashattn",
            "flashattnfwd",
            "reshape_and_cache",
            "prepare_varlen_num_blocks",
        )
    ):
        return "attention"
    if "nvjet" in lower and occurrences <= steps * 2:
        return "output_head"
    if any(
        token in lower
        for token in (
            "nvjet",
            "cublaslt",
            "rms_norm",
            "rsqrt",
            "triton_red_fused_2",
            "triton_poi_fused_3",
            "triton_red_fused_0",
            "triton_poi_fused_1",
        )
    ):
        return "linear_norm_rope"
    return "other"


def stats(values: list[float]) -> dict[str, float | int]:
    ordered = sorted(values)
    return {
        "n": len(values),
        "mean_ms": statistics.fmean(values),
        "population_std_ms": statistics.pstdev(values),
        "p50_ms": statistics.median(ordered),
        "p95_ms": ordered[math.ceil(0.95 * len(ordered)) - 1],
    }


def load_events(path: Path) -> list[dict]:
    opener = gzip.open if path.suffix == ".gz" else open
    with opener(path, "rt") as source:
        return json.load(source)["traceEvents"]


def analyze_rank(path: Path) -> dict:
    events = load_events(path)
    kernels = [event for event in events if event.get("cat") == "kernel"]
    all_windows = sorted(
        (
            event
            for event in events
            if event.get("cat") == "gpu_user_annotation"
            and str(event.get("name", "")).startswith("execute_")
        ),
        key=lambda event: float(event["ts"]),
    )
    if not all_windows:
        raise ValueError(f"{path}: no GPU execute annotations")
    window_names = Counter(str(window["name"]) for window in all_windows)
    selected_name = window_names.most_common(1)[0][0]
    windows = [
        window for window in all_windows if str(window["name"]) == selected_name
    ]

    selected: list[dict] = []
    step_kernels: list[list[dict]] = []
    for window in windows:
        start = float(window["ts"])
        end = start + float(window["dur"])
        current = [
            kernel for kernel in kernels if start <= float(kernel["ts"]) < end
        ]
        selected.extend(current)
        step_kernels.append(current)

    occurrences = Counter(str(kernel["name"]) for kernel in selected)
    component_steps: dict[str, list[float]] = defaultdict(list)
    busy_steps: list[float] = []
    wall_steps = [float(window["dur"]) / 1000.0 for window in windows]
    for current in step_kernels:
        per_component: dict[str, float] = defaultdict(float)
        for kernel in current:
            name = str(kernel["name"])
            per_component[classify(name, occurrences[name], len(windows))] += (
                float(kernel["dur"]) / 1000.0
            )
        for name in COMPONENT_NAMES:
            component_steps[name].append(per_component[name])
        busy_steps.append(sum(per_component.values()))

    return {
        "trace": str(path),
        "selected_execute_annotation": selected_name,
        "execute_annotation_histogram": dict(sorted(window_names.items())),
        "steps": len(windows),
        "execute_wall": stats(wall_steps),
        "gpu_kernel_busy": stats(busy_steps),
        "non_kernel_gap": stats(
            [wall - busy for wall, busy in zip(wall_steps, busy_steps)]
        ),
        "components": {
            name: stats(values) for name, values in component_steps.items()
        },
    }


def main() -> None:
    args = parse_args()
    traces = sorted(args.trace_root.rglob("*.pt.trace.json*"))
    if not traces:
        raise SystemExit(f"no traces under {args.trace_root}")
    ranks = [analyze_rank(path) for path in traces]
    payload = {
        "schema": "frontier-decode-batch-trace.v1",
        "label": args.label,
        "contract": {
            "timing": "CUDA graph-on GPU execute annotations",
            "component_time": "sum of CUDA kernel durations inside execute range",
            "tp_aggregation": "per-rank; slowest rank mean approximates critical path",
        },
        "ranks": ranks,
        "rank_summary": {
            "ranks": len(ranks),
            "slowest_rank_execute_mean_ms": max(
                rank["execute_wall"]["mean_ms"] for rank in ranks
            ),
            "slowest_rank_kernel_busy_mean_ms": max(
                rank["gpu_kernel_busy"]["mean_ms"] for rank in ranks
            ),
            "component_rank_mean_ms": {
                name: statistics.fmean(
                    rank["components"][name]["mean_ms"] for rank in ranks
                )
                for name in COMPONENT_NAMES
            },
        },
    }
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")


if __name__ == "__main__":
    main()
