ruvector_turboquant/
rotation.rs1const ROUNDS: usize = 3;
25
26pub(crate) struct SplitMix64(pub u64);
30
31impl SplitMix64 {
32 #[inline]
33 pub fn next_u64(&mut self) -> u64 {
34 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
35 let mut z = self.0;
36 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
37 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
38 z ^ (z >> 31)
39 }
40
41 #[inline]
43 fn next_below(&mut self, bound: u64) -> u64 {
44 debug_assert!(bound > 0);
45 loop {
46 let v = self.next_u64();
47 if v < u64::MAX - (u64::MAX % bound) {
49 return v % bound;
50 }
51 }
52 }
53}
54
55struct Round {
58 sign_words: Vec<u64>,
60 perm: Vec<u32>,
62}
63
64pub struct Rotation {
67 dim: usize,
68 rounds: Vec<Round>,
69 blocks: Vec<usize>,
73}
74
75impl Rotation {
76 pub fn new(dim: usize, seed: u64) -> Self {
78 assert!(dim >= 2, "Turbo4 rotation requires dim >= 2, got {dim}");
79 let mut rng = SplitMix64(seed ^ 0x5175_6472_616E_7434); let n_words = dim.div_ceil(64);
81
82 let rounds = (0..ROUNDS)
83 .map(|_| {
84 let sign_words: Vec<u64> = (0..n_words).map(|_| rng.next_u64()).collect();
85 let mut perm: Vec<u32> = (0..dim as u32).collect();
87 for i in (1..dim).rev() {
88 let j = rng.next_below(i as u64 + 1) as usize;
89 perm.swap(i, j);
90 }
91 Round { sign_words, perm }
92 })
93 .collect();
94
95 let mut blocks = Vec::new();
97 let mut bit = usize::BITS - 1 - dim.leading_zeros();
98 loop {
99 if dim & (1 << bit) != 0 {
100 blocks.push(1usize << bit);
101 }
102 if bit == 0 {
103 break;
104 }
105 bit -= 1;
106 }
107
108 Self {
109 dim,
110 rounds,
111 blocks,
112 }
113 }
114
115 #[inline]
117 pub fn dim(&self) -> usize {
118 self.dim
119 }
120
121 pub fn apply_in_place(&self, v: &mut [f32], scratch: &mut [f32]) {
124 assert_eq!(v.len(), self.dim);
125 assert_eq!(scratch.len(), self.dim);
126 for round in &self.rounds {
127 for (i, x) in v.iter_mut().enumerate() {
129 if round.sign_words[i / 64] >> (i % 64) & 1 != 0 {
130 *x = -*x;
131 }
132 }
133 for (i, &src) in round.perm.iter().enumerate() {
135 scratch[i] = v[src as usize];
136 }
137 v.copy_from_slice(scratch);
138 let mut off = 0;
140 for &b in &self.blocks {
141 fwht_normalized(&mut v[off..off + b]);
142 off += b;
143 }
144 }
145 }
146
147 pub fn apply(&self, v: &[f32]) -> Vec<f32> {
149 let mut out = v.to_vec();
150 let mut scratch = vec![0.0f32; self.dim];
151 self.apply_in_place(&mut out, &mut scratch);
152 out
153 }
154
155 pub fn apply_inverse(&self, v: &[f32]) -> Vec<f32> {
157 assert_eq!(v.len(), self.dim);
158 let mut out = v.to_vec();
159 let mut scratch = vec![0.0f32; self.dim];
160 for round in self.rounds.iter().rev() {
161 let mut off = 0;
163 for &b in &self.blocks {
164 fwht_normalized(&mut out[off..off + b]);
165 off += b;
166 }
167 for (i, &src) in round.perm.iter().enumerate() {
169 scratch[src as usize] = out[i];
170 }
171 out.copy_from_slice(&scratch);
172 for (i, x) in out.iter_mut().enumerate() {
174 if round.sign_words[i / 64] >> (i % 64) & 1 != 0 {
175 *x = -*x;
176 }
177 }
178 }
179 out
180 }
181}
182
183fn fwht_normalized(v: &mut [f32]) {
187 let n = v.len();
188 debug_assert!(n.is_power_of_two());
189 if n == 1 {
190 return;
191 }
192 let mut h = 1;
193 while h < n {
194 let mut i = 0;
195 while i < n {
196 for j in i..i + h {
197 let x = v[j];
198 let y = v[j + h];
199 v[j] = x + y;
200 v[j + h] = x - y;
201 }
202 i += h * 2;
203 }
204 h *= 2;
205 }
206 let scale = 1.0 / (n as f32).sqrt();
207 for x in v.iter_mut() {
208 *x *= scale;
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn gauss_vec(dim: usize, seed: u64) -> Vec<f32> {
217 let mut rng = SplitMix64(seed);
219 let mut out = Vec::with_capacity(dim);
220 while out.len() < dim {
221 let u1 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
222 let u2 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
223 let r = (-2.0 * u1.max(1e-12).ln()).sqrt();
224 let (s, c) = (2.0 * std::f64::consts::PI * u2).sin_cos();
225 out.push((r * c) as f32);
226 if out.len() < dim {
227 out.push((r * s) as f32);
228 }
229 }
230 out
231 }
232
233 fn norm(v: &[f32]) -> f32 {
234 v.iter().map(|x| x * x).sum::<f32>().sqrt()
235 }
236
237 #[test]
238 fn preserves_norm_pow2_and_non_pow2() {
239 for dim in [64usize, 128, 96, 1536, 1000, 3] {
240 let rot = Rotation::new(dim, 42);
241 let v = gauss_vec(dim, 7);
242 let r = rot.apply(&v);
243 let (n0, n1) = (norm(&v), norm(&r));
244 assert!(
245 (n0 - n1).abs() < 1e-3 * n0.max(1.0),
246 "dim {dim}: norm {n0} -> {n1}"
247 );
248 }
249 }
250
251 #[test]
252 fn preserves_inner_products() {
253 let dim = 96; let rot = Rotation::new(dim, 9);
255 let a = gauss_vec(dim, 1);
256 let b = gauss_vec(dim, 2);
257 let dot = |x: &[f32], y: &[f32]| x.iter().zip(y).map(|(p, q)| p * q).sum::<f32>();
258 let (ra, rb) = (rot.apply(&a), rot.apply(&b));
259 assert!((dot(&a, &b) - dot(&ra, &rb)).abs() < 1e-2 * dim as f32);
260 }
261
262 #[test]
263 fn inverse_roundtrips() {
264 let dim = 200; let rot = Rotation::new(dim, 5);
266 let v = gauss_vec(dim, 3);
267 let back = rot.apply_inverse(&rot.apply(&v));
268 for (x, y) in v.iter().zip(&back) {
269 assert!((x - y).abs() < 1e-4, "{x} vs {y}");
270 }
271 }
272
273 #[test]
274 fn deterministic_across_builds() {
275 let dim = 128;
276 let (r1, r2) = (Rotation::new(dim, 42), Rotation::new(dim, 42));
277 let v = gauss_vec(dim, 11);
278 assert_eq!(r1.apply(&v), r2.apply(&v));
279 let r3 = Rotation::new(dim, 43);
281 assert_ne!(r1.apply(&v), r3.apply(&v));
282 }
283
284 #[test]
285 fn spreads_spike_across_coordinates() {
286 let dim = 1536;
288 let rot = Rotation::new(dim, 42);
289 let mut v = vec![0.0f32; dim];
290 v[17] = 1.0;
291 let r = rot.apply(&v);
292 let max = r.iter().fold(0.0f32, |m, x| m.max(x.abs()));
293 assert!(max < 0.25, "spike not spread: max coord {max}");
294 }
295}