Skip to content

CUDA: gated_delta_net - share per-token k/q via shared memory for long prefills - #54

Draft
roberteg16 wants to merge 1 commit into
gfx11from
rogarcia.gdn-shared-kq
Draft

CUDA: gated_delta_net - share per-token k/q via shared memory for long prefills#54
roberteg16 wants to merge 1 commit into
gfx11from
rogarcia.gdn-shared-kq

Conversation

@roberteg16

@roberteg16 roberteg16 commented Jul 16, 2026

Copy link
Copy Markdown

What

GATED_DELTA_NET is 18.3% of prefill GPU time on Qwen3.5-35B-A3B / gfx1151. The existing kernel is
a token-by-token scan with one warp per state column, which is latency bound: k_t/q_t are
re-fetched by all 128 warps of a head on every token (256x read amplification, MemUnitBusy 94.5%
while moving only 17.9 GB/s).

This replaces it, for the scalar-gate case, with a chunked delta rule (WY representation / UT
transform) as a single fused kernel: one block per (head, seq), the 128x128 state resident in
registers, no scratch buffer. The chunk's intermediates never leave LDS.

Keeping a whole chunk in 64 KB of LDS needs two things:

  1. CS = 32, not 64. At CS = 64 the chunk's U + W + kq alone are 80 KB.
  2. Never materialising U and W. Instead of v_new = U - W S with U = Tinv(V beta) and
    W = Tinv(K beta exp(g_cs)), use the identical
    v_new = Tinv (V beta - (K beta exp(g_cs)) S), so one [CS][S] buffer holds V*beta, becomes
    Y, then becomes v_new in place.

Result: 64 KB of global traffic per (chunk, head) instead of 360 KB, and MemUnitBusy drops from
94.5% to 38.6%.

Single commit, 439 lines, 4 files, additions only. The existing kernel is untouched apart from a
26-line dispatch branch.

Results

GDN op at pp2048: 220.9 -> 116.7 ms (1.89x).

End to end, Qwen3.5-35B-A3B Q4_K_M on gfx1151, interleaved A/B against 0f0db6292:

baseline this PR
pp1024 (-ub 4096) 1839.2 1944.5 +5.7%
pp2048 (-ub 4096) 1649.8 1823.3 +10.5%
pp4096 (-ub 4096) 1520.3 1824.9 +20.0%
pp2048 (default -ub 512) 1807.7 1865.9 +3.2%
pp4096 (default -ub 512) 1655.6 1696.2 +2.5%
tg128 @ d128 57.61 57.48 -0.2% (noise)

Decode is structurally untouched: the op's n_tokens is tokens per sequence, so it is 1 during
generation and the >= 128 gate can never be reached.

test-backend-ops -o GATED_DELTA_NET: 42/42 in all modes. Five cases added, because no existing GDN
case combined head_size=128 with more than 64 tokens, and none covered GQA (v_repeat > 1) at all
despite the model running H_k=16, H_v=32.

Caveat: this is gated by -ub, not by prompt length

The op only ever sees n_ubatch tokens. A 4096-token prompt at the default ubatch is eight separate
512-token GDN calls, so the gain is capped at the pp512 level no matter how long the prompt is.

That is why the two -ub rows above differ so much. Read it as a column, at pp4096:

ubatch baseline this PR
512 (default) 1655.6 1696.2
4096 1520.3 1824.9

With the sequential kernel, raising the ubatch is a pessimisation (1520 vs 1656) because the
scan's serial length grows with n_tokens. With this kernel it becomes the best configuration.

So best-config to best-config the gain is 1655.6 -> 1824.9, +10.2%, and it is only reachable if
the caller raises -ub, which costs larger activation buffers. At the default ubatch the PR is
worth ~+2.5-3%.

Scope

Taken only when !kda && K == 1 && S_v == 128 && n_tokens >= 128. KDA, K > 1 snapshots, other head
sizes and single-chunk shapes fall back to the existing kernel.

Off by default. GGML_CUDA_GDN_CHUNKED=1 enables it from 128 tokens, =2 forces it for any
multi-chunk shape so the tests can reach it.

