Cast attention inputs to the autocast dtype in dispatch_attention_fn#14160
Cast attention inputs to the autocast dtype in dispatch_attention_fn#14160danielxmed wants to merge 5 commits into
Conversation
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 via its own autocast registration, but backends that call external kernels (flash-attn, sage, xformers, ...) receive the tensors as-is and fail, e.g. "FlashAttention only support fp16 and bf16 data type". Mirror SDPA's autocast behavior at the dispatch boundary so every backend sees the same dtypes as the native one. Fixes huggingface#14104. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sayakpaul
left a comment
There was a problem hiding this comment.
Left two high-level comments.
| # which `is_autocast_enabled` raises. It is skipped when compiling: Dynamo cannot trace it before | ||
| # torch 2.12, and compiled graphs never run on such devices anyway. | ||
| device_type = query.device.type | ||
| if (torch.compiler.is_compiling() or torch.amp.is_autocast_available(device_type)) and torch.is_autocast_enabled( |
There was a problem hiding this comment.
Why does it have to depend on torch.compiler.is_compiling()?
There was a problem hiding this comment.
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 the torch.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 with fullgraph=True fails with
Unsupported: Graph break due to unsupported builtin torch._C._is_autocast_available
torch.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_autocast locks 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.
There was a problem hiding this comment.
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.
Sure, plain version: torch.is_autocast_enabled crashes on device types without autocast support ("meta" is the real case). The is_autocast_available check protects against that, but torch 2.6, our minimum, can't trace that check under torch.compile(fullgraph=True). So is_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.
So, we are saying:
Torch SDPA casts its inputs to the autocast dtype internally, but backends calling external kernels
How does SDPA handle it in a manner that doesn't offend Dynamo?
torch.is_autocast_enabled crashes on device types without autocast support ("meta" is the real case).
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?
but torch 2.6, our minimum, can't trace that check under torch.compile(fullgraph=True).
There was a problem hiding this comment.
Fair challenge, and it made the code simpler.
On SDPA: it never checks anything at the Python level. scaled_dot_product_attention has autocast kernels registered in C++ (torch._C._dispatch_dump("aten::scaled_dot_product_attention") lists AutocastCUDA/CPU/XPU/MPS entries from aten/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 require is_autocast_available, though. A string check covers it, and the guard is now
if device_type != "meta" and torch.is_autocast_enabled(device_type):Which also answers the last question: with is_autocast_available gone there is nothing left that Dynamo can't trace. is_autocast_enabled and get_autocast_dtype compile fine under fullgraph=True on torch 2.6 (zero graph breaks per torch._dynamo.explain, correct recompile when autocast toggles between calls), so is_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.
| self.assertTrue(np.allclose(conversion, after_conversion, atol=1e-3)) | ||
|
|
||
|
|
||
| class AttentionDispatchAutocastTests(unittest.TestCase): |
There was a problem hiding this comment.
This should be implemented with pytest.
There was a problem hiding this comment.
Done in d26452b: dropped unittest.TestCase for plain asserts, pytest.raises/pytest.skip, and the monkeypatch fixture for the checks toggle. Renamed the class to TestAttentionDispatchAutocast so default pytest collection picks it up. Re-ran on torch 2.6.0 and 2.12.1 (CPU): 5 passed, 1 GPU-skip on both.
There was a problem hiding this comment.
🤗 Serge says:
Clean, well-scoped fix. dispatch_attention_fn now mirrors SDPA's autocast input-casting so external-kernel backends (flash, sage, xformers, ...) stop receiving the fp32/bf16 mix produced by the torch.nn.RMSNorm QK-norm + autocast interaction on torch 2.12. The guards are sound: is_autocast_available correctly shields device types (e.g. meta) where is_autocast_enabled would raise, and the torch.compiler.is_compiling() short-circuit keeps the branch fullgraph-traceable on the 2.6 floor. All three autocast APIs accept the string device_type form since torch 2.4, so the floor is safe.
Tests
- The new
AttentionDispatchAutocastTestscover the casting, mask eligibility (float vs bool), the opt-in dtype checks, the pass-through-outside-autocast invariant, the end-to-end RMSNorm pattern (accelerator-gated with a runtime probe), and fullgraph compile. Imports (is_torch_compile,require_torch_accelerator, registry internals) all resolve.
Minor (non-blocking)
- The float64 eligibility gate keys only off
query.dtype != torch.float64before casting all of q/k/v, whereas SDPA evaluates autocast eligibility per tensor. A genuinely mixed float64/bf16 q/k/v set would be handled slightly differently, but that combination doesn't arise in the models this targets, so it's not worth complicating the branch.
LGTM.
serge v0.1.0 · model: claude-opus-4-8 · 5 LLM turns · 5 tool calls · 36.3s · 75302 in / 2174 out tokens
Requested in review: drop unittest.TestCase in favor of plain asserts, pytest.raises/pytest.skip, and the monkeypatch fixture. Renamed to TestAttentionDispatchAutocast so default pytest collection picks it up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| finally: | ||
| _AttentionBackendRegistry._backends[AttentionBackendName.NATIVE] = original | ||
|
|
||
| def test_autocast_casts_inputs_to_autocast_dtype(self): |
There was a problem hiding this comment.
I would mark these tests with require_torch_accelerator because I don't think they need to run on our fast PR CI.
There was a problem hiding this comment.
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.
| dtypes the same way SDPA's autocast registration does. | ||
| """ | ||
|
|
||
| def _qkv(self, qk_dtype=torch.float32, v_dtype=torch.bfloat16): |
There was a problem hiding this comment.
Is the qk_type set to torch.float32 to emulate the QKnorm behaviour?
There was a problem hiding this comment.
Yes, exactly. fp32 q/k with bf16 v is what the aten::rms_norm policy 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).
| 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 |
There was a problem hiding this comment.
This is already tested in test_autocast_casts_inputs_to_autocast_dtype
There was a problem hiding this comment.
They overlapped on the happy path, agreed. Merged in e864233: the checks toggle now lives inside test_autocast_casts_inputs_to_autocast_dtype and the standalone test is gone. What it guarded is the opt-in DIFFUSERS_ATTN_CHECKS validation, which runs before the backend is invoked and false-raised under autocast on main; keeping the toggle enabled in the surviving test covers that with one test fewer.
| with pytest.raises(RuntimeError): | ||
| dispatch_attention_fn(query, key, value, backend=AttentionBackendName.NATIVE) |
There was a problem hiding this comment.
I would even be specific and check that there's a dtype mismatch error in the RuntimeError message (assuming that's the case).
There was a problem hiding this comment.
Done in e864233: match="Expected query, key, and value to have the same dtype". That's SDPA's shared input validation message, identical on torch 2.6.0 and 2.12.1, and the same on CUDA (checked on an A4000).
| if device_type not in ("cuda", "xpu"): | ||
| pytest.skip("aten::rms_norm only registers an fp32 autocast policy for CUDA/XPU.") |
There was a problem hiding this comment.
(nit): Would use pytest.mark.skipif and use if torch_device not in ["cuda", "xpu"] as the reason.
There was a problem hiding this comment.
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.
| assert out.dtype == torch.bfloat16 | ||
|
|
||
| @is_torch_compile | ||
| def test_torch_compile_fullgraph_under_autocast(self): |
sayakpaul
left a comment
There was a problem hiding this comment.
Left further comments and apologies if my statement on LLM-ish framing came off as offensive. Thanks for speaking your mind!
|
No offense taken, and thanks for saying it. All good. Round summary, everything pushed:
|
What does this PR do?
Fixes #14104.
Starting with torch 2.12,
aten::rms_normregisters an fp32 cast policy forAutocastCUDA/AutocastXPU. Bisected viatorch._C._dispatch_dump("aten::rms_norm"): absent on 2.6 through 2.11, present on 2.12, and verified behaviorally on GPU for 2.8 vs 2.12. The policy is intentional on the torch side (pytorch/pytorch#174824, a numerics fix), currently debated as a memory/throughput regression in pytorch/pytorch#188955, but torch 2.12 stable ships with it either way. Under an activetorch.autocastcontext thetorch.nn.RMSNormQK norms used by Flux-family attention processors therefore return float32 even for bf16/fp16 inputs, whilevalue, which never passes through a norm, keeps the autocast dtype.dispatch_attention_fnforwarded that fp32/bf16 mix untouched:aten::scaled_dot_product_attentionhas its own autocast registration that downcasts its autocast-eligible floating inputs;flash,flash_hub,sage,aiter,xformers,npu,xlacall the kernel directly, outside autocast's dispatch handling. flash-attn dies withRuntimeError: FlashAttention only support fp16 and bf16 data type, andflexrejects the dtype mix as well;DIFFUSERS_ATTN_CHECKSdtype checks false-raised on the native backend under autocast, although the op itself would have run fine.The affected pattern (
torch.nn.RMSNormQK-norm, then RoPE, thendispatch_attention_fn) appears in 18 model files: Flux, Flux2, Chroma, Bria, BriaFibo, OvisImage, LongCatImage, LTX, LTX2, Wan, WanAnimate, ChronoEdit, SkyReelsV2, Helios, JoyImage, JoyImage Edit Plus (merged July 8, a week after the issue was filed), MotifVideo, ErnieImage, plus the Anima condition embedder. So this fixes the dispatcher once rather than patching every processor: when autocast is active for the tensors' device type,dispatch_attention_fnnow casts q/k/v and floating-point masks totorch.get_autocast_dtype(device_type), mirroring SDPA's own autocast registration, including its eligibility rule (float64 tensors and bool/int masks pass through untouched). Sincedispatch_attention_fnis the documented drop-in forF.scaled_dot_product_attention, this is parity with the op it replaces, not new policy:_native_cudnn/_native_flashpaths, which call the decomposed aten ops directly and bypassed the composite's autocast handling;torch.autocast(..., enabled=False), same as with raw SDPA.Two implementation details worth flagging for review:
torch.is_autocast_enabled(device_type)raises for device types without an autocast dispatch key. The only such device type diffusers tensors realistically report ismeta(meta-device forward works on main and keeps working), so the guard is a literaldevice_type != "meta"check. Everything left in the branch is Dynamo-traceable on torch 2.6, the repo floor:torch.compile(dispatch_attention_fn, fullgraph=True)traces with zero graph breaks, autocast on and off, verified on 2.6.0 and 2.12.1 and locked in by a@is_torch_compileregression test.examples/use it, including Flux LoRA training validation), and this failure hits any trainer that selects a non-native attention backend. The issue was reported from OneTrainer, which has since made mixed precision always-on (always enable mixed-precision / autocasting Nerogar/OneTrainer#1481), so their whole user base lands on this path once they move to torch 2.12.Tests
New
TestAttentionDispatchAutocast(plain pytest style, gated behindrequire_torch_acceleratorso it stays off the fast PR CI) intests/models/test_attention_processor.py, the first unit tests fordispatch_attention_fn:test_autocast_casts_inputs_to_autocast_dtype: mixed fp32 q/k + bf16 v under autocast, with theDIFFUSERS_ATTN_CHECKSvalidation enabled: the backend fn must receive uniform bf16 and the checks must not false-raise (both fail on main);test_autocast_casts_floating_point_mask_but_not_bool_mask(fails on main);test_no_autocast_dtypes_pass_through_unchanged: outside autocast the dispatcher still doesn't touch dtypes, the mismatch keeps raising SDPA's exact "same dtype" message;test_rms_norm_qk_pattern_under_autocast(skipifCUDA/XPU only): end-to-end shape of the bug through a realtorch.nn.RMSNorm, asserting the fp32 upcast actually happens (guards against PyTorch policy changes hollowing the test) and that the backend receives bf16;test_torch_compile_fullgraph_under_autocast(@is_torch_compile): the autocast branch stays fullgraph-traceable.Validation:
GPU validation (CUDA, real flash-attn2 kernels, RunPod RTX A4000, torch 2.12.1+cu126):
Limitations
Before submitting
.ai/review-rules.md.docs/source/en/optimization/attention_backends.mddescribes backend selection and opt-in checks, neither of which changes; autocast now simply behaves consistently across backends.Who can review?
@sayakpaul (attention dispatcher) @DN6 (recent
attention_dispatch.pyfixes)