Generated banner for Grpo Rlvr Training
Generated logo for Grpo Rlvr Training

Grpo Rlvr Training

Seth HobsonCommunityCreated by Seth Hobson

  • Skill
  • Tool integration
  • Data and AI
  • MIT
  • No issues found

From the author

Train reasoning and verifiable-task behavior with GRPO and reinforcement learning from verifiable rewards (RLVR). Use when task success is algorithmically checkable (math, code, tool calls, structured output), when designing GRPO reward functions, or when a GRPO run diverges or reward-hacks.
— the description by Seth Hobson

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a standardized recipe for using Group Relative Policy Optimization (GRPO) to refine LLM reasoning on tasks with algorithmically checkable success criteria. It emphasizes the importance of verifiable rewards, manual inspection of reward functions, and specific hardware-aware configurations for training stability. The approach is designed for tasks where the model already demonstrates baseline capability but requires reinforcement to improve consistency and reasoning depth.

What it does

  • Defines the GRPO training configuration using TRL's GRPOTrainer.
  • Provides a framework for implementing composite reward functions (format and correctness).
  • Establishes a mandatory inspection gate for reward functions before training.
  • Offers guidance on hardware-specific memory management for GRPO rollouts.
  • Outlines variant selection for specific failure modes like entropy collapse or MoE instability.

When to use it

  • When task success is algorithmically verifiable (e.g., code, math, JSON schema).
  • When the model already succeeds inconsistently and needs policy refinement.
  • When you need to optimize reasoning traces rather than just output preferences.
  • When a GRPO run exhibits specific issues like reward hacking or length-bias.

Advantages

  • Enables policy refinement without requiring large datasets of human demonstrations.
  • Provides a clear, repeatable recipe for verifiable task training.
  • Includes a critical inspection gate to prevent training on flawed reward logic.
  • Offers specific variants to address common RL training instabilities.

Usage examples

“I have a model that solves math problems but is inconsistent. How do I use GRPO to improve it?”
The agent provides the GRPOTrainer configuration, explains the requirement for a verifiable correctness reward function, and details the mandatory inspection process.
“My GRPO training run is reward hacking by just increasing output length. What should I do?”
The agent identifies this as a length-normalization bias and recommends switching to the Dr.GRPO variant to decouple reward from completion length.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/wshobson/grpo-rlvr-training/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip grpo-rlvr-training.zip -d .agents/skills

Documentation by Seth Hobson

GRPO & RLVR Training

This skill assumes finetuning-method-selection already routed here because the target behavior has a verifiable pass/fail signal — not demonstrations (lora-qlora-recipes) or preference pairs (preference-optimization). What follows is when RL is the right tool, the reference recipe, the mandatory reward-inspection gate, and how to pick a GRPO variant when the base recipe misbehaves.

Input: a routing decision (RLVR via GRPO) plus a verifier (code executor, test suite, schema checker, or grader) for the target task. Output format: a validated GRPO config — the kwarg values in references/grpo-memory.md and the reward functions in references/reward-functions.md, not free-form advice — that llm-finetuning-training-engineer consumes directly.

When RL Applies

GRPO+RLVR only pays off when task success is algorithmically checkable — a unit test passes, a parser accepts the output, a tool call matches an expected schema, a math answer matches a ground truth. If grading the output requires human judgment or a subjective rubric, that's an eval-harness and judge-calibration problem first — see eval-harness-first — not a reason to skip straight to RL.

Before opening a GRPO run, confirm the model can sometimes succeed on the target task already. RL sharpens an existing capability by reweighting toward the samples that already work; it does not install a capability from zero.

  • The model never succeeds, even at low temperature across many samples: the gap is format or task understanding, not policy refinement. Route back to SFT first (lora-qlora-recipes) and only return to this skill once the base success rate is nonzero.

  • The model succeeds sometimes, inconsistently: this is the GRPO sweet spot — proceed to The Recipe below.

The standing rule for the whole plugin: DPO for taste, GRPO for reasoning. If the signal is a preference between two acceptable outputs, that's preference-optimization, not this skill.

