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 &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>