Skip to content

CUDA: Support of GDN chunked kernel for prefill - #26001

Open
BLSharda wants to merge 7 commits into
ggml-org:masterfrom
BLSharda:bhaviksharda/chunked_gdn
Open

CUDA: Support of GDN chunked kernel for prefill#26001
BLSharda wants to merge 7 commits into
ggml-org:masterfrom
BLSharda:bhaviksharda/chunked_gdn

Conversation

@BLSharda

Copy link
Copy Markdown
Contributor

Overview

This PR adds a chunked mode to the Gated Delta Net (GDN) CUDA operator, significantly speeding up prefill for >=128 tokens to the previous recurrent (token-by-token) kernel. All changes are implemented within the CUDA backend as a single GDN operator update.

The new chunked execution path utilizes a three-stage pipeline, inspired by the (vLLM) Triton/FLA GDN kernel, and leverages a mix of BF16, FP16, and FP32 to achieve throughput improvements via 16-bit tensor cores. This mode is enabled on supported hardware (NVIDIA Ampere+ with BF16 tensor core), otherwise original recurrent kernel is used as a fallback. The chunked kernel is GQA-aware and reads the fused QK and strided V tensor, similarly to the recurrent kernel.

Implementation in chunk_gated_delta_net.cu

  1. FP32 forward substitution (WY inverse)
    Computes intra-chunk corrections: V_corr, K_cumdecay, and g_cum

  2. BF16 WMMA masked attention
    Performs Q @ K^T with causal masking and cumulative decay

  3. WMMA state update + fused output

    • BF16 H-state GEMMs (FP32-range exponent prevents saturation)
    • FP16 GEMMs for k^T · v and qk · v_new
    • FP32 accumulation

Accuracy

  • Perplexity and KL divergence are effectively lossless compared to the FP32 recurrent baseline on the qwen_3_6_35b_a3b_q4_k_m model:
    • Perplexity: chunked 5.5201 vs recurrent 5.5052 (+0.27%, within error)
    • KL divergence: mean 0.0083, median 0.0019, top-1 96.8% — near-lossless
  • To preserve accuracy, several safeguards are in place: the kernel handles g as a log decay by computing FP32 prefix sums and evaluating exp(g_cum[t] - g_cum[s]), which avoids problematic divisions by small exponentials. The recurrent H-state is maintained in FP32 across chunks, with BF16 only used when staging H-related WMMA operands. Additionally, WMMA operations use FP32 accumulators, while FP16 is utilized for K^T V_new and intra-chunk output multiplications.
  • Additional safeguards: FP16 conversions are clamped to [-65504, 65504] and the output exponent for safety it capped at 88.72, and non-finite intermediate/state values are reset to zero.

Addtional Memory

The buffers are pool-allocated and freed per op, so they're reused across all GDN layers and stays independent of model quant (always FP32).

Model 128 256 512 1024 2048 4096 8192 16384
qwen_3_6_35b_a3b_q4_k_m (H=32) 4.3 MiB 8.5 MiB 17.1 MiB 34.1 MiB 68.3 MiB 136.5 MiB 273.0 MiB 546.0 MiB
qwen_3_6_27b_q4_k_m (H=48) 6.4 MiB 12.8 MiB 25.6 MiB 51.2 MiB 102.4 MiB 204.8 MiB 409.5 MiB 819.0 MiB

Performance

Below are the measured performance improvements for Qwen 3.6 35B MoE and 27B models; performance gains apply broadly to all supported NVIDIA Ampere+ GPUs

Hardware & Model ISL=1000 ISL=2000 ISL=4000 ISL=8000 ISL=16000 Max Δ%
ad102 qwen_3_6_27b_q4_k_m +5.5% +9.9% +10.1% +10.2% +10.3% +10.3%
ga102 qwen_3_6_27b_q4_k_m +4.8% +9.8% +9.7% +9.7% +9.2% +9.8%
GB202 qwen_3_6_27b_q4_k_m +8.8% +18.7% +15.2% +16.3% +16.0% +18.7%
GB202 qwen_3_6_35b_a3b_nvfp4 +3.9% +7.3% +6.2% +5.9% +5.8% +7.3%
GB202 qwen_3_6_35b_a3b_q4_k_m +3.0% +5.6% +5.1% +5.5% +6.4% +6.4%

-ub perf-sweep

pp=16384 with qwen_3_6_35b_a3b_q4_k_m

RTX 6000 Pro MaxQ

ub Chunked Recurrent 35B Gain
128 3030 2851 +6.3%
256 4706 4378 +7.5%
512 6775 6204 +9.2%
1024 8834 8023 +10.1%
2048 10382 9131 +13.7%
4096 10767 9610 +12.0%
8192 10435 9431 +10.6%

DGX Spark

ub Chunked Recurrent Gain
128 1222.6 1184.9 +3.2%
256 1729.5 1672.4 +3.4%
512 2285.1 2137.2 +6.9%
1024 2664.9 2494.8 +6.8%
2048 2825.2 2638.0 +7.1%
4096 2886.0 2645.4 +9.1%

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES, I have used an AI assistant to help draft and review portions of this change, and I manually checked and revised all code prior to submission. I acknowledge that I bear full responsibility for all changes included here.

Known limitations / future work

  • Supports NVIDIA Ampere+ GPUs as BF16 tensor cores are required.
  • Unit test CPU comparison threshold slightly relaxed to match chunked GDN BF16 precision

