1use crate::VectorIndex;
10use ipfrs_core::{Cid, Error, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::{Arc, RwLock};
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct ModelVersion {
18 pub major: u32,
20 pub minor: u32,
22 pub patch: u32,
24 pub tag: Option<String>,
26}
27
28impl ModelVersion {
29 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
31 Self {
32 major,
33 minor,
34 patch,
35 tag: None,
36 }
37 }
38
39 pub fn with_tag(mut self, tag: String) -> Self {
41 self.tag = Some(tag);
42 self
43 }
44
45 pub fn is_compatible_with(&self, other: &ModelVersion) -> bool {
47 self.major == other.major
48 }
49}
50
51impl std::fmt::Display for ModelVersion {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
54 if let Some(tag) = &self.tag {
55 write!(f, "-{}", tag)?;
56 }
57 Ok(())
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct EmbeddingTransform {
64 pub from_version: ModelVersion,
66 pub to_version: ModelVersion,
68 pub transform_matrix: Option<Vec<Vec<f32>>>,
70 pub bias: Option<Vec<f32>>,
72}
73
74impl EmbeddingTransform {
75 pub fn identity(version: ModelVersion) -> Self {
77 Self {
78 from_version: version.clone(),
79 to_version: version,
80 transform_matrix: None,
81 bias: None,
82 }
83 }
84
85 pub fn linear(
87 from_version: ModelVersion,
88 to_version: ModelVersion,
89 matrix: Vec<Vec<f32>>,
90 ) -> Self {
91 Self {
92 from_version,
93 to_version,
94 transform_matrix: Some(matrix),
95 bias: None,
96 }
97 }
98
99 pub fn apply(&self, embedding: &[f32]) -> Vec<f32> {
101 let mut result = embedding.to_vec();
102
103 if let Some(matrix) = &self.transform_matrix {
105 let out_dim = matrix[0].len();
106 let mut transformed = vec![0.0; out_dim];
107
108 for (i, row) in matrix.iter().enumerate() {
109 if i >= embedding.len() {
110 break;
111 }
112 for (j, &val) in row.iter().enumerate() {
113 transformed[j] += embedding[i] * val;
114 }
115 }
116
117 result = transformed;
118 }
119
120 if let Some(bias) = &self.bias {
122 for (i, &b) in bias.iter().enumerate() {
123 if i < result.len() {
124 result[i] += b;
125 }
126 }
127 }
128
129 result
130 }
131}
132
133pub struct DynamicIndex {
135 indices: Arc<RwLock<HashMap<ModelVersion, VectorIndex>>>,
137 active_version: Arc<RwLock<ModelVersion>>,
139 transforms: Arc<RwLock<HashMap<(ModelVersion, ModelVersion), EmbeddingTransform>>>,
141 dimension: usize,
143}
144
145impl DynamicIndex {
146 pub fn new(initial_version: ModelVersion, dimension: usize) -> Result<Self> {
148 let mut indices = HashMap::new();
149 let index = VectorIndex::with_defaults(dimension)?;
150 indices.insert(initial_version.clone(), index);
151
152 Ok(Self {
153 indices: Arc::new(RwLock::new(indices)),
154 active_version: Arc::new(RwLock::new(initial_version)),
155 transforms: Arc::new(RwLock::new(HashMap::new())),
156 dimension,
157 })
158 }
159
160 pub fn active_version(&self) -> ModelVersion {
162 self.active_version
163 .read()
164 .unwrap_or_else(|e| e.into_inner())
165 .clone()
166 }
167
168 pub fn add_version(
170 &self,
171 version: ModelVersion,
172 transform: Option<EmbeddingTransform>,
173 ) -> Result<()> {
174 let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
175
176 if indices.contains_key(&version) {
177 return Err(Error::InvalidInput(format!(
178 "Version {} already exists",
179 version
180 )));
181 }
182
183 let index = VectorIndex::with_defaults(self.dimension)?;
184 indices.insert(version.clone(), index);
185
186 if let Some(t) = transform {
188 let mut transforms = self.transforms.write().unwrap_or_else(|e| e.into_inner());
189 transforms.insert((t.from_version.clone(), t.to_version.clone()), t);
190 }
191
192 Ok(())
193 }
194
195 pub fn set_active_version(&self, version: ModelVersion) -> Result<()> {
197 let indices = self.indices.read().unwrap_or_else(|e| e.into_inner());
198
199 if !indices.contains_key(&version) {
200 return Err(Error::InvalidInput(format!(
201 "Version {} does not exist",
202 version
203 )));
204 }
205
206 let mut active = self
207 .active_version
208 .write()
209 .unwrap_or_else(|e| e.into_inner());
210 *active = version;
211
212 Ok(())
213 }
214
215 pub fn insert(
217 &self,
218 cid: &Cid,
219 embedding: &[f32],
220 version: Option<ModelVersion>,
221 ) -> Result<()> {
222 let version = version.unwrap_or_else(|| self.active_version());
223
224 let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
225 let index = indices
226 .get_mut(&version)
227 .ok_or_else(|| Error::InvalidInput(format!("Version {} does not exist", version)))?;
228
229 index.insert(cid, embedding)?;
230 Ok(())
231 }
232
233 pub fn update(
235 &self,
236 cid: &Cid,
237 new_embedding: &[f32],
238 version: Option<ModelVersion>,
239 ) -> Result<()> {
240 let version = version.unwrap_or_else(|| self.active_version());
241
242 let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
243 let index = indices
244 .get_mut(&version)
245 .ok_or_else(|| Error::InvalidInput(format!("Version {} does not exist", version)))?;
246
247 index.delete(cid)?;
249 index.insert(cid, new_embedding)?;
251
252 Ok(())
253 }
254
255 pub fn migrate(&self, from: &ModelVersion, to: &ModelVersion) -> Result<usize> {
257 let transforms = self.transforms.read().unwrap_or_else(|e| e.into_inner());
258 let transform = transforms
259 .get(&(from.clone(), to.clone()))
260 .ok_or_else(|| Error::InvalidInput(format!("No transform from {} to {}", from, to)))?
261 .clone();
262 drop(transforms);
263
264 let indices = self.indices.read().unwrap_or_else(|e| e.into_inner());
266 let source_index = indices.get(from).ok_or_else(|| {
267 Error::InvalidInput(format!("Source version {} does not exist", from))
268 })?;
269
270 if !indices.contains_key(to) {
272 return Err(Error::InvalidInput(format!(
273 "Target version {} does not exist",
274 to
275 )));
276 }
277
278 let embeddings = source_index.get_all_embeddings();
280 drop(indices);
281
282 let mut migrated_count = 0;
284 for (cid, embedding) in embeddings {
285 let transformed = transform.apply(&embedding);
287
288 let mut indices = self.indices.write().unwrap_or_else(|e| e.into_inner());
290 if let Some(target_index) = indices.get_mut(to) {
291 if !target_index.contains(&cid) {
293 target_index.insert(&cid, &transformed)?;
294 migrated_count += 1;
295 }
296 }
297 drop(indices);
298 }
299
300 Ok(migrated_count)
301 }
302
303 pub fn version_stats(&self) -> HashMap<ModelVersion, VersionStats> {
305 let indices = self.indices.read().unwrap_or_else(|e| e.into_inner());
306
307 indices
308 .iter()
309 .map(|(version, index)| {
310 let stats = VersionStats {
311 version: version.clone(),
312 num_embeddings: index.len(),
313 is_active: version == &self.active_version(),
314 };
315 (version.clone(), stats)
316 })
317 .collect()
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct VersionStats {
324 pub version: ModelVersion,
326 pub num_embeddings: usize,
328 pub is_active: bool,
330}
331
332pub struct OnlineUpdater {
334 learning_rate: f32,
336 momentum: f32,
338 velocity: Arc<RwLock<HashMap<Cid, Vec<f32>>>>,
340}
341
342impl OnlineUpdater {
343 pub fn new(learning_rate: f32, momentum: f32) -> Self {
345 Self {
346 learning_rate,
347 momentum,
348 velocity: Arc::new(RwLock::new(HashMap::new())),
349 }
350 }
351
352 pub fn update(&self, cid: &Cid, embedding: &[f32], gradient: &[f32]) -> Vec<f32> {
354 let mut velocity = self.velocity.write().unwrap_or_else(|e| e.into_inner());
355
356 let v = velocity
358 .entry(*cid)
359 .or_insert_with(|| vec![0.0; embedding.len()]);
360
361 for i in 0..embedding.len().min(gradient.len()) {
363 v[i] = self.momentum * v[i] - self.learning_rate * gradient[i];
364 }
365
366 embedding
368 .iter()
369 .zip(v.iter())
370 .map(|(&e, &vel)| e + vel)
371 .collect()
372 }
373
374 pub fn reset(&self) {
376 let mut velocity = self.velocity.write().unwrap_or_else(|e| e.into_inner());
377 velocity.clear();
378 }
379
380 pub fn stats(&self) -> OnlineUpdaterStats {
382 let velocity = self.velocity.read().unwrap_or_else(|e| e.into_inner());
383
384 OnlineUpdaterStats {
385 learning_rate: self.learning_rate,
386 momentum: self.momentum,
387 num_tracked: velocity.len(),
388 }
389 }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct OnlineUpdaterStats {
395 pub learning_rate: f32,
397 pub momentum: f32,
399 pub num_tracked: usize,
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_model_version() {
409 let v1 = ModelVersion::new(1, 0, 0);
410 let v2 = ModelVersion::new(1, 1, 0);
411 let v3 = ModelVersion::new(2, 0, 0);
412
413 assert!(v1.is_compatible_with(&v2));
414 assert!(!v1.is_compatible_with(&v3));
415
416 assert_eq!(v1.to_string(), "1.0.0");
417 assert_eq!(v1.with_tag("alpha".into()).to_string(), "1.0.0-alpha");
418 }
419
420 #[test]
421 fn test_embedding_transform() {
422 let v1 = ModelVersion::new(1, 0, 0);
423 let v2 = ModelVersion::new(1, 1, 0);
424
425 let identity = EmbeddingTransform::identity(v1.clone());
427 let embedding = vec![1.0, 2.0, 3.0];
428 let result = identity.apply(&embedding);
429 assert_eq!(result, embedding);
430
431 let matrix = vec![vec![1.0, 0.0], vec![0.0, 2.0]];
433 let transform = EmbeddingTransform::linear(v1, v2, matrix);
434 let embedding = vec![1.0, 2.0];
435 let result = transform.apply(&embedding);
436 assert_eq!(result, vec![1.0, 4.0]);
437 }
438
439 #[test]
440 fn test_dynamic_index_creation() {
441 let version = ModelVersion::new(1, 0, 0);
442 let index = DynamicIndex::new(version.clone(), 128)
443 .expect("test: DynamicIndex creation should succeed");
444
445 assert_eq!(index.active_version(), version);
446 }
447
448 #[test]
449 fn test_add_version() {
450 let v1 = ModelVersion::new(1, 0, 0);
451 let v2 = ModelVersion::new(1, 1, 0);
452
453 let index =
454 DynamicIndex::new(v1.clone(), 128).expect("test: DynamicIndex creation should succeed");
455 index
456 .add_version(v2.clone(), None)
457 .expect("test: add_version should succeed");
458
459 let stats = index.version_stats();
460 assert_eq!(stats.len(), 2);
461 assert!(stats.contains_key(&v1));
462 assert!(stats.contains_key(&v2));
463 }
464
465 #[test]
466 fn test_set_active_version() {
467 let v1 = ModelVersion::new(1, 0, 0);
468 let v2 = ModelVersion::new(1, 1, 0);
469
470 let index =
471 DynamicIndex::new(v1.clone(), 128).expect("test: DynamicIndex creation should succeed");
472 index
473 .add_version(v2.clone(), None)
474 .expect("test: add_version should succeed");
475
476 assert_eq!(index.active_version(), v1);
477
478 index
479 .set_active_version(v2.clone())
480 .expect("test: set_active_version should succeed");
481 assert_eq!(index.active_version(), v2);
482 }
483
484 #[test]
485 fn test_insert_and_update() {
486 use multihash_codetable::{Code, MultihashDigest};
487
488 let version = ModelVersion::new(1, 0, 0);
489 let index =
490 DynamicIndex::new(version, 3).expect("test: DynamicIndex creation should succeed");
491
492 let data = "test_embedding";
493 let hash = Code::Sha2_256.digest(data.as_bytes());
494 let cid = Cid::new_v1(0x55, hash);
495
496 let embedding = vec![1.0, 2.0, 3.0];
497 index
498 .insert(&cid, &embedding, None)
499 .expect("test: insert should succeed");
500
501 let stats = index.version_stats();
502 assert_eq!(
503 stats
504 .values()
505 .next()
506 .expect("test: stats should have at least one entry")
507 .num_embeddings,
508 1
509 );
510
511 let new_embedding = vec![4.0, 5.0, 6.0];
513 index
514 .update(&cid, &new_embedding, None)
515 .expect("test: update should succeed");
516
517 let stats = index.version_stats();
518 assert_eq!(
519 stats
520 .values()
521 .next()
522 .expect("test: stats should have at least one entry")
523 .num_embeddings,
524 1
525 );
526 }
527
528 #[test]
529 fn test_online_updater() {
530 use multihash_codetable::{Code, MultihashDigest};
531
532 let updater = OnlineUpdater::new(0.1, 0.9);
533
534 let data = "test";
535 let hash = Code::Sha2_256.digest(data.as_bytes());
536 let cid = Cid::new_v1(0x55, hash);
537
538 let embedding = vec![1.0, 1.0, 1.0];
539 let gradient = vec![0.1, 0.1, 0.1];
540
541 let updated = updater.update(&cid, &embedding, &gradient);
542
543 assert!(updated[0] < 1.0);
545 assert_eq!(updated.len(), 3);
546
547 let stats = updater.stats();
548 assert_eq!(stats.num_tracked, 1);
549 }
550
551 #[test]
552 fn test_updater_momentum() {
553 use multihash_codetable::{Code, MultihashDigest};
554
555 let updater = OnlineUpdater::new(0.1, 0.9);
556
557 let data = "test";
558 let hash = Code::Sha2_256.digest(data.as_bytes());
559 let cid = Cid::new_v1(0x55, hash);
560
561 let embedding = vec![1.0];
562 let gradient = vec![0.1];
563
564 let updated1 = updater.update(&cid, &embedding, &gradient);
566
567 let updated2 = updater.update(&cid, &updated1, &gradient);
569
570 let delta1 = (embedding[0] - updated1[0]).abs();
572 let delta2 = (updated1[0] - updated2[0]).abs();
573 assert!(delta2 > delta1);
574 }
575}