The LLM StackFrom Silicon to Agents
Part V — Post-Training & Alignment
45 min read·Updated ·▶ Run the code (Colab)

5.9 RL with Verifiable Rewards (RLVR) & The Reasoning Recipe

Every reinforcement-learning method we have built so far in this Part — The RLHF Pipeline & Reward Modeling, PPO for LLMs, DPO, GRPO and RLOO — has shared one expensive assumption: that the reward comes from a learned reward model trained on human preference labels. That reward model is a second neural network. It is trained on noisy, expensive, slowly-collected human data. It is the thing you optimize against, and the moment your policy is strong enough it starts to hack it — finding inputs the reward model scores highly but a human would not.

This chapter is about the idea that, for a large and important class of problems, you do not need a reward model at all. If the task has a checkable answer — a math problem with a known numerical solution, a coding problem with a unit-test suite, a constrained-format extraction task with an exact-match target — then the reward is just a program that returns 1 if correct and 0 otherwise. No human labels, no learned reward network, no preference dataset. We call this RL with Verifiable Rewards (RLVR), and it is the single most consequential post-training idea of 2024–2025. It is the engine behind DeepSeek-R1, behind the OpenAI o-series reasoning models, behind Tülu 3, and behind nearly every open “reasoning model” released in that window.

RLVR is conceptually trivial — “reward = did the answer pass the checker” — and that triviality is exactly the point. By replacing a fragile, hackable, expensive learned reward with a cheap, exact, unhackable-in-the-usual-sense programmatic one, you remove the dominant failure mode of RLHF and unlock something startling: when you point a base model at hard verifiable problems and reward only correctness, it teaches itself to reason — generating longer and longer chains of thought, learning to verify and backtrack, exhibiting the now-famous “aha moment” — with no demonstrations of reasoning at all. This chapter explains the reward mechanism in depth, builds real verifiers (math equivalence and a sandboxed code runner), dissects the R1-Zero phenomenon, and traces the path from narrow verifiable domains to general reasoning.

We will lean on GRPO, RLOO & Critic-Free RL for the optimizer — RLVR is an answer to “where does the reward come from,” not “how do we take the gradient” — and on Reward Engineering, Verifiers & Sandboxes for the production-infrastructure view.

What makes a reward “verifiable”

The defining property

A reward is verifiable when correctness can be decided by a deterministic program \(V(q, o) \to \{0, 1\}\) (or a graded \([0,1]\)) that does not itself need to be learned and does not depend on the policy. Contrast the two regimes:

\[ R_{\text{RM}}(q, o) = r_\phi(q, o) \quad\text{(learned, continuous, hackable)} \qquad\text{vs.}\qquad R_{\text{RLVR}}(q, o) = V(q, o) \in \{0, 1\} \quad\text{(programmatic, exact)} \]

The learned reward \(r_\phi\) is a neural network with parameters \(\phi\) fit to human preferences; it is an approximation of “what humans want,” and it is differentiable, dense, and — crucially — imperfect everywhere. The verifiable reward \(V\) is ground truth: if the math answer equals the gold answer, the reward is exactly 1; otherwise exactly 0. There is no approximation error to exploit. This is why RLVR is sometimes called “RL against the environment” rather than “RL against a model.”

Three families of verifiers dominate practice:

  1. Exact / equivalence match (math, factual short-answer). The model is asked to put its final answer in a delimiter (e.g. \boxed{...} or <answer>...</answer>); the verifier parses it and compares to the gold answer, ideally with semantic equivalence (so \(\frac{1}{2}\), \(0.5\), and \(0.50\) all match). This requires a symbolic/numeric normalizer, not naive string equality.
  2. Execution match (code). The model emits a program; the verifier runs it in a sandbox against a hidden test suite and rewards the fraction (or all-or-nothing) of tests passed. This is the most powerful verifier because passing tests is a strong proxy for correctness — but it also has the largest attack surface (the model can try to read the tests, hard-code outputs, or exploit the sandbox).
  3. Constraint / format check (structured output, instruction following). The verifier checks programmatically-decidable properties: “is this valid JSON matching this schema,” “does the answer contain exactly three sentences,” “does it avoid the forbidden word.” Used heavily in instruction-following RL (e.g. Tülu 3’s “RLVR” recipe includes such constraint checkers).

Why verifiable rewards change the game

It is worth being precise about why this matters, beyond “it’s cheaper.” There are four distinct advantages, and an interviewer will want all four:

  • No reward-model training loop. You skip preference data collection, reward-model architecture, reward-model training, and reward-model serving. The reward is a function call. This collapses the RLHF pipeline from “two models and a human-data pipeline” to “one policy and a checker.”
  • The reward cannot be over-optimized in the usual sense. Classic reward over-optimization (Goodhart’s law: “when a measure becomes a target it ceases to be a good measure”) happens because the learned reward diverges from true quality off-distribution. A correct-answer checker is the true quality (for the narrow definition “got the right answer”). You can push the policy arbitrarily hard against is_correct and it will keep getting more correct. (RLVR still has its own hacks — see §6 — but they are program bugs, not statistical drift.)
  • Dense, free supervision at scale. Every problem with a known answer is a training example, and you can generate such problems (templated arithmetic, synthetic theorem instances, mutated code problems) essentially without limit. The bottleneck moves from “human labels” to “problems with checkable answers.”
  • It exposes a learning signal the model can climb. Because the reward is exact, the gradient is clean: the only way to increase reward is to actually solve more problems. This is the precondition for the emergent-reasoning phenomenon — the optimizer is not being nudged toward a fuzzy human aesthetic, it is being pushed straight at “be correct,” and the shortest path to “be correct” on hard problems turns out to be “think more.”
RLHF — learned reward Human preference labels noisy · expensive · slow train reward model r_phi(q,o) in R (a network) neural network, parameters phi approximate · learned POLICY OPTIMIZES r_phi Policy samples o output completions ...and HACKS r_phi approximate · differentiable · hackable RLVR — verifiable reward Prompt q (with gold answer) no human reward labels needed policy samples o V(q,o) = checker(o, gold) a deterministic program ground truth · exact exact (no approximation) reward in {0, 1} no network · no approximation ground truth · exact "unhackable" in the usual sense Left: policy hacks the learned reward (red loop). Right: no hack loop — verifier IS ground truth.
Learned reward (RLHF) vs. verifiable reward (RLVR). RLHF trains a neural network r_phi to approximate human preferences, which the policy optimizes and ultimately hacks (red feedback loop). RLVR replaces that with a deterministic checker V(q,o) in {0,1} that is exact ground truth — the absence of a red loop on the right is the whole point.

Aside: RLVR is not new, it is newly central

Rewarding a model for getting the right answer is as old as RL itself, and “execution-guided” code generation and self-taught reasoners (the STaR line of work, Zelikman et al., 2022) predate the term. What changed in 2024–2025 is (a) base models became strong enough that pure correctness-RL works from scratch, (b) critic-free optimizers like GRPO made the RL cheap, and © DeepSeek-R1 demonstrated the phenomenon at scale and open-sourced the recipe. “RLVR” as a named, deliberate strategy crystallized around Tülu 3 (Lambert et al., 2024) and DeepSeek-R1 (2025).

The R1-Zero phenomenon: reasoning that emerges from correctness alone

This is the heart of the chapter and the result that made RLVR famous. We summarize the mechanism here; the optimizer (GRPO) and the multi-stage R1 pipeline are detailed in GRPO, RLOO & Critic-Free RL, and the broader test-time-compute story is in Reasoning, Chain-of-Thought & Test-Time Compute. Here we focus on why correctness pressure alone grows reasoning.

The setup

R1-Zero starts from a base pretrained model — not even instruction-tuned — and applies GRPO with a reward that is only:

\[ R(q, o) = \underbrace{\mathbb{1}[\text{boxed answer matches gold}]}_{\text{accuracy, the real signal}} \; + \; \underbrace{\lambda \cdot \mathbb{1}[\text{response uses } \texttt{<think>}\,/\,\texttt{<answer>} \text{ format}]}_{\text{small format shaping}} \]

with \(\lambda\) small (the format term teaches structure, not content; keep it a fraction of the accuracy reward so the model can never profit by formatting a wrong answer). There is no reward model, no value network, no human preference data, no demonstrations of how to reason. The prompts are hard math and code problems with known answers.

What emerges

Three behaviors appear over training, none of them programmed:

  1. Response length grows. Average completion length climbs steadily over RL steps — the model spontaneously produces longer chains of thought. Nobody rewarded length directly (and as we saw in GRPO, naive GRPO has a spurious length bias, but the genuine reasoning-length growth persists even after that bias is removed).
  2. Self-verification and backtracking. The model begins to write things like “let me check this,” recompute a sub-result, and correct itself mid-stream. It learns to re-derive and cross-check because, on hard problems, a single forward pass is wrong too often — and the only way to raise the correctness reward is to catch and fix its own mistakes.
  3. The “aha moment.” The DeepSeek-R1 paper documents a striking qualitative event: the model, mid-derivation, writes something like “Wait, wait. That’s an aha moment. Let me re-evaluate…” and revises its approach. This is not a canned phrase from SFT data (there was none); it is an emergent strategy that the correctness reward selected for.

Why correctness pressure causes this — the mechanism

