Skip to content

Commit b4ae6ec

Browse files
committed
add metrics
1 parent d9ebce1 commit b4ae6ec

5 files changed

Lines changed: 71 additions & 17 deletions

File tree

torchtitan/components/loss.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -365,15 +365,13 @@ def __init__(self, config: Config, *, compile_config: CompileConfig | None = Non
365365
self._maybe_compile(compile_config)
366366

367367

368-
def compute_logprobs(
369-
logits: torch.Tensor,
370-
labels: torch.Tensor,
371-
) -> torch.Tensor:
372-
"""Compute per-position logprobs from logits and labels.
368+
def _gather_vocab_logits(logits: torch.Tensor) -> torch.Tensor:
369+
"""Redistribute vocab-sharded TP logits to full-vocab local logits.
373370
374-
Output shape matches input: ``[batch, seq_len]``. Any DTensor placement
375-
handling is centralized here so RL losses that call ``compute_logprobs`` do
376-
not need to duplicate the vocab-gather logic.
371+
Per-token quantities that need the full vocab distribution (logprobs,
372+
entropy) share this gather so the DTensor / spmd_types handling lives in one
373+
place. Returns a plain (local) tensor; a non-vocab-sharded input is returned
374+
unchanged.
377375
"""
378376
if isinstance(logits, DTensor):
379377
# TODO: pass `grad_placements=[Replicate(), ...]` to make the autograd
@@ -401,7 +399,17 @@ def compute_logprobs(
401399
src=spmd.S(-1),
402400
dst=spmd.I,
403401
)
402+
return logits
403+
404404

405+
def _logprobs_from_full_vocab(
406+
logits: torch.Tensor, labels: torch.Tensor
407+
) -> torch.Tensor:
408+
"""Per-token logprobs from already-gathered full-vocab ``logits`` [B, L, V].
409+
410+
Split out so ``compute_logprobs`` and ``compute_logprobs_and_entropy`` share
411+
one logprob code path (and one gather at the caller).
412+
"""
405413
B, L, V = logits.shape
406414
return -F.cross_entropy(
407415
logits.float().reshape(B * L, V),
@@ -411,6 +419,44 @@ def compute_logprobs(
411419
).reshape(B, L)
412420

413421

422+
def compute_logprobs(
423+
logits: torch.Tensor,
424+
labels: torch.Tensor,
425+
) -> torch.Tensor:
426+
"""Compute per-position logprobs from logits and labels.
427+
428+
Output shape matches input: ``[batch, seq_len]``. Vocab-sharded TP logits are
429+
gathered via ``_gather_vocab_logits`` so callers do not duplicate that logic.
430+
"""
431+
return _logprobs_from_full_vocab(_gather_vocab_logits(logits), labels)
432+
433+
434+
def compute_logprobs_and_entropy(
435+
logits: torch.Tensor,
436+
labels: torch.Tensor,
437+
) -> tuple[torch.Tensor, torch.Tensor]:
438+
"""Per-token logprobs and Shannon entropy from a single vocab gather.
439+
440+
Equivalent to ``compute_logprobs`` plus a separate entropy pass, but gathers
441+
the (TP-sharded) vocab only once.
442+
443+
- logprobs use the same fused ``F.cross_entropy`` path as ``compute_logprobs``.
444+
- entropy ``H(p) = logsumexp(logits) - sum(softmax(logits) * logits)`` is a
445+
metric only, so it is computed under ``no_grad``: it never contributes
446+
gradient and must not build an autograd graph over the [B, L, V] softmax.
447+
448+
Both outputs are ``[batch, seq_len]``.
449+
"""
450+
logits = _gather_vocab_logits(logits)
451+
logprobs = _logprobs_from_full_vocab(logits, labels)
452+
with torch.no_grad():
453+
logits_fp32 = logits.float()
454+
entropy = torch.logsumexp(logits_fp32, dim=-1) - (
455+
torch.softmax(logits_fp32, dim=-1) * logits_fp32
456+
).sum(dim=-1)
457+
return logprobs, entropy
458+
459+
414460
class GradAccumulator:
415461
"""Accumulates chunk gradients into a pre-allocated buffer.
416462

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: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import torch
1414

15-
from torchtitan.components.loss import BaseLoss, compute_logprobs
15+
from torchtitan.components.loss import BaseLoss, compute_logprobs_and_entropy
1616
from torchtitan.config import CompileConfig
1717

1818
# Clamp |log(pi_theta/pi_old)| before exp() so a large generator/trainer
@@ -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+
# One vocab gather feeds both: differentiable logprobs (for the loss) and
82+
# detached per-token entropy (a metric, already under no_grad in the helper).
83+
trainer_logprobs, token_entropy = compute_logprobs_and_entropy(logits, labels)
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)