Skip to content

Gather per-example tensors before reducing logged means - #7045

Closed
behroozazarkhalili wants to merge 11 commits into
mainfrom
fix/7010-gather-before-mean
Closed

behroozazarkhalili wants to merge 11 commits into
mainfrom
fix/7010-gather-before-mean

Conversation

@behroozazarkhalili

@behroozazarkhalili behroozazarkhalili commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

Several trainers reduced metrics before gathering them, preventing removal of duplicated examples in the final distributed batch. I changed the affected paths to gather per-example values before reduction and corrected related padding and label handling. Tests that simulate duplicate removal pass and fail when the changes are reverted, but I did not run multiple processes.
Fixes #7010.


Note

Medium Risk
Touches metric logging across many RLHF trainers; training losses are mostly unchanged but logged dashboards and early-stopping on metrics could shift on uneven distributed datasets.

Overview
Fixes biased training/eval metrics when dataset size does not divide evenly across distributed ranks. gather_for_metrics only strips Accelerate’s duplicated last-batch examples along the batch dimension, so metrics that were averaged (or summed) before the gather kept the extra padding example baked in.

Across DPO, KTO, Reward, CPO, ORPO, TPO, XPO, Online DPO (and related paths), logging now passes one value per example (or per prompt where batches stack chosen/rejected/reference or two completions), gathers, then reduces. Entropy and average logits use per-row token sums and counts; stacked batches are folded so one row maps to one example before gather. CPO/ORPO expose per-example NLL sum and token count from forward; the training loss is unchanged but nll_loss is computed after gather. Online DPO aggregates KL, entropy, scores, and EOS flags per prompt and masks padding in KL/entropy. KTO casts labels to bool, gathers KL per row, and scatters chosen/rejected rewards/logps into full-batch tensors so single-class batches and bfloat16 dtypes work with gather. Reward and XPO log accuracy, margins, and rewards from per-pair tensors instead of pre-reduced scalars.

Tests add drop_last_for_metrics in testing_utils (mimics Accelerate de-duplication) and oracle tests that assert logged values match the mean over non-outlier examples when the last batch row is an outlier.

Reviewed by Cursor Bugbot for commit 000fc99. Bugbot is set up for automated code reviews on this repo. Configure here.

Several trainers reduced a metric to a rank-local scalar and then passed
it to gather_for_metrics. That defeats the de-duplication the gather
exists for: accelerate trims the padded final batch by slicing the batch
dimension, and a scalar has none left, so on a dataset that does not
divide evenly across ranks the duplicated example stays in the mean.

Every affected site now hands the per-example tensor to the gather and
reduces afterwards: RewardTrainer accuracy, margin and reward range; XPO
log-probs, rewards, KL, entropy, margins and accuracy; Online DPO score
margin, scores, KL, non-score reward, RLHF reward and entropy (per
prompt, over its two completions); PPO objective KL, entropy, non-score
reward, RLHF reward and scores; CPO and ORPO logits means; TPO entropy
(per-token entropy and mask gathered together).

Each trainer gains an oracle test built on a shared stand-in gather that
drops the final example: the fixed code reports the mean over the
remaining examples, the previous code reports the mean over all of them.

Fixes #7010
@bot-ci-comment

bot-ci-comment Bot commented Sep 4, 2026

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

kashif and others added 4 commits September 4, 2026 09:51
…st round missed

DPO and KTO reduced entropy and token counts to scalars before
gather_for_metrics, TPO gathered logits and accuracy as scalar sums and
its rewards, margins and log-probs with plain gather, Online DPO never
gathered val/contain_eos_token, and CPO and ORPO logged nll_loss from
the scalar mean of nn.CrossEntropyLoss. None of those can drop the
example accelerate duplicates on the last batch. Every site now hands
one row per example to gather_for_metrics and reduces afterwards; the
CPO and ORPO forward returns per-example NLL sums and token counts and
the training loss is still their total-sum over total-count.

gather_for_metrics de-duplicates by slicing the leading dimension, and
the DPO and TPO forward batches stack chosen and rejected rows (and a
reference row for TPO), as Online DPO stacks two completions per
prompt, so those tensors are folded to one row per example before the
gather.

Each site has an oracle test that fails with its change reverted, and
PPO gains the regression test the earlier round lacked. The
training-token counters keep their existing pattern.
…o fix/7010-gather-before-mean

# Conflicts:
#	tests/experimental/test_online_dpo_trainer.py
Comment thread trl/trainer/dpo_trainer.py
TPO copies DPO's metric block and received the per-example fix in the
earlier round while DPO and KTO kept the old shape: logits and token
accuracy were summed to a rank-local scalar before gather_for_metrics,
and DPO's rewards, accuracies, margins and log-probs went through plain
gather, which never removes the duplicated last-batch padding at all.

DPO now hands per-example logit sums, token counts, correct-token counts,
rewards, accuracies, margins and log-probs to gather_for_metrics in both
the standard and the Liger block. KTO does the same for the KL term, the
token count, the chosen and rejected counts (one boolean per row), the
logits and the rewards and log-probs, which are scattered into a
zero-filled row-aligned tensor so the opposite label contributes zero to
the sum and to the count.

The Liger logits in both trainers stay scalar: liger_kernel returns
chosen_logits_mean and rejected_logits_mean as 0-d sums, and the KTO
fused loss returns its logits, log-probs and rewards the same way, so no
per-example quantity exists to gather. The PR body lists this as a limit.

The DPO and KTO oracle tests now assert every changed metric against the
mean over the first N-1 examples and no longer stub accelerator.gather,
so a leftover gather call fails loudly. On the previous commit the DPO
oracle fails on that stub and both KTO cases keep the duplicated KL.
The previous commit moved KTO's num_tokens counter to a per-row gather.
The PR keeps the attention_mask.sum() counters unchanged in every trainer
as a separate convention, and DPO still uses the scalar form, so KTO
follows it too. The oracle no longer asserts the token count.
Comment thread trl/trainer/kto_trainer.py
A batch that holds only one label class gets the empty float32 default
for the other class's rewards, and index_copy refuses a float32 source
into the bfloat16 row tensor under mixed precision. Cast both reward
tensors to the target dtype first. The new test runs a bfloat16 batch
with a single label class each way; on the previous commit it raises
the index_copy dtype error.
PPOTrainer left main in #7020, so the PPO objective-metric changes and their oracle test have no target. The testing_utils conflict keeps drop_last_for_metrics and takes main's removal of the bitsandbytes helpers.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4ee8527. Configure here.

Comment thread trl/trainer/kto_trainer.py Outdated
Datasets can carry 0/1 integer labels. On an integer tensor `~` is a
bitwise inversion, so the rejected count and the rejected logits mask
became -1 and -2 rows. The loss path already casts the field to bool;
the metric path now does the same. The oracle runs over bool and int64
labels; on the previous commit the int64 rows fail with an IndexError.
@behroozazarkhalili

Copy link
Copy Markdown
Collaborator Author

Closing this one myself. It came from reading the code, not from a failure in a real training run, and per the discussion on #6862 that isn't the bar for this repo.

@kashif, thanks for the commits you put into this. If you think it's worth keeping, say the word and I'll reopen it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metrics are reduced to a rank-local scalar before gather_for_metrics, biasing the final batch

2 participants