系列目录 · 算子与布局 · Read in English
“框架里一行就能算,怎么到了后端还要分类讨论?”
广播的数学定义非常紧凑:对齐维度,从右往左看,长度相同或其中一边为 1 就可兼容。然而一条机器指令还要知道从哪里读、一次读多少、下次地址增加多少,以及哪一个输入按向量读取。数学说“这个值复用”,机器追问“复用几次,再移动几个字节”。
把广播翻译成地址轨迹
考虑教学主张量 A:[2,3,5]。与 B:[1,1,5] 相加时,每个长度为 5 的向量都使用同一份 B:
1 | for outer in range(2): |
若暂时假设紧凑、每元素 e 字节,主输入与输出每步前进 5e,B 的步长为 0,循环 6 次。广播在这里被翻译成“一个固定向量地址,配两个移动地址”。是否紧凑是明确假设,下一篇再处理对齐带来的变化。
换成 B:[2,3,1],每行复用自己的一个标量,B 每次前进 e 字节。换成 B:[1,3,5],每个外层块复用同一整个矩阵,B 的外层步长为 0。再换成 B:[2,1,1],一个标量要覆盖整块矩阵,主输入每次前进一个块,B 每次换一个标量。数学上同属于广播,循环计划却不相同。
历史实现将这类情况分类成原生路径、右侧广播循环以及可交换操作数后的对应路径,再构造块形状、循环次数、输入输出步长及向量或标量来源类型。这种设计比在发射器里零散检查 shape 更容易审查:分类回答“是什么情况”,计划回答“地址怎么走”,发射器回答“怎样把计划变成指令”。
交换操作数的前提必须摆在台面上。Add 与 Mul 可以利用交换律把完整张量放在一侧,方便复用循环;Sub 不能。举一个足够小的例子:完整张量 [4,9],广播标量 2,A-2=[2,7],而 2-A=[-2,-7]。形状完全一样,交换后也很容易生成指令,但答案变号。已检查的减法分类没有照搬加法的交换路径,这是关键约束。
中间轴与低秩输入
中间轴广播则经常让一层循环不够用。例如 A:[2,3,5] 与 B:[1,3,1]:中间的三个值各自要扩展到最后一维,再在第一维复用。某个后端若擅长末轴向量广播,可以先把后两个轴交换:
1 | A' = transpose(A, [0,2,1]) # [2,5,3] |
逐元素算子与同步作用于两个输入、输出的相同坐标置换可组合。这里交换恰好是自身的逆,但任意排列并不如此,通用实现需要真正计算逆置换。历史中间轴规则选择这种转换,并限制减法只能在完整输入位于允许位置时触发。
代价呢?如果这些 Permute 都要实际搬运,原本一个便宜的 Add 会背上多次内存访问。若常量可以预先重排、相邻置换可以合并,代价可能下降。正确性与性能应有独立验收标准。
接着,一个二维模型闯进测试室。表达式 [3,5] * [5] 在数学上完全合理,但一个只接受三维 shape 的分类函数可能直接返回“不支持”,甚至落入错误的普通路径。历史中的乘法修改通过左侧补 1 归一化:[3,5] -> [1,3,5],[5] -> [1,1,5]。从右对齐,所以补的是前面,不能补到后面。
这个修复的范围也要说准确:被检查的改动针对乘法分类及其广播发射入口。它不自动证明所有逐元素算子的低 rank 路径都已统一。读历史时常见的误读是“一处公共概念被修了,所以所有模块都修了”。若实现仍有重复分类代码,回归清单就必须按算子分别列。
更有意思的是只有一个元素的乘法。形状 [1,1,1] 同时满足“标量形状”和“长度为 1 的向量”。高层完全不矛盾,机器指令却可能不允许两个输入都使用标量来源模式。历史修复让普通单元素乘法保留一个向量来源,并在指令构造处拒绝双标量组合。数学标量与机器标量寻址模式,是两个不同概念。
为什么两层都要检查?内核选择合法模式避免正常路径失败;指令构造检查防止其他调用者生成非法编码。相关测试既检查文本指令,又检查二进制来源类型位,还加入应抛异常的负例。只有文本正确不足以证明编码正确;只有编码器拒绝不足以证明普通用户能顺利编译。
地址边界与回归设计
地址字段还存在表示范围。即使循环逻辑无误,步长可能超过目标字段能表达的范围。最坏的方案是直接截断,让大模型读到一个完全不同的地址;更合理的是提前报告不支持,或显式拆分成更小的循环计划。对于按位打包的低比特元素,字节宽度也不能用一个整数随便代替,历史路径对此保守拒绝。支持范围应该被看见,而不是藏在整数转换里。
本文建议把广播测试写成坐标映射测试。让主输入随三个坐标变化,小输入只随被保留的坐标变化,预期值可以手算。例如 A[a,b,c]=100a+10b+c,B[b]=b+1。如果结果沿错轴变化,错误会形成清晰图案。全随机测试适合补充覆盖,却不如这种标签数据容易定位轴错位。
回归组合还应包括:两个输入都需要扩展的合法数学情形是否被支持;输出形状是否确实等于广播结果;循环次数为 1;低 rank 补维;加乘交换;减法左右不对称;单元素来源模式;不支持 rank 的诊断。历史代码主要实现了完整一侧加若干受限小形状的计划,不能把它宣传为完整的任意广播框架。
性能测量建议分开统计算术量、循环控制指令、参数装载和实际搬运。一个长度很短、循环次数很多的广播,可能大部分时间花在循环控制上;一个可以用原生向量模式完成的大块广播,情况又不同。没有测量时,可以列出成本组成,不能给出“广播几乎免费”的结论。
通用参考与量化约束
还有一类数学上合法、实现上容易漏掉的情况:两边都不是完整输出形状。教学输入 A:[2,1,5] 与 B:[1,3,1],输出为 [2,3,5]。A 需要沿中间轴复用,B 需要沿首尾两轴复用。若分类器只寻找“有一边与输出完全相同”,两边都不符合。它不能因为各轴分别兼容,就自动选择旧的原生路径。
对这种形状,可以考虑更一般的多维地址计划,也可以先显式扩展某个输入,还可以在当前阶段拒绝。三种选择各有成本。显式扩展增加内存与搬运,多维循环增加控制和描述复杂度,拒绝则限制模型覆盖。文章应把“数学可广播”和“当前后端计划可表达”分成两项判断。
如果要设计通用地址参考,可以给每个输入的每个输出轴分配一个有效 stride:该轴输入长度为一时 stride 为零,否则使用其实际物理 stride。于是输出坐标 i 的输入地址,就是基址加所有 i_axis*effective_stride_axis 的和。这个公式很适合作为独立参考,但直接照搬为逐元素循环可能太慢。优化后的块计划应当证明它产生同样的地址集合与重复次数。
循环次数也不能只相信乘法结果。动态或零长度维、相乘溢出、符号转换,都可能让计数器得到异常值。许多“先执行一次,再减计数并检查”的循环结构默认次数至少为一;若次数为零,机器程序可能仍然执行一次甚至持续循环。对于已经在上游禁止这些维度的实现,需要说明前置不变量;若没有保证,就应在计划构造处拒绝或生成专用空张量路径。这是代码审查建议,不是对历史运行事故的断言。
量化操作数交换也不只交换两个地址。加法两侧可能具有不同的比例与 zero point,把完整张量换到固定侧时,参数构造也必须使用交换后的语义输入。历史加乘代码在确定左右值之后构造参数,这是一个值得注意的顺序。若先生成参数,再只交换地址,数学交换律不会替参数表自动换座位。
最后,一个长向量能否分块处理,还取决于尾部策略。块长不能整除逻辑长度时,是缩短最后一块、使用掩码,还是允许读到定义好的填充?广播小输入和输出都必须采用一致的尾部规则。已有差异主要围绕整块形状和地址循环,不能由此推出所有任意尾部都安全。本文建议将“长度刚好整块”和“多一个元素”并列测试,避免向量规模变化后又出现一位熟悉的访客:第一段正常,最后一段失踪。
当广播问题被翻译成地址轨迹以后,调试会突然朴素起来:第 0 次读哪里,第 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 !