系列目录 · 算子与布局 · Read in English
“普通卷积已经跑通了。分组卷积,不就是多传一个参数吗?”
这句话的危险程度,和“这个文件只改一行”差不多。算子定义里的一个整数,到了后端可能意味着一个新图:输入要切,权重要切,偏置要切,输出要拼起来。更麻烦的是,整数权重旁边还有一套解释这些整数的量化信息。蛋糕分好了,标签拿错,大家吃到的味道仍然不对。
分组的数学与合法性
先把问题缩小。考虑教学输入 X:[1,6,5,7],按 NCHW 解释;输出通道为 10,分为两组,每组输入 3 个通道、输出 5 个通道。权重逻辑形状为 [10,3,KH,KW]。我们希望复用已经可靠的普通卷积路径,而不是从机器指令开始另写一套。
分组的数学语义可以直接写成:
1 | 对每个组 g: |
这里切片的步长全为 1,合并顺序也不是任意的。输出第八个通道属于第二组的第三个输出通道;如果 Concat 的顺序翻转,形状、元素个数甚至数值范围都可能正常,语义却完全不同。
可复用的后端能力越成熟,拆图越有吸引力。历史实现采用更高匹配优先级的专用 lowering 处理受支持的分组情况,然后让生成的普通卷积继续走已有路径。普通卷积本身交回原来的规则。这个策略避免让一条巨大的 lowering 同时承担普通路径、分组路径和所有失败诊断。
问题在于:你不能把“拆得出来”当成“所有分组都支持”。已检查的实现只接受有限分组集合,并要求静态四维输入、权重和输出。教学例子中的分组数只是数学演示,不是某个设备能力清单。实际编译器必须把目标限制表达出来,不能看见通道能整除就自动承诺硬件可执行。
合法性检查最好在创建新节点以前完成。输入通道和输出通道都必须被组数整除;权重的输出通道数必须对应整个输出,输入通道数必须对应单组输入。输出空间大小还由卷积的步长、扩张和边界条件决定,拆图时应继承这些属性。只复制形状、忘记属性,相当于把食谱标题抄对、火候全凭猜。
偏置与量化参数跟着通道走
偏置也值得单独盘问。“偏置就是一个向量”只是常见形态。已有改动区分无偏置、单元素偏置、每输出通道一个偏置。单元素可以复用;逐通道偏置必须沿真正承载通道的轴切分。若偏置有 10 个元素,形状可能是 [10],也可能是 [1,10,1,1]。元素个数一致是必要条件,识别承载通道的轴也是必要步骤。对于更一般的多维广播偏置,不能用“找到一个长度相等的轴”草率推广。
最容易被低估的是权重量化。设每个输出通道的量化比例分别是:
1 | s = [s0,s1,s2,s3,s4, s5,s6,s7,s8,s9] |
第二组的权重必须带走 s5...s9,而不是重新从 s0 开始。否则乘加整数可能完全正确,反量化的结果却整体偏离。对每通道 zero point 和其他通道参数也一样处理。每张量参数只有一个值,可以原样复用;多值数组必须核对长度及量化轴。历史代码明确检查逐通道量化轴,并为各组派生新的元数据记录。
有趣的故障是:全体比例都相同时,错误切分几乎隐身。测试若使用每通道都相同的比例,能验证数据流,却很难验证标签是否跟着通道移动。教学测试应给不同组使用明显不同但合理的比例,再选不会饱和的输入,把错误暴露成可观察差异。不要为了“容易过”让所有参数都一样。
新权重有了新名字,外部量化映射也得增加条目。历史修改加入合并保存逻辑:保留不受本次变换影响的条目,再补入新生成权重的编码。这里藏着一个编译器工程问题:图重写与外部元数据更新不是天然原子的。若图已经替换,元数据写入失败,应该如何传播失败、避免不完整产物?检查到的代码有错误诊断,但不能由此断言事务已经完整。更稳健的设计建议是先构造完整计划与派生元数据,验证成功后再提交。
拆图的性能成本
拆解还带来性能账单。假设输出逻辑大小为 E 个元素,每元素 b 字节。如果 Concat 必须真正搬运,忽略对齐填充与缓存影响,可能额外读取约 E·b 字节、写出约 E·b 字节,合计约 2E·b;若后端能让各组直接写入最终输出的互不重叠区域,部分搬运有机会消失。切片也可能是视图,可能是拷贝,不能只看高层图里的节点数判断。更多小卷积还会增加启动次数,减少单次任务的并行规模。组数更大并不自动更快。
我们可以提出一个测量方案:保持输入输出总通道数不变,只改变分组数;分别记录 lowering 后的节点数、真正的内存搬运字节数、任务启动数和端到端时间。再把小空间尺寸与大空间尺寸分开。前者容易被启动开销主导,后者可能更受数据搬运和算术吞吐影响。没有这些数据,文章只能说“复用了普通卷积能力”,不能写“显著提升性能”。
回归、替代方案与变换收敛
回归测试应形成两条线。一条检查图的结构:切片数量、每组通道边界、普通卷积的组数、输出拼接轴,以及原分组节点是否完全消失。另一条检查语义:使用不同组的不同常量、不同量化参数,验证组间没有串扰。历史测试还检查量化条目存在,并包含不支持分组及通道不整除的拒绝案例。
另一个历史用例扩展为深度卷积准备了不同边界条件和步长的模型,并用 IR 断言确认走专用深度卷积路径。深度卷积是分组思想的特殊情形,但编译器未必应该把它一律展开为很多普通卷积。专用实现可能有不同的权重排布和性能性质。数学关系可以统一,工程路径需要证据。
还可以把代码评审变成一次“拆组前后的守恒检查”。输入通道切片的并集应覆盖原输入通道区间,相邻切片不重叠;输出各组通道数相加等于原输出通道数;每一份权重所指向的输入组与它实际消费的输入切片一致。第一条保证没漏,第二条保证没多,第三条保证没串。这些关系可以在编译期检查,不必等一个复杂模型的最终输出给出模糊反馈。
为了测试串组,教学上可以给组零的输入全设为零,只激活另一组。没有跨组连接的卷积,组零输出除偏置影响外应保持为零。再把偏置单独关闭或设成容易识别的值,就能把“偏置切错”和“输入切错”分离。这比全输入同时随机更容易形成明确结论。若还有后续归一化混合通道,应在合并之后、进入下一类运算之前观察。
“把所有组当成一个普通卷积,把跨组权重填零呢?”这是一个有趣的替代方案。在数学上,它可以把分组连接嵌入一个块对角结构;在工程上,却可能把稀疏的零权重当成密集矩阵计算。假设每组输入输出通道都相同,组数增加时,填零版本中的无用跨组乘加也增加。除非后端能识别并跳过这些零块,否则它可能用较简单的图换来更大权重和计算量。
还有一个看似行政性的问题:新节点叫什么?名字通常承担诊断定位、量化信息关联和产物检查的作用。给每组名字加序号很直观,但多个原节点同名、pass 重复执行或已有派生权重时,仍需保证不会碰撞。更稳健的设计可以把身份与显示名分离,用稳定的内部引用连接元数据。历史实现采用派生名称;这是值得增加的失败用例。
最后,规则优先级也应纳入回归。专门的分组规则先运行,生成普通卷积后交回已有 lowering,是一种分阶段消解复杂语义的方式。但若另一条更宽泛规则抢先消费节点,或生成的新节点仍保留旧组数,就可能绕过预期验证或再次进入拆解。测试应确认固定点:优化再跑一次不继续增加组节点,也不改变各组切片边界。这里检验的是变换收敛性,而不是一次调用碰巧产出正确数量。
当同事再问“能不能只加一个 group 参数”,可以请他先回答四个具体问题:通道归谁,权重怎样切,量化参数跟谁走,输出怎么落地。能够逐项回答,拆图才是一份设计,而不是一场幸运的字符串替换。
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 !