04 · Trainer、算法与进程编排
本章讲什么: 前两章是执行侧和翻译层,这一章是学习侧的「总装车间」。
Trainer负责把所有零件接线并发动;Algorithm是可插拔的训练大脑(RL、提示词优化都行);ExecutionStrategy决定这些零件跑在同一进程还是跨进程/跨机。
1. Trainer:总装工
1.1 它把什么接到什么
Trainer(trainer/trainer.py:36)本身不训练,它是接线板。构造时把用户给的一堆「组件说明」解析成实例(依据:trainer/trainer.py:120-250):
| 组件 | 默认值 | 干什么 |
|---|---|---|
algorithm | 无(dev 模式给 Baseline) | 训练大脑 |
store | InMemoryLightningStore | 中央账本(第 01 章) |
runner | LitAgentRunner | 跑 agent(第 02 章) |
tracer | AgentOpsTracer | 录 span(第 02 章) |
adapter | TracerTraceToTriplet | span→triplet(第 03 章) |
strategy | ClientServerExecutionStrategy | 进程编排 |
llm_proxy | 无(算法可自建) | token id 回传(第 03 章) |
每个组件都能用「实例 / 类 / 工厂函数 / 注册名字符串 / 配置字典」任一形态传入,由 build_component(trainer/init_utils.py)统一解析(依据:trainer/trainer.py:141-146 的说明)。这让配置很灵活:既能 Trainer(algorithm=VERL(config)),也能用字典声明式配。
构造函数里还有大量向后兼容处理——n_workers→n_runners、max_tasks→max_rollouts、triplet_exporter→adapter,旧参数会 warn 并映射到新参数(依据:trainer/trainer.py:166-216)。这说明项目在快速演进、且认真维护迁移路径。
1.2 两个入口:fit 和 dev
| 入口 | 用途 | 约束 |
|---|---|---|
fit(agent, train, val) | 正式训练 | 完整跑算法+Runner 循环 |
dev(agent, train, val) | 快速干跑/调试 | 要求算法继承 FastAlgorithm,没给就用 Baseline |
依据:trainer/trainer.py:394 fit、:440 dev(dev 里 isinstance(algorithm, FastAlgorithm) 的强校验在 :472)。
1.3 接线的真相:两个 bundle 交给策略
fit() 的核心其实很短(依据:trainer/trainer.py:428-438):把「算法要干的活」和「Runner 要干的活」各打包成一个函数(bundle),连同 store 一起丢给执行策略:
# 示意,对应 trainer/trainer.py:430-438
algorithm_bundle = partial(self._algorithm_bundle, train_dataset=..., algorithm=self.algorithm)
runner_bundle = partial(self._runner_bundle, agent=agent)
self.strategy.execute(algorithm_bundle, runner_bundle, self.store)
- 算法 bundle(
_algorithm_bundle,trainer/trainer.py:487):把 store、adapter、初始资源、llm_proxy 绑给算法,然后调algorithm.run()。若没给算法,就单纯等停止信号(方便手动往队列灌任务)。 - Runner bundle(
_runner_bundle,trainer/trainer.py:535):初始化 runner、绑 agent 和 hooks、init_worker,然后进runner.iter()主循环;finally里保证 teardown。
关键分层:Trainer 只管「接线和打包」,不管「进程怎么起」。后者是 ExecutionStrategy 的事。这个解耦让同一套训练逻辑能在单机调试和分布式生产之间无缝切换。
2. Algorithm:可插拔的训练大脑
2.1 统一契约
所有算法继承 Algorithm(algorithm/base.py:25),核心就一个 run(train_dataset, val_dataset)(algorithm/base.py:135)。基类还提供一组「被 Trainer 注入依赖」的 setter:set_store / set_adapter / set_llm_proxy / set_initial_resources(依据:algorithm/base.py:86-130)。算法在 run 里就靠这些拿到 store、往里排任务、读 span、发布新资源——它不直接认识 Runner,全程隔着 store 对话。
用 is_async()(algorithm/base.py:34)自动探测 run 是不是协程,决定同步/异步执行。
2.2 三派算法
| 算法 | 优化什么 | 路子 | 定义 |
|---|---|---|---|
VERL | 模型权重 | 把 triplet 喂给 VERL 的 PPO/GRPO 训练器 | algorithm/verl/interface.py:20 |
APO | 提示词 | 文本梯度 + beam search,不碰权重 | algorithm/apo/apo.py:81 |
Baseline | 不优化 | 把数据集全跑一遍、打日志——冒烟测试 | algorithm/fast.py:37 |
VERL —— RL 权重训练
VERL(algorithm/verl/interface.py:20)把训练委托给 VERL(一个 RL 训练库)的 PPO 运行器。你给它一个大 config 字典(镜像 VERL CLI 的覆盖项),它用 Hydra 合并进 VERL 默认配置再启动(依据:interface.py:28-34)。config 里能配 adv_estimator: grpo、批大小、vLLM rollout、FSDP offload、LoRA 等(真实完整示例见 examples/calc_x/train_calc_agent.py:44-107 verl_default_config)。
一个进阶特性是轨迹级聚合(trajectory aggregation)(依据:interface.py:35-58):把一整条多轮轨迹合并成一个带 mask 的训练样本,GPU 只算一次而不是每轮算一次,省算力。开关是 config["agentlightning"]["trace_aggregator"] = {"level": "trajectory", ...}。
APO —— 提示词优化
APO(algorithm/apo/apo.py:81)走完全不同的路——不动权重,只搜提示词。它用 LLM 生成「文本梯度(textual gradient)」:读一批 rollout 的结果和 span,让一个 LLM 批评「这个提示词哪里不好」,再让另一个 LLM 按批评改写出新提示词,然后 beam search 保留 top-k(依据:apo/apo.py:81-99 docstring,思想源自 ProTeGi / TextGrad)。它优化的资源是 PromptTemplate。这对应第 02 章 @prompt_rollout 的 agent。
Baseline —— 冒烟测试
Baseline(algorithm/fast.py:37,继承 FastAlgorithm)不训练,就是把数据集流过队列、等每题跑完、把每个 span 和奖励打成日志(依据:fast.py:37-44)。作用是验证整条管道通不通——Trainer.dev() 默认用它。它还会边跑边用 adapter 把 span 转成 triplet 打印出来(fast.py:141-144),方便你肉眼检查提取对不对。它的排队逻辑做了背压:队列里超过 max_queue_length 就等一等再灌(fast.py:233-245)。
3. ExecutionStrategy:进程怎么起
3.1 契约
ExecutionStrategy(execution/base.py:35)就一个方法 execute(algorithm_bundle, runner_bundle, store)(execution/base.py:50)。它不看 bundle 内部,只决定:起几个 runner、算法和 runner 之间靠共享内存还是 HTTP 通信、怎么响应停止信号(依据:execution/base.py:36-48)。
3.2 两种主要策略
| 策略 | 怎么跑 | 适合 | 定义 |
|---|---|---|---|
SharedMemoryExecutionStrategy | 同机多进程,store 走共享内存 | 单机 | execution/shared_memory.py:20 |
ClientServerExecutionStrategy | 算法侧起一个 LightningStoreServer(HTTP),runner 侧用 LightningStoreClient 连 | 跨机分布式 | execution/client_server.py:22 |
客户端-服务端策略是默认(依据:trainer/trainer.py:326-329)。它把内存 store 包一层 HTTP 服务端(LightningStoreServer),runner 通过 LightningStoreClient 远程调用——对上层完全透明,runner 代码里 store.dequeue_rollout() 长得和本地调用一模一样,实际是一次 HTTP 请求(依据:client_server.py:132-181)。默认端口 4747(client_server.py:111)。
这解释了为什么第 01 章说「客户端-服务端模式下内存 store 要 thread_safe=True」— —因为服务端会并发处理多个 runner 的请求。
3.3 角色分离与外部 store
通过环境变量还能把算法侧和 runner 侧拆成两条独立命令分别启动,并连到一个外部 store 服务(agl store --port 9999)——依据:examples/calc_x/train_calc_agent.py:11-28 的用法说明(AGL_MANAGED_STORE=0、AGL_CURRENT_ROLE=algorithm|runner)。这是生产环境把训练节点和一堆 rollout worker 解耦部署的方式。
4. 完整拼图:一条命令背后发生了什么
把本章和前几章连起来,看 trainer.fit(calc_agent, train, val) 一句话背后的全链路:
trainer.fit(agent, train, val)
│
├─ 打包 algorithm_bundle + runner_bundle
└─ strategy.execute(...)
│
├─【算法侧进程】起 LightningStoreServer,跑 algorithm.run():
│ 发布初始 resources → 循环 enqueue_rollout → wait/读 span
│ → adapter 转 triplet → VERL 训一步 → update_resources
│
└─【runner 侧 ×N 进程】LightningStoreClient 连上,跑 runner.iter():
dequeue → 进 tracer → 调 agent → 落 span → 标成败
两侧唯一的交汇点永远是 store。这就是开篇那句「谁也不认识谁」的完整兑现。
小结: Trainer 是接线板,把六大组件解析、打包成两个 bundle,交给 ExecutionStrategy 决定跨不跨进程地跑起来。算法侧统一继承 Algorithm、隔着 store 干活,分 VERL(权重)/APO(提示词)/Baseline(冒烟) 三派。最后一章收口:巧妙之处、边界、横向对比、代码地图。