-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Cast attention inputs to the autocast dtype in dispatch_attention_fn #14160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
danielxmed
wants to merge
5
commits into
huggingface:main
Choose a base branch
from
danielxmed:fix/issue-14104-autocast-attention-dtype
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+142
−1
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
31f876c
Cast attention inputs to the autocast dtype in dispatch_attention_fn
danielxmed d26452b
Use plain pytest style for the autocast dispatch tests
danielxmed 049feef
Tighten the autocast comment in dispatch_attention_fn
danielxmed 38a870d
Simplify the autocast guard to a literal meta-device check
danielxmed e864233
Gate the dispatcher autocast tests behind require_torch_accelerator a…
danielxmed File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import contextlib | ||
| import importlib.metadata | ||
| import tempfile | ||
| import unittest | ||
|
|
@@ -8,9 +9,14 @@ | |
| from packaging import version | ||
|
|
||
| from diffusers import DiffusionPipeline | ||
| from diffusers.models.attention_dispatch import ( | ||
| AttentionBackendName, | ||
| _AttentionBackendRegistry, | ||
| dispatch_attention_fn, | ||
| ) | ||
| from diffusers.models.attention_processor import Attention, AttnAddedKVProcessor | ||
|
|
||
| from ..testing_utils import torch_device | ||
| from ..testing_utils import is_torch_compile, require_torch_accelerator, torch_device | ||
|
|
||
|
|
||
| class AttnAddedKVProcessorTests(unittest.TestCase): | ||
|
|
@@ -133,3 +139,125 @@ def test_conversion_when_using_device_map(self): | |
|
|
||
| self.assertTrue(np.allclose(pre_conversion, conversion, atol=1e-3)) | ||
| self.assertTrue(np.allclose(conversion, after_conversion, atol=1e-3)) | ||
|
|
||
|
|
||
| @require_torch_accelerator | ||
| class TestAttentionDispatchAutocast: | ||
| """Regression tests for https://github.com/huggingface/diffusers/issues/14104. | ||
|
|
||
| Under an active torch.autocast context, ops with an fp32 cast policy (e.g. the `torch.nn.RMSNorm` | ||
| QK norms in Flux-family models) return float32 while `value` keeps the autocast dtype. Native SDPA | ||
| downcasts all of its inputs itself, but backends calling external kernels (flash-attn, sage, ...) | ||
| receive the tensors as-is and reject the mismatch, so `dispatch_attention_fn` must normalize | ||
| dtypes the same way SDPA's autocast registration does. | ||
| """ | ||
|
|
||
| def _qkv(self, qk_dtype=torch.float32, v_dtype=torch.bfloat16): | ||
| # The fp32 q/k + bf16 v defaults reproduce what the fp32 autocast policy for `aten::rms_norm` | ||
| # does to the QK-norm pattern: q/k leave the norm upcast, value never passes through it. | ||
| query = torch.randn(1, 8, 2, 16, device=torch_device, dtype=qk_dtype) | ||
| key = torch.randn(1, 8, 2, 16, device=torch_device, dtype=qk_dtype) | ||
| value = torch.randn(1, 8, 2, 16, device=torch_device, dtype=v_dtype) | ||
| return query, key, value | ||
|
|
||
| @contextlib.contextmanager | ||
| def _record_native_backend_input_dtypes(self): | ||
| original = _AttentionBackendRegistry._backends[AttentionBackendName.NATIVE] | ||
| received = {} | ||
|
|
||
| def recording_backend(query, key, value, **kwargs): | ||
| received["query"], received["key"], received["value"] = query.dtype, key.dtype, value.dtype | ||
| if kwargs.get("attn_mask") is not None: | ||
| received["attn_mask"] = kwargs["attn_mask"].dtype | ||
| return original(query, key, value, **kwargs) | ||
|
|
||
| _AttentionBackendRegistry._backends[AttentionBackendName.NATIVE] = recording_backend | ||
| try: | ||
| yield received | ||
| finally: | ||
| _AttentionBackendRegistry._backends[AttentionBackendName.NATIVE] = original | ||
|
|
||
| def test_autocast_casts_inputs_to_autocast_dtype(self, monkeypatch): | ||
| # Checks on: the opt-in DIFFUSERS_ATTN_CHECKS validation sees the tensors the backend will | ||
| # actually receive, so it must not raise on mixed autocast inputs (on main it false-raised). | ||
| monkeypatch.setattr(_AttentionBackendRegistry, "_checks_enabled", True) | ||
| query, key, value = self._qkv() | ||
| device_type = torch.device(torch_device).type | ||
|
|
||
| with self._record_native_backend_input_dtypes() as received: | ||
| with torch.autocast(device_type, dtype=torch.bfloat16): | ||
| out = dispatch_attention_fn(query, key, value, backend=AttentionBackendName.NATIVE) | ||
|
|
||
| assert received["query"] == torch.bfloat16 | ||
| assert received["key"] == torch.bfloat16 | ||
| assert received["value"] == torch.bfloat16 | ||
| assert out.dtype == torch.bfloat16 | ||
|
|
||
| def test_autocast_casts_floating_point_mask_but_not_bool_mask(self): | ||
| query, key, value = self._qkv() | ||
| device_type = torch.device(torch_device).type | ||
| float_mask = torch.zeros(1, 2, 8, 8, device=torch_device, dtype=torch.float32) | ||
| bool_mask = torch.ones(1, 2, 8, 8, device=torch_device, dtype=torch.bool) | ||
|
|
||
| with self._record_native_backend_input_dtypes() as received: | ||
| with torch.autocast(device_type, dtype=torch.bfloat16): | ||
| dispatch_attention_fn(query, key, value, attn_mask=float_mask, backend=AttentionBackendName.NATIVE) | ||
| assert received["attn_mask"] == torch.bfloat16 | ||
|
|
||
| with self._record_native_backend_input_dtypes() as received: | ||
| with torch.autocast(device_type, dtype=torch.bfloat16): | ||
| dispatch_attention_fn(query, key, value, attn_mask=bool_mask, backend=AttentionBackendName.NATIVE) | ||
| assert received["attn_mask"] == torch.bool | ||
|
|
||
| def test_no_autocast_dtypes_pass_through_unchanged(self): | ||
| # Outside autocast the dispatcher must not touch dtypes: mismatched inputs keep raising | ||
| # (from SDPA itself for the native backend), exactly as before. | ||
| query, key, value = self._qkv() | ||
| with pytest.raises(RuntimeError, match="Expected query, key, and value to have the same dtype"): | ||
| dispatch_attention_fn(query, key, value, backend=AttentionBackendName.NATIVE) | ||
|
|
||
| @pytest.mark.skipif( | ||
| torch_device not in ["cuda", "xpu"], reason="aten::rms_norm has an fp32 autocast policy for CUDA/XPU only" | ||
| ) | ||
| def test_rms_norm_qk_pattern_under_autocast(self): | ||
| # End-to-end shape of the Flux-family bug: bf16 q/k go through `torch.nn.RMSNorm` under | ||
| # autocast and come out fp32, value stays bf16. The policy only exists on recent torch | ||
| # (and is debated upstream), so probe for it instead of assuming. | ||
| device_type = torch.device(torch_device).type | ||
|
|
||
| norm = torch.nn.RMSNorm(16, eps=1e-6, device=torch_device, dtype=torch.bfloat16) | ||
| query, key, value = self._qkv(qk_dtype=torch.bfloat16) | ||
|
|
||
| with torch.autocast(device_type, dtype=torch.bfloat16): | ||
| if norm(query).dtype != torch.float32: | ||
| pytest.skip("this torch version has no fp32 autocast policy for aten::rms_norm.") | ||
|
|
||
| with self._record_native_backend_input_dtypes() as received: | ||
| with torch.autocast(device_type, dtype=torch.bfloat16): | ||
| query, key = norm(query), norm(key) | ||
| out = dispatch_attention_fn(query, key, value, backend=AttentionBackendName.NATIVE) | ||
|
|
||
| assert received["query"] == torch.bfloat16 | ||
| assert received["key"] == torch.bfloat16 | ||
| assert received["value"] == torch.bfloat16 | ||
| assert out.dtype == torch.bfloat16 | ||
|
|
||
| @is_torch_compile | ||
| def test_torch_compile_fullgraph_under_autocast(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool test! |
||
| # The autocast branch must stay fullgraph-traceable: probing autocast state with APIs that | ||
| # Dynamo cannot trace would graph-break every compiled model at each attention call. | ||
| # `backend` is left unset (default active backend, i.e. native): passing the enum explicitly | ||
| # takes the `AttentionBackendName(backend)` path, which Dynamo cannot trace before torch 2.12, | ||
| # and model processors compile with `backend=None` anyway. | ||
| query, key, value = self._qkv(qk_dtype=torch.bfloat16) | ||
| device_type = torch.device(torch_device).type | ||
|
|
||
| compiled = torch.compile(dispatch_attention_fn, fullgraph=True) | ||
| with torch.autocast(device_type, dtype=torch.bfloat16): | ||
| out = compiled(query, key, value) | ||
| assert out.dtype == torch.bfloat16 | ||
|
|
||
| torch.compiler.reset() | ||
| compiled = torch.compile(dispatch_attention_fn, fullgraph=True) | ||
| out = compiled(query, key, value) | ||
| assert out.dtype == torch.bfloat16 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the
qk_typeset totorch.float32to emulate the QKnorm behaviour?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. fp32 q/k with bf16 v is what the
aten::rms_normpolicy produces on the QK-norm path, and value keeps the autocast dtype because it never passes through the norm. The helper now says this in a comment (e864233).