Skip to main content

burn_mamba/mamba3/moments/
backward.rs

1//! # Custom autodiff node for the physical-frame state moments
2//!
3//! Implements [`Mamba3MomentsBackendExt`] for `Autodiff<B>` via a single Burn
4//! [`Backward`] node. The forward saves only its five leaf inputs; backprop
5//! re-materialises **one chunk of states at a time** (the `SerialRecalculated`
6//! discipline) and evaluates the analytic VJPs, so the full per-token state
7//! trajectory plain autodiff would retain is never kept alive.
8//!
9//! ## Gradient math (per chunk, reverse order, cotangent carry `d_h₊`)
10//!
11//! Forward: `h̃[t] = dₜ·h₋ + Σⱼ L[t,j]·Wⱼ` with writes `Wⱼ = Σ_c x̂ⱼ_c ⊗ b̂ⱼ_c`,
12//! `cₜ = Rₜ†·h̃[t]` (de-rotation), `m2 += Σₜ maskₜ·cₜᵀcₜ`, `m1 += Σₜ maskₜ·cₜ`.
13//!
14//! - `d_c[t] = maskₜ·(cₜ·(d_m2 + d_m2ᵀ) + d_m1)` — the moments' VJP.
15//! - `d_h̃[t] = Rₜ·d_c[t]` (the de-rotation's transpose), plus the **rotation's
16//!   own gradient**: per complex pair, `∂cₜ/∂θ = (c_y, −c_x)`, so
17//!   `d_θ = Σ_p (d_c_x·c_y − d_c_y·c_x)`; per quaternion block (`c = Q ⊗ h̃`),
18//!   `d_Q = Σ_p d_c ⊗ conj(h̃)` (bilinear-product VJP, exact for all
19//!   quaternions). This is what lets a PR penalty shape the rotation itself.
20//! - `G[t] = d_h̃[t] + δ_{t,l−1}·d_h₊` (the next chunk consumed `h̃[l−1]` as
21//!   its `h₋`).
22//! - `d_h₋ = Σₜ dₜ·G[t]` (the new carry), `d_Wⱼ = Σₜ L[t,j]·G[t]`, then
23//!   `d_x̂ⱼ_c = d_Wⱼ·b̂ⱼ_c`, `d_b̂ⱼ_c = d_Wⱼᵀ·x̂ⱼ_c`.
24//! - `d_da`: through `dₜ = exp(Σ_{s≤t} da_s)` and `L[t,j] = exp(Σ_{j<s≤t} da_s)`,
25//!   with `scalₜ = ⟨G[t], h₋⟩·dₜ` and `A[t,j] = ⟨G[t], Wⱼ⟩·L[t,j]`:
26//!   `d_da_s = Σ_{t≥s} scalₜ + Σ_{j<s≤t} A[t,j]`
27//!   `      = rev_cumsum(scal + Σⱼ A[·,j])_s − rev_cumsum(Σₜ A[t,·])_s`
28//!   (the pair `(t, j)` contributes to exactly the positions `j < s ≤ t`, and
29//!   `1[s≤t] − 1[s≤j]` is that indicator; `A` is lower-triangular).
30//! - The boundary carries `h₋(n)` are recomputed by a cheap chunk-level pass
31//!   (decay-to-chunk-end GEMM, `l×` cheaper than the states GEMM).
32//!
33//! Chunks entirely past `valid_len` were never entered by the forward
34//! (`break`), so their gradients are zero and the reverse loop skips them.
35//!
36//! The two outputs `(m2, m1)` are flattened into one tracked tensor via
37//! [`crate::utils::combined_grad`] so a single `Backward<B, 5>` node covers
38//! both.
39
40#![allow(non_snake_case)]
41
42use super::recalculated::{
43    Mamba3MomentsBackendExt, RotPrim, chunk_decay, chunk_mask, chunk_states, chunk_writes,
44    quat_conj_prim, quat_mul_prim, rotate_chunk,
45};
46use crate::utils::fprim::F;
47use burn::backend::autodiff::{
48    Autodiff,
49    checkpoint::{base::Checkpointer, strategy::CheckpointStrategy},
50    grads::Gradients,
51    ops::{Backward, Ops, OpsKind},
52};
53use burn::backend::tensor::FloatTensor;
54use burn::backend::{Backend, BackendTypes};
55
56/// All input gradients of the moments node.
57struct MomentsGrads<B: Backend> {
58    d_xhat_bnlchp: F<B, 6>,
59    d_bhat_bnlchr: F<B, 6>,
60    d_da_bnlh: F<B, 4>,
61    d_rot: <B as BackendTypes>::FloatTensorPrimitive,
62    d_initial_bhpr: F<B, 4>,
63}
64
65/// The per-pair angle gradient of the de-rotation (see the module header).
66fn d_theta_chunk<B: Backend>(
67    phys_bhlpr: &F<B, 5>,
68    d_phys_bhlpr: &F<B, 5>,
69    num_angles_total: usize,
70    rope_dim: usize,
71    rotate_pairwise: bool,
72) -> F<B, 4> {
73    let [batch, nheads, len, _p, state_rank] = phys_bhlpr.dims();
74    if rope_dim == 0 {
75        let device = phys_bhlpr.device();
76        let dtype = phys_bhlpr.dtype();
77        return F::<B, 4>::zeros([batch, nheads, len, num_angles_total], &device, dtype);
78    }
79    let num_angles = rope_dim / 2;
80    let (px, py, dx, dy) = if rotate_pairwise {
81        let split = |t: &F<B, 5>| {
82            let a = t.clone().narrow(4, 0, rope_dim).reshape([
83                batch,
84                nheads,
85                len,
86                phys_bhlpr.dims()[3],
87                num_angles,
88                2,
89            ]);
90            (
91                a.clone().narrow(5, 0, 1).squeeze_dim::<5>(5),
92                a.narrow(5, 1, 1).squeeze_dim::<5>(5),
93            )
94        };
95        let (px, py) = split(phys_bhlpr);
96        let (dx, dy) = split(d_phys_bhlpr);
97        (px, py, dx, dy)
98    } else {
99        let half = state_rank / 2;
100        (
101            phys_bhlpr.clone().narrow(4, 0, num_angles),
102            phys_bhlpr.clone().narrow(4, half, num_angles),
103            d_phys_bhlpr.clone().narrow(4, 0, num_angles),
104            d_phys_bhlpr.clone().narrow(4, half, num_angles),
105        )
106    };
107    // ∂c/∂θ = (c_y, −c_x) ⇒ d_θ = Σ_p (d_c_x·c_y − d_c_y·c_x).
108    (dx * py - dy * px).sum_dim(3).squeeze_dim::<4>(3)
109}
110
111/// The per-block quaternion gradient of the de-rotation (`c = Q ⊗ h̃` ⇒
112/// `d_Q = Σ_p d_c ⊗ conj(h̃)`).
113fn d_quat_chunk<B: Backend>(
114    states_bhlpr: &F<B, 5>,
115    d_phys_bhlpr: &F<B, 5>,
116    blocks: usize,
117) -> F<B, 5> {
118    let [batch, nheads, len, per_head_dim, _r] = states_bhlpr.dims();
119    let rope_width = 4 * blocks;
120    let as_blocks = |t: &F<B, 5>| {
121        t.clone().narrow(4, 0, rope_width).reshape([
122            batch,
123            nheads,
124            len,
125            per_head_dim,
126            blocks,
127            4,
128        ])
129    };
130    quat_mul_prim(as_blocks(d_phys_bhlpr), quat_conj_prim(as_blocks(states_bhlpr)))
131        .sum_dim(3)
132        .squeeze_dim::<5>(3)
133}
134
135/// Cotangent scatter: `[b,h,p,r]` placed at the last position of a
136/// `[b,h,l,p,r]` chunk (zeros elsewhere).
137fn scatter_last<B: Backend>(d_carry_bhpr: F<B, 4>, chunk_len: usize) -> F<B, 5> {
138    let [batch, nheads, per_head_dim, state_rank] = d_carry_bhpr.dims();
139    let device = d_carry_bhpr.device();
140    let dtype = d_carry_bhpr.dtype();
141    let dc = d_carry_bhpr.unsqueeze_dim::<5>(2);
142    if chunk_len == 1 {
143        dc
144    } else {
145        F::cat(
146            vec![
147                F::<B, 5>::zeros(
148                    [batch, nheads, chunk_len - 1, per_head_dim, state_rank],
149                    &device,
150                    dtype,
151                ),
152                dc,
153            ],
154            2,
155        )
156    }
157}
158
159/// Suffix sum along `dim` (`out[s] = Σ_{t≥s} in[t]`).
160fn rev_cumsum<B: Backend, const D: usize>(t: F<B, D>, dim: usize) -> F<B, D> {
161    t.flip(&[dim]).cumsum(dim).flip(&[dim])
162}
163
164/// Recompute the boundary carries `h₋(n)` entering each of the `processed`
165/// chunks (a chunk-level pass: decay-to-chunk-end GEMM + serial accumulation).
166fn boundary_carries<B: Backend>(
167    xhat_bnlchp: &F<B, 6>,
168    bhat_bnlchr: &F<B, 6>,
169    da_bnlh: &F<B, 4>,
170    initial_state_bhpr: &F<B, 4>,
171    processed: usize,
172) -> Vec<F<B, 4>> {
173    let [batch, _n, chunk_len, chan, nheads, per_head_dim] = xhat_bnlchp.dims();
174    let [.., state_rank] = bhat_bnlchr.dims();
175
176    let mut h_bhpr = initial_state_bhpr.clone();
177    let mut carries = Vec::with_capacity(processed);
178    for n in 0..processed {
179        carries.push(h_bhpr.clone());
180        if n + 1 == processed {
181            break;
182        }
183        let a_bhl = da_bnlh
184            .clone()
185            .narrow(1, n, 1)
186            .squeeze_dim::<3>(1)
187            .permute([0, 2, 1])
188            .cumsum(2);
189        let a_last_bh1 = a_bhl.clone().narrow(2, chunk_len - 1, 1);
190        let scale_bhj = (a_last_bh1
191            .clone()
192            .expand([batch, nheads, chunk_len])
193            - a_bhl)
194            .exp();
195        let xh_bhjcp = xhat_bnlchp
196            .clone()
197            .narrow(1, n, 1)
198            .squeeze_dim::<5>(1)
199            .permute([0, 3, 1, 2, 4]);
200        let bh_bhJr = bhat_bnlchr
201            .clone()
202            .narrow(1, n, 1)
203            .squeeze_dim::<5>(1)
204            .permute([0, 3, 1, 2, 4])
205            .reshape([batch, nheads, chunk_len * chan, state_rank]);
206        let xw_bhJp = (xh_bhjcp
207            * scale_bhj
208                .unsqueeze_dims::<5>(&[3, 4])
209                .expand([batch, nheads, chunk_len, chan, per_head_dim]))
210        .reshape([batch, nheads, chunk_len * chan, per_head_dim]);
211        let contribution_bhpr = xw_bhJp.permute([0, 1, 3, 2]).matmul(bh_bhJr);
212        let d_last_bhpr = a_last_bh1
213            .exp()
214            .unsqueeze_dim::<4>(3)
215            .expand([batch, nheads, per_head_dim, state_rank]);
216        h_bhpr = d_last_bhpr * h_bhpr + contribution_bhpr;
217    }
218    carries
219}
220
221/// The full recompute backward (see the module header).
222#[allow(clippy::too_many_arguments)]
223fn moments_phys_bwd<B: Backend>(
224    d_m2_bhrr: F<B, 4>,
225    d_m1_bhr: F<B, 3>,
226    xhat_bnlchp: F<B, 6>,
227    bhat_bnlchr: F<B, 6>,
228    da_bnlh: F<B, 4>,
229    rot: RotPrim<B>,
230    initial_state_bhpr: F<B, 4>,
231    valid_len: usize,
232) -> MomentsGrads<B> {
233    let [batch, nchunks, chunk_len, chan, nheads, per_head_dim] = xhat_bnlchp.dims();
234    let [.., state_rank] = bhat_bnlchr.dims();
235    let device = xhat_bnlchp.device();
236    let dtype = xhat_bnlchp.dtype();
237    let processed = valid_len.div_ceil(chunk_len).min(nchunks);
238
239    let h_ins = boundary_carries::<B>(
240        &xhat_bnlchp,
241        &bhat_bnlchr,
242        &da_bnlh,
243        &initial_state_bhpr,
244        processed,
245    );
246    let d_m2sym_bhrr = d_m2_bhrr.clone() + d_m2_bhrr.permute([0, 1, 3, 2]);
247
248    let zero_dx = || F::<B, 5>::zeros([batch, chunk_len, chan, nheads, per_head_dim], &device, dtype);
249    let zero_db = || F::<B, 5>::zeros([batch, chunk_len, chan, nheads, state_rank], &device, dtype);
250    let zero_dda = || F::<B, 3>::zeros([batch, chunk_len, nheads], &device, dtype);
251    let mut d_xhat_chunks: Vec<F<B, 5>> = (processed..nchunks).map(|_| zero_dx()).collect();
252    let mut d_bhat_chunks: Vec<F<B, 5>> = (processed..nchunks).map(|_| zero_db()).collect();
253    let mut d_da_chunks: Vec<F<B, 3>> = (processed..nchunks).map(|_| zero_dda()).collect();
254    // d_rot chunks, kept rank-erased (angles [b,l,h,a] | quats [b,l,h,J,4]).
255    let mut d_rot_chunks_rev: Vec<<B as BackendTypes>::FloatTensorPrimitive> = Vec::new();
256
257    let mut d_carry_bhpr = F::<B, 4>::zeros([batch, nheads, per_head_dim, state_rank], &device, dtype);
258
259    for n in (0..processed).rev() {
260        let start = n * chunk_len;
261        // ── Recompute this chunk's states and physical states ────────────────
262        let da_blh = da_bnlh.clone().narrow(1, n, 1).squeeze_dim::<3>(1);
263        let (d_bhl, l_bhll) = chunk_decay::<B>(da_blh);
264        let xhat_blchp = xhat_bnlchp.clone().narrow(1, n, 1).squeeze_dim::<5>(1);
265        let bhat_blchr = bhat_bnlchr.clone().narrow(1, n, 1).squeeze_dim::<5>(1);
266        let states_bhlpr =
267            chunk_states::<B>(xhat_blchp.clone(), bhat_blchr.clone(), &d_bhl, &l_bhll, &h_ins[n]);
268        let phys_bhlpr = rotate_chunk::<B>(states_bhlpr.clone(), &rot, start, false);
269
270        // ── Moments VJP: d_c = mask·(c·(d_m2 + d_m2ᵀ) + d_m1) ────────────────
271        let mask_bhlpr = chunk_mask::<B>(chunk_len, start, valid_len, &device, dtype).expand([
272            batch,
273            nheads,
274            chunk_len,
275            per_head_dim,
276            state_rank,
277        ]);
278        let d_phys_bhlpr = (phys_bhlpr.clone().matmul(
279            d_m2sym_bhrr
280                .clone()
281                .unsqueeze_dim::<5>(2)
282                .expand([batch, nheads, chunk_len, state_rank, state_rank]),
283        ) + d_m1_bhr
284            .clone()
285            .unsqueeze_dims::<5>(&[2, 3])
286            .expand([batch, nheads, chunk_len, per_head_dim, state_rank]))
287            * mask_bhlpr;
288
289        // ── Rotation VJP: cotangent back to the cache frame + d_rot ──────────
290        let d_states_bhlpr = rotate_chunk::<B>(d_phys_bhlpr.clone(), &rot, start, true);
291        let d_rot_chunk = match &rot {
292            RotPrim::Complex {
293                cum_bsha,
294                rope_dim,
295                rotate_pairwise,
296            } => {
297                let num_angles_total = cum_bsha.dims()[3];
298                d_theta_chunk::<B>(
299                    &phys_bhlpr,
300                    &d_phys_bhlpr,
301                    num_angles_total,
302                    *rope_dim,
303                    *rotate_pairwise,
304                )
305                .permute([0, 2, 1, 3]) // [b, l, h, a]
306                .inner()
307            }
308            RotPrim::Quaternion { cum_bshj4 } => {
309                let blocks = cum_bshj4.dims()[3];
310                d_quat_chunk::<B>(&states_bhlpr, &d_phys_bhlpr, blocks)
311                    .permute([0, 2, 1, 3, 4]) // [b, l, h, J, 4]
312                    .inner()
313            }
314        };
315        d_rot_chunks_rev.push(d_rot_chunk);
316
317        // ── Chunk-state cotangent (moments + the next chunk's h₋ use) ────────
318        let g_bhlpr = d_states_bhlpr + scatter_last::<B>(d_carry_bhpr.clone(), chunk_len);
319
320        // ── d_h₋ (the new carry) ──────────────────────────────────────────────
321        d_carry_bhpr = (g_bhlpr.clone()
322            * d_bhl
323                .clone()
324                .unsqueeze_dims::<5>(&[3, 4])
325                .expand([batch, nheads, chunk_len, per_head_dim, state_rank]))
326        .sum_dim(2)
327        .squeeze_dim::<4>(2);
328
329        // ── d_W, then d_x̂ / d_b̂ ─────────────────────────────────────────────
330        let g_bhtPR = g_bhlpr
331            .clone()
332            .reshape([batch, nheads, chunk_len, per_head_dim * state_rank]);
333        let dw_bhjpr = l_bhll
334            .clone()
335            .permute([0, 1, 3, 2])
336            .matmul(g_bhtPR.clone())
337            .reshape([batch, nheads, chunk_len, per_head_dim, state_rank]);
338        let xh_bhjcp = xhat_blchp.clone().permute([0, 3, 1, 2, 4]);
339        let bh_bhjcr = bhat_blchr.clone().permute([0, 3, 1, 2, 4]);
340        let d_xh_bhjcp = bh_bhjcr.matmul(dw_bhjpr.clone().permute([0, 1, 2, 4, 3]));
341        let d_bh_bhjcr = xh_bhjcp.matmul(dw_bhjpr);
342        d_xhat_chunks.push(d_xh_bhjcp.permute([0, 2, 3, 1, 4])); // [b, l, c, h, p]
343        d_bhat_chunks.push(d_bh_bhjcr.permute([0, 2, 3, 1, 4])); // [b, l, c, h, r]
344
345        // ── d_da ──────────────────────────────────────────────────────────────
346        let w_bhjpr = chunk_writes::<B>(xhat_blchp, bhat_blchr);
347        let scal_bhl = (g_bhlpr
348            * h_ins[n]
349                .clone()
350                .unsqueeze_dim::<5>(2)
351                .expand([batch, nheads, chunk_len, per_head_dim, state_rank]))
352        .sum_dim(4)
353        .sum_dim(3)
354        .reshape([batch, nheads, chunk_len])
355            * d_bhl;
356        let gw_bhtj = g_bhtPR.matmul(
357            w_bhjpr
358                .reshape([batch, nheads, chunk_len, per_head_dim * state_rank])
359                .permute([0, 1, 3, 2]),
360        );
361        let a_bhtj = gw_bhtj * l_bhll;
362        let row_bhl = a_bhtj.clone().sum_dim(3).squeeze_dim::<3>(3);
363        let col_bhl = a_bhtj.sum_dim(2).squeeze_dim::<3>(2);
364        let d_da_bhl = rev_cumsum::<B, 3>(scal_bhl + row_bhl, 2) - rev_cumsum::<B, 3>(col_bhl, 2);
365        d_da_chunks.push(d_da_bhl.permute([0, 2, 1])); // [b, l, h]
366    }
367
368    // The reverse loop pushed processed chunks newest-first after the zero pads
369    // for the skipped tail — restore forward chunk order.
370    fn reorder<T>(mut v: Vec<T>, nchunks: usize, processed: usize) -> Vec<T> {
371        let tail = v.split_off(nchunks - processed); // the processed chunks, reversed
372        let mut ordered: Vec<T> = tail.into_iter().rev().collect();
373        ordered.extend(v); // the zero chunks for the skipped tail
374        ordered
375    }
376
377    let d_xhat_bnlchp = F::stack::<6>(reorder(d_xhat_chunks, nchunks, processed), 1);
378    let d_bhat_bnlchr = F::stack::<6>(reorder(d_bhat_chunks, nchunks, processed), 1);
379    let d_da_bnlh = F::stack::<4>(reorder(d_da_chunks, nchunks, processed), 1);
380
381    // d_rot: processed chunks (reversed) followed by zero pads to the full
382    // (padded) sequence length, concatenated along the seq axis.
383    let d_rot = {
384        let pad = (nchunks - processed) * chunk_len;
385        match &rot {
386            RotPrim::Complex { cum_bsha, .. } => {
387                let num_angles = cum_bsha.dims()[3];
388                let mut chunks: Vec<F<B, 4>> = d_rot_chunks_rev
389                    .into_iter()
390                    .rev()
391                    .map(F::<B, 4>::new)
392                    .collect();
393                if pad > 0 {
394                    chunks.push(F::<B, 4>::zeros(
395                        [batch, pad, nheads, num_angles],
396                        &device,
397                        dtype,
398                    ));
399                }
400                F::cat(chunks, 1).inner()
401            }
402            RotPrim::Quaternion { cum_bshj4 } => {
403                let blocks = cum_bshj4.dims()[3];
404                let mut chunks: Vec<F<B, 5>> = d_rot_chunks_rev
405                    .into_iter()
406                    .rev()
407                    .map(F::<B, 5>::new)
408                    .collect();
409                if pad > 0 {
410                    chunks.push(F::<B, 5>::zeros(
411                        [batch, pad, nheads, blocks, 4],
412                        &device,
413                        dtype,
414                    ));
415                }
416                F::cat(chunks, 1).inner()
417            }
418        }
419    };
420
421    MomentsGrads {
422        d_xhat_bnlchp,
423        d_bhat_bnlchr,
424        d_da_bnlh,
425        d_rot,
426        d_initial_bhpr: d_carry_bhpr,
427    }
428}
429
430impl<B: Backend + Mamba3MomentsBackendExt, C: CheckpointStrategy> Mamba3MomentsBackendExt
431    for Autodiff<B, C>
432{
433    fn mamba3_state_moments_phys(
434        xhat_bnlchp: FloatTensor<Self>,
435        bhat_bnlchr: FloatTensor<Self>,
436        da_bnlh: FloatTensor<Self>,
437        rot: FloatTensor<Self>,
438        initial_state_bhpr: FloatTensor<Self>,
439        valid_len: usize,
440        quaternion: bool,
441        rope_dim: usize,
442        rotate_pairwise: bool,
443    ) -> (FloatTensor<Self>, FloatTensor<Self>) {
444        // ── Backward node ────────────────────────────────────────────────────
445        #[derive(Debug)]
446        struct MomentsBackward;
447
448        #[derive(Clone, Debug)]
449        struct State<B: Backend> {
450            xhat: <B as BackendTypes>::FloatTensorPrimitive,
451            bhat: <B as BackendTypes>::FloatTensorPrimitive,
452            da: <B as BackendTypes>::FloatTensorPrimitive,
453            rot: <B as BackendTypes>::FloatTensorPrimitive,
454            initial: <B as BackendTypes>::FloatTensorPrimitive,
455            flat_len_m2: usize,
456            flat_len_m1: usize,
457            shape_m2: [usize; 4],
458            shape_m1: [usize; 3],
459            valid_len: usize,
460            quaternion: bool,
461            rope_dim: usize,
462            rotate_pairwise: bool,
463        }
464
465        impl<B: Backend + Mamba3MomentsBackendExt> Backward<B, 5> for MomentsBackward {
466            type State = State<B>;
467
468            fn backward(
469                self,
470                ops: Ops<Self::State, 5>,
471                grads: &mut Gradients,
472                _checkpointer: &mut Checkpointer,
473            ) {
474                let [node_xhat, node_bhat, node_da, node_rot, node_initial] = ops.parents;
475                let d_combined: <B as BackendTypes>::FloatTensorPrimitive =
476                    grads.consume::<B>(&ops.node);
477
478                let State {
479                    xhat,
480                    bhat,
481                    da,
482                    rot,
483                    initial,
484                    flat_len_m2,
485                    flat_len_m1,
486                    shape_m2,
487                    shape_m1,
488                    valid_len,
489                    quaternion,
490                    rope_dim,
491                    rotate_pairwise,
492                } = ops.state;
493
494                let (d_m2, d_m1) = crate::utils::combined_grad::unflatten_pair::<B, 4, 3>(
495                    d_combined,
496                    flat_len_m2,
497                    flat_len_m1,
498                    shape_m2,
499                    shape_m1,
500                );
501
502                let grads_all = moments_phys_bwd::<B>(
503                    F::<B, 4>::new(d_m2),
504                    F::<B, 3>::new(d_m1),
505                    F::<B, 6>::new(xhat),
506                    F::<B, 6>::new(bhat),
507                    F::<B, 4>::new(da),
508                    RotPrim::<B>::wrap(rot, quaternion, rope_dim, rotate_pairwise),
509                    F::<B, 4>::new(initial),
510                    valid_len,
511                );
512
513                if let Some(n) = node_xhat {
514                    grads.register::<B>(n.id, grads_all.d_xhat_bnlchp.inner());
515                }
516                if let Some(n) = node_bhat {
517                    grads.register::<B>(n.id, grads_all.d_bhat_bnlchr.inner());
518                }
519                if let Some(n) = node_da {
520                    grads.register::<B>(n.id, grads_all.d_da_bnlh.inner());
521                }
522                if let Some(n) = node_rot {
523                    grads.register::<B>(n.id, grads_all.d_rot);
524                }
525                if let Some(n) = node_initial {
526                    grads.register::<B>(n.id, grads_all.d_initial_bhpr.inner());
527                }
528            }
529        }
530
531        // ── Shape extraction ─────────────────────────────────────────────────
532        use burn::backend::TensorMetadata;
533        let [batch, _n, _l, _c, nheads, _p] = xhat_bnlchp.primitive.shape().dims();
534        let [.., state_rank] = bhat_bnlchr.primitive.shape().dims::<6>();
535        let shape_m2: [usize; 4] = [batch, nheads, state_rank, state_rank];
536        let shape_m1: [usize; 3] = [batch, nheads, state_rank];
537        let flat_len_m2 = batch * nheads * state_rank * state_rank;
538        let flat_len_m1 = batch * nheads * state_rank;
539
540        // ── Register backward / run forward ──────────────────────────────────
541        match MomentsBackward
542            .prepare::<C>([
543                xhat_bnlchp.node.clone(),
544                bhat_bnlchr.node.clone(),
545                da_bnlh.node.clone(),
546                rot.node.clone(),
547                initial_state_bhpr.node.clone(),
548            ])
549            .compute_bound()
550            .stateful()
551        {
552            OpsKind::Tracked(prep) => {
553                let (prim_m2, prim_m1) = B::mamba3_state_moments_phys(
554                    xhat_bnlchp.primitive.clone(),
555                    bhat_bnlchr.primitive.clone(),
556                    da_bnlh.primitive.clone(),
557                    rot.primitive.clone(),
558                    initial_state_bhpr.primitive.clone(),
559                    valid_len,
560                    quaternion,
561                    rope_dim,
562                    rotate_pairwise,
563                );
564                let (prim_combined, _, _) =
565                    crate::utils::combined_grad::flatten_pair::<B>(prim_m2, prim_m1);
566                let state = State {
567                    xhat: xhat_bnlchp.primitive.clone(),
568                    bhat: bhat_bnlchr.primitive.clone(),
569                    da: da_bnlh.primitive.clone(),
570                    rot: rot.primitive.clone(),
571                    initial: initial_state_bhpr.primitive.clone(),
572                    flat_len_m2,
573                    flat_len_m1,
574                    shape_m2,
575                    shape_m1,
576                    valid_len,
577                    quaternion,
578                    rope_dim,
579                    rotate_pairwise,
580                };
581                let tracked_combined: FloatTensor<Autodiff<B, C>> =
582                    prep.finish(state, prim_combined);
583                crate::utils::combined_grad::autodiff_unflatten_pair::<B, C, 4, 3>(
584                    tracked_combined,
585                    flat_len_m2,
586                    flat_len_m1,
587                    shape_m2,
588                    shape_m1,
589                )
590            }
591            OpsKind::UnTracked(prep) => {
592                let (prim_m2, prim_m1) = B::mamba3_state_moments_phys(
593                    xhat_bnlchp.primitive,
594                    bhat_bnlchr.primitive,
595                    da_bnlh.primitive,
596                    rot.primitive,
597                    initial_state_bhpr.primitive,
598                    valid_len,
599                    quaternion,
600                    rope_dim,
601                    rotate_pairwise,
602                );
603                let (prim_combined, _, _) =
604                    crate::utils::combined_grad::flatten_pair::<B>(prim_m2, prim_m1);
605                let tracked_combined: FloatTensor<Autodiff<B, C>> = prep.finish(prim_combined);
606                crate::utils::combined_grad::autodiff_unflatten_pair::<B, C, 4, 3>(
607                    tracked_combined,
608                    flat_len_m2,
609                    flat_len_m1,
610                    shape_m2,
611                    shape_m1,
612                )
613            }
614        }
615    }
616}