Skip to main content

burn_mamba/mamba2/ssd/serial_recalculated/
combined_backward.rs

1//! # Recompute-based gradient math for the Mamba-2 SSD
2//!
3//! The analytic backward of the five-kernel serial scan, mirroring
4//! `_mamba_chunk_scan_combined_bwd` in the reference `ssd_combined.py`.  The
5//! forward intermediates (K1–K4) are **recomputed** from the saved leaf inputs
6//! rather than stashed, then a reverse per-chunk loop fuses the K5 and K4
7//! backwards; K1/K2/K3 backwards run as batched ops once the loop has gathered
8//! the per-chunk slices.  Comment colours (BLUE / ORANGE / …) tag the
9//! corresponding terms of the chunk-scan gradient, matching the reference.
10//!
11//! Everything here operates on backend **primitives** through the rank-tagged
12//! [`F`] wrapper: a custom [`Backward`](burn::backend::autodiff::ops::Backward)
13//! node runs with a generic backend `B`, so the high-level `Tensor` (pinned to
14//! the global `Dispatch` backend) is unavailable and the math must use `B`'s
15//! `float_*` ops.  The recomputed K1/K2/K4 kernels are local primitive ports of
16//! the high-level [`crate::mamba2::ssd::serial`] kernels.
17
18#![allow(non_snake_case)]
19
20use super::serial_recalculated::{k1_ssd_chunk_cumsum, k2_ssd_bmm, k4_ssd_state_passing};
21use crate::utils::fprim::{F, Mask, san};
22use burn::backend::Backend;
23use burn::tensor::s;
24
25/// Per-input gradients produced by [`combined_backward`] (one field per
26/// differentiable forward input).
27#[non_exhaustive]
28pub struct CombinedGrads<B: Backend> {
29    /// Gradient of the input `x`.
30    pub d_x_bnlhp: F<B, 5>,
31    /// Gradient of the discretised step `Δ` (`dt`).
32    pub d_dt_discretized_bhnl: F<B, 4>,
33    /// Gradient of the input projection `B`.
34    pub d_b_bnlhr: F<B, 5>,
35    /// Gradient of the output projection `C`.
36    pub d_c_bnlhr: F<B, 5>,
37    /// Gradient of the per-head skip term `D`.
38    pub d_d_h: F<B, 1>,
39    /// Gradient of the initial SSM state.
40    pub d_initial_state_bhpr: F<B, 4>,
41    /// Gradient of the per-head decay rate `A` (as `a_decay_h`).
42    pub d_a_decay_h: F<B, 1>,
43    /// Local same-chunk contribution to `d_da_cumsum` from BLUE+ORANGE only
44    /// (excludes K3 and K4 cross-chunk contributions). Exposed for the
45    /// `out_x · dout − ddt · dt` oracle test from Tri Dao's reference
46    /// (`_chunk_scan_bwd_ddAcs_unstable`). Test-only; absent in release builds.
47    #[cfg(test)]
48    pub d_da_local_bhnl: F<B, 4>,
49    /// Same-chunk d_dt contribution from ORANGE only (= what Tri Dao calls
50    /// `ddt` in `_chunk_scan_bwd_ddAcs_unstable`). Test-only; absent in release
51    /// builds.
52    #[cfg(test)]
53    pub d_dt_orange_bhnl: F<B, 4>,
54}
55
56// ─── Recomputed forward kernels ──────────────────────────────────────────────
57// The recompute backward replays the forward's K1/K2/K4 (imported above from
58// [`super::serial_recalculated`]) plus the extended K3 below, which returns the
59// extra intermediates the gradient math needs.
60
61/// Same as [`k3_ssd_chunk_state`](super::serial_recalculated::k3_ssd_chunk_state)
62/// but also returns intermediates needed by the custom backward:
63/// - `intra_chunk_state_bnhpr` — chunk-end state assuming zero initial state
64/// - `b_bar_scale_bhnl` — the K3 scaling factor `dt · exp(cumA_last − cumA)`
65/// - `forward_decay_to_chunk_end_bhnl` — the decay factor `exp(cumA_last − cumA)`
66/// - `b_scaled_bnhlr` — B already scaled by `b_bar_scale`
67pub fn k3_ssd_chunk_state_extended<B: Backend>(
68    x_bnlhp: F<B, 5>,
69    b_bnlhr: F<B, 5>,
70    da_cumsum_bhnl: F<B, 4>,
71    dt_discretized_bhnl: F<B, 4>,
72) -> (F<B, 5>, F<B, 4>, F<B, 4>, F<B, 5>) {
73    let [batch, nchunks, chunk_len, nheads, per_head_dim] = x_bnlhp.dims();
74    let [.., state_rank] = b_bnlhr.dims();
75
76    let x_bnhpl = x_bnlhp.permute([0, 1, 3, 4, 2]);
77    let b_bnhlr = b_bnlhr.permute([0, 1, 3, 2, 4]);
78
79    // K3 scaling factor: dt · exp(cumA_last − cumA)
80    let da_cumsum_last_bhn1 = da_cumsum_bhnl.clone().slice(s![.., .., .., -1]);
81    let forward_decay_to_chunk_end_bhnl =
82        (da_cumsum_last_bhn1.expand([batch, nheads, nchunks, chunk_len]) - da_cumsum_bhnl).exp();
83    san(&forward_decay_to_chunk_end_bhnl);
84    let b_bar_scale_bhnl = forward_decay_to_chunk_end_bhnl.clone() * dt_discretized_bhnl;
85    san(&b_bar_scale_bhnl);
86
87    let b_bar_scale_bnhlr = b_bar_scale_bhnl
88        .clone()
89        .permute([0, 2, 1, 3]) // b_bar_scale_bnhl
90        .unsqueeze_dim::<5>(4) // b_bar_scale_bnhl1
91        .expand([batch, nchunks, nheads, chunk_len, state_rank]); // b_bar_scale_bnhlr
92    let b_scaled_bnhlr = b_bnhlr * b_bar_scale_bnhlr;
93    san(&b_scaled_bnhlr);
94
95    let intra_chunk_state_bnhpr = x_bnhpl.matmul(b_scaled_bnhlr.clone());
96    assert_eq!(
97        [batch, nchunks, nheads, per_head_dim, state_rank],
98        intra_chunk_state_bnhpr.dims()
99    );
100    san(&intra_chunk_state_bnhpr);
101
102    (
103        intra_chunk_state_bnhpr,
104        b_bar_scale_bhnl,
105        forward_decay_to_chunk_end_bhnl,
106        b_scaled_bnhlr,
107    )
108}
109
110/// Memory-efficient backward for the Mamba-2 chunkwise SSD.
111///
112/// Recomputes the forward intermediates (K1-K4) from the saved inputs, then
113/// runs a reverse per-chunk loop that fuses the K5 (BLUE + ORANGE) backward
114/// with the K4 state-passing backward. K3/K2/K1 backwards run as single
115/// batched ops once the loop has collected all per-chunk slices.
116///
117/// # Arguments
118/// - `d_y_bnlhp` — upstream gradient of the SSD output
119/// - `d_final_bhpr` — upstream gradient of the final SSM state
120/// - `x_bnlhp`, `dt_discretized_bhnl`, `b_bnlhr`, `c_bnlhr`, `d_h`,
121///   `initial_state_bhpr`, `a_decay_h` — the seven saved forward inputs
122///
123/// # Returns
124/// One [`CombinedGrads`] struct containing gradients for all 7 inputs.
125#[allow(clippy::too_many_arguments)]
126pub fn combined_backward<B: Backend>(
127    d_y_bnlhp: F<B, 5>,
128    d_final_bhpr: F<B, 4>,
129    //
130    x_bnlhp: F<B, 5>,
131    dt_discretized_bhnl: F<B, 4>,
132    b_bnlhr: F<B, 5>,
133    c_bnlhr: F<B, 5>,
134    d_h: F<B, 1>,
135    initial_state_bhpr: F<B, 4>,
136    a_decay_h: F<B, 1>,
137) -> CombinedGrads<B> {
138    let [batch, nheads, nchunks, chunk_len] = dt_discretized_bhnl.dims();
139    let [.., per_head_dim] = x_bnlhp.dims();
140    let [.., state_rank] = b_bnlhr.dims();
141    let device = dt_discretized_bhnl.device();
142    let dtype = dt_discretized_bhnl.dtype();
143
144    san(&d_y_bnlhp);
145    san(&d_final_bhpr);
146    san(&x_bnlhp);
147    san(&dt_discretized_bhnl);
148    san(&b_bnlhr);
149    san(&c_bnlhr);
150    san(&d_h);
151    san(&initial_state_bhpr);
152    san(&a_decay_h);
153
154    // ═══════════════════════════════════════════════════════════════════════
155    // RECOMPUTE FORWARD INTERMEDIATES (the memory-saving heart of this op)
156    // ═══════════════════════════════════════════════════════════════════════
157
158    // K1 — pre-combined Δ·A → intra-chunk cumsum
159    let (da_cumsum_bhnl, da_chunk_end_bhn) =
160        k1_ssd_chunk_cumsum(dt_discretized_bhnl.clone(), a_decay_h.clone());
161    san(&da_cumsum_bhnl);
162
163    // K2 — CB matrix used in K5 ORANGE
164    let cb_bnhll = k2_ssd_bmm(c_bnlhr.clone(), b_bnlhr.clone());
165    san(&cb_bnhll);
166
167    // K3 — intra-chunk state + decay/decayed-B intermediates
168    let (
169        intra_chunk_state_bnhpr,
170        b_bar_scale_bhnl,
171        forward_decay_to_chunk_end_bhnl,
172        b_scaled_bnhlr,
173    ) = k3_ssd_chunk_state_extended(
174        x_bnlhp.clone(),
175        b_bnlhr.clone(),
176        da_cumsum_bhnl.clone(),
177        dt_discretized_bhnl.clone(),
178    );
179
180    // K4 — chunk-input state stream consumed by K5 BLUE
181    let (chunk_input_state_bnhpr, _final_state_bhpr) = k4_ssd_state_passing(
182        intra_chunk_state_bnhpr.clone(),
183        da_chunk_end_bhn.clone(),
184        initial_state_bhpr,
185    );
186    san(&chunk_input_state_bnhpr);
187
188    // ═══════════════════════════════════════════════════════════════════════
189    // SKIP backward — y += D · x
190    // ═══════════════════════════════════════════════════════════════════════
191    let d_d_h = (d_y_bnlhp.clone() * x_bnlhp.clone())
192        .permute([3, 0, 1, 2, 4]) // _hbnlp
193        .reshape([nheads, batch * nchunks * chunk_len * per_head_dim]) // _hBNLP
194        .sum_dim(1) // _h1
195        .reshape([nheads]);
196    san(&d_d_h);
197
198    let d_x_skip_bnlhp = d_y_bnlhp.clone()
199        * d_h
200            .clone()
201            .unsqueeze_dims::<5>(&[0, 1, 2, 4]) // _111h1
202            .expand([batch, nchunks, chunk_len, nheads, per_head_dim]); // _bnlhp
203    san(&d_x_skip_bnlhp);
204
205    // ═══════════════════════════════════════════════════════════════════════
206    // REVERSE PER-CHUNK LOOP — K5 (BLUE + ORANGE) + K4 fused
207    //
208    // Per-iteration working set is _bhll (not _bnhll).
209    // ═══════════════════════════════════════════════════════════════════════
210
211    // Reusable [chunk_len, chunk_len] upper-triangle base mask for ORANGE.
212    let causal_mask_ll: Mask<B> = Mask::tril_mask(chunk_len, chunk_len, 0, &device);
213
214    let mut vec_orange_d_x_bhlp: Vec<F<B, 4>> = Vec::with_capacity(nchunks);
215    let mut vec_orange_d_dt_bhl: Vec<F<B, 3>> = Vec::with_capacity(nchunks);
216    let mut vec_orange_d_da_bhl: Vec<F<B, 3>> = Vec::with_capacity(nchunks);
217    let mut vec_d_cb_bhll: Vec<F<B, 4>> = Vec::with_capacity(nchunks);
218    let mut vec_blue_d_c_bhlr: Vec<F<B, 4>> = Vec::with_capacity(nchunks);
219    let mut vec_blue_d_da_bhl: Vec<F<B, 3>> = Vec::with_capacity(nchunks);
220    let mut vec_d_intra_bhpr: Vec<F<B, 4>> = Vec::with_capacity(nchunks);
221    let mut vec_d_da_end_bh: Vec<F<B, 2>> = Vec::with_capacity(nchunks);
222
223    let mut d_running_state_bhpr: F<B, 4> = d_final_bhpr;
224
225    for i_chunk in (0..nchunks).rev() {
226        // ── Per-chunk slices ───────────────────────────────────────────────
227        let da_cumsum_bhl: F<B, 3> = da_cumsum_bhnl
228            .clone()
229            .slice(s![.., .., i_chunk, ..]) // _bh1l
230            .squeeze_dim::<3>(2); // _bhl
231        let dt_bhl: F<B, 3> = dt_discretized_bhnl
232            .clone()
233            .slice(s![.., .., i_chunk, ..]) // _bh1l
234            .squeeze_dim::<3>(2); // _bhl
235        let x_bhlp: F<B, 4> = x_bnlhp
236            .clone()
237            .slice(s![.., i_chunk, .., .., ..]) // _b1lhp
238            .squeeze_dim::<4>(1) // _blhp
239            .permute([0, 2, 1, 3]); // _bhlp
240        let d_y_bhlp: F<B, 4> = d_y_bnlhp
241            .clone()
242            .slice(s![.., i_chunk, .., .., ..]) // _b1lhp
243            .squeeze_dim::<4>(1) // _blhp
244            .permute([0, 2, 1, 3]); // _bhlp
245        let c_bhlr: F<B, 4> = c_bnlhr
246            .clone()
247            .slice(s![.., i_chunk, .., .., ..]) // _b1lhr
248            .squeeze_dim::<4>(1) // _blhr
249            .permute([0, 2, 1, 3]); // _bhlr
250        let cb_bhll: F<B, 4> = cb_bnhll
251            .clone()
252            .slice(s![.., i_chunk, .., .., ..]) // _b1hll
253            .squeeze_dim::<4>(1); // _bhll
254        let chunk_input_state_bhpr: F<B, 4> = chunk_input_state_bnhpr
255            .clone()
256            .slice(s![.., i_chunk, .., .., ..]) // _b1hpr
257            .squeeze_dim::<4>(1); // _bhpr
258        san(&chunk_input_state_bhpr);
259
260        // ── BLUE backward ──────────────────────────────────────────────────
261        //   blue[l,p] = exp(cumA[l]) · Σᵣ C[l,r] · state[p,r]
262        let exp_da_cumsum_bhl: F<B, 3> = da_cumsum_bhl.clone().exp();
263        let exp_da_cumsum_bhlp: F<B, 4> = exp_da_cumsum_bhl
264            .clone()
265            .unsqueeze_dim::<4>(3) // _bhl1
266            .expand([batch, nheads, chunk_len, per_head_dim]); // _bhlp
267        let d_ch_bhlp: F<B, 4> = d_y_bhlp.clone() * exp_da_cumsum_bhlp.clone();
268        san(&d_ch_bhlp);
269
270        // d_chunk_input_state = C^T @ d_ch
271        let d_chunk_input_state_bhpr: F<B, 4> = c_bhlr
272            .clone()
273            .permute([0, 1, 3, 2]) // c_bhrl
274            .matmul(d_ch_bhlp.clone()) // d_chunk_input_state_bhrp
275            .permute([0, 1, 3, 2]); // _bhpr
276        san(&d_chunk_input_state_bhpr);
277
278        // d_C_blue = d_ch @ state
279        let d_c_blue_bhlr: F<B, 4> = d_ch_bhlp.clone().matmul(chunk_input_state_bhpr.clone());
280        san(&d_c_blue_bhlr);
281        vec_blue_d_c_bhlr.push(d_c_blue_bhlr);
282
283        // d_da from BLUE:  d_da[l] = (Σₚ d_y[l,p] · ch[l,p]) · exp_da[l]
284        let ch_bhlp: F<B, 4> = c_bhlr.clone().matmul(
285            chunk_input_state_bhpr.clone().permute([0, 1, 3, 2]), // _bhrp
286        ); // _bhlp
287        let d_da_blue_bhl: F<B, 3> = (d_y_bhlp.clone() * ch_bhlp * exp_da_cumsum_bhlp)
288            .sum_dim(3) // _bhl1
289            .squeeze_dim::<3>(3); // _bhl
290        san(&d_da_blue_bhl);
291        vec_blue_d_da_bhl.push(d_da_blue_bhl);
292
293        // ── ORANGE backward ────────────────────────────────────────────────
294        //   w[lₜ,lₛ] = CB[lₜ,lₛ] · exp(cumA[lₜ] − cumA[lₛ]) · dt[lₛ]   (causal)
295        //   orange[lₜ,p] = Σ_{lₛ} w[lₜ,lₛ] · x[lₛ,p]
296        let diff_bhll = {
297            let target_bhll = da_cumsum_bhl
298                .clone()
299                .unsqueeze_dim::<4>(3) // _bhlt1
300                .expand([batch, nheads, chunk_len, chunk_len]); // _bhltls
301            let source_bhll = da_cumsum_bhl
302                .unsqueeze_dim::<4>(2) // _bh1ls
303                .expand([batch, nheads, chunk_len, chunk_len]); // _bhltls
304            target_bhll - source_bhll
305        };
306        let causal_mask_bhll: Mask<B> = causal_mask_ll
307            .clone()
308            .reshape([1, 1, chunk_len, chunk_len]) // _11ll
309            .expand([batch, nheads, chunk_len, chunk_len]); // _bhll
310        let decay_bhll = diff_bhll
311            .mask_fill(causal_mask_bhll.clone(), f32::NEG_INFINITY)
312            .exp();
313        san(&decay_bhll);
314
315        let dt_source_bhll: F<B, 4> = dt_bhl
316            .unsqueeze_dim::<4>(2) // _bh1l
317            .expand([batch, nheads, chunk_len, chunk_len]); // _bhll
318        let cb_decay_bhll = cb_bhll.clone() * decay_bhll.clone();
319        let w_bhll = cb_decay_bhll.clone() * dt_source_bhll.clone();
320
321        let d_orange_bhlp = d_y_bhlp; // = d_y_partial
322        // d_w = d_orange @ x^T
323        let d_w_bhll: F<B, 4> = d_orange_bhlp.clone().matmul(
324            x_bhlp.permute([0, 1, 3, 2]), // x_bhpl
325        );
326        san(&d_w_bhll);
327
328        // d_x = w^T @ d_orange
329        let d_x_orange_bhlp: F<B, 4> = w_bhll
330            .permute([0, 1, 3, 2]) // w_bhlslt
331            .matmul(d_orange_bhlp);
332        san(&d_x_orange_bhlp);
333        vec_orange_d_x_bhlp.push(d_x_orange_bhlp);
334
335        // Mask off above-diagonal contributions, then split d_w into its factors.
336        let d_w_masked_bhll = d_w_bhll.mask_fill(causal_mask_bhll, 0.);
337        let d_cb_decay_bhll = d_w_masked_bhll.clone() * dt_source_bhll;
338        san(&d_cb_decay_bhll);
339
340        // d_dt[s] = Σ_{lₜ ≥ lₛ} d_w[lₜ,lₛ] · CB[lₜ,lₛ] · decay[lₜ,lₛ]
341        let d_dt_orange_bhl: F<B, 3> = (d_w_masked_bhll * cb_decay_bhll)
342            .sum_dim(2) // _bh1ls
343            .squeeze_dim::<3>(2); // _bhls
344        san(&d_dt_orange_bhl);
345        vec_orange_d_dt_bhl.push(d_dt_orange_bhl);
346
347        // d_cb = d_cb_decay · decay ;  d_decay = d_cb_decay · cb
348        let d_cb_bhll = d_cb_decay_bhll.clone() * decay_bhll.clone();
349        vec_d_cb_bhll.push(d_cb_bhll);
350        let d_decay_bhll = d_cb_decay_bhll * cb_bhll;
351        let d_diff_bhll = d_decay_bhll * decay_bhll;
352
353        // d_da from ORANGE:  d_da_tgt[l] += Σₛ d_diff[l,s]; d_da_src[s] −= Σₗ d_diff[l,s].
354        let d_da_tgt_bhl: F<B, 3> = d_diff_bhll
355            .clone()
356            .sum_dim(3) // _bhlt1
357            .squeeze_dim::<3>(3); // _bhlt
358        let d_da_src_bhl: F<B, 3> = d_diff_bhll
359            .sum_dim(2) // _bh1ls
360            .squeeze_dim::<3>(2); // _bhls
361        let d_da_orange_bhl = d_da_tgt_bhl - d_da_src_bhl;
362        san(&d_da_orange_bhl);
363        vec_orange_d_da_bhl.push(d_da_orange_bhl);
364
365        // ── K4 backward step for chunk i ───────────────────────────────────
366        //   Forward: sᵢ₊₁ = decayᵢ · sᵢ + intra_stateᵢ
367        //   - d_intra_stateᵢ      = d_sᵢ₊₁  (current d_running_state)
368        //   - d_decayᵢ            = d_sᵢ₊₁ · sᵢ
369        //   - d_sᵢ (propagated)   = decayᵢ · d_sᵢ₊₁ + d_chunk_input_state
370        vec_d_intra_bhpr.push(d_running_state_bhpr.clone());
371
372        let decay_chunk_bhpr: F<B, 4> = da_chunk_end_bhn
373            .clone()
374            .slice(s![.., .., i_chunk]) // _bh1
375            .exp() // _bh
376            .unsqueeze_dim::<4>(3) // _bh11
377            .expand([batch, nheads, per_head_dim, state_rank]); // _bhpr
378        san(&decay_chunk_bhpr);
379
380        let d_decay_chunk_bhpr = d_running_state_bhpr.clone() * chunk_input_state_bhpr;
381        // d_da_chunk_end[b,h] = Σ_{p,r} d_decay · decay   (decay = exp(da_chunk_end))
382        let d_da_chunk_end_bh: F<B, 2> = (d_decay_chunk_bhpr * decay_chunk_bhpr.clone())
383            .reshape([batch, nheads, per_head_dim * state_rank]) // _bhPR
384            .sum_dim(2) // _bh1
385            .squeeze_dim::<2>(2); // _bh
386        san(&d_da_chunk_end_bh);
387        vec_d_da_end_bh.push(d_da_chunk_end_bh);
388
389        d_running_state_bhpr = decay_chunk_bhpr * d_running_state_bhpr + d_chunk_input_state_bhpr;
390        san(&d_running_state_bhpr);
391    }
392    // d_initial_state = the trailing d_running_state after the reverse loop.
393    let d_initial_state_bhpr = d_running_state_bhpr;
394
395    // ── Restore natural (forward) chunk order ─────────────────────────────
396    vec_orange_d_x_bhlp.reverse();
397    vec_orange_d_dt_bhl.reverse();
398    vec_orange_d_da_bhl.reverse();
399    vec_d_cb_bhll.reverse();
400    vec_blue_d_c_bhlr.reverse();
401    vec_blue_d_da_bhl.reverse();
402    vec_d_intra_bhpr.reverse();
403    vec_d_da_end_bh.reverse();
404
405    // ── Stack per-chunk slices back into batched tensors ──────────────────
406    let d_x_orange_bnlhp: F<B, 5> = F::stack::<5>(vec_orange_d_x_bhlp, 1).permute([0, 1, 3, 2, 4]);
407    let d_dt_orange_bhnl: F<B, 4> = F::stack(vec_orange_d_dt_bhl, 2);
408    let d_da_orange_bhnl: F<B, 4> = F::stack(vec_orange_d_da_bhl, 2);
409    let d_cb_bnhll: F<B, 5> = F::stack(vec_d_cb_bhll, 1);
410    let d_da_blue_bhnl: F<B, 4> = F::stack(vec_blue_d_da_bhl, 2);
411    let d_intra_chunk_state_bnhpr: F<B, 5> = F::stack(vec_d_intra_bhpr, 1);
412    let d_c_blue_bnhlr: F<B, 5> = F::stack(vec_blue_d_c_bhlr, 1);
413    let d_da_end_bhn: F<B, 3> = F::stack(vec_d_da_end_bh, 2);
414    san(&d_x_orange_bnlhp);
415    san(&d_dt_orange_bhnl);
416    san(&d_da_orange_bhnl);
417    san(&d_cb_bnhll);
418    san(&d_da_blue_bhnl);
419    san(&d_intra_chunk_state_bnhpr);
420    san(&d_c_blue_bnhlr);
421
422    // d_da_cumsum from K4: only the last-l position of each chunk gets the
423    // d_da_chunk_end contribution (da_chunk_end = cumA[chunk_len-1]).
424    let d_da_cumsum_k4_bhnl = {
425        let zeros = F::<B, 4>::zeros([batch, nheads, nchunks, chunk_len - 1], &device, dtype);
426        F::cat(vec![zeros, d_da_end_bhn.unsqueeze_dim::<4>(3)], 3)
427    };
428
429    let d_c_blue_bnlhr = d_c_blue_bnhlr.permute([0, 1, 3, 2, 4]);
430
431    // ═══════════════════════════════════════════════════════════════════════
432    // K3 BACKWARD
433    //
434    // Forward (recap):
435    //   b_bnhlr = b_bnlhr.permute
436    //   x_bnhpl = x_bnlhp.permute
437    //   forward_decay_to_chunk_end = exp(cumA_last − cumA)
438    //   b_bar_scale = forward_decay · dt
439    //   b_scaled = b · b_bar_scale_broadcast
440    //   intra_state = x_bnhpl @ b_scaled
441    // ═══════════════════════════════════════════════════════════════════════
442    let x_bnhpl = x_bnlhp.permute([0, 1, 3, 4, 2]);
443
444    // d_x_k3 = d_intra_state @ b_scaled^T
445    let d_x_k3_bnlhp = d_intra_chunk_state_bnhpr
446        .clone()
447        .matmul(
448            b_scaled_bnhlr.clone().permute([0, 1, 2, 4, 3]), // b_scaled_bnhrl
449        ) // d_x_k3_bnhpl
450        .permute([0, 1, 4, 2, 3]); // d_x_k3_bnlhp
451    san(&d_x_k3_bnlhp);
452
453    // d_b_scaled = x^T @ d_intra_state
454    let d_b_scaled_bnhlr = x_bnhpl
455        .permute([0, 1, 2, 4, 3]) // x_bnhlp
456        .matmul(d_intra_chunk_state_bnhpr);
457    san(&d_b_scaled_bnhlr);
458
459    // Split d_b_scaled via the product rule: d_b = d_b_scaled · b_bar_scale ; d_b_bar_scale = d_b_scaled · b
460    let b_bar_scale_bnhlr = b_bar_scale_bhnl
461        .clone()
462        .permute([0, 2, 1, 3]) // _bnhl
463        .unsqueeze_dim::<5>(4) // _bnhl1
464        .expand([batch, nchunks, nheads, chunk_len, state_rank]);
465    let d_b_k3_bnhlr = d_b_scaled_bnhlr.clone() * b_bar_scale_bnhlr;
466    let d_b_k3_bnlhr = d_b_k3_bnhlr.permute([0, 1, 3, 2, 4]);
467    san(&d_b_k3_bnlhr);
468
469    let b_bnhlr = b_bnlhr.clone().permute([0, 1, 3, 2, 4]);
470    let d_b_bar_scale_bhnl = (d_b_scaled_bnhlr * b_bnhlr)
471        .sum_dim(4) // _bnhl1
472        .squeeze_dim::<4>(4) // _bnhl
473        .permute([0, 2, 1, 3]); // _bhnl
474    san(&d_b_bar_scale_bhnl);
475
476    // Through the product rule for b_bar_scale = forward_decay · dt:
477    //   d_forward_decay = d_b_bar_scale · dt
478    //   d_dt_k3         = d_b_bar_scale · forward_decay
479    let d_forward_decay_bhnl = d_b_bar_scale_bhnl.clone() * dt_discretized_bhnl.clone();
480    let d_dt_discretized_k3_bhnl = d_b_bar_scale_bhnl * forward_decay_to_chunk_end_bhnl.clone();
481
482    // Through exp: d_(cumA_last − cumA) = d_forward_decay · forward_decay
483    let d_da_delta_bhnl = d_forward_decay_bhnl * forward_decay_to_chunk_end_bhnl;
484    // Subtraction splits into:
485    //   d_cumA_last = +Σ_l d_da_delta (it was broadcast over l), scattered into last-l position.
486    //   d_cumA      = −d_da_delta
487    let d_da_cumsum_sub_bhnl = -d_da_delta_bhnl.clone();
488    let d_da_cumsum_last_bhn = d_da_delta_bhnl
489        .sum_dim(3) // _bhn1
490        .squeeze_dim::<3>(3); // _bhn
491    let d_da_cumsum_k3_bhnl = {
492        let zeros = F::<B, 4>::zeros([batch, nheads, nchunks, chunk_len - 1], &device, dtype);
493        d_da_cumsum_sub_bhnl + F::cat(vec![zeros, d_da_cumsum_last_bhn.unsqueeze_dim::<4>(3)], 3)
494    };
495    san(&d_da_cumsum_k3_bhnl);
496
497    // ═══════════════════════════════════════════════════════════════════════
498    // K2 BACKWARD (from d_cb_bnhll)
499    //
500    //   cb_bnhll = c_bnhlr @ b_bnhrl
501    //   d_c = d_cb @ b ;  d_b = d_cb^T @ c
502    // ═══════════════════════════════════════════════════════════════════════
503    let c_bnhlr = c_bnlhr.permute([0, 1, 3, 2, 4]);
504    let b_bnhlr = b_bnlhr.permute([0, 1, 3, 2, 4]);
505
506    let d_c_k2_bnhlr = d_cb_bnhll.clone().matmul(b_bnhlr.clone());
507    let d_c_k2_bnlhr = d_c_k2_bnhlr.permute([0, 1, 3, 2, 4]);
508    san(&d_c_k2_bnlhr);
509
510    let d_b_k2_bnhlr = d_cb_bnhll
511        .permute([0, 1, 2, 4, 3]) // d_cb_bnhsltl (target/source swap)
512        .matmul(c_bnhlr);
513    let d_b_k2_bnlhr = d_b_k2_bnhlr.permute([0, 1, 3, 2, 4]);
514    san(&d_b_k2_bnlhr);
515
516    // ═══════════════════════════════════════════════════════════════════════
517    // SUM GRADIENT CONTRIBUTIONS + K1 BACKWARD
518    // ═══════════════════════════════════════════════════════════════════════
519
520    // Test-only: local same-chunk d_da contribution (BLUE + ORANGE) snapshot
521    // for the `out_x · dout − ddt · dt` oracle. Production builds skip the
522    // extra add and the retained _bhnl tensor.
523    #[cfg(test)]
524    let d_da_local_bhnl = d_da_blue_bhnl.clone() + d_da_orange_bhnl.clone();
525    #[cfg(test)]
526    san(&d_da_local_bhnl);
527
528    let d_da_cumsum_bhnl =
529        d_da_blue_bhnl + d_da_orange_bhnl + d_da_cumsum_k3_bhnl + d_da_cumsum_k4_bhnl;
530    san(&d_da_cumsum_bhnl);
531
532    // K1 forward:  da_cumsum[l] = cumsumₗ(dt[l] · a_decay)
533    // Reverse:     d_da[l] = Σ_{k ≥ l} d_da_cumsum[k]   (suffix sum)
534    //                      = total − cumsum(d_da_cumsum)[l−1]   (cumsum[−1] = 0)
535    let d_da_bhnl = {
536        let d_total_bhnl = d_da_cumsum_bhnl
537            .clone()
538            .sum_dim(3) // _bhn1
539            .expand([batch, nheads, nchunks, chunk_len]);
540        let prefix_bhnl = d_da_cumsum_bhnl.cumsum(3);
541        let zeros_bhn1 = F::<B, 4>::zeros([batch, nheads, nchunks, 1], &device, dtype);
542        let prefix_shifted_bhnl =
543            F::cat(vec![zeros_bhn1, prefix_bhnl.narrow(3, 0, chunk_len - 1)], 3);
544        d_total_bhnl - prefix_shifted_bhnl
545    };
546    san(&d_da_bhnl);
547
548    // d_dt from K1: d_dt = d_da · a_decay
549    let a_decay_111h1 = a_decay_h
550        .unsqueeze_dims::<4>(&[0, 2, 3])
551        .expand([batch, nheads, nchunks, chunk_len]);
552    let d_dt_k1_bhnl = d_da_bhnl.clone() * a_decay_111h1;
553    san(&d_dt_k1_bhnl);
554
555    // d_a_decay[h] = Σ_{b,n,l} d_da[b,h,n,l] · dt[b,h,n,l]
556    let d_a_decay_h = (d_da_bhnl * dt_discretized_bhnl.clone())
557        .permute([1, 0, 2, 3]) // _hbnl
558        .reshape([nheads, batch * nchunks * chunk_len]) // _hBNL
559        .sum_dim(1) // _h1
560        .squeeze_dim::<1>(1); // _h
561    san(&d_a_decay_h);
562
563    // ── Combine per-input gradient contributions ──────────────────────────
564    #[cfg(test)]
565    let d_dt_orange_bhnl_save = d_dt_orange_bhnl.clone();
566    let d_dt_discretized_bhnl = d_dt_orange_bhnl + d_dt_discretized_k3_bhnl + d_dt_k1_bhnl;
567    san(&d_dt_discretized_bhnl);
568
569    let d_x_bnlhp = d_x_skip_bnlhp + d_x_k3_bnlhp + d_x_orange_bnlhp;
570    san(&d_x_bnlhp);
571
572    let d_b_bnlhr = d_b_k2_bnlhr + d_b_k3_bnlhr;
573    san(&d_b_bnlhr);
574    let d_c_bnlhr = d_c_k2_bnlhr + d_c_blue_bnlhr;
575    san(&d_c_bnlhr);
576
577    CombinedGrads {
578        d_a_decay_h,
579        d_dt_discretized_bhnl,
580        d_x_bnlhp,
581        d_b_bnlhr,
582        d_c_bnlhr,
583        d_d_h,
584        d_initial_state_bhpr,
585        #[cfg(test)]
586        d_da_local_bhnl,
587        #[cfg(test)]
588        d_dt_orange_bhnl: d_dt_orange_bhnl_save,
589    }
590}
591
592#[cfg(all(test, feature = "_dev-test"))]
593mod tests;