lm-playground-api / web /chapters /quantization.html
deploy
Deploy LM Playground API
f0a527a
Raw
History Blame Contribute Delete
34.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>23. Quantization β€” LM Playground</title>
<link rel="stylesheet" href="/platform/platform.css"/>
<script type="module" src="/platform/platform.js"></script>
<style>
:root{
--bg:#0f1117; --panel:#181b24; --panel-2:#1f2330; --border:#2a2f3d;
--text:#e6e8ee; --muted:#9aa3b2; --accent:#7c8cff; --accent-2:#5be0c0;
--good:#5be08a; --warn:#ffc06b; --bad:#ff9090;
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0 20px 80px;background:radial-gradient(1200px 600px at 50% -10%,#1a1f2e 0%,var(--bg) 55%);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.5;-webkit-font-smoothing:antialiased}
header{max-width:980px;margin:0 auto;padding:34px 0 0}
.header-inner{display:flex;justify-content:space-between;align-items:flex-start;padding-bottom:12px}
h1{font-size:30px;margin:0 0 6px;letter-spacing:-.5px}
.sub{color:var(--muted);max-width:780px;margin:0;font-size:14.5px;line-height:1.6}
.sub b{color:var(--text);font-weight:600}
.page-tabs{display:flex;border-bottom:1px solid var(--border)}
.page-tab{background:none;border:none;color:var(--muted);padding:11px 22px 10px;font-size:14px;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:.12s}
.page-tab:hover{color:var(--text)}.page-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.tab-panel{display:none}.tab-panel.active{display:block}
.guide{max-width:980px;margin:0 auto;padding:30px 0 80px;display:grid;grid-template-columns:1fr 220px;gap:0 48px}
.guide-toc{grid-column:2;grid-row:1/20;position:sticky;top:64px;align-self:start;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:4px}
.toc-label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);font-weight:700;margin-bottom:6px}
.guide-toc a{color:var(--muted);text-decoration:none;font-size:12.5px;padding:5px 8px;border-radius:6px;transition:.1s}
.guide-toc a:hover{color:var(--text);background:var(--panel-2)}
.chapter{grid-column:1;padding:0 0 48px;border-bottom:1px solid var(--border);margin-bottom:48px}
.chapter:last-of-type{border-bottom:none}
.chapter h2{font-size:22px;margin:0 0 18px;display:flex;align-items:center;gap:12px;letter-spacing:-.3px}
.ch-num{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:99px;background:var(--accent-2);color:#0b0d14;font-size:14px;font-weight:800;flex-shrink:0}
.chapter h3{font-size:15px;margin:24px 0 8px;color:var(--accent-2);font-weight:700}
.chapter p{margin:0 0 14px;line-height:1.7;color:#ced3de;font-size:15px}
.chapter strong{color:var(--text)}.chapter em{font-style:italic}
code{background:var(--panel-2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-family:var(--mono);font-size:12.5px;color:var(--accent-2)}
.callout{border-radius:10px;padding:14px 16px;margin:20px 0;font-size:14px;line-height:1.65;color:#ced3de}
.callout strong{display:block;margin-bottom:5px;font-size:12px;text-transform:uppercase;letter-spacing:.6px}
.callout.insight{background:#7c8cff18;border-left:3px solid var(--accent)}.callout.insight strong{color:var(--accent)}
.callout.warn{background:#ffc06b18;border-left:3px solid var(--warn)}.callout.warn strong{color:var(--warn)}
.formula-box{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:18px 16px;margin:16px 0;text-align:center}
.formula{font-family:var(--mono);font-size:14px;color:var(--text);line-height:1.7}
.formula .hl{color:var(--accent);font-weight:700}
.formula-note{font-size:12.5px;color:var(--muted);margin-top:10px;line-height:1.5}
.try-it{display:inline-block;margin-top:10px;padding:10px 18px;border-radius:9px;background:var(--panel-2);border:1px solid var(--border);color:var(--accent);font-size:13.5px;font-weight:600;cursor:pointer;transition:.14s;font-family:inherit}
.try-it:hover{background:#7c8cff22;border-color:var(--accent)}
.try-it.large{font-size:15px;padding:14px 28px;background:var(--accent-2);color:#0b0d14;border-color:var(--accent-2)}
.try-it.large:hover{filter:brightness(1.08)}
.guide-end{text-align:center;padding:18px 0 0}.guide-end p{color:var(--muted);margin-bottom:14px}
.panel-guide-item{display:flex;gap:12px;align-items:baseline;padding:9px 0;border-bottom:1px dashed var(--border)}
.panel-guide-item:last-child{border-bottom:none}
.pgi-label{font-size:13px;font-weight:800;color:var(--accent-2);min-width:20px;font-family:var(--mono)}
.panel-guide-item p{margin:0;font-size:13.5px;color:#ced3de;line-height:1.55}
.wrap{max-width:980px;margin:20px auto 0}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px}
.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px}
.panel-label{font-size:13px;font-weight:700;color:var(--text)}.panel-note{color:var(--muted);font-size:12px}
.ctrl-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin:4px 0 6px}
.ctrl{display:flex;flex-direction:column;gap:6px}
.ctrl .lab{font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);font-weight:600}
.ctrl .lab b{color:var(--text);font-family:var(--mono)}
.seg{display:inline-flex;background:var(--panel-2);border:1px solid var(--border);border-radius:10px;padding:3px;gap:3px;flex-wrap:wrap}
.seg button{border:0;background:transparent;color:var(--muted);padding:7px 12px;border-radius:7px;cursor:pointer;font-size:12.5px;font-weight:600;font-family:inherit}
.seg button.on{background:var(--accent);color:#0b0d14}
.toggle{display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none;font-size:13px}
.switch{width:40px;height:22px;border-radius:12px;background:var(--panel-2);border:1px solid var(--border);position:relative;transition:.15s}
.switch::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--muted);transition:.15s}
.switch.on{background:var(--accent-2);border-color:var(--accent-2)}.switch.on::after{left:20px;background:#0b0d14}
svg{display:block;width:100%;height:auto;overflow:visible}
.bigstats{display:flex;flex-wrap:wrap;gap:26px;margin:10px 0}
.bs{display:flex;flex-direction:column;gap:2px}
.bs .v{font-family:var(--mono);font-size:20px;font-weight:700}
.bs .l{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
.card{background:var(--panel-2);border:1px solid var(--border);border-radius:11px;padding:14px}
.card h4{margin:0 0 4px;font-size:13px}.card .cap{color:var(--muted);font-size:11.5px;margin:0 0 8px;line-height:1.5}
.verdict{font-size:12.5px;margin-top:10px;padding:9px 12px;border-radius:7px;font-weight:600;line-height:1.5}
.verdict.good{background:#5be08a18;color:var(--good)}.verdict.warn{background:#ffc06b18;color:var(--warn)}.verdict.bad{background:#ff909018;color:var(--bad)}
.hint{color:var(--muted);font-size:12.5px;margin-top:10px;line-height:1.5}
.legend{display:flex;flex-wrap:wrap;gap:14px;font-size:11.5px;color:var(--muted);margin-top:6px}
.legend i{display:inline-block;width:18px;height:3px;border-radius:2px;vertical-align:middle;margin-right:5px}
.fig{margin:22px 0;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 16px 12px}
.fig svg{display:block;width:100%;height:auto;overflow:visible}
.fig figcaption{margin-top:10px;font-size:12px;color:var(--muted);text-align:center;line-height:1.55}
footer{max-width:980px;margin:30px auto 0;color:var(--muted);font-size:12px;text-align:center;line-height:1.6}
@media(max-width:760px){.guide{grid-template-columns:1fr}.guide-toc{grid-column:1;grid-row:auto;position:static;display:grid;grid-template-columns:1fr 1fr}.toc-label{grid-column:1/-1}.grid2{grid-template-columns:1fr}}
</style>
</head>
<body data-chapter="quantization">
<header>
<div class="header-inner">
<div>
<h1>Quantization</h1>
<p class="sub">
Model weights are stored in 16 bits, but they don't <b>need</b> 16 bits β€” most of that
precision is describing differences too small to matter. Quantization throws those bits
away, storing each weight in 8, 4, or even fewer, for a model that's smaller and, on the
memory-bound side of the roofline, faster. The whole art is doing it without wrecking the
accuracy β€” and a few oversized weights make that surprisingly hard.
</p>
</div>
</div>
<nav class="page-tabs" role="tablist">
<button class="page-tab active" data-tab="guide">β‘  Guide</button>
<button class="page-tab" data-tab="playground">β‘‘ Playground</button>
</nav>
</header>
<!-- ═══════════════════════════════════════════════════════════════ GUIDE -->
<section id="guide-tab" class="tab-panel active">
<article class="guide">
<nav class="guide-toc">
<span class="toc-label">Contents</span>
<a href="#ch1">1 Β· Spare bits</a>
<a href="#ch2">2 Β· Rounding to a grid</a>
<a href="#ch3">3 Β· One scale isn't enough</a>
<a href="#ch4">4 Β· The outlier problem</a>
<a href="#ch5">5 Β· Quality vs size</a>
<a href="#ch6">6 Β· Reading the playground</a>
</nav>
<!-- 1 -->
<section class="chapter" id="ch1">
<h2><span class="ch-num">1</span> Spare bits</h2>
<p>
A trained weight like <code>0.0143729</code> is stored in 16-bit floating point, but the model
barely cares about the trailing digits β€” round it to <code>0.014</code> and the network behaves
almost identically. Multiply that slack across billions of weights and you have a lot of bits
doing nothing. Quantization reclaims them, representing each weight with a small integer instead
of a float.
</p>
<figure class="fig"><svg viewBox="0 0 700 200" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,Segoe UI,Roboto,sans-serif"><defs><marker id="arqz" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--muted)"/></marker></defs>
<text x="40" y="24" fill="var(--accent)" font-size="11.5" font-weight="700">FP32 β€” continuous values</text>
<line x1="70" y1="40" x2="640" y2="40" stroke="var(--border)"/>
<circle cx="90" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="130" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="170" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="210" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="250" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="290" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="330" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="370" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="410" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="450" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="490" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="530" cy="40" r="3.4" fill="var(--accent-2)"/><circle cx="570" cy="40" r="3.4" fill="var(--accent-2)"/>
<text x="40" y="100" fill="var(--accent-2)" font-size="11.5" font-weight="700">INT8 β€” snap to a small grid of levels</text>
<line x1="70" y1="40" x2="640" y2="40" stroke="var(--border)" stroke-dasharray="2 4" opacity="0.6"/><line x1="70" y1="64" x2="640" y2="64" stroke="var(--border)" stroke-dasharray="2 4" opacity="0.6"/><line x1="70" y1="88" x2="640" y2="88" stroke="var(--border)" stroke-dasharray="2 4" opacity="0.6"/><line x1="70" y1="112" x2="640" y2="112" stroke="var(--border)" stroke-dasharray="2 4" opacity="0.6"/><circle cx="90" cy="64" r="3.4" fill="var(--accent)"/><line x1="90" y1="40" x2="90" y2="64" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="130" cy="64" r="3.4" fill="var(--accent)"/><line x1="130" y1="40" x2="130" y2="64" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="170" cy="88" r="3.4" fill="var(--accent)"/><line x1="170" y1="40" x2="170" y2="88" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="210" cy="88" r="3.4" fill="var(--accent)"/><line x1="210" y1="40" x2="210" y2="88" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="250" cy="88" r="3.4" fill="var(--accent)"/><line x1="250" y1="40" x2="250" y2="88" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="290" cy="88" r="3.4" fill="var(--accent)"/><line x1="290" y1="40" x2="290" y2="88" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="330" cy="112" r="3.4" fill="var(--accent)"/><line x1="330" y1="40" x2="330" y2="112" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="370" cy="112" r="3.4" fill="var(--accent)"/><line x1="370" y1="40" x2="370" y2="112" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="410" cy="112" r="3.4" fill="var(--accent)"/><line x1="410" y1="40" x2="410" y2="112" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="450" cy="112" r="3.4" fill="var(--accent)"/><line x1="450" y1="40" x2="450" y2="112" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="490" cy="112" r="3.4" fill="var(--accent)"/><line x1="490" y1="40" x2="490" y2="112" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="530" cy="112" r="3.4" fill="var(--accent)"/><line x1="530" y1="40" x2="530" y2="112" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/><circle cx="570" cy="112" r="3.4" fill="var(--accent)"/><line x1="570" y1="40" x2="570" y2="112" stroke="var(--muted)" stroke-dasharray="2 2" opacity="0.4"/>
<circle cx="450" cy="60" r="9" fill="none" stroke="var(--bad)" stroke-width="1.5"/>
<text x="466" y="64" fill="var(--bad)" font-size="9.5">outlier β€” needs care</text>
<text x="350" y="172" text-anchor="middle" fill="var(--muted)" font-size="10">fewer bits = less memory + faster, at the cost of rounding error (worst at outliers)</text>
</svg><figcaption>Quantization stores weights in fewer bits by snapping continuous values onto a small grid of levels. It shrinks the model and speeds up memory-bound work, but every value picks up rounding error β€” and a few large outliers, if not handled separately, can dominate that error and hurt quality.</figcaption></figure>
<p>
The payoff is twofold. The model shrinks β€” int8 halves it, int4 quarters it β€” so it fits on
smaller or fewer GPUs. And because decode is memory-bound (Chapter 22), moving fewer bytes per
weight directly speeds it up: halving the weight size roughly doubles the achievable decode
throughput. Smaller and faster, for the price of a little precision.
</p>
<div class="callout insight">
<strong>Precision is mostly slack.</strong>
Neural-network weights tolerate rounding in a way exact computations don't. That tolerance
is what quantization spends β€” and there's far more of it available than 16 bits suggest.
</div>
<button class="try-it" data-action="open">β–Ά Quantize a real tensor</button>
</section>
<!-- 2 -->
<section class="chapter" id="ch2">
<h2><span class="ch-num">2</span> Rounding to a grid</h2>
<p>
Quantizing to <code>B</code> bits means snapping every weight to one of <code>2^B</code> evenly
spaced levels. You pick a <em>scale</em> that maps the floating-point range onto the integer
range, round each weight to the nearest integer, and store that:
</p>
<div class="formula-box">
<div class="formula">q = round(w / scale) &nbsp;Β·&nbsp; <span class="hl">Ε΅ = q Β· scale</span></div>
<div class="formula-note">scale = max|w| Γ· (2^(Bβˆ’1) βˆ’ 1). The reconstruction Ε΅ differs from w by the rounding error.</div>
</div>
<p>
With 8 bits you have 256 levels β€” fine enough that the rounding error is tiny and the model is
all but unchanged. With 4 bits you have 16 levels, and the spacing between them starts to
matter. The error you introduce is exactly the gap between each weight and its nearest level,
so everything comes down to placing those levels well.
</p>
</section>
<!-- 3 -->
<section class="chapter" id="ch3">
<h2><span class="ch-num">3</span> One scale isn't enough</h2>
<p>
The naive approach uses a single scale for the entire weight matrix β€” <em>per-tensor</em>
quantization. It's simple, but it forces every weight, large and small, onto the same grid. If
one part of the matrix has a much wider range than another, the levels get stretched to cover
the widest part, leaving the rest of the weights crammed coarsely between a few of them.
</p>
<p>
The fix is to use more scales. <strong>Per-channel</strong> quantization gives each row (or
column) its own scale, so a quiet channel gets a fine grid and a loud one gets a coarse grid,
each appropriate to its range. <strong>Group-wise</strong> quantization goes further, a separate
scale for every block of, say, 128 weights β€” the modern default, because the handful of extra
scale values costs almost nothing and the error drops sharply.
</p>
<div class="callout insight">
<strong>More scales, less error β€” for almost free.</strong>
A scale is one number per group; storing it adds a fraction of a bit per weight. In exchange,
every group gets a grid sized to its own weights, which is most of what makes 4-bit
quantization viable at all.
</div>
<button class="try-it" data-action="method">β–Ά Switch from per-tensor to group-wise</button>
</section>
<!-- 4 -->
<section class="chapter" id="ch4">
<h2><span class="ch-num">4</span> The outlier problem</h2>
<p>
Here's what actually breaks low-bit quantization: <strong>outliers</strong>. A small number of
weights β€” and, even more troublesome, a small number of activation channels β€” have magnitudes
many times larger than everything else. Because the scale is set by the maximum, a single
outlier stretches the grid so wide that all the ordinary weights collapse onto just a couple of
levels. One weight ruins the precision of thousands.
</p>
<p>
The methods you hear about β€” <strong>GPTQ</strong>, <strong>AWQ</strong>, and friends β€” are
mostly clever ways to handle outliers. Some keep the salient weights in higher precision while
quantizing the rest; AWQ scales weights by how much their activations matter, protecting the
ones that do; GPTQ adjusts the remaining weights to compensate for the error introduced. All of
them exist because, without outlier handling, four-bit quantization falls apart.
</p>
<div class="callout warn">
<strong>A few weights hold the precision hostage.</strong>
Outliers are why "just round it to 4 bits" doesn't work, and why a whole research literature
grew up around quantization. Protect the outliers and the rest quantizes beautifully; ignore
them and the model degrades fast.
</div>
<button class="try-it" data-action="outlier">β–Ά Protect the outliers and watch error drop</button>
</section>
<!-- 5 -->
<section class="chapter" id="ch5">
<h2><span class="ch-num">5</span> Quality vs size</h2>
<p>
Put the curve together and a clear story emerges. <strong>Int8</strong> is, for most models,
effectively lossless β€” half the size, no measurable quality drop, an easy win. <strong>Int4</strong>
with group-wise scales and proper outlier handling loses very little and is the workhorse of
local model serving. Push below four bits β€” three, two β€” and quality starts dropping off a
cliff that no amount of cleverness fully arrests.
</p>
<p>
So quantization isn't a free lunch all the way down; it's a steep trade-off curve, and the
sweet spot for inference today sits around 4 bits. The same arithmetic applies to the KV cache
(Chapter 13) and even to training in lower precision β€” but the principle is always the one the
playground makes visible: find the bits that aren't carrying information, and only those.
</p>
<div class="callout insight">
<strong>Int8 is free; int4 is cheap; below that, you pay.</strong>
The quality-versus-size curve is gentle down to about four bits and steep below it. Knowing
where that knee is β€” for your model and your tolerance β€” is the whole decision.
</div>
</section>
<!-- 6 -->
<section class="chapter" id="ch6">
<h2><span class="ch-num">6</span> Reading the playground</h2>
<p>
A real weight tensor β€” a realistic distribution with a few genuine outliers β€” is quantized live
at the bit-width and method you choose, and the reconstruction error is measured directly. The
numbers are exact arithmetic on the tensor in front of you.
</p>
<div class="panel-guide-item"><span class="pgi-label">β–¦</span>
<p>The weight histogram with the quantization grid laid over it. An outlier under per-tensor
stretches the grid so the bulk falls between just a few levels.</p></div>
<div class="panel-guide-item"><span class="pgi-label">↔</span>
<p>Choose the bits and the method, toggle outlier protection, and watch the reconstruction error
and the compression ratio respond.</p></div>
<div class="panel-guide-item"><span class="pgi-label">⌣</span>
<p>The error-versus-bits curve for each method β€” the gentle slope to 4 bits, the cliff below.</p></div>
<div class="guide-end">
<p>The reading is the setup. The playground is the point.</p>
<button class="try-it large" data-action="open">β–Ά Open the Playground</button>
</div>
</section>
</article>
</section>
<!-- ═══════════════════════════════════════════════════════════════ PLAYGROUND -->
<section id="playground-tab" class="tab-panel">
<div class="wrap">
<div class="panel">
<div class="panel-head"><span class="panel-label">Quantize the tensor</span>
<span class="panel-note">2,048 real weights Β· gaussian bulk + a few outliers</span></div>
<div class="ctrl-row">
<div class="ctrl"><span class="lab">Bits</span>
<div class="seg" id="bitsSeg"><button data-b="8">8</button><button data-b="4" class="on">4</button><button data-b="3">3</button><button data-b="2">2</button></div></div>
<div class="ctrl"><span class="lab">Method</span>
<div class="seg" id="methodSeg"><button data-m="tensor" class="on">Per-tensor</button><button data-m="channel">Per-channel</button><button data-m="group">Group-wise (64)</button></div></div>
<label class="toggle" id="outlierToggle"><span class="switch"></span><span>Protect outliers (1% in fp16)</span></label>
</div>
</div>
<div class="panel" id="panelGrid">
<div class="panel-head"><span class="panel-label">Step 1 Β· weights on the quantization grid</span>
<span class="panel-note">histogram of the bulk; level lines for the per-tensor grid</span></div>
<svg id="histPlot" viewBox="0 0 900 220"></svg>
<div class="bigstats">
<div class="bs"><span class="v" id="stErr" style="color:var(--accent-2)">β€”</span><span class="l">reconstruction error</span></div>
<div class="bs"><span class="v" id="stBits">β€”</span><span class="l">effective bits / weight</span></div>
<div class="bs"><span class="v" id="stComp" style="color:var(--accent)">β€”</span><span class="l">compression</span></div>
<div class="bs"><span class="v" id="stLevels">β€”</span><span class="l">levels</span></div>
</div>
<div class="verdict" id="quantVerdict"></div>
</div>
<div class="panel" id="panelCurve">
<div class="panel-head"><span class="panel-label">Step 2 Β· error vs bits</span>
<span class="panel-note">measured on this tensor, per method</span></div>
<div class="grid2">
<div class="card">
<h4>Reconstruction error by bit-width</h4>
<p class="cap">Per-tensor (orange) is wrecked by the outlier at low bits; group-wise (teal)
holds. Note the gentle slope to 4 bits and the cliff below.</p>
<svg id="curvePlot" viewBox="0 0 420 230"></svg>
<div class="legend"><span><i style="background:var(--warn)"></i>per-tensor</span><span><i style="background:var(--accent)"></i>per-channel</span><span><i style="background:var(--accent-2)"></i>group-wise</span></div>
</div>
<div class="card">
<h4>The bytes you keep</h4>
<svg id="memPlot" viewBox="0 0 420 230"></svg>
<p class="hint">int8 β‰ˆ Β½ the size, near-lossless. int4 β‰ˆ ΒΌ the size, the inference workhorse.
Below 4 bits the error climbs faster than the size falls.</p>
</div>
</div>
</div>
<footer>
A real 2,048-weight tensor (gaussian bulk plus a few large outliers) quantized live. Errors and
compression are exact arithmetic; "protect outliers" keeps the top 1% of magnitudes in fp16, the
idea behind mixed-precision methods like LLM.int8() and AWQ.
</footer>
</div>
</section>
<script>
"use strict";
/* ── tabs / toc / try-it ── */
function switchTab(name){document.querySelectorAll(".page-tab").forEach(b=>{const on=b.dataset.tab===name;b.classList.toggle("active",on);b.setAttribute("aria-selected",on);});document.querySelectorAll(".tab-panel").forEach(p=>p.classList.toggle("active",p.id===name+"-tab"));}
document.querySelectorAll(".page-tab").forEach(b=>b.addEventListener("click",()=>switchTab(b.dataset.tab)));
document.querySelectorAll(".guide-toc a").forEach(a=>a.addEventListener("click",e=>{e.preventDefault();document.querySelector(a.getAttribute("href"))?.scrollIntoView({behavior:"smooth",block:"start"});}));
document.querySelectorAll(".try-it[data-action]").forEach(b=>b.addEventListener("click",()=>{const a=b.dataset.action;switchTab("playground");window.scrollTo({top:0,behavior:"smooth"});const t=(a==="method"||a==="outlier")?"panelGrid":(a==="open"?null:"panelCurve");if(t)setTimeout(()=>document.getElementById(t)?.scrollIntoView({behavior:"smooth",block:"start"}),350);}));
/* ════════════════ ENGINE β€” real quantization ════════════════ */
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
const ROWS=16, COLS=128, NW=ROWS*COLS, GROUP=64;
// realistic weights: gaussian bulk N(0,0.02) + ~1.2% outliers at Β±(0.15..0.4)
const W=(()=>{const r=mulberry32(7),w=new Array(NW);for(let i=0;i<NW;i++){const u=r()||1e-9,v=r();let g=Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v)*0.02;if(r()<0.005)g=(r()<0.5?-1:1)*(0.2+r()*0.3);w[i]=g;}return w;})();
function absmax(a){let m=0;for(const x of a)m=Math.max(m,Math.abs(x));return m;}
// quantize a block to B bits (symmetric), return dequantized values
function quantBlock(vals,B){const maxq=Math.pow(2,B-1)-1,mab=absmax(vals)||1e-9,scale=mab/maxq;
return vals.map(v=>{let q=Math.round(v/scale);if(q>maxq)q=maxq;if(q<-maxq)q=-maxq;return q*scale;});}
// full quantize with method + optional outlier protection. returns {recon, err, effBits, levels}
function quantize(B,method,protect){
const idx=[...Array(NW).keys()];
let prot=new Set();
if(protect){const sorted=idx.slice().sort((a,b)=>Math.abs(W[b])-Math.abs(W[a]));const n=Math.ceil(NW*0.01);for(let i=0;i<n;i++)prot.add(sorted[i]);}
// values to quantize (outliers kept exact if protected)
const recon=new Array(NW);
const doBlock=(indices)=>{const vals=indices.map(i=>prot.has(i)?0:W[i]);const dq=quantBlock(vals,B);indices.forEach((i,j)=>{recon[i]=prot.has(i)?W[i]:dq[j];});};
if(method==="tensor"){doBlock(idx);}
else if(method==="channel"){for(let r=0;r<ROWS;r++){const row=[];for(let c=0;c<COLS;c++)row.push(r*COLS+c);doBlock(row);}}
else{for(let g=0;g<NW;g+=GROUP){const grp=[];for(let i=g;i<Math.min(NW,g+GROUP);i++)grp.push(i);doBlock(grp);}}
// error (relative L2)
let se=0,sn=0;for(let i=0;i<NW;i++){const e=W[i]-recon[i];se+=e*e;sn+=W[i]*W[i];}
const err=Math.sqrt(se/sn);
// effective bits: B per weight + scale overhead (16 bits per block) + outlier overhead
const blocks=method==="tensor"?1:method==="channel"?ROWS:Math.ceil(NW/GROUP);
const scaleBits=16*blocks/NW;
const protBits=protect?(0.01*(16+11)):0; // kept weight 16b + ~index
const effBits=B+scaleBits+protBits;
return {recon,err,effBits,levels:Math.pow(2,B)};
}
/* ════════════════ UI ════════════════ */
const $=id=>document.getElementById(id);
const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim();
const S={B:4,method:"tensor",protect:false};
function render(){
const Q=quantize(S.B,S.method,S.protect);
$('stErr').textContent=(Q.err*100).toFixed(1)+'%';
$('stErr').style.color=Q.err<0.03?css('--good'):Q.err<0.12?css('--warn'):css('--bad');
$('stBits').textContent=Q.effBits.toFixed(2);
$('stComp').textContent=(16/Q.effBits).toFixed(1)+'Γ—';
$('stLevels').textContent=Q.levels;
const v=$('quantVerdict');
if(Q.err<0.08){v.className="verdict good";v.textContent='Low weight error β€” '+(Q.err*100).toFixed(1)+'% at '+(16/Q.effBits).toFixed(1)+'Γ— compression. Model quality moves even less than this; a setting like this ships.';}
else if(Q.err<0.20){v.className="verdict warn";v.textContent='Lossy β€” '+(Q.err*100).toFixed(1)+'% weight error. Finer blocks (group-wise) plus outlier protection would tighten it considerably.';}
else{v.className="verdict bad";v.textContent='Degraded β€” '+(Q.err*100).toFixed(0)+'% weight error. An outlier is stretching the grid so the bulk collapses onto a few levels. Protect outliers, use group-wise, or raise the bits.';}
drawHist(Q);drawCurve();drawMem(Q);
}
function drawHist(Q){
const W2=900,H=220,padL=10,padR=10,padB=24,padT=10,rng=0.10,bins=70;
const X=x=>padL+((x+rng)/(2*rng))*(W2-padL-padR);
const hist=new Array(bins).fill(0);for(const w of W){if(Math.abs(w)<rng){const b=Math.floor(((w+rng)/(2*rng))*bins);if(b>=0&&b<bins)hist[b]++;}}
const mx=Math.max(...hist);let s="";
const bw=(W2-padL-padR)/bins;
for(let b=0;b<bins;b++){const h=(hist[b]/mx)*(H-padT-padB);s+='<rect x="'+(padL+b*bw).toFixed(1)+'" y="'+(H-padB-h).toFixed(1)+'" width="'+(bw-0.5).toFixed(1)+'" height="'+h.toFixed(1)+'" fill="'+css('--accent')+'" opacity="0.5"/>';}
// per-tensor grid levels (the visible problem)
const maxq=Math.pow(2,S.B-1)-1,mab=absmax(W),scale=mab/maxq;
let shown=0;for(let q=-maxq;q<=maxq;q++){const lv=q*scale;if(Math.abs(lv)<=rng){s+='<line x1="'+X(lv).toFixed(1)+'" y1="'+padT+'" x2="'+X(lv).toFixed(1)+'" y2="'+(H-padB)+'" stroke="'+css('--accent-2')+'" stroke-width="1" opacity="0.7"/>';shown++;}}
s+='<text x="'+(padL+4)+'" y="'+(padT+12)+'" font-size="10" fill="'+css('--accent-2')+'">'+shown+' of '+Q.levels+' grid levels land in the bulk (Β±0.10) under per-tensor</text>';
s+='<text x="'+(W2-padR)+'" y="'+(padT+12)+'" font-size="9.5" text-anchor="end" fill="'+css('--warn')+'">outliers at Β±0.15–0.40 stretch the grid β†’</text>';
s+='<text x="'+(W2/2)+'" y="'+(H-6)+'" font-size="9.5" text-anchor="middle" fill="'+css('--muted')+'">weight value (bulk shown; teal lines = quantization levels)</text>';
$('histPlot').innerHTML=s;
}
function drawCurve(){
const W2=420,H=230,padL=42,padR=14,padT=14,padB=30,bitsList=[2,3,4,6,8];
const methods=[["tensor",css('--warn')],["channel",css('--accent')],["group",css('--accent-2')]];
const data=methods.map(m=>bitsList.map(b=>quantize(b,m[0],S.protect).err));
const maxE=Math.max(...data.flat(),0.05);
const X=b=>padL+((b-2)/(8-2))*(W2-padL-padR),Y=e=>H-padB-(Math.min(e,maxE)/maxE)*(H-padT-padB);
let s="";
s+='<line x1="'+padL+'" y1="'+(H-padB)+'" x2="'+(W2-padR)+'" y2="'+(H-padB)+'" stroke="'+css('--border')+'"/>';
methods.forEach((m,mi)=>{let p="";bitsList.forEach((b,i)=>{p+=(i?"L":"M")+X(b).toFixed(1)+" "+Y(data[mi][i]).toFixed(1)+" ";});
s+='<path d="'+p+'" fill="none" stroke="'+m[1]+'" stroke-width="2"/>';
bitsList.forEach((b,i)=>{const on=(m[0]===S.method&&b===S.B);s+='<circle cx="'+X(b)+'" cy="'+Y(data[mi][i])+'" r="'+(on?5:2.5)+'" fill="'+m[1]+'"'+(on?' stroke="#fff" stroke-width="1.2"':'')+'/>';});});
bitsList.forEach(b=>{s+='<text x="'+X(b)+'" y="'+(H-padB+14)+'" font-size="9" text-anchor="middle" fill="'+css('--muted')+'">'+b+'b</text>';});
s+='<text x="8" y="'+(padT+30)+'" font-size="8.5" fill="'+css('--muted')+'" transform="rotate(-90 12 '+(padT+40)+')">error</text>';
s+='<text x="'+((padL+W2-padR)/2)+'" y="'+(H-2)+'" font-size="9" text-anchor="middle" fill="'+css('--muted')+'">bits per weight β†’</text>';
$('curvePlot').innerHTML=s;
}
function drawMem(Q){
const W2=420,H=230,padL=10,padR=10,padB=30,padT=14;
const items=[["fp16",16,css('--muted')],["int8",8,css('--accent')],["int4",4,css('--accent-2')],["this",Q.effBits,css('--good')]];
const bw=(W2-padL-padR)/items.length;let s="";
items.forEach((it,i)=>{const h=(it[1]/16)*(H-padT-padB),x=padL+i*bw;
s+='<rect x="'+(x+10)+'" y="'+(H-padB-h)+'" width="'+(bw-20)+'" height="'+h.toFixed(1)+'" rx="3" fill="'+it[2]+'"/>';
s+='<text x="'+(x+bw/2)+'" y="'+(H-padB-h-5)+'" font-size="10" text-anchor="middle" fill="'+it[2]+'" font-weight="700">'+it[1].toFixed(it[1]<10?1:0)+'b</text>';
s+='<text x="'+(x+bw/2)+'" y="'+(H-padB+15)+'" font-size="9.5" text-anchor="middle" fill="'+css('--muted')+'">'+it[0]+'</text>';});
$('memPlot').innerHTML=s;
}
/* ── events ── */
$('bitsSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;[...e.currentTarget.children].forEach(x=>x.classList.remove('on'));b.classList.add('on');S.B=+b.dataset.b;render();});
$('methodSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;[...e.currentTarget.children].forEach(x=>x.classList.remove('on'));b.classList.add('on');S.method=b.dataset.m;render();});
$('outlierToggle').addEventListener('click',()=>{S.protect=!S.protect;$('outlierToggle').querySelector('.switch').classList.toggle('on',S.protect);render();});
render();
</script>
</body>
</html>