跳到主要内容

数据截至 (上游 commit 352f1bd7c1a0)

01 · Rollout 与事件:中央账本

本章讲什么: v1.0 把旧版那套庞大的 LightningStore(带 SQLite/Mongo 后端、心跳、重试状态机)砍成了一个纯内存的 FastAPI 服务。它只记三样东西——rollout(任务)、event(事件)、model(模型端点)。搞懂这三本账和 Rollout 的状态机,你就抓住了整个框架的骨架。

1. 为什么账本长这么「穷」

先看背景。学习侧(Trainer,GPU 集群)和执行侧(Controller + agent,CPU 机器或 K8s 集群)可能完全不在一起。它们要交换两样东西:

  • 执行侧要往学习侧送遥测(这趟 agent 干了啥、得了多少分)。
  • 学习侧要往执行侧送任务和模型端点(跑哪些题、模型在哪)。

旧版 v0.x 的解法是自研一套多后端 store(内存/SQLite/Mongo 三种实现),v1.0 的解法是把这笔账砍到最小(依据:README.md:15 “simplicity as the first principle”):

设计选择落点
状态全放进程内存,三本模块级字典,无锁agentlightning/server/store.py:13-18
单进程单事件循环,天然串行、不加锁agentlightning/server/__main__.py:17-22uvicorn.run(..., workers=1)
对外只有 REST API,客户端只认 HTTPagentlightning/server/routes/
状态一变即终局,没有重试/心跳/租约这些概念agentlightning/schemas.py:87-100

store.py 的模块 docstring 直说了这是刻意的:“single-threaded, no locks, plain dict/list”(依据:agentlightning/server/store.py:3)。代价也明确:重启即丢——所以学习侧用「完成即删」来控制账本体积(见第 04 章),长期持久化不是这个框架的目标。

2. Rollout:一道题的执行单元

2.1 字段一览

Rollout(依据:agentlightning/schemas.py:175-183)是账本的主实体,一次「agent 在一个输入上跑一趟」:

  • rollout_id:全局唯一 id,创建时可由调用方预指定(幂等的关键,见 §4)。
  • input:任务载荷,Any 类型——一道数学题、一个 GitHub issue 都行。
  • is_train:训练/验证标记,决定代理走哪套采样参数(见第 02 章)。
  • config:执行配置(RolloutConfigagentlightning/schemas.py:118-123),含超时秒数、local/k8s 专属配置。
  • metadata:算法侧的批上下文(RolloutMetadataagentlightning/schemas.py:126-132,字段 batch_idxsample_idx_in_batch,且 extra="allow" 可扩展)。
  • status:生命周期状态(见 §3)。

执行配置按执行环境分两支:local 模式指定 agent_class(Python 类路径)和 env_map(环境变量映射,agentlightning/schemas.py:105-110);k8s 模式指定一整份 job_template(Jinja2 模板字符串,agentlightning/schemas.py:112-115)。

2.2 rollout ≠ 训练样本

一个容易混的点:一个 rollout 只是一次执行,不是一条训练数据。GRPO 这类算法要比较「同一道题的多个答案」,所以 Trainer 会为同一样本创建多个独立 rollout(依据:agentlightning/verl/agl_rollout_manager.py:250-257,每个样本按 rollout.n 重复排队,共用一个 data_id 分组)。一次 rollout 内部又可能包含多轮模型调用——每轮都会变成一条训练行(见第 04 章)。

3. 四态状态机(Store 强校验)

3.1 状态与转移

RolloutState(依据:agentlightning/schemas.py:77-83)只有四个状态,比旧版的七态砍掉了一半:

Controller 领取并启动 agent 执行完成 执行出错/超时
queuing ─────────────▶ running ─────────────▶ succeeded
│ │
│ └───────▶ failed ◀──┘(含启动失败)
└──(启动即失败也直接 failed)
终态(succeeded/failed)不可再转移

合法转移表是数据不是约定——VALID_TRANSITIONS 字典定义在 schemas 里(agentlightning/schemas.py:87-93),Store 的 PATCH 端点在每次更新时查表,非法转移直接返回 409(agentlightning/server/routes/rollouts.py:191-194_invalid_transition 构造冲突响应)。

对比旧版:没有 requeuing(重试)、没有 cancelled、没有 attempt 级别的 unresponsive/timeout失败就是失败,要不要重来由算法侧自己决定(重排一个新 rollout 即可)。

3.2 终态单向性带来的免费好处:完成序日志

因为终态不可逆,一个 rollout 恰好进入终态一次。Store 利用这一点维护了一本 append-only 的完成顺序日志 _terminal_order(依据:agentlightning/server/store.py:18agentlightning/server/routes/rollouts.py:210-212——PATCH 进终态时 append 恰好一次)。

