Skip to content

Widen forward head-dim support on Blackwell (SM100)#24

Open
Costa-SM wants to merge 7 commits into
QwenLM:mainfrom
Costa-SM:support-head-dim-64
Open

Widen forward head-dim support on Blackwell (SM100)#24
Costa-SM wants to merge 7 commits into
QwenLM:mainfrom
Costa-SM:support-head-dim-64

Conversation

@Costa-SM

@Costa-SM Costa-SM commented Jul 4, 2026

Copy link
Copy Markdown

Hi folks.

corgi-wave

I really wanted to try FlashQLA out with some different head dimension values (e.g. GDN-hybrid expand_v=2.0 layout w/ head_dim_k=64, head_dim_v=128) and I noticed that although the chunk gated_delta_rule kernels are parameterized on the head dims (DK/DV), the Python entry points assert head_dim == 128. So here's a PR to widen the accepted head_dim values during forward passes on Blackwell GPUs :D

I 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 few head_dim_k values (96 / 160 / 192) run without erroring and return incorrect results. So this PR also swaps the bare assert for a real guard that rejects these values.

Measurements

The parity sweep demonstrated that only selected values of head_dim_k work while head_dim_v basically accepts anything you throw at it. The parity checks for head_dim_k were run for values {16, 32, 48, 64, 80, 96, 128, 160, 192, 256} using head_dim_v=128. For head_dim_v, every multiple of 16 in [32, 256] was tested and is correct.

head_dim_k Result Notes
64, 128 ✅ correct matches the fp64 ref across the full config matrix
16, 48, 80, 256 🛑 raises no valid MMA / shared-memory config
96, 160, 192 silently wrong runs with no error, rel-err ~0.4
32 silently wrong (vk only) correct for the kv layout, wrong for vk

So the final guards are head_dim_k==64 or head_dim_k==128 and head_dim_v % 16 == 0 and 32 <= head_dim_v <= 256.

Performance

In practice, we could still run head_dim_k=64 before 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, via tilelang.profiler.do_bench (cudagraph backend):

batch × seqlen pad K→128 native K=64 saved
64 × 4096 1602 µs 1396 µs ~13%
32 × 8192 1596 µs 1378 µs ~14%
8 × 16384 1268 µs 1117 µs ~12%
Benchmark script
import torch, tilelang
import torch.nn.functional as F
from flash_qla import chunk_gated_delta_rule as qla
from flash_qla.utils import l2norm

H, V = 12, 128
kw = dict(warmup=10, rep=100, backend="cudagraph")

def run(q, k, v, g, beta, scale):
    o = qla(q=q, k=k, v=v, g=g, beta=beta, scale=scale, output_final_state=True)
    return o[0] if isinstance(o, tuple) else o

for b, t in [(64, 4096), (32, 8192), (8, 16384)]:
    q = l2norm(torch.randn(b, t, H, 64, device="cuda", dtype=torch.bfloat16))
    k = l2norm(torch.randn(b, t, H, 64, device="cuda", dtype=torch.bfloat16))
    v = torch.randn(b, t, H, V, device="cuda", dtype=torch.bfloat16)
    g = torch.nn.functional.logsigmoid(torch.randn(b, t, H, device="cuda", dtype=torch.float32)) / 16
    beta = torch.rand(b, t, H, device="cuda", dtype=torch.float32)
    scale = 64 ** -0.5
    qp, kp = F.pad(q, (0, 64)).contiguous(), F.pad(k, (0, 64)).contiguous()  # pad-to-128 workaround
    native = tilelang.profiler.do_bench(lambda: run(q, k, v, g, beta, scale), **kw)
    padded = tilelang.profiler.do_bench(lambda: run(qp, kp, v, g, beta, scale), **kw)
    print(f"b={b} t={t}: pad->128={padded*1e3:.0f}us native64={native*1e3:.0f}us saved={100*(padded-native)/padded:.0f}%")

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_k in {64, 128} across the whole CORE_CONFIGS matrix (GQA, varlen, h0, both kv/vk layouts) in test_fwd_head_dim_k, head_dim_v at multiples of 16 spanning [32, 256] in test_fwd_head_dim_v, and the head_dim_k=128 x head_dim_v!=128 corner that neither single-axis sweep hits in test_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.

Costa-SM and others added 7 commits July 2, 2026 16:37
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant