use crate::quantization::turboquant::permutation::Permutation;
use crate::quantization::turboquant::simd;
const N_PERMUTATIONS: usize = 3;
const PERMUTATION_SEEDS: [u64; 3] = [654605292835415893, 8636605637963351413, 1775280196666917949];
pub struct HadamardRotation {
forward_maps: [Vec<u32>; N_PERMUTATIONS],
backward_maps: [Vec<u32>; N_PERMUTATIONS],
dim: usize,
}
impl HadamardRotation {
pub fn new(dim: usize) -> Self {
assert!(
u32::try_from(dim).is_ok(),
"HadamardRotation dim {dim} must fit in u32",
);
let forward_maps: [_; N_PERMUTATIONS] = std::array::from_fn(|index| {
let permutation = Permutation::new_one_way(PERMUTATION_SEEDS[index], dim);
let mut map: Vec<u32> = (0..dim as u32).collect();
permutation.permute(&mut map);
map
});
let backward_maps = std::array::from_fn(|index| {
let mut inverse = vec![0u32; dim];
for (position, &source) in forward_maps[index].iter().enumerate() {
inverse[source as usize] = position as u32;
}
inverse
});
Self {
forward_maps,
backward_maps,
dim,
}
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn apply(&self, x: &mut [f64]) {
assert_eq!(x.len(), self.dim);
wht_and_gather_rounds(x, self.forward_maps.iter());
}
pub fn apply_inverse(&self, y: &mut [f64]) {
assert_eq!(y.len(), self.dim);
wht_and_gather_rounds(y, self.backward_maps.iter().rev());
}
}
thread_local! {
static GATHER_SCRATCH: std::cell::RefCell<Vec<f64>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn wht_and_gather_rounds<'a>(x: &mut [f64], maps: impl Iterator<Item = &'a Vec<u32>>) {
GATHER_SCRATCH.with(|cell| {
let mut scratch = cell.borrow_mut();
if scratch.len() < x.len() {
scratch.resize(x.len(), 0.0);
}
wht_normalized_chunks(x);
let mut src: &mut [f64] = x;
let mut dst: &mut [f64] = &mut scratch[..src.len()];
let mut rounds = 0;
for map in maps {
unsafe { gather_permuted(dst, src, map) };
wht_normalized_chunks(dst);
std::mem::swap(&mut src, &mut dst);
rounds += 1;
}
if rounds % 2 == 1 {
dst.copy_from_slice(src);
}
});
}
unsafe fn gather_permuted(dst: &mut [f64], src: &[f64], map: &[u32]) {
assert_eq!(dst.len(), src.len());
assert_eq!(dst.len(), map.len());
for (d, &s) in dst.iter_mut().zip(map) {
debug_assert!((s as usize) < src.len());
*d = unsafe { *src.get_unchecked(s as usize) };
}
}
pub fn in_place_walsh_hadamard_transform(x: &mut [f64]) {
let n = x.len();
debug_assert!(n.is_power_of_two(), "WHT requires power-of-2 length");
let mut h = 1;
while h < n {
for i in (0..n).step_by(h * 2) {
for j in i..i + h {
let a = x[j];
let b = x[j + h];
x[j] = a + b;
x[j + h] = a - b;
}
}
h *= 2;
}
}
#[cfg(test)]
pub fn random_vector_rotation(x: &mut [f64]) {
let dim = x.len();
let permutations: [_; N_PERMUTATIONS] =
std::array::from_fn(|index| Permutation::new_one_way(PERMUTATION_SEEDS[index], dim));
apply_rotation_with_permutations(x, &permutations);
}
#[cfg(test)]
pub fn random_vector_rotation_inverse(y: &mut [f64]) {
let dim = y.len();
let permutations: [_; N_PERMUTATIONS] =
std::array::from_fn(|index| Permutation::new_reversible(PERMUTATION_SEEDS[index], dim));
apply_inverse_rotation_with_permutations(y, &permutations);
}
fn compute_chunk_sizes(dim: usize) -> impl Iterator<Item = usize> {
debug_assert!(dim > 0);
let mut bits = dim;
std::iter::from_fn(move || {
if bits == 0 {
return None;
}
let highest = 1 << bits.ilog2();
bits ^= highest;
Some(highest)
})
}
#[cfg(test)]
fn apply_rotation_with_permutations(x: &mut [f64], permutations: &[Permutation; N_PERMUTATIONS]) {
wht_normalized_chunks(x);
for permutation in permutations {
permutation.permute(x);
wht_normalized_chunks(x);
}
}
#[cfg(test)]
fn apply_inverse_rotation_with_permutations(
y: &mut [f64],
permutations: &[Permutation; N_PERMUTATIONS],
) {
wht_normalized_chunks(y);
for permutation in permutations.iter().rev() {
permutation.unpermute(y);
wht_normalized_chunks(y);
}
}
fn wht_normalized_chunks(buf: &mut [f64]) {
let mut offset = 0;
for size in compute_chunk_sizes(buf.len()) {
let chunk = &mut buf[offset..offset + size];
let norm = 1.0 / (size as f64).sqrt();
simd::hadamard::wht_dispatch_scaled(chunk, norm);
offset += size;
}
debug_assert_eq!(offset, buf.len());
}
#[cfg(test)]
mod test {
use rand::prelude::StdRng;
use rand::{Rng, RngExt, SeedableRng};
use super::*;
#[test]
fn test_compute_chunk_sizes() {
for dim in [5, 128, 129, 300, 700, 712, 1536, 4096] {
let sizes = compute_chunk_sizes(dim).collect::<Vec<_>>();
assert!(
sizes.iter().all(|s| s.is_power_of_two()),
"dim={dim}: not all power-of-2: {sizes:?}"
);
assert_eq!(
sizes.iter().sum::<usize>(),
dim,
"dim={dim}: chunks don't sum to dim: {sizes:?}"
);
assert!(
sizes.windows(2).all(|w| w[0] >= w[1]),
"dim={dim}: not decreasing: {sizes:?}"
);
}
assert_eq!(compute_chunk_sizes(128).collect::<Vec<_>>(), vec![128]);
assert_eq!(
compute_chunk_sizes(700).collect::<Vec<_>>(),
vec![512, 128, 32, 16, 8, 4],
);
assert_eq!(
compute_chunk_sizes(1536).collect::<Vec<_>>(),
vec![1024, 512]
);
assert_eq!(compute_chunk_sizes(4096).collect::<Vec<_>>(), vec![4096]);
}
#[test]
fn hadamard_reduces_distortion() {
use crate::quantization::vector_stats::VectorStats;
for dim in [100, 101, 300, 384, 512, 1024, 1025, 1586] {
let n_vectors = 200;
let rot = HadamardRotation::new(dim);
let mut rng = StdRng::seed_from_u64(42);
let vectors: Vec<Vec<f64>> = (0..n_vectors)
.map(|_| {
let random_vector: Vec<f64> = (0..dim)
.map(|d| {
let scale = if d < 5 { 100.0 } else { 0.01 };
rng.random_range(-1.0f64..1.0) * scale
})
.collect();
cosine_preprocess(random_vector)
})
.collect();
let mut rotated = vectors.clone();
for vector in rotated.iter_mut() {
rot.apply(vector);
}
let rotated: Vec<_> = rotated
.into_iter()
.map(|v| v.into_iter().map(|i| i as f32).collect::<Vec<_>>())
.collect();
let vectors: Vec<_> = vectors
.into_iter()
.map(|v| v.into_iter().map(|i| i as f32).collect::<Vec<_>>())
.collect();
let stats_before = VectorStats::build(vectors.iter(), dim);
let stats_after = VectorStats::build(rotated.iter(), rot.dim);
let stddevs_before: Vec<f32> = stats_before
.elements_stats
.iter()
.map(|s| s.stddev)
.collect();
let stddevs_after: Vec<f32> = stats_after
.elements_stats
.iter()
.map(|s| s.stddev)
.collect();
let ratio = |s: &[f32]| {
let max = s.iter().copied().fold(f32::MIN, f32::max);
let min = s.iter().copied().fold(f32::MAX, f32::min);
max / min
};
let ratio_before = ratio(&stddevs_before);
let ratio_after = ratio(&stddevs_after);
let allowed_ratio_ratio = 0.002;
assert!(
ratio_before > 1000.0,
"expected distorted input, got ratio {ratio_before}"
);
assert!(
ratio_after <= ratio_before * allowed_ratio_ratio,
"rotation didn't spread energy enough, stddev ratio {ratio_after} ({})",
ratio_after / ratio_before
);
}
}
#[test]
fn static_rotation_matches_struct_and_roundtrips() {
for &dim in &[5, 50, 128, 300, 1024, 1536] {
let mut rng = StdRng::seed_from_u64(7);
let input: Vec<f64> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let rot = HadamardRotation::new(dim);
let mut via_static = input.clone();
let mut via_struct = input.clone();
random_vector_rotation(&mut via_static);
rot.apply(&mut via_struct);
assert_eq!(via_static, via_struct, "dim={dim}: forward mismatch");
let mut inv_static = via_static.clone();
let mut inv_struct = via_struct.clone();
random_vector_rotation_inverse(&mut inv_static);
rot.apply_inverse(&mut inv_struct);
assert_eq!(inv_static, inv_struct, "dim={dim}: inverse mismatch");
for (orig, recovered) in input.iter().zip(&inv_static) {
assert!(
(orig - recovered).abs() < 1e-5,
"dim={dim}: static roundtrip failed: {orig} vs {recovered}",
);
}
}
}
#[test]
fn hadamard_roundtrip() {
let power_of_two_dims = [128, 512, 1024, 4096];
let rand_dims = [50, 127, 300, 500, 1025];
let dim_iter = power_of_two_dims.iter().chain(&rand_dims);
for &dim in dim_iter {
for seed in [0, 10, 42, 100] {
let rot = HadamardRotation::new(dim);
let mut rng = StdRng::seed_from_u64(seed);
let input: Vec<f64> = (0..dim)
.map(|_| f64::from(rng.next_u32() % 1_000) / 100.0)
.collect();
let mut rotated = input.clone();
rot.apply(&mut rotated);
rot.apply_inverse(&mut rotated);
for (orig, recovered) in input.iter().zip(rotated.iter()) {
assert!(
(orig - recovered).abs() < 1e-5,
"hadamard roundtrip failed: {orig} vs {recovered}",
);
}
}
}
}
fn is_length_zero_or_normalized(length: f64) -> bool {
length < f64::EPSILON || (length - 1.0).abs() <= 1.0e-6
}
fn cosine_preprocess(vector: Vec<f64>) -> Vec<f64> {
let mut length: f64 = vector.iter().map(|x| x * x).sum();
if is_length_zero_or_normalized(length) {
return vector;
}
length = length.sqrt();
vector.iter().map(|x| x / length).collect()
}
}