ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

嵌入式AI模型量化五步法:INT8端侧部署与精度平衡

嵌入式AI模型量化五步法:INT8端侧部署与精度平衡

模型在开发机上跑得好好的,一烧进板子就卡成幻灯片——做嵌入式 AI 的人多半都撞过这堵墙。权重文件几十兆,Flash 装不下;单帧推理几百毫秒,实时性完全谈不上;想换个小模型,精度又掉得没法看。嵌入式 AI 模型量化,本质上就是在这三件事之间找一个能落地的平衡点:把 FP32 的权重和激活压到 INT8 甚至更低,让模型体积缩到四分之一,让端侧算力跑得动,同时把精度损失控制在业务可接受的范围内。

这篇内容讲的是一套我自己反复用过的五步流程,从一个已经训练收敛的浮点模型开始,到最后在目标板子上跑出稳定的延迟和精度数据。它不绑定某个特定框架,也不要求你手上有 NPU,纯 MCU、带 DSP 扩展的核、轻量级 NPU 都适用。适合两类人看:一类是刚把模型训完、准备往端侧搬的算法同学,另一类是已经上手部署、但量化后精度或速度总有一头不理想的工程同学。文中涉及的模型和硬件都以虚构的示例代称,参数和数字是按常见场景估算的,你替换成自己的实测值即可。

1. 摸清家底:嵌入式量化到底在压什么、速度从哪来

动手之前先把原理捋清楚,比上来就调工具链参数有用得多。很多人量化失败,不是工具不好用,而是根本不知道自己压掉的是什么、加速又是从哪个环节省出来的,出了问题只能瞎试。

1.1 一次 INT8 量化到底发生了什么

量化的数学形式很朴素,就是给每个张量配一组缩放因子和零点,把连续的浮点值映射到有限的整数格点上。对非对称量化,映射关系是:

q = clamp(round(x / S) + Z, qmin, qmax) x_approx = S * (q - Z) S = (x_max - x_min) / (q_max - q_min) Z = round(q_min - x_min / S)

INT8 时qmin=-128、qmax=127。S 决定了一个整数格代表多大的浮点跨度,Z 决定零点的位置。对称量化则强制 Z=0,只保留 S,公式退化成q = round(x/S),取值范围通常是 -127 到 127。

这里的核心矛盾在于:S 一旦定下来,整个张量就共用这一把尺子。如果某个张量的激活值分布极不均匀,绝大多数值挤在 0 附近,只有极少数尖峰跑到很远的地方,那么按最大值算出来的 S 会很大,0 附近那些值全部被压到相邻的几个整数格上,细节信息直接丢了。反过来,S 取小了,尖峰又被 clamp 削掉。校准算法要解决的,就是怎么在这一堆取值里挑一个让信息损失最小的截断阈值。

理解这一点,你就能明白为什么“量化后精度掉了”几乎总是发生在特定层:那些激活分布长尾严重、动态范围跨几个数量级的层,才是重灾区。后面讲的敏感度体检,就是把这些层找出来。

1.2 为什么端侧几乎只认定点

FP32 乘加在通用 CPU 上很自然,但到了资源受限的嵌入式环境,问题就暴露了。一个 FP32 乘加,在纯整数流水线的 MCU 上要靠软件模拟浮点运算,几十个周期起步;而 INT8 乘加可以直接走硬件乘法器,一个周期一个。差距不是百分之几十,是几十倍。

除了算力,还有三笔账。第一是存储:一个 700 万参数的模型,FP32 权重要 28MB,INT8 只要 7MB,对于只有几百 KB 到几 MB 片上 Flash 的器件,这是能不能装下的区别。第二是带宽:权重从 Flash 搬到 SRAM 的时间往往比计算本身还长,位宽减半,搬运量就减半,很多模型的瓶颈其实在这里。第三是缓存和 TCM 的占用,INT8 激活让同一块 SRAM 能放下更大的张量,减少反复换入换出。

所以不是大家不想用 FP16,而是在多数没有浮点单元的嵌入式核上,INT8 是唯一能在算力、存储、功耗三条线上同时交代得过去的方案。带 FPU 的核可以用 FP16 或者 BF16 做些折中,但一旦模型规模上去,INT8 仍然是主力。

