@@ -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+
414460class GradAccumulator :
415461 """Accumulates chunk gradients into a pre-allocated buffer.
416462
0 commit comments