Fix double-shifted training loss in GitForCausalLM#47395
Conversation
|
[For maintainers] Suggested jobs to run (before merge) run-slow: git |
forward already shifts logits and labels by one before calling self.loss_function, but ForCausalLMLoss shifts again when shift_labels is not supplied, so position t was trained to predict token t + 2. The labels were also flattened before the call, so the internal pad-and-slice kept the shape valid and pulled each batch row's final target in from the next row instead of raising. Pass shift_labels explicitly and keep the tensors 2-D, following the convention already used by csm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fa9fc1f to
9456864
Compare
CI recapDashboard: View test results in Grafana |
| loss = self.loss_function( | ||
| shifted_logits.view(-1, self.config.vocab_size), | ||
| labels.view(-1), | ||
| logits=shifted_logits, |
There was a problem hiding this comment.
since forward now pre-shifts and passes shift_labels with labels=None, the shift_labels[..., 1:] step inside ForCausalLMLoss is skipped, which is the fix. but the manual shifted_logits[:, :-1] + labels[:, 1:] pattern here predates that helper. worth checking Git-derived models (e.g. GitForSequenceClassification path or any copy) for the same double-shift, or is git the only one hitting it?
There was a problem hiding this comment.
Good question. I checked, and git is the only model hitting this specific bug.
Git-derived models: there are none. modeling_git.py defines only GitVisionModel, GitModel and GitForCausalLM — there is no GitForSequenceClassification, and no modular_*.py file imports from models/git, so there is no copy to keep in sync.
The manual pattern elsewhere is safe. Models that still do shift_logits = logits[..., :-1, :] / shift_labels = labels[..., 1:] (gpt2, imagegpt, openai, blip_2, clvp, mamba, falcon_mamba, xlstm, gemma3, llama4, qwen2_audio, granite_speech, modernbert_decoder, ...) pass the result to a plain nn.CrossEntropyLoss / fixed_cross_entropy, not to self.loss_function. ForCausalLMLoss is never involved, so there is no second shift. Those are pre-loss_function leftovers, not bugs.
One other model does hit it: moshi. MoshiForCausalLM.forward (src/transformers/models/moshi/modeling_moshi.py#L1003-L1014) pre-shifts, flattens, and then calls self.loss_function(shift_logits, shift_labels, vocab_size=...) positionally — shift_labels= is never passed as a keyword, so ForCausalLMLoss pads and shifts again (loss/loss_utils.py#L61-L64). Because the tensors are already flattened, the shapes stay valid and it fails silently, exactly like git did.
I left that out of this PR to keep the diff scoped to git; happy to open a separate PR for moshi (or fold it in here if a maintainer prefers).
zucchini-nlp
left a comment
There was a problem hiding this comment.
I'd prefer to keep a sinlge PR to fix all models and add a proper tests. A contrib was working on it in #46903 (will nudge them again) so let's close this PR
Fixing and reviewing the same issue in all models one-by-one takes too much time for maintainers and for contribs
|
Thanks for the review, and understood on wanting a single consolidated PR — that makes sense from a maintenance standpoint. One thing worth flagging before this gets folded into #46903: the two cases have different root paths, and I don't think #46903 as currently written would cover GIT. #46903 targets encoder-decoder models, where GIT is decoder-only. The double shift here comes from So a genuinely model-agnostic PR would need to cover both the encoder-decoder path and the decoder-only-with-prefix-tokens path, with a test that isn't gated on Happy to go either way:
Just let me know which you prefer and I'll act on it. |
What does this PR do?
GitForCausalLM.forwardshifts the labels manually and then passes them positionally toself.loss_function, so they bind tolabelsandshift_labelsstaysNone.ForCausalLMLoss(loss_utils.py:61-64) then shifts a second time, and positiontis trained to predict tokent + 2.Because the manual shift flattens the labels to 1-D before the call, the internal pad-and-slice keeps the shape consistent (
N → N+1 → N), so nothing raises — and each batch row's final target is silently pulled in from the next row.This regressed in #35875, which swapped
CrossEntropyLoss(no shift) forself.loss_function(shifts) while leaving the manual shift in place. First released in v4.49.0. Same mechanism as the Moonshine fix in #46784.Unlike Moonshine, GIT cannot just drop the manual shift:
logitscarriesnum_image_tokensleading image positions that must be sliced off regardless. So this PR passesshift_labelsexplicitly to suppress the internal shift, following the convention already used bycsm(modeling_csm.py:619-621), and keeps the tensors 2-D sinceForCausalLMLossflattens them itself — which also removes the cross-row leak.Verified with the repro from the issue (CPU, no pretrained weights). Before:
After:
Also checked that the
num_items_in_batchpath used byTrainerunder gradient accumulation still matches the mean reduction when all targets are valid.Added
test_training_loss_no_double_shifttotests/models/git/test_modeling_git.py, covering both plain labels and a-100-padded copy. It asserts the loss equals the aligned cross-entropy and is not equal to the double-shifted one, so it fails onmain(AssertionError: False is not true) and passes with this change.ruff checkandruff format --check(pinned 0.14.10) are clean on both files. No# Copied fromblocks reference this code and GIT has no modular file, so nothing needs regenerating.Fixes #47394
Disclosure: this fix was implemented with the assistance of a code agent, per my proposal in the linked issue, and reviewed and validated by me.
Before submitting
Pull Request checks?
Who can review?
@zucchini-nlp