☰
模型量化入门:INT8原理、PTQ/QAT选择与边缘部署实践
2026/10/28 23:24:55 网站建设 项目流程

把一个训练好的模型塞进手机、摄像头或者一块边缘板卡里,最先撞上的往往不是算法问题,而是内存和带宽。我遇到过一次很典型的场面:一个视觉模型在服务器上跑得好好的,单卡推理延迟低得几乎看不见,结果换到一台内存只有几百兆的嵌入式设备上,权重文件刚加载完,运行时就报内存不足。模型本身没有变,输入尺寸也没有变,唯一变的是——我们把它从 32 位浮点换成了 8 位整数,而那一刻我才真正意识到,量化这件事不是"压缩一下文件"这么简单,它是在重新定义整个计算过程里的数值表示方式。

这篇文章想讲清楚的就是"什么是量化"。不是那种一句话定义——"把浮点转成定点"——这种说法对新手几乎没有任何帮助。我会从一个从业者的角度,把这件事故意拆得碎一点:量化在数轴上到底做了什么动作、省下来的空间和算力从哪里来、精度又是从哪个缝隙里漏出去的、训练后量化和量化感知训练该怎么选、以及真正落地时那些文档上一个字都不会写的坑。适合刚接触模型部署的读者打底子,也适合已经在做推理优化、但一直靠试参数过日子的同行补一补底层直觉。

1. 量化到底把什么变小了:从一次显存爆掉的部署说起

1.1 一张特征图带来的疑问

很多人第一次接触量化,是从"模型文件从 100MB 变成 25MB"这个现象开始的,于是很容易形成一个误解:量化就是压缩权重。但只要你自己动手跑一遍完整的推理链路,就会发现权重只是整条流水线上的一小段。以一个典型的卷积网络为例,假设某一层输入是 1×64×224×224 的特征图,输出通道 128,卷积核 3×3。这一层的权重参数数量是 128×64×3×3 约 7.4 万个,按 FP32 算大概 295KB;而它输出的特征图有 128×224×224 约 642 万个元素,按 FP32 算是 25MB 左右。权重比激活小了将近两个数量级。

这就解释了一个很常见的疑惑:为什么有些模型权重压到了 4 位,推理速度却没有明显提升,甚至反而变慢了。因为推理过程中的访存大头往往在激活值上,权重压得再狠,中间结果还是 32 位浮点,内存带宽该占还是占。量化的收益必须从"权重 + 激活 + 中间缓存"三个方向同时去看,只盯着权重文件大小,很容易得出错误结论。我在早期做移动端部署时就吃过这个亏,看着权重压缩比很漂亮,端到端延迟只降了百分之十几,后来把激活也一起量化,才把延迟真正压下来。

1.2 用一组数字把账算清楚

把账算细一点会更直观。一个 FP32 数值占 4 字节,INT8 占 1 字节,理论上的存储压缩比是 4 倍。但真正的收益不止于此,因为现代处理器在整数运算上的吞吐通常远高于浮点,尤其是在没有独立浮点单元的嵌入式芯片上。一颗典型的边缘 SoC,INT8 的峰值算力可能是 FP32 的 4 到 8 倍,这个倍数是硬件设计决定的,不是软件优化能追上的。

还有一个容易被忽略的点是带宽。推理过程本质上是"把数据搬进计算单元,算完再搬出去",很多层是访存受限而不是计算受限。数据宽度从 32 位降到 8 位,同样一条内存总线在单位时间内能搬运的元素数量直接翻四倍,这对访存受限的层(比如逐点卷积、归一化层)提升非常显著。所以量化的收益可以拆成三块:存储占用下降、计算吞吐上升、访存压力缓解。三者叠加,才是我们在端侧感受到的那个"变快了很多"。

不过收益永远伴随代价。浮点数能表示的动态范围极大,一个 FP32 最小能到 10 的负 38 次方量级,最大能到 10 的 38 次方量级,中间还有二十多位有效数字;而 INT8 只有 256 个离散取值。把连续分布塞进 256 个格子里,必然丢信息。量化的全部技术难点,本质上就是"如何用 256 个格子尽可能逼近原来的分布"。

1.3 权重量化、激活量化与缓存量化是三件不同的事

