Spaces:
Sleeping
Sleeping
| <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) Β· <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> | |
| ; | |
| /* ββ 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> | |