最大值、最小值和求和:长得像三兄弟,不能共用一份数学作业

归约框架可以共享,初始化、累加与重定标不能照搬。

Posted by Bruce Lee on 2026-01-18

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

“平均值已经有了,求和把除法删掉,最大最小换个符号,下午就收工?”

如果算子支持清单是一张菜单,这个提议十分诱人。可编译器接一个算子,除了厨房会做,还要让点单系统识别它、仓库知道食材放哪、服务员把它送到正确桌子。三个新菜名,可能触发十几处接入点。

一、算子从模型走到指令,至少要经过几道门

历史中的求和、最大值和最小值并非只增加一个内核函数。它们各自补充了算子发射入口、底层指令、后端注册、轴合法化与测试;最小值还补了上层归约模式到目标操作的映射。

这些差异能证明一条接入链逐渐完整,却不能仅凭文件数量断言端到端正确。每道门有自己的故障方式:

层次 它回答的问题 缺失时常见表现
转换入口 模型里的模式对应哪个操作? 导入成功,降低时找不到支持
图合法化 这个轴组合能否被目标处理? 少数轴成功,其他轴到后端报错
资源描述 哪个执行单元、什么缓冲区? 已生成 IR,却无法安排执行
代码生成 地址、形状、量化参数是什么? 类型看似合理,实际结果错位
指令编码 字段在二进制中如何表达? 汇编看起来正确,机器行为不同

因此“算子支持”最好有分层定义。某个 IR 节点能够打印出来,是第一步;能通过 lowering,是另一层;有数值验证和边界测试,才继续向完整支持靠近。

二、共享的是几何,差异在数值

对于输入 [A,B,C],若归约末轴,三种操作的输出形状都是 [A,B,1]。轴置换、有效秩检查、输出元素数检查和地址查找,可以共用同一套几何规则。历史实现把归约轴合法化推广为多个操作共享的模式,正是复用这个部分。

但数值不能只替换助记符。设 x=s(q-z),同一个归约组内采用统一正尺度:

1
2
3
sum(x_i) = s * (sum(q_i) - R*z)
max(x_i) = s * (max(q_i) - z)
min(x_i) = s * (min(q_i) - z)

求和的零点扣除 R 次,最大最小只对选出的值扣一次。硬件可能在累加时先逐元素减零点,也可能后处理,编译器必须知道是哪一种。仅看参数寄存器里写了 z,无法反推出执行单元完整语义。

正尺度保证顺序关系不变,所以统一量化域中的最大最小可以在整数上比较。若同一个归约组混入不同尺度,整数大小就不再等于实数大小。教学例子:整数 20 乘尺度 0.1 表示 2,整数 12 乘尺度 0.5 表示 6;直接选 20 会选错实数最大值。逐通道量化若跨通道归约,尤其需要注意这一前提。

三、求和有增长,极值有边界

若输入是有符号 b 位整数,每项绝对值有界,R 项求和所需位宽一般随 ceil(log2 R) 增长,但精确上下界要分别考虑最小值与最大值。零点修正、乘系数和舍入前的临时值,还可能扩大需求。

最大最小通常不因 R 增大而扩大选中值范围,却有别的坑:初始化值能否覆盖整个输入域?有符号输入是否被误当无符号?若目标支持浮点,NaN 与正负零如何处理?历史查看的整数量化路径不能证明浮点特殊值语义,因此这属于扩展实现时应明确的规则。

还有一个很实用的问题:多块归约如何合并?求和需要把各块和加起来;最大最小取各块极值即可。平均值若块大小不等,必须按元素数加权。把所有归约都抽成“先每块做一次,再对块结果做一次”,会在平均值上踩坑。三兄弟确实能共用书包,但作业要各写各的。

四、把三种指令合并成一个实现,改变了什么?

后续历史差异把三个重复的指令类合并为一个带种类枚举的公共实现,统一汇编输出、二进制字段和复制逻辑,保留不同操作种类。

这是减少实现重复的重构。它主要降低字段修复漏改某一种操作的风险。例如新增一个模式检查,如果三处复制粘贴,很容易只改到两处;公共编码让三者遵守同一约束。

