spb/localvm-research Public License
Running LLMs larger than memory on a consumer Mac — falsification-driven research: margin-gated deferred refinement, out-of-core verification on Apple Silicon. TR-01 published.
Python 63.2%
JavaScript 23.5%
CSS 11.8%
Shell 0.9%
Makefile 0.5%
1// ============================================================================2// Project : localvm-research3// File : web/lib/render.js4// Purpose : HTML layout, markdown/code rendering, SVG chart for the platform5// Author : Simon-Pierre Boucher6// Contact : contact@spboucher.ai7// Created : 2026-08-128// Modified : 2026-08-129// Platform : macOS / Apple Silicon (arm64) — Node.js10// License : All rights reserved (research code)11// ============================================================================12"use strict";1314const { Marked } = require("marked");15const hljs = require("highlight.js");1617function esc(s) {18 return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));19}2021function slugify(s) {22 return s.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 80);23}2425/**26 * Render markdown → { html, toc }. A fresh Marked instance per call (a shared27 * global `marked.use()` would stack walkTokens handlers on every render).28 * Headings get stable anchor ids collected into a table of contents.29 */30function markdownToHtml(md, baseRel) {31 const toc = [];32 const seen = new Set();33 const m = new Marked({34 renderer: {35 code({ text, lang }) {36 const html = lang && hljs.getLanguage(lang)37 ? hljs.highlight(text, { language: lang }).value38 : esc(text);39 return `<pre class="codeblock"><code class="hljs">${html}</code></pre>`;40 },41 heading({ tokens, depth }) {42 const html = this.parser.parseInline(tokens);43 const plain = html.replace(/<[^>]+>/g, "");44 let id = slugify(plain) || "section";45 while (seen.has(id)) id += "-x";46 seen.add(id);47 if (depth >= 2 && depth <= 3) toc.push({ depth, id, text: plain });48 return `<h${depth} id="${id}">${html}<a class="hanchor" href="#${id}" aria-label="Link to section">#</a></h${depth}>\n`;49 },50 },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 }57 if (token.type === "link" && token.href && !/^(https?:|mailto:|#|\/)/.test(token.href)) {58 const base = baseRel.includes("/") ? baseRel.slice(0, baseRel.lastIndexOf("/")) : "";59 const joined = (base ? base + "/" : "") + token.href;60 const norm = joined.split("/").reduce((acc, part) => {61 if (part === "..") acc.pop();62 else if (part !== "." && part !== "") acc.push(part);63 return acc;64 }, []).join("/");65 token.href = norm.endsWith(".md") ? "/doc/" + norm : "/file/" + norm;66 }67 },68 });69 return { html: m.parse(md), toc };70}7172/** Sticky table-of-contents sidebar for long documents. */73function tocHtml(toc) {74 if (!toc || toc.length < 3) return "";75 const items = toc76 .map((t) => `<li class="toc-d${t.depth}"><a href="#${t.id}">${esc(t.text)}</a></li>`)77 .join("");78 return `<nav class="toc" aria-label="Table of contents"><span class="toc-title">On this page</span><ul>${items}</ul></nav>`;79}8081/** Professional document header from front matter. */82function docHeader(data, rel) {83 const title = (data.document || rel).split("/").pop().replace(/[_-]/g, " ");84 const status = data.status || "draft";85 const meta = [86 data.created ? `created ${data.created}` : null,87 data.modified ? `updated ${data.modified}` : null,88 data.author || null,89 ].filter(Boolean).join(" · ");90 return `<header class="doc-head">91 <div class="doc-head-row">92 <h1>${esc(title)}</h1>93 <span class="badge badge-${status === "reviewed" || status === "final" ? "done" : "in-progress"}">${esc(status)}</span>94 </div>95 <p class="doc-head-meta">${esc(data.document || rel)}${meta ? " · " + esc(meta) : ""}</p>96 </header>`;97}9899/** Research-log timeline: entries already split by content.parseLogEntries. */100function logTimeline(entries, baseRel) {101 const items = entries102 .slice()103 .reverse()104 .map((e) => {105 const [when, ...rest] = e.title.split(" — ");106 const title = rest.join(" — ") || when;107 const body = markdownToHtml(e.body, baseRel).html;108 return `<article class="tl-entry">109 <div class="tl-rail"><span class="tl-dot"></span></div>110 <div class="tl-card">111 <time class="tl-time">${esc(rest.length ? when : "")}</time>112 <h3>${esc(title)}</h3>113 <div class="md md-compact">${body}</div>114 </div>115 </article>`;116 })117 .join("");118 return `<div class="timeline">${items}</div>`;119}120121function highlightFile(text, filename) {122 const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase();123 const langMap = {124 py: "python", sh: "bash", js: "javascript", css: "css", json: "json",125 toml: "ini", yaml: "yaml", yml: "yaml", cff: "yaml", md: "markdown",126 metal: "cpp", cpp: "cpp", h: "cpp", hpp: "cpp", swift: "swift", m: "objectivec",127 };128 const lang = filename === "Makefile" ? "makefile" : langMap[ext];129 if (lang && hljs.getLanguage(lang)) return hljs.highlight(text, { language: lang }).value;130 return esc(text);131}132133const NAV = [134 ["/", "Home"],135 ["/research", "Research"],136 ["/experiments", "Experiments"],137 ["/results", "Results"],138 ["/code", "Code"],139 ["/doc/research/bibliography.md", "Bibliography"],140 ["/publications", "Publications"],141 ["/comments", "Comments"],142 ["/about", "About"],143];144145function layout({ title, active, body, buildInfo = {} }) {146 const nav = NAV.map(147 ([href, label]) =>148 `<a href="${href}" class="${active === label ? "active" : ""}">${label}</a>`149 ).join("");150 const commit = buildInfo.commit ? buildInfo.commit.slice(0, 7) : "dev";151 const synced = buildInfo.generated ? buildInfo.generated.slice(0, 10) : "";152 return `<!DOCTYPE html>153<html lang="en">154<head>155<meta charset="utf-8">156<meta name="viewport" content="width=device-width, initial-scale=1">157<title>${esc(title)} · localvm-research</title>158<meta name="description" content="localvm-research — running LLMs larger than memory on consumer Apple Silicon. Research by Simon-Pierre Boucher.">159<link rel="stylesheet" href="/static/style.css?v=${esc(commit)}">160<link rel="icon" type="image/svg+xml" href="/static/logo.svg">161<link rel="apple-touch-icon" href="/static/logo.svg">162</head>163<body>164<header class="site-header">165 <div class="wrap header-row">166 <a class="brand" href="/"><img class="brand-logo" src="/static/logo.svg" alt="" width="26" height="26">localvm<span class="brand-dim">-research</span></a>167 <button class="nav-toggle" id="nav-toggle" aria-label="Menu" aria-expanded="false">168 <span></span><span></span><span></span>169 </button>170 <nav class="site-nav" id="site-nav">${nav}</nav>171 </div>172</header>173<main class="wrap">${body}</main>174<footer class="site-footer">175 <div class="wrap footer-grid">176 <div>177 <span class="footer-brand">localvm-research</span>178 <p class="footer-tag">Running LLMs larger than memory on consumer Apple Silicon —179 an open, falsification-driven research trail.</p>180 </div>181 <div class="footer-col">182 <span class="footer-h">Project</span>183 <a href="/doc/CLAUDE.md">Charter</a>184 <a href="/doc/research/LOG.md">Research log</a>185 <a href="/experiments">Experiments</a>186 </div>187 <div class="footer-col">188 <span class="footer-h">Author</span>189 <span>Simon-Pierre Boucher</span>190 <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>191 <span class="footer-meta">snapshot ${esc(commit)}${synced ? " · " + esc(synced) : ""}</span>192 </div>193 </div>194 <div class="wrap footer-legal">© 2026 Simon-Pierre Boucher · All rights reserved (research code) · Apple M5 Max / 48 GB / macOS 27</div>195</footer>196<script src="/static/app.js?v=${esc(commit)}" defer></script>197</body>198</html>`;199}200201/**202 * SSD throughput line chart (log-y), server-rendered SVG.203 * Form: change-of-magnitude across an ordered log2 x (block size), 3 series (QD).204 * Palette: validated categorical slots 1–3 (see dataviz reference palette);205 * relief for the aqua WARN is provided by direct labels at line ends.206 */207function ssdChartSvg(chart) {208 if (!chart) return "";209 const SERIES_COLORS = { 1: "#2a78d6", 4: "#eb6834", 8: "#1baf7a" };210 const W = 720, H = 340, ML = 64, MR = 96, MT = 18, MB = 44;211 const iw = W - ML - MR, ih = H - MT - MB;212 const blocks = [4096, 16384, 65536, 262144, 1048576];213 const xPos = (b) => ML + (Math.log2(b) - 12) / (20 - 12) * iw;214 const yMin = Math.log10(50), yMax = Math.log10(20000);215 const yPos = (v) => MT + ih - ((Math.log10(v) - yMin) / (yMax - yMin)) * ih;216217 const gridVals = [100, 1000, 10000];218 let g = "";219 for (const v of gridVals) {220 g += `<line x1="${ML}" y1="${yPos(v)}" x2="${ML + iw}" y2="${yPos(v)}" class="grid"/>` +221 `<text x="${ML - 8}" y="${yPos(v) + 4}" class="tick" text-anchor="end">${v >= 1000 ? v / 1000 + " GB/s" : v + " MB/s"}</text>`;222 }223 let xticks = "";224 for (const b of blocks) {225 const lbl = b >= 1048576 ? "1 MiB" : b >= 1024 ? b / 1024 + " KiB" : b + " B";226 xticks += `<text x="${xPos(b)}" y="${MT + ih + 20}" class="tick" text-anchor="middle">${lbl}</text>`;227 }228 let lines = "", dots = "", labels = "";229 const qds = Object.keys(chart.series).map(Number).sort((a, b) => a - b);230 for (const qd of qds) {231 const pts = chart.series[qd].filter((p) => blocks.includes(p.block));232 if (!pts.length) continue;233 const c = SERIES_COLORS[qd] || "#2a78d6";234 const d = pts.map((p, i) => `${i ? "L" : "M"}${xPos(p.block).toFixed(1)},${yPos(p.mbps).toFixed(1)}`).join(" ");235 lines += `<path d="${d}" fill="none" stroke="${c}" stroke-width="2" stroke-linejoin="round"/>`;236 for (const p of pts) {237 const val = p.mbps >= 1000 ? (p.mbps / 1000).toFixed(1) + " GB/s" : Math.round(p.mbps) + " MB/s";238 dots += `<circle cx="${xPos(p.block).toFixed(1)}" cy="${yPos(p.mbps).toFixed(1)}" r="4.5" fill="${c}" stroke="#fcfcfb" stroke-width="2" class="pt" data-tip="QD${qd} · ${p.block >= 1048576 ? "1 MiB" : p.block / 1024 + " KiB"} random (cold): ${val}"/>`;239 }240 const last = pts[pts.length - 1];241 labels += `<text x="${xPos(last.block) + 12}" y="${yPos(last.mbps) + 4}" class="series-label" fill="${c}">QD ${qd}</text>`;242 }243 return `<figure class="chart-fig">244<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="Cold random SSD read throughput by block size and queue depth, log scale">245<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/>246${g}${xticks}${lines}${dots}${labels}247<text x="${ML + iw / 2}" y="${H - 6}" class="axis-title" text-anchor="middle">read block size (random, F_NOCACHE cold, log–log)</text>248</svg>249<div id="chart-tip" class="chart-tip" hidden></div>250<figcaption>Measured on this project's M5 Max (expH, run ${esc(chart.run)}, 3 repeats/cell, iostat-validated ceiling ≈ 13.1 GB/s). Lines: reader queue depth.</figcaption>251</figure>`;252}253254module.exports = { esc, markdownToHtml, tocHtml, docHeader, logTimeline, highlightFile, layout, ssdChartSvg };255