两道同样的输入分别送进参考实现与新生成的内核,输出在一张比对台上按容差逐元素对齐,旁边挂着一块显示耗时区间的标尺

一、错的定义变了:四类失败模式里多数不报错

据公开报道,有团队把模型用在自研芯片软件栈的代码编写上,配套的说法是“写代码最难的不是逻辑,而是写给硬件听得懂的逻辑”。这句话恰好点中了评审为什么在这类代码上会失灵。业务系统的错通常以异常、错误码或者一条肉眼可见的后果呈现,测试和回滚都是围绕“错会被喊出来”设计的。硬件后端代码的错大多不喊。

失败模式 现场表现 常规评审为什么拦不住 该由哪道闸拦
结果错误且报错 编译失败、运行崩溃、断言触发 最容易的一类,正常流程能接住 现有测试与持续集成
结果错误但不报错 输出能跑通,数值偏离参考实现 肉眼看数值“差不多对”,逐元素比才看得出 闸一:等价性比对
结果正确但性能塌陷 精度全过,耗时或吞吐退了一个量级 功能测试不包含时间判定 闸四:性能基线门禁
只在特定条件下出错 某个形状、某类数据类型、某种并行配置下才现形 抽样用例覆盖不到那个组合 闸二:边界矩阵

第二类最危险,因为它会穿过所有以“通过”为判定的检查。第三类次之,因为性能退化在功能视角下完全不可见,等到用户侧察觉已经是几周之后。第四类的麻烦在于组合空间大,靠人脑枚举不现实,而生成式改动恰恰容易在边界上写出“看起来通用”的处理。所以我们把评审的重心从“这段代码读起来对不对”挪到“有没有机器可判定的等价性与基线”。

二、可读性评审收益低,因为关键约束不是可读性问题

代码评审里最省力的动作是检查命名、结构、函数长度、重复度,这些在业务代码上确实有拦截力。硬件后端代码的关键约束是另外几样:数据在片上内存与高带宽内存之间的布局与排布方式、对齐要求、同步点的位置与粒度、寄存器压力与共享内存占用、访存合并与否。这些约束的后果不体现在“能不能读懂”上。一段命名混乱但布局正确的内核是慢一点的债;一段命名漂亮但同步点错了一个屏障的实现是事故。

更麻烦的是,生成出来的代码往往篇幅大、控制流直、展开充分,看起来工整,读起来顺。人的阅读在这里最容易得出“没什么问题”的结论,因为真正决定正确性的东西不写在明面上:某个偏移量对不对,取决于上一次布局转换的语义;某个归约顺序对不对,取决于它与容差声明是否一致。这些只能靠比对和矩阵去判定,不能靠读。

这不代表评审取消,而是评审的对象换掉。人的注意力要收拢到三处:这份改动的能力边界写清了没有、它的容差与基线是不是被诚实设定、它有没有绕开任何一道闸直接进主干。

三、闸一与闸三:等价性比对,容差按累加长度声明

闸一是参考实现等价性比对。前提是要有一份被信任的实现存在——通常是编译器已经验证过的旧路径或一份手写内核。做法是同一批输入分别喂给两者,逐元素比较,判定标准不是“相等”而是“在声明的容差内”。输入集要覆盖正常值、极小值、极大值、次正规数、全零与全等值这类容易触发不同分支的数据;比较时同时记录最大绝对偏差、最大相对偏差和偏差的分布形状,只看最大值会被单个离群点误导,只看均值会把局部错误藏起来。

容差的声明方式是这份比对可信与否的核心。按单次运算定容差是不对的:一次浮点加法的误差和一段长累加的误差量级不同,累加越长,误差上界越大,而不同实现的重排方式不同会带来本质不同的舍入序列。正确的做法是按累加长度或者归约规模来定容差档位,把“这个形状下允许到多少”写成表而不是写成一个全局常量。这张表要进代码仓库,和内核放在一起,改容差必须走评审。

