Skip to main content

burn_mamba/mamba3/step_constant/
mod.rs

1//! # Constant-input stepping shortcuts — `step_n_approx` / `step_infinite`
2//!
3//! When the **same token** is fed to [`Mamba3::step`] over and over, every
4//! data-dependent quantity is the same at each step: the trapezoid
5//! coefficients `α`, `β`, `γ`, the QK-normed `b`/`c`, and the per-step
6//! rotation increment (angle `θ̂` for `Complex2D`, unit quaternion `q` for
7//! `Quaternion4D`). Only the *cumulative* rotation moves — by that same
8//! increment each step — so with `P` the per-step rotation operator and `R₁`
9//! the cumulative rotation at the first constant step,
10//!
11//! ```text
12//!   Bₜ = R₁ Pᵗ⁻¹ b ,   Cₜ = R₁ Pᵗ⁻¹ c ,
13//!   hₜ = α hₜ₋₁ + x ⊗ (β Bₜ₋₁ + γ Bₜ)            (t ≥ 2)
14//! ```
15//!
16//! and `K = n − 1` unrolled steps collapse to a **matrix geometric series** in
17//! `α P⁻¹` (spectral radius `α < 1`, since `a_floor > 0` and `Δ > 0`):
18//!
19//! ```text
20//!   h_n = α^K h₁  +  x ⊗ R₁ P^{K−1} (I − (αP⁻¹)^K)(I − αP⁻¹)⁻¹ (β + γP) b
21//! ```
22//!
23//! `P` is block-diagonal: per RoPE pair it is the complex scalar `e^{iθ̂}`, per
24//! quaternion block left-multiplication by `w = q*`. In both cases all the
25//! factors live in one **abelian** subalgebra (powers of a single rotation
26//! commute — for quaternions, `span{1, û} ≅ ℂ`), so the series is a handful of
27//! scalar complex / quaternion ops per (head, pair/block): `n` steps cost O(1).
28//!
29//! In the readout `y_n = Cₙᵀ h_n + D x` the unbounded phase cancels
30//! (`⟨R c, R v⟩ = ⟨c, v⟩`), leaving only the *relative* rotation `P⁻¹`. As
31//! `n → ∞` the `h₁` term and the partial-sum correction decay like `αⁿ`, so the
32//! **output converges** even though the state `h` orbits forever:
33//!
34//! ```text
35//!   y_∞ = xᵀ · cᵀ (γ + β P⁻¹)(I − α P⁻¹)⁻¹ b  +  D x
36//! ```
37//!
38//! — the block's stationary fixed point, independent of any starting cache.
39//! [`Mamba3::step_infinite`] evaluates exactly this (and therefore takes and
40//! returns no cache); [`Mamba3::step_n_approx`] evaluates the finite-`n` form,
41//! returning the exact step-`n` output *and* cache.
42//!
43//! Numerics: `α^K` is `exp(K·ΔA)`; every accumulated rotation power has its
44//! (half-)angle reduced mod `2π` with the value-exact [`wrap_angle`]; the
45//! geometric denominators satisfy `|1 − α e^{−iθ̂}|² ≥ (1 − α)²` and are floored
46//! by [`div_eps`](crate::utils::div_eps). When `α → 1` *and* `θ̂ → 0` the series
47//! value `(β+γ)(1−α^K)/(1−α) → K·(β+γ)`-ish stays finite, but its fp32
48//! evaluation loses precision once `1 − α` nears the epsilon floor — the same
49//! regime where the unrolled recurrence itself accumulates `K` near-undamped
50//! terms.
51
52use crate::mamba3::double_ssd::double_ssd::StepProjection;
53use crate::mamba3::double_ssd::prelude::*;
54use crate::mamba3::helpers;
55use crate::mamba3::prelude::*;
56use crate::mamba3::rotation::{
57    RotationState, quat_conj, quat_from_scaled_axis, quat_mul, quat_normalize,
58    rotate_state_rank_blocks,
59};
60use crate::modules::sanity as san;
61use crate::modules::{apply_rope_partial, wrap_angle};
62use crate::utils::div_eps;
63use burn::prelude::*;
64use core::f32::consts::PI;
65
66// ---------------------------------------------------------------------------
67// Public API
68// ---------------------------------------------------------------------------
69
70impl Mamba3 {
71    /// Stationary **fixed point** of the block under a constant token: the
72    /// limit of `step(input, …)` outputs as the same `input_bd` is stepped
73    /// forever.
74    ///
75    /// Closed form, O(1) in the horizon (see the [module header](self) for the
76    /// derivation). The limit forgets any starting state (`αⁿ → 0`) and the
77    /// SSM state itself never converges (it keeps rotating; only the output
78    /// does), so this takes **no cache and returns none**. Use
79    /// [`Self::step_n_approx`] when the post-horizon cache is needed.
80    ///
81    /// Both rotation kinds and both SSD pathways are covered (the recurrence
82    /// is pathway-agnostic at boundaries). Differentiable; gradients are the
83    /// limit of the unrolled gradients.
84    ///
85    /// # Shapes
86    /// - `input_bd` : `[batch, d_model]`
87    /// - output     : `[batch, d_model]`
88    pub fn step_infinite(&self, input_bd: Tensor<2>) -> Tensor<2> {
89        let StepProjection {
90            z_bi,
91            x_bhp,
92            b_bmhr,
93            c_bmhr,
94            rot_ba,
95            dt_bh,
96            da_bh: _,
97            alpha_bh,
98            beta_bh,
99            gamma_bh,
100        } = self.step_project(input_bd);
101        let [batch, mimo_rank, nheads, _state_rank] = b_bmhr.dims();
102        let eps = div_eps(alpha_bh.dtype());
103
104        // Rotation-free channels: (β + γ) / (1 − α).
105        let tail_bh = (beta_bh.clone() + gamma_bh.clone())
106            / (-alpha_bh.clone() + 1.0).clamp_min(eps);
107
108        // Per-pair/block readout factor  m = (γ + β P⁻¹)(1 − α P⁻¹)⁻¹  applied
109        // to `b`; the cumulative rotation cancels against `Cₙ` (orthogonality).
110        let b_eff_bmhr = match self.rotation_kind() {
111            RotationKind::Complex2D => {
112                let theta_bha = per_step_angle(rot_ba, dt_bh); // θ̂
113                let (cos, sin) = cos_sin(theta_bha);
114                let a_bh1 = alpha_bh.unsqueeze_dim::<3>(2);
115                let beta_bh1 = beta_bh.unsqueeze_dim::<3>(2);
116                let gamma_bh1 = gamma_bh.unsqueeze_dim::<3>(2);
117                // (γ + β e^{−iθ̂}) / (1 − α e^{−iθ̂})
118                let num_re = gamma_bh1 + beta_bh1.clone() * cos.clone();
119                let num_im = -beta_bh1 * sin.clone();
120                let den_re = -a_bh1.clone() * cos + 1.0;
121                let den_im = a_bh1 * sin;
122                let (m_re, m_im) = complex_div(num_re, num_im, den_re, den_im);
123                mul_complex_partial(
124                    b_bmhr,
125                    m_re,
126                    m_im,
127                    tail_bh,
128                    self.rope_dim,
129                    mimo_rank == 1,
130                )
131            }
132            RotationKind::Quaternion4D => {
133                let g_bhj3 = per_step_generator(rot_ba, dt_bh, self.num_quat_blocks);
134                let q_bhj4 = quat_from_scaled_axis::<4>(g_bhj3); // P⁻¹ ↔ q
135                let alpha_bh11 = alpha_bh.unsqueeze_dims::<4>(&[2, 3]);
136                let beta_bh11 = beta_bh.unsqueeze_dims::<4>(&[2, 3]);
137                let gamma_bh11 = gamma_bh.unsqueeze_dims::<4>(&[2, 3]);
138                // (γ + β q) ⊗ (1 − α q)⁻¹  — all in the abelian subalgebra of q.
139                let num = quat_scalar_affine(gamma_bh11, beta_bh11, q_bhj4.clone());
140                let den_inv = quat_inv(quat_one_minus(q_bhj4 * alpha_bh11));
141                let f_bhj4 = quat_mul(num, den_inv);
142                mul_quat_partial(b_bmhr, f_bhj4, tail_bh, self.num_quat_blocks * 4)
143            }
144        };
145        san(&b_eff_bmhr);
146
147        // y_∞[m] = Σ_m' ⟨c[m], m·b[m']⟩ · x_vals[m']   (then D-skip/gate/out-proj).
148        let mimo_x_hmp = self.mimo_x_hmp.as_ref().map(|p| p.val());
149        let x_vals_bmhp = helpers::build_v_with_mimo::<3, 4>(x_bhp, mimo_x_hmp.as_ref(), 1);
150        let gram_bhmm = {
151            let c_bhmr = c_bmhr.permute([0, 2, 1, 3]);
152            let b_bhrm = b_eff_bmhr.permute([0, 2, 3, 1]);
153            c_bhmr.matmul(b_bhrm) // [batch, nheads, mimo_rank, mimo_rank']
154        };
155        let out_m_bmhp = {
156            let x_bhmp = x_vals_bmhp.clone().permute([0, 2, 1, 3]);
157            gram_bhmm.matmul(x_bhmp).permute([0, 2, 1, 3])
158        };
159        assert_eq!(
160            [batch, mimo_rank, nheads, self.per_head_dim()],
161            out_m_bmhp.dims()
162        );
163        san(&out_m_bmhp);
164
165        self.step_finish(out_m_bmhp, x_vals_bmhp, z_bi)
166    }
167
168    /// Net effect of `n` consecutive [`Mamba3::step`] calls on the **same**
169    /// token, in O(1): the step-`n` output and the step-`n` cache.
170    ///
171    /// For a single block this is *exact* (an algebraic reformulation of the
172    /// unroll — same values, same gradients, up to float associativity); the
173    /// `_approx` suffix refers to the stacked
174    /// [`Layers`](crate::modules::Layers)-level composition, where inputs to
175    /// deeper layers are held constant at their final value (error decays
176    /// geometrically in `n`). `n = 1` is exactly one `step`.
177    ///
178    /// The first step runs as an ordinary [`Mamba3::step`] (consuming the
179    /// cache's previous-token trapezoid contribution, which may belong to a
180    /// *different* token); the remaining `n − 1` are the closed-form jump. The
181    /// returned cache keeps the supplied pathway variant (`None` defaults to
182    /// single-SSD, as in `step`).
183    ///
184    /// # Shapes
185    /// - `input_bd` : `[batch, d_model]`
186    /// - output     : `[batch, d_model]` (the **last** step's output)
187    pub fn step_n_approx(
188        &self,
189        input_bd: Tensor<2>,
190        n: usize,
191        cache: Option<Mamba3Cache>,
192    ) -> (Tensor<2>, Mamba3Cache) {
193        assert!(n >= 1, "step_n_approx requires at least one step");
194        let keep_single = !matches!(cache, Some(Mamba3Cache::DoubleSsd(_)));
195        let cache = cache.map(|c| match c {
196            Mamba3Cache::DoubleSsd(c) => c,
197            Mamba3Cache::SingleSsd(c) => c.into(),
198        });
199
200        // Step 1: an ordinary recurrent step (boundary semantics are the
201        // double-ssd ones — identical for both pathway caches at boundaries).
202        let (out, cache) = self.step_double_ssd(input_bd.clone(), cache);
203        let (out, cache) = if n == 1 {
204            (out, cache)
205        } else {
206            self.jump_constant(self.step_project(input_bd), n - 1, cache)
207        };
208
209        let cache = if keep_single {
210            Mamba3Cache::SingleSsd(cache.into())
211        } else {
212            Mamba3Cache::DoubleSsd(cache)
213        };
214        (out, cache)
215    }
216
217    /// The closed-form jump: advance `cache` (the state *after* the first
218    /// constant step) by `k ≥ 1` further steps of the same token, whose
219    /// projection is `p`.
220    fn jump_constant(
221        &self,
222        p: StepProjection,
223        k: usize,
224        cache: Mamba3DoubleSsdCache,
225    ) -> (Tensor<2>, Mamba3DoubleSsdCache) {
226        let StepProjection {
227            z_bi,
228            x_bhp,
229            b_bmhr,
230            c_bmhr,
231            rot_ba,
232            dt_bh,
233            da_bh,
234            alpha_bh,
235            beta_bh,
236            gamma_bh,
237        } = p;
238        let [batch, mimo_rank, nheads, _state_rank] = b_bmhr.dims();
239        let kf = k as f32;
240        let eps = div_eps(alpha_bh.dtype());
241
242        // α^K = exp(K·ΔA) — exact in log space.
243        let alpha_k_bh = (da_bh * kf).exp();
244        // Rotation-free channels: (β + γ)(1 − α^K)/(1 − α).
245        let tail_bh = (beta_bh.clone() + gamma_bh.clone())
246            * (-alpha_k_bh.clone() + 1.0)
247            / (-alpha_bh.clone() + 1.0).clamp_min(eps);
248
249        // Driven term Σₜ α^{n−t}(β Bₜ₋₁ + γ Bₜ) = R₁ P^{K−1} (1−(αP⁻¹)^K)(1−αP⁻¹)⁻¹ (β+γP) b,
250        // plus the rotated Bₙ/Cₙ and the cumulative rotation at step n.
251        let (b_drv_bmhr, b_n_bmhr, c_n_bmhr, rotation_n) = match self.rotation_kind() {
252            RotationKind::Complex2D => {
253                let angle1_bha = cache.rotation.clone().angle(); // Θ₁
254                let theta_bha = per_step_angle(rot_ba, dt_bh); // θ̂
255                let num_rope_angles = theta_bha.dims()[2];
256                let rotate_pairwise = mimo_rank == 1;
257
258                // Θₙ = Θ₁ + K·θ̂ (value-exact wrap; gradient w.r.t. θ̂ stays K).
259                let angle_n_bha = wrap_angle(angle1_bha.clone() + theta_bha.clone() * kf);
260                let expand_m = |a_bha: Tensor<3>| {
261                    a_bha.unsqueeze_dim::<4>(1).expand([
262                        batch,
263                        mimo_rank,
264                        nheads,
265                        num_rope_angles,
266                    ])
267                };
268                let b_n = apply_rope_partial::<4>(
269                    b_bmhr.clone(),
270                    expand_m(angle_n_bha.clone()),
271                    self.rope_dim,
272                    rotate_pairwise,
273                );
274                let c_n = apply_rope_partial::<4>(
275                    c_bmhr,
276                    expand_m(angle_n_bha.clone()),
277                    self.rope_dim,
278                    rotate_pairwise,
279                );
280
281                // φ = e^{i(Θ₁+(K−1)θ̂)} · (1 − α^K e^{−iKθ̂})/(1 − α e^{−iθ̂}) · (β + γ e^{iθ̂})
282                let (cos1, sin1) = cos_sin(theta_bha.clone());
283                let (cos_k, sin_k) = cos_sin(theta_bha.clone() * kf);
284                let (lead_re, lead_im) = cos_sin(angle1_bha + theta_bha * (kf - 1.0));
285                let a_bh1 = alpha_bh.clone().unsqueeze_dim::<3>(2);
286                let ak_bh1 = alpha_k_bh.clone().unsqueeze_dim::<3>(2);
287                let beta_bh1 = beta_bh.clone().unsqueeze_dim::<3>(2);
288                let gamma_bh1 = gamma_bh.clone().unsqueeze_dim::<3>(2);
289                let num_re = -ak_bh1.clone() * cos_k + 1.0;
290                let num_im = ak_bh1 * sin_k;
291                let den_re = -a_bh1.clone() * cos1.clone() + 1.0;
292                let den_im = a_bh1 * sin1.clone();
293                let (rat_re, rat_im) = complex_div(num_re, num_im, den_re, den_im);
294                let bg_re = beta_bh1 + gamma_bh1.clone() * cos1;
295                let bg_im = gamma_bh1 * sin1;
296                let (t_re, t_im) = complex_mul(rat_re, rat_im, bg_re, bg_im);
297                let (phi_re, phi_im) = complex_mul(t_re, t_im, lead_re, lead_im);
298
299                let b_drv = mul_complex_partial(
300                    b_bmhr,
301                    phi_re,
302                    phi_im,
303                    tail_bh,
304                    self.rope_dim,
305                    rotate_pairwise,
306                );
307                (b_drv, b_n, c_n, RotationState::Angle(angle_n_bha))
308            }
309            RotationKind::Quaternion4D => {
310                let q1_bhj4 = cache.rotation.clone().quaternion(); // Q₁
311                let blocks = self.num_quat_blocks;
312                let rope_width = blocks * 4;
313                let g_bhj3 = per_step_generator(rot_ba, dt_bh, blocks);
314
315                // Qₙ = q^K ⊗ Q₁ ;  q^K = exp(K·g/2) (half-angle wrapped).
316                let q_pow_k = quat_pow(g_bhj3.clone(), kf);
317                let q_n_bhj4 = quat_normalize(quat_mul(q_pow_k.clone(), q1_bhj4.clone()));
318                let conj_qn_bmhj4 = quat_conj(q_n_bhj4.clone())
319                    .unsqueeze_dim::<5>(1)
320                    .expand([batch, mimo_rank, nheads, blocks, 4]);
321                let b_n = crate::mamba3::rotation::rotate_blocks_partial::<4, 5>(
322                    b_bmhr.clone(),
323                    conj_qn_bmhj4.clone(),
324                    rope_width,
325                );
326                let c_n = crate::mamba3::rotation::rotate_blocks_partial::<4, 5>(
327                    c_bmhr,
328                    conj_qn_bmhj4,
329                    rope_width,
330                );
331
332                // f = Q₁* ⊗ w^{K−1} ⊗ (1 − α^K q^K) ⊗ (1 − α q)⁻¹ ⊗ (β + γ w),  w = q*.
333                // Everything right of Q₁* commutes (powers of one quaternion).
334                let q_bhj4 = quat_from_scaled_axis::<4>(g_bhj3.clone());
335                let w_bhj4 = quat_conj(q_bhj4.clone());
336                let w_pow_km1 = quat_pow(-g_bhj3, kf - 1.0);
337                let alpha_bh11 = alpha_bh.clone().unsqueeze_dims::<4>(&[2, 3]);
338                let alpha_k_bh11 = alpha_k_bh.clone().unsqueeze_dims::<4>(&[2, 3]);
339                let beta_bh11 = beta_bh.clone().unsqueeze_dims::<4>(&[2, 3]);
340                let gamma_bh11 = gamma_bh.clone().unsqueeze_dims::<4>(&[2, 3]);
341                let num = quat_one_minus(q_pow_k * alpha_k_bh11);
342                let den_inv = quat_inv(quat_one_minus(q_bhj4 * alpha_bh11));
343                let bg = quat_scalar_affine(beta_bh11, gamma_bh11, w_bhj4);
344                let f_bhj4 = quat_mul(
345                    quat_conj(q1_bhj4),
346                    quat_mul(w_pow_km1, quat_mul(num, quat_mul(den_inv, bg))),
347                );
348
349                let b_drv = mul_quat_partial(b_bmhr, f_bhj4, tail_bh, rope_width);
350                (b_drv, b_n, c_n, RotationState::Quaternion(q_n_bhj4))
351            }
352        };
353        san(&b_drv_bmhr);
354        san(&b_n_bmhr);
355        san(&c_n_bmhr);
356        rotation_n.sanity();
357
358        // hₙ = α^K h₁ + Σₘ x_vals[m] ⊗ b_drv[m]   (einsum('bmhp,bmhr->bhpr')).
359        let mimo_x_hmp = self.mimo_x_hmp.as_ref().map(|p| p.val());
360        let x_vals_bmhp =
361            helpers::build_v_with_mimo::<3, 4>(x_bhp.clone(), mimo_x_hmp.as_ref(), 1);
362        let xb_bhpr = {
363            let b_bhmr = b_drv_bmhr.permute([0, 2, 1, 3]);
364            let x_bhpm = x_vals_bmhp.clone().permute([0, 2, 3, 1]);
365            x_bhpm.matmul(b_bhmr)
366        };
367        let alpha_k_bh11 = alpha_k_bh.unsqueeze_dims::<4>(&[2, 3]);
368        let state_n_bhpr = alpha_k_bh11 * cache.ssm_bhpr + xb_bhpr;
369        san(&state_n_bhpr);
370
371        // yₙ readout + block tail, exactly as in `step`.
372        let out_m_bmhp = Self::step_readout(state_n_bhpr.clone(), c_n_bmhr);
373        san(&out_m_bmhp);
374        let out_bm = self.step_finish(out_m_bmhp, x_vals_bmhp, z_bi);
375
376        let cache = Mamba3DoubleSsdCache {
377            ssm_bhpr: state_n_bhpr,
378            k_state_bmhr: b_n_bmhr,
379            v_state_bhp: x_bhp,
380            rotation: rotation_n,
381        };
382        (out_bm, cache)
383    }
384}
385
386// ---------------------------------------------------------------------------
387// Per-step rotation increments (mirroring `rotate_bc_step`)
388// ---------------------------------------------------------------------------
389
390/// `θ̂ = Δ · π·tanh(rot)` — the constant per-step RoPE angle increment.
391fn per_step_angle(rot_ba: Tensor<2>, dt_bh: Tensor<2>) -> Tensor<3> {
392    dt_bh.unsqueeze_dim::<3>(2) * (rot_ba.tanh() * PI).unsqueeze_dim::<3>(1)
393}
394
395/// `g = Δ · π·tanh(rot)` per quaternion block — the constant per-step rotation
396/// generator (`q = exp(g/2)`).
397fn per_step_generator(rot_ba: Tensor<2>, dt_bh: Tensor<2>, blocks: usize) -> Tensor<4> {
398    let [batch, _a] = rot_ba.dims();
399    (rot_ba.tanh() * PI)
400        .reshape([batch, blocks, 3])
401        .unsqueeze_dim::<4>(1)
402        * dt_bh.unsqueeze_dim::<3>(2).unsqueeze_dim::<4>(3)
403}
404
405// ---------------------------------------------------------------------------
406// Complex helpers (per RoPE pair, `[batch, nheads, num_rope_angles]`)
407// ---------------------------------------------------------------------------
408
409/// `(cos, sin)` of an angle tensor, reduced mod `2π` first (value-exact).
410fn cos_sin(angle_bha: Tensor<3>) -> (Tensor<3>, Tensor<3>) {
411    let a = wrap_angle(angle_bha);
412    (a.clone().cos(), a.sin())
413}
414
415/// Component-wise complex product `(ar + i·ai)(br + i·bi)`.
416fn complex_mul(
417    ar: Tensor<3>,
418    ai: Tensor<3>,
419    br: Tensor<3>,
420    bi: Tensor<3>,
421) -> (Tensor<3>, Tensor<3>) {
422    (
423        ar.clone() * br.clone() - ai.clone() * bi.clone(),
424        ai * br + ar * bi,
425    )
426}
427
428/// Component-wise complex quotient; `|den|²` floored by `div_eps`.
429fn complex_div(
430    nr: Tensor<3>,
431    ni: Tensor<3>,
432    dr: Tensor<3>,
433    di: Tensor<3>,
434) -> (Tensor<3>, Tensor<3>) {
435    let eps = div_eps(dr.dtype());
436    let d2 = (dr.clone() * dr.clone() + di.clone() * di.clone()).clamp_min(eps);
437    (
438        (nr.clone() * dr.clone() + ni.clone() * di.clone()) / d2.clone(),
439        (ni * dr - nr * di) / d2,
440    )
441}
442
443/// Multiply the rotation-active entries of `x` by the complex scalar
444/// `(re, im)` per pair — same pairing conventions as [`apply_rope_partial`] —
445/// and the pass-through entries by the real scalar `tail`.
446///
447/// `re`/`im` are `[batch, nheads, num_rope_angles]` (broadcast over the
448/// `mimo_rank` axis); `tail` is `[batch, nheads]`.
449fn mul_complex_partial(
450    x_bmhr: Tensor<4>,
451    re_bha: Tensor<3>,
452    im_bha: Tensor<3>,
453    tail_bh: Tensor<2>,
454    rope_dim: usize,
455    rotate_pairwise: bool,
456) -> Tensor<4> {
457    let [batch, mimo_rank, nheads, state_rank] = x_bmhr.dims();
458    let tail_b1h1 = tail_bh.unsqueeze_dims::<4>(&[1, 3]);
459    if rope_dim == 0 {
460        // RoPE disabled: every channel is a plain geometric series.
461        return x_bmhr * tail_b1h1;
462    }
463    let re_b1ha = re_bha.unsqueeze_dim::<4>(1);
464    let im_b1ha = im_bha.unsqueeze_dim::<4>(1);
465
466    if rotate_pairwise {
467        // Interleaved (SISO/NeoX): pairs are local, so the first `rope_dim`
468        // entries can be handled standalone.
469        let n2 = rope_dim / 2;
470        let head = x_bmhr.clone().narrow(3, 0, rope_dim);
471        let head_pairs = head.reshape([batch, mimo_rank, nheads, n2, 2]);
472        let x0 = head_pairs.clone().narrow(4, 0, 1).squeeze_dim::<4>(4);
473        let x1 = head_pairs.narrow(4, 1, 1).squeeze_dim::<4>(4);
474        let x0m = re_b1ha.clone() * x0.clone() - im_b1ha.clone() * x1.clone();
475        let x1m = im_b1ha * x0 + re_b1ha * x1;
476        let head = Tensor::cat(
477            vec![x0m.unsqueeze_dim::<5>(4), x1m.unsqueeze_dim::<5>(4)],
478            4,
479        )
480        .reshape([batch, mimo_rank, nheads, rope_dim]);
481        if rope_dim == state_rank {
482            head
483        } else {
484            let tail = x_bmhr.narrow(3, rope_dim, state_rank - rope_dim) * tail_b1h1;
485            Tensor::cat(vec![head, tail], 3)
486        }
487    } else {
488        // Half-and-half (MIMO/GPT-J): entry `i` pairs with `i + state_rank/2`;
489        // only the first `rope_dim / 2` pairs are active.
490        let half = state_rank / 2;
491        let active = rope_dim / 2;
492        let x_h1 = x_bmhr.clone().narrow(3, 0, half);
493        let x_h2 = x_bmhr.narrow(3, half, half);
494        let x_h1_rope = x_h1.clone().narrow(3, 0, active);
495        let x_h2_rope = x_h2.clone().narrow(3, 0, active);
496        let h1_m = re_b1ha.clone() * x_h1_rope.clone() - im_b1ha.clone() * x_h2_rope.clone();
497        let h2_m = im_b1ha * x_h1_rope + re_b1ha * x_h2_rope;
498        if active == half {
499            Tensor::cat(vec![h1_m, h2_m], 3)
500        } else {
501            let h1_pass = x_h1.narrow(3, active, half - active) * tail_b1h1.clone();
502            let h2_pass = x_h2.narrow(3, active, half - active) * tail_b1h1;
503            Tensor::cat(vec![h1_m, h1_pass, h2_m, h2_pass], 3)
504        }
505    }
506}
507
508// ---------------------------------------------------------------------------
509// Quaternion helpers (per block, `[batch, nheads, blocks, 4]`)
510// ---------------------------------------------------------------------------
511
512/// `exp(k·g/2)` — the per-step rotation quaternion raised to the real power
513/// `k`, built like [`quat_from_scaled_axis`] but with the half-angle reduced
514/// mod `2π` so large `k` stays fp-accurate (the reduction is value-exact and
515/// the wrap offset is detached, so `d/dg` matches the unrolled product).
516fn quat_pow(g_bhj3: Tensor<4>, k: f32) -> Tensor<4> {
517    let eps = div_eps(g_bhj3.dtype());
518    let angle = (g_bhj3.clone() * g_bhj3.clone())
519        .sum_dim(3)
520        .clamp_min(eps)
521        .sqrt(); // [b, h, j, 1]
522    let half = wrap_angle(angle.clone() * (0.5 * k));
523    let w = half.clone().cos();
524    let v = g_bhj3 * (half.sin() / angle);
525    quat_normalize(Tensor::cat(vec![w, v], 3))
526}
527
528/// `1 − sq` for a (scalar-scaled) quaternion `sq`: negate, add 1 to the real
529/// part.
530fn quat_one_minus(sq_bhj4: Tensor<4>) -> Tensor<4> {
531    let w = -sq_bhj4.clone().narrow(3, 0, 1) + 1.0;
532    let xyz = -sq_bhj4.narrow(3, 1, 3);
533    Tensor::cat(vec![w, xyz], 3)
534}
535
536/// `a + b·q` for per-head scalars `a`, `b` (`[batch, nheads, 1, 1]`) and a
537/// quaternion tensor `q`.
538fn quat_scalar_affine(a_bh11: Tensor<4>, b_bh11: Tensor<4>, q_bhj4: Tensor<4>) -> Tensor<4> {
539    let w = a_bh11 + b_bh11.clone() * q_bhj4.clone().narrow(3, 0, 1);
540    let xyz = b_bh11 * q_bhj4.narrow(3, 1, 3);
541    Tensor::cat(vec![w, xyz], 3)
542}
543
544/// Quaternion inverse `q⁻¹ = q* / ‖q‖²`, with `‖q‖²` floored by `div_eps`.
545/// (Used on `1 − α q`, whose norm is bounded below by `1 − α > 0`.)
546fn quat_inv(q_bhj4: Tensor<4>) -> Tensor<4> {
547    let eps = div_eps(q_bhj4.dtype());
548    let n2 = (q_bhj4.clone() * q_bhj4.clone()).sum_dim(3).clamp_min(eps);
549    quat_conj(q_bhj4) / n2
550}
551
552/// Left-multiply the first `rope_width` state-rank entries of `x` by the (not
553/// necessarily unit) quaternion `f` per block, and scale the pass-through
554/// entries by the real scalar `tail` — the quaternion analogue of
555/// [`mul_complex_partial`].
556fn mul_quat_partial(
557    x_bmhr: Tensor<4>,
558    f_bhj4: Tensor<4>,
559    tail_bh: Tensor<2>,
560    rope_width: usize,
561) -> Tensor<4> {
562    let [batch, mimo_rank, nheads, state_rank] = x_bmhr.dims();
563    let blocks = f_bhj4.dims()[2];
564    let tail_b1h1 = tail_bh.unsqueeze_dims::<4>(&[1, 3]);
565    if rope_width == 0 {
566        return x_bmhr * tail_b1h1;
567    }
568    let f_bmhj4 = f_bhj4
569        .unsqueeze_dim::<5>(1)
570        .expand([batch, mimo_rank, nheads, blocks, 4]);
571    if rope_width == state_rank {
572        rotate_state_rank_blocks::<4, 5>(x_bmhr, f_bmhj4)
573    } else {
574        let head = rotate_state_rank_blocks::<4, 5>(
575            x_bmhr.clone().narrow(3, 0, rope_width),
576            f_bmhj4,
577        );
578        let tail = x_bmhr.narrow(3, rope_width, state_rank - rope_width) * tail_b1h1;
579        Tensor::cat(vec![head, tail], 3)
580    }
581}
582
583// ---------------------------------------------------------------------------
584// Tests
585// ---------------------------------------------------------------------------
586
587#[cfg(all(test, feature = "_dev-test"))]
588mod tests;