1.3 压缩率与精度的三条硬边界

做量化之前先认清三条边界,能避免很多不切实际的期待。

第一条是权重量化几乎无损,激活量化才是难点。权重是静态的,分布稳定,per-channel 量化后 INT8 通常掉点极小;激活是动态的,随输入变化,长尾和离群值都在这边,精度损失主要来自激活。

第二条是首尾层敏感。第一层卷积直接吃原始输入,输入动态范围由传感器决定,往往很大;最后一层分类头或检测头输出维度小,量化误差会被放大。这两处默认保留高精度,是性价比最高的操作。

第三条是深度可分离卷积和逐元素操作是量化难点。深度卷积每个通道单独做,通道间数值范围差异大,per-tensor 量化很容易把小通道压死;残差相加、逐元素相乘需要两个张量的 S 和 Z 对齐,否则要么重新量化引入误差,要么多一次搬运。这两类结构在现代轻量骨干里到处都是,必须提前规划。

2. 五步法整体设计:把量化做成可回滚的流水线

量化最怕的不是精度掉,是掉完之后不知道哪一步掉的、回退回去又不知道改哪个参数。所以流程设计的第一原则是可回滚、可定位、可复现。五步流程就是按这个原则排的。

2.1 五步流程与每步的交付物

我把整条流水线拆成五步,每一步都有明确的输入和产出,任何一步出问题都能退回到上一步而不影响全局。

步骤主要动作交付物判断通过的标准
第一步 模型体检逐层敏感度分析、算子清单核对敏感层名单、不支持算子列表明确哪些层必须保留高精度
第二步 图清理BN 折叠、算子融合、布局统一量化友好图图中算子全部在目标后端支持列表内
第三步 校准校准集筛选、校准算法选型每张量的 S/Z 参数表校准后逐层余弦相似度达标
第四步 量化执行权重量化、激活量化、混合精度回退定点模型文件浮点与定点输出误差在阈值内
第五步 端侧实测精度回归、延迟与内存实测部署报告精度损失和延迟均满足业务指标

这个表看起来简单,实际执行时最容易跳过的是第一步。很多人拿到模型直接丢进量化工具,跑完发现掉点,再回头一层层试,时间全浪费在返工上。先把体检做了,后面每一步都是有的放矢。

2.2 为什么先做基线而不是先量化

基线不只是“量化前跑个精度”这么简单。你需要一份完整的三件套:浮点模型在目标数据集上的精度、浮点模型在目标硬件上的延迟和内存占用、以及逐层输出的特征图快照。

前两项是后面所有对比的参照物,第三项是定位问题的关键。有了逐层特征快照,量化后哪一层开始偏离、偏离多少,一比对就清楚;没有的话,你只能看到最终精度掉了两个点,然后从第一层开始猜。

基线还有个大用处:判断这次量化到底值不值得做。如果浮点模型在目标板子上已经是 30ms 一帧,业务要求 50ms 以内,那你费劲量化可能只省下 5ms,收益有限;如果浮点是 300ms,那就是必须做。先量一遍,再决定投入多少精力。

做基线时有个细节要注意:延迟测量必须在目标硬件、目标主频、目标编译器优化选项下进行,开发机上测出来的数字没有参考价值。同一份代码,开不开编译优化、数据放不放在 TCM 里,延迟能差出两三倍。

2.3 方案选型的决策树

方案选型不用搞得太复杂,按下面的顺序问自己三个问题就够了。

第一个问题:工具链支持训练后量化(PTQ)吗,校准数据够不够?如果模型结构规整、校准集能覆盖真实分布,先走 PTQ,成本最低,一两个小时就能看到结果。

第二个问题:PTQ 掉点是否在可接受范围内?一般分类任务掉 1 个点以内可以接受,检测和分割对定位精度敏感,阈值要卡得更紧。超了就先试混合精度回退,把敏感层保住。

第三个问题:回退之后还达不到要求吗?到这一步再上量化感知训练(QAT)。QAT 要改训练代码、要重新跑若干 epoch,成本高,但它能让模型在训练时就适应量化误差,通常能比 PTQ 多挽回一半以上的损失。