我在跟新人沟通时发现,最大的概念混乱来自把所有量化混为一谈。实际上按照被量化的对象不同,至少要分成三类,它们的难度和策略差异很大。

权重量化最简单。权重在训练结束后就固定了,分布是静态的,我们可以慢慢统计每一层的取值范围,甚至可以在离线阶段做逐层的误差补偿。这一类量化已经很成熟,把权重压到 8 位甚至 4 位,精度损失通常可以控制在很小的范围内。

激活量化难得多。激活值是输入数据的函数,会随着输入内容变化,一张明亮的图和一张昏暗的图,同一层的激活分布可能完全不同。而且激活是运行时产生的,没有"离线慢慢算"的机会,只能靠一小批校准数据去估计范围。估计得保守,浪费量化精度;估计得激进,一旦遇到超范围的值就会溢出截断,直接毁掉这一层的输出。

缓存量化是近几年随着长上下文场景火起来的。自回归生成时,每一步都要把历史的键值对缓存下来供后续使用,序列一长,这块缓存的开销会超过模型权重本身。把缓存压到 8 位甚至更低,是长文本推理的刚需。但它的难点在于缓存是逐步写入的,分布随时间漂移,而且早期的量化误差会一直留在缓存里,被后续所有步骤反复读取。

三类对象,三种分布特性,三套处理策略。搞清楚这一点,后面所有的技术细节才有落脚点。

2. 浮点映射到整数:量化公式里的两个关键参数

2.1 仿射量化的完整推导

量化的核心动作只有一个:建立一个从实数域到整数域的映射。最通用的形式是仿射映射,它有两个参数——缩放因子 S 和零点 Z。

整数表示与实数表示之间的关系可以写成:

r = S × (q − Z)

其中 r 是原始的浮点值,q 是量化后的整数。反过来,把一个浮点数变成整数,就是:

q = clamp(round(r / S) + Z, q_min, q_max)

这个 clamp 是必须的,因为超出整数表示范围的值必须被截断到边界,否则会发生数值溢出,结果可能从正的最大值直接翻转成负的最大值,这种错误比精度损失可怕得多。

那么 S 和 Z 怎么确定?假设我们统计出某一层某个张量的最小值是 r_min,最大值是 r_max,目标是映射到 [q_min, q_max] 这个整数区间(INT8 对称量化常用 [-127, 127],非对称常用 [-128, 127])。缩放因子就是两段区间的长度之比:

S = (r_max − r_min) / (q_max − q_min)

零点则是为了让 r_min 精确落在 q_min 上:

Z = round(q_min − r_min / S)

注意 Z 必须取整,因为它本身也要以整数形式存储。这个取整会带来一点点偏移,但影响很小。用一段最朴素的代码把整个过程写出来,比看公式更清楚:

def calc_params(r_min, r_max, q_min=-128, q_max=127): # 防止整个张量取值相同导致除零 if r_max == r_min: r_max = r_min + 1e-8 scale = (r_max - r_min) / (q_max - q_min) zero_point = round(q_min - r_min / scale) zero_point = max(q_min, min(q_max, zero_point)) return scale, zero_point def quantize(x, scale, zero_point, q_min=-128, q_max=127): q = round(x / scale) + zero_point return max(q_min, min(q_max, q)) def dequantize(q, scale, zero_point): return scale * (q - zero_point)

这段代码没有任何框架依赖,纯粹是把数学式子翻译成可运行的逻辑。你可以拿一个实际的张量手动跑一遍,看着浮点数被压成整数再还原回来,那个"误差"会变得非常具体。

2.2 零点存在的意义

零点这个参数经常被新手当成可有可无的东西,尤其是看到对称量化里 Z 恒等于 0 之后,更容易觉得它多余。但它在非对称场景下承担着一个非常实际的职责:保证实数 0 能被精确表示。

为什么 0 这么重要?因为在神经网络里,0 有特殊语义。填充(padding)用的是 0,ReLU 的输出下界是 0,稀疏化后的权重里大量的 0。如果 0 不能被精确表示,那么卷积在边界补零的位置就会引入额外的偏移误差,ReLU 之后所有本该严格为零的位置也会变成一个小正数,这些误差会一层层累积。

