系列目录 · 工程与交付 · Read in English
“形状、类型、内存大小都对,怎么还差一个像素?”
排查人盯着放大后的特征图,发现每隔几列就有一条细小的差异。另一边,池化测试只有边缘不一致。大家把它们当成两个独立问题,后来才意识到,它们问的是同一句话:输出坐标到底怎样对应输入?
形状描述了有多少个答案,却没有定义每个答案如何计算。窗口边界、采样位置、舍入规则和索引选择,才决定了形状里面装什么。
平均值的分母,可能藏在 padding 里
用一维教学例子说明。输入为 [2,6],窗口大小为 3,左侧需要补一个位置。第一个窗口可以被理解成 [padding,2,6]。
如果 padding 参与平均,且其数学值为零,结果为 8/3。如果只统计真实输入元素,结果为 8/2=4。两者输出形状相同,都是“平均池化”,差别却不是浮点误差。
实现时有几个容易混在一起的数量:
- 窗口的逻辑大小。
- 当前窗口实际覆盖的输入元素数。
- 对齐后执行单元真正处理的槽位数。
第三个数量通常不应该自动进入数学分母。若为了向量化补齐了执行槽位,应确保这些槽位不会改变求和或计数。
MaxPool 也有类似问题。边界补入数学零,在真实输入全为负数时会错误地成为最大值。实现可能使用掩码、足够小的值或硬件的边界行为,具体方式要与数据类型和规范一致。不能把“给内存清零”当作通用 padding 策略。
全局池化与 ReduceMean,不能只按名字合并
对于某些输入布局和轴集合,全局平均池化与对空间轴做 ReduceMean 可以表达相同数学计算。这种相等需要条件:归约轴一致、输出维度规则一致、数值类型和舍入行为兼容。
如果一个操作保留被归约的长度为 1 的维度,另一个直接删除这些维度,那么后续广播就可能不同。对于量化计算,是否先累加再做一次缩放,也会影响精度。
历史规范梳理将池化、归约分开描述,这种组织方式提醒我们:可以共享底层内核,但共享之前应证明语义对应关系。把名称相近的操作统一转成某个低层指令,是一个实现选择,不是证明。
最近邻:选“最近”的谁
考虑把长度为 3 的序列缩放到长度为 5。至少存在两种常见的连续坐标思路:
1 | 端点对齐:x = j * (3 - 1) / (5 - 1) |
前者在 j=1 时得到 0.5,后者得到 0.4。随后还要决定怎样把连续坐标变成整数索引:向下、向上、四舍五入,或者采用指定的平局规则。
这只是教学中的两类映射,并不是说所有前端都默认其中某一种。编译器必须读取具体算子版本与属性,而不是看到 “nearest” 就选择熟悉的公式。
输出长度为 1 时,端点公式的分母也会暴露边界问题。生产实现应有明确规则,不能把这个情况交给浮点除零后再碰运气转换成整数。
双线性插值:四个邻居之外,还有坐标体系
在二维情况下,双线性插值通常使用横纵两个方向的权重组合四个邻近点。但“四个点加权”只描述了计算的后半段。
前半段仍要回答:
- 输出像素中心映射到输入哪里?
- 越界坐标先裁剪,还是让边界规则处理?
- 相邻索引怎样选?
- 权重用什么精度表示?
- 两个方向分步计算时,在哪里舍入?
数学上等价的分步表达,在定点实现中未必逐位相同。先水平插值并舍入,再垂直插值,与保留更宽中间值后统一舍入,可能产生不同结果。
如果输入是整数编码的量化值,还要确认 zero_point 的贡献怎样处理。权重和精确为 1 时,仿射映射有一些便利性质;有限精度权重若破坏了这个和,便需要更仔细的误差分析。不能由“输出仍是整数”倒推出实现采用了哪种内部精度。
这里的性能问题也很具体:权重和索引能否复用,边界路径是否引起额外分支,访存是否连续。未执行基准之前,不宜直接给某种计算顺序贴上“更快”的标签。
ArgMax 的结果是一个位置
ReduceMax 只需要传播最大值。ArgMax 还需要传播它的位置。这意味着归约状态更像:
1 | (best_value, best_index) |
当两个值相等时,选哪个索引,必须由算子契约决定。例如输入 [7,9,9,4] 的最大值是 9,但最大值所在索引可以按规则返回 1 或 2。一个树形并行归约如果随意改变比较的左右顺序,可能在数值最大值完全正确时返回错误索引。
浮点 NaN、带符号零以及索引输出类型也需要明确。这些情况的答案取决于具体运算定义;编译器的责任是忠实保留输入契约,并在无法实现时给出可理解的限制。
保持维度和负轴归一化同样属于语义。值归约与索引归约可以共享遍历结构,但后者不能仅靠“最后顺便记一个下标”就保证正确。
为什么随机测试常常抓不住这些问题
随机输入中出现完全相等最大值的概率可能很低,因此 tie-breaking 错误会藏很久。内部像素占比高的大图像,又可能稀释边界差异。若测试只比较整体均方误差,小块但系统性的坐标偏移也可能被容忍。
更有针对性的输入包括:
| 输入设计 | 容易揭示的问题 |
|---|---|
| 常量图 | 权重和、padding 与量化零点 |
| 单调斜坡 | 坐标映射、索引舍入 |
| 单个脉冲 | 插值核的邻域和边界 |
| 重复最大值 | ArgMax 平局规则 |
| 全负窗口 | MaxPool 错误补零 |
| 长度为 1 的轴 | 除零、特殊坐标路径 |
| 奇数尺寸缩放 | 比例不能整除时的索引计算 |
这些建议测试没有在历史仓库上运行。它们的作用是把模糊的“有一点误差”,拆成能被反例验证的假设。
从规范页走到实现,最后还差一张对照表
真正有用的算子规范不只写公式,还应把前端属性、归一化后的内部语义、底层能力和拒绝条件放在一起。
例如,若硬件只支持一种取整方式,就需要判断是否能用前处理补偿,是否有替代内核,还是必须拒绝某些模式。若文档尚未说明某个字段,保留“待确认”可以阻止后来的人把示意图误读成实现保证。
那条每隔几列出现的差异,最终不一定需要改一大段算法。它可能只需要纠正一行坐标映射。真正费时间的是在修正之前,团队一直拿“输出形状相同”当作两条路径等价的证据。
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 !