首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >21-EngineRegistry如何让上层Trainer不关心底层后端

21-EngineRegistry如何让上层Trainer不关心底层后端

作者头像
anzhsoft
发布2026-07-16 21:30:11
发布2026-07-16 21:30:11
100
举报

Registry 隔离的不是复杂度本身,而是复杂度进入系统的位置。 从FSDP/Megatron的具体差异回到抽象层,追踪EngineRegistry如何按model_type、strategy和device选择后端,BaseEngine又如何固定训练、推 理、rank、checkpoint与权重导出合同。读完能明白:verl不是消除后端复杂度,而是把复杂度交给正确的所有者

第 19 篇和第 20 篇分别拆了 FSDP/FSDP2 与 Megatron/MCore。读到这里,很容易产生一个反问:既然两个后端差异这么大,为什么上层 trainer 还能像调用同一个训练对象一样调用它们?第 21 篇回到抽象层,回答 EngineRegistryBaseEngine在 verl 里的真实作用。

本文的核心判断是:EngineRegistry不是一个“按名字 new 类”的小工具,而是 worker 内部的后端边界。它用 model_type + strategy + device选出具体 engine;BaseEngine固定训练、推理、rank、checkpoint、权重导出的最低合同;TrainingWorker只依赖这组合同组织 mini-batch、loss、metric 和 dispatch/collect。这样,上层 PPO/GRPO 不需要知道 FSDP 怎么 shard、Megatron 怎么 pipeline,也不需要知道 NPU 上 MindSpeed 怎么 patch;但每个后端必须把自己的分布式现实翻译成同一组方法。

先看这层抽象在第四组里的位置。读图时注意:EngineRegistry并不替代 FSDP 或 Megatron 的内部逻辑,它只固定从 worker 进入后端的门。

EngineRegistry 在训练 worker 中的位置

源码入口很短:TrainingWorker.__init__()verl.workers.engine导入 BaseEngineEngineRegistry,拿到 model_typeengine_config.strategy、model/engine/optimizer/checkpoint config 后调用 EngineRegistry.new(...);随后 worker 立即用 engine.get_data_parallel_rank()engine.is_mp_src_rank_with_outputs()注册 dispatch/collect 信息(verl/workers/engine_workers.py:83-146)。这说明 engine 一创建,就必须能回答“我在哪个 DP rank、哪些 rank 负责返回输出”。

1. 后端选择从配置开始,但不止是 strategy

很多人会把这层理解成 strategy=fsdpstrategy=megatron的字符串分发。这个理解只对了一半。verl 的 registry key 实际包含三层:模型类型、后端名和设备类型。

下面这张图把选择路径拆开。看图时注意:actor/critic/ref 先把自己的配置变成 TrainingWorkerConfig,worker 再把 model_typestrategy交给 registry;registry 最后还会读取当前设备名。

从配置到 engine 实例的选择路径

TrainingWorkerConfig里显式保存model_typemodel_configengine_configoptimizer_configcheckpoint_config;如果没有 engine config,还可以通过auto_select_engine_optim_fn(model_config, device_name)自动选择(verl/workers/config/engine.py:595-605verl/workers/engine_workers.py:97-115)。actor 配置里,基础ActorConfig要求strategy存在;McoreActorConfig.__post_init__()会把self.engine指向self.megatronFSDPActorConfig.__post_init__()会把self.engine指向self.fsdp_config,并把actor.strategy同步给 engine config,避免fsdp2被错误落回默认fsdpverl/workers/config/actor.py:149-183259-317)。

到 registry 这一层,EngineRegistry.get_engine_cls()不是只看 backend。它先检查 model_type,再检查 backend,再用 get_device_name()选择设备专属实现(verl/workers/engine/base.py:313-337)。这就是为什么同一个 backend="megatron"在 CUDA 和 NPU 上可能落到不同类:MindSpeed 在 NPU 上用 @EngineRegistry.register(model_type="language_model", backend="megatron", device="npu")提供设备专属入口,CUDA Megatron 则保留自己的注册路径(verl/workers/engine/mindspeed/transformer_impl.py:53-75verl/workers/engine/megatron/transformer_impl.py:789-790)。

2. 注册靠导入副作用完成

EngineRegistry自己没有扫描目录,也没有读配置文件来发现后端。它靠 Python import 执行各后端模块里的装饰器注册。这个机制简单,但很关键:如果后端模块没有被 import,它的 @EngineRegistry.register(...)就不会进入 registry。

下面这张图展示注册过程。看图时注意:verl.workers.engine.__init__的职责不只是导出名字,它还触发内置后端模块加载;每个后端类再通过 decorator 写入 registry 的嵌套字典。

后端类如何注册进 EngineRegistry

