TT Lab
开始
学习 学习路径 课程

AI瘦身失败事件

并不是每个算子都能变成 int8

在 TT Lab 中继续学习

一句话总结

量化工具只会把自己认识的算子改成 int8 的样子。其余的仍然是 float,在这些边界处就会产生 Q/DQ 往返。

为什么这是个问题

运行 quantize_static 之后文件变小了,似乎事情就完成了。可是打开计算图一看,有一半还是老样子。量化工具里有一张表,写着哪些算子该怎么处理,表里没有的算子,它就不碰。没碰过的算子必须用 float 计算,所以前面变成 int8 的值要重新解开成 float,后面再重新装回 int8。

所以“量化了”和“以 int8 运行”是两回事。如果把这两者当成一回事,就会发现既没有达到预期的速度,也没有变得那么小,却找不到原因。原因都写在计算图里——数一数就知道了。

读 QDQ 版本的方法

静态量化的 QDQ 格式不会替换算子。它在每个张量上插入 QuantizeLinear 和 DequantizeLinear。计算图里的 MatMul 仍然写着 float,而执行器会识别出 DQ -> 연산자 -> Q 这一组,把它换成 int8 内核来运行。

所以只看计算图,要判定“哪些能走 int8”,就需要规则。这个实验这样规定:所有输入都来自 DequantizeLinear、所有输出都只流向 QuantizeLinear 的算子,视为被 int8 包裹。只要有一处不符,这个算子就仍然是 float。

감싸인 모양                        남은 모양
  DQ -> MatMul -> Q                 DQ -> MatMul -> Add -> Relu -> Q
        (int8 커널로 접힌다)                    (Add·Relu 는 float 섬)

仍为 float 的算子如果彼此相连,就是一整块。我们把它叫做 float 孤岛。孤岛不只看个数,边界数 更重要。每个孤岛都会附带流入的 DequantizeLinear 和流出的 QuantizeLinear。孤岛越多,边界越多,每个边界上都要多一次值的转装和舍入。

实测中还有一点很显眼。如果不收窄范围就做量化,计算图里的 Relu 会消失。这并不奇怪,而是它被折叠进了一个边界。如果把随后的 QuantizeLinear 的零点(zero_point)定在 int8 的下端 -128,能装下的值的范围就恰好从 0 开始,负数一装进去就被截成 0。Relu 做的事,由边界代劳了。在这个实验的模型上测一下,隐藏层的零点确实是 -128,表示范围从 0.0 开始。不去数一数,就无法解释 census 里 Relu 为什么是 0。

还有一点。动态量化是完全不同的样子。MatMul 换成 MatMulInteger,并附上 DynamicQuantizeLinear。如果把测量 QDQ 版本的那把尺子原样用在这里,就会得出离谱的答案。尺子必须按格式重新确定。

现场要做的三件事

第一,收窄范围。如果某一层的准确率崩了,只要把那个节点去掉就行。工具用 nodes_to_exclude 接收节点名称,用 op_types_to_quantize 接收算子类型。按名称去掉,该节点的权重就保持为 float;按类型收窄,该类型之外的算子就整个变成 float 孤岛。无论哪种,都要 数一数确认改变了什么——不要以为给了选项就照办了。

第二,改模型,让它变成可量化的样子。MatMul 后面跟 Add 的形状非常常见,把这两个合并成一个 Gemm,中间那个张量就消失了。中间张量一消失,附在它上面的那一对 Q/DQ 也随之消失。值保持不变,只是边界减少了。实测中,三层模型的 QuantizeLinear 从 7 个减少到 4 个。

收窄范围时有一点常被忽视:收窄不是免费的。去掉某个节点后,该节点用 float 运行,但它前后的 Q/DQ 大多还在。也就是说,出现了失去 int8 的好处却要付出边界成本的位置。所以“准确率不稳,先去掉再说”这种判断,也要数过再下。把去掉之后的孤岛个数和边界数,与没有收窄的版本并排摆放,失去了什么、得到了什么,就以数字的形式留了下来。

第三,数出来再报告。算子 census、Q/DQ 的个数和往返数、孤岛的个数和大小、边界数。有了这五个数字,就能用计算图回答“为什么没有缩小到预期”这个问题。在测时间之前,先读计算图要快得多。

实际工作中真正重要的事

下一项实验要做什么

把同一个模型导出成五种版本,再一步步扩展分析计算图的分析器。依次数出算子 census、Q/DQ 往返、float 孤岛、每个孤岛的边界数,再给出两个收窄范围的版本与原版本的差异。最后把 MatMul 和 Add 合并成 Gemm 重新量化,数一数 Q/DQ 少了多少个。评分器每次都会用不同的层数和不同的排除节点构建自己的模型,真正运行你的分析器,并数出同样的数来核对。