A frame to help you reason through the assignment — concepts, diagrams, and the API map. It deliberately does not contain the answers or filled-in activity code. Instead it gives you the questions to ask yourself and the method to get there. The work — and the learning — is in running the cells, reading the completions your verifier rejected, and writing your own conclusions.
Notebook:
01_RLVR_Verifiable_Rewards.ipynb(34 cells) · README:16_RLVR/README.mdNo corpus and no GPU: the policy is an API model (gpt-4.1-nano) sampled at temperature 1.0.
| You want to… | Reach for | One-liner |
|---|---|---|
| Sample one completion from the policy | simple_complete |
simple_complete(prompt, system="...", temperature=1.0) |
| Carry ground truth alongside a prompt | Problem |
Problem("What is 12 * 13?", "156") → .question, .answer |
| Pull the final answer out of a completion | extract_number |
first \boxed{...}, else the last number in the text, else "" |
Make "80" match "80.0" |
MathRewardFunction._normalize |
float(value) when it parses, else value.strip() |
| Turn a check into a scalar reward | MathRewardFunction.compute |
reward_fn.compute(response, problem.answer) → +1.0 / -0.1 |
| Sample a group and verify every member | sample_and_verify |
sample_and_verify(problem, n_samples=4) → list[Sample] |
| Flag a suspicious verified-correct sample | looks_like_hack |
< 20 words or < 2 numbers outside the box → True |
| Log every verifier decision | audit_record |
appends {**asdict(sample), "suspected_hack": …} to artifacts/verifier.jsonl |
| Score code by tests instead of exact match | CodeVerifier.verify |
verifier.verify(code, test_cases) → passed / len(tests) |
| Execute one candidate program | CodeVerifier._run |
subprocess.run([sys.executable, "-c", code], input=…, timeout=5) |
| Clean markdown fences off generated code | strip_fences |
strip_fences(simple_complete(CODING_TASK)) |
| Turn audited groups into training data | build_preferences |
build_preferences(groups, records) → {prompt, chosen, rejected} |
The one sentence that anchors everything: the verifier's quality is the ceiling on the policy's quality — once you train against a checker, the checker is the objective. Every question and the activity reward reasoning about how a checker can be fooled, not about how to get a high score.
One prompt fans out into a group of completions; a deterministic program — not a human, not a model — checks each against known ground truth and emits a scalar; audited groups become training data; the policy updates; repeat.
flowchart TD
P["Problem<br/>question + ground-truth answer"] --> S["Sample N completions<br/>simple_complete · temperature 1.0"]
S --> G["Group of N Samples"]
GT[("Ground truth<br/>Problem.answer · code_tests")] --> V
G --> V{{"VERIFIER — a deterministic program<br/>extract_number → _normalize → compare<br/>CodeVerifier.verify → passed/len(tests)"}}
V --> R["Scalar reward<br/>+1.0 / -0.1 (binary) · 0.0–1.0 (fractional)"]
R --> H["looks_like_hack → audit_record<br/>artifacts/verifier.jsonl (append-only)"]
H --> D["build_preferences<br/>chosen = correct AND unflagged<br/>rejected = incorrect"]
D --> U["Policy update<br/>DPO pairs · GRPO group rewards"]
U -.->|next iteration| S
RM["RLHF instead puts a <b>learned neural reward model</b> here —<br/>trained on human preference comparisons.<br/>Approximate · expensive · non-reproducible · over-optimizable."]
RM -.->|"RLVR deletes this box"| V
ASCII fallback:
ground truth (Problem.answer / code_tests)
|
prompt --> sample N --> group --> [ VERIFIER: deterministic program ] --> scalar reward
^ extract_number | CodeVerifier.verify |
| v
| looks_like_hack --> audit_record --> verifier.jsonl
| |
+------- policy update <-- build_preferences <--------------------+
(DPO pairs / GRPO group rewards)
RLHF, for contrast:
prompt --> completions --> [ LEARNED reward model: a neural net fitted to
human preference labels ] --> scalar reward --> policy update
Why this shape? The only box that changes between RLVR and RLHF is the one that produces the scalar — and it changes from a model you trained to a program you wrote. Everything downstream (groups, advantages, preference pairs, the policy update) is identical. That single swap is what makes the signal cheap, objective and reproducible — and what makes it a target: a program has blind spots you can read, version and harden, which is why hack detection (§8) and the audit trail sit inside the loop rather than beside it.
uv sync # from the 16_RLVR folder, then select the uv-created Python/Jupyter environmentNo GPU (unlike Session 15). One key: OPENAI_API_KEY, entered via getpass in Cell 4 and kept in
memory for the session only. The sampling loop makes a few dozen small-model calls (roughly cents).
| Role | Where it lives | What it does |
|---|---|---|
| Policy | simple_complete / MODEL = "gpt-4.1-nano" |
the model being sampled; small on purpose at temp 1.0 so some answers are wrong |
| Verifiable domain | Problem, problems |
5 math word problems, each carrying a ground-truth answer string |
| Math verifier | extract_number + MathRewardFunction |
binary reward: exact match after numeric normalization |
| Code verifier | CodeVerifier + code_tests |
fractional reward: fraction of unit tests passed |
| Hack detector | looks_like_hack |
flags verified-correct samples showing no visible work |
| Audit trail | AUDIT_LOG → artifacts/verifier.jsonl |
append-only record of every verifier decision |
| Training data | build_preferences → artifacts/preferences.jsonl |
{prompt, chosen, rejected} for DPO-style trainers |
16_RLVR/.gitignore ignores artifacts/, so verifier.jsonl and preferences.jsonl stay out of the
repo. The README's step 4 is the reason: keep the cell outputs (verified-correct rates, flagged
counts, a preference-pair example) — those are the durable evidence.
A reward is verifiable when a deterministic program — not a human and not a model — decides whether a completion is correct, because ground truth is known and checking it is mechanical. The notebook states the contrast directly (Cell 0):
"Unlike RLHF, there is no learned reward model and no human labeler in the loop. The reward comes from a deterministic program." … and Cell 17: "cheap, objective, reproducible."
Two such programs exist in this notebook:
| Verifier | Ground truth | Verdict |
|---|---|---|
MathRewardFunction.compute |
Problem.answer (a string) |
binary: +1.0 / -0.1 |
CodeVerifier.verify |
code_tests (input → expected stdout) |
fractional: passed / len(tests) |
RLVR is narrow by construction — it only exists where a checker exists. Question #1 asks you to say what that boundary is and why it is a boundary in principle, not just in practice.
📄 Tülu 3 (coined "RLVR") · DeepSeek-R1
The prompt instructs the policy to put its final answer in \boxed{} — the GSM8K convention — so
extraction is a regex, not a judgment call. Read both branches carefully; they behave differently:
boxed = re.search(r"\\boxed\{([^}]+)\}", text) # branch 1: the FIRST box wins
if boxed: return boxed.group(1).strip()
numbers = re.findall(r"-?\d+\.?\d*", text) # branch 2: fallback = the LAST number in the text
return numbers[-1] if numbers else "" # branch 3: nothing numeric at allThen _normalize compares numerically when it can (float(value)), and falls back to
value.strip() when float() raises. Cell 9 flags the design risk out loud: "a verifier that fails
on formatting technicalities punishes correct reasoning, which is the fastest way to teach a policy
the wrong lesson." The failure runs in both directions — too lenient rewards non-answers, too
strict punishes right answers. Hold that thought for Question #3.
class MathRewardFunction:
correct_reward = 1.0
incorrect_penalty = -0.1 # NOT -1.0 — Question #2 asks what -1.0 would teachThree facts worth having at your fingertips, all asserted in Cell 10:
compute(r"The speed is \boxed{80}", "80.0") == 1.0— normalization makes"80"match"80.0".compute(r"The speed is \boxed{81}", "80") == -0.1— a wrong answer.compute("I cannot solve this.", "80") == -0.1— a refusal is not free.extract_numberreturns"", which fails normalization and earns the same penalty as a wrong answer, not0.0.
Cell 17 states the design intent: "Asymmetric rewards (+1.0 / −0.1) keep early training from collapsing into refusal." Question #2 asks you to supply the mechanism behind that sentence.
groups = [sample_and_verify(p) for p in problems] # 5 problems x n_samples=4 -> 20 SamplesEach Sample carries problem, response, extracted, reward, verified_correct
(= reward > 0). Sampling groups rather than one completion per prompt does two jobs at once:
one prompt
|
+---------+---------+---------+ GRPO : advantage = reward - group mean (Session 15)
v v v v DPO : pair a correct winner with an incorrect loser
sample sample sample sample ... both need the SAME group structure
+1.0 -0.1 +1.0 -0.1
Cell 15's NOTE is the operational guardrail: "If your verified-correct rate is 100%, the contrast that drives learning is missing — swap in a smaller model, raise the temperature, or add harder problems until some samples fail." Cell 16 then prints one verifier-rejected completion — reading failures is how you learn what the policy actually gets wrong.
Goodhart's law — "when a measure becomes a target, it ceases to be a good measure." Cell 19 names three routes the notebook expects: a bare boxed answer with no reasoning, parroting numbers from the prompt, and exploiting extraction quirks instead of solving the problem.
def looks_like_hack(sample: Sample) -> bool:
if not sample.verified_correct: return False # only CORRECT samples can be "hacks"
work = sample.response.replace(f"\\boxed{{{sample.extracted}}}", "")
numbers_in_work = re.findall(r"-?\d+\.?\d*", work)
return len(sample.response.split()) < 20 or len(numbers_in_work) < 2One signature, two thresholds — and a detector is itself a measure. audit_record then appends
every decision (flagged or not) to artifacts/verifier.jsonl in append-only mode, so re-running the
notebook grows the file rather than replacing it. Question #3 asks for two other hacks plus
mechanism-matched hardening.
def verify(self, code: str, test_cases: list[dict]) -> float:
passed = 0
for tc in test_cases:
try:
output = self._run(code, tc.get("input", ""))
if output.strip() == str(tc["expected"]).strip(): passed += 1
except Exception:
pass # crash, timeout, or non-zero exit -> no credit for THIS case
return passed / len(test_cases) if test_cases else 0.0Note the shape difference: math is binary (+1.0 / -0.1), code is fractional (0.0–1.0).
code_tests has three visible fixtures (1→1, 3→14, 10→385), and strip_fences cleans markdown
fences off the candidate before execution. Question #4 asks what partial credit buys and costs.
📄 TRL DPOTrainer · TRL GRPOTrainer
result = subprocess.run(
[sys.executable, "-c", code], # a FULL Python interpreter
input=input_data, capture_output=True, text=True,
timeout=self.timeout_seconds, # = 5
)Read that call argument by argument: it runs as your user, in your working directory, with
your environment (Cell 4 just put OPENAI_API_KEY there) and your network. Cell 22 and Cell 33
say it plainly:
"in production, this verifier runs inside a sandbox (container, gVisor, firecracker VM) — never on the host" … "the policy will eventually generate code that reads the filesystem, opens sockets, or fork-bombs the host (and our timeout catches none of that)" … "the verifier defines the reward, so the verifier's execution environment is a security boundary."
The precise question — which dimensions of that threat does timeout=5 actually constrain, and which
does it not touch at all? — is the second half of Question #4.
📄 subprocess.run · Vercel Sandbox · gVisor · Firecracker
flagged_responses = {r["response"] for r in records if r["suspected_hack"]}
winners = [s for s in group if s.verified_correct and s.response not in flagged_responses]
losers = [s for s in group if not s.verified_correct]
# cross product WITHIN a group -> {"prompt": …, "chosen": …, "rejected": …}Two consequences fall straight out of the code:
- A group with no incorrect sample (or no unflagged correct one) contributes zero pairs. Cell 28 has an explicit else-branch for exactly that outcome — it is not a bug.
- Flagged samples are excluded from
chosenon purpose: "a hack-suspect completion used as 'chosen' would teach the next policy iteration to hack more."
Same verifier, two consumers: DPO eats the {prompt, chosen, rejected} pairs; GRPO
(Session 15) skips the pairing and uses the group rewards directly.
| Issue | Cause | Fix |
|---|---|---|
| Verified-correct rate is 100% | policy too strong / problems too easy for this setup | Cell 15's NOTE: smaller model, higher temperature, or harder problems — you need failures |
0 preference pairs printed |
a group needs ≥1 correct and ≥1 incorrect sample | Not a bug — Cell 28's else-branch anticipates it. Get contrast first (row above) |
artifacts/*.jsonl missing from your repo |
16_RLVR/.gitignore ignores artifacts/ |
Expected. Keep the cell outputs as evidence (README step 4) |
| Audit trail record count keeps climbing | AUDIT_LOG.open("a") is append-only across re-runs |
Expected. Delete artifacts/verifier.jsonl to reset |
| A crashing candidate silently scores 0 | verify catches bare Exception around _run |
By design — crash/timeout/non-zero exit = no credit for that case, not an error |
Code candidate scores 0.00 but looks right |
markdown fences, or extra printed text | strip_fences; the task says print only the number |
Correct answer scored -0.1 |
\boxed{80 km/h} / 1,250 → float() raises → string compare fails |
Verifier brittleness, not student error — exactly what Cell 9 warns about |
| Numbers differ on every run | live sampling at temperature 1.0 on a small policy | Report the shape of the evidence, never memorized decimals |
getpass prompt reappears |
the key is in-memory for the session only (Cell 4) | Re-enter it; never hardcode it into the notebook |
Answer all four in the notebook, in the ❓ markdown cells under ##### Answer:, in your own words.
| # | Where | The skill it's testing | Concept section |
|---|---|---|---|
| Q1 | after Task 3 | what "verifiable" means, and the RLVR ↔ RLHF boundary | §4 |
| Q2 | after Task 3 | reward shaping in the failure-dominated early regime | §6, §7 |
| Q3 | after Task 5 | adversarial reading of your own verifier | §5, §8 |
| Q4 | after Task 6 | sparse vs dense signal, and a real threat model | §9, §10 |
In your own words: what makes a reward "verifiable," and how does RLVR differ from RLHF's learned reward model? Give one task where a verifiable reward exists and one where it fundamentally cannot (and explain why).
Getting there. The notebook's intro hands you one sentence of definition — push past it. Who or what issues the verdict, and what has to already be known for a verdict to be possible at all? Now list the properties that follow from "a program decides": what does that buy you in cost, in repeatability six months from now, and in your ability to prove to a third party how a sample was scored? (Look at what
artifacts/verifier.jsonlexists for.)For the RLHF half, be precise about what is actually in the loop. Is a human scoring each rollout, or is a model being scored against — one that was fitted to human comparisons? If it's a fitted approximation, ask the uncomfortable question: what happens to its scores when the policy improves and moves off the data it was fitted on, and what does an optimizer do with that?
For "fundamentally cannot," the trap is picking something that is merely hard. Test your candidate against three questions: is there a single ground truth? Would two competent experts agree on the verdict? Is the criterion a computable predicate, or is it defined by preference? If you reach for an LLM judge as the counter-example's rescue — is a second model's opinion deterministic and reproducible, and does calling it a "verifier" survive that test?
One more, worth a sentence: is RLVR immune to reward hacking? Task 5 exists for a reason — check your claim against it before you write it down.
The reward is asymmetric: +1.0 for a correct answer but only −0.1 for an incorrect one. Suppose we used −1.0 instead. What behavior might a policy learn during early training, when most of its attempts fail? (Hint: think about a model that discovers it can hedge, refuse, or produce no parseable answer at all.)
Getting there. Do the arithmetic before you write prose. Write the expected reward of attempting once as an expression in the success probability
pand the failure penaltyc. Solve for thepat which attempting becomes worth it — once withc = 0.1, once withc = 1.0. Cell 5 tells you which side of those thresholds an early, deliberately-small policy sits on.Then switch from economics to gradients. An update pushes probability mass away from whatever earned the penalty. In a regime where most attempts fail, away from what, exactly — and what else shares tokens with those trajectories? Name the concrete cheapest behaviors that stop earning penalties (Cell 12's hint lists three; a fourth is about output length and entropy).
Then follow the damage downstream — this is what separates a partial answer from a full one. If a group contains no verified-correct sample, what does
build_preferencesreturn? And under GRPO, what is an advantage worth when every reward in a group is identical?Finally, a code check rather than a guess: what does
extract_number("I cannot solve this.")return, and what reward does Cell 10 assert for it? Does that change the story you were about to tell about refusal being "free"? Say what it does and doesn't change.
Our detector flags one signature: "right answer, no visible work." Name two other ways a policy could hack a
\boxed{}exact-match verifier, and for each, describe how you would harden the verifier or the prompt against it. (Session 15's stacked format rewards are one relevant hardening example.)
Getting there. Reread
extract_number(§5) line by line as an attacker. It has two independent branches — the first\boxed{}wins, and when there's no box the last number in the text wins. Design one exploit per branch, then ask which one a policy would stumble into by accident rather than by design.Now reread
_normalize. What doesfloat()do with"1,250"? With"80 km/h"? With a unicode minus? Sort the results into two piles: cases where a policy gains reward it didn't earn, and cases where correct reasoning loses reward. Cell 9 warns about the second pile — both are verifier failures.Now reread
looks_like_hack(§8) and count. Exactly what would you have to emit to make both thresholds false? Is that expensive? A detector is a measure too.Two rules for the hardening half: (1) each fix must defeat that specific mechanism — "make the verifier better", "train longer", "use a bigger model" are not fixes; (2) "penalize wrong answers more heavily" is disqualified by your own Q2 answer, so check it against that before you write it. And the named hint: Session 15's stacked format rewards add a separate reward term for structure. What does paying separately for a parseable answer and a correct one buy you that a stricter correctness check cannot?
The code verifier returns fractional rewards (fraction of tests passed) while the math verifier is binary. What are the benefits and risks of partial credit as a training signal? And concretely: what could a policy-generated program do to a verifier that runs candidates directly on the host, and which parts of that threat does our timeout not cover?
Getting there — part 1 (the signal). Run the thought experiment on paper. Imagine sampling 8 completions for a hard task under a binary verifier and write down the 8 rewards. What is each sample's advantage relative to the group mean? What does
build_preferencesreturn for that group? Now redo it withpassed / len(tests)and compare. That contrast is the benefit — name it with the right vocabulary, not just "it's more informative."For the risks, look at the actual fixtures in Cell 25. What is the cheapest program that scores well on them without solving the stated task? Then ask a second question: if you added three more easy or near-duplicate test cases, whose score would go up — and did the policy get any better? What is the reward now really measuring? (And a third: what happens to reproducibility if a test is flaky?)
Getting there — part 2 (the host). Reread
_run(§10) argument by argument. Which user does it run as? Which working directory? Which environment — and what did Cell 4 put into that environment three tasks ago? What else on your machine is readable by that user? Give at least one attack with its mechanism, not a vibe. Then ask the pointed one: what is the one file in this pipeline whose whole purpose is to prove the run was honest, and what happens if generated code can write to it?Then answer the "timeout" half systematically. Make a list of the dimensions along which a process can do damage — time, CPU, memory, disk, file descriptors, filesystem, network, credentials, child processes, persistence — and mark which single one
timeout=5constrains. For each of the rest, say why it slips through. Two specifics worth checking rather than assuming: how long does reading a key and POSTing it somewhere actually take relative to 5 seconds, and does killing a child also kill a grandchild it started withstart_new_session=True? Finally: can any timeout undo a file that was already written?
The deliverable: the ### YOUR CODE HERE cell after Task 7, filled in and executed with output
kept — a reward function for a new verifiable domain, a sample-and-verify run over at least 3
prompts with n_samples >= 3, a printed verified-correct rate, and a note on hack-suspect behavior
and how you'd detect it. The notebook suggests JSON-schema conformance, SQL vs a reference query, or a
regex/string transformation; anything genuinely checkable qualifies.
Work the checklist — the discipline is what's being assessed, not cleverness:
- Pick a genuinely new domain. Not the notebook's math or code verifier reskinned. Sanity test: does your domain have ground truth that exists before the policy answers?
- Write down your ground truth before you write any code. What object holds it? Can a program compare a completion against it with no judgment call? If your honest answer is "I'd ask a model whether it looks right," stop — §4 says why that isn't a verifiable reward.
- Design your extraction step — the
\boxed{}of your domain. What exactly is the parse, and what does it return when the policy replies with prose, or fences, or nothing usable? (Look at howstrip_fencesandextract_numbereach handle their failure case.) - Mirror the notebook's reward shape: one completion + ground truth → one float. Keeping
MathRewardFunction.compute's signature makes everything downstream reusable. - State AND justify binary vs. fractional. The activity asks for the justification explicitly — it is a graded item on its own. Either choice can be right; your reason should connect to what you worked out in Q4 about sparse gradients and about farming the easy checks.
- Decide what an unparseable output earns. Before you default to
0.0, check whatMathRewardFunctiongives a refusal, and re-read your own Q2 reasoning about why. - Run it for real. ≥3 prompts ×
n_samples >= 3— at least 9 actualsimple_completecalls against the policy. Hand-written "completions" or a hardcoded rate don't count. - Print a verified-correct rate (or a per-group breakdown), not just a list of raw rewards. Be
explicit about what counts as "correct" in your domain — is it
reward > 0, or must every check pass? - Write the hack-suspect observation. The question that gets you there: which part of my reward is the cheapest to earn without actually doing the task? Name a signature specific to your domain and say how you'd detect it in code. "I saw no hacks" only counts if you state the signature you looked for and the method you'd use.
- Optional depth (not required, but it's where the session's ideas connect): log your decisions
into the same
AUDIT_LOG, or build preference pairs from your new domain's groups the way Task 7 does — and see whether your flagged samples change the result.
⚠️ A low verified-correct rate is not a failure here — Cell 15's NOTE says a policy that never fails produces no signal at all. Reporting an honest low rate with a clear-eyed observation beats a tidy 100%.
- Tülu 3 — the paper that coined "RLVR". https://arxiv.org/abs/2411.15124
- DeepSeek-R1 — RL with verifiable rewards at scale. https://arxiv.org/abs/2501.12948
- DPO — Direct Preference Optimization. https://arxiv.org/abs/2305.18290
- TRL —
DPOTrainer(the{prompt, chosen, rejected}format). https://huggingface.co/docs/trl/dpo_trainer - TRL —
GRPOTrainer(the Session 15 consumer). https://huggingface.co/docs/trl/grpo_trainer - GSM8K — the grade-school-math benchmark behind the
\boxed{}convention. https://arxiv.org/abs/2110.14168 - OpenAI — Chat Completions API. https://platform.openai.com/docs/api-reference/chat
- Python —
subprocess.run(and whattimeoutdoes). https://docs.python.org/3/library/subprocess.html#subprocess.run - Python —
re(regex extraction). https://docs.python.org/3/library/re.html - Python —
dataclasses(Problem,Sample,asdict). https://docs.python.org/3/library/dataclasses.html - Vercel Sandbox — ephemeral microVMs for untrusted LLM-generated code. https://vercel.com/docs/vercel-sandbox
- gVisor — application kernel sandbox. https://gvisor.dev/docs/
- Firecracker — microVMs. https://firecracker-microvm.github.io/
- SymPy — symbolic equivalence for hardening a math verifier. https://docs.sympy.org/latest/index.html
- uv — the project/dependency manager used by
uv sync. https://docs.astral.sh/uv/