2023年,有一支团队在开发自己的混合架构语言模型时,接连撞上了三次几乎一模一样的诡异问题。
第一次,是输出的位置被莫名其妙地多偏移了一格,暴露出了一个本不该被看到的未来词。第二次,是一段手写的注意力代码,本该只允许模型看过去的信息,结果被写成了前后都能看的对称结构。第三次,是一个"做减法"的操作,被安排在了一个"先把所有时间步信息搅拌在一起"的步骤之后执行,相当于减法用的两个数,早就已经被污染过了。
三次事故,三种完全不同的代码写法,但本质是同一件事:模型偷看了未来。
这篇论文的题目叫《The Mask Is Not the Model》,面具不是模型,讲的正是这件事。作者们发现,业内检查一个语言模型是否"守规矩"(也就是它在预测第t个位置的内容时,绝对不能用到第t个位置之后的信息)的标准做法,几乎形同虚设。而他们只用两次前向传播、不需要训练、不需要梯度,就能不仅告诉你"模型有没有偷看",还能精确告诉你"它是在哪一层偷看的"。
这件事听起来技术性很强,但它牵动的问题其实很朴素:我们现在评价一个AI语言模型好不好,看的是它在测试集上的准确率、困惑度这些分数。可如果这些分数本身就是"作弊"得来的呢?
自回归模型:什么是"不能偷看未来"
要理解这篇论文在讲什么,得先明白一件事。
现在几乎所有的语言模型,从GPT到各种聊天助手,底层逻辑都是自回归的。
自回归模型(autoregressive model):一种逐词生成的模型,它预测第t个位置的内容时,理论上只能依赖第1到第t-1个位置已经出现的内容,不能看到后面还没生成的部分。
这个规则听起来像是废话,写作文时你当然不能先偷看结尾再写开头。但对AI模型来说,这个"不能偷看"必须靠代码层面的严格约束来保证,一旦某个环节写岔了,模型就有可能在训练阶段悄悄"抄了后面的答案"。
论文把这个规则起了个正式名字,叫前缀不变性。
前缀不变性(prefix invariance):只要输入序列的前t个词是一样的,那么不管第t个词之后的内容怎么变,模型在第t个位置算出来的中间表示都必须一模一样。换句话说,未来的内容变了,过去位置的计算结果不应该受任何影响。
这就像你在考场上做阅读理解,第5题的答案必须只能从前面读过的段落里推导,如果你偷偷瞄了后面还没读的段落,就算蒙对了也是违规的。而这篇论文要做的,就是设计一套办法,去抓这种"偷瞄未来段落"的行为,而且不需要监考老师盯着,只需要对比两次答题记录。
为什么"看注意力掩码"这套老办法不管用了
在Transformer架构一统天下的年代,业内检查因果性的方法很简单:看注意力掩码(attention mask)。
注意力掩码:Transformer模型里用来限制每个位置只能"看"到它之前位置的一种技术手段,通常用一个上三角形状的矩阵来实现,强制未来位置的注意力权重设为零。
因为Transformer里几乎所有的时序混合都靠自注意力机制完成,而自注意力的因果行为就体现在这个掩码上,所以过去大家默认:查一查掩码对不对,就等于查完了整个模型的因果正确性。
但现在的架构早就不是这么回事了。
近几年冒出来的各种新型序列模型,比如状态空间模型(State Space Model, SSM,一种用连续时间动态系统来建模序列的方法,代表作是Mamba)、线性递归网络、还有把注意力和递归结构揉在一起的混合架构,它们的因果行为往往不是靠一个显式的掩码矩阵来控制的,而是靠内部的扫描过程、聚合操作、归一化步骤这些隐藏在代码深处的计算逻辑。
这意味着,注意力掩码这套检查手段,压根就没有覆盖到这些新架构的大部分计算图。
这就好比你原来检查一辆车安不安全,只需要看刹车片,因为所有车的刹车系统都长一个样。但现在有的车用了电子刹车、有的用了能量回收系统、有的甚至没有传统刹车片这个部件了。你还盯着"有没有刹车片"这一件事去查,那些真正藏着问题的新系统,根本不在你的检查范围里。
论文用实际数据把这个漏洞捅穿了:他们在8个公开模型上,故意注入了192次人为的因果泄露错误(涵盖8种不同的错误模式,每种模式在3个不同深度的层里各测一次),然后用注意力掩码检查法去查,结果是0/192,一次都没查出来。而他们自己设计的方法,192次全部精确定位到了出错的具体层。
两次前向传播,就够了
论文提出的检测方法,简单到只需要一页纸就能写完。
具体做法是这样的:先随机生成一个词序列,叫它序列一。然后复制一份,只把最后一个位置的词换掉,叫它序列二。接着把这两个序列分别喂给模型,跑两次前向传播,不训练、不算梯度、不用GPU也行。然后逐层比较两次跑出来的中间表示,如果在最后一个位置之前的所有位置上,两次的输出完全一样,那就说明这一层是干净的。一旦某一层出现了差异,那就是"泄露"发生的地方。
这个设计里有几个细节值得说一说,因为每一个都不是随便定的。
第一个细节,是为什么要逐层检查,而不是只看模型最终吐出来的结果(logits)。
如果只看最终结果,你只能知道"模型是不是泄露了",但没法知道"是哪一层泄露的"。这就像医院体检,如果医生只告诉你"你的血液指标有问题",却不告诉你到底是肝功能异常还是肾功能异常,你除了焦虑之外什么都做不了。而这篇论文的方法相当于把每个脏器都单独抽血化验一遍,直接告诉你"问题出在肝脏第三叶"。论文用数据验证了这个必要性:只看最终结果的检测方法(论文里叫B1)确实能查出全部96个人为注入的错误,但一个都定位不出具体层。
第二个细节,是为什么要在对比时排除最后一个位置。
这个很好理解,最后一个位置本来就是故意改动的地方,它理应不一样。如果把这个位置也纳入比较,那就相当于拿一个"本该不同"的地方去证明"有问题",这是在制造假阳性。
第三个细节,是为什么要关掉缓存机制。
增量缓存(KV cache):模型在生成文本时,为了加速会把之前算过的中间结果存下来,之后直接复用,不用每次都从头算一遍。
如果开着缓存跑这两次前向传播,两次调用可能会走上不同的代码路径,复用了不该复用的状态,那测出来的差异就没法准确归因到"计算图本身有没有问题",因为你没法确定这个差异是缓存机制导致的,还是真正的架构缺陷导致的。关掉缓存,就是让两次计算走一模一样的路径,这样才能做一个公平的对照。
第四个细节,也是最反直觉的一点:在一个完全正确的模型里,这个差值不是"很小",而是精确地等于零。
这个"精确为零"很关键,因为它意味着,判断"干净"还是"泄露"这件事,根本不需要纠结阈值该设多大。论文里试了从万分之一到十的负六次方的各种阈值,结果都一样,因为干净的模型差值是数学意义上的0.0,而有问题的模型差值动辄是0.01甚至0.7,两者之间隔着一道悬崖,根本不需要精确调参去踩钢丝。
两个必须先补的坑:假阴性和沉默的模型
论文里最诚实的一部分,不是讲方法多好用,而是坦白了自己这套方法一开始也栽了两个跟头。
第一个坑,是"死掉的检测器"问题。
作者们在测试一个叫Falcon-H1的模型家族(三个不同大小的版本)时,得到的结果全是"干净",每一层的差值都是0.0。如果照单全收,这就是三个完美的合格证书。
但他们多留了个心眼,去跑了一遍"阳性对照"实验:故意往模型里注入一个已知的错误,看看检测器能不能抓出来。结果是0/24,一个都没抓出来,就连最夸张的注入强度都测不出差异。
再往下查,发现这几个模型加载进来之后,对不同的输入喂进去,吐出来的输出居然是逐位比特相同的。换句话说,这几个模型根本没在"响应输入",它们已经变成了一个不管你喂什么都输出同一套结果的"哑巴"。在这种状态下,差值当然永远是零,这不是因为模型没有偷看未来,而是因为模型压根就没在正常工作。
这就好比你去查一台空调有没有制冷功能,结果发现它压根没插电。它当然不会显示"制冷失败"的报错,因为它根本没启动。而你如果只看"有没有报错"这个表面信号,就会误以为它工作正常。
这次教训让作者定下了一条硬性规矩:任何一个"干净"的检测结果,只有在同一个加载好的模型上,阳性对照测试也通过的前提下,才能被认定为真正的干净。否则只能标记为"未能审计",绝不能算作"合格"。
第二个坑,是测试序列长度不够长的问题。
这个坑更隐蔽,因为它不是让检测器彻底失效,而是让检测器根本没机会碰到问题。
论文里最初的普查实验,用的测试序列长度是48个词。这个数字是"继承"下来的,没人认真论证过它够不够用。后来作者反思了一下,才意识到这是个大问题。
原因在于,很多混合架构模型内部有一些"分块"的参数,比如状态空间扫描用的分块长度、局部注意力用的窗口宽度、卷积核的宽度。如果测试序列长度比这些内部参数还短,那么这些依赖"跨块"计算的代码路径根本不会被触发,模型退化成了"单块模式",压根不存在"块与块之间"的交互,自然也就查不出跨块交互中藏着的错误。
这就好比你想测试一辆车换挡时会不会有异响,可你测试的路程太短,车子从头到尾都没超过一档的速度,那你永远听不到换挡那一瞬间的杂音。不是你的耳朵不灵,是你压根没让车子跑到需要换挡的速度。
在Zamba2和Nemotron-H这两个真实存在的公开模型上,这个坑差点让作者错过一个真正的缺陷。
一次代码审查,预测出了两个真实模型的缺陷
这是整篇论文里最让人拍案叫绝的部分。
作者没有直接去跑模型测试,而是先做了一次纯代码层面的静态审查,翻看Hugging Face的transformers库里,所有实现了"分块扫描"(chunked scan,一种Mamba2类架构用来高效处理长序列的计算技巧)这个功能的源代码文件。
分块扫描:一种把长序列切成若干小块分别计算,再把块与块之间的状态信息传递、合并起来的算法,目的是在保持长序列建模能力的同时降低计算开销。
Mamba2的官方参考实现里,块与块之间传递状态的这段计算,遵循一个特定的坐标轴处理顺序,必须沿着"输入块"这个维度做归约(reduce)操作。作者把这个参考实现的写法当作标准答案,去比对其他几个模型的实现代码。结果发现,有四份实现(分别属于Bamba、Falcon-H1、GraniteMoeHybrid等模型)完全符合标准,但另外两份,分别属于Zamba2和Nemotron-H,做了一个看似无关紧要的改动:它们把归约操作错误地放在了"输出块"这个维度上,而不是"输入块"维度。
更微妙的是,这两份出问题的代码,逐字节比对下来是完全一样的,连变量名和空格都不差。这说明这不是两个人各自独立犯的错,而是同一段有问题的代码被复制粘贴到了两个不同的项目里。
光凭代码审查,作者就大胆预测:这两个模型会在各自的分块大小边界上出现因果泄露。这是一个在跑任何模型之前就做出的、可以被证伪的预测。
然后他们真的去跑了模型验证。
结果完全应验。Zamba2的分块大小是256,测试发现,只要序列长度不超过255,模型的输出严丝合缝,任何一个位置的偏差都是精确的0.0。可一旦序列长度跨过256这条线,偏差瞬间跳到0.0057这个量级。Nemotron-H的分块大小是128,同样的现象在128这个边界精准复现,序列长度127时是干净的0.0,跨过128之后偏差一下窜到0.74。
这不是巧合导致的噪声,这是代码逻辑上明确可以推导出来的必然结果。作者甚至做了进一步的证明:随便造一个没训练过的、参数是随机初始化的Mamba2模型(用官方正确代码构建),测出来在各种长度下都严格保持0.0;而同样随机初始化的Zamba2式错误代码结构,依然会在256这个边界处出现泄露。这说明这个问题跟训练出来的具体参数完全没有关系,是代码结构本身的缺陷,与权重无关。
最后,作者把这段有问题的三行代码,换成参考实现的正确写法,重新测试,泄露完全消失,偏差精确归零。这是一个闭环的诊断:发现问题、定位原因、验证修复,三步都做到了。
其中Zamba2的这个缺陷,作者已经通过官方渠道(GitHub issue和pull request)提交给了transformers项目组;Nemotron-H的这个同源缺陷,是这篇论文首次公开披露的。
如何分辨"真泄露"和"数值噪声"
这里还有一个特别值得说的细节,论文作者拒绝了两个"看起来像发现"的结果。
在审查过程中,他们发现另外两个模型(Jamba-tiny和Granite-4.0-h-tiny)也出现了微小的非零偏差,大约在十万分之一到十万分之三这个量级。这个偏差不是随机噪声导致的(因为计算是确定性的,重复跑结果完全一样),如果换一个不那么严谨的团队,很可能直接把这两个也当成"发现"报告出去。
但作者多做了一步验证:扰动强度扫描。
具体做法是,不再只用一个固定强度的扰动去测试,而是把扰动强度从0.0001一路调到1,看偏差是不是随着扰动强度一起变大。如果真的存在因果泄露,那么这条路径传导的信息量应该跟着扰动强度的变化而变化,你往里面塞的扰动越大,泄露出来的偏差也应该越大。而如果只是数值计算本身的舍入误差,那这条曲线应该是平的,不管你怎么调扰动强度,噪声水平基本不变。
结果,Zamba2的曲线在对数坐标下呈现出0.63±0.07的明显正斜率,扰动越大,泄露越明显,这是真实信息路径的特征。而Jamba-tiny和Granite的曲线斜率几乎是0(分别是约负0.03和约0),说明它们那点微小偏差纯粹是数值噪声,跟因果泄露毫无关系。
这个区分方法就像医生诊断发烧,如果吃了退烧药之后体温应声下降,说明确实是炎症在起作用;但如果不管你怎么调整用药剂量,体温始终纹丝不动,那这个"发烧"很可能压根不是炎症导致的,只是体温计本身的读数误差。只看单次测量结果容易被这种表面现象误导,只有观察它对干预的响应模式,才能分辨出真假。
跟其他检测手段比,差距在哪里
论文还系统对比了六种其他可能被用来做类似检查的方法,放在同一批注入错误上做测试。
结果显示,只看最终输出logits的方法、以及对未来词做重采样的方法,检测能力和这篇论文的方法一样强,都能查出全部错误,但一个都定位不到具体层。而基于梯度的方法(对着前缀部分的输出反向传播,去看梯度是否传到了未来位置)跟这篇论文的方法打了个平手,同样能做到精确定位,但代价是需要一次反向传播,而且要求路径上所有计算都是可微的,这就排除了量化权重、某些自定义内核等大量实际场景。
至于"打乱后缀再算困惑度"这种做法,论文发现它在最常见的一类错误(论文里叫C1类,比如"输出往前偏移一位")上是结构性失明的,不是阈值没调好,而是这种检测方法的设计逻辑本身,根本碰不到这一类泄露发生的区域。还有一种检查"缓存推理和完整推理是否一致"的方法,在一个完全正确的模型上居然也报了假警报,因为它测的其实是另一个完全不同的性质,而且必须针对不同模型重新校准阈值,而这篇论文的方法从头到尾都不需要校准,因为干净的答案永远是精确的0。
写在后面
读完这篇论文,最触动我的不是那个检测方法本身有多精巧,老实说,两次前向传播加逐层对比,这个思路并不复杂,聪明的工程师大概率也能想到。真正让我愣了一下的,是作者反复强调的那个态度:"干净"的结论不能凭空相信,必须用同一个已加载的模型跑一次阳性对照来自证清白。
这跟我们平时判断一件事"没问题"的直觉是反的。我们通常觉得,没查出毛病就等于没毛病。但这篇论文用Falcon-H1那个案例狠狠打了这个直觉一巴掌:一个彻底哑火、对任何输入都给出相同输出的模型,会呈现出最完美的"零偏差",看起来比任何正常模型都干净。沉默不代表没问题,沉默有可能恰恰是最大的问题。
另一个让我反复咀嚼的地方,是那个静态代码审查预测动态错误的案例。这提醒我一件更普遍的事:很多软件缺陷其实是可以脱离运行环境、单纯靠读代码逻辑就能提前预判的。我们太习惯用"跑起来看结果"的思路去验证一切,却忘了代码本身的结构,往往已经把答案写在那里了,只是没人愿意花时间去对照参考实现,逐行核对那些看起来无关紧要的坐标轴顺序。
这篇论文没有解决的问题也很明显:它没法告诉我们这类缺陷在整个开源模型生态里到底有多普遍,作者自己也反复强调,两个案例共享同一段代码,只能算一条被追溯出来的血统,不能算作一个比例。而那些依赖GPU专属编译内核、普通人根本没法在自己电脑上加载运行的模型,可能藏着更多类似的问题,却连被审查的机会都没有。
如果一个模型连加载都做不到,它到底算不算被验证过?这个问题,恐怕比论文里那些具体的技术细节,更值得放在心里多想一会儿。
Q&A
Q1:前缀不变性是什么意思?
A:前缀不变性是指模型在预测某个位置的内容时,只要这个位置之前的输入不变,不管后面的内容怎么改变,模型算出来的中间结果都不应该受到影响。这是自回归语言模型必须遵守的基本规则,不能偷看未来信息。
Q2:为什么注意力掩码检查不能保证模型没有因果泄露?
A:因为现代很多架构(比如状态空间模型、线性递归网络)根本不靠注意力掩码来控制因果性,泄露可能发生在扫描、聚合、归一化等其他计算环节。论文实测中,掩码检查对192个人为注入的错误一个都没查出来,而作者的新方法全部精确定位到了具体出错的层。
Q3:Zamba2和Nemotron-H这两个模型的因果泄露问题是怎么发现的?
A:作者先通过静态代码审查,发现这两个模型的分块扫描代码在坐标轴处理上和官方参考实现不一致,据此预测它们会在各自的分块边界处出现泄露。随后动态测试完全验证了这个预测,且用两行代码修复后偏差归零。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.