fix(loss): count num_valid_samples from the sample mask, not the batch dim - #3850
Open
tianyi-zhang-02 wants to merge 2 commits into
Open
fix(loss): count num_valid_samples from the sample mask, not the batch dim#3850tianyi-zhang-02 wants to merge 2 commits into
tianyi-zhang-02 wants to merge 2 commits into
Conversation
…h dim
Six losses in this file derive num_valid_samples from the sample mask.
Three derive it from the batch dimension instead: MseValueLossFn from
values.shape[0], and both distillation losses from input_ids.shape[0].
That number is a gate, not just a log line. dtensor_policy_worker,
dtensor_policy_worker_v2 and dtensor_value_worker_v2 all do
if num_valid_samples > 0:
mb_losses.append(loss.item())
all_mb_metrics.append(loss_metrics)
so a microbatch whose sample_mask is entirely zero -- which is what
overlong filtering, env-flagged masking and the critic's
seq_logprob_error_threshold filter produce -- reports the raw batch size,
passes the gate, and contributes its zero loss to the step's reported mean.
dpo.py and rm.py additionally use the value as a weighted-average
denominator.
DistillationLossFn keeps a batch-dimension fallback: its masking branch is
conditional, and with no mask every sample is valid by definition.
Tests are CPU-only -- they build the loss inputs directly rather than going
through prepare_loss_input, so no GPU and no distributed init. The
cross-tokenizer loss gets the same one-line change but no new test: every
CPU test for it calls the private helpers, and none reaches __call__ where
the metrics dict is built.
Signed-off-by: Tianyi Zhang <zhangtianyi975@gmail.com>
Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
tianyi-zhang-02
force-pushed
the
fix-num-valid-samples
branch
from
August 26, 2026 18:16
cfdf9c2 to
6634ab2
Compare
…IA-NeMo#3496 NVIDIA-NeMo#3496 replaces DistillationLossFn's unmasked-mean fallback with a raise. Until it lands the fallback is live and must report the batch size; after it lands the branch is gone and the raise is the only correct behaviour. Asserting one of them unconditionally makes the two PRs fail as a pair while each passes alone. That is not hypothetical -- it is what happened when I merged the whole stack onto main to check exactly this. Signed-off-by: Tianyi Zhang <123608656+tianyi-zhang-02@users.noreply.github.com>
tianyi-zhang-02
force-pushed
the
fix-num-valid-samples
branch
from
August 28, 2026 15:39
7c69abb to
a3ec715
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do ?
Makes
num_valid_samplesmean the same thing in every loss: the number of samples the microbatch actually contributes, taken from the sample mask.Issues
None to close.
Usage
No config change. The metric is internal.
Before your PR is "Ready for review"
Pre checks:
Additional Information
Seven losses in
loss_functions.pyreportnum_valid_samples. Four take it from the sample mask. Three take it from the batch dimension:MseValueLossFnint(values.shape[0])DistillationLossFndata["input_ids"].shape[0]CrossTokenizerDistillationLossFndata["input_ids"].shape[0]That number is a gate, not just a log line. All three DTensor workers do:
So a microbatch whose
sample_maskis entirely zero reports the raw batch size, passes the gate, and contributes its zero loss to the step's reported mean.ClippedPGLossFnskips that microbatch; the critic and the distillation losses do not.A fully-masked microbatch is not hypothetical on these three paths in particular — overlong filtering and env-flagged masking zero
loss_multiplieron the distillation side, and the critic'sseq_logprob_error_thresholdfilter zeroessample_maskbefore the value loss runs.dpo.pyandrm.pyalso use the value as a weighted-average denominator.DistillationLossFnkeeps a batch-dimension fallback rather than requiring the mask: its masking branch is conditional today, and with no mask every sample is valid by definition. (#3496 turns thatelseinto a raise; this is written so it is correct either way, and I'll rebase whichever lands second.)Tests are CPU-only — they build the loss inputs directly rather than going through
prepare_loss_input, so no GPU and no distributed init. Mutation-tested: reverting either site to the batch dimension, and breaking the no-mask fallback, each turn them red.The cross-tokenizer loss gets the same one-line change but no new test. Every CPU test for it calls the private helpers (
_compute_gold,_compute_p_kl); none reaches__call__, where the metrics dict is built. Flagging that rather than claiming coverage I don't have. :)