mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-23 11:11:00 -05:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user