这个顺序不能颠倒。我见过不少团队一上来就上 QAT,结果发现 QAT 的训练配置本身就写错了,白白烧了几天算力。PTQ 是探路,QAT 是兜底,混合精度是中间的调节阀。

3. 第一步:模型体检与量化方案定档

这一步的目标很明确:在动手量化之前,把模型里哪些地方会出问题、出多大问题,尽可能地提前暴露出来。

3.1 逐层敏感度体检怎么做

敏感度体检的常见做法是逐层替换:把浮点模型的某一层单独量化成 INT8,其余层保持浮点,跑一遍验证集,看精度掉多少。掉得多的层就是敏感层。

这个做法准确,但费时间。一个几十层的模型,每层跑一遍验证集,如果验证集有几千张图,在开发机上也要跑上几个小时。实际工程里我用一个更粗但快得多的版本:不跑完整验证集,只跑几百个样本,看输出和浮点输出的余弦相似度或者均方误差。余弦相似度低于 0.99 的层标为可疑,再对这些层做精细验证。

还有一个更省事的替代方案:直接看激活值的统计特性。对每一层,统计校准集上激活的最大值、99.9 分位数、均值、标准差,算一个“长尾比”= 最大值 / 99.9 分位数。这个比值超过 10 的层,基本可以判定为量化敏感层,因为截断阈值一旦按最大值取,大部分有效信息就被压没了。这个方法不需要跑推理验证,一次前向统计就够,非常适合快速筛查。

体检的产出是一张表:层名、类型、长尾比、单独量化后的相似度、建议精度。这张表后面会反复用到,尤其是做混合精度回退的时候。

3.2 PTQ 和 QAT 的取舍

两者的差别在误差的来源和补偿方式。PTQ 是事后补救,模型已经收敛了,量化误差只能靠校准参数去逼近;QAT 是事前预防,在训练过程中插入伪量化节点,让梯度穿过量化操作(用直通估计器 STE),模型自己学会把权重往量化友好的方向调整。

PTQ 的优点是快,不用改训练代码,不用重新训练;缺点是遇到分布复杂的模型,精度上限有限。QAT 的优点是精度高,通常能接近浮点;缺点是要改代码、要调超参、训练不稳定时容易崩。

实际选择时看两个指标:一是 PTQ 后的精度差距,二是业务对精度的容忍度。差距在阈值内就收工,别为了多 0.3 个点去折腾 QAT。差距超出阈值,先看是不是校准集的问题、是不是某些层的问题,把能通过工程手段解决的先解决掉,最后再考虑 QAT。

QAT 有个容易被忽略的前提:伪量化节点插入的位置必须和最终部署时的量化方案一致。训练时按 per-tensor 插的节点,部署时用 per-channel,两边对不上,QAT 白做。所以 QAT 之前必须先把部署端的量化配置定死。

3.3 对称、非对称与 per-tensor、per-channel 的组合

这四个概念两两组合出四种配置,选错了直接影响精度。

配置适用对象优点风险
对称 + per-tensor权重(部分后端)实现简单,零点为 0 便于优化分布偏移时误差大
对称 + per-channel卷积/全连接权重精度高,各通道独立缩放需要后端支持逐通道缩放
非对称 + per-tensor激活(默认选择)能表达有偏移的分布需要额外的零点处理
非对称 + per-channel激活(部分后端)精度最高计算和存储开销大

权重的分布通常围绕 0 对称,用对称量化就够,配合 per-channel 能几乎无损。激活用非对称,因为 ReLU 之后的分布明显偏斜,零点不为 0,非对称能多挤出一些有效位。

这里有个实操判断:如果你的目标后端只支持 per-tensor,那深度可分离卷积那几层要格外小心,必要时把它们单独回退到高精度,或者把深度卷积和逐点卷积在图上合并考虑。硬上 per-tensor 量化,掉点往往就出在这几层。

4. 第二步:图清理与算子可量化改造

量化工具不是万能的,它只能处理它能理解的图结构。很多掉点和部署失败,根源在量化之前——图里有奇怪的算子、有可以折叠但没折叠的结构、有布局来回转换的多余节点。

