☰
模型量化从FP32到INT8:PTQ与QAT原理及工程实践指南
2026/10/7 15:13:32 网站建设 项目流程

1. 模型量化到底在解决什么问题

第一次接触模型量化的人,脑子里冒出来的问题通常很朴素:模型跑得好好的,为什么非要折腾它?我刚开始做推理优化的时候也有这个疑问,直到把一个 7B 参数的模型往边缘设备上部署,才发现显存直接爆掉,推理延迟高得没法看。那一刻我才真正理解,量化不是学术圈的自娱自乐,而是把模型从实验室搬到真实产品里的必经之路。

先把概念说清楚。模型量化,本质上是把神经网络里原本用高精度浮点数表示的权重和激活值,转换成低精度的定点数或低比特浮点数。最常见的场景是把 FP32(32 位浮点)压到 INT8(8 位整数),压缩比直接是 4 倍,如果再激进一点做到 INT4,那就是 8 倍。这个压缩带来的好处是三重的:模型体积变小、内存带宽占用降低、在支持低精度计算的硬件上吞吐量显著提升。

但这里有个很多人一开始会误解的点:量化不等于简单的“砍精度”。它是一套有数学依据的映射方法,核心是把一个连续的浮点区间,映射到有限的整数格点上,并且尽量让映射后的计算结果逼近原始结果。做得好,精度损失可以控制在 1% 以内;做得糙,模型直接废掉。这中间的差别,就是 PTQ(Post-Training Quantization,训练后量化)和 QAT(Quantization-Aware Training,量化感知训练)这两大范式的价值所在。

这篇文章我打算把量化的底层逻辑从头捋一遍,从浮点数在计算机里怎么表示讲起,再到 PTQ 和 QAT 两条路线的取舍,最后落到实际工程里怎么选、怎么调。适合两类人看:一类是刚接触推理优化、想把模型部署到资源受限设备上的工程师;另一类是对数值表示好奇、想搞明白“为什么量化会掉点”的算法同学。不需要你有深厚的数值分析背景,但最好写过一点 PyTorch 或者 TensorFlow 的代码,这样看实操部分会更顺。

2. 浮点表示:理解量化的第一块基石

2.1 浮点数在计算机里是怎么存的

要搞懂量化,绕不开浮点数的存储结构。很多人用了好几年 FP32,却从来没认真看过它的位布局,结果在量化时对“动态范围”“精度损失”这些词只有模糊的感觉。我用最直白的方式拆一遍。

一个 FP32 浮点数占 32 个比特,被切成三段:1 位符号位(sign)、8 位指数位(exponent)、23 位尾数位(mantissa)。它表示的数值形式是:

value = (-1)^sign × 1.mantissa × 2^(exponent - 127)

符号位决定正负,指数位决定数量级(也就是这个数大概有多大),尾数位决定有效数字的精度(也就是这个数有多“细”)。这里有个关键认知:浮点数的精度是相对的,不是绝对的。同样是 FP32,表示 1.0 附近的数时,相邻两个可表示数之间的间隔大约是 2 的负 23 次方,也就是约 1.19e-7;但表示 1000.0 附近的数时,这个间隔就变成了约 1.19e-4。数量级越大,绝对精度越差。

这个特性直接解释了量化为什么难。神经网络里的权重分布往往跨越好几个数量级,大部分权重集中在 0 附近,但偶尔有几个“离群值”特别大。当你用一个统一的缩放因子把这些数全部映射到 INT8 的 [-128, 127] 区间时,那些小权重就会被压得几乎分辨不出来,这就是量化掉点的根源之一。

2.2 FP16、BF16 与 INT8 的差异对比

理解了 FP32,再看其他格式就顺了。FP16 把指数位砍到 5 位、尾数位砍到 10 位,动态范围大幅缩小,最大只能表示到约 65504,训练时很容易溢出,所以现在训练里更流行 BF16。BF16 的思路很聪明:它保留和 FP32 一样的 8 位指数(动态范围不变),只把尾数砍到 7 位。这样虽然精度低了,但不会溢出,训练稳定性好很多。

而 INT8 是完全不同的物种。它没有指数位,就是一个纯粹的整数,取值范围固定是 [-128, 127] 或者 [0, 255]。它本身没有“小数点在哪”的概念,小数点位置是靠一个额外的缩放因子(scale)和零点(zero point)来约定的。这就是量化的核心机制:用 scale 和 zero point 把浮点区间线性映射到整数区间。

