这个归约什么也没做,为什么还不能删?

数学恒等不代表量化编码恒等,空轴和单位轴也有契约。

Posted by Bruce Lee on 2026-01-22

系列目录 · 数值与量化 · Read in English

“沿长度为一的轴求和,就是它自己。删掉,省一条指令。”这句话数学上很稳,工程上却差一个问题:“它自己”是同一个实数,还是同一串整数?

有时删掉一个明明没有计算量的算子,反而让整个模型的数值变了。这样的优化像把温度计从摄氏刻度换成华氏刻度,却坚持显示数字不能动。

一、长度为一,不代表输出表示完全相同

设输入张量 shape 为 [1,5,7,9],沿第一轴归约。对求和、平均、最大最小而言,每组只有一个元素,实数值不变。保留维度时,逻辑 shape 也不变。

现在给它量化类型:

1
2
输入:q_in,scale = 0.03,zero_point = 0
输出:q_out,scale = 0.12,zero_point = 0

输入整数 20 表示实数 0.6。输出若仍保存整数 20,就表示 2.4;正确输出应接近整数 5。算子虽然没有改变实数函数,却承诺了输出表示转换。

一般关系为:

1
q_out = clamp(round[(s_in/s_out)*(q_in-z_in)] + z_out)

因此删除算子的充分条件,不是“归约长度等于一”,而是“归约的数值函数是恒等,且输出完整类型及相关语义允许直接用输入替代”。历史修复明确检查输入与输出类型一致才直接替换;类型不同则保留重量化操作。

二、空 axes 的两副面孔

还没讨论量化前,形状阶段就可能先制造一个陷阱。一个四维输入的首轴长度为一,形状规范化将它删除。原本 axes=[0],映射后没有任何有效轴。

可在许多内部表示里,axes=[]又约定表示“全部轴”。这两种空列表表面一模一样:

1
2
情况 A:用户原本要求全部归约。
情况 B:用户只要求归约一个后来消失的单位轴。

如果下游只看列表,就会把情况 B 解释成情况 A。结果不再是一个保留所有元素的恒等映射,而可能变成单个标量。空不是信息;空是信息被丢掉后留下的位置。

历史中先增加了记录被移除轴的元信息,并调整轴合法化在形状阶段之后运行;后来又把对这种特殊情况生成的重排和归约链简化为直传或重量化。两个步骤解决不同问题:前一步保存语义来源,后一步利用来源进行安全简化。

三、为什么不能拿 reshape 代替重量化

reshape 的职责是重新解释元素索引关系,在允许的前提下保持元素序列。它不是一般数值变换。把结果类型写成另一种量化尺度,并不会自动计算新的整数值。

教学伪代码应当写成:

1
2
3
4
5
6
7
8
assert normalized_input_shape == normalized_output_shape

if input_type == output_type:
result = input
else if supported_quantized_conversion(input_type, output_type):
result = requantize(input, output_type)
else:
report unsupported conversion

这里“支持”需要明确:整数存储范围怎么得出?有符号还是无符号?零点是否合法?位宽与计算中间值是否能表示?历史代码为输出整数存储类型计算夹取范围,并拒绝不能安全处理的宽度;这展示了保守失败的价值。比起靠默认范围勉强生成结果,早一点说明缺少支持,更容易定位。

另外,完整类型比较是否足够,取决于 IR 还把哪些契约放在属性里。例如舍入模式、饱和策略或布局标记如果不在类型里,就应一并检查。

四、同尺度测试为何能长期掩盖问题

假设输入输出 scale 都为 0.1,零点都为 0。无论你插入重量化,还是直接转发,很多常见输入都得到相同整数。用全零数据时,甚至不同尺度也可能看不出问题。

因此这种测试不应该“正常得太正常”。至少选择不相等的尺度、非零值、靠近舍入边界的值,并包含饱和情况。若是非零零点,还要测试表示实数零的那个整数;它通常不是整数 0。

举个专门设计的检查:

1
2
3
s_in = 0.03,s_out = 0.12
q_in = [4, 8, 20, -12]
理想未饱和 q_out = [1, 2, 5, -3]

这些数只用于说明比例,没有覆盖舍入。再加入不能被 4 整除的值,才开始检验具体舍入规则。应以目标定义计算期望值,不能拿宿主语言默认的 round 当作通用答案。