4.1 量化友好的图长什么样

一个量化友好的图,大致满足几个特征:算子种类少且都在目标后端支持列表里;没有动态 shape 和动态控制流;卷积后面紧跟 BN 和激活,能被融合成一个算子;张量的内存布局从头到尾一致,不需要反复 permute;没有无意义的 reshape 和 slice 链条。

反过来,下面这些结构是量化的天敌:自定义算子、动态索引、复杂的广播机制、把张量拆成多路再拼回来的分支结构、跨层的注意力计算中出现的 softmax 和 layernorm。不是说这些结构不能量化,而是它们要么需要后端专门支持,要么会引入额外的量化误差。

图清理的产出应该是一份算子清单,逐条对照目标后端的支持列表打勾。打不了勾的,要么替换成等价的可支持算子,要么标记为回退高精度。

4.2 BN 折叠与算子融合的收益计算

BN 折叠是性价比最高的一步,几乎没有精度损失,还能直接省掉一次完整的逐通道乘加。

原理很简单:卷积输出y = W*x + b,BN 做的是z = gamma * (y - mean) / sqrt(var + eps) + beta。把 BN 的参数吸收进卷积,得到新的权重和偏置:

scale = gamma / sqrt(var + eps) W' = W * scale b' = (b - mean) * scale + beta

折叠之后,推理时只需要一次卷积,BN 完全不出现。收益可以算一笔账:假设某层输出通道 128,特征图 56×56,BN 本身要做 128×56×56 次乘加,约 40 万次运算,折叠后归零。整个模型有几十个这样的层,累计省下来的运算量相当可观,在算力紧张的核上,延迟能降 5% 到 15%。

除了 BN 折叠,还有几个常规融合:卷积 + 激活融合、卷积 + 加法(残差)融合、逐点卷积与深度卷积合并调度。融合的价值不只是省计算,更重要的是减少中间张量的落内存次数。每多一个中间张量,就要多一次写 SRAM 和一次读 SRAM,对于带宽受限的嵌入式核,这个开销往往比计算本身还大。

注意:BN 折叠必须在量化之前做,而且要用校准集重新统计折叠后的激活分布。折叠改变了数值范围,拿折叠前的统计去做校准,阈值必然偏。

4.3 不支持的算子怎么处理

遇到后端不支持的算子,有三条路可走,按优先级排。

第一条是等价替换。比如某些后端不支持带 dilation 的卷积,可以用插入零值的方式等效成大核卷积;不支持某些激活函数,可以用查表或者分段线性的近似实现。替换的前提是数值等价性要在容差内验证过。

第二条是算子拆分。把一个大算子拆成几个后端支持的小算子,代价是多了中间张量和调度开销。拆分时要算清楚收益:拆完之后如果多了三次内存往返,省下的那点计算可能还不够填坑。

第三条是回退高精度。这个算子保持浮点,前后插入量化和反量化节点。代价是这两次转换的开销,以及可能打断整个图的定点流水。回退要尽量放在图的边缘或者分支上,避免出现在主干路径中间。

我个人的经验是,回退的算子数量控制在总算子的 5% 以内,性能损失基本可接受;超过 10%,就要重新考虑是不是该换个后端或者换个模型结构了。

5. 第三步:校准集与校准算法落地

校准是 PTQ 的灵魂。同一份模型、同一个工具,校准集换一换、算法换一换,精度能差出好几个点。这一步值得花时间。

5.1 校准集怎么选、选多少

校准集的作用是估计每个张量在真实输入下的数值分布,所以第一原则是分布要贴近真实场景,不是越干净越好,也不是越多越好。

常见错误是直接拿训练集的一小部分当校准集。训练集经过清洗和筛选,分布和线上实际输入有偏差;有些场景下这个偏差还很大,比如光照变化、噪声水平、目标尺度分布。正确做法是从验证集或者真实采集的数据里抽,覆盖各种边界情况:最暗的、最亮的、目标最大的、目标最小的、遮挡严重的。

