1use std::fs::File;
14use std::io::{Read, Seek, SeekFrom, Write};
15use std::ops::Deref;
16use std::path::Path;
17use std::ptr::NonNull;
18use std::sync::Arc;
19
20use rayon::prelude::*;
21
22use super::{
23 bfs_order, code_at, put_varint, quantize, quantize4, zigzag, Graph, GraphView, VertexLayout,
24};
25use super::{
26 block_record_bytes, get_varint, unzigzag, Line, Lines, Metric, Rotation, VGraph, VMetric,
27};
28use super::{BLOCK, BLOCK_SEED, NONE, NONE3, SCREEN_SEED};
29
30pub(super) unsafe trait Plain: Copy + 'static {}
36unsafe impl Plain for u8 {}
38unsafe impl Plain for i8 {}
40unsafe impl Plain for u32 {}
42unsafe impl Plain for u64 {}
44unsafe impl Plain for f32 {}
46unsafe impl Plain for Line {}
48
49fn as_bytes<T: Plain>(xs: &[T]) -> &[u8] {
51 unsafe { std::slice::from_raw_parts(xs.as_ptr().cast::<u8>(), std::mem::size_of_val(xs)) }
53}
54
55enum Backing {
57 Map(memmap2::Mmap),
58 Heap(Vec<Line>, usize),
60}
61
62impl Backing {
63 fn bytes(&self) -> &[u8] {
64 match self {
65 Self::Map(m) => m,
66 Self::Heap(lines, len) => &as_bytes(lines)[..*len],
67 }
68 }
69}
70
71enum Owner<T> {
73 Vec { _buf: Vec<T> },
74 File { _file: Arc<Backing> },
75}
76
77pub(super) struct Slab<T: Plain> {
81 ptr: NonNull<T>,
82 len: usize,
83 owner: Owner<T>,
84}
85
86unsafe impl<T: Plain + Send + Sync> Send for Slab<T> {}
90unsafe impl<T: Plain + Sync> Sync for Slab<T> {}
92
93impl<T: Plain> From<Vec<T>> for Slab<T> {
94 fn from(mut v: Vec<T>) -> Self {
95 let ptr = NonNull::new(v.as_mut_ptr()).expect("a vector pointer is never null");
96 Self {
97 ptr,
98 len: v.len(),
99 owner: Owner::Vec { _buf: v },
100 }
101 }
102}
103
104impl<T: Plain> Deref for Slab<T> {
105 type Target = [T];
106
107 #[inline(always)]
108 fn deref(&self) -> &[T] {
109 unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
111 }
112}
113
114impl<T: Plain> Slab<T> {
115 pub(super) fn as_mut_slice(&mut self) -> &mut [T] {
118 assert!(
119 matches!(self.owner, Owner::Vec { .. }),
120 "the arrays of an opened index file are read-only"
121 );
122 unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
125 }
126
127 fn file(backing: &Arc<Backing>, offset: usize, len: usize) -> Self {
130 let ptr = if len == 0 {
131 NonNull::dangling()
132 } else {
133 let bytes = &backing.bytes()[offset..offset + len * std::mem::size_of::<T>()];
134 let p = bytes.as_ptr().cast::<T>().cast_mut();
135 assert!(p.is_aligned(), "index section is misaligned");
138 NonNull::new(p).expect("a slice pointer is never null")
139 };
140 Self {
141 ptr,
142 len,
143 owner: Owner::File {
144 _file: Arc::clone(backing),
145 },
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub enum Residency {
153 Mapped,
157 #[default]
160 Prefaulted,
161 Heap,
163}
164
165const MAGIC: &[u8; 8] = b"HMSVGR01";
166const ORDER_MARK: u64 = 0x0102_0304_0506_0708;
168const ALIGN: usize = 4096;
170const HEADER_WORDS: usize = 19;
171const SECTIONS: [&str; 15] = [
173 "half_norms",
174 "adj",
175 "adj3",
176 "adjv",
177 "adjv_off",
178 "codes",
179 "scales",
180 "residual",
181 "res_scales",
182 "ids",
183 "screen",
184 "blocks",
185 "mean",
186 "upper_ids",
187 "layers",
188];
189const PREFIX: usize = 8 + 8 * HEADER_WORDS + 16 * SECTIONS.len();
190
191fn bad(message: String) -> std::io::Error {
192 std::io::Error::new(
193 std::io::ErrorKind::InvalidData,
194 format!("index file: {message}"),
195 )
196}
197
198fn le_words(bytes: &[u8]) -> impl Iterator<Item = u64> + '_ {
199 bytes
200 .as_chunks::<8>()
201 .0
202 .iter()
203 .map(|c| u64::from_le_bytes(*c))
204}
205
206fn encode_layers(layers: &[Vec<Vec<u32>>]) -> Vec<u32> {
209 let mut out = Vec::new();
210 for layer in layers {
211 out.push(layer.len() as u32);
212 out.extend(layer.iter().map(|l| l.len() as u32));
213 }
214 for layer in layers {
215 layer.iter().for_each(|l| out.extend_from_slice(l));
216 }
217 out
218}
219
220fn decode_layers(words: &[u32], count: usize, top: usize) -> std::io::Result<Vec<Vec<Vec<u32>>>> {
221 let short = || bad("layers section is truncated".into());
222 let mut shapes = Vec::with_capacity(count);
223 let mut at = 0;
224 for _ in 0..count {
225 let size = *words.get(at).ok_or_else(short)? as usize;
226 let lens = words.get(at + 1..at + 1 + size).ok_or_else(short)?;
227 shapes.push(lens);
228 at += 1 + size;
229 }
230 let mut layers = Vec::with_capacity(count);
231 let mut prev = top;
232 for lens in shapes {
233 if lens.is_empty() || lens.len() > prev {
235 return Err(bad("upper layer sizes do not shrink upwards".into()));
236 }
237 prev = lens.len();
238 let mut layer = Vec::with_capacity(lens.len());
239 for &l in lens {
240 let list = words.get(at..at + l as usize).ok_or_else(short)?;
241 if list.iter().any(|&u| u as usize >= lens.len()) {
242 return Err(bad("upper-layer id out of range".into()));
243 }
244 layer.push(list.to_vec());
245 at += l as usize;
246 }
247 layers.push(layer);
248 }
249 if at != words.len() {
250 return Err(bad("layers section has trailing data".into()));
251 }
252 Ok(layers)
253}
254
255fn parent_dir(path: &Path) -> &Path {
258 path.parent()
259 .filter(|p| !p.as_os_str().is_empty())
260 .unwrap_or_else(|| Path::new("."))
261}
262
263fn mul(a: usize, b: usize, what: &str) -> std::io::Result<usize> {
265 a.checked_mul(b)
266 .ok_or_else(|| bad(format!("{what} overflows")))
267}
268
269impl VGraph {
270 pub(crate) fn release_pages(&self) -> std::io::Result<()> {
271 #[cfg(target_os = "linux")]
272 if let Owner::File { _file } = &self.codes.lines.owner {
273 if let Backing::Map(map) = _file.as_ref() {
274 unsafe { map.unchecked_advise(memmap2::UncheckedAdvice::DontNeed)? };
277 }
278 }
279 Ok(())
280 }
281
282 pub(crate) fn save_from_graph(
283 g: &Graph,
284 path: &Path,
285 bits: u8,
286 residual_bits: u8,
287 align: bool,
288 reorder: bool,
289 id_bytes: u8,
290 ) -> std::io::Result<()> {
291 save_graph(
292 g.view(),
293 path,
294 bits,
295 residual_bits,
296 align,
297 reorder,
298 id_bytes,
299 )
300 }
301
302 #[cfg(not(target_arch = "wasm32"))]
303 pub(crate) fn save_prepared(
304 g: &super::super::PreparedGraph,
305 path: &Path,
306 bits: u8,
307 residual_bits: u8,
308 align: bool,
309 reorder: bool,
310 id_bytes: u8,
311 ) -> std::io::Result<()> {
312 save_graph(
313 g.view(),
314 path,
315 bits,
316 residual_bits,
317 align,
318 reorder,
319 id_bytes,
320 )
321 }
322
323 pub fn dim(&self) -> usize {
325 self.dim
326 }
327
328 pub fn metric(&self) -> VMetric {
330 self.metric
331 }
332
333 pub fn save(&self, path: &Path) -> std::io::Result<()> {
337 if cfg!(target_endian = "big") {
338 return Err(std::io::Error::new(
339 std::io::ErrorKind::Unsupported,
340 "index files are little-endian; this target is big-endian",
341 ));
342 }
343 let id_bytes = if !self.adjv_off.is_empty() {
344 0
345 } else if !self.adj3.is_empty() {
346 3
347 } else {
348 4
349 };
350 let header: [u64; HEADER_WORDS] = [
351 ORDER_MARK,
352 self.n as u64,
353 self.dim as u64,
354 self.degree as u64,
355 u64::from(self.bits),
356 u64::from(self.res_bits),
357 self.stride as u64,
358 id_bytes,
359 u64::from(self.entry),
360 self.metric().code(),
361 self.screen_words as u64,
362 u64::from(self.screen_sigma.to_bits()),
363 u64::from(self.block_bits),
364 self.block_dim as u64,
365 self.block_m as u64,
366 self.blocks_per_vertex as u64,
367 u64::from(self.block_kappa.to_bits()),
368 u64::from(!self.ids.is_empty()),
369 self.layers.len() as u64,
370 ];
371 let layers = encode_layers(&self.layers);
372 let sections: [&[u8]; SECTIONS.len()] = [
373 as_bytes(&self.half_norms),
374 as_bytes(&self.adj),
375 as_bytes(&self.adj3),
376 as_bytes(&self.adjv),
377 as_bytes(&self.adjv_off),
378 as_bytes(&self.codes.lines),
379 as_bytes(&self.scales),
380 as_bytes(&self.residual),
381 as_bytes(&self.res_scales),
382 as_bytes(&self.ids),
383 as_bytes(&self.screen),
384 as_bytes(&self.blocks.lines),
385 as_bytes(&self.mean),
386 as_bytes(&self.upper_ids),
387 as_bytes(&layers),
388 ];
389 let mut table = Vec::with_capacity(sections.len());
390 let mut at = PREFIX;
391 for (index, s) in sections.iter().enumerate() {
392 let len = if !self.adj_off.is_empty() && index == if id_bytes == 3 { 2 } else { 1 } {
393 self.n * self.degree * id_bytes as usize
394 } else {
395 s.len()
396 };
397 at = at.next_multiple_of(ALIGN);
398 table.push((at as u64, len as u64));
399 at += len;
400 }
401 if path.file_name().is_none() {
402 return Err(bad(format!("{} names no file", path.display())));
403 }
404 let parent = parent_dir(path);
405 let mut temp = tempfile::NamedTempFile::new_in(parent)?;
406 {
407 let mut w = std::io::BufWriter::with_capacity(1 << 20, temp.as_file_mut());
408 w.write_all(MAGIC)?;
409 for x in header {
410 w.write_all(&x.to_le_bytes())?;
411 }
412 for (off, len) in &table {
413 w.write_all(&off.to_le_bytes())?;
414 w.write_all(&len.to_le_bytes())?;
415 }
416 let mut pos = PREFIX;
417 let zeros = [0u8; ALIGN];
418 for (index, (s, (off, len))) in sections.iter().zip(&table).enumerate() {
419 w.write_all(&zeros[..*off as usize - pos])?;
420 if !self.adj_off.is_empty() && index == if id_bytes == 3 { 2 } else { 1 } {
421 let width = id_bytes as usize;
422 let mut row = vec![0xff; self.degree * width];
423 for v in 0..self.n {
424 row.fill(0xff);
425 let mut at = 0;
426 self.for_each_neighbour(v, |u| {
427 row[at..at + width].copy_from_slice(&u.to_le_bytes()[..width]);
428 at += width;
429 });
430 w.write_all(&row)?;
431 }
432 } else {
433 w.write_all(s)?;
434 }
435 pos = *off as usize + *len as usize;
436 }
437 w.flush()?;
438 }
439 temp.as_file().sync_all()?;
440 crate::core::durable_file::sync_mapped_file(temp.as_file())?;
441 temp.persist(path).map_err(|e| e.error)?;
442 crate::core::durable_file::sync_directory(parent).map_err(std::io::Error::other)
443 }
444
445 pub fn open(path: &Path, residency: Residency) -> std::io::Result<Self> {
452 if cfg!(target_endian = "big") {
453 return Err(std::io::Error::new(
454 std::io::ErrorKind::Unsupported,
455 "index files are little-endian; this target is big-endian",
456 ));
457 }
458 let mut file = File::open(path)?;
459 let len = usize::try_from(file.metadata()?.len())
460 .map_err(|_| bad("larger than the address space".into()))?;
461 if len < PREFIX {
462 return Err(bad(format!("{len} bytes is shorter than the header")));
463 }
464 let backing = Arc::new(match residency {
465 Residency::Heap => {
466 let count = len.div_ceil(std::mem::size_of::<Line>());
467 let mut lines = Vec::with_capacity(count);
468 super::huge_capacity(lines.as_ptr(), count);
469 lines.resize(count, Line([0; 128]));
470 let buf =
473 unsafe { std::slice::from_raw_parts_mut(lines.as_mut_ptr().cast::<u8>(), len) };
474 file.read_exact(buf)?;
475 Backing::Heap(lines, len)
476 }
477 Residency::Mapped | Residency::Prefaulted => {
480 Backing::Map(unsafe { memmap2::Mmap::map(&file)? })
481 }
482 });
483 let out = Self::from_backing(&backing)?;
484 if residency == Residency::Prefaulted {
485 let bytes = backing.bytes();
486 let mut sum = 0u8;
487 for at in (0..bytes.len()).step_by(ALIGN) {
488 sum ^= unsafe { std::ptr::read_volatile(bytes.as_ptr().add(at)) };
491 }
492 std::hint::black_box(sum);
493 }
494 Ok(out)
495 }
496
497 fn from_backing(backing: &Arc<Backing>) -> std::io::Result<Self> {
498 let bytes = backing.bytes();
499 if bytes.len() < PREFIX {
500 return Err(bad(format!(
501 "{} bytes is shorter than the header",
502 bytes.len()
503 )));
504 }
505 if &bytes[..8] != MAGIC {
506 return Err(bad(
507 "bad magic (not an index file, or another format)".into()
508 ));
509 }
510 let h: Vec<u64> = le_words(&bytes[8..8 + 8 * HEADER_WORDS]).collect();
511 if h[0] != ORDER_MARK {
512 return Err(bad("byte-order mark mismatch".into()));
513 }
514 let small = |i: usize, max: u64, what: &str| -> std::io::Result<usize> {
515 if h[i] > max {
516 Err(bad(format!("{what} {} exceeds {max}", h[i])))
517 } else {
518 Ok(h[i] as usize)
519 }
520 };
521 let n = small(1, u64::from(NONE) - 1, "vertex count")?;
522 let dim = small(2, 1 << 20, "dimension")?;
523 let degree = small(3, 1 << 16, "degree")?;
524 let bits = small(4, 8, "code bits")? as u8;
525 let res_bits = small(5, 8, "residual bits")? as u8;
526 let stride = small(6, 1 << 24, "row stride")?;
527 let id_bytes = small(7, 4, "id width")?;
528 let entry = small(8, u64::from(NONE), "entry")?;
529 let metric = match small(9, 2, "metric")? {
530 0 => VMetric::Angular,
531 1 => VMetric::Euclidean,
532 2 => VMetric::InnerProduct,
533 _ => unreachable!(),
534 };
535 let euclidean = metric == VMetric::Euclidean;
536 let screen_words = small(10, 1 << 20, "screen words")?;
537 let screen_sigma = f32::from_bits(h[11] as u32);
538 let block_bits = small(12, 4, "block bits")? as u8;
539 let block_dim = small(13, 1 << 22, "block dimension")?;
540 let block_m = small(14, 1 << 22, "block groups")?;
541 let blocks_per_vertex = small(15, 1 << 16, "blocks per vertex")?;
542 let block_kappa = f32::from_bits(h[16] as u32);
543 let reordered = small(17, 1, "reorder flag")? == 1;
544 let layer_count = small(18, 64, "layer count")?;
545
546 if n == 0 || dim == 0 || degree == 0 || !degree.is_multiple_of(2) || entry >= n {
547 return Err(bad(
548 "empty index, zero dimension, odd degree or entry out of range".into(),
549 ));
550 }
551 if !matches!(bits, 4 | 8) || !matches!(res_bits, 0 | 4 | 8) {
552 return Err(bad(format!("code bits {bits} / residual bits {res_bits}")));
553 }
554 if !(id_bytes == 4 || id_bytes == 0 || (id_bytes == 3 && (n as u64) < u64::from(NONE3))) {
555 return Err(bad(format!("id width {id_bytes} for {n} vertices")));
556 }
557 let padded = dim.next_multiple_of(if bits == 4 { 32 } else { 16 });
559 let cbytes = if bits == 4 { padded / 2 } else { padded };
560 let aligned = if cbytes <= 64 {
561 cbytes.next_power_of_two()
562 } else {
563 cbytes.next_multiple_of(128)
564 };
565 if stride != cbytes && stride != aligned {
566 return Err(bad(format!("row stride {stride} for {cbytes}-byte codes")));
567 }
568 let res_stride = match res_bits {
569 8 => padded,
570 4 => padded / 2,
571 _ => 0,
572 };
573 let screen_rot = (screen_words > 0).then(|| Rotation::new(dim, 0, SCREEN_SEED));
574 if let Some(rot) = &screen_rot {
575 if metric != VMetric::Angular || screen_words != rot.padded() / 64 {
576 return Err(bad("screen shape does not match the dimension".into()));
577 }
578 }
579 let block_rot = (block_bits > 0).then(|| Rotation::new(dim, 0, BLOCK_SEED));
580 let record = match &block_rot {
581 Some(rot) => {
582 if metric != VMetric::Angular
583 || !matches!(block_bits, 1 | 2 | 4)
584 || block_dim != rot.padded()
585 || block_m != block_dim * usize::from(block_bits) / 4
586 || blocks_per_vertex != degree.div_ceil(BLOCK)
587 {
588 return Err(bad("neighbour-block shape does not match the graph".into()));
589 }
590 block_record_bytes(block_m)
591 }
592 None if block_dim == 0 && block_m == 0 && blocks_per_vertex == 0 => 0,
593 None => return Err(bad("block shape without blocks".into())),
594 };
595
596 let table: Vec<(usize, usize)> = le_words(&bytes[8 + 8 * HEADER_WORDS..PREFIX])
597 .collect::<Vec<_>>()
598 .as_chunks::<2>()
599 .0
600 .iter()
601 .map(|&[o, l]| (o as usize, l as usize))
602 .collect();
603 let mut end = PREFIX;
604 for (name, &(off, len)) in SECTIONS.iter().zip(&table) {
605 let fits = off.is_multiple_of(ALIGN)
606 && off >= end
607 && off.checked_add(len).is_some_and(|e| e <= bytes.len());
608 if !fits {
609 return Err(bad(format!(
610 "section {name} lies outside the file or overlaps"
611 )));
612 }
613 end = off + len;
614 }
615 let row_lines = mul(n, stride, "code rows")?.div_ceil(128);
616 let block_bytes = mul(mul(n, blocks_per_vertex, "blocks")?, record, "blocks")?;
617 let nd = mul(n, degree, "adjacency")?;
618 let expect = |s: usize| -> Option<usize> {
619 Some(match s {
620 0 => 4 * if euclidean { n } else { 0 },
621 1 => 4 * if id_bytes == 4 { nd } else { 0 },
622 2 => 3 * if id_bytes == 3 { nd } else { 0 },
623 3 if id_bytes == 0 => return None,
624 3 => 0,
625 4 => 4 * if id_bytes == 0 { n + 1 } else { 0 },
626 5 => 128 * row_lines,
627 6 => 4 * n,
628 7 => n.checked_mul(res_stride)?,
629 8 => 4 * if res_bits > 0 { n } else { 0 },
630 9 => 4 * if reordered { n } else { 0 },
631 10 => {
632 8 * if screen_words > 0 {
633 n.checked_mul(screen_words + 1)?
634 } else {
635 0
636 }
637 }
638 11 => 128 * block_bytes.div_ceil(128),
639 12 => 4 * padded,
640 _ => return None,
641 })
642 };
643 for (s, (name, &(_, len))) in SECTIONS.iter().zip(&table).enumerate() {
644 let element = match s {
645 2 | 3 | 7 => 1,
646 5 | 11 => 128,
647 10 => 8,
648 _ => 4,
649 };
650 let ok = match expect(s) {
651 Some(want) => len == want,
652 None => len.is_multiple_of(element),
653 };
654 if !ok {
655 return Err(bad(format!(
656 "section {name} has {len} bytes, not the expected size"
657 )));
658 }
659 }
660 let sec = |s: usize| table[s];
661 let slab = |s: usize, size: usize| (sec(s).0, sec(s).1 / size);
662 macro_rules! view {
663 ($s:expr, $t:ty) => {{
664 let (off, count) = slab($s, std::mem::size_of::<$t>());
665 Slab::<$t>::file(backing, off, count)
666 }};
667 }
668 let adj = view!(1, u32);
669 let adj3 = view!(2, u8);
670 let adjv = view!(3, u8);
671 let adjv_off = view!(4, u32);
672 let ids = view!(9, u32);
673 let upper_ids: Vec<u32> = view!(13, u32).to_vec();
674 let layer_words = view!(14, u32);
675
676 let in_range = |u: u32, none: u32| (u as usize) < n || u == none;
678 let ids_ok = match id_bytes {
679 4 => adj.iter().all(|&u| in_range(u, NONE)),
680 3 => adj3
681 .as_chunks::<3>()
682 .0
683 .iter()
684 .all(|b| in_range(u32::from_le_bytes([b[0], b[1], b[2], 0]), NONE3)),
685 _ => delta_lists_ok(&adjv, &adjv_off, n, degree),
686 };
687 if !ids_ok {
688 return Err(bad("neighbour id out of range or malformed list".into()));
689 }
690 if ids.iter().any(|&u| u as usize >= n) || upper_ids.iter().any(|&u| u as usize >= n) {
691 return Err(bad("vertex id out of range".into()));
692 }
693 if upper_ids.len() > n {
694 return Err(bad("more upper-layer vertices than vertices".into()));
695 }
696 let layers = decode_layers(&layer_words, layer_count, upper_ids.len())?;
697 let mean: Vec<f32> = view!(12, f32).to_vec();
698 Ok(Self {
699 n,
700 dim,
701 metric,
702 padded,
703 bits,
704 cbytes,
705 stride,
706 degree,
707 mean,
708 half_norms: view!(0, f32),
709 adj,
710 adj3,
711 adj_off: Vec::new(),
712 adjv,
713 adjv_off,
714 codes: Lines {
715 lines: view!(5, Line),
716 len: n * stride,
717 },
718 scales: view!(6, f32),
719 res_bits,
720 res_stride,
721 residual: view!(7, i8),
722 res_scales: view!(8, f32),
723 entry: entry as u32,
724 upper_ids,
725 layers,
726 ids,
727 screen: view!(10, u64),
728 screen_words,
729 screen_rot,
730 screen_sigma,
731 block_bits,
732 block_dim,
733 block_m,
734 blocks_per_vertex,
735 blocks: Lines {
736 lines: view!(11, Line),
737 len: block_bytes,
738 },
739 block_rot,
740 block_kappa,
741 })
742 }
743}
744
745fn section_write<T: Plain>(
746 w: &mut std::io::BufWriter<&mut File>,
747 table: &[(usize, usize)],
748 section: usize,
749 offset: usize,
750 values: &[T],
751) -> std::io::Result<()> {
752 let bytes = as_bytes(values);
753 let (start, len) = table[section];
754 if offset.checked_add(bytes.len()).is_none_or(|end| end > len) {
755 return Err(bad("section write exceeds computed geometry".into()));
756 }
757 w.seek(SeekFrom::Start((start + offset) as u64))?;
758 w.write_all(bytes)
759}
760
761fn save_graph(
762 g: GraphView<'_>,
763 path: &Path,
764 bits: u8,
765 residual_bits: u8,
766 align: bool,
767 reorder: bool,
768 id_bytes: u8,
769) -> std::io::Result<()> {
770 if cfg!(target_endian = "big") {
771 return Err(std::io::Error::new(
772 std::io::ErrorKind::Unsupported,
773 "index files are little-endian",
774 ));
775 }
776 let VertexLayout {
777 padded,
778 cbytes,
779 stride,
780 res_stride,
781 } = VertexLayout::new(g.n, g.dim, bits, residual_bits, align, id_bytes)
782 .map_err(|e| bad(e.to_string()))?;
783 let (n, dim, r) = (g.n, g.dim, g.degree);
784 let ids = if reorder { bfs_order(g) } else { Vec::new() };
785 let old = |v: usize| if reorder { ids[v] as usize } else { v };
786 let mut new_ids = vec![0u32; if reorder { n } else { 0 }];
787 for (v, &id) in ids.iter().enumerate() {
788 new_ids[id as usize] = v as u32;
789 }
790 let new = |v: u32| if reorder { new_ids[v as usize] } else { v };
791 let list = |v: usize| g.neighbors(old(v)).iter().take(r).map(|&v| new(v));
792 let capacity = || bad("encoded index size overflows".into());
793 let mul = |a: usize, b: usize| {
794 a.checked_mul(b)
795 .filter(|&n| n <= isize::MAX as usize)
796 .ok_or_else(capacity)
797 };
798 let mut sorted = Vec::new();
799 let mut encoded = Vec::new();
800 let mut encode_list = |v: usize, dst: &mut Vec<u8>| {
801 sorted.clear();
802 sorted.extend(list(v));
803 sorted.sort_unstable();
804 let mut prev = v as u32;
805 for (i, &u) in sorted.iter().enumerate() {
806 let delta = if i == 0 {
807 zigzag(i64::from(u) - i64::from(prev))
808 } else {
809 u64::from(u - prev - 1)
810 };
811 put_varint(dst, delta);
812 prev = u;
813 }
814 };
815 let mut adjv_len = 0usize;
816 if id_bytes == 0 {
817 for v in 0..n {
818 encoded.clear();
819 encode_list(v, &mut encoded);
820 adjv_len = adjv_len
821 .checked_add(encoded.len())
822 .filter(|&n| n <= u32::MAX as usize)
823 .ok_or_else(capacity)?;
824 }
825 }
826 let layers_len = g.layers.iter().try_fold(0usize, |size, layer| {
827 let words = layer.iter().try_fold(
828 1usize.checked_add(layer.len()).ok_or_else(capacity)?,
829 |words, list| words.checked_add(list.len()).ok_or_else(capacity),
830 )?;
831 size.checked_add(mul(words, 4)?).ok_or_else(capacity)
832 })?;
833 let codes_len = mul(n, stride)?
834 .checked_next_multiple_of(128)
835 .ok_or_else(capacity)?;
836 let lengths = [
837 if g.metric == Metric::Euclidean {
838 mul(n, 4)?
839 } else {
840 0
841 },
842 if id_bytes == 4 {
843 mul(mul(n, r)?, 4)?
844 } else {
845 0
846 },
847 if id_bytes == 3 {
848 mul(mul(n, r)?, 3)?
849 } else {
850 0
851 },
852 adjv_len,
853 if id_bytes == 0 {
854 mul(n.checked_add(1).ok_or_else(capacity)?, 4)?
855 } else {
856 0
857 },
858 codes_len,
859 mul(n, 4)?,
860 mul(n, res_stride)?,
861 if residual_bits > 0 { mul(n, 4)? } else { 0 },
862 mul(ids.len(), 4)?,
863 0,
864 0,
865 mul(padded, 4)?,
866 mul(g.upper_ids.len(), 4)?,
867 layers_len,
868 ];
869 let mut table = Vec::with_capacity(SECTIONS.len());
870 let mut end = PREFIX;
871 for len in lengths {
872 end = end.checked_next_multiple_of(ALIGN).ok_or_else(capacity)?;
873 table.push((end, len));
874 end = end
875 .checked_add(len)
876 .filter(|&n| n <= isize::MAX as usize)
877 .ok_or_else(capacity)?;
878 }
879 let parent = parent_dir(path);
880 let mut temp = tempfile::NamedTempFile::new_in(parent)?;
881 temp.as_file().set_len(end as u64)?;
882 let mut w = std::io::BufWriter::with_capacity(1 << 20, temp.as_file_mut());
883 w.write_all(MAGIC)?;
884 for word in [
885 ORDER_MARK,
886 n as u64,
887 dim as u64,
888 r as u64,
889 u64::from(bits),
890 u64::from(residual_bits),
891 stride as u64,
892 u64::from(id_bytes),
893 u64::from(new(g.entry)),
894 g.metric.code(),
895 0,
896 0,
897 0,
898 0,
899 0,
900 0,
901 0,
902 u64::from(reorder),
903 g.layers.len() as u64,
904 ] {
905 w.write_all(&word.to_le_bytes())?;
906 }
907 for &(offset, len) in &table {
908 w.write_all(&(offset as u64).to_le_bytes())?;
909 w.write_all(&(len as u64).to_le_bytes())?;
910 }
911 let edge_batch = ((1 << 20) / r.saturating_mul(10).max(1)).max(1);
912 let mut adjv_at = 0usize;
913 for start in (0..n).step_by(edge_batch) {
914 let stop = n.min(start + edge_batch);
915 if id_bytes == 0 {
916 let mut offsets = Vec::with_capacity(stop - start);
917 encoded.clear();
918 for v in start..stop {
919 offsets.push((adjv_at + encoded.len()) as u32);
920 encode_list(v, &mut encoded);
921 }
922 section_write(&mut w, &table, 3, adjv_at, &encoded)?;
923 section_write(&mut w, &table, 4, start * 4, &offsets)?;
924 adjv_at += encoded.len();
925 } else if id_bytes == 3 {
926 encoded.clear();
927 for v in start..stop {
928 let mut count = 0;
929 for u in list(v) {
930 encoded.extend_from_slice(&u.min(NONE3).to_le_bytes()[..3]);
931 count += 1;
932 }
933 for _ in count..r {
934 encoded.extend_from_slice(&NONE3.to_le_bytes()[..3]);
935 }
936 }
937 section_write(&mut w, &table, 2, start * r * 3, &encoded)?;
938 } else {
939 let mut edges = vec![NONE; (stop - start) * r];
940 for (v, row) in (start..stop).zip(edges.chunks_exact_mut(r)) {
941 for (dst, u) in row.iter_mut().zip(list(v)) {
942 *dst = u;
943 }
944 }
945 section_write(&mut w, &table, 1, start * r * 4, &edges)?;
946 }
947 }
948 if id_bytes == 0 {
949 section_write(&mut w, &table, 4, n * 4, &[adjv_at as u32])?;
950 }
951 let mut sum = vec![0f64; dim];
952 for row in g.vectors.chunks_exact(dim) {
953 sum.iter_mut()
954 .zip(row)
955 .for_each(|(s, &v)| *s += f64::from(v));
956 }
957 let mut mean: Vec<f32> = sum.iter().map(|s| (s / n as f64) as f32).collect();
958 mean.resize(padded, 0.0);
959 g.release_rows()?;
960 let batch = ((1 << 20) / (stride + res_stride + 16)).max(1);
961 for start in (0..n).step_by(batch) {
962 let count = batch.min(n - start);
963 let mut codes = vec![0i8; count * stride];
964 let mut scales = vec![0f32; count];
965 let centred = |v: usize| -> Vec<f32> {
966 g.vectors[old(v) * dim..(old(v) + 1) * dim]
967 .iter()
968 .zip(&mean)
969 .map(|(a, m)| a - m)
970 .collect()
971 };
972 codes
973 .par_chunks_exact_mut(stride)
974 .zip(scales.par_iter_mut())
975 .enumerate()
976 .for_each(|(v, (dst, scale))| {
977 let x = centred(start + v);
978 *scale = if bits == 8 {
979 quantize(&x, &mut dst[..cbytes])
980 } else {
981 quantize4(&x, &mut dst[..cbytes])
982 };
983 });
984 section_write(&mut w, &table, 5, start * stride, &codes)?;
985 section_write(&mut w, &table, 6, start * 4, &scales)?;
986 if residual_bits > 0 {
987 let mut residual = vec![0i8; count * res_stride];
988 let mut res_scales = vec![0f32; count];
989 residual
990 .par_chunks_exact_mut(res_stride)
991 .zip(res_scales.par_iter_mut())
992 .enumerate()
993 .for_each(|(v, (dst, scale))| {
994 let mut x = centred(start + v);
995 let c = &codes[v * stride..v * stride + cbytes];
996 x.iter_mut()
997 .enumerate()
998 .for_each(|(i, a)| *a -= scales[v] * f32::from(code_at(c, bits, i)));
999 *scale = if residual_bits == 8 {
1000 quantize(&x, dst)
1001 } else {
1002 quantize4(&x, dst)
1003 };
1004 });
1005 section_write(&mut w, &table, 7, start * res_stride, &residual)?;
1006 section_write(&mut w, &table, 8, start * 4, &res_scales)?;
1007 }
1008 if g.metric == Metric::Euclidean {
1009 let half: Vec<f32> = (start..start + count)
1010 .map(|v| 0.5 * g.norms[old(v)])
1011 .collect();
1012 section_write(&mut w, &table, 0, start * 4, &half)?;
1013 }
1014 g.release_rows()?;
1015 }
1016 section_write(&mut w, &table, 9, 0, &ids)?;
1017 section_write(&mut w, &table, 12, 0, &mean)?;
1018 let upper: Vec<u32> = g.upper_ids.iter().map(|&v| new(v)).collect();
1019 section_write(&mut w, &table, 13, 0, &upper)?;
1020 w.seek(SeekFrom::Start(table[14].0 as u64))?;
1021 for layer in g.layers {
1022 w.write_all(&(layer.len() as u32).to_le_bytes())?;
1023 for list in layer {
1024 w.write_all(&(list.len() as u32).to_le_bytes())?;
1025 }
1026 }
1027 for layer in g.layers {
1028 for list in layer {
1029 w.write_all(as_bytes(list))?;
1030 }
1031 }
1032 w.flush()?;
1033 drop(w);
1034 temp.as_file().sync_all()?;
1035 crate::core::durable_file::sync_mapped_file(temp.as_file())?;
1036 temp.persist(path).map_err(|e| e.error)?;
1037 crate::core::durable_file::sync_directory(parent).map_err(std::io::Error::other)
1038}
1039
1040fn delta_lists_ok(adjv: &[u8], off: &[u32], n: usize, degree: usize) -> bool {
1044 if off.len() != n + 1 || off[0] != 0 || off[n] as usize != adjv.len() {
1045 return false;
1046 }
1047 for v in 0..n {
1048 let (a, b) = (off[v] as usize, off[v + 1] as usize);
1049 if a > b || b > adjv.len() {
1050 return false;
1051 }
1052 let span = &adjv[a..b];
1053 let (mut at, mut prev, mut count) = (0, v as i64, 0);
1054 while at < span.len() {
1055 let rest = &span[at..span.len().min(at + 10)];
1056 if rest.iter().all(|&b| b & 0x80 != 0) {
1057 return false;
1058 }
1059 let (delta, used) = get_varint(rest);
1060 at += used;
1061 let u = if count == 0 {
1062 prev.checked_add(unzigzag(delta))
1063 } else {
1064 i64::try_from(delta)
1065 .ok()
1066 .and_then(|d| d.checked_add(1))
1067 .and_then(|d| prev.checked_add(d))
1068 };
1069 match u {
1070 Some(u) if (0..n as i64).contains(&u) => prev = u,
1071 _ => return false,
1072 }
1073 count += 1;
1074 if count > degree {
1075 return false;
1076 }
1077 }
1078 }
1079 true
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use crate::core::qgraph::tests::clustered;
1086 use crate::core::qgraph::{BuildParams, Graph, Metric, VSearchParams};
1087
1088 fn index_variants() -> Vec<(String, VGraph, Vec<f32>)> {
1091 let d = 40;
1092 let mut out = Vec::new();
1093 for metric in [Metric::Angular, Metric::Euclidean] {
1094 let data = clustered(2000, d, 7);
1095 let queries = clustered(40, d, 8);
1096 let params = BuildParams {
1097 metric,
1098 ..BuildParams::default()
1099 };
1100 let g = Graph::build(&data, d, ¶ms).expect("build");
1101 type Config = (u8, u8, bool, bool, u8, bool);
1102 let configs: &[Config] = if metric == Metric::Angular {
1103 &[
1104 (4, 8, true, true, 3, false),
1105 (8, 4, false, false, 4, true),
1106 (4, 0, true, true, 0, false),
1107 ]
1108 } else {
1109 &[(8, 8, true, true, 3, false), (4, 4, false, false, 0, false)]
1110 };
1111 for &(bits, res, align, reorder, id_bytes, screen) in configs {
1112 let v = VGraph::from_graph_with(&g, bits, res, align, reorder, id_bytes, screen)
1113 .expect("encode");
1114 let name =
1115 format!("{metric:?} b{bits} r{res} a{align} o{reorder} i{id_bytes} s{screen}");
1116 out.push((name, v, queries.clone()));
1117 }
1118 if metric == Metric::Angular {
1119 let v = VGraph::from_graph_with(&g, 4, 8, true, true, 3, false)
1120 .and_then(|v| v.with_blocks(&g, 2))
1121 .expect("encode");
1122 out.push(("angular blocks".into(), v, queries.clone()));
1123 } else {
1124 for (bits, id_bytes) in [(4, 0), (8, 3), (8, 4)] {
1125 let v = VGraph::from_lifted_ip_graph(&g, bits, 8, true, true, id_bytes)
1126 .expect("encode direct IP");
1127 let mut probes: Vec<f32> = queries
1128 .chunks_exact(d)
1129 .flat_map(|q| q[..d - 1].iter().copied())
1130 .collect();
1131 probes.extend(vec![0.0; d - 1]);
1132 out.push((format!("direct IP b{bits} i{id_bytes}"), v, probes));
1133 }
1134 }
1135 }
1136 out
1137 }
1138
1139 fn same_results(what: &str, a: &VGraph, b: &VGraph, queries: &[f32]) {
1140 let (mut sa, mut sb) = (a.searcher(), b.searcher());
1141 let (mut oa, mut ob) = (Vec::new(), Vec::new());
1142 for q in queries.chunks_exact(a.dim()) {
1143 for (ef, rerank, reservoir) in [(16, 0, 0), (48, 20, 0), (16, 40, 40)] {
1144 if reservoir > 0 && (a.has_screen() || a.has_blocks()) {
1145 continue;
1146 }
1147 let p = VSearchParams {
1148 ef,
1149 rerank,
1150 reservoir,
1151 ..VSearchParams::default()
1152 };
1153 let ea = sa.search(q, 10, p, &mut oa).expect("search");
1154 let eb = sb.search(q, 10, p, &mut ob).expect("search");
1155 assert_eq!((ea, &oa), (eb, &ob), "{what}");
1156 }
1157 }
1158 }
1159
1160 #[test]
1161 fn streamed_and_mapped_builds_preserve_exact_vertex_bytes() {
1162 let dir = tempfile::tempdir().unwrap();
1163 let d = 100;
1164 for metric in [Metric::Angular, Metric::Euclidean] {
1165 for n in [4095, 4096, 4097] {
1166 let data = clustered(n, d, 7);
1167 let params = BuildParams {
1168 metric,
1169 degree: 8,
1170 build_ef: 16,
1171 ..BuildParams::default()
1172 };
1173 let graph = Graph::build(&data, d, ¶ms).unwrap();
1174 let mut rows = super::super::super::PreparedRows::new(n, d).unwrap();
1175 for chunk in data.chunks(17 * d) {
1176 rows.append(chunk).unwrap();
1177 }
1178 let mapped = rows.build(¶ms).unwrap();
1179 for (bits, res, align, reorder, id_bytes) in [
1180 (8, 8, true, false, 4),
1181 (4, 8, true, true, 3),
1182 (8, 4, false, false, 0),
1183 (4, 0, true, true, 0),
1184 ] {
1185 let owned = dir.path().join("owned");
1186 let streamed = dir.path().join("streamed");
1187 let spilled = dir.path().join("mapped");
1188 let index =
1189 VGraph::from_graph_with(&graph, bits, res, align, reorder, id_bytes, false)
1190 .unwrap();
1191 index.save(&owned).unwrap();
1192 VGraph::save_from_graph(&graph, &streamed, bits, res, align, reorder, id_bytes)
1193 .unwrap();
1194 VGraph::save_prepared(&mapped, &spilled, bits, res, align, reorder, id_bytes)
1195 .unwrap();
1196 let expected = std::fs::read(&owned).unwrap();
1197 assert_eq!(
1198 std::fs::read(&streamed).unwrap(),
1199 expected,
1200 "{metric:?} {n} {bits}/{res} {id_bytes}"
1201 );
1202 assert_eq!(
1203 std::fs::read(&spilled).unwrap(),
1204 expected,
1205 "mapped {metric:?} {n} {bits}/{res} {id_bytes}"
1206 );
1207 let reopened = VGraph::open(&spilled, Residency::Mapped).unwrap();
1208 reopened.release_pages().unwrap();
1209 same_results("mapped encoding", &index, &reopened, &data[..3 * d]);
1210 }
1211 }
1212 }
1213 }
1214
1215 #[test]
1216 fn prepared_rows_reject_incomplete_excess_and_invalid_matrices() {
1217 use super::super::super::PreparedRows;
1218 let params = BuildParams::default();
1219 assert!(PreparedRows::new(1, 0).is_err());
1220 assert!(PreparedRows::new(usize::MAX, 4).is_err());
1221 for count in [1, 2, 3] {
1222 let mut rows = PreparedRows::new(2, 4).unwrap();
1223 let append = rows.append(&vec![1.; count * 4]);
1224 if count > 2 {
1225 assert!(append.is_err());
1226 } else {
1227 append.unwrap();
1228 assert_eq!(rows.build(¶ms).is_ok(), count == 2);
1229 }
1230 }
1231 let mut rows = PreparedRows::new(1, 4).unwrap();
1232 assert!(rows.append(&[1.; 3]).is_err());
1233 rows.append(&[f32::NAN; 4]).unwrap();
1234 assert!(rows.build(¶ms).is_err());
1235 let mut rows = PreparedRows::new(1, 4).unwrap();
1236 rows.append(&[f32::MAX; 4]).unwrap();
1237 assert!(rows
1238 .build(&BuildParams {
1239 metric: Metric::Euclidean,
1240 ..params
1241 })
1242 .is_err());
1243 }
1244
1245 #[test]
1246 fn saved_index_opens_with_identical_results_in_every_residency() {
1247 let dir = tempfile::tempdir().unwrap();
1248 for (name, index, queries) in index_variants() {
1249 let path = dir.path().join("index");
1250 index.save(&path).unwrap();
1251 for residency in [Residency::Mapped, Residency::Prefaulted, Residency::Heap] {
1252 let opened = VGraph::open(&path, residency).unwrap();
1253 assert_eq!(opened.index_bytes(), index.index_bytes(), "{name}");
1254 assert_eq!(opened.metric(), index.metric(), "{name}");
1255 same_results(&format!("{name} {residency:?}"), &index, &opened, &queries);
1256 }
1257 }
1258 }
1259
1260 #[test]
1261 fn damaged_files_are_errors() {
1262 let dir = tempfile::tempdir().unwrap();
1263 let (_, index, _) = index_variants().swap_remove(0);
1264 let path = dir.path().join("a");
1265 index.save(&path).unwrap();
1266 let good = std::fs::read(&path).unwrap();
1267 let open = |bytes: &[u8]| {
1268 let p = dir.path().join("b");
1269 std::fs::write(&p, bytes).unwrap();
1270 VGraph::open(&p, Residency::Heap).map(|_| ())
1271 };
1272 let section = |s: usize| {
1273 let t = 8 + 8 * HEADER_WORDS + 16 * s;
1274 u64::from_le_bytes(good[t..t + 8].try_into().unwrap()) as usize
1275 };
1276 assert!(open(&good).is_ok());
1277 assert!(open(&good[..100]).is_err(), "truncated header");
1278 assert!(open(&good[..section(5) + 10]).is_err(), "truncated codes");
1279 let mut bytes = good.clone();
1280 let at = section(2);
1281 bytes[at..at + 3].copy_from_slice(&[0xfe, 0xff, 0xff]);
1282 let err = open(&bytes).unwrap_err().to_string();
1283 assert!(err.contains("neighbour id"), "{err}");
1284 let mut bytes = good.clone();
1285 bytes[8 + 8 * 6] ^= 1;
1286 assert!(open(&bytes).is_err(), "stride");
1287 let mut bytes = good;
1288 bytes[7] = b'9';
1289 assert!(open(&bytes).is_err(), "magic");
1290 }
1291
1292 #[test]
1293 fn damaged_delta_offsets_are_errors() {
1294 let dir = tempfile::tempdir().unwrap();
1295 let (name, index, _) = index_variants().swap_remove(2);
1296 assert!(name.contains("i0"), "{name}");
1297 let path = dir.path().join("a");
1298 index.save(&path).unwrap();
1299 let mut bytes = std::fs::read(&path).unwrap();
1300 let t = 8 + 8 * HEADER_WORDS + 16 * 4;
1301 let offsets = u64::from_le_bytes(bytes[t..t + 8].try_into().unwrap()) as usize;
1302 bytes[offsets + 4..offsets + 8].copy_from_slice(&u32::MAX.to_le_bytes());
1304 let damaged = dir.path().join("b");
1305 std::fs::write(&damaged, &bytes).unwrap();
1306 for residency in [Residency::Mapped, Residency::Heap] {
1307 let err = VGraph::open(&damaged, residency)
1308 .err()
1309 .expect("damaged offsets");
1310 assert!(err.to_string().contains("neighbour id"), "{err}");
1311 }
1312
1313 let mut adjv = vec![0x02];
1315 adjv.extend([0xff; 9]);
1316 adjv.push(0x00);
1317 let end = adjv.len() as u32;
1318 assert!(!delta_lists_ok(&adjv, &[0, end, end], 2, 2));
1319 assert!(delta_lists_ok(&[0x02, 0x00], &[0, 2, 2, 2], 3, 2));
1320 }
1321
1322 #[test]
1323 fn a_backing_shorter_than_the_header_is_an_error() {
1324 let mut line = Line([0; 128]);
1325 for (dst, &src) in line.0.iter_mut().zip(MAGIC) {
1326 *dst = src as i8;
1327 }
1328 let backing = Arc::new(Backing::Heap(vec![line], 100));
1329 assert!(VGraph::from_backing(&backing).is_err());
1330 }
1331
1332 #[test]
1333 fn save_leaves_a_sibling_temp_name_alone() {
1334 let dir = tempfile::tempdir().unwrap();
1335 let (_, index, queries) = index_variants().swap_remove(0);
1336 let path = dir.path().join("index");
1337 let sibling = dir.path().join("index.tmp");
1338 std::fs::write(&sibling, b"another writer's bytes").unwrap();
1339 index.save(&path).unwrap();
1340 assert_eq!(std::fs::read(&sibling).unwrap(), b"another writer's bytes");
1341 let opened = VGraph::open(&path, Residency::Heap).unwrap();
1342 same_results("saved beside a temp name", &index, &opened, &queries);
1343 assert_eq!(parent_dir(Path::new("index")), Path::new("."));
1344 assert_eq!(parent_dir(&path), dir.path());
1345 }
1346}