Transformers 模型集成契约
本文目录
发布于 2026 年 11 月 3 日
作者:Tarek Ziadé(tarekziade)
2026 年 7 月 30 日,Transformers 为 T5 启用了 PyTorch 的缩放点积注意力(scaled dot-product attention,SDPA)。作为一个副作用,UMT5 的解码器可能会关注到未来的 token。在训练期间,它可能偷看它本应预测的答案。没有任何东西崩溃,这个 bug 在 main 分支上存在了将近两个月。
当我们后来重放那些提交时,一个在 CPU 上运行的小型 UMT5 模型在每一个提交上都标记出了这个问题。本文讲的就是这个模型以及围绕它构建的测试。
为什么 bug 会进入 main#
Transformers 支持超过 750 种模型类型,其测试套件规模庞大:main 分支上有 88,000 多个 CPU 测试,此外每日运行中还有 200,000 多个 GPU 测试。模型测试只是其中一部分:我们还会测试流水线(pipeline)、训练、分布式执行、量化和注意力内核。
集成测试会加载真实检查点,并在真实硬件上演练模型的 API。在每个 PR 上都运行这一切,意味着要让大量 GPU 整天忙碌,这并不现实。等到一次定时运行发现回归时,它可能已经进入 main 分支了。
我们的方法是缩小模型。我们构建一个夹具(fixture):用真实检查点的配置构建一个小模型,层数更少、更窄,但保留它拥有的每一种层,并且它的分词器和处理器文件保持不变。它发布一次,然后被重复使用。我们在廉价的 CPU 运行器上对它运行「快速集成测试」。在我们的 CPU 研究中,实验性的 UMT5 夹具使用了 66 MB 的权重;源检查点则是 1,227 MB。
这也覆盖了非常大的模型:有些模型无法放入我们标准的 GPU 运行器,但它们的夹具可以在 CPU 上运行。
模型集成者已经在构建微型测试模型(Transformers 的 tiny-random-* 检查点)。我们想要一个通用的配方,以及超越「加载模型并运行一次前向传播」的检查。UMT5 的 bug 本来可以轻松通过一次前向传播。
夹具不会取代完整的集成测试,但在 PR 上,它们给了我们在 bug 进入 main 之前将其捕获的机会。
引入模型集成契约#
transformers-mic 是一个用于打包这些测试的提议标准。一个模型集成契约(Model Integration Contract,MIC)由三部分组成:
- 一个夹具:即上文描述的缩小模型,发布一次并固定版本
- 一条使用路径:一个可运行的 Markdown 示例,展示人们对该模型所做的一类事情,例如生成文本或运行一个训练步骤
- 基线:运行该示例时记录下来的输出,并由人工审查
如果在夹具上运行该使用路径仍然产生这些基线,那么契约成立。
一个契约覆盖一种架构和一条使用路径,而不是单个检查点:兼容的检查点可以共享一个夹具,同时各自保留自己的预期结果。我们将它们发布在 huggingface-mic 中。
例如,SmolLM 契约会从诸如「The capital of France is」的提示词生成内容,并将其 token ID 与记录下来的答案进行比较。在加载模型和分词器之后,生成代码简化为单个提示词如下:
inputs = tokenizer("The capital of France is", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=4, do_sample=False)
new_tokens = outputs[:, inputs["input_ids"].shape[1]:]
print(tokenizer.batch_decode(new_tokens, skip_special_tokens=True))
它还会比较训练损失,检查梯度是否为有限值,并验证一次优化器步骤会改变权重。参见完整的 usage.md。
Hub 上的一个 fleet 数据集将每个 MIC 固定到一个冻结的修订版本,以及录制其基线时所用的包版本。每次 CI 运行都会记录它使用的 fleet 修订版本,以便我们日后可以复现。
在 PR 上,我们根据补丁更改的文件来选择 MIC。
UMT5 回归#
我们在五个月的 Transformers 提交上重放了冻结的 MIC。小型 UMT5 夹具从该回归于 2026 年 7 月 30 日引入,直到 9 月 28 日修复,一直标记出它。标签对数概率与审查过的基线相差 1.22,训练损失相差约 0.20。
有一个细节很重要:该契约使用了 SDPA,也就是默认的注意力路径。只运行 eager 注意力变体会漏掉这个 bug。只有当夹具走的是与真实检查点相同的代码时,它才有帮助。
随后,我们在 Transformers 的 CI 中,于一个验证分支上回退了该修复。检查以相同的差异失败,并且所选 T5 契约的运行器步骤在 CPU 上耗时 13.4 秒(不含环境准备)。
重放结果和 CI 演示中包含了测量数据和复现细节。
模型如何被缩小#
第一个配方很简单:把维度变小,保留更少的层。对于混合架构来说这还不够。如果两层都使用滑动注意力,而全注意力层消失了,那么保留两层也没有用。
现在的配方有四个步骤:
- 缩小配置。 从真实检查点开始,减小隐藏维度、注意力头数和深度。保留不同的层类型,包括滑动注意力与全注意力的混合,或稠密层与 MoE 层的混合。有些架构需要额外的规则来保持其组件兼容。我们还会查看模型自身的 Transformers 测试:
test_modeling_<model>.py中的ModelTester已经手工挑选了一个小配置,因此对于我们的通用规则不认识的任何宽度,我们采用它的尺寸。
- 保持预处理不变。 保留词表、特殊 token 和输入维度,并逐字节复制分词器和处理器文件。加载和预处理正是我们想要测试的一部分。
- 构建并验证权重。 尝试带种子的随机权重,并改变输入以检查模型是否有响应。如果没有响应,就尝试在契约的小批量示例上训练它。重新加载保存的模型,检查输出、损失和梯度是否为有限值。拒绝构建失败的产物。
- 冻结并记录。 保存被接受的权重,并在一个已知良好的 Transformers 构建上测量可重复的输出。针对每个支持的平台和精度审查基线,然后发布一个固定版本的快照。CI 复用它;它不会为每个 PR 重新构建夹具。
我们还保留原始的权重文件格式。否则,我们就会跳过用户加载真实检查点时所运行的转换代码。完整配方有更多细节。
Llama 4 Scout 的缩小报告展示了这在大型模型上的样子。1086 亿参数变成 4470 万。视觉栈从 34 层变为 2 层,语言栈从 48 层变为 4 层,同时保留已检查的层类型和 MoE 路由风格。
用代理构建 MIC#
我们的 CLI,tmic,负责创建契约并构建夹具。对于一个新模型,由 LLM 起草使用代码和清单,CPU 和 GPU 运行会测量候选基线。LLM 负责起草;它不负责批准。更改预期结果需要人工审查。
要在你的机器上试用,请用你的源检查点替换 org/model:
pip install "transformers-mic[runtime]" "hf-doc-builder @ git+https://github.com/huggingface/doc-builder.git"
hf auth login
tmic run org/model --local-only --fixtures-only --no-history --bill-to ''
它会起草契约、构建夹具并在你的机器上记录其基线,请求你审查每一步,并在上传任何内容之前停止。对于新模型,草稿来自通过 Inference Providers 调用的 LLM,费用记在你自己的账户上(--bill-to '')。CLI 文档涵盖了各个步骤。
我们使用代理跨模型家族构建 MIC。当它们遇到边缘情况时,我们修复配方本身,而不是修复单个夹具,并得到了模型专家的帮助。已发布的模型和剩余缺口见覆盖空间。
随机权重 vs 过拟合#
使用随机权重时,我们的 Whisper 夹具对音频变化几乎没有反应。它能运行,但这使它成为音频路径的一个糟糕测试。
于是我们用几个音调配上诸如「red」和「green」之类的词来训练它。我们并不是在教它语音识别,只是让音频变化在它的输出中可见。在我们的权重初始化研究中,随机 Whisper 夹具捕获了 24 个注入故障中的 18 个。过拟合夹具捕获了全部 24 个。
这给了我们两种构建权重的方法:
- 从随机权重和固定种子开始。
- 在少数示例上过拟合,例如「The capital of France is」=>「Paris」。
Whisper 的结果并不能决定哪种方法更好。那些是合成故障,而且我们还没有发现一个仅由过拟合版本捕获的真实回归。两个版本都漏掉了一个由真实 Whisper 检查点暴露的辅助解码(assisted-decoding)bug。
因此,当随机权重通过检查时,我们就保留它们;当需要更好的敏感性或可读的答案时,我们就用过拟合。训练只需几秒钟,但它是又一个可能无法收敛的步骤,也是又一件需要维护的事情。对比中列出了两者的结果。
开放问题#
截至 2026 年 11 月 1 日,Transformers CI 中的 MIC 作业会报告失败,但不阻塞 PR。它是否应该阻塞,取决于我们尚未测量的事情:
- 它在 PR 上(而不仅仅是在重放中)能捕获多少真实 bug?
- 它有多频繁地让我们去追查误报?
- 基线审查需要多少工作量?
我们也不知道有多少 bug(比如 Whisper 的辅助解码 bug)只在真实权重下才会出现,或者过拟合是否能捕获随机权重漏掉的真实 bug。
覆盖仍然是部分的,无论是在模型家族方面,还是在人们使用的 API 方面。当有回归漏过时,我们可以添加缺失的使用路径,并在有问题的版本和修复后的版本上重放它。我们会保留现有测试,直到它们的覆盖范围被纳入考量。
最后,我们希望将原始 Hub 模型页面链接到它们的 MIC,这样使用某个检查点的开发者或代理就可以从一个在该检查点、其环境和预期输出上测试过的示例开始。
想帮忙吗?从覆盖空间中选择一个未覆盖的模型,试用 tmic 工作流,并贡献一条使用路径或审查一个生成的契约。