手把手跑通 verl 强化学习训练框架

摘要
让大模型学会“做对题”“说人话”,靠的是强化学习。但真把强化学习跑起来,麻烦一大堆:一个训练循环里要同时伺候好几个模型,还要在“生成”和“训练”两种完全不同的工作模式之间来回切换。verl 就是专门解决这件事的开源框架,它把复杂的流程拆开、编排好,你只需要改几行配置。这篇文章从最基础的概念讲起,一步一步带你在一张显卡上跑通第一个强化学习训练,再换成更省资源的 GRPO,最后讲怎么扩到多卡多机。
背景与问题
先搞懂:强化学习在训练什么
想象你在教一个学生做数学题:
- 学生先做一遍题,写出答案。
- 老师批改,对了给 1 分,错了给 0 分。
- 学生根据分数调整自己的解题习惯:多用得分高的思路,少用得分低的思路。
- 重复很多轮。
这就是强化学习的全部想法。放到大模型上:
- 学生是要训练的模型,叫策略模型(也叫 actor)。
- 做题是让模型针对一个问题生成回答,这一步叫 rollout(生成)。
- 老师批改是奖励。奖励可以来自一个专门训练的打分模型,也可以直接来自规则,比如数学题答案对不对。
- 调整习惯是用奖励去更新模型参数。
RLHF 的全称是基于人类反馈的强化学习,早期的奖励来自人类偏好。近两年更流行的是奖励可以自动验证的场景,比如数学题、代码能不能通过测试,思路一模一样。
为什么这件事这么难做
用一句话概括:一个循环里,既要生成,又要训练,还要好几个模型一起上。
以最经典的 PPO 算法为例,一轮循环里要用到四个角色:
- Actor:正在被训练的策略模型,负责生成回答,也负责被更新。
- Reference(参考模型):训练前模型的一份冻结副本。用来算“你现在和最初相比偏离了多少”,防止模型为了骗高分而学歪。
- Critic(价值模型):估计“照这样写下去,最后大概能拿多少分”。它对回答里的每个位置都给一个预估分,用来判断某一步写得比预期好还是差。它本身也是一个要训练的大模型。
- Reward(奖励):打分的那一方,可以是模型,也可以是一段规则代码。
更麻烦的是,这些角色的工作模式差别很大:
- 生成阶段需要专门的推理引擎(比如 vLLM),追求吞吐;
- 训练阶段需要训练框架(比如 FSDP、Megatron-LM),要存梯度和优化器状态。
同一份 actor 权重,一会儿要放在推理引擎里,一会儿要放在训练框架里,显存怎么切、权重怎么同步,处理不好就是显存爆炸或者 GPU 大量空转。而且你会发现,整个循环里生成往往是最慢的一步,后面的实际日志里就能看到。
以前的做法哪里不好
如果用一个中心节点指挥所有事情(单控制器),灵活,但每个小操作都要经过它,调度开销很大。如果让所有节点各干各的(多控制器),效率高,但要改一个算法流程就得大动干戈。
核心思路与优势
verl 是什么
verl 是字节跳动 Seed 团队起头、现在由 verl 社区共同维护的开源强化学习训练库,是论文 HybridFlow 的开源实现,这篇论文发表在系统领域的顶会 EuroSys 2025 上。它的定位是灵活、高效、能用于生产环境的大模型强化学习框架。项目原来放在火山引擎的组织下,2026 年 1 月迁到了独立的 verl-project 组织,旧仓库地址会自动跳转到新地址。截至 2026 年 9 月,最新正式版是 v0.9.1(9 月 20 日发布)。
关键设计一:混合控制器
verl 把两种思路结合起来:
- 全局控制器只负责“各角色之间的数据怎么流”,比如先让 actor 生成,再交给 reward 打分,再交给 critic 估值。这样算法流程写起来清晰,改起来也方便。
- 分布式控制器负责每个角色内部的并行计算,这部分效率高,不经过全局节点。
好处是:想换个算法,只需要改数据流的那几行代码,不必碰底层的并行逻辑。
关键设计二:3D-HybridEngine
这是解决“同一份 actor 在训练和生成间切换”的办法。它负责在两种阶段之间,把 actor 的权重重新切分,做到内存零冗余,通信开销也小。论文报告,在不同的强化学习算法上,吞吐比当时的基线系统提升了 1.53 倍到 20.57 倍。
关键设计三:即插即用的后端
verl 不自己重复造轮子,而是把成熟的组件接进来:
- 训练后端:FSDP、FSDP2、Megatron-LM,另外还接入了 TorchTitan、VeOmni 等
- 生成引擎:vLLM、SGLang、HF Transformers,0.8 版起还加入了 TensorRT-LLM
- 支持的算法:PPO、GRPO、GSPO、RLOO、REINFORCE++、DAPO、DrGRPO 等一大批
- 硬件:英伟达 GPU、AMD ROCm、昇腾
- 模型:Qwen 系列、Llama 3.1、Gemma 2、DeepSeek 等
这意味着:小模型用 FSDP,超大模型换成 Megatron-LM,生成部分选 vLLM 或 SGLang,都只是改一个配置项。
PPO 和 GRPO:先看懂这一张表
这是入门时最容易被绕晕的地方,用最简单的话对比:
| PPO | GRPO | |
|---|---|---|
| 要不要 Critic | 要,额外多一个模型 | 不要 |
| 每个问题生成几条回答 | 通常 1 条 | 多条(一组) |
| 怎么判断回答好不好 | 让 Critic 估一个基准分 | 和同组其他回答的平均分比 |
| KL 惩罚加在哪 | 通常折算进奖励 | 直接加进损失 |
| 显存开销 | 较大 | 较小 |
GRPO 的思路很朴素:同一道题让模型答 5 遍,得分比这 5 遍的平均分高的回答就“表扬”,低的就“批评”(默认还会再除以这组分数的标准差,让不同题目的分数尺度一致)。它省掉了 Critic 这个大模型,所以更省显存、更容易上手,在数学、代码这类奖励可验证的任务上特别流行。
面向人群
- 想给自己的模型做强化学习后训练,但不知道从哪里入手的算法工程师
- 已经会做监督微调,想进一步提升推理、数学、代码能力的同学
- 想弄明白 PPO、GRPO 到底怎么落地,而不是只看公式的学习者
- 需要为团队选强化学习框架的技术负责人
实践步骤
我们分三段来走:先用官方的入门例子跑通 PPO,再换成 GRPO,最后讲怎么扩展。全程用的是数学应用题数据集 GSM8K 和一个 5 亿参数的小模型,一张显存 24GB 及以上的英伟达显卡就能跑(官方快速入门给出的最低要求)。
第一步:准备环境
先说清楚硬性条件:一台 Linux 机器,英伟达显卡,CUDA 12.8 及以上。官方的 uv 流程只支持 Linux,Mac 上跑不起来。
官方给了两条路,任选其一。
路线一:uv(新版文档的主推方式)
verl 仓库里提交了一份锁定好所有依赖版本的 uv.lock,你不需要手动 pip install 一堆东西。第一次执行 uv run 时,uv 会按锁文件自动建好虚拟环境 .venv,之后直接复用:
curl -LsSf https://astral.sh/uv/install.sh | sh # 安装 uv,已装可跳过
git clone https://github.com/verl-project/verl.git
cd verl
uv run --frozen --all-packages --extra vllm --extra fsdp python3 -c "import verl; print('ok')"
最后一行各部分的意思:
--frozen:严格按仓库里的锁文件装,不自己重新解析版本。--all-packages:把仓库里的所有子包都装上。--extra vllm --extra fsdp:选择“生成用 vLLM、训练用 FSDP”这套组合。生成引擎 vllm 和 sglang 只能二选一;训练后端在 fsdp 和 megatron 里选。
看到输出 ok,环境就建好了。第一次会下载不少东西,要等一会儿。接着激活这个环境,后面所有 python3 命令都在里面执行:
source .venv/bin/activate
路线二:Docker
快速入门文档推荐用 Docker 镜像,好处是 CUDA 驱动之外的依赖都已经装好。仓库里自带一份 uv 版的 Dockerfile,同样先 clone 仓库,然后在仓库根目录构建一次即可:
DOCKER_BUILDKIT=1 docker build -f docker/Dockerfile.uv.cu130 -t verl:uv-cu130 .
docker run --rm -it --gpus all verl:uv-cu130 bash
这个镜像里预先缓存好了所有依赖包,但还没有建好具体的环境。进入容器后,工作目录就是 verl 仓库,先执行一次路线一里那行 uv run ... python3 -c "import verl; print('ok')",它会用镜像里的缓存离线建好 .venv,然后就可以往下走了。Docker Hub 上也有官方预构建的 verlai/verl 镜像,用的话要挑和你的 verl 版本匹配的标签。
需要留意的是,生成引擎的版本要求很严格:verl 的版本更新会调整支持的 vLLM 范围,比如 0.9.0 起就不再支持 0.18.0 以前的 vLLM。所以请严格按照你所用版本的官方文档来,不要随手升级组件。
第二步:准备数据
GSM8K 是一个小学数学应用题数据集。verl 自带了预处理脚本,会把它转成 verl 需要的 parquet 格式:
python3 examples/data_preprocess/gsm8k.py --local_save_dir ~/data/gsm8k
跑完后,~/data/gsm8k 下会有 train.parquet 和 test.parquet 两个文件,分别是训练集和测试集。脚本还顺手做了一件重要的事:在每道题后面加上一句英文提示,要求模型“一步步思考,并把最终答案写在 #### 之后”。后面算奖励时,就靠这个标记找答案。
第三步:下载模型
我们用通义千问的 5 亿参数指令模型,小到一张卡就能装下 PPO 的全部角色:
python3 -c "import transformers; transformers.pipeline('text-generation', model='Qwen/Qwen2.5-0.5B-Instruct')"
这条命令的作用只是触发一次下载,把模型缓存到本地。如果从国内访问 Hugging Face 不方便,官方文档提供了一个开关:设置环境变量 VERL_USE_MODELSCOPE=True,训练时就会改从魔搭社区下载模型。
第四步:启动第一次 PPO 训练
PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \
data.train_files=$HOME/data/gsm8k/train.parquet \
data.val_files=$HOME/data/gsm8k/test.parquet \
data.train_batch_size=256 \
data.max_prompt_length=512 \
data.max_response_length=512 \
actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \
actor_rollout_ref.actor.optim.lr=1e-6 \
actor_rollout_ref.actor.ppo_mini_batch_size=64 \
actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \
actor_rollout_ref.rollout.name=vllm \
actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \
actor_rollout_ref.rollout.tensor_model_parallel_size=1 \
actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \
actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \
critic.optim.lr=1e-5 \
critic.model.path=Qwen/Qwen2.5-0.5B-Instruct \
critic.ppo_micro_batch_size_per_gpu=4 \
algorithm.use_kl_in_reward=True \
algorithm.kl_ctrl.kl_coef=0.001 \
trainer.logger=console \
trainer.val_before_train=False \
trainer.n_gpus_per_node=1 \
trainer.nnodes=1 \
trainer.save_freq=10 \
trainer.test_freq=10 \
trainer.total_epochs=15
这条命令基本照搬官方快速入门,只多加了一行 algorithm.use_kl_in_reward=True,原因下面会讲。如果你走的是 uv 路线,记得先激活 .venv 再执行。
参数很多,别慌,按前缀分组来看就清楚了:
data.*:喂什么数据
train_batch_size=256:每一轮训练(日志里的一个 step)取 256 个问题。注意单位是问题,不是回答。max_prompt_length和max_response_length:问题和回答各自最多多少个 token。回答写到 512 个 token 还没写完,就会被强行停下。
actor_rollout_ref.*:策略模型、生成引擎、参考模型三合一
model.path:要训练的模型。actor.optim.lr=1e-6:学习率。强化学习的学习率要比监督微调小得多,否则模型很容易学崩。ppo_mini_batch_size=64:每做一次参数更新用多少个问题,这是所有显卡加起来的全局值。256 个问题会被切成 4 份,所以每个 step 里 actor 实际更新 4 次参数。ppo_micro_batch_size_per_gpu=4:每张卡一次前向、反向计算处理几条样本(一条样本就是“一个问题加一条回答”)。显卡处理不了一整份 mini batch 时,就拆成几小口分别算,再把梯度累加起来。所以它不影响训练效果,只是显存和速度的折中:显存不够就调小。rollout.name=vllm:用 vLLM 来生成回答。rollout.gpu_memory_utilization=0.4:vLLM 最多能占用整张卡显存的 40%,用来放模型权重和生成时的缓存。这里给得比较少,是因为一张卡上还同时驻留着 actor、critic、参考模型的参数,训练时还要放梯度和优化器状态,得给它们留出空间。rollout.tensor_model_parallel_size=1:生成时用几张卡做张量并行。框架默认值是 2,单卡必须显式改成 1。rollout.log_prob_micro_batch_size_per_gpu和ref.log_prob_micro_batch_size_per_gpu:重新计算“每个词的概率”时,每张卡一次处理几条样本。同样只影响显存和速度。
critic.*:价值模型
- 这里用同一个模型来初始化 Critic,学习率
1e-5比 actor 大一些。
algorithm.*:KL 惩罚
use_kl_in_reward=True:把“偏离参考模型多少”折算成扣分,从奖励里扣掉,这是经典 PPO 的做法。新版本里这个开关默认是关的,而官方快速入门命令没有写它。不打开的话,下面的kl_coef不起作用,参考模型也根本不会启动。kl_ctrl.kl_coef=0.001:KL 惩罚系数。数值越大,模型越“保守”,越不敢偏离最初的样子。
trainer.*:训练调度
n_gpus_per_node=1、nnodes=1:一台机器一张卡。val_before_train=False:开训前不先跑一遍验证,省点时间。save_freq=10、test_freq=10:每 10 个 step 存一次检查点、跑一次验证。total_epochs=15:把训练数据过 15 遍。GSM8K 训练集约 7473 道题,每个 step 用 256 道,一遍大约 29 个 step,15 遍一共四百多个 step。
如果显存不到 32GB 跑不起来,官方建议先把 actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu 和 critic.ppo_micro_batch_size_per_gpu 都改成 1。
第五步:看懂训练日志
启动后,每个 step 终端会输出一长行指标,节选出来大概是这样(数字取自官方文档的示例日志):
step:0 - timing_s/gen:21.470 - timing_s/ref:4.360 - ... - critic/score/mean:0.004 - critic/vf_loss:14.947 - actor/pg_loss:-0.005 - response_length/mean:239.133
注意一个版本差异:官方快速入门文档里的示例日志还是旧名字 timing/gen、val/test_score/openai/gsm8k,新版本已经改成下面这些名字了,含义不变。如果你用的是较老的 verl 版本,看到旧名字也不用慌。
初学者最需要盯住的几个指标:
critic/score/mean:这一批回答的平均得分。训练的目标就是让它一路上升。刚开始接近 0,说明小模型基本答不对。val-core/openai/gsm8k/reward/mean@1:在测试集上的平均得分,每test_freq个 step 计算一次。GSM8K 答对 1 分、答错 0 分,所以它就是准确率,这是最终要看的“真实成绩”。timing_s/gen:生成阶段的耗时,单位是秒。上面这行里生成花了 21 秒,而参考模型计算(timing_s/ref)只花了 4 秒。这印证了前面说的:生成往往是最慢的一环。response_length/mean:回答的平均长度。如果它突然暴涨或骤降,通常说明模型学偏了。critic/vf_loss和actor/pg_loss:价值模型和策略模型各自的损失,用来判断训练是否稳定,不必逐个细看。
第六步:奖励是怎么来的
GSM8K 的奖励规则非常简单:模型的回答里,最终答案要写在 #### 之后。程序用正则表达式把它抽出来,和标准答案比较:答对得 1 分,答错得 0 分,没给答案也是 0 分。
verl 是怎么知道该用这条规则的?预处理脚本给每条数据打了一个来源标签 data_source(这里是 openai/gsm8k),标准答案则存在 reward_model.ground_truth 字段里。verl 看到这个来源,就自动调用内置的 GSM8K 打分函数。
这就是“可验证奖励”的魅力:不需要训练专门的打分模型,一段几行的规则代码就够了。想让模型学别的任务,思路是一样的:换一份数据,再写一个判断对错的函数交给 verl。比如新建一个 my_reward.py:
def compute_score(data_source, solution_str, ground_truth, extra_info=None, **kwargs):
# solution_str:模型生成的回答文本
# ground_truth:数据里存的标准答案
if "####" not in solution_str:
return 0.0 # 没按格式给答案
answer = solution_str.split("####")[-1].strip()
return 1.0 if answer == str(ground_truth).strip() else 0.0
函数的参数名要照这个写,返回一个分数即可。然后在训练命令里加两行,告诉 verl 去用它:
reward.custom_reward_function.path=$(pwd)/my_reward.py \
reward.custom_reward_function.name=compute_score \
name 默认就是 compute_score,函数叫这个名字时第二行可以省略。老版本的教程里这两个配置没有 reward. 前缀,新版本会自动兼容,但建议按新写法来。注意:自定义函数会同时用于训练和验证两边的打分。
第七步:换成 GRPO
PPO 要额外训练一个 Critic,比较占资源。换成 GRPO,就是不再要 Critic,让每个问题答多遍。需要改的关键点只有这几个:
PYTHONUNBUFFERED=1 python3 -m verl.trainer.main_ppo \
algorithm.adv_estimator=grpo \
algorithm.use_kl_in_reward=False \
data.train_files=$HOME/data/gsm8k/train.parquet \
data.val_files=$HOME/data/gsm8k/test.parquet \
data.train_batch_size=256 \
data.max_prompt_length=512 \
data.max_response_length=1024 \
actor_rollout_ref.model.path=Qwen/Qwen2.5-0.5B-Instruct \
actor_rollout_ref.actor.optim.lr=1e-6 \
actor_rollout_ref.actor.ppo_mini_batch_size=64 \
actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \
actor_rollout_ref.actor.use_kl_loss=True \
actor_rollout_ref.actor.kl_loss_coef=0.001 \
actor_rollout_ref.actor.kl_loss_type=low_var_kl \
actor_rollout_ref.rollout.name=vllm \
actor_rollout_ref.rollout.n=5 \
actor_rollout_ref.rollout.tensor_model_parallel_size=1 \
actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \
actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \
actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=8 \
trainer.logger=console \
trainer.val_before_train=False \
trainer.n_gpus_per_node=1 \
trainer.nnodes=1 \
trainer.save_freq=10 \
trainer.test_freq=10 \
trainer.total_epochs=15
和 PPO 版本对比,变化有四处:
algorithm.adv_estimator=grpo:把优势估计方式从默认的 GAE 换成 GRPO,同时去掉了整个critic.*配置,因为不再需要 Critic(优势估计方式不是 GAE 时,框架会自动关掉 Critic)。rollout.n=5:每个问题生成 5 条回答,构成一组。这个值必须大于 1,GRPO 才有“组内比较”可做。此时每个 step 实际生成的回答总数是train_batch_size × n,也就是 256 × 5 = 1280 条。同理,ppo_mini_batch_size=64虽然写的是 64 个问题,框架内部会自动乘以 5,每次参数更新实际用 320 条回答。use_kl_loss=True、kl_loss_coef=0.001、kl_loss_type=low_var_kl:GRPO 把 KL 惩罚直接加进损失里,而不是像 PPO 那样折算进奖励,所以要打开这个开关,同时让use_kl_in_reward保持关闭。系数 0.001 和估算方式low_var_kl都是框架默认值,写出来是为了看得明白。打开use_kl_loss后,参考模型会自动启动。max_response_length调到 1024:给模型多一点空间写解题过程。回答变长、条数变多,生成会更慢,显存也更紧,跑不动时先调小各个micro_batch_size_per_gpu。
另外两个 GRPO 相关的设置不用改,默认值就是官方推荐的:损失聚合方式 loss_agg_mode 默认就是 token-mean,熵奖励系数 entropy_coeff 默认是 0。想看更完整的写法,可以参考官方仓库 examples/grpo_trainer/ 下的脚本,比如 run_qwen3_8b_fsdp.sh,它面向的是 8 卡、80 亿参数的模型,额外开了动态批大小、梯度检查点、参考模型参数卸载等选项。
第八步:扩展到多卡、多机、大模型
小模型跑通之后,往上扩展主要动这几处:
- 单机多卡:最简单,把
trainer.n_gpus_per_node改成这台机器的显卡数,比如 8,其余不用动。 - 多机:verl 底层用 Ray 调度,多台机器要先组成一个 Ray 集群,再把训练任务提交上去。官方文档的手动步骤是这样的:
# 1. 在主节点上启动 Ray,输出里会给出一个集群地址
ray start --head --dashboard-host=0.0.0.0
# 2. 在每台其他机器上,用上一步拿到的地址加入集群
ray start --address=<主节点给出的地址>
# 3. 任意一台机器上确认节点都到齐了
ray status
# 4. 在主节点上,通过 Ray 控制台地址(默认 8265 端口)提交训练任务
ray job submit --address="http://127.0.0.1:8265" \
--runtime-env=verl/trainer/runtime_env.yaml \
--no-wait \
-- \
python3 -m verl.trainer.main_ppo \
trainer.n_gpus_per_node=8 \
trainer.nnodes=2 \
...
第 4 步的 ... 就是前面那一串训练参数,nnodes 填机器台数。提交后用 ray job logs <任务 ID> --follow 看实时日志,用 ray job stop <任务 ID> 停止任务。每台机器的环境、代码和数据路径要保持一致。如果控制台端口访问不通,多半是防火墙的问题。
- 生成并行度:模型变大后,把
rollout.tensor_model_parallel_size调大,让一个模型副本跨多张卡做生成。 - 换训练后端:模型大到 FSDP 吃力时,换成 Megatron-LM。官方示例脚本的做法是在训练命令里加一行
model_engine=megatron;uv 环境要同时把--extra fsdp换成--extra megatron。可以参考examples/grpo_trainer/run_qwen3_8b_megatron.sh。 - 参考模型卸载:官方建议超过 70 亿参数的模型,开启参考模型的参数卸载(把暂时不用的参数挪到内存里),配置是
actor_rollout_ref.ref.fsdp_config.param_offload=True。 - 监控:把
trainer.logger改成列表形式,比如trainer.logger='["console","wandb"]'(也支持 tensorboard 等),并设好trainer.project_name和trainer.experiment_name,方便对比多次实验。
常见问题排查
- 显存不足:优先调小各类
micro_batch_size_per_gpu,其次降低rollout.gpu_memory_utilization,再考虑缩短max_response_length或开启梯度检查点(actor_rollout_ref.model.enable_gradient_checkpointing=True)。 - 训练很慢:先看日志里
timing_s/gen在timing_s/step(整个 step 的总耗时)里是不是占大头。如果是,说明瓶颈在生成,可以增大生成并行度,或者换用吞吐更高的生成引擎。 - 奖励迟迟不涨:检查奖励函数是否真的能给出区分度。如果一组回答全对或全错,GRPO 就学不到东西。可以适当增大
rollout.n,或者换一份难度更合适的数据。 - 模型输出变得奇怪:可能是学偏了,适当调大 KL 系数(PPO 是
kl_ctrl.kl_coef,GRPO 是kl_loss_coef),或调小学习率。 - 改了 KL 系数却没反应:检查 KL 开关是否真的打开了。PPO 要
algorithm.use_kl_in_reward=True,GRPO 要actor.use_kl_loss=True,两个都关着时参考模型根本不会参与计算。 - 版本报错:强化学习框架依赖的组件多、更新快,遇到不兼容,第一反应是回到官方文档核对该版本要求的 vLLM、PyTorch 版本。
我的看法
强化学习后训练,过去是少数大厂才玩得起的事,原因不是算法多神秘,而是工程太重:多个模型、两种工作模式、复杂的并行。verl 的价值,就是把这层工程复杂度替你扛下来,让你能把精力放在数据和奖励设计上。
对新手来说,最务实的路线是:先用小模型加 GSM8K 把 PPO 跑通、看懂日志;再换成 GRPO,体会没有 Critic 的省心;然后换成你自己的任务,认真设计奖励函数。这一步往往比调参更决定成败。等到模型变大,再考虑多机、Megatron-LM 这些扩展手段,一步一步来,出问题也好定位。
项目地址
github.com/verl-project/verl