3.1 From next-token prediction to a policy
A language model with parameters defines a distribution over the next token given a prefix (the prompt plus every token before position ). For a response the probability factorizes autoregressively: This factorization makes the model a policy at two granularities at once. At the token level, the state is the prefix and the action is one token . At the turn level (Chapter 2’s casting), the action is a whole message, such as<call> read </call>, and its log-probability is the sum of the token log-probabilities inside it. Both views use the same per-token numbers. They differ only in how later chapters assign credit to those numbers.
The model outputs logits , one per vocabulary entry. The next-token distribution is the softmax of the logits, so the log-probability of the chosen token is
The second term, the log-partition function, couples the chosen token to every other entry of the vocabulary. In code this is a log_softmax over the vocabulary axis followed by a gather of the chosen index. The batch and time axes pass through untouched: logits of shape [B, T, V] and ids of shape [B, T] produce log-probabilities of shape [B, T].
gather reads one entry along the last axis for every leading index. ids.unsqueeze(-1) gives the index tensor the required trailing axis of length 1, and squeeze(-1) removes it again. The function upcasts half-precision logits before normalizing. Section 3.6 measures why this matters.
We check the function against the negative cross-entropy that every pretraining loop already computes, and against the property that separates a log-probability from a raw logit: adding a constant to every logit leaves the distribution unchanged.
Pitfall. logits.gather(...) followed by a loss runs without error and trains something. It increases the chosen logit without pushing down the alternatives through the partition function, so it optimizes a different objective. Always normalize over the full vocabulary before gathering.
3.2 Stable log-probabilities and entropy
The formula is exact, but each way of evaluating it in floating point fails differently. Computingsoftmax(z) and then taking log underflows: when a logit is far below the maximum, its probability rounds to zero in fp32 and its log becomes -inf. Exponentiating large logits directly overflows instead. The log-sum-exp identity removes both problems by factoring out the maximum :
Every exponent is now at most zero, so nothing overflows, and the largest term contributes exactly 1, so the sum never underflows to zero. torch.log_softmax and torch.logsumexp implement this identity.
softmax().log() reports -inf while log_softmax returns the exact . Policy-gradient code multiplies this value by an advantage. A -inf turns into nan and destroys the whole batch, so the failure is not cosmetic. The naive log-sum-exp of [1000, 999] overflows to inf, and the shifted version returns .
The entropy of the next-token distribution, , measures how spread out the policy is at a prefix. RL monitors it constantly, because a collapsing entropy means the policy has stopped exploring (Chapter 10). We compute it from log_softmax as well. One detail matters: sampling filters such as top-p set some logits to . For those tokens and , and the product is nan in floating point. The limit is 0, so we write that explicitly.
nan. The maximum is worth remembering as a scale. For Qwen3’s vocabulary it is about 11.9 nats, and a trained model’s per-token entropy on code is usually a small fraction of that.
3.3 Sampling defines the behavior policy
A deployed model rarely samples from directly. Inference stacks transform the logits first, and those transformations define the policy that generated the data. Three are standard:- Temperature divides the logits: . A value sharpens the distribution, flattens it, and approaches greedy decoding.
- Top-k keeps the largest logits and renormalizes.
- Top-p (nucleus) sorts the tokens by probability and keeps the smallest prefix whose cumulative probability reaches , then renormalizes.
sampling_log_probs returns the full log-distribution of the sampler, with -inf for excluded tokens, and sample_next draws from it and returns the log-probability of the drawn token under that same distribution.
scatter writes the drop decision from sorted order back to vocabulary order. Greedy decoding is treated as a deterministic policy: the chosen token has probability 1 under it, so its log-probability is 0. That value is correct, but it also shows why greedy data cannot feed a policy-gradient estimator, since the policy assigns zero probability to every alternative it might have explored.
The next block draws 20,000 samples at each of three settings from one six-token distribution and compares the empirical frequencies with the probabilities that sampling_log_probs reports.
3.4 TinyAgent
The rest of the book needs a policy that is small enough to train with RL on a CPU in about a minute, yet is a real autoregressive transformer, so that every phenomenon we study (format learning, entropy collapse, reward hacking, length effects) arises from the same mechanics as in a large model. TinyAgent is a pre-norm decoder-only transformer with learned positional embeddings, a causal mask, and an untied output head. Its interface is the only thing later chapters rely on: ids[B, T] in, logits [B, T, V] out. rollout (Chapter 5) accepts any module with that interface.
We use PyTorch’s nn.TransformerEncoderLayer with a causal mask instead of writing attention by hand. The reader already knows the transformer block, and the book’s subject is what happens around it. Dropout is disabled, because RL needs the forward pass at sampling time and at training time to compute the same distribution. Section 3.7 shows what a mismatch costs.
sequence_log_probs connects the model to the training objective. It runs one forward pass over a complete sequence and returns the log-probability of every token after the first. The logits at position predict token , so the logits are trimmed at the end and the ids at the start. The output has shape [B, T-1], and its column belongs to token ids[:, j+1]. Every mask and every stored log-probability in this book uses that alignment, which we call the predicted positions.
3.5 Generating with a KV-free loop
To act, the policy must generate. The simplest correct generator calls the model on the whole prefix, samples the next token from the logits at the last position, appends the token, and repeats. It recomputes every earlier position at every step, so it does not need a key-value (KV) cache. We record the sampler’s log-probability of each token, because that number is the behavior policy’s probability of the action, and the trainer cannot reconstruct it if the sampling settings are lost.3.6 Real vocabularies: Qwen3-0.6B
TinyAgent’s vocabulary is small enough that the[B, T, V] logits tensor is negligible. In a real model it is often the largest activation in the step. We load the Qwen3-0.6B tokenizer and configuration from the check host’s local cache. Nothing is downloaded.
log_softmax materializes a second tensor of the same size, and autograd keeps what it needs for the backward pass. A typical agentic RL batch of 64 long trajectories would need terabytes. The model weights themselves (0.6 B parameters) are a rounding error by comparison.
Before addressing the memory, we look at what these log-probabilities say. The next block loads the model in fp32 on the CPU and scores the buggy clamp from Chapter 1 and its fix, token by token.
return line. The model finds the buggy body much less likely: min and (lower cost 4.03 and 5.17 nats in the buggy version, while max and (lower cost 0.90 and 0.26 in the fixed one. The sequence log-probabilities differ by more than 10 nats. A pretrained model already carries a prior about what correct code looks like, and RL on coding tasks starts from that prior rather than from the near-uniform policy of an untrained TinyAgent.
clamp (there is no context to predict a function name). The rows diverge at min and max, where the buggy row turns dark. The closing ))\n is unlikely in both rows because the model expected the function to continue.
Now the memory. Log-probabilities of chosen tokens do not require the full [B, T, V] tensor at once. The output layer is a matrix product of the final hidden states [B, T, D] with the embedding matrix [V, D], so we can process the time axis in chunks. Each chunk builds its own logits, normalizes them, gathers the chosen tokens, and discards the logits. The peak logits memory drops from to for chunk length .
3.7 Design problems in practice
Problem: temperature mismatch between rollout and training. Symptom: at the first optimization step after a rollout, before any parameter has changed, the PPO ratio is not 1 and the approximate KL is already positive. The clip fraction is nonzero on a policy that has not moved. Cause: the sampler drew tokens from , and the trainer scores them with . The two are different policies (Section 3.5 measured gaps of up to 0.48 nats on a near-uniform model, and the gap grows with the sharpness of the logits). Options: (a) sample RL rollouts at , which costs whatever quality the lower temperature bought. (b) Divide the trainer’s logits by the same , which is cheap but must be applied everywhere log-probs are computed, including the reference model and entropy. (c) Treat the rollout as off-policy data and correct with importance weights, which adds variance. Default: the book samples at during training and uses the sampling log-probs that the rollout stores, so mismatches show up as a measurable ratio rather than a silent bias. If a deployment needs , apply the same inside the trainer.The next block reproduces the symptom: tokens sampled at , then scored by an untempered trainer, give a ratio far from 1 with no update in between.
Problem: top-p truncation makes log-probs undefined. Symptom:infornanin importance ratios or in KL estimates, usually in a small number of tokens. Cause: under top-p or top-k the behavior distribution gives probability exactly 0 to tokens outside the kept set, so . Any quantity that divides by or evaluates on a token sampled by a different distribution breaks. More subtly, puts mass where has none, so importance sampling from cannot estimate expectations under at all, however many samples are drawn. Options: (a) sample training rollouts without truncation. (b) Store the truncation (or recompute it identically) and evaluate the trainer’s policy on the truncated support, which changes the objective to that of the truncated policy. (c) Clip or mask tokens with extreme ratios, which hides the problem. Default: no top-p or top-k in training rollouts. Truncate only for evaluation and deployment.
Problem: the precision of bf16 log-probs. Symptom: per-token ratios that scatter around 1 by several percent at step 0, and a KL to the reference model that is never exactly 0 even when the policy is the reference. Cause: bf16 has an 8-bit mantissa. Section 3.6 measured a mean error of 0.003 nats per token on Qwen3-0.6B, single-token ratios between 0.955 and 1.135, and a sequence-level gap of 0.52 nats over 499 tokens. Options: (a) compute the output layer and the log-softmax in fp32 (asselected_log_probsdoes for half-precision inputs), which is cheap. (b) Run the whole model in fp32 for log-prob recomputation, which roughly doubles memory and time. (c) Recompute the behavior log-probs in the trainer instead of trusting the engine’s values, and correct for the remaining mismatch (Chapter 13). Default: fp32 normalization always. Recomputeold_logpin the trainer when the engine and trainer use different kernels, and monitor the step-0 ratio as a health metric.
Problem: the memory blow-up of [B, T, V] logits. Symptom: out-of-memory errors that appear only when the response length grows mid-training. Agentic RL lengthens responses over time, so a run that fit at step 100 fails at step 2,000. Cause: the logits tensor scales with , and is about 152k for Qwen3. Section 3.6 computed 20 GB for 8 sequences of 4,096 tokens. Options: (a) chunk the time axis (Section 3.6), recomputing chunks in the backward pass. (b) Use a fused linear-plus-log-softmax kernel that never materializes the logits. (c) Compute logits only at action positions, since tool-output tokens have no loss (Chapter 5), which saves the most when observations dominate the context. (d) Reduce the micro-batch size, which lowers throughput. Default: chunked or fused log-probs from the start, combined with a token-budget batch size (Chapter 14) instead of a fixed number of sequences.
Summary
- A language model is a policy at the token level, and at the turn level through sums of token log-probabilities. The log-probability of a chosen token is , computed with
log_softmaxandgather, and it equals the negative cross-entropy. - Compute log-probabilities and entropy with log-sum-exp, never with
softmax().log(). Handle explicitly when filters set logits to . - Temperature, top-k, and top-p define the behavior policy.
sample_nextreturns the log-probability under the distribution actually sampled, and a trainer that scores the same tokens must use the same transformation or treat the data as off-policy. - TinyAgent is a 158k-parameter causal transformer with the interface ids
[B, T]→ logits[B, T, V].sequence_log_probsaligns its outputs to the predicted positionsids[:, 1:]. - At production vocabulary sizes, the logits tensor dominates memory, so chunked or fused log-probs are required. bf16 log-probs differ from fp32 by about 0.003 nats per token on average and up to 0.13 nats on single tokens, and the errors accumulate over a sequence (measured here on Qwen3-0.6B).
Exercises
- Add a
top_kargument to a copy ofsampling_log_probsand verify, with 20,000 samples as in Section 3.3, that the empirical frequencies match. What is the entropy of a top-k distribution when the remaining logits are equal? - Change an assumption: train-time dropout. Give TinyAgent
dropout=0.1, put it in training mode, and repeat the sampler/trainer comparison of Section 3.5 at temperature 1. How large is the step-0 ratio spread now, and what does that imply for using dropout during RL? - Show that greedy decoding is the limit of temperature sampling by computing
sampling_log_probsof the argmax token for . Explain why greedy rollouts give a policy gradient of zero. - Extend Section 3.6: compute bf16 log-probabilities with the log-softmax also in bf16 (skip the upcast). How much larger is the error? Which tokens are affected most?
- Using
chunked_log_probs, write a version that computes log-probs only at positions where a given mask is 1. For a trajectory in which 80% of tokens are tool output, by how much does the peak logits memory fall? - Design review: a colleague proposes to store only the sampled token ids from the inference engine and to recompute all log-probabilities in the trainer, “since the weights are the same.” List the conditions under which this is exact, the conditions under which it silently changes the objective (temperature, top-p, precision, kernels), and the metric you would add to the dashboard to detect a violation.