tmickleydoyle commited on
Commit
b8f3723
·
verified ·
1 Parent(s): b46c110

Add performance benchmarks: 6.2 s CPU compile, speed, held-out accuracy

Browse files
Files changed (3) hide show
  1. README.md +57 -0
  2. docs/benchmarks.json +47 -0
  3. scripts/benchmark.py +193 -0
README.md CHANGED
@@ -49,6 +49,61 @@ Full construction details are in [docs/how-it-works.md](docs/how-it-works.md).
49
  The last rows show its main weakness. It sees only 4 tokens of context, so longer
50
  completions can drift between topics.
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  ## Use it with transformers
53
 
54
  ```python
@@ -161,6 +216,7 @@ pip install -r requirements.txt
161
  python scripts/fetch_coding_prompts.py # optional: re-download data/coding_prompts.txt
162
  python build_weights.py # writes config, weights and tokenizer
163
  python test_model.py # HF loading, n-gram exactness, pipeline, TypeScript parity
 
164
  ```
165
 
166
  Tested with transformers 4.52.4 and 5.22.1, Bun 1.3 and Node 24.
@@ -180,6 +236,7 @@ excerpts from OpenAssistant oasst2 (Apache-2.0), MBPP (CC-BY-4.0) and CoNaLa (MI
180
  | `build_weights.py` | the compiler that writes every file above |
181
  | `phrases/*.txt`, `data/coding_prompts.txt` | source text |
182
  | `scripts/fetch_coding_prompts.py`, `data/SOURCES.md` | data download and attribution |
 
183
  | `test_model.py` | verification suite |
184
  | `model.ts`, `autocomplete.ts`, `ui.ts`, `ui.html` | zero-dependency TypeScript runtime, CLI and web UI |
185
  | `docs/how-it-works.md` | full construction details |
 
49
  The last rows show its main weakness. It sees only 4 tokens of context, so longer
50
  completions can drift between topics.
51
 
52
+ ## Performance: why compile a model instead of training one
53
+
54
+ Measured with `python scripts/benchmark.py` on a 10-core Apple Silicon laptop (32 GB RAM).
55
+ CPU only, no GPU. Raw numbers are in [`docs/benchmarks.json`](docs/benchmarks.json).
56
+
57
+ **Building it**
58
+
59
+ | | |
60
+ |---|---|
61
+ | Compile all 380,290,320 weights from text | **6.2 s** (tokenize 0.5 s, build tensors 2.7 s, write 1.5 GB) |
62
+ | Text in, model out, from scratch | about 10 s to download the data + 6 s to compile |
63
+ | Peak memory while compiling | 2.3 GB |
64
+ | GPU hours / compute cost | **0 / $0** |
65
+ | Reproducible | two rebuilds give a byte-identical `model.safetensors` (sha256 `93c23040…`) |
66
+ | Exact | matches its reference n-gram to within 2.5e-5 nats |
67
+
68
+ For scale: the sibling experiment [tiny-research](https://tiny-research.vercel.app/)
69
+ trained a 34M model with a 5-minute budget on an RTX 4090. Compiling this 380M model takes
70
+ 6 seconds on a laptop CPU.
71
+
72
+ **Using it**
73
+
74
+ | | |
75
+ |---|---|
76
+ | PyTorch (CPU, 8 threads) | 70 ms per next word; 16 tokens/s greedy generation; 0.3 s to load |
77
+ | Plain TypeScript (Bun, no ML library) | 0.44 s per token; 0.4 s to load |
78
+
79
+ **How good is it on text it has never seen?** The benchmark compiles the model without a
80
+ fixed 10% of the coding sentences (2,040 lines, 35,039 tokens), then scores each next token:
81
+
82
+ | | |
83
+ |---|---|
84
+ | Top-1 next-token accuracy | **25.8%** |
85
+ | Top-3 next-token accuracy | **39.6%** |
86
+ | Next token outside the 3,000-word vocabulary | 8.8% (always counted as a miss) |
87
+
88
+ On the source phrases themselves it completes almost every line exactly.
89
+
90
+ **Why build a model like this**
91
+
92
+ - **Iteration in seconds:** edit a `.txt` file, rebuild, and test in under 10 s. There is no
93
+ training run, no hyperparameter tuning, and no GPU.
94
+ - **Auditable:** every MLP neuron is one context from the source text, so you can trace any
95
+ prediction back to the phrases that caused it.
96
+ - **Editable:** to remove a behavior, delete the line and recompile. Nothing has to be
97
+ fine-tuned away.
98
+ - **Deterministic and exact:** the same text always gives the same bytes, and the network
99
+ computes a known formula.
100
+ - **Standard format:** it's a plain `LlamaForCausalLM`, so it works with `transformers`,
101
+ `pipeline`, and any runtime that loads Llama safetensors.
102
+
103
+ **What it costs you:** it doesn't generalize like a trained model, and only 7% of the weights
104
+ are non-zero (26.8M of 380M), so the dense file is mostly empty space. A sparse runtime could
105
+ be 10× smaller and faster.
106
+
107
  ## Use it with transformers
108
 
109
  ```python
 
216
  python scripts/fetch_coding_prompts.py # optional: re-download data/coding_prompts.txt
217
  python build_weights.py # writes config, weights and tokenizer
218
  python test_model.py # HF loading, n-gram exactness, pipeline, TypeScript parity
219
+ python scripts/benchmark.py # compile time, speed, held-out accuracy -> docs/benchmarks.json
220
  ```
221
 
222
  Tested with transformers 4.52.4 and 5.22.1, Bun 1.3 and Node 24.
 
236
  | `build_weights.py` | the compiler that writes every file above |
237
  | `phrases/*.txt`, `data/coding_prompts.txt` | source text |
238
  | `scripts/fetch_coding_prompts.py`, `data/SOURCES.md` | data download and attribution |
239
+ | `scripts/benchmark.py`, `docs/benchmarks.json` | performance measurements |
240
  | `test_model.py` | verification suite |
241
  | `model.ts`, `autocomplete.ts`, `ui.ts`, `ui.html` | zero-dependency TypeScript runtime, CLI and web UI |
242
  | `docs/how-it-works.md` | full construction details |
docs/benchmarks.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hardware": {
3
+ "cpu": "Darwin arm64",
4
+ "cores": 10,
5
+ "ram_gb": 32,
6
+ "gpu": "none used",
7
+ "python": "3.13.4",
8
+ "torch": "2.15.0"
9
+ },
10
+ "compile": {
11
+ "compile_seconds": 6.2,
12
+ "compile_runs": [
13
+ 6.4,
14
+ 6.2
15
+ ],
16
+ "peak_memory_gb": 2.3,
17
+ "byte_identical_rebuild": true,
18
+ "model_sha256": "93c23040ccf6393710b0ee1d2fff98d57dc4193a27c4911cac7374e2c28f50f6",
19
+ "file_gb": 1.52,
20
+ "tokenize_seconds": 0.49,
21
+ "build_tensors_seconds": 2.74,
22
+ "source_lines": 21106,
23
+ "params": 380290320,
24
+ "nonzero_params": 26792230,
25
+ "contexts_seen": 434284,
26
+ "neurons": 24774
27
+ },
28
+ "pytorch": {
29
+ "load_seconds": 0.3,
30
+ "next_word_ms": 70.3,
31
+ "generate_tokens_per_second": 16.4,
32
+ "threads": 8
33
+ },
34
+ "typescript": {
35
+ "runtime": "bun, plain TypeScript, no ML library",
36
+ "load_seconds": 0.4,
37
+ "seconds_per_token": 0.44
38
+ },
39
+ "max_logprob_error_vs_ngram": 2.4948840973593178e-05,
40
+ "heldout": {
41
+ "heldout_lines": 2040,
42
+ "heldout_tokens": 35039,
43
+ "next_token_top1": 0.258,
44
+ "next_token_top3": 0.396,
45
+ "oov_rate": 0.088
46
+ }
47
+ }
scripts/benchmark.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Measure compile cost, inference speed, exactness and held-out accuracy.
2
+
3
+ Writes docs/benchmarks.json. Run from the repo root after build_weights.py:
4
+ python scripts/benchmark.py
5
+ """
6
+
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import platform
11
+ import random
12
+ import resource
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ import zlib
18
+ from pathlib import Path
19
+
20
+ import torch
21
+
22
+ ROOT = Path(__file__).resolve().parent.parent
23
+ sys.path.insert(0, str(ROOT))
24
+ import build_weights as bw # noqa: E402
25
+
26
+ PROMPTS = [
27
+ "Once upon a", "I hope this email", "The quick brown", "Thank you for", "Looking forward to",
28
+ "In Python,", "How to convert a", "Write a python function to", "You can use the", "Here is an example of",
29
+ ]
30
+
31
+
32
+ def sha256(path: Path) -> str:
33
+ h = hashlib.sha256()
34
+ with open(path, "rb") as f:
35
+ for chunk in iter(lambda: f.read(1 << 24), b""):
36
+ h.update(chunk)
37
+ return h.hexdigest()
38
+
39
+
40
+ def hardware() -> dict:
41
+ cpu = ""
42
+ if sys.platform == "darwin":
43
+ cpu = subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, text=True).stdout.strip()
44
+ cpu = cpu or f"{platform.system()} {platform.machine()}"
45
+ try:
46
+ ram = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
47
+ except (ValueError, OSError):
48
+ ram = 0
49
+ return {"cpu": cpu, "cores": os.cpu_count(), "ram_gb": round(ram / 2**30), "gpu": "none used",
50
+ "python": platform.python_version(), "torch": torch.__version__}
51
+
52
+
53
+ def compile_runs(n: int = 2) -> dict:
54
+ """Full `python build_weights.py` runs: wall time, peak memory, determinism."""
55
+ times, hashes = [], []
56
+ for _ in range(n):
57
+ t0 = time.perf_counter()
58
+ subprocess.run([sys.executable, "build_weights.py"], cwd=ROOT, check=True, capture_output=True)
59
+ times.append(time.perf_counter() - t0)
60
+ hashes.append(sha256(ROOT / "model.safetensors"))
61
+ peak = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
62
+ peak_gb = peak / 2**30 if sys.platform == "darwin" else peak / 2**20
63
+ return {
64
+ "compile_seconds": round(min(times), 1),
65
+ "compile_runs": [round(t, 1) for t in times],
66
+ "peak_memory_gb": round(peak_gb, 1),
67
+ "byte_identical_rebuild": len(set(hashes)) == 1,
68
+ "model_sha256": hashes[0],
69
+ "file_gb": round((ROOT / "model.safetensors").stat().st_size / 1e9, 2),
70
+ }
71
+
72
+
73
+ def compile_stages() -> dict:
74
+ t0 = time.perf_counter()
75
+ phrases, data = bw.load_phrases(), bw.load_data()
76
+ t1 = time.perf_counter()
77
+ _, ngram, state, _, info = bw.build(phrases, data)
78
+ t2 = time.perf_counter()
79
+ nonzero = sum(int((t != 0).sum()) for t in state.values())
80
+ return {
81
+ "tokenize_seconds": round(t1 - t0, 2),
82
+ "build_tensors_seconds": round(t2 - t1, 2),
83
+ "source_lines": len(phrases) + len(data),
84
+ "params": info["params"],
85
+ "nonzero_params": nonzero,
86
+ "contexts_seen": info["contexts_seen"],
87
+ "neurons": info["neurons"],
88
+ }
89
+
90
+
91
+ def torch_inference() -> dict:
92
+ from transformers import AutoModelForCausalLM, AutoTokenizer
93
+
94
+ t0 = time.perf_counter()
95
+ tok = AutoTokenizer.from_pretrained(ROOT)
96
+ model = AutoModelForCausalLM.from_pretrained(ROOT).eval()
97
+ load = time.perf_counter() - t0
98
+
99
+ with torch.no_grad():
100
+ ids = tok("Write a python function to", return_tensors="pt")
101
+ model(**ids) # warm-up
102
+ t0 = time.perf_counter()
103
+ for _ in range(20):
104
+ model(**ids)
105
+ forward_ms = (time.perf_counter() - t0) / 20 * 1000
106
+
107
+ new_tokens, t0 = 0, time.perf_counter()
108
+ for p in PROMPTS:
109
+ ids = tok(p, return_tensors="pt")
110
+ out = model.generate(**ids, max_new_tokens=16)
111
+ new_tokens += out.shape[1] - ids["input_ids"].shape[1]
112
+ gen = time.perf_counter() - t0
113
+ return {
114
+ "load_seconds": round(load, 1),
115
+ "next_word_ms": round(forward_ms, 1),
116
+ "generate_tokens_per_second": round(new_tokens / gen, 1),
117
+ "threads": torch.get_num_threads(),
118
+ }
119
+
120
+
121
+ def ts_inference() -> dict | None:
122
+ bun = shutil.which("bun")
123
+ if not bun:
124
+ return None
125
+ t0 = time.perf_counter()
126
+ subprocess.run([bun, "autocomplete.ts", "--json"], cwd=ROOT, check=True, capture_output=True)
127
+ load = time.perf_counter() - t0
128
+ t0 = time.perf_counter()
129
+ out = subprocess.run([bun, "autocomplete.ts", "--json", *PROMPTS], cwd=ROOT, check=True, capture_output=True, text=True)
130
+ total = time.perf_counter() - t0 - load
131
+ words = sum(len(bw.pieces(r["completion"])) + 1 for r in json.loads(out.stdout)) # +1: the final EOS step
132
+ return {"runtime": "bun, plain TypeScript, no ML library", "load_seconds": round(load, 1), "seconds_per_token": round(total / words, 2)}
133
+
134
+
135
+ def exactness() -> float:
136
+ from transformers import AutoModelForCausalLM
137
+
138
+ _, vocab, ngram, _, _, _ = bw.compile_model()
139
+ model = AutoModelForCausalLM.from_pretrained(ROOT).eval()
140
+ rng, worst = random.Random(0), 0.0
141
+ with torch.no_grad():
142
+ for _ in range(50):
143
+ ids = [0] + [rng.randrange(2, len(vocab)) for _ in range(rng.randint(1, 80))]
144
+ got = model(torch.tensor([ids])).logits[0, -1].double().log_softmax(-1)
145
+ worst = max(worst, (got - ngram.reference_logits(ids).log_softmax(-1)).abs().max().item())
146
+ return worst
147
+
148
+
149
+ def heldout(frac: float = 0.1) -> dict:
150
+ """Compile without 10% of the data lines, then score next-word prediction on them."""
151
+ phrases, data = bw.load_phrases(), bw.load_data()
152
+ test = [t for t in data if zlib.crc32(" ".join(t).encode()) % 100 < frac * 100]
153
+ train = [t for t in data if zlib.crc32(" ".join(t).encode()) % 100 >= frac * 100]
154
+ vocab, ngram, _, _, _ = bw.build(phrases, train)
155
+ index = {p: i for i, p in enumerate(vocab)}
156
+ top1 = top3 = total = oov = 0
157
+ for toks in test:
158
+ ids = [0] + [index.get(p, 1) for p in toks] + [0]
159
+ for t in range(len(ids) - 1):
160
+ target = ids[t + 1]
161
+ total += 1
162
+ if target == 1:
163
+ oov += 1
164
+ continue
165
+ ranked = ngram.reference_logits(ids[: t + 1]).topk(3).indices.tolist()
166
+ top1 += ranked[0] == target
167
+ top3 += target in ranked
168
+ return {
169
+ "heldout_lines": len(test),
170
+ "heldout_tokens": total,
171
+ "next_token_top1": round(top1 / total, 3),
172
+ "next_token_top3": round(top3 / total, 3),
173
+ "oov_rate": round(oov / total, 3),
174
+ }
175
+
176
+
177
+ def main() -> None:
178
+ results = {"hardware": hardware()}
179
+ print("compiling (2 full runs)...", flush=True)
180
+ results["compile"] = {**compile_runs(), **compile_stages()}
181
+ print("inference...", flush=True)
182
+ results["pytorch"] = torch_inference()
183
+ results["typescript"] = ts_inference()
184
+ results["max_logprob_error_vs_ngram"] = exactness()
185
+ print("held-out accuracy...", flush=True)
186
+ results["heldout"] = heldout()
187
+ out = ROOT / "docs" / "benchmarks.json"
188
+ out.write_text(json.dumps(results, indent=2) + "\n")
189
+ print(json.dumps(results, indent=2))
190
+
191
+
192
+ if __name__ == "__main__":
193
+ main()