Repository navigation
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
Changes from 3 commits
31f876c
d26452b
049feef
38a870d
e864233
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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,130 @@ 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)) | ||
|
|
||
|
|
||
| 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): | ||
|
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. Is the
Author
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. Yes, exactly. fp32 q/k with bf16 v is what the |
||
| 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): | ||
|
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. I would mark these tests with
Author
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. Done in e864233. The class is gated now, so all of it skips on the CPU-only fast PR CI and runs on the GPU workflows instead. |
||
| 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_autocast_passes_backend_dtype_checks(self, monkeypatch): | ||
| # The opt-in debugging checks (DIFFUSERS_ATTN_CHECKS) validate the tensors the backend will | ||
| # actually receive, so mixed autocast inputs must not trip them. | ||
| query, key, value = self._qkv() | ||
| device_type = torch.device(torch_device).type | ||
|
|
||
| monkeypatch.setattr(_AttentionBackendRegistry, "_checks_enabled", True) | ||
| with torch.autocast(device_type, dtype=torch.bfloat16): | ||
| out = dispatch_attention_fn(query, key, value, backend=AttentionBackendName.NATIVE) | ||
| assert out.dtype == torch.bfloat16 | ||
|
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. This is already tested in
Author
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. They overlapped on the happy path, agreed. Merged in e864233: the checks toggle now lives inside |
||
|
|
||
| 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): | ||
| dispatch_attention_fn(query, key, value, backend=AttentionBackendName.NATIVE) | ||
|
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. I would even be specific and check that there's a dtype mismatch error in the
Author
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. Done in e864233: |
||
|
|
||
| @require_torch_accelerator | ||
| 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 fp32 autocast policy for `aten::rms_norm` | ||
| # only registers for CUDA/XPU and only on recent torch, so probe for it instead of assuming. | ||
| device_type = torch.device(torch_device).type | ||
| if device_type not in ("cuda", "xpu"): | ||
| pytest.skip("aten::rms_norm only registers an fp32 autocast policy for CUDA/XPU.") | ||
|
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. (nit): Would use
Author
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. Done in e864233. The runtime probe right below it stays: the fp32 policy also depends on the torch version (new in 2.12), so the version half can't be a static skipif. |
||
|
|
||
| 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 | ||
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.
Why does it have to depend on
torch.compiler.is_compiling()?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.
Two stacked constraints. In eager,
torch.is_autocast_enabled(device_type)raises for device types without an autocast dispatch key ("meta" is the one diffusers actually hits, e.g. during meta-device init), so it needs thetorch.amp.is_autocast_available(device_type)guard. But Dynamo cannot trace that guard before torch 2.12 (it only entered the trace rules there): on torch 2.6, the floor in setup.py, compiling this function withfullgraph=Truefails withtorch.compiler.is_compiling()resolves both sides: it folds to a constant inside the graph, so compiled code skips the untraceable guard, and nothing is lost because compiled graphs never execute on meta anyway. Eager keeps the guard.test_torch_compile_fullgraph_under_autocastlocks the fullgraph behavior in; verified on 2.6.0 and 2.12.1.Once the minimum torch reaches 2.12 the short-circuit can be dropped and the condition becomes just
is_autocast_available(...) and is_autocast_enabled(...). Happy to restructure if you would rather have it factored into a small helper.Uh oh!
There was an error while loading. Please reload this page.
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.
This all feels very very LLM-ish. Could you please cut the jargon and explain things out of your own understanding? It's just easier for everyone to follow. Otherwise, it's too much unnecessary cognitive load.
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.
Sure, plain version:
torch.is_autocast_enabledcrashes on device types without autocast support ("meta" is the real case). Theis_autocast_availablecheck protects against that, but torch 2.6, our minimum, can't trace that check undertorch.compile(fullgraph=True). Sois_compiling()skips it inside compiled code, which is safe: compiled graphs never execute on meta tensors. Eager keeps the protection, compile skips the untraceable call. Once the minimum torch is 2.12, this collapses to just the two autocast calls.I also tightened the comment block in the code itself (049feef), same story in fewer lines.
One note: happy to simplify whenever it helps, and a plain "please make this simpler" does the job. The "LLM-ish" framing wasn't necessary.
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.
So, we are saying:
How does SDPA handle it in a manner that doesn't offend Dynamo?
But is there any real merit to consider that? One would probably never initialize a model on a meta device and then switch between attention backends, I think. If we consider that, does the following still apply?
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.
Fair challenge, and it made the code simpler.
On SDPA: it never checks anything at the Python level.
scaled_dot_product_attentionhas autocast kernels registered in C++ (torch._C._dispatch_dump("aten::scaled_dot_product_attention")lists AutocastCUDA/CPU/XPU/MPS entries fromaten/src/ATen/autocast_mode.cpp), so the dispatcher casts the inputs before Dynamo ever sees a branch. When AOTAutograd traces through the dispatcher, the casts just show up in the graph as ordinary ops. Our cast lives in Python, which is why traceability became a topic at all.On meta: agreed that nobody switches attention backends on a meta model. I swept the codebase to be sure, and every meta usage in src/ and tests/ is weight init or offload bookkeeping, none reaches attention. The catch is that
torch.is_autocast_enabled("meta")raises whether autocast is active or not, so an unguarded call would break plain meta-device forwards (shape or memory tracing) that work on main today. That protection doesn't requireis_autocast_available, though. A string check covers it, and the guard is nowWhich also answers the last question: with
is_autocast_availablegone there is nothing left that Dynamo can't trace.is_autocast_enabledandget_autocast_dtypecompile fine underfullgraph=Trueon torch 2.6 (zero graph breaks pertorch._dynamo.explain, correct recompile when autocast toggles between calls), sois_compiling()is gone too. Done in 38a870d; re-validated on an A4000 (torch 2.12.1+cu126): flash_hub under cuda autocast passes, the full test class is green on CUDA, and the fullgraph test passes on 2.6.0 and 2.12.1.