Skip to main content

burn_mamba/mamba2/ssd/
minimal.rs

1//! ## The Chunkwise SSD Algorithm
2//!
3//! During training (and prefill), a naive sequential recurrence cannot
4//! exploit GPU tensor cores.  The **chunkwise SSD algorithm** (§4 of the
5//! paper) achieves this by splitting the sequence into chunks of length chunk_len
6//! and decomposing the computation into four steps:
7//!
8//! ```text
9//!   Step 1  (intra-chunk, quadratic form)   →  Y_diag
10//!   Step 2  (input → chunk state)           →  state_bnhpr
11//!   Step 3  (inter-chunk state scan)        →  state_bnhpr, final_state
12//!   Step 4  (chunk state → output)          →  Y_off
13//!
14//!   Y = Y_diag + Y_off
15//! ```
16//!
17//! Steps 1, 2, 4 are fully parallel across chunks and use batched matrix
18//! multiplications (exploiting tensor cores).  Step 3 is a short sequential
19//! scan over `sequence/chunk_len` elements rather than `sequence`.
20
21use crate::mamba2::prelude::*;
22use crate::modules::{sanity as san, segsum};
23use burn::prelude::*;
24
25impl Mamba2SsdInput {
26    // -----------------------------------------------------------------------
27    // chunked_selective_scan
28    // -----------------------------------------------------------------------
29
30    /// Minimal chunkwise SSD algorithm.
31    ///
32    /// Implements the four-step decomposition of the semiseparable matrix
33    /// multiplication described in §4 of the paper.  The sequence of length
34    /// is split into `nchunks = ⌈sequence/chunk_len⌉` chunks of length chunk_len.
35    ///
36    /// ## The four steps
37    ///
38    /// ### Step 1 — Intra-chunk outputs (Y_diag)
39    ///
40    /// Within each chunk, compute the output assuming the initial hidden state
41    /// is zero.  This is the *quadratic attention form* of the SSD layer
42    /// restricted to a window of chunk_len tokens (§4.1):
43    ///
44    /// ```text
45    ///   Y_diag[n] = (L[n] ∘ C[n] B[n]ᵀ) · X[n]
46    /// ```
47    ///
48    /// where `L[n]` is the chunk_len×chunk_len 1-semiseparable mask for chunk n.
49    /// This step is a batched GEMM (exploits tensor cores).
50    ///
51    /// ### Step 2 — Chunk state (state_bnhpr)
52    ///
53    /// Compute the final SSM state of each chunk assuming zero initial state
54    /// (§4.1, Eq. 20):
55    ///
56    /// ```text
57    ///   s[n] = Σ_{t ∈ chunk n}  exp(A_cum[end] - A_cum[t]) · B̄[t] · x[t]ᵀ
58    /// ```
59    ///
60    /// This is also a batched GEMM and is fully parallel across chunks.
61    ///
62    /// ### Step 3 — Inter-chunk state scan (state passing)
63    ///
64    /// Propagate the true hidden state across chunk boundaries using the
65    /// recurrence (§4.1, Eq. 22):
66    ///
67    /// ```text
68    ///   h[n] = Ā[n]_chunk · h[n-1] + s[n]
69    /// ```
70    ///
71    /// where `Ā[n]_chunk = exp(Σ_{t ∈ chunk n} Δₜ · A)` is the cumulative
72    /// decay over the whole chunk.  This step is implemented as a single
73    /// batched matrix multiplication using the 1-semiseparable structure of
74    /// the inter-chunk decay matrix (same `segsum` trick, now over chunks).
75    /// The scan has length `nchunks = sequence/chunk_len` rather than sequence, so its cost is
76    /// negligible for typical chunk sizes.
77    ///
78    /// ### Step 4 — State-to-output (Y_off)
79    ///
80    /// For each chunk n, compute the contribution of the true initial state
81    /// `h[n-1]` to the outputs within that chunk (§4.1, Eq. 23):
82    ///
83    /// ```text
84    ///   Y_off[n, t] = C[n, t]ᵀ · exp(A_cum[t]) · h[n-1]
85    /// ```
86    ///
87    /// This is again a batched GEMM.
88    ///
89    /// ### Final output (with D skip-connection)
90    ///
91    /// ```text
92    ///   Y = Y_diag + Y_off + D · X
93    /// ```
94    #[allow(non_snake_case)]
95    pub fn ssd_minimal(self) -> (Tensor<5>, Tensor<4>) {
96        let input = self;
97        let [batch, nchunks, chunk_len, nheads, per_head_dim] = input.x_bnlhp.dims();
98        let [.., state_rank] = input.b_bnlhr.dims();
99        let device = &input.x_bnlhp.device();
100
101        assert!(nchunks >= 1, "sequence must be non-empty");
102        assert!(chunk_len > 0, "chunk_len must be positive");
103
104        // ── Compute discretised parameters ────────────────────────────────────
105        // Ā = exp(Δ · A)   stored in log-space as  a_bnlh = Δ · A  (negative)
106        // B̄ = Δ · B        (Euler/ZOH approximation)
107
108        // B/C are already GQA-expanded to per-head.
109        let b_bnlhr = input.b_bnlhr.clone();
110        let c_bnlhr = input.c_bnlhr.clone();
111
112        // B̄ₜ = Δₜ · Bₜ
113        let delta_b_bnlhr = input.dt_bnlh.clone().unsqueeze_dim(4) * b_bnlhr.clone();
114        assert_eq!(
115            [batch, nchunks, chunk_len, nheads, state_rank],
116            delta_b_bnlhr.dims()
117        );
118        san(&delta_b_bnlhr);
119
120        // Ā in log-space: a_bnlh = Δₜ · A
121        let a_bnlh = input.dt_bnlh.clone()
122            * input
123                .a_decay_h
124                .clone()
125                .unsqueeze_dims::<4>(&[0, 1, 2]) // a_head_decay_111h
126                .expand([batch, nchunks, chunk_len, nheads]) // a_decay_bnlh
127            ;
128        san(&a_bnlh);
129
130        // ── Reshape ───────────────────────────────────────────────────────────
131        // a (log-decay)
132        let a_bhnl = a_bnlh.permute([0, 3, 1, 2]);
133        assert_eq!([batch, nheads, nchunks, chunk_len], a_bhnl.dims());
134
135        // Cumulative sum of log-decays within each chunk.
136        // a_cumsum_bhnl[b, h, n, t] = Σ_{k=0..t} Δ_{n,k} · A
137        // This is the log of the cumulative decay factor from the start of the
138        // chunk to position t (inclusive).
139        let a_cumsum_bhnl = a_bhnl.clone().cumsum(3);
140        assert_eq!([batch, nheads, nchunks, chunk_len], a_cumsum_bhnl.dims());
141        san(&a_cumsum_bhnl);
142
143        // =============================================================
144        // STEP 1: Intra-chunk outputs (diagonal blocks, Y_diag)
145        // =============================================================
146        //
147        // For each chunk n, compute Y_diag[n] = (L[n] ∘ C[n] B[n]ᵀ) · X[n]
148        // where L[n] ∈ ℝ^{chunk_len×chunk_len} is the 1-semiseparable mask for the chunk.
149        //
150        // L[n]_{i,j} = exp(Σ_{k=j+1..i} a_{n,k})  for i ≥ j
151        //            = exp(a_cumsum[n,i] - a_cumsum[n,j])   (using segsum trick)
152        //
153        // Implementation uses three batched matmuls:
154        //   (a) C[n] · B[n]ᵀ  (contract over state_rank state_rank)  → temp1
155        //   (b) temp1 ∘ L[n]                                 → temp2
156        //   (c) temp2 · X[n]  (contract over chunk_len)              → Y_diag
157        let y_diag_bnlhp = {
158            // Permute for the matmul along chunk_len and state_rank.
159            let b_bnhlr = delta_b_bnlhr.clone().permute([0, 1, 3, 2, 4]);
160            let c_bnhlr = c_bnlhr.clone().permute([0, 1, 3, 2, 4]);
161            assert_eq!(
162                [batch, nchunks, nheads, chunk_len, state_rank],
163                b_bnhlr.dims()
164            );
165            assert_eq!(
166                [batch, nchunks, nheads, chunk_len, state_rank],
167                c_bnhlr.dims()
168            );
169
170            // (a) C[n] · B[n]ᵀ
171            //     Contracts over state_rank.
172            let b_bnhrl = b_bnhlr.permute([0, 1, 2, 4, 3]);
173            let cb_bnhll = c_bnhlr.matmul(b_bnhrl);
174            assert_eq!(
175                [batch, nchunks, nheads, chunk_len, chunk_len],
176                cb_bnhll.dims()
177            );
178            san(&cb_bnhll);
179
180            // (b) Element-wise multiply with the 1-SS mask L.
181            //     L = exp(segsum(a_bhnl))
182            //     Lᵢⱼ = exp(a_cumsum[n,i] - a_cumsum[n,j])  (Eq. 4–5)
183            let l_bhnll = segsum(a_bhnl.clone()).exp();
184            assert_eq!(
185                [batch, nheads, nchunks, chunk_len, chunk_len],
186                l_bhnll.dims()
187            );
188            san(&l_bhnll);
189
190            // Permute both for the broadcast multiply.
191            let cb_bnlhl = cb_bnhll.permute([0, 1, 3, 2, 4]);
192            assert_eq!(
193                [batch, nchunks, chunk_len, nheads, chunk_len],
194                cb_bnlhl.dims()
195            );
196            let l_bnlhl = l_bhnll.permute([0, 2, 3, 1, 4]);
197            assert_eq!(
198                [batch, nchunks, chunk_len, nheads, chunk_len],
199                l_bnlhl.dims()
200            );
201            san(&cb_bnlhl);
202            san(&l_bnlhl);
203            let masked_cb_bnlhl = cb_bnlhl * l_bnlhl;
204            san(&masked_cb_bnlhl);
205
206            // (c) masked_CB · X → Y_diag.
207            //     Contract over the last chunk_len dimension.
208            let masked_cb_bnhll = masked_cb_bnlhl.permute([0, 1, 3, 2, 4]);
209            assert_eq!(
210                [batch, nchunks, nheads, chunk_len, chunk_len],
211                masked_cb_bnhll.dims()
212            );
213
214            let x_bnhlp = input.x_bnlhp.clone().permute([0, 1, 3, 2, 4]);
215            assert_eq!(
216                [batch, nchunks, nheads, chunk_len, per_head_dim],
217                x_bnhlp.dims()
218            );
219
220            let y_diag_bnhlp = masked_cb_bnhll.matmul(x_bnhlp);
221            assert_eq!(
222                [batch, nchunks, nheads, chunk_len, per_head_dim],
223                y_diag_bnhlp.dims()
224            );
225            san(&y_diag_bnhlp);
226
227            y_diag_bnhlp.permute([0, 1, 3, 2, 4]) // y_diag_bnlhp
228        };
229        assert_eq!(
230            [batch, nchunks, chunk_len, nheads, per_head_dim],
231            y_diag_bnlhp.dims()
232        );
233
234        // =============================================================
235        // STEP 2: Compute chunk state (input → state)
236        // =============================================================
237        //
238        // For each chunk n, compute the SSM state at the end of the chunk
239        // assuming the initial state is zero (Eq. 20):
240        //
241        //   s[n] = Σ_{t ∈ [0, chunk_len)} exp(a_cumsum[n,-1] - a_cumsum[n,t]) · B̄[n,t] · x[n,t]ᵀ
242        //
243        // Equivalently:
244        //   decay_state[n, t] = exp(a_cum_last[n] - a_cum[n, t])
245        //   s[n] = Σₜ  decay_state[n, t] · x[n, t]ᵀ · B̄[n, t]     (outer product over per_head_dim and state_rank)
246        //
247        // This is a batched GEMM, fully parallel across n and b.
248        let state_bnhpr = {
249            // Decay from each position t to the end of the chunk:
250            //   decay_state[n, t] = exp(a_cum[n, chunk_len-1] - a_cum[n, t])
251            let a_cumsum_last_bhn1 = a_cumsum_bhnl.clone().slice(s![.., .., .., -1]);
252            assert_eq!([batch, nheads, nchunks, 1], a_cumsum_last_bhn1.dims());
253
254            let decay_state_bhnl = (a_cumsum_last_bhn1 - a_cumsum_bhnl.clone()).exp();
255            assert_eq!([batch, nheads, nchunks, chunk_len], decay_state_bhnl.dims());
256            san(&decay_state_bhnl);
257
258            // Multiply decay into x: decay[n, t] · x[n, t]
259            let decay_state_bnlh1 = decay_state_bhnl.permute([0, 2, 3, 1]).unsqueeze_dim(4);
260            assert_eq!(
261                [batch, nchunks, chunk_len, nheads, 1],
262                decay_state_bnlh1.dims()
263            );
264            let decayed_x_bnlhp = decay_state_bnlh1 * input.x_bnlhp.clone();
265            assert_eq!(
266                [batch, nchunks, chunk_len, nheads, per_head_dim],
267                decayed_x_bnlhp.dims()
268            );
269            san(&decayed_x_bnlhp);
270
271            // Contract over chunk_len: (decayed_x[n, :, h, :])ᵀ · B̄[n, :, h, :]
272            let decayed_x_bnhpl = decayed_x_bnlhp.permute([0, 1, 3, 4, 2]);
273            assert_eq!(
274                [batch, nchunks, nheads, per_head_dim, chunk_len],
275                decayed_x_bnhpl.dims()
276            );
277            let b_bnhlr = delta_b_bnlhr.clone().permute([0, 1, 3, 2, 4]);
278            assert_eq!(
279                [batch, nchunks, nheads, chunk_len, state_rank],
280                b_bnhlr.dims()
281            );
282
283            decayed_x_bnhpl.matmul(b_bnhlr)
284        };
285        assert_eq!(
286            [batch, nchunks, nheads, per_head_dim, state_rank],
287            state_bnhpr.dims()
288        );
289        san(&state_bnhpr);
290
291        // =============================================================
292        // STEP 3: Inter-chunk state scan (state passing)
293        // =============================================================
294        //
295        // Propagate hidden state across chunk boundaries.  The recurrence is
296        //
297        //   h[n] = Ā_chunk[n] · h[n-1] + s[n]     (Eq. 22)
298        //
299        // where Ā_chunk[n] = exp(Σ_{t ∈ chunk n} Δₜ · A) = exp(a_cum[n, chunk_len-1]).
300        //
301        // Unrolling the recurrence gives a matrix form identical to Step 2 but
302        // at the chunk level: each new state is a weighted sum of all previous
303        // chunk state.  We implement this with the same 1-SS segsum trick,
304        // now applied over the nchunks dimension.
305        //
306        // The result is `new_state[n]`, the true hidden state entering chunk n,
307        // for n ∈ {0, ..., nchunks-1}, plus the final state after all chunks.
308        let (state_bnhpr, final_state_bnpr) = {
309            // Prepend the initial state h₀ to the array of chunk state.
310            let initial_state_b1hpr = input.initial_state_bhpr.unsqueeze_dim(1);
311            assert_eq!(
312                [batch, 1, nheads, per_head_dim, state_rank],
313                initial_state_b1hpr.dims()
314            );
315
316            // Optionally add learnable initial state (broadcast over batch).
317            let initial_state_b1hpr = if let Some(init_hpr) = input.init_state_hpr {
318                let init_b1hpr = init_hpr.unsqueeze_dim::<4>(0).expand([
319                    batch,
320                    1,
321                    nheads,
322                    per_head_dim,
323                    state_rank,
324                ]);
325                initial_state_b1hpr + init_b1hpr
326            } else {
327                initial_state_b1hpr
328            };
329            san(&initial_state_b1hpr);
330
331            let state_bNhpr = Tensor::cat(vec![initial_state_b1hpr, state_bnhpr], 1);
332            assert_eq!(
333                [batch, 1 + nchunks, nheads, per_head_dim, state_rank],
334                state_bNhpr.dims()
335            );
336
337            // Build the inter-chunk decay matrix using segsum.
338            // a_cum_last[n] = Σ_{t ∈ chunk n} Δₜ · A   (the total log-decay of chunk n)
339            let a_cumsum_last_bhn = a_cumsum_bhnl
340                .clone()
341                .slice(s![.., .., .., -1]) // a_cumsum_bhn1
342                .squeeze_dim(3); // a_cumsum_bhn
343            assert_eq!([batch, nheads, nchunks], a_cumsum_last_bhn.dims());
344
345            // Prepend a zero for the initial state (no decay before chunk 0).
346            let a_chunk_pad_bhN = Tensor::cat(
347                vec![
348                    Tensor::zeros(Shape::new([batch, nheads, 1]), device),
349                    a_cumsum_last_bhn,
350                ],
351                2,
352            );
353            assert_eq!([batch, nheads, 1 + nchunks], a_chunk_pad_bhN.dims());
354
355            // 1-SS inter-chunk decay matrix.
356            //   decay_chunk[i, j] = exp(Σ_{k=j+1..i} a_cum_last[k])  (i ≥ j)
357            // Row i of this matrix, when multiplied by the state vector,
358            // gives the true hidden state entering chunk i.
359            let decay_chunk_bhNN = segsum(a_chunk_pad_bhN).exp();
360            assert_eq!(
361                [batch, nheads, 1 + nchunks, 1 + nchunks],
362                decay_chunk_bhNN.dims()
363            );
364            san(&decay_chunk_bhNN);
365
366            // Flatten the state's (per_head_dim, state_rank) dimensions for the matmul.
367            let flat_state_dim = per_head_dim * state_rank; // f = per_head_dim·state_rank
368            let state_bhNf = state_bNhpr
369                .clone()
370                .permute([0, 2, 1, 3, 4]) // state_bhNpr
371                .reshape([batch, nheads, 1 + nchunks, flat_state_dim]); // state_bhNf
372            assert_eq!(
373                [batch, nheads, 1 + nchunks, flat_state_dim],
374                state_bhNf.dims()
375            );
376
377            let new_state_bhNf = decay_chunk_bhNN.matmul(state_bhNf);
378            assert_eq!(
379                [batch, nheads, 1 + nchunks, flat_state_dim],
380                new_state_bhNf.dims()
381            );
382            san(&new_state_bhNf);
383
384            let new_state_bhNpr =
385                new_state_bhNf.reshape([batch, nheads, 1 + nchunks, per_head_dim, state_rank]);
386
387            // Slice to get:
388            //   state[0..nchunks]  — the initial state entering each chunk
389            //   state[nchunks]     — the final state after the last real token
390            //
391            // For padded sequences the padding steps are identity operations
392            // (Δ=0 ⇒ Ā=1, B̄=0), so the state is carried unchanged through the
393            // pad region, and `state[nchunks]` is the correct final state.
394            let state_bhnpr = new_state_bhNpr
395                .clone()
396                .slice(s![.., .., 0..nchunks, .., ..]);
397            let final_state_bhpr = new_state_bhNpr
398                .slice(s![.., .., nchunks, .., ..])
399                .squeeze_dim(2);
400
401            (
402                state_bhnpr.permute([0, 2, 1, 3, 4]), // state_bnhpr
403                final_state_bhpr,
404            )
405        };
406        assert_eq!(
407            [batch, nchunks, nheads, per_head_dim, state_rank],
408            state_bnhpr.dims()
409        );
410        assert_eq!(
411            [batch, nheads, per_head_dim, state_rank],
412            final_state_bnpr.dims()
413        );
414
415        // =============================================================
416        // STEP 4: State-to-output contribution (Y_off)
417        // =============================================================
418        //
419        // For each chunk n, compute the contribution of the true initial state
420        // h[n-1] to the outputs within that chunk (Eq. 23):
421        //
422        //   Y_off[n, t] = C[n, t]ᵀ · exp(a_cumsum[n, t]) · h[n-1]
423        //               = exp(a_cum[n,t]) · (C[n,t]ᵀ · h[n-1])
424        //
425        // where the scalar `exp(a_cum[n,t])` is the cumulative decay from the
426        // start of the chunk to position t.
427        //
428        // Implementation:
429        //   (a) C[n] · h[n-1]ᵀ  (contract over state_rank)
430        //   (b) element-wise multiply with exp(a_cum)
431        let y_off_bnlhp = {
432            // exp(a_cumsum[n, t]): decay from start of chunk to position t.
433            let state_decay_out_bhnl = a_cumsum_bhnl.exp();
434            assert_eq!(
435                [batch, nheads, nchunks, chunk_len],
436                state_decay_out_bhnl.dims()
437            );
438            san(&state_decay_out_bhnl);
439
440            // (a) C[n] · h[n-1]ᵀ
441            let c_bnhlr = c_bnlhr.permute([0, 1, 3, 2, 4]);
442            assert_eq!(
443                [batch, nchunks, nheads, chunk_len, state_rank],
444                c_bnhlr.dims()
445            );
446
447            let state_bnhrp = state_bnhpr.permute([0, 1, 2, 4, 3]);
448            assert_eq!(
449                [batch, nchunks, nheads, state_rank, per_head_dim],
450                state_bnhrp.dims()
451            );
452
453            let ch_bnhlp = c_bnhlr.matmul(state_bnhrp);
454            assert_eq!(
455                [batch, nchunks, nheads, chunk_len, per_head_dim],
456                ch_bnhlp.dims()
457            );
458            san(&ch_bnhlp);
459
460            // (b) Multiply by the intra-chunk cumulative decay.
461            let state_decay_out_bnhl1 = state_decay_out_bhnl.permute([0, 2, 1, 3]).unsqueeze_dim(4);
462            assert_eq!(
463                [batch, nchunks, nheads, chunk_len, 1],
464                state_decay_out_bnhl1.dims()
465            );
466
467            let y_off_bnhlp = ch_bnhlp * state_decay_out_bnhl1;
468            assert_eq!(
469                [batch, nchunks, nheads, chunk_len, per_head_dim],
470                y_off_bnhlp.dims()
471            );
472            san(&y_off_bnhlp);
473
474            y_off_bnhlp.permute([0, 1, 3, 2, 4]) // y_off_bnlhp
475        };
476        assert_eq!(
477            [batch, nchunks, chunk_len, nheads, per_head_dim],
478            y_off_bnlhp.dims()
479        );
480
481        // ── Combine Y_diag and Y_off, undo padding ────────────────────────────
482        let y_bnlhp = y_diag_bnlhp + y_off_bnlhp;
483        san(&y_bnlhp);
484
485        // ── D skip connection ─────────────────────────────────────────────────
486        // yₜ += D · xₜ
487        // D is a per-head scalar; broadcast over batch, sequence, and per_head_dim.
488        let d_bnlhp = input
489            .d_h
490            .unsqueeze_dims::<5>(&[0, 1, 2, 4]) // d_111h1
491            .expand([batch, nchunks, chunk_len, nheads, per_head_dim]);
492        let y_bnlhp = y_bnlhp + d_bnlhp * input.x_bnlhp;
493        san(&y_bnlhp);
494
495        (y_bnlhp, final_state_bnpr)
496    }
497}