
Registry 隔离的不是复杂度本身,而是复杂度进入系统的位置。 从FSDP/Megatron的具体差异回到抽象层,追踪EngineRegistry如何按model_type、strategy和device选择后端,BaseEngine又如何固定训练、推 理、rank、checkpoint与权重导出合同。读完能明白:verl不是消除后端复杂度,而是把复杂度交给正确的所有者
第 19 篇和第 20 篇分别拆了 FSDP/FSDP2 与 Megatron/MCore。读到这里,很容易产生一个反问:既然两个后端差异这么大,为什么上层 trainer 还能像调用同一个训练对象一样调用它们?第 21 篇回到抽象层,回答 EngineRegistry和 BaseEngine在 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导入 BaseEngine和 EngineRegistry,拿到 model_type、engine_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 负责返回输出”。
很多人会把这层理解成 strategy=fsdp或 strategy=megatron的字符串分发。这个理解只对了一半。verl 的 registry key 实际包含三层:模型类型、后端名和设备类型。
下面这张图把选择路径拆开。看图时注意:actor/critic/ref 先把自己的配置变成 TrainingWorkerConfig,worker 再把 model_type和 strategy交给 registry;registry 最后还会读取当前设备名。

从配置到 engine 实例的选择路径
TrainingWorkerConfig里显式保存model_type、model_config、engine_config、optimizer_config和checkpoint_config;如果没有 engine config,还可以通过auto_select_engine_optim_fn(model_config, device_name)自动选择(verl/workers/config/engine.py:595-605,verl/workers/engine_workers.py:97-115)。actor 配置里,基础ActorConfig要求strategy存在;McoreActorConfig.__post_init__()会把self.engine指向self.megatron,FSDPActorConfig.__post_init__()会把self.engine指向self.fsdp_config,并把actor.strategy同步给 engine config,避免fsdp2被错误落回默认fsdp(verl/workers/config/actor.py:149-183,259-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-75,verl/workers/engine/megatron/transformer_impl.py:789-790)。
EngineRegistry自己没有扫描目录,也没有读配置文件来发现后端。它靠 Python import 执行各后端模块里的装饰器注册。这个机制简单,但很关键:如果后端模块没有被 import,它的 @EngineRegistry.register(...)就不会进入 registry。
下面这张图展示注册过程。看图时注意:verl.workers.engine.__init__的职责不只是导出名字,它还触发内置后端模块加载;每个后端类再通过 decorator 写入 registry 的嵌套字典。

后端类如何注册进 EngineRegistry
verl/workers/engine/__init__.py先导入 BaseEngine、EngineRegistry和 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_model和 value_model(verl/workers/engine/fsdp/transformer_impl.py:908-909,1254-1255,verl/workers/engine/megatron/transformer_impl.py:789-790,1004-1005)。第二,新增后端不是只加一个配置项,还必须导入模块、注册类,并完整实现 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-173,587-600,verl/workers/engine/megatron/transformer_impl.py:363-368,530-543,verl/workers/engine/torchtitan/transformer_impl.py:210-286)。
有了 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_bsz、max_token_len_per_gpu、micro_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-436,663-743)。因此,FSDP 的 state dict 转换、Megatron 的 bridge export、TorchTitan 的 model state dict 收集,都必须在各自 engine 内部完成,不能让 rollout 层猜后端。
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-115,149-218,220-266,390-446,449-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-146,190-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-858,verl/workers/engine/megatron/transformer_impl.py:715-744,verl/workers/engine/torchtitan/transformer_impl.py:484-492)。抽象没有消灭这些成本,只是把成本放在正确的所有者里。
本文从 TrainingWorker.__init__()追到 EngineRegistry和 BaseEngine,看到 verl 的训练后端抽象不是“隐藏所有复杂度”,而是把复杂度分配清楚:
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-146:TrainingWorker如何读取配置、选择 engine,并注册 dispatch/collect 信息。verl/workers/config/engine.py:595-605:TrainingWorkerConfig的模型、engine、optimizer、checkpoint 配置,以及自动选择 hook。verl/workers/config/actor.py:149-183、259-317:actor strategy 与 engine config 如何绑定,尤其是 Megatron 和 FSDP/FSDP2。verl/workers/engine/__init__.py:14-64:内置后端如何通过 import 触发注册,MindSpeed 为什么要在 Megatron 前导入。verl/workers/engine/base.py:29-226:BaseEngine规定的训练、推理、rank、checkpoint、权重导出接口。verl/workers/engine/base.py:229-264:BaseEngineCtx如何处理 train/eval mode 与 offload 切换。verl/workers/engine/base.py:267-337:EngineRegistry.register()、get_engine_cls()和 new()的 key 结构与实例化逻辑。verl/workers/engine/fsdp/transformer_impl.py:168-173、587-600、740-858、908-909、1254-1255:FSDP 的输出 rank、DP group、checkpoint、权重导出和注册。verl/workers/engine/megatron/transformer_impl.py:363-368、443-461、530-543、551-744、789-790、1004-1005:Megatron 的输出 rank、mode、DP group、checkpoint、权重导出和注册。verl/workers/engine/torchtitan/transformer_impl.py:210-286、484-492、573-574:TorchTitan 如何实现同一组 rank、mode 和权重导出接口。verl/workers/engine/mindspeed/transformer_impl.py:53-75:同一个 backend="megatron"在 NPU 设备上如何注册到 MindSpeed 实现。verl/workers/engine_workers.py:238-428:TrainingWorker如何通过 engine 合同执行 train/infer,并做 metrics 后处理。verl/workers/engine_workers.py:430-436、663-743:checkpoint 和 rollout 权重同步如何通过 engine 接口收束。