Skip to main content

burn_mamba/mamba3/moments/
recalculated.rs

1//! # Physical-frame moments with a custom, memory-efficient backward
2//!
3//! The forward is the same serial chunkwise computation as
4//! [`Mamba3MomentsInput::state_moments_phys`](super::Mamba3MomentsInput::state_moments_phys),
5//! but routed through the [`Mamba3MomentsBackendExt`] trait so `Autodiff`
6//! backends substitute a **custom recompute backward** (see
7//! [`super::backward`]): the node saves only its five leaf inputs and the tiny
8//! `(m2, m1)` outputs; backprop re-materialises one chunk of states at a time
9//! — the full per-token state trajectory that plain autodiff would retain is
10//! never kept alive. This is the at-scale execution model.
11//!
12//! The default body (every plain backend) runs the identical math on `B`'s
13//! primitives via the rank-tagged [`F`] wrapper. The rotation is passed as a
14//! rank-erased primitive plus a `quaternion` flag (`[b, s, h, a]` cumulative
15//! angles, or `[b, s, h, J, 4]` cumulative unit quaternions), so a single
16//! trait method covers both algebras.
17
18#![allow(non_snake_case)]
19
20use crate::utils::fprim::{F, Mask, san};
21use burn::backend::tensor::{Device, FloatTensor};
22use burn::backend::*;
23use burn::backend::{Backend, backend_extension};
24
25// ---------------------------------------------------------------------------
26// Rotation primitive (rank-erased seam form)
27// ---------------------------------------------------------------------------
28
29/// The per-token cumulative rotation on `B`'s primitives — the rank-tagged
30/// counterpart of [`RotationSeq`](crate::mamba3::rotation::RotationSeq).
31pub(crate) enum RotPrim<B: Backend> {
32    /// Cumulative angles `[batch, sequence, nheads, num_rope_angles]`.
33    Complex {
34        cum_bsha: F<B, 4>,
35        rope_dim: usize,
36        rotate_pairwise: bool,
37    },
38    /// Cumulative unit quaternions `[batch, sequence, nheads, blocks, 4]`.
39    Quaternion { cum_bshj4: F<B, 5> },
40}
41
42impl<B: Backend> RotPrim<B> {
43    /// Wrap the rank-erased seam arguments.
44    pub fn wrap(
45        rot: FloatTensor<B>,
46        quaternion: bool,
47        rope_dim: usize,
48        rotate_pairwise: bool,
49    ) -> Self {
50        if quaternion {
51            RotPrim::Quaternion {
52                cum_bshj4: F::<B, 5>::new(rot),
53            }
54        } else {
55            RotPrim::Complex {
56                cum_bsha: F::<B, 4>::new(rot),
57                rope_dim,
58                rotate_pairwise,
59            }
60        }
61    }
62}
63
64// ---------------------------------------------------------------------------
65// Primitive helpers (shared by the forward default body and the backward)
66// ---------------------------------------------------------------------------
67
68/// Chunk-local decay: `dₜ = exp(a_cum[t])` (`[b, h, l]`) and the
69/// 1-semiseparable mask `L[t, j] = exp(a_cum[t] − a_cum[j])` (`[b, h, l, l]`,
70/// zero strictly above the diagonal).
71pub(crate) fn chunk_decay<B: Backend>(da_blh: F<B, 3>) -> (F<B, 3>, F<B, 4>) {
72    let [batch, chunk_len, nheads] = da_blh.dims();
73    let device = da_blh.device();
74    let a_bhl = da_blh.permute([0, 2, 1]).cumsum(2);
75    let d_bhl = a_bhl.clone().exp();
76    let target_bhll = a_bhl
77        .clone()
78        .unsqueeze_dim::<4>(3)
79        .expand([batch, nheads, chunk_len, chunk_len]);
80    let source_bhll = a_bhl
81        .unsqueeze_dim::<4>(2)
82        .expand([batch, nheads, chunk_len, chunk_len]);
83    let above_diag = Mask::tril_mask(chunk_len, chunk_len, 0, &device)
84        .reshape([1, 1, chunk_len, chunk_len])
85        .expand([batch, nheads, chunk_len, chunk_len]);
86    let l_bhll = (target_bhll - source_bhll)
87        .mask_fill(above_diag, f32::NEG_INFINITY)
88        .exp();
89    (d_bhl, l_bhll)
90}
91
92/// One chunk of cache-frame states
93/// `h̃[t] = dₜ·h₋ + Σ_{j≤t,c} L[t,j]·x̂[j,c] ⊗ b̂[j,c]` — the channel axis is
94/// folded into the write index for one batched GEMM.
95///
96/// # Shapes
97/// - `xhat_blchp` / `bhat_blchr`: one chunk of the combined injections.
98/// - returns `states_bhlpr`.
99pub(crate) fn chunk_states<B: Backend>(
100    xhat_blchp: F<B, 5>,
101    bhat_blchr: F<B, 5>,
102    d_bhl: &F<B, 3>,
103    l_bhll: &F<B, 4>,
104    h_in_bhpr: &F<B, 4>,
105) -> F<B, 5> {
106    let [batch, chunk_len, chan, nheads, per_head_dim] = xhat_blchp.dims();
107    let [.., state_rank] = bhat_blchr.dims();
108
109    let xhat_bh1jcp = xhat_blchp
110        .permute([0, 3, 1, 2, 4]) // [b, h, j, c, p]
111        .unsqueeze_dim::<6>(2); // [b, h, 1, j, c, p]
112    let l_bhtj11 = l_bhll.clone().unsqueeze_dims::<6>(&[4, 5]);
113    let xw_bhtJp = (l_bhtj11
114        * xhat_bh1jcp.expand([batch, nheads, chunk_len, chunk_len, chan, per_head_dim]))
115    .reshape([batch, nheads, chunk_len, chunk_len * chan, per_head_dim]);
116
117    let bhat_bh1Jr = bhat_blchr
118        .permute([0, 3, 1, 2, 4]) // [b, h, j, c, r]
119        .reshape([batch, nheads, 1, chunk_len * chan, state_rank])
120        .expand([batch, nheads, chunk_len, chunk_len * chan, state_rank]);
121
122    let intra_bhtpr = xw_bhtJp.permute([0, 1, 2, 4, 3]).matmul(bhat_bh1Jr);
123
124    let carried_bhtpr = d_bhl
125        .clone()
126        .unsqueeze_dims::<5>(&[3, 4])
127        .expand([batch, nheads, chunk_len, per_head_dim, state_rank])
128        * h_in_bhpr
129            .clone()
130            .unsqueeze_dim::<5>(2)
131            .expand([batch, nheads, chunk_len, per_head_dim, state_rank]);
132    carried_bhtpr + intra_bhtpr
133}
134
135/// The per-token write `W_j = Σ_c x̂[j,c] ⊗ b̂[j,c]` (`[b, h, j, p, r]`) — used
136/// by the backward's `da` gradient.
137pub(crate) fn chunk_writes<B: Backend>(xhat_blchp: F<B, 5>, bhat_blchr: F<B, 5>) -> F<B, 5> {
138    let xh_bhjpc = xhat_blchp.permute([0, 3, 1, 4, 2]); // [b, h, j, p, c]
139    let bh_bhjcr = bhat_blchr.permute([0, 3, 1, 2, 4]); // [b, h, j, c, r]
140    xh_bhjpc.matmul(bh_bhjcr)
141}
142
143/// Quaternion Hamilton product on the packed trailing `(w, x, y, z)` axis
144/// (primitive port of `rotation::quat_mul`).
145pub(crate) fn quat_mul_prim<B: Backend, const D: usize>(a: F<B, D>, b: F<B, D>) -> F<B, D> {
146    let n = D - 1;
147    let comp = |t: &F<B, D>, i: usize| t.clone().narrow(n, i, 1);
148    let (aw, ax, ay, az) = (comp(&a, 0), comp(&a, 1), comp(&a, 2), comp(&a, 3));
149    let (bw, bx, by, bz) = (comp(&b, 0), comp(&b, 1), comp(&b, 2), comp(&b, 3));
150    let w = aw.clone() * bw.clone()
151        - ax.clone() * bx.clone()
152        - ay.clone() * by.clone()
153        - az.clone() * bz.clone();
154    let x = aw.clone() * bx.clone() + ax.clone() * bw.clone() + ay.clone() * bz.clone()
155        - az.clone() * by.clone();
156    let y = aw.clone() * by.clone() - ax.clone() * bz.clone()
157        + ay.clone() * bw.clone()
158        + az.clone() * bx.clone();
159    let z = aw * bz + ax * by - ay * bx + az * bw;
160    F::cat(vec![w, x, y, z], n)
161}
162
163/// Quaternion conjugate on the packed trailing axis.
164pub(crate) fn quat_conj_prim<B: Backend, const D: usize>(q: F<B, D>) -> F<B, D> {
165    let n = D - 1;
166    let w = q.clone().narrow(n, 0, 1);
167    let xyz = q.narrow(n, 1, 3);
168    F::cat(vec![w, -xyz], n)
169}
170
171/// The rotation's per-pair `cos`/`sin` for one chunk, expanded over
172/// `per_head_dim`: `[b, h, l, p, a]` each. `transpose` negates the angle —
173/// `false` applies the **de-rotation** `R(−θ)` (cache → physical), `true` its
174/// transpose `R(+θ)` (used on cotangents).
175fn chunk_cos_sin<B: Backend>(
176    cum_bsha: &F<B, 4>,
177    start: usize,
178    len: usize,
179    per_head_dim: usize,
180    transpose: bool,
181) -> (F<B, 5>, F<B, 5>) {
182    let [batch, _seq, nheads, num_angles] = cum_bsha.dims();
183    let theta_bhlpa = cum_bsha
184        .clone()
185        .narrow(1, start, len)
186        .permute([0, 2, 1, 3]) // [b, h, l, a]
187        .unsqueeze_dim::<5>(3)
188        .expand([batch, nheads, len, per_head_dim, num_angles]);
189    let theta = if transpose { theta_bhlpa } else { -theta_bhlpa };
190    (theta.clone().cos(), theta.sin())
191}
192
193/// Rotate the `state_rank` axis of one chunk of states by the per-token
194/// cumulative rotation. `transpose = false` is the **de-rotation** to the
195/// physical frame (`R(−θ)` / left-multiply by `Q`); `transpose = true` is its
196/// transpose (`R(+θ)` / left-multiply by `conj(Q)`), the map cotangents take
197/// back to the cache frame.
198///
199/// # Shapes
200/// - `v_bhlpr`: `[batch, nheads, len, per_head_dim, state_rank]`
201pub(crate) fn rotate_chunk<B: Backend>(
202    v_bhlpr: F<B, 5>,
203    rot: &RotPrim<B>,
204    start: usize,
205    transpose: bool,
206) -> F<B, 5> {
207    let [batch, nheads, len, per_head_dim, state_rank] = v_bhlpr.dims();
208    match rot {
209        RotPrim::Complex {
210            cum_bsha,
211            rope_dim,
212            rotate_pairwise,
213        } => {
214            let rope_dim = *rope_dim;
215            if rope_dim == 0 {
216                return v_bhlpr;
217            }
218            let (cos, sin) = chunk_cos_sin::<B>(cum_bsha, start, len, per_head_dim, transpose);
219            // R(φ) on a pair (x, y): x' = cos·x − sin·y ; y' = sin·x + cos·y.
220            if *rotate_pairwise {
221                // Interleaved: adjacent pairs within the rotated prefix.
222                let num_angles = rope_dim / 2;
223                let active = v_bhlpr.clone().narrow(4, 0, rope_dim).reshape([
224                    batch,
225                    nheads,
226                    len,
227                    per_head_dim,
228                    num_angles,
229                    2,
230                ]);
231                let x0 = active.clone().narrow(5, 0, 1).squeeze_dim::<5>(5);
232                let x1 = active.narrow(5, 1, 1).squeeze_dim::<5>(5);
233                let x0r = cos.clone() * x0.clone() - sin.clone() * x1.clone();
234                let x1r = sin * x0 + cos * x1;
235                let rotated = F::stack::<6>(vec![x0r, x1r], 5).reshape([
236                    batch,
237                    nheads,
238                    len,
239                    per_head_dim,
240                    rope_dim,
241                ]);
242                if rope_dim == state_rank {
243                    rotated
244                } else {
245                    let tail = v_bhlpr.narrow(4, rope_dim, state_rank - rope_dim);
246                    F::cat(vec![rotated, tail], 4)
247                }
248            } else {
249                // Half-and-half: pair distance is state_rank/2.
250                let half = state_rank / 2;
251                let num_angles = rope_dim / 2;
252                let h1 = v_bhlpr.clone().narrow(4, 0, num_angles);
253                let h2 = v_bhlpr.clone().narrow(4, half, num_angles);
254                let h1r = cos.clone() * h1.clone() - sin.clone() * h2.clone();
255                let h2r = sin * h1 + cos * h2;
256                if num_angles == half {
257                    F::cat(vec![h1r, h2r], 4)
258                } else {
259                    let h1_pass = v_bhlpr.clone().narrow(4, num_angles, half - num_angles);
260                    let h2_pass = v_bhlpr.narrow(4, half + num_angles, half - num_angles);
261                    F::cat(vec![h1r, h1_pass, h2r, h2_pass], 4)
262                }
263            }
264        }
265        RotPrim::Quaternion { cum_bshj4 } => {
266            let blocks = cum_bshj4.dims()[3];
267            let rope_width = 4 * blocks;
268            let q_bhlpj4 = cum_bshj4
269                .clone()
270                .narrow(1, start, len)
271                .permute([0, 2, 1, 3, 4]) // [b, h, l, J, 4]
272                .unsqueeze_dim::<6>(3)
273                .expand([batch, nheads, len, per_head_dim, blocks, 4]);
274            // De-rotation left-multiplies by Q (B/C absorbed conj(Q));
275            // the transpose left-multiplies by conj(Q).
276            let q = if transpose {
277                quat_conj_prim(q_bhlpj4)
278            } else {
279                q_bhlpj4
280            };
281            let active = v_bhlpr.clone().narrow(4, 0, rope_width).reshape([
282                batch,
283                nheads,
284                len,
285                per_head_dim,
286                blocks,
287                4,
288            ]);
289            let rotated = quat_mul_prim(q, active).reshape([
290                batch,
291                nheads,
292                len,
293                per_head_dim,
294                rope_width,
295            ]);
296            if rope_width == state_rank {
297                rotated
298            } else {
299                let tail = v_bhlpr.narrow(4, rope_width, state_rank - rope_width);
300                F::cat(vec![rotated, tail], 4)
301            }
302        }
303    }
304}
305
306/// Float validity mask `[1, 1, l, 1, 1]` for a chunk starting at `start`
307/// (`1` for global positions `< valid_len`, `0` for pads).
308pub(crate) fn chunk_mask<B: Backend>(
309    chunk_len: usize,
310    start: usize,
311    valid_len: usize,
312    device: &Device<B>,
313    dtype: FloatDType,
314) -> F<B, 5> {
315    let valid = valid_len.saturating_sub(start).min(chunk_len);
316    let ones = F::<B, 1>::full([valid], 1.0, device, dtype);
317    let mask = if valid < chunk_len {
318        F::cat(
319            vec![ones, F::<B, 1>::zeros([chunk_len - valid], device, dtype)],
320            0,
321        )
322    } else {
323        ones
324    };
325    mask.reshape([1, 1, chunk_len, 1, 1])
326}
327
328/// The serial chunkwise forward on primitives — the exact math of
329/// [`Mamba3MomentsInput::state_moments_phys`](super::Mamba3MomentsInput::state_moments_phys).
330///
331/// Returns `(m2_bhrr, m1_bhr)`.
332pub(crate) fn moments_phys_fwd<B: Backend>(
333    xhat_bnlchp: F<B, 6>,
334    bhat_bnlchr: F<B, 6>,
335    da_bnlh: F<B, 4>,
336    rot: &RotPrim<B>,
337    initial_state_bhpr: F<B, 4>,
338    valid_len: usize,
339) -> (F<B, 4>, F<B, 3>) {
340    let [batch, nchunks, chunk_len, _chan, nheads, per_head_dim] = xhat_bnlchp.dims();
341    let [.., state_rank] = bhat_bnlchr.dims();
342    let device = xhat_bnlchp.device();
343    let dtype = xhat_bnlchp.dtype();
344
345    let mut h_bhpr = initial_state_bhpr;
346    let mut m2_bhrr = F::<B, 4>::zeros([batch, nheads, state_rank, state_rank], &device, dtype);
347    let mut m1_bhr = F::<B, 3>::zeros([batch, nheads, state_rank], &device, dtype);
348
349    for n in 0..nchunks {
350        let start = n * chunk_len;
351        if start >= valid_len {
352            break;
353        }
354        let da_blh = da_bnlh.clone().narrow(1, n, 1).squeeze_dim::<3>(1);
355        let (d_bhl, l_bhll) = chunk_decay::<B>(da_blh);
356        let xhat_blchp = xhat_bnlchp.clone().narrow(1, n, 1).squeeze_dim::<5>(1);
357        let bhat_blchr = bhat_bnlchr.clone().narrow(1, n, 1).squeeze_dim::<5>(1);
358
359        let states_bhlpr = chunk_states::<B>(xhat_blchp, bhat_blchr, &d_bhl, &l_bhll, &h_bhpr);
360        h_bhpr = states_bhlpr
361            .clone()
362            .narrow(2, chunk_len - 1, 1)
363            .squeeze_dim::<4>(2);
364
365        let phys_bhlpr = rotate_chunk::<B>(states_bhlpr, rot, start, false);
366        let masked_bhlpr =
367            phys_bhlpr * chunk_mask::<B>(chunk_len, start, valid_len, &device, dtype).expand([
368                batch,
369                nheads,
370                chunk_len,
371                per_head_dim,
372                state_rank,
373            ]);
374
375        let masked_bhLr =
376            masked_bhlpr.reshape([batch, nheads, chunk_len * per_head_dim, state_rank]);
377        m2_bhrr = m2_bhrr
378            + masked_bhLr
379                .clone()
380                .permute([0, 1, 3, 2])
381                .matmul(masked_bhLr.clone());
382        m1_bhr = m1_bhr + masked_bhLr.sum_dim(2).squeeze_dim::<3>(2);
383    }
384    san(&m2_bhrr);
385    san(&m1_bhr);
386    (m2_bhrr, m1_bhr)
387}
388
389// ---------------------------------------------------------------------------
390// Backend extension trait
391// ---------------------------------------------------------------------------
392
393/// Extends the backend with the physical-frame state-moments computation.
394///
395/// The default body runs the serial chunkwise forward on primitive tensors.
396/// The `Autodiff` wrapper overrides it with the memory-efficient recompute
397/// backward (in [`super::backward`]) that saves only the leaf inputs.
398#[backend_extension(
399    Cpu:  cfg(feature = "backend-cpu"),
400    Cuda: cfg(feature = "backend-cuda"),
401    Rocm:  cfg(feature = "backend-rocm"),
402    Metal:  cfg(feature = "backend-metal"),
403    Vulkan:  cfg(feature = "backend-vulkan"),
404    Wgpu:  cfg(feature = "backend-wgpu"),
405    WebGpu:  cfg(feature = "backend-webgpu"),
406    Flex:  cfg(feature = "backend-flex"),
407    NdArray:  cfg(feature = "backend-ndarray"),
408    LibTorch:  cfg(any(feature = "backend-tch-cpu", feature = "backend-tch-gpu")),
409    Autodiff:  cfg(feature = "autodiff"),
410)]
411pub trait Mamba3MomentsBackendExt: Backend {
412    /// Pooled physical-frame state moments of the combined injections (see
413    /// `mamba3/moments.rs` for the math). Returns `(m2_bhrr, m1_bhr)` raw
414    /// sums, masked to `valid_len`.
415    ///
416    /// `rot` is rank-erased: cumulative angles `[b, s, h, a]` when
417    /// `quaternion = false` (with `rope_dim` / `rotate_pairwise` selecting the
418    /// pairing), cumulative unit quaternions `[b, s, h, J, 4]` otherwise
419    /// (`rope_dim` / `rotate_pairwise` ignored).
420    #[allow(clippy::too_many_arguments)]
421    fn mamba3_state_moments_phys(
422        xhat_bnlchp: FloatTensor<Self>,
423        bhat_bnlchr: FloatTensor<Self>,
424        da_bnlh: FloatTensor<Self>,
425        rot: FloatTensor<Self>,
426        initial_state_bhpr: FloatTensor<Self>,
427        valid_len: usize,
428        quaternion: bool,
429        rope_dim: usize,
430        rotate_pairwise: bool,
431    ) -> (FloatTensor<Self>, FloatTensor<Self>) {
432        let rot = RotPrim::<Self>::wrap(rot, quaternion, rope_dim, rotate_pairwise);
433        let (m2, m1) = moments_phys_fwd::<Self>(
434            F::<Self, 6>::new(xhat_bnlchp),
435            F::<Self, 6>::new(bhat_bnlchr),
436            F::<Self, 4>::new(da_bnlh),
437            &rot,
438            F::<Self, 4>::new(initial_state_bhpr),
439            valid_len,
440        );
441        (m2.inner(), m1.inner())
442    }
443}
444
445// Per-backend impls delegate to the trait's default body; the custom autodiff
446// backward lives in `super::backward` as a separate `Autodiff<B>` impl.
447crate::impl_ssd_backend_ext_for_burn_backends!(Mamba3MomentsBackendExt);