Translation
MLX
Core ML
ONNX
Safetensors
Japanese
Chinese
jmangatranslator-fast
manga
japanese
chinese
Instructions to use muscgab/JMangaTranslator-Fast with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use muscgab/JMangaTranslator-Fast with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download muscgab/JMangaTranslator-Fast --local-dir JMangaTranslator-Fast
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Download src/bench.py from muscgab/JMangaTranslator-Fast: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/muscgab/JMangaTranslator-Fast/resolve/main/src/bench.py
- Command line
-
hf download hf://muscgab/JMangaTranslator-Fast/src/bench.py
-
curl -L -o bench.py https://huggingface.co/muscgab/JMangaTranslator-Fast/resolve/main/src/bench.py
10.4 kB
| #!/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: <out>/<system>.jsonl (id, hyp, ms, ...) and <out>/<system>.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) | |
| 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) | |
| 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) | |
| 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() | |