minicpm47woek_EPQ / bench /run_negative.py
HelloSun's picture
EPM: expert paging CPU inference for MiniCPM-V-4.7-35B-A3B Q4_K_M
12eb3a5 verified
Raw History Blame Contribute Delete
6.05 kB
#!/usr/bin/env python3
"""Negative results: settings that looked promising and did not pay off.
Two knobs are measured end to end under the same 8 GiB ceiling as the headline:
prefetch
Paging the next layer's experts while the current layer computes. The pager
can predict them from the previous token's routing, but only a small share
of the predicted experts are actually used, so the extra reads are mostly
waste. ``SDQ_PREFETCH_LAYER_AHEAD=1`` turns it on.
threads
Only n_expert_used = 8 experts per token can run in parallel inside one
layer, so more threads mostly contend for the same rows.
Output: bench/results/strict_prefetch.json and bench/results/strict_threads.json
"""
from __future__ import annotations
import argparse
import json
import os
import re
import statistics
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
REPO = HERE.parent
PROMPTS = [
"Explain in one sentence why SSDs are slow.",
"請用一句話介紹你自己。",
"Write a short paragraph about SSD caching.",
]
def run(strict8g: Path, binary: str, model: str, prompt: str, threads: int,
n_predict: int, ctx: int, ubatch: int, budget_mb: int,
record: Path, reserve_mb: int, env_extra: dict) -> dict:
cmd = [
sys.executable, str(strict8g),
"--limit-mb", str(budget_mb), "--record", str(record), "--",
binary, "-m", model, "-p", prompt, "-n", str(n_predict),
"-c", str(ctx), "-t", str(threads), "-b", str(ubatch),
"--temp", "0", "--no-chat", "--no-interactive",
"--ram-budget-mb", str(budget_mb),
"--stats-file", str(record.with_suffix(".stats")),
]
env = dict(os.environ, SDQ_RESERVE_MB=str(reserve_mb))
env.update(env_extra)
p = subprocess.run(cmd, capture_output=True, text=True, env=env)
out = p.stdout + p.stderr
rec, stats = {}, {}
if record.exists():
try:
rec = json.loads(record.read_text())
except Exception:
pass
sp = record.with_suffix(".stats")
if sp.exists():
for line in sp.read_text().splitlines():
if line.strip().startswith("{"):
stats = json.loads(line)
m = re.search(r"= ([\d.]+) tok/s", out)
return {
"rc": p.returncode,
"strict_ok": bool(rec.get("strict_ok", False)),
"tps": float(m.group(1)) if m else None,
"hit_pct": stats.get("hit_rate"),
"mb_per_token": (stats["ssd_bytes_per_token"] / 1048576.0) if stats.get("ssd_bytes_per_token") else None,
"prefetch_issued": stats.get("prefetch_issued"),
"prefetch_used": stats.get("prefetch_used"),
"peak_anon_mb": rec.get("peak_anon_mb"),
"peak_swap_mb": rec.get("peak_swap_mb"),
}
def sweep(arms: dict, a, label: str, out_name: str) -> None:
strict8g = REPO / "tools" / "strict8g.py"
(HERE / "results").mkdir(parents=True, exist_ok=True)
rows = []
for r in range(a.rounds):
for name, cfg in arms.items():
prompt = PROMPTS[r % len(PROMPTS)]
rec = HERE / "results" / f".{out_name}-{name}-{r}.json"
row = run(strict8g, a.bin, a.model, prompt, cfg["threads"],
a.n_predict, a.ctx, a.ubatch, a.budget_mb, rec,
cfg["reserve_mb"], cfg.get("env", {}))
row["arm"] = name
row["round"] = r
rows.append(row)
print(f"[{r + 1}/{a.rounds}] {name:<14} tps={row['tps']} "
f"hit={row['hit_pct']} MB/tok={row['mb_per_token']} "
f"ok={row['strict_ok']}", flush=True)
rec.unlink(missing_ok=True)
Path(str(rec) + ".stats").with_suffix(".stats").unlink(missing_ok=True)
summary = {}
for name in arms:
tps = [r["tps"] for r in rows if r["arm"] == name and r["tps"]]
summary[name] = {
"n": len(tps),
"median_tps": statistics.median(tps) if tps else None,
"mean_tps": statistics.mean(tps) if tps else None,
"stdev_tps": statistics.stdev(tps) if len(tps) > 1 else None,
"min_tps": min(tps) if tps else None,
"max_tps": max(tps) if tps else None,
"all_strict_ok": all(r["strict_ok"] for r in rows if r["arm"] == name),
}
out = HERE / "results" / out_name
out.write_text(json.dumps({"label": label, "rows": rows, "summary": summary},
indent=2, ensure_ascii=False))
print(json.dumps(summary, indent=2, ensure_ascii=False))
print(f"-> {out}")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--model", required=True)
ap.add_argument("--bin", required=True)
ap.add_argument("--which", choices=["prefetch", "threads"], required=True)
ap.add_argument("--rounds", type=int, default=6)
ap.add_argument("--budget-mb", type=int, default=8192)
ap.add_argument("--reserve-mb", type=int, default=512)
ap.add_argument("--ctx", type=int, default=4096)
ap.add_argument("--ubatch", type=int, default=128)
ap.add_argument("--n-predict", type=int, default=48)
a = ap.parse_args()
if a.which == "prefetch":
arms = {
"prefetch_off": {"threads": 8,
"reserve_mb": a.reserve_mb, "env": {}},
"prefetch_layer_ahead": {"threads": 8, "reserve_mb": a.reserve_mb,
"env": {"SDQ_PREFETCH_LAYER_AHEAD": "1"}},
"prefetch_end_token": {"threads": 8, "reserve_mb": a.reserve_mb,
"env": {"SDQ_PREFETCH": "1",
"SDQ_PREFETCH_LAYER_AHEAD": "0"}},
}
sweep(arms, a, "prefetch", "strict_prefetch.json")
else:
arms = {
f"threads_{t}": {"threads": t, "reserve_mb": a.reserve_mb, "env": {}}
for t in (4, 8, 10, 12, 16)
}
sweep(arms, a, "threads", "strict_threads.json")
if __name__ == "__main__":
main()