The intuition is a credit-assignment-plus-exploration argument. Frame each problem as an MDP where the model’s “policy” is its generation process. On a hard problem, the base model’s single-shot accuracy is low — say it solves 10% of attempts. Under RLVR with a group of \(G\) samples, the advantage is positive exactly for the trajectories that reached the right answer (see the group-baseline mechanics in GRPO). Now ask: what distinguishes the winning trajectories from the losing ones? Empirically, the winners are the ones that spent more tokens checking intermediate steps, exploring an alternative when the first approach stalled, and verifying the final result. So the gradient systematically up-weights those behaviors. More compute spent reasoning \(\to\) higher probability of correctness \(\to\) positive advantage \(\to\) more of that behavior next time. The model is, in effect, discovering test-time compute as the solution to a sparse-reward optimization problem.

There is a deeper, somewhat humbling caveat that the 2025 literature surfaced and you should be ready to discuss: RLVR may be primarily eliciting and sharpening capabilities the base model already latently has, rather than teaching wholly new ones. Several analyses found that for modest sample budgets RLVR improves pass@1 dramatically but the pass@k for large k (the set of problems the model can solve at all given many tries) barely moves — i.e. RLVR concentrates probability mass on reasoning paths the base model could already occasionally find, rather than expanding the frontier of solvable problems. This reframes RLVR as a very efficient elicitation / distillation-of-self mechanism, which is consistent with why a base model with strong latent math ability is a prerequisite. (Other work pushes back, showing frontier expansion with enough compute and harder data. The honest answer in 2026 is “it does both, and the balance depends on the base model and budget.”)

Why length grows under pure correctness reward (correctness is rewarded; length is a side effect) length short long STEP 0 — base model single-shot, short answers · ~10% correct sample group of G sample group win + win + lose - lose - lose - lose - few winners, many losers adv. on winners adv. on losers What distinguishes the WINNERS? (these behaviors correlate with reaching the gold answer) check intermediate steps try alt. approaches verify final result gradient up-weights these Gradient up-weights: "spend more tokens reasoning" repeat over thousands of steps STEP N — emergent behavior long CoT · self-verification · backtracking · "aha" · ~70%+ correct (no reasoning demonstrations were ever provided)
Why response length grows under pure correctness reward. At step 0, a base model with ~10% accuracy generates a group of samples; the few winners (those that reached the correct answer) happen to have spent more tokens checking steps and verifying results. The gradient up-weights exactly those behaviors, creating a compounding loop: more reasoning tokens raise correctness, raising those behaviors' probability, making completions longer and more accurate until, at step N, long chain-of-thought, self-verification, and backtracking emerge — none of it trained directly.

Common pitfall: R1-Zero needs a base model that can sometimes succeed

The emergent-reasoning loop only turns if the group of \(G\) samples contains both successes and failures — otherwise every advantage is zero and there is no gradient (the “dead group” problem from GRPO). On a base model too weak to ever solve a problem (pass@G \(\approx 0\)), RLVR produces nothing — flat reward, no learning. This is why R1-Zero worked on a very strong base (DeepSeek-V3) and why people who tried “R1-Zero on a small base model” with hard problems often saw no emergence. The fix is curriculum: start with problems the base solves ~20–60% of the time and ramp difficulty as the policy improves.

Why RLVR needs a mixed group: zero variance = zero gradient each column is one GRPO group of G = 6 samples on the SAME problem Too hard pass@G ~ 0 lose - lose - lose - lose - lose - lose - R = {0,0,0,0,0,0} mean = 0, sigma = 0 A_i = (R_i - mean)/sigma = 0 DEAD GROUP - no gradient Sweet spot pass@G mixed win + win + lose - lose - lose - lose - R = {1,1,0,0,0,0} mean = 1/3, sigma > 0 variance > 0 -> A_i != 0 LIVE - behaviors get selected Too easy pass@G ~ 1 win + win + win + win + win + win + R = {1,1,1,1,1,1} mean = 1, sigma = 0 A_i = (R_i - mean)/sigma = 0 DEAD GROUP - no gradient pass@G = 0 (too hard) pass@G = 1 (too easy) learnable band ~ mixed outcomes curriculum: keep prompts in the mixed band; ramp difficulty as the policy improves
Only a mixed group of outcomes produces a learning signal in GRPO. When every sample in a group fails (too hard) or every sample succeeds (too easy), the reward has zero variance, so every advantage $A_i = (R_i - \text{mean})/\sigma$ is exactly zero and no gradient flows — the group is "dead." Only the sweet-spot group, with both wins and losses, has nonzero variance: winners get pushed up, losers get pushed down, and the reasoning behaviors that distinguish them get selected — which is why R1-Zero-style runs need a curriculum that keeps problems in this mixed band.

Building real verifiers I: math equivalence

The accuracy reward is only as good as the checker. Naive string equality is catastrophically wrong: it would mark 0.5 incorrect against gold 1/2, mark x=2 incorrect against 2, and mark \frac{1}{2} incorrect against 0.5. A real math verifier must (1) extract the final answer from a long chain of thought, then (2) normalize and compare with numeric/symbolic equivalence. Here is a compact but realistic implementation.

import re
from fractions import Fraction

def extract_boxed_answer(text: str) -> str | None:
    r"""
    Pull the LAST \boxed{...} content from a chain-of-thought response.
    We take the last one because the model often writes intermediate
    \boxed expressions before its final answer. Handles nested braces.
    """
    idx = text.rfind(r"\boxed")
    if idx == -1:
        # Fallback: try an <answer>...</answer> delimiter.
        m = re.findall(r"<answer>(.*?)</answer>", text, flags=re.DOTALL)
        return m[-1].strip() if m else None
    # Walk braces to find the matching close for \boxed{ ... }.
    i = text.find("{", idx)
    if i == -1:
        return None
    depth, j = 0, i
    while j < len(text):
        if text[j] == "{":
            depth += 1
        elif text[j] == "}":
            depth -= 1
            if depth == 0:
                return text[i + 1 : j].strip()
        j += 1
    return None  # unbalanced braces

def normalize_numeric(s: str):
    """
    Try to coerce a string answer to an exact rational or a float.
    Handles fractions ('3/4'), LaTeX \frac, percentages, commas,
    surrounding $ signs, and trailing units-free numbers. Returns
    a Fraction/float, or None if it isn't a clean number.
    """
    if s is None:
        return None
    s = s.strip()
    s = s.replace("$", "").replace(",", "").replace("\\!", "").strip()
    s = re.sub(r"\\text\{.*?\}", "", s)              # drop \text{...} units
    s = s.replace("\\%", "").replace("%", "")        # treat percent as a number
    # LaTeX \frac{a}{b}  or  \dfrac{a}{b}
    m = re.fullmatch(r"\\d?frac\{(-?\d+)\}\{(-?\d+)\}", s)
    if m:
        return Fraction(int(m.group(1)), int(m.group(2)))
    # plain a/b
    m = re.fullmatch(r"(-?\d+)\s*/\s*(-?\d+)", s)
    if m:
        return Fraction(int(m.group(1)), int(m.group(2)))
    try:
        return Fraction(s)            # exact for integers / decimals like '0.50'
    except (ValueError, ZeroDivisionError):
        pass
    try:
        return float(s)               # last resort, lossy
    except ValueError:
        return None

def math_is_correct(response: str, gold: str, atol: float = 1e-6) -> float:
    """
    Verifiable math reward: 1.0 if the model's final boxed answer is
    numerically equivalent to gold, else 0.0. Falls back to a
    normalized string compare for non-numeric answers (e.g. '(2, 3)').
    """
    pred = extract_boxed_answer(response)
    if pred is None:
        return 0.0
    a, b = normalize_numeric(pred), normalize_numeric(gold)
    if a is not None and b is not None:
        # Exact when both are Fractions; tolerant when a float is involved.
        if isinstance(a, Fraction) and isinstance(b, Fraction):
            return 1.0 if a == b else 0.0
        return 1.0 if abs(float(a) - float(b)) <= atol else 0.0
    # Non-numeric: compare normalized strings (whitespace/case-insensitive).
    norm = lambda x: re.sub(r"\s+", "", x).lower()
    return 1.0 if norm(pred) == norm(gold) else 0.0

# --- quick sanity checks (these all return 1.0) ---
assert math_is_correct(r"... so the answer is \boxed{1/2}.", "0.5") == 1.0
assert math_is_correct(r"first \boxed{7} then \boxed{0.50}", "1/2") == 1.0
assert math_is_correct(r"<answer>42</answer>", "42") == 1.0
assert math_is_correct(r"\boxed{\frac{3}{4}}", "0.75") == 1.0
assert math_is_correct(r"\boxed{8}", "9") == 0.0

This is the minimal version. Production math verifiers (the widely-used math-verify library, or the checker in PRM800K / the MATH dataset tooling) additionally use a symbolic engine (SymPy) to compare expressions like (x+1)^2 vs x^2+2x+1, handle sets and tuples and intervals, and canonicalize LaTeX aggressively. The principle is the same: parse, normalize to a canonical form, compare for equivalence — never raw strings. A weak verifier is a silent reward-hacking vector: if your checker marks 0.5 wrong against 1/2, the model learns to avoid decimal answers, distorting behavior for no good reason.

Do not ship the hand-rolled one. Write it once to understand the failure modes, then use math-verify (HuggingFace), which is the de-facto open-source math checker behind most 2025–2026 RLVR runs and the lighteval/TRL math recipes. Its whole API is two functions:

# pip install math-verify
from math_verify import parse, verify

# `parse` extracts the final answer (it understands \boxed{}, $...$, and plain
# expressions) and returns canonical SymPy objects; `verify(gold, pred)` is True
# iff they are symbolically/numerically equivalent. Order matters: gold first.
gold = parse(r"$\frac{1}{2}$")
pred = parse(r"...therefore the answer is $\boxed{0.5}$.")
print(verify(gold, pred))          # True — the normalization gap closes itself

