#!/usr/bin/env python3 """Per-box (batch=1) latency on a modern NVIDIA GPU (user 2026-10-07): 500 Manga109-s boxes (clean input), drawn with seed 20261007 from the 3,817-box comparison set (make_sample.py), the first --warm boxes translated once before timing. ours_eager Zakkuri v1 as on the Titan Xp run: PyTorch eager fp32, ARMT.generate_ctx (run_ours.Ours) ours_graph Zakkuri v1, fp16 with the per-norm scales of the Core ML export (norm_scales.json; fp16 overflows in the encoder's massive activations otherwise), encoder = EncoderStatic per length bucket (32/64/128) and one decoder step (DecoderExport, KV cache T=128, cross length 130) each captured as a CUDA graph; kana byte rules, argmax and the cache update run inside the step graph; the host replays one step per token and reads the token back (one sync per step). Same function as the Core ML / ANE path. ours_graph32 the same graphs in fp32 (the norm scales are powers of two, so in fp32 they change nothing). Tesla P4 (Pascal, 10-07): 32.0 ms p50 vs 30.0 ms for ours_graph -- cuBLAS half GEMMs accumulate in fp32, so the fp16 path still works there nano NanoSakura-2.2-0.2B, fp32 (run_ours.Nano) gal GalTransl-v4-4B-2601 Q6_K via llama-server, one request at a time (run_galtransl.Client) ms = wall time of one box: tokenization -> generation -> CUDA sync -> detokenization (same definition as run_ours.timed). Output: /.jsonl (id, hyp, ms, ...) and /.summary.json.""" from __future__ import annotations import argparse import json import os import math import platform import subprocess import sys import time from pathlib import Path import numpy as np import torch ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "benchmarks/sakura_cmp")) sys.path.insert(0, str(ROOT / "ar_mt")) from train import BOS, EOS, PAD, decode_ids # noqa: E402 CKPT = ROOT / "artifacts/r2_20261006/weights/r2_step54000.pt" ENC = ROOT / "artifacts/ar_mt_20261005/weights/dapt_v1_model" DATA = ROOT / "artifacts/ar_mt_20261005/data_v2_ext" SCALES = ROOT / "artifacts/coreml_20261007/norm_scales.json" NANO = ROOT / "models/nanosakura_2_2_188m" class OursGraph: def __init__(self, buckets=(32, 64, 128), T: int = 128, dev: str = "cuda", dt=torch.float16, graphs: bool = True): from run_ours import Ours import export_coreml as ex o = Ours(CKPT, str(ENC), DATA, "cpu") m = o.model.float().eval() saved = json.loads(SCALES.read_text()) for n, mod in m.named_modules(): if n in saved: mod._s = saved[n]["s"] self.tok, self.sp, self.dat_ids, self.step = o.tok, o.sp, o.dat_ids, o.step self.buckets, self.T, self.M = buckets, T, 2 + max(buckets) self.dt = dt self.encs = {L: ex.EncoderStatic(m, L).to(dev, dt).eval() for L in buckets} self.dec = ex.DecoderExport(m, 1.0).to(dev, dt).eval() self.tok_emb = m.encoder.embeddings.tok_embeddings.to(dev, dt) self.emb_s = (m.emb.weight.detach() * math.sqrt(m.d)).to(dev, dt) self.pos = m.pos.weight.detach().to(dev, dt) H, hd = m.layers[0].h, m.layers[0].hd self.rules = [(p2, p1, mk) for p2, p1, mk in m.kana_byte_rules(o.byte_compact, self.emb_s.shape[0], dev)] z = lambda *s: torch.zeros(*s, device=dev, dtype=dt) # noqa: E731 self.cross = [z(1, H, self.M, hd) for _ in range(4)] self.cmask = torch.full((1, 1, 1, self.M), ex.NEG, device=dev, dtype=dt) self.caches = [z(1, H, T, hd) for _ in range(4)] self.smask = torch.full((1, 1, 1, T), ex.NEG, device=dev, dtype=dt) self.ids = {L: torch.full((1, L), 3, dtype=torch.long, device=dev) for L in buckets} self.kmask = {L: torch.zeros((1, 1, 1, L), device=dev, dtype=dt) for L in buckets} lt = lambda v: torch.full((1,), v, dtype=torch.long, device=dev) # noqa: E731 self.t, self.cur, self.prev1, self.prev2, self.nxt = lt(0), lt(BOS), lt(-1), lt(-1), lt(0) self.NEG = ex.NEG t0 = time.perf_counter() if graphs: self.g_enc = {L: self._capture(lambda L=L: self._enc(L)) for L in buckets} self._reset() self.g_dec = self._capture(self._dec_step) else: # debug path (CPU / no capture): same functions, eager self.g_enc = {L: type("G", (), {"replay": staticmethod(lambda L=L: self._enc(L))})() for L in buckets} self.g_dec = type("G", (), {"replay": staticmethod(self._dec_step)})() self.capture_s = round(time.perf_counter() - t0, 1) @torch.no_grad() def _enc(self, L): outs = self.encs[L](self.tok_emb(self.ids[L]), self.kmask[L]) for buf, x in zip(self.cross, outs): buf[:, :, :2 + L].copy_(x) @torch.no_grad() def _dec_step(self): x = (self.emb_s[self.cur] + self.pos[self.t])[None] # [1, 1, d] logits, *news = self.dec(x, *self.caches, self.smask, *self.cross, self.cmask) lg = logits.view(-1).float() for p2, p1, mk in self.rules: hit = self.prev1.eq(p1) if p2 is None else self.prev1.eq(p1) & self.prev2.eq(p2) lg = lg.masked_fill(hit & mk, float("-inf")) nxt = lg.argmax().view(1) for c, nw in zip(self.caches, news): c.index_copy_(2, self.t, nw) self.smask.index_fill_(3, self.t, 0.0) self.prev2.copy_(self.prev1) self.prev1.copy_(nxt) self.cur.copy_(nxt) self.nxt.copy_(nxt) self.t.add_(1) def _capture(self, fn): s = torch.cuda.Stream() s.wait_stream(torch.cuda.current_stream()) with torch.cuda.stream(s): for _ in range(3): self._reset() fn() torch.cuda.current_stream().wait_stream(s) self._reset() g = torch.cuda.CUDAGraph() with torch.cuda.graph(g): fn() return g def _reset(self): self.smask.fill_(self.NEG) self.t.zero_() self.cur.fill_(BOS) self.prev1.fill_(-1) self.prev2.fill_(-1) @torch.no_grad() def translate(self, texts): text, = texts ids = self.tok(text, add_special_tokens=True, truncation=True, max_length=256)["input_ids"] n = len(ids) L = next((b for b in self.buckets if b >= n), self.buckets[-1]) ids, n = ids[:L], min(n, L) a = torch.full((1, L), 3, dtype=torch.long) a[0, :n] = torch.tensor(ids) km = torch.zeros((1, 1, 1, L), dtype=self.dt) km[..., n:] = self.NEG cm = torch.full((1, 1, 1, self.M), self.NEG, dtype=self.dt) cm[..., :2 + n] = 0.0 self.ids[L].copy_(a, non_blocking=True) self.kmask[L].copy_(km, non_blocking=True) self.cmask.copy_(cm, non_blocking=True) self.g_enc[L].replay() self._reset() out = [] for _ in range(min(3 * n + 10, self.T)): self.g_dec.replay() v = int(self.nxt.item()) if v in (EOS, PAD): break out.append(v) self.last = {"ntok": n, "nout": len(out)} return [decode_ids(out, self.dat_ids, self.sp)] def load(system: str): if system == "ours_eager": from run_ours import Ours return Ours(CKPT, str(ENC), DATA, "cuda") if system == "ours_graph": return OursGraph() if system == "ours_graph32": return OursGraph(dt=torch.float32) if system == "nano": from run_ours import Nano return Nano(NANO, "cuda") from run_galtransl import Client return Client(os.environ.get("GAL_URL", "http://127.0.0.1:18080"), 20261006) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--system", required=True, choices=("ours_eager", "ours_graph", "ours_graph32", "nano", "gal")) ap.add_argument("--sample", type=Path, default=Path(__file__).with_name("sample500.jsonl")) ap.add_argument("--out", type=Path, required=True) ap.add_argument("--warm", type=int, default=20) ap.add_argument("--limit", type=int, default=0) args = ap.parse_args() args.out.mkdir(parents=True, exist_ok=True) rows = [json.loads(x) for x in open(args.sample, encoding="utf-8")][:args.limit or None] t0 = time.perf_counter() m = load(args.system) load_s = round(time.perf_counter() - t0, 1) gal = args.system == "gal" def one(text): if gal: r = m(text) return r["hyp"], r["ms"], {k: r.get(k) for k in ("prompt_n", "prompt_ms", "predicted_n", "predicted_ms")} torch.cuda.synchronize() t = time.perf_counter() h = m.translate([text])[0] torch.cuda.synchronize() return h, round(1000 * (time.perf_counter() - t), 2), dict(getattr(m, "last", {})) for r in rows[:args.warm]: one(r["clean"]) res = [] with open(args.out / f"{args.system}.jsonl", "w", encoding="utf-8") as f: for r in rows: h, ms, extra = one(r["clean"]) rec = {"id": r["id"], "hyp": h, "ms": ms, **extra} res.append(rec) f.write(json.dumps(rec, ensure_ascii=False) + "\n") a = np.array([x["ms"] for x in res]) cpu = subprocess.run("lscpu | grep 'Model name' | head -1", shell=True, capture_output=True, text=True).stdout summ = {"system": args.system, "n": len(a), "warm": args.warm, "p50_ms": round(float(np.percentile(a, 50)), 1), "p90_ms": round(float(np.percentile(a, 90)), 1), "mean_ms": round(float(a.mean()), 1), "total_s": round(float(a.sum()) / 1000, 1), "load_s": load_s, "capture_s": getattr(m, "capture_s", None), "gpu": torch.cuda.get_device_name() if torch.cuda.is_available() else None, "cpu": cpu.split(":", 1)[-1].strip(), "torch": torch.__version__, "python": platform.python_version(), "peak_gib": round(torch.cuda.max_memory_allocated() / 2**30, 2) if not gal else None} (args.out / f"{args.system}.summary.json").write_text(json.dumps(summ, ensure_ascii=False, indent=1)) print(json.dumps(summ, ensure_ascii=False), flush=True) if __name__ == "__main__": main()