LFM2.5-350M-RLCD / rlcd /benchmark.py
notnotsamuel's picture
Release unchanged-weight LFM2.5 parallel constrained inference and benchmarks
a99edfc verified
Raw
History Blame Contribute Delete
6.71 kB
"""python -m rlcd.benchmark --device mps --output results/m2-max.json"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.metadata
import json
import platform
import random
import statistics
import subprocess
import time
from pathlib import Path
import jsonschema
import numpy as np
import torch
from .engine import Engine, MODEL_ID, REVISION
from .tasks import CASES
def evaluate(text, schema, expected):
try:
obj = json.loads(text, parse_constant=lambda s: (_ for _ in ()).throw(ValueError(s)))
syntax = True
except (ValueError, TypeError):
obj, syntax = None, False
compliant = syntax and jsonschema.Draft202012Validator(schema).is_valid(obj)
correct = sum(type(obj.get(k)) is type(v) and obj.get(k) == v for k, v in expected.items()) if isinstance(obj, dict) else 0
return {"syntax_valid": syntax, "schema_compliant": compliant, "correct_fields": correct,
"field_count": len(expected), "exact_match": compliant and correct == len(expected)}
def metadata(engine):
info = {"recorded_at_utc": datetime.now(timezone.utc).isoformat(),
"all_dependencies": {d.metadata["Name"]:d.version for d in importlib.metadata.distributions()},
"platform": platform.platform(), "python": platform.python_version(),
"versions": {k: importlib.metadata.version(k) for k in ["torch", "transformers", "accelerate", "huggingface-hub", "jsonschema", "modal"]},
"device": engine.device, "dtype": engine.dtype, "attention": "eager", "quantization": None,
"model": MODEL_ID, "revision": REVISION, "cuda_runtime": torch.version.cuda,
"mps_available": torch.backends.mps.is_available(),
"source_sha256": {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(Path(__file__).parent.glob("*.py"))}}
if engine.device == "mps":
info["hardware"] = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"], text=True).strip()
info["memory_bytes"] = int(subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True))
if engine.device.startswith("cuda"):
info["hardware"] = torch.cuda.get_device_name()
info["gpu_info"] = subprocess.check_output(["nvidia-smi", "--query-gpu=name,driver_version,memory.total", "--format=csv,noheader"], text=True).strip()
info["tf32_matmul"] = torch.backends.cuda.matmul.allow_tf32
return info
def summarize(rows):
result = {}
for method in ["constrained", "autoregressive"]:
selected = [r for r in rows if r["method"] == method]
times = [r["latency_ms"] for r in selected]
result[method] = {"n": len(times), "latency_mean_ms": statistics.mean(times),
"latency_median_ms": statistics.median(times), "latency_p95_ms": float(np.percentile(times, 95)),
"syntax_valid_rate": statistics.mean(r["syntax_valid"] for r in selected),
"schema_compliant_rate": statistics.mean(r["schema_compliant"] for r in selected),
"field_accuracy": sum(r["correct_fields"] for r in selected) / sum(r["field_count"] for r in selected),
"exact_match_rate": statistics.mean(r["exact_match"] for r in selected)}
result["speedup_ratio_of_mean_latency"] = result["autoregressive"]["latency_mean_ms"] / result["constrained"]["latency_mean_ms"]
return result
def run(device="mps", dtype="float16", warmups=2, repeats=3, limit=None, suite="diagnostic", max_new_tokens=192):
torch.manual_seed(42)
torch.set_num_threads(4)
engine = Engine(device, dtype)
if warmups < 1 or repeats < 2:
raise ValueError("Use at least one warmup and two measured repetitions")
from .stress_tasks import CASES as STRESS_CASES
all_cases = CASES if suite == "diagnostic" else STRESS_CASES
cases = all_cases[:limit] if limit else all_cases
def invoke(method, context, schema):
kwargs = {"max_new_tokens": max_new_tokens} if method == "autoregressive" else {}
return getattr(engine, method)(context, schema, **kwargs)
# Warm every case and both methods, then discard timings.
for _ in range(warmups):
for _, schema, context, _ in cases:
for method in ["constrained", "autoregressive"]:
invoke(method, context, schema)
engine.sync()
rows = []
for rep in range(repeats):
order = list(cases)
random.Random(42 + rep).shuffle(order)
for case_id, schema, context, expected in order:
methods = ["constrained", "autoregressive"]
if (rep + all_cases.index(next(c for c in all_cases if c[0] == case_id))) % 2:
methods.reverse()
for method in methods:
engine.sync()
started = time.perf_counter()
output = invoke(method, context, schema)
engine.sync()
latency = (time.perf_counter() - started) * 1000
row = {"case": case_id, "repeat": rep, "method": method, "latency_ms": latency,
**output, **evaluate(output["text"], schema, expected)}
rows.append(row)
print(f'{device} {rep} {case_id} {method}: {latency:.1f}ms exact={row["exact_match"]}', flush=True)
return {"metadata": metadata(engine), "protocol": {"warmups_per_case_per_method": warmups, "repeats": repeats,
"seed": 42, "max_new_tokens": max_new_tokens, "suite": suite, "timing": "synchronized end-to-end request; includes tokenization, cache copy, model calls, scoring, decode/assembly; excludes model load and validation",
"cases": len(cases), "order": "seeded shuffled cases; alternating paired method order", "batch_size_requests": 1},
"summary": summarize(rows), "rows": rows}
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("--device", default="mps")
p.add_argument("--dtype", default="float16", choices=["float16", "bfloat16", "float32"])
p.add_argument("--warmups", type=int, default=2)
p.add_argument("--repeats", type=int, default=3)
p.add_argument("--limit", type=int)
p.add_argument("--suite", choices=["diagnostic", "stress"], default="diagnostic")
p.add_argument("--max-new-tokens", type=int, default=192)
p.add_argument("--output", default="results/m2-max.json")
args = vars(p.parse_args())
output = Path(args.pop("output"))
result = run(**args)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, indent=2) + "\n")
print(json.dumps(result["summary"], indent=2))