Explainer for NVIDIA-NeMo/RL #4367,
which builds on #4154 (already on main).
Permalinks pin the PR head ca01e85; "main" links pin the PR's base 8661b47.
token_mask, so the actor loss skips it β but so do the reference KL and the sequence-error check.token_mask whole and leaves -inf in the logprobs; the loss builds its own actor mask. Reference KL now keeps the token.seq_logprob_error_threshold is set, one such token now throws away the whole response. Raised in the review.-inf: in its view the token could not be sampled. NeMo-RL compares three logprobs per token:
curr,
prev and
generation.
Dotted words have a short explanation: hover or tap them.Before this PR, the worker replaced that -inf with 0 and removed the token from token_mask
(main: loss_input.py,
main: grpo.py write-back).
token_mask is shared by everything after it, so the token also left the reference KL.
This PR keeps -inf on valid tokens
(megatron/train.py)
and lets the loss drop it from the actor terms only
(loss_functions.py).
But one more reader of prev_logprobs runs before the loss and was not changed: the sequence-error check.
π΄ review comment β loss_input.py:270
Where the fix lands: RL only.
the callers of
compute_seq_logprob_errors
(the drivers and prepare_loss_input). No vLLM change: vLLM's own logprob is right; the trainer's filter is the side that disagrees.
Nothing below is implemented or run. The check must skip a -inf token only when top-k/top-p is on.
With filtering off, a -inf is a real bug, and existing tests
(test_single_forward_grpo.py)
require that such a response is rejected. Skipping it unconditionally breaks one of them (NaN gradients).
compute_seq_logprob_errors itself stays as it is. The caller narrows the mask first, because only
the caller knows whether filtering is on. The driver side, as one example:
@@ nemo_rl/algorithms/grpo.py L2822-L2855 Β· compute_and_apply_seq_logprob_error_masking train_data: BatchedDataDict, rewards: torch.Tensor, seq_logprob_error_threshold: Optional[float], + filtering_on: bool = False, ) -> dict: ... prev_logprobs = train_data["prev_logprobs"][:, 1:] generation_logprobs = train_data["generation_logprobs"][:, 1:] + if filtering_on: + # Under top-k/top-p, -inf means "outside the trainer's filtered set": + # skip that token (as main did), not the whole response. NaN still fails. + token_mask = token_mask * (~torch.isneginf(prev_logprobs)).to(token_mask.dtype) seq_mult_prob_error, valid_seq_mask = compute_seq_logprob_errors(
Each driver passes filtering_on=need_top_k_or_top_p_filtering(...) with its sampling settings β the same
test the workers use to decide whether to filter
(loss_input.py:269).
For the in-loss check
(loss_functions.py:625),
prepare_loss_input builds the same "inside the top-k/top-p set" mask in that branch and the loss ANDs it
into token_mask before the check. With either, the example above gives error 1.035 again and the response stays.
The loss's own actor mask should use the same gated mask, with -inf only, so a NaN still shows up.
π΄ review comment β loss_input.py:270 π review comment β loss_functions.py:667
seq_logprob_error_threshold together with top-k/top-p, one disagreeing token now throws
away the whole response, and long responses are the most likely to contain one. No shipped recipe sets both, so CI will
not catch it. Fix the check as above, and add a test with filtering on.