perf(segment): software-prefetch vectors in dense batch scoring

During HNSW graph construction (and any dense batch scoring), candidate
vectors are resolved to pointers first and only touched by the distance
kernel, so every vector starts as a cold DRAM access. Issue a prefetch of
the first cache line when resolving each vector in the batch, and prefetch
the next vector in full while the current one is being scored.

HNSW build throughput (200k x 512d cosine, M=16, ef_construct=100,
4 pinned cores, in-RAM chunked storage): 3173 -> 3292 pts/s (+3.7%) on
AMD Zen 4/5. Expected to help more on CPUs with weaker hardware
prefetchers / higher memory latency (e.g. EPYC Rome class).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
generall
2026-06-12 08:57:09 +02:00
parent 9acece1e2c
commit 352602b4c7

View File

@@ -214,14 +214,22 @@ impl<T: bytemuck::Pod + Send, S: UniversalRead> ChunkedVectorsRead<T, S> {
let (vectors, _) = maybe_uninit_fill_from(
&mut vectors_buffer,
keys.iter().map(|&key| {
self.get_many_impl(key.offset(), 1, force_sequential)
.expect("vectors read")
let vector = self
.get_many_impl(key.offset(), 1, force_sequential)
.expect("vectors read");
// Start pulling the first lines into cache early; the rest
// is prefetched one vector ahead in the scoring loop below.
prefetch_first_line(vector.as_ref());
vector
}),
);
let batch_offset = VECTOR_READ_BATCH_SIZE * batch_idx;
for (vector_idx, vec) in vectors.iter().enumerate() {
if let Some(next) = vectors.get(vector_idx + 1) {
prefetch_full(next.as_ref());
}
callback(batch_offset + vector_idx, vec.as_ref());
}
}
@@ -319,6 +327,34 @@ impl<T: bytemuck::Pod + Send, S: UniversalRead> ChunkedVectorsRead<T, S> {
}
}
#[inline(always)]
fn prefetch_first_line<T>(slice: &[T]) {
#[cfg(target_arch = "x86_64")]
unsafe {
use std::arch::x86_64::{_MM_HINT_T0, _mm_prefetch};
_mm_prefetch(slice.as_ptr().cast::<i8>(), _MM_HINT_T0);
}
#[cfg(not(target_arch = "x86_64"))]
let _ = slice;
}
#[inline(always)]
fn prefetch_full<T>(slice: &[T]) {
#[cfg(target_arch = "x86_64")]
unsafe {
use std::arch::x86_64::{_MM_HINT_T0, _mm_prefetch};
let bytes = std::mem::size_of_val(slice);
let ptr = slice.as_ptr().cast::<i8>();
let mut offset = 0;
while offset < bytes {
_mm_prefetch(ptr.add(offset), _MM_HINT_T0);
offset += 64;
}
}
#[cfg(not(target_arch = "x86_64"))]
let _ = slice;
}
pub(super) fn read_status_len<Fs: UniversalReadFs>(
fs: &Fs,
status_file: &Path,