对称量化强制 Z = 0,好处是计算时可以省掉零点校正这一步,整数矩阵乘法做完直接乘缩放因子还原即可,实现简单、速度快。代价是当数据分布明显不对称时(比如 ReLU 之后的激活全部非负,范围是 [0, 6]),对称量化会把 [-6, 0] 这段根本用不到的范围也留着,等于把 256 个格子的一半浪费在空区间上,有效精度直接减半。

非对称量化能贴合这种偏斜分布,代价是每次计算都要处理零点,卷积里的零点校正会带来额外开销。这里的选择逻辑很朴素:权重通常围绕 0 对称分布,用对称量化;激活经过 ReLU 之后严重偏斜,用非对称量化。这也是绝大多数量化方案默认的组合方式。当然,如果你的硬件对非对称支持不好,也可以强行把激活改成对称量化,代价是需要重新校准并接受一部分精度损失。

2.3 粒度选择:per-tensor、per-channel 与 per-group

确定了映射公式,下一个问题是:S 和 Z 到底按什么粒度来统计?这个问题对精度的影响,往往比选对称还是非对称更大。

per-tensor是整个张量共享一组参数。实现最简单,硬件最友好,但只要张量内部各部分的数值范围差异大,小范围的那部分就会被"挤"到很少的几个格子里,几乎等于被抹平。这种情况在卷积权重里非常常见,不同输出通道的权重量级可能差一个数量级。

per-channel是每个输出通道各有一组参数。它精准解决了通道间量级不一致的问题,代价是需要存储更多参数,且在硬件上做向量化计算时要处理多组缩放因子。这是目前权重量化的主流做法,几乎所有成熟的量化工具链都支持。

per-group是折中方案,把通道再切成若干组,每组共享一组参数。在低比特场景(比如 4 位、3 位)下几乎是标配,因为比特数越低,格子越少,越需要更细的粒度来保住有效精度。常见的组大小是 32、64、128,组越小精度越高,但元数据的开销也越大。

粒度参数开销精度表现硬件友好度典型使用场景
per-tensor极低一般最高激活量化、对精度要求不高的场景
per-channel中等较好较高8 位权重量化的默认选择
per-group较高最好中等4 位及以下的低比特权重量化

这里要提醒一个实操细节:粒度的收益存在明显的边际递减。从 per-tensor 换到 per-channel,精度提升通常很显著;从 per-channel 换到 per-group(组大小 128),在 8 位下提升可能只有零点几个百分点,但推理引擎的适配成本会上升。我一般建议 8 位场景老老实实用 per-channel,只有在比特数压到 4 位以下时,才值得为 per-group 付出额外的工程代价。

3. 精度为什么会掉:三类典型的误差来源

3.1 离群值把整层的刻度撑坏

量化误差最凶的来源不是"格子太少",而是"格子被浪费了"。假设某一层的激活值绝大多数集中在 [-1, 1] 之间,但偶尔冒出几个 40 左右的极端值。按 MinMax 策略统计范围,S 会被这个 40 撑大,原本 [-1, 1] 这个真正承载信息的区间,只能分到 256 个格子里的寥寥几个。信息密度骤降,这一层的输出基本就废了。

这类离群值在 Transformer 架构里尤其普遍,某些注意力头的激活会出现极端的尖峰,而且这些尖峰位置在不同输入下还不固定。这就导致了很矛盾的局面:为了不溢出必须把范围开大,范围一开大有效精度就没了。

应对思路大致有几类。一类是截断,不追求覆盖全部取值,主动放弃极少数离群点,用一个分位数(比如 99.9%)代替最大值。这样绝大部分数据获得了更细的刻度,被截断的少数点产生误差,但总体误差反而下降。另一类是平滑,在量化前把激活的尖峰按通道"摊平"到权重上,让两边的分布都变得温和一些,量化完再把补偿还原回去。还有一类是保留高精度,把这些离群值所在的位置单独用高比特表示,其他位置正常量化。

需要强调的是,这几类方法都不是免费的。截断需要调分位数阈值,调高了没用调低了掉点;平滑会改变计算图,增加一次逐通道缩放的开销;混合精度会让实现复杂度和内核适配难度上升。选哪种,取决于你的精度预算和工程预算。