数量上,100 到 500 个样本通常够用。太少(比如 20 个)统计不稳定,同一份模型跑两次校准结果都不一样;太多(几千个)收益递减,校准时间线性增长。我一般取 200 个左右,先跑一遍看逐层统计的方差,方差大的层再针对性补充样本。

还有一个细节:校准样本要打乱顺序,不要按类别聚在一起。有些工具是按批次统计的滑动平均,样本顺序会影响最终结果。

5.2 四种校准算法的对比与选择

主流校准算法有四种,适用场景不同。

算法原理优点缺点适用场景
MinMax直接取最大最小值实现简单、无截断误差对离群值极敏感分布均匀、无长尾
百分位截断取 99.9% 或 99.99% 分位抗离群值百分位需调参通用场景的稳妥选择
KL 散度最小化量化前后分布差异自适应强、精度好计算稍慢激活分布复杂的模型
MSE 最小化最小化反量化误差平方和对回归类任务友好对分类任务不一定最优检测、回归头

默认我推荐 KL 散度,它在多数视觉模型上表现稳定。做法是把激活值统计成直方图(通常 2048 个 bin),然后枚举截断阈值,把超过阈值的部分合并到最后一个 bin,计算量化前后两个分布的 KL 散度,取散度最小的那个阈值作为 S 的依据。

百分位截断是更省事的替代,效果接近 KL,调参更直观。如果你的模型激活里有明显的极端离群值,先用百分位截断兜底,再考虑 KL。

MinMax 只在分布确实均匀的时候用,一旦有长尾,它会把大量有效值压成同一个整数,精度崩得很快。很多“量化后精度暴跌”的案例,最后一查就是默认用了 MinMax。

5.3 KL 校准的实操细节

用伪代码示意一下核心逻辑,方便你对照自己工具链里的实现:

def kl_calibrate(hist, num_bins=2048, target_bins=128): # hist: 激活值直方图,已按最大值归一化 candidates = range(target_bins, num_bins) best_threshold, best_kl = None, float("inf") for t in candidates: # 把 [0, t] 映射到 target_bins 个量化格 p = hist[:t].copy() q = quantize_hist(p, target_bins) # 量化后分布 # 把超出 t 的部分并入最后一格 p[-1] += hist[t:].sum() kl = entropy(p) - entropy_of(q, p) if kl < best_kl: best_kl, best_threshold = kl, t return best_threshold / num_bins * hist_max

几个容易踩的点。第一,直方图的 bin 数不要设太小,1024 以下会损失分辨率,导致阈值估计不准;2048 是常见折中。第二,KL 校准是逐张量做的,不同层的阈值互相独立,不要拿全局阈值套所有层。第三,校准完之后要把每层的 S 和 Z 存下来,部署时直接用,不要指望部署端重新算一遍——端侧没有校准数据。

还有一点:如果你发现某层的校准阈值明显偏离直觉(比如把 99% 的值都截掉了),先别急着调算法,回头看看这一层的输入是不是本身就有问题,比如前面某层的输出爆炸了。校准结果异常往往是上游问题的症状。

6. 第四步:量化执行与混合精度回退

到这一步,配置都定了,开始真正把浮点图转成定点图。这一步的重点不是“跑通”,而是“跑对”。

6.1 量化配置清单

执行前把配置逐项确认一遍,能省掉大量返工。下面是我常用的清单。

配置项推荐值说明
权重量化方案对称 + per-channel卷积和全连接层默认开启
激活量化方案非对称 + per-tensor后端支持时可选 per-channel
量化位宽INT8混合精度时个别层用 INT16
校准算法KL 散度有离群值时改百分位
校准样本数200 左右覆盖各类边界场景
首尾层精度保持 FP32 或 INT16输入输出层默认回退
BN 折叠开启必须在量化前完成
量化粒度对齐与部署端一致QAT 与 PTQ 都要遵守

这张表里最容易被忽略的是最后一项。训练侧和部署侧的量化粒度不一致,会导致两边算出来的量化参数对不上,精度莫名其妙地掉。定配置的时候就把部署端的限制摸清楚,别等部署了才发现。

6.2 敏感层回退与混合精度

混合精度的本质是“花钱买精度”:把最敏感的少数层保留高精度,用少量的性能和存储代价换回大部分精度。