这不意味着硬件把三条指令融合成一条,也不意味着原模型执行的归约次数减少。提交标题中的性能措辞,必须由实际差异解释。这里能直接看到的是软件表示合并;运行时间、二进制大小或编译耗时是否改善,需要额外测量。公开文章尤其不该把“合并类”写成“融合算子加速”。

公共实现也有边界。若平均值的寄存器布局、倒数处理或控制字段不同,强行放进同一个参数包,可能制造更多条件判断。抽象应该围绕稳定的共同结构,而不是围绕名字里都带 Reduce。

五、汇编测试和机器字测试为何要同时存在

历史新增测试同时检查了发射文本与二进制编码。这两种检查互补:

  • 文本检查帮助发现参数顺序、操作种类和默认模式错误。
  • 二进制检查帮助发现位移、掩码、符号位和字段宽度错误。

一个编码器完全可能打印“最大值”,二进制却选择“最小值”。如果测试也调用同一个编码辅助函数生成期望值,两个错误可能彼此认同。更可信的黄金值应独立于被测路径计算,或与可信解码器进行交叉校验。

但只验证一个机器字也不够。模式、位宽、符号、累加开关等字段的组合需要覆盖。若共享实现接收整数枚举,还应验证非法值被拒绝,避免无法识别的种类悄悄落到默认分支。

六、建议的回归矩阵与性能假设

类别 建议样例 观察目标
操作种类 sum、max、min、mean 对照 公共几何保持一致,数值各自正确
符号 全负、正负混合、无符号边界 比较与零点处理
归约长度 1、奇数、跨分块边界 单元素、尾块和累加行为
尺度 输入输出相同、不同 重量化和饱和位置
指令参数 各合法模式与非法值 文本与机器字一致
接入路径 模型转换、IR、发射器分别测试 防止只测到链条中段

这些是待执行建议。历史已有的测试文件证明作者考虑了编码与发射形态,不等于这里已经取得执行通过报告。

性能可先分成三个成本:读取输入的流量、归约树或串行累加的计算量、调用与配置成本。极值与求和都需要读同一批元素,但执行单元吞吐、数据依赖和类型扩展可能不同。小张量容易受配置成本主导,大张量更可能受到带宽或归约吞吐限制。公共 C++ 类的数量,不属于这个运行时模型中的关键项。

做完这一轮,最值得留下的不是“又支持了三个算子”,而是一张清楚的责任地图:什么可以统一,什么必须分别证明。下次再添一个归约成员,就知道该带它去哪个窗口办手续。

七、用零点反例检验抽象有没有偷懒

给一组教学量化数设置非零零点 z=5,整数数据为 6、7、8。它们在中心化整数域分别是 1、2、3。求和的中心化结果为 6,而直接求原始整数和得到 21;只减一次零点会得到 16,必须逐元素扣除或一次扣除三倍零点才正确。最大值则选出 8,再减一次零点得到 3。两条路径的参数表可能长得一样,内部使用方式却必须不同。

若输出与输入尺度不同,还要在归约后执行合适的比例转换。最大最小可以先在统一量化域选值,再换输出尺度,因为正的统一尺度保持顺序;求和需要确保累加没有在转换前溢出。若为了复用极值代码而把求和累加器也限制在输入位宽,模型会在归约长度增加时出现一种很典型的现象:短向量正常,长向量突然折返或饱和。

分块时还可以用恒等元检验初始化。求和的空累加起点是零,最大值应使用不大于任何合法输入的起点,最小值则相反。若把所有操作都初始化为整数零,全负数据的最大值就会错误地返回零;全正数据的最小值也可能错误地返回零。这个例子不依赖特定指令格式,适合放在任何后端的数值回归里。

对有符号与无符号比较,建议选择具有相同位模式却不同解释的值,检查发射器的符号模式确实传递到机器字。仅使用很小的正数无法区分两种解释。把这种辨别原则扩展到每个控制字段,就能写出数量不多、但每个都能排除一种错误实现的测试。

这些反例不是说历史实现一定犯过同样错误,而是说明为什么抽象评审不能停留在代码相似度。共享类应当集中稳定的表示规则,让数学差异显式可见;若为了减少行数把差异埋进默认值,下一次扩展反而更难证明正确。


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


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 !