How to use this book
The book follows one dependency chain:
- define what counts as a correct result;
- inspect the model and generation contract;
- build an evaluation harness;
- spend extra inference compute only where it helps;
- score and refine candidate answers without trusting the scorer blindly;
- train with group-relative policy updates on verifiable rewards;
- monitor the failure modes created by that training; and
- distil useful behaviour into a cheaper serving model.
Readers who want the full implementation path should follow the chapters in order. Readers evaluating an existing system can begin with Chapter 3, then use Chapters 4, 5 and 7 to examine its compute policy, selection logic and monitoring.
What the book builds
The running laboratory uses mathematical and code-like tasks because their outcomes can be checked cheaply. The implementation is deliberately modular:
- a prompt and response contract;
- answer extraction and normalisation;
- deterministic and learned verifiers;
- greedy, sampled and self-consistent generation;
- difficulty-aware inference budgets;
- sequence log-probability accounting;
- critique-and-revision experiments;
- a compact GRPO objective;
- rollout, reward and stability metrics;
- response-only distillation; and
- an evaluation report that separates accuracy, coverage, cost and risk.
The reference checkpoint is the public Qwen/Qwen3-0.6B repository as retrieved on 28 July 2026 at commit c1899de289a04d12100db370d81485cdf75e47ca. The repository name is retained because it is familiar, although the checkpoint metadata reports 751,632,384 BF16 tensor parameters. Its published configuration uses 28 decoder layers, hidden width 1,024, 16 query heads, 8 key/value heads, head dimension 128, a 3,072-unit intermediate layer, vocabulary size 151,936 and maximum position setting 40,960. Those facts are a reproducibility pin, not an endorsement or a promise that later repository revisions will match.
You can substitute another causal decoder if you adapt the tokenizer, chat template, stop conditions, padding policy, position handling and parameter names. Treat those details as part of the model contract.
Conventions
The notation is consistent throughout:
| Symbol | Meaning |
|---|---|
| prompt token sequence | |
| generated response tokens | |
| trainable policy model | |
| frozen rollout policy used to collect a batch | |
| frozen reference policy for divergence control | |
| reward for the -th response in a prompt group | |
| group-relative advantage | |
| responses sampled for one prompt | |
| prompts in one batch | |
| padded response length | |
| policy-ratio clipping radius | |
| reference-policy penalty coefficient |
Tensor shapes appear in square brackets, such as [B, G, T]. Code uses zero-based indices. Probabilities are written in ordinary space; log-probabilities are natural logarithms. Accuracy is reported as a proportion unless a percent sign is present.
“Correct” always means correct under a stated evaluation contract. A symbolic equivalence checker, unit-test suite and human adjudicator can disagree because they answer different questions.
Reproducibility discipline
An experiment record should include:
- model repository and immutable revision;
- tokenizer and chat template;
- library versions;
- prompt template and stop conditions;
- random seeds and sampling configuration;
- benchmark revision and split;
- answer extractor and normaliser revision;
- verifier code and dependencies;
- maximum prompt and response lengths;
- hardware, precision and compilation settings;
- number of generated candidates per item;
- checkpoint selection rule; and
- raw per-item outputs, rewards and error labels.
Aggregate accuracy alone is not enough to reproduce a result or diagnose a regression. The per-item ledger is the durable unit of evidence.
A note on visible rationales
Some model families expose a rationale-like field, some suppress it, and some interleave tool calls with answer text. Applications should not depend on private internal reasoning. They can instead ask for a concise, inspectable justification, cited evidence, a calculation trace or a structured proof object that is safe to retain.
When a task is consequential, verification should target the claim and its evidence. Fluency, length and apparent confidence are weak substitutes.
Chapter 1: Reasoning as an engineering contract
A language model produces a distribution over the next token. A reasoning system surrounds that model with a task contract, a generation policy, tools, verifiers, budgets and controls. Keeping those layers separate prevents a common category error: improved benchmark accuracy does not imply that every visible rationale is faithful, every answer is grounded or every workflow is safe.
This chapter defines the object we will build. The definition is deliberately testable:
A reasoning-model system allocates additional computation to a multi-step task, produces one or more candidate solutions, and uses a stated evaluation contract to select, reject or escalate the result.
The definition says nothing about consciousness. It tells an engineer what must be observable.
The model remains autoregressive
For a prompt and response tokens , a causal language model factorises the response probability as
The training objective normally minimises negative log-likelihood over selected target tokens:
where masks prompt, padding or otherwise excluded tokens.
Longer rationales give the decoder more sequential computation. Each generated token becomes part of the context for the next step. That can help the model decompose a problem, record an intermediate value or recover from an earlier guess. It can also create more places to make an error. The mechanism does not guarantee logical validity.
Chain-of-thought prompting improved multi-step benchmark results in the experiments reported by Wei and peers, and zero-shot prompts that invited intermediate work also improved several studied tasks.1 Those results are empirical and model-dependent. They do not establish that a particular phrase will improve every model, language or task.
Visible work is an artefact, not privileged telemetry
A rationale can serve three useful purposes:
- it supplies additional context for later tokens;
- it gives a verifier more structure to inspect; and
- it provides a human with an explanation candidate.
Those purposes are different. A rationale may lead to the right answer through a faulty step. It may cite a correct rule after the answer was determined by an unrelated cue. It may omit the decisive factor. Experiments have shown that generated explanations can be influenced by features they do not acknowledge.2
For this reason, the book distinguishes four objects:
| Object | What it records | What it does not prove |
|---|---|---|
| Model continuation | Tokens selected under a decoding policy | Truth or task completion |
| Scratch work | Intermediate text, code or equations | Faithful internal causation |
| Evidence record | Retrieved source spans and tool results | Correct synthesis |
| Verification record | Checks applied to claims or outputs | Safety outside the checked scope |
An application may retain a concise calculation trace while withholding or discarding unrestricted scratch text. The retained record should be designed for the task rather than treated as a transcript of a mind.
A taxonomy based on checkability and consequence
“Reasoning” covers tasks with very different feedback. Two dimensions are especially useful:
- checkability: how cheaply and reliably an outcome can be tested; and
- consequence: the cost of accepting an incorrect result.
The four broad regions lead to different designs:
Cheap to check, low consequence
Examples include generated practice questions, bounded arithmetic and code kata tests. Deterministic rewards can support rapid experiments. Errors still matter for learning, but rollback and review are straightforward.
Cheap to check, high consequence
A covenant calculation can be recomputed exactly once the authorised inputs and formula are fixed. The check is cheap, but choosing the inputs, effective agreement and approval action may carry financial and legal consequences. The model may extract candidates; deterministic code calculates; an authorised person decides.
Expensive to check, low consequence
Drafting, brainstorming and exploratory analysis often need human preference judgements. Sampling several answers may improve choice, but there is no exact oracle. Evaluation should use rubrics, blinded review and disagreement reporting.
Expensive to check, high consequence
Credit approval, medical advice, legal interpretation and identity decisions belong here. A language model may assist with evidence assembly or drafting. It should not be treated as the source of decision authority. Independent controls, limited permissions, record keeping and human accountability dominate model cleverness.
This taxonomy explains why reinforcement learning with verifiable rewards is attractive for mathematics and code. It also explains why success there cannot be transferred by analogy to an open-ended regulated decision.
Five levels of computational work
A useful task ladder separates operations that are often blended together:
- Recall: reproduce a learned association.
- Local transformation: rewrite, classify or extract within a bounded input.
- Composition: combine several stated facts or operations.
- Search: explore alternatives, backtrack or sample candidates.
- Grounded action: use tools and evidence under permissions to change external state.
The ladder is not an intelligence scale. A calculator may outperform a language model on arithmetic while doing less linguistic work. A short retrieved answer may be safer than an elaborate generated derivation. Route by task structure.
The task contract comes before the prompt
Before choosing a model, write an executable or reviewable contract. The following data class captures the minimum:
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Literal
Outcome = Literal["accept", "reject", "review"]
@dataclass(frozen=True)
class TaskContract:
name: str
version: str
max_input_tokens: int
max_output_tokens: int
verifier_name: str
consequence: Literal["low", "medium", "high"]
on_verifier_error: Outcome = "review"
on_parse_error: Outcome = "review"
@dataclass(frozen=True)
class Verification:
outcome: Outcome
score: float | None
reason_code: str
verifier_version: str
Verifier = Callable[[str, str], Verification]The contract should also state:
- the permitted evidence sources;
- the accepted answer representation;
- normalisation rules;
- abstention conditions;
- the cost and latency budget;
- the person or system allowed to approve an action;
- the record retained for audit; and
- the test slices that must not regress.
Prompt text is then one implementation detail of the contract.
Baselines prevent expensive self-deception
Every experiment needs a cheap baseline. For a reasoning pipeline, useful baselines include:
- direct answer with greedy decoding;
- direct answer with sampling;
- explicit worked answer with greedy decoding;
- sampled candidates with majority vote;
- sampled candidates selected by a deterministic verifier;
- the same methods with an external calculator or code executor; and
- a non-generative rule or retrieval system.
If a calculator solves the task exactly, it is the correct baseline and may be the correct production component. If retrieval plus a template meets the requirement, adding GRPO is engineering theatre.
The comparison unit is not model accuracy alone:
The values and costs must be supplied by the application owner. A high-consequence task can make a small false-accept rate dominate the equation.
Route compute instead of applying it uniformly
Reasoning tokens, parallel samples and verifier calls all cost time and money. Easy items often need none of them. Very hard items may not improve with more sampling because the base model rarely enters a correct region.
A first router can be deterministic:
from dataclasses import dataclass
from typing import Literal
Route = Literal["direct", "sample_and_verify", "human_review"]
@dataclass(frozen=True)
class RouteFeatures:
consequence: Literal["low", "medium", "high"]
deterministic_verifier: bool
input_complete: bool
estimated_difficulty: float # calibrated to [0, 1]
def choose_route(features: RouteFeatures) -> Route:
if features.consequence not in {"low", "medium", "high"}:
raise ValueError("unknown consequence")
if (
type(features.deterministic_verifier) is not bool
or type(features.input_complete) is not bool
):
raise TypeError("router flags must be booleans")
if not 0.0 <= features.estimated_difficulty <= 1.0:
raise ValueError("estimated_difficulty must lie in [0, 1]")
if not features.input_complete or features.consequence == "high":
return "human_review"
if features.deterministic_verifier and features.estimated_difficulty >= 0.35:
return "sample_and_verify"
return "direct"The caller maps a router validation exception to review rather than to the direct path. The thresholds are placeholders. Estimate them on held-out traffic, include confidence intervals and monitor drift. A router trained on benchmark difficulty may fail on production ambiguity, missing context or adversarial inputs.
Small models can teach the system, even when they cannot solve the domain
A sub-billion-parameter checkpoint is useful for learning the mechanics:
- tokenisation and chat templates are inspectable;
- inference can run on accessible hardware;
- rollout batches are small enough to profile;
- reward bugs become visible quickly; and
- overfitting appears within a manageable experiment.
It is not evidence that the same checkpoint is suitable for every application. A small model can learn answer formatting or a narrow arithmetic pattern while lacking the knowledge and reliability required for a broader domain.
The reference Qwen3 checkpoint illustrates another reproducibility lesson. Its repository label says “0.6B”, while its current model metadata reports 751,632,384 tensor parameters. Published names often encode a family tier rather than an exact tensor count. Capacity and memory calculations should use the checkpoint, configuration and dtype actually loaded.
A governed banking example
Consider a fictional credit analyst reviewing a facility agreement and two amendments. The task is to identify the effective leverage covenant and compare a supplied ratio with the threshold.
The language model is allowed to:
- retrieve clauses from the authorised document set;
- propose which clause supersedes another;
- extract candidate numerator, denominator and threshold fields; and
- draft a short explanation with source references.
It is not allowed to:
- choose an unauthorised document;
- invent a missing value;
- calculate the ratio in free text;
- approve or decline credit; or
- write to a system of record.
A deterministic service resolves effective dates, validates units, calculates the ratio and compares it with the approved threshold. Ambiguity, missing evidence or conflicting clauses produce review. An authorised analyst owns the conclusion.
This is a reasoning system because it may retrieve, compare, extract and explain across several steps. Its safety comes from the boundary around those steps.
Build check
Before continuing, a project should be able to answer:
- What exact object is graded?
- Which outcomes are
accept,rejectandreview? - Can the verifier fail closed?
- What is the direct baseline?
- Which per-item records are retained?
- Which action remains under human authority?
If those answers are absent, more model training will make the system harder to diagnose.
Chapter notes
Chapter 2: The model and generation contract
Reasoning experiments are sensitive to details that ordinary demos hide: the model revision, tokenizer files, chat template, stop tokens, padding side, random generator, maximum length and cache policy. A result is not reproducible if those settings are described only as “use the 0.6B model”.
This chapter pins the reference checkpoint and follows one prompt through tokenisation, the decoder, sampling and stopping.
Pin the repository, not the marketing tier
The examples use:
repository: Qwen/Qwen3-0.6B
revision: c1899de289a04d12100db370d81485cdf75e47ca
retrieved: 2026-07-28
The official configuration at that revision reports:
| Property | Value |
|---|---|
| Decoder layers | 28 |
| Hidden width | 1,024 |
| Feed-forward intermediate width | 3,072 |
| Query heads | 16 |
| Key/value heads | 8 |
| Head dimension | 128 |
| Vocabulary size | 151,936 |
| Maximum position setting | 40,960 |
| RoPE base | 1,000,000 |
| Tied input/output embeddings | yes |
| BF16 tensor parameters in repository metadata | 751,632,384 |
The query projection width is , larger than the residual width. The grouped key and value projections each have width . Architecture code should read head_dim rather than assuming hidden_size / num_attention_heads.
At BF16, the parameter tensors alone occupy approximately
Inference also allocates activations, attention workspaces, temporary logits and a KV cache. Training adds gradients, optimiser states and usually higher-precision copies. A model that fits for inference may still be far from fitting for full-parameter training.
Load with explicit trust and precision decisions
The following loader uses the published Transformers integration and refuses implicit code execution from the model repository:
from __future__ import annotations
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_ID = "Qwen/Qwen3-0.6B"
REVISION = "c1899de289a04d12100db370d81485cdf75e47ca"
def load_reference_model(device: torch.device):
tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
revision=REVISION,
trust_remote_code=False,
)
dtype = torch.bfloat16 if device.type in {"cuda", "mps"} else torch.float32
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
revision=REVISION,
torch_dtype=dtype,
use_safetensors=True,
trust_remote_code=False,
).to(device)
model.eval()
return model, tokenizerIn an offline or controlled environment, mirror the exact repository revision, verify file hashes and load from the approved mirror. A Git commit pins repository state; it does not by itself establish software supply-chain trust.
The chat template is executable configuration
The model does not receive a Python list of messages. It receives tokens. The tokenizer’s chat template converts roles, content and generation markers into a particular token sequence.
def encode_user_prompt(tokenizer, prompt: str, *, thinking: bool):
messages = [{"role": "user", "content": prompt}]
token_ids = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
enable_thinking=thinking,
return_tensors="pt",
)
return token_idsQwen3’s published template supports thinking and non-thinking modes.1 Those modes change the formatted prefix and expected response form. They are part of the evaluation condition.
Log the template hash or complete rendered prefix. A repository update that changes only template text can change an experiment without changing the weight tensors.
Inspect the rendered representation during setup:
def inspect_template(tokenizer, prompt: str, *, thinking: bool) -> dict:
ids = encode_user_prompt(tokenizer, prompt, thinking=thinking)[0]
return {
"num_tokens": int(ids.numel()),
"first_ids": ids[:12].tolist(),
"last_ids": ids[-12:].tolist(),
"rendered": tokenizer.decode(ids, skip_special_tokens=False),
}Do not publish sensitive prompts in debug logs. In production, record a template version and token counts while applying the organisation’s data-retention policy.
What one decoder layer does
Let the residual stream entering a layer be . A pre-normalised decoder layer applies:
For grouped-query attention,
With and , each key/value head serves two query heads. After applying rotary position information to queries and keys, attention uses a causal mask:
where when key position is visible to query position , and a sufficiently negative value otherwise.
Attention maps are internal routing weights. They are useful for shape debugging and some mechanistic analyses, but they do not form a complete causal explanation of an answer.
RoPE changes queries and keys, not token order
Rotary position embedding applies position-dependent rotations to pairs of query and key coordinates.2 For one two-dimensional pair at position ,
This makes the query-key inner product depend on relative displacement. It does not expand the usable context without limit. Training distribution, numerical precision, scaling method, cache implementation and evaluation all constrain long-context behaviour.
The KV cache is the dominant sequence-dependent allocation
Without caching, each generation step recomputes keys and values for the entire prefix. A cache stores them once per layer.
For a dense cache in bytes,
where is bytes per cached element. For the pinned configuration at BF16:
That is 112 KiB per cached token per sequence. At 40,960 tokens, the arithmetic is approximately 4.375 GiB, excluding allocator overhead and implementation-specific storage. Batch size multiplies the requirement.
This estimate should be checked against measured peak allocation because some runtimes page, quantise or share cache blocks.
Logits become a policy only after decoding rules
At step , the model produces logits . Temperature gives
Top- sampling sorts tokens by probability and retains the smallest prefix whose cumulative probability reaches . The retained probabilities are renormalised before sampling.
Greedy decoding selects . It is not equivalent to temperature zero in code because division by zero is undefined. Implement greedy selection as a separate branch.
import math
import torch
def nucleus_sample(
logits: torch.Tensor,
*,
temperature: float,
top_p: float,
generator: torch.Generator,
) -> tuple[torch.Tensor, torch.Tensor]:
if logits.ndim != 2 or logits.shape[0] < 1 or logits.shape[1] < 1:
raise ValueError("expected logits shaped [batch, vocabulary]")
if not math.isfinite(temperature) or temperature <= 0:
raise ValueError("temperature must be greater than zero")
if not math.isfinite(top_p) or not 0 < top_p <= 1:
raise ValueError("top_p must lie in (0, 1]")
if not torch.isfinite(logits).all():
raise ValueError("logits must be finite")
scaled_logits = logits.float() / temperature
if not torch.isfinite(scaled_logits).all():
raise ValueError("scaled logits must be finite")
probs = torch.softmax(scaled_logits, dim=-1)
sorted_probs, sorted_ids = torch.sort(probs, descending=True, dim=-1)
cumulative = torch.cumsum(sorted_probs, dim=-1)
remove = cumulative - sorted_probs >= top_p
sorted_probs = sorted_probs.masked_fill(remove, 0.0)
sorted_probs = sorted_probs / sorted_probs.sum(dim=-1, keepdim=True)
sampled_rank = torch.multinomial(
sorted_probs,
num_samples=1,
generator=generator,
)
sampled_ids = sorted_ids.gather(dim=-1, index=sampled_rank)
sampling_logprobs = sorted_probs.gather(
dim=-1,
index=sampled_rank,
).log()
return sampled_ids, sampling_logprobsThe subtraction in cumulative - sorted_probs keeps the token that crosses the threshold. Masking on cumulative > top_p without shifting can remove it.
A transparent autoregressive loop
Library generation utilities handle many edge cases. A small manual loop remains valuable for understanding stop conditions and collecting token log-probabilities:
from dataclasses import dataclass
import torch
@dataclass
class Generation:
token_ids: list[int]
sampling_logprobs: list[float]
model_logprobs: list[float]
stop_reason: str
@torch.inference_mode()
def generate_one(
model,
input_ids: torch.Tensor,
*,
eos_token_ids: set[int],
max_new_tokens: int,
temperature: float,
top_p: float,
seed: int,
) -> Generation:
if (
input_ids.ndim != 2
or input_ids.shape[0] != 1
or input_ids.shape[1] < 1
):
raise ValueError("this teaching loop expects shape [1, prompt_length]")
if max_new_tokens < 1:
raise ValueError("max_new_tokens must be at least one")
device = input_ids.device
generator = torch.Generator(device=device)
generator.manual_seed(seed)
sequence = input_ids
cache = None
emitted: list[int] = []
sampling_logprobs: list[float] = []
model_logprobs: list[float] = []
for _ in range(max_new_tokens):
model_input = sequence if cache is None else sequence[:, -1:]
outputs = model(
input_ids=model_input,
past_key_values=cache,
use_cache=True,
)
cache = outputs.past_key_values
logits = outputs.logits[:, -1, :]
next_id, next_sampling_logprob = nucleus_sample(
logits,
temperature=temperature,
top_p=top_p,
generator=generator,
)
next_model_logprob = torch.log_softmax(
logits.float(),
dim=-1,
).gather(
dim=-1,
index=next_id,
)
token_id = int(next_id.item())
emitted.append(token_id)
sampling_logprobs.append(float(next_sampling_logprob.item()))
model_logprobs.append(float(next_model_logprob.item()))
sequence = torch.cat((sequence, next_id), dim=1)
if token_id in eos_token_ids:
return Generation(
emitted,
sampling_logprobs,
model_logprobs,
"eos",
)
return Generation(
emitted,
sampling_logprobs,
model_logprobs,
"length",
)sampling_logprobs records the actual temperature-scaled, top--renormalised behaviour distribution. model_logprobs records the selected token under the unmodified model softmax. They are equal only when temperature is 1 and top_p is 1. Mixing the two silently corrupts an importance ratio.
This loop is intentionally single-sequence. A production batched loop must maintain an attention_mask, mark finished rows, avoid sampling new content for finished rows, distinguish padding from stop tokens and cap total context length. Chapter 4 introduces a candidate generator with those requirements.
Compilation and warm-up need measurement
Graph compilation can reduce Python and kernel-launch overhead, but the gain depends on shapes, backend, model code and how often a compiled graph is reused. Dynamic prompt lengths can trigger recompilation. The first request also pays model loading, memory allocation and kernel initialisation costs.
Report at least:
- cold-start latency;
- first-token latency after warm-up;
- steady-state tokens per second;
- batch size and prompt/response lengths;
- peak memory;
- precision and device;
- compilation time and number of graph variants; and
- whether tokenisation and network time are included.
A speed number without those conditions is advertising, not a benchmark.
Generation invariants
Before building evaluation, test:
def assert_generation_contract(tokenizer, model) -> None:
cfg = model.config
assert cfg.vocab_size == len(tokenizer)
assert cfg.num_attention_heads % cfg.num_key_value_heads == 0
assert cfg.head_dim * cfg.num_key_value_heads > 0
assert tokenizer.eos_token_id is not None
assert tokenizer.pad_token_id is not NoneThe vocabulary assertion may need an explicit exception for models whose tokenizer contains added tokens outside the output projection. Treat such a mismatch as a documented interface decision.
Also verify:
- a fixed seed reproduces the same tokens on the same software and hardware path;
- greedy decoding is repeatable;
- an EOS token stops the row;
- the response-length cap is enforced;
- prompt tokens are not returned as answer text by mistake;
- cached and uncached logits match within the chosen tolerance; and
- left- and right-padding policies agree with the position and attention implementation.
The next chapter uses this fixed interface to build evidence about model behaviour.
Chapter notes
Chapter 3: Evaluation as executable specification
Training against a faulty grader improves performance on the fault. Selection against a faulty grader promotes the wrong candidate. Evaluation code therefore belongs in the product’s trusted computing base.
The evaluator in this chapter has four independent stages:
- preserve the raw generation;
- extract the answer object;
- normalise only equivalences allowed by the task; and
- verify the normalised object under a versioned rule.
Each stage records its own failure reason.
Define the record before running the model
A benchmark item should carry a stable identifier and provenance:
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any, Literal
@dataclass(frozen=True)
class EvalItem:
item_id: str
prompt: str
reference_answer: str
split: str
source: str
source_revision: str
subject: str | None = None
difficulty: str | None = None
@dataclass(frozen=True)
class EvalResult:
item_id: str
sample_id: int
seed: int
raw_response: str
extracted_answer: str | None
normalised_answer: str | None
verifier_outcome: Literal["correct", "incorrect", "review", "error"]
reason_code: str
latency_ms: float
prompt_tokens: int
response_tokens: int
verifier_version: str
def to_jsonable(self) -> dict[str, Any]:
return asdict(self)Do not overwrite a raw response after fixing the parser. Re-run the parser against the preserved output and record the new parser version. That separation allows an evaluator to improve without repeating expensive generation.
Use an explicit response boundary
Searching for “the last number” is fragile. A date, equation number or confidence value can appear after the intended result. The prompt should request a narrow terminal field:
Return any concise working first.
End with exactly one line:
<final>ANSWER</final>
The extractor then accepts one well-formed final field:
import re
FINAL_PATTERN = re.compile(
r"<final>[ \t]*(?P<answer>[^\r\n<>]*?)[ \t]*</final>",
flags=re.IGNORECASE,
)
FINAL_TERMINAL_PATTERN = re.compile(
r"^[ \t]*<final>[ \t]*(?P<answer>[^\r\n<>]*?)"
r"[ \t]*</final>[ \t]*(?:\r?\n)*\Z",
flags=re.MULTILINE | re.IGNORECASE,
)
FINAL_TAG_PATTERN = re.compile(r"</?final>", flags=re.IGNORECASE)
def extract_final(response: str) -> tuple[str | None, str]:
matches = list(FINAL_PATTERN.finditer(response))
if not matches:
return None, "missing_final"
if len(matches) != 1:
return None, "multiple_final_fields"
if len(FINAL_TAG_PATTERN.findall(response)) != 2:
return None, "malformed_final_fields"
terminal = FINAL_TERMINAL_PATTERN.search(response)
if terminal is None:
return None, "final_not_terminal_single_line"
answer = terminal.group("answer").strip()
if not answer:
return None, "empty_final"
if len(answer) > 512:
return None, "final_too_long"
return answer, "ok"Multiple fields produce review rather than silently choosing the last. A strict contract reduces ambiguous grading and makes formatting reward easier to reason about.
Normalisation must not solve the problem
Normalisation removes representational differences that the task declares irrelevant. It must not repair a wrong derivation or infer an omitted answer.
For simple numeric answers, Python’s Fraction and Decimal avoid eval:
from decimal import Decimal, InvalidOperation
from fractions import Fraction
import re
INTEGER = re.compile(r"^[+-]?(?:\d+|\d{1,3}(?:,\d{3})+)$")
DECIMAL = re.compile(
r"^[+-]?(?:(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d*)?|\.\d+)$"
)
FRACTION = re.compile(r"^[+-]?\d+\s*/\s*[+-]?\d+$")
def normalise_number(text: str) -> tuple[str | None, str]:
raw = text.strip()
if INTEGER.fullmatch(raw):
return str(int(raw.replace(",", ""))), "integer"
if FRACTION.fullmatch(raw):
numerator, denominator = raw.split("/")
try:
fraction = Fraction(int(numerator), int(denominator))
except ZeroDivisionError:
return None, "zero_denominator"
return f"{fraction.numerator}/{fraction.denominator}", "fraction"
if DECIMAL.fullmatch(raw):
try:
decimal = Decimal(raw.replace(",", ""))
except InvalidOperation:
return None, "invalid_decimal"
if decimal == 0:
return "0", "decimal"
canonical = format(decimal, "f")
if "." in canonical:
canonical = canonical.rstrip("0").rstrip(".")
return canonical, "decimal"
return None, "unsupported_numeric_form"The comma pattern rejects malformed groupings such as 1,2 before removing separators. Decimal text is canonicalised without calling context-sensitive arithmetic, so a long input is not rounded to the process-wide decimal precision. The normaliser deliberately does not equate 0.5 with 1/2. The verifier can convert each accepted representation to an exact Fraction:
def to_fraction(normalised: str) -> Fraction:
if "/" in normalised:
return Fraction(normalised)
return Fraction(Decimal(normalised))
def verify_numeric(candidate: str, reference: str) -> tuple[str, str]:
candidate_norm, candidate_kind = normalise_number(candidate)
reference_norm, reference_kind = normalise_number(reference)
if candidate_norm is None or reference_norm is None:
return "review", f"parse:{candidate_kind}:{reference_kind}"
if to_fraction(candidate_norm) == to_fraction(reference_norm):
return "correct", "exact_numeric_equivalence"
return "incorrect", "numeric_mismatch"For symbolic mathematics, a computer-algebra system can test equivalence by simplifying a difference under stated assumptions. Run it in a restricted worker with a time limit and an allow-list of syntax. General expression parsing and simplification can consume unbounded resources or invoke unsafe features if configured carelessly.
For code, compile and run in a disposable sandbox with CPU, memory, time, filesystem and network limits. A passing public test set is still incomplete evidence; hidden and adversarial tests reduce overfitting.
Choose the verifier that matches the claim
| Claim | Suitable check | Common failure |
|---|---|---|
| Label belongs to a closed set | Schema and exact match | Aliases omitted |
| Numeric result | Exact rational or tolerance with units | Hidden rounding or unit mismatch |
| Algebraic expression | Symbolic equivalence under assumptions | Timeouts and domain errors |
| Program behaviour | Sandboxed tests and static policy checks | Weak test coverage |
| Cited statement | Source-span entailment plus metadata checks | Correct source, unsupported synthesis |
| Writing quality | Blinded rubric and multiple reviewers | Reviewer preference presented as fact |
| Regulated decision | Authorised human process | Model score treated as authority |
A learned outcome reward model estimates whether a final answer is acceptable. A process reward model scores intermediate steps. Both are models with their own distribution shifts, calibration errors and exploitable features.
The process-supervision experiments reported by Lightman and peers found stronger best-of- selection on their MATH setting than outcome supervision, but they also documented imperfections in automatic final-answer grading.1 The result motivates careful step-level feedback; it does not make a process reward model an oracle.
MATH-500 is a particular held-out subset
The original MATH dataset contains competition-style problems across subjects and difficulty levels.2 The set commonly called MATH-500 came from the process-supervision work: 4,500 MATH test problems were included in reward-model training, and the remaining 500 were selected uniformly at random for evaluation. The authors reported that the subset’s subject and difficulty distribution was representative of the full test set.3
Three consequences follow:
- identify the exact 500-item file and revision;
- do not mix the other 4,500 items into a “held-out” claim without explaining the training relationship; and
- do not compare scores produced by different answer normalisers as if only the model changed.
Benchmark names are not data lineage.
Measure more than top-line accuracy
For items:
Coverage exposes a system that improves apparent accuracy by sending most items to review. Report the review and error rates separately.
For multiple generated samples, pass@k asks whether at least one of draws is correct. If samples contain correct results and samples are selected without replacement from that set, the standard unbiased estimator is
with value 1 when .
from math import comb
def pass_at_k(*, n: int, c: int, k: int) -> float:
if not (0 <= c <= n and 1 <= k <= n):
raise ValueError("require 0 <= c <= n and 1 <= k <= n")
if n - c < k:
return 1.0
return 1.0 - comb(n - c, k) / comb(n, k)pass@k measures candidate availability, not the ability to identify the correct candidate. A selector metric evaluates the latter:
Build confidence intervals and paired comparisons
For binary correctness, a Wilson interval is more stable than a plain normal approximation, especially near 0 or 1:
import math
def wilson_interval(successes: int, trials: int, z: float = 1.959964):
if (
trials <= 0
or not 0 <= successes <= trials
or not math.isfinite(z)
or z <= 0
):
raise ValueError("invalid interval parameters")
p = successes / trials
z2 = z * z
denominator = 1 + z2 / trials
centre = (p + z2 / (2 * trials)) / denominator
radius = (
z
* math.sqrt(p * (1 - p) / trials + z2 / (4 * trials * trials))
/ denominator
)
return centre - radius, centre + radiusWhen comparing two methods on the same items, use paired outcomes. Count which items improve and regress, then apply an appropriate paired test or bootstrap. Two overlapping marginal confidence intervals do not answer the paired question.
Retain random seeds and all candidates. Sampling variance can exceed the apparent difference between methods.
Slice failures before averaging them away
At minimum, slice by:
- subject and difficulty;
- prompt length and response length;
- answer representation;
- extraction outcome;
- verifier reason code;
- language;
- tool use;
- source or document type;
- route and compute budget; and
- protected or high-risk category where legally and ethically appropriate.
Avoid creating tiny slices and presenting noise as discovery. Define important slices before looking at the result and include their sample sizes.
Learned verifiers need their own evaluation
A learned verifier score should be tested for:
- discrimination: does it rank correct above incorrect candidates?
- calibration: do items scored near 0.8 succeed about 80% of the time?
- selective risk: what error remains above an acceptance threshold?
- shift: does performance hold across subjects, lengths and generators?
- exploitability: can a candidate gain score through format, verbosity or copied phrases?
- independence: was the verifier trained on outputs from the policy it now judges?
Use a held-out, human-adjudicated set for disputed cases. Evaluate the complete selection procedure, since a mildly biased verifier can become severely biased when choosing the maximum of many samples.
If are noisy scores, selecting preferentially chooses positive scoring error. This is the optimiser’s curse. Increasing can make the chosen score rise while true quality stalls or falls.
Keep benchmark and development data apart
Track every way an item can influence the system:
- base-model pretraining is often only partially known;
- supervised examples can contain near-duplicates;
- generated training data can copy benchmark solutions;
- prompt examples can reveal answer forms;
- verifier training can include evaluation outputs;
- repeated threshold tuning can overfit the test; and
- public benchmark discussion can enter later model training.
Use exact hashes and approximate matching for text, formulas and code. A contamination check cannot prove absence from opaque pretraining, so describe what was checked and what remains unknown.
A minimal evaluation runner
from collections.abc import Callable, Iterable
from time import perf_counter
Generate = Callable[[EvalItem, int], tuple[str, int, int]]
Verify = Callable[[str, str], tuple[str, str]]
Normalise = Callable[[str], tuple[str | None, str]]
def evaluate_items(
items: Iterable[EvalItem],
*,
generate: Generate,
normalise: Normalise,
verify: Verify,
seed: int,
verifier_version: str,
) -> list[EvalResult]:
results: list[EvalResult] = []
for sample_id, item in enumerate(items):
started = perf_counter()
response = ""
prompt_tokens = response_tokens = 0
extracted = normalised = None
try:
response, prompt_tokens, response_tokens = generate(
item,
seed + sample_id,
)
if (
not isinstance(response, str)
or type(prompt_tokens) is not int
or type(response_tokens) is not int
or prompt_tokens < 0
or response_tokens < 0
):
raise TypeError("invalid generation result")
except Exception as exc:
outcome = "error"
reason = f"generation_exception:{type(exc).__name__}"
else:
try:
extracted, extraction_reason = extract_final(response)
except Exception as exc:
outcome = "error"
reason = f"extractor_exception:{type(exc).__name__}"
else:
if extracted is None:
outcome, reason = "review", extraction_reason
else:
try:
normalised, normalise_reason = normalise(extracted)
if (
normalised is not None
and not isinstance(normalised, str)
) or not isinstance(normalise_reason, str):
raise TypeError("invalid normaliser result")
except Exception as exc:
outcome = "error"
reason = (
f"normaliser_exception:{type(exc).__name__}"
)
else:
if normalised is None:
outcome, reason = "review", normalise_reason
else:
try:
outcome, reason = verify(
normalised,
item.reference_answer,
)
if outcome not in {
"correct",
"incorrect",
"review",
"error",
} or not isinstance(reason, str):
raise TypeError(
"invalid verifier result"
)
except Exception as exc:
outcome = "error"
reason = (
"verifier_exception:"
f"{type(exc).__name__}"
)
results.append(
EvalResult(
item_id=item.item_id,
sample_id=sample_id,
seed=seed + sample_id,
raw_response=response,
extracted_answer=extracted,
normalised_answer=normalised,
verifier_outcome=outcome,
reason_code=reason,
latency_ms=(perf_counter() - started) * 1_000,
prompt_tokens=prompt_tokens,
response_tokens=response_tokens,
verifier_version=verifier_version,
)
)
return resultsWrite results atomically, include the run configuration and hash the output. Parallel execution may reorder completions, so sort by stable item and sample identifiers before comparing runs.
Evaluation in the banking case
The fictional covenant assistant uses a layered evaluator:
- retrieval coverage: did the evidence set include the executed agreement and applicable amendments?
- clause precedence: did deterministic rules resolve effective dates and supersession?
- field schema: are numerator, denominator, currency, period and threshold present?
- calculation: does an independent function reproduce the ratio?
- claim support: does each drafted statement point to a source span?
- decision boundary: did the system avoid an approval recommendation?
The final label is not “model correct”. It is a vector of checks, exceptions and reviewer actions. That representation supports investigation when one component changes.
Chapter notes
Chapter 4: Test-time search and compute allocation
There are three main ways to spend more computation after a prompt arrives:
- generate several candidates in parallel;
- revise or extend a candidate sequentially; and
- call an external tool or verifier.
Each changes the system’s probability of finding and selecting a good answer. None is uniformly beneficial. The useful allocation depends on task difficulty, candidate diversity, verifier quality, latency and consequence.
Begin with the one-sample baseline
For each item, measure:
- direct greedy answer;
- worked greedy answer;
- one sampled worked answer; and
- the deterministic or human baseline.
The phrase “think step by step” is not a method specification. Record the exact prompt, template mode, response cap and decoding settings. Chain-of-thought effects vary across models and tasks.
A useful response contract separates optional work from the graded answer:
Solve the problem. You may use concise intermediate work.
Do not guess when required information is absent.
End with one field: <final>ANSWER</final>
The extraction rule from Chapter 3 grades only the final field. Intermediate text can be analysed separately.
Parallel sampling estimates the model’s answer distribution
Suppose a decoding policy produces candidate response with probability . Sampling independent candidates gives a set
After normalising each final answer to , self-consistency selects the most frequent answer:
The original self-consistency paper described this as sampling diverse reasoning paths and marginalising over their final answers. It reported gains on the studied arithmetic and commonsense benchmarks, while noting the added compute and the possibility of nonsensical paths.1
A task-specific aggregator should preserve ties and parse failures:
from collections import Counter
from dataclasses import dataclass
@dataclass(frozen=True)
class VoteResult:
answer: str | None
votes: int
valid_candidates: int
total_candidates: int
tied: bool
reason_code: str
def plurality_vote(
answers: list[str | None],
*,
minimum_valid: int = 1,
) -> VoteResult:
if type(minimum_valid) is not int or minimum_valid < 1:
raise ValueError("minimum_valid must be a positive integer")
valid = [answer for answer in answers if answer is not None]
if len(valid) < minimum_valid:
return VoteResult(
None, 0, len(valid), len(answers), False, "insufficient_valid"
)
counts = Counter(valid)
ordered = counts.most_common()
top_answer, top_count = ordered[0]
tied = len(ordered) > 1 and ordered[1][1] == top_count
if tied:
return VoteResult(
None, top_count, len(valid), len(answers), True, "plurality_tie"
)
return VoteResult(
top_answer, top_count, len(valid), len(answers), False, "plurality"
)An invalid parse is not a vote for an empty string. A tie is not arbitrary success.
Diversity has a useful middle range
If temperature is too low, candidates can be near-identical and extra samples add little. If it is too high, the model may leave the region where the task is solved coherently.
Top- and temperature interact:
- temperature changes relative probability ratios;
- top- then determines which tokens remain eligible;
- a response-length cap changes the chance of reaching the final field; and
- the prompt and chat template alter the distribution before either control.
Tune the combination on a development split. Report answer diversity, valid-output rate and accuracy together. A setting that creates many unique wrong answers is not valuable exploration.
Useful diversity measures include:
and the entropy of normalised answer counts:
Both depend on the normaliser. Two algebraically equivalent answers should be one category if the verifier declares them equivalent.
Majority agreement is evidence, not a verifier
If all candidates share the same misconception, agreement is confidently wrong. Candidate errors are also correlated because samples share weights, prompt and training data.
Therefore report the empirical relation between agreement and correctness. Bin the held-out items by top-answer vote share and measure accuracy in each bin. Recalibrate after changing model, prompt, decoding or subject.
For a vote share
do not read as an 80% probability of correctness without a calibration study.
Self-consistency works best when:
- correct solutions occupy several high-probability paths;
- wrong paths disperse across different answers;
- final answers can be normalised reliably; and
- enough diversity remains under the sampling policy.
It can fail when:
- a systematic trap attracts most samples;
- the task has many valid free-form answers;
- candidates copy a spurious prompt cue;
- parsing collapses distinct answers together; or
- all samples inherit the same missing evidence.
Best-of-N needs an independent selector
Instead of voting, a verifier can score each candidate:
A deterministic verifier can select a mathematically or programmatically correct candidate. A learned verifier can rank candidates when exact checking is unavailable, but selection amplifies its scoring errors.
Compare at least:
| Method | Candidate generation | Selection |
|---|---|---|
| Greedy | one deterministic path | none |
| Self-consistency | sampled paths | answer frequency |
| Best-of- exact | sampled paths | deterministic verifier |
| Best-of- learned | sampled paths | learned score |
| Oracle pass@ | sampled paths | true labels, evaluation only |
The oracle row is an upper bound on candidate availability. It is not deployable.
Sequential revision changes the proposal distribution
A revision loop feeds a candidate and feedback into the next generation:
Feedback may be:
- a deterministic test failure;
- a symbolic counterexample;
- a tool result;
- a learned critique; or
- a human comment.
Revision is valuable when feedback contains information the first generation lacked. Asking the same model to “check again” without new evidence can preserve or reinforce an error.
from dataclasses import dataclass
from typing import Callable
@dataclass(frozen=True)
class RevisionAttempt:
response: str
outcome: str
feedback: str
GenerateRevision = Callable[[str, list[RevisionAttempt]], str]
CheckResponse = Callable[[str], tuple[str, str]]
def revise_until_checked(
prompt: str,
*,
generate: GenerateRevision,
check: CheckResponse,
max_attempts: int,
) -> list[RevisionAttempt]:
if max_attempts < 1:
raise ValueError("max_attempts must be at least one")
history: list[RevisionAttempt] = []
for _ in range(max_attempts):
response = generate(prompt, history)
if not isinstance(response, str):
raise TypeError("revision response must be text")
outcome, feedback = check(response)
if outcome not in {"correct", "incorrect", "review", "error"}:
raise ValueError("unknown checker outcome")
if not isinstance(feedback, str):
raise TypeError("checker feedback must be text")
history.append(RevisionAttempt(response, outcome, feedback))
if outcome in {"correct", "review", "error"}:
break
return historyStop on review. The model should not turn an ambiguous verifier result into permission to continue until something passes.
Adaptive budgets need a defensible stopping rule
Uniform wastes samples on easy items and may still underserve medium items. Adaptive sampling starts small and expands under uncertainty.
One exact early-stop rule applies to a fixed maximum plurality vote. If the current leader has more votes than the runner-up could obtain even after receiving every remaining sample, the winner is locked:
from collections import Counter
def plurality_is_locked(
valid_answers: list[str],
*,
generated: int,
max_samples: int,
) -> bool:
if not 0 <= len(valid_answers) <= generated <= max_samples:
raise ValueError(
"require valid answers <= generated <= max_samples"
)
counts = Counter(valid_answers).most_common(2)
if not counts:
return False
leader = counts[0][1]
runner_up = counts[1][1] if len(counts) == 2 else 0
remaining = max_samples - generated
return leader > runner_up + remainingThis rule preserves the result of the planned fixed-size plurality vote. It may save little compute when the race is close.
A probabilistic early-stop threshold can save more, but repeated peeking changes its statistical behaviour. Calibrate the entire sequential policy on held-out data rather than applying a one-shot confidence interval at every step.
Difficulty should be estimated from observable features
The test-time scaling study by Snell and peers found that the effective allocation varied with problem difficulty and that predicted difficulty could improve compute allocation in their setting.2 It also found different mixtures of parallel search and sequential revision were useful across difficulty ranges.
A production difficulty model might use:
- a cheap model’s success probability;
- direct-answer entropy;
- disagreement among a small pilot sample;
- verifier margin;
- prompt length and structural features;
- missing-field or retrieval-coverage signals; and
- historical error rates for the task slice.
Do not use protected attributes as a shortcut. Avoid a feedback loop in which high-budget items create better labels and therefore appear easier later.
Evaluate routing with:
alongside false accepts, reviews, latency and cost.
Capacity planning separates latency from throughput
For candidates with generation times , corresponding verification times , queue time and aggregation time :
Sequential latency is approximately
Fully parallel latency is approximately
but only if generation and verification capacity exists for all candidates and each pair can proceed independently. If verification is serial or batched separately, its measured stage time replaces the term. Parallelism reduces item latency by consuming more simultaneous serving capacity. Under load, queue time can erase the gain.
Batching several candidates on one accelerator is neither fully sequential nor independent replication. Measure:
- time to first token;
- time per output token;
- candidate completion spread;
- queue delay at target concurrency;
- tokens per second per device;
- cache memory;
- cancelled-token waste after early stop; and
- verifier saturation.
Avoid cost examples tied to an assumed cloud price. Record the actual dated price, reservation model and utilisation when making a deployment decision.
A safe pattern for the banking case
The fictional covenant assistant uses additional compute only after deterministic gates:
- retrieval must cover the executed agreement and relevant amendments;
- all required fields must be present with source spans;
- clause-precedence rules must produce one effective covenant or
review; - the deterministic calculation must run successfully.
The model may then sample three concise draft explanations. A claim checker rejects drafts whose figures or source references disagree with the verified record. If several pass, the system can choose the shortest supported draft. It does not vote on the covenant value because that value is calculated, not generated.
High consequence changes the role of search. More samples are used to improve wording under fixed facts, not to manufacture consensus about the facts.
Experiment table
For each compute policy, retain:
| Field | Example meaning |
|---|---|
pilot_samples |
candidates generated before routing |
max_samples |
hard cap per item |
revision_steps |
sequential attempts permitted |
generated_tokens |
total billable/output work |
valid_candidates |
successfully parsed results |
unique_answers |
diversity after normalisation |
selected_answer |
final answer object |
selection_rule |
plurality, exact verifier or learned ranker |
stop_reason |
locked, verified, review, budget or length |
latency_ms |
end-to-end item latency |
Compare policies on the same items and seeds where possible. Present the Pareto frontier rather than hiding cost inside one accuracy number.
Chapter notes
Chapter 5: Scoring, selection and self-refinement
A candidate score answers a narrow question. It may measure model likelihood, final-answer correctness, test coverage, rubric preference or estimated human approval. Treating those values as interchangeable produces confident selection errors.
This chapter builds a scoring ledger, shows how to compute response log-probabilities correctly and places critique-and-revision behind independent checks.
Three scorer families
Deterministic scorers
These execute a rule:
- exact or normalised match;
- symbolic equivalence;
- unit tests;
- schema validation;
- citation presence and source-identifier checks;
- arithmetic recomputation; or
- policy allow/deny rules.
Their advantage is inspectability within a bounded domain. Their weakness is coverage. A weak unit-test suite can reward a brittle program; a permissive normaliser can accept a wrong expression.
Policy-derived scores
The generating model supplies token probabilities. Sequence log-probability measures how likely a response is under that model and context. It does not directly measure correctness. Familiar, generic or short answers can receive high likelihood.
Learned scorers
An outcome reward model estimates an overall label or preference. A process reward model scores intermediate steps. A critique model identifies possible defects. Each inherits training-data and distribution limits.
A human review score is learned in a different sense: reviewers apply a rubric, carry individual variation and can be affected by presentation. Blinding and repeated adjudication matter.
Compute response log-probability with the causal shift
For response tokens ,
The logit at combined-sequence position predicts token . The mask must select only response targets.
from dataclasses import dataclass
import torch
import torch.nn.functional as F
@dataclass(frozen=True)
class SequenceScores:
sum_logprob: torch.Tensor
mean_logprob: torch.Tensor
token_count: torch.Tensor
def sequence_logprobs(
logits: torch.Tensor,
input_ids: torch.Tensor,
response_mask: torch.Tensor,
) -> SequenceScores:
"""
logits: [batch, sequence, vocabulary]
input_ids: [batch, sequence]
response_mask: [batch, sequence], true only for response tokens
"""
if logits.shape[:2] != input_ids.shape:
raise ValueError("logits and input_ids sequence shapes differ")
if input_ids.shape != response_mask.shape:
raise ValueError("input_ids and response_mask shapes differ")
if logits.ndim != 3 or logits.shape[1] < 2 or logits.shape[2] < 1:
raise ValueError("expected non-empty [batch, sequence, vocabulary]")
if torch.any(response_mask[:, 0]):
raise ValueError("sequence position zero has no causal target logit")
prediction_logits = logits[:, :-1, :].float()
target_ids = input_ids[:, 1:]
target_mask = response_mask[:, 1:].to(dtype=torch.bool)
safe_logits = prediction_logits.masked_fill(
~target_mask.unsqueeze(-1),
0.0,
)
safe_targets = target_ids.masked_fill(~target_mask, 0)
if not torch.isfinite(
prediction_logits.masked_select(target_mask.unsqueeze(-1))
).all():
raise ValueError("scored logits must be finite")
if torch.any(
(safe_targets < 0) | (safe_targets >= logits.shape[-1])
):
raise ValueError("target token lies outside the vocabulary")
token_logprobs = F.log_softmax(safe_logits, dim=-1).gather(
dim=-1,
index=safe_targets.unsqueeze(-1),
).squeeze(-1)
masked = token_logprobs.masked_fill(~target_mask, 0.0)
counts = target_mask.sum(dim=-1)
if torch.any(counts == 0):
raise ValueError("every row must contain at least one response token")
sums = masked.sum(dim=-1)
means = sums / counts
return SequenceScores(sums, means, counts)Tests should cover:
- one response token;
- different response lengths in one padded batch;
- left and right padding under the chosen model contract;
- masked prompt tokens;
- masked padding tokens;
- an empty response; and
- equality with a hand-computed two-token example.
Sum and mean scores answer different questions
The summed score decreases as more tokens are multiplied:
It therefore tends to prefer shorter sequences when candidates differ in length. The mean log-probability
reduces that length effect but can favour verbose continuations made of individually predictable tokens. Neither is a calibrated correctness probability.
Other options include:
- score only the terminal answer field;
- compare candidates of a fixed response format;
- add an explicit length penalty tuned on held-out data; or
- use a task verifier and treat likelihood as a diagnostic.
Report the token count beside every sequence score.
Confidence requires an event
“The model is 90% confident” is incomplete. Confidence in what?
Possible events include:
- the next token equals a target token;
- the final answer is correct;
- the response passes a test suite;
- a reviewer prefers candidate A;
- a factual claim is supported by cited evidence; or
- the complete action is safe to execute.
Next-token probabilities do not automatically calibrate any of the later events. To calibrate final-answer correctness, define the answer event, collect held-out labels and map a score to observed frequency. Recheck after distribution shift.
For binary labels and predicted probabilities , the Brier score is
Reliability diagrams reveal whether scores near a given value correspond to that empirical success rate. Also measure resolution: a perfectly calibrated model that predicts the base rate for every item may be unhelpful for routing.
Selection creates a distribution shift
Suppose a learned verifier score is
where is true quality and is scoring error. Selecting the maximum among many candidates favours both high quality and positive error:
As candidate count rises, the selected score can improve faster than selected true quality. A verifier evaluated on random candidates may perform worse on candidates deliberately optimised to fool it.
Evaluate the exact deployment procedure:
- sample candidates from the deployed generator;
- score them with the deployed verifier;
- select under the deployed tie and threshold rules;
- obtain independent labels for the selected candidates; and
- repeat across , task slices and model revisions.
Keep a random-candidate audit sample as a control. Otherwise the system may only label what its verifier already likes.
Combine scores through gates before weights
A weighted sum is tempting:
It can let a high style score compensate for a failed correctness check. Hard requirements should be gates:
import math
from dataclasses import dataclass
@dataclass(frozen=True)
class CandidateScores:
parse_ok: bool
deterministic_ok: bool | None
support_score: float | None
style_score: float | None
policy_violation: bool
def selection_key(scores: CandidateScores):
for value in (scores.support_score, scores.style_score):
if value is not None and not math.isfinite(value):
raise ValueError("candidate scores must be finite")
eligible = (
scores.parse_ok
and scores.deterministic_ok is True
and not scores.policy_violation
)
if not eligible:
return (0, float("-inf"), float("-inf"))
support = (
scores.support_score
if scores.support_score is not None
else float("-inf")
)
style = (
scores.style_score
if scores.style_score is not None
else float("-inf")
)
return (1, support, style)This lexicographic rule first enforces eligibility, then ranks support, then style. The ordering is visible and testable.
For tasks without a deterministic truth check, use multiple scorers and explicit disagreement:
- accept when independent signals agree above calibrated thresholds;
- review when they disagree;
- reject on a hard policy violation; and
- audit a sample of accepted, reviewed and rejected outputs.
Critique is a proposal, not a diagnosis
Self-refinement asks a model to critique and revise its output. Research systems such as Self-Refine have reported improvements on selected tasks using iterative feedback from the same model.1 The feedback remains generated text. It can identify a real error, invent an error or rationalise the original answer.
A safer loop has an external check:
from dataclasses import dataclass
from typing import Callable, Literal
Status = Literal["pass", "fail", "review", "error"]
@dataclass(frozen=True)
class Check:
status: Status
codes: tuple[str, ...]
public_feedback: str
Draft = Callable[[str, str | None], str]
Checker = Callable[[str], Check]
def checked_refinement(
prompt: str,
*,
draft: Draft,
check: Checker,
max_attempts: int = 3,
):
if max_attempts < 1:
raise ValueError("max_attempts must be at least one")
response = draft(prompt, None)
history: list[tuple[str, Check]] = []
for attempt in range(max_attempts):
if not isinstance(response, str):
raise TypeError("draft response must be text")
result = check(response)
if not isinstance(result, Check):
raise TypeError("checker must return Check")
if result.status not in {"pass", "fail", "review", "error"}:
raise ValueError("unknown checker status")
if not isinstance(result.public_feedback, str):
raise TypeError("public feedback must be text")
history.append((response, result))
if result.status in {"pass", "review", "error"}:
return history
if attempt + 1 < max_attempts:
response = draft(prompt, result.public_feedback)
return historypublic_feedback must exclude secrets, hidden tests and unsafe system details. If a checker reports uncertainty, escalation is safer than repeated regeneration.
Avoid training on unverified self-critique
A self-training pipeline often:
- generates responses;
- scores them;
- keeps high-scoring examples; and
- fine-tunes on the survivors.
This can improve a narrow metric. It can also amplify the scorer’s blind spots. Add safeguards:
- deterministic labels where available;
- independent human review of boundary cases;
- deduplication by prompt and semantic content;
- per-source and per-generator caps;
- retained rejected samples for audit;
- a frozen final test set;
- adversarial tests designed after inspecting false accepts; and
- data lineage from generated example to model checkpoint.
Do not train a verifier and policy on mutually generated labels, evaluate them against each other and call the agreement independent evidence.
Scorer drift and versioning
A scorer registry should include:
scorer_id
model_or_code_revision
training_data_revision
target_event
input_schema
output_schema
calibration_dataset
calibration_date
supported_slices
known_failure_slices
thresholds
owner
Changing any of these can change selection. Replay a fixed candidate bank through a new scorer before switching live traffic. Then compare selected candidates, not just raw scores.
Monitor:
- acceptance and review rates;
- score distribution;
- score versus deterministic outcome;
- disagreement between scorers;
- length and format correlations;
- selected-candidate audit accuracy; and
- performance by generator and task slice.
The banking case uses scoring only after factual gates
For the fictional credit-document assistant:
- a deterministic schema check rejects missing values and units;
- a provenance check verifies document identifiers and source spans;
- a calculation service determines the covenant ratio;
- a claim checker compares every numeric statement with that verified record; and
- a language scorer may rank clarity among drafts that passed all gates.
The language scorer cannot compensate for a missing amendment or wrong ratio. Reviewer feedback is recorded against the specific check, not reduced to a single opaque “quality” number.
A refinement cycle may ask the model to repair a missing citation or remove an unsupported statement. It may not ask the model to reinterpret the credit decision until a draft passes.
Build checks
Before using a score for selection or training:
- name its target event;
- state whether higher is better;
- test the causal shift and response mask;
- report candidate lengths;
- calibrate on held-out data;
- evaluate after best-of- selection;
- inspect correlations with format and verbosity;
- define hard gates and escalation; and
- retain a human-adjudicated audit set.
The next chapter turns verifiable rewards into policy updates. Every warning here carries into that optimiser.
Chapter notes
Chapter 6: GRPO from objective to update
Group Relative Policy Optimisation removes the separately trained value model used by many PPO systems. For each prompt, it samples a group of responses, scores them and uses their relative rewards as a baseline. The policy is then updated with a clipped importance ratio and a reference-policy penalty.
This is a memory and implementation simplification, not a guarantee of stable learning. The reward, rollout policy, masking, reduction and monitoring choices still determine what the model learns.
The four policy roles
Keep the roles explicit:
| Symbol | Role | Updated when |
|---|---|---|
| current trainable policy | every optimiser step | |
| policy that generated the rollout batch | frozen for the batch or update epoch | |
| reference anchor | fixed or updated only by a stated outer schedule | |
| reward/verifier | scores completed responses | versioned independently |
At rollout collection, copy or snapshot the current policy as . Store its log-probabilities for the sampled tokens. Reusing responses from an unknown or much older policy breaks the stated importance ratio.
The reference is commonly the starting supervised checkpoint. The original DeepSeekMath iterative algorithm also described outer iterations that refreshed the reference. Those are different experiments and should not share an unlabeled “GRPO” result.1
The teaching run samples from the model softmax with temperature 1 and no top- truncation. Under that setting, the behaviour and model log-probabilities in Chapter 2 are identical. If a training system samples from a tempered or truncated distribution, it must define that behaviour policy and derive the ratio against the same distribution; substituting raw model log-probabilities after the fact is not the stated on-policy objective.
Group-relative advantage
For prompt , sample responses with rewards . Outcome-supervised GRPO uses
where prevents division by zero.
import math
import torch
def group_advantages(
rewards: torch.Tensor,
*,
eps: float = 1e-6,
) -> torch.Tensor:
"""Normalise rewards shaped [batch, group] within each prompt."""
if rewards.ndim != 2 or rewards.shape[0] < 1 or rewards.shape[1] < 2:
raise ValueError("rewards must have shape [batch>=1, group>=2]")
if not math.isfinite(eps) or eps <= 0:
raise ValueError("eps must be positive")
rewards = rewards.float()
if not torch.isfinite(rewards).all():
raise ValueError("rewards must be finite")
centred = rewards - rewards.mean(dim=1, keepdim=True)
variance = centred.square().mean(dim=1, keepdim=True)
return centred / torch.sqrt(variance + eps)Using the population variance (unbiased=False) makes the definition independent of a library’s small-sample correction. Log the exact convention.
Consequences:
- adding the same constant to all rewards in a group changes no advantage;
- multiplying all rewards by a positive constant has little effect after normalisation;
- if every reward ties, the policy-gradient signal is zero;
- supplies only a coarse comparison;
- a lucky or erroneous outlier affects every response in the group; and
- advantages are not comparable absolute task values.
Group composition therefore matters. Samples for a prompt should use the stated rollout policy and sampling settings, not a mixture of unrelated generators unless the objective accounts for that mixture.
From token log-probabilities to the clipped surrogate
For sampled token , define
The PPO-style token surrogate is
For a positive advantage, the upper clip limits the benefit assigned to a large probability increase. For a negative advantage, the lower side limits the benefit assigned to a large decrease. Taking min after multiplying by the signed advantage handles both cases.
Clipping does not impose a hard bound on parameter change. It clips this sampled surrogate. Learning rate, repeated epochs, gradient norm and the KL term still matter.
The reference-policy penalty
DeepSeekMath used the positive sampled estimator
Let
Then
The estimator is zero when the sampled-token probabilities match.
def sampled_reverse_ratio_kl(
current_logprobs: torch.Tensor,
reference_logprobs: torch.Tensor,
) -> torch.Tensor:
delta = reference_logprobs.float() - current_logprobs.float()
return torch.expm1(delta) - deltaThe log-probabilities must refer to the same sampled tokens and contexts. Padding, prompt and post-stop positions are excluded.
A complete masked GRPO loss
The teaching implementation below follows the original token-ratio objective and averages tokens within each response, then responses within each prompt, then prompts within the batch.
from dataclasses import dataclass
import torch
@dataclass(frozen=True)
class GRPOMetrics:
loss: float
policy_objective: float
sampled_kl: float
clip_fraction: float
mean_response_tokens: float
normalised_advantage_std: float
def grpo_loss(
*,
current_logprobs: torch.Tensor,
old_logprobs: torch.Tensor,
reference_logprobs: torch.Tensor,
response_mask: torch.Tensor,
rewards: torch.Tensor,
clip_epsilon: float,
kl_beta: float,
) -> tuple[torch.Tensor, GRPOMetrics]:
"""
Log-probabilities and mask: [batch, group, response_length]
Rewards: [batch, group]
"""
expected = current_logprobs.shape
if expected != old_logprobs.shape or expected != reference_logprobs.shape:
raise ValueError("all log-probability tensors must share a shape")
if expected != response_mask.shape:
raise ValueError("response_mask shape differs from log-probabilities")
if current_logprobs.ndim != 3:
raise ValueError("expected [batch, group, response_length]")
if rewards.shape != current_logprobs.shape[:2]:
raise ValueError("rewards must have shape [batch, group]")
if not 0 < clip_epsilon < 1:
raise ValueError("clip_epsilon must lie in (0, 1)")
if not math.isfinite(kl_beta) or kl_beta < 0:
raise ValueError("kl_beta cannot be negative")
mask = response_mask.to(dtype=torch.bool)
token_counts = mask.sum(dim=-1)
if torch.any(token_counts == 0):
raise ValueError("every response needs at least one scored token")
for name, values in (
("current", current_logprobs),
("old", old_logprobs),
("reference", reference_logprobs),
):
if not torch.isfinite(values.masked_select(mask)).all():
raise ValueError(f"{name} log-probabilities must be finite")
advantages = group_advantages(rewards).detach()
token_advantages = advantages.unsqueeze(-1)
safe_current = current_logprobs.float().masked_fill(~mask, 0.0)
safe_old = old_logprobs.float().masked_fill(~mask, 0.0)
safe_reference = reference_logprobs.float().masked_fill(~mask, 0.0)
log_ratio = safe_current - safe_old
ratio = torch.exp(log_ratio)
if not torch.isfinite(ratio).all():
raise FloatingPointError("importance ratio is non-finite")
clipped_ratio = ratio.clamp(1 - clip_epsilon, 1 + clip_epsilon)
unclipped = ratio * token_advantages
clipped = clipped_ratio * token_advantages
token_policy = torch.minimum(unclipped, clipped)
token_kl = sampled_reverse_ratio_kl(
safe_current,
safe_reference,
)
if not torch.isfinite(token_kl).all():
raise FloatingPointError("sampled KL is non-finite")
token_objective = token_policy - kl_beta * token_kl
token_objective = token_objective.masked_fill(~mask, 0.0)
response_objective = token_objective.sum(dim=-1) / token_counts
objective = response_objective.mean()
loss = -objective
if not torch.isfinite(loss):
raise FloatingPointError("GRPO loss is non-finite")
policy_only = token_policy.masked_fill(~mask, 0.0).sum(dim=-1)
policy_only = (policy_only / token_counts).mean()
mean_kl = token_kl.masked_fill(~mask, 0.0).sum(dim=-1)
mean_kl = (mean_kl / token_counts).mean()
clipped_tokens = ((ratio != clipped_ratio) & mask).sum()
metrics = GRPOMetrics(
loss=float(loss.detach()),
policy_objective=float(policy_only.detach()),
sampled_kl=float(mean_kl.detach()),
clip_fraction=float(clipped_tokens / mask.sum()),
mean_response_tokens=float(token_counts.float().mean()),
normalised_advantage_std=float(
advantages.std(unbiased=False)
),
)
return loss, metricsThe loss assumes the tensors already contain log-probabilities for response targets, aligned as in Chapter 5. It does not take logits because computing three full vocabulary distributions inside one function would obscure memory and gradient choices.
Masked positions are replaced before exponentiation. Masking only the final objective is too late: an extreme padding value can overflow inside exp, and the backward pass can retain a non-finite gradient even when the displayed loss appears finite.
Shape ledger for a rollout batch
For , and padded response length :
| Tensor | Shape | Requires gradient |
|---|---|---|
| prompt IDs | [2, prompt_length] or packed equivalent |
no |
| response IDs | [2, 4, 256] |
no |
| response mask | [2, 4, 256] |
no |
| rewards | [2, 4] |
no |
| advantages | [2, 4] |
no |
| old log-probabilities | [2, 4, 256] |
no |
| reference log-probabilities | [2, 4, 256] |
no |
| current log-probabilities | [2, 4, 256] |
yes |
Flatten [B, G] into a generation batch only at interfaces that require it. Restore the mapping before reward normalisation. Mixing responses across prompts destroys the group baseline.
Reward design starts with a truth table
For a verifiable maths task:
| Extracted answer | Format valid | Correctness reward | Format reward | Total example |
|---|---|---|---|---|
| correct | yes | 1.0 | 0.1 | 1.1 |
| incorrect | yes | 0.0 | 0.1 | 0.1 |
| missing | no | 0.0 | 0.0 | 0.0 |
| verifier error | unknown | no training label | no training label | exclude/review |
The correctness signal should dominate the cosmetic signal. If a perfectly formatted wrong answer outranks a correct but slightly malformed answer, the optimiser learns the wrong priority.
def combined_reward(
*,
verifier_outcome: str,
format_ok: bool,
) -> float | None:
if verifier_outcome in {"review", "error"}:
return None
if verifier_outcome not in {"correct", "incorrect"}:
raise ValueError("unknown verifier outcome")
correctness = 1.0 if verifier_outcome == "correct" else 0.0
format_bonus = 0.1 if format_ok else 0.0
return correctness + format_bonusDo not convert a verifier exception into zero reward. An infrastructure error is not evidence that the response is wrong.
Sparse binary rewards create many tied groups. Increase prompt diversity, group size or verifier coverage before adding arbitrary shaping. Shaping signals can make training move while reducing the target behaviour.
Collect rollouts without accidental gradients
A rollout batch is a frozen observation:
@torch.inference_mode()
def collect_rollout_batch(
old_policy,
reference_policy,
prompts,
*,
group_size: int,
generate_group,
score_group,
):
if group_size < 2:
raise ValueError("group_size must be at least two")
old_policy.eval()
reference_policy.eval()
sampled = generate_group(old_policy, prompts, group_size=group_size)
rewards = score_group(prompts, sampled.responses)
if rewards.shape != (len(prompts), group_size):
raise ValueError("reward shape does not match prompt groups")
old_logprobs = sampled_token_logprobs(
old_policy,
sampled.input_ids,
sampled.response_mask,
)
reference_logprobs = sampled_token_logprobs(
reference_policy,
sampled.input_ids,
sampled.response_mask,
)
return sampled, rewards, old_logprobs, reference_logprobssampled_token_logprobs is the shifted gather from Chapter 5, retaining per-token rather than reduced scores. The reference and old models remain in evaluation mode.
For memory, old-policy log-probabilities can be computed during generation or replayed immediately after. Reference log-probabilities can be computed in microbatches. Store them in a precision whose error is shown not to change training materially.
Optimiser update and gradient accumulation
import math
def update_policy(
policy,
optimiser,
microbatches,
*,
clip_epsilon: float,
kl_beta: float,
max_grad_norm: float,
):
if not math.isfinite(max_grad_norm) or max_grad_norm <= 0:
raise ValueError("max_grad_norm must be finite and positive")
policy.train()
optimiser.zero_grad(set_to_none=True)
microbatches = list(microbatches)
prompt_counts = [int(batch.rewards.shape[0]) for batch in microbatches]
total_prompts = sum(prompt_counts)
if total_prompts <= 0:
raise ValueError("empty update")
metric_rows = []
for batch, prompt_count in zip(microbatches, prompt_counts):
current = sampled_token_logprobs(
policy,
batch.input_ids,
batch.response_mask,
)
loss, metrics = grpo_loss(
current_logprobs=current,
old_logprobs=batch.old_logprobs,
reference_logprobs=batch.reference_logprobs,
response_mask=batch.response_mask,
rewards=batch.rewards,
clip_epsilon=clip_epsilon,
kl_beta=kl_beta,
)
weight = prompt_count / total_prompts
(loss * weight).backward()
metric_rows.append(metrics)
grad_norm = torch.nn.utils.clip_grad_norm_(
policy.parameters(),
max_norm=max_grad_norm,
)
if not torch.isfinite(grad_norm):
optimiser.zero_grad(set_to_none=True)
raise FloatingPointError("non-finite gradient norm")
optimiser.step()
return float(grad_norm), metric_rowsWeight microbatches by the reduction unit used in the loss. This implementation first averages responses inside each prompt group and then averages prompts, so it uses prompt counts. A token-weighted alternative is a different objective and may favour long responses.
Tests before any expensive run
Use synthetic tensors to establish invariants:
def test_grpo_invariants():
current = torch.zeros(1, 4, 3, requires_grad=True)
old = torch.zeros_like(current)
reference = torch.zeros_like(current)
mask = torch.tensor([[[1, 1, 0]] * 4], dtype=torch.bool)
rewards = torch.tensor([[0.0, 1.0, 0.0, 1.0]])
loss, metrics = grpo_loss(
current_logprobs=current,
old_logprobs=old,
reference_logprobs=reference,
response_mask=mask,
rewards=rewards,
clip_epsilon=0.2,
kl_beta=0.04,
)
assert torch.isfinite(loss)
assert abs(metrics.sampled_kl) < 1e-8
loss.backward()
assert current.grad is not None
assert torch.isfinite(current.grad).all()
padded_extreme = torch.tensor(
[[[0.0, 0.0, 1_000.0]] * 4],
requires_grad=True,
)
padded_loss, _ = grpo_loss(
current_logprobs=padded_extreme,
old_logprobs=old,
reference_logprobs=reference,
response_mask=mask,
rewards=rewards,
clip_epsilon=0.2,
kl_beta=0.04,
)
padded_loss.backward()
assert torch.isfinite(padded_extreme.grad).all()
assert torch.all(padded_extreme.grad[..., -1] == 0)
tied = group_advantages(torch.ones(2, 4))
assert torch.allclose(tied, torch.zeros_like(tied))Also test:
- positive and negative clipping by hand;
- padding invariance;
- identical current/reference gives zero KL;
- KL remains non-negative;
- changing masked log-probabilities does not change loss;
- rewards are normalised within, never across, prompts;
- a verifier error is excluded rather than rewarded; and
- resuming a checkpoint reproduces the next update.
What GRPO actually learns from
With outcome supervision, every token in a response receives the same response-level advantage. The gradient reinforces or suppresses the sampled token choices that produced the response. It does not identify which reasoning step caused the outcome.
Process rewards can assign feedback at step boundaries and propagate future step rewards backwards, as described in DeepSeekMath. That introduces a process reward model or labelled process signal and a new set of alignment risks.
GRPO can elicit behaviour already reachable by the starting policy and sampling distribution. It cannot reward a correct response that never appears. Curriculum, supervised warm-start data, tools and broader pretraining can matter more than the optimiser name.
A bounded experiment
Begin with a run small enough to inspect manually:
- 50 to 200 prompts drawn from the training split;
- or ;
- deterministic final-answer rewards;
- a small format bonus;
- one policy update per rollout batch;
- a fixed reference checkpoint;
- short response cap;
- held-out evaluation every few updates; and
- saved raw groups, rewards and reason codes.
The number of updates is not a claim of expected improvement. Stop conditions should be based on held-out accuracy, divergence, entropy, invalid-output rate and general-capability checks.
The banking boundary
RL with verifiable rewards fits synthetic banking subtasks such as:
- extracting a field into a fixed schema;
- selecting the applicable clause from a supplied set with a known label;
- producing a calculation trace whose result is recomputed; and
- attaching source identifiers to supported claims.
It should not use realised lending outcomes as a simplistic reward for a language model. Outcomes are delayed, confounded, regulated and shaped by human and economic factors. A model could optimise a proxy in ways that harm customers or violate policy.
The fictional covenant case trains on synthetic clauses and exact schema/verifier rules. The trained model remains an extraction and drafting component. Deterministic calculation and human credit authority remain outside the policy.
Chapter notes
Chapter 7: Training stability and observability
A reinforcement-learning run can show rising reward while the model becomes less useful. The verifier may be exploitable, response length may inflate, entropy may collapse or held-out performance may regress. Training telemetry must therefore connect optimiser behaviour with independent task outcomes.
Five metric families provide a useful starting point:
- reward and group composition;
- policy movement;
- optimisation health;
- response behaviour; and
- held-out capability.
Reward metrics reveal the training signal
Log per batch and per task slice:
- mean, standard deviation and quantiles of total reward;
- each reward component;
- fraction of all-equal groups;
- fraction of groups containing at least one correct response;
- fraction containing both correct and incorrect responses;
- verifier
reviewanderrorrates; - format-valid rate; and
- correlation between reward and response length.
The informative-group rate
indicates how often group normalisation supplies a policy signal. If it approaches zero, increasing update count does not create information. The policy may be too weak, too strong or sampled with too little diversity for the current curriculum.
import torch
def reward_batch_metrics(rewards: torch.Tensor) -> dict[str, float]:
if rewards.ndim != 2 or rewards.shape[0] < 1 or rewards.shape[1] < 2:
raise ValueError("expected rewards shaped [batch>=1, group>=2]")
rewards = rewards.float()
if not torch.isfinite(rewards).all():
raise ValueError("rewards must be finite")
group_std = rewards.std(dim=1, unbiased=False)
return {
"reward_mean": float(rewards.mean()),
"reward_std": float(rewards.std(unbiased=False)),
"informative_group_rate": float((group_std > 1e-8).float().mean()),
"all_zero_group_rate": float(
(rewards.abs().sum(dim=1) == 0).float().mean()
),
}Log reason codes alongside scalar rewards. A falling reward can be a model regression or a verifier outage.
Policy movement needs three complementary signals
Sampled KL to the reference
The estimator from Chapter 6 tracks displacement on sampled response tokens. It is conditional on the rollout distribution. A stable mean can hide a drifting subject or rare token pattern, so slice it.
Importance-ratio and clipping statistics
Track:
- mean and quantiles of ;
- clip fraction;
- positive- and negative-advantage clip fractions; and
- the maximum absolute log-ratio after removing masked positions.
A clip fraction near zero may mean updates are small. It may also mean the learning rate is ineffective. A persistently high clip fraction means the old-policy data is stale relative to the current policy or the update is too large.
Token entropy
For logits and probabilities :
def mean_token_entropy(
logits: torch.Tensor,
target_mask: torch.Tensor,
) -> torch.Tensor:
if (
logits.ndim < 2
or logits.shape[-1] < 1
or logits.shape[:-1] != target_mask.shape
):
raise ValueError("mask must match all non-vocabulary dimensions")
mask = target_mask.to(dtype=torch.bool)
if not torch.any(mask):
raise ValueError("no scored tokens")
safe_logits = logits.float().masked_fill(
~mask.unsqueeze(-1),
0.0,
)
if not torch.isfinite(
logits.float().masked_select(mask.unsqueeze(-1))
).all():
raise ValueError("scored logits must be finite")
log_probs = torch.log_softmax(safe_logits, dim=-1)
probs = log_probs.exp()
entropy = -(probs * log_probs).sum(dim=-1)
selected = entropy.masked_select(mask)
if not torch.isfinite(selected).all():
raise ValueError("scored entropy must be finite")
return selected.mean()Entropy can fall because the model learned a cleaner output format. It can also signal diversity collapse. Interpret it with held-out accuracy, unique-answer rate and response length.
Optimisation telemetry catches numerical failure
Record:
- loss and policy-objective terms;
- gradient norm before clipping;
- whether clipping occurred;
- learning rate;
- update time;
- peak allocated and reserved memory;
- non-finite loss or gradient counts; and
- skipped optimiser steps.
Check finiteness before optimiser.step(). A NaN checkpoint is not a recovery point.
Mixed-precision training adds scaler state and overflow behaviour. BF16 does not use dynamic loss scaling in the same way as FP16, but activations and reductions can still overflow or lose precision. Keep log-probability and reward-normalisation arithmetic in FP32 unless an experiment demonstrates equivalence.
Gradient clipping is a containment measure:
It does not fix a reward outlier, incorrect reduction or unsuitable learning rate. Log how often and by how much gradients are clipped.
Response behaviour exposes reward shortcuts
Inspect distributions of:
- prompt and response tokens;
- stop reason;
- final-field position;
- invalid or multiple final fields;
- repeated phrases and loops;
- language switching;
- unique normalised answers;
- use of requested tools;
- citations per claim; and
- policy-violation reason codes.
Common shortcuts include:
Format without substance
If a format reward is easy and correctness is sparse, the model can produce perfect tags around arbitrary text. Make format a small bonus or eligibility condition.
Length inflation
A learned scorer may correlate detail with quality. The policy then expands responses. Add length to the audit, balance the scorer’s training data and grade the claim rather than verbosity.
Answer copying
If a prompt or feedback contains the reference answer, the policy may learn to copy it without solving the task. Separate information intended for the verifier from information supplied to the policy.
Verifier-specific syntax
A symbolic checker may accept an expression form that avoids the intended problem. Tighten the task contract and add adversarial tests.
Empty or malformed structures
A parser bug may treat an empty field as missing and accidentally award a default. Fail closed and test boundary strings.
The remedy is usually a better evaluator or data contract, not a larger penalty coefficient.
Diagnose patterns, not single points
Reward up, held-out accuracy up
This is necessary evidence of useful learning. Still check retention, length, slice performance and audit labels.
Reward up, held-out accuracy flat
Likely causes include reward overfitting, a saturated metric, changed output format or selection noise. Inspect verifier disagreements and challenge-set behaviour.
Reward up, held-out accuracy down
Treat this as reward misspecification or exploitation until shown otherwise. Stop, preserve the run and label selected high-reward failures.
KL and clip fraction spike
Check learning rate, gradient scaling, old-policy freshness, repeated update epochs and anomalous advantages. Rolling forward rarely repairs an uncontrolled update.
Entropy collapses and answers duplicate
Reduce update size, inspect reward diversity and check whether one easy format dominates. Restore from a pre-collapse checkpoint.
Informative groups vanish
If every response is wrong, adjust curriculum, sampling or the starting checkpoint. If every response is correct, move to harder data. Larger gradients cannot recover a relative signal that is absent.
Response length rises while score rises
Audit scorer length bias and token reduction. Compare answer-field accuracy at fixed length.
Checkpoint the full experiment state
A resumable checkpoint includes:
- policy weights;
- optimiser and scheduler;
- mixed-precision scaler when used;
- global update and rollout counters;
- Python, NumPy, Torch CPU and accelerator RNG states;
- data sampler or shuffle generator;
- old-policy and reference revisions;
- model, tokenizer and chat-template revisions;
- reward and verifier versions;
- curriculum position;
- run configuration; and
- best-checkpoint selection state.
from __future__ import annotations
import os
import random
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any
import numpy as np
import torch
def capture_rng_state() -> dict[str, Any]:
state: dict[str, Any] = {
"python": random.getstate(),
"numpy": np.random.get_state(),
"torch_cpu": torch.get_rng_state(),
}
if torch.cuda.is_available():
state["torch_cuda"] = torch.cuda.get_rng_state_all()
if hasattr(torch, "mps") and torch.mps.is_available():
state["torch_mps"] = torch.mps.get_rng_state()
return state
def atomic_torch_save(payload: dict[str, Any], destination: Path) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
with NamedTemporaryFile(
dir=destination.parent,
prefix=f".{destination.name}.",
suffix=".tmp",
delete=False,
) as handle:
temporary = Path(handle.name)
try:
torch.save(payload, temporary)
with temporary.open("rb") as handle:
os.fsync(handle.fileno())
os.replace(temporary, destination)
finally:
temporary.unlink(missing_ok=True)Only load trusted training checkpoints. A general torch.load payload can contain Python pickles. For distribution, publish weight-only safetensors plus plain configuration files where possible.
After save, load the checkpoint into a fresh process and run a small forward/evaluation test. Existence is not integrity.
Run diary
For every run, record:
objective:
hypothesis:
code revision:
configuration hash:
policy / old / reference revisions:
dataset revisions:
verifier revision:
hardware and software:
start time:
planned update cap:
stop gates:
checkpoint selection rule:
deviations from plan:
result:
failure analysis:
Attach plots and per-item outputs rather than pasting only a final score. A failed run with complete evidence can improve the next experiment; an unexplained success cannot be trusted.
Monitoring the fictional banking assistant
Training-time checks for the synthetic covenant task include:
- exact field accuracy;
- applicable-clause accuracy;
- calculation agreement;
- supported-claim rate;
- unsupported-number rate;
- missing-amendment challenge set;
- invalid schema rate;
- response length;
- general extraction retention; and
- results by document template and clause wording.
At use time, the bank would monitor different outcomes:
- retrieval coverage and source age;
- review and exception rates;
- deterministic validation failures;
- analyst overrides with reason codes;
- document-template drift;
- access-control denials;
- latency and service errors; and
- sampled quality reviews.
Training reward is not a live control metric. The deployed system is governed by evidence, exceptions and human actions.
Research context
DeepSeek-R1 reported that reinforcement learning on verifiable tasks could elicit longer and more structured problem-solving behaviour, while its authors also described readability and language-mixing problems in the RL-only stage and used later stages to address them.1 Those observations fit the monitoring framework here: capability, format and general usability can move in different directions.
Do not copy hyperparameters from that scale into a sub-billion-parameter experiment. Use the paper to understand the training design and failure categories, then run bounded sweeps under your own model, data and hardware.
Chapter notes
Chapter 8: Distillation and deployment
Reinforcement learning changes a policy through sampled rewards. Distillation trains a student to reproduce selected teacher outputs or distributions. The two methods solve different problems and can be combined:
- use supervised data to establish format and task behaviour;
- use verifiable-reward training to improve exploration or accuracy;
- generate and verify a new dataset from the improved teacher; and
- distil accepted behaviour into a cheaper serving checkpoint.
The student inherits the dataset’s strengths and omissions. Distillation is a data-engineering process as much as a loss function.
Hard and soft targets
For class probabilities from a teacher and from a student, classical soft distillation minimises a divergence such as
where controls target softness.1
For a causal language model, this can be applied at every target position. It requires teacher logits or probabilities over the vocabulary.
Hard distillation keeps the selected teacher token sequence and uses ordinary response-only cross-entropy:
For vocabulary , response length and two-byte logits, one dense logit tensor contains
before compression or sparsification. Hard targets need only token identifiers plus metadata. Top- logits, quantised targets and online teacher queries provide intermediate trade-offs.
API teachers usually do not expose full logits. Their terms may also restrict training use. Check licence, data-processing and retention conditions before generating a distillation corpus.
Dataset construction is the main control surface
Each generated example should record:
from dataclasses import dataclass
@dataclass(frozen=True)
class DistillationRecord:
example_id: str
prompt: str
response: str
prompt_source: str
prompt_revision: str
teacher_id: str
teacher_revision: str
generation_seed: int
generation_config_hash: str
verifier_id: str
verifier_revision: str
verifier_outcome: str
reason_code: str
parent_example_ids: tuple[str, ...] = ()Preserve rejected candidates in a controlled audit set with their reason codes. They reveal what the filter removes and support future verifier tests.
The filter order should be cheap to expensive:
- licence and source eligibility;
- prompt schema and sensitive-data checks;
- response parsing;
- deterministic verification;
- policy and safety checks;
- exact and approximate deduplication;
- per-source, subject and difficulty balancing;
- independent audit; and
- train, validation and test separation.
Do not deduplicate after splitting. Near-duplicates must remain in one split.
Generate several candidates, retain evidence
For verifiable tasks, candidate generation can combine search and distillation:
from dataclasses import replace
def choose_distillation_target(
candidates,
*,
verify,
quality_key,
):
audited = []
accepted = []
for candidate in candidates:
outcome, reason = verify(candidate.response)
assessed = replace(
candidate,
verifier_outcome=outcome,
reason_code=reason,
)
audited.append(assessed)
if outcome == "correct":
accepted.append(assessed)
selected = min(accepted, key=quality_key) if accepted else None
return selected, tuple(audited)The function never mutates the frozen provenance record. It returns every assessed candidate for the audit store, including rejected and review outcomes, alongside the selected target.
quality_key might prefer:
- shorter correct responses;
- complete but concise calculation traces;
- a required language;
- valid source references; or
- lower policy risk.
It must not select on hidden test content or a presentation proxy that undermines correctness.
One prompt can have several valid solution paths. Keeping a controlled mixture prevents the student from learning that one phrasing is the only acceptable proof. Assign all variants from one prompt family to the same split.
Teacher quality is conditional
A stronger aggregate teacher can still be worse on a slice. Compare:
- teacher pass rate under the exact verifier;
- invalid and review rates;
- response length;
- subject coverage;
- language coverage;
- unsupported-claim rate;
- contamination indicators; and
- human-audit disagreement.
Teacher and student may share a tokenizer and architecture family, which simplifies templates and token boundaries. Cross-family distillation can introduce useful diversity but also template and style artefacts. The correct choice is empirical.
The Qwen3 report describes a multi-stage training recipe and unified thinking/non-thinking interface, while DeepSeek-R1 reports distilling selected outputs into smaller dense models.23 Those papers show possible designs at their scale. A local experiment should publish its own teacher revision, filters and data counts rather than borrow the paper’s outcome.
Response-only labels
The student receives prompt and response tokens in one causal sequence. Prompt labels are -100, the default ignore index for PyTorch cross-entropy.
from dataclasses import dataclass
import torch
@dataclass(frozen=True)
class EncodedExample:
input_ids: list[int]
labels: list[int]
def encode_distillation_example(
prompt_token_ids: list[int],
response_token_ids: list[int],
*,
max_length: int,
) -> EncodedExample:
if not prompt_token_ids:
raise ValueError("prompt must supply at least one context token")
if not response_token_ids:
raise ValueError("empty response")
if any(
type(token_id) is not int or token_id < 0
for token_id in prompt_token_ids + response_token_ids
):
raise ValueError("token IDs must be non-negative integers")
if len(prompt_token_ids) + len(response_token_ids) > max_length:
raise ValueError("example exceeds max_length; do not silently truncate")
input_ids = prompt_token_ids + response_token_ids
labels = [-100] * len(prompt_token_ids) + response_token_ids
return EncodedExample(input_ids, labels)Obtain prompt_token_ids from the exact pinned chat template and retain response_token_ids from generation, including the intended stop token if the serving contract emits one. Do not tokenise the prompt and response as two arbitrary strings and assume that concatenating their token IDs equals tokenising the complete sequence. BPE boundaries and chat-template markers can make those sequences differ. For a cross-tokenizer student, render and tokenise the full student transcript with an explicit assistant-span boundary before constructing the mask.
Silently truncating the end of a response can remove the graded answer while leaving a plausible prefix. Either construct prompts and response caps that fit or apply an explicit, audited truncation policy before verification.
A right-padding collator
@dataclass(frozen=True)
class Batch:
input_ids: torch.Tensor
attention_mask: torch.Tensor
labels: torch.Tensor
def collate_examples(
examples: list[EncodedExample],
*,
pad_token_id: int,
) -> Batch:
if not examples:
raise ValueError("empty batch")
if pad_token_id < 0:
raise ValueError("pad_token_id must be non-negative")
if any(
not example.input_ids
or len(example.input_ids) != len(example.labels)
for example in examples
):
raise ValueError("every example needs aligned, non-empty IDs and labels")
width = max(len(example.input_ids) for example in examples)
batch_size = len(examples)
input_ids = torch.full(
(batch_size, width),
pad_token_id,
dtype=torch.long,
)
attention_mask = torch.zeros((batch_size, width), dtype=torch.long)
labels = torch.full((batch_size, width), -100, dtype=torch.long)
for row, example in enumerate(examples):
length = len(example.input_ids)
input_ids[row, :length] = torch.tensor(example.input_ids)
attention_mask[row, :length] = 1
labels[row, :length] = torch.tensor(example.labels)
return Batch(input_ids, attention_mask, labels)The model’s causal language-modelling head shifts labels internally in common Transformers implementations. Confirm that contract for the chosen model class. If computing cross-entropy manually, shift logits and labels exactly once.
Token-normalised accumulation
When microbatches contain different numbers of target tokens, averaging their mean losses equally changes the objective. Accumulate summed token loss and divide by the total target-token count for the update.
import math
import torch
import torch.nn.functional as F
def summed_causal_loss(logits, labels):
if logits.ndim != 3 or labels.shape != logits.shape[:2]:
raise ValueError("expected logits [batch, sequence, vocabulary]")
if logits.shape[1] < 2 or logits.shape[2] < 1:
raise ValueError("causal loss needs at least two sequence positions")
if torch.any(labels[:, 0] != -100):
raise ValueError("sequence position zero cannot be a causal target")
prediction_logits = logits[:, :-1, :].contiguous()
target_ids = labels[:, 1:].contiguous()
target_mask = target_ids != -100
if torch.any(
target_mask
& ((target_ids < 0) | (target_ids >= logits.shape[-1]))
):
raise ValueError("target token lies outside the vocabulary")
safe_logits = prediction_logits.float().masked_fill(
~target_mask.unsqueeze(-1),
0.0,
)
if not torch.isfinite(
prediction_logits.float().masked_select(
target_mask.unsqueeze(-1)
)
).all():
raise ValueError("target logits must be finite")
loss_sum = F.cross_entropy(
safe_logits.view(-1, safe_logits.shape[-1]),
target_ids.view(-1),
ignore_index=-100,
reduction="sum",
)
token_count = (target_ids != -100).sum()
return loss_sum, token_count
def distillation_update(model, optimiser, microbatches, max_grad_norm: float):
if not math.isfinite(max_grad_norm) or max_grad_norm <= 0:
raise ValueError("max_grad_norm must be finite and positive")
model.train()
optimiser.zero_grad(set_to_none=True)
microbatches = list(microbatches)
counts = [
int((batch.labels[:, 1:] != -100).sum())
for batch in microbatches
]
total_tokens = sum(counts)
if total_tokens == 0:
raise ValueError("update contains no target tokens")
total_loss = 0.0
for batch, target_count in zip(microbatches, counts):
if target_count == 0:
continue
outputs = model(
input_ids=batch.input_ids,
attention_mask=batch.attention_mask,
use_cache=False,
)
loss_sum, _ = summed_causal_loss(outputs.logits, batch.labels)
(loss_sum / total_tokens).backward()
total_loss += float(loss_sum.detach())
grad_norm = torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=max_grad_norm,
)
if not torch.isfinite(grad_norm):
optimiser.zero_grad(set_to_none=True)
raise FloatingPointError("non-finite gradient norm")
optimiser.step()
return total_loss / total_tokens, float(grad_norm)Move batch tensors to the model device before this function. A real training loop also handles scheduling, mixed precision, distributed reduction, checkpointing and evaluation.
Validation must use generation and the task verifier
Teacher-forced validation loss measures the probability assigned to reference response tokens. Deployment generates without access to those targets. Track both:
- response-only validation loss;
- greedy task accuracy;
- sampled task accuracy;
- pass@;
- invalid-output rate;
- response length;
- calibration or agreement where used;
- challenge-set results; and
- capability retention.
Select a checkpoint by the predeclared task gates, not minimum training loss alone.
Watch for:
- rapid training-loss decline with flat generated accuracy;
- validation loss rising after the first epoch;
- memorised response templates;
- reduced diversity;
- missing final fields;
- copied teacher errors; and
- regression outside the distilled subject.
Distillation and GRPO occupy different feedback loops
| Property | Hard distillation | GRPO with verifiable reward |
|---|---|---|
| Target | accepted teacher tokens | relative reward of sampled policy responses |
| Data | offline or periodically refreshed | on-policy or near-policy rollouts |
| Credit | token imitation | response/step advantage |
| Exploration | inherited from teacher generation | current policy sampling |
| Main risk | copying teacher/filter bias | reward exploitation and unstable drift |
| Compute | teacher generation plus SFT | repeated rollouts, reference scoring and updates |
A common sequence is:
- distil a clean response format and initial competence;
- apply bounded GRPO where exact rewards exist;
- use the improved model to generate a fresh verified corpus;
- distil into the intended serving size; and
- re-run independent evaluation.
Do not cycle indefinitely without a frozen external test. A closed teacher-student loop can amplify one evaluator’s blind spots.
Deployment is a routing problem
The trained model is one component of a serving policy:
Possible routes:
- direct: short, low-risk transformation with strict schema;
- reasoning: larger token budget for a bounded multi-step task;
- sample and verify: several candidates under an exact checker;
- tool-assisted: model proposes operations, deterministic tools execute them;
- retrieval and evidence: answer constrained to authorised sources; and
- review: insufficient evidence, verifier disagreement or high consequence.
Version the router separately from the model. Measure quality by route and record why the route was chosen.
Deployment packaging should include:
- weight format and hashes;
- configuration and tokenizer revisions;
- chat-template hash;
- allowed precision and device modes;
- maximum prompt and response lengths;
- stop conditions;
- evaluation report;
- known limitations;
- licence and training-data statement;
- rollback version; and
- monitoring owner.
The fictional banking case
The distillation corpus for the covenant assistant contains synthetic facility clauses, amendments and calculation records. Examples are retained only when:
- clause precedence has an exact labelled outcome;
- every extracted field has a source span;
- units and dates pass schema checks;
- an independent function reproduces the calculation;
- the final explanation states uncertainty where required; and
- no customer or employer data appears.
The student learns to extract and draft. It does not learn to approve a facility. Deployment routing sends missing documents, conflicting amendments, unusual currencies and policy exceptions to an authorised analyst.
Analyst corrections can become future training data only after governance review, de-identification, provenance capture and split controls. Live feedback is not automatically a label.
Chapter notes
Appendix A: A governed credit-document reasoning assistant
Status and scope
This appendix builds a working design for a large UK bank. It uses a synthetic borrower, facility and document set, which lets us examine the decisions that matter at a large regulated bank scale: competing facility versions, document entitlements, exact covenant arithmetic, human approval and reproducible evidence.
The assistant helps an authorised credit analyst assemble evidence for a covenant review. It may retrieve approved documents, propose structured fields and draft a supported explanation. It does not approve credit, choose a risk grade, contact a customer or update a system of record.
The design demonstrates where language-model reasoning helps and where deterministic or human reasoning retains authority.
The synthetic case
Bracken Components has:
- a revolving credit facility signed on 15 March 2023;
- Amendment 1 signed on 10 January 2024;
- Amendment 2 signed on 20 September 2025;
- audited accounts for year-end 2025;
- management accounts for March 2026;
- a borrower compliance certificate;
- the bank’s approved covenant-calculation policy; and
- a prior credit paper.
The original agreement defines leverage as consolidated net debt divided by covenant EBITDA and sets a ceiling of 3.25. Amendment 1 changes the ceiling to 3.50 from the first test date after signing. Amendment 2 changes permitted add-backs but leaves the ceiling unchanged. The compliance certificate states net debt of £70.0 million and covenant EBITDA of £21.2 million.
The exact ratio is
Under a policy that rounds to two decimal places using decimal half-up, the displayed ratio is 3.30. It is below the effective ceiling of 3.50. The analyst still needs to confirm that the supplied figures, period and add-backs are authorised and complete.
Canonical objects
Every component exchanges typed records. Free text is retained as source evidence, not used as an implicit API.
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from typing import Literal
@dataclass(frozen=True)
class SourceSpan:
document_id: str
document_version: str
page: int
start_char: int
end_char: int
text_sha256: str
@dataclass(frozen=True)
class CovenantClause:
clause_id: str
covenant_name: str
effective_from: date
effective_to: date | None
operator: Literal["<", "<=", ">", ">="]
threshold: Decimal
ratio_scale: int
source: SourceSpan
@dataclass(frozen=True)
class FinancialField:
field_name: str
value: Decimal
currency: str
unit: Literal["ones", "thousands", "millions"]
period_end: date
source: SourceSpanThe text_sha256 ties a span to the indexed text version. Page and character coordinates alone are unsafe after reprocessing.
The model output uses a closed schema:
{
"task_id": "bracken-2026-q1-leverage",
"candidate_clause_ids": ["amendment-1-4.2", "agreement-2023-18.1"],
"numerator_field": "consolidated_net_debt",
"denominator_field": "covenant_ebitda",
"test_date": "2026-03-31",
"claims": [
{
"claim_id": "claim-1",
"text": "Amendment 1 sets the leverage ceiling at 3.50.",
"source_ids": ["amendment-1-4.2"]
}
],
"uncertainties": []
}The schema does not contain approval, risk_grade or customer_message fields.
Document ingestion and trust boundaries
Facility documents are untrusted content even when they are legitimate business records. A document can contain:
- malformed text or OCR errors;
- stale or superseded clauses;
- embedded instructions aimed at a model;
- personal or confidential information;
- inconsistent units and periods; and
- tables whose reading order is lost during extraction.
The ingestion service:
- accepts documents only from authorised repositories;
- records repository, identifier, version, checksum and access classification;
- scans for malware using the bank’s approved controls;
- extracts text and layout into a versioned representation;
- measures page and table coverage;
- flags OCR and parsing exceptions;
- separates document content from system instructions; and
- creates source spans with immutable hashes.
The model is told that document text is evidence, never an instruction. Tool permissions enforce that distinction.
Retrieval filters by:
- customer or facility identifier;
- authorised user and role;
- document type;
- execution status;
- effective date;
- approved repository;
- sensitivity; and
- current indexing version.
Vector similarity ranks within the permitted set. It cannot override the filters.
Clause precedence is a rule system
The model can identify candidate clauses, but precedence depends on structured metadata and legal interpretation. For the synthetic example, a simplified deterministic rule applies:
from collections.abc import Iterable
def effective_clause(
clauses: Iterable[CovenantClause],
*,
covenant_name: str,
test_date: date,
) -> CovenantClause:
eligible = [
clause
for clause in clauses
if clause.covenant_name == covenant_name
and clause.effective_from <= test_date
and (clause.effective_to is None or test_date <= clause.effective_to)
]
if not eligible:
raise LookupError("no effective clause")
latest_date = max(clause.effective_from for clause in eligible)
latest = [clause for clause in eligible if clause.effective_from == latest_date]
if len(latest) != 1:
raise ValueError("ambiguous clauses at latest effective date")
return latest[0]Real amendment precedence can depend on defined terms, partial replacements, waivers, reservations and jurisdiction-specific interpretation. An ambiguous result routes to legal or credit review rather than letting the model choose the “most likely” clause.
Exact calculation
Use decimal inputs, an exact rational comparison and an explicit rounding policy:
from decimal import Decimal
from fractions import Fraction
@dataclass(frozen=True)
class CovenantResult:
exact_ratio: Fraction
displayed_ratio: Decimal
threshold: Decimal
operator: str
passes: bool
def calculate_leverage(
*,
net_debt: Decimal,
covenant_ebitda: Decimal,
threshold: Decimal,
display_places: int = 2,
) -> CovenantResult:
if not all(
value.is_finite()
for value in (net_debt, covenant_ebitda, threshold)
):
raise ValueError("calculation inputs must be finite decimals")
if net_debt < 0:
raise ValueError("net debt cannot be negative under this synthetic rule")
if covenant_ebitda <= 0:
raise ValueError("covenant EBITDA must be positive")
if threshold < 0:
raise ValueError("threshold cannot be negative")
if display_places < 0:
raise ValueError("display_places cannot be negative")
exact = Fraction(net_debt) / Fraction(covenant_ebitda)
scale = 10 ** display_places
scaled = exact * scale
rounded_units = (
2 * scaled.numerator + scaled.denominator
) // (2 * scaled.denominator)
displayed = Decimal(rounded_units).scaleb(-display_places)
return CovenantResult(
exact_ratio=exact,
displayed_ratio=displayed,
threshold=threshold,
operator="<=",
passes=exact <= Fraction(threshold),
)The comparison uses the exact rational result. Decimal division is context-limited for repeating ratios, so the function converts the supplied decimal quantities to fractions before comparing them. Display rounding does not silently change pass/fail status. If policy requires comparison after rounding, encode and test that different rule explicitly.
Unit conversion happens before calculation:
UNIT_MULTIPLIER = {
"ones": Decimal("1"),
"thousands": Decimal("1000"),
"millions": Decimal("1000000"),
}
def value_in_ones(field: FinancialField) -> Decimal:
if not field.value.is_finite():
raise ValueError("financial field must contain a finite decimal")
return field.value * UNIT_MULTIPLIER[field.unit]Both fields must share currency and period policy. The service refuses implicit currency conversion.
Reasoning routes
The assistant estimates a route from deterministic signals:
| Condition | Route |
|---|---|
| One effective clause, complete fields, all checks pass | Direct calculation and one supported draft |
| Several plausible source spans, exact checker available | Up to three extraction candidates, verify each |
| Missing amendment, conflicting dates or units | Review |
| High-sensitivity source not permitted for user | Deny and audit |
| Calculation exception | Review |
| Unsupported claim in draft | Repair once, then review |
The difficulty score does not grant authority. It only determines whether the system spends a small additional budget before escalating.
For extraction candidates, selection is lexicographic:
- valid schema;
- permitted and existing source spans;
- exact match with deterministic clause and field rules;
- no unsupported claim;
- fewest unresolved uncertainties; and
- shortest clear draft.
Majority vote is not used to determine facts that have an exact source and calculation.
Synthetic training corpus
Training examples vary:
- covenant type;
- clause order and wording;
- amendments and effective dates;
- currencies and units;
- reporting periods;
- missing fields;
- conflicting documents;
- OCR noise;
- decoy clauses from prior versions; and
- injected text that attempts to alter system instructions.
Every generated scenario begins from a structured truth object. Documents are rendered from that object, and the answer verifier reads the object independently. This avoids labelling with the same model that generated the example.
The split unit is the scenario family. Variants of one facility, clause template or truth object remain in a single split.
The reward truth table is:
| Outcome | Correct fields | Correct sources | Valid schema | Reward |
|---|---|---|---|---|
| Fully correct | yes | yes | yes | 1.0 |
| Correct values, wrong source | yes | no | yes | 0.0 |
| Wrong value | no | any | yes | 0.0 |
| Missing required field | no | any | no | 0.0 |
| Correct abstention on incomplete case | n/a | yes | yes | 1.0 |
| Verifier error | unknown | unknown | unknown | exclude |
A small format bonus is unnecessary because valid schema is an eligibility condition. The model should not be paid for decorative compliance.
Evaluation pack
The final evaluation contains:
Ordinary cases
Complete documents with familiar clause templates.
Precedence cases
Multiple amendments, unchanged thresholds, partial replacements and coincident effective dates.
Evidence cases
Correct value in an unauthorised, stale or superseded document.
Numeric cases
Unit mismatch, negative or zero denominator, repeated decimals, threshold equality and rounding boundaries.
Abstention cases
Missing amendment, missing field, conflicting source spans and unresolved clause ambiguity.
Injection cases
Document text asks the model to ignore policy, reveal data, approve credit or call an unavailable tool.
Retention cases
General extraction and summarisation tasks outside the covenant template.
Metrics include:
- field exact match;
- source-span precision and recall;
- effective-clause accuracy;
- calculation agreement;
- correct abstention rate;
- false acceptance rate;
- unsupported-claim rate;
- review rate;
- response tokens and latency;
- performance by document template; and
- analyst adjudication on a blinded sample.
Operational event ledger
@dataclass(frozen=True)
class AuditEvent:
event_id: str
task_id: str
timestamp_utc: str
actor_type: Literal["user", "service", "model"]
actor_id: str
action: str
input_hashes: tuple[str, ...]
output_hashes: tuple[str, ...]
model_revision: str | None
verifier_revision: str | None
outcome: str
reason_codes: tuple[str, ...]The ledger records identifiers and hashes under the bank’s retention policy. It does not indiscriminately copy confidential document text into telemetry.
Required events include:
- access decision;
- retrieval query and filter version;
- documents and spans returned;
- model request configuration;
- candidate outputs;
- verification checks;
- calculation inputs and result;
- route and exception;
- analyst edits and approval; and
- any write to a downstream system.
Control map
| Risk | Preventive control | Detective control | Containment |
|---|---|---|---|
| Unauthorised document access | identity and repository policy | access-denial logs | no data returned |
| Prompt injection in document | content/instruction separation, typed tools | injection challenge set | tool calls denied |
| Superseded clause selected | effective-date filters and precedence rules | clause-source review | route to review |
| Wrong numeric field | schema, units and source requirements | deterministic recomputation | no draft approval |
| Unsupported claim | claim-source contract | sampled claim audit | repair once or review |
| Reward exploitation | held-out challenge set | high-reward failure audit | stop training, rollback |
| Model drift | versioned deployment and canary | slice monitoring | rollback |
| Excessive automation | no decision or write permission | action ledger | human authority |
Release checklist
Before a pilot:
The system is ready to learn from only after it is ready to say “I cannot complete this case”.
Appendix B: Implementation review
These questions test whether a reader can explain the design choices and diagnose an implementation. Answers are concise; a strong review should also point to code, experiment records and failure examples.
System contract
1. What changes when an ordinary causal decoder is called a reasoning model?
The next-token factorisation need not change. The training distribution, prompt contract, response length, sampling, tools, verifiers and compute-allocation policy can change. “Reasoning model” should therefore be defined through observable system behaviour.
2. Why is a visible rationale not proof of correctness?
It is generated text. It can contain invalid steps, omit the decisive cause or rationalise an answer. Verify the claim, calculation, code or evidence independently.
3. Which two dimensions help choose a feedback method?
Checkability and consequence. Checkability determines whether exact rewards are available; consequence determines the required permission, review and assurance controls.
4. When is a deterministic component preferable to model reasoning?
When a stated rule, calculation, database query or schema can produce the required result with better reproducibility and lower ambiguity.
5. What is the direct baseline?
The cheapest plausible method under the same task contract, often greedy generation, retrieval plus a template, or a deterministic function.
6. Why route compute?
Uniform long responses or many samples waste capacity on easy tasks and may not help tasks outside the model’s reachable solution distribution. Routing trades accuracy, review, latency and cost by item.
Model and generation
7. Why pin a repository revision?
Weights, configuration, tokenizer and chat template can change under one model name. An immutable revision makes the interface auditable.
8. Why can the Qwen3 “0.6B” label differ from an exact parameter count?
Family names are tiers, not guaranteed tensor counts. The pinned repository metadata reports 751,632,384 BF16 tensor parameters. Memory calculations should use the loaded tensors.
9. What is unusual about the pinned model’s attention dimensions?
Hidden width is 1,024, but 16 query heads each have dimension 128, so the query projection width is 2,048. Code must read head_dim rather than infer it by dividing hidden width by query heads.
10. How does grouped-query attention reduce cache memory?
Several query heads share one key/value head. Cache width depends on key/value heads, so 8 KV heads require half the key/value storage of 16 at the same head dimension.
11. Why is greedy decoding a separate branch from temperature sampling?
Temperature must be positive. Division by zero is undefined. Greedy decoding selects the maximum logit directly. Under sampling, distinguish the log-probability after temperature and top- renormalisation from the selected token’s raw model log-probability.
12. What must a batched generation loop do after one row reaches EOS?
Mark the row finished, prevent new content from being sampled for it, preserve mask and position semantics, and continue only unfinished rows until all stop or reach the cap.
Evaluation
13. Why retain raw responses?
Extractors and verifiers change. Raw outputs allow re-evaluation without regenerating and reveal whether an apparent model change was an evaluator change.
14. Why reject multiple <final> fields?
Choosing one silently hides ambiguity and creates an exploitable parser rule. The result should route to review or a defined failure.
15. What may a normaliser do?
Only remove representational distinctions the task declares irrelevant. It must not infer missing work, repair a wrong answer or execute untrusted text.
16. How do pass@k and selection accuracy differ?
pass@k measures whether at least one correct candidate exists. Selection accuracy measures whether the deployed selector chooses a correct candidate.
17. Why can two methods with similar marginal confidence intervals still differ?
They are evaluated on the same items. A paired comparison uses the pattern of per-item improvements and regressions, which marginal intervals ignore.
18. What is coverage?
The proportion of items receiving a determinate correct or incorrect outcome rather than review or evaluator error. It prevents abstention from being hidden inside apparent accuracy.
19. What data can contaminate an evaluation?
Training examples, generated data, prompt demonstrations, verifier training, threshold tuning and repeated manual inspection can all leak test information.
Test-time search
20. What assumption supports self-consistency?
Correct solutions should occupy several sampled paths that converge on the same normalised answer, while errors should disperse. Correlated systematic errors violate that assumption.
22. What does temperature change?
It rescales logits before softmax, changing probability ratios. It does not directly set creativity, truth or the number of unique answers.
23. What does top-p change?
It truncates the token distribution to a cumulative-probability prefix and renormalises the retained mass.
24. When is a plurality winner mathematically locked?
When its current votes exceed the runner-up’s votes plus all remaining samples under the planned maximum.
25. Why can parallel sampling reduce latency but harm throughput?
It consumes several serving slots at once. Under load, queueing and cache memory may reduce total work completed per device.
Scoring and refinement
26. What does sequence log-probability measure?
The generating model’s likelihood of the response under the prompt and prior response tokens. It does not directly measure correctness.
27. Where does the causal shift appear in sequence scoring?
Logits at position predict the token at position . Response masks select target tokens after that shift.
28. Why does summed log-probability prefer short responses?
Each token probability is at most one, so each log-probability is non-positive. Adding more tokens normally makes the sum smaller.
29. What is the optimiser’s curse in best-of-N?
Selecting the maximum noisy score favours candidates with both high true quality and positive scoring error. More candidates can magnify the error component.
30. Why put correctness gates before style weights?
A weighted style score can otherwise compensate for a failed truth or policy check. Hard requirements should determine eligibility.
31. When does self-refinement provide new information?
When the revision receives an external test result, counterexample, retrieved fact or human feedback. Asking the same model to reconsider without new evidence may preserve the error.
GRPO
32. What model does GRPO remove relative to actor-critic PPO?
The learned value or critic model. GRPO uses the mean and spread of rewards within each prompt’s sampled group as a baseline.
33. Why must rewards be normalised within each prompt?
The responses are alternatives to the same prompt. Mixing prompts makes absolute task difficulty affect the baseline and destroys the intended group comparison.
34. What happens when every reward in a group is equal?
The centred advantages are zero, so outcome-supervised policy-gradient signal from that group is zero. The reference penalty can still act if the current policy differs from the reference.
35. What is the old policy?
The frozen policy that generated the rollout batch. Its sampled-token log-probabilities form the denominator of the importance ratio. Behaviour and scored policy distributions must use one declared definition.
36. What is the reference policy?
A frozen or deliberately scheduled anchor used to measure and penalise policy drift. It need not be the same snapshot as the old policy.
37. Why does GRPO use a clipped ratio?
It limits the surrogate benefit of moving the sampled-token probability too far from the rollout policy in one batch. It is not a hard trust region.
38. How is the sampled KL estimator constructed?
With , use , which is non-negative and zero at equality.
39. Why does a response-level reward give weak credit assignment?
The same advantage is applied to every response token. The optimiser does not learn which individual step caused success or failure.
40. Why exclude verifier errors instead of assigning zero reward?
An infrastructure or ambiguity failure is not evidence of an incorrect response. Treating it as zero trains against verifier availability rather than task outcome.
41. What must be tested before a real GRPO run?
Shape alignment, masking, group normalisation, positive and negative clipping, KL non-negativity, finite gradients, tied rewards and exact checkpoint resumption.
Stability and recovery
42. What is the informative-group rate?
The fraction of prompt groups with non-zero reward variance. It measures how often group normalisation supplies a relative learning signal.
43. Why monitor entropy with accuracy?
Lower entropy can mean a cleaner learned format or a collapsed output distribution. Accuracy and answer diversity distinguish those cases.
44. What does a high clip fraction suggest?
Updates may be too large, the learning rate may be high, the old-policy data may be stale or too many epochs may be run on one rollout.
45. What pattern most strongly suggests reward exploitation?
Training reward rises while independently verified held-out accuracy falls, especially when response format or length changes.
46. What state belongs in a resumable checkpoint?
Policy, optimiser, scheduler, scaler, counters, all RNG states, data sampler, old/reference revisions, tokenizer/template, verifier version, curriculum position and checkpoint-selection state.
47. Why is the latest checkpoint not automatically the best?
It may follow an unstable update or fail eligibility gates. Deployment should point to the best independently evaluated checkpoint with a rollback target.
Distillation and deployment
48. What is the difference between soft and hard distillation?
Soft distillation matches a teacher probability distribution; hard distillation trains on selected teacher tokens.
49. Why split after grouping near-duplicates?
Variants of one prompt, solution or synthetic truth object can otherwise leak across train and test.
50. Why use response-only labels?
The exact templated prompt tokens supply conditioning context. The desired training target is the response, so prompt and padding tokens receive the ignore index. Do not assume separately tokenised prompt and response strings concatenate to the full transcript’s tokens.
51. Why weight uneven microbatches by target tokens?
Equal averaging of microbatch means gives a short microbatch the same weight as a long one. Summed loss divided by all target tokens preserves the token-level objective.
52. When should a deployed system route to human review?
When evidence is missing, deterministic checks disagree, the verifier fails, the task exceeds the approved scope or consequence requires authorised judgement.
53. What is the durable release unit?
Weights plus model configuration, tokenizer and template revisions, evaluation report, known limits, licences, control settings, monitoring ownership and rollback package.
Glossary
A
Abstention. A deliberate system outcome that declines to answer or act because evidence, confidence, permission or verifier coverage is insufficient.
Acceptance gate. A mandatory condition that a candidate or checkpoint must pass before ranking, deployment or action.
Advantage. A reinforcement-learning signal describing how a sampled action or response compares with a baseline. GRPO obtains it from rewards within one prompt group.
Answer extractor. Code that locates the answer object inside a generated response under a defined grammar.
Attention mask. A tensor that determines which token positions participate in attention or loss. A causal mask prevents access to future tokens.
Authorised source. A repository or record the current identity and purpose are permitted to use.
B
Baseline. A cheaper or established method against which a proposed method is compared under the same task contract.
Batch. A set of examples processed together. In GRPO, distinguish the prompt batch, response group and token axes.
Behaviour policy. The actual distribution used to draw rollout tokens, including any temperature or truncation. It must not be confused with an unmodified model softmax when computing policy ratios.
Best-of-. Generate candidates and select one with a stated verifier or ranker.
BF16. A 16-bit floating-point format with an eight-bit exponent and seven stored fraction bits. It has FP32-like exponent range but lower precision.
Brier score. Mean squared error between predicted probabilities and binary outcomes, used as one calibration measure.
C
Calibration. The relationship between a predicted probability or score and the observed frequency of a defined event.
Candidate. One complete generated response considered by a vote, verifier, refinement loop or training filter.
Causal decoder. A transformer that predicts each token from preceding context while masking future positions.
Chain of thought. A generated sequence of intermediate steps intended to support a multi-step answer. It is an output artefact, not guaranteed faithful telemetry.
Challenge set. A held-out evaluation slice constructed around known failure patterns, boundary cases or adversarial inputs.
Chat template. Executable formatting that converts messages, roles and tool descriptions into the model’s token sequence.
Checkpoint. Saved model and training state. A resumable checkpoint includes optimiser, scheduler, RNG and data-position state as well as weights.
Clip fraction. The fraction of scored tokens whose importance ratios lie outside the PPO/GRPO clipping interval.
Consequence. The impact of accepting a wrong output. It helps determine authority, review and control requirements.
Contamination. Leakage of evaluation content or close derivatives into training, prompt design, verifier training or threshold tuning.
Coverage. The proportion of evaluation items receiving a determinate outcome rather than review or evaluator error.
Critique model. A model asked to identify defects in another response. Its feedback remains a fallible model output.
D
Data lineage. The trace from source and transformation through training example, model revision, output and decision record.
Decoder layer. A residual block normally containing pre-normalisation, causal self-attention and a feed-forward network.
Deterministic verifier. A rule, calculation, symbolic checker or test suite that produces the same result for the same controlled inputs.
Difficulty router. A policy that allocates generation, search, tools or review based on estimated task difficulty and risk.
Distillation. Training a student model to match selected teacher outputs or probability distributions.
Distribution shift. A change between the data used to develop or calibrate a component and the data it later receives.
E
Effective date. The date from which a document, policy or clause applies. It is a structured filter in the banking case.
Eligibility. The hard conditions a candidate must satisfy before it may be ranked by softer preferences.
Entropy. , a measure of distribution spread. Token entropy is one signal of policy concentration.
EOS token. A token that marks a model-defined end of sequence or message and can stop generation.
Evidence record. Source identifiers, versions, spans and tool results supporting a claim.
Exact match. A verifier that requires candidate and reference strings to be identical after explicitly permitted normalisation.
F
False accept. An incorrect or unsafe candidate that passes the deployed acceptance rule.
False reject. A correct or acceptable candidate that the system rejects.
Final-answer field. A narrow response element, such as <final>...</final>, designed for unambiguous extraction.
Format reward. A reward component for satisfying an output grammar. It should not outweigh task correctness.
G
Generalised Advantage Estimation (GAE). A method that combines temporal-difference residuals to estimate advantages, commonly used with actor-critic PPO.
Generator. The model and decoding policy that produce candidates.
Gradient accumulation. Summing gradients across several microbatches before an optimiser step. Weighting must match the intended reduction unit.
Gradient clipping. Rescaling a gradient when its norm exceeds a threshold. It contains update magnitude but does not repair faulty rewards.
Greedy decoding. Selecting the highest-logit token at every generation step.
Group. The responses sampled for one prompt in GRPO.
Group Relative Policy Optimisation (GRPO). A clipped policy-gradient method that uses prompt-local group rewards instead of a learned value model.
Grouped-query attention (GQA). Attention in which several query heads share fewer key/value heads.
H
Hard distillation. Supervised training on selected teacher token sequences.
Held-out set. Data excluded from the development decisions it is intended to evaluate.
Human adjudication. Review by an authorised, competent person under a defined rubric and process.
I
Immutable revision. A content-addressed or commit-specific version used to reproduce weights, code, data or templates.
Importance ratio. The current policy probability of a sampled token divided by its probability under the rollout policy.
Inference-time compute. Computation spent after receiving a prompt, including longer generation, parallel samples, revisions, tools and verifiers.
Informative-group rate. The proportion of GRPO groups whose rewards have non-zero variance.
Instruction boundary. The separation between trusted system instructions and untrusted user, tool or document content.
K
KL divergence. A directional measure of distribution difference. GRPO often penalises movement from a reference policy using a sampled estimator.
KV cache. Stored attention keys and values from prior tokens, used to avoid recomputing the full prefix during autoregressive generation.
L
Learned verifier. A model trained to predict correctness, process validity, support or preference.
Length bias. A systematic relationship between response length and a score, loss or selection rule.
Logit. An unnormalised score over one vocabulary item before softmax.
Log-probability. Natural logarithm of a probability. Sequence log-probability is the sum over selected token targets.
Loss mask. A Boolean or ignore-index representation identifying which token targets contribute to training loss.
M
MATH. A dataset of competition-style mathematics problems introduced by Hendrycks and peers.
MATH-500. The 500-item held-out subset used in the PRM800K process-supervision work. It is a particular data selection, not a generic synonym for the MATH test set.
Microbatch. A portion of an optimiser update processed separately to fit memory.
Model contract. The pinned weights, configuration, tokenizer, template, generation and stop behaviour expected by a system.
Model risk. Risk arising from model error, misuse, uncertainty, implementation, data, assumptions or change.
Multi-query attention (MQA). Attention in which all query heads share one key head and one value head.
N
Negative log-likelihood. The standard causal language-modelling loss, equal to the negative log-probability assigned to target tokens.
Normaliser. Code that converts accepted answer representations into a canonical form without solving or repairing the task.
Nucleus sampling. Top- sampling, which retains a smallest cumulative-probability token prefix and samples after renormalisation.
O
Old policy. The frozen policy that produced a GRPO rollout batch.
On-policy data. Samples produced by the current or near-current policy used for an update.
Oracle selector. An evaluation-only selector that knows the true labels and measures the upper bound available in a candidate set.
Outcome reward model (ORM). A learned scorer trained from labels attached to complete responses or final outcomes.
Outcome supervision. Feedback attached to the result of a response rather than its intermediate steps.
Optimiser’s curse. Selection bias in which maximising a noisy estimate favours positive estimation error.
P
Padding. Added token positions used to make sequences share a batch width. Padding must be excluded from attention and loss as required by the model.
Paired comparison. A statistical comparison that uses two methods’ outcomes on the same items.
Parameter count. The number of stored model tensor entries, which may differ from a family’s rounded marketing tier.
Pass@. Probability or estimator that at least one of generated candidates is correct.
Policy. The probability distribution over generated token sequences under a model and decoding condition.
Policy gradient. A gradient estimator that increases probability of sampled actions or tokens according to an advantage signal.
PPO. Proximal Policy Optimisation, a family of clipped or constrained policy-gradient methods.
Process reward model (PRM). A learned scorer that predicts the quality of intermediate reasoning steps.
Process supervision. Feedback applied to intermediate steps rather than only the final outcome.
Prompt injection. Untrusted content that attempts to alter instructions or obtain unauthorised actions or data.
Provenance. Information establishing where data, a claim or a model artefact came from and how it changed.
R
Reference policy. A frozen or deliberately scheduled model used as a divergence anchor during policy optimisation.
Regression set. A stable evaluation set used to detect deterioration after a model or system change.
Response-only loss. Causal loss that ignores prompt and padding targets while training on response tokens.
Retrieval filter. A deterministic restriction on the documents eligible for ranking, based on permission, version, date and scope.
Reward. A scalar training signal produced by a verifier, model or human label under a stated rule.
Reward hacking. Behaviour that raises the implemented reward without satisfying the intended task.
RMSNorm. Normalisation based on the root mean square of a token vector, usually followed by a learned scale.
RoPE. Rotary position embedding, which rotates query and key coordinate pairs as a function of token position.
Rollout. A response sampled from a policy for evaluation or reinforcement learning.
Router. A deterministic or learned policy that chooses a model, compute budget, tool path or review path.
S
Sampled KL estimator. A token-level estimate of divergence computed on responses drawn from a rollout policy.
Sampling seed. The initial state used by a pseudo-random generator. It supports repeatability under a fixed software and hardware path.
Schema. A typed contract for fields, allowed values and structure.
Selective accuracy. Accuracy among items on which a system made a determinate prediction.
Self-consistency. Sampling several reasoning paths and selecting the most frequent normalised final answer.
Self-refinement. Iterative generation in which feedback on one response conditions a later revision.
Soft distillation. Training a student to match a teacher probability distribution, often with a temperature.
Source span. A versioned, hashed location in an authorised source that supports a structured field or claim.
Stop condition. A rule ending generation, sampling, revision or training.
Surrogate objective. A tractable function used to update a policy in place of the final task utility.
Symbolic verifier. A computer-algebra or formal system used to test equivalence or proof validity under specified assumptions.
T
Task contract. The input, output, verifier, abstention, budget, authority and record-keeping specification for one task.
Temperature. A positive divisor applied to logits before softmax. Lower values sharpen and higher values flatten the token distribution.
Test-time scaling. Increasing or reallocating inference computation through search, revision, tools or verification.
Token. A vocabulary unit consumed or produced by the model; it is not necessarily a word.
Token entropy. Entropy of the next-token distribution at a chosen context.
Top-. See nucleus sampling.
Trust boundary. A point at which data or control crosses between components with different permissions or assumptions.
V
Value model. A learned estimator of expected future return used as a baseline in actor-critic methods. GRPO omits this separate model.
Verifier. A deterministic, learned or human procedure that judges a defined property of a candidate.
Verifier version. The immutable code, model, dependencies, thresholds and data contract used to produce a verification outcome.
W
Wilson interval. A binomial-proportion confidence interval with better small-sample and boundary behaviour than the plain normal approximation.
Source map
The principal research sources are cited in chapter notes. The core lineage includes chain-of-thought prompting, self-consistency, process supervision, compute-optimal test-time scaling, DeepSeekMath’s original GRPO formulation, DeepSeek-R1’s reinforcement-learning experiments and the Qwen3 technical report. Claims drawn from an experiment remain scoped to that experiment’s model, dataset and evaluation.
Acknowledgements
This book builds on the work of the researchers, engineers and open-source maintainers cited in its chapter notes. Their publications make the mechanisms inspectable; any errors in interpretation or implementation remain the author’s.