verl 教程体系与集群启动器实战:从 Agent Loop 工具调用到 Ray/SLURM/SkyPilot 分布式训练
【免费下载链接】verlverl/HybridFlow: A Flexible and Efficient RL Post-Training Framework项目地址: https://gitcode.com/GitHub_Trending/ve/verl
本篇技术指南围绕 verl(HybridFlow:灵活高效的 RL 后训练框架)仓库中的 examples/tutorial/README.md 展开,系统梳理 verl 官方提供的"与具体算法解耦"的学习型教程与启动器资源:Agent Loop API 入门笔记本、Ray API 速成笔记本、Ray-on-SLURM 任务模板,以及基于 SkyPilot 的云上 PPO/GRPO 训练规格。读完本文,你将掌握如何在 verl 中自定义 Tool(工具)、实现带代码沙箱的 ReAct 智能体、用 Ray 资源池组织分布式 Worker,并能在 SLURM 集群与 Kubernetes/云平台上直接拉起 verl 强化学习训练任务。
一、教程与启动器总览
verl 的examples/tutorial/目录专门存放"学习导向"的内容,特点是不绑定具体算法——无论是 PPO、GRPO 还是其他策略优化算法,这些教程都适用。官方将其定位为起点,而可运行的完整训练脚本则位于各 trainer 目录(grpo_trainer/、ppo_trainer/、sft/等)下。
| 子目录 | 内容说明 |
|---|---|
agent_loop_get_started/ | 以 Jupyter Notebook(agent_loop_tutorial.ipynb)逐步演示 Agent Loop API 的使用,核心是训练一个带代码沙箱的 ReAct 智能体解决数学问题 |
ray/ | Ray API 速成笔记本(tutorial.ipynb),从 Ray 基础讲到 RayWorkerGroup 与 Megatron 并行组 |
slurm/ | Ray-on-SLURM 任务模板(ray_on_slurm.slurm),用于在 SLURM 集群上启动 Ray 集群并运行 verl 训练 |
skypilot/ | SkyPilot PPO 与 GRPO 任务规格(verl-ppo.yaml、verl-grpo.yaml),面向 Kubernetes 集群或云平台 GPU 节点 |
下文按"单机上手 → 分布式抽象 → 集群/云端部署"的进阶路径逐一展开。
二、Agent Loop 入门:训练一个会调用代码沙箱的 ReAct 智能体
examples/tutorial/agent_loop_get_started/agent_loop_tutorial.ipynb是本仓库中最完整的端到端 Agent 教程。它演示了如何训练一个 ReAct(Reasoning + Acting)智能体解决数学问题,工作流程如下:
- 给定一道数学题,智能体先让 LLM 生成回复与工具调用(工具调用即要在沙箱中执行的 Python 代码);
- 若有工具调用,智能体在代码沙箱中执行该 Python 代码;
- 执行完毕后,智能体将沙箱返回的结果追加到对话历史;
- 智能体再次询问 LLM,直到不再产生工具调用或达到最大上下文长度。
2.1 前置准备
首先安装 verl 包:
git clone https://github.com/verl-project/verl cd verl pip install -e .随后在 Notebook 中初始化 Ray 并准备演示资源:
import ray import verl ray.init() verl_config_dir = os.path.join(os.path.dirname(verl.__file__), "trainer/config")教程使用Qwen/Qwen3-1.7B作为 LLM,并下载verl-team/lighteval-MATH-preprocessed数学评测数据集(含 AMC 10/12、AIME 等竞赛题),同时从测试集中截取前 100 条作为快速验证集。rollout 服务器支持 vllm 与 sglang 两种高性能推理后端,教程在两者上都验证通过,用户按需选择rollout_name = "vllm"或"sglang"。
2.2 基础工具调用:自定义 WeatherTool
verl 中要使用工具,需要定义一个继承BaseTool的类,并实现两个方法:
get_openai_tool_schema:以OpenAIFunctionToolSchema格式返回工具的 OpenAI 函数 Schema;execute:用给定参数执行工具,并以ToolResponse格式返回结果。
BaseTool定义于 verl/tools/base_tool.py,其完整接口还包括create(为一条轨迹创建工具实例)、calc_reward(依据工具状态计算奖励)、release(释放工具实例),execute返回三元组(tool_response, tool_reward_score, tool_metrics),其中tool_reward_score可作为工具调用的步骤级奖励。从源码结构看,这是 verl 将"工具调用"与"奖励计算"打通的关键抽象,为 Agentic RL 提供了统一的工具协议。
教程中的天气工具实现如下(借助 transformers 的get_json_schema从函数签名自动生成 Schema):
from transformers.utils import get_json_schema from verl.tools.base_tool import BaseTool, OpenAIFunctionToolSchema, ToolResponse class WeatherTool(BaseTool): def get_current_temperature(self, location: str, unit: str = "celsius"): """Get current temperature at a location. Args: location: The location to get the temperature for, in the format "City, State, Country". unit: The unit to return the temperature in. Defaults to "celsius". (choices: ["celsius", "fahrenheit"]) Returns: the temperature, the location, and the unit in a dict """ return {"temperature": 26.1, "location": location, "unit": unit} def get_openai_tool_schema(self) -> OpenAIFunctionToolSchema: schema = get_json_schema(self.get_current_temperature) return OpenAIFunctionToolSchema(**schema) async def execute(self, instance_id: str, parameters: dict, **kwargs) -> tuple[ToolResponse, float, dict]: try: result = self.get_current_temperature(**parameters) return ToolResponse(text=json.dumps(result)), 0, {} except Exception as e: return ToolResponse(text=str(e)), 0, {} weather_tool = WeatherTool(config={}, tool_schema=None)生成的 Schema 会自动包含函数名、描述、参数类型(string)、枚举约束(celsius/fahrenheit)与必填项(location)。
2.3 启动独立 Rollout 服务器
为快速验证工具调用,教程绕过较重的混合引擎,用 Hydra 组合ppo_trainer配置并启动一个独立(standalone)rollout 服务器。这里体现了多个关键配置项:
from hydra import compose, initialize_config_dir from verl.workers.rollout.replica import get_rollout_replica_class with initialize_config_dir(config_dir=verl_config_dir): config = compose( config_name="ppo_trainer", overrides=[ "actor_rollout_ref.rollout.name=" + rollout_name, # vllm 或 sglang "actor_rollout_ref.rollout.mode=async", "actor_rollout_ref.rollout.tensor_model_parallel_size=1", "actor_rollout_ref.model.path=" + model_path, "actor_rollout_ref.rollout.response_length=4096", "actor_rollout_ref.rollout.skip_tokenizer_init=False", # 独立服务器没有 trainer 同步权重,因此必须用 auto 加载格式 "actor_rollout_ref.rollout.load_format=auto", "+actor_rollout_ref.rollout.engine_kwargs.vllm.enable_auto_tool_choice=True", "+actor_rollout_ref.rollout.engine_kwargs.vllm.tool_call_parser=hermes", "+actor_rollout_ref.rollout.engine_kwargs.sglang.tool_call_parser=qwen25", ], ) rollout_server_class = get_rollout_replica_class(config.actor_rollout_ref.rollout.name) rollout_server = rollout_server_class( replica_rank=0, config=config.actor_rollout_ref.rollout, model_config=config.actor_rollout_ref.model, ) await rollout_server.init_standalone()随后用 OpenAI 兼容客户端向其发起聊天请求,注意需把工具 Schema 传给服务器以引导 LLM 生成工具调用:
from openai import AsyncOpenAI client = AsyncOpenAI(api_key="dummy", base_url=f"http://{rollout_server._server_address}/v1") messages = [{"role": "user", "content": "Hey, what's the temperature in Paris right now?"}] completion = await client.chat.completions.create( model=config.actor_rollout_ref.model.path, messages=messages, tools=[weather_tool.tool_schema.model_dump(exclude_unset=True, exclude_none=True)], extra_body={"chat_template_kwargs": {"enable_thinking": False}}, ) message = completion.choices[0].message.model_dump(exclude_unset=True, exclude_none=True) messages.append(message)模型会正确生成get_current_temperature的调用(参数为{"location": "Paris, France"})。将工具响应以role: "tool"追加回对话历史后再次查询,LLM 即可给出最终答案。这一"生成工具调用 → 执行 → 回填 → 再生成"的循环正是 ReAct 的核心。
2.4 高级工具调用:实现一个朴素代码沙箱
真实场景中更常见的是执行 LLM 生成的 Python 代码。教程实现了一个演示用沙箱:一个基于 FastAPI 的 HTTP 服务器,提供/run_code端点,收到请求后把代码写入临时文件、用子进程执行并返回 stdout/stderr。它被包装为 Ray Actor:
@ray.remote(num_cpus=1) class Sandbox: def __init__(self): self.address = ray._private.services.get_node_ip_address() self.port = self._get_free_port() asyncio.create_task(self._start_fastapi_server()) async def code_execution(self, request: Request): request_json = await request.json() code = request_json["code"] _, temp_file = tempfile.mkstemp(suffix=".py", prefix="temp_code", dir=None, text=True) with open(temp_file, "w") as f: f.write(code) try: process = await asyncio.create_subprocess_exec( sys.executable, temp_file, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() response = { "status": "Success" if process.returncode == 0 else "Failed", "run_result": {"status": "Finished", "stdout": stdout.decode(), "stderr": stderr.decode(), "return_code": process.returncode}, } return JSONResponse(content=response) finally: try: os.unlink(temp_file) except Exception: pass注意:该朴素沙箱仅用于演示,严禁用于生产环境。生产部署应使用 docker/kata 容器以获得更强的隔离与安全限制。
对应的SandboxTool通过aiohttp把代码片段 POST 到沙箱地址。仓库中同目录下的 examples/tutorial/agent_loop_get_started/sandbox.py 提供了同样思路的独立实现。其execute中还有两个实用细节:用正则```py(.*?)```抽取代码块,以及在脚本末尾自动补print(...)语句(因为部分模型生成的脚本不会显式打印结果;更优的做法是在 SFT 阶段让模型默认打印,教程中省略了该阶段)。
import re import aiohttp class SandboxTool(BaseTool): def __init__(self, config: dict, tool_schema: OpenAIFunctionToolSchema): super().__init__(config, tool_schema) self.code_pattern = re.compile(r"```py(.*?)```", re.DOTALL) async def code_interpreter(self, code: str) -> str: async with aiohttp.ClientSession() as session: async with session.post(self.config.get("sandbox_fusion_url"), json={"code": code}) as resp: resp.raise_for_status() result = await resp.json() stdout, stderr = result["run_result"]["stdout"], result["run_result"]["stderr"] return stdout + stderr def get_openai_tool_schema(self) -> OpenAIFunctionToolSchema: schema = get_json_schema(self.code_interpreter) return OpenAIFunctionToolSchema(**schema) async def execute(self, instance_id: str, parameters: dict, **kwargs) -> tuple[str, float, dict]: code = parameters["code"] matches = self.code_pattern.findall(code) if matches: code = matches[0].strip() lines = code.split("\n") for i, line in reversed(list(enumerate(lines))): if line == "": continue if not lines[i].startswith("print"): lines[i] = f"print({line})" break code = "\n".join(lines) result = await self.code_interpreter(code) return ToolResponse(text=result), 0.0, {}对有效代码(如sympy.sqrt(3))与无效代码(如未导入sympy就调用)分别测试,可看到沙箱正确返回 stdout 与完整 Traceback——错误信息对 LLM 非常关键,它能让模型在下一轮生成中修复代码。
2.5 手写 ReAct 循环验证
在进入 RL 训练前,教程先用一个简单的 while 循环模拟 ReAct 智能体,在真实数学题上验证"生成 → 执行 → 回填"闭环:每次聊天若finish_reason == "tool_calls"就解析参数并调用sandbox_tool.execute,把结果以role: "tool"追加回messages,否则结束。实际运行中,模型会为y = 2/(x²+x-6)的垂直渐近线问题写出求解x²+x-6=0的 sympy 代码,沙箱返回[-3, 2],模型据此给出\boxed{2}的最终答案。
2.6 端到端 RL 训练:接入内置 ToolAgentLoop
工具验证通过后即可进行端到端强化学习训练。为简化 Agentic RL,verl 提供 Agent Loop 抽象(详见 docs/advance/agent_loop.rst),允许用户自定义搜索智能体、数学智能体、SWE 智能体、GUI 智能体等;并内置两个开箱即用的循环:
SingleTurnAgentLoop:单轮对话、不调用工具;ToolAgentLoop:多轮对话、支持工具调用与交互。
使用ToolAgentLoop时,只需在 JSON/YAML 配置文件中为每个工具声明两个字段:
class_name:工具的完整限定类名,用于动态加载自定义工具类;config:初始化工具实例的关键字参数。
把沙箱工具配置导出为tool_config.json:
{ "tools": [ { "class_name": "sandbox.SandboxTool", "config": { "type": "native", "sandbox_fusion_url": "http://<sandbox_address>/run_code" } } ] }随后通过 Hydra 覆盖ppo_trainer配置,接入 GRPO 算法与多轮工具调用训练。以下为教程中的完整配置覆盖项:
config = compose( config_name="ppo_trainer", overrides=[ "algorithm.adv_estimator=grpo", "data.train_files=" + train_file, "data.val_files=" + test_file, "data.return_raw_chat=True", "data.train_batch_size=32", "data.max_prompt_length=1024", "data.max_response_length=1024", "+data.apply_chat_template_kwargs.enable_thinking=False", # actor 相关 "actor_rollout_ref.model.path=" + model_path, "actor_rollout_ref.actor.ppo_mini_batch_size=8", "actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=8", "actor_rollout_ref.actor.fsdp_config.param_offload=True", "actor_rollout_ref.actor.fsdp_config.optimizer_offload=True", # rollout 相关 "actor_rollout_ref.rollout.name=" + rollout_name, "actor_rollout_ref.rollout.mode=async", "actor_rollout_ref.rollout.tensor_model_parallel_size=1", "actor_rollout_ref.rollout.n=8", "actor_rollout_ref.rollout.multi_turn.tool_config_path=" + tool_config_path, "actor_rollout_ref.rollout.agent.default_agent_loop=tool_agent", "actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8", # trainer 相关 "trainer.val_before_train=True", "trainer.log_val_generations=10", "trainer.n_gpus_per_node=8", "trainer.test_freq=-1", "trainer.total_training_steps=5", "trainer.logger=['console','tensorboard', 'wandb']", "trainer.project_name=verl", "trainer.experiment_name=" + os.path.basename(model_path), ], ) from verl.trainer.main_ppo import main main(config)其中两个关键开关:actor_rollout_ref.rollout.multi_turn.tool_config_path指向工具配置 JSON,actor_rollout_ref.rollout.agent.default_agent_loop=tool_agent启用多轮工具智能体循环。演示仅训练 5 步,可通过 wandb 指标验证训练过程:num_turns(每步对话轮数的 min/max/mean)与critic rewards(每步奖励的 min/max/mean)。更贴近论文复现的 Agentic RL 配方(如 ReTool、CollabLLM、DeepEyes 等)存放在仓库之外维护的 verl-recipe 项目中,本文不再展开。
三、Ray API 速成:从单 Actor 到 RayWorkerGroup
examples/tutorial/ray/tutorial.ipynb是 verl 底层分布式抽象(SingleController,见 verl/single_controller)的速成教材,共 4 章。
3.1 Ray 基础:Accumulator
ray.init()在本机建立 head/worker 一体的本地 Ray 集群。把普通类标注@ray.remote后,其实例化即产生一个独立进程(可视为 RPC 服务),accumulator.add.remote(10)立即返回ObjectRef,需用ray.get()获取实际结果:
@ray.remote class Accumulator: def __init__(self): self.value = 0 def add(self, x): self.value += x def get_value(self): return self.value accumulator = Accumulator.remote() value = ray.get(accumulator.get_value.remote()) # 0 accumulator.add.remote(10) new_value = ray.get(accumulator.get_value.remote()) # 103.2 资源池与 RayWorkerGroup
verl 在 Ray 之上抽象出ResourcePool(资源池)与WorkerGroup(Worker 组)。RayResourcePool([4], use_gpu=True)声明 4 个 GPU 资源;RayClassWithInitArgs封装 Worker 类的构造参数;RayWorkerGroup则在资源池上实例化一组 Worker。参数传递原则:execute_all_sync的入参是长度为world_size的列表,各元素按序分发给每个 Worker,返回值同样是每个 Worker 返回值的列表:
from verl.single_controller.base import Worker from verl.single_controller.ray.base import RayClassWithInitArgs, RayResourcePool, RayWorkerGroup, merge_resource_pool resource_pool = RayResourcePool([4], use_gpu=True) @ray.remote class GPUAccumulator(Worker): def __init__(self) -> None: super().__init__() self.value = torch.zeros(size=(1,), device="cuda") + self.rank # 初始值为 rank def add(self, x): self.value += x return self.value.cpu() class_with_args = RayClassWithInitArgs(cls=GPUAccumulator) worker_group = RayWorkerGroup(resource_pool, class_with_args) print(worker_group.execute_all_sync("add", x=[1, 1, 1, 1])) # [tensor([1.]), 2, 3, 4]GPU 资源共享:映射到同一资源池的多个 RayWorkerGroup 共享 GPU。merge_resource_pool(resource_pool, resource_pool_1)可合并两个资源池得到更大的组;上面的 4 GPU 组再加 1(新 4 GPU 组)后合并为 8 GPU 组,各组world_size分别为 4、4、8,且运行在独立 GPU 上互不干扰。
3.3 装饰器驱动的 Dispatch / Execute / Collect
直接手写execute_all_sync不便编码,verl 提供@register(Dispatch.X)装饰器让 Worker 内的方法被 WorkerGroup 直接调用,并自动完成参数分发与结果收集:
from verl.single_controller.base.decorator import Dispatch, Execute, register @ray.remote class GPUAccumulatorDecorator(Worker): def __init__(self) -> None: super().__init__() self.value = torch.zeros(size=(1,), device="cuda") + self.rank @register(Dispatch.ONE_TO_ALL) # 单个输入自动分发给所有 Worker def add(self, x): self.value = self.value + x return self.value.cpu() gpu_accumulator_decorator = RayWorkerGroup(resource_pool_merge, class_with_args) print(gpu_accumulator_decorator.add(x=10)) # 10 被自动广播到每个 Worker用户还可自定义 dispatch/collect 函数(如把长度为 2 的输入按i % 2交错扩充到全组),并支持仅在 rank_zero 上执行 RPC:
from verl.single_controller.base.decorator import Dispatch, collect_all_to_all, register def two_to_all_dispatch_fn(worker_group, *args, **kwargs): for arg in args: assert len(arg) == 2 for i in range(worker_group.world_size - 2): arg.append(arg[i % 2]) ... return args, kwargs @ray.remote class TestActor(Worker): def __init__(self, x) -> None: super().__init__() self._x = x def foo(self, y): return self._x + y @register(dispatch_mode=Dispatch.ALL_TO_ALL, execute_mode=Execute.RANK_ZERO) def foo_rank_zero(self, x, y): return self._x + y + x @register(dispatch_mode={"dispatch_fn": two_to_all_dispatch_fn, "collect_fn": collect_all_to_all}) def foo_custom(self, x, y): return self._x + y + x worker_group = RayWorkerGroup(resource_pool, RayClassWithInitArgs(cls=TestActor, x=2)) assert worker_group.foo_custom(x=[1, 2], y=[5, 6]) == [8, 10, 8, 10] assert worker_group.foo_rank_zero(x=1, y=2) == 53.4 NVMegatronRayWorkerGroup:Megatron 透明化
第四章演示NVMegatronRayWorkerGroup(位于 verl/single_controller/ray/megatron)。受 Ray 限制,目前RayResourcePool的max_colocate_count只能为 1(即每块 GPU 一个进程)。该组在内部创建 Megatron 并行状态并运行一个 TP=4 切分的 Llama MLP 层,使用复杂的Dispatch.MEGATRON_COMPUTE分发模式:假设用户按 DP 维传入已切分的数据,数据被分发给同一 DP 组内的所有 tp/pp rank,最终只从 tp=0、最后一个 pp 收集输出。这样对于只在 driver 上写代码的用户,RPC 背后的 Megatron 是完全透明的。
@ray.remote class MLPLayerWorker(MegatronWorker): def __init__(self): super().__init__() rank = int(os.environ["LOCAL_RANK"]) torch.distributed.init_process_group(backend="nccl") torch.cuda.set_device(rank) mpu.initialize_model_parallel(tensor_model_parallel_size=4, pipeline_model_parallel_size=1, ...) @register(Dispatch.ONE_TO_ALL) def init_model(self, config): ... self.parallel_layer = ParallelLlamaMLP(config=config, megatron_config=megatron_config) @register(Dispatch.MEGATRON_COMPUTE) def run_layer(self, x): x = x.to("cuda") return self.parallel_layer(x) layer_worker_group = NVMegatronRayWorkerGroup(resource_pool=resource_pool, ray_cls_with_init=layer_cls) print(layer_worker_group.world_size, layer_worker_group.tp_size, layer_worker_group.pp_size, layer_worker_group.dp_size) # 4 4 1 1 x = torch.rand(size=(seq_len, batch_size, hidden_size), dtype=torch.float32) output = layer_worker_group.run_layer([x]) # 入参必须是 size=1 的列表,保证输入等于 DP 维 print(output[0].shape) # torch.Size([2048, 16, 4096])四、Ray-on-SLURM:在 SLURM 集群上拉起 verl 训练
examples/tutorial/slurm/ray_on_slurm.slurm 是一个可直接sbatch提交的模板,负责在 SLURM 分配的计算节点上先启动 Ray 集群,再运行 verl 的 PPO 训练。
4.1 头部参数与替换项
#!/bin/bash #SBATCH --job-name=verl-ray-on-slurm #SBATCH --nodes=2 #SBATCH --ntasks-per-node=1 #SBATCH --mem=200G #SBATCH --partition=your-partition #SBATCH --time=01:00:00 #SBATCH --account=your-account #SBATCH --gpus-per-node=4 #SBATCH --cpus-per-task=64 #SBATCH --output=slurm-%j.out #SBATCH --error=slurm-%j.err # 替换为你的实际路径 verl_workdir=/path/to/verl train_files=/path/to/gsm8k/train.parquet val_files=/path/to/gsm8k/test.parquet apptainer_image_path=/path/to/verl-ngc.sif # 多网卡集群可选:将 Ray 绑定到特定网卡,例如 # RAY_NETWORK_INTERFACE=ib0 sbatch ray_on_slurm.slurm RAY_NETWORK_INTERFACE=${RAY_NETWORK_INTERFACE:-}其中apptainer_image_path对应 verl 官方 NGC 容器镜像,运行时通过 Apptainer 的--nv --bind挂载 GPU 与工作目录。
4.2 关键机制
- 获取节点 IP:
get_node_ipv4()用srun --overlap -w <node>在指定节点上执行ip -4 -o addr show dev <iface>提取 IPv4 地址;未指定网卡时退化为hostname --ip-address,并处理 IPv6 多地址回退。 - 启动 Ray head:在
nodes_array[0]上ray start --head --node-ip-address=$head_node_ip --port=6379 --num-cpus --num-gpus --block。 - 启动 Ray worker:对剩余节点逐一
ray start --address $ip_head ...,间隔sleep 5避免并发冲突。 - 提交训练:最后在 head 节点上以
PYTHONUNBUFFERED=1 srun --overlap运行python3 -m verl.trainer.main_ppo,通过tee verl_demo_slurm.log留存日志。
4.3 训练命令行解析
模板中的 PPO 训练(GSM8K + Qwen2.5-0.5B-Instruct,2 节点 × 4 GPU)展示了 verl 配置的组织结构,可归纳为五大块:
- data:
train_files/val_files、train_batch_size=256、max_prompt_length=512、max_response_length=256; - actor_rollout_ref:模型路径、
optim.lr=1e-6、ppo_mini_batch_size=64、ppo_micro_batch_size_per_gpu=4、log_prob_micro_batch_size_per_gpu=8、tensor_model_parallel_size=1、rollout 后端name=vllm、gpu_memory_utilization=0.4; - critic:
optim.lr=1e-5、model.path与 actor 相同、ppo_micro_batch_size_per_gpu=4; - algorithm:
adv_estimator=gae、kl_ctrl.kl_coef=0.001、use_kl_in_reward=False; - trainer:
n_gpus_per_node=${SLURM_GPUS_PER_NODE}、nnodes=${SLURM_NNODES}、save_freq=10、test_freq=10、total_epochs=15、logger=console、val_before_train=False。
训练脚本会随 SLURM 变量自动适配节点规模(trainer.n_gpus_per_node与trainer.nnodes取自SLURM_GPUS_PER_NODE/SLURM_NNODES),因此该模板可复用于不同规模的集群分配。
五、SkyPilot:在 Kubernetes 与云平台上跑 PPO/GRPO
examples/tutorial/skypilot/README.md 讲解如何使用 SkyPilot 在 Kubernetes 集群或 AWS/GCP/Azure 云平台的 GPU 节点上运行 verl 强化学习训练。
5.1 安装与配置
按目标平台选择安装方式:
# 仅 Kubernetes pip install "skypilot[kubernetes]" # AWS pip install "skypilot[aws]" # Google Cloud Platform pip install "skypilot[gcp]" # Azure pip install "skypilot[azure]" # 多平台 pip install "skypilot[kubernetes,aws,gcp,azure]"平台凭据配置完成后,导出实验追踪所需的环境变量:
export WANDB_API_KEY="your-wandb-api-key" # Weights & Biases 追踪 export HF_TOKEN="your-huggingface-token" # 如需访问 gated Hugging Face 模型5.2 PPO 与 GRPO 任务
在仓库根目录执行:
# PPO:GSM8K 数据集 + Qwen2.5-0.5B-Instruct,2 节点 H100,参考 examples/ppo_trainer/ sky launch -c verl-ppo examples/tutorial/skypilot/verl-ppo.yaml --secret WANDB_API_KEY -y # GRPO:MATH 数据集 + Qwen2.5-7B-Instruct,2 节点内存优化配置,参考 examples/grpo_trainer/ sky launch -c verl-grpo examples/tutorial/skypilot/verl-grpo.yaml --secret WANDB_API_KEY -yAgent Loop 与工具使用训练目前没有预置的 SkyPilot 任务;官方建议先用 Agent Loop 教程 验证模型、工具与沙箱配置,再适配为集群任务。
5.3 任务规格解析
两个 YAML 的资源配置一致:infra: k8s(可改为infra: aws/infra: gcp等)、accelerators: H100:1、memory: 128+、官方 CUDA 12.6 Docker 镜像(verlai/verl:base-verl0.5-cu126-cudnn9.8-torch2.7.0-fa2.7.4)、暴露 8265 端口(Ray Dashboard)、2 节点,并通过secrets: WANDB_API_KEY声明密钥。
setup阶段自动执行:克隆并pip3 install -v -e .[vllm]安装 verl、安装flashinfer-python、运行examples/data_preprocess/下的数据预处理脚本下载数据集(PPO 用 gsm8k.py,GRPO 用 math_dataset.py)。
run阶段通过 SkyPilot 注入的环境变量协调多节点:SKYPILOT_NODE_IPS(节点 IP 列表)、SKYPILOT_NUM_NODES、SKYPILOT_NODE_RANK、SKYPILOT_NUM_GPUS_PER_NODE。head 节点(rank 0)启动ray start --head --port=6379 --dashboard-host=0.0.0.0 --dashboard-port=8265,并以最多 30 次、每次 10 秒的轮询等待所有节点通过ray status加入集群;worker 节点则ray start --address $HEAD_IP:6379。集群就绪后在 head 节点运行python3 -m verl.trainer.main_ppo。
两个任务的训练配置差异体现了 PPO 与 GRPO 的典型设置:
| 维度 | verl-ppo.yaml(GSM8K + 0.5B) | verl-grpo.yaml(MATH + 7B) |
|---|---|---|
| 算法 | 默认 PPO(algorithm.adv_estimator未指定) | algorithm.adv_estimator=grpo |
| 批大小 | train_batch_size=256、ppo_mini_batch_size=64 | train_batch_size=32、ppo_mini_batch_size=16 |
| 长度限制 | max_prompt_length=512、max_response_length=256 | max_prompt_length=256、max_response_length=256 |
| 内存优化 | — | use_remove_padding=True、enable_gradient_checkpointing=True、FSDP 参数/优化器卸载、gpu_memory_utilization=0.4、enable_chunked_prefill=True、max_num_batched_tokens=2048 |
| 损失细节 | — | use_kl_loss=False、entropy_coeff=0、use_kl_in_reward=False、critic_warmup=0 |
| 保存/测试 | save_freq=20、test_freq=20、total_epochs=2 | save_freq=-1、test_freq=-1、total_epochs=1 |
GRPO 的 7B 模型通过"移除 padding + 梯度检查点 + FSDP 参数/优化器双卸载 + vLLM 低显存利用率 + chunked prefill"组合,将内存开销压到 2 节点可承载的水平,可作为中等规模模型的参考配方。
5.4 启动选项与作业监控
sky launch常用选项:-c <name>指定集群名以便管理作业;--secret KEY传递 API 密钥(可重复使用);-y跳过确认提示。使用 gated Hugging Face 模型时,需在任务的secrets段增加HF_TOKEN并用--secret HF_TOKEN传入。
作业监控命令:
sky status # 查看集群状态 sky logs verl-ppo # 查看 PPO 作业日志 ssh verl-ppo # SSH 进入 head 节点 sky status --endpoint 8265 verl-ppo # 获取 Ray Dashboard URL sky down verl-ppo # 停止集群六、如何继续深入
本文覆盖的 4 个教程资源对应 verl 使用路径上的不同阶段:先用 Agent Loop 笔记本打通"模型 + 工具 + 沙箱"的单机验证(配合 verl/tools/base_tool.py 与 docs/advance/agent_loop.rst 理解工具协议与循环抽象),再通过 Ray 教程理解分布式 Worker 组织(源码见 verl/single_controller),最后借助 SLURM/SkyPilot 启动器将训练规模化。需要投入真实训练时,以各 trainer 目录下的可运行脚本为准——例如 examples/grpo_trainer、examples/ppo_trainer 与 examples/sft 提供了覆盖 FSDP、Megatron、veomni 等多种后端与不同规模模型的启动脚本;更复杂的多轮工具数据预处理可参考 examples/data_preprocess 下的gsm8k_multiturn_w_tool.py、dapo_multiturn_w_tool.py等脚本。把教程中的配置项与真实训练脚本两相对照,即可快速完成从"跑通 demo"到"复现论文级实验"的过渡。
【免费下载链接】verlverl/HybridFlow: A Flexible and Efficient RL Post-Training Framework项目地址: https://gitcode.com/GitHub_Trending/ve/verl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考