Policy Gradient · 30 minutes

From REINFORCE to Score Centering

REINFORCE → Actor-Critic → TRPO → PPO → GRPO → Train–Inference Mismatch → Score Centering

Yichuan Wang · yichuan-w.github.io/blog/GDN-train-inference-mismatch-asyncRL
Running example adapted from Tyler Romero’s Policy Gradient for LLMs / Off-Policy RL for LLMs (2026)

← → to navigate · N for notes · F for fullscreen

From REINFORCE to Score Centering1 / 26
Map

In one line: REINFORCE + patches

1992
REINFORCE
can’t backprop through sampling
2000
Actor-Critic
too noisy; credit assignment
2015
TRPO
one step too far breaks training
2017
PPO
TRPO is too expensive
2024
GRPO
the critic is too expensive
2025
Mismatch
trainer ≠ inference engine
2026
Score Centering
make the algorithm robust to it
Classic RL
LLM post-training

Running example

Prompt: “17 × 24 = ?”. After writing “340 +”, the next token is 68 (correct, prob. 0.82) or 58 (slip, prob. 0.07).

Notation

$\pi_\theta$: the policy (LLM) · $R\in\{0,1\}$: verifier reward
$A$: advantage, “how much better than expected”

From REINFORCE to Score Centering2 / 26
1 · REINFORCE

An LLM is a policy

  • State = prefix, action = next token
  • Reward once at the end: correct $R=1$, wrong $R=0$
  • Goal: make high-reward answers more likely

Two completions

“… = 340 + 68 = 408” → $R=1$

“… = 340 + 58 = 398” → $R=0$

From REINFORCE to Score Centering3 / 26
1 · REINFORCE

The hard part: computing $\nabla_\theta J$

Written out, the objective is a sum over every possible completion:

  • $\theta$ appears only in $\pi_\theta(y)$ — how likely each completion is — not in $R(y)$
  • If we could evaluate this sum, we could differentiate it directly…
  • …but there are far too many completions: 150k vocab, 100 tokens → $>10^{{500}}$

Goal: rewrite $\nabla_\theta J$ as an average over samples of something we can differentiate.

From REINFORCE to Score Centering4 / 26
1 · REINFORCE

Applying the log-derivative trick

One identity

Chain rule: $\nabla\log\pi=\nabla\pi/\pi$. Multiply both sides by $\pi$.

REINFORCE: sample N, grade them, average “score × reward”. Unbiased but noisy.

A special case of the policy gradient theorem — Sutton, McAllester, Singh & Mansour (NeurIPS 1999); estimator: REINFORCE (Williams, 1992).

From REINFORCE to Score Centering5 / 26
1 · REINFORCE

What one update does

4 samples for “17 × 24 = ?”:

“… = 408”
R = 1
push ↑
“… = 398”
R = 0
weight 0
“… = 408”
R = 1
push ↑
“… = 418”
R = 0
weight 0

Correct answers get pushed up. Probabilities sum to 1, so the wrong ones lose share.

The problem: the weight is the raw $R$

An easy prompt the model always solves — 4 correct, each worded differently:

“… = 408”
R = 1
push ↑
“… = 408”
R = 1
push ↑
“… = 408”
R = 1
push ↑
“… = 408”
R = 1
push ↑
  • $\hat g$ = the average score, toward the 4 wordings that happened to be sampled. $\mathbb{E}[\hat g]=0$, but a random direction every batch: high variance
  • Fix: weight by $R-b$ (baseline) → expectation still 0, variance ↓; here $b=1$ → $\hat g=0$. $b$ = group mean (group REINFORCE, GRPO) or critic $V$ (Actor-Critic, next)
From REINFORCE to Score Centering6 / 26
2 · Actor-Critic

Why Actor-Critic: what REINFORCE gets wrong

Two problems with REINFORCE

  • Noise: the weight is the raw $R$ — on an always-solved prompt every sample is still pushed up, in a random direction (last slide)
  • Credit assignment: every token gets the same $R$ — good steps and the slip alike

Fix: train a critic $V_\phi(s_t)$ — expected reward from this prefix — and subtract it as a baseline:

Example at “340 +”

$V \approx 0.85$ → pick 68: $A\approx+0.15$, pick 58: $A\approx-0.85$

The slip is pushed down where it happened; an always-solved prompt gives $A\approx0$: no noise. (illustrative numbers)

Why can we subtract $V$?

