Widen forward head-dim support on Blackwell (SM100)#24
Open
Costa-SM wants to merge 7 commits into
Open
Conversation
FlashQLA's chunk kernels are parameterized on head_dim (DK/DV) but the entry
points hard-assert head_dim == 128 (the Qwen3.5/3.6 config), which blocks
GDN-hybrid models using other head dims (e.g. head_dim_k=64, head_dim_v=128 from
expand_v=2.0) -- they'd otherwise have to zero-pad K to 128 (2x the K-work).
This enables the forward/inference path for the head dims that are actually
correct on Blackwell (SM100), established by a forward parity sweep vs the fp64
reference across the full config matrix (GQA, varlen, h0, both state layouts):
head_dim_k (the tcgen05 MMA contraction dim): {64, 128} correct on all configs.
16/48/80/256 -> raise; 96/160/192 -> run but SILENTLY miscompute (rel-err ~0.4);
32 -> correct for state_v_first=False but SILENTLY wrong for the vk layout.
So head_dim_k is guarded to the explicit set {64, 128} -- a range/%16 guard
would admit the silently-wrong widths (and even 32 is layout-unsafe).
head_dim_v (free/output dim): flexible; every multiple of 16 in [32, 256]
validated (no silent-wrong), both state layouts.
Changes:
- Central, arch-aware guard in chunk_gated_delta_rule_fwd. Raises ValueError (not
assert -- survives `python -O`, important since the failure mode is silent) with a
clear message. Single source of truth in chunk/head_dim.py; the blackwell kernels
reference it.
- Forward guards relaxed on Blackwell only. Hopper (SM90) uses different MMA atoms,
was not swept, and retains head_dim == 128.
- Backward kept at K == V == 128: non-128 backward produces incorrect gradients
(follow-up).
- Tests: forward parity over head_dim_k {64,128} x full CORE_CONFIGS and head_dim_v
{32,48,64,96,128,160,192,256}; negative tests assert unsupported dims
(16/32/96/160, non-mul-16/out-of-range V) raise.
Performance (B200, GDN scan, H=12, V=128, vs FLA Triton): head_dim_k=64 -> 2.4-3.6x;
head_dim_k=128 (original path) -> 1.2-1.4x (not regressed).
…benchmark
- Hoist `from ..head_dim import ...` to module top in the four Blackwell forward
kernels (kkt_solve, fused_fwd, prepare_h, cp_fwd) instead of importing inside
each function body. head_dim.py has no package deps, so this is not circular.
- Fix a stale comment in chunk_gated_delta_rule_fwd that read head_dim_k in
{32,64,128}; the guard is {64,128} (32 is excluded -- silently wrong for the vk
state layout).
- Mark the new head-dim tests @pytest.mark.blackwell so they skip on SM90: they
assert Blackwell-only behavior (the ValueError guard is gated on SM100) and
would otherwise fail on Hopper CI. Rename the dk32 negative-test id to
dk32_layout_unsafe.
- Drop the module-global shadowing in _make_inputs; use the head_dim_k/head_dim_v
params directly.
- Add benchmark/bench_head_dim_k.py, which reproduces the perf table via the
repo's do_bench harness (event/cudagraph), sweeping head_dim_k in {64,128} at
head_dim_v=128 vs FLA. The existing bench hardcodes HEAD_DIM=128.
Perf numbers re-measured with do_bench (stable across backends, B200):
head_dim_k=64 -> 2.6-4.2x, head_dim_k=128 (original path) -> 2.5-4.0x vs FLA.
Replace the relative `from ..head_dim import ...` in the four Blackwell forward kernels with the absolute `from flash_qla.ops.gated_delta_rule.chunk.head_dim import ...`, matching the repo convention (these files already import `flash_qla.utils` absolutely). No behavior change.
The guard admits any (head_dim_k in {64,128}) x (head_dim_v mul-16 in [32,256]),
but the single-axis sweeps only covered head_dim_k at V=128 (test_fwd_head_dim_k)
and head_dim_v at K=64 (test_fwd_head_dim_v) -- the head_dim_k=128 x head_dim_v!=128
combinations were admitted but untested.
Add test_fwd_head_dim_kv_cross: forward parity vs the fp64 reference for
head_dim_k=128 with head_dim_v in {32,64,96,160,256} over CORE_CONFIGS[:3] x both
state layouts (30 cases, all pass). Includes V=96/160 -- widths that silently
miscompute as head_dim_k but are fine as the free head_dim_v -- to lock in that
K/V asymmetry.
Keep the perf-repro script out of the PR for now; it's provided inline in the PR description instead. Correctness is covered by tests/test_gdr_unit.py and the perf numbers reference the repo's existing do_bench harness.
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # flash_qla/ops/gated_delta_rule/chunk/__init__.py # flash_qla/ops/gated_delta_rule/chunk/blackwell/prepare_h.py
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.
Hi folks.
I really wanted to try FlashQLA out with some different head dimension values (e.g. GDN-hybrid
expand_v=2.0layout w/head_dim_k=64,head_dim_v=128) and I noticed that although the chunkgated_delta_rulekernels are parameterized on the head dims (DK/DV), the Python entry points asserthead_dim == 128. So here's a PR to widen the acceptedhead_dimvalues during forward passes on Blackwell GPUs :DI ran a parity sweep against the fp64 reference over the full config matrix (GQA, varlen, non-null
h0, both state layouts) to get the head dims that are actually correct on the forward/inference path. During this I discovered that a fewhead_dim_kvalues (96 / 160 / 192) run without erroring and return incorrect results. So this PR also swaps the bareassertfor a real guard that rejects these values.Measurements
The parity sweep demonstrated that only selected values of
head_dim_kwork whilehead_dim_vbasically accepts anything you throw at it. The parity checks forhead_dim_kwere run for values{16, 32, 48, 64, 80, 96, 128, 160, 192, 256}usinghead_dim_v=128. Forhead_dim_v, every multiple of 16 in[32, 256]was tested and is correct.head_dim_k64,12816,48,80,25696,160,19232vkonly)kvlayout, wrong forvkSo the final guards are
head_dim_k==64 or head_dim_k==128andhead_dim_v % 16 == 0 and 32 <= head_dim_v <= 256.Performance
In practice, we could still run
head_dim_k=64before by zero-padding it to 128. But since this PR adds support for 64, below is a comparison between the two. It was measured on B200/SM100,H=12,head_dim_v=128, causal, viatilelang.profiler.do_bench(cudagraphbackend):Benchmark script
Correctness
I added forward parity tests against the fp64 reference (
RTOL=0.02). For the dims we reject, the guard fires before the kernel runs. And for the dims we accept, they match the reference:head_dim_kin{64, 128}across the wholeCORE_CONFIGSmatrix (GQA, varlen,h0, bothkv/vklayouts) intest_fwd_head_dim_k,head_dim_vat multiples of 16 spanning[32, 256]intest_fwd_head_dim_v, and thehead_dim_k=128xhead_dim_v!=128corner that neither single-axis sweep hits intest_fwd_head_dim_kv_cross.Scope / limitations
This PR only widens the forward pass on Blackwell (SM100). So there's still the need for a follow-up on the backward pass, and running the sweep on Hopper (SM90), and on SM120.