The Recipe

The reference recipe is TRL's GRPOTrainer with vLLM-backed generation:

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()
  • vllm_mode="colocate" runs generation and training on the same GPU — the default for a single-GPU box.

  • vllm_mode="server" points at a separate vLLM server process and is the multi-GPU path — generation and training don't compete for the same device.

  • num_generations ≥ 8 is a floor, not a suggestion: GRPO's advantage estimate is relative to the group mean, and fewer than 8 samples per prompt produces a noisy baseline.

  • Reward is composite — a format reward (did the output parse / match the required structure) plus a correctness reward (did the answer verify). A well-formed-but-wrong answer and a malformed one should not score identically; correctness alone loses that signal.

  • learning_rate=5e-7 and beta=0.01 are the settled starting point; deviate only after the base run is stable and reward-inspected (below).

Memory sizing for this recipe by target size class: references/grpo-memory.md.

The Inspection Rule

Run the reward function against 50–100 sampled outputs and manually read the results before starting the actual training run. This is a gate, not a one-time sanity check.

If the reward function's judgment disagrees with a human reading of that sample, fix the reward function first. Training against an uninspected reward, or tuning hyperparameters to compensate for one silently scoring the wrong thing, is how a run reward-hacks: the model optimizes cleanly toward the wrong target, and that doesn't surface as a training-loop bug.

This inspection is a Phase 1 gate input for /finetune — the same 50–100-sample read that catches a broken reward function here is what that command checks for before it lets a GRPO brief proceed.

Complete reward function implementations to inspect against — exact-match, schema-validation, unit-test-execution, a length-penalty wrapper, and a rubric-as-reward judge pattern: references/reward-functions.md.

Variant Selection

The base recipe above is the default. Reach for a variant only when a specific failure mode shows up, not preemptively:

Failure modeVariantWhy
Entropy collapse / degenerate long chain-of-thoughtDAPODecouples clip bounds and relaxes the KL penalty that over-regularizes exploration on long reasoning traces
Reward or output length trends up regardless of qualityDr.GRPORemoves GRPO's length-normalization bias so reward tracks correctness, not completion length
Training a mixture-of-experts modelGSPOMoves the importance-sampling ratio to the sequence level instead of per-token — per-token ratios are unstable on MoE routing, so GSPO is required here, not optional

Start with plain GRPO. Watch for the specific symptom — collapsing entropy on long CoT, a length-reward correlation, or MoE instability — and only then swap in the matching variant above. Don't pre-select a variant before the base recipe has actually shown the failure mode.

VLM RL Is Reference-Only

Vision-language RL is not executed by this plugin in v1 — it's documented here for context, not as a runnable path. Tooling is fragmented across ms-swift and EasyR1-derived forks with no one-line TRL command yet, and naive text-only GRPO applied to a VLM tends to reward-hack by optimizing the text-reasoning trace while ignoring the image — the model learns to sound right without looking at the input. A VLM RL run is a research spike outside this skill's supported recipe, not a variant of The Recipe above.

References

  • references/reward-functions.md — complete Python reward functions (exact-match correctness, schema validation, unit-test execution, a length-penalty wrapper, and a rubric-as-reward judge pattern) to inspect under The Inspection Rule before any training run.

  • references/grpo-memory.md — memory sizing by target size class, vLLM sleep-mode and optimizer-state tactics, Unsloth's long-context RL chunking, and the DGX Spark bandwidth caveat for decode-heavy rollouts.

Related skills: finetuning-method-selection routes here once a verifiable pass/fail signal exists; preference-optimization is the sibling skill for preference pairs rather than verifiable rewards; eval-harness-first covers judge calibration for any reward that isn't purely code-checkable. On DGX Spark, defer to the dgx-spark-ops plugin's skills, when installed, for the memory/thermal remediation ladder this skill's memory table doesn't cover.

Files

references/reward-functions.md

Last verified: 2026-07-13

GRPO Reward Function Library