Fix a prefix $s_t$. There $V(s_t)$ is one number; the next token $a$ is sampled from $\pi(\cdot\mid s_t)$ (we drop $s_t$ below).

Subtracting $V$ changes each sample, not the average: same expected gradient, less noise. Needs $a\sim\pi$ itself — remember this for part 6.

Cost

For LLMs the critic is usually as big as the policy, and hard to train well.

From REINFORCE to Score Centering7 / 26
3 · TRPO

From Actor-Critic to TRPO: limit the step in policy space

Why move on from Actor-Critic?

  • The PG formula is on-policy: $\mathbb{E}_{y\sim\pi_\theta}[\cdots]$ needs samples from the current policy. After one step the batch is stale → sample, step once, throw away
  • Rollouts are expensive → reuse the batch via importance sampling, weighting by $r=\pi/\mu$. But this is only accurate while $\pi$ stays near $\mu$
  • And a small change in $\theta$ can be a big change in $\pi$ → bound the step in policy space (KL)

$\mu$: the policy that generated the rollout. At the first update $\pi=\mu$, so $r$ should be exactly 1 — remember this for part 5.

Downsides of TRPO

  • Fisher-vector products: second-order information, extra backward passes
  • Conjugate gradient: ~10 iterations to approximate $F^{{-1}}g$, each one an FVP
  • Line search: shrink the step until the KL constraint holds and the loss improves — more forward passes
  • Its own optimizer, not Adam — awkward with huge sharded models

PPO’s question: can plain Adam + one simple trick get most of this?

From REINFORCE to Score Centering8 / 26
3 · PPO

PPO: turn the trust region into a clip

A > 0 (good action)gradient = 000.811.22ratio r
A < 0 (bad action)gradient = 000.811.22ratio r

How to read it

  • Good action: up to $1.2\times$, then stop
  • Bad action: down to $0.8\times$, then stop
  • No KL constraint, no second order — plain Adam, several steps per batch
From REINFORCE to Score Centering9 / 26
4 · GRPO

GRPO: no critic — the group mean is the baseline

Sample $G=4$ answers for one prompt:

“… = 408”
R = 1
A = +1
“… = 398”
R = 0
A = −1
“… = 408”
R = 1
A = +1
“… = 418”
R = 0
A = −1
  • No critic → one less large model
  • Every token in an answer shares its $\hat A_i$
  • Everything else from PPO: ratio $r$ + clip

Follow-ups (Dr. GRPO, DAPO) fix details: std / length normalization, all-correct groups, clip-higher.

From REINFORCE to Score Centering10 / 26
5 · Train–Inference Mismatch

Same weights, same tokens — should the log-probs match?

  • RL runs on two engines: the trainer (TorchTitan / Megatron) recomputes $\log\pi$, the generator (vLLM / SGLang) reports $\log\mu$
  • Weights are bit-identical (same tensor, copied across); the tokens are the same tokens
  • So at the first update we expect $r=\exp(\log\pi-\log\mu)=1$ exactly

$r$ is an exponential of a difference: an error of 0.1 in log-prob is ~10% in $r$ — and PPO’s clip is only ±0.2. In MoE RL, router mismatch alone can collapse training (R3).

They don’t match. Why can two systems with the same weights and the same tokens produce different log-probs? The answer is three layers down:

③ Engine
Why would the computation differ?
trainer and generator are two codebases
② Kernel
Why would the order change?
the kernel picks the order from the batch shape
① Floating point
Why ≠ with the same inputs?
addition depends on the order
read bottom → top ↑
From REINFORCE to Score Centering11 / 26
5 · Train–Inference Mismatch

① Floating point: addition depends on the order

① Floating point→② Kernel→③ Engine

In math: $(a+b)+c=a+(b+c)$. In floating point, $\mathrm{{fl}}(\cdot)$ rounds to a representable value after every operation:

Every rounding step throws away bits. Which bits depends on what you added first — the error is a function of the order.

bf16 — what we train in:

sign
1
exponent
8
mantissa
7
  • Same range as fp32, but only 7 mantissa bits (fp32: 23) ≈ 2–3 decimal digits
  • To add, shift the smaller number to the larger exponent → bits past the 7th fall off
From REINFORCE to Score Centering12 / 26
5 · Train–Inference Mismatch

① Example: $a+b+b$ in bf16

① Floating point→② Kernel→③ Engine

$(a+b)+b$

$a+(b+b)$, with $b+b=2^4(1+\cdots+\tfrac1{{2^7}})$ exact

Same three numbers — the two orders discard different terms, so the results differ.

Here extra terms are simply dropped. GPUs round to nearest-even — same effect, e.g. $a=127.5,\ b=1$: $(a+b)+b=129$, $a+(b+b)=130$.

From REINFORCE to Score Centering13 / 26
5 · Train–Inference Mismatch

② Kernel: the batch shape picks the summation order

① Floating point→② Kernel→③ Engine

Every linear layer is a matmul; every output number is one big sum:

Qwen3.5-9B: 4096 additions per output number.

  • A GPU (~150 SMs) doesn’t add left to right — it partitions the sum to keep busy
  • Large batch: many output tiles, GPU already full → one worker walks $k=0\ldots4095$ → a chain
  • Small batch: too few tiles, SMs idle → split K across workers, add the partials afterwards → a tree
small batch → split-K (tree)large batch → one pass (chain)p0p0p1p1p2p2p3p3p0+p1p2+p3(p0+p1)+(p2+p3)((p0+p1)+p2)+p3tree ≠ chain in floating point → different bits

Both are correct; neither is more correct. A heuristic picks one at launch time, from the shapes. So the reduction order is a property of the shape, not of the math.

From REINFORCE to Score Centering14 / 26
5 · Train–Inference Mismatch

Measured: hold row 0 fixed, change only the batch

① Floating point→② Kernel→③ Engine

4096×4096 fp32 matmul, one B300. Row 0’s input bytes are identical in every row of this table.

batch Mmax |diff| vs M=1cuBLAS strategy
10split-K
2384split-K
8384split-K
128416split-K
2561249.25no split-K
20481249.25no split-K

No randomness: rerun it and every number reproduces exactly. cuBLAS is deterministic given a shape — the shape just picks the algorithm.

You can watch cuBLAS decide (torch.profiler, kernel names):

M= 1 splitKreduce_kernel M= 2 cutlass_80_simt_sgemm_128x32 + splitKreduce_kernel M= 128 cutlass_80_simt_sgemm_256x128 + splitKreduce_kernel M= 256 cutlass3x_sm100_simt_sgemm_128x64x16 M=2048 cutlass3x_sm100_simt_sgemm_128x64x16

splitKreduce only adds up the partial sums (the tree) — gone at M ≥ 256.

At the model level — Qwen3.5-9B, bf16, same prompt repeated in the batch:

0.11
max |Δ logit|, batch 1 vs 32
0.824
measured $r$ over 133 tokens
1.000
what it should be

An 18% error in $r$ — from nothing but batch composition. PPO’s clip range is ±0.2.

From REINFORCE to Score Centering15 / 26
5 · Train–Inference Mismatch

What we need: batch invariance — not determinism

① Floating point→② Kernel→③ Engine
Bitwise parity
trainer’s $\pi$ = generator’s $\mu$, bit for bit
two engines agree
Batch invariance
$f(x\mid B_1)=f(x\mid B_2)$
don’t be influenced by your neighbours
Determinism
$f(B)=f(B)$
don’t contradict yourself
each rung needs the one below ↑
Trainer batch: [ x, train sample 1, train sample 2, … ] Rollout batch: [ x, request 17, request 42, request 93, … ]

They never see the same batch → determinism doesn’t help.

It always comes down to one thing: a reduction along the batch axis. Thinking Machines’ batch_invariant_ops:

OpWhy batch-variantFix
GEMMtile size and split-K picked per shapefixed partition
RMSNormfew rows → reduction split across the batch axisdon’t reduce along batch
attentionsplit-KV count depends on the batchnum_splits=1

DP / FSDP add no mismatch — they don’t reduce along the batch axis.

From REINFORCE to Score Centering16 / 26
5 · Train–Inference Mismatch

③ Engine: same weights, two codebases

① Floating point→② Kernel→③ Engine
same θ — one checkpoint, copied across
TRAINER
TorchTitan / Megatron
tuned for throughput: big packed batches
↓
model implementation A
↓
kernels A
↓
$\log\pi(a\mid s)$
GENERATOR
vLLM / SGLang
tuned for latency: paged KV, continuous batching, split-KV
↓
model implementation B
↓
kernels B
↓
$\log\mu(a\mid s)$

Batch-invariant kernels make $f_A(x)$ consistent with itself — but we need $f_A=f_B$. If the two sides implement the forward differently, aligning the kernels underneath is aligning the wrong layer.

Real example

radixark/miles#975: Qwen3.5’s A_log was rounded to bf16 on the Megatron side while SGLang kept fp32. Fix: keep it fp32 through Megatron’s Float16Module wrap. Found by hand — per op, per model, per release.

Async RL loops (slime, open-instruct) compute $r$ from raw $\mu$ and $\pi$ — they only see the sum. When a run breaks, you can’t tell async from infra.

From REINFORCE to Score Centering17 / 26
5 · Train–Inference Mismatch

The fix, layer by layer: drive it to exactly zero

① Floating point→② Kernel→③ Engine
LayerFix
① Floating pointCan’t change the arithmetic → fix the order, same dtype on both sides
② KernelBatch-invariant kernels (the table two slides back)
③ EngineOne model definition: vLLM keeps its KV cache and scheduling, every layer is the trainer’s op
New in Qwen3.5Gated DeltaNet ships chunked (train) and recurrent (decode) kernels — two algorithms. → recurrent on every forward, chunked for backward

Result: $\log\pi=\log\mu$ — not small, exactly zero, bit for bit. Parity only needs the forward to match ($\pi$, $\mu$ are forward quantities), so backward stays fast.

Qwen3.5 batch-invariant mode dataflow
From REINFORCE to Score Centering18 / 26
5 · Train–Inference Mismatch

Example: MATH at offpolicy = 12 — three levels of alignment

MATH offpolicy=12: logprob gap and training reward for three arms

Three arms

red vLLM native (two engines) · brown unified model, stock kernels · grey unified + BI kernels

What you see

Step 0: exactly 0. Grey keeps the lowest gap and reaches the highest train reward (and held-out peak).

How to use it

Unified model is free — always do it. BI costs 2–3× (≈5× on agents) → turn it on for 20 steps to debug: gap = 0 and still broken → not infra.

From REINFORCE to Score Centering19 / 26
6 · Score Centering

Why mismatch hurts: drift

Notation. The update from a rollout sums its token scores, each weighted by the rollout’s reward (or advantage) $R$. Look at one token $y_t$ given its prefix $y_{{<t}}$; $v$ indexes the vocabulary. $\pi_v$ and $\mu_v$ are the next-token probabilities of $v$ under the trainer $\pi$ and the sampler $\mu$; $s_v=\nabla_\theta\log\pi_v$ is its score; $\bar s=\sum_v\mu_v s_v$ is the expected score under the sampler.

Constant reward $+1$: no signal, so there should be no learning. On-policy that holds at every prefix:

Under mismatch the token is sampled from $\mu$ but the score goes through $\pi$, so $\bar s=\mathbb{E}_\mu[s_{{y_t}}]\neq0$. With $\mathbb{E}[AB]=\mathbb{E}[A]\,\mathbb{E}[B]+\mathrm{{Cov}}(A,B)$:

Only the signal sees which token led to which reward. The drift depends on the rewards only through their mean.

Drift = distillation toward the sampler

  • $\bar s$ is minus the gradient of the cross-entropy with $\mu$ as the teacher → drift = SFT on the sampler’s distribution, at every prefix, scaled by $\mathbb{E}_\mu[R]$
  • With a constant reward ($R=1$), minimizing that loss trains $\pi$ to imitate $\mu$: off-policy policy gradient is supervised distillation of the trainer toward the sampler. It pulls $\pi$ toward whatever $\mu$ does — including every quirk of its numerics or staleness.

On-policy ($\mu=\pi$): $\bar s=0$, no drift. Group centering shrinks it but can’t remove it: the expected advantage at a prefix isn’t 0.

From REINFORCE to Score Centering20 / 26
6 · Score Centering

Score centering: subtract the expected score

  • Drift is cancelled exactly, at every prefix
  • At each prefix, same as on-policy except the covariance is under $\mu$ (the token and the rest of the rollout), not $\pi$. Prefixes are also visited under $\mu$.
  • No mismatch ($\pi=\mu$) → $\bar s=0$ exactly → SC changes nothing, sample by sample (top-$k$: $\rho=1$, all coefficients 0)

$y_t$: sampled token · $s_v=\nabla_\theta\log\pi_v$: score of token $v$ (through the trainer) · $\pi_v$ / $\mu_v$: trainer / sampler prob. · $\bar s$: expected score under the sampler · $\mathrm{{Cov}}_\mu$: covariance with $y_t$ and the rest of the rollout drawn from $\mu$

