uqa_storage/diskann_index/build/
partitions.rs1use sha2::{Digest, Sha256};
8use std::path::Path;
9
10use super::runs::{RunReader, RunWriter};
11use super::temporary::TemporaryRun;
12use super::{invalid, DiskANNBuildCoverage, DiskANNBuildInput};
13use crate::diskann_index::PQTrainingOptions;
14use crate::{
15 read_control::StorageReadControl, vector_index::DiskANNIndexParams, StorageBackendResult,
16};
17
18mod leaf;
19mod source;
20#[cfg(test)]
21mod tests;
22
23use source::Ids;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct DiskANNPartitionOptions {
28 pub max_partition_points: usize,
29 pub coarse_training: PQTrainingOptions,
30 pub max_depth: u8,
31}
32
33impl DiskANNPartitionOptions {
34 pub const WORK_ORDER_REVISION: u32 = 1;
35
36 pub(in crate::diskann_index) fn validate(self) -> StorageBackendResult<Self> {
37 self.coarse_training.validate()?;
38 if self.max_partition_points < 2
39 || self.max_depth > 32
40 || self.coarse_training.max_centroids < 3
41 || self.coarse_training.max_samples < u32::from(self.coarse_training.max_centroids)
42 {
43 return Err(invalid("partition capacity must be at least two, depth at most 32 and coarse samples must cover at least three centroids"));
44 }
45 Ok(self)
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct DiskANNPartitionSummary {
52 pub work_order_revision: u32,
53 pub options: DiskANNPartitionOptions,
54 pub parameters: DiskANNIndexParams,
55 pub coverage: DiskANNBuildCoverage,
56 pub partitions: u64,
57 pub memberships: u64,
58 pub edges: u64,
59 pub maximum_depth: u8,
60 pub maximum_partition_points: usize,
61 pub assignment_digest: [u8; 32],
62}
63
64pub struct DiskANNPartitionRuns {
66 edges: TemporaryRun,
67 summary: DiskANNPartitionSummary,
68 nodes: u64,
69 control: StorageReadControl,
70}
71
72impl DiskANNPartitionRuns {
73 pub fn summary(&self) -> &DiskANNPartitionSummary {
74 &self.summary
75 }
76
77 pub(super) fn into_source(
78 self,
79 input: &DiskANNBuildInput,
80 ) -> StorageBackendResult<(TemporaryRun, DiskANNPartitionSummary)> {
81 input.control.check()?;
82 if self.nodes != input.node_count()
83 || self.summary.coverage != input.coverage
84 || !self.control.shares_context(&input.control)
85 || !self.edges.uses_allowance(&input.temporary)
86 {
87 return Err(invalid(
88 "partition source, coverage or build allowance differs",
89 ));
90 }
91 Ok((self.edges, self.summary))
92 }
93
94 pub fn visit_edges(
95 &self,
96 visitor: &mut dyn FnMut(u64, u64) -> StorageBackendResult<()>,
97 ) -> StorageBackendResult<()> {
98 self.edges.read(&self.control, |file| {
99 if self.summary.edges == 0 {
100 return Ok(());
101 }
102 let mut reader = RunReader::new(file, &self.control)?;
103 for _ in 0..self.summary.edges {
104 let bytes = reader.record::<16>()?;
105 let source = u64::from_le_bytes(bytes[..8].try_into().expect("source ID"));
106 let neighbor = u64::from_le_bytes(bytes[8..].try_into().expect("neighbor ID"));
107 if source >= self.nodes || neighbor >= self.nodes || source == neighbor {
108 return Err(invalid("invalid global partition edge"));
109 }
110 visitor(source, neighbor)?;
111 }
112 Ok(())
113 })
114 }
115}
116
117impl DiskANNBuildInput {
118 pub fn build_partitions(
120 &self,
121 directory: &Path,
122 parameters: DiskANNIndexParams,
123 options: DiskANNPartitionOptions,
124 ) -> StorageBackendResult<DiskANNPartitionRuns> {
125 self.control.check()?;
126 let parameters = parameters.validate(self.dimensions)?;
127 let options = options.validate()?;
128 let writer = RunWriter::new(directory, &self.temporary, &self.control)?;
129 let mut builder = Builder {
130 input: self,
131 directory,
132 writer,
133 summary: DiskANNPartitionSummary {
134 work_order_revision: DiskANNPartitionOptions::WORK_ORDER_REVISION,
135 options,
136 parameters,
137 coverage: self.coverage,
138 partitions: 0,
139 memberships: 0,
140 edges: 0,
141 maximum_depth: 0,
142 maximum_partition_points: 0,
143 assignment_digest: [0; 32],
144 },
145 hash: provenance(self.coverage, parameters, options),
146 };
147 builder.partition(Ids::All(self.node_count()), 0, options.coarse_training.seed)?;
148 self.control.check()?;
149 builder
150 .hash
151 .update(builder.summary.partitions.to_le_bytes());
152 builder
153 .hash
154 .update(builder.summary.memberships.to_le_bytes());
155 builder.summary.assignment_digest = builder.hash.finalize().into();
156 Ok(DiskANNPartitionRuns {
157 edges: builder.writer.finish()?,
158 summary: builder.summary,
159 nodes: self.node_count(),
160 control: self.control.clone(),
161 })
162 }
163}
164
165struct Builder<'a> {
166 input: &'a DiskANNBuildInput,
167 directory: &'a Path,
168 writer: RunWriter,
169 summary: DiskANNPartitionSummary,
170 hash: Sha256,
171}
172
173impl Builder<'_> {
174 fn partition(&mut self, source: Ids<'_>, depth: u8, seed: u64) -> StorageBackendResult<()> {
175 self.input.control.check()?;
176 let count = source.len();
177 if count == 0 {
178 return Ok(());
179 }
180 self.summary.maximum_depth = self.summary.maximum_depth.max(depth);
181 let options = self.summary.options;
182 if count <= options.max_partition_points as u64 || depth == options.max_depth {
183 return self.capacity(source);
184 }
185 let split = source::split(
186 self.input,
187 source,
188 self.directory,
189 options.coarse_training,
190 seed,
191 )?;
192 let reduces = split
193 .counts
194 .iter()
195 .all(|&child| u128::from(child) * 4 <= u128::from(count) * 3);
196 self.hash.update([b'S', depth, u8::from(reduces)]);
197 self.hash.update(seed.to_le_bytes());
198 self.hash.update(split.digest);
199 if !reduces {
200 drop(split);
201 return self.capacity(source);
202 }
203 for label in 0..split.counts.len() {
204 self.partition(
205 Ids::Child(&split, label as u8),
206 depth + 1,
207 source::child_seed(seed, label as u8),
208 )?;
209 }
210 Ok(())
211 }
212
213 fn capacity(&mut self, source: Ids<'_>) -> StorageBackendResult<()> {
214 leaf::windows(
215 source,
216 self.summary.options.max_partition_points,
217 &self.input.control,
218 &mut |ids| self.leaf(ids),
219 )
220 }
221}
222
223fn provenance(
224 coverage: DiskANNBuildCoverage,
225 parameters: DiskANNIndexParams,
226 options: DiskANNPartitionOptions,
227) -> Sha256 {
228 let mut hash = Sha256::new();
229 hash.update(b"UQA DiskANN partitions\0\x01");
230 hash.update(coverage.digest());
231 for value in [
232 parameters.max_degree as u64,
233 parameters.build_list_size as u64,
234 parameters.search_list_size as u64,
235 parameters.alpha.get().to_bits(),
236 parameters.beam_width as u64,
237 parameters.pq_bytes as u64,
238 parameters.seed,
239 u64::from(parameters.format_revision),
240 u64::from(parameters.algorithm_revision),
241 options.max_partition_points as u64,
242 u64::from(options.max_depth),
243 u64::from(options.coarse_training.max_samples),
244 u64::from(options.coarse_training.max_iterations),
245 u64::from(options.coarse_training.max_centroids),
246 options.coarse_training.seed,
247 ] {
248 hash.update(value.to_le_bytes());
249 }
250 hash
251}