格式总位数指数位尾数位动态范围典型用途
FP3232823极大训练、高精度推理
FP1616510较小混合精度训练
BF161687大训练、推理
INT88无无固定区间量化推理

这张表建议存下来,面试和实际选型时都用得上。我踩过的一个坑是:早期以为 BF16 精度不如 FP16,实际上在训练场景里 BF16 往往更稳,因为它不会因为动态范围不够而溢出。精度和范围,永远是两个独立的维度。

2.3 为什么浮点的“冗余”正是量化的空间

现在回答一个核心问题:量化凭什么能压缩这么多还不怎么掉点?答案藏在神经网络权重的统计特性里。

大量实验和论文都观察到同一个现象:训练好的模型权重,绝大多数值都集中在 0 附近的一个窄区间内,近似服从正态分布或者拉普拉斯分布。也就是说,FP32 提供的 23 位尾数精度,对于大部分权重来说是“过剩”的。你用一个 8 位的整数格点去近似这些集中在窄区间的值,只要缩放因子选得好,误差完全可以接受。

但激活值就没这么友好了。激活值的分布随输入数据变化,动态范围大,还经常出现离群值。这就是为什么权重量化通常比激活量化容易,也是为什么很多方案对权重用 per-channel 量化、对激活用 per-tensor 量化。理解了这个不对称性,后面看 PTQ 和 QAT 的各种技巧就不会觉得是玄学。

提示:判断一个模型好不好量化,先看它的权重和激活分布。如果激活里离群值特别多,PTQ 大概率会掉点严重,这时候要么上 QAT,要么用更细粒度的量化方案。

3. 量化的数学原理:从映射公式到误差来源

3.1 线性量化的核心公式拆解

量化最常用的方法是线性量化(也叫仿射量化),公式不复杂,但每个符号都得吃透:

q = round(x / scale + zero_point) x_hat = (q - zero_point) × scale

这里 x 是原始浮点值,q 是量化后的整数,scale 是缩放因子,zero_point 是零点偏移。round 是四舍五入到最近整数。反量化时用第二个公式还原。

为什么要 zero_point?因为浮点区间不一定对称。比如 ReLU 之后的激活值全是非负的,范围是 [0, 6],而 INT8 的区间是 [-128, 127]。如果不加零点偏移,你就没法把 0 准确映射到某个整数上,会引入系统性偏差。zero_point 的作用就是让浮点里的 0 精确对应到某个整数,保证零值不被扭曲。

scale 的计算方式是:

scale = (x_max - x_min) / (q_max - q_min)

对于 INT8 对称量化,q_max=127,q_min=-128,如果浮点区间对称,scale 就约等于 x_max / 127。这个公式看着简单,但 x_max 和 x_min 怎么取,是整个量化里最讲究的地方。取全局最大值?还是用百分位裁剪?还是用 KL 散度搜索最优截断?不同的取法直接决定量化质量。

3.2 对称量化与非对称量化的取舍

对称量化强制 zero_point=0,浮点区间关于 0 对称,即 [-x_max, x_max]。它的好处是计算简单,整数运算时不用额外处理零点偏移,硬件实现友好。缺点是如果实际数据分布不对称(比如全是正数),就会浪费一半的整数格点。

非对称量化允许 zero_point 非零,能精确覆盖任意区间 [x_min, x_max]。它更贴合真实分布,但计算时多了一次零点偏移的加减,硬件上稍复杂。

我的经验是:权重量化优先用对称量化,因为权重通常近似对称分布,而且对称量化在推理时能省掉一些运算;激活量化看情况,如果是 ReLU 这类非负激活,非对称量化更合适。很多推理框架(比如 TensorRT)内部就是这么混合处理的。

3.3 量化误差从哪来:截断误差与舍入误差

量化误差主要有两个来源,分清楚它们才能对症下药。

第一个是舍入误差。round 操作把连续的浮点值压到离散格点上,必然有误差,最大是半个 scale。这个误差是均匀分布的,通常影响不大,而且可以通过增加比特数缓解。

