Skip to content

Commit baddba0

Browse files
committed
add metrics
1 parent 9524278 commit baddba0

5 files changed

Lines changed: 43 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
@@ -438,7 +438,7 @@ async def optim_step(self) -> OptimStepOutput:
438438
if len(current_lrs) != 1:
439439
raise ValueError(
440440
"RL metrics only support a single optimizer LR for "
441-
f"train/lr; got {current_lrs}"
441+
f"trainer/lr; got {current_lrs}"
442442
)
443443
current_lr = float(current_lrs[0])
444444

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

torchtitan/experiments/rl/losses/dapo.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ 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+
# One vocab gather feeds both: differentiable logprobs (for the loss) and
82+
# detached per-token entropy (a metric, computed under no_grad inside).
83+
trainer_logprobs, token_entropy = compute_logprobs(
84+
logits, labels, return_entropy=True
85+
)
8286
# A non-finite generator logprob (notably under cudagraph) has no valid
8387
# old-policy reference, so DROP that token from the loss + denominator (cleaner
8488
# than nan->0, which trains it as if it were on-policy). `response_mask` keeps
@@ -123,13 +127,18 @@ def __call__(
123127
(~torch.isfinite(generator_logprobs)).float() * response_mask
124128
).sum()
125129
/ loss_denominator,
130+
# Mean per-token log-ratio (log p_trainer - log q_generator) over
131+
# sampled tokens. This is the k1 Monte-Carlo estimate of -KL(q || p).
126132
"bit_wise/logprob_diff/mean": diff_for_metrics.float().sum()
127133
/ loss_denominator,
128134
"bit_wise/ratio_tokens_different/mean": (
129135
(diff_for_metrics.abs() > 1e-6).float() * loss_mask
130136
).sum()
131137
/ loss_denominator,
132138
"bit_wise/logprob_diff/max": diff_for_metrics.abs().max(),
139+
# Mean trainer-policy entropy H(p) over response tokens.
140+
"trainer/entropy/mean": (token_entropy * response_mask).sum()
141+
/ loss_denominator,
133142
}
134143

135144
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)