五、无用归约删掉以后,性能账怎么记?

历史修复把一条为了处理被删单位轴而构造的变换链,缩为直接输入或重量化。结构上减少了辅助节点,这是可以从差异直接观察的事实。

若输入输出类型一致,直接转发可能同时省掉配置、执行和临时缓冲区;但前提是下游允许别名,没有额外复制需求。若需要重量化,仍要遍历元素并读取、写入,流量通常随张量大小增长。此时“原归约长度只有一,所以成本为零”的说法是错的,输出表示转换本身有成本。

性能模型可以分成:

1
2
同类型路径:主要检查是否能消除物化与调度开销。
异类型路径:T ≈ 启动成本 + 元素数 / 有效吞吐,另计读写流量。

这里还可能有融合机会:重量化是否可并入后继算子?但融合需要保留原舍入点的语义,不能一看到两个乘系数就无条件合并。

六、回归矩阵:把“恒等”拆开来测

维度 待执行覆盖 预期检查
归约种类 sum、mean、max、min 单元素组的实数恒等性
类型关系 完全相同、仅尺度不同、零点不同 直传与重量化分流
形状来源 原三维、由四维删首轴 不把映射后空轴当全轴
混合轴 单位轴加非单位轴 仍保留真实归约
存储范围 有符号、无符号、不同位宽 clamp 边界来自输出类型
数值 零、非零、舍入边界、饱和边界 数值契约完整

历史测试包含了单位轴归约消失,以及不同量化尺度时保留重量化的结构断言。这比只检查“没有 Reduce”更强,因为后者会奖励一个删掉数值转换的错误优化。

这个故事的核心问题适用于很多地方:乘一、加零、只含一个元素的拼接、某些拷贝和类型转换,都可能在数学函数不变的同时携带表示契约。优化器需要证明的是整个契约可以消失,而不是纸上那一行公式看起来没有动静。

下一次看到“这不是恒等吗”的评论,可以继续往下写一句:“在哪一个数值空间里?”这通常能帮团队省掉一轮很有教育意义的排查。

七、把“可以删除”写成一份证明义务

一个更完整的优化判据可以分成三层。第一层证明每个输出实数等于对应输入实数;单位长度归约能解决这一层。第二层证明输出索引与输入索引一一对应;形状规范化和 keep_dims处理负责这一层。第三层证明直接复用输入的存储表示仍满足输出契约;量化尺度、零点、位宽及相关属性在这里接受检查。缺少任何一层,都不能直接用输入替换结果。

这份分层证明也能解释为什么“类型一样”并非所有场景下都足够。假如某个操作额外约定了复制语义、地址空间转换或可观察的异常处理,数值与类型相同仍可能不等价。本文讨论的纯张量归约没有把这些外部语义当作已存在条件,但通用优化框架应允许它们被表达,不能把所有看起来像恒等的节点都归到一个模式里。

对于重量化,可以再做一次零点手算。设输入尺度为 0.04、零点为 7,输出尺度为 0.08、零点为 3。输入整数 11表示实数 0.16,对应输出整数 5。若遗漏输入零点,会按 11直接缩放;若遗漏输出零点,则会得到 2。测试只看两者差值可能都像一个小偏差,但偏差在不同输入上的形态有助于区分比例错和零点错。

一个很好用的变形测试是:先选一组能够精确表示的实数,分别按两种量化域编码,再比较单位轴归约的输出是否对应同一组实数。它不要求固定某套原始测试向量,也不靠全零输入。若目标采用特定舍入模式,应优先选精确可表示点验证主公式,再单独测试舍入边界,避免两类问题混成一个难以解释的误差。

还可以做结构与数值的双向检查:同类型情形希望归约节点消失,但不应出现多余转换;异类型情形希望无用归约消失,同时保留必要的数值转换。只统计节点减少数量,会鼓励过度删除;只比较一个数值样例,又可能遗漏本该被消掉的大量物化。优化测试需要同时说明“什么必须消失”和“什么必须留下”。

这些判断即使不运行硬件,也能先在 IR层进行一轮证明审查。真正执行时,再确认重量化内核实现了同一套舍入与夹取规则,才能把结构正确连接到数值正确。


返回系列目录 · 上一篇 · 下一篇


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !