Skip to content

Commit 529a2bb

Browse files
committed
add metrics
1 parent d9ebce1 commit 529a2bb

6 files changed

Lines changed: 82 additions & 19 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/examples/search_r1/config_registry.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
)
3636
from torchtitan.experiments.rl.actors.trainer import PolicyTrainer
3737
from torchtitan.experiments.rl.components.batcher import BatchConfig, Batcher
38+
from torchtitan.experiments.rl.components.training_sample_builder import (
39+
TrainingSampleBuilder,
40+
)
3841
from torchtitan.experiments.rl.controller import (
3942
AsyncLoopConfig,
4043
Controller,
@@ -63,10 +66,16 @@ def rl_grpo_qwen3_1_7b_search_r1() -> Controller.Config:
6366
num_training_steps=500,
6467
num_groups_per_train_step=8,
6568
group_size=8,
69+
max_offpolicy_steps=0,
6670
validation=ValidationConfig(num_samples=500),
6771
batcher=Batcher.Config(
6872
batch=BatchConfig(local_batch_size=1, seq_len=4096),
6973
),
74+
# Keep zero-reward-variance groups instead of dropping them (default
75+
# is to drop, since they give zero GRPO advantage / no learning signal).
76+
training_sample_builder=TrainingSampleBuilder.Config(
77+
drop_zero_std_reward_groups=False,
78+
),
7079
),
7180
compile=CompileConfig(enable=True, backend="aot_eager"),
7281
rollouter=SearchR1Rollouter.Config(
@@ -106,7 +115,7 @@ def rl_grpo_qwen3_1_7b_search_r1() -> Controller.Config:
106115
model_dtype="bfloat16",
107116
parallelism=InferenceParallelismConfig(
108117
data_parallel_degree=1,
109-
tensor_parallel_degree=4,
118+
tensor_parallel_degree=1,
110119
),
111120
# cudagraph on: decode-only graphs (FULL_DECODE_ONLY) are safe at this
112121
# config's large batch; plain full graphs corrupted here before. See #3668.
@@ -135,7 +144,7 @@ def rl_grpo_qwen3_8b_search_r1() -> Controller.Config:
135144
config.trainer = dataclasses.replace(
136145
config.trainer,
137146
parallelism=dataclasses.replace(
138-
config.trainer.parallelism, tensor_parallel_degree=4
147+
config.trainer.parallelism, data_parallel_shard_degree=4
139148
),
140149
)
141150
# 0.6 (vs the 0.9 default) reserves room for the weight-sync memory spike, which

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)