Skip to main content

burn_mamba/mamba2/ssd/serial_recalculated/
serial_recalculated.rs

1//! # Serial SSD with a custom, memory-efficient backward (Mamba-2)
2//!
3//! This is the `SerialRecalculated` path.  The forward is the same five-kernel
4//! serial scan as [`super::super::serial`], but it is routed through the
5//! [`Mamba2BackendExt`] trait so that `Autodiff` backends can substitute a
6//! **custom backward** that recomputes the per-chunk intermediates instead of
7//! storing them (see [`super::backward`] / [`super::combined_backward`]),
8//! trading a little extra compute for ~⅓ less training memory.
9//!
10//! Every plain (non-autodiff) backend uses the trait's default body, which
11//! simply replays the [`super::super::serial`] kernels K1–K5.  The
12//! [`crate::impl_ssd_backend_ext_for_burn_backends!`] /
13//! [`crate::decl_ssd_autodiff_backend_ext!`] macros wire up the per-backend
14//! impls and the autodiff marker trait.
15
16use crate::mamba2::prelude::*;
17use crate::utils::fprim::{F, Mask, san};
18use burn::backend::tensor::FloatTensor;
19use burn::backend::*;
20use burn::backend::{Backend, Dispatch, backend_extension};
21use burn::tensor::Tensor;
22use burn::tensor::s;
23
24impl Mamba2SsdInput {
25    /// Forward pass for the Mamba-2 SSD module (recompute-backward path).
26    ///
27    /// Returns:
28    /// - `y_bnlhp`.
29    /// - `final_state_bhpr`.
30    #[allow(non_snake_case)]
31    pub fn ssd_serial_recalculated(self) -> (Tensor<5>, Tensor<4>) {
32        let input = self;
33        // Must use a backend-dependent method.
34        //
35        // For inference, this will ultimately replicate Mamba2::ssd_serial;
36        // For autodiff, this will call the custom implementation.
37
38        let [batch, nchunks, chunk_len, nheads, _per_head_dim] = input.x_bnlhp.dims();
39        assert!(nchunks > 0, "sequence length must be at least 1");
40
41        assert!(
42            input.init_state_hpr.is_none(),
43            "init_state_hpr not yet implemented"
44        );
45
46        // ── Permutes ──────────────────────────────────────────────────────────────────
47        // Note: dt_bnlh calculation (originally in Kernel 1) moved to Step 4 (before padding).
48        let dt_discretized_bhnl = input.dt_bnlh.permute([0, 3, 1, 2]);
49        assert_eq!(
50            [batch, nheads, nchunks, chunk_len],
51            dt_discretized_bhnl.dims()
52        );
53
54        // K1 is now computed inside the custom op (both forward and backward).
55        // a_decay_h is passed directly; da_cumsum is no longer an autodiff-tracked
56        // intermediate crossing the boundary.
57        let (y_bnlhp, final_state_bhpr) = <Dispatch as Mamba2BackendExt>::ssd_serial_recalculated(
58            input.x_bnlhp.into_dispatch(),
59            dt_discretized_bhnl.into_dispatch(),
60            input.b_bnlhr.into_dispatch(),
61            input.c_bnlhr.into_dispatch(),
62            input.d_h.into_dispatch(),
63            input.initial_state_bhpr.into_dispatch(),
64            input.a_decay_h.into_dispatch(),
65        );
66        let y_bnlhp = Tensor::from_dispatch(y_bnlhp);
67        let final_state_bhpr = Tensor::from_dispatch(final_state_bhpr);
68        (y_bnlhp, final_state_bhpr)
69    }
70}
71
72/// Extends the backend and wraps it for `burn`.
73#[backend_extension(
74    Cpu:  cfg(feature = "backend-cpu"),
75    Cuda: cfg(feature = "backend-cuda"),
76    Rocm:  cfg(feature = "backend-rocm"),
77    Metal:  cfg(feature = "backend-metal"),
78    Vulkan:  cfg(feature = "backend-vulkan"),
79    Wgpu:  cfg(feature = "backend-wgpu"),
80    WebGpu:  cfg(feature = "backend-webgpu"),
81    Flex:  cfg(feature = "backend-flex"),
82    NdArray:  cfg(feature = "backend-ndarray"),
83    LibTorch:  cfg(any(feature = "backend-tch-cpu", feature = "backend-tch-gpu")),
84    Autodiff:  cfg(feature = "autodiff"),
85)]
86pub trait Mamba2BackendExt: Backend {
87    /// Returns:
88    /// - `y_bnlhp`.
89    /// - `final_state_bhpr`.
90    fn ssd_serial_recalculated(
91        x_bnlhp: FloatTensor<Self>,
92        dt_discretized_bhnl: FloatTensor<Self>,
93        b_bnlhr: FloatTensor<Self>,
94        c_bnlhr: FloatTensor<Self>,
95        d_h: FloatTensor<Self>,
96        initial_state_bhpr: FloatTensor<Self>,
97        a_decay_h: FloatTensor<Self>,
98    ) -> (FloatTensor<Self>, FloatTensor<Self>) {
99        // Default impl essentially replicates Mamba2SsdInput::ssd_serial, but on
100        // backend primitives: this body runs under a generic `B`, where the
101        // high-level `Tensor` (pinned to `Dispatch`) is unavailable, so the math
102        // goes through the rank-tagged [`F`] primitive wrapper and the local
103        // primitive K-kernels below.
104        let x_bnlhp = F::<Self, 5>::new(x_bnlhp);
105        let dt_discretized_bhnl = F::<Self, 4>::new(dt_discretized_bhnl);
106        let b_bnlhr = F::<Self, 5>::new(b_bnlhr);
107        let c_bnlhr = F::<Self, 5>::new(c_bnlhr);
108        let d_h = F::<Self, 1>::new(d_h);
109        let initial_state_bhpr = F::<Self, 4>::new(initial_state_bhpr);
110        let a_decay_h = F::<Self, 1>::new(a_decay_h);
111
112        let nchunks = x_bnlhp.dims()[1];
113        assert!(nchunks > 0, "sequence length must be at least 1");
114
115        // ── Kernel 1 ──────────────────────────────────────────────────────────
116        let (da_cumsum_bhnl, da_chunk_end_bhn) =
117            k1_ssd_chunk_cumsum::<Self>(dt_discretized_bhnl.clone(), a_decay_h);
118        san(&da_cumsum_bhnl);
119
120        // ── Kernel 2 ──────────────────────────────────────────────────────────
121        let cb_bnhll = k2_ssd_bmm::<Self>(c_bnlhr.clone(), b_bnlhr.clone());
122        san(&cb_bnhll);
123
124        // ── Kernel 3 ──────────────────────────────────────────────────────────
125        let intra_chunk_state_bnhpr = k3_ssd_chunk_state::<Self>(
126            x_bnlhp.clone(),
127            b_bnlhr,
128            da_cumsum_bhnl.clone(),
129            dt_discretized_bhnl.clone(),
130        );
131        san(&intra_chunk_state_bnhpr);
132
133        // ── Kernel 4 ──────────────────────────────────────────────────────────
134        let (chunk_input_state_bnhpr, final_state_bhpr) = k4_ssd_state_passing::<Self>(
135            intra_chunk_state_bnhpr,
136            da_chunk_end_bhn,
137            initial_state_bhpr,
138        );
139        san(&chunk_input_state_bnhpr);
140        san(&final_state_bhpr);
141
142        // ── Kernel 5 ──────────────────────────────────────────────────────────
143        let y_bnlhp = k5_ssd_chunk_scan::<Self>(
144            da_cumsum_bhnl,
145            dt_discretized_bhnl,
146            x_bnlhp,
147            c_bnlhr,
148            cb_bnhll,
149            chunk_input_state_bnhpr,
150            d_h,
151        );
152        san(&y_bnlhp);
153
154        (y_bnlhp.inner(), final_state_bhpr.inner())
155    }
156}
157
158// ─── Primitive forward kernels (K1–K5) ───────────────────────────────────────
159// Primitive ports of the high-level [`crate::mamba2::ssd::serial`] kernels,
160// expressed on `B`'s primitives via [`F`] so the trait default body can run
161// under a generic backend. K1/K2/K4 are reused by the recompute backward in
162// [`super::combined_backward`]; K5 is forward-only (the backward computes K5's
163// gradient analytically rather than recomputing it).
164
165/// Primitive port of [`crate::mamba2::ssd::serial::k1_ssd_chunk_cumsum`].
166///
167/// Returns the intra-chunk cumsum `da_cumsum_bhnl` and the per-chunk last value
168/// `da_chunk_end_bhn`.
169pub(crate) fn k1_ssd_chunk_cumsum<B: Backend>(
170    dt_discretized_bhnl: F<B, 4>,
171    a_decay_h: F<B, 1>,
172) -> (F<B, 4>, F<B, 3>) {
173    let [batch, nheads, nchunks, chunk_len] = dt_discretized_bhnl.dims();
174    let da_cumsum_bhnl: F<B, 4> = {
175        let a_decay_bhnl = a_decay_h
176            .unsqueeze_dims::<4>(&[0, 2, 3])
177            .expand([batch, nheads, nchunks, chunk_len]);
178        (dt_discretized_bhnl * a_decay_bhnl).cumsum(3)
179    };
180    let da_chunk_end_bhn = da_cumsum_bhnl
181        .clone()
182        .slice(s![.., .., .., -1])
183        .squeeze_dim::<3>(3);
184    (da_cumsum_bhnl, da_chunk_end_bhn)
185}
186
187/// Primitive port of [`crate::mamba2::ssd::serial::k2_ssd_bmm`].
188///
189/// Returns the intra-chunk `C·Bᵀ` block matrix `cb_bnhll`.
190pub(crate) fn k2_ssd_bmm<B: Backend>(c_bnlhr: F<B, 5>, b_bnlhr: F<B, 5>) -> F<B, 5> {
191    let c_bnhlr = c_bnlhr.permute([0, 1, 3, 2, 4]);
192    let b_bnhrl = b_bnlhr.permute([0, 1, 3, 4, 2]);
193    c_bnhlr.matmul(b_bnhrl)
194}
195
196/// Primitive port of [`crate::mamba2::ssd::serial::k3_ssd_chunk_state`] (lean:
197/// returns only the chunk-end state).
198///
199/// Returns `intra_chunk_state_bnhpr` — each chunk's contribution to its end
200/// state assuming a zero state at the chunk's start.
201pub(crate) fn k3_ssd_chunk_state<B: Backend>(
202    x_bnlhp: F<B, 5>,
203    b_bnlhr: F<B, 5>,
204    da_cumsum_bhnl: F<B, 4>,
205    dt_discretized_bhnl: F<B, 4>,
206) -> F<B, 5> {
207    let [batch, nchunks, chunk_len, nheads, per_head_dim] = x_bnlhp.dims();
208    let [.., state_rank] = b_bnlhr.dims();
209
210    let x_bnhpl = x_bnlhp.permute([0, 1, 3, 4, 2]);
211    let b_bnhlr = b_bnlhr.permute([0, 1, 3, 2, 4]);
212
213    // K3 scaling factor: dt · exp(cumA_last − cumA)
214    let da_cumsum_last_bhn1 = da_cumsum_bhnl.clone().slice(s![.., .., .., -1]);
215    let forward_decay_to_chunk_end_bhnl =
216        (da_cumsum_last_bhn1.expand([batch, nheads, nchunks, chunk_len]) - da_cumsum_bhnl).exp();
217    let b_bar_scale_bhnl = forward_decay_to_chunk_end_bhnl * dt_discretized_bhnl;
218
219    let b_bar_scale_bnhlr = b_bar_scale_bhnl
220        .permute([0, 2, 1, 3]) // b_bar_scale_bnhl
221        .unsqueeze_dim::<5>(4) // b_bar_scale_bnhl1
222        .expand([batch, nchunks, nheads, chunk_len, state_rank]);
223    let b_scaled_bnhlr = b_bnhlr * b_bar_scale_bnhlr;
224
225    let intra_chunk_state_bnhpr = x_bnhpl.matmul(b_scaled_bnhlr);
226    assert_eq!(
227        [batch, nchunks, nheads, per_head_dim, state_rank],
228        intra_chunk_state_bnhpr.dims()
229    );
230    intra_chunk_state_bnhpr
231}
232
233/// Primitive port of [`crate::mamba2::ssd::serial::k4_ssd_state_passing`].
234///
235/// Returns the per-chunk input-state stream `chunk_input_state_bnhpr` and the
236/// `final_state_bhpr`.
237pub(crate) fn k4_ssd_state_passing<B: Backend>(
238    intra_chunk_state_bnhpr: F<B, 5>,
239    da_chunk_end_bhn: F<B, 3>,
240    initial_state_bhpr: F<B, 4>,
241) -> (F<B, 5>, F<B, 4>) {
242    let [batch, nchunks, nheads, per_head_dim, state_rank] = intra_chunk_state_bnhpr.dims();
243
244    let mut running_state_bhpr = initial_state_bhpr;
245    let mut chunk_input_state_vec_bhpr = Vec::with_capacity(nchunks + 1);
246    chunk_input_state_vec_bhpr.push(running_state_bhpr.clone());
247
248    for i_chunk in 0..nchunks {
249        let intra_state_bhpr = intra_chunk_state_bnhpr
250            .clone()
251            .slice(s![.., i_chunk, .., .., ..])
252            .squeeze_dim::<4>(1);
253        let decay_bhpr = da_chunk_end_bhn
254            .clone()
255            .slice(s![.., .., i_chunk])
256            .exp()
257            .unsqueeze_dim::<4>(3)
258            .expand([batch, nheads, per_head_dim, state_rank]);
259        running_state_bhpr = (decay_bhpr * running_state_bhpr) + intra_state_bhpr;
260        chunk_input_state_vec_bhpr.push(running_state_bhpr.clone());
261    }
262
263    let final_state_bhpr = chunk_input_state_vec_bhpr.pop().unwrap();
264    let chunk_input_state_bnhpr = F::stack(chunk_input_state_vec_bhpr, 1);
265    (chunk_input_state_bnhpr, final_state_bhpr)
266}
267
268/// Primitive port of [`crate::mamba2::ssd::serial::k5_ssd_chunk_scan`].
269///
270/// Combines the intra-chunk (ORANGE, attention-like) and inter-chunk (BLUE,
271/// state-carried) contributions plus the `D` skip term into the output
272/// `y_bnlhp`. Forward-only.
273#[allow(clippy::too_many_arguments)]
274fn k5_ssd_chunk_scan<B: Backend>(
275    da_cumsum_bhnl: F<B, 4>,
276    dt_discretized_bhnl: F<B, 4>,
277    x_bnlhp: F<B, 5>,
278    c_bnlhr: F<B, 5>,
279    cb_bnhll: F<B, 5>,
280    chunk_input_state_bnhpr: F<B, 5>,
281    d_h: F<B, 1>,
282) -> F<B, 5> {
283    let [batch, nchunks, chunk_len, nheads, per_head_dim] = x_bnlhp.dims();
284    let device = x_bnlhp.device();
285
286    let da_cumsum_bnhl = da_cumsum_bhnl.permute([0, 2, 1, 3]);
287    let dt_bnhl = dt_discretized_bhnl.permute([0, 2, 1, 3]);
288    let x_bnhlp = x_bnlhp.clone().permute([0, 1, 3, 2, 4]);
289    let c_bnhlr = c_bnlhr.permute([0, 1, 3, 2, 4]);
290
291    // ── BLUE: exp(dA[l]) · C[l,:] @ state_in^T ─────────────────────────────
292    let exp_da_cumsum_bnhlp = da_cumsum_bnhl
293        .clone()
294        .exp()
295        .unsqueeze_dim::<5>(4) // exp_da_cumsum_bnhl1
296        .expand([batch, nchunks, nheads, chunk_len, per_head_dim]);
297    let chunk_input_state_bnhrp = chunk_input_state_bnhpr.permute([0, 1, 2, 4, 3]);
298    let blue_scaled_bnhlp = c_bnhlr.matmul(chunk_input_state_bnhrp) * exp_da_cumsum_bnhlp;
299    san(&blue_scaled_bnhlp);
300
301    // ── ORANGE: causal CB_weighted @ X ──────────────────────────────────────
302    let da_cumsum_target_bnhll = da_cumsum_bnhl
303        .clone()
304        .unsqueeze_dim::<5>(4) // da_cumsum_bnhl1
305        .expand([batch, nchunks, nheads, chunk_len, chunk_len]);
306    let da_cumsum_source_bnhll = da_cumsum_bnhl
307        .unsqueeze_dim::<5>(3) // da_cumsum_bnh1l
308        .expand([batch, nchunks, nheads, chunk_len, chunk_len]);
309    let da_cumsum_diff_bnhll = da_cumsum_target_bnhll - da_cumsum_source_bnhll;
310
311    // Causal mask + exp stabiliser: strictly-above-diagonal set to −∞.
312    let causal_mask_bnhll: Mask<B> = Mask::tril_mask(chunk_len, chunk_len, 0, &device)
313        .reshape([1, 1, 1, chunk_len, chunk_len])
314        .expand([batch, nchunks, nheads, chunk_len, chunk_len]);
315    let da_cumsum_diff_exp_bnhll = da_cumsum_diff_bnhll
316        .mask_fill(causal_mask_bnhll, f32::NEG_INFINITY)
317        .exp();
318    san(&da_cumsum_diff_exp_bnhll);
319
320    let dt_source_bnhll = dt_bnhl
321        .unsqueeze_dim::<5>(3) // dt_bnh1l
322        .expand([batch, nchunks, nheads, chunk_len, chunk_len]);
323    let orange_lhs_bnhll = cb_bnhll * da_cumsum_diff_exp_bnhll * dt_source_bnhll;
324    let orange_bnhlp = orange_lhs_bnhll.matmul(x_bnhlp);
325    san(&orange_bnhlp);
326
327    // ── SKIP: D[h] · x[l,p] ─────────────────────────────────────────────────
328    let skip_bnlhp = d_h
329        .unsqueeze_dims::<5>(&[0, 1, 2, 4]) // d_111h1
330        .expand([batch, nchunks, chunk_len, nheads, per_head_dim])
331        * x_bnlhp;
332
333    let y_partial_bnhlp = blue_scaled_bnhlp + orange_bnhlp;
334    let y_partial_bnlhp = y_partial_bnhlp.permute([0, 1, 3, 2, 4]);
335    let y_bnlhp = y_partial_bnlhp + skip_bnlhp;
336    assert_eq!(
337        [batch, nchunks, chunk_len, nheads, per_head_dim],
338        y_bnlhp.dims()
339    );
340    y_bnlhp
341}
342
343// Per-backend impls: each delegates to the trait's default body. The custom
344// autodiff backward lives in `super::backward` as a separate impl.
345//
346// TODO: somehow avoid leaking backend-* features into the library.
347crate::impl_ssd_backend_ext_for_burn_backends!(Mamba2BackendExt);
348
349crate::decl_ssd_autodiff_backend_ext!(Mamba2AutodiffBackendExt, Mamba2BackendExt);