模型部署圈里,量化是被提到最多的词之一,但也是最容易被误解的。很多人觉得量化就是把权重从 FP32 换成 INT8,跑起来变快就行,结果遇到精度崩、某些算子死活不加速、甚至同一份 ONNX 在不同后端上跑出完全不同的延迟,才开始怀疑自己是不是用错了方式。这一篇把量化从数学原理到工程落地完整讲透——INT8 矩阵乘到底快在哪、校准是怎么做的、QAT 和 PTQ 该怎么选、大模型量化为什么不能直接套用小模型的经验。内容偏实践,适合正在做模型部署、想把推理延迟和显存压下来,但还没系统过一遍量化底层的朋友。
1. 量化到底在解决什么问题:算力、带宽与显存的三角博弈
很多人一上来就盯着 INT8 矩阵乘的加速倍率,但量化真正的价值要看你的模型到底卡在哪。我从三个维度来说:算力、内存带宽、显存容量。
1.1 算力维度:INT8 为什么比 FP32 快
先说算力。以 NVIDIA A100 为例,FP32 的峰值算力是 19.5 TFLOPS,而 INT8 的 Tensor Core 算力是 624 TOPS,差距在 30 倍以上。数字很吓人,但这是峰值,实际上很难跑满。INT8 的 Tensor Core 在单位时钟周期内能处理更多乘累加操作,这是硬件层面的设计。GPU 里的 Tensor Core 本来就是为矩阵乘设计的专用电路,INT8 模式下用更小的数据宽度换取更高的并行度,类似把一条四车道改成了八车道,车道变窄了但车流量翻倍。
但算力只是天花板,你实际能摸到多高,取决于数据能不能喂得够快。这就是带宽问题。
1.2 带宽维度:量化省下的是搬运成本
推理过程中,计算单元需要不断从显存里读权重和激活值。假设一个模型有 1B 参数,FP32 下权重占 4GB,每次前向都要把这些数据从显存读到计算单元。如果转成 INT8,权重变成 1GB,搬运量直接砍掉 75%。
这里的关键是:很多模型是memory-bound的,不是compute-bound的。尤其是小 batch 推理、实时流式输出这类场景,计算单元大部分时间在等数据。你算得再快,数据搬不过来也没用。量化真正解决的问题,在这里是减少数据搬运的字节数。
我把这个类比成搬家和做饭的关系:算力是你厨房的灶眼数量和厨师的手速,带宽是你从冰箱到灶台的动线长度。量化相当于把冰箱搬到灶台旁边,你用不着一次搬一大块肉再切,而是直接把切好的小方块放在手边。切配好的肉可能精度上稍有损失,但出菜速度质变。
1.3 显存容量维度:塞下更大的模型
第三个维度是显存占用。这个最直观,8GB 显存装不下 FP16 的 7B 模型,但 Q4 量化后可能就能跑。这对本地部署大模型是刚需。蒸馏和剪枝是改变模型结构来省资源,量化是改变数据表示,两者是独立的优化方向,可以叠加。
不过我得泼一盆冷水:量化不是银弹。如果你的模型本身是 compute-bound 的,而且已经用了 FP16 且算力没吃满,换成 INT8 不一定能等比例提速,有时甚至会因为额外的量化/反量化算子开销变得更慢。我在实际项目里见过很多次:一个很小的分类模型,输入输出都是 FP32,强行套 INT8 量化,每一层都在做 quantize-dequantize,最后端到端延迟反而涨了 30%。所以动手之前先 profile,看看你的模型瓶颈在哪。
2. 量化数学本质:把连续实数塞进有限整数格点
聊完为什么要做量化,接下来看看量化在数学上到底做了什么。简单说,就是找一个映射关系,把 FP32 的连续数值空间压缩到 INT8 的离散整数空间。这个映射关系做得好不好,直接决定精度损失有多大。
2.1 对称量化与非对称量化
最常见的对称量化公式是:
x_int = clamp(round(x / scale), -128, 127)其中 scale = max(|x|) / 127。反量化是 x_dequant = x_int * scale。
这个方案假设数值范围从 -max 到 +max 是对称的,实现最简单,INT8 的零点正好是整数 0,后续计算不需要处理零点偏移。但问题在于,如果你的激活值分布明显偏向正数(比如 ReLU 的输出全是非负),对称量化会浪费掉负半轴的表示能力,只用一个阈值 max,精打细算的话不太划算。
非对称量化引入一个 zero_point:
x_int = clamp(round(x / scale) + zero_point, 0, 255)配合 uint8 使用,能充分利用非负分布的动态范围。代价是矩阵乘里多了一个偏移项的计算,稍微增加一点算子复杂度和推理开销。所以很多框架默认用对称量化处理权重(权重分布往往近似对称),激活值按需选对称或非对称。
2.2 Per-tensor 与 Per-channel:量化粒度快慢权衡
量化粒度是另一个重要维度。per-tensor 是整个张量共享一个 scale,计算最简单,但遇到通道间数值范围差异大的张量时,小数值通道的精度会被大数值通道拖累。
per-channel 是每个输出通道或每个输入通道单独一个 scale。拿卷积来说,常见做法是权重按输出通道单独算 scale,激活值由于是运行时动态的,通常只能 per-tensor 或按 token 统计。per-channel 精度更好,但某些硬件后端不支持,或者会引入额外的 repack 开销。
我测试过一个 YOLOv8 检测模型,per-tensor 权重量化后 mAP50 从 0.52 掉到 0.47,改成 per-channel 后恢复到 0.51。这个差距非常典型:小数值通道被大数值通道的 scale 钳制,是权重 per-tensor 量化最常见的精度损失来源。
2.3 INT8、FP8、BF16、FP16、FP32:算力需求与精度分布差异
这里我把常见的几种数值格式放在一起对比一下。
| 格式 | 位宽 | 类型 | 动态范围 | 适用场景 | 典型加速 |
|---|---|---|---|---|---|
| FP32 | 32 | 浮点 | 极大 | 训练、精度基准 | 1x |
| FP16 | 16 | 浮点 | 大 | 训练 / 推理 | 约 2x |
| BF16 | 16 | 浮点 | 与FP32相同 | 训练 / 大模型推理 | 约 2x(带宽减半) |
| INT8 | 8 | 定点 | 有限(需要 scale) | 推理加速 | 约 4x-30x(Tensor Core) |
| FP8 | 8 | 浮点 | 大但尾数短 | 训练 / 推理前沿 | 接近 INT8 但精度更稳 |
INT8 是定点数,格点均匀分布;FP8 是浮点数,在小数值附近精度高、大数值附近精度低。FP8 更适合动态范围跨度大的场景,比如激活值和梯度的混合精度训练,但硬件生态还在普及中。BF16 本质还是浮点,它能省带宽但不直接利用整数 Tensor Core,所以“BF16 和 INT8 哪个快”的答案是:取决于算子。速率上 INT8 单纯峰值更高,但 BF16 无需量化校准,风险低很多。
注意:量化精度损失的本质是格点间距变大。FP32 转 INT8 相当于把一个连续数轴压缩到 256 个格点。格点怎么放、范围怎么截断,是量化方案设计的核心。
3. INT8 矩阵乘的硬件加速原理:VNNI、DP4A 与算子融合
现在进入标题里最硬核的部分:INT8 矩阵乘到底为什么快。我之前看过很多人给出了 INT8 加速的结论,但很少能说清硬件层发生了什么。这里拆开讲。
3.1 从浮点乘加到整数乘加:一个指令处理多个数据
传统 FP32 矩阵乘,每个乘加运算需要一个浮点单元。GPU 的 CUDA core 是做浮点运算的主力,整数乘加需要一个不同的数据通路。INT8 加速的核心是硬件增加了针对低精度整数的专用指令。
以 Intel 的 VNNI(Vector Neural Network Instruction)和 NVIDIA 的 DP4A(Dot Product of 4 8-bit Integers and Accumulate)为例:一条指令可以把 4 个 INT8 乘法结果累加到一个 INT32 累加器。CPU 侧的 VNNI 类似,一条指令执行 4 个 INT8 乘加。这意味着单位时钟周期内可以处理的数据量是原来的数倍,而且用的是独立的硬件单元,不占用浮点通路。
这个在工程上的意义是:如果你的 AI 推理跑在 GPU 上,Tensor Core 本身就是为低精度矩阵乘设计的,INT8 走的是专门的高速通道;跑在 CPU 上,支持 VNNI 的芯片可以用整数指令加速卷积和全连接层,常见的有 Intel 的 AVX512 VNNI 和后续 AMX 扩展。
3.2 避免频繁反量化:偏置计算留在 FP32
矩阵乘在 INT8 域里完成,但偏置项一般是 FP32。如果在 GPU 上把偏置也强行量化为 INT8,精度会明显下降,因为偏置的动态范围可能和权重不在一个量级。工程上的做法是:输入和权重量化为 INT8,乘累加用 INT32 中间表示,最后再加上 FP32 偏置,然后反量化回 FP32。
也就是说,完整的 INT8 GEMM 流程是:
INT8 输入 * INT8 权重 -> INT32 累加 -> 加 FP32 偏置 -> 反量化到 FP32 -> 传给下一层关键点是:反量化不是每一层都做。如果你的推理框架支持 QDQ(Quantize-Dequantize)节点融合,可以在连续多个算子之间保持 INT8 域的传播,只在必要的边界(例如残差连接、归一化层输出)转回 FP32。我在 TensorRT 里见过一个 ResNet-50 的案例,开启 QDQ 融合后,INT8 卷积组之间直接传递 INT8 数据,延迟比每层都来回 quantize-dequantize 的版本快了 60%。
3.3 数据布局对 INT8 矩阵乘的影响
数据布局经常被人忽略,但影响非常大。INT8 的矩阵乘对数据排列敏感,一方面是因为硬件指令要求对齐(例如 32 字节对齐),另一方面是卷积的 im2col 变换在高维数据上的存储效率差异。常见的影响是,一个 NCHW 的量化模型在 CPU 上跑,NHWC 的数据布局往往更快,因为通道维度的连续存储能更好地利用矩阵乘的访存模式。
实际项目里,我一般会先用框架默认布局跑一版,然后用 profile 工具看内存访问的命中率。很多 INT8 性能不达标不是算子本身的问题,而是布局导致大量 cache miss,读写路径上浪费时间。
4. 校准:PTQ 工作流中的关键环节
校准(Calibration)是量化从理论走向实践的第一步。量化需要 scale 和 zero_point,这两个参数怎么定?校准的过程就是统计模型在真实数据上每一层的激活值范围,然后确定最合适的数值映射。校准做得好不好,直接影响量化后模型的精度。我自己经常把它比作给新员工设定业绩指标的取值范围:取太窄了,好员工直接被当异常值裁掉;取太宽了,大多数人的差距在标准化后变得无意义。
4.1 校准数据集的构建:别太贪心也别太随意
校准数据集首先要有代表性。一般取验证集的子集,500 到 1000 张图片或 1000 条文本足够。不需要打标签,因为校准不计算 loss 更新梯度,只统计激活分布。数据要尽量覆盖真实部署场景中的分布,但不要把训练集里所有类型都塞进去。太杂的校准集会拉伸动态范围,让 scale 变大,小数值激活的精度反而受损。
一个常见的坑是:有人用纯色图或全零输入做校准,结果所有层的激活分布都集中在一个很小的区间,量化后的模型在真实数据上精度崩得一塌糊涂。校准数据的分布必须和部署场景匹配,而不是数据数量越多越好。
4.2 常用的校准方法:MinMax、Percentile、KL 散度、MSE
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| MinMax | 直接用校准集的 min/max 作为量化范围 | 简单,快的夸张 | 对离群值极度敏感 |
| Percentile | 取分布的某个百分位点(如 99.99%) | 抗离群值,实现简单 | 百分位需要调参 |
| KL 散度 | 按信息熵最小原则截断分布 | 保留了信息量大的区间 | 计算开销略大,TensorRT 老版本默认方式 |
| MSE | 让量化前后数值误差的均方差最小 | 精度表现稳定 | 要做阈值搜索,计算稍大 |
实际经验是:当模型激活分布比较规整(比如 BN 层做过归一化),MinMax 和 Percentile 就够用了。当输出特征跨度很大、偶尔出现离群特征时,MSE 或 KL 效果更好。我在量化一个目标检测模型的 neck 特征时,MinMax 一律出现过 mAP50 掉 8% 的惨案,把校准方法换成 MSE,差距缩到 1% 以内。
校准的伪代码长这样:
def calibrate(model, calib_loader, method="mse"): model.eval() # 注册 hook 收集每层激活值 activations = collect_activations(model, calib_loader) scale_map = {} for layer_name, act_tensor in activations.items(): # act_tensor: 形状 (num_samples, channels, ...) # 统计全局最小值/最大值 min_val = act_tensor.min() max_val = act_tensor.max() if method == "mse": # 在候选 scale 中搜索最小化量化误差的那个 best_scale = search_scale_by_mse(act_tensor, min_val, max_val) elif method == "percentile": p = torch.quantile(act_tensor.abs(), 0.9999) best_scale = p / 127.0 else: best_scale = max(max_val, -min_val) / 127.0 scale_map[layer_name] = best_scale return scale_map这只是一个框架层面的示意。实际工程中,范围搜索可以更精细,例如在 FP32 推理结束后用 KL 散度选最合适的截断点,再得到最终的 scale。
4.3 校准和训练的区别:别把灯开错地方
校准过程不更新权重,所以不需要反向传播,更不会修改模型参数。很多人第一次用 PTQ 时以为要跑几轮训练,这是误解。校准只是“统计”,不是“学习”。
但也正因为校准不更新权重,PTQ 的精度天花板有限。如果校准之后精度仍然达不到要求,就得准备上 QAT 了。校准做的再精细,也只是把表示范围调到最合适,模型参数本身的容错能力并没有提升。
5. QAT 量化感知训练:伪量化、直通估计器与训练细节
QAT(Quantization-Aware Training)的思路是:让模型在训练过程中就“感知”到量化误差,通过微调权重来适应量化带来的噪声。它在模拟量化的条件下做前向和反向传播,使得模型学到的权重分布对量化更鲁棒。
5.1 伪量化:模拟量化误差,就像给模型带上“近视眼镜”训练
伪量化(Fake Quantize)是 QAT 的核心工具:前向传播时量化为 INT8 再反量化回 FP32,让模型吃到量化带来的误差,但实际存储的权重仍然是 FP32。就像一个人戴着近视眼镜打篮球,训练时能看到模糊的篮筐,比赛时才不会被模糊的视野吓到。
在 PyTorch 里,torch.quantization.FakeQuantize就是干这个的。它在前向时把 FP32 张量映射到离散的整数格点再映射回来,效果是让训练时的数值分布接近真实部署时的分布。
我画一个最简化的伪量化实现:
class FakeQuantize(torch.autograd.Function): @staticmethod def forward(ctx, x, scale, zero_point, qmin, qmax): # 量化到整数域,再反量化回到浮点域 x_int = torch.round(x / scale) + zero_point x_int = torch.clamp(x_int, qmin, qmax) x_q = (x_int - zero_point) * scale return x_q @staticmethod def backward(ctx, grad_output): # 直通估计器:梯度直接穿过 return grad_output, None, None, None, None这里的 backward 就是 QAT 能跑起来的关键:round和clamp都是不可导的操作,但我们假装它们是“直通的”——梯度直接穿过,不做任何修改。这就是直通估计器(STE,Straight-Through Estimator)的核心思想。这个技巧是工程上的妥协,因为量化本身是离散操作,数学上导数不存在,但为了能训练,只能让梯度直接流过量化节点。
5.2 QAT 训练流程:先预训练,再微调
QAT 不是从零训练一个模型。常规做法是:
- 先在 FP32 下正常训练或加载预训练权重。
- 插入伪量化节点(等价于给模型加“量化噪声”)。
- 用小学习率微调几个 epoch,让模型重新适应量化带来的扰动。
- 微调结束后,把伪量化节点移除,替换成真正的量化算子导出。
为什么不能直接从头 QAT?一方面是从头训练的成本太高,另一方面是伪量化节点引入的梯度噪声会让训练早期不稳定。实践经验是:加载 FP32 预训练权重之后,学习率降到原来的 1/10 到 1/100,微调 5 到 10 个 epoch 就足够。
我踩过的一个坑是:微调阶段没有冻结 BN 层的统计量。BN 层在推理时用的是滑动平均的均值/方差,训练时用的是 batch 内的统计量。如果微调时不冻结 BN,训练时模型看到的是 batch 统计量下的“假分布”,部署时切换成全局统计量,量化后的误差会突然变大。做法是微调阶段把 BN 层设为 eval 模式,或者使用联合统计。
5.3 QAT 和 PTQ 怎么选:精度兜底还是成本优先
| 维度 | PTQ | QAT |
|---|---|---|
| 所需数据 | 少量无标签校准集 | 需要训练数据和完整微调流程 |
| 耗时 | 分钟级别 | 小时到天级别 |
| 精度 | 可能损失较多 | 通常能接近 FP32 |
| 适用场景 | 快速部署、模型本身鲁棒性较好 | 小模型、精度要求高、量化后掉点严重的场景 |
实践中,如果一个模型的 PTQ 掉点超过 2%-3%,我会优先考虑先做一层“混合量化”——不对 embedding 和首尾层做量化,看精度影响。还不行的再上 QAT。热词里提到的 “resnet34 剪枝量化全部流程” 就是一个典型例子:剪枝之后模型的鲁棒性会下降,这时候直接 PTQ 往往崩得惨,加一步 QAT 微调能明显拉回来。
6. LLM 量化:离群值、SmoothQuant、AWQ、GPTQ 与 GGUF 选型
大模型量化是最近两年部署领域最热门的方向,也是量化工程化挑战最大的场景。LLM 和小模型在数值分布上有个本质差异:激活值里存在明显的离群值(outlier)。少数几个维度上的激活值比其他维度大出好几个数量级,这让量化的动态范围选择变得极其困难。用一句话概括:你为了让那 1% 的离群值不丢信息,把 99% 的正常值都挤进了一个狭小的格点区间。
6.1 为什么 LLM 量化难:离群值把动态范围撑爆了
在 LLM 的隐藏层激活中,少数通道的数值可能达到几十甚至上百,而大部分通道的数值集中在 -1 到 1 之间。如果按最大绝对值确定 scale,量化步长会被拉得很大,正常值部分只有几个格点可以表示,精度损失等于把高分辨率照片压缩成 4 色 GIF。
离群值的出现是有结构的:它们集中在特定的特征通道上。针对这个观察,诞生了两种代表性思路:一种是 SmoothQuant,把激活的离群值吸收到权重里;一种是 AWQ,从模型权重角度识别重要通道,做保护性量化。
6.2 SmoothQuant:把激活的“重担”转移给权重
SmoothQuant 的核心思想是:激活难量化,权重相对好量化。既然权重分布比激活更均匀,那就通过一个通道维度的缩放因子,把激活的离群幅度“平滑”一部分到权重上。
数学上,对线性和注意力层:Y = XW,其中X是激活,W是权重。可以改写成Y = (X · diag(s)) · (diag(s)^{-1} · W)。s是通道维度的缩放因子。通过调节s,让激活的动态范围变小、权重的动态范围变大,降低激活量化的难度而不过度损伤权重量化。这个迁移比例alpha是超参数,通常在 0.5 左右表现最好。
6.3 AWQ:不重训,只挑重要通道重点保护
AWQ(Activation-aware Weight Quantization)不需要反向传播,思路也比 SmoothQuant 更直接:先统计校准集上哪些通道的激活值幅度大,这些通道对应的权重在量化时分配更小的量化误差。
具体做法是对显著通道的权重乘以一个小的缩放因子(比如 absmax 的 0.01 次方),等效于在量化时挤压掉这些通道的数值范围,让量化步长更细。AWQ 的精度表现通常优于同级别的 GPTQ,而且不需要重训模型权重。
6.4 GPTQ:基于二阶信息的逐层重建
GPTQ 的思路是逐层最小化量化误差。它使用 Hessian 矩阵的近似信息,找到一组量化后的权重,让整层输出的误差尽可能小。做法很巧妙:把量化误差散布到剩余未量化的权重上,通过补偿机制减少累计损失。用大白话说,它像修瓷砖时发现一块砖铺错了,不是把那块砖单独敲掉重新铺,而是顺带调整周围几块砖的位置,让整体效果看不出差别。
GPTQ 对 4-bit 权重量化效果很好,是很多 7B/13B 模型 4bit 部署的基础方案之一。但它的校准过程需要一定的计算资源——主要是 Hessian 估计的内存开销,小内存环境下跑 70B 模型量化会有点吃力。我在量化一个 13B 模型时,用 24GB 显存的机器跑 GPTQ 校准,峰值显存占用到了 20GB 左右,差点爆掉。
6.5 GGUF 与 llama.cpp 生态:本地部署量化档怎么选
GGUF 是 llama.cpp 生态的模型格式,底层用了一种叫 k-quants 的混合量化方案。不同于常规的整层统一量化,k-quants 会把权重张量拆分成不同精度的小块,用不同的 bit 宽度表示。常见档位有 Q4_K_M、Q5_K_M、Q6_K、Q8_0 等。
我在选档位时的一般经验是:
- Q2 以下基本只用于技术尝鲜,聊天质量明显下降,不值得省那点空间。
- Q4_K_M 是目前“性价比”最高的档位,体积小、质量损失可控,适合日常使用。
- Q5_K_M 比 Q4_K_M 体积大约多 700MB(7B 模型),但在复杂推理和指令跟随上感觉更稳。
- Q8_0 几乎无损,但体积优势不明显,如果显存充足,这是稳妥之选。
- 如果跑长文本、执行代码、结构化输出等对细节敏感的任务,宁可多占用几百 MB 也要上 Q5 或 Q8。
GGUF 的文件头里保存了模型超参数,如果用了不匹配的量化档位或者context length设置不对,会出现加载报错、输出错乱、甚至 “clip 5120 与 4096 不匹配” 之类的维度对齐问题。这种问题通常不是量化本身的锅,是模型结构参数和推理端配置没对齐,排查时先检查n_ctx、n_embed的配置,别把锅甩给量化。
6.6 LLM 量化方案横向对比
| 方案 | 类型 | 是否需要微调 | 精度保留 | 部署难度 |
|---|---|---|---|---|
| GPTQ | PTQ 权重量化 | 否 | 优秀(4bit 时依然能打) | 中 |
| AWQ | PTQ 权重量化 | 否 | 优秀(4bit 表现不输 GPTQ) | 中 |
| SmoothQuant | PTQ 量化(权重+激活) | 否 | 好,适合 8bit 场景 | 低 |
| GGUF (k-quants) | PTQ 混合量化 | 否 | 取决于档位,Q5 以上很稳 | 极低,llama.cpp 一键用 |
| QAT / LoRA 量化微调 | 训练 | 是 | 最佳,但成本高 | 高 |
选型的核心逻辑很简单:如果只是本地跑聊天,GGUF 最省心;如果要接入生产服务,需要批处理、调优延迟,GPTQ 或 AWQ 在 vLLM 等框架里支持更完善;如果想压榨到极致同时保证精度,就要考虑 QAT 了。
7. 实战经验与常见问题排查实录
这一节整理我在多个项目里反复踩过的坑,以及排查思路。很多问题不是量化理论能直接告诉你的,必须在真实部署中摸一遍。
7.1 哪些层不要量化:embedding、LayerNorm、最后的分类层
Embedding 层的查表操作是离散索引映射,它的数值范围是词向量空间,整体分布和 Transformer 的激活分布差异很大。对 embedding 做量化,通常收益不高但风险大——一个 token 的错误映射可能直接改变语义。LayerNorm 和 Softmax 这类逐元素归一化算子,计算量占比不大,但数值敏感性极高,量化它们往往得不偿失。最后一层分类层直接决定 logits 的排序,轻微精度损失可能导致 Top-1 结果变化。
实际操作中,我倾向于只量化线性层和卷积层这些“重计算”的地方。Transformer 架构下,单层里量化 attention 的 QKV 投影和 FFN 的两个线性层就够了,其余保持 FP32。
7.2 INT8 vs BF16 vs FP16:快速选型总结
| 你的目标 | 推荐方案 | 理由 |
|---|---|---|
| 在 GPU 上压推理延迟 | INT8(权重+激活量化) | 可用 Tensor Core 整数通道 |
| 仅在 CPU 上省内存/带宽 | INT8(权重量化) | 带宽减半,但不一定提速 |
| 大模型、显存吃紧 | 4bit(GPTQ/AWQ/GGUF) | 显存占用降低 4 倍以上 |
| 训练/微调阶段混合精度 | BF16/FP16 | 不需要校准,精度稳 |
| 生产环境精度优先 | 先 PTQ 试水,不行再 QAT | 成本可控,精度兜底 |
需要特别说明的是 BF16 和 INT8 的关系。BF16 只是把尾数砍了、指数不变,它支持的范围和 FP32 一致,跑训练和推理不会出现 overflow/underflow 问题。但它本质上还是浮点格式,带宽省了,计算峰值不如 INT8。一些场景里,比如在 A100 上用 BF16 的 Tensor Core,延迟可能比 FP32 低一倍,但仍然不如纯 INT8 通道快。
7.3 量化后精度怎么评估:不能只看 loss
我在评估量化模型精度时,习惯搭一套“端到端输出对比”的白名单测试:用固定 prompt,对比 FP32 模型和量化模型在 logits、top-5 概率、输出文本的语义相似度上的差异。不要只盯着 loss 曲线,因为量化误差是非线性的,loss 接近不一定代表生成质量一致。
另一个经验是:量化模型的误差有累积效应。Transformer 里每一层的微小误差经过多层传递会被放大,尤其在长文本生成场景里。如果量化模型在短 prompt 上精度正常、长上下文下输出质量剧烈下滑,多半是激活值在长序列上动态范围变大导致的。此时把校准集换成带有长样本的混合集,通常能缓解。
7.4 常见问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 量化后精度掉点严重 | 校准集分布与真实部署不匹配 | 换校准集,扩大数据覆盖面 |
| 某些层速度反而变慢 | 反量化在每一层频繁发生 | 查看是否支持 QDQ 融合,检查算子是否落到 INT8 内核 |
| 显存占用没怎么降 | 只量化了权重,激活还是 FP32 | 做权重+激活量化,或者给长序列流式处理降缓存 |
| 模型输出乱码/维度对不上 | GGUF 配置与模型参数不匹配 | 检查 context length、embedding 维度等配置 |
| CPU 推理没有加速 | 芯片不支持 VNNI/AVX512指令 | 换成支持 VNNI 的 CPU,或用 GPU 推理 |
| 量化后过拟合严重 | QAT 微调学习率过大、epoch 过多 | 降低学习率,减少到 3-5 个 epoch |
7.5 最后再分享一个小技巧
量化方案不要做成全量开关。我一般先跑一版纯权重 INT8(激活保持 FP32)看看延迟收益和精度损失,再决定是否增加激活量化。很多时候仅权重量化就能省掉大量显存,而激活量化带来的额外收益有限、精度风险却翻倍。一个支持 fine-grained 配置的推理框架会让这个调优过程快很多。
我自己的习惯是在部署流程里把量化和精度验证写进同一个自动化脚本:模型导出后立刻跑白名单测试,产出对比报告。这样每次改动都能快速拿到“损失在哪里、瓶颈在哪里”的反馈,而不是模型量化完就丢到线上,等用户反馈才发现问题。这套流程能不能覆盖所有场景我不知道,但至少能保证每一次量化决策都有据可查。