@roberteg16
roberteg16 force-pushed the rogarcia.gdn-shared-kq branch from e332179 to 5bb1972 Compare July 16, 2026 13:51
@roberteg16

This comment was marked as outdated.

@roberteg16
roberteg16 force-pushed the rogarcia.gdn-shared-kq branch from 5bb1972 to 2619b60 Compare July 28, 2026 17:29
GATED_DELTA_NET is 18% of prefill GPU time on Qwen3.5-35B-A3B / gfx1151. The
existing kernel is a token-by-token scan with one warp per state column, which
leaves the machine latency bound: k/q are re-fetched by all 128 warps of a head
on every token, a 256x read amplification that puts MemUnitBusy at 94.5% while
moving only 17.9 GB/s.

Add a chunked delta rule (WY representation / UT transform) as a single fused
kernel, one block per (head, seq), state resident in registers. Keeping a whole
chunk in 64 KB of LDS needs two things:

- CS = 32 rather than 64. At CS = 64 the chunk's U + W + kq alone are 80 KB.
- Never materialising U and W. Instead of v_new = U - W S with U = Tinv(V beta)
  and W = Tinv(K beta exp(g_cs)), use the identical
  v_new = Tinv (V beta - (K beta exp(g_cs)) S), so one [CS][S] buffer holds
  V*beta, becomes Y, then becomes v_new in place. The in-place triangular
  product needs no second buffer because rows CS/2..CS-1 are accumulated into
  registers before any row is overwritten.

The two triangular products (Gram and kq) are packed across all 1024 threads
with a triangle-to-rectangle fold: 496 + 496 + 32 is exactly the block size.
The natural (tid/CS, tid%CS) mapping makes a wave issue a full 128-MAC dot with
most of its lanes masked off.

Other things that mattered: __launch_bounds__(1024, 16) caps VGPRs at 96, which
is what lets two 32-wave workgroups share a WGP; folding the lane-uniform
exp(g_cs[i]) into the value before the butterfly halves the number of values to
reduce; two state columns per thread so each staged K/Q read feeds two FMAs.

That occupancy target is only reachable on wave32, where 1024 threads are 32
waves and two workgroups per WGP give 16 waves per SIMD. On wave64 the same
workgroup is 16 waves and tops out at 4, which -Wpass-failed turns into a build
error, so the request is conditioned on the physical warp size. The dispatch is
also gated on wave32: the lane mapping, the 16-lane butterflies and the LDS
padding are all tuned for RDNA3.5. Verified to build clean with -Werror
-Wpass-failed for gfx908/90a/942/950, gfx1030, gfx1100-1103, gfx1150-1153 and
gfx1200/1201.

GDN op at pp2048: 220.9 -> 116.7 ms. End to end against 0f0db62, interleaved:
+5.7% at pp1024, +10.5% at pp2048, +20.0% at pp4096 with -ub 4096.

Note the op only ever sees n_ubatch tokens, so at the default -ub 512 the gain
is ~+2.5%. The larger figures need a larger ubatch - which is itself the point:
with the sequential kernel -ub 4096 is a pessimisation at pp4096 (1520 vs 1656
t/s at -ub 512) because the scan's serial length grows with n_tokens, while with
this kernel it becomes the best configuration (1825 t/s).

Scalar gate and K == 1 only, S_v == 128, n_tokens >= 128; everything else falls
back to the existing kernel, which is untouched. Off by default;
GGML_CUDA_GDN_CHUNKED=1 enables it, =2 forces it for any multi-chunk shape so
the tests can reach it.

Also add five test-backend-ops cases: no existing GDN case combined
head_size=128 with more than 64 tokens, and none covered GQA (v_repeat > 1),
which this model actually uses (H_k=16, H_v=32).

Assisted-by: Claude Opus 4.7
@roberteg16
roberteg16 force-pushed the rogarcia.gdn-shared-kq branch from 2619b60 to fa2898d Compare July 28, 2026 18:07
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