Skip to main content

combs_models/
rope.rs

1//! Rotary positional embeddings (standard Llama RoPE, half-split /
2//! `rotate_half` convention as used by HuggingFace `transformers`),
3//! including the frequency-scaling variants from `modeling_rope_utils`
4//! (linear, llama3 piecewise, YaRN NTK-by-parts).
5
6use burn::tensor::{Tensor, TensorData, backend::Backend, Device};
7use combs_formats::RopeScaling;
8
9/// Precomputed RoPE cosine/sine tables.
10///
11/// Frequencies: `inv_freq[i] = theta^(-2i / head_dim)` for `i in 0..head_dim/2`.
12/// Tables are `[max_position, head_dim]` with the half-dim frequencies
13/// duplicated (`cat([cos, cos])`) to match the half-split convention
14/// (`config.rope_interleaved = false`, the HF Llama default).
15pub struct RotaryEmbedding<B: Backend> {
16    cos: Tensor<B, 2>,
17    sin: Tensor<B, 2>,
18}
19
20/// Computes the `[max_position, head_dim]` cos/sin tables on the host.
21/// Exposed for unit tests.
22pub fn build_tables(
23    head_dim: usize,
24    theta: f64,
25    max_position: usize,
26) -> (Vec<f32>, Vec<f32>) {
27    build_tables_scaled(head_dim, theta, max_position, &RopeScaling::None)
28}
29
30/// Per-frequency scaled `inv_freq` plus the attention (mscale) multiplier
31/// applied to both tables — pure host f64 math matching HF
32/// `modeling_rope_utils` formulas. Exposed for the formula harmony tests.
33pub fn scaled_inv_freq(head_dim: usize, theta: f64, scaling: &RopeScaling) -> (Vec<f64>, f64) {
34    let half = head_dim / 2;
35    let base: Vec<f64> = (0..half)
36        .map(|i| theta.powf(-2.0 * i as f64 / head_dim as f64))
37        .collect();
38    match scaling {
39        RopeScaling::None => (base, 1.0),
40        RopeScaling::Linear { factor } => (base.iter().map(|f| f / factor).collect(), 1.0),
41        RopeScaling::Llama3 {
42            factor,
43            low_freq_factor,
44            high_freq_factor,
45            original_max_position_embeddings,
46        } => {
47            let orig = *original_max_position_embeddings as f64;
48            let low_wavelen = orig / low_freq_factor;
49            let high_wavelen = orig / high_freq_factor;
50            let scaled = base
51                .iter()
52                .map(|&f| {
53                    let wavelen = 2.0 * std::f64::consts::PI / f;
54                    if wavelen < high_wavelen {
55                        f
56                    } else if wavelen > low_wavelen {
57                        f / factor
58                    } else {
59                        let smooth = (orig / wavelen - low_freq_factor)
60                            / (high_freq_factor - low_freq_factor);
61                        (1.0 - smooth) * f / factor + smooth * f
62                    }
63                })
64                .collect();
65            (scaled, 1.0)
66        }
67        RopeScaling::Yarn {
68            factor,
69            original_max_position_embeddings,
70            beta_fast,
71            beta_slow,
72            attention_factor,
73        } => {
74            let dim = head_dim as f64;
75            let orig = *original_max_position_embeddings as f64;
76            let corr_dim = |rotations: f64| {
77                dim * (orig / (rotations * 2.0 * std::f64::consts::PI)).ln()
78                    / (2.0 * theta.ln())
79            };
80            let low = corr_dim(*beta_fast).floor().max(0.0);
81            let mut high = corr_dim(*beta_slow).ceil().min(dim - 1.0);
82            if (high - low).abs() < f64::EPSILON {
83                high += 0.001; // avoid a zero-width ramp
84            }
85            let scaled = (0..half)
86                .map(|i| {
87                    let pos_freq = theta.powf(2.0 * i as f64 / dim);
88                    let extrapolation = 1.0 / pos_freq;
89                    let interpolation = 1.0 / (factor * pos_freq);
90                    let ramp = ((i as f64 - low) / (high - low)).clamp(0.0, 1.0);
91                    let extrapolation_factor = 1.0 - ramp;
92                    interpolation * (1.0 - extrapolation_factor)
93                        + extrapolation * extrapolation_factor
94                })
95                .collect();
96            let mscale = attention_factor.unwrap_or(0.1 * factor.ln() + 1.0);
97            (scaled, mscale)
98        }
99        RopeScaling::LongRope {
100            short_factor,
101            long_factor: _,
102            original_max_position_embeddings,
103            factor,
104            attention_factor,
105        } => {
106            // v1 builds the short-context tables (exact for sequences within
107            // the pretraining context, e.g. 4096 for phi-3 128k variants);
108            // the runtime long-table switch lands with the first
109            // beyond-original-context preset.
110            let scaled = base
111                .iter()
112                .enumerate()
113                .map(|(i, f)| f / short_factor.get(i).copied().unwrap_or(1.0))
114                .collect();
115            let mscale = attention_factor.unwrap_or_else(|| {
116                if *factor <= 1.0 {
117                    1.0
118                } else {
119                    (1.0 + factor.ln() / (*original_max_position_embeddings as f64).ln())
120                        .sqrt()
121                }
122            });
123            (scaled, mscale)
124        }
125    }
126}
127
128/// [`build_tables`] with RoPE frequency scaling. The YaRN attention factor
129/// multiplies both tables (temperature on the rotation, HF convention).
130pub fn build_tables_scaled(
131    head_dim: usize,
132    theta: f64,
133    max_position: usize,
134    scaling: &RopeScaling,
135) -> (Vec<f32>, Vec<f32>) {
136    let (inv_freq, mscale) = scaled_inv_freq(head_dim, theta, scaling);
137    let mut cos = Vec::with_capacity(max_position * head_dim);
138    let mut sin = Vec::with_capacity(max_position * head_dim);
139    for pos in 0..max_position {
140        // Half-split layout: the half-dim frequencies appear twice so the
141        // table can be applied elementwise against [x1, x2].
142        for _ in 0..2 {
143            for f in &inv_freq {
144                let angle = pos as f64 * f;
145                cos.push((angle.cos() * mscale) as f32);
146                sin.push((angle.sin() * mscale) as f32);
147            }
148        }
149    }
150    (cos, sin)
151}
152
153impl<B: Backend> RotaryEmbedding<B> {
154    /// Builds tables on `device` (no frequency scaling).
155    pub fn new(head_dim: usize, theta: f64, max_position: usize, device: &Device<B>) -> Self {
156        Self::new_scaled(head_dim, theta, max_position, &RopeScaling::None, device)
157    }
158
159    /// Builds tables on `device` with RoPE frequency scaling applied.
160    pub fn new_scaled(
161        head_dim: usize,
162        theta: f64,
163        max_position: usize,
164        scaling: &RopeScaling,
165        device: &Device<B>,
166    ) -> Self {
167        let (cos, sin) = build_tables_scaled(head_dim, theta, max_position, scaling);
168        RotaryEmbedding {
169            cos: Tensor::from_data(
170                TensorData::new(cos, [max_position, head_dim]),
171                device,
172            ),
173            sin: Tensor::from_data(
174                TensorData::new(sin, [max_position, head_dim]),
175                device,
176            ),
177        }
178    }
179
180    /// Applies RoPE to a `[batch, heads, seq, head_dim]` tensor whose first
181    /// sequence position is at absolute position `pos`.
182    pub fn apply(&self, x: Tensor<B, 4>, pos: usize) -> Tensor<B, 4> {
183        let [batch, heads, seq, dim] = x.dims();
184        let half = dim / 2;
185        let cos = self
186            .cos
187            .clone()
188            .narrow(0, pos, seq)
189            .reshape([1, 1, seq, dim]);
190        let sin = self
191            .sin
192            .clone()
193            .narrow(0, pos, seq)
194            .reshape([1, 1, seq, dim]);
195
196        // rotate_half(x) = cat([-x2, x1]) along the head_dim axis.
197        let x1 = x.clone().narrow(3, 0, half);
198        let x2 = x.clone().narrow(3, half, half);
199        let rotated = Tensor::cat(vec![x2.neg(), x1], 3);
200
201        let out = x * cos + rotated * sin;
202        debug_assert_eq!(out.dims(), [batch, heads, seq, dim]);
203        out
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn tables_match_known_math() {
213        let head_dim = 8;
214        let theta = 10000.0f64;
215        let max_pos = 4;
216        let (cos, sin) = build_tables(head_dim, theta, max_pos);
217        // inv_freq[i] = theta^(-2i/d): i=0 -> 1, i=1 -> 10000^-0.25 = 0.1,
218        // i=2 -> 0.01, i=3 -> 0.001.
219        let inv = [1.0f64, 0.1, 0.01, 0.001];
220        for pos in 0..max_pos {
221            for i in 0..4 {
222                let angle = pos as f64 * inv[i];
223                // half-split layout: index i and i + half share the frequency.
224                for idx in [pos * head_dim + i, pos * head_dim + i + 4] {
225                    assert!(
226                        (cos[idx] as f64 - angle.cos()).abs() < 1e-5,
227                        "cos mismatch at pos {pos} i {i}"
228                    );
229                    assert!(
230                        (sin[idx] as f64 - angle.sin()).abs() < 1e-5,
231                        "sin mismatch at pos {pos} i {i}"
232                    );
233                }
234            }
235        }
236        // Position 0 must be the identity rotation.
237        for i in 0..head_dim {
238            assert!((cos[i] - 1.0).abs() < 1e-6);
239            assert!(sin[i].abs() < 1e-6);
240        }
241    }
242
243    /// Reference values computed independently (Python f64) from the HF
244    /// `modeling_rope_utils` formulas.
245    #[test]
246    fn linear_scaling_divides_frequencies() {
247        let (inv, mscale) = scaled_inv_freq(8, 10_000.0, &RopeScaling::Linear { factor: 2.0 });
248        let expected = [0.5, 0.05, 0.005, 0.0005];
249        for (i, e) in expected.iter().enumerate() {
250            assert!((inv[i] - e).abs() < 1e-12, "linear inv[{i}]");
251        }
252        assert_eq!(mscale, 1.0);
253    }
254
255    #[test]
256    fn llama3_scaling_matches_reference() {
257        // llama-3.2-1b config: dim 64, theta 500000, factor 32, low 1,
258        // high 4, original 8192.
259        let scaling = RopeScaling::Llama3 {
260            factor: 32.0,
261            low_freq_factor: 1.0,
262            high_freq_factor: 4.0,
263            original_max_position_embeddings: 8192,
264        };
265        let (inv, mscale) = scaled_inv_freq(64, 500_000.0, &scaling);
266        let expected = [
267            (0usize, 1.0),
268            (8, 0.037606030931),
269            (16, 0.000429556797),
270            (24, 1.661967e-06),
271            (31, 9.4183e-08),
272        ];
273        for (i, e) in expected {
274            let rel = ((inv[i] - e) / e).abs();
275            assert!(rel < 1e-6, "llama3 inv[{i}]: {} vs {e}", inv[i]);
276        }
277        assert_eq!(mscale, 1.0);
278    }
279
280    #[test]
281    fn yarn_scaling_matches_reference() {
282        // Qwen-style: dim 128, theta 1e6, factor 4, original 32768,
283        // beta 32/1 -> correction range [23, 40], mscale 0.1·ln4 + 1.
284        let scaling = RopeScaling::Yarn {
285            factor: 4.0,
286            original_max_position_embeddings: 32768,
287            beta_fast: 32.0,
288            beta_slow: 1.0,
289            attention_factor: None,
290        };
291        let (inv, mscale) = scaled_inv_freq(128, 1_000_000.0, &scaling);
292        let expected = [
293            (0usize, 1.0),
294            (16, 0.03162277660168379),
295            (32, 0.0006029411764705882),
296            (48, 7.905694150420949e-06),
297            (63, 3.102344401879299e-07),
298        ];
299        for (i, e) in expected {
300            let rel = ((inv[i] - e) / e).abs();
301            assert!(rel < 1e-6, "yarn inv[{i}]: {} vs {e}", inv[i]);
302        }
303        assert!((mscale - 1.138629436112).abs() < 1e-9, "mscale {mscale}");
304    }
305
306    #[test]
307    fn longrope_scaling_matches_reference() {
308        // Synthetic phi-style config: dim 8 (base inv_freq 1/0.1/0.01/0.001),
309        // short divisors [1,2,4,8], context extended 4096 → 131072
310        // (factor 32). HF `_compute_longrope_parameters`:
311        //   inv_freq[i] = base[i] / short_factor[i]
312        //   attention_factor = sqrt(1 + ln(32)/ln(4096)) = sqrt(17/12).
313        let scaling = RopeScaling::LongRope {
314            short_factor: vec![1.0, 2.0, 4.0, 8.0],
315            long_factor: vec![1.0; 4],
316            original_max_position_embeddings: 4096,
317            factor: 32.0,
318            attention_factor: None,
319        };
320        let (inv, mscale) = scaled_inv_freq(8, 10_000.0, &scaling);
321        let expected = [1.0, 0.05, 0.0025, 0.000125];
322        for (i, e) in expected.iter().enumerate() {
323            assert!((inv[i] - e).abs() < 1e-15, "longrope inv[{i}]: {}", inv[i]);
324        }
325        assert!(
326            (mscale - (17.0f64 / 12.0).sqrt()).abs() < 1e-12,
327            "mscale {mscale}"
328        );
329        // Unextended context (factor 1) keeps attention untouched.
330        let scaling = RopeScaling::LongRope {
331            short_factor: vec![1.0; 4],
332            long_factor: vec![1.0; 4],
333            original_max_position_embeddings: 4096,
334            factor: 1.0,
335            attention_factor: None,
336        };
337        let (_, mscale) = scaled_inv_freq(8, 10_000.0, &scaling);
338        assert_eq!(mscale, 1.0);
339    }
340
341    #[test]
342    fn scaled_tables_apply_mscale() {
343        // Position 0 cos = mscale (not 1) under YaRN.
344        let scaling = RopeScaling::Yarn {
345            factor: 4.0,
346            original_max_position_embeddings: 32768,
347            beta_fast: 32.0,
348            beta_slow: 1.0,
349            attention_factor: Some(1.25),
350        };
351        let (cos, sin) = build_tables_scaled(8, 10_000.0, 2, &scaling);
352        assert!((cos[0] - 1.25).abs() < 1e-6);
353        assert!(sin[0].abs() < 1e-6);
354    }
355}