#!/usr/bin/env python3
from __future__ import annotations

import argparse
import math
from pathlib import Path

import torch
import torch.distributed as dist
import torch.distributed.checkpoint as dcp
import torch.nn.functional as F
from tokenizers import Tokenizer


CKPT = Path(
    "runs/pretrain_8192_8gpu_dp8_mbs14_full_recompute_weighted_heldoutval_resume10000/"
    "checkpoints/iter_0107500"
)
TOKENIZER = Path("tokenizer/glm5.2/tokenizer.json")

HIDDEN = 1536
HEADS = 24
KV_HEADS = 4
HEAD_DIM = 64
DENSE_FFN = 4608
MOE_FFN = 6144
MOE_LAYERS = {2, 4, 6, 8, 10}
TOP_K = 4
N_LAYERS = 12
ROPE_BASE = 10000.0


def init_dist() -> None:
    if not dist.is_initialized():
        dist.init_process_group("gloo", init_method="tcp://127.0.0.1:29641", rank=0, world_size=1)


def rms_norm(x: torch.Tensor, weight: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
    y = x.float() * torch.rsqrt(x.float().pow(2).mean(dim=-1, keepdim=True) + eps)
    return (y * weight.float()).to(torch.bfloat16)


def rotate_half(x: torch.Tensor) -> torch.Tensor:
    x1 = x[..., ::2]
    x2 = x[..., 1::2]
    out = torch.stack((-x2, x1), dim=-1)
    return out.flatten(-2)


def apply_rope(q: torch.Tensor, k: torch.Tensor, seq_len: int) -> tuple[torch.Tensor, torch.Tensor]:
    inv_freq = 1.0 / (ROPE_BASE ** (torch.arange(0, HEAD_DIM, 2, dtype=torch.float32) / HEAD_DIM))
    pos = torch.arange(seq_len, dtype=torch.float32)
    freqs = torch.einsum("t,d->td", pos, inv_freq)
    emb = torch.repeat_interleave(freqs, 2, dim=-1)
    cos = emb.cos()[None, :, None, :]
    sin = emb.sin()[None, :, None, :]
    qf = q.float()
    kf = k.float()
    return (qf * cos + rotate_half(qf) * sin).to(torch.bfloat16), (
        kf * cos + rotate_half(kf) * sin
    ).to(torch.bfloat16)


def swiglu(x: torch.Tensor) -> torch.Tensor:
    a, b = x.chunk(2, dim=-1)
    return F.silu(a.float()) * b.float()


def load_model_tensors(ckpt: Path) -> dict[str, torch.Tensor]:
    init_dist()
    metadata = dcp.FileSystemReader(str(ckpt)).read_metadata()
    state: dict[str, torch.Tensor] = {}
    for key, meta in metadata.state_dict_metadata.items():
        if key.startswith("optimizer.") or not hasattr(meta, "size"):
            continue
        state[key] = torch.empty(tuple(meta.size), dtype=meta.properties.dtype)
    print(f"loading {len(state)} model tensors from {ckpt}", flush=True)
    dcp.load(state, checkpoint_id=str(ckpt))
    print("loaded model tensors", flush=True)
    return state


def dense_mlp(x: torch.Tensor, state: dict[str, torch.Tensor], layer: int) -> torch.Tensor:
    prefix = f"decoder.layers.{layer}.mlp"
    h = rms_norm(x, state[f"{prefix}.linear_fc1.layer_norm_weight"])
    fc1 = F.linear(h.float(), state[f"{prefix}.linear_fc1.weight"].float())
    hidden = swiglu(fc1)
    return F.linear(hidden, state[f"{prefix}.linear_fc2.weight"].float()).to(torch.bfloat16)


def moe_mlp(x: torch.Tensor, state: dict[str, torch.Tensor], layer: int) -> torch.Tensor:
    prefix = f"decoder.layers.{layer}.mlp"
    h = rms_norm(x, state[f"decoder.layers.{layer}.pre_mlp_layernorm.weight"])
    router = F.linear(h.float(), state[f"{prefix}.router.weight"].float())
    topv, topi = torch.topk(router, TOP_K, dim=-1)
    probs = torch.softmax(topv, dim=-1)

    fc1_w = state[f"{prefix}.experts.experts.linear_fc1.weight"].float()
    fc2_w = state[f"{prefix}.experts.experts.linear_fc2.weight"].float()
    out = torch.zeros_like(h.float())
    flat_h = h.float().reshape(-1, HIDDEN)
    flat_topi = topi.reshape(-1, TOP_K)
    flat_probs = probs.reshape(-1, TOP_K)
    flat_out = out.reshape(-1, HIDDEN)
    for expert_id in range(fc1_w.shape[0]):
        mask = flat_topi == expert_id
        if not mask.any():
            continue
        token_idx, choice_idx = mask.nonzero(as_tuple=True)
        expert_in = flat_h[token_idx]
        expert_hidden = swiglu(F.linear(expert_in, fc1_w[expert_id]))
        expert_out = F.linear(expert_hidden, fc2_w[expert_id])
        flat_out[token_idx] += expert_out * flat_probs[token_idx, choice_idx].unsqueeze(-1)
    return out.to(torch.bfloat16)


def layer_forward(x: torch.Tensor, state: dict[str, torch.Tensor], layer: int) -> torch.Tensor:
    prefix = f"decoder.layers.{layer}"
    attn_in = rms_norm(x, state[f"{prefix}.self_attention.linear_qkv.layer_norm_weight"])
    qkv = F.linear(attn_in.float(), state[f"{prefix}.self_attention.linear_qkv.weight"].float())
    q, k, v = torch.split(qkv, [HEADS * HEAD_DIM, KV_HEADS * HEAD_DIM, KV_HEADS * HEAD_DIM], dim=-1)
    bsz, seq_len, _ = q.shape
    q = q.view(bsz, seq_len, HEADS, HEAD_DIM)
    k = k.view(bsz, seq_len, KV_HEADS, HEAD_DIM)
    v = v.view(bsz, seq_len, KV_HEADS, HEAD_DIM)
    q, k = apply_rope(q, k, seq_len)
    repeat = HEADS // KV_HEADS
    k = k.repeat_interleave(repeat, dim=2)
    v = v.repeat_interleave(repeat, dim=2)
    q = q.transpose(1, 2).float()
    k = k.transpose(1, 2).float()
    v = v.transpose(1, 2).float()
    scores = torch.matmul(q, k.transpose(-1, -2)) / math.sqrt(HEAD_DIM)
    mask = torch.triu(torch.ones(seq_len, seq_len, dtype=torch.bool), diagonal=1)
    scores = scores.masked_fill(mask[None, None, :, :], float("-inf"))
    ctx = torch.matmul(torch.softmax(scores, dim=-1), v)
    ctx = ctx.transpose(1, 2).contiguous().view(bsz, seq_len, HIDDEN)
    attn_out = F.linear(ctx, state[f"{prefix}.self_attention.linear_proj.weight"].float()).to(torch.bfloat16)
    x = x + attn_out
    x = x + (moe_mlp(x, state, layer) if layer in MOE_LAYERS else dense_mlp(x, state, layer))
    return x


@torch.inference_mode()
def logits_for_ids(ids: list[int], state: dict[str, torch.Tensor]) -> torch.Tensor:
    input_ids = torch.tensor(ids, dtype=torch.long).unsqueeze(0)
    x = F.embedding(input_ids, state["embedding.word_embeddings.weight"])
    for layer in range(N_LAYERS):
        x = layer_forward(x, state, layer)
    x = rms_norm(x, state["decoder.final_layernorm.weight"])
    return torch.matmul(x[:, -1, :].float(), state["embedding.word_embeddings.weight"].float().T)


def generate(prompt: str, state: dict[str, torch.Tensor], tokenizer: Tokenizer, max_new_tokens: int) -> str:
    ids = tokenizer.encode(prompt).ids
    print(f"prompt={prompt!r} input_tokens={len(ids)}", flush=True)
    for step in range(max_new_tokens):
        logits = logits_for_ids(ids, state)
        next_id = int(torch.argmax(logits, dim=-1)[0])
        ids.append(next_id)
        piece = tokenizer.decode([next_id], skip_special_tokens=False)
        print(f"step={step} id={next_id} piece={piece!r}", flush=True)
    return tokenizer.decode(ids, skip_special_tokens=False)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--max-new-tokens", type=int, default=8)
    parser.add_argument("--prompt", action="append", default=None)
    args = parser.parse_args()

    tokenizer = Tokenizer.from_file(str(TOKENIZER))
    prompts = args.prompt or [
        "The capital of France is",
        "中国的首都是",
    ]
    state = load_model_tensors(CKPT)
    for prompt in prompts:
        text = generate(prompt, state, tokenizer, args.max_new_tokens)
        print("=== GENERATED ===")
        print(text)


if __name__ == "__main__":
    main()
