File size: 8,520 Bytes
94c7b95 b849d7f 94c7b95 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | <!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 & 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>
|