Skip to main content

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}