burn_mamba/mamba3/single_ssd/ssd/minimal.rs
1//! # Single-Pass SSD (Minimal / segsum variant)
2//!
3//! This is the MIMO-first, single SSD pass implementation of the
4//! Mamba-3 trapezoid recurrence. It is the Burn analogue of the official
5//! Tilelang MIMO kernel and Triton SISO kernel; SISO is the `mimo_rank = 1`
6//! degenerate case.
7//!
8//! ## Background — the single-ssd recurrence
9//!
10//! The double-ssd trapezoid hidden state is
11//!
12//! ```text
13//! hₜ = αₜ hₜ₋₁ + βₜ (Bₜ₋₁ ⊗ xₜ₋₁) + γₜ (Bₜ ⊗ xₜ)
14//! ```
15//!
16//! Expanding the recurrence and grouping by `(Bₛ ⊗ xₛ)` gives the coefficient
17//! `(Πᵣ₌ₛ₊₁ᵗ αᵣ) · [γₛ + (1−λₛ₊₁)·Δₛ₊₁]` for the contribution of step `s` to
18//! state `t` (for `s < t`). At `s = t` the coefficient is just `γₜ`.
19//!
20//! Define `scaleₜ = γₜ + (1−λₜ₊₁)·Δₜ₊₁` (with `scaleₜ = γₜ` at the last
21//! position). The single-SSD
22//!
23//! ```text
24//! h'ₜ = αₜ h'ₜ₋₁ + scaleₜ (Bₜ ⊗ xₜ)
25//! ```
26//!
27//! produces the same outputs `yₜ = Cₜᵀ h'ₜ` as the double-ssd one **except**
28//! at the same-step diagonal (`s = t`), where the single-ssd form has `scaleₜ`
29//! instead of `γₜ`. We compensate by:
30//!
31//! 1. Using a **strict** lower-triangular mask in the intra-chunk path (the
32//! `s = t` block is excluded from the trapezoid sum).
33//! 2. Adding a separate γ-weighted same-step term `γₜ · (Cₜᵀ Bₜ) · xₜ`.
34//!
35//! ## Algorithm (per chunk, MIMO-first)
36//!
37//! ```text
38//! K_scaled[t, m, h, n] = scaleₜ · B[t, m, h, n] // K scaled inside the SSD
39//!
40//! y_lower = (C ⊗ K_scaledᵀ ⊙ L_strict) · PsiV // strict lower-tri
41//! y_diag = γₜ · (C ⊗ Bᵀ at same step) · PsiV // diagonal correction
42//! y_off = C · h'_chunk_in · exp(da_cs) // state-to-output
43//!
44//! y = y_lower + y_diag + y_off
45//!
46//! h'_chunk_out = exp(da_cs_last) · h'_chunk_in
47//! + K_scaled · exp(da_cs_rev)ᵀ · PsiV // standard state update
48//! ```
49//!
50//! The MIMO causal mask is identical to [`crate::mamba3::double_ssd::ssd::minimal`] but
51//! with a stricter inequality (`i_time > j_time` rather than `i_time ≥ j_time`).
52//!
53//! Reference implementations:
54//! - SISO: `refs/state-spaces/mamba/mamba_ssm/ops/triton/mamba3/mamba3_siso_fwd.py`
55//! - MIMO: `refs/state-spaces/mamba/mamba_ssm/ops/tilelang/mamba3/mamba3_mimo_fwd.py`
56
57use crate::mamba3::single_ssd::prelude::*;
58use crate::mamba3::single_ssd::ssd::diag::y_diag_correction;
59use burn_stack::modules::segsum;
60use burn::prelude::*;
61
62impl Mamba3SingleSsdInput {
63 /// MIMO-first single-SSD — segsum variant.
64 ///
65 /// See module documentation for the algorithm. Returns the chunked outputs
66 /// and the final single-ssd accumulator.
67 ///
68 /// # Shapes
69 /// - input: see [`Mamba3SingleSsdInput`]
70 /// - output `(y_bnlmhp, final_state_bhpr)`:
71 /// - `y_bnlmhp`: `[batch, nchunks, chunk_len, mimo_rank, nheads, per_head_dim]`
72 /// - `final_state_bhpr`: `[batch, nheads, per_head_dim, state_rank]`
73 #[allow(non_snake_case)]
74 pub fn single_ssd_minimal(self) -> (Tensor<6>, Tensor<4>) {
75 let input = self;
76 input.sanity();
77 let [batch, nchunks, chunk_len, mimo_rank, nheads, per_head_dim] = input.v_bnlmhp.dims();
78 let [.., state_rank] = input.b_bnlmhr.dims();
79 let device = &input.v_bnlmhp.device();
80
81 assert!(nchunks >= 1, "sequence must be non-empty");
82 assert!(chunk_len > 0, "chunk_len must be positive");
83 assert_eq!(
84 [batch, nchunks, chunk_len, nheads],
85 input.gamma_bnlh.dims(),
86 "gamma must align with da"
87 );
88 assert_eq!(
89 [batch, nchunks, chunk_len, nheads],
90 input.scale_bnlh.dims(),
91 "scale must align with da"
92 );
93
94 // ── Fuse mimo_rank into chunk_len (matches `ssd_minimal`) ─────────────
95 let c_bnLMhr = input.c_bnlmhr.clone().reshape([
96 batch,
97 nchunks,
98 chunk_len * mimo_rank,
99 nheads,
100 state_rank,
101 ]);
102 let v_bnLMhp = input.v_bnlmhp.clone().reshape([
103 batch,
104 nchunks,
105 chunk_len * mimo_rank,
106 nheads,
107 per_head_dim,
108 ]);
109
110 // Per-time-step cumulative log-decay (used for L_strict, decay_states, y_off)
111 let a_bhnl = input.da_bnlh.permute([0, 3, 1, 2]);
112 let a_cumsum_bhnl = a_bhnl.clone().cumsum(3);
113
114 // K scaled for lower-triangular and state recurrence paths
115 // (the diagonal correction reuses the unscaled `b_bnlmhr`).
116 // scale_bnlh broadcast over (mimo_rank, state_rank):
117 let scale_bnlh11 = input
118 .scale_bnlh
119 .clone()
120 .unsqueeze_dims::<6>(&[3, 5]) // scale_bnlh -> scale_bnl1h1
121 ;
122 let k_scaled_bnlmhr = input.b_bnlmhr.clone() * scale_bnlh11;
123 let k_scaled_bnLMhr =
124 k_scaled_bnlmhr.reshape([batch, nchunks, chunk_len * mimo_rank, nheads, state_rank]);
125
126 // =============================================================
127 // STEP 1a: Strict lower-triangular intra-chunk output (y_lower)
128 //
129 // y_lower[t1] = Σ_{t2 < t1} (C[t1] · K_scaled[t2]^T)
130 // · exp(cumA[t1] - cumA[t2]) · PsiV[t2]
131 // (block-diagonal in time t1 = t2 is excluded — handled by y_diag.)
132 // =============================================================
133 let y_lower_bnLMhp = {
134 let c_bnhLMr = c_bnLMhr.clone().swap_dims(2, 3);
135 let k_bnhrLM = k_scaled_bnLMhr.clone().permute([0, 1, 3, 4, 2]);
136 // [batch, nchunks, nheads, chunk_len*mimo_rank, chunk_len*mimo_rank]
137 let cb_bnhLMLM = c_bnhLMr.matmul(k_bnhrLM);
138
139 // L_strict_base[i, j] = exp(cumA[i] - cumA[j]) for i > j, else 0.
140 //
141 // Like `segsum` but with -inf on the diagonal as well (so exp = 0
142 // there). Replaces the existing `triu(1)` masking with `triu(0)`.
143 let l_strict_base_bhnll = {
144 let x_cumsum = a_bhnl.clone().cumsum(3);
145 let row: Tensor<5> = x_cumsum.clone().unsqueeze_dim(4); // [..., l, 1]
146 let col: Tensor<5> = x_cumsum.unsqueeze_dim(3); // [..., 1, l]
147 let diff = row - col; // [..., l, l]
148 let neg_inf_strict = Tensor::full_like(&diff, f32::NEG_INFINITY).triu(0);
149 (diff + neg_inf_strict).exp()
150 };
151
152 // Interleave-expand to fused length (L_strict[i,j] = L_strict_base[i//m, j//m]):
153 let l_strict_bhnLMLM = l_strict_base_bhnll
154 .unsqueeze_dim::<6>(4)
155 .expand([batch, nheads, nchunks, chunk_len, mimo_rank, chunk_len])
156 .reshape([batch, nheads, nchunks, chunk_len * mimo_rank, chunk_len])
157 .unsqueeze_dim::<6>(5)
158 .expand([
159 batch,
160 nheads,
161 nchunks,
162 chunk_len * mimo_rank,
163 chunk_len,
164 mimo_rank,
165 ])
166 .reshape([
167 batch,
168 nheads,
169 nchunks,
170 chunk_len * mimo_rank,
171 chunk_len * mimo_rank,
172 ]);
173
174 // (CB ⊙ L_strict) · V (back in MIMO-fused layout)
175 let cb_bnLMhLM = cb_bnhLMLM.swap_dims(2, 3);
176 let l_bnLMhLM = l_strict_bhnLMLM.permute([0, 2, 3, 1, 4]);
177 let masked_cb_bnhLMLM = (cb_bnLMhLM * l_bnLMhLM).swap_dims(2, 3);
178
179 let v_bnhLMp = v_bnLMhp.clone().swap_dims(2, 3);
180 let y_lower_bnhLMp = masked_cb_bnhLMLM.matmul(v_bnhLMp);
181
182 y_lower_bnhLMp.swap_dims(2, 3) // y_lower_bnLMhp
183 };
184
185 // =============================================================
186 // STEP 1b: γ-weighted same-step diagonal correction (y_diag)
187 //
188 // y_diag[t, m_out, h, p] = γₜ · Σ_{m_in} (C[t, m_out, h, ·] · B[t, m_in, h, ·]) · PsiV[t, m_in, h, p]
189 // =============================================================
190 let y_diag_bnlmhp = y_diag_correction(
191 input.v_bnlmhp,
192 input.b_bnlmhr,
193 input.c_bnlmhr,
194 input.gamma_bnlh,
195 input.siso_specialization,
196 );
197 // Reshape to fused layout for combination with y_lower / y_off.
198 let y_diag_bnLMhp =
199 y_diag_bnlmhp.reshape([batch, nchunks, chunk_len * mimo_rank, nheads, per_head_dim]);
200
201 // =============================================================
202 // STEP 2: Per-chunk single-ssd state (standard SSD with K_scaled)
203 //
204 // s[n] = Σ_{t,m} exp(cumA[n,-1] - cumA[n,t]) · V[n,t*M+m] · K_scaled[n,t*M+m]^T
205 // =============================================================
206 let state_bnhpr = {
207 let a_cumsum_last_bhn1 = a_cumsum_bhnl.clone().slice(s![.., .., .., -1]);
208 let a_cumsum_bhnLM = a_cumsum_bhnl
209 .clone()
210 .unsqueeze_dim::<5>(4)
211 .expand([batch, nheads, nchunks, chunk_len, mimo_rank])
212 .reshape([batch, nheads, nchunks, chunk_len * mimo_rank]);
213 let decay_bhnLM = (a_cumsum_last_bhn1 - a_cumsum_bhnLM).exp();
214
215 let decay_bnLMh1 = decay_bhnLM.permute([0, 2, 3, 1]).unsqueeze_dim(4);
216 let decayed_v_bnLMhp = decay_bnLMh1 * v_bnLMhp.clone();
217
218 let decayed_v_bnhpLM = decayed_v_bnLMhp.permute([0, 1, 3, 4, 2]);
219 let k_scaled_bnhLMr = k_scaled_bnLMhr.swap_dims(2, 3);
220 decayed_v_bnhpLM.matmul(k_scaled_bnhLMr) // state_bnhpr
221 };
222
223 // =============================================================
224 // STEP 3: Inter-chunk state scan (segsum-based state passing)
225 //
226 // h'[n] = Ā_chunk[n] · h'[n-1] + s[n]
227 // =============================================================
228 let (state_bnhpr, final_state_bhpr) = {
229 let initial_state_b1hpr = input.initial_state_bhpr.unsqueeze_dim(1);
230 let initial_state_b1hpr = if let Some(init_hpr) = input.init_state_hpr {
231 let init_b1hpr = init_hpr.unsqueeze_dim::<4>(0).expand([
232 batch,
233 1,
234 nheads,
235 per_head_dim,
236 state_rank,
237 ]);
238 initial_state_b1hpr + init_b1hpr
239 } else {
240 initial_state_b1hpr
241 };
242
243 let state_bNhpr = Tensor::cat(vec![initial_state_b1hpr, state_bnhpr], 1);
244
245 let a_cumsum_last_bhn: Tensor<3> = a_cumsum_bhnl
246 .clone()
247 .slice(s![.., .., .., -1])
248 .squeeze_dim(3);
249 let a_chunk_pad_bhN = Tensor::cat(
250 vec![Tensor::zeros([batch, nheads, 1], device), a_cumsum_last_bhn],
251 2,
252 );
253 let decay_chunk_bhNN = segsum::<3, 4>(a_chunk_pad_bhN).exp();
254
255 let flat = per_head_dim * state_rank;
256 let state_bhNPR =
257 state_bNhpr
258 .clone()
259 .swap_dims(1, 2)
260 .reshape([batch, nheads, 1 + nchunks, flat]);
261
262 let new_state_bhNPR = decay_chunk_bhNN.matmul(state_bhNPR);
263 let new_state_bhNpr =
264 new_state_bhNPR.reshape([batch, nheads, 1 + nchunks, per_head_dim, state_rank]);
265
266 let new_state_bnhpr = new_state_bhNpr
267 .clone()
268 .slice(s![.., .., 0..nchunks, .., ..])
269 .swap_dims(1, 2);
270 let last_state_bhpr: Tensor<4> = new_state_bhNpr
271 .slice(s![.., .., nchunks, .., ..])
272 .squeeze_dim(2);
273
274 (new_state_bnhpr, last_state_bhpr)
275 };
276
277 // =============================================================
278 // STEP 4: State-to-output (y_off)
279 //
280 // y_off[n, t*M+m] = C[t*M+m]^T · exp(cumA[t]) · h'[n-1]
281 // =============================================================
282 let y_off_bnLMhp = {
283 let state_decay_bhnLM = a_cumsum_bhnl
284 .unsqueeze_dim::<5>(4)
285 .expand([batch, nheads, nchunks, chunk_len, mimo_rank])
286 .reshape([batch, nheads, nchunks, chunk_len * mimo_rank])
287 .exp();
288
289 let c_bnhLMr = c_bnLMhr.swap_dims(2, 3);
290 let state_bnhrp = state_bnhpr.transpose();
291 let ch_bnhLMp = c_bnhLMr.matmul(state_bnhrp);
292
293 let decay_bnhLM1 = state_decay_bhnLM.swap_dims(1, 2).unsqueeze_dim(4);
294 let y_off_bnhLMp = ch_bnhLMp * decay_bnhLM1;
295 y_off_bnhLMp.swap_dims(2, 3)
296 };
297
298 // ── Combine and reshape ───────────────────────────────────────────────
299 let y_bnLMhp = y_lower_bnLMhp + y_diag_bnLMhp + y_off_bnLMhp;
300 let y_bnlmhp =
301 y_bnLMhp.reshape([batch, nchunks, chunk_len, mimo_rank, nheads, per_head_dim]);
302
303 (y_bnlmhp, final_state_bhpr)
304 }
305}