Skip to main content

burn_mamba/mamba3/rotation/
rope.rs

1//! Rotary (RoPE) application helpers for the abelian rotation pathway.
2//!
3//! Purely mechanical: given per-pair cumulative angles, rotate the paired
4//! channels of B/C. See the [module header](crate::mamba3::rotation) for why
5//! these angles are a *state transition*, not a positional encoding.
6
7use burn::prelude::*;
8
9// ---------------------------------------------------------------------------
10// RoPE utility
11// ---------------------------------------------------------------------------
12
13/// Reduce angles modulo `2π` into `[−π, π]`, leaving the autodiff graph intact.
14///
15/// `sin`/`cos` are `2π`-periodic, so subtracting an integer multiple of `2π` is
16/// value-exact. Keeping `|angle| ≤ π` preserves precision in low-bit floats —
17/// roughly half of `f16`'s representable values lie in `|x| ≤ 1`, and the
18/// periodic `sin`/`cos` only lose accuracy when the argument is allowed to drift
19/// to large magnitudes. The same applies to the cumulative angle accumulator,
20/// which would otherwise grow without bound across a long sequence / many decode
21/// steps.
22///
23/// The integer multiple `k` is `detach`ed, so it is a constant with respect to
24/// autodiff: `d/dx (x − k·2π) = 1`, i.e. the backward pass is identical to the
25/// un-wrapped angle. This mirrors the detached `max` rescaling in
26/// [`RmsNormGated`](burn_stack::modules::norm::rms_norm_gated::RmsNormGated).
27pub fn wrap_angle<const D: usize>(angles: Tensor<D>) -> Tensor<D> {
28    let two_pi = 2.0 * std::f32::consts::PI;
29    let k = (angles.clone().detach() * (1.0f32 / two_pi)).round();
30    angles - k * two_pi
31}
32
33/// Apply a rotary embedding (RoPE) to `x` along its last dimension.
34///
35/// Purely the mechanical pairwise rotation. In Mamba-3 the `angles` are the
36/// cumulative rotation of the *state transition* (see [`crate::mamba3::mamba3`]),
37/// not a position index.
38///
39/// Two pairing conventions are supported, selected by `rotate_pairwise`:
40///
41/// - `rotate_pairwise = true` — **interleaved** (NeoX / Triton style): adjacent
42///   pairs `(0,1)`, `(2,3)`, … are rotated together. Used by the SISO Triton
43///   kernel (`mamba3_siso_*.py`).
44/// - `rotate_pairwise = false` — **half-and-half** (GPT-J style): position `n`
45///   is paired with `n + state_rank/2`. Used by the MIMO Tilelang kernel
46///   (`mamba3_mimo_fwd.py`).
47///
48/// Reference: `mamba3.py:335` sets `rotate_pairwise = not self.is_mimo`.
49///
50/// # Shapes
51/// - `x`:      `[..., state_rank]` where `state_rank` is even
52/// - `angles`: `[..., state_rank / 2]`  (one angle per pair)
53/// - output:   same shape as `x`
54pub fn apply_rope<const D: usize>(
55    x: Tensor<D>,
56    angles: Tensor<D>,
57    rotate_pairwise: bool,
58) -> Tensor<D> {
59    let dims = x.dims();
60    let n = dims[D - 1];
61    let n2 = n / 2;
62    let leading: usize = dims[..D - 1].iter().product();
63
64    let angles_flat = wrap_angle(angles.reshape([leading, n2]));
65    let cos = angles_flat.clone().cos();
66    let sin = angles_flat.sin();
67
68    if rotate_pairwise {
69        // Interleaved: reshape to [leading, n2, 2], pairs along last axis.
70        let x_pairs = x.reshape([leading, n2, 2]);
71        let x0 = x_pairs.clone().narrow(2, 0, 1).squeeze_dim(2);
72        let x1 = x_pairs.narrow(2, 1, 1).squeeze_dim(2);
73
74        let x0r = cos.clone() * x0.clone() - sin.clone() * x1.clone();
75        let x1r = sin * x0 + cos * x1;
76
77        Tensor::cat(
78            vec![x0r.unsqueeze_dim::<3>(2), x1r.unsqueeze_dim::<3>(2)],
79            2,
80        )
81        .reshape(dims)
82    } else {
83        // Half-and-half: reshape to [leading, 2, n2], halves along middle axis.
84        let x_halves = x.reshape([leading, 2, n2]);
85        let x0 = x_halves.clone().narrow(1, 0, 1).squeeze_dim(1);
86        let x1 = x_halves.narrow(1, 1, 1).squeeze_dim(1);
87
88        let x0r = cos.clone() * x0.clone() - sin.clone() * x1.clone();
89        let x1r = sin * x0 + cos * x1;
90
91        Tensor::cat(
92            vec![x0r.unsqueeze_dim::<3>(1), x1r.unsqueeze_dim::<3>(1)],
93            1,
94        )
95        .reshape(dims)
96    }
97}
98
99/// Apply RoPE to only the rotation-active entries of the last dimension; the
100/// remainder passes through unchanged. Falls back to [`apply_rope`] when
101/// `rope_dim == state_rank` (full RoPE). `rope_dim` must be positive — a block
102/// that rotates nothing is
103/// [`RotationKind::Real1D`](crate::mamba3::rotation::RotationKind::Real1D),
104/// which never reaches here.
105///
106/// Pairing scheme (must match the reference kernels — see Section
107/// "Data-Dependent RoPE" in the paper, and `mamba3_siso_fwd.py` /
108/// `mamba3_mimo_fwd.py`):
109///
110/// - `rotate_pairwise = true` (SISO, interleaved/NeoX): pairs `(0,1), (2,3), …`.
111///   Only pairs `0..num_rope_angles` are rotated; pairs beyond are passed
112///   through. Equivalent to slicing the first `rope_dim` entries and rotating
113///   them.
114/// - `rotate_pairwise = false` (MIMO, half-and-half/GPT-J): pair distance is
115///   always `state_rank/2`, i.e. element `n` is paired with element
116///   `state_rank/2 + n`. With partial RoPE only the first `num_rope_angles`
117///   pairs are rotated; the remaining elements in both halves pass through.
118pub fn apply_rope_partial<const D: usize>(
119    x: Tensor<D>,
120    angles: Tensor<D>,
121    rope_dim: usize,
122    rotate_pairwise: bool,
123) -> Tensor<D> {
124    debug_assert!(rope_dim > 0, "partial RoPE requires rope_dim > 0");
125
126    let state_rank = x.dims()[D - 1];
127    if rope_dim == state_rank {
128        return apply_rope::<D>(x, angles, rotate_pairwise);
129    }
130
131    if rotate_pairwise {
132        // Pairs are local — slicing the first rope_dim entries gives the same
133        // result as the reference (which rotates the whole headdim but with
134        // identity cos/sin for the tail pairs).
135        let x_rope = x.clone().narrow(D - 1, 0, rope_dim);
136        let x_rest = x.narrow(D - 1, rope_dim, state_rank - rope_dim);
137        let x_rope_rotated = apply_rope::<D>(x_rope, angles, true);
138        return Tensor::cat(vec![x_rope_rotated, x_rest], D - 1);
139    }
140
141    // Half-and-half partial RoPE: pair distance must be `state_rank/2`, not
142    // `rope_dim/2`. Slicing the first `rope_dim` entries and calling
143    // `apply_rope` would pair within the slice and produce the wrong rotation.
144    let half = state_rank / 2;
145    let num_rope_angles = rope_dim / 2;
146    debug_assert!(
147        num_rope_angles < half,
148        "partial RoPE requires rope_dim < state_rank here"
149    );
150
151    // Split x into the two halves, then within each half separate the
152    // rotation-active prefix from the pass-through suffix.
153    let x_h1 = x.clone().narrow(D - 1, 0, half);
154    let x_h2 = x.narrow(D - 1, half, half);
155    let x_h1_rope = x_h1.clone().narrow(D - 1, 0, num_rope_angles);
156    let x_h1_pass = x_h1.narrow(D - 1, num_rope_angles, half - num_rope_angles);
157    let x_h2_rope = x_h2.clone().narrow(D - 1, 0, num_rope_angles);
158    let x_h2_pass = x_h2.narrow(D - 1, num_rope_angles, half - num_rope_angles);
159
160    // angles: [..., num_rope_angles] — broadcasts element-wise against the rope-active slices.
161    let angles = wrap_angle(angles);
162    let cos = angles.clone().cos();
163    let sin = angles.sin();
164    let x_h1_rot = cos.clone() * x_h1_rope.clone() - sin.clone() * x_h2_rope.clone();
165    let x_h2_rot = sin * x_h1_rope + cos * x_h2_rope;
166
167    // Reassemble: [ first-half-rotated | first-half-passthrough | second-half-rotated | second-half-passthrough ]
168    let x_h1_out = Tensor::cat(vec![x_h1_rot, x_h1_pass], D - 1);
169    let x_h2_out = Tensor::cat(vec![x_h2_rot, x_h2_pass], D - 1);
170    Tensor::cat(vec![x_h1_out, x_h2_out], D - 1)
171}