Skip to main content

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}