第二个是截断误差,这才是大头。当你选定 [x_min, x_max] 这个区间时,超出区间的值会被直接截断到边界。如果区间选窄了,离群值被砍掉,信息丢失严重;如果区间选宽了,大部分值挤在中间一小段格点上,分辨率下降。这是一个典型的权衡。

举个具体例子。假设某层激活值范围是 [-1, 1],但有一个离群值是 10。如果你把区间设成 [-10, 10],scale = 20/255 ≈ 0.078,那么原本 [-1, 1] 内的值只能用到约 25 个格点,精度惨不忍睹。如果你把区间设成 [-1, 1],那个 10 被截断成 1,虽然丢了一个值,但其余 99.9% 的值精度大幅提升。实践中后者往往更优,这就是为什么很多方案会用百分位裁剪而不是取绝对最大值。

注意:截断误差和舍入误差的权衡,是 PTQ 调参的核心。不要盲目追求覆盖所有值,学会“舍弃离群值换取整体精度”是量化的关键思维。

4. PTQ 范式:不训练也能量化的工程路线

4.1 PTQ 的整体流程与适用场景

PTQ 全称 Post-Training Quantization,翻译过来就是训练后量化。顾名思义,模型已经训练好了,你拿过来直接量化,不需要重新训练。它的流程大致是:准备一批校准数据(通常几百张图片或几百条文本就够)→ 前向跑一遍,统计每层激活值的分布 → 根据统计结果计算 scale 和 zero_point → 把权重和激活都转成整数 → 生成量化模型。

PTQ 最大的优势是快。一个中等规模的模型,校准加转换可能就几十分钟,不需要 GPU 集群,不需要标注数据,对工程团队极其友好。它的适用场景也很明确:模型本身对精度不敏感、或者量化后掉点可以接受、或者你没有资源和时间做 QAT。

但 PTQ 的短板同样明显。当模型对数值敏感(比如检测、分割、生成类任务),或者激活分布离群值严重时,PTQ 掉点可能非常夸张,甚至直接不可用。我见过一个分割模型,PTQ 之后 mIoU 掉了 15 个点,最后只能转 QAT。所以 PTQ 不是万能药,它适合作为第一选择去尝试,但要随时准备好退路。

4.2 校准数据的选取与统计方法

校准数据是 PTQ 的灵魂。很多人随便拿几张图就跑校准,结果量化质量很差,还以为是算法问题。校准数据的核心要求是有代表性:它应该覆盖模型在实际推理时会遇到的数据分布。

数量上,通常 100 到 500 个样本就足够,太多收益递减。选取上,最好从真实业务数据里采样,而不是用公开数据集凑数。如果你的模型是做人脸识别的,校准数据就该是人脸图,而不是 ImageNet 的猫猫狗狗。

统计方法主要有几种:

  • Min-Max:直接取校准数据里的全局最小值和最大值。简单粗暴,但对离群值极其敏感,一个异常值就能毁掉整个区间。
  • Moving Average Min-Max:对多批数据的 min/max 做滑动平均,平滑掉单批的波动。这是很多框架的默认做法。
  • KL 散度:把激活值分布和量化后的分布做 KL 散度,搜索让散度最小的截断阈值。TensorRT 早期版本就用这个方法,效果不错但计算稍慢。
  • 百分位裁剪:取 99.9% 或 99.99% 分位数作为区间边界,主动舍弃极端离群值。简单有效,我个人最常用。

4.3 逐层量化与逐通道量化的实操差异

PTQ 里还有一个关键选择:粒度的粗细。

Per-tensor(逐张量):整个张量共用一个 scale。实现简单,硬件友好,但如果张量内不同通道的数值范围差异大,就会掉点。

Per-channel(逐通道):每个输出通道单独算一个 scale。精度明显更好,尤其是卷积层,因为不同卷积核的权重范围可能差很多。代价是存储 scale 的开销增加,硬件实现稍复杂。

实操上,我的默认策略是:权重用 per-channel,激活用 per-tensor。这是精度和性能的甜点区。PyTorch 的量化 API 里,torch.quantization和torch.ao.quantization都支持这种配置,用QConfig指定 observer 类型即可。

import torch.ao.quantization as tq # 权重用逐通道 MinMax,激活用逐张量 MinMax qconfig = tq.QConfig( activation=tq.MinMaxObserver.with_args(dtype=torch.quint8), weight=tq.PerChannelMinMaxObserver.with_args(dtype=torch.qint8) )