闸三与闸一是同一件事的两面:容差是声明,比对是执行。如果一份改动修改了归约顺序或者并行分块方式,等价性报告里必须显式说明它改了累加结构,并按新的累加长度重新取档,而不是沿用旧的容差蒙过去。

四、闸二与闸四:边界形状矩阵,加上性能基线门禁

闸二是形状与数据类型的覆盖矩阵。矩阵维度至少包括:输入形状(含零长度、单元素、非对齐尺寸、极端长宽比)、数据类型组合、并行配置与分块参数、是否触发跨片或跨层搬运。评审时的硬要求是边界形状作为强制用例,不允许“业务上碰不到”作为豁免理由——边界之所以是边界,正因为编译器与生成器在那里最可能给出未定义的处理。矩阵跑不全就不能合入,跑不完的比例要写进报告。

闸四是性能基线门禁。为关键内核维持一条基线带:同一个形状与配置下,耗时与吞吐的历史分布,允许波动的区间写在配置里。生成结果落在带内才算通过,落在带外要给出解释并重新定基线,重新定基线本身要评审。这条闸的规则只有一句:不允许“正确但慢一个数量级”的改动合入,因为它会被功能测试放过,然后在真实负载里以整体吞吐的形式重新计价,而那时排查成本极高。

性能基线要防两件事。一是环境漂移,机器、驱动版本、时钟设置都会改变绝对数值,所以基线必须绑定环境标识,跨环境不得直接比。二是统计噪声,单次运行的抖动可能盖过真实退化,所以门禁判据用多次运行的分位数,不用单次值。

五、闸五与“没人读得懂”的长期处置

闸五是可复现记录。凡由模型参与的改动,随代码入库的应当包括:生成时使用的模型与提示词的版本标识、随机种子、目标硬件与环境版本、输入集与容差表的引用、等价性报告和基线报告的存放位置。这件事在业务代码上常被当成形式主义,在后端代码上却是唯一能让半年后的人重新跑一遍同一条判定的东西。

“生成的代码没人读得懂”是结构性后果,不是过渡期问题。最小处置有三条。第一条,每个生成文件附一份人工撰写的声明,写清它支持到什么范围、不支持什么、容差取自哪一档、已验证过哪些形状与并行配置;没有这份声明的文件不允许进主干。第二条,维护一份手写的参考实现作为长期对照基线,它的价值不在性能而在“永远有一个可解释的正确答案可比”。第三条,在没有参考实现的路径上禁止直接采用生成结果——新算子、新数据类型、新硬件代际这类场景里,没有可信对照,等价性这道闸就无从谈起,这时必须回到人写或者先用最简实现兜住。

六、评审清单五项重写,与定位和成本分配

把上面的东西落到评审模板上,检查项从“命名与结构”换成五项。在洹拓承接的加速栈相关交付里,这五项是合入前必须逐条签字的:容差是否显式声明并按累加规模取档;边界形状与数据类型是否覆盖完整,缺口比例是多少;同步点与并行分块改动是否单独说明;访存布局与共享内存、寄存器压力是否给出实测占用;失败可见性——如果这份实现在某个条件下算错,它会以什么形式暴露出来,是断言、报告还是静默。最后一项最容易被跳过,也最关键:宁可实现得保守,也不要静默。

缺陷定位要有办法,因为这类问题复现条件常是二维的。我们的做法是按并行配置与输入形状二分:先在最小可跑配置上验证,再逐个放开分块与形状,找出使结果偏离的那一个维度;同时要求可疑改动单独合入,不与其它改动捆绑,否则一旦偏差出现,只能整批回退。

性能回归的测试成本靠分层来控制,而不是靠减少覆盖。提交级只跑小矩阵和关键内核,判定门禁;夜间跑全矩阵,把边界形状与罕见数据类型摊开;发布前跑真实负载的代表性模型,验证分层跑没漏掉系统级效应。三层各管一件事:提交级守住“不引入退化”,夜间守住“不在边界翻车”,发布前守住“整体仍然可用”。把这三层混成一层,结果要么评审带宽撑不住,要么静默错误漏过去。