<!-- Demo: linear SSM scan vs quadratic attention operation count visualizer. -->
<p class="hint">Drag the slider to set sequence length <em>n</em>. Press <strong>Step</strong> to advance one token at a time and watch the operation counts diverge: the SSM updates its hidden state in <em>1</em> op; attention attends to all previous tokens.</p>
<div class="controls">
<label for="seqLen">Sequence length n = <span id="lenDisplay">16</span></label>
<input type="range" id="seqLen" min="4" max="128" value="16" step="4">
</div>
<div class="chart-wrap">
<canvas id="chart" width="440" height="200"></canvas>
</div>
<div class="stats-grid">
<div class="stat ssm-stat">
<div class="stat-label">SSM ops (linear)</div>
<div class="stat-value" id="ssmOps">—</div>
<div class="stat-note">O(n)</div>
</div>
<div class="stat attn-stat">
<div class="stat-label">Attention ops (quadratic)</div>
<div class="stat-value" id="attnOps">—</div>
<div class="stat-note">O(n²)</div>
</div>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="stepBtn" type="button">Step →</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<!-- Token sequence visualization strip. -->
<div id="seqViz" class="seq-viz"></div>
/* Layout and color coding: green = SSM (O(n)), red = attention (O(n²)). */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 2px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; font-size: .9rem; }
input[type=range] { flex: 1; accent-color: #1d3557; }
.chart-wrap { background: #f4f7fa; border-radius: 10px; padding: 8px; margin-bottom: .7rem; }
canvas { display: block; width: 100%; height: auto; }
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; margin-bottom: .7rem; }
.stat { border-radius: 10px; padding: .5rem .7rem; text-align: center; }
.ssm-stat { background: #e0f0e9; }
.attn-stat { background: #fde8e8; }
.stat-label { font-size: .78rem; color: #555; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.stat-value { font-size: 1.35rem; font-weight: 700; margin: .1rem 0; }
.ssm-stat .stat-value { color: #0a7d33; }
.attn-stat .stat-value { color: #c92f3c; }
.stat-note { font-size: .75rem; color: #888; font-family: ui-monospace, monospace; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Token strip styles — processed tokens turn green, current token is highlighted. */
.seq-viz { display: flex; flex-wrap: wrap; gap: 4px; margin-top: .3rem; }
.tok { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 11px ui-monospace, monospace;
background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; transition: background .2s; }
.tok.processed { background: #0a7d33; color: #fff; border-color: #0a7d33; }
.tok.current { background: #1d3557; color: #fff; border-color: #1d3557; box-shadow: 0 0 0 2px #a8c4e0; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// State-space model (SSM) vs attention cost comparison.
// SSM processes each token with a single hidden-state update (O(n) total).
const slider = document.getElementById('seqLen');
const lenDisplay = document.getElementById('lenDisplay');
const ssmOpsEl = document.getElementById('ssmOps');
const attnOpsEl = document.getElementById('attnOps');
const statusEl = document.getElementById('status');
const seqVizEl = document.getElementById('seqViz');
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
let n = 16;
let stepIndex = 0; // how many tokens have been processed so far
let animating = false;
// Step history for chart replay.
let history = [];
function computeCosts(len) {
return { ssm: len, attn: len * len };
}
function buildHistory() {
// Pre-compute cumulative ops at each position.
history = [];
for (let i = 1; i <= n; i++) {
history.push({ pos: i - 1, ssmOps: i, attnOps: i * i });
}
}
function drawChart() {
// Draw the cost curves on the canvas.
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const pad = { top: 12, right: 16, bottom: 28, left: 52 };
const cw = W - pad.left - pad.right;
const ch = H - pad.top - pad.bottom;
const maxOps = n * n;
const xScale = (i) => pad.left + (i / (n - 1 || 1)) * cw;
const yScale = (v) => pad.top + ch - (v / maxOps) * ch;
// Draw axis lines.
ctx.strokeStyle = '#bbb';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad.left, pad.top);
ctx.lineTo(pad.left, pad.top + ch);
ctx.lineTo(pad.left + cw, pad.top + ch);
ctx.stroke();
// Draw axis labels.
ctx.fillStyle = '#888';
ctx.font = '11px system-ui';
ctx.textAlign = 'right';
ctx.fillText(maxOps, pad.left - 4, pad.top + 5);
ctx.fillText('0', pad.left - 4, pad.top + ch + 5);
ctx.textAlign = 'center';
ctx.fillText('1', pad.left, pad.top + ch + 14);
ctx.fillText(n, pad.left + cw, pad.top + ch + 14);
// Green line: SSM cumulative ops = position (O(n)).
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 2.2;
ctx.beginPath();
for (let i = 0; i < n; i++) {
const x = xScale(i), y = yScale(i + 1);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Red line: attention cumulative ops = position^2 (O(n^2)).
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2.2;
ctx.beginPath();
for (let i = 0; i < n; i++) {
const x = xScale(i), y = yScale((i + 1) * (i + 1));
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Highlight the current step on both curves.
if (stepIndex > 0) {
const si = Math.min(stepIndex - 1, n - 1);
ctx.fillStyle = '#0a7d33';
ctx.beginPath();
ctx.arc(xScale(si), yScale(si + 1), 5, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#c92f3c';
ctx.beginPath();
ctx.arc(xScale(si), yScale((si + 1) * (si + 1)), 5, 0, 2 * Math.PI);
ctx.fill();
}
// Draw curve legend.
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'left';
ctx.fillStyle = '#0a7d33';
ctx.fillText('SSM O(n)', pad.left + 4, pad.top + 13);
ctx.fillStyle = '#c92f3c';
ctx.fillText('Attn O(n²)', pad.left + 4, pad.top + 26);
}
function renderSeqViz() {
// Render the token strip showing processed vs pending tokens.
seqVizEl.innerHTML = '';
for (let i = 0; i < n; i++) {
const d = document.createElement('div');
d.className = 'tok' + (i < stepIndex ? ' processed' : '') + (i === stepIndex - 1 ? ' current' : '');
d.textContent = 't' + (i + 1);
seqVizEl.appendChild(d);
}
}
function updateStats() {
const pos = Math.max(0, stepIndex);
ssmOpsEl.textContent = pos > 0 ? pos : '—';
attnOpsEl.textContent = pos > 0 ? pos * pos : '—';
}
function setStatus(msg) {
statusEl.textContent = msg;
}
function reset() {
// Reset to step 0 and redraw everything.
stepIndex = 0;
animating = false;
buildHistory();
drawChart();
renderSeqViz();
updateStats();
setStatus('');
}
function step() {
// Advance one token and update the display.
if (stepIndex >= n) {
const ratio = (n * n / n).toFixed(0);
setStatus(fmt('Done! n=%n%: SSM used %v% ops; attention used %ratio%× more.', { n: n, v: n * n, ratio: ratio }));
return;
}
stepIndex++;
drawChart();
renderSeqViz();
updateStats();
if (stepIndex < n) {
setStatus(fmt('Token %stepIndex% of %n%: SSM = %stepIndex% ops, Attention = %v% ops.', { stepIndex: stepIndex, n: n, v: stepIndex * stepIndex }));
} else {
const ratio = (n * n / n).toFixed(0);
setStatus(fmt('Done! n=%n%: SSM used %v% ops; attention used %ratio%× more.', { n: n, v: n * n, ratio: ratio }));
}
}
slider.addEventListener('input', () => {
n = parseInt(slider.value);
lenDisplay.textContent = n;
reset();
});
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('resetBtn').addEventListener('click', reset);
// Initial render.
buildHistory();
reset();
<!-- Demo: linear SSM scan vs quadratic attention operation count visualizer. -->
<p class="hint">Drag the slider to set sequence length <em>n</em>. Press <strong>Step</strong> to advance one token at a time and watch the operation counts diverge: the SSM updates its hidden state in <em>1</em> op; attention attends to all previous tokens.</p>
<div class="controls">
<label for="seqLen">Sequence length n = <span id="lenDisplay">16</span></label>
<input type="range" id="seqLen" min="4" max="128" value="16" step="4">
</div>
<div class="chart-wrap">
<canvas id="chart" width="440" height="200"></canvas>
</div>
<div class="stats-grid">
<div class="stat ssm-stat">
<div class="stat-label">SSM ops (linear)</div>
<div class="stat-value" id="ssmOps">—</div>
<div class="stat-note">O(n)</div>
</div>
<div class="stat attn-stat">
<div class="stat-label">Attention ops (quadratic)</div>
<div class="stat-value" id="attnOps">—</div>
<div class="stat-note">O(n²)</div>
</div>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="stepBtn" type="button">Step →</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<!-- Token sequence visualization strip. -->
<div id="seqViz" class="seq-viz"></div>
/* Layout and color coding: green = SSM (O(n)), red = attention (O(n²)). */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 2px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; font-size: .9rem; }
input[type=range] { flex: 1; accent-color: #1d3557; }
.chart-wrap { background: #f4f7fa; border-radius: 10px; padding: 8px; margin-bottom: .7rem; }
canvas { display: block; width: 100%; height: auto; }
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .5rem; margin-bottom: .7rem; }
.stat { border-radius: 10px; padding: .5rem .7rem; text-align: center; }
.ssm-stat { background: #e0f0e9; }
.attn-stat { background: #fde8e8; }
.stat-label { font-size: .78rem; color: #555; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.stat-value { font-size: 1.35rem; font-weight: 700; margin: .1rem 0; }
.ssm-stat .stat-value { color: #0a7d33; }
.attn-stat .stat-value { color: #c92f3c; }
.stat-note { font-size: .75rem; color: #888; font-family: ui-monospace, monospace; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Token strip styles — processed tokens turn green, current token is highlighted. */
.seq-viz { display: flex; flex-wrap: wrap; gap: 4px; margin-top: .3rem; }
.tok { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center;
justify-content: center; font: 700 11px ui-monospace, monospace;
background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; transition: background .2s; }
.tok.processed { background: #0a7d33; color: #fff; border-color: #0a7d33; }
.tok.current { background: #1d3557; color: #fff; border-color: #1d3557; box-shadow: 0 0 0 2px #a8c4e0; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// State-space model (SSM) vs attention cost comparison.
// SSM processes each token with a single hidden-state update (O(n) total).
const slider = document.getElementById('seqLen');
const lenDisplay = document.getElementById('lenDisplay');
const ssmOpsEl = document.getElementById('ssmOps');
const attnOpsEl = document.getElementById('attnOps');
const statusEl = document.getElementById('status');
const seqVizEl = document.getElementById('seqViz');
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
let n = 16;
let stepIndex = 0; // how many tokens have been processed so far
let animating = false;
// Step history for chart replay.
let history = [];
function computeCosts(len) {
return { ssm: len, attn: len * len };
}
function buildHistory() {
// Pre-compute cumulative ops at each position.
history = [];
for (let i = 1; i <= n; i++) {
history.push({ pos: i - 1, ssmOps: i, attnOps: i * i });
}
}
function drawChart() {
// Draw the cost curves on the canvas.
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const pad = { top: 12, right: 16, bottom: 28, left: 52 };
const cw = W - pad.left - pad.right;
const ch = H - pad.top - pad.bottom;
const maxOps = n * n;
const xScale = (i) => pad.left + (i / (n - 1 || 1)) * cw;
const yScale = (v) => pad.top + ch - (v / maxOps) * ch;
// Draw axis lines.
ctx.strokeStyle = '#bbb';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pad.left, pad.top);
ctx.lineTo(pad.left, pad.top + ch);
ctx.lineTo(pad.left + cw, pad.top + ch);
ctx.stroke();
// Draw axis labels.
ctx.fillStyle = '#888';
ctx.font = '11px system-ui';
ctx.textAlign = 'right';
ctx.fillText(maxOps, pad.left - 4, pad.top + 5);
ctx.fillText('0', pad.left - 4, pad.top + ch + 5);
ctx.textAlign = 'center';
ctx.fillText('1', pad.left, pad.top + ch + 14);
ctx.fillText(n, pad.left + cw, pad.top + ch + 14);
// Green line: SSM cumulative ops = position (O(n)).
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 2.2;
ctx.beginPath();
for (let i = 0; i < n; i++) {
const x = xScale(i), y = yScale(i + 1);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Red line: attention cumulative ops = position^2 (O(n^2)).
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2.2;
ctx.beginPath();
for (let i = 0; i < n; i++) {
const x = xScale(i), y = yScale((i + 1) * (i + 1));
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Highlight the current step on both curves.
if (stepIndex > 0) {
const si = Math.min(stepIndex - 1, n - 1);
ctx.fillStyle = '#0a7d33';
ctx.beginPath();
ctx.arc(xScale(si), yScale(si + 1), 5, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#c92f3c';
ctx.beginPath();
ctx.arc(xScale(si), yScale((si + 1) * (si + 1)), 5, 0, 2 * Math.PI);
ctx.fill();
}
// Draw curve legend.
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'left';
ctx.fillStyle = '#0a7d33';
ctx.fillText('SSM O(n)', pad.left + 4, pad.top + 13);
ctx.fillStyle = '#c92f3c';
ctx.fillText('Attn O(n²)', pad.left + 4, pad.top + 26);
}
function renderSeqViz() {
// Render the token strip showing processed vs pending tokens.
seqVizEl.innerHTML = '';
for (let i = 0; i < n; i++) {
const d = document.createElement('div');
d.className = 'tok' + (i < stepIndex ? ' processed' : '') + (i === stepIndex - 1 ? ' current' : '');
d.textContent = 't' + (i + 1);
seqVizEl.appendChild(d);
}
}
function updateStats() {
const pos = Math.max(0, stepIndex);
ssmOpsEl.textContent = pos > 0 ? pos : '—';
attnOpsEl.textContent = pos > 0 ? pos * pos : '—';
}
function setStatus(msg) {
statusEl.textContent = msg;
}
function reset() {
// Reset to step 0 and redraw everything.
stepIndex = 0;
animating = false;
buildHistory();
drawChart();
renderSeqViz();
updateStats();
setStatus('');
}
function step() {
// Advance one token and update the display.
if (stepIndex >= n) {
const ratio = (n * n / n).toFixed(0);
setStatus(fmt('Done! n=%n%: SSM used %v% ops; attention used %ratio%× more.', { n: n, v: n * n, ratio: ratio }));
return;
}
stepIndex++;
drawChart();
renderSeqViz();
updateStats();
if (stepIndex < n) {
setStatus(fmt('Token %stepIndex% of %n%: SSM = %stepIndex% ops, Attention = %v% ops.', { stepIndex: stepIndex, n: n, v: stepIndex * stepIndex }));
} else {
const ratio = (n * n / n).toFixed(0);
setStatus(fmt('Done! n=%n%: SSM used %v% ops; attention used %ratio%× more.', { n: n, v: n * n, ratio: ratio }));
}
}
slider.addEventListener('input', () => {
n = parseInt(slider.value);
lenDisplay.textContent = n;
reset();
});
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('resetBtn').addEventListener('click', reset);
// Initial render.
buildHistory();
reset();
Notice the gap. For short sequences both are manageable. As the slider climbs, attention's cost rockets while the linear scan stays flat by comparison. This is exactly why SSMs matter at the scale of genomics, audio, and very long documents.
Comments
Loading comments...