系列目录 · 算子与布局 · Read in English
第一次遇到这种错误,很容易怀疑卷积核翻转、步长或 padding:单组结果很好,打开分组就不对。于是有人把边界参数来回改了三次,最终得到一种更令人不安的状态——某些输入又对了。
真正该问的第一句话可能是:“这块权重的类型写着什么,字节实际又按什么顺序存?”
逻辑形状与实际存储
张量形状是解释规则的一部分,不保证已经说完全部布局信息。尤其经过模型导入、权重预处理和格式规范化后,一个类型可能为后续算子保留某种逻辑视图,数据则已经按更适合后端的组块顺序重排。此时按类型某一轴直接切片,可能切出形状完全正确、内容来自不同组的混合物。
先回到通用数学。转置卷积不是“求卷积的逆”。它描述与某个卷积线性映射相对应的转置映射,常可以通过输入点向输出窗口散布贡献来理解。分组则规定不同输入通道组向各自的输出通道组贡献。无论用哪种计算实现,组间连接关系都不能串线。
教学设定为两组,每组输入 3 个通道、输出 2 个通道,空间核暂时取一个点,方便看清数据。假设导入后实际连续存储为:
1 | [group, output_in_group, input_in_group] |
两个组块各 6 个元素。若上层类型却写成 [2,6],朴素实现可能认为“第二个轴是全部输入通道”,于是每行取前三列给组 0、后三列给组 1。按类型重解释,第一行是整个组 0,第二行是整个组 1;这样切会把 a00...a02 与 b00...b02 拼到同一组,明显串线。
正确方案依赖已经确认的数据契约:先按连续组块切,再为每组建立普通转置卷积期望的权重形状。通用计算如下:
1 | elements_per_group = OC_per_group * IC_per_group * KH * KW |
已检查的历史实现正是显式读取连续数据、按组提取并重建权重,而不是机械复用普通分组卷积沿输出通道轴的切分办法。它还核对总元素数与各组元素数的乘积,拒绝存储大小不匹配。相似算子之间最危险的复用,往往发生在两段代码“看起来差不多”的地方。
为什么不能把“每组一块”当成普遍真理?因为它只对满足这个导入契约的表示成立。另一个框架或前一阶段可能按输入通道优先存储,或者在核空间上先打包。移植时必须重新证明:给定组号、输入组内通道、输出组内通道和核坐标,字节偏移公式是什么。
再往下看,提取函数为什么关心元素存储类型?因为“读成 float 再写回”不是永远无害。整型量化权重、半精度浮点或特殊浮点格式携带的位模式,应按正确的宽度和解释提取。历史代码为多个宽度与有符号类别选择了相应的数据读取方式;对于没有覆盖的格式,返回失败。更一般的做法可以用受控字节拷贝,但它仍需证明元素大小、对齐、字节序和位打包规则。压缩的低比特元素尤其不能照搬逐元素字节切片。
量化参数与通道身份
量化参数还会玩第二个把戏。对分组转置卷积,可能有每张量一套比例,也可能有每组输出通道数长度的一套共享参数,还可能按总输出通道列出所有参数。历史修改识别了这几种长度:空或单值保留;长度等于每组输出通道时复用;长度等于总输出通道时按组截取;其余长度拒绝。这样的规则必须和导入端的含义一致,不能仅凭数组长度“猜对”。
比如教学比例 [u,v] 可以表示所有组共享两个组内输出通道比例;[u0,v0,u1,v1] 则可以按组分配。二者长度都合理,但意义不同。公开的通用建议是:在 IR 中尽量显式记录比例沿哪个语义轴作用,减少后端靠长度推理。这里“更显式的表示”是设计建议,而不是宣称历史系统已经这样做。
偏置仍按总输出通道划分,单元素偏置则复用。输入按输入通道切片,各组单独计算,结果沿输出通道拼接。布局变换后,逻辑通道轴可能不再是同一个轴编号。因此测试既应验证刚 lowering 完的 Concat 轴,也应验证经过布局转换后的轴。历史测试确实关注这两个阶段,并继续推进到内存分配和代码生成。
小测试与性能成本
最有效的小测试不需要一大串随机数。给组 0 仅一个输入通道置 1,其余全 0;给组 1 另一个输入通道置 1。再让两组权重使用不同的低整数标签。如果输出出现另一组标签,数据路径的问题会比余弦相似度更直观。随后再补入非平凡空间核,以区分组错位与核位置错位。两种错误如果一起测试,往往像两个嫌疑人互相提供不在场证明。
性能方面,编译期重建常量权重会消耗时间与内存,但它可能把运行时转换成本搬走。这只是可能的交换,仍应测量常量是否复制多份、是否能共享只读原数据、序列化时是否再次复制。运行时的输入切片与输出合并是否造成搬运,要看下游实现。不要看到“编译期处理”就宣称免费;大模型编译峰值内存也是真实预算。
历史修改还补充了一个通道轴识别分支,使三维形状中间的通道位置能够被识别。这类小变化提示我们:算子支持经常跨越权重、图结构和相邻缩放算子的形状约定。文章若只写“增加分组转置卷积”,就会漏掉真正决定能否串起来的接口细节。
本文建议的拒绝测试包括:组数不受支持、通道不能整除、权重形状与契约不符、数据长度不对、量化轴不被支持、参数长度不属于任何已定义语义,以及非常规偏置形状。已存在的负例覆盖其中部分项目;未列在历史测试中的,应作为后续验证建议记录。
贡献映射与失败边界
如果权重顺序已经确认,仍有一种常见误会需要拆开:所谓“转置”到底转置了什么?用一维教学例子表示输入为两个位置,核有两个系数,步长为二。第一个输入位置向输出的前两个位置贡献,第二个向后两个位置贡献;若改成步长一,中间位置就会同时收到两份贡献。这个观察强调的是线性映射中连接关系的转置,并不意味着把权重张量最后两个维度交换一下就完成了算子定义。
对分组版本,可以把这张贡献图复制为互不连通的几张图。一个组中的输入脉冲,只应在本组输出通道产生贡献。先测试没有窗口重叠的情况,观察组和核位置;再引入窗口重叠,验证累加。这样能区分“取错组块”与“正确数据累加错位置”。如果一开始就使用大核、大步长、多组和复杂 padding,最后的误差图往往像一张抽象画,难以决定先查哪条索引公式。
“按连续块提取,会不会把原始权重里一些空洞也带走?”这正是需要明确数据契约的原因。如果常量存储已经包含填充、压缩头或块内重排,那么元素数量公式不能直接当作字节长度公式。教学实现假设组块包含普通元素序列;实际扩展到其他存储表示时,应把逻辑元素提取与底层打包分开,或让提取接口明确知道物理格式。已查看的差异按受支持元素存储类型读取,没有提供任意压缩格式的证明。
量化长度的三个合法分支还适合做一组互相区分的测试。共享组内参数用两组相同的比例,但权重数值故意不同;总通道参数用两组不同的比例,但整数权重故意相同。前者查数据是否混合,后者查标签是否混合。若数值与比例同时不同,失败结果虽明显,却可能难以判断错误发生在哪一层。测试设计不是追求最复杂输入,而是让不同错误具有不同指纹。
异常路径也应像普通路径一样被走读。先验证所有组的量化编码,再开始创建组节点,可以避免到最后一组才发现比例长度不合法;先确认存储类型是否可读,再改动图,也能减少部分重写状态。历史代码确实在循环前完成一部分检查,但仍有权重创建和元数据更新等可能失败的位置。把这件事写成“完整事务已实现”没有依据;更有价值的文章做法,是把已经前移的检查和仍需加强的提交边界分别说明。
这个故事最值得保留的调试习惯是:看到一个四维类型,不要立刻拿刀切。先画出八到十二个带名字的元素在内存中的排列。画得出来,才知道刀应该落在哪。
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 !