快速上手:单机 GRPO 例子
3.1 选哪个 example 跑
examples/grpo_trainer/ 下大约 20+ 个启动脚本,命名规则统一:
run_<模型>_<后端>.sh
| 脚本 | 规模 | 训练后端 | 采样后端 |
|---|---|---|---|
run_qwen3_4b_fsdp.sh | 4B | FSDP | vLLM |
run_qwen2_5_32b_fsdp.sh | 32B | FSDP | vLLM |
run_qwen3_30b_a3b_megatron.sh | 30B MoE | Megatron | vLLM |
run_qwen2_5_vl_7b_fsdp.sh | VL 7B | FSDP | vLLM |
run_deepseek_v3_671b_megatron.sh | 671B | Megatron | vLLM |
新手首选 run_qwen3_4b_fsdp.sh:4B 模型在 8 张 A100/H100 上能跑、依赖最少、报错最容易看懂。
3.2 准备数据:GSM8K
verl 内置了 GSM8K(小学数学题)的预处理脚本,是 GRPO 训练的"hello world":
# 在 verl 目录里
cd /Users/zjw/Documents/LLM/train/repos/verl
# 用 verl 自带预处理脚本把 HF datasets 转 parquet(带 prompt 模板)
python examples/data_preprocess/gsm8k.py --local_dir ~/data/gsm8k
# 看产出
ls ~/data/gsm8k/
# train.parquet test.parquet
每条样本包含:
prompt—— 拼好对话模板的输入;data_source—— 标识 reward 应该用哪种打分器(这里是"openai/gsm8k",verl 自带规则 reward);reward_model—— ground truth 答案,用于自动验证。
3.3 启动训练
cd /Users/zjw/Documents/LLM/train/repos/verl
# 单机 8 卡 H100/A100;脚本会自动 ray start --head
bash examples/grpo_trainer/run_qwen3_4b_fsdp.sh
第一次启动会做:
ray start --head起 Ray head;- 从 HuggingFace 下载
Qwen/Qwen3-4B(约 8 GB); - 在每张 GPU 上 init FSDP;
- 起 vLLM rollout worker(
ROLLOUT_TP=2时占 2 卡); - 跑数据预处理、tokenize;
- 正式进入训练循环。
10 分钟左右你会看到 step 0 的输出:
step:1 - reward: 0.12, kl: 0.00, response_length: 256, ...
step:2 - reward: 0.14, ...
3.4 启动脚本是怎么组织的
打开 examples/grpo_trainer/run_qwen3_4b_fsdp.sh,会看到它本质上是一长串 Hydra override。脚本分这几段:
| 段 | 变量 / Hydra key | 含义 |
|---|---|---|
| 用户可调 | MODEL_PATH / TRAIN_FILE / NNODES / ... | 暴露给用户的最常用旋钮 |
| DATA | algorithm.adv_estimator=grpodata.train_files=... | 算法与数据 |
| MODEL | actor_rollout_ref.model.path=... | 基座模型与开关 |
| ACTOR | actor_rollout_ref.actor.optim.lr=...+ batch / grad accum | 训练 actor 的超参 |
| ROLLOUT | actor_rollout_ref.rollout.tensor_model_parallel_size=... | vLLM 采样配置 |
| REF / CRITIC | ... | 引用模型 / 价值模型 |
| REWARD | reward_model.style=rule | 用规则 reward(GSM8K 自动验证) |
| TRAINER | trainer.project_name=...trainer.total_epochs=... | 全局训练超参 |
最后一行非常长,类似:
python3 -m verl.trainer.main_ppo \
"${DATA[@]}" \
"${MODEL[@]}" \
"${ACTOR[@]}" \
"${ROLLOUT[@]}" \
...
这就是 verl 的入口范式:python -m verl.trainer.main_ppo + 一堆 key=value。
3.5 训练时的进程拓扑
单机 8 卡 + ROLLOUT_TP=2 的运行时长这样:
(main_ppo.py 跑算法控制流)"] C -->|Ray RPC| A0["Actor Worker 0
GPU 0,1"] C -->|Ray RPC| A1["Actor Worker 1
GPU 2,3"] C -->|Ray RPC| R0["Rollout Worker 0
vLLM, GPU 4,5"] C -->|Ray RPC| R1["Rollout Worker 1
vLLM, GPU 6,7"] C -->|Ray RPC| Ref["Reference forward
colocated 在 actor 上"] C -->|Ray RPC| Rwd["Reward (rule-based)
CPU 上跑"]
这种"controller + 多 worker group"的样子,就是 HybridFlow 架构,第 4 章会展开。
3.6 训练过程中关注什么
启动后,verl 会输出标准日志 + 写到 WandB / TensorBoard(看 trainer.logger 配置)。重点关注:
| 指标 | 含义 | 预期 |
|---|---|---|
reward/mean | 当前 batch 平均奖励 | 逐步上升,GSM8K 从 ~0.1 升到 ~0.6 |
kl/mean | policy ↔ reference 的 KL | 稳定在小值(< 1) |
response_length/mean | 生成长度 | 稳定或略升(思考链变长) |
entropy | 策略熵 | 不应坍塌到 0 |
actor/grad_norm | 梯度范数 | 不应剧烈跳变;爆涨 → 学习率太高 |
throughput | 每秒 token 数 | 视硬件而定 |
3.7 看完这章你应该能做什么
- 跑通一个单机 GRPO 训练,loss 曲线可见;
- 能看懂
run_*.sh的 Hydra override 风格; - 知道训练时的进程拓扑(controller + 多 worker group);
- 知道关注哪几个指标判断训练是否正常;
- 还不需要懂 worker group 的实现细节——第 4-5 章会讲。
3.8 跑完 GSM8K 之后
4B + GSM8K + 15 epoch 大约 1-3 小时(视硬件)。跑完后可以:
- 用 verl 自带的
main_eval.py跑测试集打分; - 用
verl/model_merger/把 ZeRO / FSDP shard 合成 HF 格式; - 挂到 vLLM 起 OpenAI 兼容服务;
- 换其他数据集(MATH / DAPO-Math-17K / 自家业务)继续玩。
这套流程在第 12 章"案例:长思考链 GRPO"会完整演示一次。
3.9 入口背后:main_ppo.py 调用栈
把 run_qwen3_4b_fsdp.sh 最后那行 python -m verl.trainer.main_ppo ... 拆开看,从命令行到训练循环之间发生了什么:
| 步 | 位置 | 做什么 |
|---|---|---|
| 1 | main_ppo.py:39 | Hydra @hydra.main(config_path="config", config_name="ppo_trainer") 拦截入口,解析所有 key=value override 后调 main(config) |
| 2 | main_ppo.py:46 | auto_set_device(config) 检测当前是 CUDA / Ascend NPU / ROCm,设 device |
| 3 | main_ppo.py:47 | migrate_legacy_reward_impl(config) 兼容旧版 reward 字段命名 |
| 4 | main_ppo.py:48 → 52 | run_ppo(config) 真正起 Ray 集群 |
| 5 | main_ppo.py:67 | 读 trainer/runtime_env.yaml 拼出 Ray runtime env |
| 6 | main_ppo.py:71 | 检测到 algorithm.transfer_queue 段就设 TRANSFER_QUEUE_ENABLE=1 |
| 7 | main_ppo.py:80 | ray.init(**ray_init_kwargs),merge 用户的 ray_init 覆盖项 |
| 8 | main_ppo.py:83 | 把 TaskRunner 包成 ray.remote(num_cpus=1) —— 这是为了把它赶下 head 节点,避免和 controller 抢资源 |
| 9 | main_ppo.py:99 | 如果 profile.tool=nsight,再包一层 runtime_env={"nsight": ...} |
| 10 | main_ppo.py:102 | ray.get(runner.run.remote(config)) 阻塞等训练完成 |
3.10 TaskRunner 角色:driver-side 装配工
TaskRunner(main_ppo.py:111 起的 Ray Actor)做的事是把 controller 需要的所有 WorkerGroup 都注册好,然后调 fit() 开始训练。关键方法:
| 方法 | 位置 | 做什么 |
|---|---|---|
add_actor_rollout_worker(config) | line 126 | 注册 ActorRolloutRefWorker(hybrid engine)或拆成单独 Actor / Rollout |
add_critic_worker(config) | line 148 | 有 critic(PPO)时注册 |
init_resource_pool_mgr(config) | line 158 | 建 ResourcePoolManager,里面默认有 global_pool,按需加 reward_pool、teacher_pool |
add_reward_model_resource_pool(config) | line 193 | 神经网络型 reward model |
add_teacher_model_resource_pool(config) | line 205 | 在线蒸馏的 teacher |
run(config) | line 223 | 构造 RayPPOTrainer 并调 fit() |
这套设计意味着:controller 不在 head 节点的 Python 进程里,而是在 TaskRunner 这个 Ray Actor 里运行。Ray dashboard 上能看到一个长寿命的 actor 名字带 "TaskRunner",它就是 controller。
3.11 同步 vs 异步:main_ppo_sync.py
verl/trainer/main_ppo_sync.py 是一个并行存在的"同步 PPO"入口(line 14–22 注释里写得很清楚)。它和 main_ppo.py 的差异:
| 维度 | main_ppo.py | main_ppo_sync.py |
|---|---|---|
| 数据传输 | 同进程 ray put/get | TransferQueue 零拷贝零 padding |
| Replay buffer | 无 | 从 TransferQueue 采 |
| 每 prompt 出几条 trajectory | 固定 rollout.n | 可变(agent loop 多输出) |
| GRPO advantage | 每条独立算 | compute_advantage_for_multi_trajectories 把同组 final 输出广播到其他 output |
| 状态 | 本书默认讲它 | 新路径,agent loop / 多轮工具调用主要走它 |
初次跑训练用 main_ppo 即可;做 agent loop / 长 R1 风格再切 main_ppo_sync。第 12 章会用到。