Files
Ivan Pleshkov e06bee8d46 Fix TQ quantized vector layout alignment (#10005)
`EncodedVectorsTQ::layout()` declared `align_of::<f32>()`, but the size it
reports is the packed dimensions plus a 4-byte-multiple extras trailer, which
is not a multiple of 4 for three quarters of all dimensions (e.g. dim=756,
Bits4: 378 + 4 = 382 bytes).

The claim was never true — the encoded storage packs vectors at
`id * quantized_vector_size` with no per-vector padding — and nothing relies on
it: packed dimensions are read through unaligned SIMD loads (`loadu` / `vld1`)
and the extras trailer through `f32::from_le_bytes` on a byte slice.

It is also actively harmful. Inline HNSW storage packs link vectors
back-to-back using this layout and rejects one whose size is not a multiple of
its alignment, so building an index with `inline_storage` enabled fails for
those dimensions — and retries forever as an optimization crashloop.

Use `align_of::<u8>()`, matching scalar and product quantization. Old links
files stay readable: both layouts are persisted in the file header and the
reader takes size and alignment from there, never from the live quantizer.

Add a test covering the `size % align == 0` invariant across awkward
dimensions, bit widths, distances and modes — `layout()` had no coverage, which
is why the mismatch went unnoticed on the multiple-of-32 dimensions everyone
uses in practice.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:34:11 +02:00

1136 lines
46 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicBool;
use common::counter::hardware_counter::HardwareCounterCell;
use quantization::encoded_storage::TestEncodedStorageBuilder;
use quantization::encoded_vectors::{DistanceType, EncodedVectors, VectorParameters};
use quantization::encoded_vectors_tq::{self, EncodedVectorsTQ, ErrorCorrectionMetadata};
use quantization::turboquant::simd::{
CODEBOOK_SCALE_SQ_2BIT, CODEBOOK_SCALE_SQ_4BIT, score_1bit_internal_scalar,
score_2bit_internal_scalar, score_2bit_internal_weighted_scalar,
score_4bit_internal_scalar, score_4bit_internal_weighted_scalar,
};
use quantization::turboquant::{TQBits, TQMode, TQRotation};
use rand::{RngExt, SeedableRng};
use crate::metrics::{dot_similarity, l1_similarity, l2_similarity};
const VECTORS_COUNT: usize = 513;
const DIMS: &[usize] = &[16, 64, 65, 128, 384, 512];
const BITS: &[TQBits] = &[TQBits::Bits4, TQBits::Bits2, TQBits::Bits1_5, TQBits::Bits1];
/// Absolute tolerance for an approximate score: an empirical per-bit
/// coefficient (≈ 1.8x observed max across VECTORS_COUNT trials) times
/// the signal-std of the input data. The mean-error / signal-std ratio
/// is shared by the Dot, Cosine, and L2 paths, so the same coefficient
/// table is used; only the caller's `signal_std` differs by metric and
/// data distribution.
fn error(bits: TQBits, signal_std: f32) -> f32 {
let coef = match bits {
TQBits::Bits1 => 5.1,
TQBits::Bits1_5 => 4.0,
TQBits::Bits2 => 3.0,
TQBits::Bits4 => 0.9,
};
coef * signal_std
}
/// Signal-std of dot products of two independent U[-1, 1]^d vectors.
fn dot_signal_std(dim: usize) -> f32 {
(dim as f32 / 9.0).sqrt()
}
/// Signal-std of cosine of two independent unit-norm random vectors in d-dim.
fn cosine_signal_std(dim: usize) -> f32 {
1.0 / (dim as f32).sqrt()
}
/// Signal-std of the L2-squared score for two independent U[-1, 1]^d
/// vectors. The score is `‖q‖² + ‖v‖² 2·<q, v>`; the norms are stored
/// exactly as extras and contribute no quantization noise, so the
/// signal variance is dominated by the `2·<q, v>` term — i.e. twice
/// the dot-product std.
fn l2_signal_std(dim: usize) -> f32 {
2.0 * dot_signal_std(dim)
}
/// Tolerance for L1 scoring. Has its own per-bit coefficient table
/// because the L1 path's noise/signal ratio doesn't track the dot/cosine
/// pattern: it dequantizes both sides (full inverse rotation + Lloyd-Max
/// noise per coord) before summing |a b|. Per-coord errors enter the
/// sum signed (sign(a_i b_i)·δ_i), so cancellation makes the total
/// scale as sqrt(dim). Coefficients are empirically calibrated
/// (~1.8x observed max across both symmetric and asymmetric paths);
/// the symmetric path dominates at low bits because both vectors carry
/// dequantization noise.
fn error_l1(dim: usize, bits: TQBits) -> f32 {
let per_sqrt_dim = match bits {
TQBits::Bits1 => 7.5,
TQBits::Bits1_5 => 4.5,
TQBits::Bits2 => 3.0,
TQBits::Bits4 => 0.7,
};
per_sqrt_dim * (dim as f32).sqrt()
}
/// Per-bits minimum dim for meaningful absolute-error testing. At low dim
/// the Hadamard rotation has too few coordinates to Gaussianize well and
/// the Lloyd-Max quantization error grows too large relative to the signal
/// for the tolerance formulas above to be useful.
fn should_test(dim: usize, bits: TQBits) -> bool {
let min_dim = match bits {
TQBits::Bits1 => 64,
TQBits::Bits1_5 => 48,
TQBits::Bits2 => 32,
TQBits::Bits4 => 8,
};
dim >= min_dim
}
fn l2_norm(v: &[f32]) -> f32 {
v.iter().map(|x| x * x).sum::<f32>().sqrt()
}
fn normalize(v: &[f32]) -> Vec<f32> {
let n = l2_norm(v);
v.iter().map(|x| x / n).collect()
}
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
dot_similarity(a, b) / (l2_norm(a) * l2_norm(b))
}
fn read_f32(src: &[u8], offset: usize) -> f32 {
f32::from_le_bytes(src[offset..offset + size_of::<f32>()].try_into().unwrap())
}
fn extra_len(distance: DistanceType, mode: TQMode) -> usize {
let distance_extra_len = match distance {
DistanceType::Dot | DistanceType::Cosine => size_of::<f32>(),
DistanceType::L2 => 2 * size_of::<f32>(),
DistanceType::L1 => unreachable!("L1 reference needs inverse rotation"),
};
let mode_extra_len = match mode {
TQMode::Normal => 0,
TQMode::Plus => size_of::<f32>(),
};
distance_extra_len + mode_extra_len
}
fn ec_correction(extra: &[u8]) -> f32 {
read_f32(extra, extra.len() - size_of::<f32>())
}
fn tq_plus_weight_scale(ec: &ErrorCorrectionMetadata) -> (Vec<i16>, f32) {
let d_prime_sq_f32: Vec<f32> = ec
.scale
.iter()
.map(|&s| {
if s.abs() > f32::EPSILON {
(s * s).recip()
} else {
0.0
}
})
.collect();
let max_d_prime_sq = d_prime_sq_f32.iter().copied().fold(0.0f32, f32::max);
const QUANT_CAP: i16 = i16::MAX - 1;
let weight_scale = if max_d_prime_sq > f32::EPSILON {
f32::from(QUANT_CAP) / max_d_prime_sq
} else {
1.0
};
let weights: Vec<i16> = d_prime_sq_f32
.iter()
.map(|&x| (x * weight_scale).round().clamp(0.0, f32::from(QUANT_CAP)) as i16)
.collect();
(weights, weight_scale)
}
fn score_scalar_reference(
v1: &[u8],
v2: &[u8],
bits: TQBits,
distance: DistanceType,
mode: TQMode,
ec: Option<&ErrorCorrectionMetadata>,
) -> f32 {
let extra_len = extra_len(distance, mode);
let (data_v1, extra_v1) = v1.split_at(v1.len() - extra_len);
let (data_v2, extra_v2) = v2.split_at(v2.len() - extra_len);
let raw_dot = match (mode, bits) {
(TQMode::Plus, TQBits::Bits2) => {
let ec = ec.expect("TQ+ reference requires error correction metadata");
let (weights, weight_scale) = tq_plus_weight_scale(ec);
let raw_int = score_2bit_internal_weighted_scalar(data_v1, data_v2, &weights);
let xm_a = ec_correction(extra_v1);
let xm_b = ec_correction(extra_v2);
let mm: f32 = ec.shift.iter().map(|&s| s * s).sum();
raw_int as f32 / (weight_scale * CODEBOOK_SCALE_SQ_2BIT) + xm_a + xm_b - mm
}
(TQMode::Plus, TQBits::Bits4) => {
let ec = ec.expect("TQ+ reference requires error correction metadata");
let (weights, weight_scale) = tq_plus_weight_scale(ec);
let raw_int = score_4bit_internal_weighted_scalar(data_v1, data_v2, &weights);
let xm_a = ec_correction(extra_v1);
let xm_b = ec_correction(extra_v2);
let mm: f32 = ec.shift.iter().map(|&s| s * s).sum();
raw_int as f32 / (weight_scale * CODEBOOK_SCALE_SQ_4BIT) + xm_a + xm_b - mm
}
(_, TQBits::Bits1 | TQBits::Bits1_5) => score_1bit_internal_scalar(data_v1, data_v2),
(_, TQBits::Bits2) => score_2bit_internal_scalar(data_v1, data_v2),
(_, TQBits::Bits4) => score_4bit_internal_scalar(data_v1, data_v2),
};
let v1_scale = read_f32(extra_v1, 0);
let v2_scale = read_f32(extra_v2, 0);
match distance {
DistanceType::Dot | DistanceType::Cosine => raw_dot * v1_scale * v2_scale,
DistanceType::L2 => {
let l2_a = read_f32(extra_v1, size_of::<f32>());
let l2_b = read_f32(extra_v2, size_of::<f32>());
l2_a * l2_a + l2_b * l2_b - 2.0 * v1_scale * v2_scale * raw_dot
}
DistanceType::L1 => unreachable!("L1 reference needs inverse rotation"),
}
}
#[test]
fn test_tq_internal_score_matches_reference() {
let dim = 128;
let vectors_count = 32;
let counter = HardwareCounterCell::new();
for &bits in BITS {
for &distance in &[DistanceType::Dot, DistanceType::Cosine, DistanceType::L2] {
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let vector_data: Vec<Vec<f32>> = (0..vectors_count)
.map(|_| {
let vector: Vec<f32> =
(0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
match distance {
DistanceType::Cosine => normalize(&vector),
DistanceType::Dot | DistanceType::L1 | DistanceType::L2 => vector,
}
})
.collect();
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: distance,
invert: false,
};
for &mode in &[TQMode::Normal, TQMode::Plus] {
let quantized_vector_size = encoded_vectors_tq::get_quantized_vector_size(
&vector_parameters,
bits,
mode,
);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
vectors_count,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let ec = encoded.get_metadata().error_correction.as_ref();
for i in 1..vectors_count {
let v1 = encoded.get_quantized_vector(0);
let v2 = encoded.get_quantized_vector(i as u32);
let optimized = encoded.score_internal(0, i as u32, &counter);
let reference = score_scalar_reference(&v1, &v2, bits, distance, mode, ec);
let tolerance = 1e-5 * reference.abs().max(1.0);
assert!(
(optimized - reference).abs() <= tolerance,
"bits={bits:?}, mode={mode:?}, distance={distance:?}, i={i}: \
optimized={optimized}, reference={reference}, tolerance={tolerance}"
);
}
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal, 1)]
#[case::plus(TQMode::Plus, 1)]
// Exercises the threaded TQ+ pre-pass — keeps the parallel coord-chunk
// push covered by integration testing (single-threaded paths stay
// covered by the cases above).
#[case::plus_parallel(TQMode::Plus, 4)]
fn test_tq_dot(#[case] mode: TQMode, #[case] num_threads: usize) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, dot_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Dot,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
num_threads,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
for (index, vector) in vector_data.iter().enumerate() {
let score = encoded.score_point(&query_u8, index as u32, &counter);
let original_score = dot_similarity(&query, vector);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, index={index}, score={score}, expected={original_score}"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_cosine(#[case] mode: TQMode) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, cosine_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
let v: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
vector_data.push(normalize(&v));
}
let raw_query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let query = normalize(&raw_query);
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Cosine,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
for (index, vector) in vector_data.iter().enumerate() {
let score = encoded.score_point(&query_u8, index as u32, &counter);
let original_score = cosine_similarity(&query, vector);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, index={index}, score={score}, expected={original_score}"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_dot_internal(#[case] mode: TQMode) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, dot_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Dot,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let counter = HardwareCounterCell::new();
for i in 1..VECTORS_COUNT {
let score = encoded.score_internal(0, i as u32, &counter);
let original_score = dot_similarity(&vector_data[0], &vector_data[i]);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, i={i}, score={score}, expected={original_score}"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_cosine_internal(#[case] mode: TQMode) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, cosine_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
let v: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
vector_data.push(normalize(&v));
}
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Cosine,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let counter = HardwareCounterCell::new();
for i in 1..VECTORS_COUNT {
let score = encoded.score_internal(0, i as u32, &counter);
let original_score = cosine_similarity(&vector_data[0], &vector_data[i]);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, i={i}, score={score}, expected={original_score}"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_zero_vector_dot(#[case] mode: TQMode) {
// A zero vector stored in the dataset, scored with Dot against a
// non-zero query, should produce a score close to the true dot
// product, which is exactly 0.
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, dot_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
// Place the zero vector at index 0; fill the rest with random
// data so the encoded batch resembles a realistic input.
let mut vector_data: Vec<Vec<f32>> = vec![vec![0.0f32; dim]];
for _ in 1..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Dot,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
let score = encoded.score_point(&query_u8, 0u32, &counter);
assert!(
score.abs() < error,
"bits={bits:?}, dim={dim}, score={score} (expected ~0)"
);
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_zero_query_dot(#[case] mode: TQMode) {
// A zero query, scored with Dot against any encoded vector, should
// produce a score close to the true dot product, which is exactly 0.
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, dot_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let query: Vec<f32> = vec![0.0f32; dim];
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Dot,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
for index in 0..VECTORS_COUNT {
let score = encoded.score_point(&query_u8, index as u32, &counter);
assert!(
score.abs() < error,
"bits={bits:?}, dim={dim}, index={index}, score={score} (expected ~0)"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_zero_vector_cosine(#[case] mode: TQMode) {
// A zero vector stored in the dataset, scored with Cosine against a
// unit-norm query, should produce a score close to 0. Cosine of a
// zero vector is mathematically undefined; the implementation's
// convention (preserve zero through preprocessing) yields a true
// dot of zero post-rotation, so the encoded score should be ~0.
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, cosine_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
// Index 0 is the zero vector (not routed through `normalize`,
// which would divide by zero); the rest are unit-norm random.
let mut vector_data: Vec<Vec<f32>> = vec![vec![0.0f32; dim]];
for _ in 1..VECTORS_COUNT {
let v: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
vector_data.push(normalize(&v));
}
let raw_query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let query = normalize(&raw_query);
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Cosine,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
let score = encoded.score_point(&query_u8, 0u32, &counter);
assert!(
score.abs() < error,
"bits={bits:?}, dim={dim}, score={score} (expected ~0)"
);
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_zero_query_cosine(#[case] mode: TQMode) {
// A zero query, scored with Cosine against any unit-norm vector,
// should produce a score close to 0. Same convention as above:
// zero is preserved through query preprocessing.
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, cosine_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
let v: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
vector_data.push(normalize(&v));
}
let query: Vec<f32> = vec![0.0f32; dim];
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Cosine,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
for index in 0..VECTORS_COUNT {
let score = encoded.score_point(&query_u8, index as u32, &counter);
assert!(
score.abs() < error,
"bits={bits:?}, dim={dim}, index={index}, score={score} (expected ~0)"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_dim_one_dot(#[case] mode: TQMode) {
// dim=1 is degenerate (no room for Hadamard Gaussianization, quant
// noise dominates). Just verify the path doesn't panic and yields
// finite, sanely bounded scores; accuracy bounds elsewhere don't apply.
let dim = 1;
for &bits in BITS {
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::Dot,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
for index in 0..VECTORS_COUNT {
let score = encoded.score_point(&query_u8, index as u32, &counter);
assert!(
score.is_finite() && score.abs() < 10.0,
"bits={bits:?}, index={index}, score={score}"
);
}
}
}
// L2 (squared) and L1 score tests. Tolerance for L2 reuses the unified
// `error()` with `l2_signal_std`; L1 has its own coefficient table
// because its noise/signal scaling per bit doesn't match dot/cosine.
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_l2(#[case] mode: TQMode) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, l2_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::L2,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
for (index, vector) in vector_data.iter().enumerate() {
let score = encoded.score_point(&query_u8, index as u32, &counter);
let original_score = l2_similarity(&query, vector);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, index={index}, score={score}, expected={original_score}"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_l2_internal(#[case] mode: TQMode) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error(bits, l2_signal_std(dim));
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::L2,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let counter = HardwareCounterCell::new();
for i in 1..VECTORS_COUNT {
let score = encoded.score_internal(0, i as u32, &counter);
let original_score = l2_similarity(&vector_data[0], &vector_data[i]);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, i={i}, score={score}, expected={original_score}"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_l1(#[case] mode: TQMode) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error_l1(dim, bits);
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let query: Vec<f32> = (0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::L1,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let query_u8 = encoded.encode_query(&query);
let counter = HardwareCounterCell::new();
for (index, vector) in vector_data.iter().enumerate() {
let score = encoded.score_point(&query_u8, index as u32, &counter);
let original_score = l1_similarity(&query, vector);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, index={index}, score={score}, expected={original_score}"
);
}
}
}
}
#[rstest::rstest]
#[case::normal(TQMode::Normal)]
#[case::plus(TQMode::Plus)]
fn test_tq_l1_internal(#[case] mode: TQMode) {
for &bits in BITS {
for &dim in DIMS {
if !should_test(dim, bits) {
continue;
}
let error = error_l1(dim, bits);
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut vector_data: Vec<Vec<f32>> = vec![];
for _ in 0..VECTORS_COUNT {
vector_data.push((0..dim).map(|_| rng.random_range(-1.0..1.0)).collect());
}
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: DistanceType::L1,
invert: false,
};
let quantized_vector_size =
encoded_vectors_tq::get_quantized_vector_size(&vector_parameters, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
VECTORS_COUNT,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let counter = HardwareCounterCell::new();
for i in 1..VECTORS_COUNT {
let score = encoded.score_internal(0, i as u32, &counter);
let original_score = l1_similarity(&vector_data[0], &vector_data[i]);
assert!(
(score - original_score).abs() < error,
"bits={bits:?}, dim={dim}, i={i}, score={score}, expected={original_score}"
);
}
}
}
}
/// Recall regression probe — non-uniform per-coordinate variance data
/// (the case where TQ+ should HELP, not hurt). Asserts Plus-mode recall@k
/// stays close to Normal-mode on data with a few high-variance "spike"
/// directions. Catches the failure mode where renorm's `cn` drifts because
/// `‖X+‖` has chi-squared spread across vectors — see
/// [`TurboQuantizer::compute_centroid_norm`] for the EC-revert that fixes it.
#[rstest::rstest]
#[case::bits1(TQBits::Bits1)]
#[case::bits2(TQBits::Bits2)]
#[case::bits4(TQBits::Bits4)]
fn recall_skewed_data(#[case] bits: TQBits) {
use rand::prelude::StdRng;
let dim = 256;
let n = 1000;
let n_queries = 50;
let topk = 10;
let mut rng = StdRng::seed_from_u64(42);
// Pre-rotation per-coord scale: a few coords have huge variance, most
// have small. Realistic embeddings have similar structure (a few
// "spike" directions).
let coord_scales: Vec<f32> = (0..dim).map(|i| if i < 8 { 100.0 } else { 1.0 }).collect();
let make_vec = |rng: &mut StdRng| -> Vec<f32> {
coord_scales
.iter()
.map(|&s| s * (rng.random_range(-1.0..1.0)))
.collect()
};
let vectors: Vec<Vec<f32>> = (0..n).map(|_| make_vec(&mut rng)).collect();
let queries: Vec<Vec<f32>> = (0..n_queries).map(|_| make_vec(&mut rng)).collect();
let true_dot =
|a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum() };
let recall_for = |mode: TQMode| -> f32 {
let vp = VectorParameters {
dim,
distance_type: DistanceType::Dot,
invert: false,
deprecated_count: None,
};
let qsize = encoded_vectors_tq::get_quantized_vector_size(&vp, bits, mode);
let encoded = EncodedVectorsTQ::encode(
vectors.iter(),
TestEncodedStorageBuilder::new(None, qsize),
&vp,
n,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let counter = HardwareCounterCell::new();
let mut total = 0.0;
for q in &queries {
let mut truth: Vec<(usize, f32)> = vectors
.iter()
.enumerate()
.map(|(i, v)| (i, true_dot(q, v)))
.collect();
truth.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let truth_top: Vec<usize> = truth.iter().take(topk).map(|x| x.0).collect();
let qq = encoded.encode_query(q);
let mut q_scores: Vec<(usize, f32)> = (0..n)
.map(|i| (i, encoded.score_point(&qq, i as u32, &counter)))
.collect();
q_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let q_top: Vec<usize> = q_scores.iter().take(topk).map(|x| x.0).collect();
let hits = truth_top.iter().filter(|i| q_top.contains(i)).count();
total += hits as f32 / topk as f32;
}
total / n_queries as f32
};
let normal = recall_for(TQMode::Normal);
let plus = recall_for(TQMode::Plus);
// Plus must not be meaningfully worse than Normal. We allow a small
// 2% slack since the codebook is the same and EC's value comes from
// distribution fit, which on this seed is marginal.
assert!(
plus >= normal - 0.02,
"bits={bits:?}: Plus recall regressed (Normal={normal:.3}, Plus={plus:.3})"
);
}
#[test]
fn test_tq_layout_size_is_multiple_of_alignment() {
const LAYOUT_DIMS: &[usize] = &[1, 7, 33, 65, 100, 756, 768];
let vectors_count = 8;
for &dim in LAYOUT_DIMS {
for &bits in BITS {
for &distance in &[
DistanceType::Dot,
DistanceType::Cosine,
DistanceType::L1,
DistanceType::L2,
] {
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let vector_data: Vec<Vec<f32>> = (0..vectors_count)
.map(|_| {
let vector: Vec<f32> =
(0..dim).map(|_| rng.random_range(-1.0..1.0)).collect();
match distance {
DistanceType::Cosine => normalize(&vector),
DistanceType::Dot | DistanceType::L1 | DistanceType::L2 => vector,
}
})
.collect();
let vector_parameters = VectorParameters {
dim,
deprecated_count: None,
distance_type: distance,
invert: false,
};
for &mode in &[TQMode::Normal, TQMode::Plus] {
let quantized_vector_size = encoded_vectors_tq::get_quantized_vector_size(
&vector_parameters,
bits,
mode,
);
let encoded = EncodedVectorsTQ::encode(
vector_data.iter(),
TestEncodedStorageBuilder::new(None, quantized_vector_size),
&vector_parameters,
vectors_count,
bits,
mode,
TQRotation::Padded,
false,
1,
None,
&AtomicBool::new(false),
)
.unwrap();
let layout = encoded.layout();
assert_eq!(layout.size(), quantized_vector_size);
assert_eq!(
layout.size() % layout.align(),
0,
"dim={dim} bits={bits:?} distance={distance:?} mode={mode:?}: \
layout size {} is not a multiple of alignment {}",
layout.size(),
layout.align(),
);
}
}
}
}
}
}