Skip to content

Fix loss alignment and Trainer token counting for encoder decoder models#46903

Merged
zucchini-nlp merged 1 commit into
huggingface:mainfrom
OmkumarSolanki:fix/pp-formulanet-double-shift-loss
Jul 23, 2026
Merged

Fix loss alignment and Trainer token counting for encoder decoder models#46903
zucchini-nlp merged 1 commit into
huggingface:mainfrom
OmkumarSolanki:fix/pp-formulanet-double-shift-loss

Conversation

@OmkumarSolanki

@OmkumarSolanki OmkumarSolanki commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

CI

What does this PR do?

Encoder decoder models build decoder_input_ids by right shifting the target with
shift_tokens_right, so the logits already line up with labels. Two related bugs get this
alignment wrong.

  1. Double shifted training loss. When such a model computes its loss through self.loss_function
    (which uses ForCausalLMLoss) without passing shift_labels, the labels are shifted a second
    time to labels[..., 1:]. The model then trains against labels[..., 1:] instead of labels,
    which is an off by one. PPFormulaNet had this bug, and Moonshine had it before Fix Moonshine training-loss double-shift (train against labels, not labels[..., 1:]) #46784.

  2. Trainer token undercount. Trainer._loss_shifts_labels assumed that any ForCausalLMLoss head
    counts num_items_in_batch over labels[..., 1:]. For encoder decoder heads every label that is
    not -100 is a real target, so this counts one target position too few per sequence and scales
    the gradient accumulation loss up. For example, if 8 targets are counted as 6, the loss is
    multiplied by about 8/6, which is roughly 1.33x.

Changes

  1. trainer.py. _loss_shifts_labels now also requires config.is_encoder_decoder to be false.
    Decoder only heads keep the old behavior, and encoder decoder heads count every valid label. This
    corrects the num_items_in_batch normalization for PPFormulaNet and, through the Moonshine change
    below, for Moonshine and MoonshineStreaming. It also corrects Florence2 and CohereASR, whose model
    fixes already landed upstream, so those files do not need any further change here.

  2. pp_formulanet. The loss now goes through self.loss_function with labels=None and the aligned
    targets passed as shift_labels. shift_labels is popped from kwargs before the inner model call,
    so a caller supplied value is honoured, does not collide with the derived one, and does not leak into
    the inner model. The edit is in modular_pp_formulanet.py and the modeling file is regenerated.

  3. moonshine and moonshine_streaming. These now use the same form instead of a plain
    CrossEntropyLoss that ignored num_items_in_batch. Both modeling files are regenerated from the
    modulars and the unused import is removed.

Only the training loss changes, and only when labels are passed. Logits and generate() are
unchanged, and -100 stays ignored.

This is the existing coordinated PR for this work. The wider scope was approved by @zucchini-nlp in
this comment: #46903 (comment). No
duplicate PR is being opened.

Fixes #46901.

Test

A new generic test test_encoder_decoder_loss_no_double_shift in tests/test_modeling_common.py
covers ModelTesterMixin sequence to sequence language model heads whose forward owns the loss. It
feeds fresh labels of length at least 2, supplies decoder_input_ids built by right shifting those
labels, and asserts that the gradient of the reported loss with respect to output.logits matches the
gradient of a cross entropy against labels and not against labels[..., 1:]. The gradient comparison
stays decisive on testers whose initialization makes the logits near uniform. There are no per model
branches in it. Dia and ProphetNet skip it in their own testers, since Dia flattens codebook channels
into the batch dimension and ProphetNet computes loss over n gram prediction streams. Trainer counting
is covered by a Bart encoder decoder test in tests/trainer/test_trainer.py, and Moonshine's move onto
the shared loss is covered in its own loss test. The existing test_num_items_in_batch_causal_lm and
test_num_items_in_batch_non_causal_lm still pass.

Commands run locally, rebased on origin/main bb3ffb9:

make typing            # 2 checks passed
make style             # 4 checks passed
git diff --check       # clean

python utils/check_modular_conversion.py --files \
  src/transformers/models/moonshine/modular_moonshine.py \
  src/transformers/models/moonshine_streaming/modular_moonshine_streaming.py \
  src/transformers/models/pp_formulanet/modular_pp_formulanet.py
