Skip to content

Commit d04f7b9

Browse files
authored
[rl] Add trainer entropy metric (#3848)
## Summary Add a trainer-policy **entropy** metric to watch for entropy -> reward collapse during RL (motivated by checking whether MoE / Qwen3-30B-A3B runs collapse), and surface the existing train/inference **KL** signal. ## Why Entropy collapse is the leading indicator of reward collapse in GRPO/DAPO: the policy sharpens to near-deterministic -> within-group reward variance -> 0 -> zero advantage -> no gradient. Entropy + KL on the dashboard catch this early. ## What - `compute_logprobs(..., return_entropy=True)` returns per-token entropy alongside logprobs from a **single vocab gather + single fp32 upcast** (no extra copy); default call is unchanged (returns logprobs). - DAPO logs **`trainer/entropy/mean`** (mean `H(p)` over response tokens); documents `bit_wise/logprob_diff/mean` as the k1 estimate of `-KL(q||p)` (`-1x` verl's `ppo_kl`). - Rename `train/*` -> `trainer/*` (`grad_norm`, `lr`, `policy_version`); add entropy to the console keys. ## Test plan - Unit: entropy matches `-sum(p log p)` / `log(V)` for uniform; logprobs bit-identical with/without the flag. - Ran locally (`rl_grpo_qwen3_1_7b` search_r1 + alphabet_sort); `trainer/entropy/mean` logs to W&B with the expected sharpen-then-plateau.
1 parent badf21a commit d04f7b9

5 files changed

Lines changed: 41 additions & 12 deletions

File tree

torchtitan/components/loss.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,12 +372,24 @@ def __init__(self, config: Config, *, compile_config: CompileConfig | None = Non
372372
def compute_logprobs(
373373
logits: torch.Tensor,
374374
labels: torch.Tensor,
375-
) -> torch.Tensor:
376-
"""Compute per-position logprobs from logits and labels.
375+
*,
376+
return_entropy: bool = False,
377+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
378+
"""Per-position logprobs from logits and labels, optionally with entropy.
377379
378380
Output shape matches input: ``[batch, seq_len]``. Any DTensor placement
379381
handling is centralized here so RL losses that call ``compute_logprobs`` do
380382
not need to duplicate the vocab-gather logic.
383+
384+
When ``return_entropy`` is set, also returns per-token Shannon entropy
385+
``H(p) = logsumexp(logits) - sum(softmax(logits) * logits)``, shape
386+
``[batch, seq_len]``. Both share the single vocab gather + fp32 upcast.
387+
Entropy is a metric only, so it is computed under ``no_grad``: it never
388+
contributes gradient and must not build an autograd graph over the [B, L, V]
389+
softmax.
390+
391+
Returns ``logprobs`` when ``return_entropy`` is False, else
392+
``(logprobs, entropy)``.
381393
"""
382394
if isinstance(logits, DTensor):
383395
# TODO: pass `grad_placements=[Replicate(), ...]` to make the autograd
@@ -406,13 +418,22 @@ def compute_logprobs(
406418
dst=spmd.I,
407419
)
408420

421+
# Single bf16->fp32 upcast, reused by both logprobs and (optionally) entropy.
422+
logits = logits.float()
409423
B, L, V = logits.shape
410-
return -F.cross_entropy(
411-
logits.float().reshape(B * L, V),
424+
logprobs = -F.cross_entropy(
425+
logits.reshape(B * L, V),
412426
labels.reshape(B * L),
413427
reduction="none",
414428
ignore_index=IGNORE_INDEX,
415429
).reshape(B, L)
430+
if not return_entropy:
431+
return logprobs
432+
with torch.no_grad():
433+
entropy = torch.logsumexp(logits, dim=-1) - (
434+
torch.softmax(logits, dim=-1) * logits
435+
).sum(dim=-1)
436+
return logprobs, entropy
416437

417438

418439
class GradAccumulator:

torchtitan/experiments/rl/actors/trainer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ async def optim_step(self) -> OptimStepOutput:
439439
if len(current_lrs) != 1:
440440
raise ValueError(
441441
"RL metrics only support a single optimizer LR for "
442-
f"train/lr; got {current_lrs}"
442+
f"trainer/lr; got {current_lrs}"
443443
)
444444
current_lr = float(current_lrs[0])
445445

@@ -467,9 +467,9 @@ async def optim_step(self) -> OptimStepOutput:
467467
return OptimStepOutput(
468468
policy_version=self.policy_version,
469469
metrics={
470-
"train/grad_norm/mean": float(grad_norm.item()),
471-
"train/lr": current_lr,
472-
"train/policy_version": float(self.policy_version),
470+
"trainer/grad_norm/mean": float(grad_norm.item()),
471+
"trainer/lr": current_lr,
472+
"trainer/policy_version": float(self.policy_version),
473473
},
474474
)
475475

torchtitan/experiments/rl/losses/dapo.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ def __call__(
7878
(loss, metrics) where loss is a scalar tensor and metrics is a dict of
7979
scalar tensors pre-normalized for SUM reduction across DP ranks.
8080
"""
81-
trainer_logprobs = compute_logprobs(logits, labels)
81+
trainer_logprobs, token_entropy = compute_logprobs(
82+
logits, labels, return_entropy=True
83+
)
8284
# A non-finite generator logprob (notably under cudagraph) has no valid
8385
# old-policy reference, so DROP that token from the loss + denominator (cleaner
8486
# than nan->0, which trains it as if it were on-policy). `response_mask` keeps
@@ -123,13 +125,18 @@ def __call__(
123125
(~torch.isfinite(generator_logprobs)).float() * response_mask
124126
).sum()
125127
/ loss_denominator,
128+
# Mean per-token log-ratio (log p_trainer - log q_generator) over
129+
# sampled tokens. This is the k1 Monte-Carlo estimate of -KL(q || p).
126130
"bit_wise/logprob_diff/mean": diff_for_metrics.float().sum()
127131
/ loss_denominator,
128132
"bit_wise/ratio_tokens_different/mean": (
129133
(diff_for_metrics.abs() > 1e-6).float() * loss_mask
130134
).sum()
131135
/ loss_denominator,
132136
"bit_wise/logprob_diff/max": diff_for_metrics.abs().max(),
137+
# Mean trainer-policy entropy H(p) over response tokens.
138+
"trainer/entropy/mean": (token_entropy * response_mask).sum()
139+
/ loss_denominator,
133140
}
134141

135142
return loss, metrics

torchtitan/experiments/rl/observability/metrics/processor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ class Config(Configurable.Config):
6464
# --- learning signals (not perf, but you want them in the same glance) ---
6565
"loss/mean",
6666
"rollout_reward/_mean",
67-
"train/grad_norm/mean",
68-
"train/lr",
67+
"trainer/entropy/mean",
68+
"trainer/grad_norm/mean",
69+
"trainer/lr",
6970
]
7071
)
7172
"""Regex search patterns selecting console keys for train log lines.

torchtitan/experiments/rl/tests/test_metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ def test_console_prefix_renders_before_step(
460460
(-0.001234, "-0.0012"),
461461
(99.999, "100.00"),
462462
(5, "5.0"),
463-
# Very small values fall back to scientific so train/lr ≈ 2e-6
463+
# Very small values fall back to scientific so trainer/lr ≈ 2e-6
464464
# doesn't render as `0.00000`.
465465
(2e-6, "2.0e-06"),
466466
(1e-5, "0.00001"), # boundary: 5 decimals still resolves the digit

0 commit comments

Comments
 (0)