3.2 舍入误差沿着网络层层放大

除了范围估计的问题,还有一个更隐蔽的误差源:舍入。把浮点数舍入到最近的整数,每次都会引入最多半个格子的误差。单次误差很小,但网络有几十上百层,每一层的输入都带着上一层传下来的误差,而误差会通过权重矩阵被重新组合、放大。

更麻烦的是舍入的方式。标准的"四舍六入五取偶"是无偏的,误差期望为零;但如果为了硬件实现的简单性采用"直接截断"(向下取整),误差就会系统性地偏负,几十层累积下来会形成明显的偏移。我在调试一个深层次序模型时就遇到过这种情况,单层误差看起来完全可以接受,但把十层拼起来一跑,输出整体出现了一个肉眼可见的偏移,改成对称舍入之后立刻恢复正常。

还有一个容易被忽略的地方是累加器。即使输入是 INT8,卷积和矩阵乘的中间累加结果通常要用 INT32 保存。因为一次矩阵乘要累加几百上千项,每项都是 INT8 的乘积(最大约 16129),累加几百项就会超过 INT16 的范围。如果你的实现里累加器位宽不够,溢出会让结果彻底崩溃。这一点在自研内核时尤其要注意,很多推理框架的文档里会明确写着累加位宽,但没人提醒你它为什么必须是 32 位。

3.3 激活分布随着输入漂移

第三类误差来源是分布漂移,它只在激活量化时出现,但杀伤力很大。校准阶段我们拿一小批数据统计出的范围,只代表那批数据的分布。真实线上输入千变万化,一旦出现校准集里没见过的数据形态,激活可能整体上移或者展宽,超出原先估计的范围,触发截断。

这个问题在视觉任务里表现为某些光照条件下的图片效果明显变差,在文本任务里表现为某些领域的长尾输入输出质量下降。它的隐蔽之处在于:离线评估时用的测试集往往和校准集同分布,指标看起来很漂亮,上线之后才发现问题。

缓解手段有几种。一是校准集要覆盖多样性,别只拿一类样本凑数,宁可少一点但要有代表性。二是在推理时做动态范围统计,也就是所谓的动态量化,每次前向传播时根据当前输入实时计算范围。它的代价是要多一次统计和参数计算,但对付分布漂移非常有效,在参数量不大的层上开销可以接受。三是在网络设计阶段就做约束,比如用归一化层把激活分布控制在一个稳定的区间里,这样量化范围就不容易失控。

4. 训练后量化与量化感知训练:路线选择的判断依据

4.1 PTQ 的校准环节才是成败关键

训练后量化(PTQ)的思路很直观:模型已经训练好了,权重不动,只需要统计一下各层的数值范围,算出一组缩放参数,然后直接换成整数推理。它最大的优势是成本低,不需要重新训练,不需要原始训练数据,只需要一小批校准样本,几十分钟就能跑完。

但"只需要校准"这个说法容易误导人。PTQ 的效果高度依赖校准这一步,而校准里的门道比想象中多。首先是校准集的规模,太少(比如几十张图)统计出来的范围不稳定,太多又会拖长流程,实践里通常取几百到一千个样本,覆盖主要的数据形态。其次是校准算法,MinMax 最简单但对离群值毫无抵抗力,百分位法要调阈值,基于信息熵的方法会搜索一个让量化前后分布最接近的截断点,效果通常更好但计算量大一些。

我用下来最稳的一套流程是:先用几百个样本做 MinMax 拿到基线,跑一遍逐层误差分析,找出误差最大的那几层,再针对这几层换熵校准或者百分位校准,最后用一批完全独立的验证数据确认。这个过程听起来繁琐,但它比"全模型统一用熵校准"要可靠得多,因为不同层的最优策略本来就不同。

还有一个细节:校准数据必须走完整的预处理链路。我见过有人直接拿原始图片去校准,跳过了归一化,导致统计出来的激活范围和实际推理时完全不是一个量级,量化参数全错,最后归咎于"PTQ 不好用"。这类问题排查起来极其痛苦,因为每一步单看都对,只有拼起来才错。