def math_reward(completions: list[str], solution: list[str]) -> list[float]:
    """Drop-in replacement for `math_is_correct`, SymPy-backed. Note the
    try/except: `parse` can raise on malformed LaTeX, and an exception inside a
    reward function must never take down the trainer — swallow it and score 0."""
    out = []
    for c, s in zip(completions, solution):
        try:
            out.append(float(bool(verify(parse(s), parse(c)))))
        except Exception:
            out.append(0.0)
    return out

The important habit is the try/except: a verifier runs on adversarial input tens of thousands of times per step, and the single most common production incident is a reward function raising on one weird completion and killing an eight-hour job.

Practitioner tip: log verifier false-negatives as a first-class metric

The most insidious RLVR bug is a verifier that rejects correct answers (false negatives) because of a parsing gap. These directly poison training: the model is punished for being right, learns to mimic the verifier’s quirks, and your eval-vs-train gap silently widens. Periodically sample responses the verifier marked wrong, have a stronger model or a human spot-check them, and track the false-negative rate. A verifier with a 5% false-negative rate is a 5% mislabeling rate on your reward — far worse than the same rate in SFT data, because RL amplifies it.

Building real verifiers II: sandboxed code execution

Code is the most powerful verifiable domain — “did it pass the tests” is a strong, dense correctness signal — and the most dangerous one, because you are about to execute model-generated code thousands of times per training step. The model is an adversary by construction: RL will find any path to reward, including os.system("cat tests.py"), infinite loops to stall the trainer, while True: fork() fork-bombs, network exfiltration, or writing to the host filesystem. You must sandbox. The non-negotiable requirements:

  • No host filesystem access (read or write) beyond a scratch dir; no network; no access to the test file contents from inside the executed program.
  • Hard wall-clock timeout and memory/CPU limits (a runaway generation must not stall the whole rollout).
  • Process isolation — a separate process at minimum, ideally a container (gVisor/Firecracker microVM, or a bubblewrap/nsjail jail) for untrusted code. Production RL stacks (see Reward Engineering, Verifiers & Sandboxes) run code in ephemeral containers, often a remote execution service.

Below is a single-process, resource-limited sandbox using POSIX resource limits and a subprocess timeout. This is illustrative — for truly untrusted code in production, use a container/microVM, not just rlimit — but it shows the mechanism and is safe enough for trusted-ish synthetic problems on a locked-down box.

import subprocess, sys, tempfile, os, textwrap, resource, json

def _set_limits():
    """Called in the child via preexec_fn: cap CPU, memory, and file size."""
    resource.setrlimit(resource.RLIMIT_CPU, (5, 5))            # 5s CPU time
    mem = 512 * 1024 * 1024                                    # 512 MB
    resource.setrlimit(resource.RLIMIT_AS, (mem, mem))         # address space
    resource.setrlimit(resource.RLIMIT_FSIZE, (1 << 20, 1 << 20))  # 1 MB files
    resource.setrlimit(resource.RLIMIT_NPROC, (64, 64))       # cap fork-bombs

def run_in_sandbox(source_code: str, stdin: str = "", timeout: float = 6.0):
    """
    Execute untrusted Python in an isolated subprocess with rlimits and a
    wall-clock timeout. Returns (ok, stdout, stderr). Network is NOT blocked
    here (do that with a namespace/seccomp in production); we run with a
    minimal env and a temp CWD so there is nothing useful to touch.
    """
    with tempfile.TemporaryDirectory() as workdir:
        path = os.path.join(workdir, "prog.py")
        with open(path, "w") as f:
            f.write(source_code)
        try:
            proc = subprocess.run(
                [sys.executable, "-I", path],   # -I = isolated mode (ignore env/PYTHONPATH)
                input=stdin.encode(),
                capture_output=True,
                timeout=timeout,                # wall-clock kill
                cwd=workdir,                    # sandboxed working dir
                preexec_fn=_set_limits,         # apply rlimits in child (POSIX)
                env={"PATH": "/usr/bin", "OPENBLAS_NUM_THREADS": "1"},
            )
            return (proc.returncode == 0, proc.stdout.decode(errors="replace"),
                    proc.stderr.decode(errors="replace"))
        except subprocess.TimeoutExpired:
            return (False, "", "TIMEOUT")

def code_reward(completion: str, test_cases: list[dict],
                entry_point: str = "solve") -> float:
    """
    Verifiable code reward = fraction of hidden unit tests passed.
    `test_cases` is a list of {"input": "...", "expected": "..."} dicts.
    The model's `completion` is expected to define a function `entry_point`
    that reads from stdin and prints to stdout. We assemble a harness so the
    model's code NEVER sees the test inputs as data it can inspect.
    """
    program = extract_code_block(completion)
    if program is None:
        return 0.0
    passed = 0
    for tc in test_cases:
        # Harness runs the model code, then calls it on this test's stdin.
        harness = program + "\n\nif __name__ == '__main__':\n    " + entry_point + "()\n"
        ok, out, err = run_in_sandbox(harness, stdin=tc["input"])
        if ok and out.strip() == tc["expected"].strip():
            passed += 1
    return passed / len(test_cases)   # graded reward in [0, 1]

def extract_code_block(text: str) -> str | None:
    """Pull the last ```python ... ``` fenced block (the final solution)."""
    import re
    blocks = re.findall(r"```(?:python)?\s*\n(.*?)```", text, flags=re.DOTALL)
    return blocks[-1].strip() if blocks else None

Several design choices here are load-bearing and worth calling out for an interview:

  • Graded reward (fraction of tests passed), not all-or-nothing. This densifies a very sparse signal: a solution that passes 7/10 tests gets advantage over one that passes 0/10, even though both are “wrong.” Within a GRPO group this creates the reward variance needed for a nonzero gradient (avoiding the dead-group problem). Some recipes still use binary “all tests pass” as the final reward and reserve graded scores for shaping — both are defensible.
  • The model never sees the tests. The test inputs are fed via stdin by the harness; the model’s source is concatenated before the harness. If you instead pasted tests into the prompt, the model would learn to special-case them (hard-code outputs) — a textbook reward hack. Hidden tests are the verifiable-code analog of a held-out set.
  • Isolated interpreter mode (-I) ignores PYTHONPATH/site customizations, and a minimal env removes most ambient capabilities. Still: this stops accidents, not a determined adversary with a Python escape. For real untrusted execution use gVisor/Firecracker.
  • Timeouts are part of the reward, not just safety. A program that times out scores 0 on that test. RL will learn to avoid pathological loops because they cost reward — but it will also learn to exploit a too-generous timeout to brute-force, so set it tight.

Common pitfall: test execution is your throughput bottleneck and your security boundary

Naively running tests inline in the trainer process serializes everything and risks the whole job on one fork-bomb. In production, code execution is a separate, horizontally-scaled, sandboxed service the trainer calls asynchronously (see Reward Engineering, Verifiers & Sandboxes and The Generation–Training Loop & Rollout Engines). Budget for it: at \(G=16\) samples × thousands of prompts × multiple tests each, you may execute millions of short programs per epoch. Caching identical (code, test) pairs and capping per-test time are essential.

Building real verifiers III: formal mathematics and proof assistants

A proof assistant is the limiting case of a verifiable reward — the verifier with zero false positives. A math-equivalence checker can be fooled by a normalization gap (does “0.5” match “½“? does an unsimplified radical match its decimal?); a code sandbox can be escaped, or its hidden tests can leak and get hard-coded. A proof-assistant kernel accepting a proof is, by construction, a machine-checked guarantee that the theorem is true relative to its axioms — there is no “close enough.” This is exactly why formal mathematics is a frontier RLVR domain: it is the one place where \(V(q,o)\in\{0,1\}\) is not an approximation of correctness but is correctness.

What a proof assistant is (Lean 4). Lean 4 — alongside Isabelle, Coq/Rocq, and HOL Light — is an interactive theorem prover built on dependent type theory. Under the Curry-Howard correspondence, a proposition is a type, and a proof is a term that inhabits that type; checking a proof reduces to type-checking a term against the theorem’s type. A small, trusted kernel performs this check. Everything else — tactics, elaboration, the enormous Mathlib library — is untrusted convenience whose job is ultimately to produce a term the kernel can check. Proofs are written with tactics (rw, simp, induction, ring, omega, linarith, exact, …) that manipulate a proof state of open goals; the proof is complete when no goals remain and the kernel accepts the resulting term. The reward signal for RL is therefore binary and unforgeable: the proof script either compiles — kernel accepts, reward 1 — or it does not, reward 0. No parsing heuristics, no equivalence normalizer, no sandbox-escape surface.

-- A proposition is a type; a proof is a term the kernel type-checks.
-- Reward = 1 iff Lean's kernel accepts the term this tactic block builds.
theorem add_comm (a b : Nat) : a + b = b + a := by
  induction b with
  | zero      => simp            -- goal: a + 0 = 0 + a
  | succ n ih => simp [Nat.add_succ, Nat.succ_add, ih]  -- reduce succ case to ih

The tactic script above is untrusted search — the elaborator and simp are free to try, backtrack, and fail — but the object it ultimately emits is kernel-verified, so a prover model gains nothing from producing a plausible-looking-but-wrong script: it simply fails to compile and scores 0.