这段配置是我在多个项目里验证过的起点,先跑通再根据掉点情况微调。

5. QAT 范式:把量化误差纳入训练过程

5.1 QAT 的核心思想:模拟量化再反向传播

QAT 全称 Quantization-Aware Training,量化感知训练。它的核心思路很巧妙:既然量化会引入误差,那我在训练的时候就把这个误差“模拟”出来,让模型提前适应它。

具体做法是在前向传播时插入伪量化节点(fake quantization),把权重和激活先量化再反量化,模拟真实量化时的精度损失。但反向传播时,由于 round 操作不可导,需要用 STE(Straight-Through Estimator,直通估计器)把梯度直接传过去,假装量化操作不存在。这样模型在训练中逐渐学会把权重调整到对量化友好的分布上。

QAT 的效果通常比 PTQ 好很多,掉点能控制在 0.5% 以内,甚至无损。代价是需要完整的训练流程、标注数据、GPU 资源,周期从几天到几周不等。所以它适合那些 PTQ 搞不定、又对精度要求极高的场景。

5.2 伪量化节点的插入位置与配置

QAT 的工程实现里,伪量化节点插在哪、怎么配,直接决定成败。标准做法是在每个卷积、全连接层的权重和激活上都插入伪量化,但有些细节要注意。

权重侧的伪量化通常在训练开始前就打开,因为权重分布相对稳定。激活侧的伪量化建议先关掉,训练几个 epoch 让模型适应权重量化后,再打开激活量化,这叫“渐进式量化”。一次性全开容易导致训练不稳定,loss 震荡。

PyTorch 里的配置大致是这样:

model.qconfig = tq.get_default_qat_qconfig('fbgemm') model_fq = tq.prepare_qat(model, inplace=False) # 先冻结激活量化,只训练权重 for name, module in model_fq.named_modules(): if isinstance(module, tq.FakeQuantize): module.disable_observer()

训练几个 epoch 后再解冻,让 observer 开始统计激活分布。这个节奏控制是我踩过坑之后总结出来的,直接全开经常训崩。

5.3 PTQ 与 QAT 的选型决策表

到底选 PTQ 还是 QAT,是每个项目都要面对的问题。我整理了一张决策表,按实际约束来选:

维度PTQQAT
所需时间分钟到小时级天到周级
所需资源少量校准数据,CPU 可跑完整训练数据,GPU 集群
精度损失通常 1%-5%,可能更多通常 <1%
适用任务分类、简单检测分割、生成、高精度检测
工程复杂度低高
迭代速度快,适合快速验证慢,适合最终交付

我的建议是:永远先试 PTQ。它成本低,能快速告诉你这个模型好不好量化。如果 PTQ 掉点在可接受范围内,直接交付;如果掉点严重,再上 QAT。不要一上来就 QAT,那是资源浪费。

6. 实操落地:从 FP32 到 INT8 的完整流程

6.1 环境准备与依赖版本确认

实操部分我以 PyTorch 为例,因为它的量化生态最成熟。环境准备有几个坑必须先说。

PyTorch 版本很关键。1.13 之后量化 API 从torch.quantization迁移到了torch.ao.quantization,老教程里的代码可能跑不通。建议用 2.0 以上的版本。另外,后端选择也影响结果:fbgemm适合 x86 CPU,qnnpack适合 ARM,选错了性能会差很多。

pip install torch>=2.0 torchvision # 确认后端可用 python -c "import torch; print(torch.backends.quantized.supported_engines)"

如果输出里没有你想要的引擎,说明编译时没开对应支持,得换预编译包或者自己编译。

6.2 一个完整的 PTQ 代码示例

下面这段是我常用的 PTQ 模板,以 ResNet18 为例,流程完整可跑:

import torch import torch.ao.quantization as tq from torchvision.models import resnet18 # 1. 加载预训练模型并切到 eval model = resnet18(pretrained=True).eval() # 2. 指定量化配置:x86 用 fbgemm model.qconfig = tq.get_default_qconfig('fbgemm') # 3. 融合 Conv+BN+ReLU,减少量化误差 model_fused = tq.fuse_modules(model, [['conv1', 'bn1', 'relu']]) # 4. 插入 observer,准备校准 model_prepared = tq.prepare(model_fused, inplace=False) # 5. 用校准数据跑前向,统计分布 def calibrate(model, data_loader, num_batches=10): model.eval() with torch.no_grad(): for i, (images, _) in enumerate(data_loader): if i >= num_batches: break model(images) calibrate(model_prepared, calib_loader) # 6. 转换为量化模型 model_int8 = tq.convert(model_prepared, inplace=False) # 7. 保存 torch.jit.save(torch.jit.script(model_int8), "resnet18_int8.pt")

