Skip to main content

burn_mamba/mamba2/ssd/
moments.rs

1//! ## Exact per-token state moments from the chunkwise SSD (no state materialisation)
2//!
3//! A token-by-token `step` loop exposes every SSM state
4//! `hₜ ∈ ℝ^{per_head_dim × state_rank}`; the chunkwise `forward` only
5//! materialises states at **chunk boundaries**. This module computes the
6//! pooled first/second moments of *all* per-token states — everything a
7//! participation-ratio diagnostic (or penalty) needs — **in closed form**,
8//! from tensors the SSD decomposition already builds, without ever
9//! materialising a `[batch, sequence, nheads, per_head_dim, state_rank]`
10//! state tensor.
11//!
12//! ### Derivation
13//!
14//! Within chunk `n` (per `(batch, head)`, positions `t ∈ [0, chunk_len)`),
15//! unrolling the recurrence `hₜ = Āₜhₜ₋₁ + xₜ ⊗ B̄ₜ` from the state `h₋`
16//! entering the chunk gives
17//!
18//! ```text
19//!   hₜ = dₜ·h₋ + sₜ ,   dₜ = exp(a_cum[t]) ,   sₜ = Σ_{j≤t} L[t,j] · xⱼ ⊗ B̄ⱼ
20//! ```
21//!
22//! with `L[t,j] = exp(a_cum[t] − a_cum[j])` — exactly the 1-semiseparable
23//! mask of SSD Step 1. Summing `hₜᵀhₜ` over `t` (the `ᵀ` contraction pools
24//! `per_head_dim`) splits into three **chunk-level** contractions:
25//!
26//! ```text
27//!   Σₜ hₜᵀhₜ = (Σₜ dₜ²)·h₋ᵀh₋                      (carried-state term)
28//!            + h₋ᵀP + Pᵀh₋ ,  P = Xᵀ·diag(w)·B̄ ,  wⱼ = Σₜ dₜ·L[t,j]
29//!            + B̄ᵀ·(K ∘ XXᵀ)·B̄ ,                    K = LᵀL
30//! ```
31//!
32//! and the first moment likewise:
33//! `Σₜ hₜ = (Σₜ dₜ)·h₋ + Xᵀ·diag(u)·B̄`, `uⱼ = Σₜ L[t,j]`. Everything is a
34//! batched GEMM in the same size class as SSD Step 1 (`[chunk_len,
35//! chunk_len]` / `[state_rank, state_rank]` intermediates). The boundary
36//! states `h₋` are recomputed with SSD Steps 2–3, so this function is
37//! **pathway-agnostic** (independent of which SSD variant produced `y`) and
38//! fully differentiable through plain autodiff.
39//!
40//! Zero-pad positions (`Δ=0 ⇒ Ā=1, B̄=0`) carry the state unchanged, so
41//! counting them would replicate the final state into the moments; a
42//! validity mask excludes them from every `Σₜ`.
43
44use crate::mamba2::prelude::*;
45use crate::modules::{StateMoments, sanity as san, segsum};
46use burn::prelude::*;
47
48impl Mamba2SsdInput {
49    /// A value-identical copy detached from any autodiff graph (used by the
50    /// diagnostic wrapper so the moments branch records no backward nodes).
51    pub fn detached(&self) -> Self {
52        Self {
53            x_bnlhp: self.x_bnlhp.clone().detach(),
54            dt_bnlh: self.dt_bnlh.clone().detach(),
55            a_decay_h: self.a_decay_h.clone().detach(),
56            b_bnlhr: self.b_bnlhr.clone().detach(),
57            c_bnlhr: self.c_bnlhr.clone().detach(),
58            d_h: self.d_h.clone().detach(),
59            initial_state_bhpr: self.initial_state_bhpr.clone().detach(),
60            init_state_hpr: self.init_state_hpr.clone().map(Tensor::detach),
61        }
62    }
63
64    /// Exact pooled moments of every per-token SSM state (see the module
65    /// header). `valid_len` is the unpadded token count — states at zero-pad
66    /// positions are excluded. `C`/`D` are not read.
67    ///
68    /// Returns raw sums; `count = valid_len · per_head_dim` samples per
69    /// `(batch, head)` slice.
70    pub fn state_moments(&self, valid_len: usize) -> StateMoments {
71        let [batch, nchunks, chunk_len, nheads, per_head_dim] = self.x_bnlhp.dims();
72        let [.., state_rank] = self.b_bnlhr.dims();
73        let device = &self.x_bnlhp.device();
74        assert!(
75            (1..=nchunks * chunk_len).contains(&valid_len),
76            "valid_len must be within the (padded) sequence"
77        );
78
79        // ── Discretised parameters (as in `ssd_minimal`) ──────────────────────
80        // B̄ₜ = Δₜ·Bₜ ; log-decay a = Δₜ·A (negative).
81        let delta_b_bnlhr = self.dt_bnlh.clone().unsqueeze_dim(4) * self.b_bnlhr.clone();
82        let a_bnlh = self.dt_bnlh.clone()
83            * self
84                .a_decay_h
85                .clone()
86                .unsqueeze_dims::<4>(&[0, 1, 2])
87                .expand([batch, nchunks, chunk_len, nheads]);
88        let a_bhnl = a_bnlh.permute([0, 3, 1, 2]);
89        let a_cumsum_bhnl = a_bhnl.clone().cumsum(3);
90        san(&a_cumsum_bhnl);
91
92        // ── Boundary states h₋ entering each chunk (SSD Steps 2–3) ────────────
93        let state_in_bnhpr = {
94            // Step 2: per-chunk end state from a zero start.
95            let a_cumsum_last_bhn1 = a_cumsum_bhnl.clone().slice(s![.., .., .., -1]);
96            let decay_state_bhnl = (a_cumsum_last_bhn1.clone() - a_cumsum_bhnl.clone()).exp();
97            let decay_state_bnlh1 = decay_state_bhnl.permute([0, 2, 3, 1]).unsqueeze_dim(4);
98            let decayed_x_bnhpl = (decay_state_bnlh1 * self.x_bnlhp.clone())
99                .permute([0, 1, 3, 4, 2]);
100            let state_bnhpr = decayed_x_bnhpl
101                .matmul(delta_b_bnlhr.clone().permute([0, 1, 3, 2, 4]));
102
103            // Step 3: inter-chunk state passing (segsum over chunks).
104            let initial_state_b1hpr = self.initial_state_bhpr.clone().unsqueeze_dim(1);
105            let initial_state_b1hpr = if let Some(init_hpr) = &self.init_state_hpr {
106                let init_b1hpr = init_hpr.clone().unsqueeze_dim::<4>(0).expand([
107                    batch,
108                    1,
109                    nheads,
110                    per_head_dim,
111                    state_rank,
112                ]);
113                initial_state_b1hpr + init_b1hpr
114            } else {
115                initial_state_b1hpr
116            };
117            let state_bNhpr = Tensor::cat(vec![initial_state_b1hpr, state_bnhpr], 1);
118            let a_chunk_pad_bhN = Tensor::cat(
119                vec![
120                    Tensor::<3>::zeros(Shape::new([batch, nheads, 1]), device),
121                    a_cumsum_last_bhn1.squeeze_dim::<3>(3),
122                ],
123                2,
124            );
125            let decay_chunk_bhNN = segsum::<3, 4>(a_chunk_pad_bhN).exp();
126            let flat_state_dim = per_head_dim * state_rank;
127            let state_bhNf = state_bNhpr
128                .permute([0, 2, 1, 3, 4])
129                .reshape([batch, nheads, 1 + nchunks, flat_state_dim]);
130            let new_state_bhNf = decay_chunk_bhNN.matmul(state_bhNf);
131            let new_state_bhNpr =
132                new_state_bhNf.reshape([batch, nheads, 1 + nchunks, per_head_dim, state_rank]);
133            // Keep the state *entering* each chunk (drop the final state).
134            new_state_bhNpr
135                .slice(s![.., .., 0..nchunks, .., ..])
136                .permute([0, 2, 1, 3, 4]) // state_in_bnhpr
137        };
138        san(&state_in_bnhpr);
139
140        // ── Validity mask over the `t` axis (zero-pad positions excluded) ─────
141        let mask_11nl = Tensor::<1, Int>::arange(0..(nchunks * chunk_len) as i64, device)
142            .lower_elem(valid_len as i64)
143            .float()
144            .reshape([1, 1, nchunks, chunk_len]);
145
146        // dₜ = exp(a_cum[t]) and the Step-1 mask L, both masked over `t`.
147        let d_bhnl = a_cumsum_bhnl.exp() * mask_11nl.clone();
148        let l_bhnll = segsum::<4, 5>(a_bhnl).exp() * mask_11nl.unsqueeze_dim::<5>(4);
149        san(&d_bhnl);
150        san(&l_bhnll);
151
152        // ── Per-chunk `Σₜ` reductions (all over the masked `t` axis) ──────────
153        let sd1_bhn = d_bhnl.clone().sum_dim(3).squeeze_dim::<3>(3); // Σₜ dₜ
154        let sd2_bhn = d_bhnl
155            .clone()
156            .powf_scalar(2.0)
157            .sum_dim(3)
158            .squeeze_dim::<3>(3); // Σₜ dₜ²
159        // wⱼ = Σₜ dₜ·L[t,j] (a row-vector × matrix product).
160        let w_bhnl = d_bhnl
161            .unsqueeze_dim::<5>(3)
162            .matmul(l_bhnll.clone())
163            .squeeze_dim::<4>(3);
164        // uⱼ = Σₜ L[t,j].
165        let u_bhnl = l_bhnll.clone().sum_dim(3).squeeze_dim::<4>(3);
166        // K[j,j'] = Σₜ L[t,j]·L[t,j'] = LᵀL.
167        let k_bhnll = l_bhnll.clone().permute([0, 1, 2, 4, 3]).matmul(l_bhnll);
168
169        // ── Assemble the three second-moment terms per chunk ──────────────────
170        let x_bnhpl = self.x_bnlhp.clone().permute([0, 1, 3, 4, 2]);
171        let bbar_bnhlr = delta_b_bnlhr.permute([0, 1, 3, 2, 4]);
172        let state_in_bnhrp = state_in_bnhpr.clone().permute([0, 1, 2, 4, 3]);
173
174        // Input² term: B̄ᵀ·(K ∘ XXᵀ)·B̄.
175        let gram_x_bnhll = x_bnhpl.clone().permute([0, 1, 2, 4, 3]).matmul(x_bnhpl.clone());
176        let kg_bnhll = k_bhnll.permute([0, 2, 1, 3, 4]) * gram_x_bnhll;
177        let term_input_bnhrr = bbar_bnhlr
178            .clone()
179            .permute([0, 1, 2, 4, 3])
180            .matmul(kg_bnhll)
181            .matmul(bbar_bnhlr.clone());
182
183        // Cross term: h₋ᵀP + Pᵀh₋, P = Xᵀ·diag(w)·B̄.
184        let w_bnhl1 = w_bhnl.permute([0, 2, 1, 3]).unsqueeze_dim::<5>(4);
185        let p_bnhpr = x_bnhpl.clone().matmul(w_bnhl1 * bbar_bnhlr.clone());
186        let cross_bnhrr = state_in_bnhrp.clone().matmul(p_bnhpr);
187        let term_cross_bnhrr = cross_bnhrr.clone() + cross_bnhrr.permute([0, 1, 2, 4, 3]);
188
189        // Carried-state term: (Σₜ dₜ²)·h₋ᵀh₋.
190        let sd2_bnh11 = sd2_bhn.permute([0, 2, 1]).unsqueeze_dims::<5>(&[3, 4]);
191        let term_carry_bnhrr = sd2_bnh11 * state_in_bnhrp.matmul(state_in_bnhpr.clone());
192
193        let m2_bhrr = (term_carry_bnhrr + term_cross_bnhrr + term_input_bnhrr)
194            .sum_dim(1)
195            .squeeze_dim::<4>(1);
196        san(&m2_bhrr);
197
198        // ── First moment: Σₜ hₜ = (Σₜ dₜ)·h₋ + Xᵀ·diag(u)·B̄, then pool `p` ────
199        let sd1_bnh11 = sd1_bhn.permute([0, 2, 1]).unsqueeze_dims::<5>(&[3, 4]);
200        let u_bnhl1 = u_bhnl.permute([0, 2, 1, 3]).unsqueeze_dim::<5>(4);
201        let sum_h_bnhpr = sd1_bnh11 * state_in_bnhpr + x_bnhpl.matmul(u_bnhl1 * bbar_bnhlr);
202        let m1_bhr = sum_h_bnhpr
203            .sum_dim(1)
204            .sum_dim(3)
205            .reshape([batch, nheads, state_rank]);
206        san(&m1_bhr);
207
208        StateMoments {
209            m2_bhrr,
210            m1_bhr,
211            count: valid_len * per_head_dim,
212        }
213    }
214}
215
216#[cfg(all(test, feature = "_dev-test"))]
217mod tests;