Skip to main content

logicaffeine_compile/codegen_cuda/
gdn.rs

1//! Gated DeltaNet lowered to CUDA — the mechanism behind 48 of Qwen3.8-27B's 64 layers.
2//!
3//! Each value head carries a fixed `D×D` associative memory `S` and rewrites it per token:
4//!
5//! ```text
6//!   u = S·k                      what the memory predicts for this key
7//!   S = α·S + β·(v − α·u)·kᵀ     decay, then correct by the prediction error
8//!   o = S·q                      read the UPDATED memory
9//!   o = rmsnorm(o ⊙ silu(z))·GN  gate FIRST, then normalize
10//! ```
11//!
12//! ## The fusion
13//!
14//! Written literally that is three passes over `S` — one for `S·k`, one for the rank-1 update, one for `S·q`.
15//! A dataflow graph of separate operators (ggml's shape) cannot do better, because each operator's output is
16//! a materialized tensor. But the update is affine in `S`, so the second matvec expands:
17//!
18//! ```text
19//!   o[a] = Σ_b S_new[a][b]·q[b]
20//!        = Σ_b (α·S_old[a][b] + coef[a]·k[b])·q[b]
21//!        = α·(S_old·q)[a]  +  coef[a]·(k·q)
22//! ```
23//!
24//! `(k·q)` is ONE SCALAR per head. So a single pass that loads `S_old[a][b]` into a register accumulates
25//! `S·k`, `S·q` and `k·q` together, and the register is still live for the write-back — **one read and one
26//! write of the memory per token**, with the post-update read costing nothing extra. That is the whole reason
27//! this op is a first-class LOGOS block rather than a composition of smaller ones.
28
29/// One block per `(value head, row)` computes that row's `u`, `w` and the shared `k·q` in a single pass over
30/// the memory, then rewrites the row from the register it already holds. `o` lands in scratch; the gated
31/// RMSNorm needs the whole head, so it is a second (tiny) kernel.
32pub const GDN_KERNELS: &str = r#"
33// Split one f32 activation into two int8 words: a ~= s*(qh + ql/127). Lives here because it is the first
34// device code emitted, and both the GDN gate norm and the projection producers need it.
35__device__ __forceinline__ void lgx_q8hl(float t, signed char* __restrict__ qh,
36                                         signed char* __restrict__ ql, long i){
37  int h = __float2int_rn(t);
38  h = h > 127 ? 127 : (h < -127 ? -127 : h);
39  int l = __float2int_rn((t - (float)h) * 127.0f);
40  qh[i] = (signed char)h;
41  ql[i] = (signed char)l;
42}
43
44__device__ __forceinline__ float gdn_warp_sum(float v){
45  for(int off=16; off>0; off>>=1) v += __shfl_down_sync(0xffffffffu, v, off);
46  return v;
47}
48// Three block-wide reductions sharing one traversal of the memory row.
49__device__ __forceinline__ void gdn_block_sum3(float a, float b, float c, float* oa, float* ob, float* oc){
50  __shared__ float sa[32], sb[32], sc[32];
51  int lane = threadIdx.x & 31, warp = threadIdx.x >> 5, nw = (blockDim.x + 31) >> 5;
52  a = gdn_warp_sum(a); b = gdn_warp_sum(b); c = gdn_warp_sum(c);
53  if(lane == 0){ sa[warp] = a; sb[warp] = b; sc[warp] = c; }
54  __syncthreads();
55  if(threadIdx.x == 0){
56    float ta = 0.f, tb = 0.f, tc = 0.f;
57    for(int i = 0; i < nw; ++i){ ta += sa[i]; tb += sb[i]; tc += sc[i]; }
58    *oa = ta; *ob = tb; *oc = tc;
59  }
60  __syncthreads();
61}
62
63// grid = (HV, D), block = D threads. Thread b owns column b of row a of head h's memory.
64// ONE WARP PER STATE ROW. The state is D x D per value head; row `a` needs S[a]·k, S[a]·q and k·q, which is
65// three reductions over D. Giving the row to a warp makes all three warp shuffles over registers the lanes
66// already hold — no __syncthreads, and the row is re-used from those same registers for the write-back, so
67// the D x D memory is read once and written once. The block-per-row form this replaces launched D blocks per
68// head (295k blocks per token across 48 layers) and paid three block-wide barriers to reduce one float each.
69// `sreg` is sized for D <= 256; the loops are lane-strided so any D, including D < 32, is handled.
70__global__ void gdn_step(const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v,
71                         const float* __restrict__ bet, const float* __restrict__ alp,
72                         float* __restrict__ S, float* __restrict__ o, int D, int NK){
73  const unsigned m = 0xffffffffu;
74  int h = blockIdx.x, lane = threadIdx.x & 31, warp = threadIdx.x >> 5;
75  int a = blockIdx.y * (blockDim.x >> 5) + warp;
76  if(a >= D) return;
77  // The key head this value head reads. llama.cpp broadcasts k/q from NK heads to NV with
78  // `ggml_repeat_4d`, and ggml_repeat TILES (index modulo) rather than repeat-interleaving — so the
79  // pairing is h % NK ([0,1,..,15,0,1,..,15,..]), NOT h / (NV/NK) ([0,0,0,1,1,1,..]).
80  int g = h % NK;
81  const float* kk = k + (long)g * D;
82  const float* qq = q + (long)g * D;
83  long base = (long)h * D * D + (long)a * D;
84
85  float sreg[8];
86  float u_ = 0.f, w_ = 0.f, kq_ = 0.f;
87  int n = 0;
88  for(int b = lane; b < D; b += 32){
89    float sv = S[base + b], kb = kk[b], qb = qq[b];
90    sreg[n++] = sv; u_ += sv * kb; w_ += sv * qb; kq_ += kb * qb;
91  }
92  for(int sh = 16; sh > 0; sh >>= 1){
93    u_ += __shfl_down_sync(m, u_, sh); w_ += __shfl_down_sync(m, w_, sh); kq_ += __shfl_down_sync(m, kq_, sh);
94  }
95  u_ = __shfl_sync(m, u_, 0); w_ = __shfl_sync(m, w_, 0); kq_ = __shfl_sync(m, kq_, 0);
96
97  float alpha = alp[h], beta = bet[h];
98  float coef  = beta * (v[(long)h * D + a] - alpha * u_);
99  // The 1/sqrt(D) output scale llama.cpp applies (`attn_col * scale`). It does NOT cancel under the
100  // following RMSNorm: the recurrence output is tiny (RMS ~1e-2), so `eps` is comparable to the scaled
101  // variance and `s*x/sqrt(s^2*m + eps) != x/sqrt(m + eps)`. Dropping it scaled the whole layer by ~1.25x.
102  if(lane == 0) o[(long)h * D + a] = (alpha * w_ + coef * kq_) * rsqrtf((float)D);
103  n = 0;
104  for(int b = lane; b < D; b += 32) S[base + b] = alpha * sreg[n++] + coef * kk[b];
105}
106
107// grid = (HV), block = D. RMSNorm the head and scale by the shared weight, THEN gate by silu(z) — the order
108// llama.cpp's `build_norm_gated` uses for this architecture. (HF's Qwen3-Next gates first; Qwen3.5 does not,
109// and the checkpoint follows Qwen3.5.)
110__global__ void gdn_gate_norm(const float* __restrict__ z, const float* __restrict__ gn,
111                              const float* __restrict__ o, float* __restrict__ out, int D, float eps){
112  int h = blockIdx.x, a = threadIdx.x;
113  long i = (long)h * D + a;
114  float zz = z[i];
115  float val = o[i];
116  __shared__ float ss[32];
117  int lane = threadIdx.x & 31, warp = threadIdx.x >> 5, nw = (blockDim.x + 31) >> 5;
118  float p = gdn_warp_sum(val * val);
119  if(lane == 0) ss[warp] = p;
120  __syncthreads();
121  __shared__ float den;
122  if(threadIdx.x == 0){ float t = 0.f; for(int j = 0; j < nw; ++j) t += ss[j]; den = sqrtf(t / (float)D + eps); }
123  __syncthreads();
124  out[i] = (gn[a] * val / den) * (zz / (1.0f + __expf(-zz)));
125}
126
127// The same epilogue with the ACTIVATION QUANTIZATION fused in. The o-projection that follows consumes int8,
128// and `blockDim == D` with `D % 32 == 0` means one warp owns exactly one 32-element quantization group — so
129// the group amax is a warp shuffle over registers the block already holds, not a second pass over global
130// memory. Saves a launch and a full round-trip of the value vector per GDN layer.
131__global__ void gdn_gate_norm_q(const float* __restrict__ z, const float* __restrict__ gn,
132                                const float* __restrict__ o, float* __restrict__ out,
133                                signed char* __restrict__ xq, float* __restrict__ xs, int D, float eps){
134  int h = blockIdx.x, a = threadIdx.x;
135  long i = (long)h * D + a;
136  float zz = z[i];
137  float val = o[i];
138  __shared__ float ss[32];
139  int lane = threadIdx.x & 31, warp = threadIdx.x >> 5, nw = (blockDim.x + 31) >> 5;
140  float p = gdn_warp_sum(val * val);
141  if(lane == 0) ss[warp] = p;
142  __syncthreads();
143  __shared__ float den;
144  if(threadIdx.x == 0){ float t = 0.f; for(int j = 0; j < nw; ++j) t += ss[j]; den = sqrtf(t / (float)D + eps); }
145  __syncthreads();
146  float y = (gn[a] * val / den) * (zz / (1.0f + __expf(-zz)));
147  out[i] = y;
148  float am = fabsf(y);
149  for(int sh = 16; sh > 0; sh >>= 1) am = fmaxf(am, __shfl_down_sync(0xffffffffu, am, sh));
150  am = __shfl_sync(0xffffffffu, am, 0);
151  float sc = am > 0.f ? am / 127.0f : 1.0f;
152  if(lane == 0) xs[i >> 5] = sc;
153  xq[i] = (signed char)__float2int_rn(y / sc);
154}
155
156// The gate norm with the two-word activation split, for the lane that keeps the activation nearly exact.
157__global__ void gdn_gate_norm_hl(const float* __restrict__ z, const float* __restrict__ gn,
158                                const float* __restrict__ o, float* __restrict__ out,
159                                signed char* __restrict__ xq, signed char* __restrict__ xl,
160                                 float* __restrict__ xs, int D, float eps){
161  int h = blockIdx.x, a = threadIdx.x;
162  long i = (long)h * D + a;
163  float zz = z[i];
164  float val = o[i];
165  __shared__ float ss[32];
166  int lane = threadIdx.x & 31, warp = threadIdx.x >> 5, nw = (blockDim.x + 31) >> 5;
167  float p = gdn_warp_sum(val * val);
168  if(lane == 0) ss[warp] = p;
169  __syncthreads();
170  __shared__ float den;
171  if(threadIdx.x == 0){ float t = 0.f; for(int j = 0; j < nw; ++j) t += ss[j]; den = sqrtf(t / (float)D + eps); }
172  __syncthreads();
173  float y = (gn[a] * val / den) * (zz / (1.0f + __expf(-zz)));
174  out[i] = y;
175  float am = fabsf(y);
176  for(int sh = 16; sh > 0; sh >>= 1) am = fmaxf(am, __shfl_down_sync(0xffffffffu, am, sh));
177  am = __shfl_sync(0xffffffffu, am, 0);
178  float sc = am > 0.f ? am / 127.0f : 1.0f;
179  if(lane == 0) xs[i >> 5] = sc;
180  lgx_q8hl(y / sc, xq, xl, i);
181}
182"#;
183
184/// The rest of the Qwen3.8-specific device code: the pieces of a GDN layer that surround the recurrence, plus
185/// the two things gated attention needs that a plain transformer does not (a partial rotary and an output
186/// gate). Each is deliberately fused past what a per-operator graph could do — the conv carries silu, the
187/// gate kernel computes both gates from one launch, and the norm+rope stay one pass over the head.
188pub const QWEN38_KERNELS: &str = r#"
189// Depthwise causal conv over C channels with a `T`-deep ring, silu FUSED into the epilogue. The ring is the
190// layer's carried state: shift up one, append this token, reduce. Depthwise means no cross-channel mixing, so
191// one thread owns a channel end to end and the whole thing is a single pass.
192__global__ void gk_conv_silu(const float* __restrict__ x, const float* __restrict__ wc,
193                             float* __restrict__ cs, float* __restrict__ y, int C, int T){
194  int c = blockIdx.x * blockDim.x + threadIdx.x; if(c >= C) return;
195  for(int t = 0; t < T - 1; ++t) cs[(long)t * C + c] = cs[(long)(t + 1) * C + c];
196  cs[(long)(T - 1) * C + c] = x[c];
197  float acc = 0.f;
198  for(int t = 0; t < T; ++t) acc += cs[(long)t * C + c] * wc[(long)t * C + c];
199  y[c] = acc / (1.0f + __expf(-acc));            // silu, fused
200}
201
202// The conv with BOTH epilogues fused: silu, then — for the leading `nl2` head-blocks, which hold q and k —
203// the per-head L2 normalize GDN applies before the delta update. Launched one block per head-sized chunk
204// (`blockDim == D`, `grid == C / D`), so the normalize is a reduction over registers the block already owns.
205// The v region (blocks >= nl2) takes the silu and stops. One launch where there were two, and q/k never
206// leave the SM between the two steps.
207__global__ void gk_conv_silu_l2(const float* __restrict__ x, const float* __restrict__ wc,
208                                float* __restrict__ cs, float* __restrict__ y, int C, int T,
209                                int nl2, float eps){
210  int c = blockIdx.x * blockDim.x + threadIdx.x; if(c >= C) return;
211  for(int t = 0; t < T - 1; ++t) cs[(long)t * C + c] = cs[(long)(t + 1) * C + c];
212  cs[(long)(T - 1) * C + c] = x[c];
213  float acc = 0.f;
214  for(int t = 0; t < T; ++t) acc += cs[(long)t * C + c] * wc[(long)t * C + c];
215  float v = acc / (1.0f + __expf(-acc));          // silu, fused
216  if(blockIdx.x >= nl2){ y[c] = v; return; }      // the v region needs no normalize
217  __shared__ float ss[32];
218  int lane = threadIdx.x & 31, warp = threadIdx.x >> 5, nw = (blockDim.x + 31) >> 5;
219  float p = gdn_warp_sum(v * v);
220  if(lane == 0) ss[warp] = p;
221  __syncthreads();
222  __shared__ float den;
223  if(threadIdx.x == 0){ float t = 0.f; for(int j = 0; j < nw; ++j) t += ss[j]; den = sqrtf(t + eps); }
224  __syncthreads();
225  y[c] = v / den;
226}
227
228// Per-head L2 normalize in place — GDN normalizes q and k before the delta update. grid = heads, block = D.
229__global__ void gk_l2norm_heads(float* __restrict__ x, int D, float eps){
230  int h = blockIdx.x, i = threadIdx.x; long o = (long)h * D + i;
231  float v = x[o];
232  __shared__ float ss[32];
233  int lane = threadIdx.x & 31, warp = threadIdx.x >> 5, nw = (blockDim.x + 31) >> 5;
234  float p = gdn_warp_sum(v * v);
235  if(lane == 0) ss[warp] = p;
236  __syncthreads();
237  __shared__ float den;
238  if(threadIdx.x == 0){ float t = 0.f; for(int j = 0; j < nw; ++j) t += ss[j]; den = sqrtf(t + eps); }
239  __syncthreads();
240  x[o] = v / den;
241}
242
243// Both GDN gates in one launch: write strength and forget rate, one value per value head.
244//   beta  = sigmoid(br)
245//   decay = (1 + e^(ar + dt))^rate
246// The paper writes decay as exp(−exp(A_log)·softplus(ar+dt)), but the GGUF conversion ALREADY stores
247// −exp(A_log) in `ssm_a` (verified: all 48 values negative, |v| < 0.08), so `rate` is used directly — 
248// exponentiating it again turns a ^(−0.04) long memory into a ^(−0.96) one that forgets almost every token.
249// softplus is ln(1+e^u), so the logarithm cancels against the outer exponential: one fewer transcendental.
250__global__ void gk_gdn_gates(const float* __restrict__ br, const float* __restrict__ ar,
251                             const float* __restrict__ dt, const float* __restrict__ alog,
252                             float* __restrict__ beta, float* __restrict__ decay, int NV){
253  int i = blockIdx.x * blockDim.x + threadIdx.x; if(i >= NV) return;
254  beta[i] = 1.0f / (1.0f + __expf(-br[i]));
255  decay[i] = powf(1.0f + __expf(ar[i] + dt[i]), alog[i]);
256}
257
258// Per-head RMSNorm + PARTIAL RoPE. Qwen3.8 rotates `rot` of each head's `hd` dims (64 of 256) and passes the
259// rest through, and the inverse frequencies are computed over `rot`, not `hd` — using `hd` there would detune
260// every rotation while still producing plausible-looking output.
261__global__ void gk_qk_norm_rope_part(float* __restrict__ base, const int* __restrict__ d_pos, int stride,
262                                     const float* __restrict__ nw, int n_heads, int hd, int rot,
263                                     float rbase, float eps){
264  int head = blockIdx.x; if(head >= n_heads) return;
265  float* h = base + (long)(*d_pos) * stride + (long)head * hd;
266  __shared__ float sh[128];
267  float s = 0.f; for(int i = threadIdx.x; i < hd; i += blockDim.x) s += h[i] * h[i];
268  sh[threadIdx.x] = s; __syncthreads();
269  for(int k = blockDim.x / 2; k > 0; k >>= 1){ if(threadIdx.x < k) sh[threadIdx.x] += sh[threadIdx.x + k]; __syncthreads(); }
270  float sc = rsqrtf(sh[0] / hd + eps); __syncthreads();
271  for(int i = threadIdx.x; i < hd; i += blockDim.x) h[i] = h[i] * sc * nw[i];
272  __syncthreads();
273  int half = rot / 2, pos = *d_pos;
274  for(int i = threadIdx.x; i < half; i += blockDim.x){
275    float f = powf(rbase, -2.0f * (float)i / (float)rot);       // over the ROTARY width
276    float a = pos * f, c = cosf(a), si = sinf(a);
277    float x0 = h[i], x1 = h[i + half];
278    h[i] = x0 * c - x1 * si; h[i + half] = x0 * si + x1 * c;
279  }
280  // dims [rot, hd) are carried through untouched — that is what "partial" means.
281}
282
283// Gated attention's output gate: ctx *= sigmoid(g). The checkpoint packs g into the Q projection, so this is
284// the only extra work a gated attention layer does over an ordinary one.
285__global__ void gk_gate_mul(float* __restrict__ ctx, const float* __restrict__ g, int n){
286  int i = blockIdx.x * blockDim.x + threadIdx.x;
287  if(i < n) ctx[i] *= 1.0f / (1.0f + __expf(-g[i]));
288}
289"#;
290
291/// Emit a self-contained CUDA program for ONE Gated DeltaNet step with every input inlined, so the fused
292/// kernels above can be checked against the LOGOS `gated delta` op on the VM at small shapes on any GPU —
293/// the same shape of proof `emit_moe_forward` gives the MoE block. Prints `O` then the mutated memory `S`,
294/// because a desugar that ignored the memory would otherwise pass.
295#[allow(clippy::too_many_arguments)]
296pub fn emit_gated_delta_forward(
297    q: &[f64],
298    k: &[f64],
299    v: &[f64],
300    z: &[f64],
301    beta: &[f64],
302    alpha: &[f64],
303    s0: &[f64],
304    gn: &[f64],
305    hv: usize,
306    hk: usize,
307    d: usize,
308) -> String {
309    let arr = |name: &str, xs: &[f64]| {
310        let body = xs
311            .iter()
312            .map(|x| format!("{:.17e}f", x))
313            .collect::<Vec<_>>()
314            .join(",");
315        format!("static const float {name}[] = {{{body}}};\n")
316    };
317    format!(
318        "#include <cstdio>\n#include <cmath>\n#include <cuda_runtime.h>\n{kern}\n\
319         {aq}{ak}{av}{az}{ab}{aa}{as_}{ag}\n\
320         int main(){{\n\
321         \x20 const int D={d}, HV={hv}, NK={hk};\n\
322         \x20 float *dq,*dk,*dv,*dz,*db,*da,*dS,*dgn,*dO,*dOut;\n\
323         \x20 cudaMalloc(&dq,sizeof(hq)); cudaMemcpy(dq,hq,sizeof(hq),cudaMemcpyHostToDevice);\n\
324         \x20 cudaMalloc(&dk,sizeof(hk_)); cudaMemcpy(dk,hk_,sizeof(hk_),cudaMemcpyHostToDevice);\n\
325         \x20 cudaMalloc(&dv,sizeof(hv_)); cudaMemcpy(dv,hv_,sizeof(hv_),cudaMemcpyHostToDevice);\n\
326         \x20 cudaMalloc(&dz,sizeof(hz)); cudaMemcpy(dz,hz,sizeof(hz),cudaMemcpyHostToDevice);\n\
327         \x20 cudaMalloc(&db,sizeof(hb)); cudaMemcpy(db,hb,sizeof(hb),cudaMemcpyHostToDevice);\n\
328         \x20 cudaMalloc(&da,sizeof(ha)); cudaMemcpy(da,ha,sizeof(ha),cudaMemcpyHostToDevice);\n\
329         \x20 cudaMalloc(&dS,sizeof(hs)); cudaMemcpy(dS,hs,sizeof(hs),cudaMemcpyHostToDevice);\n\
330         \x20 cudaMalloc(&dgn,sizeof(hg)); cudaMemcpy(dgn,hg,sizeof(hg),cudaMemcpyHostToDevice);\n\
331         \x20 cudaMalloc(&dO,(size_t)HV*D*4); cudaMalloc(&dOut,(size_t)HV*D*4);\n\
332         \x20 gdn_step<<<dim3(HV,(D+3)/4),128>>>(dq,dk,dv,db,da,dS,dO,D,NK);\n\
333         \x20 gdn_gate_norm<<<HV,D>>>(dz,dgn,dO,dOut,D,1e-6f);\n\
334         \x20 cudaDeviceSynchronize();\n\
335         \x20 float* out=(float*)malloc((size_t)HV*D*4); cudaMemcpy(out,dOut,(size_t)HV*D*4,cudaMemcpyDeviceToHost);\n\
336         \x20 float* st=(float*)malloc(sizeof(hs)); cudaMemcpy(st,dS,sizeof(hs),cudaMemcpyDeviceToHost);\n\
337         \x20 for(int i=0;i<HV*D;++i) printf(\"%.9g\\n\", out[i]);\n\
338         \x20 for(size_t i=0;i<sizeof(hs)/4;++i) printf(\"%.9g\\n\", st[i]);\n\
339         \x20 return 0;\n}}\n",
340        kern = GDN_KERNELS,
341        aq = arr("hq", q), ak = arr("hk_", k), av = arr("hv_", v), az = arr("hz", z),
342        ab = arr("hb", beta), aa = arr("ha", alpha), as_ = arr("hs", s0), ag = arr("hg", gn),
343        d = d, hv = hv, hk = hk,
344    )
345}