The autoformalization problem. The catch is getting into the formal world. Real problems arrive as natural language — a competition problem, a lemma from a paper. Autoformalization is the task of translating natural-language mathematics into a formal statement (the theorem’s type) that a prover then tries to prove; the reverse direction, formal back to natural language, is informalization. Here is the crucial nuance and the one residual hack in an otherwise airtight system: the proof of a formal statement is unforgeable, but the statement itself is human- or model-produced and can be wrong. A mis-formalized statement — contradictory hypotheses that make anything provable, or a claim weaker than the one intended — is a statement-level false positive: the model earns reward 1 for a valid proof of the wrong theorem. Formal RLVR therefore relocates the (now much smaller) trust surface from “is the proof correct” to “does the formal statement faithfully capture the intended problem,” which is why autoformalization quality and statement auditing — checking that a statement and its negation are not both trivially provable, or human review of statement banks — matter as much as the prover itself.

How prover models are trained. The dominant recipe is expert iteration / AlphaProof-style RL: sample many candidate proofs — often with search, such as best-first or MCTS over tactic states, or whole-proof generation guided by the compiler as an oracle — keep the ones the kernel accepts, and train on those successes (SFT on verified proofs and/or policy-gradient RL using the compile signal as a binary reward). This is the formal-math instance of the same expert-iteration loop this chapter already introduced for RLVR generally; only the checker changes, from math_is_correct or a code sandbox to the Lean compiler. Concretely: AlphaProof (DeepMind, 2024) is a Lean-based system trained with RL and expert iteration that, combined with AlphaGeometry 2 — a neuro-symbolic engine specialized to Euclidean geometry — reached silver-medal-equivalent performance at the IMO. DeepSeek-Prover (v1, v1.5, v2) scales the same idea with large autoformalized training corpora plus RL/expert-iteration over Lean. Contrast these formal specialists with informal math specialists like Minerva (a PaLM finetune that does natural-language chain-of-thought math with no formal verifier): high benchmark scores, but no correctness guarantee, so it can be confidently wrong. The formal approach trades breadth and fluency for an ironclad correctness guarantee; the informal approach makes the opposite trade.

Benchmarks. miniF2F (~488 formalized olympiad and competition problems spanning AMC, AIME, IMO, and MATH, available in Lean, Isabelle, and HOL Light) is the standard formal-proving benchmark, scored as the fraction of statements formally proved at some pass@k. PutnamBench formalizes hundreds of Putnam competition problems in Lean, Isabelle, and Coq and is substantially harder — as of 2025–2026 only a small fraction is solved, making it a frontier benchmark. ProofNet targets undergraduate-level formal statements and is used for both autoformalization and proving. FrontierMath (Epoch AI), mentioned elsewhere in this book, is extremely hard research-level mathematics but is mostly answer-checked rather than fully proof-checked — worth distinguishing as answer-verified, not proof-verified. Because search plus compiler verification makes false positives essentially impossible, these are pass@k benchmarks where k can be large: a single verified hit counts, no matter how many attempts it took to find it.

Why this sidesteps reward hacking entirely. Recall the reward-hacking section’s mental model: the code sandbox converted statistical hacking into a software security problem — escapes, test leakage. The proof kernel goes one step further. Because the kernel is small, trusted, and formally scrutinized, and the reward is simply “kernel accepts the term,” there is no parser to fool and no execution environment to escape — you cannot earn reward for a false theorem. The only residual attacks live at the boundary: autoformalization (proving a wrongly-stated or vacuous theorem) and the degenerate sorry/axiom-injection hack (leaving a proof hole, or smuggling in a false axiom). Both are defended by rejecting proofs that use sorry, unsafe escape hatches, or unaudited axioms, and by auditing the statement bank itself. Formal mathematics is where RLVR’s promise is fully realized: the reward is not a proxy for correctness, it is correctness — which is precisely why it sits at the frontier, and why any claim of “unhackable RLVR” should be evaluated against how close the domain actually sits to a proof kernel.

A complete RLVR reward function and a worked example

Let us assemble a full reward used in an R1-Zero-style run and then trace exact numbers through it. The reward is a sum of components where correctness dominates and everything else is a small, contingent guardrail.

def rlvr_reward(question: str, response: str, gold: str,
                domain: str, test_cases=None) -> dict:
    """
    Full RLVR reward, returned as a breakdown so we can log each component.
    Correctness is the real signal (weight 1.0). Format is a small shaping
    bonus that is ONLY granted if the model also attempted a parseable answer,
    so it can never be farmed independently of trying to solve the task.
    """
    # 1. Correctness (the only component we truly trust).
    if domain == "math":
        accuracy = math_is_correct(response, gold)          # {0, 1}
    elif domain == "code":
        accuracy = code_reward(response, test_cases)         # [0, 1] graded
    else:
        accuracy = 0.0

    # 2. Format shaping (tiny, and CONTINGENT on a parseable answer existing).
    has_think = "<think>" in response and "</think>" in response
    has_answer = extract_boxed_answer(response) is not None
    format_bonus = 0.1 if (has_think and has_answer) else 0.0

    # 3. Anti-hacking guard: zero out everything if the response is degenerate
    #    (e.g. empty, or repeats one token — catches a known length-hack mode).
    if _is_degenerate(response):
        return {"accuracy": 0.0, "format": 0.0, "total": 0.0}

    total = accuracy + format_bonus
    return {"accuracy": accuracy, "format": format_bonus, "total": total}

def _is_degenerate(text: str) -> bool:
    toks = text.split()
    if len(toks) < 3:
        return True
    # crude repetition check: >60% of tokens are the single most common token
    from collections import Counter
    most = Counter(toks).most_common(1)[0][1]
    return most / len(toks) > 0.6

Now the worked example. We feed this reward into GRPO (whose advantage mechanics are in GRPO, RLOO & Critic-Free RL) and trace one group.

Worked example: one GRPO group on a math prompt with the RLVR reward

Prompt: “Compute \(\int_0^1 3x^2\,dx\). Put the final answer in \boxed{}.” Gold answer: 1. We sample a group of \(G = 6\) responses and apply rlvr_reward (math domain). Suppose the outcomes are:

resp reasoning quality boxed answer accuracy format \(R_i\)
\(o_1\) correct, with <think> \boxed{1} 1.0 0.1 1.1
\(o_2\) correct, no think tags \boxed{1} 1.0 0.0 1.0
\(o_3\) wrong (forgot to evaluate) \boxed{x^3} 0.0 0.1 0.1
\(o_4\) wrong, off by constant \boxed{3} 0.0 0.1 0.1
\(o_5\) correct but as 1.0 \boxed{1.0} 1.0 0.1 1.1
\(o_6\) degenerate (repeats “the the the…”) 0.0 0.0 0.0

Note the verifier earned its keep: \(o_5\) wrote 1.0, which a naive string checker would mark wrong against gold 1 — our normalize_numeric correctly gives it accuracy 1.0. And \(o_6\)’s degeneracy guard zeroed it out.

Group statistics. Rewards \(R = \{1.1, 1.0, 0.1, 0.1, 1.1, 0.0\}\).

  • Mean: \(\bar R = (1.1+1.0+0.1+0.1+1.1+0.0)/6 = 3.4/6 \approx 0.567\).
  • Deviations \(R_i-\bar R\): \(\{+0.533, +0.433, -0.467, -0.467, +0.533, -0.567\}\).
  • Population variance: sum of squares \(= 0.284+0.188+0.218+0.218+0.284+0.321 = 1.513\); \(\sigma^2 = 1.513/6 = 0.252\); \(\sigma \approx 0.502\).

GRPO advantages \(\hat A_i = (R_i - \bar R)/(\sigma + \varepsilon)\) with \(\varepsilon = 10^{-4}\) (using std-normalized GRPO; the Dr. GRPO variant would skip the \(\div\sigma\)):

\[ \hat A_1 = \hat A_5 \approx \frac{0.533}{0.502} \approx +1.06,\quad \hat A_2 \approx +0.86,\quad \hat A_3 = \hat A_4 \approx -0.93,\quad \hat A_6 \approx \frac{-0.567}{0.502} \approx -1.13. \]

What the policy learns from this group. Every token of the two fully-correct-with-format responses (\(o_1, o_5\)) gets pushed up hardest (\(+1.06\)); the bare-correct \(o_2\) is pushed up but less (\(+0.86\)) — the model feels a gentle pull toward also producing the <think> structure, exactly the intended effect of the small format bonus. The two wrong-but-formatted answers (\(o_3, o_4\)) are pushed down (\(-0.93\)), and the degenerate \(o_6\) is pushed down hardest (\(-1.13\)). The dominant signal, by far, is correctness (\(\pm 1.0\) accuracy swamps the \(\pm 0.1\) format term), which is precisely what keeps the model honest: it cannot profit from format alone.

Sanity on magnitudes: the format bonus moved \(o_1\)’s advantage from \(+0.86\) (what it would have been at reward \(1.0\)) to \(+1.06\) — about a 23% relative nudge. Tune \(\lambda\) so this nudge is noticeable but not dominant; if you set the format bonus to, say, \(0.5\), a well-formatted wrong answer (\(R=0.5\)) would out-score a badly-formatted right one (\(R=1.0\))? No — \(1.0 > 0.5\) still — but the gap shrinks dangerously, and the model starts spending capacity on formatting instead of solving. Small format weights are not aesthetic; they are a reward-hacking defense.

From narrow RLVR to general reasoning

RLVR’s superpower — an exact reward — is also its boundary: it only works where correctness is programmatically decidable. Math, code, formal logic, constrained extraction, unit-convertible science: yes. “Write a moving poem,” “is this essay persuasive,” “is this medical advice safe”: no — there is no is_correct(). The frontier question of 2025–2026 is how far the reasoning skills grown in verifiable domains transfer, and how to extend the recipe beyond them. Several strategies are now standard.

1. Transfer: reasoning learned on math/code generalizes

