perf(krea2): keep the K/V heads unexpanded where a fused kernel takes them - #174
Draft
Pfannkuchensack wants to merge 1 commit into
Draft
perf(krea2): keep the K/V heads unexpanded where a fused kernel takes them#174Pfannkuchensack wants to merge 1 commit into
Pfannkuchensack wants to merge 1 commit into
Conversation
… them
Krea-2 has 48 query heads over 12 K/V heads. This processor expands the K/V
heads with repeat_interleave so that enable_gqa is not needed, because the
fused SDPA kernels used not to support it -- and the alternative is the math
backend, which materialises the full [heads, seq, seq] score matrix at ~9 GB.
That premise has become build-dependent. cuDNN does serve grouped-query
attention, mask included: measured on the real shape ([1, 48q/12kv, 4608,
128], bf16) at 3.53 ms masked and 3.64 ms unmasked, against the memory-
efficient kernel refusing it outright and flash refusing the additive mask.
So the expansion is now decided per call by asking the dispatcher instead of
assuming, and skipped where a permitted kernel takes the shape as it is.
Measured on an RTX 4090, live allocation at the SDPA call:
seq 4608 expanded 216.0 MB -> grouped 135.5 MB 13.09 -> 12.44 ms
seq 9216 expanded 432.0 MB -> grouped 271.0 MB 36.53 -> 33.10 ms
The expansion allocates two tensors four times larger than the originals,
+108 MB at 4608 tokens, every call. What it does *not* do is raise the peak
of the whole attention block: that peak is set after SDPA by the output
path, when the K/V tensors are already free. So this is less allocator
traffic and a few percent of time, not a lower ceiling -- worth stating,
because an isolated-kernel measurement (270 MB -> 197 MB) reads like the
latter.
Two things make the check conservative rather than optimistic, both because
guessing wrong costs ~9 GB rather than some speed:
- Only backends this call is *permitted* to reach are consulted. Asking
about cuDNN while the resolved list excludes it -- which the `efficient`
override does -- would answer for a kernel the dispatcher cannot use, and
the call would fail with `No available kernel`. An existing CUDA test
caught exactly that during development.
- Anything unexpected -- a non-CUDA tensor, a torch whose SDPAParams
signature differs -- answers no and expands, which is correct everywhere
and merely costs memory.
The answer depends only on the call shape, dtype, device, mask presence and
permitted backends, all fixed for a given block within a generation, so it
is cached per processor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Performance / simplification.
Krea2MemoryEfficientAttnProcessorexpands Krea-2's 12 K/V heads to 48 withrepeat_interleavebefore every SDPA call, so thatenable_gqais not needed. The module docstring gives the reason: the fused kernels did not support grouped-query attention, and the fallback is the math backend, which materialises the full[heads, seq, seq]score matrix at ~9 GB.That premise has become build-dependent. cuDNN serves grouped-query attention, mask included. Measured on the real shape (
[1, 48q/12kv, 4608, 128], bf16), RTX 4090:So the expansion is now decided per call by asking the dispatcher, and skipped where a permitted kernel takes the shape as it is.
What it is worth
Live allocation at the SDPA call, and time for the whole attention block, on an RTX 4090:
repeat_interleaveon K and V allocates +108 MB at 4608 tokens, every call, and the copy is work as well as memory.What this does not do, stated because it is easy to read the other way: it does not lower the peak of the attention block. That peak is set after SDPA by the output path — transpose, flatten, gate,
to_out— at a moment when the K/V tensors are already free, and it is unchanged at 460.8 MB (4608) and 921.7 MB (9216). An isolated-kernel measurement on a 30-series card shows 270 MB → 197 MB, which is the same 80 MB but reads like a ceiling reduction; it is not one. This is less allocator traffic and a few percent of time.Why the check is conservative
Guessing wrong in the optimistic direction does not cost speed, it costs ~9 GB — the exact failure this processor exists to prevent. Two things follow:
INVOKE_KREA2_SDPA_BACKEND=efficientdoes — answers for a kernel the dispatcher cannot use, and the call then fails outright withNo available kernel. That was a real bug in the first version of this change, caught by an existing CUDA test rather than by reasoning.SDPAParamssignature differs, falls back to the expansion — correct everywhere, merely costs memory. ROCm lands here too for masked blocks: no cuDNN, and flash refuses the mask.The answer depends only on the call shape, dtype, device, mask presence and permitted backends — all fixed for a given block within a generation — so it is cached per processor and queried once per kind.
Related Issues / Discussions
Stacked on #172 (
feat/krea2-cudnn-attention), which this PR's base branch is set to. It depends on that PR's ranked backend list: with the old permissive list the memory-efficient kernel would be reached first and refuse the unexpanded shape.The measurements that prompted this are in the local
.ideas/krea2-sdpa-shape-probe.pyoutput from a 30-series card, which showedcudnn gqa=Trueat 19.02 ms / 197 MB against 20.15 ms / 279 MB for the expanded path.QA Instructions
Measured on RTX 4090, torch 2.7.1+cu128, Windows; the shape probe that prompted it ran on a Linux 30-series card.
tests/backend/krea2/test_grouped_query_attention.py— 9 tests: the decision answers no on non-CUDA tensors, no when the permitted list excludes the capable kernels, and no whenSDPAParamsraises; the answer is cached once per call kind and re-asked when the mask state changes; the CPU path still matches the stockKrea2AttnProcessorexactly; and on CUDA the two paths agree numerically.tests/backend/krea2 tests/app/util tests/app/invocations— 1119 passed, 1 skipped, 6 xfailed.pytest --collect-onlyclean.ruff check/format --checkclean. Noopenapi.jsonchange.To verify by hand: generate with Krea-2 and compare against
main; the images should be indistinguishable (bf16 kernels differ in accumulation order, so the tests bound correlation at > 0.9999 and max difference at 0.05 rather than demanding equality). Then setINVOKE_KREA2_SDPA_BACKEND=efficientand generate again — that path must still work, because it falls back to the expansion.Not run: a generation on ROCm. The masked blocks there have no fused kernel that takes the grouped shape, so they should fall back to the expansion — which is today's behaviour, and the tests cover the decision rather than the platform.
Merge Plan
Merge after #172. No DB schema, no redux slice, no API change, no dependency change.
Checklist
What's Newcopy (if doing a release after this PR) — probably not worth a line on its own🤖 Generated with Claude Code