Skip to main content

ferrox_core/
mamba2.rs

1//! The Mamba selective-state-space step, as ggml computes it, for both
2//! generations.
3//!
4//! Two kernels and a state. `ggml_ssm_conv` (`ggml-cpu/ops.cpp:9557-9608`)
5//! is a causal depthwise convolution of width `d_conv` over the
6//! projected rows, with the previous `d_conv - 1` rows as the state;
7//! `ggml_ssm_scan` (`:9627-9850`) is the recurrence
8//!
9//! ```text
10//! dt'      = softplus(dt_h)                     (ggml-impl.h:107-109)
11//! dA       = exp(dt' * A)                       Mamba-2: one scalar per head (:9682-9724)
12//!                                               Mamba-1: one per state element (:9779-9820)
13//! S[h,d,:] = S[h,d,:] * dA + B[g,:] * (x[h,d] * dt')
14//! y[h,d]   = S[h,d,:] . C[g,:]
15//! ```
16//!
17//! with `g = h / (n_head / n_group)` (`repeat_interleave`) and a float
18//! accumulator. The state is `[n_head][head_dim][d_state]` with the
19//! state index fastest, ggml's `{d_state, head_dim, n_head}`. Mamba-1
20//! (`build_mamba_layer`) is the same kernel with `n_head = d_inner`,
21//! `head_dim = 1`, `n_group = 1` and A `{d_state, d_inner}`; the
22//! kernel branches on `src3->ne[0] == 1` and so does [`Decay`].
23//!
24//! This file is the arithmetic only: no weights, no norms, no
25//! projections. `ferrox_models::mamba2` owns those and the residual
26//! topology; [`crate::recurrent_state::RecurrentState`] owns the two
27//! buffers between tokens.
28
29/// `log(1 + exp(x))`, in ggml's precision (`ggml_compute_softplus_f32`).
30#[inline]
31pub fn softplus(x: f32) -> f32 {
32    if x > 20.0 {
33        x
34    } else {
35        (1.0 + x.exp()).ln()
36    }
37}
38
39/// One token of the causal conv: `out[c] = sum_i taps[c][i] * window[i][c]`,
40/// where `window` is the previous `d_conv - 1` rows (the state, oldest
41/// first) followed by this token's `x`, and the state is then shifted
42/// by one row with `x` appended.
43///
44/// `state` is `[(d_conv - 1)][width]`, `taps` is `[width][d_conv]`
45/// (ggml `{d_conv, width}`: channel `c`'s taps contiguous, oldest input
46/// on tap 0), `x` and `out` are `[width]`.
47pub fn conv_step(state: &mut [f32], taps: &[f32], d_conv: usize, x: &[f32], out: &mut [f32]) {
48    let width = x.len();
49    assert_eq!(state.len(), (d_conv - 1) * width);
50    assert_eq!(taps.len(), width * d_conv);
51    assert_eq!(out.len(), width);
52    for c in 0..width {
53        let t = &taps[c * d_conv..(c + 1) * d_conv];
54        // ops.cpp:9598-9603: a float accumulator over the window in
55        // order, the newest input last.
56        let mut acc = 0.0f32;
57        for (i, tap) in t.iter().enumerate().take(d_conv - 1) {
58            acc += state[i * width + c] * tap;
59        }
60        acc += x[c] * t[d_conv - 1];
61        out[c] = acc;
62    }
63    // Shift: drop the oldest row, append `x`.
64    if d_conv > 1 {
65        state.copy_within(width.., 0);
66        state[(d_conv - 2) * width..].copy_from_slice(x);
67    }
68}
69
70/// The stored `A`, in the two shapes the kernel takes.
71#[derive(Debug, Clone, Copy)]
72pub enum Decay<'a> {
73    /// Mamba-2: `[n_head]`, `{1, n_head}` in ggml; `dA` is one scalar
74    /// per head.
75    PerHead(&'a [f32]),
76    /// Mamba-1: `[n_head][d_state]`, `{d_state, n_head}` in ggml; `dA`
77    /// is one scalar per state element.
78    PerState(&'a [f32]),
79}
80
81/// The geometry one scan step needs.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct ScanDims {
84    pub n_head: usize,
85    pub head_dim: usize,
86    pub d_state: usize,
87    pub n_group: usize,
88}
89
90impl ScanDims {
91    /// Floats in one sequence's SSM state.
92    pub fn state_len(self) -> usize {
93        self.n_head * self.head_dim * self.d_state
94    }
95}
96
97/// One token of the Mamba-2 scan, in place on `state`
98/// (`[n_head][head_dim][d_state]`).
99///
100/// `x` is `[n_head][head_dim]`, `dt` is `[n_head]` (BEFORE softplus,
101/// the bias already added), `a` is the stored `ssm_a` (already
102/// negative) in either shape, `b` and `c` are `[n_group][d_state]`,
103/// `y` is `[n_head][head_dim]`.
104#[allow(clippy::too_many_arguments)] // the seven operands ggml_ssm_scan takes, plus the dims
105pub fn scan_step(
106    dims: ScanDims,
107    state: &mut [f32],
108    x: &[f32],
109    dt: &[f32],
110    a: Decay<'_>,
111    b: &[f32],
112    c: &[f32],
113    y: &mut [f32],
114) {
115    let ScanDims {
116        n_head,
117        head_dim,
118        d_state,
119        n_group,
120    } = dims;
121    assert_eq!(state.len(), dims.state_len());
122    assert_eq!(x.len(), n_head * head_dim);
123    assert_eq!(dt.len(), n_head);
124    match a {
125        Decay::PerHead(a) => assert_eq!(a.len(), n_head),
126        Decay::PerState(a) => assert_eq!(a.len(), n_head * d_state),
127    }
128    assert_eq!(b.len(), n_group * d_state);
129    assert_eq!(c.len(), n_group * d_state);
130    assert_eq!(y.len(), n_head * head_dim);
131    assert_eq!(n_head % n_group, 0, "ops.cpp:9659");
132    let heads_per_group = n_head / n_group;
133    for h in 0..n_head {
134        let dt_sp = softplus(dt[h]);
135        let g = h / heads_per_group;
136        let (bg, cg) = (
137            &b[g * d_state..(g + 1) * d_state],
138            &c[g * d_state..(g + 1) * d_state],
139        );
140        for d in 0..head_dim {
141            let ii = h * head_dim + d;
142            let x_dt = x[ii] * dt_sp;
143            let s = &mut state[ii * d_state..(ii + 1) * d_state];
144            let mut sum = 0.0f32;
145            match a {
146                Decay::PerHead(a) => {
147                    // ops.cpp:9689: dA outside the state-wise loop.
148                    let da = (dt_sp * a[h]).exp();
149                    for k in 0..d_state {
150                        let v = s[k] * da + bg[k] * x_dt;
151                        sum += v * cg[k];
152                        s[k] = v;
153                    }
154                }
155                Decay::PerState(a) => {
156                    // ops.cpp:9814: `expf(dt_soft_plus * A[i0 + h*nc])`.
157                    let ah = &a[h * d_state..(h + 1) * d_state];
158                    for k in 0..d_state {
159                        let v = s[k] * (dt_sp * ah[k]).exp() + bg[k] * x_dt;
160                        sum += v * cg[k];
161                        s[k] = v;
162                    }
163                }
164            }
165            y[ii] = sum;
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn softplus_is_ggml_s() {
176        assert!((softplus(0.0) - 2f32.ln()).abs() < 1e-7);
177        assert_eq!(softplus(25.0), 25.0);
178        assert!((softplus(-30.0)).abs() < 1e-6);
179    }
180
181    /// Two channels, taps `[1, 2, 3]` and `[0, 0, 1]`: the newest input
182    /// on the LAST tap, the state shifting one row per step.
183    #[test]
184    fn conv_step_puts_the_newest_input_on_the_last_tap_and_shifts() {
185        let taps = [1.0, 2.0, 3.0, 0.0, 0.0, 1.0];
186        let mut state = vec![0.0f32; 2 * 2];
187        let mut out = [0.0f32; 2];
188        conv_step(&mut state, &taps, 3, &[1.0, 5.0], &mut out);
189        assert_eq!(out, [3.0, 5.0]);
190        assert_eq!(state, vec![0.0, 0.0, 1.0, 5.0]);
191        conv_step(&mut state, &taps, 3, &[2.0, 6.0], &mut out);
192        assert_eq!(out, [3.0 * 2.0 + 2.0 * 1.0, 6.0]);
193        conv_step(&mut state, &taps, 3, &[1.0, 7.0], &mut out);
194        assert_eq!(out, [3.0 + 4.0 + 1.0, 7.0]);
195        assert_eq!(state, vec![2.0, 6.0, 1.0, 7.0]);
196    }
197
198    /// One head, one channel, one state: the recurrence by hand.
199    #[test]
200    fn scan_step_is_the_recurrence() {
201        let dims = ScanDims {
202            n_head: 1,
203            head_dim: 1,
204            d_state: 2,
205            n_group: 1,
206        };
207        let mut state = vec![0.0f32; 2];
208        let mut y = [0.0f32];
209        let a = Decay::PerHead(&[-1.0f32]);
210        // dt = 0 -> softplus = ln 2; dA = exp(-ln 2) = 0.5.
211        scan_step(
212            dims,
213            &mut state,
214            &[2.0],
215            &[0.0],
216            a,
217            &[1.0, 3.0],
218            &[1.0, 1.0],
219            &mut y,
220        );
221        let x_dt = 2.0 * 2f32.ln();
222        assert!((state[0] - x_dt).abs() < 1e-6 && (state[1] - 3.0 * x_dt).abs() < 1e-6);
223        assert!((y[0] - 4.0 * x_dt).abs() < 1e-5);
224        scan_step(
225            dims,
226            &mut state,
227            &[0.0],
228            &[0.0],
229            a,
230            &[1.0, 3.0],
231            &[1.0, 0.0],
232            &mut y,
233        );
234        assert!((state[0] - 0.5 * x_dt).abs() < 1e-6, "decayed by dA");
235        assert!((y[0] - 0.5 * x_dt).abs() < 1e-6, "C selects state 0");
236    }
237
238    /// Mamba-1's per-state decay: two states of one head decay
239    /// differently.
240    #[test]
241    fn per_state_decay_is_one_exp_per_state_element() {
242        let dims = ScanDims {
243            n_head: 1,
244            head_dim: 1,
245            d_state: 2,
246            n_group: 1,
247        };
248        let mut state = vec![1.0f32; 2];
249        let mut y = [0.0f32];
250        // dt = 30 -> softplus = 30; dA = exp(30 * A).
251        let a = Decay::PerState(&[0.0, -(2f32.ln()) / 30.0]);
252        scan_step(
253            dims,
254            &mut state,
255            &[0.0],
256            &[30.0],
257            a,
258            &[0.0, 0.0],
259            &[1.0, 1.0],
260            &mut y,
261        );
262        assert!(
263            (state[0] - 1.0).abs() < 1e-6 && (state[1] - 0.5).abs() < 1e-5,
264            "{state:?}"
265        );
266        assert!((y[0] - 1.5).abs() < 1e-5);
267    }
268
269    /// Groups: head `h` reads B/C group `h / (n_head / n_group)`.
270    #[test]
271    fn heads_read_their_group_s_b_and_c() {
272        let dims = ScanDims {
273            n_head: 4,
274            head_dim: 1,
275            d_state: 1,
276            n_group: 2,
277        };
278        let mut state = vec![0.0f32; 4];
279        let mut y = [0.0f32; 4];
280        scan_step(
281            dims,
282            &mut state,
283            &[1.0; 4],
284            &[30.0; 4],
285            Decay::PerHead(&[0.0; 4]),
286            &[1.0, 10.0],
287            &[1.0, 1.0],
288            &mut y,
289        );
290        // softplus(30) = 30, dA = 1: state = B[g] * 30.
291        assert_eq!(y, [30.0, 30.0, 300.0, 300.0]);
292    }
293}