回退策略按优先级排:先保首尾层,再保体检表里长尾比最高的几层,最后保深度可分离卷积层。每次回退一层,跑一次验证,记录精度和延迟的变化。这里要做的是边际收益分析——回退某一层带来的精度提升除以它带来的延迟增加,按这个比值排序,从高到低回退,直到精度达标就停。

回退的代价要提前算清楚。一层 INT8 卷积改成 FP32,在某些没有浮点单元的核上,延迟可能翻十倍;在有 FPU 的核上可能只多 50%。所以回退方案必须结合目标硬件来定,不能只看精度。INT16 是中间的折中选项,很多后端支持 INT16 累加,精度比 INT8 好不少,性能损失比 FP32 小得多。

6.3 逐层误差定位的方法

量化跑完之后如果精度不达标,不要急着整体调参,先定位误差从哪一层开始放大。

具体做法:准备一批校准样本,同时跑浮点模型和定点模型,在每一层输出处取特征图,计算两者的余弦相似度和相对误差。正常情况下,误差应该逐层缓慢累积;如果某一层突然从 0.999 掉到 0.95,那一层就是问题源头。

定位到问题层之后,按这个顺序排查:这一层的校准阈值是不是被离群值带偏了;这一层的输入是不是本身就有异常值;这一层的算子是不是走了不支持的路径被拆分了;这一层的 per-tensor 量化是不是把某些通道压死了。四个问题问下来,大部分问题都能找到答案。

7. 第五步:精度回归与端侧实测

开发机上跑通不算数,端侧的数字才是最终答案。这一步要同时盯精度、延迟、内存三件事。

7.1 精度指标体系不能只看 top-1

分类任务看 top-1 和 top-5 就够,但检测、分割、关键点这类任务,光看一个总指标会掩盖局部问题。

检测任务要分开看 mAP、小目标 AP、大目标 AP。量化误差往往对小目标更不友好,因为小目标的特征响应弱,量化噪声的相对影响更大。如果总 mAP 只掉 0.5 但小目标掉了 3 个点,这个模型上线后在实际场景里会很难受。

分割任务要看 mIoU 和边界区域的精度。量化误差容易在边界处放大,整体 mIoU 可能只掉 0.3,但边界模糊明显,视觉效果差很多。

回归类任务建议同时看绝对误差和相对误差的分布,不要只看均值。量化误差的分布往往是有偏的,均值好看不代表没有长尾。

7.2 端侧延迟测量方法

延迟测量的坑比精度还多。几个必须遵守的原则:

第一,固定主频和电源模式。动态调频会让测量结果飘忽不定,测之前把频率锁死,或者至少记录每次测量的频率。

第二,充分预热。第一次推理包含缓存冷启动、权重从 Flash 搬运等开销,不能计入稳态延迟。一般预热 10 到 20 次再开始计时。

第三,多次测量取中位数。取平均值容易被偶发的长尾干扰,中位数更能代表稳态表现。

第四,区分纯推理时间和端到端时间。纯推理只算模型前向,端到端还包括预处理、后处理、数据搬运。业务关心的是端到端,但排查性能瓶颈时要看纯推理。

第五,用硬件周期计数器而不是软件计时。软件计时精度受调度影响,周期计数器能精确到单个指令周期,定位瓶颈时非常有用。

测量结果的呈现建议分三档:最小延迟、中位数延迟、95 分位延迟。最小延迟代表理想情况,中位数代表典型情况,95 分位代表最坏情况。实时系统要看 95 分位,不能只看平均值。

7.3 内存与功耗实测

内存要分三块看:权重占用的 Flash、激活和中间张量占用的 SRAM、以及栈和堆的峰值。

权重占用相对固定,量化后基本就是参数个数乘以位宽除以 8。激活占用是动态的,取决于推理时的张量生命周期,好的内存规划能让多个张量复用同一块 SRAM。峰值 SRAM 占用要实测,不能靠估算,因为框架的内部缓冲、算子临时空间都不在纸面计算里。

