burn_mamba/mamba3/single_ssd/ssd/serial.rs
1//! # SingleSsd Serial (K1–K5) SSD
2//!
3//! Chunk-serial counterpart to [`crate::mamba3::single_ssd::ssd::minimal`].
4//! Whereas the Minimal variant uses a segsum-based quadratic state passing,
5//! this one reuses the K1–K4 helpers from [`crate::mamba3::double_ssd::ssd::serial`]
6//! (which run a sequential loop for K4) and supplies a **new K5** that bakes
7//! in the single-ssd logic:
8//!
9//! - Strict lower-triangular intra-chunk path (the same-time-step block is
10//! excluded from the SSM sum; it is the “diagonal correction” territory).
11//! - K is scaled by `scaleₜ = γₜ + (1−λₜ₊₁) Δₜ₊₁` per source-time column.
12//! - Same-time-step block contributes via an explicit `γₜ · (C·Bᵀ at t) · Vₜ`
13//! correction term, restoring the right diagonal weighting.
14//!
15//! K1–K4 are identical to the double-SSD because:
16//! - K1 (`da_cumsum`, `da_chunk_end`) depends only on `da = Δ·A`.
17//! - K2 (`cb = C · Bᵀ`) is computed on **unscaled** B / C; the single-ssd
18//! algorithm wants the unscaled CB so it can apply `scaleₜ` per-column
19//! (lower triangular) and reuse the same-step block for the γ-correction.
20//! - K3 (chunk-end state from V·decay·K) is form-invariant: passing the
21//! scale-multiplied K (`K_scaled = scaleₜ · B`) recovers the single-ssd
22//! chunk state, with no other changes needed.
23//! - K4 (sequential state passing across chunks) operates on a `[H, P, R]`
24//! per-chunk state and a per-chunk decay total; both are mode-agnostic.
25//!
26//! Reference kernels (same as `single_ssd_minimal`):
27//! - `refs/state-spaces/mamba/mamba_ssm/ops/triton/mamba3/mamba3_siso_fwd.py`
28//! - `refs/state-spaces/mamba/mamba_ssm/ops/tilelang/mamba3/mamba3_mimo_fwd.py`
29
30#![allow(non_snake_case)]
31
32pub use crate::mamba3::double_ssd::ssd::serial::{
33 k1_ssd_chunk_cumsum, k2_ssd_bmm, k3_ssd_chunk_state, k4_ssd_state_passing,
34};
35use crate::mamba3::single_ssd::prelude::*;
36use crate::mamba3::single_ssd::ssd::diag::y_diag_correction;
37use burn::prelude::*;
38
39impl Mamba3SingleSsdInput {
40 /// MIMO-first Single-SSD — chunk-serial (K1–K5) variant.
41 ///
42 /// Sequence of kernels (matches the double-ssd `ssd_serial`):
43 /// 1. **K1**: intra-chunk cumulative log-decay and per-chunk decay totals.
44 /// 2. **K2**: `cb = C · Bᵀ` block matrix (unscaled).
45 /// 3. **K3**: per-chunk hidden state assuming zero initial state, fed
46 /// `K_scaled = scaleₜ · B`.
47 /// 4. **K4**: sequential state passing across chunks (loop over chunks).
48 /// 5. **K5** (this module's new function): single-ssd chunk scan with
49 /// strict lower-triangular masking, scale broadcasting, and the
50 /// `γₜ`-weighted same-step diagonal correction.
51 ///
52 /// # Returns
53 /// - `y_bnlmhp`: `[batch, nchunks, chunk_len, mimo_rank, nheads, per_head_dim]`
54 /// - `final_state_bhpr`: `[batch, nheads, per_head_dim, state_rank]` —
55 /// the single-ssd accumulator at the last token.
56 pub fn single_ssd_serial(self) -> (Tensor<6>, Tensor<4>) {
57 let input = self;
58 input.sanity();
59 let [batch, nchunks, chunk_len, _mimo_rank, nheads, per_head_dim] = input.v_bnlmhp.dims();
60 let [.., state_rank] = input.b_bnlmhr.dims();
61
62 assert!(
63 input.init_state_hpr.is_none(),
64 "init_state_hpr is not yet supported in single_ssd_serial; use single_ssd_minimal instead"
65 );
66 assert!(nchunks > 0, "sequence length must be at least 1");
67 assert_eq!(
68 [batch, nchunks, chunk_len, nheads],
69 input.gamma_bnlh.dims(),
70 "gamma must align with da"
71 );
72 assert_eq!(
73 [batch, nchunks, chunk_len, nheads],
74 input.scale_bnlh.dims(),
75 "scale must align with da"
76 );
77
78 // ── K1: chunk cumulative decay ────────────────────────────────────────
79 let (da_cumsum_bhnl, da_chunk_end_bhn) = k1_ssd_chunk_cumsum(input.da_bnlh.clone());
80
81 // ── K2: CB matrix on unscaled B/C ─────────────────────────────────────
82 // SingleSsd K5 applies the `scale` and `gamma` weights post-hoc, so K2 is
83 // identical to the double-ssd K2.
84 let cb_bnhLMLM: Tensor<5> = k2_ssd_bmm(input.c_bnlmhr.clone(), input.b_bnlmhr.clone());
85
86 // ── K3: chunk state using K_scaled = scaleₜ · B ───────────────────────
87 // The existing K3 computes `state = (V * decay)^T @ B_input`, so passing
88 // `B_input = K_scaled` recovers the single-ssd per-chunk state.
89 let scale_bnlh11 = input.scale_bnlh.clone().unsqueeze_dims::<6>(&[3, 5]);
90 let k_scaled_bnlmhr = input.b_bnlmhr.clone() * scale_bnlh11;
91 let intra_chunk_state_bnhpr: Tensor<5> = k3_ssd_chunk_state(
92 input.v_bnlmhp.clone(),
93 k_scaled_bnlmhr,
94 da_cumsum_bhnl.clone(),
95 );
96
97 // ── K4: sequential state passing across chunks ────────────────────────
98 let (chunk_input_state_bnhpr, final_state_bhpr): (Tensor<5>, Tensor<4>) =
99 k4_ssd_state_passing(
100 intra_chunk_state_bnhpr,
101 da_chunk_end_bhn,
102 input.initial_state_bhpr,
103 );
104 assert_eq!(
105 [batch, nchunks, nheads, per_head_dim, state_rank],
106 chunk_input_state_bnhpr.dims()
107 );
108
109 // ── K5: single-ssd chunk scan (strict-lower + diag γ-correction + Y_off)
110 let y_bnlmhp = k5_single_ssd_chunk_scan(
111 da_cumsum_bhnl,
112 input.v_bnlmhp,
113 input.c_bnlmhr,
114 input.b_bnlmhr,
115 cb_bnhLMLM,
116 input.gamma_bnlh,
117 input.scale_bnlh,
118 chunk_input_state_bnhpr,
119 input.siso_specialization,
120 );
121
122 (y_bnlmhp, final_state_bhpr)
123 }
124}
125
126// ---------------------------------------------------------------------------
127// K5 (single-ssd) — strict-lower intra-chunk + γ-correction + state-to-output
128// ---------------------------------------------------------------------------
129
130/// SingleSsd chunk scan.
131///
132/// Computes the per-chunk output from three contributions:
133/// - **Strict lower triangular intra-chunk** (`t1 > t2`):
134/// `(cb[i,j] · scale[t2] · exp(cumA[t1] − cumA[t2])) · V[t2]`
135/// - **Same-time-step (`t1 == t2`) γ-correction**:
136/// `γ[t] · (Σₙ C[t,r_out,n] · B[t,r_in,n]) · V[t,r_in,p]`
137/// - **State-to-output (Y_off)** — same formula as the double-ssd K5:
138/// `exp(cumA[t]) · C[t] · h'[n-1]`
139///
140/// `cb_bnhLMLM` is the unscaled `C · Bᵀ` matrix from K2; `b_bnlmhr` is the
141/// unscaled K/B tensor (used for the γ-correction matmul). The strict-lower
142/// MIMO mask excludes the same-step `R × R` block, leaving only `t1 > t2`
143/// contributions in the masked CB.
144///
145/// # Shapes
146/// - `da_cumsum_bhnl`: `[B, H, N, L]` (base time grid, not fused)
147/// - `v_bnlmhp`: `[B, N, L, M, H, P]`
148/// - `c_bnlmhr`, `b_bnlmhr`: `[B, N, L, M, H, R]`
149/// - `cb_bnhLMLM`: `[B, N, H, L·M, L·M]` (output of K2)
150/// - `gamma_bnlh`, `scale_bnlh`: `[B, N, L, H]`
151/// - `chunk_input_state_bnhpr`: `[B, N, H, P, R]` (h' at chunk start)
152///
153/// # Returns
154/// - `y_bnlmhp`: `[B, N, L, M, H, P]`
155#[allow(clippy::too_many_arguments)]
156pub fn k5_single_ssd_chunk_scan(
157 da_cumsum_bhnl: Tensor<4>,
158 v_bnlmhp: Tensor<6>,
159 c_bnlmhr: Tensor<6>,
160 b_bnlmhr: Tensor<6>,
161 cb_bnhLMLM: Tensor<5>,
162 gamma_bnlh: Tensor<4>,
163 scale_bnlh: Tensor<4>,
164 chunk_input_state_bnhpr: Tensor<5>,
165 siso_specialization: bool,
166) -> Tensor<6> {
167 let [batch, nchunks, chunk_len, mimo_rank, nheads, per_head_dim] = v_bnlmhp.dims();
168 let [.., state_rank] = c_bnlmhr.dims();
169 let device = v_bnlmhp.device();
170 let fused = chunk_len * mimo_rank;
171
172 // Fuse mimo_rank into chunk_len for the SSM-style matmul.
173 let v_bnLMhp = v_bnlmhp
174 .clone()
175 .reshape([batch, nchunks, fused, nheads, per_head_dim]);
176 let c_bnLMhr = c_bnlmhr
177 .clone()
178 .reshape([batch, nchunks, fused, nheads, state_rank]);
179
180 // Per-fused-step cumulative decay (interleave-expand the base grid).
181 let da_cumsum_bhnLM = da_cumsum_bhnl
182 .unsqueeze_dim::<5>(4)
183 .expand([batch, nheads, nchunks, chunk_len, mimo_rank])
184 .reshape([batch, nheads, nchunks, fused]);
185
186 // ── Y_off: exp(cumA[t]) · C[t] · h'[n-1] (same form as double-ssd K5) ──
187 let exp_da_bnhLMp = da_cumsum_bhnLM
188 .clone()
189 .exp()
190 .swap_dims(1, 2) // bnhLM
191 .unsqueeze_dim::<5>(4) // bnhLM1
192 .expand([batch, nchunks, nheads, fused, per_head_dim]);
193
194 let c_bnhLMr = c_bnLMhr.swap_dims(2, 3);
195 let chunk_input_state_bnhrp = chunk_input_state_bnhpr.transpose();
196 let ch_bnhLMp = c_bnhLMr.matmul(chunk_input_state_bnhrp);
197 let y_off_bnhLMp = ch_bnhLMp * exp_da_bnhLMp;
198
199 // ── Y_lower: strict lower-tri intra-chunk with scale and decay ────────
200 //
201 // Mask `cb` to keep only `t1 > t2`, multiply by `exp(cumA[t1] - cumA[t2])`
202 // and by `scale[t2]` along the source axis, then matmul with V.
203 let da_cumsum_bnhLM = da_cumsum_bhnLM.swap_dims(1, 2); // bnhLM
204 let target_da_cumsum_bnhLMLM = da_cumsum_bnhLM
205 .clone()
206 .unsqueeze_dim::<5>(4) // bnhLM1
207 .expand([batch, nchunks, nheads, fused, fused]);
208 let source_da_cumsum_bnhLMLM = da_cumsum_bnhLM
209 .unsqueeze_dim::<5>(3) // bnh1LM
210 .expand([batch, nchunks, nheads, fused, fused]);
211 let diff_bnhLMLM = target_da_cumsum_bnhLMLM - source_da_cumsum_bnhLMLM;
212
213 // Strict-upper -inf mask on the base time grid (`t1 <= t2` → -inf),
214 // then interleave-expand to fused length so that MIMO same-time blocks
215 // are zeroed out.
216 let inf_upper_ll =
217 Tensor::<2>::full([chunk_len, chunk_len], f32::NEG_INFINITY, &device).triu(0); // upper triangle INCLUDING diagonal
218 let inf_upper_bnhll = inf_upper_ll
219 .unsqueeze_dims::<5>(&[0, 1, 2])
220 .expand([batch, nchunks, nheads, chunk_len, chunk_len]);
221 let inf_upper_bnhLMLM = inf_upper_bnhll
222 .unsqueeze_dim::<6>(4)
223 .expand([batch, nchunks, nheads, chunk_len, mimo_rank, chunk_len])
224 .reshape([batch, nchunks, nheads, fused, chunk_len])
225 .unsqueeze_dim::<6>(5)
226 .expand([batch, nchunks, nheads, fused, chunk_len, mimo_rank])
227 .reshape([batch, nchunks, nheads, fused, fused]);
228 let decay_strict_bnhLMLM = (diff_bnhLMLM + inf_upper_bnhLMLM).exp();
229
230 // Per-column scale: `scale[t2]` lives on the source axis (column).
231 let scale_bnhLM = scale_bnlh
232 .transpose() // bnhl
233 .unsqueeze_dim::<5>(4) // bnhl1
234 .expand([batch, nchunks, nheads, chunk_len, mimo_rank])
235 .reshape([batch, nchunks, nheads, fused]);
236 let scale_col_bnhLMLM = scale_bnhLM
237 .unsqueeze_dim::<5>(3) // bnh1LM
238 .expand([batch, nchunks, nheads, fused, fused]);
239
240 let kernel_bnhLMLM = decay_strict_bnhLMLM * scale_col_bnhLMLM;
241 let masked_cb_bnhLMLM = cb_bnhLMLM * kernel_bnhLMLM;
242
243 let v_bnhLMp = v_bnLMhp.swap_dims(2, 3);
244 let y_lower_bnhLMp = masked_cb_bnhLMLM.matmul(v_bnhLMp);
245
246 // ── Y_diag: γ-weighted same-step correction ───────────────────────────
247 //
248 // y_diag[t, m_out, h, p] = γ[t] · Σ_{m_in} (Σ_n C[t,m_out,n] · B[t,m_in,n]) · V[t,m_in,p]
249 //
250 // Computed fresh (small same-step product) rather than extracting the
251 // block-diagonal from `cb_bnhLMLM` (which would require a fiddly reshape).
252 let y_diag_bnlmhp = y_diag_correction(
253 v_bnlmhp,
254 b_bnlmhr,
255 c_bnlmhr,
256 gamma_bnlh,
257 siso_specialization,
258 );
259
260 // Back to fused layout `[B, N, H, L·M, P]` to match y_lower / y_off.
261 let y_diag_bnLMhp = y_diag_bnlmhp.reshape([batch, nchunks, fused, nheads, per_head_dim]);
262 let y_diag_bnhLMp = y_diag_bnLMhp.swap_dims(2, 3);
263
264 // ── Combine and reshape ───────────────────────────────────────────────
265 let y_bnhLMp = y_off_bnhLMp + y_lower_bnhLMp + y_diag_bnhLMp;
266 let y_bnLMhp = y_bnhLMp.swap_dims(2, 3);
267 y_bnLMhp.reshape([batch, nchunks, chunk_len, mimo_rank, nheads, per_head_dim])
268}