Importance samplingScore centering
Correction× ratio $\pi/\mu$− $\bar s$ (additive)
Depends on token?yes, huge on rare tokensno, fixed per prefix
In practiceclipped → drift leaks backexact, no hyperparameters

They compose (SC on top of TIS / MIS). Qwen3-30B-A3B, INT8 + INT4-KV sampler: SC 30%, TIS 12%, others < 5%.

From REINFORCE to Score Centering21 / 26
6 · Score Centering

Implementation: sampler top-$k$ + one scalar loss

$\bar s$ needs the sampler’s full distribution: 152K vocab × fp32 = 608 KB per token → 1024 rollouts × 32K tokens = 20 TB. So log only the sampler’s top-$k$ ($k=128$) as the head $H$, and fill the tail with the trainer, rescaled:

Same $\bar s$ as before; only the tail is approximated, $\mu_v\approx\rho\,\pi_v$ — and it collapses to only $k$ terms:

$k=128$ and even $k=32$ match full SC; <1% wall-clock overhead. vLLM / SGLang already return top-$k$ logprobs.

As a scalar loss (sg = stop-gradient):

def sc_loss(train_logp, samp_logp, topk_ids, tok, adv): head = train_logp[topk_ids] rho = (1 - exp(samp_logp).sum()) / (1 - exp(head).sum()) coef = exp(samp_logp) - rho * exp(head) corr = (stop_gradient(coef) * head).sum() return -adv * (train_logp[tok] - corr)

Adapted from the paper’s JAX snippet (Sec. 4.1). Differentiating gives $A\,(s_{y_t}-\bar s)$. $\pi_v$ / $\mu_v$: trainer / sampler prob. of token $v$ · $y_t$: sampled token · $A$: advantage · $H$: sampler’s top-$k$ tokens · sg: stop-gradient.

From REINFORCE to Score Centering22 / 26
6 · Score Centering

Why only LLMs?

Subtracting the expected score is an obvious idea — why wasn’t it used before?

Classical off-policy RL (robotics)

  • $s$: state, $a$: action — continuous
  • On-policy it’s 0; off-policy it’s an intractable integral
  • → everyone uses importance sampling instead

LLMs under train–inference mismatch

  • $\mathcal V$: vocabulary; $\mu_k,\pi_k$: sampler / trainer prob. of token $k$
  • Nonzero: $\mu\neq\pi$ (numerics, staleness)
  • Computable exactly: a finite sum over next-token logprobs (top-$k$=128 is enough)

“RL training of LLMs under TIM is unusual in that the expected score is both nonzero and computable exactly.” — Marek & Ryabinin (2026)

From REINFORCE to Score Centering23 / 26
6 · Score Centering

Async RL — and SC + async is unexplored

Async RL architecture: rollouters, controller loops, bounded off-policy buffer, vLLM engines, trainer, TorchStore weight sync

Rollouts ∥ training · buffer bounds off-policyness · TorchStore weight sync

In-place weight update: pause, load new weights, resume decoding with the KV cache kept

In-place update: pause → load $w_{{k+1}}$ → resume, KV kept → one sequence, two weight versions

Future research: SC + async RL has not been explored

Theory. Signal is $\mathrm{{Cov}}_\mu$, not $\mathrm{{Cov}}_\pi$ — under staleness, a bias nobody has bounded.

Practice. Only test: 0.6B, sampler synced every 64 steps — no real async pipeline; SC alone < SC + TIS.

Key questions. ① How many async (off-policy) steps can SC tolerate? ② Can SC replace IS in async RL — removing the bias that clipped IS introduces?

From REINFORCE to Score Centering24 / 26
Summary

Takeaways

MethodProblem it fixesCore idea
REINFORCEcan’t differentiate sampling$R\,\nabla\log\pi$
Actor-Criticnoise, credit assignmentcritic baseline $R-V(s_t)$
TRPOsteps that go too farratio $r$ + KL trust region
PPOTRPO is too expensiveclip $r$ to $[0.8,\,1.2]$
GRPOcritic is expensivegroup mean as baseline

Mismatch, route 1: make it zero (systems)

Fix all three layers (floating point → kernel → engine) → bitwise parity. Exact, but 2–3× slower → use it to debug.

Mismatch, route 2: make the algorithm robust

IS (TIS / MIS) or score centering: subtract $\mathbb{E}_\mu[\nabla\log\pi]$. Cheap (log top-k probs). + async: still open.

From REINFORCE to Score Centering25 / 26
References

References · Q&A

Q & A
Speaker notes
⏱ 00:00