Skip to content

Keep attention_mask as None in OPT's causal mask creation - #49002

Merged
vasqu merged 4 commits into
huggingface:mainfrom
jiqing-feng:fix-opt-causal-mask-attention-mask
Sep 24, 2026
Merged

vasqu merged 4 commits into
huggingface:mainfrom
jiqing-feng:fix-opt-causal-mask-attention-mask

Conversation

@jiqing-feng

@jiqing-feng jiqing-feng commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

CPU CI GPU run-slow

Fixes #48924

Split out of #48975 on review request.

Context

The same change was proposed in #48929 and closed, because it does not move the SDPA path on its own. That is correct, and this PR does not claim otherwise: the skip only takes effect together with the masking_utils fix in #48975. Measured on one A100 80GB with torch 2.14.0, opt-125m, batch 8, sequence 1024, bf16, sdpa, torch.compile(dynamic=False), no padding mask:

fwd+bwd
main 63.78 ms
this PR alone 64.16 ms
with #48975 49.17 ms

Why it is still worth doing

Two things that may not be obvious from the earlier discussion.

The all-ones tensor is allocated on every forward, but the caller can already supply position_ids. At this call site position_ids is always computed before self.embed_positions(...), so OPTLearnedPositionalEmbedding.forward never reads its attention_mask argument. Replacing it with zeros leaves the output bit-identical. The tensor exists only to be passed to something that ignores it, and to create_causal_mask, which is exactly where it does harm.

Separately, create_causal_mask is the one consumer that can tell the difference between "no mask" and "a mask that happens to be all ones". Handing it a synthetic mask discards information the caller gave us. Today only the compiled path observes this, but the constraint is not specific to torch.compile.

Change

The dense tensor stays local, in position_attention_mask. The caller's attention_mask reaches create_causal_mask unchanged.

This is a no-op numerically. A dump of 30 intermediate tensors across fp32/bf16, unpadded, left and right padded inputs, in eager and under torch.compile, is bit-identical before and after.

Tests

test_attention_mask_is_not_overwritten_for_causal_mask is new and fails on main.

tests/models/opt: 150 passed, 148 skipped.

Note

Happy to close this if you would rather keep OPT as is until #48975 lands, or to fold it back in. It is also possible to drop the all-ones tensor entirely and derive the positions from arange, but that is a larger change and I left it out here.

`OPTDecoder` replaces a `None` `attention_mask` with a dense all-ones tensor
so that the learned positional embeddings can be computed. That tensor was
then also forwarded to `create_causal_mask`, which therefore had to inspect
its values and could never fall back to sdpa's `is_causal` argument.

Keep the dense tensor local to the positional embeddings and pass the
original `attention_mask` to `create_causal_mask`.
@jiqing-feng

Copy link
Copy Markdown
Contributor Author

Hi @vasqu . Would you please review this PR? Thanks!

@vasqu vasqu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just test design related

config_and_inputs = self.model_tester.prepare_config_and_inputs()
self.model_tester.create_and_check_decoder_model_past_large_inputs(*config_and_inputs)

def test_attention_mask_is_not_overwritten_for_causal_mask(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

let's make it mamba2 style like splitting into the tester but otherwise LGTM

@jiqing-feng jiqing-feng Sep 24, 2026 •

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, the body moved into OPTModelTester.

Comment thread tests/models/opt/test_modeling_opt.py Outdated
model(inputs_dict["input_ids"], attention_mask=None)

self.assertTrue(mocked_create_causal_mask.called)
self.assertIsNone(mocked_create_causal_mask.call_args.kwargs["attention_mask"])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tbh wouldnt it also be possible to force the FA backend and run one forward? Just as alternative maybe, feels a bit more appropriate

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.

You are right, and it works now that #48975 is in main. Added as test_compiled_forward_dispatches_to_flash_attention: a compiled forward under sdpa_kernel([SDPBackend.FLASH_ATTENTION]), which only runs if the mask creation was skipped, since flash attention rejects any attn_mask.

eager compiled
main passes raises
this PR passes passes

Eager cannot discriminate: _ignore_causal_mask_sdpa reads the all-ones tensor and returns None anyway. Only the compiled path sees the difference, because reading the values is declined while tracing.

I kept the mocked assertion as well, since the new test needs a GPU and is skipped on the CPU runners. Happy to drop it if you prefer just the one.

@github-actions

Copy link
Copy Markdown
Contributor

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

run-slow: opt

@vasqu
vasqu enabled auto-merge September 24, 2026 13:32
@vasqu

vasqu commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Nice thanks merging

@github-actions

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 36004732510:2
Result: success | Jobs: 8 | Tests: 586 | Failures: 0 | Duration: 7m 22s

@vasqu
vasqu added this pull request to the merge queue Sep 24, 2026
@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.

Merged via the queue into huggingface:main with commit 83445dd Sep 24, 2026
42 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.

OPT training with torch.compile decomposes SDPA into Triton kernels due to attention_mask

3 participants