prolly/prolly/proximity/accelerator/
catalog.rs1use super::composite::{config_fingerprint as composite_fingerprint, CompositeAccelerator};
2use super::hnsw::storage::config_fingerprint as hnsw_fingerprint;
3use super::pq::config_fingerprint as pq_fingerprint;
4use super::{AcceleratorSet, HnswIndex, ProductQuantizer};
5use crate::prolly::cid::Cid;
6use crate::prolly::content_graph::{ContentObjectKind, TypedContentRoot};
7use crate::prolly::error::Error;
8use crate::prolly::proximity::storage::codec::{put_cid, put_varint, Reader, MAX_OBJECT_ENTRIES};
9use crate::prolly::proximity::ProximityTree;
10use crate::prolly::store::{NodePublication, PublicationOrigin, Store};
11
12const MAGIC: &[u8; 4] = b"PACL";
13const VERSION: u8 = 1;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum CatalogAcceleratorKind {
17 Hnsw,
18 ProductQuantized,
19 Composite,
20}
21
22impl CatalogAcceleratorKind {
23 pub(crate) const fn id(self) -> u8 {
24 match self {
25 Self::Hnsw => 1,
26 Self::ProductQuantized => 2,
27 Self::Composite => 3,
28 }
29 }
30
31 fn from_id(id: u8) -> Result<Self, Error> {
32 match id {
33 1 => Ok(Self::Hnsw),
34 2 => Ok(Self::ProductQuantized),
35 3 => Ok(Self::Composite),
36 _ => Err(invalid("unknown catalog accelerator kind")),
37 }
38 }
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct AcceleratorCatalogEntry {
43 pub kind: CatalogAcceleratorKind,
44 pub configuration_fingerprint: Cid,
45 pub manifest: Cid,
46}
47
48pub struct AcceleratorCatalog<S: Store> {
49 manifest: Cid,
50 source: Cid,
51 entries: Vec<AcceleratorCatalogEntry>,
52 accelerators: AcceleratorSet<S>,
53}
54
55impl<S> AcceleratorCatalog<S>
56where
57 S: Store + Clone + Send + Sync,
58 S::Error: Send + Sync,
59{
60 pub fn build(
61 store: S,
62 source: &ProximityTree,
63 accelerators: AcceleratorSet<S>,
64 ) -> Result<Self, Error> {
65 let entries = entries_from_set(&accelerators);
66 if entries.is_empty() {
67 return Err(invalid("accelerator catalog must not be empty"));
68 }
69 let object = Manifest {
70 source: source.descriptor.clone(),
71 entries: entries.clone(),
72 };
73 let bytes = object.encode()?;
74 let manifest = Cid::from_bytes(&bytes);
75 put_content(&store, &manifest, &bytes)?;
76 Ok(Self {
77 manifest,
78 source: source.descriptor.clone(),
79 entries,
80 accelerators,
81 })
82 }
83
84 pub fn load(store: S, manifest: Cid, source: &ProximityTree) -> Result<Self, Error> {
85 let bytes = load_content(&store, &manifest)?;
86 let object = Manifest::decode(&bytes)?;
87 if object.source != source.descriptor {
88 return Err(invalid("catalog is bound to a different source snapshot"));
89 }
90 let mut accelerators = AcceleratorSet::empty();
91 for entry in &object.entries {
92 accelerators = match entry.kind {
93 CatalogAcceleratorKind::Hnsw => {
94 let index = HnswIndex::load(store.clone(), entry.manifest.clone())?;
95 if hnsw_fingerprint(index.config()) != entry.configuration_fingerprint {
96 return Err(invalid("catalog HNSW fingerprint mismatch"));
97 }
98 accelerators.with_hnsw(source, index)?
99 }
100 CatalogAcceleratorKind::ProductQuantized => {
101 let index = ProductQuantizer::load(store.clone(), entry.manifest.clone())?;
102 if pq_fingerprint(index.config()) != entry.configuration_fingerprint {
103 return Err(invalid("catalog PQ fingerprint mismatch"));
104 }
105 accelerators.with_pq(source, index)?
106 }
107 CatalogAcceleratorKind::Composite => {
108 let index = CompositeAccelerator::load(store.clone(), entry.manifest.clone())?;
109 if composite_fingerprint(index.config()) != entry.configuration_fingerprint {
110 return Err(invalid("catalog composite fingerprint mismatch"));
111 }
112 accelerators.with_composite(source, index)?
113 }
114 };
115 }
116 Ok(Self {
117 manifest,
118 source: object.source,
119 entries: object.entries,
120 accelerators,
121 })
122 }
123
124 pub fn manifest_cid(&self) -> &Cid {
125 &self.manifest
126 }
127 pub fn typed_root(&self) -> TypedContentRoot {
128 TypedContentRoot::new(ContentObjectKind::AcceleratorCatalog, self.manifest.clone())
129 }
130 pub fn source_descriptor(&self) -> &Cid {
131 &self.source
132 }
133 pub fn entries(&self) -> &[AcceleratorCatalogEntry] {
134 &self.entries
135 }
136 pub fn accelerators(&self) -> &AcceleratorSet<S> {
137 &self.accelerators
138 }
139 pub fn into_accelerators(self) -> AcceleratorSet<S> {
140 self.accelerators
141 }
142}
143
144fn entries_from_set<S>(set: &AcceleratorSet<S>) -> Vec<AcceleratorCatalogEntry>
145where
146 S: Store + Clone + Send + Sync,
147 S::Error: Send + Sync,
148{
149 let mut entries = Vec::new();
150 if let Some(index) = set.hnsw() {
151 entries.push(AcceleratorCatalogEntry {
152 kind: CatalogAcceleratorKind::Hnsw,
153 configuration_fingerprint: hnsw_fingerprint(index.config()),
154 manifest: index.manifest_cid().clone(),
155 });
156 }
157 if let Some(index) = set.pq() {
158 entries.push(AcceleratorCatalogEntry {
159 kind: CatalogAcceleratorKind::ProductQuantized,
160 configuration_fingerprint: pq_fingerprint(index.config()),
161 manifest: index.manifest_cid().clone(),
162 });
163 }
164 if let Some(index) = set.composite() {
165 entries.push(AcceleratorCatalogEntry {
166 kind: CatalogAcceleratorKind::Composite,
167 configuration_fingerprint: composite_fingerprint(index.config()),
168 manifest: index.manifest_cid().clone(),
169 });
170 }
171 entries.sort_by(compare_entry);
172 entries
173}
174
175#[derive(Clone)]
176pub(crate) struct Manifest {
177 pub(crate) source: Cid,
178 pub(crate) entries: Vec<AcceleratorCatalogEntry>,
179}
180
181impl Manifest {
182 pub(crate) fn encode(&self) -> Result<Vec<u8>, Error> {
183 self.validate()?;
184 let mut bytes = Vec::new();
185 bytes.extend_from_slice(MAGIC);
186 bytes.push(VERSION);
187 put_cid(&self.source, &mut bytes);
188 put_varint(self.entries.len() as u64, &mut bytes);
189 for entry in &self.entries {
190 bytes.push(entry.kind.id());
191 put_cid(&entry.configuration_fingerprint, &mut bytes);
192 put_cid(&entry.manifest, &mut bytes);
193 }
194 Ok(bytes)
195 }
196
197 pub(crate) fn decode(bytes: &[u8]) -> Result<Self, Error> {
198 let mut reader = Reader::new(bytes, "accelerator catalog");
199 reader.exact(MAGIC)?;
200 if reader.u8()? != VERSION {
201 return Err(reader.invalid("unsupported accelerator catalog version"));
202 }
203 let source = reader.cid()?;
204 let count = reader.bounded_usize(MAX_OBJECT_ENTRIES)?;
205 let mut entries = Vec::with_capacity(count);
206 for _ in 0..count {
207 entries.push(AcceleratorCatalogEntry {
208 kind: CatalogAcceleratorKind::from_id(reader.u8()?)?,
209 configuration_fingerprint: reader.cid()?,
210 manifest: reader.cid()?,
211 });
212 }
213 reader.finish()?;
214 let object = Self { source, entries };
215 object.validate()?;
216 Ok(object)
217 }
218
219 fn validate(&self) -> Result<(), Error> {
220 if self.entries.is_empty()
221 || self.entries.windows(2).any(|pair| {
222 compare_entry(&pair[0], &pair[1]) != std::cmp::Ordering::Less
223 || pair[0].kind == pair[1].kind
224 })
225 {
226 return Err(invalid(
227 "catalog entries must be sorted, unique, and contain one entry per kind",
228 ));
229 }
230 Ok(())
231 }
232}
233
234fn compare_entry(
235 left: &AcceleratorCatalogEntry,
236 right: &AcceleratorCatalogEntry,
237) -> std::cmp::Ordering {
238 left.kind
239 .cmp(&right.kind)
240 .then_with(|| {
241 left.configuration_fingerprint
242 .as_bytes()
243 .cmp(right.configuration_fingerprint.as_bytes())
244 })
245 .then_with(|| left.manifest.as_bytes().cmp(right.manifest.as_bytes()))
246}
247
248fn load_content<S: Store>(store: &S, cid: &Cid) -> Result<Vec<u8>, Error> {
249 let bytes = store
250 .get(cid.as_bytes())
251 .map_err(|error| Error::Store(Box::new(error)))?
252 .ok_or_else(|| Error::NotFound(cid.clone()))?;
253 let actual = Cid::from_bytes(&bytes);
254 if actual != *cid {
255 return Err(Error::CidMismatch {
256 expected: cid.clone(),
257 actual,
258 });
259 }
260 Ok(bytes)
261}
262
263fn put_content<S: Store>(store: &S, cid: &Cid, bytes: &[u8]) -> Result<(), Error> {
264 if let Some(existing) = store
265 .get(cid.as_bytes())
266 .map_err(|error| Error::Store(Box::new(error)))?
267 {
268 let actual = Cid::from_bytes(&existing);
269 if actual != *cid {
270 return Err(Error::CidMismatch {
271 expected: cid.clone(),
272 actual,
273 });
274 }
275 return Ok(());
276 }
277 let entries = [(cid.as_bytes(), bytes)];
278 store
279 .publish_nodes(NodePublication::new(
280 &entries,
281 PublicationOrigin::Maintenance,
282 ))
283 .map_err(|error| Error::Store(Box::new(error)))
284}
285
286fn invalid(reason: impl Into<String>) -> Error {
287 Error::InvalidProximityObject {
288 kind: "accelerator catalog",
289 reason: reason.into(),
290 }
291}