Repository navigation
Fix UMT5 decoder self-attention not being causal - #49135
Conversation
vasqu
left a comment
There was a problem hiding this comment.
Yep that was indeed missed but also note that we will probably deprecate / remove this feature soon
| def test_decoder_self_attention_is_causal(self): | ||
| # The logits at a decoder position must not depend on the tokens that follow it. | ||
| # Regression test for the decoder self-attention losing `is_causal`: under SDPA the | ||
| # causal mask is skipped when there is no padding, so causality comes from the flag | ||
| # alone. See https://github.com/huggingface/transformers/issues/49134 | ||
| config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common() | ||
|
|
||
| # The common tester initializes weights with initializer_factor=0.002, which makes the | ||
| # attention output too small for a causality violation to be measurable. | ||
| config = copy.deepcopy(config) | ||
| config.initializer_factor = 1.0 | ||
|
|
||
| model = UMT5ForConditionalGeneration(config).to(torch_device).eval() | ||
| input_ids = inputs_dict["input_ids"] | ||
| decoder_input_ids = inputs_dict["decoder_input_ids"] | ||
| self.assertGreater(decoder_input_ids.shape[1], 1) | ||
|
|
||
| # No decoder_attention_mask on purpose: that is the case where SDPA skips the mask. | ||
| with torch.no_grad(): | ||
| truncated = model(input_ids=input_ids, decoder_input_ids=decoder_input_ids[:, :1]) | ||
| full = model(input_ids=input_ids, decoder_input_ids=decoder_input_ids) | ||
|
|
||
| torch.testing.assert_close(truncated.logits[:, 0], full.logits[:, 0], rtol=1e-4, atol=1e-4) | ||
|
|
||
| def test_decoder_self_attention_is_causal_flag(self): | ||
| # The decoder self-attention must be built causal, the encoder and the cross-attention | ||
| # bidirectional. | ||
| config, _ = self.model_tester.prepare_config_and_inputs_for_common() | ||
| model = UMT5ForConditionalGeneration(config) | ||
|
|
||
| self.assertTrue(all(block.layer[0].SelfAttention.is_causal for block in model.decoder.block)) | ||
| self.assertFalse(any(block.layer[0].SelfAttention.is_causal for block in model.encoder.block)) | ||
| self.assertFalse(any(block.layer[1].EncDecAttention.is_causal for block in model.decoder.block)) |
There was a problem hiding this comment.
Can we add a test as in #48760? I think that sole test would be enough then
…lidation and improve test reliability
|
[For maintainers] Suggested jobs to run (before merge) run-slow: umt5 |
| pass | ||
|
|
||
| def test_decoder_causal_mask(self): | ||
| # Regression test for #49134 |
There was a problem hiding this comment.
| # Regression test for #49134 | |
| # Regression test for #49135 |
no?
There was a problem hiding this comment.
happy to switch it if you'd rather point at the PR
There was a problem hiding this comment.
Ah it's the issue sorry i didnt notice, no should be fine
|
run-slow: umt5 |
AMD CIThis comment contains models: ["models/umt5"] |
Nvidia CIThis comment contains models: ["models/umt5"] |
CI recapDashboard: View test results in Grafana |
|
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. |
Adds a contribution table for the merged huggingface/transformers#49135 and OpenNMT/CTranslate2#2103 PRs (a UMT5 correctness fix found while adding UMT5 support to CTranslate2), plus a merged-PRs badge, inspired by how github.com/Bruce-Yii surfaces upstream OSS work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
What does this PR do?
UMT5LayerSelfAttentionnever passesis_causaltoUMT5Attention, so the decoder'sself-attention is built with
is_causal=False. Under SDPA the causal mask is skipped when thereis no padding (
_ignore_causal_mask_sdpa→create_causal_maskreturnsNone), leaving theflag as the only thing applying causality. The decoder can therefore attend to future positions.
T5LayerSelfAttentionandMT5LayerSelfAttentionboth passis_causal=config.is_decoder(t5,
mt5).
UMT5 is the only member of the family missing it, so this PR applies the same approach.
First bad commit is 560f36c (#47014, which moved the family to SDPA): it added the
is_causalparameter toUMT5Attentionbut not the call sites.v5.14.1is correct (eager,mask materialised);
v5.15.0onward is not.The fix is one line, plus
is_causal=Falsestated explicitly on the cross-attention to match T5.Reproducer
A causal decoder's logits at position 0 must not change when tokens are appended after it.
The patched values match
v5.14.1, so this restores previous behaviour rather than changing it.generate()is unaffected — incremental decoding feeds one query position at a time, so thereis no future to leak from. Only full-sequence teacher forcing is affected, i.e. training loss,
evaluation perplexity and offline scoring.
The behaviour was also batch-shape dependent, since the mask is only skipped without padding:
DataCollatorForSeq2Seqemits nodecoder_attention_mask, so the defaultSeq2SeqTrainerpathwas in the leaking case.
test_decoder_self_attention_is_causalasserts the position-0 logits are unchanged when thedecoder input is extended, with no
decoder_attention_mask(the case where SDPA skips the mask).test_decoder_self_attention_is_causal_flagasserts the decoder self-attention is causal whilethe encoder and cross-attention are not. Both fail on
main(greatest absolute difference 3.40against a 1e-4 tolerance) and pass here.
The behavioural test raises
initializer_factorfrom the common tester's0.002to1.0. At0.002the violation measures ~6e-11, so a test written against the default config passes evenwith the bug present — worth knowing if you add similar tests elsewhere.
tests/models/umt5/test_modeling_umt5.py: 231 passed, 279 skipped, 2995 subtests passed.Note on
test_eager_matches_sdpa_inferenceNot addressed here, but that test cannot catch this for the T5 family: it builds the eager model
as
deepcopy(model_sdpa)thenset_attn_implementation("eager"), whileT5Stack/UMT5Stackdeepcopythe config in__init__— and it is that copy whichcreate_causal_maskand theattention-interface lookup read. The stacks keep
sdpa, so it compares sdpa against sdpa.from_pretrained(attn_implementation="eager")does propagate, which is why the dispatch testspass. Happy to open a separate issue.
Fixes #49134
Code Agent Policy
I can't tick that box honestly: I used an AI assistant for this work, and I'd rather disclose it
than leave it ambiguous.
For context on how I got here, since I appreciate the policy is aimed at drive-by agent PRs: I
wasn't looking at Transformers at all. I was adding UMT5 support to CTranslate2
(OpenNMT/CTranslate2#2103), and its maintainer asked me to explain a per-token log-probability
mismatch against Transformers rather than rely on matching greedy output. Tracking that down over
a few hours is what surfaced this — the reference was the thing that was wrong, not my port. So
this is a byproduct of work I was already doing, not an issue I went looking for.
The parts your guide asks for were done and verified: the first bad commit is identified, the
reproducer was run on real weights, the behaviour is compared against T5/MT5 with links, and the
diff is one line plus tests.
Before submitting
is_causalnever reachesUMT5Attention#49134Who can review?
@vasqu @jiqing-feng — from
git blame: #47014 introduced this, authored by @jiqing-feng andmerged by @vasqu, who is also listed for attention.