Complete, runnable reward functions for TRL's GRPOTrainer. Every function here follows the current TRL reward-function signature: it accepts completions plus any extra dataset columns as keyword arguments, and returns a list[float] the same length as completions. Base models are never named here — SFT_CHECKPOINT/JUDGE_MODEL are placeholders; see finetuning-method-selection's references/model-catalog.md for actual checkpoints.

These examples assume the standard (string) completion format — completions: list[str] — and call .strip(), json.loads(), .split(), etc. directly on each completion. TRL's conversational dataset format instead passes each completion as [{"role": "assistant", "content": "..."}]; on that format, extract completion[0]["content"] before applying any of the string operations below, in every function in this file.

Before wiring any of these into a training run, inspect them against 50–100 sampled outputs by hand — this is SKILL.md's Inspection Rule, not optional. A reward function that looks correct in isolation can still disagree with human judgment on real model outputs.

Format Reward

Checks structural compliance — did the completion follow the required response shape at all — as a prerequisite to grading correctness:

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]

Correctness Reward — Exact Match

The baseline verifiable-answer reward, for tasks with a single ground-truth string (math final answers, closed-form lookups):

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

Correctness Reward — Schema Validation

For structured-output and tool-call tasks, where "correct" means "conforms to the required JSON schema," not string equality:

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

Correctness Reward — Unit Test Execution

For code-generation tasks, where "correct" means the generated function passes a held-out test suite. Execute in a subprocess with a hard timeout — never exec() untrusted completions in-process.

WARNING — this function executes model-generated code and REQUIRES an isolated environment: a network-disabled container, gVisor/firejail, or a dedicated CI sandbox, with no secrets or credentials in the environment — no HF tokens, experiment-tracker keys, cloud credentials, or SSH keys. GRPO will, by design, push adversarial completions through this path as the policy explores. Never run it directly on a training host holding credentials. The timeout below protects training-loop liveness only — it is NOT a security boundary. Likewise, TemporaryDirectory confines where the harness writes its files, not what the executed code can read or reach. The function below enforces this: it takes the sandbox boundary as a required argument and refuses to run at all — returning reward 0.0 — when the caller doesn't supply one. It never falls back to host execution.

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

Length-Penalty Wrapper

Wraps any reward function above to discourage runaway completion length without replacing the underlying correctness signal — use when a correctness-only reward starts trending toward longer, padded outputs:

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

Note that len(completion.split()) counts words as a cheap proxy for tokens — use the model's own tokenizer for true token counts when tuning target_len.

This is a targeted fix for observed length creep, not a substitute for Dr.GRPO — if length bias is systemic rather than an occasional overflow, route to the Dr.GRPO variant in SKILL.md's Variant Selection instead of stacking penalty wrappers.

Rubric-as-Reward Judge Pattern

For tasks where correctness isn't code-checkable but the pass/fail line is still crisp enough for a judge to apply consistently — e.g., "did the response follow the requested format and stay on-topic" rather than "is this a good essay." Binary pass/fail, not a Likert score:

TRL calls reward functions with completions plus whatever dataset columns the trainer was given, via **kwargs — it does not inject arbitrary objects like a judge client. Bind judge_client and the fixed rubric in a closure before handing the result to GRPOTrainer(reward_funcs=[...]), rather than declaring them as parameters TRL is expected to supply:

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

Calibration is a hard prerequisite, not a nice-to-have. An uncalibrated judge is a noisier, more expensive version of the exact-match reward above — before wiring rubric_judge_reward into a GRPO run, the judge must be calibrated against human labels (train/dev/sealed-test splits, TPR/TNR reported, judge pinned to a fixed snapshot). That calibration workflow lives in eval-harness-first; do not skip it because the rubric "looks obviously right" — the same plugin-wide judge-calibration prerequisite applies here as everywhere else a judge grades a reward.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Seth Hobson
Publisher
Seth Hobson (Community)
Source
wshobson/agents · plugins/llm-finetuning/skills/grpo-rlvr-training · commit 4bde316
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Seth Hobson or Seth Hobson.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download grpo-rlvr-training.zip