4.2 QAT 的伪量化与直通估计

当 PTQ 怎么调都掉点的时候,就轮到量化感知训练(QAT)出场了。它的核心思想是:既然量化会引入误差,那就让模型在训练阶段就"体验"这种误差,学会在误差存在的情况下依然给出正确结果。

具体做法是在前向传播里插入伪量化节点——把浮点数先按量化公式压成整数,再还原成浮点数。数据形式上还是浮点,但数值已经被限制在量化格点上,模型因此能感受到量化带来的失真。反向传播时麻烦来了:量化里的 round 和 clamp 操作的导数几乎处处为零,梯度传不回去,整个网络就没法训练。

解决方案是直通估计器(STE),思路非常朴素:反向传播时假装量化这个操作不存在,直接把上游梯度原样传给下游。数学上这显然不是真正的导数,但实践中效果出奇地好。可以这么理解——我们不告诉模型"量化这一步该怎么调",而是告诉它"你就当量化没发生,按原来的方向优化,但每次更新后我帮你把参数重新对齐到格点上"。这种近似在量化噪声不大的时候是合理的,梯度方向基本正确,只是幅度可能有偏。

QAT 的成本主要在训练资源上。通常需要原始训练数据(或者同分布的数据),需要几个 epoch 的微调,还需要调整学习率——一般要比正常训练小一到两个数量级,因为模型已经收敛了,大学习率会把预训练学到的特征直接冲掉。我见过有人用原训练的学习率跑 QAT,结果精度比 PTQ 还差,原因就是这个。

4.3 什么情况下必须上 QAT

不是所有场景都值得上 QAT,判断标准可以归成几条。

如果目标是 8 位量化,模型是常规的卷积网络或成熟的 Transformer,PTQ 通常足够,掉点能控制在 1% 以内,没必要为此付出训练成本。如果目标压到 4 位甚至更低,PTQ 基本撑不住,这时候 QAT 几乎是唯一选择,因为低比特下量化格点太稀疏,靠后处理调参数已经无能为力。

如果模型里有大量敏感结构,比如检测头、分割头、小目标相关的浅层特征,这些地方对数值扰动非常敏感,PTQ 往往会在这些位置掉点明显,QAT 能让模型主动适应。如果业务对精度几乎是零容忍(比如某些测量类任务),那 QAT 也是更稳妥的路线。

对比维度训练后量化 PTQ量化感知训练 QAT
所需数据少量校准样本完整训练数据或同分布数据
时间成本分钟到小时级小时到天级
典型比特8 位为主8 位、4 位甚至更低
精度损失通常 1% 以内可控制在 0.5% 以内
工程复杂度低,流程标准化高,需要改训练代码
适用场景快速验证、成熟架构低比特、敏感任务

我的实际建议是:先花半天时间把 PTQ 跑通,拿到一个基线数字。如果这个数字能满足业务要求,就直接收工,别为了追求极致去上 QAT。如果差得不多(比如差一到两个点),再考虑优化校准策略。只有当 PTQ 优化到头仍然不达标,才启动 QAT。这个顺序能省下大量时间。

5. 一次 INT8 量化的完整落地链路

5.1 动手前的四项检查

很多人一上来就开始调量化参数,其实有几项前置检查没做,后面全是白费功夫。

第一,确认硬件的整数算力真的被用上了。有些推理后端在遇到不支持的算子时会自动回退到浮点实现,结果就是模型"量化"了,但大部分计算还在跑浮点,甚至因为多了量化/反量化的转换开销而变慢。检查方法很简单:看推理时的算子分布,确认整数算子占比。如果回退比例很高,先解决算子支持问题,再谈精度。

第二,确认输入输出的处理链路。模型的输入通常需要量化,输出需要反量化。这两处的缩放参数如果和前后处理对不上,会导致结果整体偏移。特别是输出端,如果下游逻辑默认拿到的是浮点,而实际拿到的是整数,可能出现非常诡异的错误。

第三,确认模型的动态shape支持情况。如果业务需要变长输入,量化参数的统计方式要根据最大范围来做,或者在运行时动态计算。固定shape的量化模型直接喂变长输入会直接崩。

