Skip to main content

burn_mamba/mamba1/
mamba1.rs

1//! # Mamba-1 SSM Block — Selective State Space Model
2//!
3//! This module implements the original selective SSM block from the paper
4//! *"Mamba: Linear-Time Sequence Modeling with Selective State Spaces"*
5//! (Gu & Dao, 2023).
6//!
7//! ## Pipeline
8//!
9//! ```text
10//!   in_proj      d_model → [x | z]                (split into two d_inner halves)
11//!   conv1d       causal depthwise conv over x + SiLU
12//!   x_proj       x → [Δ_raw | B | C]
13//!   dt_proj      Δ_raw → Δ;  Δ = softplus(Δ)
14//!   scan         selective scan (ZOH for A, Euler for B) → y
15//!   gate         y = y · SiLU(z)
16//!   out_proj     d_inner → d_model
17//! ```
18//!
19//! Unlike Mamba-2/3, the recurrence is run as a plain **sequential selective
20//! scan** rather than a chunkwise SSD; there is no pluggable SSD path.  Both
21//! [`Mamba1::forward`] (full sequence) and [`Mamba1::step`] (single token)
22//! thread the same [`Mamba1Cache`] (convolution window + SSM state).
23//!
24//! ## Notation / Dimension Keys
25//!
26//! Tensor names carry a shape suffix (see the crate-level notation table).
27//! The letters used here:
28//!
29//! | Letter | Dimension                         | Typical |
30//! |--------|-----------------------------------|---------|
31//! | `b`    | `batch`                           | varies  |
32//! | `s`    | `sequence` length                 | varies  |
33//! | `d`    | `d_model`                         | 768     |
34//! | `i`    | `d_inner` = `expand`·`d_model`     | 2·d_model |
35//! | `k`    | `conv_kernel`                     | 4       |
36//! | `r`    | `state_rank` (latent SSM state)   | 16      |
37//!
38//! The Δ-projection rank `dt_rank` has no single-letter key; tensors carrying
39//! it are annotated with an explicit shape comment.
40
41use crate::mamba1::prelude::*;
42use crate::modules::Silu;
43use crate::modules::sanity as san;
44use crate::modules::split_into;
45use burn::prelude::*;
46use burn::{
47    module::{Module, Param},
48    nn::conv::{Conv1d, Conv1dConfig},
49    nn::{Initializer, Linear, LinearConfig, PaddingConfig1d},
50};
51
52/// The Mamba-1 selective SSM block.
53#[derive(Module, Debug)]
54pub struct Mamba1 {
55    /// Input channel: d_model.
56    /// Output channel: 2 * d_inner.
57    pub in_proj: Linear,
58
59    /// Input channel: d_inner.
60    /// Output channel: d_inner.
61    pub conv1d: Conv1d,
62
63    /// Input channel: d_inner.
64    /// Output channel: dt_rank + 2 * state_rank.
65    pub x_proj: Linear,
66
67    /// Input channel: dt_rank.
68    /// Output channel: d_inner.
69    pub dt_proj: Linear,
70
71    /// Dims: `[d_inner, state_rank]`.
72    pub a_log: Param<Tensor<2>>,
73
74    /// Dims: `[d_inner]`.
75    pub d: Param<Tensor<1>>,
76
77    /// Input channel: d_inner.
78    /// Output channel: d_model.
79    pub out_proj: Linear,
80}
81
82/// Configuration / factory for [`Mamba1`].
83#[derive(Config, Debug)]
84pub struct Mamba1Config {
85    /// Hidden dimension.
86    pub d_model: usize,
87
88    /// State rank — the latent dimension of the SSM hidden state
89    /// (`N` in Algorithm 2 from the Mamba paper).
90    #[config(default = 16)]
91    pub state_rank: usize,
92
93    /// Causal convolution window length.
94    #[config(default = 4)]
95    pub conv_kernel: usize,
96
97    /// Expansion factor for `d_inner = expand · d_model`.
98    #[config(default = 2)]
99    pub expand: usize,
100
101    /// Minimum dt value.
102    #[config(default = 1e-3)]
103    pub dt_min: f64,
104
105    /// Maximum dt value.
106    #[config(default = 1e-1)]
107    pub dt_max: f64,
108
109    /// Scale for dt initialization.
110    #[config(default = 1.)]
111    pub dt_scale: f64,
112
113    /// Floor for dt initialization.
114    #[config(default = 1e-4)]
115    pub dt_init_floor: f64,
116
117    /// Whether the depthwise convolution should have a bias.
118    #[config(default = true)]
119    pub has_conv_bias: bool,
120
121    /// Whether in_proj and out_proj should have a bias.
122    #[config(default = false)]
123    pub has_proj_bias: bool,
124
125    /// Rank of Δ (See Section 3.6 "Parameterization of ∆" from the Mamba paper).
126    /// Δ or delta: input-dependent step size.
127    ///
128    /// By default, set to `d_model.div_ceil(state_rank)`.
129    pub dt_rank: Option<usize>,
130
131    /// d_model * expand (`D` in Algorithm 2 from the Mamba paper).
132    ///
133    /// By default, set to expand * d_model.
134    pub d_inner: Option<usize>,
135}
136
137impl Mamba1Config {
138    /// Returns the initialized model.
139    pub fn init(&self, device: &Device) -> Mamba1 {
140        let d_inner = self.d_inner();
141        assert_ne!(self.state_rank, 0);
142        assert!(self.d_model + self.state_rank > 0);
143        let dt_rank = self.dt_rank();
144
145        // Helper function for PyTorch-style uniform initialization
146        let uniform_init = |d_input: usize| {
147            let bound = 1.0 / (d_input as f64).sqrt();
148            Initializer::Uniform {
149                min: -bound,
150                max: bound,
151            }
152        };
153
154        let dt_proj = {
155            use burn::tensor::Distribution;
156            let weight: Tensor<2> = {
157                let dt_init_std = (dt_rank as f64).powf(-0.5) * self.dt_scale;
158                Tensor::random(
159                    [dt_rank, d_inner],
160                    Distribution::Uniform(-dt_init_std, dt_init_std),
161                    device,
162                )
163            };
164            assert_eq!([dt_rank, d_inner], weight.dims());
165            let bias: Tensor<1> = {
166                // note: this placeholder impl may lose precision for very small values,
167                // and a Taylor series could approximate it: e^x - 1 = x + x^2/2! + x^3/3! + ⋯
168                // but with the clamp at dt_init_floor, this isn't necessary
169                let expm1 = |t: Tensor<1>| t.exp() - 1.;
170                let dt = Tensor::random([d_inner], Distribution::Uniform(0.0, 1.0), device)
171                    * (f64::ln(self.dt_max) - f64::ln(self.dt_min))
172                    + f64::ln(self.dt_min);
173                let dt = dt.exp().clamp_min(self.dt_init_floor);
174                // Inverse of softplus
175                dt.clone() + (-expm1(-dt)).log()
176            };
177            assert_eq!([d_inner], bias.dims());
178            Linear {
179                weight: Param::from_tensor(weight),
180                bias: Some(Param::from_tensor(bias)),
181            }
182        };
183
184        let a_log = {
185            let a_row: Tensor<1> =
186                Tensor::<1, Int>::arange(1..self.state_rank as i64 + 1, device).float();
187            assert_eq!([self.state_rank], a_row.dims());
188            let a_row = a_row.unsqueeze();
189            assert_eq!([1, self.state_rank], a_row.dims());
190            let a = a_row.repeat(&[d_inner, 1]);
191            assert_eq!([d_inner, self.state_rank], a.dims());
192            let a_log = a.log();
193            Param::from_tensor(a_log)
194        };
195
196        Mamba1 {
197            in_proj: LinearConfig::new(self.d_model, 2 * d_inner)
198                .with_bias(self.has_proj_bias)
199                // follows PyTorch's default initializer
200                .with_initializer(uniform_init(self.d_model))
201                .init(device),
202            conv1d: Conv1dConfig::new(d_inner, d_inner, self.conv_kernel)
203                // Causal left-padding is applied manually in `forward` (from the
204                // conv cache window), so the convolution itself uses no padding.
205                .with_padding(PaddingConfig1d::Valid)
206                .with_groups(d_inner)
207                .with_bias(self.has_conv_bias)
208                // follows PyTorch's default initializer
209                // fan_in = in_channels / groups * kernel_size
210                .with_initializer(uniform_init(self.conv_kernel))
211                .init(device),
212            x_proj: LinearConfig::new(d_inner, dt_rank + 2 * self.state_rank)
213                .with_bias(false)
214                // follows PyTorch's default initializer
215                .with_initializer(uniform_init(d_inner))
216                .init(device),
217            dt_proj,
218            a_log,
219            d: Initializer::Ones.init([d_inner], device),
220            out_proj: LinearConfig::new(d_inner, self.d_model)
221                .with_bias(self.has_proj_bias)
222                // follows PyTorch's default initializer
223                .with_initializer(uniform_init(d_inner))
224                .init(device),
225        }
226    }
227    /// Inner (expanded) channel width: the `d_inner` override if set, else
228    /// `expand · d_model`.
229    pub fn d_inner(&self) -> usize {
230        self.d_inner.unwrap_or(self.expand * self.d_model)
231    }
232    /// Rank of the Δ projection: the `dt_rank` override if set, else
233    /// `ceil(d_model / state_rank)`.
234    pub fn dt_rank(&self) -> usize {
235        self.dt_rank
236            .unwrap_or(self.d_model.div_ceil(self.state_rank))
237    }
238}
239
240impl Mamba1 {
241    /// See also [`Self::step`].
242    ///
243    /// Mirrors [`crate::mamba2::mamba2::Mamba2::forward`]: an optional `cache`
244    /// supplies the initial convolution window and SSM state (zero-initialised
245    /// when `None`), and the updated cache is returned so a sequence can be
246    /// processed in segments (prefill then decode, or chunked prefill).
247    ///
248    /// # Shapes
249    ///   - Input `[batch, sequence, d_model]`
250    ///   - Output `[batch, sequence, d_model]`
251    pub fn forward(&self, x: Tensor<3>, cache: Option<Mamba1Cache>) -> (Tensor<3>, Mamba1Cache) {
252        let [batch, sequence, d_model] = x.dims();
253        let [d_inner] = self.d.dims();
254        let [_, _, conv_kernel] = self.conv1d.weight.dims();
255        let [_d_inner, state_rank] = self.a_log.dims();
256        let device = x.device();
257        assert!(sequence > 0, "sequence length must be at least 1");
258
259        // Zero-initialise the cache (conv window + SSM state) when not provided.
260        let mut cache = cache.unwrap_or_else(|| Mamba1Cache {
261            conv_bik: Tensor::zeros([batch, d_inner, conv_kernel], &device),
262            ssm_bir: Tensor::zeros([batch, d_inner, state_rank], &device),
263        });
264        cache.sanity();
265
266        // layer 1 (in_proj): projects the input d_model into 2 * d_inner.
267        let [xs_bsi, res_bsi] = {
268            let xs_and_res = self.in_proj.forward(x);
269            assert_eq!([batch, sequence, 2 * d_inner], xs_and_res.dims());
270            split_into(xs_and_res, [d_inner, d_inner], 2)
271        };
272        assert_eq!([batch, sequence, d_inner], xs_bsi.dims());
273        assert_eq!([batch, sequence, d_inner], res_bsi.dims());
274
275        // layer 2 (conv1d) — causal, with the cache window threaded as left context
276        let xs_bsi = {
277            assert!(conv_kernel > 0);
278            let conv_in_bis = xs_bsi.permute([0, 2, 1]);
279            assert_eq!([batch, d_inner, sequence], conv_in_bis.dims());
280
281            // Left-pad with the last (conv_kernel - 1) columns of the cached
282            // window so the convolution is strictly causal and continues a
283            // prior segment.
284            let conv_in_padded = if conv_kernel >= 2 {
285                let tail = cache.conv_bik.clone().narrow(2, 1, conv_kernel - 1);
286                assert_eq!([batch, d_inner, conv_kernel - 1], tail.dims());
287                Tensor::cat(vec![tail, conv_in_bis], 2)
288            } else {
289                conv_in_bis
290            };
291            assert_eq!(
292                [batch, d_inner, (conv_kernel - 1) + sequence],
293                conv_in_padded.dims()
294            );
295
296            // Update the conv window: the last conv_kernel columns of the padded
297            // input.
298            cache.conv_bik = conv_in_padded.clone().narrow(2, sequence - 1, conv_kernel);
299            assert_eq!([batch, d_inner, conv_kernel], cache.conv_bik.dims());
300
301            let xs = self.conv1d.forward(conv_in_padded);
302            assert_eq!([batch, d_inner, sequence], xs.dims());
303
304            // restore original positioning as per before the layer 2
305            let xs = xs.permute([0, 2, 1]);
306            assert_eq!([batch, sequence, d_inner], xs.dims());
307
308            // activation
309            let xs = Silu::new().forward(xs);
310            assert_eq!([batch, sequence, d_inner], xs.dims());
311
312            xs
313        };
314        assert_eq!([batch, sequence, d_inner], xs_bsi.dims());
315
316        let (scan_bsi, final_ssm) = self.ssm(xs_bsi, cache.ssm_bir.clone());
317        assert_eq!([batch, sequence, d_inner], scan_bsi.dims());
318        cache.ssm_bir = final_ssm;
319
320        // activation
321        let ys = scan_bsi * Silu::new().forward(res_bsi);
322        assert_eq!([batch, sequence, d_inner], ys.dims());
323
324        let y = self.out_proj.forward(ys);
325        assert_eq!([batch, sequence, d_model], y.dims());
326        san(&y);
327
328        (y, cache)
329    }
330
331    /// Computes the selective-SSM parameters (Δ, A, B, C) from the conv output
332    /// and runs the [`Self::selective_scan`] recurrence over the full sequence.
333    ///
334    /// # Shapes
335    ///   - Input u `[batch, sequence, d_inner]`
336    ///   - Input init_ssm `[batch, d_inner, state_rank]`
337    ///   - Output `[batch, sequence, d_inner]`
338    ///   - Output (final state) `[batch, d_inner, state_rank]`
339    pub fn ssm(&self, u: Tensor<3>, init_ssm: Tensor<3>) -> (Tensor<3>, Tensor<3>) {
340        let [batch, sequence, d_inner] = u.dims();
341        let [_d_inner, state_rank] = self.a_log.dims();
342        let [dt_rank, _d_inner] = self.dt_proj.weight.dims();
343
344        // Compute ∆ A B C D, the state space parameters.
345
346        // A
347        // this is input independent (see Section 3.5.2 "Interpretation of A" form the Mamba paper for why A isn't selective)
348        let a = self.a_log.val().exp().neg();
349        assert_eq!([d_inner, state_rank], a.dims());
350
351        let x_dbl = self.x_proj.forward(u.clone());
352        assert_eq!([batch, sequence, dt_rank + 2 * state_rank], x_dbl.dims());
353
354        // ∆ (part 1/2)
355        // ∆ is input-dependent
356        // B and C are input-dependent
357        let [delta, b, c] = split_into(x_dbl, [dt_rank, state_rank, state_rank], 2);
358        assert_eq!([batch, sequence, dt_rank], delta.dims()); // [batch, sequence, dt_rank]
359        assert_eq!([batch, sequence, state_rank], b.dims());
360        assert_eq!([batch, sequence, state_rank], c.dims());
361
362        // ∆ (part 2/2)
363        // ∆ is input-dependent
364        let delta = self.dt_proj.forward(delta);
365        assert_eq!([batch, sequence, d_inner], delta.dims());
366
367        let delta = burn::tensor::activation::softplus(delta, 1.);
368
369        let delta = delta.permute([1, 0, 2]);
370        assert_eq!([sequence, batch, d_inner], delta.dims());
371
372        let c = c.permute([1, 0, 2]);
373        assert_eq!([sequence, batch, state_rank], c.dims());
374
375        Self::selective_scan(delta, a, b, c, self.d.val(), u, init_ssm)
376    }
377
378    /// Selective Scan.
379    ///
380    /// Does selective scan algorithm. See:
381    /// - Section 2 State Space Models from the Mamba paper;
382    /// - Algorithm 2 in Section 3.2 from the Mamba paper;
383    /// - run_SSM(A, B, C, u) from The Annotated S4.
384    ///
385    /// # Shapes
386    ///   - Input delta `[sequence, batch, d_inner]`
387    ///   - Input a `[d_inner, state_rank]`
388    ///   - Input b `[batch, sequence, state_rank]`
389    ///   - Input c `[sequence, batch, state_rank]`
390    ///   - Input d `[d_inner]`
391    ///   - Input u `[batch, sequence, d_inner]`
392    ///   - Input init_ssm `[batch, d_inner, state_rank]`
393    ///   - Output `[batch, sequence, d_inner]`
394    ///   - Output (final state) `[batch, d_inner, state_rank]`
395    pub fn selective_scan(
396        delta: Tensor<3>,
397        a: Tensor<2>,
398        b: Tensor<3>,
399        c: Tensor<3>,
400        d: Tensor<1>,
401        u: Tensor<3>,
402        init_ssm: Tensor<3>,
403    ) -> (Tensor<3>, Tensor<3>) {
404        let [sequence, batch, d_inner] = delta.dims();
405        let [_d_inner, state_rank] = a.dims();
406        let outer_shape = [sequence, batch, d_inner, state_rank];
407
408        // Discretize continuous parameters (A, B)
409        //  - A is discretized using zero-order hold (ZOH) discretization (see Section 2 Equation 4 in the Mamba paper)
410        //  - B is discretized using a simplified Euler discretization instead of ZOH. From a discussion with authors:
411        //    "A is the more important term and the performance doesn't change much with the simplification on B"
412        let (delta_a, delta_bu) = {
413            let delta = delta.unsqueeze_dim(3);
414            assert_eq!([sequence, batch, d_inner, 1], delta.dims());
415            let delta = delta.expand(outer_shape);
416            assert_eq!(outer_shape, delta.dims());
417
418            let a = a.unsqueeze_dims(&[0, 1]);
419            assert_eq!([1, 1, d_inner, state_rank], a.dims());
420            let a = a.expand(outer_shape);
421            assert_eq!(outer_shape, a.dims());
422            let delta_a = (delta.clone() * a).exp();
423            assert_eq!(outer_shape, delta_a.dims());
424
425            let b = b.permute([1, 0, 2]);
426            assert_eq!([sequence, batch, state_rank], b.dims());
427            let b = b.unsqueeze_dim(2);
428            assert_eq!([sequence, batch, 1, state_rank], b.dims());
429            let b = b.expand(outer_shape);
430            assert_eq!(outer_shape, b.dims());
431            let delta_b = delta * b;
432            assert_eq!(outer_shape, delta_b.dims());
433
434            let u = u.clone().permute([1, 0, 2]);
435            assert_eq!([sequence, batch, d_inner], u.dims());
436            let u = u.unsqueeze_dim(3);
437            assert_eq!([sequence, batch, d_inner, 1], u.dims());
438            let u = u.expand(outer_shape);
439            assert_eq!(outer_shape, u.dims());
440            let delta_bu = delta_b * u;
441            assert_eq!(outer_shape, delta_bu.dims());
442
443            (delta_a, delta_bu)
444        };
445        assert_eq!(outer_shape, delta_a.dims());
446        assert_eq!(outer_shape, delta_bu.dims());
447
448        // Perform selective scan (see scan_SSM() from The Annotated S4)
449        // Note that the below is sequential, while the official implementation does a much faster parallel scan that
450        // is additionally hardware-aware (like FlashAttention).
451
452        // unstack the Sequence axis
453
454        let delta_a = delta_a.split(1, 0);
455        assert_eq!(delta_a.len(), sequence);
456
457        let delta_bu = delta_bu.split(1, 0);
458        assert_eq!(delta_bu.len(), sequence);
459
460        let c = c.unsqueeze_dim(3);
461        assert_eq!([sequence, batch, state_rank, 1], c.dims());
462        let c = c.split(1, 0);
463        assert_eq!(c.len(), sequence);
464
465        let inner_shape = [batch, d_inner, state_rank];
466        assert_eq!(inner_shape, init_ssm.dims());
467        let mut xs: Tensor<3> = init_ssm;
468        let mut ys = Vec::with_capacity(sequence); // inner shape: [batch, d_inner]
469        for ((delta_a, delta_bu), c) in delta_a
470            .into_iter()
471            .zip(delta_bu.into_iter())
472            .zip(c.into_iter())
473        {
474            let delta_a = delta_a.squeeze_dim(0);
475            assert_eq!(inner_shape, delta_a.dims());
476            let delta_bu = delta_bu.squeeze_dim(0);
477            assert_eq!(inner_shape, delta_bu.dims());
478            let c = c.squeeze_dim(0);
479            assert_eq!([batch, state_rank, 1], c.dims());
480
481            xs = (xs.clone() * delta_a) + delta_bu;
482            let y = xs.clone().matmul(c);
483            assert_eq!([batch, d_inner, 1], y.dims());
484            let y = y.squeeze_dim(2);
485            assert_eq!([batch, d_inner], y.dims());
486            ys.push(y);
487        }
488
489        let ys = Tensor::stack(ys, 1);
490        assert_eq!([batch, sequence, d_inner], ys.dims());
491
492        let d = d.unsqueeze_dims(&[0, 1]);
493        assert_eq!([1, 1, d_inner], d.dims());
494        let d = d.expand([batch, sequence, d_inner]);
495
496        let ys = ys + (d * u);
497        assert_eq!([batch, sequence, d_inner], ys.dims());
498
499        (ys, xs)
500    }
501}
502
503mod step {
504    use super::*;
505
506    impl Mamba1 {
507        /// # Shapes
508        ///   - Input `[batch, d_model]`
509        ///   - Output `[batch, d_model]`
510        pub fn step(&self, x: Tensor<2>, cache: Option<Mamba1Cache>) -> (Tensor<2>, Mamba1Cache) {
511            let [batch, d_model] = x.dims();
512            let [d_inner] = self.d.dims();
513            let [_, _, conv_kernel] = self.conv1d.weight.dims();
514            let [_d_inner, state_rank] = self.a_log.dims();
515            let device = x.device();
516
517            // Zero-initialise the cache (conv window + SSM state) when not
518            // provided, mirroring `forward` so `step` is `Option`-coherent with
519            // the Mamba-2/3 blocks.
520            let mut cache = cache.unwrap_or_else(|| Mamba1Cache {
521                conv_bik: Tensor::zeros([batch, d_inner, conv_kernel], &device),
522                ssm_bir: Tensor::zeros([batch, d_inner, state_rank], &device),
523            });
524            cache.sanity();
525
526            // layer 1 (in_proj): projects the input d_model into 2 * d_inner.
527            let [xs_bi, res_bi] = {
528                let xs_and_res = self.in_proj.forward(x);
529                assert_eq!([batch, 2 * d_inner], xs_and_res.dims());
530                split_into(xs_and_res, [d_inner, d_inner], 1)
531            };
532            assert_eq!([batch, d_inner], xs_bi.dims());
533            assert_eq!([batch, d_inner], res_bi.dims());
534
535            // layer 2 (conv1d): roll the window leftwards and insert the new
536            // token's projection as the newest (rightmost) column.
537            cache.conv_bik = {
538                let t0 = cache.conv_bik.clone().narrow(2, 1, conv_kernel - 1);
539                assert_eq!([batch, d_inner, conv_kernel - 1], t0.dims());
540
541                let conv = Tensor::cat(vec![t0, xs_bi.unsqueeze_dim(2)], 2);
542                assert_eq!([batch, d_inner, conv_kernel], conv.dims());
543
544                conv
545            };
546            let xs_bi = {
547                let conv1d = self.conv1d.weight.val();
548                // [channels_out, channels_in / groups, kernel_size]
549                assert_eq!([d_inner, 1, conv_kernel], conv1d.dims());
550                let conv1d = conv1d.permute([1, 0, 2]);
551                assert_eq!([1, d_inner, conv_kernel], conv1d.dims());
552                let conv1d = conv1d.expand([batch, d_inner, conv_kernel]);
553                assert_eq!([batch, d_inner, conv_kernel], conv1d.dims());
554
555                let xs = cache.conv_bik.clone() * conv1d;
556                let xs = xs.sum_dim(2);
557                assert_eq!([batch, d_inner, 1], xs.dims());
558                let xs = xs.squeeze_dim(2);
559                assert_eq!([batch, d_inner], xs.dims());
560
561                // conv1d bias
562                let conv1d_bias = self.conv1d.bias.as_ref().unwrap().val();
563                // [channels_out]
564                assert_eq!([d_inner], conv1d_bias.dims());
565                let conv1d_bias = conv1d_bias.unsqueeze();
566                assert_eq!([1, d_inner], conv1d_bias.dims());
567                let xs = xs + conv1d_bias;
568
569                // activation
570                let xs = Silu::new().forward(xs);
571                assert_eq!([batch, d_inner], xs.dims());
572
573                xs
574            };
575            assert_eq!([batch, d_inner], xs_bi.dims());
576
577            let (scan_bi, cache) = self.ssm_step(xs_bi, cache);
578            assert_eq!([batch, d_inner], scan_bi.dims());
579
580            // activation
581            let ys = scan_bi * Silu::new().forward(res_bi);
582            assert_eq!([batch, d_inner], ys.dims());
583
584            let y = self.out_proj.forward(ys);
585            assert_eq!([batch, d_model], y.dims());
586
587            (y, cache)
588        }
589
590        /// Single-token counterpart of [`Mamba1::ssm`]: computes the
591        /// selective-SSM parameters (Δ, A, B, C) for one token and advances the
592        /// recurrence by one step via [`Self::selective_scan_step`].
593        ///
594        /// # Shapes
595        ///   - Input u `[batch, d_inner]`
596        ///   - Output `[batch, d_inner]`
597        pub fn ssm_step(&self, u: Tensor<2>, cache: Mamba1Cache) -> (Tensor<2>, Mamba1Cache) {
598            let [batch, d_inner, state_rank] = cache.ssm_bir.dims();
599            let [dt_rank, _d_inner] = self.dt_proj.weight.dims();
600
601            // Compute ∆ A B C D, the state space parameters.
602
603            // A
604            // this is input independent (see Section 3.5.2 "Interpretation of A" form the Mamba paper for why A isn't selective)
605            let a = self.a_log.val().exp().neg();
606            assert_eq!([d_inner, state_rank], a.dims());
607
608            let x_dbl = self.x_proj.forward(u.clone());
609            assert_eq!([batch, dt_rank + 2 * state_rank], x_dbl.dims());
610
611            // ∆ (part 1/2)
612            // ∆ is input-dependent
613            // B and C are input-dependent
614            let [delta, b, c] = split_into(x_dbl, [dt_rank, state_rank, state_rank], 1);
615            assert_eq!([batch, dt_rank], delta.dims()); // [batch, dt_rank]
616            assert_eq!([batch, state_rank], b.dims());
617            assert_eq!([batch, state_rank], c.dims());
618
619            // ∆ (part 2/2)
620            // ∆ is input-dependent
621            let delta = self.dt_proj.forward(delta);
622            assert_eq!([batch, d_inner], delta.dims());
623            let delta = burn::tensor::activation::softplus(delta, 1.);
624
625            Self::selective_scan_step(delta, a, b, c, self.d.val(), u, cache)
626        }
627
628        /// Selective Scan.
629        ///
630        /// Does selective scan algorithm. See:
631        /// - Section 2 State Space Models from the Mamba paper;
632        /// - Algorithm 2 in Section 3.2 from the Mamba paper;
633        /// - run_SSM(A, B, C, u) from The Annotated S4.
634        ///
635        /// # Shapes
636        ///   - Input delta `[batch, d_inner]`
637        ///   - Input a `[d_inner, state_rank]`
638        ///   - Input b `[batch, state_rank]`
639        ///   - Input c `[batch, state_rank]`
640        ///   - Input d `[d_inner]`
641        ///   - Input u `[batch, d_inner]`
642        ///   - Output `[batch, d_inner]`
643        pub fn selective_scan_step(
644            delta: Tensor<2>,
645            a: Tensor<2>,
646            b: Tensor<2>,
647            c: Tensor<2>,
648            d: Tensor<1>,
649            u: Tensor<2>,
650            mut cache: Mamba1Cache,
651        ) -> (Tensor<2>, Mamba1Cache) {
652            let [batch, d_inner, state_rank] = cache.ssm_bir.dims();
653            let outer_shape = [batch, d_inner, state_rank];
654
655            // Discretize continuous parameters (A, B)
656            //  - A is discretized using zero-order hold (ZOH) discretization (see Section 2 Equation 4 in the Mamba paper)
657            //  - B is discretized using a simplified Euler discretization instead of ZOH. From a discussion with authors:
658            //    "A is the more important term and the performance doesn't change much with the simplification on B"
659            let (delta_a, delta_bu) = {
660                let delta = delta.unsqueeze_dim(2);
661                assert_eq!([batch, d_inner, 1], delta.dims());
662                let delta = delta.expand(outer_shape);
663                assert_eq!(outer_shape, delta.dims());
664
665                let a = a.unsqueeze();
666                assert_eq!([1, d_inner, state_rank], a.dims());
667                let a = a.expand(outer_shape);
668                assert_eq!(outer_shape, a.dims());
669                let delta_a = (delta.clone() * a).exp();
670                assert_eq!(outer_shape, delta_a.dims());
671
672                let b = b.unsqueeze_dim(1);
673                assert_eq!([batch, 1, state_rank], b.dims());
674                let b = b.expand(outer_shape);
675                assert_eq!(outer_shape, b.dims());
676                let delta_b = delta * b;
677                assert_eq!(outer_shape, delta_b.dims());
678
679                let u = u.clone().unsqueeze_dim(2);
680                assert_eq!([batch, d_inner, 1], u.dims());
681                let u = u.expand(outer_shape);
682                assert_eq!(outer_shape, u.dims());
683                let delta_bu = delta_b * u;
684                assert_eq!(outer_shape, delta_bu.dims());
685
686                (delta_a, delta_bu)
687            };
688            assert_eq!(outer_shape, delta_a.dims());
689            assert_eq!(outer_shape, delta_bu.dims());
690
691            cache.ssm_bir = (cache.ssm_bir.clone() * delta_a) + delta_bu;
692
693            let c = c.unsqueeze_dim(2);
694            assert_eq!([batch, state_rank, 1], c.dims());
695
696            let y = cache.ssm_bir.clone().matmul(c);
697            assert_eq!([batch, d_inner, 1], y.dims());
698            let y = y.squeeze_dim(2);
699            assert_eq!([batch, d_inner], y.dims());
700
701            let d = d.unsqueeze();
702            assert_eq!([1, d_inner], d.dims());
703            let d = d.expand([batch, d_inner]);
704            assert_eq!([batch, d_inner], d.dims());
705
706            let y = y + (d * u);
707            assert_eq!([batch, d_inner], y.dims());
708
709            (y, cache)
710        }
711    }
712}
713
714// ---------------------------------------------------------------------------
715// Tests
716// ---------------------------------------------------------------------------
717
718#[cfg(all(test, feature = "_dev-test"))]
719mod tests;