<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ferric — a pure-Rust transformer in your browser (WebGPU)</title>
<style>
:root { --bg:#0b0e14; --panel:#111725; --line:#1e2842; --ink:#e6ebf5; --dim:#8ea0c0; --gold:#e8b24a; --ok:#4ade80; --accent:#6ea8fe; }
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.55 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif; }
.wrap { max-width:820px; margin:0 auto; padding:48px 22px 72px; }
h1 { font-size:34px; margin:0 0 6px; letter-spacing:-.02em; }
h1 .phi { color:var(--gold); }
.sub { color:var(--dim); font-size:16px; margin:0 0 28px; }
.panel { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:20px 22px; margin:16px 0; }
.row { display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
label { color:var(--dim); font-size:13px; }
input { background:#0a0f1c; color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:9px 11px; font:14px ui-monospace,monospace; }
input#prompt { flex:1; min-width:180px; }
input#steps { width:70px; }
button { background:var(--gold); color:#0b0e14; border:0; border-radius:8px; padding:10px 18px; font-weight:650; font-size:14px; cursor:pointer; }
button:disabled { opacity:.5; cursor:default; }
.chips { display:flex; flex-wrap:wrap; gap:7px; margin-top:6px; }
.chip { font:13px ui-monospace,monospace; padding:5px 9px; border-radius:7px; border:1px solid var(--line); background:#0a0f1c; color:var(--dim); }
.chip.gen { background:rgba(232,178,74,.12); border-color:rgba(232,178,74,.5); color:var(--gold); }
.kv { display:grid; grid-template-columns:auto 1fr; gap:6px 16px; font:13px ui-monospace,monospace; }
.kv b { color:var(--dim); font-weight:500; }
.ok { color:var(--ok); }
.muted { color:var(--dim); font-size:13px; }
a { color:var(--accent); text-decoration:none; }
.banner { border-left:3px solid var(--gold); padding:10px 14px; background:rgba(232,178,74,.07); border-radius:0 8px 8px 0; color:var(--dim); font-size:13.5px; }
code { color:var(--gold); font-family:ui-monospace,monospace; }
</style>
</head>
<body>
<div class="wrap">
<h1><span class="phi">Ferric</span> — a transformer, in your browser</h1>
<p class="sub">A complete Llama/SmolVLA-style language model — RoPE attention, RMSNorm, SwiGLU — running on <b>WebGPU</b>, compiled from <b>pure Rust</b> to WebAssembly. The exact same kernels run on a datacenter GPU.</p>
<div class="panel" id="statusPanel">
<div class="kv">
<b>status</b><span id="status">initializing…</span>
<b>backend</b><span id="backend">—</span>
<b>adapter</b><span id="adapter">—</span>
</div>
</div>
<div class="panel">
<div class="row">
<label>prompt (token ids)</label>
<input id="prompt" value="3,14,1,15" spellcheck="false">
<label>steps</label>
<input id="steps" type="number" value="6" min="1" max="32">
<button id="run" disabled>Generate ▸</button>
</div>
<div id="result" style="margin-top:16px; display:none">
<div class="muted">prompt → generated</div>
<div class="chips" id="chips"></div>
<div class="kv" style="margin-top:16px">
<b>correctness</b><span id="diff">—</span>
<b>latency</b><span id="ms">—</span>
<b>model</b><span id="model">—</span>
</div>
</div>
</div>
<div class="banner" id="gpuNote" style="display:none">
This page needs <b>WebGPU</b>. Open it in Chrome or Edge (recent), or Safari Technology Preview with WebGPU enabled.
</div>
<p class="muted" style="margin-top:24px">
Weights here are fixed pseudo-random (an untrained demo net), so generation is deterministic, not meaningful text — the point is that a real transformer's <i>compute</i> runs correctly in a browser tab. The GPU logits are checked live against an in-page CPU reference. ·
<a href="https://github.com/dcharlot-physicalai-bmi/ferric" target="_blank">source on GitHub</a>
</p>
</div>
<script type="module">
import init, { ferric_lm_demo } from './pkg/ferric_web.js';
const $ = id => document.getElementById(id);
const set = (id, t) => { $(id).textContent = t; };
async function boot() {
if (!navigator.gpu) { set('status','WebGPU not available'); $('gpuNote').style.display='block'; return; }
await init();
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { set('status','no WebGPU adapter'); $('gpuNote').style.display='block'; return; }
set('status','ready'); $('status').className='ok';
$('run').disabled = false;
$('run').onclick = run;
run();
}
async function run() {
$('run').disabled = true; set('status','running on WebGPU…'); $('status').className='';
try {
const prompt = $('prompt').value.trim();
const steps = Math.max(1, Math.min(32, parseInt($('steps').value) || 6));
const res = JSON.parse(await ferric_lm_demo(prompt, steps));
set('backend', res.backend);
set('adapter', res.adapter);
$('result').style.display = 'block';
const chips = res.prompt.map(t => `<span class="chip">${t}</span>`)
.concat(res.generated.map(t => `<span class="chip gen">${t}</span>`)).join('');
$('chips').innerHTML = chips;
$('diff').innerHTML = `GPU vs CPU reference max|Δ| = <b>${res.logit_diff}</b> ${res.logit_diff < 1e-3 ? '<span class="ok">✓ match</span>':''}`;
set('ms', `${res.ms.toFixed(1)} ms (${res.generated.length} tokens)`);
set('model', `${res.layers} layers · vocab ${res.vocab} · RoPE + RMSNorm + SwiGLU`);
set('status','ready'); $('status').className='ok';
} catch(e) { set('status','error: ' + (e.message||e)); $('status').className=''; }
$('run').disabled = false;
}
boot();
</script>
</body>
</html>