uqa_storage/diskann_index/format/
quantization.rs1use uqa_core::memory::BudgetedVec;
8
9use super::{field, invalid, record, zeros, DiskANNGeneration, DiskANNManifest};
10use crate::diskann_index::{
11 metric::checkpoint,
12 pq::{PQCodebook, PQTrainingOptions, PQTrainingSummary},
13};
14use crate::{read_control::StorageReadControl, StorageBackendResult};
15
16const MAGIC: [u8; 8] = *b"UQADNPQ\0";
17const METADATA_BYTES: usize = 64;
18const SCALAR_REVISION: u32 = 1;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct DiskANNQuantizationIdentity {
22 pub(super) generation: DiskANNGeneration,
23 pub(super) dimensions: u32,
24 pub(super) chunks: usize,
25 pub(super) centroids: u16,
26 pub(super) nodes: u64,
27 pub(super) digest: [u8; 32],
28}
29
30impl DiskANNQuantizationIdentity {
31 pub fn codebook_digest(self) -> [u8; 32] {
32 self.digest
33 }
34 pub fn node_count(self) -> u64 {
35 self.nodes
36 }
37 pub fn pq_bytes(self) -> usize {
38 self.chunks
39 }
40
41 fn new(generation: DiskANNGeneration, codebook: &PQCodebook, digest: [u8; 32]) -> Self {
42 Self {
43 generation,
44 dimensions: codebook.dimensions() as u32,
45 chunks: codebook.pq_bytes(),
46 centroids: codebook.centroid_count(),
47 nodes: codebook.training().observed_vectors,
48 digest,
49 }
50 }
51}
52
53fn size(chunks: usize, scalars: usize) -> StorageBackendResult<usize> {
54 chunks
55 .checked_add(1)
56 .and_then(|count| count.checked_mul(4))
57 .and_then(|offsets| {
58 scalars
59 .checked_mul(8)
60 .and_then(|coordinates| offsets.checked_add(coordinates))
61 })
62 .and_then(|body| body.checked_add(METADATA_BYTES))
63 .ok_or_else(|| invalid("codebook body size overflow"))
64}
65
66pub fn encode_codebook(
67 generation: DiskANNGeneration,
68 codebook: &PQCodebook,
69 control: &StorageReadControl,
70) -> StorageBackendResult<(BudgetedVec<u8>, DiskANNQuantizationIdentity)> {
71 let summary = codebook.training();
72 let options = summary.options;
73 let dimensions =
74 u32::try_from(codebook.dimensions()).map_err(|_| invalid("codebook dimension range"))?;
75 let chunks = codebook.pq_bytes();
76 let count = codebook.centroid_count();
77 let scalars = summary.validate(dimensions, chunks, count)?;
78 let mut bytes = record::begin(MAGIC, generation, size(chunks, scalars)?, control)?;
79 bytes.extend_from_slice(&dimensions.to_le_bytes())?;
80 bytes.extend_from_slice(&(chunks as u32).to_le_bytes())?;
81 bytes.extend_from_slice(&count.to_le_bytes())?;
82 bytes.extend_from_slice(&[0; 2])?;
83 for value in [
84 SCALAR_REVISION,
85 PQCodebook::CODEC_REVISION,
86 PQCodebook::TRAINING_REVISION,
87 options.max_samples,
88 options.max_iterations,
89 ] {
90 bytes.extend_from_slice(&value.to_le_bytes())?;
91 }
92 bytes.extend_from_slice(&options.max_centroids.to_le_bytes())?;
93 bytes.extend_from_slice(&[0; 2])?;
94 bytes.extend_from_slice(&summary.sampled_vectors.to_le_bytes())?;
95 for value in [options.seed, summary.observed_vectors, scalars as u64] {
96 bytes.extend_from_slice(&value.to_le_bytes())?;
97 }
98 for chunk in 0..chunks {
99 checkpoint(chunk, control)?;
100 let start = codebook.chunk_range(chunk).expect("validated chunk").start;
101 bytes.extend_from_slice(&(start as u32).to_le_bytes())?;
102 }
103 bytes.extend_from_slice(&dimensions.to_le_bytes())?;
104 for chunk in 0..chunks {
105 for (offset, coordinate) in codebook
106 .chunk_centroids(chunk)
107 .expect("validated chunk")
108 .iter()
109 .enumerate()
110 {
111 checkpoint(offset, control)?;
112 bytes.extend_from_slice(&coordinate.to_bits().to_le_bytes())?;
113 }
114 }
115 let bytes = record::finish(bytes, control)?;
116 let digest = record::artifact_digest(&bytes, control)?;
117 Ok((
118 bytes,
119 DiskANNQuantizationIdentity::new(generation, codebook, digest),
120 ))
121}
122
123pub fn decode_codebook(
124 manifest: &DiskANNManifest,
125 bytes: &[u8],
126 control: &StorageReadControl,
127) -> StorageBackendResult<(PQCodebook, DiskANNQuantizationIdentity)> {
128 let input = manifest.input();
129 let body = record::open(MAGIC, input.generation, bytes, control)?;
130 if input.nodes == 0
131 || body.len() < METADATA_BYTES
132 || record::artifact_digest(bytes, control)? != input.artifacts.codebook
133 {
134 return Err(invalid(
135 "codebook is absent or differs from manifest digest",
136 ));
137 }
138 let dimensions = record::u32_at(body, 0)?;
139 let chunks = record::u32_at(body, 4)? as usize;
140 let count = u16::from_le_bytes(field(body, 8)?);
141 zeros(&body[10..12], control)?;
142 zeros(&body[34..36], control)?;
143 for (offset, expected) in [
144 (12, SCALAR_REVISION),
145 (16, PQCodebook::CODEC_REVISION),
146 (20, PQCodebook::TRAINING_REVISION),
147 ] {
148 if record::u32_at(body, offset)? != expected {
149 return Err(invalid(
150 "unrecognized codebook scalar, codec or training revision",
151 ));
152 }
153 }
154 let summary = PQTrainingSummary {
155 options: PQTrainingOptions {
156 max_samples: record::u32_at(body, 24)?,
157 max_iterations: record::u32_at(body, 28)?,
158 max_centroids: u16::from_le_bytes(field(body, 32)?),
159 seed: record::u64_at(body, 40)?,
160 },
161 sampled_vectors: record::u32_at(body, 36)?,
162 observed_vectors: record::u64_at(body, 48)?,
163 };
164 if dimensions != input.dimensions
165 || chunks != input.parameters.pq_bytes
166 || summary.observed_vectors != input.nodes
167 || summary.options.seed != input.parameters.seed
168 || manifest
169 .build_provenance()
170 .is_some_and(|build| build.training() != summary.options)
171 {
172 return Err(invalid(
173 "codebook shape, corpus or seed differs from manifest",
174 ));
175 }
176 let scalars = summary.validate(dimensions, chunks, count)?;
177 if record::u64_at(body, 56)? != scalars as u64 || body.len() != size(chunks, scalars)? {
178 return Err(invalid("codebook scalar count or length differs"));
179 }
180 for chunk in 0..=chunks {
181 checkpoint(chunk, control)?;
182 let expected = if chunk == chunks {
183 dimensions as usize
184 } else {
185 crate::diskann_index::pq::chunk_range(dimensions as usize, chunks, chunk).start
186 };
187 if record::u32_at(body, METADATA_BYTES + chunk * 4)? as usize != expected {
188 return Err(invalid("codebook chunk offsets differ"));
189 }
190 }
191 let mut centroids = BudgetedVec::new(control.memory());
192 centroids.reserve(scalars)?;
193 let start = METADATA_BYTES + (chunks + 1) * 4;
194 for (offset, scalar) in body[start..].chunks_exact(8).enumerate() {
195 checkpoint(offset, control)?;
196 centroids.push(f64::from_bits(u64::from_le_bytes(field(scalar, 0)?)))?;
197 }
198 let codebook = PQCodebook::restore(dimensions, chunks, count, summary, centroids, control)?;
199 let identity =
200 DiskANNQuantizationIdentity::new(input.generation, &codebook, input.artifacts.codebook);
201 Ok((codebook, identity))
202}