verl/workers/engine/__init__.py先导入 BaseEngineEngineRegistry和 FSDP;然后尝试导入 TorchTitan、VeOmni、Automodel、MindSpeed、Megatron。这里还特别注明 MindSpeed 必须在 Megatron 之前导入,以确保 monkey patch 生效(verl/workers/engine/__init__.py:14-64)。真正写入 registry 的地方在 EngineRegistry.register():它要求 engine class 是 BaseEngine子类,然后按 model_type -> backend -> device -> engine_class的结构保存(verl/workers/engine/base.py:278-311)。

这个结构解释了两个工程事实。第一,FSDP 同一个类可以同时注册 backend=["fsdp", "fsdp2"]device=["cuda", "npu"];Megatron language/value model 则分别注册到 language_modelvalue_modelverl/workers/engine/fsdp/transformer_impl.py:908-9091254-1255verl/workers/engine/megatron/transformer_impl.py:789-7901004-1005)。第二,新增后端不是只加一个配置项,还必须导入模块、注册类,并完整实现 BaseEngine合同。

3. BaseEngine 固定的是最低运行合同

如果 registry 只负责选类,那么真正让 trainer 不关心底层后端的是 BaseEngine。它规定每个 engine 至少要能初始化模型与优化器、进入 train/eval 模式、执行 forward/backward、更新 optimizer、保存/加载 checkpoint、返回 DP rank/group、导出逐 tensor 权重,并说明哪些 rank 负责返回输出。

下面这张图把 BaseEngine分成四组能力。看图时注意:train_batch()infer_batch()是基类提供的统一外壳,但 forward_backward_batch()、rank、checkpoint 和权重导出必须由后端实现。

BaseEngine 规定的最低运行合同

BaseEngine.train_batch()的顺序固定为:修正 3D position ids、optimizer_zero_grad()forward_backward_batch(..., forward_only=False)optimizer_step(),并只在 is_mp_src_rank_with_outputs()为真时把 grad_norm写回 metrics;infer_batch()则用 torch.no_grad()forward_backward_batch(..., forward_only=True)verl/workers/engine/base.py:112-148)。这让 worker 可以统一调用训练和推理,但每个后端仍然决定 forward/backward 内部怎么跑。

BaseEngineCtx则把 train/eval mode 和 offload 接起来:进入上下文时,如果参数或 optimizer offload 开启,就调用 engine.to(device=...);退出时再切回 CPU(verl/workers/engine/base.py:229-264)。FSDP、Megatron、TorchTitan 等后端都各自实现 train_mode()eval_mode()、rank 查询和 get_per_tensor_param();例如 FSDP 的输出收集主要看 Ulysses SP rank,Megatron 要同时检查 TP rank、最后 PP rank 和 CP rank,TorchTitan 也按 TP/PP/CP mesh 判断输出 rank(verl/workers/engine/fsdp/transformer_impl.py:168-173587-600verl/workers/engine/megatron/transformer_impl.py:363-368530-543verl/workers/engine/torchtitan/transformer_impl.py:210-286)。

4. TrainingWorker 只编排合同,不进入后端内部

有了 registry 和 base contract,TrainingWorker的工作就变成编排:它负责把上层来的 TensorDict 改造成 engine 能执行的 batch,注入 dynamic batch、micro-batch、remove padding、fused kernels 等工程参数,再调用 engine 的统一方法。

下面这张图展示 worker 生命周期。看图时注意:worker 会频繁调用 engine,但它没有分支去判断“如果是 FSDP 怎么办、如果是 Megatron 怎么办”;分支留在 engine 内部。

TrainingWorker 如何只依赖 BaseEngine 合同

reset()只做一件事:self.engine.initialize()verl/workers/engine_workers.py:169-176)。train_mini_batch()先按 DP size/rank 计算每个 GPU 的 mini-batch,构造 dataloader,再在 engine.train_mode()里循环调用 self.train_batch()train_batch()注入 use_dynamic_bszmax_token_len_per_gpumicro_batch_size_per_gpu等字段,然后调用 self.engine.train_batch(data, loss_function=self.loss_fn)verl/workers/engine_workers.py:238-382)。infer_batch()也类似,只是进入 engine.eval_mode(),可选通过 engine.disable_adapter()暂停 LoRA adapter(verl/workers/engine_workers.py:384-428)。

checkpoint 和权重同步也延续同一个边界。TrainingWorker.save_checkpoint()load_checkpoint()只是转给 engine;ActorRolloutRefWorker.update_weights()在非 naive 模式下拿 self.actor.engine.get_per_tensor_param()交给 checkpoint engine,在 naive 模式下也先通过同一个接口拿逐 tensor 权重,再调用 rollout 的 update_weights()verl/workers/engine_workers.py:430-436663-743)。因此,FSDP 的 state dict 转换、Megatron 的 bridge export、TorchTitan 的 model state dict 收集,都必须在各自 engine 内部完成,不能让 rollout 层猜后端。

5. 抽象隔离了调用方式,但没有消除配置差异

