系列目录 · 算子与布局 · Read in English
如果让人选两个最容易实现的算子,Abs 和 Sign 很可能榜上有名。一个取绝对值,一个判断正负。于是有人决定从现成二元算子复制一份代码,把两个输入都填成同一个值,再换一下操作码。
编译成功了。结果也出来了。接下来,那个本该最简单的算子,花掉了一整段调试时间。
真正的疑问是:“二元参数结构长得一样,是否意味着每个字段在一元指令里含义也一样?”
从量化关系推导参数
先写教学均匀量化关系:
1 | x_real = sx * (qx - zx), sx > 0 |
对 Abs,有 y_real=|x_real|。若先暂时限定输出 zero point 为 0,理想整数关系为:
1 | qy ≈ round((sx/sy) * abs(qx-zx)) |
由此能看见至少两个关键参数:输入 zero point 与输入输出比例之比。它不是把 x+x 的二元量化系数套进去就行。历史修改为 Abs 单独构造参数,将比例转换为定点乘数和位移,并设置输入 zero point。文章可以准确说“参数构造变得针对 Abs”,不能推断所有非零输出 zero point 语义都由这段差异解决。
举一个重新设计的例子:sx=0.25、zx=5、sy=0.5。整数输入 qx=1 对应真实值 -1,Abs 后为 1,输出整数应为 2。若直接对原始整数 1 取绝对值再解释,得到的是另一个值。错误来自忽略了量化原点,不是绝对值本身算错。
Sign 更容易误用同一套缩放。因为 sx 为正,符号由 qx-zx 决定,比例大小不会改变正负。若底层指令直接输出原始的 -1、0、1,则它主要需要输入 zero point,而不应再按一个普通输出比例解释成很小的小数。历史实现单独构造 Sign 参数,并在直接写出 Sign 结果时调整模型输出元数据,使这些整数按单位比例、零偏移解释。
“结果整数是对的,为什么对比工具认为错了?”这类问题常出在输出解释层。比如原始整数 1 本来表示 Sign 的正值,元数据却告诉上层乘以 0.125,结果自然成了 0.125。机器输出与宿主解释必须共用一份契约,不能只修计算,不修包装。
同时需要坦诚边界。历史检测针对特定的直接 Store(Sign) 输出形态;如果 Sign 后面还有其他算子,或者绕过另一种包装,不能仅凭这个输出修正保证整条图的量化语义。更通用的设计可能把原始符号值的类型直接表达在 IR 中,并在需要其他量化编码时显式转换。这是建议,不是已查看差异里的既成事实。
完整链路与数值测试
初始一元支持覆盖的链路相当完整:算子库增加入口,内核装载地址与参数,发射器创建指令对象,指令提供文本与二进制表示,代码生成注册到统一入口。初始提交同时还接入了乘法,尽管标题聚焦一元算子。阅读变更必须看文件和行为,不能把标题当作完整目录。
为什么仍然会后续修正参数?因为端到端“能编译”只能证明连接打通,不能证明每个字段符合数值协议。最小模型有助于把问题缩到一个算子,但若输入输出比例相同、zero point 为零,某些错误可能被对称性遮住。真正验证参数,需要故意打破这些巧合。
本文建议的 Abs 数值矩阵至少包括:负数、零、正数;不相等的输入输出比例;非零输入 zero point;整数最小值附近;输出饱和边界。整数最小值的绝对值可能无法用相同有符号位宽表示,这时应由明确的饱和、扩宽或其他目标约定决定,不能由宿主语言溢出行为碰运气。
Sign 的矩阵则以量化原点为中心:zx-1、zx、zx+1,再检查输出整数与解码后的实值是否分别符合约定。更改输入比例而保持这些整数关系,可以验证符号判断没有错误地依赖比例。对于特殊浮点值的支持,需要另查对应算子语义与目标实现,不能从整数测试顺带推出。
地址、组合模型与成本
算法正确以后,地址仍然可能错。历史另一次修改为 Abs 引入显式物理步长循环:多通道块按物理对齐步进,单通道时按行处理,并区分分组内部的紧凑路径与其他路径。Abs 不改变 shape,并不意味着它可以把整个内存视作无缝的逻辑向量。padding 恰好证明“同形状”与“紧凑连续”不是同义词。
这提供一个很有用的定位次序。先让 shape 足够小、内存布局简单,验证数值参数;再保持数值不变,改变通道与宽度,使其跨越对齐边界,验证地址;最后放回完整图,验证元数据和相邻算子。一次同时改变比例、形状和后处理,失败时就很难知道是哪张契约没有兑现。
历史还扩展过一组更高层模型用例:深度卷积、归一化、参数化激活和切片,脚本检查 lowering 后是否出现期待的原语,以及旧高层算子是否消失。它们的意义在于验证基础算子组合能否支撑更复杂表达式。比如参数化激活可涉及正负分支、减法、乘法和相加;基础算子局部正确,组合仍可能因广播与量化边界失配而失败。
这些脚本主要提供结构性断言,不能自动证明数值精度或设备性能。本文建议再加入分阶段参考结果:每个原语输出的逻辑值、最终组合值,以及必要的误差阈值来源。阈值应该与量化误差分析或产品要求相关,不能只因某个宽松数字让测试变绿就采用。
性能方面,一元算术通常很便宜,所以参数装载、循环分支和数据搬运可能占较大比例。将参数放近一些以复用已有基址,或在有效立即数范围内使用短装载路径,都是可能的工程手段;相关内核确实区分近远参数地址。
参数协议的边界
为什么不能让所有一元算子共用一张“通用量化参数表”?结构体可以共用,含义未必共用。Abs 需要幅度缩放,Sign 主要需要零点比较,某些其他函数需要查表输入坐标。若共用构造函数时把无关字段也填进去,短期可能没有问题;当指令模式改变、某字段不再被忽略时,就可能突然暴露潜在依赖。按算子写清消费哪些字段,比盲目填满所有字段更可审查。
定点比例还有精度问题。把实数比例表示成整数乘数与右移,并不一定精确;比例很小、很大或恰好落在舍入边界时,误差表现不同。工程上可以对一组教学比例计算实际表示误差,再把它传播到最大输入幅度,估计最坏输出偏差;最终仍需与目标舍入和饱和顺序一致。
对 Abs,先减输入 zero point 再取绝对值也涉及中间位宽。教学上若输入存储很窄,差值的范围可能比原存储更大,绝对值后也可能超过正向上界。因此要明确设备是在扩宽后计算、饱和到原宽度,还是使用其他定义。直接用宿主语言同宽整数写参考,很可能让参考程序和被测程序一起犯错。独立参考应使用足够宽的整数或明确的饱和函数。
对于 Sign 的图内使用,可以再做一个问题链:输出是否保留原始符号整数?下一算子把它当作单位比例,还是仍按旧量化类型解码?如果需要重新编码,转换发生在哪条边?仅在最终模型输出改元数据,可能解决主机读取,却无法改变已在图内消费错误数值的算子。这是为什么本文强调直接输出特判的范围。更强的设计必须把数值域表达给所有消费者。
参数装载还需要验证近远地址两条分支。近地址可以在已有基址上用偏移读取,远地址则需要重新装载完整位置。如果测试永远把两份参数挨在一起,就无法发现远地址分支把基址覆盖后没有按预期使用的问题。用很小的算子测试不同参数距离,能让地址装载错误与算术错误分离。这里比较的是生成结构与地址关系,不需要公开任何真实设备内存布局。
最后,组合模型可以用来检查基础算子接口是否稳定,但应保留足够小的单位测试。一个归一化模型最终失败,可能来自归约、倒数平方根、广播或常量编码;单独的 Sign 原点三点测试却能非常直接地指向比较与输出解释。大模型提供真实组合,小模型提供明确因果,两者的职责不同。将二者配合起来,才能避免每次遇到复杂图都从第一条指令重新猜起。
这个故事适合留给每一次“复制一个相似算子”的代码审查:请先把实数公式、量化公式、指令字段和输出元数据写在同一张纸上。只要其中一项无法对应,就先别庆祝算子简单。最短的数学表达式,也值得拥有自己的参数说明书。
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 !