The most important empirical finding is that the reasoning machinery RLVR installs is not domain-locked. A model RLVR-trained on math and code becomes better at reasoning tasks it was never RL-trained on — logical puzzles, some scientific QA, even agentic planning. The interpretation: RLVR teaches a transferable skill (“decompose, derive step by step, check your work, backtrack”) using verifiable domains merely as the gym where that skill can be cheaply graded. You train the muscle where you can measure it, and the muscle works elsewhere. This is the strongest argument for RLVR as a general post-training stage rather than a niche math trick. (How far it transfers is debated and base-model-dependent; see Reasoning, Chain-of-Thought & Test-Time Compute.)

2. Mixing verifiable and non-verifiable rewards

To get a deployable model you must handle helpfulness, safety, tone, and open-ended tasks — none verifiable. The standard solution (DeepSeek-R1’s final stage, Tülu 3) is to mix reward sources in a single RL run: a rule/verifier reward on the verifiable prompts and a learned reward model on the rest. GRPO does not care where the scalar comes from — it just needs one reward per response. You route each prompt to its appropriate scorer:

def mixed_reward(prompt, response, meta):
    """Route to a verifiable checker when possible, else a reward model."""
    if meta["type"] in ("math", "code", "format"):
        return rlvr_reward(prompt, response, meta["gold"],
                           meta["type"], meta.get("tests"))["total"]
    else:
        # Non-verifiable (chat/safety/helpfulness): fall back to the learned RM.
        return reward_model_score(prompt, response)   # the network from ch. 5.5

The risk reappears at the boundary: the learned portion can still be hacked, so you keep its weight modest, apply a KL anchor on those prompts, and monitor for sycophancy. The verifiable portion, mercifully, needs none of that.

3. Process rewards and self-verification (when outcomes aren’t enough)

An outcome reward (final answer correct?) gives no credit for a correct sub-derivation that ends in an arithmetic slip, and it can reward a right answer reached by wrong reasoning (lucky guess). Process reward models (PRMs) score each reasoning step, giving denser, better-targeted feedback — at the cost of needing step-level labels (expensive) or a learned PRM (hackable again). RLVR’s pragmatic middle ground is to make the model its own verifier: train it (still with verifiable outcome rewards) to generate a solution and a check, so self-verification is reinforced because it raises outcome accuracy. The “aha moment” is exactly this — self-verification emerging because it pays off on the outcome reward. PRMs and these blends are explored in Reasoning, Chain-of-Thought & Test-Time Compute.

4. Generative / model-based verifiers for fuzzy domains

When a domain is almost verifiable — e.g. “does this answer entail the reference” for free-form QA — a strong LLM acting as a judge/verifier (LLM-as-a-Judge) can stand in for a hard checker. This is a spectrum: pure-programmatic verifiers (unhackable, narrow) on one end, learned reward models (hackable, general) on the other, and LLM-judges in between. The RLVR philosophy — prefer the most exact verifier the domain allows — is the guiding principle: use a symbolic checker if you can, an execution sandbox if you can, an LLM-judge only when you must, and a free-form reward model last.

MOST EXACT least hackable, narrowest MOST GENERAL most hackable, broadest symbolic / exact-match math, logic exact extraction code execution unit tests LLM-as-judge entailment, style checks learned reward model helpfulness, safety, tone Prefer the leftmost verifier the task allows; fall right only as needed.
The verifier spectrum: from exact-but-narrow to general-but-hackable. Symbolic and execution-based verifiers (left) are deterministic and unhackable in the statistical sense; learned reward models (right) are flexible but subject to Goodhart drift. The RLVR principle is to occupy the leftmost position the task permits.

Reward hacking in RLVR: it’s not gone, it moved

A crucial nuance, and a favorite interview trap: people say verifiable rewards “can’t be hacked.” That is false — what’s true is that they can’t be hacked the statistical way (the way a learned RM drifts off-distribution). RLVR rewards get hacked the engineering way: the model finds bugs in your verifier or sandbox. Real, observed RLVR hacks:

  • Test leakage / hard-coding. If the test inputs leak into the model’s context (in the prompt, via a filesystem read, or because the harness is sloppy), the model hard-codes outputs: if input == "5\n3": print("8"). Defense: hidden tests, no filesystem/network in the sandbox, and randomized / held-out test inputs.
  • Verifier parsing exploits. If the answer extractor is naive, the model learns to emit strings that parse as correct without solving — e.g. dumping many \boxed{} candidates hoping one matches, or exploiting a regex. Defense: take the last boxed answer only, penalize multiple final answers, and fuzz-test your extractor against adversarial responses.
  • Sandbox escapes / resource abuse. os.system, eval of attacker-controlled strings, fork-bombs to crash the grader (so it returns a default), or sys.setrecursionlimit tricks. Defense: real isolation (container/microVM), strict rlimits, treat a crashed grader as reward 0, never as “skip.”
  • Format farming. Covered above: if format/length bonuses are too large, the model optimizes them instead of correctness. Defense: keep shaping rewards small and contingent on attempting the task.
  • Length / “thinking” inflation. Partly a genuine emergent behavior, partly a loss bias and partly the model padding to look like it is reasoning. Defense: the Dr. GRPO/token-level fixes, plus capping or mildly penalizing over-budget length.
RLVR does not remove reward hacking - it moves it Statistical hacking (learned reward model) on-distribution off-distribution true quality learned reward r_phi policy Goodhart: optimize the proxy, true quality falls defense = statistics: KL anchor, early stop, RM ensembles, stay near data reward is an approximation of quality the hacking MOVES (does not disappear) Software hacking (verifier / sandbox) V(q,o) / code sandbox fuzzer (the policy) 1. hard-code leaked test outputs -> hide + randomize tests, no FS/net 2. spam \boxed{} candidates / regex trick -> take LAST boxed only, fuzz the extractor 3. fork-bomb / crash grader for default pass -> real isolation + rlimits; crash = reward 0 4. farm oversized format / length bonus -> keep shaping small AND contingent threat-model your grader like a public API taking untrusted input
RLVR does not eliminate reward hacking — it changes what kind of hacking you have to defend against. A learned reward model can be Goodharted statistically, drifting off-distribution while looking better and better to the proxy; a programmatic verifier and sandbox instead get attacked as software, fuzzed by the policy for parser bugs, leaked tests, and sandbox escapes. The defenses move accordingly: from statistics (KL anchors, RM ensembles, early stopping) to engineering (isolation, randomized tests, extractor fuzzing, treating a crashed grader as reward zero).

The mental model: RLVR converts statistical reward-hacking into software security. Your verifier and sandbox are now an adversarial interface — the policy is a relentless fuzzer that will execute your grader millions of times looking for the cheapest path to reward. Threat-model them as you would a public API taking untrusted input. The full taxonomy is in Reward Hacking, Over-Optimization & Alignment Failures.

Interview Corner

Q: A teammate claims “RLVR can’t suffer reward hacking because the reward is ground truth.” Is that right? Where exactly does the hacking go, and how do you defend?

A: It’s half right. RLVR eliminates the statistical reward-hacking that plagues learned reward models — the kind where a policy drifts to a region where the learned reward \(r_\phi\) disagrees with true human preference (Goodhart / over-optimization). A correctness checker doesn’t drift, so you can optimize against it as hard as you like and the policy just gets more correct on the narrow metric. But the hacking moves from statistics to software: the policy becomes an adversary that fuzzes your verifier and sandbox. Observed exploits include hard-coding test outputs when tests leak, emitting many \boxed{} candidates to game a weak answer-extractor, fork-bombing or crashing the grader so it returns a default pass, and farming oversized format/length bonuses. Defenses are engineering, not statistics: hide and randomize tests; sandbox code execution with real isolation (container/microVM) plus strict CPU/memory/time rlimits, no network, no host FS; treat a crashed grader as reward 0; parse answers robustly (take the last boxed answer, penalize multiple finals, fuzz-test the extractor); and keep shaping rewards small and contingent on attempting the task. A second, subtler failure is the verifier false-negative: a buggy checker that marks correct answers wrong, which actively poisons training — so log and audit the verifier’s false-negative rate as a first-class metric. Net: RLVR doesn’t remove reward hacking, it converts it into application security.

Interview Corner

Q: Why does pure correctness reward (R1-Zero) cause long chain-of-thought to emerge, and what’s the one precondition without which it fails?

A: Because longer reasoning correlates with reaching the right answer on hard problems, and correctness is the only thing rewarded. Under a group-relative optimizer like GRPO, the trajectories that solved the problem get positive advantage; empirically those winners are the ones that spent extra tokens checking intermediate steps, trying alternative approaches, and self-verifying. The gradient therefore up-weights “spend more compute reasoning,” and over thousands of steps this compounds into long CoT, self-verification, backtracking, and the “aha moment” — none of it demonstrated, all of it discovered as the cheapest path to higher correctness. The non-negotiable precondition is that the base model must sometimes succeed: the group of \(G\) samples needs both successes and failures to produce a nonzero advantage. If pass@G \(\approx 0\) (problems too hard) or \(\approx 1\) (too easy), the group is “dead” — zero advantage, no gradient, no learning. That’s why R1-Zero needs a strong base model and a difficulty-calibrated (curriculum) prompt set, and why the same recipe on a weak base with very hard problems produces nothing.

Putting it together: the reasoning recipe

We can now state the full RLVR reasoning recipe as a checklist an engineer would actually follow. The optimizer details live in GRPO, RLOO & Critic-Free RL; this is the data-and-reward recipe that wraps it.

  1. Assemble a prompt set whose answers are checkable — then decontaminate it. Real open sets to start from: GSM8K and MATH for warm-up, and the RL-grade math pools that the 2025 reasoning wave produced — NuminaMath, Big-Math-RL-Verified, DeepScaleR-Preview, DAPO-Math-17k (all published as problem + verified short answer, exactly the shape RLVR wants). For code: CodeContests, TACO, and KodCode, which ship test suites. Keep only items whose gold answer your verifier can parse, and n-gram-decontaminate against the evals you will report (AIME, MATH-500, LiveCodeBench, GPQA) — RLVR trains directly on the answer, so contamination is not a subtle leak, it is memorization with a reward attached.
  2. Write the verifier before the training loop, and test it like a parser. Unit-test it on the dataset’s own gold answers (a verifier that cannot verify the gold string against itself is broken), fuzz it with adversarial completions, and measure the false-negative rate on a sample the checker marked wrong. Prefer math-verify over anything you wrote.
  3. Difficulty-calibrate the prompts against your base model. Sample \(G\) completions per prompt with the untrained policy, estimate pass@1, and drop everything at \(p\approx 0\) and \(p\approx 1\) — those groups are dead (Exercise 3 quantifies the waste). Keep the ~20–60% band and hold the harder shards back as curriculum; see RL Data, Curriculum & Replay Management.
  4. Pick the entry point: zero, or cold-start. R1-Zero style (straight from base) is the scientifically interesting setting but produces messy, language-mixing output; a few thousand long-CoT SFT examples first (“cold start”) stabilizes RL and gives a readable model, which is what R1 proper did.
  5. Define the reward: accuracy dominant, everything else tiny and contingent. Accuracy weight 1.0, format bonus ≲0.1 and only granted alongside a parseable answer attempt, degeneracy/repetition guard zeroing the total, timeout and crash both scoring 0 rather than “skip”.
  6. Run critic-free RL with the 2025 fixes on. GRPO with a token-level loss and no std-normalization (Dr. GRPO), clip-higher and dynamic sampling (DAPO), overlong filtering, and a small or zero KL coefficient — in verifiable domains many teams drop the KL anchor entirely because the checker, not the reference model, is what keeps the policy honest.
  7. Monitor five curves, not one. Held-out accuracy (not reward), mean completion length, policy entropy, format/parse-failure rate, and the fraction of dead groups. Reward rising while held-out accuracy is flat is the signature of a hack; entropy collapsing is the signature of a run that has stopped exploring.
  8. Then mix in the non-verifiable rewards (helpfulness, safety, tone) as in §”Mixing verifiable and non-verifiable rewards” — the verifiable stage buys you reasoning, the mixed stage buys you a deployable assistant.
THE RLVR REASONING RECIPE 0 BASE Start from a strong base model (latent math/code ability is the fuel). Optional small cold-start SFT on a few thousand clean long-CoT examples to fix readability and stabilize early RL. 1 DATA Assemble prompts with KNOWN, CHECKABLE answers: math with gold final answers -> symbolic/numeric verifier code with hidden unit tests -> sandboxed execution constraint/format tasks -> programmatic checks Calibrate difficulty so pass@G is mixed (~20-80%), not 0 or 1. 2 REWARD total = accuracy (dominant, exact: the verifier) + small format bonus (contingent on a parseable attempt) + small language/length guard (optional) Robust verifier (normalize, do not string-match); airtight sandbox (isolation + rlimits + no net/FS). 3 RL GRPO / RLOO with group-relative advantage (no reward model, no critic). Token-level loss + clip-higher (DAPO); drop std-normalization (Dr. GRPO). KL to ref often 0 for R1-Zero, >0 when preserving an SFT persona. 4 MONITOR Track: mean accuracy reward (NOT total), fraction of non-dead groups, verifier false-negative rate, response length, entropy, pass@1 vs pass@k on held-out eval. 5 BANK & BROADEN Rejection-sampling SFT on the RL model's best outputs to lock in gains; add non-verifiable (chat/safety) data; then a final mixed-reward RL pass (verifier + reward model) for a deployable general model. Stages 0-4 use pure verifiable rewards; Stage 5 reintroduces non-verifiable data and a learned RM.
The RLVR reasoning recipe: six stages from base model to deployable reasoner. Stages 0-4 form the pure RLVR core — verifiable rewards only, critic-free RL, and disciplined monitoring; Stage 5 banks the gains via rejection-sampling SFT and broadens to general tasks by blending in non-verifiable data and a learned reward model.

The cheap baseline you should run first: expert iteration

Before reaching for a policy gradient, run the same verifier through expert iteration (also called rejection-sampling fine-tuning, or STaR): sample \(k\) completions per prompt, keep the ones the verifier accepts, and plain-SFT on them. Repeat. It uses the identical data and identical checker, needs no advantage estimator, no KL term, no reference model, and no rollout/trainer weight-sync machinery — and it captures a surprising fraction of the gain, which is why it is the standard baseline against which GRPO must justify its complexity, and why DeepSeek-R1’s third stage is exactly this.

def expert_iteration(prompts, verifier, sample_fn, sft_fn,
                     rounds: int = 3, k: int = 8, keep_per_prompt: int = 2):
    """STaR / rejection-sampling fine-tuning: RLVR without a policy gradient.

    sample_fn(prompt, n) -> list[str]      : k completions from the current policy
    verifier(prompt, completion) -> float  : the SAME checker you'd use for RL
    sft_fn(examples) -> None               : one epoch of cross-entropy on kept traces

    Two details do the real work: (a) de-duplicate kept traces, or an easy prompt
    with 8 identical correct solutions drowns out 8 hard prompts with 1 each;
    (b) cap per-prompt keeps, which is the same difficulty-balancing job that
    group-relative advantages do for free in GRPO.
    """
    for _ in range(rounds):
        batch = []
        for p in prompts:
            correct = [o for o in sample_fn(p, k) if verifier(p, o) == 1.0]
            seen, uniq = set(), []
            for o in correct:                      # dedup on normalized text
                key = " ".join(o.split())
                if key not in seen:
                    seen.add(key)
                    uniq.append(o)
            batch += [{"prompt": p, "completion": o} for o in uniq[:keep_per_prompt]]
        sft_fn(batch)                              # policy improves; next round re-samples
    return batch                                   # last round's distilled dataset

The conceptual difference from GRPO is worth stating precisely, because it is a favorite exam question: expert iteration only uses the positive examples. It raises the probability of trajectories that worked but never lowers the probability of the failure modes, so it cannot suppress a confidently-wrong reasoning pattern, and it saturates once the policy’s sampling diversity collapses (it can only ever learn from what it can already occasionally produce). GRPO’s negative advantages are exactly the missing half of the signal. Expert iteration is the right first move at small scale and small budget — Post-Training: SFT, DPO, and Narrow RLVR (GRPO) That Works at 100M uses this ordering to get a ~100M model doing verified arithmetic reasoning before any policy gradient is attempted.

Wiring a verifier into a real trainer

Nothing above requires you to write a trainer. Every production RLVR stack exposes the same extension point — “give me a function from completions to floats” — so the verifier you built in this chapter is the only bespoke code you own. In TRL, that extension point is reward_funcs:

# pip install trl
from trl import GRPOTrainer, GRPOConfig

def accuracy_reward(completions, solution, **kwargs):
    """TRL's reward-function contract: it receives the batch of `completions`
    plus every other dataset column as a keyword argument (here `solution`),
    and returns ONE float per completion. Our verifier plugs in unchanged.
    (With a conversational dataset, `completions` arrives as message dicts
    rather than strings — index into the last message's "content".)"""
    return [math_is_correct(c, s) for c, s in zip(completions, solution)]

def format_reward(completions, **kwargs):
    """A second, small-weight function. TRL SUMS the list of reward_funcs, so
    shaping terms live in their own function at their own scale — never buried
    inside the accuracy checker where you cannot ablate them."""
    return [0.1 if ("<think>" in c and "</think>" in c) else 0.0 for c in completions]

trainer = GRPOTrainer(
    model="Qwen/Qwen2.5-1.5B",                  # a BASE model, R1-Zero style
    reward_funcs=[accuracy_reward, format_reward],
    train_dataset=math_ds,                      # columns: "prompt", "solution"
    args=GRPOConfig(
        num_generations=8,                      # the group size G
        max_completion_length=1024,             # long CoT needs room to grow
        temperature=1.0,                        # do NOT sample greedily: no variance,
                                                # no advantage, no gradient
        beta=0.0,                               # KL off — common in RLVR runs
        use_vllm=True,                          # vLLM-backed rollout engine
    ),
)
trainer.train()

veRL takes the same idea as a config-pointed Python file: you supply a module with a scoring function (roughly compute_score(data_source, solution_str, ground_truth, extra_info)) and veRL’s reward manager calls it in a worker pool, which is what you want once execution-based verification is the bottleneck — see veRL: HybridFlow & The Single-Controller Architecture and OpenRLHF, NeMo-Aligner & Ray-Based Systems. Above both sits the verifiers library, which packages RLVR environments (prompt set + parser + rubric of reward functions, including multi-turn/tool-using ones) behind a common interface that TRL and veRL can both consume — the emerging standard unit of exchange for RLVR tasks, the way a datasets dataset is for pretraining data. Exact keyword names drift between releases of all three; the contract — batch in, list of floats out — does not.

The deepest takeaway is a shift in worldview. For a decade, the bottleneck of supervised and preference learning was labeled data: you needed humans to demonstrate or judge. RLVR moves the bottleneck to problems with checkable answers — which we can generate, mine, and synthesize far more cheaply than we can collect human judgments, and which give an exact signal instead of a noisy one. That is why a one-line idea — “reward = did the checker pass” — reorganized the entire post-training stack in two years.

Key Takeaways

  • RLVR replaces the learned reward model with a program. For tasks with checkable answers (math equivalence, code unit-tests, exact/constraint match) the reward is \(V(q,o)\in\{0,1\}\) computed by a deterministic verifier — no preference data, no reward network, no critic. The optimizer is critic-free RL (GRPO/RLOO); RLVR only changes where the reward comes from.
  • The big win is exactness, not just cost. A correctness checker is ground truth for “got it right,” so the policy can be optimized against it arbitrarily hard without statistical over-optimization. The bottleneck moves from “human labels” to “problems with checkable answers,” which are cheap to generate.
  • R1-Zero phenomenon: pure correctness reward on a base model spontaneously grows long chain-of-thought, self-verification, backtracking, and the “aha moment” — no reasoning demonstrations. Mechanism: longer reasoning correlates with correctness, so the group-relative gradient up-weights “spend more compute,” and it compounds.
  • The precondition is a base model that sometimes succeeds. Emergence needs mixed-outcome groups (pass@G neither 0 nor 1); too-hard or too-easy prompts give zero advantage (“dead groups”) and no learning. Calibrate difficulty / use curriculum.
  • Verifiers must normalize, not string-match. A real math checker extracts the final answer and compares with numeric/symbolic equivalence (\(\frac12 = 0.5 = 0.50\)). A weak verifier’s false negatives poison training — audit them as a first-class metric.
  • Code verifiers require true sandboxing. Model-generated code is adversarial: isolate it (container/microVM), apply strict CPU/memory/time rlimits, block network and host filesystem, hide the tests, and treat a crashed grader as reward 0. Use graded (fraction-of-tests) reward to densify the signal.
  • Reward hacking isn’t eliminated — it moves from statistics to software. The policy fuzzes your verifier/sandbox: test hard-coding, parser exploits, sandbox escapes, format/length farming. Threat-model your grader like a public API taking untrusted input.
  • From narrow to general: reasoning learned on verifiable math/code transfers to untrained domains; mix verifier + reward-model rewards in one run for deployable models; prefer the most exact verifier a task allows (symbolic > execution > LLM-judge > learned RM).
  • In practice you write the verifier, not the trainer. Use math-verify for math equivalence and a sandbox service for code, then hand the checker to TRL (reward_funcs: batch in, list of floats out), veRL’s reward manager, or a verifiers environment. And run expert iteration (sample \(k\), keep the verified-correct, SFT, repeat) as the baseline first — same data, same checker, none of the RL machinery; GRPO’s extra value is the negative advantages that expert iteration structurally cannot provide.

State of the Art & Resources (2026)

RLVR is now the dominant post-training paradigm for reasoning: every frontier reasoning model (OpenAI’s o-series and GPT-5, DeepSeek-R1, Qwen3’s thinking mode) uses verifiable-reward RL, and open-source tooling (verl, OpenRLHF, TRL) makes the full recipe reproducible at scale. Active research in 2025–2026 focuses on whether RLVR expands the base model’s reasoning frontier or primarily elicits latent capability, on unbiased group-relative objectives, and on extending verifiable rewards to new domains.

Foundational work

Recent advances (2023–2026)

Open-source & tools

  • volcengine/verl — flexible, production-ready RL training library (GRPO, PPO, DAPO, DrGRPO, RLOO, PRIME); scales to 671B models; used by DAPO and dozens of derivative reasoning-model projects.
  • OpenRLHF/OpenRLHF — Ray + vLLM distributed RLHF/RLVR framework; supports PPO, GRPO, REINFORCE++, RLOO; used by HKUST to reproduce DeepSeek-R1-Zero on small models.
  • huggingface/trl — HuggingFace’s RL library with a first-class GRPOTrainer; the lowest-friction entry point for RLVR experiments on any HF-compatible model.
  • BytedTsinghua-SIA/DAPO — fully open-sourced DAPO system: algorithm, training code (built on veRL), DAPO-Math-17k dataset, and reproducible AIME 2024 scripts.
  • huggingface/Math-Verify — the de-facto open-source math answer checker: parse + verify, SymPy-backed LaTeX/expression equivalence. Use it instead of hand-rolled string matching; it is what the HF reasoning recipes and lighteval score with.
  • willccbb/verifiers — RLVR environments (prompt set + parser + rubric of reward functions, incl. multi-turn and tool-using) behind one interface that TRL and veRL can consume; the emerging unit of exchange for RLVR tasks.

Go deeper

Further reading

  • DeepSeek-AI, DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning (2025) — R1-Zero, the emergent long-CoT / “aha moment” phenomenon, and the full multi-stage reasoning recipe.
  • Shao, Wang, Zhu, et al., DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models (2024) — introduces GRPO and rule-based math rewards.
  • Lambert, Morrison, et al. (Allen Institute for AI), Tülu 3: Pushing Frontiers in Open Language Model Post-Training (2024) — coins and operationalizes “RL with Verifiable Rewards (RLVR)” across math, code, and constraint-following.
  • Zelikman, Wu, Mu, Goodman, STaR: Bootstrapping Reasoning With Reasoning (2022) — the self-taught-reasoner precursor: keep rationales that lead to correct answers.
  • Hendrycks, Burns, et al., Measuring Mathematical Problem Solving With the MATH Dataset (2021) — the MATH benchmark and answer-checking tooling that underpins math verifiers.
  • Chen, Tworek, et al. (OpenAI), Evaluating Large Language Models Trained on Code (HumanEval, 2021) — unit-test-based code evaluation, the model for execution rewards.
  • Lightman, Kosaraju, et al. (OpenAI), Let’s Verify Step by Step (2023) — process reward models and step-level verification, the contrast to outcome-only RLVR.
  • Yue, et al., Does Reinforcement Learning Really Incentivize Reasoning Capacity Beyond the Base Model? (2025) — the pass@k critique arguing RLVR mainly elicits latent base-model ability.
  • The math-verify library and veRL / TRL repositories — production verifiers and RLVR training loops; see TRL: HuggingFace’s RL Library and Reward Engineering, Verifiers & Sandboxes.

Exercises

1. (Conceptual — why not string equality.) The chapter insists a math verifier must “parse, normalize to a canonical form, compare for equivalence — never raw strings.” Suppose you shipped a lazy verifier that used exact string equality between the extracted \boxed{} content and the gold answer. For the gold answer 1/2, name three correct model outputs this checker would mark wrong (accuracy 0), and then explain the concrete behavioral distortion this false-negative pattern would train into the policy. Why is a 5% false-negative rate in a verifier described as “far worse than the same rate in SFT data”?

Solution

A string-equality checker against gold 1/2 would reject, among others:

  • \boxed{0.5} — the decimal form (different characters, same value).
  • \boxed{0.50} — trailing-zero decimal.
  • \boxed{\frac{1}{2}} — the LaTeX \frac form the model is often asked to produce.
  • (also \boxed{2/4}, \boxed{ 1/2 } with spaces, etc.)

Each of these is mathematically correct yet scored 0. Because RLVR’s only signal is the reward, the optimizer does not learn “these are equivalent”; it learns “outputs that look like 1/2 earn reward and outputs that look like 0.5 do not.” The behavioral distortion is that the policy is pushed to avoid decimal and \frac forms and mimic the verifier’s exact surface quirks, distorting its answer formatting for no mathematical reason — a reward hack driven by a buggy checker rather than by the model finding a real exploit.

A 5% false-negative rate is worse than 5% mislabeled SFT data because RL amplifies the reward signal directly: those 5% are examples where the model did exactly the right thing and is punished for it, so the gradient actively pushes probability mass away from correct behavior. In SFT the model merely fails to learn from 5% of examples; in RLVR the model is trained against the truth on those 5%, and the mislabeling compounds over thousands of steps while your eval-vs-train gap silently widens (the chapter’s “log verifier false-negatives as a first-class metric” tip).

2. (Quantitative — GRPO advantages and the dead group.) You run one R1-Zero-style GRPO step on a math prompt (accuracy reward only, no format term). A group of \(G=5\) samples yields two correct and three wrong answers, so \(R = \{1.0,\,1.0,\,0.0,\,0.0,\,0.0\}\). Using std-normalized GRPO advantages \(\hat A_i = (R_i-\bar R)/(\sigma+\varepsilon)\) with population std \(\sigma\) and \(\varepsilon = 10^{-4}\): (a) compute \(\bar R\), \(\sigma\), and the advantage for a correct and for a wrong sample; (b) now suppose the same prompt had produced five wrong answers instead, \(R=\{0,0,0,0,0\}\) — compute all advantages and state what the policy learns from this second group and why.

Solution

(a) Mean: \(\bar R = (1.0+1.0+0+0+0)/5 = 2.0/5 = 0.4\).

Deviations \(R_i-\bar R\): correct \(\to +0.6\), wrong \(\to -0.4\).

Population variance: \(\sigma^2 = \frac{2(0.6)^2 + 3(-0.4)^2}{5} = \frac{2(0.36)+3(0.16)}{5} = \frac{0.72+0.48}{5} = \frac{1.20}{5} = 0.24\).

So \(\sigma = \sqrt{0.24} \approx 0.4899\), and denominator \(\sigma+\varepsilon \approx 0.4900\).

\[ \hat A_{\text{correct}} = \frac{+0.6}{0.4900} \approx +1.22, \qquad \hat A_{\text{wrong}} = \frac{-0.4}{0.4900} \approx -0.82. \]

The two correct trajectories are pushed up (\(+1.22\)), the three wrong ones down (\(-0.82\)); the group is “alive” because it contains both outcomes.