每一步都有讲究。融合那步尤其重要,Conv+BN+ReLU 融合后只算一次量化,能显著减少误差累积。校准批次不用太多,10 到 20 批通常够用。

6.3 量化前后的精度与性能对比测试

量化完必须做对比测试,不能只看模型能不能跑。我一般测三个指标:精度、延迟、模型体积。

精度上,用完整的验证集跑一遍,对比 FP32 和 INT8 的 top-1 准确率。延迟上,用固定 batch size 跑几百次取平均,注意要 warmup 掉前几次。体积上,直接看文件大小。

指标FP32INT8变化
Top-1 准确率69.8%69.2%-0.6%
推理延迟(ms)4518-60%
模型体积(MB)44.711.2-75%

这是我实测 ResNet18 的典型数据。可以看到精度只掉了 0.6 个点,但延迟和体积的收益巨大。这个性价比,就是量化存在的意义。

提示:测延迟时一定要在目标硬件上测。在服务器 CPU 上测出来的数字,和实际部署的边缘设备可能差好几倍,别拿错平台的数做决策。

7. 常见问题与排查技巧实录

7.1 量化后精度暴跌的排查思路

精度暴跌是量化最常见的坑,排查要按顺序来,别乱试。

第一步,确认是不是校准数据的问题。换一批更有代表性的数据重新校准,如果精度回升,那就是数据问题。第二步,检查是不是离群值作祟。把激活的 min/max 打印出来,看看有没有异常大的值。如果有,改用百分位裁剪。第三步,看是不是某些层特别敏感。逐层做敏感度分析,把最敏感的几层保持 FP32,其余量化,这叫混合精度量化。

我遇到过一个案例,某检测模型量化后 mAP 掉了 20 个点,最后定位到是某个注意力层的激活离群值特别严重。把那一层单独保留 FP32,其余量化,mAP 只掉了 1.5 个点。所以逐层敏感度分析是排查精度问题的利器,值得花时间做。

7.2 量化模型部署时的兼容性坑

量化模型训好了,部署时还有一堆坑等着。

首先是算子支持问题。不是所有算子都有 INT8 实现,遇到不支持的算子,框架可能会回退到 FP32,导致性能不升反降。部署前一定要用框架的工具检查算子覆盖情况。

其次是后端匹配问题。用 fbgemm 量化的模型,拿到 ARM 设备上可能跑不了,得用 qnnpack 重新量化。这个坑我在跨平台部署时踩过,白折腾了一天。

最后是输入输出格式问题。量化模型的输入通常也需要量化,如果预处理没对齐,结果会完全错乱。建议把预处理也纳入量化流程,或者确保输入 scale 和模型期望的一致。

7.3 量化参数调优的独家经验

最后分享几个我积累的调优经验,都是文档里不会写的。

第一,校准数据的 batch size 别太大。大 batch 会让统计的分布偏向平均,反而丢失细节。我一般用 8 到 16 的小 batch。

第二,先量化权重,再量化激活。分两步走,每步都验证精度,出问题好定位。一次性全量化,出了问题根本不知道是哪儿的锅。

第三,保留原始 FP32 模型做对照。每次量化后都和 FP32 对比,一旦掉点超过阈值就回退。这个习惯帮我避免了好几次线上事故。

第四,关注第一层和最后一层。这两层对精度影响往往最大,如果掉点严重,优先考虑把它们保持 FP32。输入层直接接触原始数据,输出层直接决定结果,它们敏感是有道理的。

量化这件事,说到底是在精度、速度、体积之间找平衡。没有银弹,只有针对具体场景的权衡。PTQ 和 QAT 不是对立的,而是工具箱里的两把扳手,什么时候用哪把,取决于你的约束和目标。我个人在实际项目里的体会是,先把 PTQ 跑通建立基线,再根据掉点情况决定要不要上 QAT,这个节奏最稳,也最省资源。

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

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

立即咨询