use kopitiam_core::{Error, Result};
use kopitiam_tensor::Tensor;
pub struct RotaryEmbedding {
rope_dim: usize,
half: usize,
cos: Vec<f32>,
sin: Vec<f32>,
}
impl RotaryEmbedding {
pub fn new(rope_dim: usize, theta: f32, max_position: usize) -> Self {
debug_assert!(rope_dim.is_multiple_of(2), "rope_dim must be even");
let half = rope_dim / 2;
let freqs: Vec<f32> = (0..half).map(|i| theta.powf(-2.0 * i as f32 / rope_dim as f32)).collect();
let mut cos = vec![0.0f32; max_position * half];
let mut sin = vec![0.0f32; max_position * half];
for pos in 0..max_position {
for i in 0..half {
let angle = pos as f32 * freqs[i];
cos[pos * half + i] = angle.cos();
sin[pos * half + i] = angle.sin();
}
}
Self { rope_dim, half, cos, sin }
}
pub fn apply(&self, x: &Tensor, positions: &[usize]) -> Result<Tensor> {
let dims = x.shape().dims();
if dims.len() != 3 {
return Err(shape_err(x));
}
let (n_heads, seq, head_dim) = (dims[0], dims[1], dims[2]);
if head_dim < self.rope_dim || seq != positions.len() {
return Err(shape_err(x));
}
let data = x.to_vec_f32()?; let mut out = data.clone();
for h in 0..n_heads {
for (s, &pos) in positions.iter().enumerate() {
let base = (h * seq + s) * head_dim;
let table_base = pos * self.half;
for i in 0..self.half {
let cos = self.cos[table_base + i];
let sin = self.sin[table_base + i];
let x1 = data[base + i];
let x2 = data[base + self.half + i];
out[base + i] = x1 * cos - x2 * sin;
out[base + self.half + i] = x2 * cos + x1 * sin;
}
}
}
Tensor::from_f32(out, x.shape().clone())
}
}
fn shape_err(x: &Tensor) -> Error {
Error::ShapeMismatch { expected: x.shape().clone(), actual: x.shape().clone() }
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(a: f32, b: f32) {
assert!((a - b).abs() < 1e-4, "expected {b}, got {a}");
}
#[test]
fn apply_matches_split_half_hand_computation_and_differs_from_interleaved() {
let rope = RotaryEmbedding::new(4, 10_000.0, 4);
let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 1, 4]).unwrap();
let out = rope.apply(&x, &[1]).unwrap().to_vec_f32().unwrap();
let expected_split_half = [-1.984_110_6, 1.959_900_7, 2.462_378, 4.019_799_7];
for (o, e) in out.iter().zip(expected_split_half) {
assert_close(*o, e);
}
let interleaved_would_give = [-1.142_639_7, 1.922_075_6, 2.959_850_7, 4.029_799_5];
assert_ne!(
out, interleaved_would_give,
"split-half and interleaved RoPE must disagree on this input"
);
}
#[test]
fn rotation_preserves_vector_norm() {
let rope = RotaryEmbedding::new(8, 10_000.0, 16);
let input: Vec<f32> = (0..8).map(|i| (i as f32) * 0.37 - 1.1).collect();
let norm_in: f32 = input.iter().map(|v| v * v).sum::<f32>().sqrt();
for pos in 0..16 {
let x = Tensor::from_f32(input.clone(), [1, 1, 8]).unwrap();
let out = rope.apply(&x, &[pos]).unwrap().to_vec_f32().unwrap();
let norm_out: f32 = out.iter().map(|v| v * v).sum::<f32>().sqrt();
assert!((norm_in - norm_out).abs() < 1e-4, "pos {pos}: {norm_in} vs {norm_out}");
}
}
#[test]
fn position_zero_is_the_identity() {
let rope = RotaryEmbedding::new(4, 10_000.0, 4);
let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 1, 4]).unwrap();
let out = rope.apply(&x, &[0]).unwrap().to_vec_f32().unwrap();
assert_close(out[0], 1.0);
assert_close(out[1], 2.0);
assert_close(out[2], 3.0);
assert_close(out[3], 4.0);
}
#[test]
fn dimensions_beyond_rope_dim_pass_through_unchanged() {
let rope = RotaryEmbedding::new(2, 10_000.0, 4);
let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 1, 4]).unwrap();
let out = rope.apply(&x, &[1]).unwrap().to_vec_f32().unwrap();
assert_close(out[2], 3.0);
assert_close(out[3], 4.0);
assert!((out[0] - 1.0).abs() > 1e-3 || (out[1] - 2.0).abs() > 1e-3);
}
#[test]
fn different_positions_in_the_same_batch_get_different_rotations() {
let rope = RotaryEmbedding::new(4, 10_000.0, 8);
let x = Tensor::from_f32(
vec![
1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0, ],
[1, 2, 4],
)
.unwrap();
let out = rope.apply(&x, &[0, 5]).unwrap().to_vec_f32().unwrap();
assert_eq!(&out[0..4], &[1.0, 2.0, 3.0, 4.0]);
assert_ne!(&out[4..8], &[1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn wrong_rank_is_rejected() {
let rope = RotaryEmbedding::new(4, 10_000.0, 4);
let x = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [4]).unwrap();
assert!(matches!(rope.apply(&x, &[0]), Err(Error::ShapeMismatch { .. })));
}
#[test]
fn mismatched_positions_length_is_rejected() {
let rope = RotaryEmbedding::new(4, 10_000.0, 4);
let x = Tensor::from_f32(vec![1.0; 8], [1, 2, 4]).unwrap();
assert!(matches!(rope.apply(&x, &[0]), Err(Error::ShapeMismatch { .. })));
}
}