abhimittal's picture
Deploy static build
b849d7f verified
Raw History Blame Contribute Delete
8.52 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quantization Error Propagation Viewer</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔬</text></svg>">
<script src="plotly.min.js"></script>
<style>
:root { --fg: #1a1a2e; --muted: #555; --card: #fff; --bg: #f4f4fa; }
body { font-family: system-ui, sans-serif; margin: 0; background: var(--bg); color: var(--fg); }
header { background: linear-gradient(90deg, #3f3f8f, #7b4fb5); color: #fff; padding: 1.2rem 2rem; }
header h1 { margin: 0 0 .3rem; font-size: 1.5rem; }
header p { margin: 0; opacity: .9; max-width: 60rem; }
main { max-width: 1200px; margin: 0 auto; padding: 1rem 1.5rem 3rem; }
.controls { display: flex; gap: 1.5rem; flex-wrap: wrap; margin: 1rem 0; align-items: center; }
.controls label { font-weight: 600; margin-right: .4rem; }
select { padding: .35rem .5rem; border-radius: 6px; border: 1px solid #bbb; font-size: 1rem; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
.card { background: var(--card); border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.08); padding: .5rem; }
table { border-collapse: collapse; margin: 1rem 0; width: 100%; background: var(--card); border-radius: 10px; overflow: hidden; }
th, td { padding: .5rem .8rem; text-align: left; border-bottom: 1px solid #eee; }
th { background: #ecebf7; }
.pedagogy { background: var(--card); border-radius: 10px; padding: 1rem 1.5rem; margin-top: 1.5rem; line-height: 1.55; }
.pedagogy h3 { margin-top: 1.2rem; }
.note { color: var(--muted); font-size: .9rem; }
code { background: #eee; padding: .1rem .3rem; border-radius: 4px; }
</style>
</head>
<body>
<header>
<h1>🔬 Quantization Error Propagation Viewer</h1>
<p>How INT8/INT4 error accumulates layer by layer through a real model on real
activations — and the outlier structure that motivates SmoothQuant &amp; AWQ.
Precomputed static demo; the repo includes a fully interactive Gradio app
(custom prompts, any Linear inspectable) you can run locally.</p>
</header>
<main>
<div class="controls">
<span><label for="model">Model</label><select id="model"></select></span>
<span><label for="alpha">α (migration strength)</label><select id="alpha"></select></span>
<span><label for="module">Calibration profile of</label><select id="module"></select></span>
</div>
<p class="note" id="prompt"></p>
<div class="grid">
<div class="card"><div id="err"></div></div>
<div class="card"><div id="heat"></div></div>
<div class="card"><div id="cos"></div></div>
<div class="card"><div id="kurt"></div></div>
</div>
<table id="summary"></table>
<div class="card"><div id="profile"></div></div>
<div class="pedagogy" id="pedagogy">
<h2>📚 What am I looking at?</h2>
<h3>Error propagation</h3>
<p>Each quantized forward pass is compared with the FP32 reference at every
transformer layer. Errors <em>compound</em>: a perturbation in layer 1 is
amplified by every subsequent matmul, LayerNorm and attention mixing step —
so the curves trend upward, and INT4 (16 levels) sits far above INT8 (256).</p>
<h3>Where outliers live</h3>
<p>The heatmap shows per-channel max |activation| after each layer. A few
<em>fixed channels</em> carry values 10–100× larger than the rest (bright
vertical stripes). They emerge in early-middle layers and persist — they
ride the residual stream. With per-tensor activation quantization, one
outlier channel sets the scale for <em>all</em> channels, crushing the rest
into a few integer levels. That's why <b>W8A8 naive</b> is so much worse
than <b>INT8 weight-only</b>.</p>
<h3>Why SmoothQuant targets what it targets</h3>
<p>Outliers live in <em>activations</em>, in <em>specific channels</em>;
weights are flat and easy. SmoothQuant migrates difficulty: divide
activations by <code>s_j = |X_j|max^α / |W_j|max^(1−α)</code> and multiply
the matching weight columns by <code>s_j</code> — a mathematical no-op that
makes both tensors "medium hard" instead of one impossible + one trivial.
Compare <b>W8A8 naive</b> vs <b>W8A8 + SmoothQuant</b>: same bits, very
different error. Try α = 0.3 / 0.5 / 0.7 above.</p>
<h3>Why AWQ targets what it targets</h3>
<p>For weight-only INT4, the weight columns that multiply <em>large
activations</em> (the same outlier channels!) contribute most of the output
error. AWQ scales those salient columns up before rounding — shrinking their
relative rounding error — and folds the inverse scale into the activation
path. Compare <b>INT4 g128</b> vs <b>INT4 g128 + AWQ-style</b> at α = 0.3.
Push α too high and the boost itself inflates the group scales and error
grows again — precisely why real AWQ <em>searches</em> for the best
per-layer scale instead of fixing α. (INT4 here is group-wise, g = 128, as
deployed in practice.)</p>
<h3>Kurtosis</h3>
<p>Excess kurtosis of hidden states per layer (0 = Gaussian). Values in the
thousands are the statistical fingerprint of extreme outlier channels.</p>
<p class="note">All quantization is simulated ("fake quant"): round-to-nearest
onto a symmetric integer grid in float32 — exactly the error a real integer
kernel introduces. Source:
<a href="https://github.com/aabhimittal/quantization-error-propagation-viewer">GitHub</a>.</p>
</div>
</main>
<script>
const LAYOUT = { template: "plotly_white", margin: {l:55,r:15,t:45,b:45}, height: 380 };
let DATA;
function runFor(model, alpha) {
return DATA.runs.find(r => r.model === model && r.alpha === +alpha);
}
function fillSelect(el, values, keep) {
const prev = keep ? el.value : null;
el.innerHTML = values.map(v => `<option>${v}</option>`).join("");
if (prev && values.includes(prev)) el.value = prev;
}
function render() {
const run = runFor(model.value, alpha.value);
fillSelect(module, Object.keys(run.profiles), true);
const layers = Array.from({length: run.n_layers}, (_, i) => i + 1);
const lines = key => run.schemes.map(s => ({x: layers, y: s[key], mode: "lines+markers", name: s.label}));
Plotly.react("err", lines("rel_err"), {...LAYOUT,
title: "Relative error of hidden states, layer by layer",
xaxis: {title: "Layer"}, yaxis: {title: "‖h_q − h_ref‖ / ‖h_ref‖", type: "log"}});
Plotly.react("cos", lines("cos_sim"), {...LAYOUT,
title: "Cosine similarity to reference",
xaxis: {title: "Layer"}, yaxis: {title: "cos(h_q, h_ref)"}});
Plotly.react("heat", [{z: run.hidden_amax.map(row => row.map(v => Math.log10(Math.max(v, 1e-6)))),
type: "heatmap", colorscale: "Inferno", colorbar: {title: "log10 |h|max"}}],
{...LAYOUT, title: "Where outliers live: per-channel |activation|max",
xaxis: {title: "Hidden channel"}, yaxis: {title: "Layer"}});
Plotly.react("kurt", [{x: layers, y: run.kurtosis, mode: "lines+markers"}],
{...LAYOUT, title: "Excess kurtosis of hidden states (0 = Gaussian)",
xaxis: {title: "Layer"}, yaxis: {title: "Excess kurtosis"}});
renderProfile(run);
summary.innerHTML =
"<tr><th>Scheme</th><th>Final-layer rel. error</th><th>Top-1 agreement</th><th>KL(ref‖quant)</th></tr>" +
run.schemes.map(s =>
`<tr><td>${s.label}</td><td>${s.rel_err.at(-1).toFixed(4)}</td>` +
`<td>${(s.top1 * 100).toFixed(1)}%</td><td>${s.kl.toFixed(4)}</td></tr>`).join("");
}
function renderProfile(run) {
run = run || runFor(model.value, alpha.value);
const y = run.profiles[module.value];
Plotly.react("profile", [{y, mode: "lines", line: {width: 1}}],
{...LAYOUT, height: 340,
title: `Input-channel |x|max into ${module.value} — the profile SmoothQuant/AWQ calibrate on`,
xaxis: {title: "Input channel"}, yaxis: {title: "|x|max", type: "log"}});
}
fetch("data.json").then(r => r.json()).then(d => {
DATA = d;
document.getElementById("prompt").textContent = `Prompt: “${d.prompt}”`;
fillSelect(model, [...new Set(d.runs.map(r => r.model))]);
fillSelect(alpha, [...new Set(d.runs.map(r => r.alpha))].sort());
alpha.value = "0.5";
model.onchange = render; alpha.onchange = render;
module.onchange = () => renderProfile();
render();
});
</script>
</body>
</html>