第四,备份一份浮点基线。这个听起来废话,但真的有人量化完之后找不到原始模型了。更重要的是,你需要一个可靠的对照基准,包括精度指标、逐层输出、延迟数据,后面每一步优化都要和它比。

5.2 逐层敏感度分析怎么做

敏感度分析是整个量化流程里性价比最高的一步,但很多人跳过它直接全模型量化,然后在掉点之后盲目调参。

做法是:每次只把一层(或一个模块)换成量化版本,其余保持浮点,跑一遍验证集,记录指标变化。把所有层的敏感度排序,就能清楚看到哪些层"碰不得"。实际操作中不需要真的逐层切换,很多工具链支持插入量化观察点,能直接输出每层的量化误差统计,效果类似。

拿到敏感度排序后,策略就清晰了:最敏感的几层保留浮点或用更高比特,其余层放心量化。经验上,网络的第一层(直接接触原始输入)和最后一层(直接决定输出)通常最敏感,中间的深层反而耐受度更好。残差相加的位置、注意力里的 softmax 前后、以及归一化层附近,也经常是敏感区。

这里有个反直觉的观察:参数量大的层不一定敏感,参数量小的层不一定安全。我遇到过某个只有几百个参数的缩放层,量化之后指标掉了两个点,因为它的输出会被广播到所有通道上,误差被成倍放大。所以敏感度必须实测,不能靠参数量猜。

5.3 混合精度与回退策略

敏感度分析的直接产出就是混合精度方案:大部分层用 INT8,少数敏感层保留 FP16 或 FP32。这个方案在精度上很有效,但要考虑两个代价。

一是性能代价。混合精度意味着计算图里有类型转换节点,每次从 INT8 切到 FP16 再切回来,都要做一次数据搬移和格式转换。如果切换太频繁,这些转换的开销可能吃掉量化带来的收益。所以我一般会把敏感层尽量集中,让图里只有少数几个"浮点孤岛",而不是到处穿插。

二是工程代价。不同推理后端对混合精度的支持程度差别很大,有些后端只支持整图统一精度,那就只能退而求其次——把敏感层替换成计算量更小的等价结构,或者干脆接受这部分精度损失。

除了混合精度,还有一种更彻底的回退策略:只量化权重,激活保持浮点。这种做法在访存受限的场景下仍然能拿到可观的加速(因为权重的读取量大大减少),同时避开了激活量化最难的分布漂移问题。当激活量化怎么调都不理想时,这是个很实用的保底方案。

5.4 验收标准不能只看准确率

量化做完了,怎么判断"成功"?只看整体准确率是不够的,我通常会看四组数据。

整体指标当然是第一位的,分类看 top-1/top-5,检测看 mAP,生成任务看人工评估或者自动指标。但要注意对比的基准必须是同一套测试流程,别拿量化模型的测试集结果和浮点模型的验证集结果比。

分层指标更重要。整体指标掉 0.5 个点可能看起来没事,但如果某一类别的指标掉了 10 个点,只是被其他类别的提升掩盖了,这在业务上可能是灾难。所以按类别、按场景切片看指标是必须的。

最坏情况指标。量化误差在极端输入下会被放大,所以要专门构造一批困难样本——过曝的、过暗的、模糊的、长尾的——看量化模型在这些样本上的表现。浮点模型和最坏情况的差距,往往就是量化模型翻车的地方。

数值稳定性。跑一批数据,统计输出里有没有 NaN、有没有异常大的值、有没有全零输出。这些问题在常规指标上可能看不出来,但会在线上偶发。我遇到过一次,量化模型在某个特定输入下输出全是边界值,原因是某一层的激活整体超出范围被全部截断,这类问题必须靠专门的数值检查才能发现。

6. 文档里不会写的几个坑

6.1 首尾两层与残差相加处

第一层和最后一层尽量别量化,这个结论很多人知道,但知道原因的人不多。第一层直接接收原始输入,输入的分布范围由业务数据决定,方差可能非常大且不稳定,量化范围很难估准;同时第一层的通道数通常很少(RGB 只有 3 通道),per-channel 量化的收益有限。最后一层直接产生输出,任何误差都没有后续层来"消化",会原封不动地体现在结果上。

