离职工程师带走的微调产物
一个微调(fine-tune)不仅是一个文件。它是训练集上流水线的闭包(closure),如果一个团队在交付文件时没有提供这种闭包,那么他们就构建了一个生产依赖,其源代码其实只存在于某个人的脑子里。当那个人带着两周的离职通知和一份整洁的交接文档离开的那天,你某个收入相关特性的巴士系数(bus factor)就降到了零,而且没人会察觉,因为权重依然在注册表中,注册表标签依然稳定,模型也依然在处理流量。清算会在以后出现,比如在一次常规的基座模型迁移中,原本应该只需要一个 sprint 的工作却耗费了一个季度。
在我观察到陷入此困境的团队中,这种模式是一致的。一名 ML 工程师花了六个月时间迭代一个微调模型——包括数据策展(data curation)、超参数搜索、以及根据感觉在留存集上评估的行为补丁(behavioral patches)。最终的适配器(adapter)权重被推送到模型注册表并打上标签。而产出这些权重的训练流水线,只是该工程师笔记本电脑里的一个 Notebook,里面充斥着硬编码路径和浮动依赖,这些依赖指向的是每个单元格最后一次执行当天的最新版本。团队理所当然地接受了交接,因为权重有效、评估分数不错,且注册表标签很稳定。18 个月后,该工程师离职了。又过了 6 个月,一次基座模型迁移需要针对更新后的基座重新生成适配器。此时 Notebook 运行后生成的权重分数降低了 3 分,并且在最困难的客户细分领域出现了明显的性能退化,团队花了 4 个月时间尝试复现原始产物,但最终以失败告终。
这篇文章讨论了为什么这种失败是结构性的而非个人层面的,以及团队需要拥有的产物——不是权重,而是围绕它们的闭包——以便让微调模型在实际的多年运行寿命中保持可维护性。
权重并非产物
让团队陷入麻烦的思维模型是将微调模型视为后端团队对待编译后的二进制文件:一个你只需构建一次、部署、且只有在更改源码时才会重新构建的不透明输出。对于微调来说,这种模型是错误的,原因有两点。
第一,微调的输入并不是一小段易于处理的源代码。它们包括训练集、基座模型、分词器(tokenizer)、数据预处理流水线、随机化方案、超参数配置、硬件环境,以及一个依赖图包含数百个固定或浮动 Python 包及多层 CUDA 的软件栈。原始运行和重新生成运行之间,任何一个输入的漂移都会导致结果权重的变化,而评估集并不一定能捕捉到这些变化。要接手别人的微调工作,你需要的远不止一个 Git 仓库——你需要知道代码正在使用什么数据,以及这些数据是如何产生的。
第二,产物会自发衰减,而非因其自身过错。基座模型会被弃用。分词器会升级。框架会发布破坏性变更。CUDA 驱动会更新。你在某个基座模型上 训练的微调权重在通用意义上是不可移植到后续基座模型上的——充其量,最近关于跨模型适配器迁移的研究表明,在某些特定条件和架构下可以避免完全重新训练,但在通常情况下,基座模型迁移需要从原始训练数据重新生成适配器,这意味着要再次运行原始流水线。如果该流水线不具备确定的可复现性,那么重新生成就成了一场瞎猜。
这两点事实会产生复合影响。产物会按照你无法控制的供应商时间表衰减,而复现产物所需的输入却无人记录。不为后者预留预算的团队,迟早会把前者当作一个“惊喜”来发现。
可复现性究竟需要什么
大多数团队低估了复现微调所需的代价,因为失败往往是无声的。复现运行通常能完成并产出权重——只是它们不再是原来的权重了,而且这种差异在规范的评估集上并不总是可见的。
一个可复现的微调流水线必须锁定每一个可能发生变动的输入:
- 训练数据:通过内容哈希锁定,并将从原始数据产出数据的确切过滤步骤捕获为代码,而不是作为一个运行一次就忘掉的手动单元格。
- 基座模型:通过上游注册表中的确切修订版本(revision)锁定,包括分词器版本,因为分词器的漂移会悄悄改变训练期间跨越上下文窗口的示例。
- 依赖闭包:Python 包、CUDA、驱动版本——作为一个具有不可变摘要(digest)的容器镜像,而不是明天可能会解析成不同版本的 requirements 文件。
- 随机化:在每个随机源设置显式种子:权重初始化、数据洗牌、dropout、增强。一个常见的陷阱是设置一个全局种子并假设它涵盖了所有内容;在分布式训练中,每个 worker 的洗牌顺序是其自身的随机源,而跨 worker 数量实现复现性本身就是一个难题。
- 超参数配置:作为注册表中与权重并列的版本化产物,而不是输入到 Notebook 单元格中的值。
- 硬件形态:加速器的数量和类型——因为在混合精度训练中,累加顺序可能会改变结果,这些改变在评估集上看起来像噪声,但在生产端部(tail)则表现为性能退化。
以上每一项背后都有相关文献,记录了各团队如何通过惨痛教训发现“显而易见”的方法并不奏效。设置 random.seed(42) 并不涵盖数据加载器的洗牌。锁定 torch==2.3 并不锁定它调用的 CUDA 内核。一个存储为单个 Parquet 文件并标记为“v1”的数据集,并没有记录那个剔除了 20 万个语言标签错误示例的过滤步骤。
弥补这一差距的纪律并非某种工具。而是一份契约:模型注册表中的每个产物都必须附带一个复现命令。运行该命令,你就能得到完全相同的产物,无论是逐字节一致,还是在你明确定义并测试的容差范围内。如果你无法为一个产物编写这样的命令,那么该产物就是不可维护的,你应该将其在生产堆栈中的存在视为一笔到期日期未知的技术债。
从 Notebook 到流水线的“转换税”
大多数 微调产物(fine-tune artifacts)不可复现的真实原因是:产出它们的工作是研究,而维护它们的工作是工程。这两项工作通常由同一个人以相同的头衔完成,且两者之间没有明确的过渡。
研究用的 notebook 是迭代的理想工具。你可以探索空间、尝试方案、丢弃无效的部分并保留有效的。你保留下来的代码单元(cells)是对最终结果有贡献的部分,但你并不会对它们进行标注——你会记得哪些是重要的,因为你昨天刚写过,明天还要再跑一遍。Notebook 积累了漂浮的代码单元、死分支以及作为环境状态编码的各种假设。它只有在你刚好按顺序执行时才能从头到尾运行;内核(kernel)会静默地保存早期运行中的变量,而下方的代码单元会在不指名的情况下依赖这些变量。
将该 notebook 转化为可维护训练任务的工程工作是真实存在的,而且并不光鲜。它包括:
- 将代码单元提取到具有版本化入口点和配置文件的脚本中。
- 将手动过滤步骤——比如“我删除了这些行,因为它们看起来很奇怪”——替换为代码、解释准则的注释以及针对小型固定数据(fixture)的单元测试。
- 将依赖图固定到容器中。
- 记录流水线涉及的每个外部资源,并验证当另一个账号以只读方式访问这些资源时,流水线是否仍能运行。
- 在干净的环境中端到端运行脚本,并断言生成的权重在定义的容差范围内与原始产物匹配。
这项工作经常被推迟。原因是结构性的:交付模型的团队因评估指标(eval)的提升而获得赞誉,而不是因为围绕它进行的工程规范。晋升案例和季度回顾奖励的是交付的功能和提升的指标。它们很少奖励一个还没人需要去碰的干净流水线。因此,流水线一直停留在 notebook 阶段,关于其运作方式的组织记忆(institutional memory)留在构建者的脑子里,而团队正在进行一场隐性赌注:赌下次需要重新生成该产物时,这个人依然在场。
这个赌注在出事之前看起来总是划算的。
弥合差距的模式
如果微调团队将这种失败模式视为可预见的,就可以通过几个具体的实践来弥合差距。这些实践都不是新鲜事。它们之所以经常被跳过,是因为它们的价值在那些你“不需要重跑”的时刻最为明显。
注册表中的复现命令。 模型注册表中的每个产物都应附带生成它的准确命令,加上容器镜像摘要(digest)、数据集哈希和运行所需的硬件规格。如果继任者无法通过该命令产出该产物,那么该产物并未真正进入注册表,进入的只是它的权重。在训练时而非训练后编写此命令的纪律,能让你在问题尚可修复时,发现缺失的部分——比如只运行过一次的手动单元,或者指向个人主目录的数据路径。
定期的重新训练演练。 每季度一次,值班工程师根据当前流水线重新生成生产环境产物的样本,并在稳定的评估集上断言行为一致性。这能捕捉到依赖漂移(dependency drift)、上游资源的意外更改,以及因为生产产物运行良好而无人问津的微调流水线的缓慢腐烂。演练还迫使团队保持复现命令是最新的;今天跑不通的命令,上个季度肯定也跑不通。
微调评审委员会。 在微调产物被允许承接生产流量之前,一个拥有工程所有权的小组会审查产出它的流水线,并签字确认该产物 符合注册表的可复现性契约。该委员会不是研究评审——评估分数和行为一致性仍由研究员决定。它是一项工程评审,其职责是在 notebook 跨越生产线之前,将其转化为可维护的训练任务。委员会比交接流程更具可持续性,因为它不与特定人员的离职挂钩。
明确交付物的招聘范畴。 当开设机器学习工程角色时,职位描述应将“留下可复现的流水线”列为与“交付模型”权重相等的交付物。这听起来很官僚,事实也的确如此,但如果不这样做的后果是:团队奖励的交付物会被写进晋升报告,而团队不奖励的交付物会被推迟到无法完成为止。
这些实践并不能消除对研究员判断力的依赖。关于包含哪些数据、扫描哪些超参数以及如何评估微调的选择仍然是真实的,且往往是隐性的知识。它们所做的是将可以制度化的工作部分——流水线、复现命令、评估契约——与无法制度化的部分分开,从而使制度层在任何一个人的离开后都能存续。
微调是一个闭包,而非一个文件
架构上实现闭环的关键认知是:微调是流水线在训练集上的闭包。只交付文件的团队,仅仅交付了该闭包在特定某一天的值。如果流水线在第二天发生了变化——无论是由于依赖项迁移、数据源轮转,还是基础模型被弃用——闭包的值就会随之改变。而那些没有保持闭包可执行性的团队,将无法重新计算它。
同样的逻辑也适用于许多其他 ML 产物。检索索引是嵌入模型(embedding model)在语料库上的闭包。评估集是标注协议在样本上的闭包。提示词模板是模型版本在指令上的闭包。在每一种情况下,如果将文件视为产物,而将闭包视为环境背景,实际上就是在交付一个源代码不在你团队掌握之中的生产依赖。
这种捷径的代价在账单到期之前是隐形的,而账单总会到期。基础模型会被弃用,工程师会离职,数据集会轮转。那些没有维护闭包的团队会发现,恢复原始产物的唯一方法是依靠记忆,而记忆并不是一种可维护的基质。而那些维护了闭包的团队,只需运行流水线并交付新的产物,将迁移视为一次冲刺(sprint),这才是它本来该有的样子。
- https://valohai.com/blog/the-bus-factor-in-machine-learning-development/
- https://introl.com/blog/model-versioning-infrastructure-mlops-artifact-management-guide-2025
- https://atlan.com/know/llm-training-data-versioning-strategies/
- https://aws.amazon.com/blogs/machine-learning/end-to-end-lineage-with-dvc-and-amazon-sagemaker-ai-mlflow-apps/
- https://speytech.com/ai-architecture/deterministic-ml-pipeline/
- https://medium.com/the-constellar-digital-technology-blog/exploring-lora-on-google-colab-the-challenges-of-base-model-upgrades-91fd9809511c
- https://arxiv.org/pdf/2501.16559
- https://www.databricks.com/blog/llm-fine-tuning