EngineRegistry + BaseEngine带来的收益很明确:上层 trainer 可以围绕 PPO/GRPO 语义组织数据,而不是把 FSDP wrapping、Megatron parallel state、TorchTitan mesh 或 NPU patch 写进算法主循环。但它不是一个完全透明的抽象。

第一,配置仍然显式暴露后端差异。EngineConfig提供 strategy、offload、dynamic batch、micro-batch、remove padding、forward_only 等共享字段;FSDP、MCore、TorchTitan、Automodel 再扩展各自的并行和包装参数(verl/workers/config/engine.py:77-115149-218220-266390-446449-535)。也就是说,trainer 不需要知道后端内部实现,但配置作者必须知道自己选择的后端需要哪些参数。

第二,rank 语义必须由后端翻译出来。worker 只问 get_data_parallel_rank()get_data_parallel_size()get_data_parallel_group()is_mp_src_rank_with_outputs();但 FSDP、Megatron、TorchTitan 对这些问题的答案不同。这个差异会影响 dispatch/collect、metrics 聚合、loss all-reduce 和哪些 rank 返回结果(verl/workers/engine_workers.py:141-146190-236)。

第三,权重导出仍然是后端责任。get_per_tensor_param()是 rollout/weight sync 层看到的统一接口,但 FSDP 会处理 DTensor full tensor、LoRA、QAT;Megatron 会通过 bridge 导出 HF/adapter 权重;TorchTitan 会从 model parts 收集 state dict(verl/workers/engine/fsdp/transformer_impl.py:785-858verl/workers/engine/megatron/transformer_impl.py:715-744verl/workers/engine/torchtitan/transformer_impl.py:484-492)。抽象没有消灭这些成本,只是把成本放在正确的所有者里。

小结:EngineRegistry 是训练后端的所有权边界

本文从 TrainingWorker.__init__()追到 EngineRegistryBaseEngine,看到 verl 的训练后端抽象不是“隐藏所有复杂度”,而是把复杂度分配清楚:

代码语言:javascript
复制
actor/critic/ref config
-> TrainingWorkerConfig
-> EngineRegistry.new(model_type, strategy, device)
-> backend-specific BaseEngine subclass
-> train_batch / infer_batch / rank / checkpoint / weight export
-> Worker dispatch, metrics, rollout weight sync

放回系列地图,第 21 篇补上的是 workers/resources -> training engine/distributed core里的抽象层:为什么第 19 篇的 FSDP 和第 20 篇的 Megatron 可以共用同一个 worker 入口。下一篇会继续向下看 batch 语义,拆开 mini-batch、micro-batch 和 dynamic batch size:算法到底在控制什么,训练 engine 又在控制什么。

本文源码索引

  • verl/workers/engine_workers.py:83-146TrainingWorker如何读取配置、选择 engine,并注册 dispatch/collect 信息。
  • verl/workers/config/engine.py:595-605TrainingWorkerConfig的模型、engine、optimizer、checkpoint 配置,以及自动选择 hook。
  • verl/workers/config/actor.py:149-183259-317:actor strategy 与 engine config 如何绑定,尤其是 Megatron 和 FSDP/FSDP2。
  • verl/workers/engine/__init__.py:14-64:内置后端如何通过 import 触发注册,MindSpeed 为什么要在 Megatron 前导入。
  • verl/workers/engine/base.py:29-226BaseEngine规定的训练、推理、rank、checkpoint、权重导出接口。
  • verl/workers/engine/base.py:229-264BaseEngineCtx如何处理 train/eval mode 与 offload 切换。
  • verl/workers/engine/base.py:267-337EngineRegistry.register()get_engine_cls()new()的 key 结构与实例化逻辑。
  • verl/workers/engine/fsdp/transformer_impl.py:168-173587-600740-858908-9091254-1255:FSDP 的输出 rank、DP group、checkpoint、权重导出和注册。
  • verl/workers/engine/megatron/transformer_impl.py:363-368443-461530-543551-744789-7901004-1005:Megatron 的输出 rank、mode、DP group、checkpoint、权重导出和注册。
  • verl/workers/engine/torchtitan/transformer_impl.py:210-286484-492573-574:TorchTitan 如何实现同一组 rank、mode 和权重导出接口。
  • verl/workers/engine/mindspeed/transformer_impl.py:53-75:同一个 backend="megatron"在 NPU 设备上如何注册到 MindSpeed 实现。
  • verl/workers/engine_workers.py:238-428TrainingWorker如何通过 engine 合同执行 train/infer,并做 metrics 后处理。
  • verl/workers/engine_workers.py:430-436663-743:checkpoint 和 rollout 权重同步如何通过 engine 接口收束。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 训推工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 后端选择从配置开始,但不止是 strategy
  • 2. 注册靠导入副作用完成
  • 3. BaseEngine 固定的是最低运行合同
  • 4. TrainingWorker 只编排合同,不进入后端内部
  • 5. 抽象隔离了调用方式,但没有消除配置差异
  • 小结:EngineRegistry 是训练后端的所有权边界
  • 本文源码索引
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档