uqa_storage/diskann_index/build/
generation.rs1use sha2::{Digest, Sha256};
8use uqa_core::memory::BudgetedVec;
9
10use super::{invalid, DiskANNBuildInput, DiskANNMergedGraph};
11use crate::diskann_index::format::{
12 encode_page, DiskANNArtifactDigests, DiskANNBuildProvenance, DiskANNGeneration,
13 DiskANNManifest, DiskANNManifestInput, DiskANNNodeInput, DiskANNNodeLayout, DiskANNSideEntry,
14 DiskANNSideLayout, PAGE_HEADER_BYTES, PAGE_PAYLOAD_BYTES,
15};
16use crate::diskann_index::pages::{DiskANNMemoryBuilder, DiskANNRecordKey};
17use crate::diskann_index::PQTrainingOptions;
18use crate::{
19 key_value::KeyValueDiskANNStage, read_control::StorageReadControl, StorageBackendResult,
20};
21
22#[cfg(test)]
23mod tests;
24mod vectors;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct DiskANNGenerationOptions {
29 pub training: PQTrainingOptions,
30 pub code_batch_nodes: usize,
31 pub side_batch_entries: usize,
32 pub max_record_bytes: usize,
33}
34
35pub trait DiskANNBuildSink {
37 fn generation(&self) -> DiskANNGeneration;
38 fn write_record(
39 &mut self,
40 key: DiskANNRecordKey,
41 bytes: &[u8],
42 maximum: usize,
43 control: &StorageReadControl,
44 ) -> StorageBackendResult<()>;
45 fn write_graph_page(
46 &mut self,
47 id: u64,
48 bytes: &[u8],
49 control: &StorageReadControl,
50 ) -> StorageBackendResult<()>;
51}
52
53impl DiskANNBuildSink for DiskANNMemoryBuilder {
54 fn generation(&self) -> DiskANNGeneration {
55 self.generation()
56 }
57 fn write_record(
58 &mut self,
59 key: DiskANNRecordKey,
60 bytes: &[u8],
61 maximum: usize,
62 control: &StorageReadControl,
63 ) -> StorageBackendResult<()> {
64 control.check_value_size(bytes.len(), maximum)?;
65 DiskANNMemoryBuilder::write_record(self, key, bytes, control)
66 }
67 fn write_graph_page(
68 &mut self,
69 id: u64,
70 bytes: &[u8],
71 control: &StorageReadControl,
72 ) -> StorageBackendResult<()> {
73 DiskANNMemoryBuilder::write_graph_page(self, id, bytes, control)
74 }
75}
76
77impl DiskANNBuildSink for KeyValueDiskANNStage {
78 fn generation(&self) -> DiskANNGeneration {
79 self.generation()
80 }
81 fn write_record(
82 &mut self,
83 key: DiskANNRecordKey,
84 bytes: &[u8],
85 maximum: usize,
86 control: &StorageReadControl,
87 ) -> StorageBackendResult<()> {
88 KeyValueDiskANNStage::write_record(self, key, bytes, maximum, control)
89 }
90 fn write_graph_page(
91 &mut self,
92 id: u64,
93 bytes: &[u8],
94 control: &StorageReadControl,
95 ) -> StorageBackendResult<()> {
96 KeyValueDiskANNStage::write_graph_page(self, id, bytes, control)
97 }
98}
99
100impl DiskANNBuildInput {
101 pub fn write_generation(
103 &self,
104 graph: &DiskANNMergedGraph,
105 options: DiskANNGenerationOptions,
106 sink: &mut dyn DiskANNBuildSink,
107 ) -> StorageBackendResult<DiskANNManifest> {
108 graph.check_input(self)?;
109 let generation = self.coverage.generation();
110 let parameters = graph.summary().partitions.parameters;
111 if sink.generation() != generation {
112 return Err(invalid("artifact sink generation differs"));
113 }
114 self.control.check_value_size(
115 DiskANNManifest::ENCODED_BYTES + DiskANNBuildProvenance::ENCODED_BYTES,
116 options.max_record_bytes,
117 )?;
118 let provenance = DiskANNBuildProvenance::from_merge(
119 graph.summary(),
120 self.node_count(),
121 options.training,
122 options.code_batch_nodes,
123 options.side_batch_entries,
124 )?;
125 let mut artifacts = DiskANNArtifactDigests::empty();
126 vectors::quantize(self, parameters.pq_bytes, options, sink, &mut artifacts)?;
127 let entry_node = vectors::entry(self, parameters.seed)?;
128 artifacts.side = self.write_sides(options, sink)?;
129 artifacts.graph = self.write_graph(graph, sink)?;
130 self.control.check()?;
131 DiskANNManifest::new(DiskANNManifestInput {
132 generation,
133 dimensions: self.dimensions,
134 parameters,
135 nodes: self.node_count(),
136 side_vectors: self.side_count(),
137 entry_node,
138 coverage: self.coverage,
139 artifacts,
140 })?
141 .with_build_provenance(provenance)
142 }
143
144 fn write_sides(
145 &self,
146 options: DiskANNGenerationOptions,
147 sink: &mut dyn DiskANNBuildSink,
148 ) -> StorageBackendResult<[u8; 32]> {
149 let layout = DiskANNSideLayout::new(
150 self.coverage.generation(),
151 self.dimensions,
152 self.side_count(),
153 )?;
154 let capacity = self.side_count().min(options.side_batch_entries as u64) as usize;
155 let mut entries = BudgetedVec::new(self.control.memory());
156 entries.reserve(capacity)?;
157 let mut first = 0;
158 let mut hash = Sha256::new();
159 for id in 0..self.side_count() {
160 let record = self.read_side(id)?;
161 entries.push(DiskANNSideEntry::from_raw(
162 self.dimensions,
163 record.doc_id(),
164 record.ordinal(),
165 record.version(),
166 record.raw(),
167 &self.control,
168 )?)?;
169 if entries.len() == capacity || id + 1 == self.side_count() {
170 let bytes = layout.encode(first, &entries, &self.control)?;
171 self.control
172 .check_value_size(bytes.len(), options.max_record_bytes)?;
173 let batch = layout.decode(first, &bytes, &self.control)?;
174 for chunk in batch.bytes().chunks(4096) {
175 self.control.check()?;
176 hash.update(chunk);
177 }
178 sink.write_record(
179 DiskANNRecordKey::Side(first),
180 &bytes,
181 options.max_record_bytes,
182 &self.control,
183 )?;
184 first = id + 1;
185 entries.clear();
186 }
187 }
188 Ok(hash.finalize().into())
189 }
190
191 fn write_graph(
192 &self,
193 graph: &DiskANNMergedGraph,
194 sink: &mut dyn DiskANNBuildSink,
195 ) -> StorageBackendResult<[u8; 32]> {
196 let layout = DiskANNNodeLayout::new(
197 self.dimensions,
198 graph.summary().partitions.parameters.max_degree,
199 self.node_count(),
200 )?;
201 let generation = self.coverage.generation();
202 let mut payload = BudgetedVec::new(self.control.memory());
203 let mut page = 0;
204 let mut hash = Sha256::new();
205 graph.visit_neighbors(&mut |node_id, neighbors| {
206 let record = self.read_node(node_id)?;
207 let encoded = layout.encode_node(
208 &DiskANNNodeInput {
209 node_id,
210 doc_id: record.doc_id(),
211 ordinal: record.ordinal(),
212 version: record.version(),
213 vector: record.raw(),
214 neighbors,
215 },
216 &self.control,
217 )?;
218 let expected = layout.page_shape(page)?.payload_bytes as usize;
219 let mut emit = |payload: &[u8]| -> StorageBackendResult<()> {
220 let bytes = encode_page(generation, layout, page, payload, &self.control)?;
221 hash.update(&bytes[PAGE_HEADER_BYTES - 32..PAGE_HEADER_BYTES]);
222 sink.write_graph_page(page, &bytes, &self.control)?;
223 page += 1;
224 Ok(())
225 };
226 if layout.node_address(node_id)?.fragments > 1 {
227 for fragment in encoded.chunks(PAGE_PAYLOAD_BYTES) {
228 self.control.check()?;
229 emit(fragment)?;
230 }
231 } else {
232 if payload.is_empty() {
233 payload.reserve(expected)?;
234 }
235 payload.extend_from_slice(&encoded)?;
236 if payload.len() == expected {
237 emit(&payload)?;
238 payload.clear();
239 }
240 }
241 Ok(())
242 })?;
243 if page != layout.page_count() || !payload.is_empty() {
244 return Err(invalid("incomplete graph page output"));
245 }
246 Ok(hash.finalize().into())
247 }
248}