GRPO 与 RLVR 训练实战:基于 TRL 的可验证奖励强化学习配方、奖励门禁与变体选型(agents24 llm-finetuning 插件)
2026/9/10 13:47:12 网站建设 项目流程

GRPO 与 RLVR 训练实战:基于 TRL 的可验证奖励强化学习配方、奖励门禁与变体选型(agents24 llm-finetuning 插件)

【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents

导读

本文围绕 agents24 仓库中plugins/llm-finetuning插件的grpo-rlvr-training技能,系统讲解如何在目标任务具备算法可验证的通过/失败信号(数学答案、代码执行、工具调用、结构化输出)时,用 GRPO(Group Relative Policy Optimization)与 RLVR(Reinforcement Learning from Verifiable Rewards)训练推理模型。你将掌握:什么时候该用 RL 而不是 SFT 或 DPO、一份可直接落地的 TRLGRPOTrainer+ vLLM 参考配方、训练前强制执行的奖励函数人工检查门禁、五种可直接运行的奖励函数实现、失败模式驱动的 GRPO 变体选型(DAPO / Dr.GRPO / GSPO),以及不同参数量级下的显存规划与 DGX Spark 带宽约束。文章所有配置与代码均来自该仓库技能文档及其 references,可复制、可运行、可继续深入源码验证。

适用范围判定:什么情况下 RL 才是对的工具

grpo-rlvr-training技能在设计上承接finetuning-method-selection的决策路由。它只处理一类信号:可验证的 pass/fail 信号——单元测试通过、解析器接受输出、工具调用匹配预期 schema、数学答案匹配 ground truth。如果你手里的是人类示范(走lora-qlora-recipes)或偏好对(走preference-optimization),则不该进入本技能。

该技能的输入是"路由决策(RLVR via GRPO)+ 一个验证器(代码执行器、测试套件、schema 检查器或评分器)",输出不是自由形式的建议,而是一份经过校验的 GRPO 配置:kwarg 值来自 references/grpo-memory.md,奖励函数来自 references/reward-functions.md,由llm-finetuning-training-engineer直接消费。

先决条件一:评分必须算法可验证

如果对输出打分需要人类主观判断或主观评分标准,那首先是 eval-harness 与 judge 校准问题(见eval-harness-first技能),而不是跳过校准直接上 RL 的理由。GRPO+RLVR 只有在"成功是否发生"可以被机器判定时才值得投入。

先决条件二:模型必须已经"偶尔能成功"

RL 只能锐化已有能力,不能从零安装能力。打开一次 GRPO 运行之前,必须确认目标模型在目标任务上有时已经能成功——RL 通过向已成功的样本重新加权来打磨策略。由此产生两条路由:

  • 模型在低温度、大量采样下从未成功:缺口在格式理解或任务理解,而非策略精化。应回退到 SFT(lora-qlora-recipes),等基础成功率非零后再回到本技能。
  • 模型有时成功、但不稳定:这正是 GRPO 的最佳适用区间(sweet spot),直接进入下面的参考配方。

插件级通则:DPO 管品味,GRPO 管推理

插件内有一条贯穿始终的准则:DPO for taste, GRPO for reasoning。如果信号是两个可接受输出之间的偏好,那是 preference-optimization 的领地,而不是本技能。

参考配方:TRL GRPOTrainer + vLLM 生成

参考配方使用 TRL 的GRPOTrainer,配合 vLLM 加速生成:

from trl import GRPOConfig, GRPOTrainer grpo_args = GRPOConfig( output_dir="./outputs-grpo", use_vllm=True, vllm_mode="colocate", # single GPU; "server" for multi-GPU num_generations=8, # floor — fewer starves the group-relative baseline learning_rate=5e-7, # settled range for GRPO beta=0.01, # KL coefficient vs the reference policy per_device_train_batch_size=8, gradient_accumulation_steps=4, bf16=True, logging_steps=10, seed=3407, ) trainer = GRPOTrainer( model=SFT_CHECKPOINT, args=grpo_args, reward_funcs=[format_reward, correctness_reward], # references/reward-functions.md train_dataset=prompts, # prompt-only — GRPO generates its own completions processing_class=tokenizer, ) trainer.train()

每个关键参数的含义与"为什么"

参数取值语义与设计约束
use_vllm=True布尔用 vLLM 引擎做 rollout 采样,生成速度远快于原生 HF 采样
vllm_mode="colocate""colocate"/"server""colocate"将生成与训练放在同一 GPU(单卡默认);"server"指向独立 vLLM server 进程,是多 GPU 路径,生成与训练不争抢同一设备
num_generations=8≥ 8,下限而非建议GRPO 的优势估计相对组均值计算;每个 prompt 少于 8 个样本会产生噪声基线
learning_rate=5e-7浮点GRPO 已收敛的起始学习率区间;只有基础运行稳定且通过奖励检查后才能偏离
beta=0.01浮点相对参考策略的 KL 惩罚系数,控制策略偏离参考模型的程度
per_device_train_batch_size=8整数每设备训练批次
gradient_accumulation_steps=4整数梯度累积步数,等效扩大批次
bf16=True布尔BF16 混合精度;在训练工程师的 Failure Triage 中,fp16 在无良好 BF16 支持的硬件上是已知的静默发散源
logging_steps=10/seed=3407整数日志间隔与随机种子

关键设计要点

  • 奖励是复合的:格式奖励(输出是否解析 / 匹配所需结构)加上正确性奖励(答案是否通过验证)。格式正确但答案错误与格式畸形不应得同分——如果只用正确性奖励,会丢失这一区分信号。
  • 数据集只含 prompt:GRPO 自己生成补全(completions),训练数据集不需要预生成答案对。
  • 内存按目标规模分档:见 references/grpo-memory.md(后文详述)。

与训练工程师执行链的衔接

该配方由 llm-finetuning-training-engineer 在/finetune生命周期 Phase 4 消费:生成train/config.yamltrain/train.py提交后才允许启动,以{"step": 340, "loss": 0.812, "lr": 1.8e-4, "mem_gb": 71, "temp_c": 68}形式输出结构化进度。发散时按顺序排查:bf16 vs fp16 → 学习率与方法的匹配(GRPO 与 SFT/DPO 的收敛学习率区间差异很大)→ packing 损坏。这也解释了为什么本配方把bf16=Truelearning_rate=5e-7写死为基准值。

奖励检查门禁:训练前必须人工阅读 50–100 个样本

在启动正式训练前,把奖励函数跑在 50–100 个采样输出上,并逐条人工阅读结果。这是一道门禁(gate),不是一次性 sanity check。

如果奖励函数的判断与人类对该样本的阅读不一致,先修奖励函数再谈训练。对着一个未经检查的奖励训练,或通过调超参去补偿一个正在静默打分错误的奖励函数——这正是 reward-hacking 的产生机制:模型朝着错误目标干净地优化,而这种问题不会以训练循环 bug 的形式浮现出来。

从工作流视角看,这道检查是/finetune命令的Phase 1 gate 输入llm-finetuning-architect在走决策树时,会确认"在 GRPO 路由上,奖励函数的 Inspection Rule 已执行"(见 finetune.md 的 Phase 1 第 4 步)。同一个 50–100 样本的人工阅读,正是/finetune在允许 GRPO brief 继续之前检查的东西。

可对照检查的完整奖励函数实现(精确匹配、schema 校验、单元测试执行、长度惩罚包装器、rubric 判官模式)见 references/reward-functions.md。

奖励函数库:五种可直接运行的实现

references/reward-functions.md 提供符合当前 TRL 奖励函数签名的完整实现:接收completions以及任意额外数据集列作为关键字参数,返回与completions等长的list[float]。文中SFT_CHECKPOINT/JUDGE_MODEL是占位符,实际 checkpoint 见finetuning-method-selection的 references/model-catalog.md。

格式说明:以下示例假定标准(字符串)补全格式completions: list[str],直接调用.strip()json.loads().split()。若使用 TRL 的对话式数据集格式(每个 completion 是[{"role": "assistant", "content": "..."}]),需先提取completion[0]["content"]再做字符串操作。

1. 格式奖励(Format Reward)

检查结构合规性——补全是否遵循要求的响应形状——作为评判正确性的前置条件:

import re def format_reward(completions, **kwargs) -> list[float]: """1.0 if the completion has a <reasoning>...</reasoning> block followed by an <answer>...</answer> block, else 0.0. This is a gate, not the correctness signal — a well-formed wrong answer still scores 0 on correctness_reward below. """ pattern = re.compile( r"^<reasoning>.*?</reasoning>\s*<answer>.*?</answer>$", re.DOTALL, ) return [1.0 if pattern.match(c.strip()) else 0.0 for c in completions]

2. 正确性奖励——精确匹配(Exact Match)

基线版可验证答案奖励,适用于单一 ground-truth 字符串的任务(数学最终答案、闭式查询):

def correctness_reward(completions, answer, **kwargs) -> list[float]: """`answer` is the ground-truth column from the training dataset, aligned index-for-index with `completions`. Extracts the <answer> block from format_reward's expected shape and compares. """ rewards = [] for completion, gold in zip(completions, answer): match = re.search(r"<answer>(.*?)</answer>", completion, re.DOTALL) predicted = match.group(1).strip() if match else None rewards.append(2.0 if predicted == gold.strip() else 0.0) return rewards

注意:正确命中给 2.0 而非 1.0——与格式奖励叠加时,格式正确但答案错误得 0(格式门 1.0 × 0),只有格式与答案都对才得满分,从而保留了"格式-正确性"的区分度。

3. 正确性奖励——Schema 校验

用于结构化输出与工具调用任务,此时"正确"意味着"符合要求的 JSON Schema"而非字符串相等:

import json from jsonschema import validate, ValidationError def schema_reward(completions, output_schema, **kwargs) -> list[float]: """`output_schema` is a JSON Schema dict, either a single constant schema for the whole batch or a per-example list the same length as `completions`. Rewards valid, schema-conformant JSON; 0.0 for anything that doesn't parse or doesn't validate. """ if isinstance(output_schema, dict): # Constant case: one schema dict for every completion — # zip()-ing a bare dict would iterate its keys instead, # not the schema itself, so normalize first. schemas = [output_schema] * len(completions) else: schemas = list(output_schema) if len(schemas) != len(completions): raise ValueError( f"schema_reward: {len(schemas)} schemas for " f"{len(completions)} completions" ) rewards = [] for completion, schema in zip(completions, schemas): try: parsed = json.loads(completion) validate(instance=parsed, schema=schema) rewards.append(1.0) except (json.JSONDecodeError, ValidationError): rewards.append(0.0) return rewards

实现细节值得注意:当output_schema是单个 dict 时先归一化为等长列表,因为直接对 dict 做zip()会迭代其键而非 schema 本身;当传入列表但长度与 completions 不一致时显式抛ValueError,避免静默错位。

4. 正确性奖励——单元测试执行(含安全边界)

用于代码生成任务,"正确"意味着生成的函数通过留出测试套件。必须在子进程中以硬超时执行——绝不在进程内exec()不可信的补全

安全警告:此函数执行模型生成的代码,强制要求隔离环境:无网络的容器、gVisor/firejail 或专用 CI 沙箱,且环境中不得有任何 secrets 或凭据(无 HF token、实验跟踪器密钥、云凭据、SSH 密钥)。GRPO 在策略探索时会按设计把对抗性补全推过这条路径,切勿在持有凭据的训练主机上直接运行。超时只保护训练循环的活性,不是安全边界TemporaryDirectory只约束 harness 写文件的落点,不约束被执行的代码能读什么、能触达什么。

import logging import subprocess import tempfile from pathlib import Path logger = logging.getLogger(__name__) def test_execution_reward( completions, test_code, sandbox_cmd, timeout_s=10, **kwargs ) -> list[float]: """`test_code` is a per-example pytest snippet that imports the candidate under a fixed module name and asserts expected behavior. Runs each candidate in its own subprocess with a wall-clock timeout; an infinite loop or crash scores 0.0 instead of hanging the training loop. SECURITY: executes model-generated code. This function REQUIRES an isolation boundary — it does not run anything on the host by itself. `sandbox_cmd` (list[str], required) is a command prefix that wraps pytest in that boundary, e.g. a network-disabled, resource-capped Docker container: # sandbox_cmd = [ # "docker", "run", "--rm", "--network=none", # "--memory=1g", "--cpus=1", # "-v", f"{workdir}:/work:ro", "-w", "/work", # "python:3.12-slim", # ] If `sandbox_cmd` is falsy, this function refuses to execute anything and returns 0.0 for every completion — it never falls back to running pytest on the host. The subprocess environment is scrubbed to a minimal PATH (no HF tokens, experiment-tracker keys, cloud credentials, or SSH keys). The timeout is a liveness guard for the training loop, NOT a security boundary — isolation comes entirely from `sandbox_cmd`; the temporary directory only confines harness writes, not what executed code can read or reach. """ if not sandbox_cmd: logger.warning( "test_execution_reward: no sandbox boundary provided " "— refusing to execute model-generated code" ) return [0.0 for _ in completions] scrubbed_env = {"PATH": "/usr/bin:/bin"} rewards = [] for completion, tests in zip(completions, test_code): with tempfile.TemporaryDirectory() as tmp: candidate_path = Path(tmp) / "candidate.py" test_path = Path(tmp) / "test_candidate.py" candidate_path.write_text(completion) test_path.write_text(tests) try: result = subprocess.run( [*sandbox_cmd, "python", "-m", "pytest", str(test_path), "-q"], cwd=tmp, capture_output=True, timeout=timeout_s, env=scrubbed_env, ) rewards.append(1.0 if result.returncode == 0 else 0.0) except subprocess.TimeoutExpired: rewards.append(0.0) return rewards

这段实现的安全姿态是三层防御:sandbox_cmd缺省时拒绝执行并全员返回 0.0(绝不回退到主机跑 pytest)、子进程环境被清洗为最小 PATH、超时只保训练循环活性。这直接呼应训练工程师 agent 的失败分类——把执行路径上的资源/环境问题与训练配置问题分开处理。

5. 长度惩罚包装器

包装上述任一奖励函数,在不替换底层正确性信号的前提下抑制补全长度失控——适用于纯正确性奖励开始向更长、填充式输出漂移的场景:

def with_length_penalty(reward_fn, target_len=512, penalty_per_token=0.001): """Returns a new reward function that subtracts a small per-token penalty for every token past `target_len`, applied on top of `reward_fn`'s output. Penalty is capped so it can't drive an otherwise-correct reward negative — it discourages padding without overriding correctness. """ def wrapped(completions, **kwargs) -> list[float]: base_rewards = reward_fn(completions, **kwargs) adjusted = [] for reward, completion in zip(base_rewards, completions): overflow = max(0, len(completion.split()) - target_len) penalty = min(reward, overflow * penalty_per_token) adjusted.append(reward - penalty) return adjusted return wrapped

注意事项:len(completion.split())用词数作为 token 数的廉价代理,调target_len时应改用模型自己的 tokenizer 做真实 token 计数。更重要的是定位——这是针对已观测到的长度蠕变的定点修复,不是 Dr.GRPO 的替代品:如果长度偏置是系统性的而非偶发溢出,应路由到技能变体选型中的 Dr.GRPO,而不是堆叠惩罚包装器。

6. 边界情况:Rubric-as-Reward 判官模式

针对正确性不可用代码检查、但 pass/fail 边界足够清晰可供判官一致应用的场景(例如"响应是否遵循了要求的格式并保持切题",而非"这是不是一篇好文章")。二元 pass/fail,不用 Likert 分

def make_rubric_judge_reward(judge_client, rubric): """`judge_client` calls JUDGE_MODEL — a model from a *different* model family than the model under training, never the model being trained or a same-family relative of it. `rubric` is a fixed pass/fail criterion string, not a free-form quality prompt, so both are bound here rather than read from TRL-supplied per-example kwargs. Returns a reward function matching TRL's actual signature. """ def rubric_judge_reward(completions, prompts, **kwargs) -> list[float]: """Returns 1.0/0.0 per completion, never an intermediate score.""" rewards = [] for prompt, completion in zip(prompts, completions): verdict = judge_client.judge( rubric=rubric, prompt=prompt, response=completion, output_format="pass_fail", # binary only — no Likert scale ) rewards.append(1.0 if verdict == "pass" else 0.0) return rewards return rubric_judge_reward

两个硬性约束:判官必须是与被训练模型不同模型家族的模型(绝不能用被训练模型或其同族近亲);judge_client与固定的rubric通过闭包绑定后再交给GRPOTrainer(reward_funcs=[...])——因为 TRL 通过**kwargs只注入 completions 与数据集列,不会注入判官客户端这类任意对象。

校准是硬性前置条件,不是锦上添花:未校准的判官只是更吵、更贵的精确匹配奖励。接入 GRPO 前,判官必须对照人类标签校准(train/dev/sealed-test 划分、报告 TPR/TNR、判官固定到某个快照),该工作流在eval-harness-first技能中,不能因为 rubric"看起来显然正确"就跳过。

变体选型:失败模式驱动的 DAPO / Dr.GRPO / GSPO

基础配方是默认。只有在特定失败模式出现时才使用变体,不要预防性使用

失败模式变体原因
熵坍缩 / 退化的超长思维链DAPO解耦裁剪界限,并放宽过度正则化长推理轨迹探索的 KL 惩罚
奖励或输出长度与质量无关地持续上升Dr.GRPO移除 GRPO 的长度归一化偏置,让奖励追踪正确性而非补全长度
训练 mixture-of-experts(MoE)模型GSPO把重要性采样比移到序列级而非逐 token——逐 token 比率在 MoE 路由上不稳定,因此 GSPO 在这里是必需的而非可选

正确的流程是:先跑普通 GRPO,观察具体症状——长思维链上的熵坍缩、长度-奖励相关性、或 MoE 不稳定——然后才换上对应的变体。不要在基础配方真正表现出失败模式之前就预先选型。

VLM RL:仅作参考,v1 不执行

视觉-语言模型的 RL 在插件 v1 中不被执行——在此记录仅为提供上下文,不是可运行路径。原因有二:工具链在 ms-swift 和 EasyR1 衍生 fork 之间碎片化,尚无一行式的 TRL 命令;且把朴素的纯文本 GRPO 直接套到 VLM 上,模型容易通过优化文本推理轨迹而忽略图像来 reward-hack——"听起来对,但没看输入"。VLM RL 运行是超出本技能支持配方的研究 spike,而不是上述参考配方的一个变体。

内存与硬件规划

GRPO 在同等参数量下的内存足迹比 SFT 或 DPO 更重:训练运行 + 共驻(或服务端)的 vLLM 生成引擎按num_generations采样每个 prompt 的补全,叠加常规优化器状态与激活成本。完整的按规模分档、vLLM 睡眠模式与优化器状态策略、Unsloth 长上下文 RL 分块、以及 DGX Spark 解码密集型 rollout 的带宽告诫,都在 references/grpo-memory.md。

按规模类的显存锚点

规模类可行性锚点
小(≤~3B)24GB 级 GPU——需 vLLM sleep mode + 8-bit AdamW + 梯度检查点三管齐下
~32B 级H200 级 GPU
~70B 级B200 级 GPU
  • 24GB 级对小模型可行,但必须三个杠杆同时启用,缺一不可:
    • vLLM sleep mode:在 rollout 阶段与训练步阶段之间释放生成引擎的 KV-cache 与权重内存,而不是让两者同时驻留。
    • 8-bit AdamWoptim="adamw_8bit"):削减优化器状态内存,机制与 SFT/DPO 相同(见 lora-qlora-recipes)。
    • 梯度检查点:以重计算换激活内存。
  • H200 级是 ~32B 级模型的锚点:策略模型、参考模型(用于 KL 项)与共驻 vLLM 生成引擎三者在该级以下无法同时放下。
  • B200 级是 ~70B 级模型的锚点:同样基于三方驻留原因,只是规模更大。

这些是起始锚点而非硬下限vllm_mode="server"(独立的生成进程,可能在独立 GPU 上)会改变驻留计算,假设某规模类在特定机器上不可行之前,应重新推导。

Unsloth 长上下文 RL 分块

Unsloth 的分块损失(chunked-loss)RL 路径可在相同显存预算下将可用 RL 上下文延长至未分块 GRPO 的约7 倍——该数量级来自 Unsloth 自己发布的基准,本插件未重新测量;精确预算规划前应核对当前 Unsloth 版本发布说明。这对 RL 特别重要,因为 rollout(尤其是长思维链补全)正是 GRPO 在基础训练成本之上新增的显存压力点;分块是不改变num_generations或 batch size就能买到余量的杠杆。

DGX Spark:带宽受限的 rollout

GRPO 的 rollout 阶段是解码密集型——每个 prompt 采样num_generations≥ 8 个补全,且常带长思维链——而解码受内存带宽约束,而非计算。在 DGX Spark 上,共享内存带宽上限是 GRPO 特有、先于裸 VRAM 咬人的约束:实测持续带宽远低于 273 GB/s 的规格值。这正是dgx-spark-ops插件spark-training-gotchas技能(SKILL.md)的 G5 陷阱;规划 rollout 吞吐应依据该技能的带宽预算(持续 180–192 GB/s,而非宣传规格上限)。

实际结论:在 Spark 上做 GRPO 优先选小模型。一个在单台 Spark 上能舒适跑 SFT 或 DPO 的规模类,一旦 rollout 解码饱和共享带宽,在 GRPO 下可能严重瓶颈——上面的显存锚点表回答"能否装下",本节回答"能否快得值得跑"。当 Spark rollout 吞吐成为绑定约束时,降到更小的规模类通常比继续调 GRPO 超参更有效。

与整个 llm-finetuning 生命周期的集成

本技能不是孤立配方,而是插件 eval-gated 微调生命周期中 GRPO 路线的实现层。/finetune命令(finetune.md)将流程划分为七个 artifact 门禁阶段:Phase 0 构建 eval harness 与基线 → Phase 1 由 architect 做离场判定与选型(GRPO 路由在此确认奖励函数 Inspection Rule 已执行)→ Phase 2 数据集 → Phase 3 环境预检 →Phase 4 训练(本技能配置在此落地为train/config.yamltrain/train.py,提交后才启动)→ Phase 5 checkpoint 门禁 → Phase 6 导出。

相关技能边界也很清晰:finetuning-method-selection在存在可验证 pass/fail 信号时路由到这里;preference-optimization 是处理偏好对的姊妹技能;eval-harness-first覆盖任何非纯代码可检查奖励的判官校准。在 DGX Spark 上,本技能内存表未覆盖的记忆/热力修复阶梯,应服从已安装的dgx-spark-ops插件的技能。

参考资料

  • 主技能:SKILL.md — 路由判据、参考配方、检查门禁、变体选型、VLM 边界
  • 奖励函数库:references/reward-functions.md — 五种可直接运行的奖励函数(精确匹配、schema 校验、单元测试执行、长度惩罚包装器、rubric 判官模式),供 Inspection Rule 对照检查
  • 显存规划:references/grpo-memory.md — 按规模类显存锚点、vLLM sleep mode、8-bit AdamW、Unsloth 分块、DGX Spark 带宽告诫
  • 消费方:llm-finetuning-training-engineer — 负责将本配方生成配置、提交、启动、监控与失败分类
  • 编排入口:finetune.md — 七阶段 artifact 门禁生命周期,Phase 1 确认奖励函数检查已执行
  • 姊妹技能:finetuning-method-selection、preference-optimization、eval-harness-first、lora-qlora-recipes

【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询