The demo below generates a small set of toy model weights drawn from a realistic distribution. Choose a bit-width with the slider — from 8 bits (INT8) down to 2 bits — and watch the histogram of quantization errors change.
<!-- Demo layout — controls, stats bar, canvas chart, legend, resample button -->
<div class="controls">
<label for="bits-slider">Bit-width <strong id="bits-val">8</strong> bits</label>
<input type="range" id="bits-slider" min="2" max="8" step="1" value="8" title="Change quantization bit-width">
</div>
<div class="stats" id="stats"></div>
<canvas id="chart" width="420" height="180" aria-label="Histogram of weight distribution (top) and quantization errors (bottom)"></canvas>
<div class="legend">
<span class="dot dot-orig"></span> Weight distribution
<span class="dot dot-err"></span> Quantization errors
</div>
<div class="info" id="info"></div>
<button id="resample" type="button">Resample weights</button>
/* Styles for the INT8 quantization demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; background: #f8f9fb; }
.controls { display: flex; flex-direction: column; gap: .25rem; margin-bottom: .6rem; font-size: .9rem; }
label { font-weight: 600; color: #1d3557; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
.stats { font-size: .82rem; color: #444; min-height: 1.3em; margin-bottom: .35rem; }
canvas { display: block; background: #fff; border: 1px solid #dde3ea; border-radius: 8px;
width: 100%; max-width: 420px; }
.legend { font-size: .8rem; color: #555; margin: .4rem 0; display: flex; gap: 1rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: middle; margin-right: 3px; }
.dot-orig { background: #457b9d; }
.dot-err { background: #e63946; }
.info { font-size: .85rem; color: #2d6a4f; font-weight: 600; min-height: 1.4em; margin: .35rem 0; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:hover { background: #163048; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// INT8 quantization demo — weight histogram vs. reconstruction error at varying bit-widths
// Deterministic LCG random number generator so the demo is reproducible
let seed = 42;
function seededRand() {
seed = (seed * 1664525 + 1013904223) & 0xffffffff;
return (seed >>> 0) / 0xffffffff;
}
function resetSeed(s) { seed = s >>> 0; }
// Generate n weights from a normal distribution (mean=0, std=0.5)
function sampleWeights(n) {
const w = [];
for (let i = 0; i < n; i++) {
// Box-Muller transform for Gaussian samples
const u1 = seededRand() + 1e-12, u2 = seededRand();
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
w.push(z * 0.5);
}
return w;
}
// Asymmetric min-max quantization — compute scale and zero-point then reconstruct
function quantize(weights, bits) {
const levels = (1 << bits) - 1;
const wMin = Math.min(...weights);
const wMax = Math.max(...weights);
const scale = (wMax - wMin) / levels;
const zp = -Math.round(wMin / scale);
const reconstructed = weights.map(w => {
const q = Math.max(0, Math.min(levels, Math.round(w / scale) + zp));
return scale * (q - zp);
});
return { reconstructed, scale, levels };
}
// Root-mean-square error between original and reconstructed weights
function rmse(a, b) {
const n = a.length;
let sum = 0;
for (let i = 0; i < n; i++) sum += (a[i] - b[i]) ** 2;
return Math.sqrt(sum / n);
}
// Bin values into a histogram of `bins` buckets spanning [lo, hi]
function histogram(values, bins, lo, hi) {
const counts = new Array(bins).fill(0);
const w = (hi - lo) / bins;
for (const v of values) {
const idx = Math.min(bins - 1, Math.max(0, Math.floor((v - lo) / w)));
counts[idx]++;
}
return counts;
}
// Draw two stacked histograms on the canvas — top=weights, bottom=errors
function draw(weights, reconstructed, errors, bits) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 14, right: 10, bottom: 28, left: 30 };
ctx.clearRect(0, 0, W, H);
const bins = 32;
const lo = -2.5, hi = 2.5;
const wHist = histogram(weights, bins, lo, hi);
const eHist = histogram(errors, bins, -0.5, 0.5);
const maxW = Math.max(...wHist, 1);
const maxE = Math.max(...eHist, 1);
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
const halfH = plotH / 2;
const barW = plotW / bins - 1;
// Draw axis lines
ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + plotH);
ctx.moveTo(PAD.left, PAD.top + halfH); ctx.lineTo(PAD.left + plotW, PAD.top + halfH);
ctx.stroke();
// Draw axis labels
ctx.fillStyle = '#666'; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
ctx.fillText('weights', PAD.left - 3, PAD.top + halfH * 0.5 + 4);
ctx.fillText('errors', PAD.left - 3, PAD.top + halfH + halfH * 0.5 + 4);
// Draw weight distribution bars (blue, top half)
ctx.fillStyle = '#457b9d';
for (let i = 0; i < bins; i++) {
const bh = (wHist[i] / maxW) * halfH * 0.9;
const x = PAD.left + i * (plotW / bins);
ctx.fillRect(x, PAD.top + halfH - bh, barW, bh);
}
// Draw quantization error bars (red, bottom half)
ctx.fillStyle = '#e63946';
for (let i = 0; i < bins; i++) {
const bh = (eHist[i] / maxE) * halfH * 0.9;
const x = PAD.left + i * (plotW / bins);
ctx.fillRect(x, PAD.top + halfH, barW, bh);
}
// Draw x-axis tick labels
ctx.fillStyle = '#888'; ctx.font = '9px system-ui'; ctx.textAlign = 'center';
ctx.fillText(lo, PAD.left, PAD.top + plotH + 12);
ctx.fillText('0', PAD.left + plotW / 2, PAD.top + plotH + 12);
ctx.fillText(hi, PAD.left + plotW, PAD.top + plotH + 12);
}
let weights = [];
let currentSeed = 42;
function update() {
const bits = parseInt(document.getElementById('bits-slider').value, 10);
document.getElementById('bits-val').textContent = bits;
const { reconstructed, scale, levels } = quantize(weights, bits);
const errors = weights.map((w, i) => reconstructed[i] - w);
const err = rmse(weights, reconstructed);
const maxErr = Math.max(...errors.map(Math.abs));
const levels2 = 1 << bits;
draw(weights, reconstructed, errors, bits);
document.getElementById('stats').textContent =
i18nFmt('%bits%-bit quantization · %levels% integer levels · scale = %scale%', { bits, levels: levels2, scale: scale.toFixed(4) });
document.getElementById('info').textContent =
i18nFmt('RMSE = %rmse% · max |error| = %maxErr%', { rmse: err.toFixed(4), maxErr: maxErr.toFixed(4) });
}
function resample() {
currentSeed = (Math.floor(Math.random() * 1e9)) >>> 0;
resetSeed(currentSeed);
weights = sampleWeights(500);
update();
}
document.getElementById('bits-slider').addEventListener('input', update);
document.getElementById('resample').addEventListener('click', resample);
// Initialize with seed 42 so the first load is deterministic
resetSeed(42);
weights = sampleWeights(500);
update();
<!-- Demo layout — controls, stats bar, canvas chart, legend, resample button -->
<div class="controls">
<label for="bits-slider">Bit-width <strong id="bits-val">8</strong> bits</label>
<input type="range" id="bits-slider" min="2" max="8" step="1" value="8" title="Change quantization bit-width">
</div>
<div class="stats" id="stats"></div>
<canvas id="chart" width="420" height="180" aria-label="Histogram of weight distribution (top) and quantization errors (bottom)"></canvas>
<div class="legend">
<span class="dot dot-orig"></span> Weight distribution
<span class="dot dot-err"></span> Quantization errors
</div>
<div class="info" id="info"></div>
<button id="resample" type="button">Resample weights</button>
/* Styles for the INT8 quantization demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; background: #f8f9fb; }
.controls { display: flex; flex-direction: column; gap: .25rem; margin-bottom: .6rem; font-size: .9rem; }
label { font-weight: 600; color: #1d3557; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
.stats { font-size: .82rem; color: #444; min-height: 1.3em; margin-bottom: .35rem; }
canvas { display: block; background: #fff; border: 1px solid #dde3ea; border-radius: 8px;
width: 100%; max-width: 420px; }
.legend { font-size: .8rem; color: #555; margin: .4rem 0; display: flex; gap: 1rem; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; vertical-align: middle; margin-right: 3px; }
.dot-orig { background: #457b9d; }
.dot-err { background: #e63946; }
.info { font-size: .85rem; color: #2d6a4f; font-weight: 600; min-height: 1.4em; margin: .35rem 0; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:hover { background: #163048; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// INT8 quantization demo — weight histogram vs. reconstruction error at varying bit-widths
// Deterministic LCG random number generator so the demo is reproducible
let seed = 42;
function seededRand() {
seed = (seed * 1664525 + 1013904223) & 0xffffffff;
return (seed >>> 0) / 0xffffffff;
}
function resetSeed(s) { seed = s >>> 0; }
// Generate n weights from a normal distribution (mean=0, std=0.5)
function sampleWeights(n) {
const w = [];
for (let i = 0; i < n; i++) {
// Box-Muller transform for Gaussian samples
const u1 = seededRand() + 1e-12, u2 = seededRand();
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
w.push(z * 0.5);
}
return w;
}
// Asymmetric min-max quantization — compute scale and zero-point then reconstruct
function quantize(weights, bits) {
const levels = (1 << bits) - 1;
const wMin = Math.min(...weights);
const wMax = Math.max(...weights);
const scale = (wMax - wMin) / levels;
const zp = -Math.round(wMin / scale);
const reconstructed = weights.map(w => {
const q = Math.max(0, Math.min(levels, Math.round(w / scale) + zp));
return scale * (q - zp);
});
return { reconstructed, scale, levels };
}
// Root-mean-square error between original and reconstructed weights
function rmse(a, b) {
const n = a.length;
let sum = 0;
for (let i = 0; i < n; i++) sum += (a[i] - b[i]) ** 2;
return Math.sqrt(sum / n);
}
// Bin values into a histogram of `bins` buckets spanning [lo, hi]
function histogram(values, bins, lo, hi) {
const counts = new Array(bins).fill(0);
const w = (hi - lo) / bins;
for (const v of values) {
const idx = Math.min(bins - 1, Math.max(0, Math.floor((v - lo) / w)));
counts[idx]++;
}
return counts;
}
// Draw two stacked histograms on the canvas — top=weights, bottom=errors
function draw(weights, reconstructed, errors, bits) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 14, right: 10, bottom: 28, left: 30 };
ctx.clearRect(0, 0, W, H);
const bins = 32;
const lo = -2.5, hi = 2.5;
const wHist = histogram(weights, bins, lo, hi);
const eHist = histogram(errors, bins, -0.5, 0.5);
const maxW = Math.max(...wHist, 1);
const maxE = Math.max(...eHist, 1);
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
const halfH = plotH / 2;
const barW = plotW / bins - 1;
// Draw axis lines
ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + plotH);
ctx.moveTo(PAD.left, PAD.top + halfH); ctx.lineTo(PAD.left + plotW, PAD.top + halfH);
ctx.stroke();
// Draw axis labels
ctx.fillStyle = '#666'; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
ctx.fillText('weights', PAD.left - 3, PAD.top + halfH * 0.5 + 4);
ctx.fillText('errors', PAD.left - 3, PAD.top + halfH + halfH * 0.5 + 4);
// Draw weight distribution bars (blue, top half)
ctx.fillStyle = '#457b9d';
for (let i = 0; i < bins; i++) {
const bh = (wHist[i] / maxW) * halfH * 0.9;
const x = PAD.left + i * (plotW / bins);
ctx.fillRect(x, PAD.top + halfH - bh, barW, bh);
}
// Draw quantization error bars (red, bottom half)
ctx.fillStyle = '#e63946';
for (let i = 0; i < bins; i++) {
const bh = (eHist[i] / maxE) * halfH * 0.9;
const x = PAD.left + i * (plotW / bins);
ctx.fillRect(x, PAD.top + halfH, barW, bh);
}
// Draw x-axis tick labels
ctx.fillStyle = '#888'; ctx.font = '9px system-ui'; ctx.textAlign = 'center';
ctx.fillText(lo, PAD.left, PAD.top + plotH + 12);
ctx.fillText('0', PAD.left + plotW / 2, PAD.top + plotH + 12);
ctx.fillText(hi, PAD.left + plotW, PAD.top + plotH + 12);
}
let weights = [];
let currentSeed = 42;
function update() {
const bits = parseInt(document.getElementById('bits-slider').value, 10);
document.getElementById('bits-val').textContent = bits;
const { reconstructed, scale, levels } = quantize(weights, bits);
const errors = weights.map((w, i) => reconstructed[i] - w);
const err = rmse(weights, reconstructed);
const maxErr = Math.max(...errors.map(Math.abs));
const levels2 = 1 << bits;
draw(weights, reconstructed, errors, bits);
document.getElementById('stats').textContent =
i18nFmt('%bits%-bit quantization · %levels% integer levels · scale = %scale%', { bits, levels: levels2, scale: scale.toFixed(4) });
document.getElementById('info').textContent =
i18nFmt('RMSE = %rmse% · max |error| = %maxErr%', { rmse: err.toFixed(4), maxErr: maxErr.toFixed(4) });
}
function resample() {
currentSeed = (Math.floor(Math.random() * 1e9)) >>> 0;
resetSeed(currentSeed);
weights = sampleWeights(500);
update();
}
document.getElementById('bits-slider').addEventListener('input', update);
document.getElementById('resample').addEventListener('click', resample);
// Initialize with seed 42 so the first load is deterministic
resetSeed(42);
weights = sampleWeights(500);
update();
Notice that at 8 bits the errors are tiny and evenly spread. Drop to 4 bits and the error bars widen noticeably. At 2 bits — only 4 discrete levels — the reconstruction can be badly wrong for weights near the edges of the range. This is why 8 bits became the practical sweet spot: enough levels to keep errors sub-percent, cheap enough to halve or quarter model size and inference cost.
Comments
Loading comments...