引言:量化在大模型落地中的位置
大模型(LLM)的推理部署面临一个核心矛盾:模型参数量动辄数十亿,以 FP16(每参数 2 字节)存储时,一个 70B 模型需要 140 GB 显存,远超消费级硬件能力。**量化(**Quantization ) 是解决这一矛盾的技术------它把浮点权重压缩成低位整数(如 INT4,每参数仅 0.5 字节),使显存占用降低 75%。
但量化并非简单地"把数字变短"。一个完整的量化体系涉及四个层面的问题:
- 可行性层面:模型凭什么能容忍量化带来的权重扰动?
- 机制层面:压缩后的整数权重如何参与实际计算?
- 误差层面:精度损失从何而来、如何控制?
- 表示层面:为什么浮点数在整条技术链路中不可替代?
这四个层面环环相扣,构成理解量化推理的完整知识框架。下文将依次展开。
第一部分:量化可行性的基础 ------ 模型鲁棒性与泛化能力
1.1 训练过程的本质:千万次扰动中的收敛
训练的每一步都在执行这样的操作:计算梯度 → 用梯度更新权重 → 检查损失是否下降。从数值角度看,每次梯度更新都是对权重的一次扰动:
每一轮训练:权重 W → W + 梯度扰动 ΔW → 损失下降 → 重复
训练结束时:模型在"被扰动了千万次"后,收敛到一个稳定解
训练好的模型,在训练过程中经历了海量扰动并能保持输出正确收敛------不稳定的权重配置在训练中会被淘汰。
1.2 泛化能力
泛化能力指模型在未见过输入 上的表现。它的数学本质是对输入扰动的不敏感性:
输入 x 稍微变化(x → x + δ,比如同义改写、错别字)
↓
模型输出基本不变(语义等价)
反过来,如果模型对输入的微小变化产生剧烈反应(输出完全不同),说明它学到的是"死记硬背训练样本"而非"底层规律"------即过拟合、泛化能力差。
1.3 输入扰动与权重扰动
泛化能力(对输入扰动不敏感)与鲁棒性(对权重扰动不敏感)之间存在深刻的数学联系。观察一层核心计算:
输出 = W × x
情况一:输入扰动 输出' = W × (x + δ) = Wx + Wδ
情况二:权重扰动 输出'' = (W + ΔW) × x = Wx + ΔW·x
两种扰动在数学上地位完全对称------都表现为输出中叠加一个小的误差项(Wδ 或 ΔW·x)。它们在计算图中属于同一类干扰。由此可以建立完整的逻辑链:
模型泛化好
⇔ 对输入扰动不敏感(输入变一点,输出语义不变)
⇔ 计算路径对"乘法中的小扰动"不敏感
⇔ 对权重扰动也不敏感(同一类数学干扰)
⇔ 量化的微小舍入误差可以被容忍
反向推论:
模型连 1% 权重扰动都受不了
→ 说明它的计算路径对任何小扰动都极度敏感
→ 那么输入稍微变化,输出就会剧变
→ 泛化能力差
1.4 边界情况
需要强调的是,上述论证是经验性的主流观点,而非严格数学定理。实践中确实存在"泛化好但对量化敏感"的模型------典型情况是某些层存在离群权重(outliers),这些个别的大数值权重对整体 scale 的确定产生扭曲,导致其余大量小权重精度崩塌。
正因如此,业界发展出了 GPTQ、AWQ 等精细补偿算法:GPTQ 通过逐列量化并补偿误差传播,AWQ 通过识别并保护重要权重通道,来处理这些特殊情况。但大方向始终成立:量化可行性的根基,就是模型在训练中获得的扰动稳定性。
第二部分:量化模型的运行机制 ------ 反量化的完整流程
2.1 量化与反量化
先建立形式化基础。量化与反量化是一对互逆操作:
量化(离线,一次性):
q = round(w / scale) + zero_point
反量化(在线,每次计算):
w' = (q - zero_point) × scale
其中:
-
w:原始浮点权重
-
q:量化后的整数值
-
scale:缩放因子(浮点数)
-
zero_point:零点偏移(整数)
两个参数的分工:scale 控制刻度密度 (决定量化精度),zero_point 移动原点位置 (处理非对称分布)。当权重分布关于零对称时,zero_point = 0,公式退化为纯缩放。
2.2 Weight-only 量化的核心设计:逐块即用即弃
以W4A16 方案(权重 INT4、激活 FP16)为例,理解 INT4 权重如何参与计算。首先需要纠正一个常见误解:
把整个模型所有 INT4 权重 → 全部还原成 FP16 → 存在显存里 → 推理
(那不就白压缩了吗?显存占用回到原点!)
✅ 实际做法:
计算到哪一层、哪个矩阵块,才还原哪个块
用完立即丢弃还原结果,显存中始终只保留 INT4 原始存储
2.3 完整数据流:反量化发生在计算单元内部
以计算 输出 = X × W(X 是激活值,W 是 INT4 权重矩阵)为例,完整流程如下:
【显存中】 【计算单元(GPU 核心/缓存)】
W_int4(INT4 存储 ──读取──→ ┌─────────────────────┐
scale, zero_poin ──读取──→ │ ① 取出一小块权重 │
│ (如 64×64 的块) │
│ ② 反量化: │
│ w = (q - z) × s │
│ INT4 → FP16 │
│ ③ 与激活值做 FP16 矩阵乘│
│ ④ 结果(FP16 激活值) │
└──────────┬──────────┘
│
▼
结果进入下一层计算
(还原的 FP16 权重块被丢弃!)
权重仅在 GPU 片上(寄存器/共享内存)按需反量化为高精度参与计算,用完即弃、不回写显存,因此显存占用始终维持在 INT4 水平。
2.4 设计动机:推理的瓶颈是访存而非计算
理解上述设计,需要认识 LLM 推理的性能特征:
Weight-only(如 W4A16)的收益与代价:
✅ 收益:
-
显存占用:INT4 = 每参数 0.5 字节,比 FP16(2字节)省 75%
-
显存带宽:推理是"访存受限"的(读权重占大头),权重小 4 倍
→ 读取速度快 4 倍 → 推理速度大幅提升
(尤其 batch 小时,瓶颈就是搬权重,而不是算)
❌ 代价:
-
计算仍是 FP16 矩阵乘 → 没有用上 INT8 Tensor Core 的算力加速
-
反量化本身有少量开销(但远小于省下的访存时间)
LLM 推理(尤其单条请求、小 batch 场景)的瓶颈是把权重从显存搬运到计算单元,而不是计算本身。INT4 让搬运量缩小 4 倍。
2.5 硬件层面的优化:反量化融合进专用指令
现代 GPU 对 Weight-only 推理有专门优化(如 NVIDIA 的 W4A16、Marlin 算子):
优化前(朴素实现):
读取 INT4 → 通用指令反量化 → 存回 → 再读取 → FP16 乘法
(多次访存,慢)
优化后(融合算子):
一条流水线内完成:读 INT4 → 片上反量化 → 直接进乘法单元
(数据不过多绕路,反量化几乎"免费")
实际部署中,反量化的开销被硬件设计压到极小,几乎可以忽略。
2.6 推理时必须反量化的原因
INT4 整数不能直接参与浮点矩阵乘,这是反量化不可省略的根本原因:
推理一层的核心计算:输出 = 激活值(X) × 权重(W)
X 是浮点向量(如 0.21, -0.35, ...)
W 存的是整数(如 123, -45, 67, ...)
问题:浮点数 × "编号整数" = 毫无意义的数字!
123 不是权重值,它只是"权重值除以 scale 后的编号"
必须先把编号翻译回真实权重值:
123 → 123 × 0.001 = 0.123 ← 这才是权重
然后才能做 0.21, ... × 0.123, ... 的矩阵乘
一个贴切的类比:收到一份用密码本加密的菜谱("盐:3 号刻度"),做饭时必须对照刻度表换算成真实克数(3 号 = 5 克),才能下锅。反量化就是推理时的**"** 实时翻译 " ------只要模型在运行,它就持续发生。
2.7 量化过程 vs 量化模型的推理
阶段一:量化过程(离线,一次性)
├── 输入:FP16 原始模型 + 校准数据
├── 做的事:统计分布 → 确定量化参数 → 把权重转成 INT4
└──输出:量化模型文件(INT4 权重 + scale/zero_point 参数)
阶段二:量化模型的推理(在线,每次用户请求都运行)
├── 输入:INT4 权重 + scale 参数 + 用户输入
├── 做的事:每层计算时,反量化权重块 → FP16 矩阵乘 → ...
└── 反量化在每次推理的每一层中发生
2.8 量化参数的生命周期
【量化阶段】(离线,一次)
FP16 权重 + 校准数据
↓ 统计分布、确定 scale/zero_point
↓ 权重 → INT4
↓
量化模型文件:
┌────────────────────────────┐
│ INT4 权重数据 │ ← 压缩的主体
│ 每块对应的 scale/zero_point │ ← 关键附件,永久保存
└────────────────────────────┘
【推理阶段】(在线,每次请求)
加载模型:INT4 权重 + scale 参数 一起进显存
↓
每层计算:
读 INT4 块 + scale参数 → 片上反量化 → 计算 → 丢弃还原值
第三部分:量化误差分析
3.1 误差的来源:量化
反量化本身不产生新误差,误差在量化过程中产生:
量化(产生误差):
w = 0.12345678 → q = round(w/scale) → 还原 w' = 0.1234
↑
误差 0.00005678 已定死
反量化(无新误差):
w' = q × scale + zero_point × scale
↑
严格按公式算,q 是什么,w' 就精确是什么
反量化是确定性运算,不"再损失"什么
需要区分两个层面的表述:
准确表述:
-
反量化不产生"新"误差
-
但它还原出的是"量化误差已包含在内"的近似权重
-
推理全程用的都是这些近似权重 → 量化精度损失由此而来
也就是说,还原出的 w' = 0.1234 不等于原始值 w = 0.12345678。误差在量化时就已"烧录"进 INT4 值里,反量化只是忠实地把这个"带误差的值"表达出来。精度损失是一次性的、在量化时刻确定的,之后不再扩大。
3.2 对称与非对称量化下的公式形态
当权重分布关于零对称时,采用对称量化,zero_point = 0:
对称 INT4:q ∈ {-8, ..., +7}
反量化:w' = q × scale ← 没有偏移项,纯乘法
非对称量化时(zero_point ≠ 0):
反量化:w' = (q - zero_point) × scale ← 多一次减法(等效于加偏移)
对称量化下的反量化极其简洁------就是 整数 × 缩放因子,一个乘法完成。非对称量化多一个偏移运算,但也只是一次减法,开销可忽略。
3.3 用具体数字完整走一遍流程
以一个权重块为例,scale = 0.001(量化时算好并存储):
【量化时】(一次性,离线完成)
原始权重:0.12345678
q = round(0.12345678 / 0.001) = round(123.45678) = 123
存储:q = 123(INT4/INT8 形式)+ scale = 0.001
【推理时】(每次计算该块都执行)
读取 q = 123 和 scale = 0.001
反量化:w' = 123 × 0.001 = 0.123
用 0.123 参与矩阵乘法
(原始值是 0.12345678,误差 0.00045678 ------ 量化时就定了)
3.4 W8A8 全量化方案
Weight-only 方案中反量化是每层每块的核心步骤,但 W8A8(权重和激活都量化)方案中,反量化的位置和角色完全不同:
W8A8(权重和激活都是整数)的推理:
激活值 X 也被量化成 INT8
↓
INT8 × INT8 → 直接做整数矩阵乘(硬件整数单元,无需反量化)
↓
结果(INT32 累加)→ 一次性反量化回浮点 → 进入下一层
这种方案下:
-
矩阵乘内部全程整数,不需要"边算边还原权重"
-
反量化只在每层输出时做一次
-
这就是 INT8 Tensor Core 加速的原理
两种方案的对比:
| 方案 | 推理时权重的处理 | 反量化的角色 |
|---|---|---|
| W4A16 / W8A16 (Weight-only) | 每块计算前临时还原为 FP16 | 核心步骤 ,每层每块都在做 |
| W8A8 (全量化) | 保持 INT8,直接整数乘 | 仅在层输出时做一次(还原结果) |
第四部分:量化为什么不能把所有运算全部整数化
4.1 动态范围爆炸
(1)整数的缺陷:固定步长
INT8(无符号):0 ~ 255,共 256 个刻度,每个刻度之间的间隔固定为 1
要表示 0.001 和 10000:
-
0.001 → 必须四舍五入到 0(丢失!)
-
10000 → 超出 255 上限(溢出!)
浮点数(FP16):约 6×10⁻⁵ ~ 65504,刻度间隔自动变化
小数附近刻度密,大数附近刻度疏
浮点数的本质结构是 尾数 × 2^指数------
指数部分让"小数点位置浮动",用有限的位数覆盖巨大的动态范围。
(2)神经网络中间值的动态范围
(量级数字为示意,实际受 hidden size 和归一化影响)
如果全程整数运算且刻度固定,每层乘法的结果会指数级增长或缩小:
模拟全程 INT8 运算(假设 scale 固定、结果存回 INT8):
第 1 层输出:量级 10² → 勉强装下
第 2 层输出:量级 10⁴ → 存回 INT8 已溢出
第 3 层输出:量级 10⁶ → 连 INT32 累加器都快满了
第 4 层输出:量级 10⁸ → 彻底崩溃
反向情况(权重 <1):
第 1 层:10⁻²
第 2 层:10⁻⁴
第 3 层:10⁻⁶ → 沿用固定 scale,比最小刻度还小 → 全部变成 0
INT8 只有 256 个刻度,全程整数运算意味着刻度间隔必须固定。而网络中间值跨越 6~8 个数量级,固定刻度要么在大数处溢出,要么在小数处归零。
(3)浮点数:指数自动"变焦"
FP16 表示 0.0001:
尾数 1.6 × 2⁻¹⁴ → 指数部分记录"小数点在很后面"
表示 10000:
尾数 1.2 × 2¹³ → 指数部分记录"小数点在很前面"
同一套 16 位,动态范围跨度约 9 个数量级
每个数量级内保持约 3 位有效数字精度
4.2 非线性运算无法整数化
矩阵乘只是网络计算的一部分,Softmax、LayerNorm、激活函数等非线性运算占据大量计算量,且除法、指数、开方------在整数体系会产生精度崩塌。
4.3 误差累积
假设单层的量化误差(INT8)为约 ±0.5% 相对误差,逐层累计后:
第 1 层误差:0.5%
第 2 层误差:0.5% + 0.5%(新误差)+ 第1层误差的传播
...
第 32 层误差:≈ 32 × 0.5% ≈ 16%(粗略估计)
4.4 刻度分布
量化公式:q = round(w / scale) + zero_point
反量化:w' = (q - zero_point) × scale
结构上看:
-
scale:缩放(改变刻度密度)
-
zero_point:偏移(移动原点位置)
从代数结构看,是可以用整数来表述,但解决不了****" 刻度分布 " 问题
整数系统:刻度均匀分布
0, 1, 2, 3, ..., 255
相邻刻度间隔恒为 1
神经网络权重的分布大量集中在小范围(如0.05)内,同时存在少数的离群值(3)
整数存在均匀刻度的困境:
-
刻度对准 0.05 附近 → 离群值 3 溢出
-
刻度对准 3 → 0.05 附近的权重全挤在几个刻度里,精度崩塌
浮点数本身具有 刻度不均匀的特性(小数值处刻度密,大数值处刻度疏),匹配"大量小权重 + 少数大权重"的分布:
FP16 刻度分布示意:
0.001 附近:0.001, 0.0011, 0.0012, ...(间隔 0.0001)
100 附近: 100.0, 100.0625, 100.125, ...(间隔 0.0625)
精度自动分配给"最需要的地方"
( 3 )权重分布的成因
训练中的正则化(weight decay)持续惩罚大权重
↓
权重分布收敛为近似高斯分布(钟形,中心在 0)
↓
绝大多数权重是小值 → 浮点"小数密刻度"完美匹配
训练中的 weight decay 正则化持续惩罚大权重,使权重分布收敛为近似高斯分布(钟形、中心在零)。绝大多数权重是小值------浮点的"小数密刻度"与这一天然分布完美匹配。
| 维度 | W4A16 ( Weight-only ) | W8A8 (全量化) |
|---|---|---|
| 权重存储 | INT4(省 75%) | INT8(省 50%) |
| 激活值 | FP16 | INT8(动态量化) |
| 矩阵乘 | FP16 | INT8 整数乘 |
| Softmax/Norm | FP16 | FP16 |
| 层间传递 | FP16 | FP16(反量化→再量化) |
| 误差 | 单次、固定 | 每层累积(校准压制) |
| 硬件加速 | 有限 | INT8 Tensor Core |
| 现实可行性 | ✅ 消费级主流 | ✅ 数据中心主流 |
消费级显卡(显存小、带宽有限):
→ W4A16:显存省 75% 是生死线,算力不是瓶颈
→ llama.cpp / GGUF 格式的核心路线
数据中心(算力贵、吞吐量大):
→ W8A8:INT8 Tensor Core 吞吐翻倍,显存充足
→ vLLM / TensorRT-LLM 的主流配置
消费级场景的瓶颈是显存容量与带宽, Weight-only 量化直击这一痛点;数据中心场景的瓶颈是算力吞吐,全量化用 Tensor Core 整数单元换取算力翻倍。 两条路线都是"在误差可控前提下,针对各自瓶颈的最优解"。
模型鲁棒性(训练中千万次扰动筛选出来的稳定性)
↓ 支撑了
量化可行性(INT4/INT8 舍入误差可被容忍)
↓ 依赖于
反量化机制(片上逐块"即用即弃"的实时翻译)
↓ 依赖于
量化参数永久存储(scale/zero_point 是模型终身组件)
↓ 但受限于
数值表示的根本约束
├── 整数:固定刻度 → 动态范围装不下、非线性算不动、误差复利
└── 浮点:指数变焦 → 小数密、大数疏的自适应刻度
→ 匹配神经网络"大量小值+少数大值"的天然分布