☰
说 INT8 量化把模型压到 1/4?实测:内存刚好 4 倍,异常值多的张量 95% 小值却被毁 29%
2026/10/19 2:23:41 网站建设 项目流程

端侧推理今年是真的火了——WebGPU 1.0 落地、WebLLM / Transformers.js 把大模型直接塞进浏览器,数据不出端、延迟压到几十毫秒。圈子里的共识也跟着简化成一句口号:「把权重从 FP32 量化成 INT8,体积砍到 1/4,端侧就跑得动了」。

这话对了一半。而且偏偏是对的那一半最不值钱。我用 Node v22 在一百万个元素的张量上把这件事拆开测了一遍:内存比确实严丝合缝是 4.0×,但精度的账完全不是「压小一点、损失可忽略」那么简单——对真实模型里那种「绝大多数很小、偶尔几个很大」的激活张量,朴素量化会把占 95% 的小值毁掉近三成;更要命的是,在纯浏览器 JS 里量化后的计算反而慢了 34 倍。下面是一组可复现的数字。

背景:为什么「压到 1/4」这句口号会误导人

端侧推理的瓶颈从来不是「能不能算」,而是「装不装得下、跑不跑得动」。一个 7B 参数的小模型,FP32 权重就要 26 GB,任何消费级设备都直接出局;量化到 INT8 后理论只剩 6.5 GB,听起来就活了。

问题出在省略号:从 FP32 到 INT8 不是无损打包,而是把连续的浮点值映射成 256 个整数档位。这一步省下的内存是真的,但腾出的空间是用精度换的——而精度的损失高度依赖你的数据长什么样。把「体积 1/4」和「精度够用」画等号,等于默认所有张量都是均匀干净的分布,可真实模型的权重和激活恰恰不是。

解剖:INT8 量化到底怎么运作

最常用的是对称逐张量(per-tensor)量化:找一个全局缩放因子scale = maxAbs / 127,把每个浮点值除以 scale 后四舍五入夹到 [-127,127] 的整数,推理时再乘回 scale 还原。十几行就能写出来:

// 对称逐张量 INT8 量化(推理侧还原) function quantizePerTensor(a) { let maxAbs = 0; for (const v of a) if (Math.abs(v) > maxAbs) maxAbs = Math.abs(v); const scale = maxAbs / 127; // 全局缩放因子 const q = new Int8Array(a.length); for (let i = 0; i < a.length; i++) { let v = Math.round(a[i] / scale); if (v > 127) v = 127; else if (v < -127) v = -127; q[i] = v; } return { q, scale }; } function dequant(q, scale) { const d = new Float32Array(q.length); for (let i = 0; i < q.length; i++) d[i] = q[i] * scale; return d; }

图1:FP32 张量 → 取全局 maxAbs 算 scale → 取整夹到 [-127,127] → 以 Int8 存储 → 推理时乘 scale 还原。整条链路唯一的「免费午餐」是存储体积。

关键在于scale是全局的:一个特别大的异常值会撑大 scale,从而把所有小值的量化步长也一起拉粗。这一步的代价,下面用数字说话。

实证一:内存——唯一严丝合缝的 4 倍

先说好的那半。在 100 万元素的张量上,Float32 占 4,000,000 字节(恰好 4 MB),量化成 Int8 占 1,000,000 字节(1 MB),比值是精确的 4.0——因为字节宽度就是 4:1,没有魔法。

# 复现:Node v22.22.2,纯 JS,零依赖 node _bench_quant.mjs # -> memory.ratio = 4 # -> 7B 模型:FP32 26.08 GB → INT8 6.52 GB

图2:1M 元素张量 4 MB → 1 MB(精确 4×);放大到 7B 参数模型,26.08 GB → 6.52 GB,正好四分之一。内存节省是确定的、可预期的。

所以「压到 1/4」这句口号,在存储维度上完全成立,且毫无意外。真正要小心的是它顺手暗示的下一句:「既然体积够了,精度也够用了」。

实证二:精度——干净数据 1%,异常值数据毁掉 29%

我用三种有代表性的分布各生成 100 万元素,量化后算归一化均方根误差(NRMSE,除以数据自身标准差,方便横向比),取 5 个随机种子的均值:

分布总体 NRMSE说明
均匀[-1,1]0.39%教科书里的「干净」数据
高斯N(0,1)1.10%权重近似这种形状
异常值型(95%×0.1 + 5%×3)4.31%像真实激活:多数很小、少数很大
异常值型 → per-channel(1k 块)2.56%改用分块独立 scale

图3:干净数据误差不到 1.1%,异常值型总体冲到 4.31%;改用 per-channel 分块缩放后回落到 2.56%。但总体指标会骗人——

这里有一个被「总体 NRMSE」藏起来的坑。上面那个异常值分布,总体 NRMSE 只有 4.31%,看起来还行——可那是因为 5% 的大异常值贡献了绝大部分方差,把指标撑住了。单独看占 95% 的「小值主体」,它们的相对误差高达 29.2%:全局 scale 被少数异常值撑大,小值的量化步长被一起拉粗,等于用 95% 数据的精度去补贴 5% 的 outlier。真实 LLM 的激活正是这种形状,所以这个 29% 不是边缘情况,而是常态。

per-channel(按 1024 个元素一块、各自算 scale)能把异常值隔离在自己的块里,让小值块拿到更紧的 scale,总体从 4.31% 压回 2.56%——但前提是你的推理框架真的实现了逐通道 / 逐 token 缩放,而不是懒懒地用全局一个 scale。

局限:纯 JS 里,量化后的计算反而慢 34 倍

最后一个边界,专治「量化后算得更快」的另一种错觉。量化的计算收益来自 INT8 的 SIMD / 专用矩阵单元——只有 WebGPU、WASM SIMD 这类真有 INT8 执行单元的地方才成立。在普通浏览器 JS里没有这东西,量化反而要额外付出「量化存储 + 推理时反量化」的搬运成本。

同一组 100 万维向量,分别测 1M 维点积:原生 FP32 点积中位数0.514 ms,而「量化成 Int8 再乘 scale 还原」的点积中位数17.394 ms——慢了33.87 倍。换句话说,在纯 JS 路径上,INT8 既没省时间,还丢了精度。

诚实的边界清单:

  • 内存 4× 是确定的、无条件的,闭眼用。
  • 精度损失取决于分布,异常值多的激活张量主体会被毁掉近三成;必须上 per-channel / per-token 缩放才稳妥。
  • 计算加速只在有真 INT8 硬件时存在(WebGPU / WASM SIMD);纯 JS 里量化计算是负优化,别指望它提速。

结论与下一步

一句话方法论:INT8 量化省内存是白送的,但「精度够用」和「算得更快」都要看硬件和缩放策略——全局 scale 配异常值数据,是在用 95% 的小值精度补贴 5% 的 outlier。

落地时先把三件事对齐再动手:存储体积(确定 4×)→ 缩放策略(per-channel 保精度)→ 执行后端(有没有真 INT8 单元决定计算是否变快)。把「压到 1/4」当存储结论用,别当性能结论用。

开源地址

  • 矩阵门户:GitHub - wangzifan396-wzf/WB: nano-tools: 1310 single-file, zero-dependency, local-first web utilities in one portal. Offline and private, nothing leaves your browser. Binary & protocol parsers, crypto, dev, audio, visualization, productivity. · GitHub
  • 单文件工具聚合器:GitHub - wangzifan396-wzf/nano-workbench: Single-file tabbed launcher for the nano-tools matrix - one tab, 382 curated tools (of 1310), instant switching. Zero-dep. Part of nano-tools. · GitHub
  • GitHub 组织主页:wangzifan396-wzf (WangZi) · GitHub

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询