/** * ssd.ts – Structured State Space Duality (SSD) kernels for Mamba-2. * * Implements a chunked SSD algorithm: * A_bar_t = exp(-softplus(A_h) · softplus(dt_t + dt_bias_h)) [scalar per head] * h_t = A_bar_t · h_{t-1} + B_t · x_t [MIMO per head] * y_t = C_t · h_t * * The sequence is split into chunks of `chunk_len` time steps. * Within each chunk the recurrence is run sequentially; the carry-over * state `h` is passed forward between chunks via the state_carry buffer. * * Dispatch for ssd_chunk_forward: (num_chunks, H, B) * Dispatch for ssd_chunk_backward: (num_chunks, H, B) * * Buffer layout (all f32, row-major): * x : [B, L, D_inner] where D_inner = H * d_head * B_proj : [B, L, n_groups, N] * C_proj : [B, L, n_groups, N] * dt : [B, L, H] * A_log : [H] log(-A), positive scalar per head * dt_bias : [H] * D_vec : [H] skip connection per head * out : [B, L, D_inner] scan output (written by kernel) * state_carry : [num_chunks+1, B, H, N, d_head] inter-chunk states */ export const SSD_FORWARD_WGSL: string = /* wgsl */` struct SsdParams { seq_len : u32, d_inner : u32, n_heads : u32, d_head : u32, // d_inner / n_heads n_groups : u32, d_state : u32, // N chunk_len : u32, n_chunks : u32, batch : u32, }; @group(0) @binding(0) var params : SsdParams; @group(0) @binding(1) var x_in : array; // [B,L,D_inner] @group(0) @binding(2) var B_proj : array; // [B,L,n_groups,N] @group(0) @binding(3) var C_proj : array; // [B,L,n_groups,N] @group(0) @binding(4) var dt_in : array; // [B,L,H] @group(0) @binding(5) var A_log : array; // [H] @group(0) @binding(6) var dt_bias : array; // [H] @group(0) @binding(7) var D_vec : array; // [H] @group(0) @binding(8) var out_buf : array; // [B,L,D_inner] @group(0) @binding(9) var state_carry : array; // [n_chunks+1,B,H,N,d_head] fn softplus(x: f32) -> f32 { // Numerically stable: max(x,0) + log1p(exp(-|x|)). The naive log(1+exp(x)) // overflows to +Inf for x ≳ 88 (f32 exp range) — and a downstream // exp(-Inf*0) then yields NaN, poisoning the state. This form never overflows. return max(x, 0.0) + log(1.0 + exp(-abs(x))); } // Workgroup: one chunk × one head × one batch item @compute @workgroup_size(1, 1, 1) fn ssd_chunk_forward(@builtin(global_invocation_id) gid: vec3) { let chunk_id = gid.x; let head_id = gid.y; let batch_id = gid.z; let L = params.seq_len; let D = params.d_inner; let H = params.n_heads; let dh = params.d_head; let G = params.n_groups; let N = params.d_state; let CL = params.chunk_len; let NC = params.n_chunks; let B = params.batch; let t_start = chunk_id * CL; let t_end = min(t_start + CL, L); // Group index: heads are partitioned across groups let group_id = head_id * G / H; // A scalar for this head let neg_A = softplus(A_log[head_id]); // A_log stores log(-A) positive let db = dt_bias[head_id]; let d_skip = D_vec[head_id]; // Load carry-in state: h[N, dh] (stored flat as N*dh floats) // state_carry layout: [NC+1, B, H, N*dh] let state_stride_chunk = B * H * N * dh; let state_base_in = chunk_id * state_stride_chunk + batch_id * H * N * dh + head_id * N * dh; // We maintain h as a local array (N * dh floats). // WebGPU WGSL does not support variable-length arrays in function scope, // so we use a fixed maximum. Max N*dh = 64*64 = 4096. Here we use dynamic // indexing into state_carry which is shared storage. // Write carry-in into temporary positions — use state_carry directly for // the running state (overwrite in-place from carry-in slot). // Copy carry-in to working slot (chunk_id+1 slot, updated each step). let state_base_out = (chunk_id + 1u) * state_stride_chunk + batch_id * H * N * dh + head_id * N * dh; // Initialise working state from carry-in for (var s: u32 = 0u; s < N * dh; s = s + 1u) { state_carry[state_base_out + s] = state_carry[state_base_in + s]; } // Sequential scan over the chunk for (var t: u32 = t_start; t < t_end; t = t + 1u) { // dt scalar for this head at time t let dt_idx = batch_id * L * H + t * H + head_id; let dt_val = softplus(dt_in[dt_idx] + db); // A_bar = exp(-neg_A * dt_val) let a_bar = exp(-neg_A * dt_val); // Head slice of x: x[batch, t, head*dh .. (head+1)*dh] let x_base = batch_id * L * D + t * D + head_id * dh; // B at this time step: B_proj[batch, t, group_id, *] shape [N] let b_base = batch_id * L * G * N + t * G * N + group_id * N; // C at this time step: C_proj[batch, t, group_id, *] shape [N] let c_base = batch_id * L * G * N + t * G * N + group_id * N; // y accumulator for this head at time t var y_acc: f32 = 0.0; for (var n: u32 = 0u; n < N; n = n + 1u) { let b_val = B_proj[b_base + n]; let c_val = C_proj[c_base + n]; for (var i: u32 = 0u; i < dh; i = i + 1u) { let s_idx = state_base_out + n * dh + i; let x_val = x_in[x_base + i]; // h_t = A_bar * h_{t-1} + B * x let h_new = a_bar * state_carry[s_idx] + b_val * x_val; state_carry[s_idx] = h_new; // y += C * h (summed over n dimension per output channel i) y_acc = y_acc + c_val * h_new; } } // Write y + skip (D * x, averaged over dh for the skip scalar) // out[batch, t, head*dh .. (head+1)*dh] for (var i: u32 = 0u; i < dh; i = i + 1u) { let out_idx = batch_id * L * D + t * D + head_id * dh + i; let x_val = x_in[x_base + i]; out_buf[out_idx] = y_acc + d_skip * x_val; } } } `; // ── Backward ────────────────────────────────────────────────────────────────── export const SSD_BACKWARD_WGSL: string = /* wgsl */` struct SsdParams { seq_len : u32, d_inner : u32, n_heads : u32, d_head : u32, n_groups : u32, d_state : u32, chunk_len : u32, n_chunks : u32, batch : u32, }; @group(0) @binding(0) var params : SsdParams; @group(0) @binding(1) var x_in : array; @group(0) @binding(2) var B_proj : array; @group(0) @binding(3) var C_proj : array; @group(0) @binding(4) var dt_in : array; @group(0) @binding(5) var A_log : array; @group(0) @binding(6) var dt_bias : array; @group(0) @binding(7) var state_carry : array; // forward states @group(0) @binding(8) var dy : array; // upstream grad @group(0) @binding(9) var dx : array; @group(0) @binding(10) var dB : array; @group(0) @binding(11) var dC : array; @group(0) @binding(12) var ddt : array; @group(0) @binding(13) var dA_log : array; @group(0) @binding(14) var dD_vec : array; fn softplus(x: f32) -> f32 { // Numerically stable: max(x,0) + log1p(exp(-|x|)). The naive log(1+exp(x)) // overflows to +Inf for x ≳ 88 (f32 exp range) — and a downstream // exp(-Inf*0) then yields NaN, poisoning the state. This form never overflows. return max(x, 0.0) + log(1.0 + exp(-abs(x))); } fn d_softplus(x: f32) -> f32 { return 1.0 / (1.0 + exp(-x)); } @compute @workgroup_size(1, 1, 1) fn ssd_chunk_backward(@builtin(global_invocation_id) gid: vec3) { let chunk_id = gid.x; let head_id = gid.y; let batch_id = gid.z; let L = params.seq_len; let D = params.d_inner; let H = params.n_heads; let dh = params.d_head; let G = params.n_groups; let N = params.d_state; let CL = params.chunk_len; let NC = params.n_chunks; let B = params.batch; let t_start = chunk_id * CL; let t_end = min(t_start + CL, L); let group_id = head_id * G / H; let neg_A = softplus(A_log[head_id]); let db = dt_bias[head_id]; let state_stride = B * H * N * dh; let state_base = chunk_id * state_stride + batch_id * H * N * dh + head_id * N * dh; // Backward: iterate time steps in reverse within the chunk // dh_next starts at zero (or propagated from future chunks — simplified here) for (var t_rev: u32 = 0u; t_rev < t_end - t_start; t_rev = t_rev + 1u) { let t = t_end - 1u - t_rev; let dt_idx = batch_id * L * H + t * H + head_id; let dt_raw = dt_in[dt_idx] + db; let dt_val = softplus(dt_raw); let a_bar = exp(-neg_A * dt_val); let x_base = batch_id * L * D + t * D + head_id * dh; let b_base = batch_id * L * G * N + t * G * N + group_id * N; let c_base = b_base; for (var i: u32 = 0u; i < dh; i = i + 1u) { let dy_val = dy[batch_id * L * D + t * D + head_id * dh + i]; let x_val = x_in[x_base + i]; // dD_vec dD_vec[head_id] = dD_vec[head_id] + dy_val * x_val; // dx from skip dx[x_base + i] = dx[x_base + i] + dy_val * /* D */ 1.0; for (var n: u32 = 0u; n < N; n = n + 1u) { let s_idx = state_base + n * dh + i; let h_val = state_carry[(chunk_id + 1u) * state_stride + batch_id * H * N * dh + head_id * N * dh + n * dh + i]; let c_val = C_proj[c_base + n]; let b_val = B_proj[b_base + n]; // dC += dy * h dC[b_base + n] = dC[b_base + n] + dy_val * h_val; // dh = C * dy let dh_val = c_val * dy_val; // dB += dh * x dB[b_base + n] = dB[b_base + n] + dh_val * x_val; // dx += dh * B dx[x_base + i] = dx[x_base + i] + dh_val * b_val; // ddt += dh * h_prev * (-neg_A) * d_softplus(dt_raw) let h_prev = state_carry[s_idx]; ddt[dt_idx] = ddt[dt_idx] + dh_val * h_prev * (-neg_A) * d_softplus(dt_raw); // dA_log += dh * h_prev * a_bar * (-dt_val) * d_softplus(A_log[head]) dA_log[head_id] = dA_log[head_id] + dh_val * h_prev * a_bar * (-dt_val) * d_softplus(A_log[head_id]); } } } } `;