@github-actions github-actions Bot added testing Everything test related ggml changes relating to the ggml tensor library for machine learning CUDA Related to the CUDA backend labels Jul 22, 2026
@ggml-gh-bot

ggml-gh-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

Hi @BLSharda, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • Multiple open PRs from a new contributor: We limit new contributors (those without a previously merged PR) to 1 open PR at a time. You currently have 2 open PRs.

  • AI-generated content: This project does not accept PRs, descriptions or commit messages that are fully or predominantly AI-generated. If you have used AI to assist you in writing code, please make sure to disclose that explicitly.


Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@pwilkin
pwilkin requested a review from am17an July 22, 2026 15:15

@gaugarg-nv gaugarg-nv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A 3.2% top-1 change looks pretty high to me. Some of the changes related to isfinite check and the FP16 clamp look suspicious to me from an accuracy POV.

Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated

@ORippler ORippler left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Does this PR offer significantly higher perf than #24561 to justify a restriction to cuda-only by means of wmma over ggml_cuda_mma?
  2. How do we resolve MTP support in this PR?

@BLSharda

Copy link
Copy Markdown
Contributor Author
  1. Does this PR offer significantly higher perf than CUDA/HIP: chunked MFMA prefill kernel for GATED_DELTA_NET (CDNA) #24561 to justify a restriction to cuda-only by means of wmma over ggml_cuda_mma?

On RTX 6000 Pro MaxQ, PR 24561 needed MIN_BLOCKS_PER_SM=1 to get the new kernel to launch. And with larger ubatch sizes (≥512) PR 24561 gives 1.9–3.9% prefill performance, against this current PR delivers about a ~10% speedup for ubatch ≥512.

  1. How do we resolve MTP support in this PR?

Chunked GDN requires at least 128 tokens, so MTP steps 1/2/3 are very short and stay on the recurrent path. Also, speculative decoding fails the K==1 check and therefore falls back to the recurrent kernel.

@ORippler

Copy link
Copy Markdown
Collaborator

On RTX 6000 Pro MaxQ, PR 24561 needed MIN_BLOCKS_PER_SM=1 to get the new kernel to launch. And with larger ubatch sizes (≥512) PR 24561 gives 1.9–3.9% prefill performance, against this current PR delivers about a ~10% speedup for ubatch ≥512.

How much effort would it be to make this PR use ggml_cuda_mma while maintaining perf on NVGPUs and not locking AMD out? Or alternatively make #24561 achieve same perf on NVGPUs as this PR?

Chunked GDN requires at least 128 tokens, so MTP steps 1/2/3 are very short and stay on the recurrent path. Also, speculative decoding fails the K==1 check and therefore falls back to the recurrent kernel.

How much effort would it be to get the same support for speculative decoding than we have for MTP? Seems like we would need to fail only based on the tokens, not the K==1 check

@BLSharda

Copy link
Copy Markdown
Contributor Author

On RTX 6000 Pro MaxQ, PR 24561 needed MIN_BLOCKS_PER_SM=1 to get the new kernel to launch. And with larger ubatch sizes (≥512) PR 24561 gives 1.9–3.9% prefill performance, against this current PR delivers about a ~10% speedup for ubatch ≥512.

How much effort would it be to make this PR use ggml_cuda_mma while maintaining perf on NVGPUs and not locking AMD out? Or alternatively make #24561 achieve same perf on NVGPUs as this PR?

Sure, I can give it a try to switch to using ggml_cuda_mma.

Chunked GDN requires at least 128 tokens, so MTP steps 1/2/3 are very short and stay on the recurrent path. Also, speculative decoding fails the K==1 check and therefore falls back to the recurrent kernel.

How much effort would it be to get the same support for speculative decoding than we have for MTP? Seems like we would need to fail only based on the tokens, not the K==1 check

This kernel is specifically designed for use in the prefill phase only and does not need to support MTP. Also, supporting K>1 is substantial work.

@Green-Sky

Copy link
Copy Markdown
Collaborator

I found this "deep dive" post from moonshot talking about flash kda https://github.com/MoonshotAI/FlashKDA/blob/master/docs/20260420-flashkda-v1-deep-dive.md

They seem to be using 16 token chunks.

Also they seem to link to a proof that f16 does not need to be clamped, but not sure how exact this applies here.

@BLSharda
BLSharda marked this pull request as ready for review August 4, 2026 15:52
@BLSharda
BLSharda requested review from a team and ggerganov as code owners August 4, 2026 15:52
@BLSharda

BLSharda commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

How much effort would it be to make this PR use ggml_cuda_mma while maintaining perf on NVGPUs and not locking AMD out?

I’ve now added ggml_cuda_mma support in this kernel, but it regresses performance by about 1–2%, so I’ve retained WMMA for NVIDIA GPUs and using ggml_cuda_mma for others (compile time checks). Also, I have not verified this kernel on AMD so kept it disabled behind runtime checks for now.

@IMbackK

IMbackK commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

hip has rocwmma with the equivalent interface to cuda wmma, but we should avoid using it if possible and not have two implementations. I can check the kernel on some hip devices.

@IMbackK IMbackK self-assigned this Aug 4, 2026
@BLSharda

BLSharda commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

hip has rocwmma with the equivalent interface to cuda wmma, but we should avoid using it if possible and not have two implementations. I can check the kernel on some hip devices.

Yeah, if you can also try the ggml_cuda_mma path with HIP/MUSA that would be really helpful. Right now I’ve explicitly disabled AMD via runtime checks in ggml_cuda_gdn_op_is_chunked.

