BigMoeOnEdge/scripts/route-viewer.py
Helldez f850fc66cd feat(scripts): route-viewer.py — read a route trace without a spreadsheet
Packs a --route-trace session into one self-contained HTML page: the step x layer matrix
with the expert ids in its cells (miss/hit coloured, ids repeated from the previous step
marked), a miss-density heatmap of the whole run, hot experts per layer, the per-token
metrics with majflt highlighted, and the raw rows behind a filter.

Companion to route-analyze.py, which answers the same questions in a terminal. Stdlib
only, and the data is packed into the page, so it opens with no network and nothing
installed. Models are discovered from the session directory rather than hardcoded.

The page rounds weights to 4 decimals for size; the CSVs beside it keep full precision,
and it says so.
2026-07-15 09:52:09 +02:00

630 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# Pack a --route-trace session into one self-contained HTML viewer.
#
# Companion to route-analyze.py: that one answers questions in a terminal, this one puts every
# routed expert on a page you can scroll — the step x layer matrix with the expert ids in its
# cells, hot experts per layer, the per-token metrics, and the raw rows. Stdlib only, and the
# data is packed into the page, so the result opens anywhere with no network and no spreadsheet.
#
# python scripts/route-viewer.py docs/bench-data/<session>/ viewer.html
#
# Reads every <tag>.route.csv in the session directory that has a <tag>.metrics sink beside it.
import json, os, sys
from collections import defaultdict, Counter
D = sys.argv[1]
OUT = sys.argv[2]
MIB = 1048576.0
def discover(d):
"""Every traced model in the session: <tag>.route.csv with a <tag>.metrics beside it."""
out = []
for fn in sorted(os.listdir(d)):
if not fn.endswith(".route.csv"):
continue
tag = fn[: -len(".route.csv")]
if not os.path.exists(os.path.join(d, tag + ".metrics")):
print("skipping %s: no %s.metrics beside it" % (tag, tag))
continue
out.append(tag)
return out
def load_route(path):
meta, eb, db = {}, {}, {}
cells = defaultdict(list) # (phase, step, layer) -> [(slot,e,w,r,bytes)]
with open(path) as f:
for line in f:
if line.startswith("#"):
kv = dict(t.split("=", 1) for t in line[1:].split() if "=" in t)
if "layer" in kv and "expert_bytes" in kv:
eb[int(kv["layer"])] = int(kv["expert_bytes"])
db[int(kv["layer"])] = int(kv["dense_bytes"])
elif "n_expert" in kv:
meta = kv
continue
if line.startswith("turn"):
continue
p = line.rstrip("\n").split(",")
if len(p) < 9:
continue
ph, st, l, sl, e = int(p[1]), int(p[2]), int(p[3]), int(p[4]), int(p[5])
w = 0.0 if p[6] == "nan" else float(p[6])
cells[(ph, st, l)].append((sl, e, w, int(p[7]), int(p[8])))
return meta, eb, db, cells
def load_metrics(path):
rows = []
summary = {}
with open(path) as f:
for line in f:
if line.startswith("# summary"):
for tok in line.split():
if "=" in tok:
k, v = tok.split("=", 1)
summary[k] = v
continue
rows.append(line.rstrip("\n"))
hdr = rows[0].split(",")
out = []
for r in rows[1:]:
if not r.strip():
continue
out.append(r.split(","))
return hdr, out, summary
payload = {}
for tag in discover(D):
meta, eb, db, cells = load_route(f"{D}/{tag}.route.csv")
hdr, mrows, summary = load_metrics(f"{D}/{tag}.metrics")
# The preamble records the model path the run used; its basename is the friendliest label.
label = os.path.basename(meta.get("model", "")).replace(".gguf", "") or tag
k = int(meta["n_expert_used"])
ne = int(meta["n_expert"])
moe = sorted([l for l in eb if eb[l] > 0])
# compact cell encoding: "phase|step|layer|e:w4:r;e:w4:r;..."
lines = []
for (ph, st, l) in sorted(cells):
rec = sorted(cells[(ph, st, l)])
body = ";".join("%d:%d:%d" % (e, round(w * 10000), r) for _s, e, w, r, _b in rec)
lines.append("%d|%d|%d|%s" % (ph, st, l, body))
# derived stats (decode only)
uniq = defaultdict(set)
freq = defaultdict(Counter)
for (ph, st, l), rec in cells.items():
if ph != 1:
continue
for _s, e, _w, _r, _b in rec:
uniq[l].add(e)
freq[l][e] += 1
per_expert = sum(eb[l] for l in moe) / len(moe) / MIB
bank = sum(eb[l] * ne for l in moe) / MIB
touched = sum(eb[l] * len(uniq[l]) for l in moe) / MIB
dense = sum(db.values()) / MIB
# residency split (decode)
res = [0, 0, 0]
for (ph, st, l), rec in cells.items():
if ph != 1:
continue
for _s, _e, _w, r, _b in rec:
res[r] += 1
payload[tag] = {
"label": label, "arch": meta["arch"], "model": meta.get("model", ""),
"n_layer": int(meta["n_layer"]), "n_expert": ne, "k": k, "moe": moe,
"expert_bytes": {str(l): eb[l] for l in moe},
"dense_bytes": {str(l): db[l] for l in sorted(db)},
"per_expert_mib": round(per_expert, 2), "bank_mib": round(bank),
"touched_mib": round(touched), "dense_mib": round(dense),
"res": res,
"summary": summary,
"metrics_hdr": hdr, "metrics": mrows,
"hot": {str(l): freq[l].most_common(12) for l in moe},
"uniq": {str(l): len(uniq[l]) for l in moe},
"cells": "\n".join(lines),
}
DATA_JSON = json.dumps(payload, separators=(",", ":"))
HTML = r"""<title>Route trace — MoE expert routing, on device</title>
<style>
:root{
--ground:#F5F8F8; --surface:#FFFFFF; --sunk:#EDF2F2; --line:#DAE3E4; --line-soft:#E8EEEE;
--ink:#0F1A1C; --ink-2:#3B4C50; --muted:#67797E;
--accent:#0E8C80; --accent-soft:#D5EDEA;
--hot:#B65A12; --hot-soft:#F7E3D2;
--shadow:0 1px 2px rgba(15,26,28,.06),0 8px 24px -12px rgba(15,26,28,.18);
}
@media (prefers-color-scheme:dark){
:root{
--ground:#0C1113; --surface:#141C1E; --sunk:#101719; --line:#253236; --line-soft:#1C282B;
--ink:#E2ECEE; --ink-2:#A8BCC0; --muted:#7B9096;
--accent:#3FC0B0; --accent-soft:#12312F;
--hot:#E08A43; --hot-soft:#33210F;
--shadow:0 1px 2px rgba(0,0,0,.4),0 12px 32px -14px rgba(0,0,0,.7);
}
}
:root[data-theme="dark"]{
--ground:#0C1113; --surface:#141C1E; --sunk:#101719; --line:#253236; --line-soft:#1C282B;
--ink:#E2ECEE; --ink-2:#A8BCC0; --muted:#7B9096;
--accent:#3FC0B0; --accent-soft:#12312F;
--hot:#E08A43; --hot-soft:#33210F;
--shadow:0 1px 2px rgba(0,0,0,.4),0 12px 32px -14px rgba(0,0,0,.7);
}
:root[data-theme="light"]{
--ground:#F5F8F8; --surface:#FFFFFF; --sunk:#EDF2F2; --line:#DAE3E4; --line-soft:#E8EEEE;
--ink:#0F1A1C; --ink-2:#3B4C50; --muted:#67797E;
--accent:#0E8C80; --accent-soft:#D5EDEA;
--hot:#B65A12; --hot-soft:#F7E3D2;
--shadow:0 1px 2px rgba(15,26,28,.06),0 8px 24px -12px rgba(15,26,28,.18);
}
*{box-sizing:border-box}
body{
margin:0;background:var(--ground);color:var(--ink);
font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
font-size:15px;line-height:1.55;-webkit-font-smoothing:antialiased;
}
.mono{font-family:ui-monospace,"SF Mono","Cascadia Mono","JetBrains Mono",Menlo,Consolas,monospace;
font-variant-numeric:tabular-nums}
.wrap{max-width:1500px;margin:0 auto;padding:28px 22px 80px}
h1{font-size:clamp(22px,3.2vw,30px);line-height:1.15;letter-spacing:-.02em;margin:0;text-wrap:balance}
.lede{color:var(--ink-2);max-width:66ch;margin:10px 0 0}
.eyebrow{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);
font-weight:600;margin:0 0 8px}
header{border-bottom:1px solid var(--line);padding-bottom:22px;margin-bottom:22px}
.warn{
display:flex;gap:10px;align-items:flex-start;margin-top:16px;padding:11px 14px;
border:1px solid var(--hot);border-left-width:3px;border-radius:5px;
background:var(--hot-soft);color:var(--ink);font-size:13.5px;max-width:78ch;
}
.warn b{color:var(--hot)}
.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 18px}
.seg{display:inline-flex;background:var(--sunk);border:1px solid var(--line);border-radius:7px;padding:3px;gap:2px}
.seg button{
font:inherit;font-size:13px;font-weight:550;color:var(--ink-2);background:none;border:0;
padding:6px 13px;border-radius:5px;cursor:pointer;white-space:nowrap;
}
.seg button:hover{color:var(--ink)}
.seg button[aria-pressed="true"]{background:var(--surface);color:var(--ink);box-shadow:var(--shadow)}
.seg button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.spacer{flex:1}
label.ctl{font-size:12.5px;color:var(--muted);display:inline-flex;gap:7px;align-items:center}
select,input[type=search]{
font:inherit;font-size:13px;background:var(--surface);color:var(--ink);
border:1px solid var(--line);border-radius:6px;padding:5px 8px;
}
select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:10px;margin-bottom:20px}
.tile{background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:12px 13px}
.tile .k{font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:600}
.tile .v{font-size:21px;font-weight:600;letter-spacing:-.02em;margin-top:3px}
.tile .s{font-size:11.5px;color:var(--muted);margin-top:1px}
.tile.hotv .v{color:var(--hot)}
.panel{background:var(--surface);border:1px solid var(--line);border-radius:9px;box-shadow:var(--shadow);
overflow:hidden;margin-bottom:18px}
.panel > h2{font-size:14px;margin:0;padding:12px 15px;border-bottom:1px solid var(--line-soft);
letter-spacing:-.01em}
.panel .note{padding:11px 15px;font-size:12.5px;color:var(--muted);border-bottom:1px solid var(--line-soft)}
.scroll{overflow:auto;max-height:70vh}
table{border-collapse:collapse;width:100%;font-size:12.5px}
th,td{text-align:right;padding:5px 9px;border-bottom:1px solid var(--line-soft);white-space:nowrap}
th{position:sticky;top:0;background:var(--sunk);z-index:2;font-size:10.5px;letter-spacing:.07em;
text-transform:uppercase;color:var(--muted);font-weight:600}
td:first-child,th:first-child{text-align:left}
tbody tr:hover td{background:var(--sunk)}
/* ── the matrix ───────────────────────────────────────────────── */
.mx{border-collapse:separate;border-spacing:0;font-size:11px}
.mx th{padding:4px 6px;text-align:center;font-size:10px}
.mx th.stepcol{position:sticky;left:0;z-index:3;text-align:right}
.mx td.stepcol{position:sticky;left:0;background:var(--sunk);z-index:1;text-align:right;
color:var(--muted);font-size:10.5px;padding:2px 7px;border-right:1px solid var(--line)}
.mx td{padding:2px 4px;border-bottom:1px solid var(--line-soft);text-align:left;vertical-align:top}
.cell{display:flex;flex-wrap:wrap;gap:2px}
.e{padding:0 2px;border-radius:3px;color:var(--ink-2);cursor:default}
.e.miss{background:var(--hot-soft);color:var(--hot);font-weight:600}
.e.spec{background:var(--accent-soft);color:var(--accent);font-weight:600}
.e.rep{box-shadow:inset 0 -2px 0 var(--accent)}
.mbcell{text-align:right;font-size:10.5px}
.legend{display:flex;flex-wrap:wrap;gap:14px;padding:10px 15px;font-size:12px;color:var(--muted);
border-bottom:1px solid var(--line-soft)}
.legend span{display:inline-flex;align-items:center;gap:6px}
.sw{width:11px;height:11px;border-radius:3px;display:inline-block}
.bars td.bar{width:100%;padding-right:14px}
.b{height:9px;background:var(--accent);border-radius:2px;min-width:2px;display:block}
.b.hot{background:var(--hot)}
canvas{display:block;width:100%;height:auto}
.pager{display:flex;gap:8px;align-items:center;padding:10px 15px;border-top:1px solid var(--line-soft);
font-size:12.5px;color:var(--muted)}
.pager button{font:inherit;font-size:12.5px;background:var(--surface);color:var(--ink);
border:1px solid var(--line);border-radius:6px;padding:4px 11px;cursor:pointer}
.pager button:disabled{opacity:.4;cursor:default}
.pager button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
input[type=range]{accent-color:var(--accent);flex:1;max-width:340px}
footer{margin-top:34px;padding-top:18px;border-top:1px solid var(--line);color:var(--muted);font-size:12.5px}
a{color:var(--accent)}
.hide{display:none}
</style>
<div class="wrap">
<header>
<p class="eyebrow">BigMoeOnEdge · bench-data / 2026-07-15</p>
<h1>Route trace — which experts each layer actually picks</h1>
<p class="lede">Every routed expert of three MoE models, captured on a OnePlus 15R with
<code class="mono">--route-trace</code>. One row per step, layer and rank slot: the expert id, the
weight the router gave it, and whether it was already in the cache.</p>
<div class="warn"><span>⚠</span><div><b>These are not benchmark numbers.</b> Every run had the trace on,
which costs a graph barrier per layer. The throughput figures are context for reading the routing —
routing itself is deterministic and unaffected. Don't compare them with the tables in
<code class="mono">benchmarks.md</code>.</div></div>
</header>
<div class="bar">
<div class="seg" id="models"></div>
<div class="spacer"></div>
<div class="seg" id="views"></div>
</div>
<div class="tiles" id="tiles"></div>
<div id="view-matrix">
<div class="panel">
<h2>Miss density — the whole run at a glance</h2>
<div class="note">Each column is a decode step, each row a layer. Amber = that layer routed an
expert it had to read from flash. This is where the time goes.</div>
<div style="padding:12px 15px"><canvas id="heat"></canvas></div>
</div>
<div class="panel">
<h2>Step × layer matrix</h2>
<div class="legend">
<span><i class="sw" style="background:var(--hot-soft);border:1px solid var(--hot)"></i> miss — read from flash</span>
<span><i class="sw" style="background:var(--sunk);border:1px solid var(--line)"></i> hit — already resident</span>
<span><i class="sw" style="background:var(--accent-soft);border:1px solid var(--accent)"></i> prefetch hit</span>
<span><i class="sw" style="box-shadow:inset 0 -2px 0 var(--accent);border:1px solid var(--line)"></i> also routed on the previous step</span>
</div>
<div class="bar" style="padding:10px 15px;margin:0">
<label class="ctl">phase
<select id="phase"><option value="1">decode</option><option value="0">prefill</option></select>
</label>
<label class="ctl">cells
<select id="cellmode">
<option value="experts">expert ids</option>
<option value="weights">ids + routing %</option>
<option value="mb">MiB read</option>
</select>
</label>
<label class="ctl">layers
<select id="layerspan"></select>
</label>
<div class="spacer"></div>
<span class="ctl mono" id="steplabel"></span>
</div>
<div class="scroll" id="mxwrap"></div>
<div class="pager">
<button id="prev">← earlier</button>
<input type="range" id="stepslider" min="0" value="0">
<button id="next">later →</button>
<span id="pagenote"></span>
</div>
</div>
</div>
<div id="view-hot" class="hide">
<div class="panel">
<h2>Hot experts per layer</h2>
<div class="note">Decode only. <b>unique</b> is how many of the bank the layer touched at all;
<b>top-8</b> is the share of activations its eight busiest experts carried. A layer with high
unique and low top-8 routes near-uniformly — nothing worth preloading there.</div>
<div class="scroll"><table id="hottab"></table></div>
</div>
</div>
<div id="view-metrics" class="hide">
<div class="panel">
<h2>Per-token metrics</h2>
<div class="note">The <code class="mono">--csv</code> sink, joined to the trace by step.
<b>majflt</b> is major page faults — dense weights being demand-paged back in after the expert
traffic evicted them.</div>
<div class="scroll"><table id="mtab"></table></div>
</div>
</div>
<div id="view-raw" class="hide">
<div class="panel">
<h2>Raw rows</h2>
<div class="bar" style="padding:10px 15px;margin:0">
<label class="ctl">filter
<input type="search" id="rawq" placeholder="e.g. layer=12 expert=87 miss" size="26">
</label>
<div class="spacer"></div>
<span class="ctl mono" id="rawcount"></span>
</div>
<div class="scroll"><table id="rawtab"></table></div>
<div class="pager">
<button id="rprev">←</button><span id="rpage" class="mono"></span><button id="rnext">→</button>
<span class="spacer"></span>
<span>Full-precision CSVs live in the repo — this viewer rounds weights to 4 decimals.</span>
</div>
</div>
</div>
<footer>
Captured 2026-07-15 · OnePlus 15R (SM8845), 11.1 GB · engine <code class="mono">feat/route-trace</code>
on <code class="mono">main@e7d2e8c</code> · device clocks were capped (policy0 2.19 of 3.32 GHz) and
are recorded per run in the <code class="mono">.state</code> files.
</footer>
</div>
<script>
const DATA = __DATA__;
const TAGS = Object.keys(DATA);
let cur = TAGS[0], view = "matrix", phase = 1, cellmode = "experts", stepPage = 0, layerFrom = 0;
let rawPage = 0;
const STEP_WIN = 26, LAYER_WIN = 16, RAW_PAGE = 300;
// ── parse the packed cells once per model ────────────────────────
const CACHE = {};
function cells(tag){
if (CACHE[tag]) return CACHE[tag];
const m = new Map();
for (const line of DATA[tag].cells.split("\n")){
if (!line) continue;
const i1 = line.indexOf("|"), i2 = line.indexOf("|", i1+1), i3 = line.indexOf("|", i2+1);
const ph = +line.slice(0,i1), st = +line.slice(i1+1,i2), l = +line.slice(i2+1,i3);
const rec = line.slice(i3+1).split(";").map(t=>{
const p = t.split(":"); return {e:+p[0], w:+p[1]/10000, r:+p[2]};
});
m.set(ph+"|"+st+"|"+l, rec);
}
CACHE[tag] = m; return m;
}
function stepsOf(tag, ph){
const s = new Set();
for (const k of cells(tag).keys()) if (+k.split("|")[0]===ph) s.add(+k.split("|")[1]);
return [...s].sort((a,b)=>a-b);
}
// ── chrome ───────────────────────────────────────────────────────
function seg(el, items, active, cb){
el.innerHTML = "";
for (const [val,label] of items){
const b = document.createElement("button");
b.textContent = label; b.setAttribute("aria-pressed", val===active);
b.onclick = ()=>cb(val); el.appendChild(b);
}
}
function fmt(n, d=0){ return n.toLocaleString(undefined,{minimumFractionDigits:d,maximumFractionDigits:d}); }
function tiles(){
const d = DATA[cur], s = d.summary;
const res = d.res, tot = res[0]+res[1]+res[2];
const hit = tot ? 100*(res[1]+res[2])/tot : 0;
const mets = d.metrics;
const mf = mets.map(r=>+r[9]).filter(x=>!isNaN(x));
const q = Math.max(1, Math.floor(mf.length/4));
const mfQ1 = mf.slice(0,q).reduce((a,b)=>a+b,0)/q, mfQ4 = mf.slice(-q).reduce((a,b)=>a+b,0)/q;
const t = [
["tok/s", (+s["tok/s"]).toFixed(3), "trace on — context only"],
["decode cache hit", hit.toFixed(1)+"%", fmt(tot)+" routings"],
["one expert", d.per_expert_mib+" MiB", "the unit the cache trades in"],
["touched this run", fmt(d.touched_mib)+" MiB", "of a "+fmt(d.bank_mib)+" MiB bank"],
["dense", fmt(d.dense_mib)+" MiB", "mmap-resident, never streamed"],
["compute / token", (+s["compute_s/tok"]).toFixed(3)+" s", "of "+(1/+s["tok/s"]).toFixed(3)+" s total"],
["majflt / token", fmt(Math.round(mfQ1))+" → "+fmt(Math.round(mfQ4)), "first quarter → last", mfQ4 > mfQ1*2],
];
document.getElementById("tiles").innerHTML = t.map(([k,v,s2,hot])=>
`<div class="tile${hot?" hotv":""}"><div class="k">${k}</div><div class="v mono">${v}</div><div class="s">${s2}</div></div>`
).join("");
}
// ── heatmap: layers x steps, miss density ────────────────────────
function heat(){
const d = DATA[cur], c = document.getElementById("heat");
const steps = stepsOf(cur, 1), layers = d.moe;
const cw = Math.max(3, Math.min(9, Math.floor(1100/steps.length)));
const ch = Math.max(4, Math.min(14, Math.floor(360/layers.length)));
const dpr = window.devicePixelRatio||1;
c.width = steps.length*cw*dpr; c.height = layers.length*ch*dpr;
c.style.height = (layers.length*ch)+"px";
const g = c.getContext("2d"); g.scale(dpr,dpr);
const cs = getComputedStyle(document.documentElement);
const hot = cs.getPropertyValue("--hot").trim(), sunk = cs.getPropertyValue("--sunk").trim();
const M = cells(cur);
steps.forEach((st,x)=>layers.forEach((l,y)=>{
const rec = M.get("1|"+st+"|"+l); if(!rec) return;
const miss = rec.filter(r=>r.r===0).length/rec.length;
g.fillStyle = sunk; g.fillRect(x*cw,y*ch,cw,ch);
if (miss>0){ g.globalAlpha = 0.18+0.82*miss; g.fillStyle = hot;
g.fillRect(x*cw,y*ch,cw,ch); g.globalAlpha = 1; }
}));
}
// ── matrix ───────────────────────────────────────────────────────
function matrix(){
const d = DATA[cur], M = cells(cur);
const steps = stepsOf(cur, phase);
const layers = d.moe.slice(layerFrom, layerFrom+LAYER_WIN);
const maxPage = Math.max(0, Math.ceil(steps.length/STEP_WIN)-1);
stepPage = Math.min(stepPage, maxPage);
const win = steps.slice(stepPage*STEP_WIN, stepPage*STEP_WIN+STEP_WIN);
const eb = d.expert_bytes;
let h = '<table class="mx mono"><thead><tr><th class="stepcol">step</th>';
for (const l of layers) h += `<th>L${l}</th>`;
h += "</tr></thead><tbody>";
const prev = {};
// seed "previous step" from the step before the window so the first row is honest
const seedIdx = stepPage*STEP_WIN-1;
if (seedIdx>=0) for (const l of layers){
const r = M.get(phase+"|"+steps[seedIdx]+"|"+l); if(r) prev[l] = new Set(r.map(x=>x.e));
}
for (const st of win){
h += `<tr><td class="stepcol">${st}</td>`;
for (const l of layers){
const rec = M.get(phase+"|"+st+"|"+l);
if (!rec){ h += "<td></td>"; continue; }
if (cellmode==="mb"){
const mb = rec.filter(r=>r.r===0).length * (eb[l]||0)/1048576;
h += `<td class="mbcell" style="color:${mb>0?"var(--hot)":"var(--muted)"}">${mb?mb.toFixed(1):"·"}</td>`;
} else {
const p = prev[l]||new Set();
h += '<td><div class="cell">' + rec.map(r=>{
const cls = "e"+(r.r===0?" miss":r.r===2?" spec":"")+(p.has(r.e)?" rep":"");
const txt = cellmode==="weights" ? `${r.e}<span style="opacity:.6">·${Math.round(r.w*100)}</span>` : r.e;
return `<span class="${cls}" title="expert ${r.e} · weight ${(r.w*100).toFixed(1)}% · ${["miss — read from flash","hit","prefetch hit"][r.r]}">${txt}</span>`;
}).join("") + "</div></td>";
}
prev[l] = new Set(rec.map(x=>x.e));
}
h += "</tr>";
}
h += "</tbody></table>";
document.getElementById("mxwrap").innerHTML = h;
document.getElementById("steplabel").textContent =
win.length ? `steps ${win[0]}–${win[win.length-1]} of ${steps.length}` : "no rows";
document.getElementById("pagenote").textContent =
`page ${stepPage+1} / ${maxPage+1} · ${d.k} experts per cell`;
const sl = document.getElementById("stepslider");
sl.max = maxPage; sl.value = stepPage;
document.getElementById("prev").disabled = stepPage===0;
document.getElementById("next").disabled = stepPage===maxPage;
}
function layerSpans(){
const d = DATA[cur], sel = document.getElementById("layerspan");
sel.innerHTML = "";
for (let i=0;i<d.moe.length;i+=LAYER_WIN){
const a = d.moe[i], b = d.moe[Math.min(i+LAYER_WIN-1, d.moe.length-1)];
const o = document.createElement("option"); o.value = i; o.textContent = `L${a}–L${b}`;
sel.appendChild(o);
}
sel.value = Math.min(layerFrom, d.moe.length-1);
}
// ── hot experts ──────────────────────────────────────────────────
function hotTab(){
const d = DATA[cur];
let h = "<thead><tr><th>layer</th><th>unique</th><th>top-8 share</th><th class='bar'>busiest experts — id·activations</th></tr></thead><tbody>";
for (const l of d.moe){
const hot = d.hot[l], uniq = d.uniq[l];
const tot = hot.reduce((a,b)=>a+b[1],0);
const all = Object.values(d.uniq); // for scale
const top8 = hot.slice(0,8).reduce((a,b)=>a+b[1],0);
const totalAct = (DATA[cur].metrics.length) * d.k;
const share = 100*top8/ (d.k * stepsOf(cur,1).length);
const max = hot[0] ? hot[0][1] : 1;
const bars = hot.slice(0,10).map(([e,c])=>
`<span style="display:inline-flex;flex-direction:column;gap:2px;width:58px;vertical-align:bottom">
<span class="b${c/max>0.6?" hot":""}" style="width:${Math.max(6,100*c/max)}%"></span>
<span style="font-size:10px;color:var(--muted)" class="mono">${e}·${c}</span></span>`).join(" ");
h += `<tr><td class="mono">L${l}</td><td class="mono">${uniq} / ${d.n_expert}</td>
<td class="mono">${share.toFixed(1)}%</td><td class="bar">${bars}</td></tr>`;
}
document.getElementById("hottab").innerHTML = h + "</tbody>";
}
// ── metrics ──────────────────────────────────────────────────────
function metTab(){
const d = DATA[cur];
const hdr = d.metrics_hdr;
let h = "<thead><tr>" + hdr.map(x=>`<th>${x}</th>`).join("") + "</tr></thead><tbody>";
for (const r of d.metrics){
h += "<tr>" + r.map((v,i)=>{
const isMaj = hdr[i]==="majflt";
return `<td class="mono"${isMaj&&+v>1000?' style="color:var(--hot);font-weight:600"':""}>${v}</td>`;
}).join("") + "</tr>";
}
document.getElementById("mtab").innerHTML = h + "</tbody>";
}
// ── raw rows ─────────────────────────────────────────────────────
function rawRows(){
const d = DATA[cur], M = cells(cur), q = document.getElementById("rawq").value.trim().toLowerCase();
const out = [];
for (const [key, rec] of M){
const [ph, st, l] = key.split("|").map(Number);
for (let i=0;i<rec.length;i++){
const r = rec[i];
out.push([ph, st, l, i, r.e, r.w, r.r, r.r===0 ? (d.expert_bytes[l]||0) : 0]);
}
}
let rows = out;
if (q){
rows = out.filter(r=>{
const s = `phase=${r[0]?"decode":"prefill"} step=${r[1]} layer=${r[2]} slot=${r[3]} expert=${r[4]} ${["miss","hit","prefetch"][r[6]]}`;
return q.split(/\s+/).every(t=>s.includes(t));
});
}
const pages = Math.max(1, Math.ceil(rows.length/RAW_PAGE));
rawPage = Math.min(rawPage, pages-1);
const win = rows.slice(rawPage*RAW_PAGE, rawPage*RAW_PAGE+RAW_PAGE);
let h = "<thead><tr><th>phase</th><th>step</th><th>layer</th><th>slot</th><th>expert</th><th>weight</th><th>residency</th><th>bytes read</th></tr></thead><tbody>";
for (const r of win){
const rn = ["miss","hit","prefetch"][r[6]];
h += `<tr><td>${r[0]?"decode":"prefill"}</td><td class="mono">${r[1]}</td><td class="mono">${r[2]}</td>
<td class="mono">${r[3]}</td><td class="mono" style="font-weight:600">${r[4]}</td>
<td class="mono">${r[5].toFixed(4)}</td>
<td class="mono"${r[6]===0?' style="color:var(--hot)"':r[6]===2?' style="color:var(--accent)"':""}>${rn}</td>
<td class="mono">${r[7]?fmt(r[7]):"·"}</td></tr>`;
}
document.getElementById("rawtab").innerHTML = h + "</tbody>";
document.getElementById("rawcount").textContent = fmt(rows.length)+" rows";
document.getElementById("rpage").textContent = ` ${rawPage+1} / ${pages} `;
document.getElementById("rprev").disabled = rawPage===0;
document.getElementById("rnext").disabled = rawPage>=pages-1;
}
// ── render ───────────────────────────────────────────────────────
function render(){
seg(document.getElementById("models"), TAGS.map(t=>[t, DATA[t].label]), cur, t=>{
cur = t; stepPage = 0; layerFrom = 0; rawPage = 0; layerSpans(); render();
});
seg(document.getElementById("views"),
[["matrix","Matrix"],["hot","Hot experts"],["metrics","Per-token"],["raw","Raw rows"]],
view, v=>{ view = v; render(); });
for (const v of ["matrix","hot","metrics","raw"])
document.getElementById("view-"+v).classList.toggle("hide", v!==view);
tiles();
if (view==="matrix"){ heat(); matrix(); }
if (view==="hot") hotTab();
if (view==="metrics") metTab();
if (view==="raw") rawRows();
}
document.getElementById("phase").onchange = e=>{ phase = +e.target.value; stepPage = 0; matrix(); };
document.getElementById("cellmode").onchange = e=>{ cellmode = e.target.value; matrix(); };
document.getElementById("layerspan").onchange = e=>{ layerFrom = +e.target.value; matrix(); };
document.getElementById("prev").onclick = ()=>{ stepPage--; matrix(); };
document.getElementById("next").onclick = ()=>{ stepPage++; matrix(); };
document.getElementById("stepslider").oninput = e=>{ stepPage = +e.target.value; matrix(); };
document.getElementById("rawq").oninput = ()=>{ rawPage = 0; rawRows(); };
document.getElementById("rprev").onclick = ()=>{ rawPage--; rawRows(); };
document.getElementById("rnext").onclick = ()=>{ rawPage++; rawRows(); };
matchMedia("(prefers-color-scheme:dark)").addEventListener("change", ()=>{ if(view==="matrix") heat(); });
new MutationObserver(()=>{ if(view==="matrix") heat(); })
.observe(document.documentElement, {attributes:true, attributeFilter:["data-theme"]});
layerSpans();
render();
</script>
"""
html = HTML.replace("__DATA__", DATA_JSON)
with open(OUT, "w", encoding="utf-8") as f:
f.write(html)
print("wrote %s — %.1f KB" % (OUT, os.path.getsize(OUT) / 1024))