degeneration-probe / layer_viz_interactive.html
mo378's picture
Helvetica font; tooltip opacity; labeling-formula spacing
8486d7b verified
Raw
History Blame Contribute Delete
12.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Apertus-8B: Layer Structure (Interactive)</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Lora:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #fff;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 32px;
}
#stackGroup { transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1); }
#detailGroup { transition: opacity 0.35s ease; }
#hintEl { transition: opacity 0.3s ease; }
</style>
</head>
<body>
<svg id="viz" width="1100" height="560" viewBox="0 -20 980 500"
style="font-family:Helvetica, Arial, sans-serif; display:block;"></svg>
<script>
const svg = document.getElementById('viz');
const ns = 'http://www.w3.org/2000/svg';
function el(tag, attrs, text) {
const e = document.createElementNS(ns, tag);
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
if (text !== undefined) e.textContent = text;
return e;
}
function txt(x, y, str, attrs = {}) {
return el('text', { x, y, 'text-anchor': 'middle',
'font-family': 'Inter,sans-serif', 'font-size': 13, fill: '#444', ...attrs }, str);
}
function arrowRight(x1, y1, x2, y2, color = '#bbb', dash = '') {
const g = el('g', {});
g.appendChild(el('line', { x1, y1, x2, y2, stroke: color,
'stroke-width': 2, 'stroke-linecap': 'round',
...(dash ? { 'stroke-dasharray': dash } : {}) }));
const a = Math.atan2(y2 - y1, x2 - x1), L = 9, s = 0.4;
g.appendChild(el('polygon', {
points: `${x2},${y2} ${x2 - L*Math.cos(a-s)},${y2 - L*Math.sin(a-s)} ${x2 - L*Math.cos(a+s)},${y2 - L*Math.sin(a+s)}`,
fill: color
}));
return g;
}
// ── Layout ────────────────────────────────────────────────────
const CY = 240;
const N_LAYERS = 32;
const REKT_W = 72;
const REKT_H = 9;
const REKT_GAP = 3;
const totalH = N_LAYERS * REKT_H + (N_LAYERS - 1) * REKT_GAP;
const stackTopY = CY - totalH / 2;
const stackX = 88; // original left-side X (used when detail is shown)
const INIT_OFFSET = 490 - stackX; // translateX to center stack initially (= 402)
const detailX = 400;
const detailW = 210;
const detailH = 330;
const detailY = CY - detailH / 2;
const vecX = 630;
// ── Groups (detail behind stack) ──────────────────────────────
const detailGroup = el('g', { id: 'detailGroup' });
detailGroup.style.opacity = '0';
const stackGroup = el('g', { id: 'stackGroup' });
stackGroup.style.transform = `translateX(${INIT_OFFSET}px)`;
// ── Top hint label (static) ───────────────────────────────
svg.appendChild(el('text', {
x: 490, y: -8,
'text-anchor': 'middle',
'font-family': 'Inter,sans-serif',
'font-size': 11,
fill: '#bbb',
'font-style': 'normal',
}, 'Select a layer to zoom into the Apertus-8B architecture'));
svg.appendChild(detailGroup);
svg.appendChild(stackGroup);
// ── Stack: header ─────────────────────────────────────────────
stackGroup.appendChild(txt(stackX, stackTopY - 28, 'Apertus-8B-Instruct',
{ 'font-size': 13, 'font-weight': 700, fill: '#2c4a7c' }));
stackGroup.appendChild(txt(stackX, stackTopY - 14, '32 transformer layers',
{ 'font-size': 10, fill: '#aaa' }));
const hintEl = txt(stackX, stackTopY + totalH + 18, 'hover to explore',
{ 'font-size': 10, fill: '#ccc', 'font-style': 'normal', id: 'hintEl' });
stackGroup.appendChild(hintEl);
// ── Stack: anchor labels (0 and 31) ───────────────────────────
stackGroup.appendChild(txt(stackX + REKT_W/2 + 18, stackTopY + 9,
'Layer 0', { 'font-size': 9, fill: '#ccc', 'text-anchor': 'start' }));
stackGroup.appendChild(txt(stackX + REKT_W/2 + 18,
stackTopY + (N_LAYERS - 1) * (REKT_H + REKT_GAP) + 9,
'Layer 31', { 'font-size': 9, fill: '#ccc', 'text-anchor': 'start' }));
// ── Stack: layer rects ────────────────────────────────────────
const layerRects = [];
for (let i = 0; i < N_LAYERS; i++) {
const ry = stackTopY + i * (REKT_H + REKT_GAP);
const r = el('rect', {
x: stackX - REKT_W / 2, y: ry,
width: REKT_W, height: REKT_H,
rx: 0,
fill: '#e5e7eb', stroke: '#d1d5db', 'stroke-width': 0.8,
'data-layer': i
});
r.style.cursor = 'pointer';
stackGroup.appendChild(r);
layerRects.push(r);
}
// Dynamic hover label (inside stackGroup: moves with stack)
const hoverLabel = txt(stackX + REKT_W/2 + 18, stackTopY,
'', { 'font-size': 9, fill: '#2c4a7c', 'font-weight': 700,
'text-anchor': 'start', opacity: 0 });
stackGroup.appendChild(hoverLabel);
// ── Detail: zoom fan (dynamic) ────────────────────────────────
const zoomGroup = el('g', {});
detailGroup.appendChild(zoomGroup);
function updateZoomLines(i) {
while (zoomGroup.firstChild) zoomGroup.removeChild(zoomGroup.firstChild);
const hlY = stackTopY + i * (REKT_H + REKT_GAP);
const hlRight = stackX + REKT_W / 2;
const zDstX = detailX - detailW / 2;
zoomGroup.appendChild(el('polygon', {
points: `${hlRight},${hlY} ${zDstX},${detailY} ${zDstX},${detailY+detailH} ${hlRight},${hlY+REKT_H}`,
fill: '#f5f3ff', stroke: 'none'
}));
zoomGroup.appendChild(el('line', {
x1: hlRight, y1: hlY, x2: zDstX, y2: detailY,
stroke: '#aab9d2', 'stroke-width': 1, 'stroke-dasharray': '5 4'
}));
zoomGroup.appendChild(el('line', {
x1: hlRight, y1: hlY + REKT_H, x2: zDstX, y2: detailY + detailH,
stroke: '#aab9d2', 'stroke-width': 1, 'stroke-dasharray': '5 4'
}));
}
// ── Detail: box ───────────────────────────────────────────────
// shadow
detailGroup.appendChild(el('rect', {
x: detailX - detailW/2 + 4, y: detailY + 4,
width: detailW, height: detailH, rx: 0, fill: '#eef1f6', opacity: 0.5
}));
// card
detailGroup.appendChild(el('rect', {
x: detailX - detailW/2, y: detailY,
width: detailW, height: detailH, rx: 0,
fill: '#fff', stroke: '#aab9d2', 'stroke-width': 2
}));
// top accent bar
detailGroup.appendChild(el('rect', {
x: detailX - detailW/2, y: detailY,
width: detailW, height: 5, rx: 0, fill: '#2c4a7c'
}));
const layerNumLabel = txt(detailX, detailY + 22, 'Layer 0',
{ 'font-size': 13, 'font-weight': 700, fill: '#1e1b4b' });
detailGroup.appendChild(layerNumLabel);
function subBlock(cy, bg, border, label, sublabel) {
const bW = detailW - 36, bH = sublabel ? 56 : 46;
const bX = detailX - bW / 2;
const bY = cy - bH / 2;
detailGroup.appendChild(el('rect', { x: bX, y: bY, width: bW, height: bH,
rx: 0, fill: bg, stroke: border, 'stroke-width': 1.3 }));
detailGroup.appendChild(txt(detailX, bY + (sublabel ? bH/2 - 6 : bH/2 + 5),
label, { 'font-size': 11, 'font-weight': 600, fill: '#333' }));
if (sublabel) detailGroup.appendChild(txt(detailX, bY + bH/2 + 9,
sublabel, { 'font-size': 9.5, fill: '#888' }));
return { top: bY, bot: bY + bH };
}
const attn = subBlock(detailY + 88, '#eef1f6', '#6c83a8', 'Multi-Head Self-Attention', '32Q / 8KV heads (GQA) Β· dβ‚• = 128');
const ffn = subBlock(detailY + 192, '#fef3c7', '#fbbf24', 'Feed-Forward Network', '4096 β†’ 21504 β†’ 4096');
const norm = subBlock(detailY + 282, '#f0fdf4', '#86efac', 'RMSNorm + Residual', null);
detailGroup.appendChild(arrowRight(detailX, attn.bot + 1, detailX, ffn.top - 1, '#bbb'));
detailGroup.appendChild(arrowRight(detailX, ffn.bot + 1, detailX, norm.top - 1, '#bbb'));
const skipX = detailX - detailW / 2 + 12;
detailGroup.appendChild(el('path', {
d: `M${skipX},${attn.top + 4} L${skipX},${norm.bot - 4}`,
stroke: '#aab9d2', 'stroke-width': 1.5, 'stroke-dasharray': '4 3', fill: 'none'
}));
detailGroup.appendChild(txt(skipX, (attn.top + norm.bot) / 2 + 4, '+',
{ 'font-size': 11, fill: '#6c83a8', 'font-weight': 700 }));
// input arrow + label
detailGroup.appendChild(arrowRight(detailX, detailY - 28, detailX, detailY - 2, '#bbb'));
detailGroup.appendChild(txt(detailX, detailY - 36, 'hβ‚œ ∈ ℝ⁴⁰⁹⁢',
{ 'font-size': 13, fill: '#888', 'font-style': 'normal' }));
// output arrow to vec
detailGroup.appendChild(arrowRight(detailX + detailW/2 + 2, CY, vecX - 30, CY, '#555'));
// ── Detail: activation vector ─────────────────────────────────
const vecW = 32;
const vecH = 260;
const vecTopY = CY - vecH / 2;
detailGroup.appendChild(el('rect', {
x: vecX - vecW/2, y: vecTopY, width: vecW, height: vecH,
rx: 0, fill: '#e0e7ff', stroke: '#a5b4fc', 'stroke-width': 1.5
}));
detailGroup.appendChild(txt(vecX, vecTopY - 12, '4 096 activations',
{ 'font-size': 13, fill: '#2c4a7c', 'font-weight': 700 }));
detailGroup.appendChild(el('line', {
x1: vecX+vecW/2+4, y1: vecTopY, x2: vecX+vecW/2+4, y2: vecTopY+vecH,
stroke: '#bbb', 'stroke-width': 1 }));
detailGroup.appendChild(el('line', {
x1: vecX+vecW/2+4, y1: vecTopY, x2: vecX+vecW/2+9, y2: vecTopY,
stroke: '#bbb', 'stroke-width': 1 }));
detailGroup.appendChild(el('line', {
x1: vecX+vecW/2+4, y1: vecTopY+vecH, x2: vecX+vecW/2+9, y2: vecTopY+vecH,
stroke: '#bbb', 'stroke-width': 1 }));
// ── Detail: PCA + fan-out ─────────────────────────────────────
const pcaX = 780, pcaW = 64, pcaH = 40;
detailGroup.appendChild(arrowRight(vecX + vecW/2 + 2, CY, pcaX - pcaW/2 - 2, CY, '#555'));
detailGroup.appendChild(el('rect', {
x: pcaX - pcaW/2, y: CY - pcaH/2, width: pcaW, height: pcaH,
rx: 0, fill: '#f8fafc', stroke: '#94a3b8', 'stroke-width': 1.8
}));
detailGroup.appendChild(txt(pcaX, CY + 5, 'PCA',
{ 'font-size': 13, 'font-weight': 700, fill: '#334155' }));
const outX = 920;
[CY - 90, CY, CY + 90].forEach((oy, i) => {
detailGroup.appendChild(arrowRight(pcaX + pcaW/2 + 2, CY, outX - 22, oy, '#94a3b8'));
detailGroup.appendChild(el('circle', {
cx: outX, cy: oy, r: 20,
fill: '#f1f5f9', stroke: '#94a3b8', 'stroke-width': 1.5
}));
detailGroup.appendChild(txt(outX, oy - 3, ['1D','2D','3D'][i],
{ 'font-size': 11, 'font-weight': 700, fill: '#334155' }));
detailGroup.appendChild(txt(outX, oy + 11, ['ℝ¹','ℝ²','ℝ³'][i],
{ 'font-size': 13, fill: '#94a3b8', 'font-style': 'normal' }));
});
// ── Interaction ───────────────────────────────────────────────
let current = null;
function show(i) {
if (current === i) return;
current = i;
layerRects.forEach((r, idx) => {
r.setAttribute('fill', idx === i ? '#2c4a7c' : '#e5e7eb');
r.setAttribute('stroke', idx === i ? '#4338ca' : '#d1d5db');
r.setAttribute('stroke-width', idx === i ? 1.5 : 0.8);
});
const ry = stackTopY + i * (REKT_H + REKT_GAP);
hoverLabel.setAttribute('y', ry + 9);
hoverLabel.textContent = `Layer ${i}`;
hoverLabel.setAttribute('opacity', 1);
updateZoomLines(i);
layerNumLabel.textContent = `Layer ${i}`;
stackGroup.style.transform = 'translateX(0)';
detailGroup.style.opacity = '1';
hintEl.style.opacity = '0';
}
function hide() {
current = null;
layerRects.forEach(r => {
r.setAttribute('fill', '#e5e7eb');
r.setAttribute('stroke', '#d1d5db');
r.setAttribute('stroke-width', 0.8);
});
hoverLabel.setAttribute('opacity', 0);
stackGroup.style.transform = `translateX(${INIT_OFFSET}px)`;
detailGroup.style.opacity = '0';
hintEl.style.opacity = '1';
}
// Show on rect hover, update active layer when moving between rects
layerRects.forEach((r, i) => r.addEventListener('mouseenter', () => show(i)));
// Only hide when mouse leaves the entire SVG
svg.addEventListener('mouseleave', hide);
</script>
</body>
</html>