Foundry/cuda_kernels/ssm_gated_delta.cu

1017 lines
44 KiB
Plaintext

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/*
* ssm_gated_delta.cu — Qwen3.5-MoE Gated-Delta-Net SSM kernels for Foundry
*
* Target: sm_37 (K80, Kepler)
* Compile: nvcc -ptx -arch=sm_37 ssm_gated_delta.cu -o ssm_gated_delta.ptx
*
* The GPU halves of the single-token (decode) Gated-Delta-Net SSM block. Each
* kernel mirrors, line-for-line, a static helper in the CPU reference
* Pantheon/src/models/qwen35moe_ssm.c
* and is validated against the golden vectors in Pantheon/tests/golden/.
*
* Kernels emitted (PTX symbols for ../cuda/fc_cuda_ptx.h, FC_PTX_SSM_GATED_DELTA).
* ★ ADDING A KERNEL HERE IS A THREE-STEP CHANGE, and skipping either of the last
* two makes the kernel INERT while the serve prints a misleading load failure:
* 1. define it here, then
* 2. `cd cuda_kernels && make` ON A HOST WITH nvcc 11.4 to regenerate ../cuda/fc_cuda_ptx.h
* (a checked-in artifact — the kernel does not exist until it is in there), and
* 3. add its name to ssm_names[] in fc_cuda.c AND bump the count argument to
* fc_load_ptx_module. The loader assigns names in order and stops at the
* first one the module lacks, so a name with no symbol behind it makes
* fc_load_ptx_module return nonzero and print "SSM gated-delta PTX load
* failed (kernels unavailable)" even though every earlier kernel loaded fine.
* The oldest three, for orientation (the rest are documented at their definitions):
* fc_ssm_recurrence — per-token delta-rule recurrence (THE centerpiece)
* mirrors qwen35moe_compute_recur_heads (ssm.c:163-196)
* fc_ssm_conv1d_silu — causal depthwise conv1d (k=4) + SiLU, rolls conv_state
* mirrors qwen35moe_conv1d_silu (ssm.c:110-135)
* fc_ssm_gated_norm — gated RMSNorm: RMSNorm(y,w) * silu(z), per V-head
* mirrors qwen35moe_gated_norm (ssm.c:206-223)
*
* Config (verified Nex-N2-mini): n_v_heads=32, n_k_heads=16, state_size S=128,
* scale=1/sqrt(128), conv_dim=8192, conv_kernel=4. Transposed state layout:
* s[h*S*S + j*S + i] == S_h[i][j] (row j contiguous over i).
* The q/k arriving here are ALREADY L2-normed and GQA-repeated to n_v_heads on
* the host (interleaved tiling vh = r*n_k_heads + kh); this kernel never sees
* the 16->32 repeat, only the post-repeat [n_v_heads*S] streams.
*
* Kepler notes: pure fp32 throughout; plain shared-memory tree reductions
* (no __shfl / _sync portability question); expf/__expf via the standard libm
* device intrinsics, matching the CPU expf bit-tolerance band (~1e-4..1e-3 rel).
*/
extern "C" {
/* SiLU(x) = x * sigmoid(x) = x / (1 + exp(-x)).
* Mirrors qwen35moe_silu (ssm.c:60-63). */
__device__ __forceinline__ float fc_ssm_silu(float x) {
return x / (1.0f + expf(-x));
}
/*
* ============================================================
* Kernel 1: per-token delta-rule recurrence (THE centerpiece)
* ============================================================
*
* Mirrors qwen35moe_compute_recur_heads (ssm.c:163-196), one (head h, row j)
* per CUDA block. The CPU walks all S rows of all heads serially; the GPU maps
* the 4096 independent (h,j) pairs onto the grid. Within a block the S=128
* threads cooperate on the two length-S dot reductions.
*
* State is transposed: s[h*S*S + j*S + i] == S_h[i][j]; row j is contiguous
* over i, so each block owns one unit-stride row `row[i] = s[h*S*S + j*S + i]`.
* For one token (CPU lines 180-193):
* (a) row[i] *= exp(g[h]) scalar decay of the whole row
* (b) pred = sum_i row[i]*k[h][i] (shared-mem tree reduction)
* delta = (v[h][j] - pred) * beta[h]
* (c) row[i] += k[h][i] * delta rank-1 write, in place
* (d) y[h][j] = scale * sum_i row[i]*q[h][i] read-AFTER-write (2nd reduce)
* The decayed+updated row is written back to s in place (state carries forward).
*
* Inputs:
* s [n_v_heads * S * S] transposed state, updated in place
* q,k,v [n_v_heads * S] q/k L2-normed + GQA-repeated on host; v straight
* g [n_v_heads] scalar decay (PRE-exp; we exp here, CPU line 173)
* beta [n_v_heads] sigmoid'd on host (CPU applies sigmoid earlier)
* y [n_v_heads * S] output, one scalar per (h,j)
* state_size S (must equal blockDim.x)
* scale 1/sqrt(S)
*
* Grid: (state_size, n_v_heads, 1) — blockIdx.x = j (row), blockIdx.y = h.
* Block: state_size threads (= S = 128), one thread per i.
* Shared mem: blockDim.x * sizeof(float) (reduction scratch).
*/
__global__ void fc_ssm_recurrence(float *s, const float *q, const float *k,
const float *v, const float *g,
const float *beta, float *y,
int state_size, float scale) {
extern __shared__ float sdata[];
const int S = state_size;
const int j = blockIdx.x; /* row j of the transposed state */
const int h = blockIdx.y; /* V-head */
const int i = threadIdx.x; /* state coordinate within the row */
if (i >= S)
return;
float *row = s + ((size_t)h * S + j) * S; /* s[h*S*S + j*S + i] over i */
const float *kh = k + (size_t)h * S;
const float *qh = q + (size_t)h * S;
const float decay = expf(g[h]);
const float beta_h = beta[h];
/* (a) scalar decay of the whole row (CPU lines 180-182). */
float ri = row[i] * decay;
/* (b) pred = sum_i row'[i]*k[i] via shared-mem tree reduction. */
sdata[i] = ri * kh[i];
__syncthreads();
for (int stride = S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
float pred = sdata[0];
/* delta = (v[h][j] - pred) * beta (CPU line 186; v indexed by j). */
float delta = (v[(size_t)h * S + j] - pred) * beta_h;
__syncthreads(); /* all threads consumed sdata[0] before the next reduce */
/* (c) rank-1 write: row[i] += k[i]*delta (CPU lines 188-190), in place. */
ri += kh[i] * delta;
row[i] = ri;
/* (d) read-after-write: y[h][j] = scale * sum_i row[i]*q[i] (CPU line 193). */
sdata[i] = ri * qh[i];
__syncthreads();
for (int stride = S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
if (i == 0)
y[(size_t)h * S + j] = scale * sdata[0];
}
/*
* ============================================================
* Kernel 2: causal conv1d (k=4) + SiLU, rolls conv_state
* ============================================================
*
* Mirrors qwen35moe_conv1d_silu (ssm.c:110-135). One thread per channel c over
* the full conv_dim stream (depthwise; channels are independent). Each channel's
* sliding window is [conv_state(c,0) .. conv_state(c,k-2), qkv(c)] (length k):
* out[c] = SiLU( sum_{t<k-1} conv_state[c][t]*w[c][t] + qkv[c]*w[c][k-1] )
* then the per-channel conv_state is rolled (drop oldest, append raw qkv[c]).
*
* conv_state layout: [conv_dim][k-1], oldest..newest per channel (CPU comment
* lines 102-108). conv weight is row-major [conv_dim, k]: w[c][tap]=weight[c*k+tap].
* conv_state is updated in place; out holds the SiLU'd convolved stream.
*
* Inputs:
* qkv [conv_dim] raw projected stream (this token)
* weight [conv_dim * k] depthwise conv weights, row-major
* conv_state [conv_dim*(k-1)] per-channel history, updated in place
* out [conv_dim] SiLU'd output
* conv_dim, conv_kernel (k)
*
* Grid: ceil(conv_dim / blockDim.x) blocks.
* Block: 256 threads. No shared memory.
*/
__global__ void fc_ssm_conv1d_silu(const float *qkv, const float *weight,
float *conv_state, float *out,
int conv_dim, int conv_kernel) {
int c = blockIdx.x * blockDim.x + threadIdx.x;
if (c >= conv_dim)
return;
const int k = conv_kernel;
const int km1 = k - 1;
const float *cw = weight + (size_t)c * k;
float *cs = conv_state + (size_t)c * km1;
/* out[c] = SiLU( history . w[0..k-2] + qkv[c]*w[k-1] ) (CPU lines 125-128). */
float acc = qkv[c] * cw[km1];
for (int t = 0; t < km1; t++)
acc += cs[t] * cw[t];
out[c] = fc_ssm_silu(acc);
/* Roll the per-channel state: drop oldest, append raw qkv (CPU lines 131-133). */
for (int t = 0; t < km1 - 1; t++)
cs[t] = cs[t + 1];
cs[km1 - 1] = qkv[c];
}
/*
* ============================================================
* Kernel 3: gated RMSNorm — RMSNorm(y, w) * silu(z), per V-head
* ============================================================
*
* Mirrors qwen35moe_gated_norm (ssm.c:206-223), minus the trailing ssm_out
* projection (that reuses fc_matvec_q3k on the host). One block per V-head; the
* S=128 threads do a shared-mem sum-of-squares reduction, then each writes
* gated[h*S+d] = (y[h*S+d] / sqrt(mean(y^2)+eps)) * w[d] * silu(z[h*S+d]).
* The norm weight w is shared across heads (length S), exactly like the CPU
* pn_vrms_norm_f32 call per head (CPU line 219) followed by the silu(z) multiply
* (CPU lines 220-221).
*
* Inputs:
* y [n_v_heads * S] recurrence output
* weight [S] ssm_norm weight (shared across heads)
* z [n_v_heads * S] attn_gate projection (pre-silu)
* gated [n_v_heads * S] output
* state_size S, eps
*
* Grid: (n_v_heads, 1, 1) one block per head.
* Block: 256 threads (or S if smaller). Shared mem: blockDim.x * sizeof(float).
*/
__global__ void fc_ssm_gated_norm(const float *y, const float *weight,
const float *z, float *gated,
int state_size, float eps) {
extern __shared__ float sdata[];
const int S = state_size;
const int h = blockIdx.x;
const int tid = threadIdx.x;
const int threads = blockDim.x;
const float *yh = y + (size_t)h * S;
const float *zh = z + (size_t)h * S;
float *gh = gated + (size_t)h * S;
/* Phase 1: partial sum of squares of y over this head's S dims. */
float sum_sq = 0.0f;
for (int d = tid; d < S; d += threads) {
float v = yh[d];
sum_sq += v * v;
}
sdata[tid] = sum_sq;
__syncthreads();
for (int stride = threads / 2; stride > 0; stride >>= 1) {
if (tid < stride)
sdata[tid] += sdata[tid + stride];
__syncthreads();
}
/* RMSNorm divisor matches pn_vrms_norm_f32: sqrt(mean(y^2) + eps). */
float inv_rms = 1.0f / sqrtf(sdata[0] / (float)S + eps);
/* Phase 2: gated[d] = RMSNorm(y)[d] * w[d] * silu(z[d]). */
for (int d = tid; d < S; d += threads)
gh[d] = yh[d] * inv_rms * weight[d] * fc_ssm_silu(zh[d]);
}
/* ================================================================
* SSM "glue" kernels — replace the host round-trips in the qwen35moe GPU
* forward (each was a D2H -> CPU -> H2D per SSM layer, ~270 sync'd PCIe
* transfers/token = the tok/s floor). Small element-wise ops, pure fp32.
* ================================================================ */
/* Per-element sigmoid, in place: x = 1/(1+exp(-x)). One thread per element.
* Replaces the host-side beta = sigmoid(beta). */
__global__ void fc_sigmoid(float *x, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n)
x[i] = 1.0f / (1.0f + expf(-x[i]));
}
/* GDA decay gate: g[h] = ssm_a[h] * softplus(alpha[h] + ssm_dt[h]), per V-head.
* softplus(v) = v>20 ? v : log1p(exp(v)); ssm_a = -exp(A_log) (pre-baked host
* side). One thread per head. Replaces the 3x D2H + CPU + H2D decay-gate glue. */
__global__ void fc_ssm_decay_gate(const float *alpha, const float *ssm_dt,
const float *ssm_a, float *g, int n) {
int h = blockIdx.x * blockDim.x + threadIdx.x;
if (h >= n)
return;
float v = alpha[h] + ssm_dt[h];
float sp = (v > 20.0f) ? v : log1pf(expf(v));
g[h] = ssm_a[h] * sp;
}
/* Per-K-head L2-norm of q and k over their S state dims, then GQA-repeat
* interleaved to n_v_heads (vh = r*n_k_heads + kh). conv_out layout is
* [q key_dim | k key_dim | v val_dim]; this handles q,k only (v is a straight
* copy done by the launcher). One block per K-head, blockDim.x == S, shared mem
* [2*S]. L2-norm matches gpu_l2_norm: x / max(sqrt(sum x^2), eps). */
__global__ void fc_ssm_qknorm_gqa(const float *conv_out, float *qg, float *kg,
int n_k_heads, int kv_groups, int S,
int key_dim, float eps) {
int kh = blockIdx.x;
if (kh >= n_k_heads)
return;
int i = threadIdx.x; /* one thread per state dim (== S) */
extern __shared__ float sh[]; /* [2*blockDim.x]: q | k sumsq partials */
const float *q_src = conv_out;
const float *k_src = conv_out + key_dim;
float qv = q_src[(size_t)kh * S + i];
float kv = k_src[(size_t)kh * S + i];
sh[i] = qv * qv;
sh[blockDim.x + i] = kv * kv;
__syncthreads();
for (int stride = blockDim.x >> 1; stride > 0; stride >>= 1) {
if (i < stride) {
sh[i] += sh[i + stride];
sh[blockDim.x + i] += sh[blockDim.x + i + stride];
}
__syncthreads();
}
float inv_q = 1.0f / fmaxf(sqrtf(sh[0]), eps);
float inv_k = 1.0f / fmaxf(sqrtf(sh[blockDim.x]), eps);
float qn = qv * inv_q;
float kn = kv * inv_k;
for (int r = 0; r < kv_groups; r++) {
int vh = r * n_k_heads + kh; /* interleaved GQA tiling */
qg[(size_t)vh * S + i] = qn;
kg[(size_t)vh * S + i] = kn;
}
}
/* ================================================================
* BATCHED (prefill) variants — n positions of ONE sequence per launch.
*
* The two stateful kernels (conv1d, recurrence) are a SCAN over the
* positions: each block loops t = 0..n-1 internally, carrying its slice of
* the state forward exactly as n single-token launches would — the per-
* position fp op order is identical to the single-token kernels above, so
* every position's output and the final carried state are bit-exact. The
* stateless glue (decay gate, qknorm+GQA) just gains a position axis.
*
* Batched activations are token-major: qkv/out [n][conv_dim],
* q/k/v/y [n][n_v_heads*S], g/beta [n][n_v_heads].
* ================================================================ */
/* Causal conv1d + SiLU over n positions (scan). One thread per channel;
* the (k-1)-deep history lives in registers across positions and is
* written back once at the end. Per position the accumulation order
* matches fc_ssm_conv1d_silu exactly. Requires conv_kernel <= 4.
*
* snaps (optional, NULL = off): per-position state snapshots for
* speculative decoding — snaps[t] = the conv_state image AFTER position t
* was consumed, [n][conv_dim][km1] (same channel-major layout as
* conv_state), so a partial accept of M positions restores state-after-M
* with one contiguous copy of snaps[M-1].
*
* LOCAL-MEMORY NOTE (why the k==4 case below is hand-scalarised):
* the generic body keeps the history in `float hist[3]` indexed by loops
* bounded by the RUNTIME parameter km1, and stores hist[km1-1] at a runtime
* index. nvcc cannot promote an array under a runtime-indexed store, so the
* whole array went to local memory — the committed PTX carries
* .local .align 4 .b8 __local_depot6[12];
* with ~3 ld.local + 3 st.local per position per thread plus two
* unroll-remainder branch ladders, while `ptxas -v` still prints
* "0 bytes spill" (read the STACK FRAME, not the spill counters).
* conv_kernel is 4 for every model that reaches here and the launcher
* already rejects conv_kernel > 4, so k==4 is specialised into three named
* scalars: no runtime index, no local traffic. BIT-EXACT — the accumulation is
* the same `mul` then three `fma`s in the same u = 0,1,2 order the generic path
* already emitted (verified against the committed PTX).
*
* MEASURED IN THE REGENERATED PTX (2026-08-10), and read this before chasing the
* frame again: `__local_depot6[12]` is STILL DECLARED, because the generic k<4
* fallback below lives in the same entry and still stores at a runtime index, so
* nvcc must reserve the frame for it. What moved is the *executed* path — the
* `setp.eq %r, 4` branch target carries **0 ld.local and 0 st.local**, against
* **26 ld.local + 14 st.local** when k==4 had no specialisation and that was the
* only path there was. Every model reaching here has conv_kernel == 4, so no
* executed instruction touches local memory and the reserved frame costs nothing
* but address space. Getting the frame to a literal zero needs the k==2 and k==3
* cases hand-scalarised too (or the launcher narrowed to conv_kernel == 4 and the
* generic body deleted); this kernel is 0.4 % of prefill, so that was not done. */
__global__ void fc_ssm_conv1d_silu_seq(const float *qkv, const float *weight,
float *conv_state, float *out,
int conv_dim, int conv_kernel, int n,
float *snaps) {
int c = blockIdx.x * blockDim.x + threadIdx.x;
if (c >= conv_dim)
return;
const int k = conv_kernel;
const int km1 = k - 1;
const float *cw = weight + (size_t)c * k;
float *cs = conv_state + (size_t)c * km1;
if (k == 4) {
const float w0 = cw[0], w1 = cw[1], w2 = cw[2], w3 = cw[3];
float h0 = cs[0], h1 = cs[1], h2 = cs[2];
for (int t = 0; t < n; t++) {
float xv = qkv[(size_t)t * conv_dim + c];
float acc = __fmul_rn(xv, w3);
acc = __fmaf_rn(h0, w0, acc);
acc = __fmaf_rn(h1, w1, acc);
acc = __fmaf_rn(h2, w2, acc);
out[(size_t)t * conv_dim + c] = fc_ssm_silu(acc);
h0 = h1;
h1 = h2;
h2 = xv;
if (snaps) {
float *sn = snaps + ((size_t)t * conv_dim + c) * 3;
sn[0] = h0;
sn[1] = h1;
sn[2] = h2;
}
}
cs[0] = h0;
cs[1] = h1;
cs[2] = h2;
return;
}
float hist[3]; /* km1 <= 3 (conv_kernel <= 4) */
for (int t = 0; t < km1; t++)
hist[t] = cs[t];
for (int t = 0; t < n; t++) {
float xv = qkv[(size_t)t * conv_dim + c];
float acc = xv * cw[km1];
for (int u = 0; u < km1; u++)
acc += hist[u] * cw[u];
out[(size_t)t * conv_dim + c] = fc_ssm_silu(acc);
for (int u = 0; u < km1 - 1; u++)
hist[u] = hist[u + 1];
hist[km1 - 1] = xv;
if (snaps) {
float *sn = snaps + ((size_t)t * conv_dim + c) * km1;
for (int u = 0; u < km1; u++)
sn[u] = hist[u];
}
}
for (int t = 0; t < km1; t++)
cs[t] = hist[t];
}
/* Delta-rule recurrence over n positions (scan). Block (j, h) loops the
* positions, carrying its state row in registers (ri) between them — the
* row is read once, updated n times with the exact per-position op order
* of fc_ssm_recurrence, and written back once. q/k/v/y advance by
* n_v_heads*S per position, g/beta by n_v_heads.
*
* snaps (optional, NULL = off): per-position state snapshots for
* speculative decoding — snaps[t] = the full recurrent state AFTER
* position t's update, [n][n_v_heads*S*S] (same layout as s), so a
* partial accept of M positions restores state-after-M with one
* contiguous copy of snaps[M-1]. */
__global__ void fc_ssm_recurrence_seq(float *s, const float *q, const float *k,
const float *v, const float *g,
const float *beta, float *y,
int state_size, int n_v_heads, int n,
float scale, float *snaps) {
extern __shared__ float sdata[];
const int S = state_size;
const int j = blockIdx.x;
const int h = blockIdx.y;
const int i = threadIdx.x;
if (i >= S)
return;
const size_t row_off = ((size_t)h * S + j) * S;
const size_t state_total = (size_t)n_v_heads * S * S;
float *row = s + row_off;
float ri = row[i];
for (int t = 0; t < n; t++) {
const float *kh = k + (size_t)t * n_v_heads * S + (size_t)h * S;
const float *qh = q + (size_t)t * n_v_heads * S + (size_t)h * S;
const float decay = expf(g[(size_t)t * n_v_heads + h]);
const float beta_h = beta[(size_t)t * n_v_heads + h];
ri *= decay;
sdata[i] = ri * kh[i];
__syncthreads();
for (int stride = S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
float pred = sdata[0];
float delta = (v[(size_t)t * n_v_heads * S + (size_t)h * S + j] - pred)
* beta_h;
__syncthreads();
ri += kh[i] * delta;
if (snaps)
snaps[(size_t)t * state_total + row_off + i] = ri;
sdata[i] = ri * qh[i];
__syncthreads();
for (int stride = S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
if (i == 0)
y[(size_t)t * n_v_heads * S + (size_t)h * S + j] = scale * sdata[0];
__syncthreads();
}
row[i] = ri;
}
/* UNROLLED-REDUCTION prefill recurrence — the cheap half of the warp rewrite.
*
* WHY. fc_ssm_recurrence_seq's two tree reductions are written
* `for (stride = S/2; stride > 0; stride >>= 1)` where S == state_size is a
* RUNTIME kernel parameter, so ptxas cannot unroll the 7-step tree and every
* step pays a predicate, a branch, a shift and a latch. From the committed PTX
* that loop control alone is 336 of the kernel's ~740 warp-issue slots per
* (block, position) — 45 %. Specialising S to 128 makes the bound a literal and
* lets ptxas unroll both trees, deleting the loop control but keeping the
* barrier chain and the shared-memory traffic: ~1.83x fewer issue slots, against
* ~10x for fc_ssm_recurrence_seq_warp below.
*
* It exists as a separately priceable arm: it is ~15 lines and cannot regress
* occupancy (identical shared and register footprint), so if the warp rewrite
* fails its ptxas gate this is the fallback that still collects the loop-control
* half of the win.
*
* BIT-EXACT, and trivially so: this is fc_ssm_recurrence_seq verbatim with the
* loop bound written as a literal. Every floating-point operation is the same
* expression in the same order with the same operands, and every intermediate
* still round-trips through shared memory (so ptxas cannot contract a product
* into an fma here either). Do NOT "tidy" the plain `*` and `+=` into
* intrinsics: matching the baseline's codegen is the point.
*
* Guarded on state_size == FC_SSM_UNROLL_S; the guard is block-uniform so it can
* never split a __syncthreads. The launcher keeps the baseline for anything else.
*
* Grid/block/shared: identical to fc_ssm_recurrence_seq — (S, n_v_heads),
* S threads, S floats of shared. */
#define FC_SSM_UNROLL_S 128 /* state_size this variant is specialised for */
__global__ void fc_ssm_recurrence_seq_unroll128(float *s, const float *q,
const float *k, const float *v,
const float *g,
const float *beta, float *y,
int state_size, int n_v_heads,
int n, float scale,
float *snaps) {
extern __shared__ float sdata[];
/* Block-uniform: every thread returns or none does. */
if (state_size != FC_SSM_UNROLL_S)
return;
const int S = FC_SSM_UNROLL_S;
const int j = blockIdx.x;
const int h = blockIdx.y;
const int i = threadIdx.x;
if (i >= S)
return;
const size_t row_off = ((size_t)h * S + j) * S;
const size_t state_total = (size_t)n_v_heads * S * S;
float *row = s + row_off;
float ri = row[i];
for (int t = 0; t < n; t++) {
const float *kh = k + (size_t)t * n_v_heads * S + (size_t)h * S;
const float *qh = q + (size_t)t * n_v_heads * S + (size_t)h * S;
const float decay = expf(g[(size_t)t * n_v_heads + h]);
const float beta_h = beta[(size_t)t * n_v_heads + h];
ri *= decay;
sdata[i] = ri * kh[i];
__syncthreads();
#pragma unroll
for (int stride = FC_SSM_UNROLL_S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
float pred = sdata[0];
float delta = (v[(size_t)t * n_v_heads * S + (size_t)h * S + j] - pred)
* beta_h;
__syncthreads();
ri += kh[i] * delta;
if (snaps)
snaps[(size_t)t * state_total + row_off + i] = ri;
sdata[i] = ri * qh[i];
__syncthreads();
#pragma unroll
for (int stride = FC_SSM_UNROLL_S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
if (i == 0)
y[(size_t)t * n_v_heads * S + (size_t)h * S + j] = scale * sdata[0];
__syncthreads();
}
row[i] = ri;
}
/* ================================================================
* WARP-REDUCTION prefill recurrence — the tree-preserving rewrite.
*
* WHY. In fc_ssm_recurrence_seq above, one 128-thread block advances ONE
* state column (h, j) and spends its whole life in two shared-memory tree
* reductions. From the committed PTX for that kernel, per (block, position):
*
* 740 warp-instruction issue slots, of which
* 488 (66 %) the two tree reductions, and inside those
* 336 (45 %) setp/bra/shr/mov loop control — the 7-step loop is NOT
* unrolled because its bound is `state_size`, a RUNTIME
* kernel parameter, so every step pays a predicate, a
* branch, a shift and a latch
* 72 ( 10 %) bar.sync (18 barriers per position, not 14)
* 48 ( 6 %) the actual ld.shared/add.f32/st.shared
* 76 (10 %) expf(g[t][h]) — 19 PTX ops incl. ex2.approx.ftz, on a
* value UNIFORM across the entire block, recomputed by all
* 128 threads at all 128 positions (16384x redundant)
* and only ~41 slots (5.5 %) doing useful floating-point work.
* Occupancy is already 100 % (512 B shared, ~40 regs, 16 blocks/SMX), the
* grid is 4096 blocks = 315 per SMX, and the kernel still sustains only
* IPC ~2/SMX. It is not bandwidth-, compute-, launch- or occupancy-bound:
* it is issue-bound on overhead, and 3 of its 4 warps idle at a barrier
* through 5 of the 7 tree steps.
*
* WHAT. One WARP owns one column (h, j); lane L carries state elements
* i = L, L+32, L+64, L+96 in registers. The length-128 dot then costs 4
* products, 3 intra-lane adds and a 5-step __shfl_xor butterfly — no shared
* memory, and __syncthreads() disappears from the position loop entirely
* (one barrier per BLOCK, for the decay/beta staging). ~72 issue slots per
* (column, position) against 740: a 10x instruction cut with 49 % of the
* slots now doing useful FP work.
*
* BIT-EXACT, and this is the whole point of the structure:
* - fc_ssm_warp_tree_dot reproduces the baseline's reduction TREE, not just
* its value. The baseline pairs index i with i+64, then i with i+32, then
* runs strides 16..1. In lane-local terms 64 and 32 are e-strides 2 and 1
* over the 4 elements a lane holds, so p[0]+=p[2]; p[1]+=p[3]; p[0]+=p[1]
* lands the identical grouping, and strides 16..1 are exactly a 32-lane
* butterfly. __shfl_xor gives lane L the pair (v_L, v_{L^st}); for L < st
* that is the baseline's (v_L, v_{L+st}) and for L >= st it is the same
* two values with the operands swapped — fp add is commutative bit-for-bit,
* so every lane ends holding the baseline's sdata[0] exactly, which also
* removes the broadcast.
* - every arithmetic op uses __fmul_rn/__fadd_rn/__fsub_rn/__fmaf_rn. This
* is NOT decoration: the baseline's per-element product went through
* shared memory, so nvcc could not contract it, while here everything is
* in registers and nvcc WOULD fuse p[0] + p[2] into fma(r2, k2, p0) and
* silently change the result. The _rn intrinsics are contraction barriers.
* - expf is hoisted, not approximated: the same expf() on the same fp32
* input, stored fp32 in shared and reloaded, is the same bits.
*
* Specialised to state_size == 128 (every model that reaches this kernel);
* the launcher keeps the baseline for anything else, and the guard below is
* block-uniform so it can never split a __syncthreads.
*
* Grid: (state_size / FC_SSM_WARP_COLS, n_v_heads) = (32, 32) = 1024 blocks
* Block: FC_SSM_WARP_COLS * 32 = 128 threads (4 warps = 4 columns)
* Shared: 2 * n floats — decay[n] | beta[n], staged once per block.
* ================================================================ */
#define FC_SSM_WARP_S 128 /* state_size this variant is specialised for */
#define FC_SSM_WARP_ELEMS 4 /* FC_SSM_WARP_S / 32 state elements per lane */
#define FC_SSM_WARP_COLS 4 /* warps per block == state columns per block */
#if FC_SSM_WARP_ELEMS != 4
#error "fc_ssm_warp_tree_dot's pairing is derived for S=128 (4 elems/lane)"
#endif
/* Length-FC_SSM_WARP_S dot of the lane-distributed vectors r and x, reducing
* in the exact binary tree fc_ssm_recurrence_seq's shared-memory sweep uses.
* Returns the identical fp32 bits in all 32 lanes.
*
* p[] is indexed only by loops with COMPILE-TIME trip counts, so it is
* register-promoted. Verify that, do not assume it: `ptxas -v` must report a
* ZERO stack frame for this kernel. On this project a small array under a
* runtime-bounded loop lands in local memory while the spill counters still
* read "0 bytes spill" (see fc_ssm_conv1d_silu_seq's __local_depot above). */
__device__ __forceinline__ float fc_ssm_warp_tree_dot(const float *r,
const float *x) {
/* Level 0: the products the baseline writes into sdata[i]. */
float p[FC_SSM_WARP_ELEMS];
#pragma unroll
for (int e = 0; e < FC_SSM_WARP_ELEMS; e++)
p[e] = __fmul_rn(r[e], x[e]);
/* Baseline strides S/2 .. 32 — entirely inside one lane (e-strides). */
#pragma unroll
for (int st = FC_SSM_WARP_ELEMS / 2; st > 0; st >>= 1)
#pragma unroll
for (int e = 0; e < st; e++)
p[e] = __fadd_rn(p[e], p[e + st]);
/* Baseline strides 16 .. 1 over the 32 survivors — a warp butterfly. */
float acc = p[0];
#pragma unroll
for (int st = 16; st > 0; st >>= 1)
acc = __fadd_rn(acc, __shfl_xor(acc, st));
return acc;
}
__global__ void fc_ssm_recurrence_seq_warp(float *s, const float *q,
const float *k, const float *v,
const float *g, const float *beta,
float *y, int state_size,
int n_v_heads, int n, float scale,
float *snaps) {
extern __shared__ float sh[]; /* [2*n]: decay[n] | beta[n] */
/* Block-uniform: every thread returns or none does. */
if (state_size != FC_SSM_WARP_S)
return;
const int S = FC_SSM_WARP_S;
const int lane = (int)(threadIdx.x & 31u);
const int col = (int)(threadIdx.x >> 5);
const int j = blockIdx.x * FC_SSM_WARP_COLS + col;
const int h = blockIdx.y;
/* Stage the block-uniform decay/beta for all n positions once. */
float *sh_decay = sh;
float *sh_beta = sh + n;
for (int t = (int)threadIdx.x; t < n; t += FC_SSM_WARP_COLS * 32) {
sh_decay[t] = expf(g[(size_t)t * n_v_heads + h]);
sh_beta[t] = beta[(size_t)t * n_v_heads + h];
}
__syncthreads(); /* the only barrier in this kernel */
if (j >= S)
return;
const size_t row_off = ((size_t)h * S + j) * S;
const size_t state_total = (size_t)n_v_heads * S * S;
const size_t pos_stride = (size_t)n_v_heads * S;
float *row = s + row_off;
float r[FC_SSM_WARP_ELEMS];
#pragma unroll
for (int e = 0; e < FC_SSM_WARP_ELEMS; e++)
r[e] = row[lane + e * 32];
const float *kh = k + (size_t)h * S + lane;
const float *qh = q + (size_t)h * S + lane;
const float *vj = v + (size_t)h * S + j;
float *yj = y + (size_t)h * S + j;
for (int t = 0; t < n; t++) {
const size_t toff = (size_t)t * pos_stride;
const float decay = sh_decay[t];
const float beta_h = sh_beta[t];
float kv[FC_SSM_WARP_ELEMS];
#pragma unroll
for (int e = 0; e < FC_SSM_WARP_ELEMS; e++) {
kv[e] = kh[toff + e * 32];
r[e] = __fmul_rn(r[e], decay);
}
const float pred = fc_ssm_warp_tree_dot(r, kv);
const float delta = __fmul_rn(__fsub_rn(vj[toff], pred), beta_h);
float qv[FC_SSM_WARP_ELEMS];
#pragma unroll
for (int e = 0; e < FC_SSM_WARP_ELEMS; e++) {
r[e] = __fmaf_rn(kv[e], delta, r[e]);
qv[e] = qh[toff + e * 32];
}
if (snaps) {
float *sn = snaps + (size_t)t * state_total + row_off;
#pragma unroll
for (int e = 0; e < FC_SSM_WARP_ELEMS; e++)
sn[lane + e * 32] = r[e];
}
const float out = fc_ssm_warp_tree_dot(r, qv);
if (lane == 0)
yj[toff] = __fmul_rn(out, scale);
}
#pragma unroll
for (int e = 0; e < FC_SSM_WARP_ELEMS; e++)
row[lane + e * 32] = r[e];
}
/* ================================================================
* MULTI-USER DECODE (slot) variants — K independent sequences, ONE token
* each, per launch. Unlike the _seq scans above there is NO cross-position
* dependency: slot u owns its OWN state image at offset u and advances it
* exactly one step. Per slot the fp op order is identical to the
* single-token kernels (fc_ssm_conv1d_silu / fc_ssm_recurrence), so each
* user's output and carried state are bit-exact vs decoding alone.
*
* Slot-major activations match the batch layout: qkv/out [K][conv_dim],
* q/k/v/y [K][n_v_heads*S], g/beta [K][n_v_heads]. States are slot-major:
* conv_state [K][conv_dim*(k-1)], s [K][n_v_heads*S*S].
* ================================================================ */
/* Causal conv1d + SiLU, one step per slot. Grid (ceil(conv_dim/block), K);
* per (slot, channel) the math is fc_ssm_conv1d_silu verbatim on that
* slot's state slice. */
__global__ void fc_ssm_conv1d_silu_slots(const float *qkv, const float *weight,
float *conv_state, float *out,
int conv_dim, int conv_kernel) {
int c = blockIdx.x * blockDim.x + threadIdx.x;
int u = blockIdx.y; /* slot (user) */
if (c >= conv_dim)
return;
const int k = conv_kernel;
const int km1 = k - 1;
const float *cw = weight + (size_t)c * k;
float *cs = conv_state + ((size_t)u * conv_dim + c) * km1;
const float *x = qkv + (size_t)u * conv_dim;
float *o = out + (size_t)u * conv_dim;
float acc = x[c] * cw[km1];
for (int t = 0; t < km1; t++)
acc += cs[t] * cw[t];
o[c] = fc_ssm_silu(acc);
for (int t = 0; t < km1 - 1; t++)
cs[t] = cs[t + 1];
cs[km1 - 1] = x[c];
}
/* Delta-rule recurrence, one step per slot. Grid (S, n_v_heads, K); block
* (j, h, u) updates row j of head h of SLOT u's state — the body is
* fc_ssm_recurrence verbatim at that slot's offsets. */
__global__ void fc_ssm_recurrence_slots(float *s, const float *q,
const float *k, const float *v,
const float *g, const float *beta,
float *y, int state_size,
int n_v_heads, float scale) {
extern __shared__ float sdata[];
const int S = state_size;
const int j = blockIdx.x; /* row j of the transposed state */
const int h = blockIdx.y; /* V-head */
const int u = blockIdx.z; /* slot (user) */
const int i = threadIdx.x; /* state coordinate within the row */
if (i >= S)
return;
float *row = s + (size_t)u * n_v_heads * S * S + ((size_t)h * S + j) * S;
const float *kh = k + (size_t)u * n_v_heads * S + (size_t)h * S;
const float *qh = q + (size_t)u * n_v_heads * S + (size_t)h * S;
const float decay = expf(g[(size_t)u * n_v_heads + h]);
const float beta_h = beta[(size_t)u * n_v_heads + h];
float ri = row[i] * decay;
sdata[i] = ri * kh[i];
__syncthreads();
for (int stride = S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
float pred = sdata[0];
float delta = (v[(size_t)u * n_v_heads * S + (size_t)h * S + j] - pred)
* beta_h;
__syncthreads();
ri += kh[i] * delta;
row[i] = ri;
sdata[i] = ri * qh[i];
__syncthreads();
for (int stride = S / 2; stride > 0; stride >>= 1) {
if (i < stride)
sdata[i] += sdata[i + stride];
__syncthreads();
}
if (i == 0)
y[(size_t)u * n_v_heads * S + (size_t)h * S + j] = scale * sdata[0];
}
/* ================================================================
* T4: 3-pass -> 2-pass state-access factorization (fable2 member T4).
*
* The baseline fc_ssm_recurrence_slots touches the recurrent state twice in
* global memory (one read of row[i], one write), but it runs TWO shared-memory
* tree reductions per (h,j) block separated by a read-after-write barrier:
* R1: pred = sum_i (decay*row_prev[i]) * k[i] -> delta
* R2: y[j] = scale * sum_i row_new[i] * q[i] (row_new needs delta)
* R2 is data-dependent on R1 (it reads the just-written row), so on Kepler the
* two length-S reductions serialize behind ~2*log2(S) __syncthreads barriers
* plus a second LDS sweep — the dominant per-block cost.
*
* The T4 algebraic identity S_t^T q = g*(S_{t-1}^T q) + (q^T k)*delta lets the
* OUTPUT ride the SAME read pass as retrieval: with row_dec[i] = decay*row_prev[i],
* y[j] = scale * sum_i (row_dec[i] + k[i]*delta) * q[i]
* = scale * ( sum_i row_dec[i]*q[i] + delta * sum_i k[i]*q[i] )
* = scale * ( oq[j] + delta * qk )
* where qk = sum_i k[i]*q[i] is a scalar shared across all rows j of the head.
* All three partials (pred = sum ri*k, oq = sum ri*q, qk = sum k*q) are reduced
* together in ONE fused pass ([3*S] shared), removing the second reduction and
* its read-after-write barrier. State traffic is unchanged (still one read, one
* write); the win is the eliminated LDS sweep + barriers.
*
* LOSSY-tiny (reassociation class): the baseline forms y as one fused dot over
* row_new[i]; this forms it as two separate sums (oq + delta*qk). The result
* differs only by FP associativity of sum((a+b)*q) vs sum(a*q)+sum(b*q) —
* same magnitude as any tile/reduction-order change, oracle-diff gated. Guarded
* by QWEN35_SSM_2PASS (default OFF); the baseline kernel stays the bit-exact
* serve path. Grid/block identical to fc_ssm_recurrence_slots. */
__global__ void fc_ssm_recurrence_slots_2pass(float *s, const float *q,
const float *k, const float *v,
const float *g, const float *beta,
float *y, int state_size,
int n_v_heads, float scale) {
extern __shared__ float sdata[]; /* [3*S]: pred | oq | qk partials */
const int S = state_size;
const int j = blockIdx.x; /* row j of the transposed state */
const int h = blockIdx.y; /* V-head */
const int u = blockIdx.z; /* slot (user) */
const int i = threadIdx.x; /* state coordinate within the row */
if (i >= S)
return;
float *row = s + (size_t)u * n_v_heads * S * S + ((size_t)h * S + j) * S;
const float *kh = k + (size_t)u * n_v_heads * S + (size_t)h * S;
const float *qh = q + (size_t)u * n_v_heads * S + (size_t)h * S;
const float decay = expf(g[(size_t)u * n_v_heads + h]);
const float beta_h = beta[(size_t)u * n_v_heads + h];
/* Decay in place (per-element, same as baseline -> bit-identical row_dec). */
const float ri = row[i] * decay;
const float ki = kh[i];
const float qi = qh[i];
/* ONE fused reduction: pred=sum ri*k, oq=sum ri*q, qk=sum k*q. */
float *p_pred = sdata;
float *p_oq = sdata + S;
float *p_qk = sdata + 2 * S;
p_pred[i] = ri * ki;
p_oq[i] = ri * qi;
p_qk[i] = ki * qi;
__syncthreads();
for (int stride = S / 2; stride > 0; stride >>= 1) {
if (i < stride) {
p_pred[i] += p_pred[i + stride];
p_oq[i] += p_oq[i + stride];
p_qk[i] += p_qk[i + stride];
}
__syncthreads();
}
const float pred = p_pred[0];
const float oq = p_oq[0];
const float qk = p_qk[0];
const float delta = (v[(size_t)u * n_v_heads * S + (size_t)h * S + j] - pred)
* beta_h;
/* rank-1 write (state carries forward, byte-layout identical). */
row[i] = ri + ki * delta;
/* T4 output: y[j] = scale*(oq + qk*delta) — no second reduction. */
if (i == 0)
y[(size_t)u * n_v_heads * S + (size_t)h * S + j] = scale * (oq + qk * delta);
}
/* Decay gate over n positions: g[t][h] = ssm_a[h] * softplus(alpha[t][h] +
* ssm_dt[h]). One thread per (t, h); n_heads gives the per-position head
* count for the broadcast vectors. total = n * n_heads. */
__global__ void fc_ssm_decay_gate_nx(const float *alpha, const float *ssm_dt,
const float *ssm_a, float *g,
int n_heads, int total) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= total)
return;
int h = idx % n_heads;
float v = alpha[idx] + ssm_dt[h];
float sp = (v > 20.0f) ? v : log1pf(expf(v));
g[idx] = ssm_a[h] * sp;
}
/* q/k L2-norm + GQA repeat over n positions (grid.y = position), with the v
* passthrough folded in (each block copies its val_dim / n_k_heads slice of
* v — a plain copy, no fp math, replacing the launcher's DtoD). */
__global__ void fc_ssm_qknorm_gqa_nx(const float *conv_out, float *qg,
float *kg, float *vg, int n_k_heads,
int kv_groups, int S, int key_dim,
int val_dim, float eps) {
int kh = blockIdx.x;
int t = blockIdx.y;
if (kh >= n_k_heads)
return;
int i = threadIdx.x;
extern __shared__ float sh[];
int conv_dim = 2 * key_dim + val_dim;
const float *q_src = conv_out + (size_t)t * conv_dim;
const float *k_src = q_src + key_dim;
const float *v_src = q_src + 2 * key_dim;
float *qg_t = qg + (size_t)t * val_dim;
float *kg_t = kg + (size_t)t * val_dim;
float *vg_t = vg + (size_t)t * val_dim;
float qv = q_src[(size_t)kh * S + i];
float kv = k_src[(size_t)kh * S + i];
sh[i] = qv * qv;
sh[blockDim.x + i] = kv * kv;
__syncthreads();
for (int stride = blockDim.x >> 1; stride > 0; stride >>= 1) {
if (i < stride) {
sh[i] += sh[i + stride];
sh[blockDim.x + i] += sh[blockDim.x + i + stride];
}
__syncthreads();
}
float inv_q = 1.0f / fmaxf(sqrtf(sh[0]), eps);
float inv_k = 1.0f / fmaxf(sqrtf(sh[blockDim.x]), eps);
float qn = qv * inv_q;
float kn = kv * inv_k;
for (int r = 0; r < kv_groups; r++) {
int vh = r * n_k_heads + kh;
qg_t[(size_t)vh * S + i] = qn;
kg_t[(size_t)vh * S + i] = kn;
}
/* v passthrough: this block copies slice kh of val_dim. */
int slice = val_dim / n_k_heads;
for (int e = i; e < slice; e += blockDim.x)
vg_t[(size_t)kh * slice + e] = v_src[(size_t)kh * slice + e];
}
} /* extern "C" */