markuz89's picture
MiniJEV v2 model export
45507bb verified
Raw History Blame Contribute Delete
6.1 kB
"""Inference performance: latency percentiles, max throughput, paced-rate CPU/RAM cost.
Latency is end-to-end `decide()` (state encoding + tokenisation + model), the
number an embedding application actually pays. Max throughput is measured
unthrottled; the paced tests (1..1000 decisions/s) show what a given decision
rate costs in CPU, which is what matters when MiniJEV lives inside other software.
"""
from __future__ import annotations
import os
import time
from pathlib import Path
import numpy as np
import psutil
def _pct(xs: list[float]) -> dict:
a = np.array(xs) * 1000
return {"p50_ms": round(float(np.percentile(a, 50)), 4), "p95_ms": round(float(np.percentile(a, 95)), 4),
"p99_ms": round(float(np.percentile(a, 99)), 4), "mean_ms": round(float(a.mean()), 4)}
def latency(decider, states, question, choices, n: int = 2000, warmup: int = 100) -> dict:
for i in range(warmup):
decider.decide(states[i % len(states)], question, choices)
xs = []
for i in range(n):
t = time.perf_counter()
decider.decide(states[i % len(states)], question, choices)
xs.append(time.perf_counter() - t)
# model-only latency (pre-encoded input), to separate pre-processing cost
enc = [decider.encode([s]) for s in states[:64]]
ys = []
for i in range(n):
t = time.perf_counter()
decider.probs(None, question, choices, encoded=enc[i % len(enc)])
ys.append(time.perf_counter() - t)
return {"end_to_end": _pct(xs), "model_only": _pct(ys),
"max_decisions_per_sec_batch1": round(1.0 / float(np.mean(xs)), 1)}
def batched_throughput(decider, states, question, choices, batch: int = 64, seconds: float = 3.0) -> dict:
enc = decider.encode(states[:batch])
decider.probs(None, question, choices, encoded=enc)
n, t0 = 0, time.perf_counter()
while time.perf_counter() - t0 < seconds:
decider.probs(None, question, choices, encoded=enc)
n += len(enc)
return {"batch": len(enc), "decisions_per_sec": round(n / (time.perf_counter() - t0), 1)}
def paced(decider, states, question, choices, rate: float, seconds: float = 3.0) -> dict:
"""Issue decisions at a fixed rate; measure achieved rate, latency and process CPU."""
proc = psutil.Process(os.getpid())
period = 1.0 / rate
n_target = max(3, int(rate * seconds))
lat = []
proc.cpu_percent(None)
cpu_t0 = proc.cpu_times()
t0 = time.perf_counter()
next_t = t0
for i in range(n_target):
now = time.perf_counter()
if next_t > now:
time.sleep(next_t - now)
t = time.perf_counter()
decider.decide(states[i % len(states)], question, choices)
lat.append(time.perf_counter() - t)
next_t += period
wall = max(time.perf_counter() - t0, n_target * period) # a paced run lasts n_target periods
cpu_t1 = proc.cpu_times()
cpu_s = (cpu_t1.user - cpu_t0.user) + (cpu_t1.system - cpu_t0.system)
achieved = n_target / wall
return {"target_per_sec": rate, "achieved_per_sec": round(achieved, 1),
"sustained": achieved >= 0.95 * rate, **_pct(lat),
"cpu_percent_of_one_core": round(100 * cpu_s / wall, 1),
"rss_mb": round(proc.memory_info().rss / 2**20, 1)}
def benchmark_decider(path: str | Path, task=None, threads: int = 1, rates=(1, 10, 100, 500, 1000),
n_latency: int = 2000, quick: bool = False) -> dict:
from minijev.inference.runtime import MiniJEV
proc = psutil.Process(os.getpid())
rss0 = proc.memory_info().rss
dec = MiniJEV.load(path, threads=threads)
rss1 = proc.memory_info().rss
task = task or dec.task
states = task.example_states(256, seed=3)
q, ch = task.default_question, list(task.default_choices)
res = {"model": str(path), "file_size_mb": round(Path(path).stat().st_size / 2**20, 3), "threads": threads,
"model_load_rss_mb": round((rss1 - rss0) / 2**20, 1)}
if hasattr(dec, "model"):
res["params"] = dec.model.num_parameters()
elif hasattr(dec, "meta"):
res["params"] = dec.meta.get("params")
res["latency"] = latency(dec, states, q, ch, n=200 if quick else n_latency)
res["batched"] = batched_throughput(dec, states, q, ch, seconds=1.0 if quick else 3.0)
max_rate = res["latency"]["max_decisions_per_sec_batch1"]
res["paced"] = []
for r in rates:
secs = 2.0 if quick else (5.0 if r <= 10 else 3.0)
if r > 1.5 * max_rate:
res["paced"].append({"target_per_sec": r, "achieved_per_sec": None, "sustained": False,
"note": f"above measured max ({max_rate}/s)"})
continue
if r == 1 and quick:
secs = 3.0
res["paced"].append(paced(dec, states, q, ch, r, secs))
res["process_rss_mb"] = round(proc.memory_info().rss / 2**20, 1)
return res
def format_perf(res: dict) -> str:
L = res["latency"]
lines = [f"{res['model']} ({res['file_size_mb']} MB, params={res.get('params')}, threads={res['threads']})",
f" latency end-to-end p50 {L['end_to_end']['p50_ms']:.3f} ms p95 {L['end_to_end']['p95_ms']:.3f} ms "
f"p99 {L['end_to_end']['p99_ms']:.3f} ms",
f" latency model-only p50 {L['model_only']['p50_ms']:.3f} ms p95 {L['model_only']['p95_ms']:.3f} ms "
f"p99 {L['model_only']['p99_ms']:.3f} ms",
f" max decisions/s: batch=1 {L['max_decisions_per_sec_batch1']:,.0f} "
f"batch={res['batched']['batch']} {res['batched']['decisions_per_sec']:,.0f}",
" paced: target achieved p50ms p99ms cpu%(1 core) rss MB"]
for p in res["paced"]:
if p.get("achieved_per_sec") is None:
lines.append(f" {p['target_per_sec']:>6} -- {p['note']}")
else:
lines.append(f" {p['target_per_sec']:>6} {p['achieved_per_sec']:>8} {p['p50_ms']:>6.3f} "
f"{p['p99_ms']:>6.3f} {p['cpu_percent_of_one_core']:>12} {p['rss_mb']:>6}")
return "\n".join(lines)