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>
This commit is contained in:
Ivan Pleshkov
2026-08-04 11:18:44 +02:00
committed by generall
co-authored by Claude Opus 5
parent 81a0e43c73
commit 8b0d092c01
2 changed files with 69 additions and 1 deletions
+1 -1
View File
@@ -342,7 +342,7 @@ impl<TStorage: EncodedStorage> EncodedVectorsTQ<TStorage> {
}
pub fn layout(&self) -> Layout {
Layout::from_size_align(self.quantized_vector_size(), align_of::<f32>()).unwrap()
Layout::from_size_align(self.quantized_vector_size(), align_of::<u8>()).unwrap()
}
pub fn get_metadata(&self) -> &Metadata {
@@ -1064,4 +1064,72 @@ mod tests {
"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(),
);
}
}
}
}
}
}