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}