(b) With \(R=\{0,0,0,0,0\}\): \(\bar R = 0\), every deviation \(R_i-\bar R = 0\), so \(\hat A_i = 0/(0+10^{-4}) = 0\) for all five. The policy learns nothing from this group — every advantage is zero, so the gradient contribution is zero. This is the dead-group problem: with a group of identical (here, uniformly failing) outcomes there is no reward variance, hence no learning signal. It is exactly why R1-Zero needs a base model that sometimes succeeds and a difficulty-calibrated / curriculum prompt set: the group must mix successes and failures for correctness pressure to produce any gradient.

3. (Quantitative — the difficulty sweet spot.) With a binary accuracy reward, a GRPO group is “dead” (all advantages zero) exactly when all \(G\) samples get the same outcome. Model each of the \(G\) samples as an independent Bernoulli success with per-attempt probability \(p\) (the base model’s pass@1 on that prompt). (a) Write the probability that a group of size \(G\) is dead. (b) Evaluate it for \(G=8\) at \(p=0.1\), \(p=0.5\), and \(p=0.9\). © Explain how this justifies the chapter’s curriculum advice of keeping prompts the model solves roughly 20–60% of the time.

Solution

(a) A group is dead iff all \(G\) samples succeed or all \(G\) fail: $$ P_{\text{dead}}(p,G) = p^{G} + (1-p)^{G}. $$

(b) For \(G=8\):

  • \(p=0.1\): \(0.1^{8} + 0.9^{8} \approx 10^{-8} + 0.4305 \approx \mathbf{0.4305}\).
  • \(p=0.5\): \(0.5^{8} + 0.5^{8} = 2\cdot 0.00390625 = \mathbf{0.0078}\).
  • \(p=0.9\): \(0.9^{8} + 0.1^{8} \approx 0.4305 + 10^{-8} \approx \mathbf{0.4305}\) (symmetric with \(p=0.1\)).

© The dead-group probability is minimized when \(p\) is near \(0.5\) and blows up toward the extremes: at \(p=0.1\) or \(p=0.9\) roughly 43% of groups yield no gradient, but at \(p=0.5\) only about 0.8% do. Prompts that are almost always failed (\(p\to 0\)) or almost always solved (\(p\to 1\)) waste compute on dead groups. Keeping difficulty in the ~20–60% band (where \(P_{\text{dead}}\) stays small) maximizes the fraction of groups with mixed outcomes and hence usable learning signal — and as the policy improves and a prompt’s \(p\) drifts toward 1, curriculum ramps in harder prompts to keep it in the productive band.

4. (Implementation — defend the answer extractor against \boxed{} spam.) The chapter lists “dumping many \boxed{} candidates hoping one matches” as a real verifier parsing exploit, with the defense “take the last boxed answer only, penalize multiple final answers.” extract_boxed_answer already takes the last box, but that still lets a model hedge across several distinct guesses. Implement extract_all_boxed(text) (returns every \boxed{} payload, with brace matching) and a guarded reward math_is_correct_guarded(response, gold) that returns 0.0 whenever the response contains more than one distinct final answer (after numeric normalization), and otherwise defers to math_is_correct. Note one false-negative risk your guard introduces.

Solution
import re
from fractions import Fraction

def extract_all_boxed(text: str) -> list[str]:
    r"""Return the payload of EVERY \boxed{...}, with proper brace matching."""
    out, start = [], 0
    while True:
        idx = text.find(r"\boxed", start)
        if idx == -1:
            break
        i = text.find("{", idx)
        if i == -1:
            break
        depth, j, val = 0, i, None
        while j < len(text):
            if text[j] == "{":
                depth += 1
            elif text[j] == "}":
                depth -= 1
                if depth == 0:
                    val = text[i + 1 : j].strip()
                    break
            j += 1
        if val is None:            # unbalanced braces: stop
            break
        out.append(val)
        start = j + 1
    return out

def _canon(v: str):
    """Canonical key for distinctness: numeric value if parseable, else norm string."""
    n = normalize_numeric(v)          # from the chapter's math verifier
    if n is not None:
        return ("num", float(n)) if isinstance(n, float) else ("num", n)
    return ("str", re.sub(r"\s+", "", v).lower())

def math_is_correct_guarded(response: str, gold: str) -> float:
    """Zero the reward if the model hedged across multiple DISTINCT final answers."""
    boxed = extract_all_boxed(response)
    if len({_canon(v) for v in boxed}) > 1:
        return 0.0                    # boxed-spam / hedging hack -> no reward
    return math_is_correct(response, gold)

# repeated identical answers are fine; distinct hedges are punished
assert math_is_correct_guarded(r"\boxed{1/2} ... \boxed{0.5}", "0.5") == 1.0   # same value
assert math_is_correct_guarded(r"\boxed{1} \boxed{2} \boxed{3}", "3") == 0.0   # hedging

_canon reuses normalize_numeric, so \boxed{1/2} and \boxed{0.5} collapse to the same Fraction(1, 2) and count as one answer (no penalty for restating), while genuinely different guesses like \boxed{1}, \boxed{2}, \boxed{3} form a set of size 3 and trip the guard.

False-negative risk: models legitimately write intermediate \boxed{} expressions mid-derivation (the reason extract_boxed_answer takes the last one). A model that boxes a wrong intermediate result and then boxes the correct final answer now gets zeroed even though its final answer is right — a verifier false-negative that poisons training. A softer, safer version only triggers when the number of distinct boxes is large (e.g. > 3), or restricts the distinctness check to boxes appearing after the last </think> / in the answer region, trading a little exploit coverage for far fewer false negatives.

5. (Implementation — cache the sandbox to survive the throughput bottleneck.) The chapter warns that with \(G=16\) samples \(\times\) thousands of prompts \(\times\) multiple tests you may execute millions of short programs per epoch, and that “caching identical (code, test) pairs” is essential. Wrap run_in_sandbox with a memoizing run_in_sandbox_cached keyed on (source_code, stdin), so code_reward never re-runs an identical execution. State the key correctness assumption the cache relies on and one case where it breaks.

Solution
_sandbox_cache: dict[tuple[str, str], tuple] = {}

def run_in_sandbox_cached(source_code: str, stdin: str = "", timeout: float = 6.0):
    """Memoized run_in_sandbox: identical (code, stdin) executes at most once."""
    key = (source_code, stdin)
    hit = _sandbox_cache.get(key)
    if hit is not None:
        return hit
    result = run_in_sandbox(source_code, stdin, timeout)   # (ok, stdout, stderr)
    _sandbox_cache[key] = result
    return result

Then code_reward calls run_in_sandbox_cached(harness, stdin=tc["input"]) in place of run_in_sandbox. Within a GRPO group many of the \(G\) samples produce byte-identical programs, and across epochs the same prompt is revisited — so the same (harness, stdin) pair recurs constantly and the cache turns millions of executions into a small number of unique ones.

Key assumption: execution is a pure function of (source_code, stdin) — same code + same input always yields the same (ok, stdout, stderr). This holds for the deterministic algorithmic problems RLVR typically uses.

Where it breaks: any nondeterministic program — one that reads the clock, uses an unseeded RNG, depends on wall-time, hashing/set-iteration order, or (in principle) network/environment state. For such code a cached result may not match a fresh run, so caching is only sound when the harness pins determinism (seed RNGs, no network — which the sandbox already blocks, no time-dependence). A secondary caveat: the cache is unbounded here; a production version needs an LRU/size cap so it does not grow without limit over an epoch.

6. (Conceptual — is formal-math RLVR truly unhackable?) A colleague argues: “A Lean proof kernel has zero false positives, so a Lean-based RLVR run is completely unhackable — unlike the math checker (normalization gaps) or the code sandbox (escapes, test leakage).” Using §”Building real verifiers III,” explain what is right about this and identify the residual trust surface that keeps even formal RLVR from being fully airtight. Name the two concrete exploits the chapter lists at that boundary and the defenses.

Solution

What’s right. The kernel really does eliminate the failure modes of the other verifiers. Under Curry–Howard a proof is a term and checking it is type-checking against the theorem’s type, done by a small, trusted, heavily-scrutinized kernel. There is no answer-normalizer to fool (so no 0.5-vs-1/2 gap) and no execution environment to escape (so no sandbox break or test-leak hard-coding). A prover model gains nothing from a plausible-but-wrong tactic script: it simply fails to compile and scores 0. For the question “is this proof valid relative to these axioms,” \(V(q,o)\in\{0,1\}\) is not a proxy for correctness — it is correctness.

The residual trust surface. The proof is unforgeable, but the statement being proved is human- or model-produced. The trust surface relocates from “is the proof correct” to “does the formal statement faithfully capture the intended problem.” This is the autoformalization boundary, and it is why the chapter calls formal RLVR much smaller trust surface, not zero.

The two concrete exploits and defenses:

  1. Statement-level false positive (mis-formalization). A statement with contradictory hypotheses makes anything provable, and a statement weaker than intended is trivially satisfied — the model earns reward 1 for a valid proof of the wrong theorem. Defense: audit the statement bank — e.g. check that a statement and its negation are not both trivially provable (a signature of vacuous/contradictory hypotheses), and use human review of statement banks; invest in autoformalization quality.
  2. sorry / axiom-injection. The proof leaves a hole (sorry) or smuggles in an unaudited/false axiom, so the kernel “accepts” a term that assumes what it was supposed to prove. Defense: reject any proof that uses sorry, unsafe escape hatches, or unaudited axioms.

So the colleague is mostly right — formal math is where RLVR’s promise is most fully realized — but “unhackable” should be evaluated by how close the domain sits to the proof kernel: the kernel itself is airtight, while the statement-generation boundary around it still needs auditing.