Site: server-rendered SVG result maps (dataviz-validated palette)
- lib/charts.js: layer-profile lines, small-multiple bar panels, grouped bars — palette validated with the six-checks script (violet/orange PASS; aqua carries mandatory direct labels; twin = dashed neutral reference) - expA probe map (trained A/B vs random-init twin) on home, experiment page and atlas entry; expH warm/cold storage panels + capture-overhead bars on home and experiment page; per-mark hover tooltips, legends Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 5 changed files with +320 and −16
added
site/lib/charts.js
+176 −0
@@ -0,0 +1,176 @@ | ||
| 1 | +// ============================================================================ | |
| 2 | +// Project : modelmap | |
| 3 | +// File : site/lib/charts.js | |
| 4 | +// Purpose : Server-rendered SVG charts for real result maps (dataviz spec) | |
| 5 | +// Author : Simon-Pierre Boucher | |
| 6 | +// Contact : contact@spboucher.ai | |
| 7 | +// Website : https://modelmap.io | |
| 8 | +// Created : 2026-08-12 | |
| 9 | +// Modified : 2026-08-12 | |
| 10 | +// Platform : macOS / Apple Silicon (arm64) — Node.js (deployed on MacLustr) | |
| 11 | +// License : All rights reserved (research code) | |
| 12 | +// ============================================================================ | |
| 13 | +"use strict"; | |
| 14 | + | |
| 15 | +// Categorical palette validated with the dataviz six-checks script | |
| 16 | +// (violet/orange PASS all; aqua passes with a contrast WARN → bars using it | |
| 17 | +// always carry direct value labels). The twin/null is NOT a categorical slot: | |
| 18 | +// it renders as a dashed neutral reference line, direct-labeled. | |
| 19 | +const S1 = "#6d4fc4"; // series 1 — violet | |
| 20 | +const S2 = "#eb6834"; // series 2 — orange | |
| 21 | +const S3 = "#1baf7a"; // series 3 — aqua (direct labels mandatory) | |
| 22 | +const REF = "#8b8798"; // reference/null line (muted ink, dashed) | |
| 23 | + | |
| 24 | +function esc(s) { | |
| 25 | + return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); | |
| 26 | +} | |
| 27 | + | |
| 28 | +const fmt = (v) => (Math.abs(v) >= 100 ? v.toFixed(0) : Math.abs(v) >= 10 ? v.toFixed(1) : v.toFixed(2)); | |
| 29 | + | |
| 30 | +/** | |
| 31 | + * Layer-profile line chart: x = layer index, y in [yMin,yMax]. | |
| 32 | + * series: [{label, color, dash?, ref?, values:[y per layer]}] | |
| 33 | + * Direct labels at line ends + legend; per-point hover targets. | |
| 34 | + */ | |
| 35 | +function layerLineSvg({ title, series, yLabel, yMin = 0, yMax = 1, caption }) { | |
| 36 | + const W = 720, H = 300, ML = 52, MR = 118, MT = 16, MB = 40; | |
| 37 | + const iw = W - ML - MR, ih = H - MT - MB; | |
| 38 | + const nx = Math.max(...series.map((s) => s.values.length)); | |
| 39 | + const X = (i) => ML + (i / (nx - 1)) * iw; | |
| 40 | + const Y = (v) => MT + ih - ((v - yMin) / (yMax - yMin)) * ih; | |
| 41 | + | |
| 42 | + let grid = ""; | |
| 43 | + for (const v of [0, 0.25, 0.5, 0.75, 1].filter((v) => v >= yMin && v <= yMax)) { | |
| 44 | + grid += `<line x1="${ML}" y1="${Y(v)}" x2="${ML + iw}" y2="${Y(v)}" class="grid"/>` + | |
| 45 | + `<text x="${ML - 8}" y="${Y(v) + 4}" class="tick" text-anchor="end">${v}</text>`; | |
| 46 | + } | |
| 47 | + let xt = ""; | |
| 48 | + for (let i = 0; i < nx; i += Math.ceil(nx / 8)) { | |
| 49 | + xt += `<text x="${X(i)}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${i}</text>`; | |
| 50 | + } | |
| 51 | + xt += `<text x="${X(nx - 1)}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${nx - 1}</text>`; | |
| 52 | + | |
| 53 | + let lines = "", dots = "", labels = ""; | |
| 54 | + const usedY = []; | |
| 55 | + for (const s of series) { | |
| 56 | + const d = s.values.map((v, i) => `${i ? "L" : "M"}${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(" "); | |
| 57 | + lines += `<path d="${d}" fill="none" stroke="${s.color}" stroke-width="2" ` + | |
| 58 | + `stroke-linejoin="round"${s.dash ? ' stroke-dasharray="6 5"' : ""}/>`; | |
| 59 | + s.values.forEach((v, i) => { | |
| 60 | + dots += `<circle cx="${X(i).toFixed(1)}" cy="${Y(v).toFixed(1)}" r="7" fill="transparent" ` + | |
| 61 | + `class="pt" data-tip="${esc(s.label)} · layer ${i}: ${fmt(v)}"/>`; | |
| 62 | + }); | |
| 63 | + let ly = Y(s.values[s.values.length - 1]); | |
| 64 | + while (usedY.some((u) => Math.abs(u - ly) < 13)) ly += 13; // collision nudge | |
| 65 | + usedY.push(ly); | |
| 66 | + labels += `<text x="${ML + iw + 8}" y="${ly + 4}" class="series-label" fill="${s.ref ? REF : s.color}">${esc(s.label)}</text>`; | |
| 67 | + } | |
| 68 | + const legend = series.map((s) => | |
| 69 | + `<span class="lg"><span class="lg-swatch" style="background:${s.ref ? "transparent" : s.color};` + | |
| 70 | + `${s.ref ? `border-top:2px dashed ${REF};height:0;` : ""}"></span>${esc(s.label)}</span>`).join(""); | |
| 71 | + | |
| 72 | + return `<figure class="chart-fig"> | |
| 73 | +${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""} | |
| 74 | +<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(title || yLabel)}"> | |
| 75 | +<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/> | |
| 76 | +${grid}${xt}${lines}${dots}${labels} | |
| 77 | +<text x="${ML + iw / 2}" y="${H - 4}" class="axis-title" text-anchor="middle">layer</text> | |
| 78 | +<text transform="rotate(-90 14 ${MT + ih / 2})" x="14" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text> | |
| 79 | +</svg> | |
| 80 | +<div class="chart-legend">${legend}</div> | |
| 81 | +<div class="chart-tip" hidden></div> | |
| 82 | +${caption ? `<figcaption>${caption}</figcaption>` : ""} | |
| 83 | +</figure>`; | |
| 84 | +} | |
| 85 | + | |
| 86 | +/** | |
| 87 | + * Small-multiple bar panels sharing one y-scale (identity on the x axis → | |
| 88 | + * single hue; every bar direct-labeled; 4px rounded data ends; 2px gaps). | |
| 89 | + * panels: [{title, bars: [{label, value}]}] | |
| 90 | + */ | |
| 91 | +function barPanelsSvg({ title, panels, yLabel, unit = "", color = S1, caption }) { | |
| 92 | + const PW = 340, H = 280, ML = 46, MR = 8, MT = 30, MB = 58; | |
| 93 | + const ih = H - MT - MB; | |
| 94 | + const vmax = Math.max(...panels.flatMap((p) => p.bars.map((b) => b.value))) * 1.15; | |
| 95 | + const W = panels.length * PW; | |
| 96 | + let out = ""; | |
| 97 | + panels.forEach((panel, pi) => { | |
| 98 | + const x0 = pi * PW; | |
| 99 | + const iw = PW - ML - MR; | |
| 100 | + const n = panel.bars.length; | |
| 101 | + const bw = Math.min(46, (iw / n) * 0.62); | |
| 102 | + out += `<text x="${x0 + ML + iw / 2}" y="${MT - 12}" class="panel-title" text-anchor="middle">${esc(panel.title)}</text>`; | |
| 103 | + for (const v of [0.25, 0.5, 0.75, 1]) { | |
| 104 | + const y = MT + ih - v * ih; | |
| 105 | + out += `<line x1="${x0 + ML}" y1="${y}" x2="${x0 + ML + iw}" y2="${y}" class="grid"/>`; | |
| 106 | + if (pi === 0) out += `<text x="${x0 + ML - 8}" y="${y + 4}" class="tick" text-anchor="end">${fmt(vmax * v)}</text>`; | |
| 107 | + } | |
| 108 | + panel.bars.forEach((b, i) => { | |
| 109 | + const cx = x0 + ML + ((i + 0.5) / n) * iw; | |
| 110 | + const h = Math.max(2, (b.value / vmax) * ih); | |
| 111 | + const y = MT + ih - h; | |
| 112 | + out += `<rect x="${(cx - bw / 2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw}" height="${h.toFixed(1)}" ` + | |
| 113 | + `rx="4" fill="${color}" class="pt" data-tip="${esc(panel.title)} · ${esc(b.label)}: ${fmt(b.value)} ${esc(unit)}"/>` + | |
| 114 | + `<rect x="${(cx - bw / 2).toFixed(1)}" y="${(MT + ih - 2).toFixed(1)}" width="${bw}" height="2" fill="${color}"/>` + | |
| 115 | + `<text x="${cx}" y="${y - 5}" class="bar-val" text-anchor="middle">${fmt(b.value)}</text>` + | |
| 116 | + `<text x="${cx}" y="${MT + ih + 16}" class="tick" text-anchor="middle">${esc(b.label)}</text>`; | |
| 117 | + }); | |
| 118 | + out += `<line x1="${x0 + ML}" y1="${MT + ih}" x2="${x0 + ML + iw}" y2="${MT + ih}" class="axis"/>`; | |
| 119 | + }); | |
| 120 | + return `<figure class="chart-fig"> | |
| 121 | +${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""} | |
| 122 | +<svg viewBox="0 0 ${panels.length * PW} ${H}" role="img" aria-label="${esc(title || yLabel)}"> | |
| 123 | +${out} | |
| 124 | +<text transform="rotate(-90 12 ${MT + ih / 2})" x="12" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text> | |
| 125 | +</svg> | |
| 126 | +<div class="chart-tip" hidden></div> | |
| 127 | +${caption ? `<figcaption>${caption}</figcaption>` : ""} | |
| 128 | +</figure>`; | |
| 129 | +} | |
| 130 | + | |
| 131 | +/** | |
| 132 | + * Grouped bars: groups on x, one bar per mode, fixed mode→color assignment, | |
| 133 | + * every bar direct-labeled (satisfies the aqua contrast WARN relief). | |
| 134 | + * groups: [{label, values: {modeKey: value}}], modes: [{key,label,color}] | |
| 135 | + */ | |
| 136 | +function groupedBarSvg({ title, groups, modes, yLabel, unit = "", caption }) { | |
| 137 | + const W = 720, H = 300, ML = 52, MR = 10, MT = 16, MB = 58; | |
| 138 | + const iw = W - ML - MR, ih = H - MT - MB; | |
| 139 | + const vmax = Math.max(...groups.flatMap((g) => modes.map((m) => g.values[m.key] || 0))) * 1.18; | |
| 140 | + const gw = iw / groups.length; | |
| 141 | + const bw = Math.min(34, (gw / modes.length) * 0.6); | |
| 142 | + let out = ""; | |
| 143 | + for (const v of [0.25, 0.5, 0.75, 1]) { | |
| 144 | + const y = MT + ih - v * ih; | |
| 145 | + out += `<line x1="${ML}" y1="${y}" x2="${ML + iw}" y2="${y}" class="grid"/>` + | |
| 146 | + `<text x="${ML - 8}" y="${y + 4}" class="tick" text-anchor="end">${fmt(vmax * v)}</text>`; | |
| 147 | + } | |
| 148 | + groups.forEach((g, gi) => { | |
| 149 | + const gx = ML + gi * gw + gw / 2; | |
| 150 | + modes.forEach((m, mi) => { | |
| 151 | + const v = g.values[m.key]; | |
| 152 | + if (v == null) return; | |
| 153 | + const cx = gx + (mi - (modes.length - 1) / 2) * (bw + 2); | |
| 154 | + const h = Math.max(2, (v / vmax) * ih); | |
| 155 | + const y = MT + ih - h; | |
| 156 | + out += `<rect x="${(cx - bw / 2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw}" height="${h.toFixed(1)}" ` + | |
| 157 | + `rx="4" fill="${m.color}" class="pt" data-tip="${esc(g.label)} · ${esc(m.label)}: ${fmt(v)} ${esc(unit)}"/>` + | |
| 158 | + `<text x="${cx}" y="${y - 5}" class="bar-val" text-anchor="middle">${fmt(v)}</text>`; | |
| 159 | + }); | |
| 160 | + out += `<text x="${gx}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${esc(g.label)}</text>`; | |
| 161 | + }); | |
| 162 | + const legend = modes.map((m) => | |
| 163 | + `<span class="lg"><span class="lg-swatch" style="background:${m.color}"></span>${esc(m.label)}</span>`).join(""); | |
| 164 | + return `<figure class="chart-fig"> | |
| 165 | +${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""} | |
| 166 | +<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(title || yLabel)}"> | |
| 167 | +${out}<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/> | |
| 168 | +<text transform="rotate(-90 12 ${MT + ih / 2})" x="12" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text> | |
| 169 | +</svg> | |
| 170 | +<div class="chart-legend">${legend}</div> | |
| 171 | +<div class="chart-tip" hidden></div> | |
| 172 | +${caption ? `<figcaption>${caption}</figcaption>` : ""} | |
| 173 | +</figure>`; | |
| 174 | +} | |
| 175 | + | |
| 176 | +module.exports = { layerLineSvg, barPanelsSvg, groupedBarSvg, S1, S2, S3, REF }; | |
modified
site/lib/content.js
+25 −0
@@ -185,6 +185,29 @@ function buildInfo() { | ||
| 185 | 185 | return readJson("build-info.json") || {}; |
| 186 | 186 | } |
| 187 | 187 | |
| 188 | +/** expA probe map from the first atlas entry (null before it exists). */ | |
| 189 | +function probeMapV1() { | |
| 190 | + return readJson("atlas/qwen3-0.6b-4bit/probes/v1/map.json"); | |
| 191 | +} | |
| 192 | + | |
| 193 | +/** Parsed expH runs for charts: warm/cold storage rows + compute rows. */ | |
| 194 | +function expHData() { | |
| 195 | + const out = { warm: null, cold: null, compute: [] }; | |
| 196 | + for (const r of listResultRuns().filter((x) => x.experiment === "expH_capture_cost_frontier")) { | |
| 197 | + const d = readJson(path.posix.join(r.rel, "results.json")); | |
| 198 | + if (!d) continue; | |
| 199 | + if (d.run === 1) { | |
| 200 | + out.warm = d.storage; | |
| 201 | + out.compute.push(...(d.compute || []).map((c) => ({ ...c, source: "synthetic" }))); | |
| 202 | + } else if (d.run === 2) { | |
| 203 | + out.cold = d.storage; | |
| 204 | + } else if (d.run === 3) { | |
| 205 | + out.compute.push(...(d.compute || []).map((c) => ({ ...c, source: "qwen3-0.6b-4bit" }))); | |
| 206 | + } | |
| 207 | + } | |
| 208 | + return out; | |
| 209 | +} | |
| 210 | + | |
| 188 | 211 | /** Site-wide stats for the home page. */ |
| 189 | 212 | function stats() { |
| 190 | 213 | const bib = readText("research/bibliography.md") || ""; |
@@ -222,4 +245,6 @@ module.exports = { | ||
| 222 | 245 | listAtlasEntries, |
| 223 | 246 | buildInfo, |
| 224 | 247 | stats, |
| 248 | + probeMapV1, | |
| 249 | + expHData, | |
| 225 | 250 | }; |
modified
site/public/app.js
+14 −14
@@ -39,20 +39,20 @@ | ||
| 39 | 39 | |
| 40 | 40 | // ---------------------------------------------------------- chart tooltips |
| 41 | 41 | (function () { |
| 42 | − const fig = document.querySelector(".chart-fig"); | |
| 43 | − if (!fig) return; | |
| 44 | − const tip = fig.querySelector(".chart-tip"); | |
| 45 | − if (!tip) return; | |
| 46 | − fig.querySelectorAll(".pt").forEach((pt) => { | |
| 47 | − pt.addEventListener("mouseenter", () => { | |
| 48 | − tip.textContent = pt.getAttribute("data-tip"); | |
| 49 | − tip.hidden = false; | |
| 42 | + document.querySelectorAll(".chart-fig").forEach((fig) => { | |
| 43 | + const tip = fig.querySelector(".chart-tip"); | |
| 44 | + if (!tip) return; | |
| 45 | + fig.querySelectorAll(".pt").forEach((pt) => { | |
| 46 | + pt.addEventListener("mouseenter", () => { | |
| 47 | + tip.textContent = pt.getAttribute("data-tip"); | |
| 48 | + tip.hidden = false; | |
| 49 | + }); | |
| 50 | + pt.addEventListener("mousemove", (ev) => { | |
| 51 | + const r = fig.getBoundingClientRect(); | |
| 52 | + tip.style.left = Math.min(ev.clientX - r.left + 14, r.width - tip.offsetWidth - 4) + "px"; | |
| 53 | + tip.style.top = ev.clientY - r.top - 34 + "px"; | |
| 54 | + }); | |
| 55 | + pt.addEventListener("mouseleave", () => { tip.hidden = true; }); | |
| 50 | 56 | }); |
| 51 | − pt.addEventListener("mousemove", (ev) => { | |
| 52 | − const r = fig.getBoundingClientRect(); | |
| 53 | − tip.style.left = Math.min(ev.clientX - r.left + 14, r.width - tip.offsetWidth - 4) + "px"; | |
| 54 | − tip.style.top = ev.clientY - r.top - 34 + "px"; | |
| 55 | − }); | |
| 56 | − pt.addEventListener("mouseleave", () => { tip.hidden = true; }); | |
| 57 | 57 | }); |
| 58 | 58 | })(); |
modified
site/public/style.css
+11 −1
@@ -296,7 +296,17 @@ h1, h2, h3, .page-title, .section-title { font-family: var(--font-display); font | ||
| 296 | 296 | .comment-body { font-size: 14.5px; color: var(--ink-2); white-space: pre-wrap; overflow-wrap: break-word; } |
| 297 | 297 | |
| 298 | 298 | /* ---------------------------------------------------------------- charts */ |
| 299 | −.chart-fig { margin: 18px 0 8px; position: relative; } | |
| 299 | +.chart-fig { margin: 22px 0 10px; position: relative; } | |
| 300 | +.chart-fig + .chart-fig { margin-top: 34px; padding-top: 26px; border-top: 1px solid var(--grid); } | |
| 301 | +.chart-title { | |
| 302 | + font-family: var(--font-body); font-size: 14.5px; font-weight: 650; | |
| 303 | + color: var(--ink); margin-bottom: 6px; | |
| 304 | +} | |
| 305 | +.panel-title { fill: var(--ink-2); font-size: 12px; font-weight: 600; font-family: var(--font-body); } | |
| 306 | +.bar-val { fill: var(--ink-2); font-size: 10.5px; font-family: var(--font-mono); } | |
| 307 | +.chart-legend { display: flex; flex-wrap: wrap; gap: 14px; margin: 6px 0 2px; font-size: 12.5px; color: var(--ink-2); } | |
| 308 | +.lg { display: inline-flex; align-items: center; gap: 6px; } | |
| 309 | +.lg-swatch { width: 14px; height: 14px; border-radius: 4px; display: inline-block; flex: 0 0 auto; } | |
| 300 | 310 | .chart-fig svg { width: 100%; height: auto; } |
| 301 | 311 | .chart-fig .grid { stroke: var(--grid); stroke-width: 1; } |
| 302 | 312 | .chart-fig .axis { stroke: var(--baseline); stroke-width: 1; } |
modified
site/server.js
+94 −1
@@ -17,6 +17,81 @@ const express = require("express"); | ||
| 17 | 17 | const C = require("./lib/content"); |
| 18 | 18 | const R = require("./lib/render"); |
| 19 | 19 | const Comments = require("./lib/comments"); |
| 20 | +const Charts = require("./lib/charts"); | |
| 21 | + | |
| 22 | +// ------------------------------------------------------------ chart builders | |
| 23 | +/** Layer-profile charts for the expA probe map (real A/B + twin null). */ | |
| 24 | +function probeMapCharts(onlyProp) { | |
| 25 | + const m = C.probeMapV1(); | |
| 26 | + if (!m || !m.properties) return ""; | |
| 27 | + const props = onlyProp ? [onlyProp] : Object.keys(m.properties); | |
| 28 | + return props.map((prop) => { | |
| 29 | + const p = m.properties[prop]; | |
| 30 | + if (!p) return ""; | |
| 31 | + const sel = (rows) => rows.map((r) => r.selectivity_mean); | |
| 32 | + return Charts.layerLineSvg({ | |
| 33 | + title: `${prop} — probe selectivity by layer (trained vs random-init twin)`, | |
| 34 | + yLabel: "selectivity", | |
| 35 | + yMin: 0, yMax: 1, | |
| 36 | + series: [ | |
| 37 | + { label: "trained · set A", color: Charts.S1, values: sel(p.per_layer.A) }, | |
| 38 | + { label: "trained · set B", color: Charts.S2, values: sel(p.per_layer.B) }, | |
| 39 | + { label: "random-init twin", color: Charts.REF, dash: true, ref: true, | |
| 40 | + values: p.twin_null_per_layer_A.map((r) => r.selectivity_mean) }, | |
| 41 | + ], | |
| 42 | + caption: "The twin (dashed) matches the trained model — the registered validity gate " + | |
| 43 | + "failed and this map is published as a NEGATIVE result: on these promptsets, probes " + | |
| 44 | + "read the tokenizer + architecture prior, not learned computation. Level 1, 5 seeds, " + | |
| 45 | + "shuffled-label controls inside every probe.", | |
| 46 | + }); | |
| 47 | + }).join(""); | |
| 48 | +} | |
| 49 | + | |
| 50 | +/** expH charts: warm/cold storage panels + capture-overhead grouped bars. */ | |
| 51 | +function expHCharts() { | |
| 52 | + const d = C.expHData(); | |
| 53 | + let html = ""; | |
| 54 | + if (d.warm && d.cold) { | |
| 55 | + const rnd = (rows) => rows.filter((r) => r.op === "random-batch") | |
| 56 | + .map((r) => ({ label: r.format.replace("zarr-", "zarr‑"), value: r.gb_per_s_mean })); | |
| 57 | + html += Charts.barPanelsSvg({ | |
| 58 | + title: "Activation-store random-batch read throughput — the ordering inverts cold", | |
| 59 | + yLabel: "GB/s", unit: "GB/s", | |
| 60 | + panels: [ | |
| 61 | + { title: "warm cache · M5 Max (run #1)", bars: rnd(d.warm) }, | |
| 62 | + { title: "cold cache · M3 Ultra (run #2)", bars: rnd(d.cold) }, | |
| 63 | + ], | |
| 64 | + caption: "Run #2 falsified the registered hypothesis: raw mmap wins warm (memory speed) " + | |
| 65 | + "but collapses cold to page-fault IO (~8–16 KiB, QD 1) while chunked zarr reads 32 MiB " + | |
| 66 | + "blocks. The rule is IO granularity, not the container.", | |
| 67 | + }); | |
| 68 | + } | |
| 69 | + if (d.compute.length) { | |
| 70 | + const key = (c) => `${c.backend}·${c.source}`; | |
| 71 | + const groupsMap = new Map(); | |
| 72 | + for (const c of d.compute) { | |
| 73 | + const ms = 1000 * c.s_per_forward.reduce((a, b) => a + b, 0) / c.s_per_forward.length; | |
| 74 | + if (!groupsMap.has(key(c))) groupsMap.set(key(c), { backend: c.backend, source: c.source, values: {} }); | |
| 75 | + groupsMap.get(key(c)).values[c.mode] = ms; | |
| 76 | + } | |
| 77 | + const label = (g) => g.backend === "torch-mps" ? "torch-MPS (synthetic)" : | |
| 78 | + g.source === "synthetic" ? "MLX (synthetic)" : "MLX (Qwen3-0.6B-4bit)"; | |
| 79 | + html += Charts.groupedBarSvg({ | |
| 80 | + title: "Capture overhead per forward pass — retention is nearly free under MLX", | |
| 81 | + yLabel: "ms / forward", unit: "ms", | |
| 82 | + groups: [...groupsMap.values()].map((g) => ({ label: label(g), values: g.values })), | |
| 83 | + modes: [ | |
| 84 | + { key: "plain", label: "plain", color: Charts.S1 }, | |
| 85 | + { key: "retain", label: "retain", color: Charts.S2 }, | |
| 86 | + { key: "retain+copy+write", label: "retain+copy+write", color: Charts.S3 }, | |
| 87 | + ], | |
| 88 | + caption: "Runs #1 and #3: per-layer retention costs 1.02× (MLX synthetic) and 1.004× " + | |
| 89 | + "(real 4-bit checkpoint) vs 1.22× on torch-MPS; copy+write stays ≤1.5× everywhere. " + | |
| 90 | + "3 repeats per cell; manifests embedded in the raw JSON.", | |
| 91 | + }); | |
| 92 | + } | |
| 93 | + return html; | |
| 94 | +} | |
| 20 | 95 | |
| 21 | 96 | const app = express(); |
| 22 | 97 | const PORT = process.env.PORT || 8140; |
@@ -105,6 +180,17 @@ app.get("/", (req, res) => { | ||
| 105 | 180 | </div> |
| 106 | 181 | </section> |
| 107 | 182 | |
| 183 | +${(() => { | |
| 184 | + const probe = probeMapCharts("lang_id"); | |
| 185 | + const exph = expHCharts(); | |
| 186 | + return probe || exph ? `<section class="card"> | |
| 187 | + <h2>First measured maps</h2> | |
| 188 | + <p class="lede-small">Every figure below is regenerated from committed code + versioned results — | |
| 189 | + raw JSON with hardware manifests under <a href="/results">Results</a>.</p> | |
| 190 | + ${probe}${exph} | |
| 191 | +</section>` : ""; | |
| 192 | + })()} | |
| 193 | + | |
| 108 | 194 | <section class="card"> |
| 109 | 195 | <h2>Latest from the research log</h2> |
| 110 | 196 | <div class="log-grid">${logHtml || "<p>No log entries yet.</p>"}</div> |
@@ -201,9 +287,13 @@ app.get("/experiments/:id", (req, res) => { | ||
| 201 | 287 | const codeLink = C.exists(path.posix.join(exp.rel, "benchmark.py")) |
| 202 | 288 | ? `<p><a class="more" href="/file/${exp.rel}/benchmark.py">View benchmark implementation (benchmark.py) →</a></p>` |
| 203 | 289 | : ""; |
| 290 | + let charts = ""; | |
| 291 | + if (exp.id === "expH_capture_cost_frontier") charts = expHCharts(); | |
| 292 | + if (exp.id === "expA_probe_reliability") charts = probeMapCharts(); | |
| 293 | + if (charts) charts = `<section class="card"><h2>Result maps</h2>${charts}</section>`; | |
| 204 | 294 | const body = `<p class="crumb"><a href="/experiments">Experiments</a> / ${R.esc(exp.id)}</p> |
| 205 | 295 | <h1 class="page-title">${R.esc(exp.id)}</h1><p class="lede-small">${R.esc(exp.purpose)}</p> |
| 206 | −${codeLink}${sections.join("")}${runsHtml}`; | |
| 296 | +${codeLink}${charts}${sections.join("")}${runsHtml}`; | |
| 207 | 297 | page(res, { title: exp.id, active: "Experiments", body }); |
| 208 | 298 | }); |
| 209 | 299 | |
@@ -251,9 +341,12 @@ app.get("/atlas/:model/:mapType/:version", (req, res) => { | ||
| 251 | 341 | const files = entry.files |
| 252 | 342 | .map((f) => `<li><a href="/results/${f.rel}">${f.name}</a> <span class="mono-small">${(f.size / 1024).toFixed(1)} KiB</span></li>`) |
| 253 | 343 | .join(""); |
| 344 | + const mapCharts = (model === "qwen3-0.6b-4bit" && mapType === "probes" && version === "v1") | |
| 345 | + ? probeMapCharts() : ""; | |
| 254 | 346 | const body = `<p class="crumb"><a href="/atlas">Atlas</a> / ${R.esc(entry.rel)}</p> |
| 255 | 347 | <h1 class="page-title">${R.esc(model)} — ${R.esc(mapType)} <span class="mono-small">${R.esc(version)}</span></h1> |
| 256 | 348 | <p>${R.levelBadge(entry.level)}</p> |
| 349 | +${mapCharts ? `<section class="card"><h2>The map</h2>${mapCharts}</section>` : ""} | |
| 257 | 350 | ${confidence ? `<section class="card"><h2>Confidence</h2><div class="md">${R.markdownToHtml(confidence.content, entry.rel + "/confidence.md")}</div></section>` : ""} |
| 258 | 351 | <section class="card"><h2>Provenance</h2>${prov}</section> |
| 259 | 352 | <section class="card"><h2>Files</h2><ul class="file-list">${files}</ul></section>`; |
| 260 | 353 | |