#   clean

rg --files tests/models -g 'test_modeling*.py' -0 | \
  xargs -0 env PYTHONPATH=src python -m pytest \
    -k test_encoder_decoder_loss_no_double_shift -q
#   38 passed, 568 skipped, 38 subtests passed

python -m pytest tests/trainer/test_trainer.py -k "num_items_in_batch and not grad_norm"
#   3 passed

python -m pytest tests/models/pp_formulanet/test_modeling_pp_formulanet.py
#   113 passed, 147 skipped, 686 subtests passed

python -m pytest tests/models/moonshine/test_modeling_moonshine.py \
                 tests/models/moonshine_streaming/test_modeling_moonshine_streaming.py
#   195 passed, 237 skipped, 2672 subtests passed

Note

AI assistance was used while preparing this change; I have reviewed every line and run the tests.

@OmkumarSolanki
OmkumarSolanki force-pushed the fix/pp-formulanet-double-shift-loss branch from c07b1f4 to b3fe54f Compare June 26, 2026 02:27
Comment on lines -1106 to -1108
loss = self.loss_function(
logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size, **kwargs
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we can pass shift_labels=labels into loss-fn to not sift it twice

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated — kept self.loss_function and passed shift_labels=labels, so this avoids the second shift while preserving the existing loss path / num_items_in_batch handling.

Comment on lines +156 to +159
def test_training_loss_no_double_shift(self):
# forward right-shifts input_ids into decoder_input_ids, so the loss must be a plain CE
# against labels (no second shift). Regression test for the ForCausalLMLoss double-shift.
from torch.nn import CrossEntropyLoss

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah, we defi need a test. Can you make it general for all models when config.is_encoder_decoder=True, and move under test_modeling_common.py
We'd want the test to run on all existing and upcoming models, this is an "easy to miss" bug so should be tested

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I prototyped the common test locally in test_modeling_common.py behind config.is_encoder_decoder. It passes Bart/MBart/LED/Pegasus and catches the same issue in Florence2, but Florence2 is already tracked in #46898 / #46897.

The current common harness also skips PPFormulaNet because its shared test inputs use decoder labels of length 1, so removing the PPFormulaNet-specific test would lose coverage. A robust common test needs non-degenerate labels and decoder length >= 2, and would be red on Florence2 until #46898 lands.

Would you prefer I keep this PR scoped to the PPFormulaNet fix + regression test and add the common test as a follow-up, or expand this PR to include the common-test design and handle the failures it exposes?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I will merge the florence today, no problem on that one, Can you push the common test and fix any other models, except florence. Lets fix it all at once that many tiny PRs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — pushed the common test (test_encoder_decoder_loss_no_double_shift in test_modeling_common.py) and fixed CohereASR, the only other affected model besides PPFormulaNet/Florence2. Full summary is in the top-level PR comment.

Comment on lines +156 to +159
def test_training_loss_no_double_shift(self):
# forward right-shifts input_ids into decoder_input_ids, so the loss must be a plain CE
# against labels (no second shift). Regression test for the ForCausalLMLoss double-shift.
from torch.nn import CrossEntropyLoss

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

import at the begonning of file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — removed the local CrossEntropyLoss import after switching the model fix back to self.loss_function.

@OmkumarSolanki

Copy link
Copy Markdown
Contributor Author

Updated the fix to use self.loss_function(..., shift_labels=labels, **kwargs) (per review) and re-ran locally on the new commit.

Tests

  • python -m pytest tests/models/pp_formulanet/test_modeling_pp_formulanet.py -q → 112 passed, 134 skipped
  • ruff check src/transformers/models/pp_formulanet/ tests/models/pp_formulanet/test_modeling_pp_formulanet.py → All checks passed
  • ruff format --check src/transformers/models/pp_formulanet/ tests/models/pp_formulanet/test_modeling_pp_formulanet.py → 7 files already formatted
  • python utils/check_modular_conversion.py --files src/transformers/models/pp_formulanet/modular_pp_formulanet.py → passed
  • Mutation check: removing shift_labels=labels makes test_training_loss_no_double_shift fail; regenerating from the modular restores it.

@OmkumarSolanki
OmkumarSolanki force-pushed the fix/pp-formulanet-double-shift-loss branch from fa9249d to 7e42355 Compare June 29, 2026 12:44
@OmkumarSolanki

Copy link
Copy Markdown
Contributor Author

Done — pushed the generic common test and the other affected model requested here.

Common test. Added test_encoder_decoder_loss_no_double_shift to tests/test_modeling_common.py, gated on config.is_encoder_decoder. It feeds non-degenerate labels, lets the model build decoder_input_ids by right-shifting them, and asserts the loss is closer to CE against labels than to CE against labels[..., 1:]. Encoder-decoder models with no checkable supervised-loss path are skipped. I moved the PPFormulaNet regression coverage into this common test and removed the per-model test.

Other affected models. A static scan for the pattern (right-shift into decoder_input_ids + loss via self.loss_function) found CohereASR only, besides PPFormulaNet and Florence2. Fixed CohereASR the same way: self.loss_function(..., shift_labels=labels) in modular_cohere_asr.py, then regenerated.

Validation (local):

  • test_encoder_decoder_loss_no_double_shift: 33 passed, 60 skipped, 18866 deselected, 0 failed
  • Full PPFormulaNet tests: 112 passed, 134 skipped
  • Full CohereASR tests: 115 passed, 134 skipped
  • make style: passed
  • modular conversion check: passed

Comment thread tests/test_modeling_common.py Outdated
Comment on lines +1749 to +1752
try:
config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common()
except ImportError as error:
self.skipTest(reason=f"Could not prepare inputs: {str(error).strip()}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why try/except, we should be fine building config once outside the loop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed both try/except blocks. A class based applicability check runs first. Then prepare_config_and_inputs_for_common() runs once outside the model class loop, and the config is copied for each model class. Preparation and forward errors now fail the test normally.

Comment thread tests/test_modeling_common.py Outdated
Comment on lines +1766 to +1770
input_tensor = next(
(value for value in inputs.values() if isinstance(value, torch.Tensor) and value.ndim > 0), None
)
if input_tensor is None:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

doesn't really look good, I suppose you wanted to infer batch size. We can try self.model_tester.batch_size

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Labels now use self.model_tester.batch_size directly. The tensor based batch size inference was removed.

Comment thread tests/test_modeling_common.py Outdated
Comment on lines +1790 to +1791
if vocab_size is None or vocab_size < 4:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we can delete this block, testers with small vocab size shouldn't exit and if they do, they'll have to use bigger vocab

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. I removed the vocabulary size skip, so small vocabulary testers no longer exit early.

Comment thread tests/test_modeling_common.py Outdated
batch_size, seq_len = labels.shape[0], max(2, labels.shape[-1])
set_seed(42)
labels = torch.randint(3, vocab_size, (batch_size, seq_len), device=torch_device)
inputs["labels"] = labels

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also weird, why do we prepare labels and then pop to prepare again, doesn't prepare_for_class already give correct labels since Seq2Seq is a common class

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I changed this to return_labels=False, so labels are no longer prepared and then replaced. Fresh labels are created once with a sequence length of at least 2, which keeps the aligned and shifted loss comparison meaningful. The tester provided decoder_input_ids and decoder_attention_mask are removed so the model exercises its target to decoder input shift path. FSMT and PPFormulaNet use their explicit model specific paths.

Comment thread tests/test_modeling_common.py Outdated
Comment on lines +1813 to +1818
try:
with torch.no_grad():
output = model(**inputs)
except (TypeError, RuntimeError, ValueError, IndexError):
# model needs inputs this generic path does not supply -> not applicable
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same, not try/except , it hides any errors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed. There is no try/except around the forward call anymore. Only explicit applicability skips remain, so unexpected model errors fail the test.

@zucchini-nlp

Copy link
Copy Markdown
Member

@OmkumarSolanki hey, any progress here or wouold you like me to wrap it up, would be great to merge this sson

@OmkumarSolanki

OmkumarSolanki commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, I completely missed these - thanks for the reminder. Will try to get to the review comments today.

@OmkumarSolanki
OmkumarSolanki force-pushed the fix/pp-formulanet-double-shift-loss branch from 7e42355 to b486454 Compare July 22, 2026 11:14
@OmkumarSolanki OmkumarSolanki changed the title Fix PPFormulaNet training-loss double-shift (train against labels, not labels[..., 1:]) Fix loss alignment and Trainer token counting for encoder decoder models Jul 22, 2026
@OmkumarSolanki

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, @zucchini-nlp.

I reworked the PR and force pushed it as one commit rebased onto the current main branch.

The main changes are:

  1. PPFormulaNet keeps self.loss_function and now passes shift_labels=kwargs.pop("shift_labels", labels). This prevents the second shift while preserving num_items_in_batch normalization and caller supplied values.

  2. The generic test test_encoder_decoder_loss_no_double_shift now covers ModelTesterMixin sequence to sequence language model heads. Dia and ProphetNet have documented exceptions. FSMT uses prepare_decoder_input_ids_from_labels.

  3. Trainer no longer applies the causal labels[..., 1:] counting rule to encoder decoder models. This fixes the num_items_in_batch undercount.

  4. Moonshine and MoonshineStreaming now use self.loss_function instead of plain CrossEntropyLoss, so they honor num_items_in_batch.

  5. Config and input preparation now runs once without try/except. Labels use self.model_tester.batch_size. The small vocabulary skip is removed. Imports are at module level.

Local validation results:

  1. Generic guard: 38 passed, 568 skipped, 38 subtests passed.
  2. Trainer num_items tests: 3 passed.
  3. PPFormulaNet: 114 passed, 147 skipped, 686 subtests passed.
  4. Moonshine and MoonshineStreaming: 196 passed, 237 skipped, 2672 subtests passed.
  5. make typing, make style, and modular conversion all passed.

I replied to the inline comments with the specific changes. Please let me know if you would like anything else adjusted.

@zucchini-nlp zucchini-nlp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice! Can you explain why we have similar tests with tiny differences per model?

# `self.loss_function` so grad-accumulation `num_items_in_batch` normalization is honored.
loss = self.loss_function(
logits=logits,
labels=labels,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: is labels are already shifted (i.e. shift_labels), lets explicitly pass labels=None and shift_labels=labels

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, in Moonshine and PPFormulaNet. Both now pass labels=None with the aligned targets as shift_labels.

One thing to note on the exact form. The value has to come out of kwargs first. If a caller passes its own shift_labels and the model also passes one by keyword, the call raises TypeError: transformers.loss.loss_utils.ForCausalLMLoss() got multiple values for keyword argument 'shift_labels'. In the previous revision of this PR the pop also sat after the inner model call, so a caller supplied value leaked down into it. Both are handled by popping at the top of forward.

Both modulars updated and the three modeling files regenerated.

self.assertTrue(torch.allclose(out.loss, aligned_ce(out.logits, lbl)))
self.assertFalse(torch.allclose(out.loss, double_shift_ce(out.logits, lbl)))

def test_num_items_in_batch_scales_training_loss(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not following here, are we adding a new test for only Moonshine? can you explain why only this model?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Folded into the existing test_training_loss_no_double_shift instead of a second test. It now runs over the plain and the padded labels.

It is Moonshine only because Moonshine is the one model here whose alignment was already correct on current main, after #46784. It computed the loss with a plain nn.CrossEntropyLoss, which ignores num_items_in_batch, so it returned the same value for any count. The generic alignment test cannot see that, and the Trainer fix does not help either: the Trainer passes a count, but the count has no effect while the model computes a plain mean. Everything else in this PR was already on self.loss_function.

test_resize_embeddings = False
is_encoder_decoder = True

def test_trainer_counts_all_labels_and_aligned_loss(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same q here, this one tests num_items_in_batch with trainer. Why we test differently each model?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed, with the unused Trainer, TrainingArguments and tempfile imports. That file matches main again.

You are right that it was a third version of the same check. Trainer counting is not specific to PPFormulaNet and is covered once on Bart in test_num_items_in_batch_encoder_decoder. Alignment is covered by the common test.

One assertion in it was not a duplicate, so I moved it into the Moonshine loss test rather than lose it: that a caller supplied shift_labels is used as given, which is the reason for the pop above.

Comment thread tests/test_modeling_common.py Outdated
Comment on lines +1769 to +1776
if config.model_type == "dia":
# dia routes through ForMaskedLMLoss and flattens its codebook/channel streams into the batch
# dimension, so its `(batch * num_channels, seq, vocab)` logits never align with 2D `labels`
self.skipTest(reason="dia flattens codebook channels into the batch dim; logits do not match 2D `labels`")
if config.model_type == "prophetnet":
# prophetnet computes NLL over `ngram` future-token prediction streams (with label smoothing),
# not a single cross-entropy over `output.logits`
self.skipTest(reason="prophetnet's loss is NLL over n-gram streams, not one cross-entropy over the logits")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can you skip directly in DIa/ProphetNet testers with @unittest.skip

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Both are @unittest.skip overrides of the test in DiaModelTest and ProphetNetModelTest, and both model_type checks are out of the common test.

Comment thread tests/test_modeling_common.py Outdated
Comment on lines +1827 to +1830
if config.model_type == "fsmt":
# fsmt derives `decoder_input_ids` from `input_ids` (the encoder source), not `labels`;
# supply aligned decoder inputs built from `labels` so the loss is a per-token CE against them.
inputs["decoder_input_ids"] = model.prepare_decoder_input_ids_from_labels(labels)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

not really following here as well, can you explain? TBH not a fan of adding too many conditions in a test for each model type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All of them are gone. There is no model_type branch left in the test.

They came from letting each model build its own decoder_input_ids, which meant the test had to know how each one derives them, plus an initializer_factor reset because the T5 testers make the logits numerically uniform and the aligned and shifted loss values stop being distinguishable.

The test now builds the right shifted decoder inputs itself and passes them in, so every model runs the same path. The first token is the model's top level decoder_start_token_id when that is set and inside the vocabulary, and 0 otherwise, which several testers need since they leave it unset. That token changes the logits, but it cannot change which cross entropy the reported loss matches, so the choice is not load bearing. The range check is defensive: it keeps an id past the tester vocabulary from failing in an embedding lookup before the assertions run.

The oracle compares gradients rather than loss values. It differentiates the reported loss, the aligned cross entropy and the shifted cross entropy with respect to output.logits and asserts the reported one matches the aligned gradient and not the shifted one. Gradients keep the target positions, so they stay decisive where the loss values collapse. On the T5 tester config the logits have std 4.2e-6, both losses come out at 4.595120 and differ by 4.8e-7 so torch.allclose cannot separate them, while the gradients differ by 1.3e-2. That is what removed the initializer_factor handling.

What the test gives up is checking how each model builds its decoder inputs. It now only checks that the loss does not shift targets that are already aligned, which is the bug it is guarding.

Mutation checked both ways. Reverting the PPFormulaNet fix fails the test, and injecting a double shift into T5ForConditionalGeneration fails it too, which the old value based oracle could only do with the initializer_factor override in place. The sweep is still 38 passed, 568 skipped.

Encoder-decoder LM heads right-shift their targets into decoder_input_ids, so
their logits are already position-aligned with `labels`. Two related bugs
mishandle that alignment:

- PPFormulaNet (and, pre-huggingface#46784, Moonshine) routed `labels` through
  ForCausalLMLoss without `shift_labels`, so the loss shifted a second time and
  the model trained against labels[..., 1:].
- Trainer._loss_shifts_labels counted num_items_in_batch over labels[..., 1:]
  for every ForCausalLMLoss head. Encoder-decoder heads have a real target at
  every non-`-100` position, so this under-counts targets (one position per
  sequence) and over-scales the gradient-accumulation loss.

Changes:
- trainer.py: _loss_shifts_labels additionally requires config.is_encoder_decoder
  to be false, so encoder-decoder heads count every valid label. Decoder-only
  heads are unchanged; Csm (decoder-only) is unaffected.
- pp_formulanet: compute the loss via self.loss_function with labels=None and the
  aligned targets passed as shift_labels. shift_labels is popped from kwargs
  before the inner model call, so a caller-supplied value is honored, does not
  collide with the derived one, and does not leak into the inner model.
- moonshine / moonshine_streaming: same form, replacing a plain CrossEntropyLoss
  that ignored num_items_in_batch.

Tests:
- tests/test_modeling_common.py: generic guard
  test_encoder_decoder_loss_no_double_shift. It supplies the right-shifted
  decoder_input_ids itself and compares gradients of the reported loss w.r.t.
  output.logits against the aligned and the shifted cross-entropy, which stays
  decisive on testers whose initialization makes the logits near-uniform. No
  per-model branches.
- Dia and ProphetNet skip it via @unittest.skip in their own testers.
- tests/trainer/test_trainer.py: Bart encoder-decoder num_items_in_batch count.
- The Moonshine loss test also covers num_items_in_batch scaling and a
  caller-supplied shift_labels.

Fixes huggingface#46901
@OmkumarSolanki
OmkumarSolanki force-pushed the fix/pp-formulanet-double-shift-loss branch from b486454 to e964019 Compare July 22, 2026 19:12
@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: dia, moonshine, moonshine_streaming, pp_formulanet, prophetnet

@OmkumarSolanki

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and for the patience across the three rounds. Pushed the cleanup.

  • Moonshine and PPFormulaNet pop shift_labels before the inner model call and pass labels=None with shift_labels into self.loss_function. All three modeling files regenerated.
  • test_encoder_decoder_loss_no_double_shift has no model_type branch left. It supplies the right shifted decoder_input_ids itself and compares gradients with respect to output.logits instead of loss values, which removed the T5 initializer_factor handling.
  • Dia and ProphetNet skip through @unittest.skip in their own testers.
  • The PPFormulaNet Trainer test is gone. Counting is covered once on Bart, alignment by the common test, and the caller supplied shift_labels assertion moved into the Moonshine loss test.
  • Moonshine's num_items_in_batch assertions are folded into its existing loss test.

The common guard now uses one model independent setup and oracle, with no model specific branches.

Local results, rebased on origin/main bb3ffb9:

rg --files tests/models -g 'test_modeling*.py' -0 | \
  xargs -0 env PYTHONPATH=src python -m pytest \
    -k test_encoder_decoder_loss_no_double_shift -q
#   38 passed, 568 skipped, 38 subtests passed

python -m pytest tests/trainer/test_trainer.py::TrainerGradientAccumulationTest
#   8 passed, 1 skipped   (the skip is require_torch_multi_accelerator)

python -m pytest tests/models/pp_formulanet/test_modeling_pp_formulanet.py
#   113 passed, 147 skipped, 686 subtests passed

python -m pytest tests/models/moonshine/test_modeling_moonshine.py \
                 tests/models/moonshine_streaming/test_modeling_moonshine_streaming.py
#   195 passed, 237 skipped, 2672 subtests passed

python utils/check_modular_conversion.py --files \
  src/transformers/models/moonshine/modular_moonshine.py \
  src/transformers/models/moonshine_streaming/modular_moonshine_streaming.py \
  src/transformers/models/pp_formulanet/modular_pp_formulanet.py
#   clean

make typing            # 2 checks passed
make style             # 4 checks passed
git diff --check       # clean

Mutation checked three ways: reverting the PPFormulaNet fix fails the common test; injecting a double shift into T5ForConditionalGeneration fails it as well, which the old value based oracle could only catch with the initializer_factor override; and turning the pop back into a kwargs.get reproduces the duplicate shift_labels TypeError that the Moonshine test now guards.

@github-actions

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 29950000929:2
Result: success | Jobs: 15 | Tests: 163,800 | Failures: 0 | Duration: 14h 33m

@zucchini-nlp zucchini-nlp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for iterating and lets merge now

@HuggingFaceDocBuilderDev

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.

@zucchini-nlp
zucchini-nlp added this pull request to the merge queue Jul 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 23, 2026
@zucchini-nlp
zucchini-nlp added this pull request to the merge queue Jul 23, 2026
Merged via the queue into huggingface:main with commit 0c1829d Jul 23, 2026
106 checks passed
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.

PPFormulaNet training-loss double-shift: trains against labels[..., 1:] instead of labels

3 participants