use super::distance::DistanceEngine;
use super::graph::{NativeHnsw, DEFAULT_ALPHA, NO_ENTRY_POINT};
use super::layer::Layer;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::Path;
const MAX_LAYERS: usize = 4096;
const MAX_NEIGHBORS_PER_NODE: usize = 1 << 20;
const GRAPH_FORMAT_VERSION: u32 = 2;
const VECTORS_FORMAT_VERSION: u32 = 2;
const VECTORS_HEADER_BYTES: u64 = 16;
const VECTORS_V2_DATA_OFFSET: u64 = 4096;
const VECTORS_V2_PAD_BYTES: usize = (VECTORS_V2_DATA_OFFSET - VECTORS_HEADER_BYTES) as usize;
const VECTORS_FLAG_UNIT_NORM: u8 = 0b0000_0001;
struct VectorsHeader {
version: u32,
count: usize,
dimension: usize,
unit_norm: bool,
}
#[cfg(feature = "persistence")]
const _: () = assert!(
VECTORS_V2_DATA_OFFSET >= crate::contiguous_file_arena::DATA_OFFSET as u64,
"a v2 .vectors payload must start at or past the arena data offset"
);
const _: () = assert!(VECTORS_V2_DATA_OFFSET > VECTORS_HEADER_BYTES);
const _: () = assert!(
VECTORS_FORMAT_VERSION == 2,
"a new .vectors version needs its own arm in `vectors_data_offset`"
);
const fn vectors_data_offset(version: u32) -> u64 {
if version == 1 {
VECTORS_HEADER_BYTES
} else {
VECTORS_V2_DATA_OFFSET
}
}
fn corrupt(msg: impl Into<String>) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::InvalidData, msg.into())
}
pub(super) struct LoadedGraph {
pub(super) layers: Vec<Layer>,
pub(super) num_layers: usize,
pub(super) max_connections: usize,
pub(super) max_connections_0: usize,
pub(super) ef_construction: usize,
pub(super) entry_point: usize,
pub(super) max_layer: usize,
pub(super) alpha: f32,
}
struct GraphFileHeader {
num_layers: u32,
max_connections: u32,
max_connections_0: u32,
ef_construction: u32,
entry_point: u64,
max_layer: u32,
alpha: f32,
}
#[allow(clippy::cast_possible_truncation)]
fn read_u32_field(reader: &mut BufReader<File>) -> std::io::Result<usize> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf) as usize)
}
#[allow(clippy::cast_possible_truncation)]
fn read_u64_field(reader: &mut BufReader<File>) -> std::io::Result<usize> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
Ok(u64::from_le_bytes(buf) as usize)
}
fn read_f32_field(reader: &mut BufReader<File>) -> std::io::Result<f32> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(f32::from_le_bytes(buf))
}
impl<D: DistanceEngine + Send + Sync> NativeHnsw<D> {
pub fn file_dump(&self, path: &Path, basename: &str) -> std::io::Result<()> {
self.file_dump_counted(path, basename).map(drop)
}
pub(crate) fn file_dump_counted(&self, path: &Path, basename: &str) -> std::io::Result<usize> {
let vectors = self.vectors.read();
let count = self.dump_vectors_file(path, basename, vectors.as_ref())?;
self.dump_graph_file(path, basename, count)?;
drop(vectors);
usize::try_from(count).map_err(std::io::Error::other)
}
pub(in crate::index::hnsw) fn stores_unit_norm(&self) -> bool {
self.distance.is_pre_normalized() && self.distance.metric() == crate::DistanceMetric::Cosine
}
fn dump_vectors_file(
&self,
path: &Path,
basename: &str,
vectors: Option<&crate::perf_optimizations::ContiguousVectors>,
) -> std::io::Result<u64> {
let vectors_path = path.join(format!("{basename}.vectors"));
#[allow(clippy::cast_possible_truncation)]
let (count, dimension): (u64, u32) = match vectors {
Some(v) => (v.len() as u64, v.dimension() as u32),
None => (0, 0),
};
let unit_norm = self.stores_unit_norm();
#[cfg(feature = "persistence")]
if let Some(adopted) = vectors.filter(|v| v.backing_path() == Some(vectors_path.as_path()))
{
Self::rewrite_adopted_vectors_header(
adopted,
&vectors_path,
count,
dimension,
unit_norm,
)?;
return Ok(count);
}
let mut writer = BufWriter::new(File::create(&vectors_path)?);
Self::write_vectors_header(&mut writer, count, dimension, unit_norm)?;
if let Some(vectors) = vectors {
Self::write_vector_data(&mut writer, vectors)?;
}
writer.flush()?;
Ok(count)
}
fn write_vectors_header(
writer: &mut impl Write,
count: u64,
dimension: u32,
unit_norm: bool,
) -> std::io::Result<()> {
writer.write_all(&VECTORS_FORMAT_VERSION.to_le_bytes())?;
writer.write_all(&count.to_le_bytes())?;
writer.write_all(&dimension.to_le_bytes())?;
writer.write_all(&[if unit_norm { VECTORS_FLAG_UNIT_NORM } else { 0 }])?;
writer.write_all(&[0u8; VECTORS_V2_PAD_BYTES - 1])?;
Ok(())
}
fn write_vector_data(
writer: &mut BufWriter<File>,
vectors: &crate::perf_optimizations::ContiguousVectors,
) -> std::io::Result<()> {
for i in 0..vectors.len() {
if let Some(vec) = vectors.get(i) {
if cfg!(target_endian = "little") {
writer.write_all(bytemuck::cast_slice(vec))?;
} else {
for &val in vec {
writer.write_all(&val.to_le_bytes())?;
}
}
}
}
Ok(())
}
#[expect(clippy::significant_drop_tightening)]
fn dump_graph_file(&self, path: &Path, basename: &str, count: u64) -> std::io::Result<()> {
let graph_path = path.join(format!("{basename}.graph"));
let layers = self.layers.read();
let mut writer = BufWriter::new(File::create(&graph_path)?);
#[allow(clippy::cast_possible_truncation)]
let header = GraphFileHeader {
num_layers: layers.len() as u32,
max_connections: self.max_connections as u32,
max_connections_0: self.max_connections_0 as u32,
ef_construction: self.ef_construction as u32,
entry_point: {
let ep = self.entry_point.load(std::sync::atomic::Ordering::Acquire);
if ep == NO_ENTRY_POINT {
0
} else {
ep as u64
}
},
max_layer: self.max_layer.load(std::sync::atomic::Ordering::Relaxed) as u32,
alpha: self.alpha,
};
Self::write_graph_header(&mut writer, &header, count)?;
Self::write_layer_data(&mut writer, &layers)?;
writer.flush()
}
fn write_graph_header(
writer: &mut BufWriter<File>,
header: &GraphFileHeader,
count: u64,
) -> std::io::Result<()> {
let fields: [&[u8]; 9] = [
&GRAPH_FORMAT_VERSION.to_le_bytes(),
&header.num_layers.to_le_bytes(),
&header.max_connections.to_le_bytes(),
&header.max_connections_0.to_le_bytes(),
&header.ef_construction.to_le_bytes(),
&header.entry_point.to_le_bytes(),
&header.max_layer.to_le_bytes(),
&count.to_le_bytes(),
&header.alpha.to_le_bytes(),
];
for field in &fields {
writer.write_all(field)?;
}
Ok(())
}
fn write_layer_data(writer: &mut BufWriter<File>, layers: &[Layer]) -> std::io::Result<()> {
let mut scratch: Vec<u8> = Vec::new();
for layer in layers {
let num_nodes = layer.neighbors.len() as u64;
writer.write_all(&num_nodes.to_le_bytes())?;
for node_neighbors in &layer.neighbors {
let neighbors = node_neighbors.read();
#[allow(clippy::cast_possible_truncation)]
let num_neighbors = neighbors.len() as u32;
scratch.clear();
scratch.extend_from_slice(&num_neighbors.to_le_bytes());
for &neighbor in neighbors.iter() {
#[allow(clippy::cast_possible_truncation)]
let neighbor_u32 = neighbor as u32;
scratch.extend_from_slice(&neighbor_u32.to_le_bytes());
}
drop(neighbors);
writer.write_all(&scratch)?;
}
}
Ok(())
}
pub fn file_load(path: &Path, basename: &str, distance: D) -> std::io::Result<Self> {
Self::file_load_with_arena(path, basename, distance, None)
}
pub(in crate::index::hnsw) fn file_load_with_arena(
path: &Path,
basename: &str,
distance: D,
arena_dir: Option<&Path>,
) -> std::io::Result<Self> {
let claimed_home = arena_dir.map(crate::index::hnsw::native::arena_home::ArenaHome::claim);
let vectors_path = path.join(format!("{basename}.vectors"));
let (mut vectors, count, unit_norm) =
Self::load_vectors_file(&vectors_path, claimed_home.as_ref())?;
if !unit_norm
&& distance.is_pre_normalized()
&& distance.metric() == crate::DistanceMetric::Cosine
{
const UNIT_NORM_EPS: f32 = 1e-5;
let off_sphere = |v: &[f32]| {
let n = crate::simd_native::norm_native(v);
n > 0.0 && (n - 1.0).abs() > UNIT_NORM_EPS
};
let first = vectors.as_ref().and_then(|storage| {
(0..storage.len()).position(|i| storage.get(i).is_some_and(off_sphere))
});
if let (Some(storage), Some(first)) = (vectors.as_mut(), first) {
#[cfg(feature = "persistence")]
if storage.backing_path() == Some(vectors_path.as_path()) {
*storage = Self::detached_copy(storage, claimed_home.as_ref())?;
}
for i in first..storage.len() {
if let Some(v) = storage.get_mut(i) {
if off_sphere(v) {
crate::simd_native::normalize_inplace_native(v);
}
}
}
}
}
#[cfg(feature = "persistence")]
let arena_home = match vectors
.as_ref()
.and_then(crate::perf_optimizations::ContiguousVectors::backing_path)
{
Some(mapped) if mapped == vectors_path => None,
_ => claimed_home,
};
#[cfg(not(feature = "persistence"))]
let arena_home = claimed_home;
let graph_path = path.join(format!("{basename}.graph"));
let graph = Self::load_graph_file(&graph_path, count)?;
let level_mult = 1.0 / (graph.max_connections as f64).ln();
let entry_point = if count > 0 {
graph.entry_point
} else {
NO_ENTRY_POINT
};
let loaded = Self {
distance,
vectors: parking_lot::RwLock::new(vectors),
layers: parking_lot::RwLock::new(graph.layers),
entry_point: std::sync::atomic::AtomicUsize::new(entry_point),
max_layer: std::sync::atomic::AtomicUsize::new(graph.max_layer),
promotion: parking_lot::Mutex::new(()),
count: std::sync::atomic::AtomicUsize::new(count),
arena_home,
rng_state: std::sync::atomic::AtomicU64::new(0x5DEE_CE66_D1A4_B5B5),
max_connections: graph.max_connections,
max_connections_0: graph.max_connections_0,
ef_construction: graph.ef_construction,
level_mult,
alpha: graph.alpha,
stagnation_limit: graph.ef_construction / 2,
pre_allocated_capacity: std::sync::atomic::AtomicUsize::new(0),
#[cfg(feature = "gpu")]
gpu_csr_cache: crate::gpu::gpu_csr::CsrCache::new(),
#[cfg(feature = "gpu")]
gpu_vectors_snapshot: parking_lot::Mutex::new(None),
#[cfg(feature = "gpu")]
gpu_snapshot_version: std::sync::atomic::AtomicU64::new(0),
};
loaded.rebuild_anchors();
Ok(loaded)
}
fn load_vectors_file(
path: &Path,
arena_home: Option<&crate::index::hnsw::native::arena_home::ArenaHome>,
) -> std::io::Result<(
Option<crate::perf_optimizations::ContiguousVectors>,
usize,
bool,
)> {
let file = File::open(path)?;
let file_len = file.metadata()?.len();
let mut reader = BufReader::new(file);
let VectorsHeader {
version,
count,
dimension,
unit_norm,
} = Self::read_vectors_header(&mut reader)?;
if count == 0 || dimension == 0 {
return Ok((None, 0, false));
}
let data_offset = vectors_data_offset(version);
Self::validate_vectors_file_len(count, dimension, file_len, data_offset)?;
#[cfg(feature = "persistence")]
if let Some(storage) = Self::adopt_durable_file(path, version, count, dimension) {
return Ok((Some(storage), count, unit_norm));
}
reader.seek(SeekFrom::Start(data_offset))?;
let storage = Self::read_vector_data(&mut reader, count, dimension, arena_home)?;
Ok((Some(storage), count, unit_norm))
}
fn validate_vectors_file_len(
count: usize,
dimension: usize,
file_len: u64,
data_offset: u64,
) -> std::io::Result<()> {
let payload = (count as u64)
.checked_mul(dimension as u64)
.and_then(|n| n.checked_mul(4))
.ok_or_else(|| corrupt("vector payload size overflows u64"))?;
let expected = payload
.checked_add(data_offset)
.ok_or_else(|| corrupt("vector file size overflows u64"))?;
if file_len < expected {
return Err(corrupt(format!(
"vector file too short: header declares {count}x{dimension} \
({expected} bytes) but file is {file_len} bytes"
)));
}
Ok(())
}
fn read_vectors_header(reader: &mut BufReader<File>) -> std::io::Result<VectorsHeader> {
let mut buf4 = [0u8; 4];
let mut buf8 = [0u8; 8];
reader.read_exact(&mut buf4)?;
let version = u32::from_le_bytes(buf4);
if version != 1 && version != VECTORS_FORMAT_VERSION {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Unsupported version: {version}"),
));
}
reader.read_exact(&mut buf8)?;
let count = u64::from_le_bytes(buf8) as usize;
reader.read_exact(&mut buf4)?;
let dimension = u32::from_le_bytes(buf4) as usize;
let mut flags = [0u8; 1];
if version == VECTORS_FORMAT_VERSION {
reader.read_exact(&mut flags)?;
}
Ok(VectorsHeader {
version,
count,
dimension,
unit_norm: flags[0] & VECTORS_FLAG_UNIT_NORM != 0,
})
}
fn read_vector_data(
reader: &mut BufReader<File>,
count: usize,
dimension: usize,
arena_home: Option<&crate::index::hnsw::native::arena_home::ArenaHome>,
) -> std::io::Result<crate::perf_optimizations::ContiguousVectors> {
let min_bytes = count
.checked_mul(dimension)
.and_then(|n| n.checked_mul(std::mem::size_of::<f32>()))
.ok_or_else(|| corrupt("vector payload size overflows usize"))?;
crate::alloc_guard::with_min_alloc_byte_limit(min_bytes, || {
let mut storage = Self::new_arena(arena_home, dimension, count)
.map_err(|e| std::io::Error::other(e.to_string()))?;
let mut buf4 = [0u8; 4];
let mut buf_vec = vec![0f32; dimension];
for _ in 0..count {
for slot in &mut buf_vec {
reader.read_exact(&mut buf4)?;
*slot = f32::from_le_bytes(buf4);
}
storage
.push(&buf_vec)
.map_err(|e| std::io::Error::other(e.to_string()))?;
}
Ok(storage)
})
}
fn load_graph_file(path: &Path, count: usize) -> std::io::Result<LoadedGraph> {
let file = File::open(path)?;
let file_len = file.metadata()?.len();
let mut reader = BufReader::new(file);
let graph_header = Self::read_graph_header(&mut reader, count)?;
let layers =
Self::read_graph_layers(&mut reader, graph_header.num_layers, count, file_len)?;
Ok(LoadedGraph {
layers,
num_layers: graph_header.num_layers,
max_connections: graph_header.max_connections,
max_connections_0: graph_header.max_connections_0,
ef_construction: graph_header.ef_construction,
entry_point: graph_header.entry_point,
max_layer: graph_header.max_layer,
alpha: graph_header.alpha,
})
}
fn read_graph_header(
reader: &mut BufReader<File>,
count: usize,
) -> std::io::Result<LoadedGraph> {
let version = Self::validate_graph_version(reader)?;
let header = Self::read_graph_header_fields(reader, count, version)?;
Self::validate_graph_header(&header, count)?;
Self::validate_graph_alpha(header.alpha)?;
Ok(header)
}
fn validate_graph_alpha(alpha: f32) -> std::io::Result<()> {
if !alpha.is_finite() || alpha < 1.0 {
return Err(corrupt(format!(
"graph alpha {alpha} is not finite and >= 1.0 (corrupt header)"
)));
}
Ok(())
}
fn validate_graph_header(header: &LoadedGraph, count: usize) -> std::io::Result<()> {
if header.max_connections < 2 {
return Err(corrupt(format!(
"max_connections {} < 2 (invalid HNSW graph)",
header.max_connections
)));
}
if header.max_connections_0 < 2 {
return Err(corrupt(format!(
"max_connections_0 {} < 2 (invalid HNSW graph)",
header.max_connections_0
)));
}
if header.ef_construction < 1 {
return Err(corrupt("ef_construction < 1 (invalid HNSW graph)"));
}
if header.num_layers > MAX_LAYERS {
return Err(corrupt(format!(
"num_layers {} exceeds cap {MAX_LAYERS}",
header.num_layers
)));
}
if header.max_layer >= header.num_layers.max(1) && count > 0 {
return Err(corrupt(format!(
"max_layer {} out of range for {} layers",
header.max_layer, header.num_layers
)));
}
if count > 0 && header.entry_point >= count {
return Err(corrupt(format!(
"entry_point {} out of range for {count} vectors",
header.entry_point
)));
}
Ok(())
}
fn validate_graph_version(reader: &mut BufReader<File>) -> std::io::Result<u32> {
let mut buf4 = [0u8; 4];
reader.read_exact(&mut buf4)?;
let version = u32::from_le_bytes(buf4);
if version == 0 || version > GRAPH_FORMAT_VERSION {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Unsupported graph version: {version}"),
));
}
Ok(version)
}
fn read_graph_header_fields(
reader: &mut BufReader<File>,
count: usize,
version: u32,
) -> std::io::Result<LoadedGraph> {
let mut header = Self::read_graph_header_params(reader)?;
let count_check = read_u64_field(reader)?;
if count_check != count {
return Err(corrupt(format!(
"graph count {count_check} != vectors count {count} (mismatched files)"
)));
}
header.alpha = Self::read_graph_header_alpha(reader, version)?;
Ok(header)
}
fn read_graph_header_params(reader: &mut BufReader<File>) -> std::io::Result<LoadedGraph> {
let num_layers = read_u32_field(reader)?;
let max_connections = read_u32_field(reader)?;
let max_connections_0 = read_u32_field(reader)?;
let ef_construction = read_u32_field(reader)?;
let entry_point = read_u64_field(reader)?;
let max_layer = read_u32_field(reader)?;
Ok(LoadedGraph {
layers: Vec::new(), num_layers,
max_connections,
max_connections_0,
ef_construction,
entry_point,
max_layer,
alpha: DEFAULT_ALPHA, })
}
fn read_graph_header_alpha(reader: &mut BufReader<File>, version: u32) -> std::io::Result<f32> {
if version >= 2 {
read_f32_field(reader)
} else {
Ok(DEFAULT_ALPHA)
}
}
fn read_graph_layers(
reader: &mut BufReader<File>,
num_layers: usize,
count: usize,
file_len: u64,
) -> std::io::Result<Vec<Layer>> {
let mut buf8 = [0u8; 8];
let mut layers = Vec::with_capacity(num_layers);
let max_nodes = (file_len / 4) as usize;
for layer_idx in 0..num_layers {
reader.read_exact(&mut buf8)?;
let num_nodes = u64::from_le_bytes(buf8) as usize;
if num_nodes > max_nodes {
return Err(corrupt(format!(
"layer num_nodes {num_nodes} exceeds file capacity {max_nodes}"
)));
}
let layer = if layer_idx == 0 {
Layer::new_base(num_nodes)
} else {
Layer::new(num_nodes)
};
for node_id in 0..num_nodes {
let neighbors = Self::read_node_neighbors(reader, count)?;
layer.set_neighbors(node_id, neighbors);
}
layers.push(layer);
}
Ok(layers)
}
fn read_node_neighbors(
reader: &mut BufReader<File>,
count: usize,
) -> std::io::Result<Vec<usize>> {
let mut buf4 = [0u8; 4];
reader.read_exact(&mut buf4)?;
let num_neighbors = u32::from_le_bytes(buf4) as usize;
if num_neighbors > MAX_NEIGHBORS_PER_NODE {
return Err(corrupt(format!(
"num_neighbors {num_neighbors} exceeds cap {MAX_NEIGHBORS_PER_NODE}"
)));
}
let mut neighbors = Vec::with_capacity(num_neighbors.min(count.max(1)));
for _ in 0..num_neighbors {
reader.read_exact(&mut buf4)?;
let neighbor = u32::from_le_bytes(buf4) as usize;
if neighbor >= count {
return Err(corrupt(format!(
"neighbor id {neighbor} out of range for {count} vectors"
)));
}
neighbors.push(neighbor);
}
Ok(neighbors)
}
}
#[cfg(test)]
#[path = "load_bound_tests.rs"]
mod load_bound_tests;
#[cfg(test)]
#[path = "vectors_format_tests.rs"]
mod vectors_format_tests;
#[cfg(feature = "persistence")]
#[path = "graph_io_adoption.rs"]
mod adoption;