Publications: TR-01 technical report + figure generator + site tab
- docs/publications/TR-01: abstract, substrate, expG/expD + three refutations, architecture, 1.7B continuum, 32B out-of-core, honest prior-art attribution (per novelty_check), limitations, repro - tools/make_pub_figures.py: 4 SVG figures regenerated from results/ (validated palette, collision-checked) - Web: /publications tab, publication cards, /raw asset route, image link rewriting in markdown, figure styling Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 10 changed files with +601 and −0
added
docs/publications/TR-01-margin-gated-deferred-refinement.md
+224 −0
@@ -0,0 +1,224 @@ | ||
| 1 | +--- | |
| 2 | +project: localvm-research | |
| 3 | +document: docs/publications/TR-01 | |
| 4 | +title: "Margin-Gated Deferred Refinement: Streaming Higher-Precision LLM Quality Than Fits in Memory on Consumer Apple Silicon" | |
| 5 | +author: Simon-Pierre Boucher | |
| 6 | +contact: contact@spboucher.ai | |
| 7 | +created: 2026-08-12 | |
| 8 | +status: final | |
| 9 | +type: technical-report | |
| 10 | +number: TR-01 | |
| 11 | +--- | |
| 12 | + | |
| 13 | +# Margin-Gated Deferred Refinement: Streaming Higher-Precision LLM Quality Than Fits in Memory on Consumer Apple Silicon | |
| 14 | + | |
| 15 | +**Technical Report TR-01 · August 12, 2026** | |
| 16 | +**Simon-Pierre Boucher** — contact@spboucher.ai | |
| 17 | +*All experiments in this report are reproducible from the repository: each result | |
| 18 | +carries a commit hash, configuration, seed, and hardware manifest. Raw results, | |
| 19 | +code, and the full research log are browsable on this platform.* | |
| 20 | + | |
| 21 | +## Abstract | |
| 22 | + | |
| 23 | +We ask whether an existing pretrained LLM whose preferred precision does not fit | |
| 24 | +in a consumer Mac's unified memory can still contribute its quality locally. On | |
| 25 | +an Apple M5 Max (48 GB), we first measure the substrate: the internal NVMe | |
| 26 | +sustains ~13.1 GB/s of sequential reads but only 67 MB/s at 4 KiB random QD1, and | |
| 27 | +saturated Metal GPU compute perturbs SSD streaming by less than 5%. We then show | |
| 28 | +that the top-1 logit margin of a low-bit quantized model strongly predicts its | |
| 29 | +disagreements with the full-precision model (AUROC 0.85–0.98), improving with | |
| 30 | +scale, while three plausible fine-grained escalation strategies — sensitive | |
| 31 | +layers, important weight blocks, temporal working sets — are each *refuted* by | |
| 32 | +measurement on a modern dense SwiGLU architecture. These results select a | |
| 33 | +specific runtime: a resident 4-bit base generates optimistically and defers | |
| 34 | +low-margin tokens; a same-checkpoint 8-bit variant, streamed from SSD (layer by | |
| 35 | +layer when it cannot co-reside), verifies windows and rolls back flips. At 1.7B, | |
| 36 | +verified output is statistically indistinguishable from resident-q8 while | |
| 37 | +streaming 237 MB/token — 9× under the checkpoint — with a runtime knob τ trading | |
| 38 | +bytes for quality. At 32B, where q8 (34.8 GB) cannot reside beside the base at | |
| 39 | +all, the system improves judge-scored quality by +0.28 nats over the only | |
| 40 | +configuration that fits, at 3.7 GB/token and 1.7 tok/s. We release all negative | |
| 41 | +results, and we attribute prior art precisely: the components are anticipated | |
| 42 | +(QSpec/QuantSpec; Lever; CATS; SpecExec; UGSD); the assembled composition, its | |
| 43 | +bytes-per-token objective, and its unified-memory instantiation were not found. | |
| 44 | + | |
| 45 | +## 1. Problem | |
| 46 | + | |
| 47 | +A pretrained checkpoint has one size; a machine has another. Standard practice | |
| 48 | +compresses the model until it fits (quantization) and accepts the quality loss, | |
| 49 | +or offloads weights and accepts catastrophic latency. The research question of | |
| 50 | +this project (charter §2) is whether these are really the only options: | |
| 51 | + | |
| 52 | +> Can total model size, resident size, and bytes-read-per-token be decoupled, | |
| 53 | +> so that RAM bounds the *working set* rather than the *model*? | |
| 54 | + | |
| 55 | +This report documents the first architecture selected and validated by our | |
| 56 | +falsification-driven campaign: eight preregistered micro-experiments (three | |
| 57 | +confirmed, three refuted, two substrate/prototype), each with explicit kill | |
| 58 | +criteria, followed by a two-scale prototype and an adversarial novelty check. | |
| 59 | + | |
| 60 | +## 2. The substrate: what an Apple SSD will actually give you | |
| 61 | + | |
| 62 | +Experiment H measured the internal NVMe (AP2048Z, 2 TB) under genuinely cold | |
| 63 | +conditions — after discovering that `F_NOCACHE` does **not** bypass pages already | |
| 64 | +resident in the unified page cache, so naïve "cold" benchmarks measure RAM | |
| 65 | +(42–115 GB/s readings that exceed the device's physics). With test files written | |
| 66 | +uncached and an `iostat` ground-truth log, the envelope is: | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | +**Figure 1.** Cold random-read throughput by block size and queue depth | |
| 71 | +(3 repeats/cell, iostat-validated). The fetch contract for any weight-streaming | |
| 72 | +design on this hardware: **≥ 256 KiB blocks at QD ≥ 4, or sequential**. Saturated | |
| 73 | +MLX matmul load concurrently costs < 5% — I/O and compute overlap freely on | |
| 74 | +unified memory. | |
| 75 | + | |
| 76 | +Two corollaries shaped everything downstream: storage bandwidth is *not* the | |
| 77 | +binding constraint (~650 MB/token is realistic at interactive rates), and the | |
| 78 | +constraint that *is* binding is the access pattern. | |
| 79 | + | |
| 80 | +## 3. Decisions are predictable; fine-grained escalation is not | |
| 81 | + | |
| 82 | +**Margins predict disagreement (expG, confirmed).** Teacher-forcing quantized | |
| 83 | +variants of Qwen3-1.7B over 48 bf16 greedy trajectories (6 domains, 6 144 | |
| 84 | +positions/bit-width), the low-bit model's top-1 logit margin predicts whether its | |
| 85 | +greedy decision matches the reference: AUROC 0.852 (3-bit), 0.898 (4-bit), 0.975 | |
| 86 | +(8-bit), uniform across domains. Disagreements concentrate sharply at small | |
| 87 | +margins (median disagree-margin 0.75 vs agree-margin 6.5 at 4-bit). Naïve affine | |
| 88 | +2-bit quantization, by contrast, destroys the model outright (3% agreement, | |
| 89 | +KL ≈ 11.9) — the known 2–3-bit representational cliff, measured here with an | |
| 90 | +amplitude perplexity tables understate. | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | +**Figure 2.** Escalation needed for 99% agreement falls with both precision and | |
| 95 | +model scale (36.6% → 22.6% at 4-bit going from 1.7B to 8B). The gating economics | |
| 96 | +improve exactly where the architecture is needed — larger models. | |
| 97 | + | |
| 98 | +**Progressive residual coding works (expD, confirmed).** Affine residual ladders | |
| 99 | +(3/3+3/3+3+3 and 4/4+4 bits, group 64) converge monotonically (hidden-state | |
| 100 | +error ÷5 per stage; KL ÷30–60), and a two-tier margin policy over them reaches | |
| 101 | +97.6% agreement while consulting the refinement for only 35% of tokens. At equal | |
| 102 | +*static* bytes, flat quantization is mildly better — the ladder's value is that | |
| 103 | +quality becomes a *runtime* variable, not a compression win. | |
| 104 | + | |
| 105 | +**Three escalation strategies die (expF, expA, expB — refuted).** To make | |
| 106 | +per-token refinement cheap we tested, with preregistered kill criteria: | |
| 107 | +layer-restricted escalation (sensitivity spread across depth groups is only | |
| 108 | +1.5×; repairing the top 29% of layers recovers just 24% of the loss); block-level | |
| 109 | +selection (95% of SwiGLU FFN energy needs 77% of 64-neuron blocks — real | |
| 110 | +neuron-level concentration exists at 20% but is *scattered*, and bundling to | |
| 111 | +SSD-viable granularity destroys it); and temporal working sets (the 95%-energy | |
| 112 | +block set's union covers 99.9% of the layer within 128 tokens; Jaccard between | |
| 113 | +consecutive tokens equals the random-set null). The DejaVu / LLM-in-a-flash | |
| 114 | +paradigm measurably does not transfer to dense modern SwiGLU models. What | |
| 115 | +remains is **batch amortization** — and Figure 1 says sequential amortized | |
| 116 | +streaming is nearly free. | |
| 117 | + | |
| 118 | +## 4. The architecture | |
| 119 | + | |
| 120 | +The evidence selects a specific runtime, assembled from measured parts: | |
| 121 | + | |
| 122 | +1. **Resident base**: the q4 quantization of the model lives in unified memory | |
| 123 | + and generates greedily with a KV cache, recording each decision's margin. | |
| 124 | +2. **Deferral**: decisions with margin < τ are flagged; nothing blocks. | |
| 125 | +3. **Amortized verification sweeps**: every W tokens, the *same checkpoint's* | |
| 126 | + q8 variant teacher-forces the window in one pass, feeding only the unseen | |
| 127 | + suffix into a persistent incremental verify-KV-cache. When q8 fits beside | |
| 128 | + the base (small models), it is simply resident; when it does not, it is | |
| 129 | + **streamed layer-by-layer** from SSD (materialize → compute → re-lazify), | |
| 130 | + one sequential pass over the checkpoint per sweep at the device ceiling. | |
| 131 | +4. **Rollback**: at the first checked flip (margin mode: deferred positions | |
| 132 | + only; verify-all mode: every position), the runtime trims both KV caches, | |
| 133 | + accepts the q8 token, and resumes. | |
| 134 | + | |
| 135 | +τ is a user-facing knob: it prices output quality in verification bytes. | |
| 136 | + | |
| 137 | +## 5. Results | |
| 138 | + | |
| 139 | +### 5.1 Quality–bytes continuum at 1.7B | |
| 140 | + | |
| 141 | +All outputs are scored by the bf16 reference model (mean per-token logprob of | |
| 142 | +the generated text). Token-exact fidelity is *not* a usable target on this | |
| 143 | +hardware: the q8 model itself flips 1.56% of its own greedy tokens between | |
| 144 | +prefill and incremental decode (Metal numerics), so even the reference only | |
| 145 | +"agrees with itself" at ~0.88 sequence similarity over 128 tokens. | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | +**Figure 3.** The τ continuum at Qwen3-1.7B (24 prompts × 128 tokens, W=32): | |
| 150 | +150 MB/token buys 47% of the q4→q8 quality gap, 211 MB/token buys 83%, and | |
| 151 | +verify-all (237 MB/token — 9× under the 2.15 GB checkpoint) is statistically | |
| 152 | +indistinguishable from resident q8 (−0.1297 vs −0.1328). Throughput 69–110 | |
| 153 | +tok/s against 287 (pure q4) and 200 (pure q8). | |
| 154 | + | |
| 155 | +### 5.2 The target regime: 32B on a 48 GB machine | |
| 156 | + | |
| 157 | +At Qwen3-32B, q4 (17.5 GB) is resident and q8 (34.8 GB) **cannot co-reside** | |
| 158 | +— total representation 52 GB on a 48 GB machine. Sweeps stream it sequentially | |
| 159 | +at ~11.6 GB/s (≈ 90% of the device ceiling), 3.0 s per sweep. | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | +**Figure 4.** Qwen3-32B, 12 prompts × 96 tokens, independent 8B-bf16 judge. | |
| 164 | +Verify-all improves quality by +0.28 nats (38% relative) over pure q4 — the | |
| 165 | +only alternative that fits — at 3.72 GB/token logical (9.4× under the streamed | |
| 166 | +checkpoint) and 1.69 tok/s. A window-doubling test (W=64) improved bytes/token | |
| 167 | +by only 5%: the effective inter-sweep distance is bounded by the rollback rate | |
| 168 | +(~1/0.12 ≈ 8–15 tokens), not by W — the identified levers are τ calibration and | |
| 169 | +overlapping sweep I/O with generation. | |
| 170 | + | |
| 171 | +## 6. Relation to prior work (from the adversarial novelty check) | |
| 172 | + | |
| 173 | +Every *component* of this system is anticipated, and we attribute precisely: | |
| 174 | +precision-tiered draft/verify with shared weights (QSpec; Apple's QuantSpec; | |
| 175 | +ML-SpecQD); resident-draft with flash-streamed target on memory-limited devices | |
| 176 | +(Lever, 2026; CATS, 2026 — the closest systems, both published after our | |
| 177 | +literature sweep began); uncertainty-gated verification skipping (U-HLM, UGSD — | |
| 178 | +against cloud verifiers); window-amortized verification economics (SpecExec); | |
| 179 | +per-layer weight streaming in MLX (mlx-flash); mmap-based weight paging | |
| 180 | +(llama.cpp, proposed-but-unbuilt resident-speculator variant in discussion | |
| 181 | +#10466). Against this, four narrow claims survived adversarial search: | |
| 182 | +**(N1)** margin-gating where the gated resource is the *weight-streaming I/O* | |
| 183 | +of the same model's higher-precision variant on a single machine; **(N2)** | |
| 184 | +bytes-streamed-per-token vs checkpoint size as the explicit objective, with τ | |
| 185 | +exposed as a bytes↔quality knob; **(N3)** the training-free same-checkpoint | |
| 186 | +q4/q8 split demonstrated where the verifier physically cannot reside; **(N4)** | |
| 187 | +the unified-memory/Metal instantiation with persistent incremental verify-KV | |
| 188 | +cache and trim rollback. The margin is thin and shrinking; we recommend | |
| 189 | +re-running the novelty check before any external submission. | |
| 190 | + | |
| 191 | +## 7. Limitations | |
| 192 | + | |
| 193 | +Prototype speed (1.69 tok/s at 32B) is below interactive. The 32B quality | |
| 194 | +ceiling is unmeasurable on this machine by construction; the 8B judge is a | |
| 195 | +proxy, with no error bars at n=12; no task-level benchmarks (e.g., GSM8K) have | |
| 196 | +been run yet. We have not baselined against resident q5/q6 at 32B — which *do* | |
| 197 | +fit in 48 GB and may dominate this system at this scale; the architecture's | |
| 198 | +necessary regime is larger models (70B-class), not yet tested. All experiments | |
| 199 | +used one model family (Qwen3) and one machine. | |
| 200 | + | |
| 201 | +## 8. Reproducibility | |
| 202 | + | |
| 203 | +Hardware: Apple M5 Max, 6P+12E CPU, 40-core GPU, 48 GB unified memory, APPLE | |
| 204 | +SSD AP2048Z 2 TB, macOS 27.0. Stack: Python 3.14, MLX 0.32.0, mlx-lm 0.31.3. | |
| 205 | +Every result JSON embeds this manifest. Code paths: | |
| 206 | +`experiments/micro/exp{A,B,D,F,G,H}_*`, `experiments/candidate_01/` | |
| 207 | +(runtime.py, streaming_verifier.py, benchmark.py, benchmark_scale.py). Figures | |
| 208 | +in this report are regenerated from committed results by | |
| 209 | +`tools/make_pub_figures.py`. The append-only research log | |
| 210 | +(`research/LOG.md`) records every decision, including the three refuted | |
| 211 | +hypotheses and both measurement traps (F_NOCACHE semantics; prefill/decode | |
| 212 | +argmax instability). | |
| 213 | + | |
| 214 | +## References | |
| 215 | + | |
| 216 | +Key prior art (full 300+ entry bibliography with access dates on the | |
| 217 | +[Bibliography](/doc/research/bibliography.md) page; complete novelty analysis in | |
| 218 | +[novelty_check.md](/doc/research/novelty_check.md)): | |
| 219 | +QSpec (arXiv 2410.11305) · QuantSpec (Apple, arXiv 2502.10424) · Lever (arXiv | |
| 220 | +2605.16786) · CATS (arXiv 2605.11186) · UGSD (arXiv 2603.11397) · U-HLM (arXiv | |
| 221 | +2412.12687) · SpecExec (arXiv 2406.02532) · LLM in a flash (Apple, arXiv | |
| 222 | +2312.11514) · PowerInfer-2 (arXiv 2406.06282) · Any-Precision LLM (ICML 2024) · | |
| 223 | +MatQuant (ICLR 2025) · BitStack (arXiv 2410.23918) · ParetoQ (arXiv 2502.02631) · | |
| 224 | +SparseGPT/Wanda/DejaVu/TEAL (per state_of_the_art.md). | |
added
docs/publications/figures/f1_ssd_envelope.svg
+37 −0
@@ -0,0 +1,37 @@ | ||
| 1 | +<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 330' width='640' height='330'> | |
| 2 | +<rect width='640' height='330' fill='#fcfcfb'/> | |
| 3 | +<line x1='64' y1='253.5' x2='556' y2='253.5' stroke='#e1e0d9'/> | |
| 4 | +<text x='56.0' y='257.5' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>100 MB/s</text> | |
| 5 | +<line x1='64' y1='152.0' x2='556' y2='152.0' stroke='#e1e0d9'/> | |
| 6 | +<text x='56.0' y='156.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>1 GB/s</text> | |
| 7 | +<line x1='64' y1='50.5' x2='556' y2='50.5' stroke='#e1e0d9'/> | |
| 8 | +<text x='56.0' y='54.5' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>10 GB/s</text> | |
| 9 | +<text x='64.0' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>4 KiB</text> | |
| 10 | +<text x='187.0' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>16 KiB</text> | |
| 11 | +<text x='310.0' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>64 KiB</text> | |
| 12 | +<text x='433.0' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>256 KiB</text> | |
| 13 | +<text x='556.0' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>1 MiB</text> | |
| 14 | +<line x1='64' y1='284' x2='556' y2='284' stroke='#c3c2b7'/> | |
| 15 | +<path d='M64.0,271.1 L187.0,209.4 L310.0,163.5 L433.0,115.3 L556.0,85.2' fill='none' stroke='#2a78d6' stroke-width='2'/> | |
| 16 | +<circle cx='64.0' cy='271.1' r='4' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 17 | +<circle cx='187.0' cy='209.4' r='4' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 18 | +<circle cx='310.0' cy='163.5' r='4' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 19 | +<circle cx='433.0' cy='115.3' r='4' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 20 | +<circle cx='556.0' cy='85.2' r='4' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 21 | +<text x='566.0' y='89.2' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#2a78d6' text-anchor='start' font-weight='600'>QD 1</text> | |
| 22 | +<path d='M64.0,212.4 L187.0,151.1 L310.0,104.8 L433.0,59.6 L556.0,36.9' fill='none' stroke='#eb6834' stroke-width='2'/> | |
| 23 | +<circle cx='64.0' cy='212.4' r='4' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 24 | +<circle cx='187.0' cy='151.1' r='4' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 25 | +<circle cx='310.0' cy='104.8' r='4' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 26 | +<circle cx='433.0' cy='59.6' r='4' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 27 | +<circle cx='556.0' cy='36.9' r='4' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 28 | +<text x='566.0' y='40.9' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#eb6834' text-anchor='start' font-weight='600'>QD 4</text> | |
| 29 | +<path d='M64.0,184.3 L187.0,123.4 L310.0,79.9 L433.0,44.0 L556.0,36.4' fill='none' stroke='#1baf7a' stroke-width='2'/> | |
| 30 | +<circle cx='64.0' cy='184.3' r='4' fill='#1baf7a' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 31 | +<circle cx='187.0' cy='123.4' r='4' fill='#1baf7a' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 32 | +<circle cx='310.0' cy='79.9' r='4' fill='#1baf7a' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 33 | +<circle cx='433.0' cy='44.0' r='4' fill='#1baf7a' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 34 | +<circle cx='556.0' cy='36.4' r='4' fill='#1baf7a' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 35 | +<text x='566.0' y='40.4' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#1baf7a' text-anchor='start' font-weight='600'>QD 8</text> | |
| 36 | +<text x='310.0' y='322.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#52514e' text-anchor='middle' font-weight='400'>random read block size (F_NOCACHE cold, log–log)</text> | |
| 37 | +</svg> | |
| \ No newline at end of file | ||
added
docs/publications/figures/f2_escalation_scale.svg
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 560 320' width='560' height='320'> | |
| 2 | +<rect width='560' height='320' fill='#fcfcfb'/> | |
| 3 | +<line x1='64' y1='201.4' x2='480' y2='201.4' stroke='#e1e0d9'/> | |
| 4 | +<text x='56.0' y='205.4' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>20%</text> | |
| 5 | +<line x1='64' y1='128.9' x2='480' y2='128.9' stroke='#e1e0d9'/> | |
| 6 | +<text x='56.0' y='132.9' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>40%</text> | |
| 7 | +<line x1='64' y1='56.3' x2='480' y2='56.3' stroke='#e1e0d9'/> | |
| 8 | +<text x='56.0' y='60.3' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>60%</text> | |
| 9 | +<line x1='64' y1='274' x2='480' y2='274' stroke='#c3c2b7'/> | |
| 10 | +<text x='113.9' y='292.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#898781' text-anchor='middle' font-weight='400'>3-bit base</text> | |
| 11 | +<text x='272.0' y='292.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#898781' text-anchor='middle' font-weight='400'>4-bit base</text> | |
| 12 | +<text x='430.1' y='292.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#898781' text-anchor='middle' font-weight='400'>8-bit base</text> | |
| 13 | +<path d='M113.9,55.7 L272.0,141.2 L430.1,255.5' fill='none' stroke='#2a78d6' stroke-width='2'/> | |
| 14 | +<circle cx='113.9' cy='55.7' r='4.5' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 15 | +<text x='113.9' y='45.7' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#2a78d6' text-anchor='middle' font-weight='600'>60%</text> | |
| 16 | +<circle cx='272.0' cy='141.2' r='4.5' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 17 | +<text x='272.0' y='131.2' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#2a78d6' text-anchor='middle' font-weight='600'>37%</text> | |
| 18 | +<circle cx='430.1' cy='255.5' r='4.5' fill='#2a78d6' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 19 | +<text x='430.1' y='245.5' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#2a78d6' text-anchor='middle' font-weight='600'>5%</text> | |
| 20 | +<text x='442.1' y='259.5' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#2a78d6' text-anchor='start' font-weight='600'>Qwen3-1.7B</text> | |
| 21 | +<path d='M113.9,125.3 L272.0,192.0 L430.1,268.2' fill='none' stroke='#eb6834' stroke-width='2'/> | |
| 22 | +<circle cx='113.9' cy='125.3' r='4.5' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 23 | +<text x='113.9' y='115.3' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#eb6834' text-anchor='middle' font-weight='600'>41%</text> | |
| 24 | +<circle cx='272.0' cy='192.0' r='4.5' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 25 | +<text x='272.0' y='182.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#eb6834' text-anchor='middle' font-weight='600'>23%</text> | |
| 26 | +<circle cx='430.1' cy='268.2' r='4.5' fill='#eb6834' stroke='#fcfcfb' stroke-width='1.5'/> | |
| 27 | +<text x='430.1' y='258.2' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#eb6834' text-anchor='middle' font-weight='600'>2%</text> | |
| 28 | +<text x='442.1' y='272.2' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#eb6834' text-anchor='start' font-weight='600'>Qwen3-8B</text> | |
| 29 | +<text x='272.0' y='312.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#52514e' text-anchor='middle' font-weight='400'>tokens needing escalation for 99% greedy agreement</text> | |
| 30 | +</svg> | |
| \ No newline at end of file | ||
added
docs/publications/figures/f3_quality_bytes_continuum.svg
+28 −0
@@ -0,0 +1,28 @@ | ||
| 1 | +<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 620 330' width='620' height='330'> | |
| 2 | +<rect width='620' height='330' fill='#fcfcfb'/> | |
| 3 | +<line x1='74' y1='268.8' x2='580' y2='268.8' stroke='#e1e0d9'/> | |
| 4 | +<text x='66.0' y='272.8' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>-0.4</text> | |
| 5 | +<line x1='74' y1='192.9' x2='580' y2='192.9' stroke='#e1e0d9'/> | |
| 6 | +<text x='66.0' y='196.9' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>-0.3</text> | |
| 7 | +<line x1='74' y1='117.1' x2='580' y2='117.1' stroke='#e1e0d9'/> | |
| 8 | +<text x='66.0' y='121.1' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>-0.2</text> | |
| 9 | +<line x1='74' y1='41.2' x2='580' y2='41.2' stroke='#e1e0d9'/> | |
| 10 | +<text x='66.0' y='45.2' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='end' font-weight='400'>-0.1</text> | |
| 11 | +<text x='74.0' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>0 MB</text> | |
| 12 | +<text x='268.6' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>100 MB</text> | |
| 13 | +<text x='463.2' y='302.0' font-family='ui-monospace, Menlo, monospace' font-size='11' fill='#898781' text-anchor='middle' font-weight='400'>200 MB</text> | |
| 14 | +<line x1='74' y1='284' x2='580' y2='284' stroke='#c3c2b7'/> | |
| 15 | +<line x1='74' y1='66.1' x2='580' y2='66.1' stroke='#898781' stroke-dasharray='5,4'/> | |
| 16 | +<text x='78.0' y='81.1' font-family='system-ui, -apple-system, sans-serif' font-size='11' fill='#898781' text-anchor='start' font-weight='400'>resident-q8 ceiling (2.15 GB in RAM)</text> | |
| 17 | +<path d='M74.0,259.1 L365.9,168.0 L484.6,99.0 L535.2,63.7' fill='none' stroke='#2a78d6' stroke-width='2'/> | |
| 18 | +<circle cx='74.0' cy='259.1' r='5' fill='#2a78d6' stroke='#fcfcfb' stroke-width='2'/> | |
| 19 | +<text x='83.0' y='273.1' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#0b0b0b' text-anchor='start' font-weight='600'>pure q4</text> | |
| 20 | +<circle cx='365.9' cy='168.0' r='5' fill='#2a78d6' stroke='#fcfcfb' stroke-width='2'/> | |
| 21 | +<text x='374.9' y='159.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#0b0b0b' text-anchor='start' font-weight='600'>τ=1.0</text> | |
| 22 | +<circle cx='484.6' cy='99.0' r='5' fill='#2a78d6' stroke='#fcfcfb' stroke-width='2'/> | |
| 23 | +<text x='493.6' y='90.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#0b0b0b' text-anchor='start' font-weight='600'>τ=2.0</text> | |
| 24 | +<circle cx='535.2' cy='63.7' r='5' fill='#2a78d6' stroke='#fcfcfb' stroke-width='2'/> | |
| 25 | +<text x='524.2' y='67.7' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#0b0b0b' text-anchor='end' font-weight='600'>verify-all</text> | |
| 26 | +<text x='327.0' y='322.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#52514e' text-anchor='middle' font-weight='400'>verification bytes streamed per token (Qwen3-1.7B, W=32)</text> | |
| 27 | +<text x='16.0' y='155.0' font-family='system-ui, -apple-system, sans-serif' font-size='12' fill='#52514e' text-anchor='middle' font-weight='400' transform='rotate(-90 16 155.0)'>bf16-judge mean logprob →</text> | |
| 28 | +</svg> | |
| \ No newline at end of file | ||
added
docs/publications/figures/f4_32b_outofcore.svg
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 210' width='640' height='210'> | |
| 2 | +<rect width='640' height='210' fill='#fcfcfb'/> | |
| 3 | +<text x='26.0' y='36.0' font-family='system-ui, -apple-system, sans-serif' font-size='12.5' fill='#52514e' text-anchor='start' font-weight='400'>pure q4 — only config that fits (17.5 GB)</text> | |
| 4 | +<line x1='26.0' y1='53' x2='90.7' y2='53' stroke='#898781' stroke-width='9' stroke-linecap='round'/> | |
| 5 | +<text x='100.7' y='57.0' font-family='ui-monospace, Menlo, monospace' font-size='12' fill='#0b0b0b' text-anchor='start' font-weight='650'>-0.751</text> | |
| 6 | +<text x='26.0' y='88.0' font-family='system-ui, -apple-system, sans-serif' font-size='12.5' fill='#52514e' text-anchor='start' font-weight='400'>margin τ=2.0 — q8 streamed on low margins</text> | |
| 7 | +<line x1='26.0' y1='105' x2='358.8' y2='105' stroke='#2a78d6' stroke-width='9' stroke-linecap='round'/> | |
| 8 | +<text x='368.8' y='109.0' font-family='ui-monospace, Menlo, monospace' font-size='12' fill='#0b0b0b' text-anchor='start' font-weight='650'>-0.546</text> | |
| 9 | +<text x='26.0' y='140.0' font-family='system-ui, -apple-system, sans-serif' font-size='12.5' fill='#52514e' text-anchor='start' font-weight='400'>verify-all — q8 streamed every window</text> | |
| 10 | +<line x1='26.0' y1='157' x2='462.7' y2='157' stroke='#2a78d6' stroke-width='9' stroke-linecap='round'/> | |
| 11 | +<text x='472.7' y='161.0' font-family='ui-monospace, Menlo, monospace' font-size='12' fill='#0b0b0b' text-anchor='start' font-weight='650'>-0.467</text> | |
| 12 | +<text x='26.0' y='200.0' font-family='system-ui, -apple-system, sans-serif' font-size='11.5' fill='#898781' text-anchor='start' font-weight='400'>8B-judge mean logprob (higher is better) · Qwen3-32B on a 48 GB Mac — resident q8 is impossible</text> | |
| 13 | +</svg> | |
| \ No newline at end of file | ||
modified
research/LOG.md
+15 −0
@@ -353,3 +353,18 @@ Format per entry: date/time (local, with timezone) · question · experiment · | ||
| 353 | 353 | (fewer checked positions → fewer rollback triggers) and sweep-I/O/generation overlap. |
| 354 | 354 | - **Decision:** Session milestone reached; next cycle: honest baselines (resident q5/q6 at 32B), |
| 355 | 355 | τ calibration on the real q4-32B margin distribution, task-level benchmarks, 70B point. |
| 356 | + | |
| 357 | +--- | |
| 358 | + | |
| 359 | +## 2026-08-12 20:10 EDT — TR-01 published: first official technical report | |
| 360 | + | |
| 361 | +- **Question:** n/a (dissemination). | |
| 362 | +- **Experiment:** n/a. | |
| 363 | +- **Result:** `docs/publications/TR-01-margin-gated-deferred-refinement.md` — full write-up of | |
| 364 | + results to date (substrate, decision stability, three refutations, architecture, 1.7B | |
| 365 | + continuum, 32B out-of-core run), with 4 SVG figures regenerated from committed results by | |
| 366 | + `tools/make_pub_figures.py`. Prior-art attribution follows novelty_check.md; limitations | |
| 367 | + section names the missing baselines (resident q5/q6 at 32B, task benchmarks, 70B). | |
| 368 | + Published on the platform under the new Publications tab. | |
| 369 | +- **Interpretation:** n/a. | |
| 370 | +- **Decision:** TR-02 planned once the honest-baseline campaign (q5/q6, tasks, 70B) completes. | |
added
tools/make_pub_figures.py
+192 −0
@@ -0,0 +1,192 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +# ============================================================================= | |
| 3 | +# Project : localvm-research | |
| 4 | +# File : tools/make_pub_figures.py | |
| 5 | +# Purpose : Generate publication SVG figures from committed results JSON | |
| 6 | +# Author : Simon-Pierre Boucher | |
| 7 | +# Contact : contact@spboucher.ai | |
| 8 | +# Created : 2026-08-12 | |
| 9 | +# Modified : 2026-08-12 | |
| 10 | +# Platform : macOS / Apple Silicon (arm64) | |
| 11 | +# License : All rights reserved (research code) | |
| 12 | +# ============================================================================= | |
| 13 | +"""Regenerates docs/publications/figures/*.svg from results/ (reproducible). | |
| 14 | + | |
| 15 | +Palette: validated categorical slots (see dataviz reference; CVD-checked): | |
| 16 | +blue #2a78d6, orange #eb6834, aqua #1baf7a. Ink/grid tokens match the site. | |
| 17 | +""" | |
| 18 | + | |
| 19 | +from __future__ import annotations | |
| 20 | + | |
| 21 | +import glob | |
| 22 | +import json | |
| 23 | +import math | |
| 24 | +from pathlib import Path | |
| 25 | + | |
| 26 | +ROOT = Path(__file__).resolve().parent.parent | |
| 27 | +OUT = ROOT / "docs" / "publications" / "figures" | |
| 28 | +OUT.mkdir(parents=True, exist_ok=True) | |
| 29 | + | |
| 30 | +INK, INK2, MUTED, GRID, BASE = "#0b0b0b", "#52514e", "#898781", "#e1e0d9", "#c3c2b7" | |
| 31 | +BLUE, ORANGE, AQUA = "#2a78d6", "#eb6834", "#1baf7a" | |
| 32 | +FONT = "font-family='system-ui, -apple-system, sans-serif'" | |
| 33 | +MONO = "font-family='ui-monospace, Menlo, monospace'" | |
| 34 | + | |
| 35 | + | |
| 36 | +def latest(pattern: str) -> dict: | |
| 37 | + return json.load(open(sorted(glob.glob(str(ROOT / pattern)))[-1])) | |
| 38 | + | |
| 39 | + | |
| 40 | +def svg_open(w: int, h: int) -> list[str]: | |
| 41 | + return [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {w} {h}' " | |
| 42 | + f"width='{w}' height='{h}'>", | |
| 43 | + f"<rect width='{w}' height='{h}' fill='#fcfcfb'/>"] | |
| 44 | + | |
| 45 | + | |
| 46 | +def text(x, y, s, size=12, fill=INK2, anchor="start", weight="400", mono=False): | |
| 47 | + f = MONO if mono else FONT | |
| 48 | + return (f"<text x='{x:.1f}' y='{y:.1f}' {f} font-size='{size}' fill='{fill}' " | |
| 49 | + f"text-anchor='{anchor}' font-weight='{weight}'>{s}</text>") | |
| 50 | + | |
| 51 | + | |
| 52 | +# ---------------------------------------------------------------- F1: SSD | |
| 53 | +def fig1_ssd() -> None: | |
| 54 | + d = latest("results/expH_ssd_feasibility/*/results.json") | |
| 55 | + cells = [c for c in d["cells"] if c["nocache"] and not c["gpu_load"] | |
| 56 | + and c["pattern"] == "random" and c["block_bytes"] <= 1 << 20] | |
| 57 | + series: dict[int, list] = {} | |
| 58 | + for c in cells: | |
| 59 | + series.setdefault(c["threads"], []).append((c["block_bytes"], c["mb_per_s_mean"])) | |
| 60 | + W, H, ML, MR, MT, MB_ = 640, 330, 64, 84, 20, 46 | |
| 61 | + iw, ih = W - ML - MR, H - MT - MB_ | |
| 62 | + xs = lambda b: ML + (math.log2(b) - 12) / 8 * iw | |
| 63 | + ys = lambda v: MT + ih - (math.log10(v) - math.log10(50)) / (math.log10(20000) - math.log10(50)) * ih | |
| 64 | + out = svg_open(W, H) | |
| 65 | + for v in (100, 1000, 10000): | |
| 66 | + out.append(f"<line x1='{ML}' y1='{ys(v):.1f}' x2='{ML+iw}' y2='{ys(v):.1f}' stroke='{GRID}'/>") | |
| 67 | + out.append(text(ML - 8, ys(v) + 4, f"{v//1000} GB/s" if v >= 1000 else f"{v} MB/s", | |
| 68 | + 11, MUTED, "end", mono=True)) | |
| 69 | + for b in (4096, 16384, 65536, 262144, 1048576): | |
| 70 | + lbl = "1 MiB" if b >= 1048576 else f"{b//1024} KiB" | |
| 71 | + out.append(text(xs(b), MT + ih + 18, lbl, 11, MUTED, "middle", mono=True)) | |
| 72 | + out.append(f"<line x1='{ML}' y1='{MT+ih}' x2='{ML+iw}' y2='{MT+ih}' stroke='{BASE}'/>") | |
| 73 | + colors = {1: BLUE, 4: ORANGE, 8: AQUA} | |
| 74 | + for qd, pts in sorted(series.items()): | |
| 75 | + pts.sort() | |
| 76 | + c = colors[qd] | |
| 77 | + pd = " ".join(f"{'M' if i == 0 else 'L'}{xs(b):.1f},{ys(v):.1f}" for i, (b, v) in enumerate(pts)) | |
| 78 | + out.append(f"<path d='{pd}' fill='none' stroke='{c}' stroke-width='2'/>") | |
| 79 | + for b, v in pts: | |
| 80 | + out.append(f"<circle cx='{xs(b):.1f}' cy='{ys(v):.1f}' r='4' fill='{c}' " | |
| 81 | + f"stroke='#fcfcfb' stroke-width='1.5'/>") | |
| 82 | + out.append(text(xs(pts[-1][0]) + 10, ys(pts[-1][1]) + 4, f"QD {qd}", 12, c, weight="600")) | |
| 83 | + out.append(text(ML + iw / 2, H - 8, "random read block size (F_NOCACHE cold, log–log)", 12, INK2, "middle")) | |
| 84 | + out.append("</svg>") | |
| 85 | + (OUT / "f1_ssd_envelope.svg").write_text("\n".join(out)) | |
| 86 | + | |
| 87 | + | |
| 88 | +# ---------------------------------------------------------------- F2: expG scale | |
| 89 | +def fig2_escalation() -> None: | |
| 90 | + runs = sorted(glob.glob(str(ROOT / "results/expG_decision_stability/*/results.json"))) | |
| 91 | + data = {} | |
| 92 | + for r in runs: | |
| 93 | + d = json.load(open(r)) | |
| 94 | + if d["n_trajectories"] < 40: | |
| 95 | + continue | |
| 96 | + scale = "8B" if "8B" in d["config"]["model"] else "1.7B" | |
| 97 | + data[scale] = {int(b): s.get("escalation_frac_for_99pct") | |
| 98 | + for b, s in d["results_by_bits"].items() if int(b) >= 3} | |
| 99 | + W, H, ML, MR, MT, MB_ = 560, 320, 64, 80, 20, 46 | |
| 100 | + iw, ih = W - ML - MR, H - MT - MB_ | |
| 101 | + xs = {3: ML + iw * 0.12, 4: ML + iw * 0.5, 8: ML + iw * 0.88} | |
| 102 | + ys = lambda v: MT + ih - v / 0.7 * ih | |
| 103 | + out = svg_open(W, H) | |
| 104 | + for v in (0.2, 0.4, 0.6): | |
| 105 | + out.append(f"<line x1='{ML}' y1='{ys(v):.1f}' x2='{ML+iw}' y2='{ys(v):.1f}' stroke='{GRID}'/>") | |
| 106 | + out.append(text(ML - 8, ys(v) + 4, f"{int(v*100)}%", 11, MUTED, "end", mono=True)) | |
| 107 | + out.append(f"<line x1='{ML}' y1='{MT+ih}' x2='{ML+iw}' y2='{MT+ih}' stroke='{BASE}'/>") | |
| 108 | + for b in (3, 4, 8): | |
| 109 | + out.append(text(xs[b], MT + ih + 18, f"{b}-bit base", 12, MUTED, "middle")) | |
| 110 | + for scale, color in (("1.7B", BLUE), ("8B", ORANGE)): | |
| 111 | + pts = [(b, v) for b, v in sorted(data.get(scale, {}).items()) if v] | |
| 112 | + pd = " ".join(f"{'M' if i == 0 else 'L'}{xs[b]:.1f},{ys(v):.1f}" for i, (b, v) in enumerate(pts)) | |
| 113 | + out.append(f"<path d='{pd}' fill='none' stroke='{color}' stroke-width='2'/>") | |
| 114 | + for b, v in pts: | |
| 115 | + out.append(f"<circle cx='{xs[b]:.1f}' cy='{ys(v):.1f}' r='4.5' fill='{color}' " | |
| 116 | + f"stroke='#fcfcfb' stroke-width='1.5'/>") | |
| 117 | + dy = 20 if (scale == "8B" and b == 8) else -10 # avoid series-label collision | |
| 118 | + out.append(text(xs[b], ys(v) + dy, f"{v*100:.0f}%", 11, color, "middle", "600", mono=True)) | |
| 119 | + out.append(text(xs[pts[-1][0]] + 12, ys(pts[-1][1]) + 4, f"Qwen3-{scale}", 12, color, weight="600")) | |
| 120 | + out.append(text(ML + iw / 2, H - 8, "tokens needing escalation for 99% greedy agreement", 12, INK2, "middle")) | |
| 121 | + out.append("</svg>") | |
| 122 | + (OUT / "f2_escalation_scale.svg").write_text("\n".join(out)) | |
| 123 | + | |
| 124 | + | |
| 125 | +# ---------------------------------------------------------------- F3: continuum | |
| 126 | +def fig3_continuum() -> None: | |
| 127 | + runs = [json.load(open(p)) for p in sorted(glob.glob(str(ROOT / "results/candidate_01/*/results.json")))] | |
| 128 | + d = [r for r in runs if "quality_bf16_judge" in r][-1] | |
| 129 | + q = d["quality_bf16_judge"] | |
| 130 | + pts = [("pure q4", 0.0, q["pure_q4"]["mean_logprob_bf16"]), | |
| 131 | + ("τ=1.0", 150, q["margin_tau1.0"]["mean_logprob_bf16"]), | |
| 132 | + ("τ=2.0", 211, q["margin_tau2.0"]["mean_logprob_bf16"]), | |
| 133 | + ("verify-all", 237, q["verify-all_tau2.0"]["mean_logprob_bf16"])] | |
| 134 | + ceiling = q["pure_q8"]["mean_logprob_bf16"] | |
| 135 | + W, H, ML, MR, MT, MB_ = 620, 330, 74, 40, 26, 46 | |
| 136 | + iw, ih = W - ML - MR, H - MT - MB_ | |
| 137 | + xs = lambda v: ML + v / 260 * iw | |
| 138 | + ys = lambda v: MT + ih - (v + 0.42) / 0.34 * ih | |
| 139 | + out = svg_open(W, H) | |
| 140 | + for v in (-0.4, -0.3, -0.2, -0.1): | |
| 141 | + out.append(f"<line x1='{ML}' y1='{ys(v):.1f}' x2='{ML+iw}' y2='{ys(v):.1f}' stroke='{GRID}'/>") | |
| 142 | + out.append(text(ML - 8, ys(v) + 4, f"{v:.1f}", 11, MUTED, "end", mono=True)) | |
| 143 | + for v in (0, 100, 200): | |
| 144 | + out.append(text(xs(v), MT + ih + 18, f"{v} MB", 11, MUTED, "middle", mono=True)) | |
| 145 | + out.append(f"<line x1='{ML}' y1='{MT+ih}' x2='{ML+iw}' y2='{MT+ih}' stroke='{BASE}'/>") | |
| 146 | + out.append(f"<line x1='{ML}' y1='{ys(ceiling):.1f}' x2='{ML+iw}' y2='{ys(ceiling):.1f}' " | |
| 147 | + f"stroke='{MUTED}' stroke-dasharray='5,4'/>") | |
| 148 | + out.append(text(ML + 4, ys(ceiling) + 15, "resident-q8 ceiling (2.15 GB in RAM)", 11, MUTED)) | |
| 149 | + pd = " ".join(f"{'M' if i == 0 else 'L'}{xs(x):.1f},{ys(y):.1f}" for i, (_, x, y) in enumerate(pts)) | |
| 150 | + out.append(f"<path d='{pd}' fill='none' stroke='{BLUE}' stroke-width='2'/>") | |
| 151 | + for name, x, y in pts: | |
| 152 | + out.append(f"<circle cx='{xs(x):.1f}' cy='{ys(y):.1f}' r='5' fill='{BLUE}' " | |
| 153 | + f"stroke='#fcfcfb' stroke-width='2'/>") | |
| 154 | + if name == "verify-all": | |
| 155 | + out.append(text(xs(x) - 11, ys(y) + 4, name, 12, INK, "end", "600")) | |
| 156 | + else: | |
| 157 | + out.append(text(xs(x) + 9, ys(y) + (14 if name == "pure q4" else -9), name, 12, INK, weight="600")) | |
| 158 | + out.append(text(ML + iw / 2, H - 8, "verification bytes streamed per token (Qwen3-1.7B, W=32)", 12, INK2, "middle")) | |
| 159 | + out.append(text(16, MT + ih / 2, "bf16-judge mean logprob →", 12, INK2, "middle") | |
| 160 | + .replace(">", f" transform='rotate(-90 16 {MT+ih/2})'>", 1)) | |
| 161 | + out.append("</svg>") | |
| 162 | + (OUT / "f3_quality_bytes_continuum.svg").write_text("\n".join(out)) | |
| 163 | + | |
| 164 | + | |
| 165 | +# ---------------------------------------------------------------- F4: 32B | |
| 166 | +def fig4_scale32b() -> None: | |
| 167 | + d = latest("results/candidate_01_scale32b/20260812T055506Z/results.json") | |
| 168 | + q = d["quality_8b_judge"] | |
| 169 | + rows = [("pure q4 — only config that fits (17.5 GB)", q["pure_q4"]["mean_logprob_8b_judge"]), | |
| 170 | + ("margin τ=2.0 — q8 streamed on low margins", q["margin_tau2.0"]["mean_logprob_8b_judge"]), | |
| 171 | + ("verify-all — q8 streamed every window", q["verify-all_tau2.0"]["mean_logprob_8b_judge"])] | |
| 172 | + W, H, ML, MR, MT = 640, 210, 26, 90, 24 | |
| 173 | + row_h = 52 | |
| 174 | + x0, x1 = -0.80, -0.40 | |
| 175 | + xs = lambda v: ML + (v - x0) / (x1 - x0) * (W - ML - MR) | |
| 176 | + out = svg_open(W, H) | |
| 177 | + for i, (name, v) in enumerate(rows): | |
| 178 | + y = MT + i * row_h + 20 | |
| 179 | + color = BLUE if i else MUTED | |
| 180 | + out.append(text(ML, y - 8, name, 12.5, INK2)) | |
| 181 | + out.append(f"<line x1='{xs(x0)}' y1='{y+9}' x2='{xs(v):.1f}' y2='{y+9}' " | |
| 182 | + f"stroke='{color}' stroke-width='9' stroke-linecap='round'/>") | |
| 183 | + out.append(text(xs(v) + 10, y + 13, f"{v:.3f}", 12, INK, weight="650", mono=True)) | |
| 184 | + out.append(text(ML, H - 10, "8B-judge mean logprob (higher is better) · Qwen3-32B on a 48 GB Mac — " | |
| 185 | + "resident q8 is impossible", 11.5, MUTED)) | |
| 186 | + out.append("</svg>") | |
| 187 | + (OUT / "f4_32b_outofcore.svg").write_text("\n".join(out)) | |
| 188 | + | |
| 189 | + | |
| 190 | +if __name__ == "__main__": | |
| 191 | + fig1_ssd(); fig2_escalation(); fig3_continuum(); fig4_scale32b() | |
| 192 | + print("figures written to", OUT) | |
modified
web/lib/render.js
+6 −0
@@ -49,6 +49,11 @@ function markdownToHtml(md, baseRel) { | ||
| 49 | 49 | }, |
| 50 | 50 | }, |
| 51 | 51 | walkTokens(token) { |
| 52 | + if (token.type === "image" && token.href && !/^(https?:|data:|\/)/.test(token.href)) { | |
| 53 | + const base = baseRel.includes("/") ? baseRel.slice(0, baseRel.lastIndexOf("/")) : ""; | |
| 54 | + token.href = "/raw/" + (base ? base + "/" : "") + token.href; | |
| 55 | + return; | |
| 56 | + } | |
| 52 | 57 | if (token.type === "link" && token.href && !/^(https?:|mailto:|#|\/)/.test(token.href)) { |
| 53 | 58 | const base = baseRel.includes("/") ? baseRel.slice(0, baseRel.lastIndexOf("/")) : ""; |
| 54 | 59 | const joined = (base ? base + "/" : "") + token.href; |
@@ -132,6 +137,7 @@ const NAV = [ | ||
| 132 | 137 | ["/results", "Results"], |
| 133 | 138 | ["/code", "Code"], |
| 134 | 139 | ["/doc/research/bibliography.md", "Bibliography"], |
| 140 | + ["/publications", "Publications"], | |
| 135 | 141 | ["/comments", "Comments"], |
| 136 | 142 | ["/about", "About"], |
| 137 | 143 | ]; |
modified
web/public/style.css
+19 −0
@@ -389,3 +389,22 @@ h2:hover .hanchor, h3:hover .hanchor, h4:hover .hanchor { opacity: 1; text-decor | ||
| 389 | 389 | @media (max-width: 420px) { |
| 390 | 390 | .hero h1 { font-size: 24px; } |
| 391 | 391 | } |
| 392 | + | |
| 393 | +/* ---- publications ---------------------------------------------------------- */ | |
| 394 | +.pub-list { max-width: 860px; margin: 24px 0; } | |
| 395 | +.pub-card { | |
| 396 | + display: block; background: var(--surface); border: 1px solid var(--grid); | |
| 397 | + border-radius: var(--radius); padding: 26px 30px; margin-bottom: 18px; | |
| 398 | + color: var(--ink); box-shadow: var(--shadow-1); | |
| 399 | + transition: box-shadow 0.18s, transform 0.18s, border-color 0.18s; | |
| 400 | +} | |
| 401 | +.pub-card:hover { border-color: #b9d2f0; box-shadow: var(--shadow-2); transform: translateY(-2px); text-decoration: none; } | |
| 402 | +.pub-meta { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; } | |
| 403 | +.pub-card h2 { margin: 0 0 6px; font-size: 20px; letter-spacing: -0.015em; line-height: 1.3; } | |
| 404 | +.pub-authors { margin: 0 0 10px; font-size: 14px; color: var(--accent-dark); font-weight: 600; } | |
| 405 | +.pub-abstract { margin: 0 0 12px; font-size: 14px; color: var(--ink-2); } | |
| 406 | +.md img { | |
| 407 | + max-width: 100%; height: auto; display: block; margin: 20px auto 6px; | |
| 408 | + background: #fff; border: 1px solid var(--grid); border-radius: 10px; padding: 12px; | |
| 409 | +} | |
| 410 | +.md img + em, .md p > img ~ em { display: block; text-align: center; } | |
modified
web/server.js
+37 −0
@@ -352,6 +352,43 @@ why an approach died, not just what survived.</p> | ||
| 352 | 352 | page(res, { title: "About", active: "About", body }); |
| 353 | 353 | }); |
| 354 | 354 | |
| 355 | +// ---------------------------------------------------------------- publications | |
| 356 | +app.get("/publications", (req, res) => { | |
| 357 | + const pubs = (C.listDir("docs/publications") || []) | |
| 358 | + .filter((f) => f.name.endsWith(".md")) | |
| 359 | + .map((f) => { | |
| 360 | + const md = C.readMarkdown(f.rel); | |
| 361 | + const abstract = (md.content.split(/## Abstract/i)[1] || "").trim().split("\n\n")[0] || ""; | |
| 362 | + return { rel: f.rel, data: md.data, abstract }; | |
| 363 | + }) | |
| 364 | + .sort((a, b) => String(b.data.created).localeCompare(String(a.data.created))); | |
| 365 | + const cards = pubs.length ? pubs.map((p) => ` | |
| 366 | + <a class="pub-card" href="/doc/${p.rel}"> | |
| 367 | + <div class="pub-meta"><span class="badge badge-done">${R.esc(p.data.number || "report")}</span> | |
| 368 | + <span class="mono-small">${R.esc(p.data.created || "")}</span> | |
| 369 | + <span class="badge badge-${p.data.status === "final" ? "done" : "in-progress"}">${R.esc(p.data.status || "draft")}</span></div> | |
| 370 | + <h2>${R.esc(p.data.title || p.rel)}</h2> | |
| 371 | + <p class="pub-authors">${R.esc(p.data.author || "")}</p> | |
| 372 | + <p class="pub-abstract">${R.esc(p.abstract.slice(0, 420))}${p.abstract.length > 420 ? "…" : ""}</p> | |
| 373 | + <span class="more">Read the full report →</span> | |
| 374 | + </a>`).join("") : "<p class='lede-small'>No publications yet.</p>"; | |
| 375 | + const body = `<h1 class="page-title">Publications</h1> | |
| 376 | +<p class="lede-small">Official write-ups of the project's results to date — every number traceable | |
| 377 | +to a committed result file with hardware manifest, every figure regenerated from raw data.</p> | |
| 378 | +<div class="pub-list">${cards}</div>`; | |
| 379 | + page(res, { title: "Publications", active: "Publications", body }); | |
| 380 | +}); | |
| 381 | + | |
| 382 | +// raw asset serving (figures referenced by publications) | |
| 383 | +app.get(/^\/raw\/(.+)$/, (req, res) => { | |
| 384 | + const rel = req.params[0]; | |
| 385 | + const ext = path.extname(rel).toLowerCase(); | |
| 386 | + const types = { ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg" }; | |
| 387 | + const abs = C.safeResolve(rel); | |
| 388 | + if (!abs || !types[ext] || !require("fs").existsSync(abs)) return notFound(res); | |
| 389 | + res.type(types[ext]).sendFile(abs); | |
| 390 | +}); | |
| 391 | + | |
| 355 | 392 | // ---------------------------------------------------------------- comments |
| 356 | 393 | app.get("/comments", (req, res) => { |
| 357 | 394 | const AVATAR_COLORS = ["#2a78d6", "#eb6834", "#1baf7a", "#4a3aa7", "#e87ba4", "#008300"]; |
| 358 | 395 | |