Skip to main content

ipfrs_semantic/
embedding_pool.rs

1//! Managed pool of pre-allocated embedding buffers for zero-copy semantic search operations.
2//!
3//! Reduces allocation overhead in high-throughput workloads by reusing fixed-dimension
4//! embedding buffers across acquire/release cycles.
5
6/// A single pre-allocated embedding buffer managed by the pool.
7#[derive(Debug, Clone)]
8pub struct EmbeddingBuffer {
9    /// Unique identifier for this buffer within the pool.
10    pub buffer_id: u64,
11    /// Raw embedding data.
12    pub data: Vec<f32>,
13    /// Fixed dimension of this buffer.
14    pub dim: usize,
15    /// Whether this buffer is currently checked out by a caller.
16    pub in_use: bool,
17    /// Monotonically increasing counter, incremented each time the buffer returns to the pool.
18    pub generation: u64,
19}
20
21impl EmbeddingBuffer {
22    /// Returns `true` if this buffer's dimension matches `expected`.
23    pub fn is_valid_dim(&self, expected: usize) -> bool {
24        self.dim == expected
25    }
26}
27
28/// Configuration for [`SemanticEmbeddingPool`].
29#[derive(Debug, Clone)]
30pub struct PoolConfig {
31    /// Fixed embedding dimension shared by all buffers in the pool.
32    pub embedding_dim: usize,
33    /// Number of buffers to pre-allocate when the pool is constructed.
34    pub initial_capacity: usize,
35    /// Hard upper limit on the total number of buffers the pool may hold.
36    pub max_capacity: usize,
37}
38
39impl PoolConfig {
40    /// Build a [`PoolConfig`] with sensible defaults for the given embedding dimension.
41    pub fn default_for(dim: usize) -> Self {
42        Self {
43            embedding_dim: dim,
44            initial_capacity: 16,
45            max_capacity: 256,
46        }
47    }
48}
49
50/// Snapshot statistics for a [`SemanticEmbeddingPool`].
51#[derive(Debug, Clone, Default)]
52pub struct PoolStats {
53    /// Total number of buffers currently managed by the pool (in-use + available).
54    pub total_buffers: usize,
55    /// Number of buffers currently checked out.
56    pub in_use: usize,
57    /// Number of buffers currently available for acquisition.
58    pub available: usize,
59    /// Cumulative number of times `acquire` was called.
60    pub total_allocations: u64,
61    /// Cumulative number of times `release` was called successfully.
62    pub total_returns: u64,
63    /// Acquisitions satisfied from an existing free buffer (cache hit).
64    pub cache_hits: u64,
65    /// Acquisitions that required allocating a new buffer (cache miss).
66    pub cache_misses: u64,
67}
68
69/// Managed pool of pre-allocated, fixed-dimension embedding buffers.
70///
71/// Callers `acquire` a buffer id, `write` data into it, `read` from it, and
72/// `release` it back to the pool. Releasing zeroes the buffer data and increments
73/// the buffer's generation counter so stale ids are detectable.
74pub struct SemanticEmbeddingPool {
75    /// All buffers managed by this pool (both in-use and free).
76    pub buffers: Vec<EmbeddingBuffer>,
77    next_id: u64,
78    /// Pool configuration.
79    pub config: PoolConfig,
80    /// Accumulated runtime statistics.
81    pub stats: PoolStats,
82}
83
84impl SemanticEmbeddingPool {
85    /// Create a new pool, pre-allocating `config.initial_capacity` zeroed buffers.
86    pub fn new(config: PoolConfig) -> Self {
87        let dim = config.embedding_dim;
88        let initial = config.initial_capacity;
89        let mut buffers = Vec::with_capacity(initial);
90        for id in 0..initial as u64 {
91            buffers.push(EmbeddingBuffer {
92                buffer_id: id,
93                data: vec![0.0_f32; dim],
94                dim,
95                in_use: false,
96                generation: 0,
97            });
98        }
99        let total = buffers.len();
100        Self {
101            buffers,
102            next_id: initial as u64,
103            config,
104            stats: PoolStats {
105                total_buffers: total,
106                available: total,
107                ..Default::default()
108            },
109        }
110    }
111
112    /// Acquire a free buffer, allocating a new one if necessary.
113    ///
114    /// Returns `Some(buffer_id)` on success, or `None` when the pool is at
115    /// `max_capacity` and every buffer is already in use.
116    pub fn acquire(&mut self) -> Option<u64> {
117        self.stats.total_allocations += 1;
118
119        // Try to find an existing free buffer first (cache hit).
120        if let Some(buf) = self.buffers.iter_mut().find(|b| !b.in_use) {
121            buf.in_use = true;
122            self.stats.cache_hits += 1;
123            self.stats.in_use += 1;
124            self.stats.available = self.stats.available.saturating_sub(1);
125            return Some(buf.buffer_id);
126        }
127
128        // No free buffer – allocate a new one if below max_capacity.
129        if self.buffers.len() < self.config.max_capacity {
130            let id = self.next_id;
131            self.next_id += 1;
132            let dim = self.config.embedding_dim;
133            self.buffers.push(EmbeddingBuffer {
134                buffer_id: id,
135                data: vec![0.0_f32; dim],
136                dim,
137                in_use: true,
138                generation: 0,
139            });
140            self.stats.cache_misses += 1;
141            self.stats.total_buffers += 1;
142            self.stats.in_use += 1;
143            // available stays the same (new buffer goes straight to in_use)
144            return Some(id);
145        }
146
147        // Pool exhausted.
148        None
149    }
150
151    /// Write `data` into the buffer identified by `buffer_id`.
152    ///
153    /// Returns `false` when:
154    /// - No buffer with `buffer_id` exists.
155    /// - The buffer is not currently in use.
156    /// - `data.len()` does not match the pool's embedding dimension.
157    pub fn write(&mut self, buffer_id: u64, data: &[f32]) -> bool {
158        let dim = self.config.embedding_dim;
159        if data.len() != dim {
160            return false;
161        }
162        match self.buffers.iter_mut().find(|b| b.buffer_id == buffer_id) {
163            Some(buf) if buf.in_use => {
164                buf.data.copy_from_slice(data);
165                true
166            }
167            _ => false,
168        }
169    }
170
171    /// Read the data slice from an in-use buffer.
172    ///
173    /// Returns `None` when the buffer is not found or is not currently in use.
174    pub fn read(&self, buffer_id: u64) -> Option<&[f32]> {
175        self.buffers
176            .iter()
177            .find(|b| b.buffer_id == buffer_id && b.in_use)
178            .map(|b| b.data.as_slice())
179    }
180
181    /// Release a buffer back to the pool.
182    ///
183    /// Zeroes the buffer data and increments its generation counter.
184    /// Returns `false` if no buffer with `buffer_id` is found.
185    pub fn release(&mut self, buffer_id: u64) -> bool {
186        match self.buffers.iter_mut().find(|b| b.buffer_id == buffer_id) {
187            Some(buf) => {
188                let was_in_use = buf.in_use;
189                buf.in_use = false;
190                buf.generation += 1;
191                for v in buf.data.iter_mut() {
192                    *v = 0.0;
193                }
194                self.stats.total_returns += 1;
195                if was_in_use {
196                    self.stats.in_use = self.stats.in_use.saturating_sub(1);
197                    self.stats.available += 1;
198                }
199                true
200            }
201            None => false,
202        }
203    }
204
205    /// Release every buffer in the pool back to the free state.
206    pub fn clear_all(&mut self) {
207        for buf in self.buffers.iter_mut() {
208            if buf.in_use {
209                buf.in_use = false;
210                self.stats.in_use = self.stats.in_use.saturating_sub(1);
211                self.stats.available += 1;
212            }
213            buf.generation += 1;
214            for v in buf.data.iter_mut() {
215                *v = 0.0;
216            }
217            self.stats.total_returns += 1;
218        }
219    }
220
221    /// Return a snapshot of the current pool statistics.
222    pub fn stats(&self) -> PoolStats {
223        self.stats.clone()
224    }
225}
226
227// ---------------------------------------------------------------------------
228// Tests
229// ---------------------------------------------------------------------------
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    fn make_pool(dim: usize, initial: usize, max: usize) -> SemanticEmbeddingPool {
236        SemanticEmbeddingPool::new(PoolConfig {
237            embedding_dim: dim,
238            initial_capacity: initial,
239            max_capacity: max,
240        })
241    }
242
243    // 1. new() pre-allocates initial_capacity buffers
244    #[test]
245    fn test_new_preallocates_buffers() {
246        let pool = make_pool(4, 8, 64);
247        assert_eq!(pool.buffers.len(), 8);
248        for buf in &pool.buffers {
249            assert!(!buf.in_use);
250            assert_eq!(buf.data.len(), 4);
251            assert_eq!(buf.generation, 0);
252        }
253    }
254
255    // 2. initial stats are consistent
256    #[test]
257    fn test_new_stats_consistent() {
258        let pool = make_pool(4, 8, 64);
259        let s = pool.stats();
260        assert_eq!(s.total_buffers, 8);
261        assert_eq!(s.in_use, 0);
262        assert_eq!(s.available, 8);
263        assert_eq!(s.total_allocations, 0);
264        assert_eq!(s.total_returns, 0);
265        assert_eq!(s.cache_hits, 0);
266        assert_eq!(s.cache_misses, 0);
267    }
268
269    // 3. acquire returns buffer_id from pool (cache_hit)
270    #[test]
271    fn test_acquire_cache_hit() {
272        let mut pool = make_pool(4, 4, 16);
273        let id = pool.acquire().expect("should return id");
274        let s = pool.stats();
275        assert_eq!(s.cache_hits, 1);
276        assert_eq!(s.cache_misses, 0);
277        assert_eq!(s.total_allocations, 1);
278        // The returned id should be marked in_use
279        let buf = pool
280            .buffers
281            .iter()
282            .find(|b| b.buffer_id == id)
283            .expect("test: buffer with acquired id should exist");
284        assert!(buf.in_use);
285    }
286
287    // 4. acquire allocates new when pool fully in-use but below max (cache_miss)
288    #[test]
289    fn test_acquire_cache_miss() {
290        let mut pool = make_pool(4, 1, 16);
291        // Exhaust the single pre-allocated buffer
292        let _id1 = pool.acquire().expect("first acquire");
293        assert_eq!(pool.stats().cache_hits, 1);
294        // Second acquire must allocate
295        let _id2 = pool.acquire().expect("second acquire");
296        let s = pool.stats();
297        assert_eq!(s.cache_hits, 1);
298        assert_eq!(s.cache_misses, 1);
299        assert_eq!(s.total_buffers, 2);
300    }
301
302    // 5. acquire returns None at max_capacity with no free buffers
303    #[test]
304    fn test_acquire_returns_none_at_max() {
305        let mut pool = make_pool(4, 2, 2);
306        let _id1 = pool.acquire().expect("first");
307        let _id2 = pool.acquire().expect("second");
308        let result = pool.acquire();
309        assert!(result.is_none());
310        assert_eq!(pool.stats().total_allocations, 3);
311    }
312
313    // 6. write copies data correctly
314    #[test]
315    fn test_write_copies_data() {
316        let mut pool = make_pool(4, 2, 8);
317        let id = pool.acquire().expect("acquire");
318        let data = [1.0_f32, 2.0, 3.0, 4.0];
319        assert!(pool.write(id, &data));
320        let read_back = pool.read(id).expect("read");
321        assert_eq!(read_back, &data);
322    }
323
324    // 7. write returns false for wrong dimension
325    #[test]
326    fn test_write_wrong_dim() {
327        let mut pool = make_pool(4, 2, 8);
328        let id = pool.acquire().expect("acquire");
329        let bad = [1.0_f32, 2.0]; // only 2 elements, dim is 4
330        assert!(!pool.write(id, &bad));
331    }
332
333    // 8. write returns false for a non-in-use buffer
334    #[test]
335    fn test_write_not_in_use() {
336        let mut pool = make_pool(4, 2, 8);
337        // Buffer 0 is free (not acquired)
338        let free_id = pool.buffers[0].buffer_id;
339        assert!(!pool.write(free_id, &[1.0, 2.0, 3.0, 4.0]));
340    }
341
342    // 9. write returns false for non-existent buffer_id
343    #[test]
344    fn test_write_nonexistent_id() {
345        let mut pool = make_pool(4, 2, 8);
346        assert!(!pool.write(9999, &[1.0, 2.0, 3.0, 4.0]));
347    }
348
349    // 10. read returns data for in-use buffer
350    #[test]
351    fn test_read_in_use() {
352        let mut pool = make_pool(3, 2, 8);
353        let id = pool.acquire().expect("acquire");
354        pool.write(id, &[0.5, 0.6, 0.7]);
355        let data = pool.read(id).expect("read");
356        assert_eq!(data, &[0.5_f32, 0.6, 0.7]);
357    }
358
359    // 11. read returns None when buffer is not in use
360    #[test]
361    fn test_read_not_in_use() {
362        let pool = make_pool(4, 2, 8);
363        let free_id = pool.buffers[0].buffer_id;
364        assert!(pool.read(free_id).is_none());
365    }
366
367    // 12. read returns None for non-existent id
368    #[test]
369    fn test_read_nonexistent_id() {
370        let pool = make_pool(4, 2, 8);
371        assert!(pool.read(9999).is_none());
372    }
373
374    // 13. release marks buffer free and zeroes data
375    #[test]
376    fn test_release_frees_and_zeroes() {
377        let mut pool = make_pool(4, 2, 8);
378        let id = pool.acquire().expect("acquire");
379        pool.write(id, &[1.0, 2.0, 3.0, 4.0]);
380        assert!(pool.release(id));
381        let buf = pool
382            .buffers
383            .iter()
384            .find(|b| b.buffer_id == id)
385            .expect("test: released buffer should exist");
386        assert!(!buf.in_use);
387        assert!(buf.data.iter().all(|&v| v == 0.0));
388    }
389
390    // 14. release increments generation
391    #[test]
392    fn test_release_increments_generation() {
393        let mut pool = make_pool(4, 2, 8);
394        let id = pool.acquire().expect("acquire");
395        let gen_before = pool
396            .buffers
397            .iter()
398            .find(|b| b.buffer_id == id)
399            .expect("test: buffer before release should exist")
400            .generation;
401        pool.release(id);
402        let gen_after = pool
403            .buffers
404            .iter()
405            .find(|b| b.buffer_id == id)
406            .expect("test: buffer after release should exist")
407            .generation;
408        assert_eq!(gen_after, gen_before + 1);
409    }
410
411    // 15. release returns false for non-existent id
412    #[test]
413    fn test_release_nonexistent_id() {
414        let mut pool = make_pool(4, 2, 8);
415        assert!(!pool.release(9999));
416    }
417
418    // 16. release increments total_returns
419    #[test]
420    fn test_release_increments_total_returns() {
421        let mut pool = make_pool(4, 2, 8);
422        let id = pool.acquire().expect("acquire");
423        pool.release(id);
424        assert_eq!(pool.stats().total_returns, 1);
425    }
426
427    // 17. released buffer becomes available again for acquire
428    #[test]
429    fn test_released_buffer_reused() {
430        let mut pool = make_pool(4, 1, 1);
431        let id1 = pool.acquire().expect("first acquire");
432        pool.release(id1);
433        // At max=1 and now free, next acquire must succeed as a cache hit
434        let id2 = pool.acquire().expect("second acquire");
435        assert_eq!(id1, id2);
436        assert_eq!(pool.stats().cache_hits, 2);
437    }
438
439    // 18. clear_all releases all buffers
440    #[test]
441    fn test_clear_all_releases_all() {
442        let mut pool = make_pool(4, 4, 16);
443        let _id0 = pool.acquire().expect("acquire 0");
444        let _id1 = pool.acquire().expect("acquire 1");
445        let _id2 = pool.acquire().expect("acquire 2");
446        pool.clear_all();
447        assert!(pool.buffers.iter().all(|b| !b.in_use));
448        assert!(pool
449            .buffers
450            .iter()
451            .all(|b| b.data.iter().all(|&v| v == 0.0)));
452    }
453
454    // 19. clear_all increments all generations
455    #[test]
456    fn test_clear_all_increments_generations() {
457        let mut pool = make_pool(4, 3, 16);
458        let _id = pool.acquire().expect("acquire");
459        pool.clear_all();
460        for buf in &pool.buffers {
461            assert!(
462                buf.generation >= 1,
463                "generation should be at least 1 after clear_all"
464            );
465        }
466    }
467
468    // 20. stats: in_use + available == total_buffers
469    #[test]
470    fn test_stats_invariant() {
471        let mut pool = make_pool(4, 6, 32);
472        let _a = pool.acquire();
473        let _b = pool.acquire();
474        let _c = pool.acquire();
475        let s = pool.stats();
476        assert_eq!(s.in_use + s.available, s.total_buffers);
477    }
478
479    // 21. stats: cache_hits + cache_misses == total_allocations (when None not returned)
480    #[test]
481    fn test_stats_hits_plus_misses_eq_allocations() {
482        let mut pool = make_pool(4, 2, 8);
483        for _ in 0..5 {
484            pool.acquire();
485        }
486        let s = pool.stats();
487        assert_eq!(s.cache_hits + s.cache_misses, s.total_allocations);
488    }
489
490    // 22. is_valid_dim works correctly
491    #[test]
492    fn test_is_valid_dim() {
493        let buf = EmbeddingBuffer {
494            buffer_id: 0,
495            data: vec![0.0; 128],
496            dim: 128,
497            in_use: false,
498            generation: 0,
499        };
500        assert!(buf.is_valid_dim(128));
501        assert!(!buf.is_valid_dim(64));
502    }
503
504    // 23. PoolConfig::default_for sets correct defaults
505    #[test]
506    fn test_pool_config_default_for() {
507        let cfg = PoolConfig::default_for(768);
508        assert_eq!(cfg.embedding_dim, 768);
509        assert_eq!(cfg.initial_capacity, 16);
510        assert_eq!(cfg.max_capacity, 256);
511    }
512
513    // 24. Pool handles zero initial_capacity gracefully
514    #[test]
515    fn test_zero_initial_capacity() {
516        let mut pool = make_pool(4, 0, 4);
517        assert_eq!(pool.buffers.len(), 0);
518        let id = pool.acquire().expect("should allocate");
519        assert_eq!(pool.stats().cache_misses, 1);
520        assert!(pool.write(id, &[1.0, 2.0, 3.0, 4.0]));
521        let data = pool.read(id).expect("read");
522        assert_eq!(data, &[1.0_f32, 2.0, 3.0, 4.0]);
523    }
524
525    // 25. Multiple writes to same buffer accumulate correctly
526    #[test]
527    fn test_multiple_writes() {
528        let mut pool = make_pool(3, 2, 8);
529        let id = pool.acquire().expect("acquire");
530        pool.write(id, &[1.0, 1.0, 1.0]);
531        pool.write(id, &[9.0, 8.0, 7.0]);
532        let data = pool.read(id).expect("read");
533        assert_eq!(data, &[9.0_f32, 8.0, 7.0]);
534    }
535
536    // 26. Release then re-acquire clears previous data
537    #[test]
538    fn test_release_clears_data_before_reuse() {
539        let mut pool = make_pool(4, 1, 4);
540        let id = pool.acquire().expect("acquire");
541        pool.write(id, &[5.0, 6.0, 7.0, 8.0]);
542        pool.release(id);
543        let id2 = pool.acquire().expect("reacquire");
544        assert_eq!(id, id2);
545        // After release the buffer was zeroed; reading without a new write should see zeros
546        let data = pool.read(id2).expect("read");
547        assert!(data.iter().all(|&v| v == 0.0));
548    }
549
550    // 27. stats available tracks correctly across acquire/release cycle
551    #[test]
552    fn test_stats_available_tracks() {
553        let mut pool = make_pool(4, 4, 16);
554        assert_eq!(pool.stats().available, 4);
555        let id1 = pool.acquire().expect("a1");
556        assert_eq!(pool.stats().available, 3);
557        let id2 = pool.acquire().expect("a2");
558        assert_eq!(pool.stats().available, 2);
559        pool.release(id1);
560        assert_eq!(pool.stats().available, 3);
561        pool.release(id2);
562        assert_eq!(pool.stats().available, 4);
563    }
564}