功耗测量需要外部工具,但如果手头没有,可以用推理时间乘以平均电流粗略估算。量化之后不仅计算量减少,权重搬运量也减少,功耗下降通常比延迟下降更明显,这对电池供电的设备是实打实的收益。

8. 常见问题排查速查表

下面这些是我在实际项目里反复遇到的症状,整理成表,出问题时按症状查。

8.1 精度塌方的典型症状与对策

症状可能原因排查动作对策
量化后精度直接归零校准集与真实分布严重不符检查校准样本是否与训练数据同分布重新选校准集
某一类目标精度暴跌该类激活分布特殊,被截断逐层比对特征图该类相关层回退高精度
整体掉 2 到 5 个点用了 MinMax 校准,离群值拉偏阈值看各层校准阈值改 KL 或百分位截断
输出全是同一个类别最后一层量化溢出或零点错误检查输出层 S/Z输出层回退 FP32
检测框位置普遍偏移回归头量化误差对比回归头输出回归头用 INT16 或 FP32
深度可分离层掉点明显per-tensor 量化压死小通道逐通道看数值范围改 per-channel 或回退

这张表覆盖了大部分常见情况,但排查时要有顺序:先确认校准集,再确认校准算法,再看敏感层,最后才怀疑工具链本身。工具链出错的概率远低于配置出错。

8.2 速度不升反降的原因

量化完发现比浮点还慢,这事不罕见,原因通常在这几个地方。

一是量化与反量化节点太多。图被切得七零八落,每两个定点算子之间夹一次反量化和量化,转换开销把省下的计算全吃掉了。解决办法是尽量减少回退层,或者把回退层集中到图的边缘。

二是后端没有真正用上定点指令。有些工具链量化后仍然走通用路径,没有调用硬件乘加指令,位宽减了但指令数没少。这种情况要检查后端配置和算子实现,必要的话换后端。

三是内存搬运成了新瓶颈。量化减少了计算,但如果内存布局不友好、缓存命中率低,瓶颈从计算转移到带宽,整体时间未必下降。这时候要做的是布局优化和张量复用,而不是继续压位宽。

四是per-channel 量化引入了额外开销。逐通道缩放需要每个通道单独乘一个系数,在通道数多、特征图小的层上,这个开销占比不小。如果后端对 per-channel 支持不好,某些层退回 per-tensor 反而更快。

8.3 一张表总结五步的关键检查点

步骤最容易出错的地方一句话检查
体检跳过体检直接量化敏感层名单有没有
图清理BN 折叠放在量化之后折叠和量化的先后顺序对不对
校准校准集不覆盖边界场景最暗最亮最小最大样本在不在里面
执行训练侧和部署侧粒度不一致两边的量化配置逐项对齐了吗
实测延迟测量没有预热和锁频测量条件写清楚了吗

9. 踩过的坑和几句实在话

量化这件事,工具链能解决的问题其实不到一半,剩下的都在流程和细节里。我自己印象最深的一次,是一个图像分类的示例模型,PTQ 之后精度掉了四个多点,怎么调校准算法都没用。折腾了两天才发现,校准集是从训练集里直接切的,而训练集已经做过归一化和裁剪,和板子上实际采集的原始图像差了一截。换成真实采集的数据重新校准,精度只掉零点几个点。问题从来不在算法,在于我默认了“训练数据就代表真实分布”。

还有一个反直觉的体会:不是所有层都值得量化。有些模型里,某一层的计算量占比很小,但量化后对精度影响很大,把它保住浮点,整体延迟几乎不变,精度能回来一大截。做边际收益分析,比无脑全量化要划算得多。

最后分享一个实用习惯:每次量化实验都留一份完整的配置快照,包括校准集的文件列表、每层的量化参数、工具链版本、编译选项。量化结果对配置极其敏感,没有快照,两周后你复现不出同一个结果。这个习惯救过我很多次,尤其是在需要向别人解释“为什么这次和上次不一样”的时候。

量化没有标准答案,只有针对你的模型、你的硬件、你的业务指标调出来的那一组参数。五步流程的价值不在于每一步多高深,而在于它把一件混沌的事拆成了可验证的小步骤,让你每次只改一个变量,每次都知道自己在改什么。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进