于是「已完成的 rollout」可以游标分页GET /api/rollouts/terminal?after=Nlist_terminal_rolloutsagentlightning/server/routes/rollouts.py:143-171)。客户端把上次的 next_after 传回来,就只拿「上次之后新完成的」,不会漏也不会重。这个端点只返回轻量投影(id/state/data_id/is_train),要细节再逐个查事件。

4. Event:插入序即身份

4.1 模型

Event(依据:agentlightning/schemas.py:13-26)四个字段:event_typerollout_idattempt_idtimestamp(Store 写入时刻分配)、data(任意 payload)。关键设计写在 docstring 里:

“Position in the list is the identity — no separate event ID needed.”

事件没有 id,它在列表里的位置就是身份。事件按 rollout → attempt 两级分桶、按插入序追加(record_eventagentlightning/server/routes/events.py:24-41)。这取代了旧版那套 OpenTelemetry span + 全序 sequence_id 的机制——因为 v1.0 里所有事件都经过同一个单线程服务写入,顺序天然确定,不再需要跨机器时钟同步方案。

4.2 两种「知名」事件,其余全透明

只有两种事件有约定结构,其他类型原样透传(依据:agentlightning/schemas.py:17-19):

事件类型谁写的结构用途
model_requestGateway 代理自动写ModelRequestDataschemas.py:36-53):完整请求/响应体、token id、logprobs、延迟、重试次数训练样本的原料
rewardagent(或评估器)写RewardDataschemas.py:56-66):标量 value + 可选 message/source/reason这趟的得分
其他任意字符串任何人写不校验诊断、监控、hook 回写

注意这两种结构只是文档约定,Store 不强制(docstring 明说 “Not enforced by the Store”,agentlightning/schemas.py:39-41)——校验发生在消费端(训练桥接会过滤坏样本,见第 04 章)。

4.3 attempt:被简化到只剩一个常量

事件按 attempt_id 分桶,但 v1.0 里它实际上恒为 "0"DEFAULT_ATTEMPT_IDagentlightning/schemas.py:102;Controller spawn 时写死这个值,见第 03 章)。URL 里保留这个字段(/attempt/{attempt_id}/...)是为将来「同一任务多次尝试」留的形状,当前版本没有重试语义(inferred:代码中无任何地方生成非零 attempt_id)。

4.4 查询与「triplet 精简」

GET /api/rollouts/{id}/events 查询默认 attempt 的事件(_query_eventsagentlightning/server/routes/events.py:44-59)。加 format=triplet 时做两步精简(query_eventsagentlightning/server/routes/events.py:196-210):

  1. 瘦身model_request 事件只保留 prompt_token_ids/response_token_ids/response_log_probs 和模型版本(_trim_model_requestevents.py:100-144);reward 只留标量(_trim_rewardevents.py:147-153)。
  2. 去重:同一个 prompt_token_ids 出现多次时只保留最后一次调用(_dedupe_model_requests_by_prompt_token_idsevents.py:172-187)——防同一前缀的重试/重复调用灌进训练集。

这条 format=triplet 通道就是旧版「span→triplet 适配器」在 v1.0 的化身:翻译逻辑从客户端库搬进了账本本身,消费端一行 HTTP 就拿到干净样本原料。

5. 创建的幂等性与删除

批量创建端点 POST /api/rolloutsenqueue_rolloutsagentlightning/server/routes/rollouts.py:95-122)有个对分布式重试至关重要的细节:请求可以自带 rollout_idRolloutCreate.rollout_idschemas.py:142-143)。已存在的 id 直接返回已有 rollout、事件原封不动——这让「创建 + 超时 + 重试」不会产生重复任务。Trainer 侧正是预生成 uuid 再配合客户端重试来用的(见第 04 章)。

删除是幂等 no-op(delete_rolloutagentlightning/server/routes/rollouts.py:216-221),连事件一起清。Trainer 拉完一个完成 rollout 的数据就删一个,账本体积因此有界。

6. 模型注册表:第三本账

_models模型名 → endpoint → Model 两级字典存(agentlightning/server/store.py:15)。POST /api/models 按 (model, endpoint) upsert(register_modelsagentlightning/server/routes/models.py:15-24)。为什么是一个模型名对应多个 endpoint?因为 verl 的 vLLM 推理引擎可以多副本并行——代理层要做负载选择,这是第 02 章的主角。


小结: 这本账本的哲学是「够用就好」——四态状态机靠一张转移表强校验、事件以插入序为身份不需要全局序号发放器、幂等创建让重试安全、完成序日志让增量拉取不重不漏。没有数据库、没有后台任务、没有锁。下一章看架在这本账本上的反向代理怎么把「跑 agent」变成「采数据」。