1use std::path::{Path, PathBuf};
4
5use xxhash_rust::xxh3::xxh3_64;
6
7use crate::graph::GraphParams;
8use crate::graph::block::{
9 GraphNodeBlockBuild, GraphNodeBlockInput, GraphNodeError, GraphNodeLayout,
10 NODE_BLOCK_TRAILER_LEN, encode_node_blocks_with_refinement_passes,
11};
12use crate::lifecycle::QueryCancellation;
13use crate::lifecycle::durability::{DurabilityPolicy, SyncRequirement};
14use crate::lifecycle::stats::{AccountedCounter, Accounting, AllocationComponent};
15use crate::lifecycle::{QueryControl, SnapshotLease, Store, StoreError};
16use crate::quant::{Bit4Factors, QuantError, est_dot_bit4, prepare_bit4_query};
17use crate::scan::ScanError;
18use crate::segment::layout::RegionKind;
19use crate::segment::reader::SegmentReader;
20use crate::segment::writer::{
21 SegmentBuild, SegmentCopiedRegion, SegmentDocumentVersions, SegmentFactors,
22 write_segment_with_encoded_graph_and_copied_regions,
23};
24use crate::segment::{SegmentError, SegmentId, SegmentMeta};
25use crate::vfs::Vfs;
26
27const ENTRY_POINT_COUNT: usize = 4;
28const MEDOID_SAMPLE_ROWS: usize = 1_000;
29const CHECKPOINT_MAGIC: [u8; 8] = *b"ZEVAMCP1";
30const CHECKPOINT_VERSION: u16 = 1;
31const CHECKPOINT_HEADER_BYTES: usize = 80;
32const CHECKPOINT_CHECKSUM_BYTES: usize = 8;
33const CANCELLATION_CHECK_ROWS: usize = 64;
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub(crate) enum GraphRewriteRegion {
38 Reencoded,
40 Replaced,
42 CopyForward,
44 Unsupported,
46}
47
48pub(crate) const fn graph_rewrite_source_region(kind: u16) -> GraphRewriteRegion {
50 match RegionKind::from_id(kind) {
51 Some(
52 RegionKind::Columns
53 | RegionKind::Alive
54 | RegionKind::VectorCodes
55 | RegionKind::VectorFactors
56 | RegionKind::VectorRescore
57 | RegionKind::DocumentVersions,
58 ) => GraphRewriteRegion::Reencoded,
59 Some(RegionKind::GraphNodeBlocks | RegionKind::ChecksumTable) => {
60 GraphRewriteRegion::Replaced
61 }
62 Some(RegionKind::Postings | RegionKind::StoredMetadata | RegionKind::StoredText) | None => {
63 GraphRewriteRegion::CopyForward
64 }
65 Some(
66 RegionKind::GraphColocatedCodes
67 | RegionKind::SignPlane
68 | RegionKind::PdxClusteredBlocks
69 | RegionKind::VectorSpaceN,
70 ) => GraphRewriteRegion::Unsupported,
71 }
72}
73
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76pub enum GraphBuildPasses {
77 #[default]
80 One,
81 Two,
83}
84
85pub struct GraphBuildArtifact {
87 encoded_region: Vec<u8>,
88 entry_points: Vec<u32>,
89 work_rows_completed: u64,
90 _memory: Option<AccountedCounter>,
91}
92
93impl GraphBuildArtifact {
94 #[must_use]
96 pub fn encoded_region(&self) -> &[u8] {
97 &self.encoded_region
98 }
99
100 #[must_use]
102 pub fn entry_points(&self) -> &[u32] {
103 &self.entry_points
104 }
105
106 #[must_use]
108 pub const fn work_rows_completed(&self) -> u64 {
109 self.work_rows_completed
110 }
111
112 pub fn write_segment_with_graph(
114 &self,
115 vfs: &dyn Vfs,
116 directory: &Path,
117 input: &SegmentReader,
118 output_id: SegmentId,
119 policy: DurabilityPolicy,
120 ) -> Result<SegmentMeta, GraphBuildError> {
121 write_segment_with_encoded_graph(
122 &self.encoded_region,
123 vfs,
124 directory,
125 input,
126 output_id,
127 policy,
128 )
129 }
130}
131
132pub(crate) fn write_segment_with_encoded_graph(
133 encoded_region: &[u8],
134 vfs: &dyn Vfs,
135 directory: &Path,
136 input: &SegmentReader,
137 output_id: SegmentId,
138 policy: DurabilityPolicy,
139) -> Result<SegmentMeta, GraphBuildError> {
140 let columns = input.columns()?;
141 let alive = input.alive()?;
142 let factors = input.bit4_factors()?;
143 let mut doc_ids = Vec::with_capacity(input.meta().row_count as usize);
144 let mut revisions = Vec::with_capacity(input.meta().row_count as usize);
145 let mut has_documents = None;
146 for row in 0..input.meta().row_count as usize {
147 let document = input.document_version(row)?;
148 let present = document.is_some();
149 if has_documents.is_some_and(|expected| expected != present) {
150 return Err(GraphBuildError::Segment(SegmentError::Geometry(
151 "document-version region is present for only part of the segment".to_owned(),
152 )));
153 }
154 has_documents = Some(present);
155 if let Some(document) = document {
156 doc_ids.push(document.doc_id());
157 revisions.push(document.revision());
158 }
159 }
160 let build = SegmentBuild {
161 id: output_id,
162 scheme: input.meta().scheme,
163 dims: input.meta().dims,
164 codes: input.bit4_codes()?,
165 factors: SegmentFactors::Bit4(factors),
166 rescore: input.rescore_f32()?,
167 columns: &columns,
168 alive: &alive,
169 };
170 let documents = (has_documents == Some(true)).then_some(SegmentDocumentVersions {
171 doc_ids: &doc_ids,
172 revisions: &revisions,
173 });
174 let mut copied_regions = Vec::new();
175 for entry in input.directory() {
176 match graph_rewrite_source_region(entry.kind) {
177 GraphRewriteRegion::CopyForward => {
178 copied_regions.push(SegmentCopiedRegion {
179 kind: entry.kind,
180 version: entry.version,
181 bytes: input.region_by_id(entry.kind)?,
182 });
183 }
184 GraphRewriteRegion::Unsupported => {
185 return Err(GraphBuildError::Segment(SegmentError::Geometry(format!(
186 "graph rewrite cannot carry source region kind {}",
187 entry.kind
188 ))));
189 }
190 GraphRewriteRegion::Reencoded | GraphRewriteRegion::Replaced => {}
191 }
192 }
193 write_segment_with_encoded_graph_and_copied_regions(
194 vfs,
195 directory,
196 build,
197 encoded_region,
198 documents,
199 &copied_regions,
200 policy,
201 )
202 .map_err(GraphBuildError::Segment)
203}
204
205#[derive(Debug)]
207pub enum GraphBuildError {
208 Profile(crate::graph::search::GraphProfileError),
210 CheckpointIo {
212 path: PathBuf,
214 source: std::io::Error,
216 },
217 CheckpointCorrupt(String),
219 BudgetExhausted {
221 rows_completed: u64,
223 },
224 UnitNorm {
226 row_id: u32,
228 squared_norm: f64,
230 tolerance: f64,
232 },
233 Cancelled {
235 partial: bool,
237 },
238 Timeout {
240 partial: bool,
242 },
243 ReadCancelled {
245 partial: bool,
247 },
248 Segment(SegmentError),
250 Quant(QuantError),
252 NodeBlock(GraphNodeError),
254 Geometry(String),
256 NodeIdOutOfRange {
258 node_id: u32,
260 node_count: u32,
262 },
263 Store(StoreError),
265}
266
267impl std::fmt::Display for GraphBuildError {
268 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 match self {
270 Self::Profile(error) => write!(formatter, "graph build {error}"),
271 Self::CheckpointIo { path, source } => {
272 write!(
273 formatter,
274 "graph build checkpoint I/O {}: {source}",
275 path.display()
276 )
277 }
278 Self::CheckpointCorrupt(detail) => {
279 write!(formatter, "graph build checkpoint is corrupt: {detail}")
280 }
281 Self::BudgetExhausted { rows_completed } => write!(
282 formatter,
283 "graph build work budget exhausted after {rows_completed} checkpointed rows"
284 ),
285 Self::UnitNorm {
286 row_id,
287 squared_norm,
288 tolerance,
289 } => write!(
290 formatter,
291 "angular graph row {row_id} has squared norm {squared_norm}; expected 1 within {tolerance}"
292 ),
293 Self::Cancelled { partial } => {
294 write!(formatter, "graph build was cancelled (partial={partial})")
295 }
296 Self::Timeout { partial } => {
297 write!(
298 formatter,
299 "graph build deadline expired (partial={partial})"
300 )
301 }
302 Self::ReadCancelled { partial } => write!(
303 formatter,
304 "store close cancelled graph build (partial={partial})"
305 ),
306 Self::Segment(error) => write!(formatter, "graph build segment input: {error}"),
307 Self::Quant(error) => write!(formatter, "graph build Bit4 score: {error}"),
308 Self::NodeBlock(error) => write!(formatter, "graph build node blocks: {error}"),
309 Self::Geometry(detail) => write!(formatter, "graph build geometry: {detail}"),
310 Self::NodeIdOutOfRange {
311 node_id,
312 node_count,
313 } => write!(
314 formatter,
315 "graph build node id {node_id} is outside dense row count {node_count}"
316 ),
317 Self::Store(error) => write!(formatter, "graph build memory accounting: {error}"),
318 }
319 }
320}
321
322impl std::error::Error for GraphBuildError {
323 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
324 match self {
325 Self::Profile(error) => Some(error),
326 Self::CheckpointIo { source, .. } => Some(source),
327 Self::Segment(error) => Some(error),
328 Self::Quant(error) => Some(error),
329 Self::NodeBlock(error) => Some(error),
330 Self::Store(error) => Some(error),
331 Self::CheckpointCorrupt(_)
332 | Self::BudgetExhausted { .. }
333 | Self::UnitNorm { .. }
334 | Self::Cancelled { .. }
335 | Self::Timeout { .. }
336 | Self::ReadCancelled { .. }
337 | Self::Geometry(_)
338 | Self::NodeIdOutOfRange { .. } => None,
339 }
340 }
341}
342
343struct GraphBuildSession<'a> {
344 vfs: &'a dyn Vfs,
345 policy: DurabilityPolicy,
346 vectors: SegmentVectors<'a>,
347 params: GraphParams,
348 seed: u64,
349 passes: GraphBuildPasses,
350 checkpoint_path: PathBuf,
351 state: BuildState,
352}
353
354impl<'a> GraphBuildSession<'a> {
355 #[allow(clippy::too_many_arguments)]
356 fn new(
357 vfs: &'a dyn Vfs,
358 reader: &'a SegmentReader,
359 params: GraphParams,
360 seed: u64,
361 passes: GraphBuildPasses,
362 checkpoint_path: &Path,
363 policy: DurabilityPolicy,
364 accounting: Option<&std::sync::Arc<Accounting>>,
365 ) -> Result<Self, GraphBuildError> {
366 let vectors = SegmentVectors::new(reader)?;
367 let state = match vfs.read(checkpoint_path) {
368 Ok(bytes) => {
369 match decode_checkpoint(&bytes, &vectors, params, seed, passes, accounting) {
370 Ok(state) => state,
371 Err(error @ GraphBuildError::CheckpointCorrupt(_)) => {
372 remove_checkpoint(vfs, checkpoint_path)?;
373 return Err(error);
374 }
375 Err(other) => return Err(other),
376 }
377 }
378 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
379 BuildState::new(&vectors, params, seed, accounting)?
380 }
381 Err(source) => {
382 return Err(GraphBuildError::CheckpointIo {
383 path: checkpoint_path.to_path_buf(),
384 source,
385 });
386 }
387 };
388 Ok(Self {
389 vfs,
390 policy,
391 vectors,
392 params,
393 seed,
394 passes,
395 checkpoint_path: checkpoint_path.to_path_buf(),
396 state,
397 })
398 }
399
400 fn advance_batch(
401 &mut self,
402 cancellation: &QueryCancellation<'_>,
403 ) -> Result<bool, GraphBuildError> {
404 check_cancellation(cancellation)?;
405 let complete = self.state.advance_batch(
406 &self.vectors,
407 self.params,
408 self.seed,
409 self.passes,
410 Some(cancellation),
411 )?;
412 check_cancellation(cancellation)?;
413 {
414 let checkpoint_bytes = checkpoint_encoded_bytes(&self.state)?;
415 let base_charge = match self.state.memory.as_mut() {
416 Some(charge) => {
417 let base = charge.bytes();
418 let peak = usize::try_from(base)
419 .ok()
420 .and_then(|base| base.checked_add(checkpoint_bytes))
421 .ok_or_else(|| {
422 GraphBuildError::Geometry("checkpoint accounting overflow".to_owned())
423 })?;
424 charge.set(peak)?;
425 Some(base)
426 }
427 None => None,
428 };
429 let written = write_checkpoint(
430 self.vfs,
431 self.policy,
432 &self.checkpoint_path,
433 &self.vectors,
434 self.params,
435 self.seed,
436 self.passes,
437 &self.state,
438 );
439 if let (Some(charge), Some(base)) = (self.state.memory.as_mut(), base_charge) {
440 let base = usize::try_from(base).map_err(|_| {
441 GraphBuildError::Geometry("base accounting exceeds usize".to_owned())
442 })?;
443 charge.set(base)?;
444 }
445 written?;
446 }
447 Ok(complete)
448 }
449
450 fn completed_work_rows(&self) -> Result<u64, GraphBuildError> {
451 let rows = u64::try_from(self.state.order.len())
452 .map_err(|_| GraphBuildError::Geometry("graph row count exceeds u64".to_owned()))?;
453 let next = u64::try_from(self.state.next_index)
454 .map_err(|_| GraphBuildError::Geometry("graph progress exceeds u64".to_owned()))?;
455 match self.state.phase {
456 BuildPhase::Build => Ok(next),
457 BuildPhase::Refine => rows
458 .checked_add(next)
459 .ok_or_else(|| GraphBuildError::Geometry("graph progress overflow".to_owned())),
460 BuildPhase::Complete => match self.passes {
461 GraphBuildPasses::One => Ok(rows),
462 GraphBuildPasses::Two => rows
463 .checked_mul(2)
464 .ok_or_else(|| GraphBuildError::Geometry("graph progress overflow".to_owned())),
465 },
466 }
467 }
468
469 fn artifact(self) -> Result<GraphBuildArtifact, GraphBuildError> {
470 if !self.state.is_complete(self.passes) {
471 return Err(GraphBuildError::Geometry(
472 "graph artifact requested before every pass completed".to_owned(),
473 ));
474 }
475 let BuildState {
476 entries,
477 adjacency,
478 memory,
479 ..
480 } = self.state;
481 let artifact = encode_artifact(&self.vectors, self.params, &entries, &adjacency, memory)?;
482 remove_checkpoint(self.vfs, &self.checkpoint_path)?;
483 Ok(artifact)
484 }
485}
486
487impl From<SegmentError> for GraphBuildError {
488 fn from(error: SegmentError) -> Self {
489 Self::Segment(error)
490 }
491}
492
493impl From<QuantError> for GraphBuildError {
494 fn from(error: QuantError) -> Self {
495 Self::Quant(error)
496 }
497}
498
499impl From<GraphNodeError> for GraphBuildError {
500 fn from(error: GraphNodeError) -> Self {
501 Self::NodeBlock(error)
502 }
503}
504
505impl From<StoreError> for GraphBuildError {
506 fn from(error: StoreError) -> Self {
507 Self::Store(error)
508 }
509}
510
511#[cfg(test)]
512fn build_graph(
513 reader: &SegmentReader,
514 params: GraphParams,
515 seed: u64,
516 passes: GraphBuildPasses,
517) -> Result<GraphBuildArtifact, GraphBuildError> {
518 let vectors = SegmentVectors::new(reader)?;
519 let mut state = BuildState::new(&vectors, params, seed, None)?;
520 while !state.advance_batch(&vectors, params, seed, passes, None)? {}
521 let BuildState {
522 entries,
523 adjacency,
524 memory,
525 ..
526 } = state;
527 encode_artifact(&vectors, params, &entries, &adjacency, memory)
528}
529
530#[derive(Clone, Copy, Debug)]
532pub struct CheckpointedGraphBuild<'a> {
533 params: GraphParams,
534 seed: u64,
535 passes: GraphBuildPasses,
536 checkpoint_path: &'a Path,
537 control: &'a QueryControl,
538 max_work_rows: Option<u64>,
539}
540
541impl<'a> CheckpointedGraphBuild<'a> {
542 #[must_use]
544 pub fn new(
545 params: GraphParams,
546 seed: u64,
547 passes: GraphBuildPasses,
548 checkpoint_path: &'a Path,
549 control: &'a QueryControl,
550 ) -> Self {
551 Self {
552 params,
553 seed,
554 passes,
555 checkpoint_path,
556 control,
557 max_work_rows: None,
558 }
559 }
560
561 #[must_use]
565 pub const fn with_max_work_rows(mut self, max_work_rows: u64) -> Self {
566 self.max_work_rows = Some(max_work_rows);
567 self
568 }
569}
570
571pub fn build_graph_checkpointed(
578 store: &Store,
579 reader: &SegmentReader,
580 request: CheckpointedGraphBuild<'_>,
581 lease: &SnapshotLease,
582) -> Result<GraphBuildArtifact, GraphBuildError> {
583 let cancellation = QueryCancellation::new(request.control, lease);
584 check_cancellation(&cancellation)?;
585 if lease.graph_profile() == Ok(crate::graph::search::EpochGraphProfile::AngularClass) {
586 validate_angular_rows(reader)?;
587 }
588 let mut session = GraphBuildSession::new(
589 store.vfs.as_ref(),
590 reader,
591 request.params,
592 request.seed,
593 request.passes,
594 request.checkpoint_path,
595 store.durability_policy,
596 Some(&store.accounting),
597 )?;
598 let started_rows = session.completed_work_rows()?;
599 loop {
600 let complete = session.advance_batch(&cancellation)?;
601 let completed_rows = session
602 .completed_work_rows()?
603 .checked_sub(started_rows)
604 .ok_or_else(|| GraphBuildError::Geometry("graph work progress regressed".to_owned()))?;
605 if complete {
606 let mut artifact = session.artifact()?;
607 artifact.work_rows_completed = completed_rows;
608 return Ok(artifact);
609 }
610 if request
611 .max_work_rows
612 .is_some_and(|maximum| completed_rows >= maximum)
613 {
614 return Err(GraphBuildError::BudgetExhausted {
615 rows_completed: completed_rows,
616 });
617 }
618 }
619}
620
621fn validate_angular_rows(reader: &SegmentReader) -> Result<(), GraphBuildError> {
622 let dimensions = reader.meta().dims as usize;
623 for (row, vector) in reader.rescore_f32()?.chunks_exact(dimensions).enumerate() {
624 if let Some(squared_norm) = crate::graph::search::non_unit_squared_norm(vector) {
625 return Err(GraphBuildError::UnitNorm {
626 row_id: u32::try_from(row).map_err(|_| {
627 GraphBuildError::Geometry("angular row id exceeds u32".to_owned())
628 })?,
629 squared_norm,
630 tolerance: crate::graph::search::UNIT_NORM_SQUARED_TOLERANCE,
631 });
632 }
633 }
634 Ok(())
635}
636
637struct SegmentVectors<'a> {
638 segment_id: [u8; 16],
639 node_count: u32,
640 dimensions: usize,
641 code_stride: usize,
642 codes: &'a [u8],
643 factors: &'a [Bit4Factors],
644 rescore: &'a [f32],
645}
646
647impl<'a> SegmentVectors<'a> {
648 #[cfg(test)]
649 fn from_native(
650 dimensions: usize,
651 codes: &'a [u8],
652 factors: &'a [Bit4Factors],
653 rescore: &'a [f32],
654 ) -> Result<Self, GraphBuildError> {
655 if dimensions == 0 || factors.is_empty() {
656 return Err(GraphBuildError::Geometry(
657 "native flat Vamana requires dimensions and rows".to_owned(),
658 ));
659 }
660 let code_stride = dimensions.div_ceil(2);
661 let expected_codes = factors
662 .len()
663 .checked_mul(code_stride)
664 .ok_or_else(|| GraphBuildError::Geometry("native code length overflow".to_owned()))?;
665 let expected_rescore = factors.len().checked_mul(dimensions).ok_or_else(|| {
666 GraphBuildError::Geometry("native rescore length overflow".to_owned())
667 })?;
668 if codes.len() != expected_codes || rescore.len() != expected_rescore {
669 return Err(GraphBuildError::Geometry(
670 "native flat Vamana vector geometry".to_owned(),
671 ));
672 }
673 Ok(Self {
674 segment_id: [0; 16],
675 node_count: u32::try_from(factors.len()).map_err(|_| {
676 GraphBuildError::Geometry("native row count exceeds u32".to_owned())
677 })?,
678 dimensions,
679 code_stride,
680 codes,
681 factors,
682 rescore,
683 })
684 }
685
686 fn new(reader: &'a SegmentReader) -> Result<Self, GraphBuildError> {
687 if reader.meta().scheme != 4 {
688 return Err(GraphBuildError::Geometry(format!(
689 "flat Vamana requires Bit4 scheme 4, got {}",
690 reader.meta().scheme
691 )));
692 }
693 let dimensions = reader.meta().dims as usize;
694 let node_count = reader.meta().row_count;
695 if node_count == 0 {
696 return Err(GraphBuildError::Geometry(
697 "flat Vamana requires at least one row".to_owned(),
698 ));
699 }
700 Ok(Self {
701 segment_id: *reader.meta().id.as_bytes(),
702 node_count,
703 dimensions,
704 code_stride: dimensions.div_ceil(2),
705 codes: reader.bit4_codes()?,
706 factors: reader.bit4_factors()?,
707 rescore: reader.rescore_f32()?,
708 })
709 }
710
711 fn validate_node(&self, node_id: u32) -> Result<usize, GraphBuildError> {
712 if node_id >= self.node_count {
713 return Err(GraphBuildError::NodeIdOutOfRange {
714 node_id,
715 node_count: self.node_count,
716 });
717 }
718 usize::try_from(node_id)
719 .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))
720 }
721
722 fn f32_row(&self, node_id: u32) -> Result<&'a [f32], GraphBuildError> {
723 let node = self.validate_node(node_id)?;
724 let start = node
725 .checked_mul(self.dimensions)
726 .ok_or_else(|| GraphBuildError::Geometry("f32 row offset overflow".to_owned()))?;
727 let end = start
728 .checked_add(self.dimensions)
729 .ok_or_else(|| GraphBuildError::Geometry("f32 row end overflow".to_owned()))?;
730 self.rescore.get(start..end).ok_or_else(|| {
731 GraphBuildError::Geometry(format!("f32 row {node_id} is outside validated rescore"))
732 })
733 }
734
735 fn code_row(&self, node_id: u32) -> Result<&'a [u8], GraphBuildError> {
736 let node = self.validate_node(node_id)?;
737 let start = node
738 .checked_mul(self.code_stride)
739 .ok_or_else(|| GraphBuildError::Geometry("Bit4 row offset overflow".to_owned()))?;
740 let end = start
741 .checked_add(self.code_stride)
742 .ok_or_else(|| GraphBuildError::Geometry("Bit4 row end overflow".to_owned()))?;
743 self.codes.get(start..end).ok_or_else(|| {
744 GraphBuildError::Geometry(format!("Bit4 row {node_id} is outside validated codes"))
745 })
746 }
747
748 fn factor(&self, node_id: u32) -> Result<Bit4Factors, GraphBuildError> {
749 let node = self.validate_node(node_id)?;
750 self.factors.get(node).copied().ok_or_else(|| {
751 GraphBuildError::Geometry(format!("Bit4 factor {node_id} is unavailable"))
752 })
753 }
754}
755
756struct Adjacency {
757 slots: Vec<u32>,
758 degrees: Vec<u8>,
759 node_count: u32,
760 r_max: u8,
761}
762
763impl Adjacency {
764 fn new(node_count: u32, r_max: u8) -> Result<Self, GraphBuildError> {
765 let slot_count = (node_count as usize)
766 .checked_mul(usize::from(r_max))
767 .ok_or_else(|| GraphBuildError::Geometry("adjacency size overflow".to_owned()))?;
768 let mut slots = Vec::new();
769
770 let capacity = slot_count;
771 let requested = capacity
772 .checked_mul(std::mem::size_of::<u32>())
773 .ok_or_else(|| GraphBuildError::Geometry("adjacency allocation overflow".to_owned()))?;
774 #[cfg(feature = "allocation-audit")]
775 let allocation = crate::allocation_audit::attributed(|| slots.try_reserve_exact(capacity));
776 #[cfg(not(feature = "allocation-audit"))]
777 let allocation = slots.try_reserve_exact(capacity);
778 allocation.map_err(|_| {
779 GraphBuildError::Store(StoreError::AllocationFailed {
780 needed: requested as u64,
781 component: "adjacency",
782 })
783 })?;
784 slots.resize(slot_count, u32::MAX);
785 let mut degrees = Vec::new();
786
787 let capacity = node_count as usize;
788 let requested = capacity
789 .checked_mul(std::mem::size_of::<u8>())
790 .ok_or_else(|| GraphBuildError::Geometry("degree allocation overflow".to_owned()))?;
791 #[cfg(feature = "allocation-audit")]
792 let allocation =
793 crate::allocation_audit::attributed(|| degrees.try_reserve_exact(capacity));
794 #[cfg(not(feature = "allocation-audit"))]
795 let allocation = degrees.try_reserve_exact(capacity);
796 allocation.map_err(|_| {
797 GraphBuildError::Store(StoreError::AllocationFailed {
798 needed: requested as u64,
799 component: "degree",
800 })
801 })?;
802 degrees.resize(node_count as usize, 0);
803 Ok(Self {
804 slots,
805 degrees,
806 node_count,
807 r_max,
808 })
809 }
810
811 fn row_range(&self, node_id: u32) -> Result<std::ops::Range<usize>, GraphBuildError> {
812 if node_id >= self.node_count {
813 return Err(GraphBuildError::NodeIdOutOfRange {
814 node_id,
815 node_count: self.node_count,
816 });
817 }
818 let start = (node_id as usize)
819 .checked_mul(usize::from(self.r_max))
820 .ok_or_else(|| GraphBuildError::Geometry("adjacency offset overflow".to_owned()))?;
821 let end = start
822 .checked_add(usize::from(self.r_max))
823 .ok_or_else(|| GraphBuildError::Geometry("adjacency end overflow".to_owned()))?;
824 Ok(start..end)
825 }
826
827 fn neighbors(&self, node_id: u32) -> Result<&[u32], GraphBuildError> {
828 let node = usize::try_from(node_id)
829 .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
830 let degree = usize::from(*self.degrees.get(node).ok_or(
831 GraphBuildError::NodeIdOutOfRange {
832 node_id,
833 node_count: self.node_count,
834 },
835 )?);
836 let range = self.row_range(node_id)?;
837 let start = range.start;
838 let active_end = start
839 .checked_add(degree)
840 .ok_or_else(|| GraphBuildError::Geometry("degree end overflow".to_owned()))?;
841 self.slots.get(start..active_end).ok_or_else(|| {
842 GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
843 })
844 }
845
846 fn set_neighbors(&mut self, node_id: u32, neighbors: &[u32]) -> Result<(), GraphBuildError> {
847 if neighbors.len() > usize::from(self.r_max) {
848 return Err(GraphBuildError::Geometry(format!(
849 "node {node_id} degree {} exceeds hard cap {}",
850 neighbors.len(),
851 self.r_max
852 )));
853 }
854 for neighbor in neighbors {
855 if *neighbor >= self.node_count {
856 return Err(GraphBuildError::NodeIdOutOfRange {
857 node_id: *neighbor,
858 node_count: self.node_count,
859 });
860 }
861 }
862 let range = self.row_range(node_id)?;
863 let row = self.slots.get_mut(range).ok_or_else(|| {
864 GraphBuildError::Geometry(format!("adjacency row {node_id} is unavailable"))
865 })?;
866 row.fill(u32::MAX);
867 let destination = row.get_mut(..neighbors.len()).ok_or_else(|| {
868 GraphBuildError::Geometry(format!("adjacency row {node_id} active slice failed"))
869 })?;
870 destination.copy_from_slice(neighbors);
871 let node = usize::try_from(node_id)
872 .map_err(|_| GraphBuildError::Geometry("node id exceeds usize".to_owned()))?;
873 let degree = self
874 .degrees
875 .get_mut(node)
876 .ok_or(GraphBuildError::NodeIdOutOfRange {
877 node_id,
878 node_count: self.node_count,
879 })?;
880 *degree = u8::try_from(neighbors.len())
881 .map_err(|_| GraphBuildError::Geometry("degree exceeds u8".to_owned()))?;
882 Ok(())
883 }
884}
885
886#[derive(Clone, Copy, Debug, Eq, PartialEq)]
887enum BuildPhase {
888 Build,
889 Refine,
890 Complete,
891}
892
893fn build_arena_bytes(node_count: u32, params: GraphParams) -> Result<usize, GraphBuildError> {
894 let rows = usize::try_from(node_count)
895 .map_err(|_| GraphBuildError::Geometry("node count exceeds usize".to_owned()))?;
896 let slots = rows
897 .checked_mul(usize::from(params.r_max()))
898 .and_then(|count| count.checked_mul(std::mem::size_of::<u32>()))
899 .ok_or_else(|| GraphBuildError::Geometry("accounted adjacency overflow".to_owned()))?;
900 let row_state = rows
901 .checked_mul(
902 std::mem::size_of::<u32>()
903 .checked_mul(2)
904 .and_then(|bytes| bytes.checked_add(2))
905 .ok_or_else(|| GraphBuildError::Geometry("row state width overflow".to_owned()))?,
906 )
907 .ok_or_else(|| GraphBuildError::Geometry("accounted row state overflow".to_owned()))?;
908 let construction = usize::from(params.l_build());
909 let scratch_per_candidate = std::mem::size_of::<ScoredNode>()
910 .checked_mul(4)
911 .and_then(|bytes| bytes.checked_add(std::mem::size_of::<u32>() * 6))
912 .ok_or_else(|| GraphBuildError::Geometry("scratch width overflow".to_owned()))?;
913 let candidate_scratch = construction
914 .checked_add(usize::from(params.r_max()))
915 .and_then(|candidates| candidates.checked_mul(scratch_per_candidate))
916 .ok_or_else(|| GraphBuildError::Geometry("scratch arena overflow".to_owned()))?;
917 let medoid_scratch = MEDOID_SAMPLE_ROWS
918 .checked_mul(std::mem::size_of::<ScoredNode>())
919 .ok_or_else(|| GraphBuildError::Geometry("medoid scratch overflow".to_owned()))?;
920 let scratch = candidate_scratch.max(medoid_scratch);
921 slots
922 .checked_add(row_state)
923 .and_then(|bytes| bytes.checked_add(ENTRY_POINT_COUNT * std::mem::size_of::<u32>()))
924 .and_then(|bytes| bytes.checked_add(scratch))
925 .ok_or_else(|| GraphBuildError::Geometry("graph build arena overflow".to_owned()))
926}
927
928struct BuildState {
929 phase: BuildPhase,
930 next_index: usize,
931 order: Vec<u32>,
932 entries: Vec<u32>,
933 adjacency: Adjacency,
934 inserted: Vec<u8>,
935 visited: Vec<u32>,
936 visit_epoch: u32,
937 memory: Option<AccountedCounter>,
938}
939
940impl BuildState {
941 fn new(
942 vectors: &SegmentVectors<'_>,
943 params: GraphParams,
944 seed: u64,
945 accounting: Option<&std::sync::Arc<Accounting>>,
946 ) -> Result<Self, GraphBuildError> {
947 let mut memory = match accounting {
948 Some(accounting) => Some(AccountedCounter::new(
949 accounting,
950 AllocationComponent::Temporary,
951 )?),
952 None => None,
953 };
954 if let Some(charge) = memory.as_mut() {
955 charge.set(build_arena_bytes(vectors.node_count, params)?)?;
956 }
957 let mut random = SplitMix64::new(seed);
958 let mut order = Vec::new();
959
960 let capacity = vectors.node_count as usize;
961 let requested = capacity
962 .checked_mul(std::mem::size_of::<u32>())
963 .ok_or_else(|| {
964 GraphBuildError::Geometry("build order allocation overflow".to_owned())
965 })?;
966 #[cfg(feature = "allocation-audit")]
967 let allocation = crate::allocation_audit::attributed(|| order.try_reserve_exact(capacity));
968 #[cfg(not(feature = "allocation-audit"))]
969 let allocation = order.try_reserve_exact(capacity);
970 allocation.map_err(|_| {
971 GraphBuildError::Store(StoreError::AllocationFailed {
972 needed: requested as u64,
973 component: "build order",
974 })
975 })?;
976 for node in 0..vectors.node_count {
977 order.push(node);
978 }
979 shuffle(&mut order, &mut random)?;
980 let entries = refined_entry_points(vectors, &order)?;
981 move_entries_to_front(&mut order, &entries)?;
982 let mut inserted = Vec::new();
983
984 let capacity = vectors.node_count as usize;
985 let requested = capacity
986 .checked_mul(std::mem::size_of::<u8>())
987 .ok_or_else(|| GraphBuildError::Geometry("inserted allocation overflow".to_owned()))?;
988 #[cfg(feature = "allocation-audit")]
989 let allocation =
990 crate::allocation_audit::attributed(|| inserted.try_reserve_exact(capacity));
991 #[cfg(not(feature = "allocation-audit"))]
992 let allocation = inserted.try_reserve_exact(capacity);
993 allocation.map_err(|_| {
994 GraphBuildError::Store(StoreError::AllocationFailed {
995 needed: requested as u64,
996 component: "inserted",
997 })
998 })?;
999 inserted.resize(vectors.node_count as usize, 0);
1000 let mut visited = Vec::new();
1001
1002 let capacity = vectors.node_count as usize;
1003 let requested = capacity
1004 .checked_mul(std::mem::size_of::<u32>())
1005 .ok_or_else(|| GraphBuildError::Geometry("visited allocation overflow".to_owned()))?;
1006 #[cfg(feature = "allocation-audit")]
1007 let allocation =
1008 crate::allocation_audit::attributed(|| visited.try_reserve_exact(capacity));
1009 #[cfg(not(feature = "allocation-audit"))]
1010 let allocation = visited.try_reserve_exact(capacity);
1011 allocation.map_err(|_| {
1012 GraphBuildError::Store(StoreError::AllocationFailed {
1013 needed: requested as u64,
1014 component: "visited",
1015 })
1016 })?;
1017 visited.resize(vectors.node_count as usize, 0);
1018 Ok(Self {
1019 phase: BuildPhase::Build,
1020 next_index: 0,
1021 order,
1022 entries,
1023 adjacency: Adjacency::new(vectors.node_count, params.r_max())?,
1024 inserted,
1025 visited,
1026 visit_epoch: 0,
1027 memory,
1028 })
1029 }
1030
1031 fn is_complete(&self, _passes: GraphBuildPasses) -> bool {
1032 self.phase == BuildPhase::Complete
1033 }
1034
1035 fn advance_batch(
1036 &mut self,
1037 vectors: &SegmentVectors<'_>,
1038 params: GraphParams,
1039 seed: u64,
1040 passes: GraphBuildPasses,
1041 cancellation: Option<&QueryCancellation<'_>>,
1042 ) -> Result<bool, GraphBuildError> {
1043 if self.phase == BuildPhase::Complete {
1044 return Ok(true);
1045 }
1046 let batch_rows = usize::try_from(params.checkpoint_batch_rows()).map_err(|_| {
1047 GraphBuildError::Geometry("checkpoint batch rows exceed usize".to_owned())
1048 })?;
1049 let end = self
1050 .next_index
1051 .checked_add(batch_rows)
1052 .map(|end| end.min(self.order.len()))
1053 .ok_or_else(|| GraphBuildError::Geometry("batch end overflow".to_owned()))?;
1054 let alpha = match self.phase {
1055 BuildPhase::Build => params.alpha_build(),
1056 BuildPhase::Refine => params.alpha_refine(),
1057 BuildPhase::Complete => return Ok(true),
1058 };
1059 let pass_seed = match self.phase {
1060 BuildPhase::Build => seed,
1061 BuildPhase::Refine => seed ^ 0xa1fa_1200_0000_0002,
1062 BuildPhase::Complete => seed,
1063 };
1064 for position in self.next_index..end {
1065 if position.is_multiple_of(CANCELLATION_CHECK_ROWS)
1066 && let Some(cancellation) = cancellation
1067 {
1068 check_cancellation(cancellation)?;
1069 }
1070 let order_position = match self.phase {
1071 BuildPhase::Build => position,
1072 BuildPhase::Refine => {
1073 self.order.len().checked_sub(position + 1).ok_or_else(|| {
1074 GraphBuildError::Geometry("refinement order underflow".to_owned())
1075 })?
1076 }
1077 BuildPhase::Complete => position,
1078 };
1079 let node_id = *self.order.get(order_position).ok_or_else(|| {
1080 GraphBuildError::Geometry(format!(
1081 "build order position {order_position} is unavailable"
1082 ))
1083 })?;
1084 process_node(
1085 vectors,
1086 params,
1087 pass_seed,
1088 alpha,
1089 node_id,
1090 &self.entries,
1091 &mut self.adjacency,
1092 &mut self.inserted,
1093 &mut self.visited,
1094 &mut self.visit_epoch,
1095 )?;
1096 }
1097 self.next_index = end;
1098 if self.next_index == self.order.len() {
1099 match (self.phase, passes) {
1100 (BuildPhase::Build, GraphBuildPasses::Two) => {
1101 self.phase = BuildPhase::Refine;
1102 self.next_index = 0;
1103 self.inserted.fill(1);
1104 }
1105 (BuildPhase::Build, GraphBuildPasses::One) | (BuildPhase::Refine, _) => {
1106 self.phase = BuildPhase::Complete;
1107 }
1108 (BuildPhase::Complete, _) => {}
1109 }
1110 }
1111 Ok(self.phase == BuildPhase::Complete)
1112 }
1113}
1114
1115fn check_cancellation(cancellation: &QueryCancellation<'_>) -> Result<(), GraphBuildError> {
1116 match cancellation.check_graph() {
1117 Ok(()) => Ok(()),
1118 Err(ScanError::Cancelled { .. }) => Err(GraphBuildError::Cancelled { partial: false }),
1119 Err(ScanError::Timeout { .. }) => Err(GraphBuildError::Timeout { partial: false }),
1120 Err(ScanError::ReadCancelled { .. }) => {
1121 Err(GraphBuildError::ReadCancelled { partial: false })
1122 }
1123 Err(error) => Err(GraphBuildError::Geometry(format!(
1124 "query cancellation seam returned non-cancellation error: {error}"
1125 ))),
1126 }
1127}
1128
1129fn checkpoint_temp_path(path: &Path) -> PathBuf {
1130 let mut temporary = path.as_os_str().to_os_string();
1131 temporary.push(".tmp");
1132 PathBuf::from(temporary)
1133}
1134
1135#[allow(clippy::too_many_arguments)]
1136fn write_checkpoint(
1137 vfs: &dyn Vfs,
1138 policy: DurabilityPolicy,
1139 path: &Path,
1140 vectors: &SegmentVectors<'_>,
1141 params: GraphParams,
1142 seed: u64,
1143 passes: GraphBuildPasses,
1144 state: &BuildState,
1145) -> Result<(), GraphBuildError> {
1146 let directory = path.parent().ok_or_else(|| GraphBuildError::CheckpointIo {
1147 path: path.to_path_buf(),
1148 source: std::io::Error::new(
1149 std::io::ErrorKind::InvalidInput,
1150 "checkpoint path has no parent directory",
1151 ),
1152 })?;
1153 let slot_bytes = state
1154 .adjacency
1155 .slots
1156 .len()
1157 .checked_mul(std::mem::size_of::<u32>())
1158 .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1159 let payload_bytes = state
1160 .adjacency
1161 .degrees
1162 .len()
1163 .checked_add(slot_bytes)
1164 .ok_or_else(|| GraphBuildError::Geometry("checkpoint payload overflow".to_owned()))?;
1165 let total_bytes = CHECKPOINT_HEADER_BYTES
1166 .checked_add(payload_bytes)
1167 .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1168 .ok_or_else(|| GraphBuildError::Geometry("checkpoint length overflow".to_owned()))?;
1169 let mut bytes = Vec::with_capacity(total_bytes);
1170 bytes.extend_from_slice(&CHECKPOINT_MAGIC);
1171 bytes.extend_from_slice(&CHECKPOINT_VERSION.to_le_bytes());
1172 bytes.push(phase_code(state.phase));
1173 bytes.push(pass_code(passes));
1174 bytes.push(params.r_target());
1175 bytes.push(params.r_max());
1176 bytes.extend_from_slice(&0_u16.to_le_bytes());
1177 bytes.extend_from_slice(¶ms.l_build().to_le_bytes());
1178 bytes.extend_from_slice(&0_u16.to_le_bytes());
1179 bytes.extend_from_slice(¶ms.checkpoint_batch_rows().to_le_bytes());
1180 bytes.extend_from_slice(&vectors.node_count.to_le_bytes());
1181 let dimensions = u32::try_from(vectors.dimensions)
1182 .map_err(|_| GraphBuildError::Geometry("checkpoint dimensions exceed u32".to_owned()))?;
1183 bytes.extend_from_slice(&dimensions.to_le_bytes());
1184 let next_index = u32::try_from(state.next_index)
1185 .map_err(|_| GraphBuildError::Geometry("checkpoint position exceeds u32".to_owned()))?;
1186 bytes.extend_from_slice(&next_index.to_le_bytes());
1187 bytes.extend_from_slice(&0_u32.to_le_bytes());
1188 bytes.extend_from_slice(&seed.to_le_bytes());
1189 bytes.extend_from_slice(¶ms.alpha_build().to_bits().to_le_bytes());
1190 bytes.extend_from_slice(¶ms.alpha_refine().to_bits().to_le_bytes());
1191 bytes.extend_from_slice(&vectors.segment_id);
1192 bytes.extend_from_slice(
1193 &u64::try_from(payload_bytes)
1194 .map_err(|_| GraphBuildError::Geometry("checkpoint payload exceeds u64".to_owned()))?
1195 .to_le_bytes(),
1196 );
1197 if bytes.len() != CHECKPOINT_HEADER_BYTES {
1198 return Err(GraphBuildError::Geometry(format!(
1199 "checkpoint header encoded {} bytes, expected {CHECKPOINT_HEADER_BYTES}",
1200 bytes.len()
1201 )));
1202 }
1203 bytes.extend_from_slice(&state.adjacency.degrees);
1204 for neighbor in &state.adjacency.slots {
1205 bytes.extend_from_slice(&neighbor.to_le_bytes());
1206 }
1207 bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
1208 let temporary = checkpoint_temp_path(path);
1209 if let Err(source) = vfs.write(&temporary, &bytes) {
1210 let _ = vfs.delete(&temporary);
1211 return Err(GraphBuildError::CheckpointIo {
1212 path: temporary,
1213 source,
1214 });
1215 }
1216 if let SyncRequirement::Sync(kind) = policy.data_file_sync()
1217 && let Err(source) = vfs.sync(&temporary, kind)
1218 {
1219 let _ = vfs.delete(&temporary);
1220 return Err(GraphBuildError::CheckpointIo {
1221 path: temporary,
1222 source,
1223 });
1224 }
1225 if let Err(source) = vfs.rename(&temporary, path) {
1226 let _ = vfs.delete(&temporary);
1227 return Err(GraphBuildError::CheckpointIo {
1228 path: path.to_path_buf(),
1229 source,
1230 });
1231 }
1232 match policy.directory_sync() {
1233 SyncRequirement::Skip => Ok(()),
1234 SyncRequirement::Sync(kind) => {
1235 vfs.sync(directory, kind)
1236 .map_err(|source| GraphBuildError::CheckpointIo {
1237 path: directory.to_path_buf(),
1238 source,
1239 })
1240 }
1241 }
1242}
1243
1244fn checkpoint_encoded_bytes(state: &BuildState) -> Result<usize, GraphBuildError> {
1245 let slot_bytes = state
1246 .adjacency
1247 .slots
1248 .len()
1249 .checked_mul(std::mem::size_of::<u32>())
1250 .ok_or_else(|| GraphBuildError::Geometry("checkpoint slot bytes overflow".to_owned()))?;
1251 CHECKPOINT_HEADER_BYTES
1252 .checked_add(state.adjacency.degrees.len())
1253 .and_then(|bytes| bytes.checked_add(slot_bytes))
1254 .and_then(|bytes| bytes.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1255 .ok_or_else(|| GraphBuildError::Geometry("checkpoint encoded bytes overflow".to_owned()))
1256}
1257
1258fn remove_checkpoint(vfs: &dyn Vfs, path: &Path) -> Result<(), GraphBuildError> {
1259 let temporary = checkpoint_temp_path(path);
1260 match vfs.delete(&temporary) {
1261 Ok(()) => {}
1262 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1263 Err(source) => {
1264 return Err(GraphBuildError::CheckpointIo {
1265 path: temporary,
1266 source,
1267 });
1268 }
1269 }
1270 match vfs.delete(path) {
1271 Ok(()) => Ok(()),
1272 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
1273 Err(source) => Err(GraphBuildError::CheckpointIo {
1274 path: path.to_path_buf(),
1275 source,
1276 }),
1277 }
1278}
1279
1280fn decode_checkpoint(
1281 bytes: &[u8],
1282 vectors: &SegmentVectors<'_>,
1283 params: GraphParams,
1284 seed: u64,
1285 passes: GraphBuildPasses,
1286 accounting: Option<&std::sync::Arc<Accounting>>,
1287) -> Result<BuildState, GraphBuildError> {
1288 validate_graph_build_checkpoint(bytes)?;
1289 let minimum = CHECKPOINT_HEADER_BYTES
1290 .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1291 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1292 if bytes.len() < minimum {
1293 return Err(GraphBuildError::CheckpointCorrupt(format!(
1294 "truncated: need at least {minimum} bytes, got {}",
1295 bytes.len()
1296 )));
1297 }
1298 if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1299 return Err(GraphBuildError::CheckpointCorrupt(
1300 "magic does not match ZEVAMCP1".to_owned(),
1301 ));
1302 }
1303 let checksum_start = bytes
1304 .len()
1305 .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1306 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1307 let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1308 let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1309 GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1310 })?;
1311 let actual_checksum = xxh3_64(checksummed);
1312 if stored_checksum != actual_checksum {
1313 return Err(GraphBuildError::CheckpointCorrupt(format!(
1314 "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1315 )));
1316 }
1317 let version = read_checkpoint_u16(bytes, 8, "version")?;
1318 if version != CHECKPOINT_VERSION {
1319 return Err(GraphBuildError::CheckpointCorrupt(format!(
1320 "version {version}, expected {CHECKPOINT_VERSION}"
1321 )));
1322 }
1323 let phase = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1324 let stored_passes = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1325 if stored_passes != passes {
1326 return Err(GraphBuildError::CheckpointCorrupt(
1327 "pass count does not match requested build".to_owned(),
1328 ));
1329 }
1330 let stored_r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1331 let stored_r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1332 let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1333 let stored_l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1334 let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1335 let stored_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1336 let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1337 let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1338 let next_index = read_checkpoint_u32(bytes, 32, "next index")?;
1339 let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved index bytes")?;
1340 let stored_seed = read_checkpoint_u64(bytes, 40, "seed")?;
1341 let stored_alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1342 let stored_alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1343 let segment_id = bytes
1344 .get(56..72)
1345 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("segment id is truncated".to_owned()))?;
1346 let payload_bytes = read_checkpoint_u64(bytes, 72, "payload length")?;
1347 if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1348 return Err(GraphBuildError::CheckpointCorrupt(
1349 "reserved header bytes are non-zero".to_owned(),
1350 ));
1351 }
1352 if stored_r_target != params.r_target()
1353 || stored_r_max != params.r_max()
1354 || stored_l_build != params.l_build()
1355 || stored_batch != params.checkpoint_batch_rows()
1356 || stored_alpha_build.to_bits() != params.alpha_build().to_bits()
1357 || stored_alpha_refine.to_bits() != params.alpha_refine().to_bits()
1358 || node_count != vectors.node_count
1359 || dimensions != vectors.dimensions as u32
1360 || stored_seed != seed
1361 || segment_id != vectors.segment_id
1362 {
1363 return Err(GraphBuildError::CheckpointCorrupt(
1364 "checkpoint identity or graph parameters do not match input".to_owned(),
1365 ));
1366 }
1367 if passes == GraphBuildPasses::One && phase == BuildPhase::Refine {
1368 return Err(GraphBuildError::CheckpointCorrupt(
1369 "one-pass build cannot resume refinement".to_owned(),
1370 ));
1371 }
1372 let actual_payload = checksum_start
1373 .checked_sub(CHECKPOINT_HEADER_BYTES)
1374 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("payload underflow".to_owned()))?;
1375 if usize::try_from(payload_bytes).ok() != Some(actual_payload) {
1376 return Err(GraphBuildError::CheckpointCorrupt(format!(
1377 "declared payload {payload_bytes} bytes, actual {actual_payload}"
1378 )));
1379 }
1380 let degree_bytes = node_count as usize;
1381 let degrees_end = CHECKPOINT_HEADER_BYTES
1382 .checked_add(degree_bytes)
1383 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree end overflow".to_owned()))?;
1384 let degrees = bytes
1385 .get(CHECKPOINT_HEADER_BYTES..degrees_end)
1386 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("degree bytes are truncated".to_owned()))?
1387 .to_vec();
1388 let expected_slots = (node_count as usize)
1389 .checked_mul(usize::from(params.r_max()))
1390 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1391 let slot_payload = bytes.get(degrees_end..checksum_start).ok_or_else(|| {
1392 GraphBuildError::CheckpointCorrupt("neighbor payload is unavailable".to_owned())
1393 })?;
1394 let expected_slot_bytes = expected_slots
1395 .checked_mul(std::mem::size_of::<u32>())
1396 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot bytes overflow".to_owned()))?;
1397 if slot_payload.len() != expected_slot_bytes {
1398 return Err(GraphBuildError::CheckpointCorrupt(format!(
1399 "neighbor payload {} bytes, expected {expected_slot_bytes}",
1400 slot_payload.len()
1401 )));
1402 }
1403 let slots = slot_payload
1404 .chunks_exact(4)
1405 .map(|chunk| {
1406 let raw: [u8; 4] = chunk.try_into().map_err(|_| {
1407 GraphBuildError::CheckpointCorrupt("neighbor width is not four bytes".to_owned())
1408 })?;
1409 Ok(u32::from_le_bytes(raw))
1410 })
1411 .collect::<Result<Vec<_>, GraphBuildError>>()?;
1412 let adjacency = Adjacency {
1413 slots,
1414 degrees,
1415 node_count,
1416 r_max: params.r_max(),
1417 };
1418 validate_checkpoint_adjacency(&adjacency)?;
1419 let mut state = BuildState::new(vectors, params, seed, accounting)?;
1420 state.phase = phase;
1421 state.next_index = usize::try_from(next_index)
1422 .map_err(|_| GraphBuildError::CheckpointCorrupt("next index exceeds usize".to_owned()))?;
1423 if state.next_index > state.order.len() {
1424 return Err(GraphBuildError::CheckpointCorrupt(format!(
1425 "next index {} exceeds row count {}",
1426 state.next_index,
1427 state.order.len()
1428 )));
1429 }
1430 state.adjacency = adjacency;
1431 state.inserted.fill(u8::from(phase != BuildPhase::Build));
1432 if phase == BuildPhase::Build {
1433 for node_id in state.order.iter().take(state.next_index) {
1434 let node = vectors.validate_node(*node_id)?;
1435 let marker = state.inserted.get_mut(node).ok_or_else(|| {
1436 GraphBuildError::CheckpointCorrupt("inserted marker is unavailable".to_owned())
1437 })?;
1438 *marker = 1;
1439 }
1440 }
1441 Ok(state)
1442}
1443
1444pub fn validate_graph_build_checkpoint(bytes: &[u8]) -> Result<(), GraphBuildError> {
1448 let minimum = CHECKPOINT_HEADER_BYTES
1449 .checked_add(CHECKPOINT_CHECKSUM_BYTES)
1450 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("minimum length overflow".to_owned()))?;
1451 if bytes.len() < minimum {
1452 return Err(GraphBuildError::CheckpointCorrupt(format!(
1453 "truncated: need at least {minimum} bytes, got {}",
1454 bytes.len()
1455 )));
1456 }
1457 if bytes.get(..8) != Some(CHECKPOINT_MAGIC.as_slice()) {
1458 return Err(GraphBuildError::CheckpointCorrupt(
1459 "magic does not match ZEVAMCP1".to_owned(),
1460 ));
1461 }
1462 let checksum_start = bytes
1463 .len()
1464 .checked_sub(CHECKPOINT_CHECKSUM_BYTES)
1465 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("checksum underflow".to_owned()))?;
1466 let stored_checksum = read_checkpoint_u64(bytes, checksum_start, "checksum")?;
1467 let checksummed = bytes.get(..checksum_start).ok_or_else(|| {
1468 GraphBuildError::CheckpointCorrupt("checksummed prefix is unavailable".to_owned())
1469 })?;
1470 let actual_checksum = xxh3_64(checksummed);
1471 if stored_checksum != actual_checksum {
1472 return Err(GraphBuildError::CheckpointCorrupt(format!(
1473 "xxh3-64 expected {stored_checksum:#018x}, computed {actual_checksum:#018x}"
1474 )));
1475 }
1476 let version = read_checkpoint_u16(bytes, 8, "version")?;
1477 if version != CHECKPOINT_VERSION {
1478 return Err(GraphBuildError::CheckpointCorrupt(format!(
1479 "version {version}, expected {CHECKPOINT_VERSION}"
1480 )));
1481 }
1482 let _ = decode_phase(read_checkpoint_u8(bytes, 10, "phase")?)?;
1483 let _ = decode_passes(read_checkpoint_u8(bytes, 11, "passes")?)?;
1484 let r_target = read_checkpoint_u8(bytes, 12, "target degree")?;
1485 let r_max = read_checkpoint_u8(bytes, 13, "maximum degree")?;
1486 let reserved_14 = read_checkpoint_u16(bytes, 14, "reserved degree bytes")?;
1487 let l_build = read_checkpoint_u16(bytes, 16, "construction width")?;
1488 let reserved_18 = read_checkpoint_u16(bytes, 18, "reserved width bytes")?;
1489 let checkpoint_batch = read_checkpoint_u32(bytes, 20, "checkpoint batch")?;
1490 let node_count = read_checkpoint_u32(bytes, 24, "node count")?;
1491 let dimensions = read_checkpoint_u32(bytes, 28, "dimensions")?;
1492 let next_index = read_checkpoint_u32(bytes, 32, "next position")?;
1493 let reserved_36 = read_checkpoint_u32(bytes, 36, "reserved position bytes")?;
1494 if reserved_14 != 0 || reserved_18 != 0 || reserved_36 != 0 {
1495 return Err(GraphBuildError::CheckpointCorrupt(
1496 "reserved header bytes are nonzero".to_owned(),
1497 ));
1498 }
1499 if r_target == 0 || r_target > r_max || l_build < u16::from(r_target) {
1500 return Err(GraphBuildError::CheckpointCorrupt(
1501 "stored degree and construction width are invalid".to_owned(),
1502 ));
1503 }
1504 if checkpoint_batch == 0 || node_count == 0 || dimensions == 0 || next_index > node_count {
1505 return Err(GraphBuildError::CheckpointCorrupt(
1506 "stored batch, shape, or next position is invalid".to_owned(),
1507 ));
1508 }
1509 let alpha_build = f32::from_bits(read_checkpoint_u32(bytes, 48, "build alpha")?);
1510 let alpha_refine = f32::from_bits(read_checkpoint_u32(bytes, 52, "refine alpha")?);
1511 if !alpha_build.is_finite()
1512 || !alpha_refine.is_finite()
1513 || alpha_build < 1.0
1514 || alpha_refine < alpha_build
1515 {
1516 return Err(GraphBuildError::CheckpointCorrupt(
1517 "stored alpha values are invalid".to_owned(),
1518 ));
1519 }
1520 let payload_bytes = usize::try_from(read_checkpoint_u64(bytes, 72, "payload bytes")?)
1521 .map_err(|_| GraphBuildError::CheckpointCorrupt("payload exceeds usize".to_owned()))?;
1522 let node_count_usize = usize::try_from(node_count)
1523 .map_err(|_| GraphBuildError::CheckpointCorrupt("node count exceeds usize".to_owned()))?;
1524 let slot_count = node_count_usize
1525 .checked_mul(usize::from(r_max))
1526 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot count overflow".to_owned()))?;
1527 let expected_payload = slot_count
1528 .checked_mul(std::mem::size_of::<u32>())
1529 .and_then(|slot_bytes| slot_bytes.checked_add(node_count_usize))
1530 .ok_or_else(|| {
1531 GraphBuildError::CheckpointCorrupt("payload geometry overflow".to_owned())
1532 })?;
1533 if payload_bytes != expected_payload {
1534 return Err(GraphBuildError::CheckpointCorrupt(format!(
1535 "payload bytes {payload_bytes}, expected {expected_payload}"
1536 )));
1537 }
1538 let expected_length = CHECKPOINT_HEADER_BYTES
1539 .checked_add(payload_bytes)
1540 .and_then(|length| length.checked_add(CHECKPOINT_CHECKSUM_BYTES))
1541 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("file length overflow".to_owned()))?;
1542 if bytes.len() != expected_length {
1543 return Err(GraphBuildError::CheckpointCorrupt(format!(
1544 "file length {}, expected {expected_length}",
1545 bytes.len()
1546 )));
1547 }
1548 let degrees_start = CHECKPOINT_HEADER_BYTES;
1549 let slots_start = degrees_start
1550 .checked_add(node_count_usize)
1551 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("slot offset overflow".to_owned()))?;
1552 for owner in 0..node_count_usize {
1553 let degree = usize::from(read_checkpoint_u8(
1554 bytes,
1555 degrees_start + owner,
1556 "node degree",
1557 )?);
1558 if degree > usize::from(r_max) {
1559 return Err(GraphBuildError::CheckpointCorrupt(format!(
1560 "node {owner} degree {degree} exceeds {r_max}"
1561 )));
1562 }
1563 let row_start = owner
1564 .checked_mul(usize::from(r_max))
1565 .and_then(|slot| slot.checked_mul(std::mem::size_of::<u32>()))
1566 .and_then(|offset| slots_start.checked_add(offset))
1567 .ok_or_else(|| GraphBuildError::CheckpointCorrupt("row offset overflow".to_owned()))?;
1568 let mut active = Vec::with_capacity(degree);
1569 for slot in 0..usize::from(r_max) {
1570 let offset = slot
1571 .checked_mul(std::mem::size_of::<u32>())
1572 .and_then(|offset| row_start.checked_add(offset))
1573 .ok_or_else(|| {
1574 GraphBuildError::CheckpointCorrupt("neighbor offset overflow".to_owned())
1575 })?;
1576 let neighbor = read_checkpoint_u32(bytes, offset, "neighbor")?;
1577 if slot < degree {
1578 if neighbor >= node_count
1579 || neighbor as usize == owner
1580 || active.contains(&neighbor)
1581 {
1582 return Err(GraphBuildError::CheckpointCorrupt(format!(
1583 "node {owner} has invalid active neighbor {neighbor}"
1584 )));
1585 }
1586 active.push(neighbor);
1587 } else if neighbor != u32::MAX {
1588 return Err(GraphBuildError::CheckpointCorrupt(format!(
1589 "node {owner} inactive slot {slot} is not the sentinel"
1590 )));
1591 }
1592 }
1593 }
1594 Ok(())
1595}
1596
1597fn validate_checkpoint_adjacency(adjacency: &Adjacency) -> Result<(), GraphBuildError> {
1598 for node_id in 0..adjacency.node_count {
1599 let neighbors = adjacency.neighbors(node_id)?;
1600 if neighbors.len() > usize::from(adjacency.r_max) {
1601 return Err(GraphBuildError::CheckpointCorrupt(format!(
1602 "node {node_id} exceeds maximum degree"
1603 )));
1604 }
1605 let mut seen = Vec::with_capacity(neighbors.len());
1606 for neighbor in neighbors {
1607 if *neighbor >= adjacency.node_count || *neighbor == node_id || seen.contains(neighbor)
1608 {
1609 return Err(GraphBuildError::CheckpointCorrupt(format!(
1610 "node {node_id} has an out-of-range, self, or duplicate neighbor {neighbor}"
1611 )));
1612 }
1613 seen.push(*neighbor);
1614 }
1615 let range = adjacency.row_range(node_id)?;
1616 let inactive_start = range.start.checked_add(neighbors.len()).ok_or_else(|| {
1617 GraphBuildError::CheckpointCorrupt("inactive offset overflow".to_owned())
1618 })?;
1619 let inactive = adjacency
1620 .slots
1621 .get(inactive_start..range.end)
1622 .ok_or_else(|| {
1623 GraphBuildError::CheckpointCorrupt("inactive slots are unavailable".to_owned())
1624 })?;
1625 if inactive.iter().any(|neighbor| *neighbor != u32::MAX) {
1626 return Err(GraphBuildError::CheckpointCorrupt(format!(
1627 "node {node_id} has non-sentinel inactive slots"
1628 )));
1629 }
1630 }
1631 Ok(())
1632}
1633
1634fn phase_code(phase: BuildPhase) -> u8 {
1635 match phase {
1636 BuildPhase::Build => 1,
1637 BuildPhase::Refine => 2,
1638 BuildPhase::Complete => 3,
1639 }
1640}
1641
1642fn decode_phase(value: u8) -> Result<BuildPhase, GraphBuildError> {
1643 match value {
1644 1 => Ok(BuildPhase::Build),
1645 2 => Ok(BuildPhase::Refine),
1646 3 => Ok(BuildPhase::Complete),
1647 _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1648 "unknown phase {value}"
1649 ))),
1650 }
1651}
1652
1653fn pass_code(passes: GraphBuildPasses) -> u8 {
1654 match passes {
1655 GraphBuildPasses::One => 1,
1656 GraphBuildPasses::Two => 2,
1657 }
1658}
1659
1660fn decode_passes(value: u8) -> Result<GraphBuildPasses, GraphBuildError> {
1661 match value {
1662 1 => Ok(GraphBuildPasses::One),
1663 2 => Ok(GraphBuildPasses::Two),
1664 _ => Err(GraphBuildError::CheckpointCorrupt(format!(
1665 "unknown pass count {value}"
1666 ))),
1667 }
1668}
1669
1670fn read_checkpoint_u8(bytes: &[u8], offset: usize, field: &str) -> Result<u8, GraphBuildError> {
1671 bytes.get(offset).copied().ok_or_else(|| {
1672 GraphBuildError::CheckpointCorrupt(format!("{field} is truncated at byte {offset}"))
1673 })
1674}
1675
1676fn read_checkpoint_u16(bytes: &[u8], offset: usize, field: &str) -> Result<u16, GraphBuildError> {
1677 let end = offset
1678 .checked_add(2)
1679 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1680 let raw: [u8; 2] = bytes
1681 .get(offset..end)
1682 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1683 .try_into()
1684 .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1685 Ok(u16::from_le_bytes(raw))
1686}
1687
1688fn read_checkpoint_u32(bytes: &[u8], offset: usize, field: &str) -> Result<u32, GraphBuildError> {
1689 let end = offset
1690 .checked_add(4)
1691 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1692 let raw: [u8; 4] = bytes
1693 .get(offset..end)
1694 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1695 .try_into()
1696 .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1697 Ok(u32::from_le_bytes(raw))
1698}
1699
1700fn read_checkpoint_u64(bytes: &[u8], offset: usize, field: &str) -> Result<u64, GraphBuildError> {
1701 let end = offset
1702 .checked_add(8)
1703 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} offset overflow")))?;
1704 let raw: [u8; 8] = bytes
1705 .get(offset..end)
1706 .ok_or_else(|| GraphBuildError::CheckpointCorrupt(format!("{field} is truncated")))?
1707 .try_into()
1708 .map_err(|_| GraphBuildError::CheckpointCorrupt(format!("{field} width is invalid")))?;
1709 Ok(u64::from_le_bytes(raw))
1710}
1711
1712#[derive(Clone, Copy, Debug)]
1713struct ScoredNode {
1714 node_id: u32,
1715 distance: f64,
1716}
1717
1718#[allow(clippy::too_many_arguments)]
1719fn process_node(
1720 vectors: &SegmentVectors<'_>,
1721 params: GraphParams,
1722 seed: u64,
1723 alpha: f32,
1724 node_id: u32,
1725 entries: &[u32],
1726 adjacency: &mut Adjacency,
1727 inserted: &mut [u8],
1728 visited: &mut [u32],
1729 visit_epoch: &mut u32,
1730) -> Result<(), GraphBuildError> {
1731 vectors.validate_node(node_id)?;
1732 let candidates = search_candidates(
1733 vectors,
1734 adjacency,
1735 inserted,
1736 node_id,
1737 entries,
1738 usize::from(params.l_build()),
1739 seed,
1740 visited,
1741 visit_epoch,
1742 )?;
1743 let mut candidate_ids = Vec::new();
1744
1745 let capacity = candidates
1746 .len()
1747 .saturating_add(adjacency.neighbors(node_id)?.len());
1748 let requested = capacity
1749 .checked_mul(std::mem::size_of::<u32>())
1750 .ok_or_else(|| GraphBuildError::Geometry("candidate id allocation overflow".to_owned()))?;
1751 #[cfg(feature = "allocation-audit")]
1752 let allocation =
1753 crate::allocation_audit::attributed(|| candidate_ids.try_reserve_exact(capacity));
1754 #[cfg(not(feature = "allocation-audit"))]
1755 let allocation = candidate_ids.try_reserve_exact(capacity);
1756 allocation.map_err(|_| {
1757 GraphBuildError::Store(StoreError::AllocationFailed {
1758 needed: requested as u64,
1759 component: "candidate id",
1760 })
1761 })?;
1762 for candidate in candidates {
1763 candidate_ids.push(candidate.node_id);
1764 }
1765 candidate_ids.extend_from_slice(adjacency.neighbors(node_id)?);
1766 let pruned = robust_prune(
1767 vectors,
1768 node_id,
1769 &candidate_ids,
1770 alpha,
1771 usize::from(params.r_target()),
1772 )?;
1773 drop(candidate_ids);
1774 adjacency.set_neighbors(node_id, &pruned)?;
1775 let node = vectors.validate_node(node_id)?;
1776 let inserted_node = inserted.get_mut(node).ok_or_else(|| {
1777 GraphBuildError::Geometry(format!("inserted marker {node_id} is unavailable"))
1778 })?;
1779 *inserted_node = 1;
1780 add_reciprocal_edges(vectors, adjacency, node_id, &pruned, params, alpha)?;
1781 drop(pruned);
1782 Ok(())
1783}
1784
1785#[allow(clippy::too_many_arguments)]
1786fn search_candidates(
1787 vectors: &SegmentVectors<'_>,
1788 adjacency: &Adjacency,
1789 inserted: &[u8],
1790 query_id: u32,
1791 entries: &[u32],
1792 width: usize,
1793 seed: u64,
1794 visited: &mut [u32],
1795 visit_epoch: &mut u32,
1796) -> Result<Vec<ScoredNode>, GraphBuildError> {
1797 let query = vectors.f32_row(query_id)?;
1798 let mut query_norm_squared = 0.0_f64;
1799 for chunk in query.chunks(256) {
1800 for value in chunk {
1801 query_norm_squared += f64::from(*value) * f64::from(*value);
1802 }
1803 }
1804
1805 let prepared =
1806 prepare_bit4_query(query, seed ^ u64::from(query_id)).map_err(|error| match error {
1807 QuantError::Allocation => GraphBuildError::Store(StoreError::AllocationFailed {
1808 needed: query.len() as u64,
1809 component: "graph query codes",
1810 }),
1811 error => GraphBuildError::Quant(error),
1812 })?;
1813 *visit_epoch = visit_epoch.wrapping_add(1);
1814 if *visit_epoch == 0 {
1815 visited.fill(0);
1816 *visit_epoch = 1;
1817 }
1818 let epoch = *visit_epoch;
1819 let mut result = Vec::new();
1820
1821 let capacity = width.saturating_add(1);
1822 let requested = capacity
1823 .checked_mul(std::mem::size_of::<ScoredNode>())
1824 .ok_or_else(|| GraphBuildError::Geometry("result allocation overflow".to_owned()))?;
1825 #[cfg(feature = "allocation-audit")]
1826 let allocation = crate::allocation_audit::attributed(|| result.try_reserve_exact(capacity));
1827 #[cfg(not(feature = "allocation-audit"))]
1828 let allocation = result.try_reserve_exact(capacity);
1829 allocation.map_err(|_| {
1830 GraphBuildError::Store(StoreError::AllocationFailed {
1831 needed: requested as u64,
1832 component: "result",
1833 })
1834 })?;
1835 let frontier_capacity = vectors.node_count as usize;
1836 let mut frontier_slots = Vec::new();
1837
1838 let capacity = frontier_capacity;
1839 let requested = capacity
1840 .checked_mul(std::mem::size_of::<BestFirst>())
1841 .ok_or_else(|| GraphBuildError::Geometry("frontier allocation overflow".to_owned()))?;
1842 #[cfg(feature = "allocation-audit")]
1843 let allocation =
1844 crate::allocation_audit::attributed(|| frontier_slots.try_reserve_exact(capacity));
1845 #[cfg(not(feature = "allocation-audit"))]
1846 let allocation = frontier_slots.try_reserve_exact(capacity);
1847 allocation.map_err(|_| {
1848 GraphBuildError::Store(StoreError::AllocationFailed {
1849 needed: requested as u64,
1850 component: "frontier",
1851 })
1852 })?;
1853 let mut frontier = std::collections::BinaryHeap::from(frontier_slots);
1854 for &entry in entries {
1855 let entry_index = vectors.validate_node(entry)?;
1856 if entry == query_id
1857 || inserted.get(entry_index).copied() != Some(1)
1858 || visited.get(entry_index).copied() == Some(epoch)
1859 {
1860 continue;
1861 }
1862 if let Some(marker) = visited.get_mut(entry_index) {
1863 *marker = epoch;
1864 }
1865 let scored = score_node(vectors, &prepared, query_norm_squared, entry)?;
1866 insert_result(&mut result, scored, width)?;
1867 push_frontier(&mut frontier, scored)?;
1868 }
1869 if frontier.is_empty()
1870 && let Some(start) = inserted.iter().position(|present| *present == 1)
1871 {
1872 let start = u32::try_from(start)
1873 .map_err(|_| GraphBuildError::Geometry("inserted start exceeds u32".to_owned()))?;
1874 let scored = score_node(vectors, &prepared, query_norm_squared, start)?;
1875 if let Some(marker) = visited.get_mut(start as usize) {
1876 *marker = epoch;
1877 }
1878 insert_result(&mut result, scored, width)?;
1879 push_frontier(&mut frontier, scored)?;
1880 }
1881 while !frontier.is_empty() {
1882 let Some(BestFirst(candidate)) = frontier.pop() else {
1883 break;
1884 };
1885 let worst = result.last().map(|node| node.distance);
1886 if result.len() >= width && worst.is_some_and(|distance| candidate.distance > distance) {
1887 break;
1888 }
1889 for &neighbor in adjacency.neighbors(candidate.node_id)? {
1890 let neighbor_index = vectors.validate_node(neighbor)?;
1891 if neighbor == query_id
1892 || inserted.get(neighbor_index).copied() != Some(1)
1893 || visited.get(neighbor_index).copied() == Some(epoch)
1894 {
1895 continue;
1896 }
1897 if let Some(marker) = visited.get_mut(neighbor_index) {
1898 *marker = epoch;
1899 }
1900 let scored = score_node(vectors, &prepared, query_norm_squared, neighbor)?;
1901 let qualifies = result.len() < width
1902 || result
1903 .last()
1904 .is_some_and(|current_worst| scored.distance < current_worst.distance);
1905 if qualifies {
1906 insert_result(&mut result, scored, width)?;
1907 push_frontier(&mut frontier, scored)?;
1908 }
1909 }
1910 }
1911 drop(frontier);
1912 drop(prepared);
1913 Ok(result)
1914}
1915
1916fn score_node(
1917 vectors: &SegmentVectors<'_>,
1918 query: &crate::quant::Bit4Query,
1919 query_norm_squared: f64,
1920 node_id: u32,
1921) -> Result<ScoredNode, GraphBuildError> {
1922 vectors.validate_node(node_id)?;
1923 let factors = vectors.factor(node_id)?;
1924 let dot = f64::from(est_dot_bit4(query, vectors.code_row(node_id)?, factors)?);
1925 let norm = factors.norm();
1926 Ok(ScoredNode {
1927 node_id,
1928 distance: (query_norm_squared + norm * norm - 2.0 * dot).max(0.0),
1929 })
1930}
1931
1932#[derive(Clone, Copy)]
1936struct BestFirst(ScoredNode);
1937
1938impl PartialEq for BestFirst {
1939 fn eq(&self, other: &Self) -> bool {
1940 self.cmp(other).is_eq()
1941 }
1942}
1943
1944impl Eq for BestFirst {}
1945
1946impl PartialOrd for BestFirst {
1947 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1948 Some(self.cmp(other))
1949 }
1950}
1951
1952impl Ord for BestFirst {
1953 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1954 scored_worst_first(&self.0, &other.0)
1955 }
1956}
1957
1958fn push_frontier(
1959 frontier: &mut std::collections::BinaryHeap<BestFirst>,
1960 node: ScoredNode,
1961) -> Result<(), GraphBuildError> {
1962 if frontier.len() == frontier.capacity() {
1963 return Err(GraphBuildError::Geometry(
1964 "frontier fixed capacity exhausted".to_owned(),
1965 ));
1966 }
1967 frontier.push(BestFirst(node));
1968 Ok(())
1969}
1970
1971fn insert_result(
1977 result: &mut Vec<ScoredNode>,
1978 node: ScoredNode,
1979 width: usize,
1980) -> Result<(), GraphBuildError> {
1981 if result.len() == result.capacity() {
1982 return Err(GraphBuildError::Geometry(
1983 "result fixed capacity exhausted".to_owned(),
1984 ));
1985 }
1986 let position = result.partition_point(|existing| scored_best_first(existing, &node).is_lt());
1987 result.insert(position, node);
1988 if result.len() > width {
1989 let _ = result.pop();
1990 }
1991 Ok(())
1992}
1993
1994fn sort_scored(
1995 values: &mut [ScoredNode],
1996 compare: fn(&ScoredNode, &ScoredNode) -> std::cmp::Ordering,
1997) -> Result<(), GraphBuildError> {
1998 fn sift(
1999 values: &mut [ScoredNode],
2000 mut root: usize,
2001 end: usize,
2002 compare: fn(&ScoredNode, &ScoredNode) -> std::cmp::Ordering,
2003 ) -> Result<(), GraphBuildError> {
2004 loop {
2005 let child = root
2006 .checked_mul(2)
2007 .and_then(|value| value.checked_add(1))
2008 .ok_or_else(|| GraphBuildError::Geometry("sort offset overflow".to_owned()))?;
2009 if child >= end {
2010 return Ok(());
2011 }
2012 let mut selected = child;
2013 if child + 1 < end
2014 && compare(
2015 values.get(child).ok_or_else(|| {
2016 GraphBuildError::Geometry("sort child unavailable".to_owned())
2017 })?,
2018 values.get(child + 1).ok_or_else(|| {
2019 GraphBuildError::Geometry("sort sibling unavailable".to_owned())
2020 })?,
2021 )
2022 .is_lt()
2023 {
2024 selected = child + 1;
2025 }
2026 if !compare(
2027 values
2028 .get(root)
2029 .ok_or_else(|| GraphBuildError::Geometry("sort root unavailable".to_owned()))?,
2030 values.get(selected).ok_or_else(|| {
2031 GraphBuildError::Geometry("sort selection unavailable".to_owned())
2032 })?,
2033 )
2034 .is_lt()
2035 {
2036 return Ok(());
2037 }
2038 values.swap(root, selected);
2039 root = selected;
2040 }
2041 }
2042 for root in (0..values.len() / 2).rev() {
2043 sift(values, root, values.len(), compare)?;
2044 }
2045 for end in (1..values.len()).rev() {
2046 values.swap(0, end);
2047 sift(values, 0, end, compare)?;
2048 }
2049 Ok(())
2050}
2051
2052fn scored_best_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
2053 left.distance
2054 .total_cmp(&right.distance)
2055 .then_with(|| left.node_id.cmp(&right.node_id))
2056}
2057
2058fn scored_worst_first(left: &ScoredNode, right: &ScoredNode) -> std::cmp::Ordering {
2059 scored_best_first(right, left)
2060}
2061
2062fn robust_prune(
2063 vectors: &SegmentVectors<'_>,
2064 owner: u32,
2065 candidates: &[u32],
2066 alpha: f32,
2067 target: usize,
2068) -> Result<Vec<u32>, GraphBuildError> {
2069 robust_prune_rows(
2070 vectors.rescore,
2071 vectors.dimensions,
2072 vectors.node_count,
2073 owner,
2074 candidates,
2075 alpha,
2076 target,
2077 )
2078}
2079#[allow(clippy::too_many_arguments)]
2080pub(crate) fn robust_prune_rows(
2081 rescore: &[f32],
2082 dimensions: usize,
2083 node_count: u32,
2084 owner: u32,
2085 candidates: &[u32],
2086 alpha: f32,
2087 target: usize,
2088) -> Result<Vec<u32>, GraphBuildError> {
2089 validate_exact_rows(rescore, dimensions, node_count)?;
2090 validate_exact_node(owner, node_count)?;
2091 let mut remaining = Vec::new();
2092
2093 let capacity = candidates.len();
2094 let requested = capacity
2095 .checked_mul(std::mem::size_of::<u32>())
2096 .ok_or_else(|| GraphBuildError::Geometry("prune allocation overflow".to_owned()))?;
2097 #[cfg(feature = "allocation-audit")]
2098 let allocation = crate::allocation_audit::attributed(|| remaining.try_reserve_exact(capacity));
2099 #[cfg(not(feature = "allocation-audit"))]
2100 let allocation = remaining.try_reserve_exact(capacity);
2101 allocation.map_err(|_| {
2102 GraphBuildError::Store(StoreError::AllocationFailed {
2103 needed: requested as u64,
2104 component: "prune",
2105 })
2106 })?;
2107 for &candidate in candidates {
2108 validate_exact_node(candidate, node_count)?;
2109 if candidate != owner && !remaining.contains(&candidate) {
2110 remaining.push(candidate);
2111 }
2112 }
2113 let mut owner_distances = Vec::new();
2114
2115 let capacity = remaining.len();
2116 let requested = capacity
2117 .checked_mul(std::mem::size_of::<ScoredNode>())
2118 .ok_or_else(|| GraphBuildError::Geometry("distance allocation overflow".to_owned()))?;
2119 #[cfg(feature = "allocation-audit")]
2120 let allocation =
2121 crate::allocation_audit::attributed(|| owner_distances.try_reserve_exact(capacity));
2122 #[cfg(not(feature = "allocation-audit"))]
2123 let allocation = owner_distances.try_reserve_exact(capacity);
2124 allocation.map_err(|_| {
2125 GraphBuildError::Store(StoreError::AllocationFailed {
2126 needed: requested as u64,
2127 component: "distance",
2128 })
2129 })?;
2130 for candidate in remaining {
2131 owner_distances.push(ScoredNode {
2132 node_id: candidate,
2133 distance: exact_row_distance(rescore, dimensions, owner, candidate)?,
2134 });
2135 }
2136
2137 sort_scored(&mut owner_distances, scored_best_first)?;
2138 let mut selected = Vec::new();
2139
2140 let capacity = target;
2141 let requested = capacity
2142 .checked_mul(std::mem::size_of::<u32>())
2143 .ok_or_else(|| GraphBuildError::Geometry("selected allocation overflow".to_owned()))?;
2144 #[cfg(feature = "allocation-audit")]
2145 let allocation = crate::allocation_audit::attributed(|| selected.try_reserve_exact(capacity));
2146 #[cfg(not(feature = "allocation-audit"))]
2147 let allocation = selected.try_reserve_exact(capacity);
2148 allocation.map_err(|_| {
2149 GraphBuildError::Store(StoreError::AllocationFailed {
2150 needed: requested as u64,
2151 component: "selected",
2152 })
2153 })?;
2154 let alpha_squared = f64::from(alpha) * f64::from(alpha);
2155 while selected.len() < target {
2156 let Some(best) = owner_distances.first().copied() else {
2157 break;
2158 };
2159 selected.push(best.node_id);
2160 let mut index = 0;
2161 while index < owner_distances.len() {
2162 let candidate = *owner_distances.get(index).ok_or_else(|| {
2163 GraphBuildError::Geometry("prune candidate unavailable".to_owned())
2164 })?;
2165 let keep = if candidate.node_id == best.node_id {
2166 false
2167 } else {
2168 let between =
2169 exact_row_distance(rescore, dimensions, best.node_id, candidate.node_id)?;
2170 alpha_squared * between > candidate.distance
2171 };
2172 if keep {
2173 index += 1;
2174 } else {
2175 owner_distances.remove(index);
2176 }
2177 }
2178 }
2179 drop(owner_distances);
2180 Ok(selected)
2181}
2182
2183fn validate_exact_rows(
2184 rescore: &[f32],
2185 dimensions: usize,
2186 node_count: u32,
2187) -> Result<(), GraphBuildError> {
2188 if dimensions == 0 {
2189 return Err(GraphBuildError::Geometry(
2190 "alpha prune requires non-zero dimensions".to_owned(),
2191 ));
2192 }
2193 let expected = (node_count as usize)
2194 .checked_mul(dimensions)
2195 .ok_or_else(|| GraphBuildError::Geometry("exact row length overflow".to_owned()))?;
2196 if rescore.len() != expected {
2197 return Err(GraphBuildError::Geometry(format!(
2198 "exact rows contain {} values, expected {expected}",
2199 rescore.len()
2200 )));
2201 }
2202 Ok(())
2203}
2204
2205fn validate_exact_node(node_id: u32, node_count: u32) -> Result<(), GraphBuildError> {
2206 if node_id >= node_count {
2207 return Err(GraphBuildError::NodeIdOutOfRange {
2208 node_id,
2209 node_count,
2210 });
2211 }
2212 Ok(())
2213}
2214
2215fn exact_row_distance(
2216 rescore: &[f32],
2217 dimensions: usize,
2218 left: u32,
2219 right: u32,
2220) -> Result<f64, GraphBuildError> {
2221 let row = |node_id: u32| {
2222 let start = (node_id as usize)
2223 .checked_mul(dimensions)
2224 .ok_or_else(|| GraphBuildError::Geometry("exact row offset overflow".to_owned()))?;
2225 let end = start
2226 .checked_add(dimensions)
2227 .ok_or_else(|| GraphBuildError::Geometry("exact row end overflow".to_owned()))?;
2228 rescore
2229 .get(start..end)
2230 .ok_or_else(|| GraphBuildError::Geometry(format!("exact row {node_id} is unavailable")))
2231 };
2232 let mut sum = 0.0;
2233 for (left_chunk, right_chunk) in row(left)?.chunks(256).zip(row(right)?.chunks(256)) {
2234 for (left, right) in left_chunk.iter().zip(right_chunk) {
2235 let difference = f64::from(*left) - f64::from(*right);
2236 sum += difference * difference;
2237 }
2238 }
2239 Ok(sum)
2240}
2241
2242fn add_reciprocal_edges(
2243 vectors: &SegmentVectors<'_>,
2244 adjacency: &mut Adjacency,
2245 owner: u32,
2246 neighbors: &[u32],
2247 params: GraphParams,
2248 alpha: f32,
2249) -> Result<(), GraphBuildError> {
2250 for &neighbor in neighbors {
2251 vectors.validate_node(neighbor)?;
2252 let source = adjacency.neighbors(neighbor)?;
2253 let mut existing = Vec::new();
2254
2255 let capacity = source.len().saturating_add(1);
2256 let requested = capacity
2257 .checked_mul(std::mem::size_of::<u32>())
2258 .ok_or_else(|| {
2259 GraphBuildError::Geometry("reciprocal allocation overflow".to_owned())
2260 })?;
2261 #[cfg(feature = "allocation-audit")]
2262 let allocation =
2263 crate::allocation_audit::attributed(|| existing.try_reserve_exact(capacity));
2264 #[cfg(not(feature = "allocation-audit"))]
2265 let allocation = existing.try_reserve_exact(capacity);
2266 allocation.map_err(|_| {
2267 GraphBuildError::Store(StoreError::AllocationFailed {
2268 needed: requested as u64,
2269 component: "reciprocal",
2270 })
2271 })?;
2272 existing.extend_from_slice(source);
2273 if existing.contains(&owner) {
2274 drop(existing);
2275 continue;
2276 }
2277 let updated = if existing.len() < usize::from(params.r_max()) {
2278 let mut updated = existing;
2279 updated.push(owner);
2280 updated
2281 } else {
2282 let mut candidates = existing;
2283 candidates.push(owner);
2284 robust_prune(
2285 vectors,
2286 neighbor,
2287 &candidates,
2288 alpha,
2289 usize::from(params.r_target()),
2290 )?
2291 };
2292 adjacency.set_neighbors(neighbor, &updated)?;
2293 drop(updated);
2294 }
2295 Ok(())
2296}
2297
2298fn refined_entry_points(
2299 vectors: &SegmentVectors<'_>,
2300 shuffled: &[u32],
2301) -> Result<Vec<u32>, GraphBuildError> {
2302 let sample = shuffled
2303 .get(..shuffled.len().min(MEDOID_SAMPLE_ROWS))
2304 .ok_or_else(|| GraphBuildError::Geometry("medoid sample is unavailable".to_owned()))?;
2305 let mut medoid_scores = Vec::new();
2306
2307 let capacity = sample.len();
2308 let requested = capacity
2309 .checked_mul(std::mem::size_of::<ScoredNode>())
2310 .ok_or_else(|| GraphBuildError::Geometry("medoid allocation overflow".to_owned()))?;
2311 #[cfg(feature = "allocation-audit")]
2312 let allocation =
2313 crate::allocation_audit::attributed(|| medoid_scores.try_reserve_exact(capacity));
2314 #[cfg(not(feature = "allocation-audit"))]
2315 let allocation = medoid_scores.try_reserve_exact(capacity);
2316 allocation.map_err(|_| {
2317 GraphBuildError::Store(StoreError::AllocationFailed {
2318 needed: requested as u64,
2319 component: "medoid",
2320 })
2321 })?;
2322 for candidate in sample {
2323 let mut distance = 0.0;
2324 for other in sample {
2325 distance +=
2326 exact_row_distance(vectors.rescore, vectors.dimensions, *candidate, *other)?;
2327 }
2328 medoid_scores.push(ScoredNode {
2329 node_id: *candidate,
2330 distance,
2331 });
2332 }
2333 sort_scored(&mut medoid_scores, scored_best_first)?;
2334 let medoid = medoid_scores
2335 .first()
2336 .map(|node| node.node_id)
2337 .ok_or_else(|| GraphBuildError::Geometry("medoid sample contains no rows".to_owned()))?;
2338 drop(medoid_scores);
2339 let mut entries = Vec::new();
2340
2341 let capacity = ENTRY_POINT_COUNT;
2342 let requested = capacity
2343 .checked_mul(std::mem::size_of::<u32>())
2344 .ok_or_else(|| GraphBuildError::Geometry("entry allocation overflow".to_owned()))?;
2345 #[cfg(feature = "allocation-audit")]
2346 let allocation = crate::allocation_audit::attributed(|| entries.try_reserve_exact(capacity));
2347 #[cfg(not(feature = "allocation-audit"))]
2348 let allocation = entries.try_reserve_exact(capacity);
2349 allocation.map_err(|_| {
2350 GraphBuildError::Store(StoreError::AllocationFailed {
2351 needed: requested as u64,
2352 component: "entry",
2353 })
2354 })?;
2355 entries.push(medoid);
2356 while entries.len() < ENTRY_POINT_COUNT && entries.len() < sample.len() {
2357 let mut farthest = None::<ScoredNode>;
2358 for &candidate in sample {
2359 if entries.contains(&candidate) {
2360 continue;
2361 }
2362 let mut nearest = f64::INFINITY;
2363 for entry in &entries {
2364 nearest = nearest.min(exact_row_distance(
2365 vectors.rescore,
2366 vectors.dimensions,
2367 candidate,
2368 *entry,
2369 )?);
2370 }
2371 let scored = ScoredNode {
2372 node_id: candidate,
2373 distance: nearest,
2374 };
2375 if farthest.is_none_or(|current| {
2376 scored.distance > current.distance
2377 || (scored.distance == current.distance && scored.node_id < current.node_id)
2378 }) {
2379 farthest = Some(scored);
2380 }
2381 }
2382 let Some(next) = farthest else {
2383 break;
2384 };
2385 entries.push(next.node_id);
2386 }
2387 Ok(entries)
2388}
2389
2390fn move_entries_to_front(order: &mut Vec<u32>, entries: &[u32]) -> Result<(), GraphBuildError> {
2391 let mut index = 0;
2392 while index < order.len() {
2393 let node = *order
2394 .get(index)
2395 .ok_or_else(|| GraphBuildError::Geometry("build order row unavailable".to_owned()))?;
2396 if entries.contains(&node) {
2397 order.remove(index);
2398 } else {
2399 index += 1;
2400 }
2401 }
2402 for &entry in entries.iter().rev() {
2403 order.insert(0, entry);
2404 }
2405 Ok(())
2406}
2407
2408fn encode_artifact(
2409 vectors: &SegmentVectors<'_>,
2410 params: GraphParams,
2411 entries: &[u32],
2412 adjacency: &Adjacency,
2413 mut memory: Option<AccountedCounter>,
2414) -> Result<GraphBuildArtifact, GraphBuildError> {
2415 let dimensions = u32::try_from(vectors.dimensions)
2416 .map_err(|_| GraphBuildError::Geometry("dimensions exceed u32".to_owned()))?;
2417 let padded_dimensions = dimensions
2418 .checked_add(127)
2419 .map(|value| value / 128 * 128)
2420 .ok_or_else(|| GraphBuildError::Geometry("padded dimensions overflow".to_owned()))?;
2421 let layout = GraphNodeLayout::new(dimensions, padded_dimensions, params.r_max())?;
2422 let padded_row_bytes = layout.code_bytes();
2423 let padded_length = (vectors.node_count as usize)
2424 .checked_mul(padded_row_bytes)
2425 .ok_or_else(|| GraphBuildError::Geometry("padded code bytes overflow".to_owned()))?;
2426 let encoded_length = (vectors.node_count as usize)
2427 .checked_mul(layout.stride() as usize)
2428 .and_then(|bytes| bytes.checked_add(NODE_BLOCK_TRAILER_LEN))
2429 .ok_or_else(|| GraphBuildError::Geometry("encoded graph bytes overflow".to_owned()))?;
2430 let node_inputs = (vectors.node_count as usize)
2431 .checked_mul(std::mem::size_of::<GraphNodeBlockInput<'_>>())
2432 .ok_or_else(|| GraphBuildError::Geometry("node input bytes overflow".to_owned()))?;
2433 let entry_bytes = entries
2434 .len()
2435 .checked_mul(std::mem::size_of::<u32>())
2436 .ok_or_else(|| GraphBuildError::Geometry("entry bytes overflow".to_owned()))?;
2437 if let Some(charge) = memory.as_mut() {
2438 let existing = usize::try_from(charge.bytes())
2439 .map_err(|_| GraphBuildError::Geometry("accounted bytes exceed usize".to_owned()))?;
2440 let peak = existing
2441 .checked_add(padded_length)
2442 .and_then(|bytes| bytes.checked_add(node_inputs))
2443 .and_then(|bytes| bytes.checked_add(encoded_length))
2444 .and_then(|bytes| bytes.checked_add(entry_bytes))
2445 .ok_or_else(|| GraphBuildError::Geometry("encode arena overflow".to_owned()))?;
2446 charge.set(peak)?;
2447 }
2448 let mut padded_codes = Vec::new();
2449
2450 let capacity = padded_length;
2451 let requested = capacity
2452 .checked_mul(std::mem::size_of::<u8>())
2453 .ok_or_else(|| GraphBuildError::Geometry("padded code allocation overflow".to_owned()))?;
2454 #[cfg(feature = "allocation-audit")]
2455 let allocation =
2456 crate::allocation_audit::attributed(|| padded_codes.try_reserve_exact(capacity));
2457 #[cfg(not(feature = "allocation-audit"))]
2458 let allocation = padded_codes.try_reserve_exact(capacity);
2459 allocation.map_err(|_| {
2460 GraphBuildError::Store(StoreError::AllocationFailed {
2461 needed: requested as u64,
2462 component: "padded code",
2463 })
2464 })?;
2465 padded_codes.resize(padded_length, 0);
2466 for node_id in 0..vectors.node_count {
2467 let source = vectors.code_row(node_id)?;
2468 let start = (node_id as usize)
2469 .checked_mul(padded_row_bytes)
2470 .ok_or_else(|| GraphBuildError::Geometry("padded code offset overflow".to_owned()))?;
2471 let end = start
2472 .checked_add(source.len())
2473 .ok_or_else(|| GraphBuildError::Geometry("padded code end overflow".to_owned()))?;
2474 let destination = padded_codes.get_mut(start..end).ok_or_else(|| {
2475 GraphBuildError::Geometry(format!("padded code row {node_id} is unavailable"))
2476 })?;
2477 for (output, input) in destination.chunks_mut(256).zip(source.chunks(256)) {
2478 output.copy_from_slice(input);
2479 }
2480 }
2481 let mut nodes = Vec::new();
2482
2483 let capacity = vectors.node_count as usize;
2484 let requested = capacity
2485 .checked_mul(std::mem::size_of::<GraphNodeBlockInput<'_>>())
2486 .ok_or_else(|| GraphBuildError::Geometry("node input allocation overflow".to_owned()))?;
2487 #[cfg(feature = "allocation-audit")]
2488 let allocation = crate::allocation_audit::attributed(|| nodes.try_reserve_exact(capacity));
2489 #[cfg(not(feature = "allocation-audit"))]
2490 let allocation = nodes.try_reserve_exact(capacity);
2491 allocation.map_err(|_| {
2492 GraphBuildError::Store(StoreError::AllocationFailed {
2493 needed: requested as u64,
2494 component: "node input",
2495 })
2496 })?;
2497 for node_id in 0..vectors.node_count {
2498 let start = (node_id as usize)
2499 .checked_mul(padded_row_bytes)
2500 .ok_or_else(|| GraphBuildError::Geometry("encoded code offset overflow".to_owned()))?;
2501 let end = start
2502 .checked_add(padded_row_bytes)
2503 .ok_or_else(|| GraphBuildError::Geometry("encoded code end overflow".to_owned()))?;
2504 let codes = padded_codes.get(start..end).ok_or_else(|| {
2505 GraphBuildError::Geometry(format!("encoded code row {node_id} is unavailable"))
2506 })?;
2507 nodes.push(GraphNodeBlockInput {
2508 codes,
2509 factors: vectors.factor(node_id)?,
2510 flags: u8::from(entries.contains(&node_id)),
2511 neighbors: adjacency.neighbors(node_id)?,
2512 });
2513 }
2514
2515 let encoded = encode_node_blocks_with_refinement_passes(
2516 GraphNodeBlockBuild {
2517 layout,
2518 nodes: &nodes,
2519 },
2520 crate::graph::refine::RefinementPasses::default(),
2521 )?;
2522 let encoded_region = encoded.into_bytes();
2523 let mut entry_points = Vec::new();
2524
2525 let capacity = entries.len();
2526 let requested = capacity
2527 .checked_mul(std::mem::size_of::<u32>())
2528 .ok_or_else(|| GraphBuildError::Geometry("entry copy allocation overflow".to_owned()))?;
2529 #[cfg(feature = "allocation-audit")]
2530 let allocation =
2531 crate::allocation_audit::attributed(|| entry_points.try_reserve_exact(capacity));
2532 #[cfg(not(feature = "allocation-audit"))]
2533 let allocation = entry_points.try_reserve_exact(capacity);
2534 allocation.map_err(|_| {
2535 GraphBuildError::Store(StoreError::AllocationFailed {
2536 needed: requested as u64,
2537 component: "entry copy",
2538 })
2539 })?;
2540 for chunk in entries.chunks(256) {
2541 entry_points.extend_from_slice(chunk);
2542 }
2543 drop(nodes);
2544 drop(padded_codes);
2545 if let Some(charge) = memory.as_mut() {
2546 let retained = encoded_region
2547 .capacity()
2548 .checked_add(
2549 entry_points
2550 .capacity()
2551 .checked_mul(std::mem::size_of::<u32>())
2552 .ok_or_else(|| {
2553 GraphBuildError::Geometry("retained entry bytes overflow".to_owned())
2554 })?,
2555 )
2556 .ok_or_else(|| GraphBuildError::Geometry("retained graph bytes overflow".to_owned()))?;
2557 charge.set(retained)?;
2558 }
2559 Ok(GraphBuildArtifact {
2560 encoded_region,
2561 entry_points,
2562 work_rows_completed: 0,
2563 _memory: memory,
2564 })
2565}
2566
2567fn shuffle(values: &mut [u32], random: &mut SplitMix64) -> Result<(), GraphBuildError> {
2568 for upper in (1..values.len()).rev() {
2569 let modulus = u64::try_from(upper + 1)
2570 .map_err(|_| GraphBuildError::Geometry("shuffle width exceeds u64".to_owned()))?;
2571 let selected = usize::try_from(random.next_u64() % modulus)
2572 .map_err(|_| GraphBuildError::Geometry("shuffle index exceeds usize".to_owned()))?;
2573 values.swap(upper, selected);
2574 }
2575 Ok(())
2576}
2577
2578#[derive(Clone, Copy, Debug)]
2579struct SplitMix64 {
2580 state: u64,
2581}
2582
2583impl SplitMix64 {
2584 const fn new(seed: u64) -> Self {
2585 Self { state: seed }
2586 }
2587
2588 fn next_u64(&mut self) -> u64 {
2589 self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
2590 let mut value = self.state;
2591 value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
2592 value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
2593 value ^ (value >> 31)
2594 }
2595}
2596
2597#[cfg(test)]
2598#[allow(
2599 clippy::expect_used,
2600 clippy::indexing_slicing,
2601 clippy::panic,
2602 clippy::unwrap_used
2603)]
2604mod tests {
2605 use proptest::prelude::*;
2606 use proptest::test_runner::{Config, RngSeed, TestRunner};
2607 use rand::{Rng, RngCore};
2608 use xxhash_rust::xxh3::xxh3_64;
2609
2610 use crate::graph::block::decode_node_blocks;
2611 use crate::lifecycle::QueryCancellation;
2612 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
2613 use crate::lifecycle::{
2614 CancelToken, ManualMonotonicClock, OpenOptions, QueryControl, Store, StoreTestDependencies,
2615 };
2616 use crate::meta::{AliveSet, ColumnStoreBuilder, Schema};
2617 use crate::quant::{Bit4Factors, quantize_bit4};
2618 use crate::segment::SegmentId;
2619 use crate::segment::reader::SegmentReader;
2620 use crate::segment::writer::{SegmentBuild, SegmentFactors, write_segment};
2621 use crate::vfs::{CountingVfs, StdVfs, Vfs};
2622 use std::sync::Arc;
2623
2624 use super::{
2625 Adjacency, CheckpointedGraphBuild, GraphBuildError, GraphBuildPasses, GraphBuildSession,
2626 SegmentVectors, build_graph, build_graph_checkpointed, decode_checkpoint,
2627 validate_graph_build_checkpoint,
2628 };
2629 use crate::graph::GraphParams;
2630
2631 fn fixture_reader(directory: &std::path::Path, rows: usize) -> SegmentReader {
2632 let dimensions = 128_usize;
2633 let mut random = crate::test_support::seeded_rng(
2634 "graph::build::vamana_build_is_deterministic_under_seed::fixture",
2635 );
2636 let mut rescore = Vec::with_capacity(rows * dimensions);
2637 let mut codes = Vec::with_capacity(rows * dimensions.div_ceil(2));
2638 let mut factors = Vec::<Bit4Factors>::with_capacity(rows);
2639 for _ in 0..rows {
2640 let row = (0..dimensions)
2641 .map(|_| random.random_range(-2.0_f32..2.0_f32))
2642 .collect::<Vec<_>>();
2643 let start = codes.len();
2644 codes.resize(start + dimensions.div_ceil(2), 0);
2645 factors.push(quantize_bit4(&row, &mut codes[start..]).expect("fixture row quantizes"));
2646 rescore.extend(row);
2647 }
2648 let mut columns = ColumnStoreBuilder::new(Schema::new(Vec::new()).expect("empty schema"));
2649 for row in 0..rows {
2650 columns
2651 .push_row(row as i64, &[])
2652 .expect("timestamp-only fixture row");
2653 }
2654 let columns = columns.finish().expect("fixture columns");
2655 let alive = AliveSet::new(rows as u32);
2656 let id = SegmentId::new(19, [7; 10]);
2657 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2658 .expect("derived policy");
2659 write_segment(
2660 &StdVfs,
2661 directory,
2662 SegmentBuild {
2663 id,
2664 scheme: 4,
2665 dims: dimensions as u32,
2666 codes: &codes,
2667 factors: SegmentFactors::Bit4(&factors),
2668 rescore: &rescore,
2669 columns: &columns,
2670 alive: &alive,
2671 },
2672 policy,
2673 )
2674 .expect("sealed Bit4 fixture");
2675 SegmentReader::open(&StdVfs, &directory.join(id.file_name()), id).expect("fixture reader")
2676 }
2677
2678 #[test]
2679 fn graph_checkpoint_io_goes_through_the_store_vfs() {
2680 let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2681 let reader = fixture_reader(segment_directory.path(), 4);
2682 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2683 let checkpoint = store_directory.path().join("graph-build.checkpoint");
2684 let vfs = Arc::new(CountingVfs::new(StdVfs).with_read_path_filter(checkpoint.clone()));
2685 let store = Store::open_with_test_dependencies(
2686 store_directory.path(),
2687 OpenOptions::default(),
2688 StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2689 )
2690 .expect("checkpoint store");
2691 let lease = store.snapshot().expect("checkpoint snapshot");
2692 let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2693 let control = QueryControl::Cancel(CancelToken::new());
2694
2695 vfs.reset();
2696 let interrupted = build_graph_checkpointed(
2697 &store,
2698 &reader,
2699 CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control)
2700 .with_max_work_rows(1),
2701 &lease,
2702 );
2703 assert!(matches!(
2704 interrupted,
2705 Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2706 ));
2707 build_graph_checkpointed(
2708 &store,
2709 &reader,
2710 CheckpointedGraphBuild::new(params, 0x06, GraphBuildPasses::One, &checkpoint, &control),
2711 &lease,
2712 )
2713 .expect("resumed graph build");
2714
2715 assert!(vfs.read_calls() >= 1, "checkpoint read bypassed Vfs");
2716 assert_eq!(
2717 vfs.filtered_read_calls(),
2718 1,
2719 "graph-build.checkpoint read bypassed Vfs"
2720 );
2721 assert!(vfs.write_calls() >= 1, "checkpoint write bypassed Vfs");
2722 assert!(vfs.rename_calls() >= 1, "checkpoint rename bypassed Vfs");
2723 assert!(vfs.delete_calls() >= 1, "checkpoint delete bypassed Vfs");
2724 drop(lease);
2725 store.close().expect("close checkpoint store");
2726 }
2727
2728 #[test]
2729 fn graph_checkpoint_publish_syncs_the_temp_file_and_its_directory() {
2730 let segment_directory = tempfile::tempdir().expect("checkpoint segment directory");
2731 let reader = fixture_reader(segment_directory.path(), 4);
2732 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2733 let checkpoint = store_directory.path().join("graph-build.checkpoint");
2734 let vfs = Arc::new(CountingVfs::new(StdVfs));
2735 let store = Store::open_with_test_dependencies(
2736 store_directory.path(),
2737 OpenOptions::default().with_durability(DurabilityMode::Durable, CommitTier::Durable),
2738 StoreTestDependencies::new(vfs.clone(), Arc::new(ManualMonotonicClock::new())),
2739 )
2740 .expect("durable checkpoint store");
2741 let lease = store.snapshot().expect("checkpoint snapshot");
2742 let params = GraphParams::new(2, 3, 1.0, 1.0, 3, 1).expect("checkpoint params");
2743 let control = QueryControl::Cancel(CancelToken::new());
2744
2745 vfs.reset();
2746 let interrupted = build_graph_checkpointed(
2747 &store,
2748 &reader,
2749 CheckpointedGraphBuild::new(params, 0x03, GraphBuildPasses::One, &checkpoint, &control)
2750 .with_max_work_rows(1),
2751 &lease,
2752 );
2753 assert!(matches!(
2754 interrupted,
2755 Err(GraphBuildError::BudgetExhausted { rows_completed: 1 })
2756 ));
2757 assert!(
2758 vfs.full_sync_calls() >= 2,
2759 "checkpoint publish skipped syncs"
2760 );
2761 assert!(vfs.rename_calls() >= 1, "checkpoint publish skipped rename");
2762 }
2763
2764 fn checkpoint_with_mutation(
2765 mut bytes: Vec<u8>,
2766 mutation: impl FnOnce(&mut Vec<u8>),
2767 ) -> Vec<u8> {
2768 mutation(&mut bytes);
2769 let checksum_start = bytes.len() - super::CHECKPOINT_CHECKSUM_BYTES;
2770 let checksum = xxh3_64(&bytes[..checksum_start]);
2771 bytes[checksum_start..].copy_from_slice(&checksum.to_le_bytes());
2772 bytes
2773 }
2774
2775 #[test]
2776 fn graph_build_errors_preserve_actionable_values_and_sources() {
2777 let io_error = GraphBuildError::CheckpointIo {
2778 path: std::path::PathBuf::from("blocked.checkpoint"),
2779 source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "blocked"),
2780 };
2781 assert!(io_error.to_string().contains("blocked.checkpoint"));
2782 assert!(std::error::Error::source(&io_error).is_some());
2783
2784 let errors = [
2785 GraphBuildError::CheckpointCorrupt("bad header".to_owned()),
2786 GraphBuildError::BudgetExhausted { rows_completed: 16 },
2787 GraphBuildError::Cancelled { partial: false },
2788 GraphBuildError::Timeout { partial: false },
2789 GraphBuildError::ReadCancelled { partial: false },
2790 GraphBuildError::Geometry("bad shape".to_owned()),
2791 GraphBuildError::NodeIdOutOfRange {
2792 node_id: 9,
2793 node_count: 3,
2794 },
2795 ];
2796 for error in errors {
2797 assert!(!error.to_string().is_empty());
2798 assert!(std::error::Error::source(&error).is_none());
2799 }
2800
2801 let sourced_errors = [
2802 GraphBuildError::from(crate::segment::SegmentError::Geometry(
2803 "bad segment".to_owned(),
2804 )),
2805 GraphBuildError::from(crate::quant::QuantError::EmptyVector),
2806 GraphBuildError::from(crate::graph::block::GraphNodeError::InvalidHeader(
2807 "bad node header".to_owned(),
2808 )),
2809 GraphBuildError::from(crate::lifecycle::StoreError::NotDirectory {
2810 path: std::path::PathBuf::from("not-a-store"),
2811 }),
2812 ];
2813 for error in sourced_errors {
2814 assert!(!error.to_string().is_empty());
2815 assert!(std::error::Error::source(&error).is_some());
2816 }
2817 }
2818
2819 #[test]
2820 fn built_graph_publishes_through_the_frozen_m2_writer() {
2821 let directory = tempfile::tempdir().expect("publication fixture directory");
2822 let reader = fixture_reader(directory.path(), 32);
2823 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 32).expect("publication params");
2824 let artifact = build_graph(
2825 &reader,
2826 params,
2827 0x0001_9000_3a71_fac7,
2828 GraphBuildPasses::Two,
2829 )
2830 .expect("publication graph");
2831 let output_id = SegmentId::new(19, [8; 10]);
2832 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2833 .expect("publication policy");
2834 let meta = artifact
2835 .write_segment_with_graph(&StdVfs, directory.path(), &reader, output_id, policy)
2836 .expect("published graph segment");
2837 assert_eq!(meta.id, output_id);
2838 let published = SegmentReader::open(
2839 &StdVfs,
2840 &directory.path().join(output_id.file_name()),
2841 output_id,
2842 )
2843 .expect("published reader");
2844 let graph = published
2845 .graph_node_blocks()
2846 .expect("published graph region");
2847 assert_eq!(graph.node_count(), 32);
2848 assert_eq!(graph.layout().max_degree(), 12);
2849 }
2850
2851 #[test]
2852 fn checked_adjacency_rejects_invalid_node_and_degree_inputs() {
2853 let directory = tempfile::tempdir().expect("checked vector directory");
2854 let reader = fixture_reader(directory.path(), 3);
2855 let vectors = SegmentVectors::new(&reader).expect("checked vectors");
2856 assert!(matches!(
2857 vectors.validate_node(3),
2858 Err(GraphBuildError::NodeIdOutOfRange { .. })
2859 ));
2860
2861 let mut adjacency = Adjacency::new(3, 2).expect("small adjacency");
2862 assert!(matches!(
2863 adjacency.neighbors(3),
2864 Err(GraphBuildError::NodeIdOutOfRange { .. })
2865 ));
2866 assert!(matches!(
2867 adjacency.set_neighbors(0, &[1, 2, 1]),
2868 Err(GraphBuildError::Geometry(_))
2869 ));
2870 assert!(matches!(
2871 adjacency.set_neighbors(0, &[3]),
2872 Err(GraphBuildError::NodeIdOutOfRange { .. })
2873 ));
2874 adjacency
2875 .set_neighbors(0, &[1, 2])
2876 .expect("valid adjacency row");
2877 assert_eq!(adjacency.neighbors(0).expect("active row"), &[1, 2]);
2878
2879 let mut self_edge = Adjacency::new(3, 2).expect("self-edge adjacency");
2880 self_edge.degrees[0] = 1;
2881 self_edge.slots[0] = 0;
2882 assert!(matches!(
2883 super::validate_checkpoint_adjacency(&self_edge),
2884 Err(GraphBuildError::CheckpointCorrupt(_))
2885 ));
2886
2887 let mut inactive_value = Adjacency::new(3, 2).expect("inactive-slot adjacency");
2888 inactive_value.slots[0] = 1;
2889 assert!(matches!(
2890 super::validate_checkpoint_adjacency(&inactive_value),
2891 Err(GraphBuildError::CheckpointCorrupt(_))
2892 ));
2893 assert!(matches!(
2894 super::decode_phase(3),
2895 Ok(super::BuildPhase::Complete)
2896 ));
2897 assert!(matches!(
2898 super::read_checkpoint_u8(&[], 0, "truncated"),
2899 Err(GraphBuildError::CheckpointCorrupt(_))
2900 ));
2901 }
2902
2903 #[test]
2904 fn checkpoint_parser_rejects_semantic_corruption_and_identity_drift() {
2905 let directory = tempfile::tempdir().expect("checkpoint parser directory");
2906 let reader = fixture_reader(directory.path(), 16);
2907 let checkpoint = directory.path().join("semantic.checkpoint");
2908 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
2909 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
2910 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
2911 let lease = store.snapshot().expect("checkpoint lease");
2912 let control = QueryControl::Cancel(CancelToken::new());
2913 let cancellation = QueryCancellation::new(&control, &lease);
2914 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
2915 .expect("derived policy");
2916 let mut session = GraphBuildSession::new(
2917 &StdVfs,
2918 &reader,
2919 params,
2920 0x0001_9000_3c0d_ec01,
2921 GraphBuildPasses::Two,
2922 &checkpoint,
2923 policy,
2924 None,
2925 )
2926 .expect("checkpoint session");
2927 assert!(
2928 !session
2929 .advance_batch(&cancellation)
2930 .expect("checkpoint batch")
2931 );
2932 let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
2933 validate_graph_build_checkpoint(&valid).expect("valid checkpoint parses");
2934
2935 let mut bad_checksum = valid.clone();
2936 bad_checksum[40] ^= 1;
2937 let corruptions = [
2938 bad_checksum,
2939 checkpoint_with_mutation(valid.clone(), |bytes| {
2940 bytes[8..10].copy_from_slice(&2_u16.to_le_bytes());
2941 }),
2942 checkpoint_with_mutation(valid.clone(), |bytes| bytes[10] = 9),
2943 checkpoint_with_mutation(valid.clone(), |bytes| bytes[11] = 9),
2944 checkpoint_with_mutation(valid.clone(), |bytes| bytes[14] = 1),
2945 checkpoint_with_mutation(valid.clone(), |bytes| bytes[12] = 0),
2946 checkpoint_with_mutation(valid.clone(), |bytes| bytes[20..24].fill(0)),
2947 checkpoint_with_mutation(valid.clone(), |bytes| {
2948 bytes[32..36].copy_from_slice(&17_u32.to_le_bytes());
2949 }),
2950 checkpoint_with_mutation(valid.clone(), |bytes| {
2951 bytes[48..52].copy_from_slice(&f32::NAN.to_bits().to_le_bytes());
2952 }),
2953 checkpoint_with_mutation(valid.clone(), |bytes| bytes[72..80].fill(0)),
2954 checkpoint_with_mutation(valid.clone(), |bytes| {
2955 bytes[80] = 1;
2956 bytes[96..100].copy_from_slice(&0_u32.to_le_bytes());
2957 }),
2958 checkpoint_with_mutation(valid.clone(), |bytes| {
2959 bytes[80] = 0;
2960 bytes[96..100].copy_from_slice(&1_u32.to_le_bytes());
2961 }),
2962 ];
2963 for corrupt in corruptions {
2964 assert!(matches!(
2965 validate_graph_build_checkpoint(&corrupt),
2966 Err(GraphBuildError::CheckpointCorrupt(_))
2967 ));
2968 }
2969
2970 let vectors = SegmentVectors::new(&reader).expect("checkpoint vectors");
2971 assert!(matches!(
2972 decode_checkpoint(
2973 &valid,
2974 &vectors,
2975 params,
2976 0x0001_9000_3c0d_ec02,
2977 GraphBuildPasses::Two,
2978 None,
2979 ),
2980 Err(GraphBuildError::CheckpointCorrupt(_))
2981 ));
2982 assert!(matches!(
2983 decode_checkpoint(
2984 &valid,
2985 &vectors,
2986 params,
2987 0x0001_9000_3c0d_ec01,
2988 GraphBuildPasses::One,
2989 None,
2990 ),
2991 Err(GraphBuildError::CheckpointCorrupt(_))
2992 ));
2993 let one_pass_refine = checkpoint_with_mutation(valid, |bytes| {
2994 bytes[10] = 2;
2995 bytes[11] = 1;
2996 });
2997 assert!(matches!(
2998 decode_checkpoint(
2999 &one_pass_refine,
3000 &vectors,
3001 params,
3002 0x0001_9000_3c0d_ec01,
3003 GraphBuildPasses::One,
3004 None,
3005 ),
3006 Err(GraphBuildError::CheckpointCorrupt(_))
3007 ));
3008 }
3009
3010 #[test]
3011 fn corrupt_checkpoint_refuses_once_clears_the_file_and_the_next_session_starts_fresh() {
3012 let directory = tempfile::tempdir().expect("corrupt checkpoint directory");
3013 let reader = fixture_reader(directory.path(), 16);
3014 let checkpoint = directory.path().join("corrupt.checkpoint");
3015 let temporary = super::checkpoint_temp_path(&checkpoint);
3016 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
3017 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
3018 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3019 let lease = store.snapshot().expect("checkpoint lease");
3020 let control = QueryControl::Cancel(CancelToken::new());
3021 let cancellation = QueryCancellation::new(&control, &lease);
3022 let mut session = GraphBuildSession::new(
3023 &StdVfs,
3024 &reader,
3025 params,
3026 0x0001_9000_c077_0001,
3027 GraphBuildPasses::Two,
3028 &checkpoint,
3029 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3030 .expect("derived policy"),
3031 None,
3032 )
3033 .expect("checkpoint session");
3034 assert!(
3035 !session
3036 .advance_batch(&cancellation)
3037 .expect("checkpoint batch")
3038 );
3039 drop(session);
3040
3041 let mut corrupt = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
3042 corrupt[40] ^= 1;
3043 StdVfs
3044 .write(&checkpoint, &corrupt)
3045 .expect("write corrupt checkpoint");
3046 StdVfs
3047 .write(&temporary, b"stale temporary checkpoint")
3048 .expect("write stale temporary checkpoint");
3049
3050 assert!(matches!(
3051 GraphBuildSession::new(
3052 &StdVfs,
3053 &reader,
3054 params,
3055 0x0001_9000_c077_0001,
3056 GraphBuildPasses::Two,
3057 &checkpoint,
3058 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3059 .expect("derived policy"),
3060 None,
3061 ),
3062 Err(GraphBuildError::CheckpointCorrupt(_))
3063 ));
3064 assert_eq!(
3065 StdVfs
3066 .read(&checkpoint)
3067 .expect_err("corrupt checkpoint was retained")
3068 .kind(),
3069 std::io::ErrorKind::NotFound
3070 );
3071 assert_eq!(
3072 StdVfs
3073 .read(&temporary)
3074 .expect_err("stale checkpoint temporary was retained")
3075 .kind(),
3076 std::io::ErrorKind::NotFound
3077 );
3078
3079 let fresh = GraphBuildSession::new(
3080 &StdVfs,
3081 &reader,
3082 params,
3083 0x0001_9000_c077_0001,
3084 GraphBuildPasses::Two,
3085 &checkpoint,
3086 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3087 .expect("derived policy"),
3088 None,
3089 )
3090 .expect("next session starts fresh");
3091 assert_eq!(fresh.completed_work_rows().expect("fresh work rows"), 0);
3092 }
3093
3094 #[test]
3095 fn accounting_rejection_on_resume_preserves_the_valid_checkpoint() {
3096 let directory = tempfile::tempdir().expect("accounting checkpoint directory");
3097 let reader = fixture_reader(directory.path(), 16);
3098 let checkpoint = directory.path().join("accounting.checkpoint");
3099 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("checkpoint params");
3100 let store_directory = tempfile::tempdir().expect("checkpoint store directory");
3101 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3102 let lease = store.snapshot().expect("checkpoint lease");
3103 let control = QueryControl::Cancel(CancelToken::new());
3104 let cancellation = QueryCancellation::new(&control, &lease);
3105 let mut session = GraphBuildSession::new(
3106 &StdVfs,
3107 &reader,
3108 params,
3109 0x0001_9000_acc0_0001,
3110 GraphBuildPasses::Two,
3111 &checkpoint,
3112 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3113 .expect("derived policy"),
3114 None,
3115 )
3116 .expect("checkpoint session");
3117 assert!(
3118 !session
3119 .advance_batch(&cancellation)
3120 .expect("checkpoint batch")
3121 );
3122 drop(session);
3123 let valid = StdVfs.read(&checkpoint).expect("valid checkpoint bytes");
3124
3125 let refused_directory = tempfile::tempdir().expect("refused store directory");
3126 let refused = Store::open(
3127 refused_directory.path(),
3128 OpenOptions::default().with_max_temp_bytes(1),
3129 )
3130 .expect("refused store");
3131 assert!(matches!(
3132 GraphBuildSession::new(
3133 &StdVfs,
3134 &reader,
3135 params,
3136 0x0001_9000_acc0_0001,
3137 GraphBuildPasses::Two,
3138 &checkpoint,
3139 DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3140 .expect("derived policy"),
3141 Some(&refused.accounting),
3142 ),
3143 Err(GraphBuildError::Store(_))
3144 ));
3145 assert_eq!(
3146 StdVfs
3147 .read(&checkpoint)
3148 .expect("accounting rejection removed the valid checkpoint"),
3149 valid
3150 );
3151 }
3152
3153 #[test]
3154 fn incomplete_session_and_checkpoint_io_fail_typed() {
3155 let directory = tempfile::tempdir().expect("typed failure directory");
3156 let reader = fixture_reader(directory.path(), 16);
3157 let params = GraphParams::new(4, 6, 1.0, 1.2, 8, 4).expect("typed failure params");
3158 let control = QueryControl::Cancel(CancelToken::new());
3159 let store_directory = tempfile::tempdir().expect("typed failure store");
3160 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3161 let lease = store.snapshot().expect("typed failure lease");
3162 let cancellation = QueryCancellation::new(&control, &lease);
3163 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3164 .expect("derived policy");
3165
3166 let unfinished = GraphBuildSession::new(
3167 &StdVfs,
3168 &reader,
3169 params,
3170 0x0001_9000_3100_0001,
3171 GraphBuildPasses::Two,
3172 &directory.path().join("unfinished.checkpoint"),
3173 policy,
3174 None,
3175 )
3176 .expect("unfinished session");
3177 assert!(matches!(
3178 unfinished.artifact(),
3179 Err(GraphBuildError::Geometry(_))
3180 ));
3181
3182 assert!(matches!(
3183 GraphBuildSession::new(
3184 &StdVfs,
3185 &reader,
3186 params,
3187 0x0001_9000_3100_0001,
3188 GraphBuildPasses::Two,
3189 directory.path(),
3190 policy,
3191 None,
3192 ),
3193 Err(GraphBuildError::CheckpointIo { .. })
3194 ));
3195
3196 let missing_parent = directory.path().join("missing").join("write.checkpoint");
3197 let mut unwritable = GraphBuildSession::new(
3198 &StdVfs,
3199 &reader,
3200 params,
3201 0x0001_9000_3100_0001,
3202 GraphBuildPasses::Two,
3203 &missing_parent,
3204 policy,
3205 None,
3206 )
3207 .expect("unwritable session starts before checkpoint write");
3208 assert!(matches!(
3209 unwritable.advance_batch(&cancellation),
3210 Err(GraphBuildError::CheckpointIo { .. })
3211 ));
3212
3213 let timeout = QueryControl::Cancel(CancelToken::new());
3214 timeout.mark_timed_out();
3215 let timed_out = QueryCancellation::new(&timeout, &lease);
3216 assert!(matches!(
3217 super::check_cancellation(&timed_out),
3218 Err(GraphBuildError::Timeout { partial: false })
3219 ));
3220 }
3221
3222 #[test]
3223 fn vamana_build_is_deterministic_under_seed() {
3224 let directory = tempfile::tempdir().expect("fixture directory");
3225 let reader = fixture_reader(directory.path(), 48);
3226 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("test params");
3227 let mut random = crate::test_support::seeded_rng(
3228 "graph::build::vamana_build_is_deterministic_under_seed",
3229 );
3230 let seed = random.next_u64();
3231 let first = build_graph(&reader, params, seed, GraphBuildPasses::Two)
3232 .expect("first deterministic build");
3233 let second = build_graph(&reader, params, seed, GraphBuildPasses::Two)
3234 .expect("second deterministic build");
3235 assert_eq!(first.encoded_region(), second.encoded_region());
3236
3237 let dimensions = 513;
3240 let mut rescore = vec![1.0_f32; dimensions * 2];
3241 rescore[0] = 134_217_728.0;
3242 rescore[dimensions..].fill(0.0);
3243 let code_stride = dimensions.div_ceil(2);
3244 let mut codes = vec![0_u8; code_stride * 2];
3245 let mut factors = Vec::new();
3246 for (row, output) in rescore
3247 .chunks_exact(dimensions)
3248 .zip(codes.chunks_exact_mut(code_stride))
3249 {
3250 factors.push(quantize_bit4(row, output).expect("513D arithmetic fixture quantizes"));
3251 }
3252 let vectors = SegmentVectors::from_native(dimensions, &codes, &factors, &rescore)
3253 .expect("513D arithmetic fixture geometry");
3254 let exact =
3255 super::exact_row_distance(&rescore, dimensions, 0, 1).expect("exact row distance");
3256 let adjacency = Adjacency::new(2, 1).expect("two-row arithmetic adjacency");
3257 let candidates = super::search_candidates(
3258 &vectors,
3259 &adjacency,
3260 &[0, 1],
3261 0,
3262 &[1],
3263 1,
3264 seed,
3265 &mut [0; 2],
3266 &mut 0,
3267 )
3268 .expect("candidate search");
3269 assert_eq!(candidates.len(), 1);
3270 assert_eq!(candidates[0].node_id, 1);
3271 assert_eq!(
3272 (exact.to_bits(), candidates[0].distance.to_bits()),
3273 (0x4350_0000_0000_0000, 0x4350_0000_0000_0000),
3274 "exact distance and candidate query norm retain sequential arithmetic",
3275 );
3276 #[cfg(feature = "allocation-audit")]
3277 {
3278 let mut allocation_observations = Vec::new();
3279 for ordinal in [1, 2] {
3280 let mut visited = [0; 2];
3281 let mut visit_epoch = 0;
3282 let (result, fires) =
3283 crate::allocation_audit::fail_attributed_allocation(ordinal, || {
3284 super::search_candidates(
3285 &vectors,
3286 &adjacency,
3287 &[0, 1],
3288 0,
3289 &[1],
3290 1,
3291 seed,
3292 &mut visited,
3293 &mut visit_epoch,
3294 )
3295 });
3296 allocation_observations.push((
3297 fires,
3298 matches!(
3299 result,
3300 Err(GraphBuildError::Store(
3301 crate::lifecycle::StoreError::AllocationFailed { needed: 513, .. }
3302 ))
3303 ),
3304 ));
3305 }
3306 assert_eq!(
3307 allocation_observations,
3308 [(1, true), (1, true)],
3309 "both actual query-code allocation failures retain their cause"
3310 );
3311 let restored = super::search_candidates(
3312 &vectors,
3313 &adjacency,
3314 &[0, 1],
3315 0,
3316 &[1],
3317 1,
3318 seed,
3319 &mut [0; 2],
3320 &mut 0,
3321 )
3322 .expect("restored query allocation control");
3323 assert_eq!(restored.len(), 1);
3324 assert_eq!(restored[0].node_id, 1);
3325 assert_eq!(restored[0].distance.to_bits(), 0x4350_0000_0000_0000);
3326 }
3327 }
3328
3329 #[test]
3330 fn cancelled_build_leaves_no_partial_artifact() {
3331 let directory = tempfile::tempdir().expect("sealed segment directory");
3332 let reader = fixture_reader(directory.path(), 48);
3333 let segment_path = directory.path().join(reader.meta().id.file_name());
3334 let before = StdVfs
3335 .read(&segment_path)
3336 .expect("sealed bytes before build");
3337 let checkpoint = directory.path().join("graph-build.checkpoint");
3338 let checkpoint_temp = directory.path().join("graph-build.checkpoint.tmp");
3339 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
3340 let store_directory = tempfile::tempdir().expect("store directory");
3341 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3342 let lease = store.snapshot().expect("snapshot lease");
3343 let token = CancelToken::new();
3344 let control = QueryControl::Cancel(token.clone());
3345 let cancellation = QueryCancellation::new(&control, &lease);
3346 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3347 .expect("derived policy");
3348 let mut session = GraphBuildSession::new(
3349 &StdVfs,
3350 &reader,
3351 params,
3352 0x19_0003,
3353 GraphBuildPasses::Two,
3354 &checkpoint,
3355 policy,
3356 None,
3357 )
3358 .expect("build session");
3359
3360 assert!(!session.advance_batch(&cancellation).expect("first batch"));
3361 token.cancel();
3362 assert!(matches!(
3363 session.advance_batch(&cancellation),
3364 Err(GraphBuildError::Cancelled { partial: false })
3365 ));
3366 assert_eq!(
3367 StdVfs
3368 .read(&segment_path)
3369 .expect("sealed bytes after cancel"),
3370 before
3371 );
3372 assert!(
3373 checkpoint.exists(),
3374 "completed batch checkpoint must remain resumable"
3375 );
3376 assert!(
3377 !checkpoint_temp.exists(),
3378 "checkpoint temp must not survive"
3379 );
3380 }
3381
3382 #[test]
3383 fn build_resumes_from_checkpoint_to_the_same_graph() {
3384 let directory = tempfile::tempdir().expect("sealed segment directory");
3385 let reader = fixture_reader(directory.path(), 64);
3386 let checkpoint = directory.path().join("resume.checkpoint");
3387 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 8).expect("test params");
3388 let store_directory = tempfile::tempdir().expect("store directory");
3389 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3390 let lease = store.snapshot().expect("snapshot lease");
3391 let control = QueryControl::Cancel(CancelToken::new());
3392 let cancellation = QueryCancellation::new(&control, &lease);
3393 let policy = DurabilityPolicy::new(DurabilityMode::Derived, CommitTier::None)
3394 .expect("derived policy");
3395 let mut interrupted = GraphBuildSession::new(
3396 &StdVfs,
3397 &reader,
3398 params,
3399 0x19_0003_00c0_ffee,
3400 GraphBuildPasses::Two,
3401 &checkpoint,
3402 policy,
3403 None,
3404 )
3405 .expect("interrupted session");
3406 assert!(
3407 !interrupted
3408 .advance_batch(&cancellation)
3409 .expect("first batch")
3410 );
3411 drop(interrupted);
3412
3413 let mut resumed = GraphBuildSession::new(
3414 &StdVfs,
3415 &reader,
3416 params,
3417 0x19_0003_00c0_ffee,
3418 GraphBuildPasses::Two,
3419 &checkpoint,
3420 policy,
3421 None,
3422 )
3423 .expect("resumed session");
3424 while !resumed.advance_batch(&cancellation).expect("resumed batch") {}
3425 let resumed = resumed.artifact().expect("resumed artifact");
3426 let uninterrupted =
3427 build_graph(&reader, params, 0x19_0003_00c0_ffee, GraphBuildPasses::Two)
3428 .expect("uninterrupted artifact");
3429 assert_eq!(resumed.encoded_region(), uninterrupted.encoded_region());
3430 assert!(!checkpoint.exists(), "completed build removes checkpoint");
3431 }
3432
3433 #[test]
3434 fn checkpointed_build_reports_exact_work_for_each_declared_pass() {
3435 let directory = tempfile::tempdir().expect("work-count fixture directory");
3436 let rows = 24_usize;
3437 let reader = fixture_reader(directory.path(), rows);
3438 let params = GraphParams::new(6, 10, 1.0, 1.2, 16, 6).expect("work-count params");
3439 let store_directory = tempfile::tempdir().expect("work-count store directory");
3440 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3441 let lease = store.snapshot().expect("work-count lease");
3442 let control = QueryControl::Cancel(CancelToken::new());
3443
3444 for (passes, expected_rows, suffix) in [
3445 (GraphBuildPasses::One, rows as u64, "one"),
3446 (GraphBuildPasses::Two, (rows * 2) as u64, "two"),
3447 ] {
3448 let checkpoint = directory
3449 .path()
3450 .join(format!("work-count-{suffix}.checkpoint"));
3451 let artifact = build_graph_checkpointed(
3452 &store,
3453 &reader,
3454 CheckpointedGraphBuild::new(params, 0x19_0003_c0a7, passes, &checkpoint, &control),
3455 &lease,
3456 )
3457 .expect("checkpointed build completes");
3458
3459 assert_eq!(artifact.work_rows_completed(), expected_rows);
3460 assert_eq!(
3461 decode_node_blocks(artifact.encoded_region())
3462 .expect("work-count graph decodes")
3463 .node_count(),
3464 rows as u32
3465 );
3466 assert!(
3467 !checkpoint.exists(),
3468 "completed build removes {suffix} checkpoint"
3469 );
3470 }
3471 }
3472
3473 #[test]
3474 fn graph_build_handles_the_minimum_shape_and_rejects_an_empty_segment() {
3475 let directory = tempfile::tempdir().expect("minimum-shape directory");
3476 let empty = fixture_reader(directory.path(), 0);
3477 assert!(matches!(
3478 SegmentVectors::new(&empty),
3479 Err(GraphBuildError::Geometry(detail)) if detail.contains("at least one row")
3480 ));
3481
3482 let one_row_directory = tempfile::tempdir().expect("one-row directory");
3483 let one_row = fixture_reader(one_row_directory.path(), 1);
3484 let params = GraphParams::new(1, 1, 1.0, 1.0, 1, 1).expect("one-row params");
3485 let artifact = build_graph(&one_row, params, 0x19_0003_0000_0001, GraphBuildPasses::One)
3486 .expect("one-row build succeeds");
3487 let graph = decode_node_blocks(artifact.encoded_region()).expect("one-row graph decodes");
3488 let node = graph.block(0).expect("row zero exists");
3489 assert_eq!(graph.node_count(), 1);
3490 assert_eq!(node.degree(), 0);
3491 assert_eq!(node.flags() & 1, 1);
3492 assert_eq!(artifact.entry_points(), &[0]);
3493 }
3494
3495 #[test]
3496 fn every_node_has_at_most_r_max_neighbours() {
3497 let mut seeded = crate::test_support::seeded_rng(
3498 "graph::build::every_node_has_at_most_r_max_neighbours",
3499 );
3500 let mut runner = TestRunner::new(Config {
3501 cases: 24,
3502 rng_seed: RngSeed::Fixed(seeded.next_u64()),
3503 ..Config::default()
3504 });
3505 let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
3506 let result = runner.run(&strategy, |(rows, r_target, seed)| {
3507 let directory = tempfile::tempdir().expect("property directory");
3508 let reader = fixture_reader(directory.path(), rows);
3509 let r_max = r_target.saturating_add(4);
3510 let params = GraphParams::new(
3511 r_target,
3512 r_max,
3513 1.0,
3514 1.2,
3515 u16::from(r_max).saturating_add(8),
3516 rows as u32,
3517 )
3518 .expect("property params");
3519 let artifact =
3520 build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
3521 let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3522 prop_assert_eq!(graph.layout().max_degree(), r_max);
3523 for node_id in 0..graph.node_count() {
3524 let block = graph.block(node_id).expect("property node");
3525 prop_assert!(block.degree() <= r_max);
3526 }
3527 Ok(())
3528 });
3529 assert!(result.is_ok(), "property result: {result:?}");
3530 }
3531
3532 #[test]
3533 fn no_node_lists_itself_or_a_duplicate() {
3534 let mut seeded =
3535 crate::test_support::seeded_rng("graph::build::no_node_lists_itself_or_a_duplicate");
3536 let mut runner = TestRunner::new(Config {
3537 cases: 24,
3538 rng_seed: RngSeed::Fixed(seeded.next_u64()),
3539 ..Config::default()
3540 });
3541 let strategy = (6_usize..40, 2_u8..=8, any::<u64>());
3542 let result = runner.run(&strategy, |(rows, r_target, seed)| {
3543 let directory = tempfile::tempdir().expect("property directory");
3544 let reader = fixture_reader(directory.path(), rows);
3545 let r_max = r_target.saturating_add(4);
3546 let params = GraphParams::new(
3547 r_target,
3548 r_max,
3549 1.0,
3550 1.2,
3551 u16::from(r_max).saturating_add(8),
3552 rows as u32,
3553 )
3554 .expect("property params");
3555 let artifact =
3556 build_graph(&reader, params, seed, GraphBuildPasses::Two).expect("property graph");
3557 let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3558 for node_id in 0..graph.node_count() {
3559 let block = graph.block(node_id).expect("property node");
3560 let mut unique = std::collections::BTreeSet::new();
3561 for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
3562 prop_assert_ne!(neighbor, node_id);
3563 prop_assert!(unique.insert(neighbor));
3564 }
3565 }
3566 Ok(())
3567 });
3568 assert!(result.is_ok(), "property result: {result:?}");
3569 }
3570
3571 #[test]
3572 fn graph_is_connected_from_the_entry_points() {
3573 let directory = tempfile::tempdir().expect("connectivity directory");
3574 let reader = fixture_reader(directory.path(), 96);
3575 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 96).expect("connectivity params");
3576 let artifact = build_graph(
3577 &reader,
3578 params,
3579 0x0001_9000_3c01_1ec7,
3580 GraphBuildPasses::Two,
3581 )
3582 .expect("connectivity graph");
3583 let graph = decode_node_blocks(artifact.encoded_region()).expect("decoded graph");
3584 let mut reached = vec![false; graph.node_count() as usize];
3585 let mut pending = std::collections::VecDeque::new();
3586 for &entry in artifact.entry_points() {
3587 pending.push_back(entry);
3588 }
3589 while let Some(node_id) = pending.pop_front() {
3590 let node = node_id as usize;
3591 if reached[node] {
3592 continue;
3593 }
3594 reached[node] = true;
3595 let block = graph.block(node_id).expect("reachable node");
3596 for neighbor in block.neighbors_padded().take(usize::from(block.degree())) {
3597 if !reached[neighbor as usize] {
3598 pending.push_back(neighbor);
3599 }
3600 }
3601 }
3602 let reached_count = reached.iter().filter(|seen| **seen).count();
3603 assert_eq!(reached_count, graph.node_count() as usize);
3604 }
3605
3606 #[test]
3607 fn graph_build_memory_is_budgeted_and_exactly_accounted() {
3608 let directory = tempfile::tempdir().expect("accounting fixture directory");
3609 let reader = fixture_reader(directory.path(), 48);
3610 let params = GraphParams::new(8, 12, 1.0, 1.2, 24, 48).expect("accounting params");
3611 let control = QueryControl::Cancel(CancelToken::new());
3612
3613 let refused_directory = tempfile::tempdir().expect("refused store directory");
3614 let refused = Store::open(
3615 refused_directory.path(),
3616 OpenOptions::default().with_max_temp_bytes(1),
3617 )
3618 .expect("refused store");
3619 let refused_lease = refused.snapshot().expect("refused lease");
3620 let refused_result = build_graph_checkpointed(
3621 &refused,
3622 &reader,
3623 CheckpointedGraphBuild::new(
3624 params,
3625 0x19_0003_acc0,
3626 GraphBuildPasses::One,
3627 &directory.path().join("refused.checkpoint"),
3628 &control,
3629 ),
3630 &refused_lease,
3631 );
3632 assert!(matches!(refused_result, Err(GraphBuildError::Store(_))));
3633 assert_eq!(refused.stats().expect("refused stats").temporary_bytes, 0);
3634
3635 let store_directory = tempfile::tempdir().expect("accounted store directory");
3636 let store = Store::open(store_directory.path(), OpenOptions::default()).expect("store");
3637 let lease = store.snapshot().expect("accounted lease");
3638 let artifact = build_graph_checkpointed(
3639 &store,
3640 &reader,
3641 CheckpointedGraphBuild::new(
3642 params,
3643 0x19_0003_acc0,
3644 GraphBuildPasses::One,
3645 &directory.path().join("accounted.checkpoint"),
3646 &control,
3647 ),
3648 &lease,
3649 )
3650 .expect("accounted graph");
3651 let expected =
3652 artifact.encoded_region().len() + std::mem::size_of_val(artifact.entry_points());
3653 assert_eq!(
3654 store.stats().expect("live graph stats").temporary_bytes,
3655 expected as u64
3656 );
3657 drop(artifact);
3658 assert_eq!(
3659 store.stats().expect("released graph stats").temporary_bytes,
3660 0
3661 );
3662 }
3663
3664 #[test]
3665 fn arbitrary_checkpoint_bytes_fail_typed_without_panicking() {
3666 let mut seeded = crate::test_support::seeded_rng(
3667 "graph::build::arbitrary_checkpoint_bytes_fail_typed_without_panicking",
3668 );
3669 let mut runner = TestRunner::new(Config {
3670 cases: 64,
3671 rng_seed: RngSeed::Fixed(seeded.next_u64()),
3672 ..Config::default()
3673 });
3674 let result = runner.run(&proptest::collection::vec(any::<u8>(), 0..2048), |bytes| {
3675 prop_assert!(matches!(
3676 validate_graph_build_checkpoint(&bytes),
3677 Err(GraphBuildError::CheckpointCorrupt(_))
3678 ));
3679 Ok(())
3680 });
3681 assert!(result.is_ok(), "property result: {result:?}");
3682 }
3683}