Repository navigation
feat(attention): add opt-in SageAttention backend for diffusion models - #9657
Conversation
New `attention_backend: auto | sage` setting; with `sage`, eligible PyTorch SDPA calls inside a denoising scope run on SageAttention 2.2 and every other call keeps SDPA unchanged. Each GPU's first SageAttention call is checked against SDPA and a failing GPU falls back for the session; no model family enters the scope yet. Adds the SageAttention docs page and the regenerated config schema.
lstein
left a comment
There was a problem hiding this comment.
Reviewed at c2efc7f. The fallback path is solid: outside the scope every call reaches torch SDPA with its arguments untouched, the per-thread ContextVar scope nests and resets correctly, the first-call GQA slice (query[:1, :group] vs key[:1, :1]) is the right head group, install cannot raise, and ROCm is never touched. Unit tests pass locally (slow tests not run; no SageAttention on this machine).
One blocker, then non-blocking notes.
Blocker
1. The kernel table does not match sageattn() for sm86 (RTX 30-series, A10/A40).
_kernel (invokeai/backend/util/sage_attention.py:134) sends every major == 8, minor != 9 device to sageattn_qk_int8_pv_fp16_cuda(pv_accum_dtype="fp32"), and the docstring says this follows sageattn's own dispatch. It does not: upstream sageattention/core.py dispatches
if arch == "sm80":
return sageattn_qk_int8_pv_fp16_cuda(..., pv_accum_dtype="fp32")
elif arch == "sm86":
return sageattn_qk_int8_pv_fp16_triton(...)and has no sm87 branch at all. So on an RTX 3090 this PR runs a kernel that SageAttention itself deliberately avoids on that architecture. At best it is an unvalidated path (only sm89 was measured); at worst the launch fails, the first-call check retires the GPU, and the feature never works on the most common Ampere consumer cards. TestKernelTable restates the same mapping, so it cannot catch this. Please either follow sageattn's table for sm86 (the Triton kernel; check whether it takes smooth_k the same way, since _unaccepted_arguments would refuse it otherwise), or drop sm86/sm87 from the table until they are measured, and correct the docstring/PR description.
Non-blocking follow-up
2. With this PR alone, attention_backend: sage has no effect but reports itself enabled. Nothing enters sage_attention_scope() yet, so startup imports SageAttention/Triton and logs "SageAttention … enabled", while the "serves … on cuda:N" line and per-generation debug counts shown in the docs' Enabling section can never appear, and the config description points at an empty Supported models list. This resolves itself as the family PRs (#9658 onward) land; just make sure the stack isn't split across a release with only this PR in it.
Advisory
-
Recovery is overstated for sticky CUDA errors. The docs say a failing GPU "uses PyTorch's attention until restart", but a sticky fault (illegal address, misaligned address,
__trap) poisons the context:_disableruns and then the fallbackoriginal(...)(:377) raises the same error. Only non-sticky failures such as "no kernel image" really fall back. Worth one sentence in the docs. -
Transient failures retire a GPU permanently (
:366-371). Onlytorch.OutOfMemoryErroris treated as transient. A Triton module-load OOM (RuntimeError("Triton Error [CUDA]: out of memory")) on a full card, a Triton cache-renamePermissionErrorwhen two Windows GPUs compile their first call concurrently, or a pending async error from an unrelated earlier kernel surfacing at the check's.item()all disable SageAttention on that device until restart. -
Validation is keyed per device, not per kernel instantiation (
:364). A first call at head_dim 128/bf16 validates the device; a later head_dim 64 or fp16 call is never compared, so a launch failure there would surface outside the fallback. -
OOM inside SageAttention propagates instead of falling back (
:366-368). The quantized copies (~1.7 B per Q/K/V element; ~2 GB per call for Wan 14B at 720p per the docs) are not in the working-memory estimators inbackend/util/attention.py, so a generation that fits with SDPA can OOM here. Falling back tooriginalafter an OOM in the Sage path would be safer, and "peak VRAM unchanged" is only measured on smaller models. -
Output layout differs from SDPA. SageAttention returns a contiguous
[B, H, N, D]tensor, while the fused SDPA kernels return[B, H, N, D]with[B, N, H, D]memory order, so callers'out.transpose(1, 2).reshape(...)becomes a full copy per call instead of a view. No caller uses.viewthere, so this is a cost to the speedup rather than a bug.
Notes for the family PRs:
- Inside the scope, SageAttention bypasses
sdpa_kernel/sdpa_policy. For Krea-2 (#9662) that silently defeatsINVOKE_KREA2_SDPA_BACKEND=cudnn, whose purpose is proving which kernel served a run, andforeign_window_entries()cannot detect it. The docs only mentionDIFFUSERS_ATTN_BACKEND. - "Text encoders and VAEs keep PyTorch SDPA" depends on scope placement: ControlNets or IP-adapter encoders run inside the denoise loop would be in scope.
- In legacy single-device mode with
device: cuda:1, the worker never callsset_device, so_cuda_indexreturns None and every call falls back asdevice, visible only at DEBUG.
Test gaps (fast suite): first-call check under GQA (a key[:1, :group] mutant survives), the autocast and is_nested exclusions, and the transient-vs-permanent error classification.
…ent SageAttention failures sm86 now runs the Triton kernel upstream's sageattn picks there, and sm87 gets none; the Windows build's CUDA routing for both is unexplained and unmeasured here. Out-of-memory and Triton cache errors fall back to SDPA for that call (three retire the device), and the first-call check runs per precision and head size. The kernel runs with the tensors' device current, so a single-device install on cuda:1 uses SageAttention too.
An out-of-memory error now moves only the rest of that generation to SDPA, logged once, instead of counting toward retiring the device; out-of-memory is recognised by the shared is_oom_error. Only Triton cache failures in a row retire a device, and a success resets the count.
# Conflicts: # invokeai/frontend/api/openapi.json
|
@lstein Thanks for the thorough review. Addressed in e9aa6c6 and ce561e3; the branch also merges current Blocker (sm86): you're right. The table mirrored the Windows wheel (woct0rdho), which moved sm86/sm87 to Non-blocking:
Family-PR notes:
Test gaps: the GQA head pairing is covered, with a distinct K/V head so a |
# Conflicts: # invokeai/frontend/api/openapi.json # tests/test_config.py
…lower with SageAttention On Windows the driver moves memory to shared system memory instead of raising out-of-memory, so the fallback never triggers and the extra memory per call slows every step; issue invoke-ai#9831 tracks a fix.
# Conflicts: # invokeai/app/services/config/config_default.py # invokeai/frontend/api/openapi.json # invokeai/frontend/api/schema.ts
> [!NOTE] > Stacked PR 1 of 8: merge after #9657. It targets `main`, so until the PRs before it merge, its diff also shows their commits and the merges that carry them forward. This PR's own change is one commit, [`e864d62574`](Pfannkuchensack@e864d62). ## Summary With `attention_backend: sage`, FLUX.1 denoising enters the SageAttention scope from #9657, so its eligible attention calls run on SageAttention, including ControlNets that run inside the denoising loop. Masked attention, such as regional prompting, and everything outside the loop stay on PyTorch SDPA. The default `auto` is unchanged. The docs page lists FLUX.1 under Supported models. ## Related Issues / Discussions Stacked on #9657 (SageAttention backend). ## QA Instructions FLUX.1 dev FP8, model loaded, three arms per seed as described in #9657: | | 1024² | 2048² | | --- | --- | --- | | Step, default SDPA | 703 ms | 3730 ms | | Step, cuDNN-first SDPA | 662 ms | 3082 ms | | Step, SageAttention | 606 ms (−14 %) | 2314 ms (−38 %) | | Whole generation | 16.1 → 14.0 s (−13 %) | 84.2 → 54.8 s (−35 %) | | Peak VRAM in the denoise scope | unchanged | unchanged | | LPIPS against default SDPA, median (cuDNN-first) | 0.024 (0.015) | 0.024 (0.009) | - Blind side-by-side review: 8 of 8 pairs equal. - Every attention call of the transformer qualified; none fell back to SDPA. - Accuracy on real activations, every call of step 0 at 1024² (114 calls): worst cosine 0.9994, relative L2 0.024 against fp32. - The timings, LPIPS and blind review come from the measurement prototype, which still used `sageattn()`'s default K smoothing; the selection #9657 ships is faster per call. For one seed at 1024², LPIPS against SDPA dropped from 0.128 to 0.041 with it. To try it: install SageAttention as the docs page from #9657 describes, set `attention_backend: sage`, restart and generate with FLUX.1. The first generation logs `SageAttention ... serves diffusion-model attention on cuda:0 (...)` with the GPU and kernel name, and with `log_level: debug` every generation logs how many calls ran on SageAttention and why the others did not. With `attention_backend: auto` the output is bit-identical to `main`. Checks: ruff clean; `pytest tests/app/invocations` passes with the whole stack applied (2090 passed). No call-site test was added: this change only enters the scope, whose behavior #9657 tests. ## Review No material findings for this call site. ## Compatibility / Rollout Merge after #9657. No API or persisted-state change; only `attention_backend: sage` behaves differently. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Meaningful regression coverage added / updated where needed; obsolete tests/code removed_ - [ ] _Persisted-state and API changes include required migrations / compatibility validation_ - [x] _Relevant performance/efficiency opportunities considered; material claims have evidence_ - [x] _Material review findings resolved and relevant checks rerun_ - [x] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
> [!NOTE] > Stacked PR 2 of 8: merge after #9658. It targets `main`, so until the PRs before it merge, its diff also shows their commits and the merges that carry them forward. This PR's own change is one commit, [`a1e97b7b09`](Pfannkuchensack@a1e97b7). ## Summary With `attention_backend: sage`, FLUX.2 denoising enters the SageAttention scope from #9657, so its eligible attention calls run on SageAttention. Everything else stays on PyTorch SDPA, and the default `auto` is unchanged. The docs page lists FLUX.2 Klein 9B under Supported models. ## Related Issues / Discussions Stacked on #9657 (SageAttention backend) and #9658, the PR before it. ## QA Instructions FLUX.2 Klein 9B FP8, model loaded, three arms per seed as described in #9657: | | 1024² | 2048² | | --- | --- | --- | | Step, default SDPA | 719 ms | 3583 ms | | Step, cuDNN-first SDPA | 699 ms | 2960 ms | | Step, SageAttention | 657 ms (−9 %) | 2483 ms (−31 %) | | Whole generation | 15.6 → 15.1 s (−3 %) | 27.1 → 23.1 s (−15 %) | | Peak VRAM in the denoise scope | unchanged | unchanged | | LPIPS against default SDPA, median (cuDNN-first) | 0.043 (0.011) | 0.059 (0.018) | - Blind side-by-side review: 8 of 8 pairs equal. The pairs with the largest LPIPS (0.14 at both sizes) show the same scene, sharpness and detail; they differ in composition details such as pose and background, the kind of drift the exact cuDNN swap also causes, only larger. - Every attention call qualified; none fell back to SDPA. - Accuracy on real activations, every call of step 0 (32 calls): worst cosine 0.9993, relative L2 0.026. - Only Klein 9B was measured; the other FLUX.2 variants enter the same scope. - The timings, LPIPS and blind review come from the measurement prototype, which still used `sageattn()`'s default K smoothing; the selection #9657 ships is faster per call. To try it: install SageAttention as the docs page from #9657 describes, set `attention_backend: sage`, restart and generate with FLUX.2. The first generation logs `SageAttention ... serves diffusion-model attention on cuda:0 (...)` with the GPU and kernel name, and with `log_level: debug` every generation logs how many calls ran on SageAttention and why the others did not. With `attention_backend: auto` the output is bit-identical to `main`. Checks: ruff clean; `pytest tests/app/invocations` passes with the whole stack applied (2090 passed). No call-site test was added: this change only enters the scope, whose behavior #9657 tests. ## Review No material findings for this call site. ## Compatibility / Rollout Merge after #9658. No API or persisted-state change; only `attention_backend: sage` behaves differently. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Meaningful regression coverage added / updated where needed; obsolete tests/code removed_ - [ ] _Persisted-state and API changes include required migrations / compatibility validation_ - [x] _Relevant performance/efficiency opportunities considered; material claims have evidence_ - [x] _Material review findings resolved and relevant checks rerun_ - [x] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
…9660) > [!NOTE] > Stacked PR 3 of 8: merge after #9659. It targets `main`, so until the PRs before it merge, its diff also shows their commits and the merges that carry them forward. This PR's own change is one commit, [`6c8718945a`](Pfannkuchensack@6c87189). ## Summary With `attention_backend: sage`, Z-Image denoising enters the SageAttention scope from #9657, so its eligible attention calls run on SageAttention. Z-Image Control passes an attention mask and therefore stays on PyTorch SDPA, like everything outside the loop. The default `auto` is unchanged. The docs page lists Z-Image under Supported models. ## Related Issues / Discussions Stacked on #9657 (SageAttention backend) and #9659, the PR before it. ## QA Instructions Z-Image FP8, model loaded, three arms per seed as described in #9657: | | 1024² | 2048² | | --- | --- | --- | | Step, default SDPA | 520 ms | 3184 ms | | Step, cuDNN-first SDPA | 490 ms | 2819 ms | | Step, SageAttention | 462 ms (−11 %) | 2231 ms (−30 %) | | Whole generation | 10.4 → 10.0 s (−4 %) | 33.7 → 25.9 s (−23 %) | | Peak VRAM in the denoise scope | unchanged | unchanged | | LPIPS against default SDPA, median (cuDNN-first) | 0.154 (0.092) | 0.168 (0.112) | - Z-Image reacts strongly to any kernel change: even the exact cuDNN swap moves LPIPS to 0.09–0.11, and SageAttention stays within 1.7× of that. Blind side-by-side review: 8 of 8 pairs equal; the pair with the largest LPIPS (0.25) shows the same scene, sharpness and detail. - Per step, the 32 calls of the main and noise-refiner layers run on SageAttention; the two text-only context-refiner layers are below the size threshold and stay on SDPA. - Accuracy on real activations, every call of step 0 (32 calls): worst cosine 0.9970, relative L2 0.043. - The timings, LPIPS and blind review come from the measurement prototype, which still used `sageattn()`'s default K smoothing; the selection #9657 ships is faster per call. To try it: install SageAttention as the docs page from #9657 describes, set `attention_backend: sage`, restart and generate with Z-Image. The first generation logs `SageAttention ... serves diffusion-model attention on cuda:0 (...)` with the GPU and kernel name, and with `log_level: debug` every generation logs how many calls ran on SageAttention and why the others did not. With `attention_backend: auto` the output is bit-identical to `main`. Checks: ruff clean; `pytest tests/app/invocations` passes with the whole stack applied (2090 passed). No call-site test was added: this change only enters the scope, whose behavior #9657 tests. ## Review No material findings for this call site. Z-Image Control gains nothing until its mask is handled (possible follow-up). ## Compatibility / Rollout Merge after #9659. No API or persisted-state change; only `attention_backend: sage` behaves differently. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Meaningful regression coverage added / updated where needed; obsolete tests/code removed_ - [ ] _Persisted-state and API changes include required migrations / compatibility validation_ - [x] _Relevant performance/efficiency opportunities considered; material claims have evidence_ - [x] _Material review findings resolved and relevant checks rerun_ - [x] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
> [!NOTE] > Stacked PR 5 of 8: merge after #9661. It targets `main`, so until the PRs before it merge, its diff also shows their commits and the merges that carry them forward. This PR's own change is one commit, [`482e7f348f`](Pfannkuchensack@482e7f3). ## Summary With `attention_backend: sage`, Krea-2 denoising enters the SageAttention scope from #9657, so its eligible attention calls run on SageAttention; its grouped-query attention (48 query and 12 K/V heads) runs natively, without expanding K/V. While `INVOKE_KREA2_SDPA_BACKEND` pins a PyTorch kernel for measurement, the scope stays closed so the pinned kernel is what runs. The default `auto` is unchanged. The docs page lists Krea-2 under Supported models. ## Related Issues / Discussions Stacked on #9657 (SageAttention backend) and #9661, the PR before it. ## QA Instructions Krea-2 Turbo GGUF Q4_K_M, model loaded, arms as described in #9657: | | 1024² | 2048² | | --- | --- | --- | | Step, SDPA (Krea-2's own cuDNN-first policy) | 1254 ms | 4976 ms | | Step, SageAttention | 1214 ms (−3 %) | 4186 ms (−16 %) | | Whole generation | 11.5 → 11.0 s (−4 %) | 42.7 → 36.2 s (−15 %) | | Peak VRAM in the denoise scope | unchanged | unchanged | | LPIPS against SDPA, median (no floor, see below) | 0.058 | 0.238 | - Krea-2 already ranks cuDNN first in its own SDPA policy, so the cuDNN-first arm is identical to the default and gives no noise floor: every composition change shows up in LPIPS. The pairs with the largest LPIPS (0.305 at 2048², 0.192 at 1024²) show the same scene with a shifted crop or a missing stack of books; hands and fabric are equally sharp at 1:1 and the lettering is correct. - Blind side-by-side review: 8 of 8 pairs equal. - Accuracy on real activations: block 0 has the same Q/K channel outlier as Qwen-Image (cosine 0.53 with K smoothing, 0.988 without, as #9657 ships). - Per step, 28 of the 32 attention calls run on SageAttention; the other 4 are below the size threshold. - The timings, LPIPS and blind review come from the measurement prototype, which still had K smoothing on; with the shipped selection one generation measured LPIPS 0.055 against SDPA. To try it: install SageAttention as the docs page from #9657 describes, set `attention_backend: sage`, restart and generate with Krea-2. The first generation logs `SageAttention ... serves diffusion-model attention on cuda:0 (...)` with the GPU and kernel name, and with `log_level: debug` every generation logs how many calls ran on SageAttention and why the others did not. With `attention_backend: auto` the output is bit-identical to `main`. Checks: ruff clean; `pytest tests/app/invocations` passes with the whole stack applied (2090 passed). No call-site test was added: this change only enters the scope, whose behavior #9657 tests. ## Review Resolved: with `INVOKE_KREA2_SDPA_BACKEND` set, SageAttention served the calls the override was meant to measure; the scope now stays closed while it is set. ## Compatibility / Rollout Merge after #9661. No API or persisted-state change; only `attention_backend: sage` behaves differently. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Meaningful regression coverage added / updated where needed; obsolete tests/code removed_ - [ ] _Persisted-state and API changes include required migrations / compatibility validation_ - [x] _Relevant performance/efficiency opportunities considered; material claims have evidence_ - [x] _Material review findings resolved and relevant checks rerun_ - [x] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
…led (#9664) > [!NOTE] > Stacked PR 7 of 8: merge after #9663. It targets `main`, so until the PRs before it merge, its diff also shows their commits and the merges that carry them forward. This PR's own changes are two commits, [`9507eccfb6`](Pfannkuchensack@9507ecc) (the scope) and [`b3c3e4300a`](Pfannkuchensack@b3c3e4300a) (the audio result in the docs). ## Summary With `attention_backend: sage`, LTX-2 denoising enters the SageAttention scope from #9657. Its video self-attention runs on SageAttention; the audio attention is below the size threshold and the text cross-attention passes a mask, so both stay on PyTorch SDPA. The default `auto` is unchanged. The docs page lists LTX-2 under Supported models. ## Related Issues / Discussions Stacked on #9657 (SageAttention backend) and #9663, the PR before it. ## QA Instructions LTX-2.5 distilled int8, model loaded, three arms per seed as described in #9657: | | 1248×704, 121 frames | | --- | --- | | Step, default SDPA | 5263 ms | | Step, cuDNN-first SDPA | 4726 ms | | Step, SageAttention | 4178 ms (−21 %) | | Whole generation | 72.0 → 63.4 s (−12 %) | | Peak VRAM in the denoise scope | unchanged | | Per-frame LPIPS against default SDPA, median (cuDNN-first) | 0.142 (0.090) | | Flicker ratio against default SDPA | 1.001 | - Per step, the 48 video self-attention calls run on SageAttention. Audio self-attention and the two audio–video cross-attentions (3 × 48) are below the size threshold, and the text cross-attention (2 × 48) passes a mask; all of these stay on SDPA. Dropping the text mask where it masks nothing is a possible follow-up. - Accuracy on real activations, every call of step 0 (130 calls): worst cosine 0.9993, relative L2 0.027. - Frames were compared per frame by LPIPS and by frame-to-frame flicker. - **Audio.** It reads the video through cross-attention, so it changes even though its own attention stays on SDPA. Measured with the code of this PR on 8 clips (4 prompts × 2 seeds, 1248×704, 121 frames; two prompts with clear sync events: four hand claps, a spoken sentence), against default SDPA, with cuDNN-first as the exact-kernel noise floor: | | SageAttention | cuDNN-first (floor) | | --- | --- | --- | | Log-mel distance, median (max) | 0.92 dB (1.58) | 0.74 dB (1.36) | | Onset-envelope cosine, median | 0.980 | 0.986 | | Shift against the SDPA audio | 0 ms in all 8 | 0 ms | | Loudness | within ±0.2 dB | within ±0.26 dB | | Clipping / silence | none | none | Sync to the picture: on the clap and speech clips, motion and sound onsets correlate clearly (0.4–0.65), and the lag between them is identical in all three arms on every clip, so SageAttention does not move the sound against the picture. On the other two prompts there are no sync events to measure. Nobody listened to the clips; the numbers are the evidence. - At 1792×1024 all three arms ran out of memory on 24 GB without partial loading, so that size was not measured. - The timings and LPIPS come from the measurement prototype, which still used `sageattn()`'s default K smoothing; the selection #9657 ships is faster per call. To try it: install SageAttention as the docs page from #9657 describes, set `attention_backend: sage`, restart and generate with LTX-2. The first generation logs `SageAttention ... serves diffusion-model attention on cuda:0 (...)` with the GPU and kernel name, and with `log_level: debug` every generation logs how many calls ran on SageAttention and why the others did not. With `attention_backend: auto` the output is bit-identical to `main`. Checks: ruff clean; `pytest tests/app/invocations` passes with the whole stack applied (2090 passed). No call-site test was added: this change only enters the scope, whose behavior #9657 tests. ## Review Resolved: the audio was not compared at first. It now is (see QA), and the docs state the result. ## Compatibility / Rollout Merge after #9663. No API or persisted-state change; only `attention_backend: sage` behaves differently. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Meaningful regression coverage added / updated where needed; obsolete tests/code removed_ - [ ] _Persisted-state and API changes include required migrations / compatibility validation_ - [x] _Relevant performance/efficiency opportunities considered; material claims have evidence_ - [x] _Material review findings resolved and relevant checks rerun_ - [x] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
> [!NOTE] > Stacked PR 8 of 8: merge after #9664. It targets `main`, so until the PRs before it merge, its diff also shows their commits and the merges that carry them forward. This PR's own changes are two commits, [`cd6e58c52d`](Pfannkuchensack@cd6e58c) (the scope) and [`3d1a8b49f0`](Pfannkuchensack@3d1a8b49f0) (the A14B measurements in the docs). ## Summary With `attention_backend: sage`, Wan 2.2 denoising ("Denoise - Wan 2.2" and "Denoise Video - Wan 2.2") enters the SageAttention scope from #9657, so its eligible attention calls run on SageAttention. Everything else stays on PyTorch SDPA, and the default `auto` is unchanged. The docs page lists Wan 2.2 under Supported models. ## Related Issues / Discussions Stacked on #9657 (SageAttention backend) and #9664, the PR before it. Related: #9529, which asks for SageAttention to speed up Wan 2.2 A14B at 1280×720 on a 24 GB GPU. This PR does not close it: at that size a 24 GB GPU is already full with PyTorch's attention, and SageAttention made it slower (see QA). #9831 tracks that, together with Wan's missing working-memory estimate. ## QA Instructions Wan 2.2 TI2V-5B GGUF Q8 through "Denoise Video - Wan 2.2", model loaded, three arms per seed as described in #9657: | | 1280×704, 121 frames | | --- | --- | | Step, default SDPA | 10419 ms | | Step, cuDNN-first SDPA | 7890 ms | | Step, SageAttention | 6358 ms (−39 %) | | Whole generation | 380.9 → 247.9 s (−35 %) | | Peak VRAM in the denoise scope | unchanged | | Per-frame LPIPS against default SDPA, median (cuDNN-first) | 0.104 (0.079) | | Flicker ratio against default SDPA | 0.998 | - All 120 attention calls per step ran on SageAttention, including the text cross-attention with 512 text tokens; none fell back. - Accuracy on real activations, every call of step 0 (120 calls): worst relative L2 0.076, in the text cross-attention; the heads with low cosine there contribute almost nothing (output norm 0.08 against a median of 5.7). - **Wan 2.2 T2V A14B** (GGUF Q4_K_M, both experts, 4-step Lightning LoRA V1.1, 81 frames, partial loading on, `device_working_mem_gb: 10`, which A14B needs on 24 GB without SageAttention too, see #9831), denoising only: | | Default SDPA | SageAttention | Peak VRAM | | --- | --- | --- | --- | | 832×480 | 121.5 s | 60.9 s (−50 %) | 18.0 / 18.1 GB | | 1280×720 | 293 s | 662 s (+126 %) | 24.1 / 24.0 GB | At 1280×720 the GPU is already full with SDPA; SageAttention's quantized copies (~1.2 GB more than SDPA per call) push it over, and Windows moves memory to shared system memory instead of raising out-of-memory, so its fallback never triggers. The docs row states both results. - **"Denoise - Wan 2.2" (image):** TI2V-5B at 1280×704 compresses each image to 880 tokens, below the 1536-token threshold, so all 2400 calls of a measured generation stayed on SDPA. - The timings and LPIPS come from the measurement prototype, which still used `sageattn()`'s default K smoothing; the selection #9657 ships is faster per call. To try it: install SageAttention as the docs page from #9657 describes, set `attention_backend: sage`, restart and generate with Wan 2.2. The first generation logs `SageAttention ... serves diffusion-model attention on cuda:0 (...)` with the GPU and kernel name, and with `log_level: debug` every generation logs how many calls ran on SageAttention and why the others did not. With `attention_backend: auto` the output is bit-identical to `main`. Checks: ruff clean; `pytest tests/app/invocations` passes with the whole stack applied (2090 passed). No call-site test was added: this change only enters the scope, whose behavior #9657 tests. ## Review No material findings for this call site. Remaining: Wan 2.2 A14B at 1280×720 on a 24 GB GPU is slower with SageAttention (#9831). ## Compatibility / Rollout Merge after #9664. No API or persisted-state change; only `attention_backend: sage` behaves differently. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Meaningful regression coverage added / updated where needed; obsolete tests/code removed_ - [ ] _Persisted-state and API changes include required migrations / compatibility validation_ - [x] _Relevant performance/efficiency opportunities considered; material claims have evidence_ - [x] _Material review findings resolved and relevant checks rerun_ - [x] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
> [!NOTE] > Stacked PR 6 of 8: merge after #9662. It targets `main`, so until the PRs before it merge, its diff also shows their commits and the merges that carry them forward. This PR's own change is one commit, [`1968eb9c58`](Pfannkuchensack@1968eb9). ## Summary With `attention_backend: sage`, SDXL denoising and SDXL tiled multi-diffusion enter the SageAttention scope from #9657, so eligible attention calls run on SageAttention, including ControlNets that run inside the loop. SD1.5 and SD2 do not enter it: SD1.5's head sizes (40, 80, 160) never qualify, and SD2 is unmeasured. With `attention_type: sliced`, SDXL's attention does not go through SDPA, so nothing changes there. The default `auto` is unchanged. The docs page lists SDXL under Supported models and notes the sliced case. ## Related Issues / Discussions Stacked on #9657 (SageAttention backend) and #9662, the PR before it. ## QA Instructions SDXL, model loaded, three arms per seed as described in #9657: | | 1024² | 2048² | | --- | --- | --- | | Step, default SDPA | 134 ms | 631 ms | | Step, cuDNN-first SDPA | 135 ms | 579 ms | | Step, SageAttention | 126 ms (−6 %) | 496 ms (−21 %) | | Whole generation | 4.4 → 4.2 s (−5 %) | 19.6 → 16.1 s (−18 %) | | Peak VRAM in the denoise scope | unchanged | unchanged | | LPIPS against default SDPA, median (cuDNN-first) | 0.027 (0.015) | 0.014 (0.006) | - At 1024², 10 of the 140 attention calls per UNet pass qualify: the 4096-token self-attention of the outer blocks. The 1024-token self-attention of the inner blocks and all 77-token text cross-attention are below the break-even and stay on SDPA. At 2048² the inner blocks qualify too. - Blind side-by-side review: 7 of 8 pairs equal, 1 judged better with SageAttention. - Accuracy on real activations, every call of step 0 (10 calls): cosine 1.0000, relative L2 0.003. - The timings, LPIPS and blind review come from the measurement prototype, which still used `sageattn()`'s default K smoothing; the selection #9657 ships is faster per call. To try it: install SageAttention as the docs page from #9657 describes, set `attention_backend: sage`, restart and generate with SDXL. The first generation logs `SageAttention ... serves diffusion-model attention on cuda:0 (...)` with the GPU and kernel name, and with `log_level: debug` every generation logs how many calls ran on SageAttention and why the others did not. With `attention_backend: auto` the output is bit-identical to `main`. Checks: ruff clean; `pytest tests/app/invocations` passes with the whole stack applied (2090 passed). No call-site test was added: this change only enters the scope, whose behavior #9657 tests. ## Review Resolved: the scope first opened for every UNet base, including the unmeasured SD2; it now opens for SDXL only. ## Compatibility / Rollout Merge after #9662. No API or persisted-state change; only `attention_backend: sage` behaves differently. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Meaningful regression coverage added / updated where needed; obsolete tests/code removed_ - [ ] _Persisted-state and API changes include required migrations / compatibility validation_ - [x] _Relevant performance/efficiency opportunities considered; material claims have evidence_ - [x] _Material review findings resolved and relevant checks rerun_ - [x] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
Summary
At high resolutions and for video, most of a diffusion step is attention, and Windows builds of PyTorch have no flash-attention kernel. This adds an opt-in
attention_backend: sagesetting that runs eligible attention inside the denoising model on SageAttention 2.2, an 8-bit quantized attention kernel. On an RTX 4090 under Windows those calls run 2.0–2.9× faster than PyTorch's fastest SDPA kernel, and whole generations get 3–13 % faster at 1024², 15–35 % at 2048² and 12–35 % for the two measured video models. Images are not pixel-identical toautoat the same seed; in a blind review of 48 image pairs none was judged worse. The default,auto, leaves PyTorch's attention untouched.This PR adds the mechanism, the setting and the docs; no model family uses SageAttention yet. Each family enters the scope in its own stacked PR, with its own measurements:
How it works:
attention_backend: sage,apply_monkeypatchescallsinstall_sage_attention(), which wrapstorch.nn.functional.scaled_dot_product_attentiononce, the way the ROCm SDPA guard already does. That one wrapper covers direct SDPA calls, diffusers attention processors and diffusers'nativedispatch backend. diffusers' ownsagebackend is not used: it raises on any mask, applies process-wide and misses the direct calls.sage_attention_scope(), a per-threadContextVarscope that denoise invocations enter. Text encoders, VAEs, other threads and every call outside the scope keep SDPA as before.device: cuda:1never makes it so). Every other call reaches the original SDPA with its arguments untouched. Withlog_level: debug, each scope logs how many calls ran on SageAttention and why the others did not.sageattn()'s table (sm80: FP16 PV in CUDA; sm86: FP16 PV in Triton; sm87: none; sm89: FP8 PV; sm90: the sm90 FP8 kernel; sm100/120/121: per-warp FP8 PV), but with K smoothing off. The Windows wheel the docs install moved sm86/sm87 to the CUDA kernel without a stated reason; neither is measured here, so upstream's choice stands, and the hardware tests also run the sm86 Triton kernel on the 4090. The default smoothing breaks block 0 of Qwen-Image and Krea-2, where one Q/K channel sits near 600 for all image tokens: cosine against fp32 drops to 0.18 and 0.53, against 0.997 and 0.988 without smoothing, which is also 2–11 % faster. A real Qwen-Image head from that block is a 192 KB test fixture.is_oom_error) moves the rest of that generation to SDPA, which needs less, with one INFO line; the next generation tries again. A Triton cache write failure (two GPUs compiling one kernel at once on Windows) falls back for that call; three in a row retire the GPU.smooth_k), startup logs one warning with the reason and nothing is wrapped.Users install SageAttention themselves;
pyproject.tomlanduv.lockare unchanged. The new docs page covers Windows (triton-windows and a prebuilt wheel, installed into InvokeAI's.venvwithuv pip) and Linux (built from source).Related Issues / Discussions
Related: #9529, which asks for flash-attn or SageAttention to speed up Wan 2.2. The Wan PR #9665 closes it; flash-attn is not part of this stack.
QA Instructions
Checks on this branch:
uv run --no-sync pytest tests/backend/util/test_sage_attention.py tests/test_config.py tests/test_imports.py: 118 passed. The unit tests use a fake kernel that declares the real keyword signature.uv run --no-sync pytest -m slow tests/backend/util/test_sage_attention.pyon an RTX 4090 with SageAttention 2.2.0: 16 passed, each test once with the GPU's own kernel and once with the sm86 Triton kernel. They cover accuracy on FLUX, SDXL, Krea-2 GQA and a partial-tile cross-attention shape, each asserting the kernel served the call; the Qwen-Image outlier head (relative L2 0.10, against 1.62 with K smoothing); results identical to calling the kernel directly inside the scope and to SDPA outside it and for masked or autocast calls; and a first call that needs no more memory than later ones.ruff checkandruff format --check;pnpm -C docs test,pnpm -C docs build(all internal links valid) andpnpm -C docs check-deploy-output.openapi.json,schema.tsandsettings.jsonwere regenerated; the new field is the only change.Manual:
auto, FLUX.1 at 1024² and a fixed seed is bit-identical tomain, and no SageAttention line is logged.attention_backend: sageand restart. Startup logsSageAttention 2.2.0+cu130torch2.10.0andhigher.post6 enabled for the attention inside supported diffusion models .... Without the package, startup logs one warning that names the reason.Measurements behind the stack, on an RTX 4090 with Windows 10, torch 2.13.0+cu130, triton-windows 3.7.1.post27 and SageAttention 2.2.0 (post6 Windows wheel):
Whole generation, model loaded:
sageattn()'s default K smoothing. The selection shipped here is 2–11 % faster per call; for one FLUX.1 seed at 1024², its LPIPS against SDPA dropped from 0.128 to 0.041.Review
Resolved:
sageattn()'s CUDA rule (fp32+fp16 only from CUDA 12.8), Blackwell is limited to the capabilities SageAttention has kernels for, installation can no longer raise, and a kernel whose signature would silently dropsmooth_kis refused.device: cuda:1in single-device mode, every call fell back silently because the worker never makes that device current; the wrapper now does for the call.Remaining risks:
autoat the same seed. In Qwen-Image block 52, V reaches about 3500 in every channel, which costs every SageAttention variant about 17 % relative error per call (bf16 SDPA: 0.2 %); those images were still judged equivalent.[B, H, N, D]tensor where the fused SDPA kernels return[B, N, H, D]memory order, so callers'transpose(1, 2).reshape(...)copies. The measured end-to-end times include that copy.Compatibility / Rollout
attention_backend(autoorsage, defaultauto, environment variableINVOKEAI_ATTENTION_BACKEND). No migration; withautonothing is wrapped.openapi.json,schema.tsanddocs/src/generated/settings.jsonadd only this field; the frontend does not use it.mainand shows the earlier commits until they merge. This PR alone enables nothing (the startup line says SageAttention is enabled, but no model enters its scope), so it should not ship in a release without at least one family PR.attention_backend: autoor uninstall the package.Checklist
What's Newcopy (if doing a release after this PR)