File size: 6,051 Bytes
12eb3a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | #!/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() |