@BLSharda

BLSharda commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

A 3.2% top-1 change looks pretty high to me.

I see similar KLD score when comparing CUDA recurrent path against Vulkan backend running same model checkpoint, so it seems this is expected due to very small difference in the values.

@am17an am17an left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some more correctness tests apart from wikitext. Perhaps take a look at llama-eval and see if running a benchmark there matches what master produces. Also my guess is that this is in-fact not ready for review? Since @gaugarg-nv @ORippler have not approved it, so maybe you can move it back to draft

Comment thread ggml/src/ggml-cuda/gated_delta_net.cu Outdated
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment thread tests/test-backend-ops.cpp Outdated
Comment thread ggml/src/ggml-cuda/gated_delta_net.cu Outdated
Comment on lines +259 to +269
return is_nvidia
&& cc_dev >= GGML_CUDA_CC_AMPERE
&& !chunk_disabled
&& !kda && K == 1
&& neq0 == 128 && S_v == 128 && nev1 % neq1 == 0
&& src_k->ne[1] == neq1
&& n_tokens >= 128
&& ggml_is_contiguous(src_q) && ggml_is_contiguous(src_k) && ggml_is_contiguous(src_g)
&& src_v->nb[0] == ggml_type_size(src_v->type) && src_v->nb[1] == (size_t)S_v * ggml_type_size(src_v->type)
&& src_v->nb[3] == (size_t) n_tokens * src_v->nb[2]
&& ggml_is_contiguous(src_beta) && ggml_is_contiguous(src_state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ggml-org/amd are you guys willing to test this on some hardware and post results? You will need modify this condition and we enable it for more hardware

Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu Outdated
Comment on lines +535 to +536
const int v_loc = idx % BV;
oi_regs[j] = s_result[t_idx * BV + v_loc] * __expf(fminf(s_gcum[t_idx], 88.72f));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's 88.72f?

@davetha

davetha commented Aug 10, 2026

Copy link
Copy Markdown

Validated the ggml_cuda_mma path on AMD CDNA, as requested. It needed four fixes, and with them it works and is worth +12% prefill on gfx90a.

Hardware: 2× MI210 (gfx90a, wave64), ROCm 7.14, Ubuntu 24.04. Branch at 1e1885f3d, built with GGML_HIP=ON -DGPU_TARGETS=gfx90a -DGGML_HIP_MMQ_MFMA=ON.

Starting point

Widening ggml_cuda_gdn_op_is_chunked to admit CDNA and changing nothing else:

chunked enabled : 36/51 GATED_DELTA_NET tests pass
chunked disabled: 51/51   (GGML_CUDA_DISABLE_GDN_CHUNK=1, same binary)

Every failure matched the chunked predicate exactly (head_size=128, n_seq_tokens>=128, K=1, kda=0); every pass was a case falling back to the recurrent kernel. So the gate is doing what it should — the kernel itself was wrong on CDNA.

The four defects

1. cgdr_precompute_qk_wmma_kernel is launched with a 32-thread block. AMD MFMA tiles are distributed across a full 64-lane wavefront (mma.cuh: ne = I * J / 64), so half the 16×16 accumulator's lanes simply don't exist and half of s_acc is never written. Also affects the load loop (i += 32) and the writeback (EPT = CS*CS/32, flat = tid + e*32), both of which assume 32 lanes own the whole tile.

2. __launch_bounds__(32, 8) on that kernel is a hard ceiling, so simply launching it with 64 threads fails outright with unspecified launch failure. Changed to __launch_bounds__(ggml_cuda_get_physical_warp_size(), 8), matching the existing pattern in mmid.cu:27. NVIDIA is unaffected — warp_size is 32 there.

3. Plain load_ldmatrix has no AMD_MFMA_AVAILABLE branch. In mma.cuh the tile<16, 8, T, dl> overload has TURING_MMA_AVAILABLE and AMD_WMMA_AVAILABLE branches and then falls through to NO_DEVICE_CODE, which traps on CDNA. Routed the three plain call sites through ggml_cuda_mma::load_generic, which is defined in terms of the same get_i/get_j and is therefore correct for MFMA fragment layouts. Note load_ldmatrix_trans does have an MFMA branch, which is why only some call sites break — worth knowing if you'd rather add the missing specialisation to mma.cuh instead.

4. The accumulator's data layout is wrong on CDNA — this was the real one. All three GEMM helpers declare ggml_cuda_mma::tile<16, 16, float> acc;, i.e. the default DATA_LAYOUT_I_MAJOR. On AMD MFMA (and RDNA4 WMMA) the 16×16 f32 fragment is DATA_LAYOUT_J_MAJOR, and mma.cuh's J_MAJOR tile is a wrapper that swaps get_i/get_j:

static __device__ __forceinline__ int get_i(const int l) {
    return tile<I_, J_, T, DATA_LAYOUT_I_MAJOR>::get_j(l);
}

So every lane reports transposed coordinates when writing its accumulator elements back to shared memory, and the entire tile is scattered. Symptom is ERR = 1.868 against a 2e-7 threshold — output uncorrelated with the reference rather than merely imprecise. Fixed with the same arch switch that #24561 used (that PR's unified ggml_cuda_mma version is, as far as I can tell, the only chunked GDN kernel previously validated on gfx90a):

#if defined(AMD_MFMA_AVAILABLE) || (defined(AMD_WMMA_AVAILABLE) && defined(RDNA4))
#  define CGDR_C_DL ggml_cuda_mma::DATA_LAYOUT_J_MAJOR
#else
#  define CGDR_C_DL ggml_cuda_mma::DATA_LAYOUT_I_MAJOR
#endif

For what it's worth I also checked and ruled out a suspicion of mine: mma(tile<16,16,float>, tile<16,8,half2>, tile<16,8,half2>) does have an AMD_MFMA_AVAILABLE branch (__builtin_amdgcn_mfma_f32_16x16x16f16), so the matrix op itself was never at fault.

Results after the fixes

Correctnesstest-backend-ops -o GATED_DELTA_NET, run separately on each MI210:

50/51 tests passed   (both devices)

Performancellama-bench, Qwen3.6-40B-A3B-class hybrid (qwen35, 97 layers, 73 of them GDN), IQ4_NL, 2× MI210 -sm layer, -fa 1 -ctk q8_0 -ctv q8_0 -b 4096 -ub 2048. Same binary, toggled only by GGML_CUDA_DISABLE_GDN_CHUNK:

test chunked off chunked on gain
pp2048 606.48 ± 8.08 677.99 ± 0.16 +11.8%
pp8192 910.75 ± 1.69 1021.18 ± 0.97 +12.1%

Token-levelllama-server, temperature 0, 376-token prompt (above the 128-token threshold, so the chunked path is live), 90 tokens generated: byte-identical output chunked vs recurrent.

For context on the ceiling: I measured the recurrent GDN op standalone at ~1.31 TFLOPS on this hardware while the same box does 75–80 TFLOPS on this model's own GEMMs, and attributed the op at ~19% of prefill wall clock. That caps any chunked implementation at ~1.24× end-to-end here, so +12% is roughly half the theoretical headroom.

One honest caveat

The single remaining failure is the longest shape:

GATED_DELTA_NET(type=f32,head_count=32,head_size=128,n_seq_tokens=2048,n_seqs=1,v_repeat=1,permuted=0,kda=0,K=1,strided_v=0)
ERR = 0.000000297 (device 0) / 0.000000370 (device 1)  >  0.000000200

That is a precision margin, not a correctness failure — NMSE ~3e-7 is ~5.5e-4 RMS relative error, right at fp16 epsilon, and 2048 tokens is 128 chunks at CS=16, the most accumulation in the suite. Every other case, including the chunk-boundary and tail shapes, passes. I can't tell you whether NVIDIA passes this same case on the ggml_cuda_mma path since I have no NVIDIA hardware here — if it does, the difference is presumably MFMA vs WMMA accumulation order, and it may warrant a slightly looser bound at the longest shape. I'd rather flag it than quietly relax the threshold.

Not covered

  • KDA path (this model uses the scalar-gate GDA path).
  • Speculative decoding / MTP — falls back to recurrent via the K == 1 gate, as you noted.
  • RDNA. The dispatch change deliberately admits CDNA only; RDNA's WMMA tile shapes are unvalidated here.

Patch

53 insertions / 18 deletions across chunk_gated_delta_net.cu and gated_delta_net.cu, NVIDIA paths unchanged throughout. Happy to open it as a PR against your branch, or you're welcome to take the changes directly — whichever is less friction for you.

@davetha

davetha commented Aug 10, 2026

Copy link
Copy Markdown
The patch (53 insertions / 18 deletions, NVIDIA paths unchanged)
diff --git a/ggml/src/ggml-cuda/chunk_gated_delta_net.cu b/ggml/src/ggml-cuda/chunk_gated_delta_net.cu
index 0dd6f5947..23e7ec5fe 100644
--- a/ggml/src/ggml-cuda/chunk_gated_delta_net.cu
+++ b/ggml/src/ggml-cuda/chunk_gated_delta_net.cu
@@ -15,6 +15,27 @@
 #define GDN_TC_MMA 0
 #endif
 
+// GDN_ACC_DL: the 16x16 f32 accumulator fragment layout is J_MAJOR on CDNA/RDNA4 and
+// I_MAJOR on NVIDIA. get_i/get_j report where each lane's accumulator element lands, so
+// declaring the wrong layout transposes the mapping and scatters the whole tile to the
+// wrong shared-memory slots. Matches PR #24561, validated 48/48 on MI250X/gfx90a.
+#if defined(AMD_MFMA_AVAILABLE) || (defined(AMD_WMMA_AVAILABLE) && defined(RDNA4))
+#  define CGDR_C_DL ggml_cuda_mma::DATA_LAYOUT_J_MAJOR
+#else
+#  define CGDR_C_DL ggml_cuda_mma::DATA_LAYOUT_I_MAJOR
+#endif
+
+// GDN_MFMA_LOAD: plain load_ldmatrix in mma.cuh has TURING and AMD_WMMA branches but
+// NO AMD_MFMA branch -- on CDNA it falls through to NO_DEVICE_CODE and traps at runtime
+// ("unspecified launch failure"). load_generic is the portable element-wise loader and is
+// defined in terms of the same get_i/get_j, so it is correct for MFMA tile layouts.
+// load_ldmatrix_trans is left alone: it does have an AMD_MFMA branch.
+#if defined(AMD_MFMA_AVAILABLE)
+#  define CGDR_LOAD ggml_cuda_mma::load_generic
+#else
+#  define CGDR_LOAD ggml_cuda_mma::load_ldmatrix
+#endif
+
 // Check if tensor-core kernels are supported on this architecture; otherwise, fallback or no-op.
 #if GDN_TC_MMA
 #  if defined(TURING_MMA_AVAILABLE) || defined(AMD_MFMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE)
@@ -40,12 +61,12 @@ template <int BK>
 __device__ __forceinline__ void cgdr_gemm_ABt_16(const __half * s_a, const __half * s_b, float * s_c, int ldc, int c_col)
 {
 #if GDN_TC_MMA
-    ggml_cuda_mma::tile<16, 16, float> acc;
+    ggml_cuda_mma::tile<16, 16, float, CGDR_C_DL> acc;
     #pragma unroll
     for (int kt = 0; kt < BK / 16; kt++) {
         ggml_cuda_mma::tile<16, 8, half2> ta, tb;
-        ggml_cuda_mma::load_ldmatrix(ta, (const half2 *) s_a + kt * 8, BK / 2);
-        ggml_cuda_mma::load_ldmatrix(tb, (const half2 *) s_b + kt * 8, BK / 2);
+        CGDR_LOAD(ta, (const half2 *) s_a + kt * 8, BK / 2);
+        CGDR_LOAD(tb, (const half2 *) s_b + kt * 8, BK / 2);
         ggml_cuda_mma::mma(acc, ta, tb);
     }
     #pragma unroll
@@ -79,7 +100,7 @@ __device__ __forceinline__ void cgdr_gemm_ktv(const __half * s_vnew, const __hal
     for (int nk = 0; nk < BK; nk += 16) {
         ggml_cuda_mma::tile<16, 8, half2> y_kch;
         ggml_cuda_mma::load_ldmatrix_trans(y_kch, (const half2 *) (s_kch + nk), BK / 2);
-        ggml_cuda_mma::tile<16, 16, float> acc;
+        ggml_cuda_mma::tile<16, 16, float, CGDR_C_DL> acc;
         ggml_cuda_mma::mma(acc, x_vnew, y_kch);
         #pragma unroll
         for (int l = 0; l < acc.ne; l++) {
@@ -107,9 +128,9 @@ __device__ __forceinline__ void cgdr_gemm_qkv(const __half * s_qk, const __half
 {
 #if GDN_TC_MMA
     ggml_cuda_mma::tile<16, 8, half2> x_qk, y_vnew;
-    ggml_cuda_mma::load_ldmatrix(x_qk, (const half2 *) s_qk, 16 / 2);
+    CGDR_LOAD(x_qk, (const half2 *) s_qk, 16 / 2);
     ggml_cuda_mma::load_ldmatrix_trans(y_vnew, (const half2 *) s_vnew + n_off / 2, BV / 2);
-    ggml_cuda_mma::tile<16, 16, float> acc;
+    ggml_cuda_mma::tile<16, 16, float, CGDR_C_DL> acc;
     ggml_cuda_mma::mma(acc, x_qk, y_vnew);
     #pragma unroll
     for (int l = 0; l < acc.ne; l++) {
@@ -283,9 +304,13 @@ __launch_bounds__(128, 4) __global__ void cgdr_fwdsub_intra_kernel(
 
 // Masked Q@K^T on tensor cores (fp16 WMMA, one warp per block):
 //   qk_buf[i,j] = (Q_ch . K_ch[j]) * exp(g_cum[i] - g_cum[j])   for j <= i, else 0.
-// Grid (B*H, num_chunks); 32 threads. Requires CS==16, BK%16==0.
+// Grid (B*H, num_chunks); one wavefront per block (32 on NVIDIA, 64 on CDNA).
+// Requires CS==16, BK%16==0.
 template <int CS, int BK>
-__launch_bounds__(32, 8) __global__ void cgdr_precompute_qk_wmma_kernel(const float * __restrict__ Q_raw,
+// GDN_WAVE64: launch bounds must match the launch, which is one full wavefront
+// (64 on CDNA). Declaring 32 here made the 64-thread launch fail outright.
+// Same pattern as mmid.cu.
+__launch_bounds__(ggml_cuda_get_physical_warp_size(), 8) __global__ void cgdr_precompute_qk_wmma_kernel(const float * __restrict__ Q_raw,
                                                                         const float * __restrict__ K_raw,
                                                                         const float * __restrict__ g_cum,
                                                                         float * __restrict__ qk_buf,
@@ -307,7 +332,7 @@ __launch_bounds__(32, 8) __global__ void cgdr_precompute_qk_wmma_kernel(const fl
 
     const int bh  = blockIdx.x;
     const int c   = blockIdx.y;
-    const int tid = threadIdx.x;  // 0..31
+    const int tid = threadIdx.x;  // 0..warp_size-1 (GDN_WAVE64)
 
     const int       b_idx = bh / H;
     const int       h_idx = bh % H;                  // v-head
@@ -326,7 +351,7 @@ __launch_bounds__(32, 8) __global__ void cgdr_precompute_qk_wmma_kernel(const fl
     // seq_len is not a multiple of CS -- zero-fill rows past valid_cs to avoid out-of-bounds reads.
     // Q*scale and K are small (unit-length vectors), so fp16 is safe.
     const int valid_cs = min(CS, seq_len - t_off);
-    for (int i = tid; i < CS * BK; i += 32) {
+    for (int i = tid; i < CS * BK; i += blockDim.x) {  // GDN_WAVE64: was hardcoded 32
         const int   row = i / BK, col = i % BK;
         const float qv = (row < valid_cs) ? Q_chunk[(long long) row * HK + col] : 0.f;
         const float kv = (row < valid_cs) ? K_chunk[(long long) row * HK + col] : 0.f;
@@ -340,11 +365,11 @@ __launch_bounds__(32, 8) __global__ void cgdr_precompute_qk_wmma_kernel(const fl
     __syncthreads();
 
     // Causal mask + cumulative-decay scaling, then write qk_buf.
-    float *       O_base = qk_buf + (long long) (bh * num_chunks + c) * CS * CS;
-    constexpr int EPT    = CS * CS / 32;
-    #pragma unroll
-    for (int e = 0; e < EPT; e++) {
-        const int flat = tid + e * 32;
+    float * O_base = qk_buf + (long long) (bh * num_chunks + c) * CS * CS;
+    // GDN_WAVE64: stride by the actual block size. A 64-lane wavefront owns CS*CS
+    // in half as many steps as a 32-lane warp, so the old constexpr trip count
+    // double-wrote on AMD.
+    for (int flat = tid; flat < CS * CS; flat += blockDim.x) {
         const int row  = flat / CS;
         const int col  = flat % CS;
         O_base[flat]   = (col <= row) ? s_acc[flat] * __expf(s_gcum[row] - s_gcum[col]) : 0.f;
@@ -704,7 +729,15 @@ static void ggml_cuda_op_gated_delta_net_chunked_impl(ggml_backend_cuda_context
     {
         const size_t qk_smem = cgdr_smem_preqk_wmma(CS, K_dim);  // 9.1 KB < 48 KB -> no opt-in needed
         const dim3   qk_grid(B * H, num_chunks, 1);
-        cgdr_precompute_qk_wmma_kernel<CS, 128><<<qk_grid, 32, qk_smem, stream>>>(
+        // GDN_WAVE64: one full wavefront. ggml_cuda_mma tiles span all 64 lanes on
+        // CDNA (mma.cuh: ne = I*J/64), so a 32-thread block half-populates the
+        // accumulator. warp_size is 32 on NVIDIA, so that path is unchanged.
+#if GDN_TC_MMA
+        const int qk_block = ggml_cuda_info().devices[ggml_cuda_get_device()].warp_size;
+#else
+        const int qk_block = 32;
+#endif
+        cgdr_precompute_qk_wmma_kernel<CS, 128><<<qk_grid, qk_block, qk_smem, stream>>>(
             q_in, k_in, g_cum_buf.get(), qk_buf.get(), num_chunks, scale, H, num_k_heads, T);
     }
     CUDA_CHECK(cudaGetLastError());
diff --git a/ggml/src/ggml-cuda/gated_delta_net.cu b/ggml/src/ggml-cuda/gated_delta_net.cu
index b3977e7ff..6fe4d66b2 100644
--- a/ggml/src/ggml-cuda/gated_delta_net.cu
+++ b/ggml/src/ggml-cuda/gated_delta_net.cu
@@ -248,15 +248,18 @@ bool ggml_cuda_gdn_op_is_chunked(const ggml_tensor * dst) {
         return s && s[0] && !(s[0] == '0' && s[1] == '\0');
     }();
     const int  cc_dev    = ggml_cuda_info().devices[ggml_cuda_get_device()].cc;
-    // NVIDIA-only for now. The HIP/MUSA ggml_cuda_mma backend intentionally not dispatched until validated.
-    const bool is_nvidia = GGML_CUDA_CC_IS_NVIDIA(cc_dev);
+    // GDN_CDNA: admit CDNA to the ggml_cuda_mma path. CDNA has the fp16 matrix
+    // cores this kernel wants (v_mfma_f32_16x16x16f16). NVIDIA's condition is
+    // unchanged. Deliberately not all of AMD -- RDNA's WMMA path is unvalidated.
+    const bool arch_ok = (GGML_CUDA_CC_IS_NVIDIA(cc_dev) && cc_dev >= GGML_CUDA_CC_AMPERE)
+                      || GGML_CUDA_CC_IS_CDNA(cc_dev);
 
     // - NVIDIA Ampere+ (fp16 WMMA); not KDA; K == 1 (final state only)
     // - Q/K/G/beta/state must be contiguous
     //   (nb[0]/nb[1] packed) with arbitrary token stride (fused QKV view) 
     // - V is packed per token (nb[2]) and across sequences (nb[3] == n_tokens*nb[2]).
     // - 128-wide heads, GQA-aligned head counts, n_tokens >= 128
-    return is_nvidia
+    return arch_ok
         && cc_dev >= GGML_CUDA_CC_AMPERE
         && !chunk_disabled
         && !kda && K == 1

@BLSharda

Copy link
Copy Markdown
Contributor Author

53 insertions / 18 deletions across chunk_gated_delta_net.cu and gated_delta_net.cu, NVIDIA paths unchanged throughout. Happy to open it as a PR against your branch, or you're welcome to take the changes directly — whichever is less friction for you.

Thanks, @davetha — this is really helpful.

I’ll take the warp_size-related changes that I can test on NVIDIA GPUs for cuda_mma. Once this PR is merged, could you open a follow-up PR with the remaining changes? Since I’m not testing on AMD, I’d prefer not to enable it in this PR.

The single remaining failure is the longest shape:
NVIDIA FORCE_MMA passes T=2048 test that AMD failing at the current 2e-7 threshold. Maybe we might have to relax threshold a slightly more for AMD.

@davetha

davetha commented Aug 13, 2026

Copy link
Copy Markdown

Sure!

Comment thread ggml/src/ggml-cuda/chunk_gated_delta_net.cu
@pich

pich commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

I used commit 1e1885f from this PR in my final Qwen3.8-27B runtime.

My model is a 17.1 GB, 5.01 BPW iMatrix/NVFP4 hybrid with an embedded MTP layer. The target model, MTP, recurrent state, CUDA graphs and 256K Q4_0 KV cache run on one RTX PRO 4000 Blackwell SFF: 24,467 MiB VRAM, sm120a, 432 GB/s rated bandwidth and a 70 W board limit. An RTX 2000 Ada holds the optional F16 vision projector.

I tested with CUDA 12.9.86, driver 610.57.04, Debian 13, batch 512, ubatch 256 and four recurrent checkpoints.

I did not isolate #26001 from the other two CUDA patches in the final runtime sweep, so I cannot honestly assign it an individual percentage. The #26001 + #26048 + #26705 bundle moved deterministic MTP decode from 45.422 to 45.866 tok/s, or +0.98%.

The more relevant result for this prefill patch was the real long-context test: 261,500 input tokens were processed at 226.750 tok/s, followed by 256 generated tokens, without truncation or OOM.

Full process and exact arguments:
https://piszczek.pl/blog/qwen38-27b-256k-50-tps-24gb-gpu

Released GGUF:
https://huggingface.co/cdiamond/Qwen3.8-27B-iMatrix-NVFP4-MTP-GGUF

Aristo94 added a commit to Aristo94/EngramHalo.cpp that referenced this pull request Aug 27, 2026
The GDN prefill kernel processed tokens serially (see the TODO in
gated_delta_net.cu). Adds a chunked kernel following the scheme of the
Metal Mamba-2 chunked prefill; related CUDA/Vulkan efforts: ggml-org#26001, ggml-org#20377.
Off by default; passes test-backend-ops GATED_DELTA_NET.
Aristo94 added a commit to Aristo94/EngramHalo.cpp that referenced this pull request Aug 28, 2026
The GDN prefill kernel processed tokens serially (see the TODO in
gated_delta_net.cu). Adds a chunked kernel following the scheme of the
Metal Mamba-2 chunked prefill; related CUDA/Vulkan efforts: ggml-org#26001, ggml-org#20377.
Passes test-backend-ops GATED_DELTA_NET.

Opt-in on RDNA3/RDNA4 via GGML_HIP_GDN_CHUNK=1: arch_ok ties that variable to
the RDNA branch only, so the kernel is default-active on NVIDIA Ampere+ and
CDNA. It is not active in the published gfx1151 numbers, which were measured
without the variable set.
Aristo94 added a commit to Aristo94/EngramHalo.cpp that referenced this pull request Aug 31, 2026
The GDN prefill kernel processed tokens serially (see the TODO in
gated_delta_net.cu). Adds a chunked kernel following the scheme of the
Metal Mamba-2 chunked prefill; related CUDA/Vulkan efforts: ggml-org#26001, ggml-org#20377.
Passes test-backend-ops GATED_DELTA_NET.

Opt-in on RDNA3/RDNA4 via GGML_HIP_GDN_CHUNK=1: arch_ok ties that variable to
the RDNA branch only, so the kernel is default-active on NVIDIA Ampere+ and
CDNA. It is not active in the published gfx1151 numbers, which were measured
without the variable set.
@BLSharda
BLSharda force-pushed the bhaviksharda/chunked_gdn branch from 1e1885f to 10c1e46 Compare September 3, 2026 06:07
@am17an

am17an commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

@BLSharda have you benchmarked this kernel w.r.t to FLA? at ISL 8192 there is a huge diff between vLLM and llama.cpp and it is entirely because of this kernel. It would be good to close the gap (it is like 3x worse rather than a few percent)

@lukdmine

lukdmine commented Sep 7, 2026

Copy link
Copy Markdown

@am17an I ran that comparison. On an RTX 3090, the chunked kernel in this PR is 2.3–2.7x
slower than fla's chunk_gated_delta_rule (bf16, the path vLLM runs). I also have a
kernel that closes the gap: the whole chunk pipeline as one fused kernel. It is 2.7–3.1x faster than the chunked kernel here,
and at parity with fla or ahead of it at most shapes I measured.

To be upfront about provenance: the kernel was generated by an LLM-driven
kernel-optimization pipeline I have been building for some time. I ported it to ggml, studied it line by line, and tested and benchmarked it thoroughly. I take
responsibility for the code and can explain any part of it.

Code based on the current head of this pr:

Measurements: RTX 3090, CUDA 12.0. µs/op via test-backend-ops perf -o GATED_DELTA_NET.
fla is 0.5.1 bf16, GPU kernel time via torch.profiler.

shape (d=128) #26001 chunked fused speedup fla bf16 (vLLM)
32 heads, T=512 328 µs 117 µs 2.80x 135 µs
32 heads, T=2048 1219 µs 457 µs 2.67x 449 µs
32 heads, T=2048, B=4 4640 µs 1513 µs 3.07x 1751 µs
16->48 heads (GQA), T=512 447 µs 150 µs 2.98x 194 µs
16->48 heads (GQA), T=2048 1715 µs 590 µs 2.91x 710 µs

End to end, Qwen3.8-27B Q4_K:

test #26001 head fused delta
pp512 1495 t/s 1577 t/s +5.5%
pp2048 1492 t/s 1573 t/s +5.4%
pp8192 1425 t/s 1506 t/s +5.7%

Correctness: test-backend-ops test -o GATED_DELTA_NET passes 51/51 at the 2e-7 NMSE
bar with the fused path both on and off, the full CUDA0 suite passes 14696/14696, and an
80-token greedy continuation after a ~1k-token prompt is byte-identical between the
fused and the three-stage path.

Limitations: tuned on GA102. The grid is n_v_heads x 4 blocks per sequence, which
under-fills bigger boards, so by default it only runs where nsm ≤ 84; compile-time
-DGGML_CUDA_FORCE_GDN_FUSED / -DGGML_CUDA_NO_GDN_FUSED override the gate (same
pattern as GGML_CUDA_NO_GDN_CHUNK). If someone with a 4090/5090/H100 runs the perf
comparison with the force flag, we would know whether the gate can be raised. Head dim
128 only (the routing predicate already guarantees that).

@BLSharda happy to open this as a PR against bhaviksharda/chunked_gdn or keep it as a follow-up once this PR lands, whichever you prefer.

@BLSharda

BLSharda commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

@BLSharda have you benchmarked this kernel w.r.t to FLA? at ISL 8192 there is a huge diff between vLLM and llama.cpp and it is entirely because of this kernel. It would be good to close the gap (it is like 3x worse rather than a few percent)

I think this #26001 (comment) comment addresses your concern (increases kernel perf 2.6+ times).

@BLSharda

BLSharda commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

@am17an I ran that comparison. On an RTX 3090, the chunked kernel in this PR is 2.3–2.7x slower than fla's chunk_gated_delta_rule (bf16, the path vLLM runs). I also have a kernel that closes the gap: the whole chunk pipeline as one fused kernel. It is 2.7–3.1x faster than the chunked kernel here, and at parity with fla or ahead of it at most shapes I measured.

To be upfront about provenance: the kernel was generated by an LLM-driven kernel-optimization pipeline I have been building for some time. I ported it to ggml, studied it line by line, and tested and benchmarked it thoroughly. I take responsibility for the code and can explain any part of it.

Code based on the current head of this pr:

Measurements: RTX 3090, CUDA 12.0. µs/op via test-backend-ops perf -o GATED_DELTA_NET. fla is 0.5.1 bf16, GPU kernel time via torch.profiler.

shape (d=128) #26001 chunked fused speedup fla bf16 (vLLM)
32 heads, T=512 328 µs 117 µs 2.80x 135 µs
32 heads, T=2048 1219 µs 457 µs 2.67x 449 µs
32 heads, T=2048, B=4 4640 µs 1513 µs 3.07x 1751 µs
16->48 heads (GQA), T=512 447 µs 150 µs 2.98x 194 µs
16->48 heads (GQA), T=2048 1715 µs 590 µs 2.91x 710 µs
End to end, Qwen3.8-27B Q4_K:

test #26001 head fused delta
pp512 1495 t/s 1577 t/s +5.5%
pp2048 1492 t/s 1573 t/s +5.4%
pp8192 1425 t/s 1506 t/s +5.7%
Correctness: test-backend-ops test -o GATED_DELTA_NET passes 51/51 at the 2e-7 NMSE bar with the fused path both on and off, the full CUDA0 suite passes 14696/14696, and an 80-token greedy continuation after a ~1k-token prompt is byte-identical between the fused and the three-stage path.

Limitations: tuned on GA102. The grid is n_v_heads x 4 blocks per sequence, which under-fills bigger boards, so by default it only runs where nsm ≤ 84; compile-time -DGGML_CUDA_FORCE_GDN_FUSED / -DGGML_CUDA_NO_GDN_FUSED override the gate (same pattern as GGML_CUDA_NO_GDN_CHUNK). If someone with a 4090/5090/H100 runs the perf comparison with the force flag, we would know whether the gate can be raised. Head dim 128 only (the routing predicate already guarantees that).

@BLSharda happy to open this as a PR against bhaviksharda/chunked_gdn or keep it as a follow-up once this PR lands, whichever you prefer.

Thanks for this work, let me try to merge my change and you can raise a separate PR.

@am17an

am17an commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

I think this #26001 (comment) comment addresses your concern (increases kernel perf 2.6+ times).

That's an entirely different kernel from what I see. So should we try merging some version of that?

BLSharda and others added 2 commits September 9, 2026 19:38
Fixed review comment

Co-authored-by: Aman Gupta <amangupta052@gmail.com>
@BLSharda

BLSharda commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

We need some more correctness tests apart from wikitext. Perhaps take a look at llama-eval and see if running a benchmark there matches what master produces.

On correctness beyond wikitext, ran the full MMLU-Pro through lm-eval against llama-server, comparing the chunked path to a recurrent baseline — i.e. similar what master produces. Tested Qwen3.6-35B-A3B UD-Q4_K_M, all 12,032 questions per arm, 5-shot, greedy, n_ctx 8192.

arm MMLU-Pro exact_match
recurrent (= master behaviour) 0.4437
chunked 0.4357

@gaugarg-nv and @ORippler, the review comments are addressed, can you help review this?

minhle-120 added a commit to minhle-120/llama.cpp that referenced this pull request Sep 11, 2026
minhle-120 added a commit to minhle-120/llama.cpp that referenced this pull request Sep 11, 2026
minhle-120 added a commit to minhle-120/llama.cpp that referenced this pull request Sep 12, 2026
minhle-120 added a commit to minhle-120/llama.cpp that referenced this pull request Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CUDA Related to the CUDA backend ggml changes relating to the ggml tensor library for machine learning testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants