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()