残差相加是另一个高危位置。两个分支的输出相加,如果它们的量化尺度不一致,相加之前必须先对齐到同一尺度,这个对齐过程会引入额外误差。更麻烦的是,残差结构会把误差直接累加到主路径上,深层网络里堆叠几十个残差块,误差累积效应很明显。稳妥的做法是在相加之前保持较高精度,相加之后再统一量化。

6.2 归一化与非线性层的特殊待遇

归一化层(各种 Norm)涉及均值、方差的计算,这些统计量本身是浮点语义,强行量化会让数值变得很不稳定。尤其是方差,它是一个平方量,动态范围比均值大得多,量化后容易出现精度崩塌。实践中通常是让归一化层保持浮点,或者把它折叠进相邻的线性层里再一起量化。

非线性层要看具体类型。分段线性且斜率固定的(比如 ReLU)量化友好,因为整数输入直接截断就能得到正确结果。涉及指数、除法的(比如 sigmoid、softmax、GELU)就麻烦,它们的输出范围有限但曲线陡峭,量化后容易在饱和区丢失区分度。常见的处理是把这类算子单独拿出来做查表或者用定点近似实现,而不是简单套用量化公式。

还有一个隐蔽的坑:算子融合会改变量化行为。把卷积、批归一化、激活融合成一个算子,量化参数需要在融合后的算子上重新计算,不能简单沿用融合前的。有些工具链会自动处理,有些需要手动指定,搞错了会导致结果对不上但没有任何报错。

6.3 硬件真的支持你选的低比特吗

这一点值得单独强调,因为它经常导致"实验室效果很好,上线完全不是那么回事"。

不同的硬件对量化格式的支持差异极大。有的芯片原生支持 INT8 且有针对性的指令,跑起来飞快;有的虽然支持 INT8 存储,但计算时还是要展开成更高位宽,加速效果打折;有的对 4 位只有存储压缩,没有对应的计算单元,实际推理反而因为反复解包而变慢。

所以在选比特数之前,一定要先确认目标硬件的指令集能力。查手册的时候重点看三个东西:支持的整数位宽、累加器位宽、以及是否有专门的矩阵乘加速单元。如果硬件只对某些特定形状(比如 4×8 的分块)有加速,那你的量化方案最好往这个形状上靠,否则拿不到峰值性能。

还有个现实问题:低位宽往往意味着更激进的误差,需要更复杂的补偿算法,而复杂算法在端侧可能根本跑不动。我见过一个方案,为了在 4 位下保住精度,加了一堆运行时校正逻辑,最后端到端延迟比 8 位方案还高。量化不是比特越低越好,找到精度和速度的平衡点才是目的。

6.4 那些看不见的溢出

最后一个坑最隐蔽:数值溢出。前面提过累加器位宽,这里再补几个具体场景。

权重的量化参数是用校准数据算的,如果线上出现了一个和校准集完全不同的输入,激活大幅超出范围,clamp 会把它截断到边界。截断本身不报错,但结果会变成常数,模型相当于对这部分输入"瞎了"。这类问题在日志里看不出来,只能靠监控输出的分布来发现。

还有一种是指数运算的溢出。如果某层有 exp 操作,量化后的整数输入在接近上限时会算出极大的值,超出后续层的表示范围。处理办法是在 exp 之前做范围限制,把输入裁到一个安全区间内。

整数乘法本身也可能溢出。两个 INT8 相乘最大是 16129,还在 INT16 范围内,但如果是 INT4 乘 INT4 再累加,位宽规划就要更小心。我自己踩过一次:某个自定义算子里用了 16 位累加,单测数据量小的时候完全正常,上了真实数据之后结果全乱,排查了大半天才发现是累加溢出。

量化这件事做久了会有个体会:真正花时间的从来不是"把浮点转成整数"这个动作,而是找到那个既能满足精度又能跑出速度的配置。这个配置没有通用答案,它和你用的模型、你的数据分布、你部署的硬件、甚至你的推理框架版本都绑在一起。所以我现在的习惯是,每接一个新场景,先花两天做一轮完整的敏感度扫描和数据分布摸底,把这张"地图"画出来,后面所有的参数调整都有据可依,不会再靠运气试。

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

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

立即咨询