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