在 Agent-lightning 下服务 LLM¶
Agent-lightning 专注于数据、学习信号和控制流——而非运行模型推理。本次深入探讨将解释如何与 Agent-lightning 一起服务模型,以便运行器可以可靠地调用它,LLM 代理如何融入循环,以及如果您关心训练和评估的正确性,为什么token IDs 很重要。
LLM 服务通用背景¶
如果您想训练模型,尤其是在使用模型自身的生成作为训练数据时,服务模型至关重要。我们将简要回顾通用背景,以确保所有读者达成一致。
现代 LLM 服务器解决了一个困难的调度问题:在处理不同长度的提示、在到达时流式传输 token,以及将大型 KV 缓存放入有限的内存中,同时保持 GPU 的完全利用率。像 连续批处理 和 分页注意力 这样的技术解决了这些挑战。连续批处理交错请求中的解码,以有效地重用权重;通过仔细的内存规划,它可以在不增加延迟的情况下实现显著的吞吐量提升。PagedAttention 减少了 KV 缓存碎片,从而使批处理在序列增长时保持有效。请参阅 vLLM 的 PagedAttention 论文和 行业分析以了解详细信息。平衡推理的正确性和效率很困难——Thinking Machines Labs 的一篇最新博客强调了推理的非确定性最终如何影响训练。
除了调度之外,服务器还会公开 HTTP API,通常是OpenAI 兼容的(/v1/chat/completions 和 /v1/responses),这本身就是一个复杂的堆栈。除了文本提示和聊天消息之外,API 还定义了许多参数和响应字段,例如 工具调用、结构化输出和 多模态支持。许多框架都投入了大量精力来实现所有这些参数。像 vLLM 和 SGLang 这样的流行引擎都配备了 OpenAI 兼容的前端,因此您可以重用现有的客户端代码。Ollama 和 llama.cpp 提供了类似的功能。但是,由于模型在内部不同,每个框架对 API 的解释和实现略有不同。即使使用相同的请求,传递给模型的 token 也会因框架而异而发生很大变化。
Agent-lightning 对已服务 LLM 的期望¶
上述大多数问题都有解决方法或仍然是开放的研究问题。请记住它们,但关键问题是:Agent-lightning 对已服务 LLM 有什么期望?答案至少包括两点
- 一个 OpenAI 兼容的 Chat Completions 或 Responses 端点,代理可以在 rollout 期间调用。
- 可选的训练和调试信号:logprobs、usage,以及理想情况下 token IDs。(OpenAI 的公共 API 公开了 usage 和 logprobs,但不公开 token IDs——稍后会讨论为什么 IDs 很重要。)
启动服务框架¶
对于许多算法,您将在 rollout 之前启动一个引擎(例如 vLLM 或 SGLang),然后在之后关闭它以释放 GPU 内存。大多数框架提供一个“serve”单行命令来启动 OpenAI 兼容的服务器。您可以使用这些命令启动带有检查点的 /v1/chat/completions,确保启用了流式传输和任何必需的工具调用功能。一个工作示例在 Unsloth SFT 中展示。
权重更新——在每个训练步骤之后发生——更加棘手。像 vLLM 这样的某些框架支持热更新模型权重,但重新启动引擎以加载新权重通常更简单、更可靠。对于中等大小的任务(数百个 rollout,耗时 10 分钟以上),重启开销(不到 30 秒)通常可以忽略不计。
如果您使用 Agent-lightning 的 VERL 集成,该算法可以自动管理服务器。 VERL 框架智能地分配计算资源,并将 vLLM/SGLang 包装在 AsyncLLMServer 抽象中。您可以直接将其用作代理的 LLM 端点。由于 VERL 可以生成多个 vLLM 副本,因此使用 LLMProxy 来管理它们可以增加额外的安全层。
一个 VERL 如何与 LLM 服务器和代理交互的完整序列图可以在 此处 找到。
LLM 代理¶
LLM 代理是 Agent-lightning 中的一个实用类,基于 LiteLLM,位于运行器和您的后端引擎或服务器之间。在 Agent-lightning 中,它充当注册在存储中的单个 URL 作为 Resource,提供三个主要好处
- 统一的端点和热交换。 您可以在 OpenAI、Anthropic、本地 vLLM/SGLang 或 canary 检查点之间重定向流量,而无需修改代理代码——只需重新指向代理即可。
- 一流的跟踪。 代理为每个调用发出 OpenTelemetry span,并将其发送到
LightningStore。它在请求标头中包含 rollout 和 attempt 标识符,以便正确地将 span 归因。通过存储以单调方式分配序列号来 防止时钟偏差问题并允许可靠地重建执行树。 - Token IDs。 代理可以连同模型输出一起返回 prompt 和 response token IDs。有关详细信息,请参阅 下一节。
从操作角度来看,将代理与算法一起运行效果最佳:算法通过 LLMProxy.update_model_list 注册后端(例如 vLLM URL),通过 LightningStore.add_resources 将代理 URL 作为资源发布,并且运行器在 rollout 期间只需使用该 URL。这模仿了许多生产客户端-服务器设置。
Token IDs 以及它们的重要性¶
本节解释了 Agent-lightning 如何处理和使用 token IDs——一个微妙但重要的细节,用于训练稳定性和准确性。
大多数代理通过 Chat Completion APIs 与 LLM 交互,交换聊天消息。从这些代理收集训练数据有两种主要方法。
注意
这里,Tokenization 指的是将 Chat Messages 转换为 Token IDs 的过程。Detokenization 是将 Token IDs 转换回 Chat Messages 的反向过程。通常,tokenizer 与预训练模型一起发布,包括词汇表、特殊 token 和处理聊天消息的聊天模板。
1. 重新标记聊天消息。 在这种方法中,您将聊天消息存储为文本,并让训练算法稍后重新标记它们,如许多 SFT 工作流程中所做(例如,HuggingFace SFT)。在实践中,我们发现这种方法不稳定且准确性较低。下表比较了训练结果。重新标记方法运行两次。所有设置相同,除了重新标记方法。
这种不稳定性有三个原因。首先,不同框架使用的聊天模板可能略有不同。例如,单个 LLaMA 模型可以使用多个聊天模板(vLLM 中有多个,HuggingFace 中有一个)。 聊天消息在 detokenization 中使用的聊天模板可能与在 tokenization 中使用的模板不同(这实际上是一个实现错误)。
其次,一个单词可能会被生成为两个 token(例如 H + AVING),但后来重新标记为 HAV + ING。文本看起来相同,但 token IDs 与模型最初生成的不同。
第三,生成的工具调用文本,例如 <tool_call>{ "name": ... }</tool_call>,会被工具调用解析器解析为聊天完成 API 所需的对象。稍后,该对象会重新呈现为 <tool_call>{ "name": ... }</tool_call> 并再次重新标记,工具调用解析和重新呈现可能会导致空格和格式的变化。在某些情况下,工具调用解析器甚至可以自动更正 JSON 错误——掩盖模型的真实生成错误并防止它们被训练掉。
2. 直接保存 token IDs。 另一种选择是保存模型生成的 token IDs,如 Tinker 等 RL 设置中所做。这需要一个将 token 作为第一类实体处理的训练管道,这意味着代理必须在 token 级别与推理引擎通信。
但是,大多数代理——尤其是那些使用 LangChain 等框架构建的代理——依赖于 OpenAI 兼容的 API,并且无法自行标记或 detokenize。如 前面 提到的,手动实现此层很复杂且容易出错。一些框架实现了自定义解决方案(例如,VERL Agent Loop,Tinker Renderer),而另一些则将其留给用户(例如,SkyRL Search-R1)。
一个更好的解决方案是使用一个与 OpenAI 兼容的 API,该 API 直接返回 token ID。 这让代理可以继续使用熟悉的 API,同时通过 tracing 捕获 token ID 用于训练。当然,限制在于服务框架必须实际支持此功能。
在 Agent-lightning 首次发布时,我们实现了一个 instrumented vLLM 服务器,通过 monkey-patching vLLM 的 OpenAI 服务器来返回 token ID。此后,Agent-lightning 和 vLLM 团队合作将此功能直接添加到 vLLM 核心。从 vLLM v0.10.2 开始,与 OpenAI 兼容的 API 包含一个 return_token_ids 参数,允许在聊天消息一起请求 token ID。SGLang 跟踪了 类似的功能请求,但其与 OpenAI 兼容的层目前还不支持它们。
简而言之,在使用 vLLM v0.10.2 或更高版本时,LLMProxy 会自动将 return_token_ids 添加到每个请求中,以便引擎在响应中包含 token ID。对于旧版本的 vLLM,您仍然需要 instrumented 版本(通过 agl vllm CLI 命令)。
最后,如果您只在 spans 中保存 token ID,它将会有自身的局限性——如果您使用来自使用不同 tokenizer 的另一个模型的 spans 来训练一个模型,可能会出现不兼容的情况。但在实践中,Agent-lightning 中的 spans 始终存储聊天消息和 token ID(实际上是完整的请求和响应对象),允许您在必要时回退到重新 tokenization。