Skip to main content

candela/tensor/skeleton/
cache.rs

1use std::borrow::Borrow;
2use std::collections::HashMap;
3use std::hash::Hash;
4use std::sync::{Arc, Mutex};
5
6use crate::tensor::backend::{Backend, ComputeFor, DefaultBackend};
7use crate::{Composable, Dimension, Layout, OpError, Tensor};
8
9use super::frame::{BakedPromise, Skeleton};
10
11/// Decides which entry a [`SkeletonCache`] drops when the cache is full.
12///
13/// The cache calls a hook on each action (insertion, removal, get). The policy
14/// keeps whatever bookkeeping it needs and answers [`evict`] when a new entry
15/// needs space. Implemented by [`LRUPolicy`] and [`UnboundedPolicy`].
16///
17/// [`evict`]: EvictionPolicy::evict
18///
19/// # Examples
20///
21/// ```
22/// // The two built-in policies plug in as a SkeletonCache's third type parameter.
23/// use candela::skeleton::{LRUPolicy, SkeletonCache, UnboundedPolicy};
24/// use candela::Layout;
25///
26/// let _lru: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
27/// let _unbounded: SkeletonCache<Box<[Layout]>, UnboundedPolicy, f32> = SkeletonCache::new(0);
28/// ```
29pub trait EvictionPolicy {
30    /// The constructor of the policy
31    ///
32    /// Creates a policy that must manage at least `cache_size` items.
33    /// The cache may grow depending on the policy eviction behavior.
34    fn new(cache_size: usize) -> Self;
35
36    /// The get action
37    ///
38    /// Is called when an element is being read. This element is guaranteed
39    /// to exist in the cache.
40    fn on_get(&mut self, idx: usize);
41
42    /// The insert action
43    ///
44    /// Is called when an element is being inserted. The element is guaranteed
45    /// to not exist in the cache.
46    fn on_insert(&mut self, idx: usize);
47
48    /// The remove action
49    ///
50    /// Is called when an element is being removed. The element is guaranteed
51    /// to exist in the cache.
52    fn on_remove(&mut self, idx: usize);
53
54    /// The eviction action
55    ///
56    /// Is called when a new element must be added to the cache and it does not
57    /// have enough space in the current arena.
58    /// Returning `None` means that the arena should grow to accommodate the new element
59    /// instead of removing an element, while `Some(idx)` means that the idx in the arena
60    /// should be used instead.
61    fn evict(&mut self) -> Option<usize>;
62}
63
64//////////////////////////////////////////////////////////////
65
66/// An [`EvictionPolicy`] that never evicts
67///
68/// The cache grows without bound, keeping every skeleton it has ever built. Use it
69/// when the set of input shapes is small and known to be finite.
70///
71/// # Examples
72///
73/// ```
74/// use candela::skeleton::{SkeletonSlot, UnboundedDynamicSkeleton};
75/// use candela::{Layout, Tensor};
76///
77/// // Selected here through the UnboundedDynamicSkeleton alias.
78/// let sk: UnboundedDynamicSkeleton<f32> =
79///     UnboundedDynamicSkeleton::new(0, Box::new(|inputs: &[Layout]| {
80///         let a = SkeletonSlot::new(inputs[0].clone());
81///         (&a * 2.0).into_skeleton(&[a]).unwrap()
82///     }));
83/// assert_eq!(sk.run(&[&Tensor::from_scalar(3.0, &[4])])?.data(), &[6.0; 4]);
84/// # Ok::<(), candela::OpError>(())
85/// ```
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
87pub struct UnboundedPolicy;
88
89impl EvictionPolicy for UnboundedPolicy {
90    fn new(_: usize) -> Self {
91        Self {}
92    }
93
94    fn on_get(&mut self, _: usize) {}
95
96    fn on_insert(&mut self, _: usize) {}
97
98    fn on_remove(&mut self, _: usize) {}
99
100    fn evict(&mut self) -> Option<usize> {
101        None
102    }
103}
104
105//////////////////////////////////////////////////////////////
106
107struct Slot<Key, T, B: Backend> {
108    key: Key,
109    sk: Arc<Skeleton<T, B>>,
110}
111
112impl<Key: Clone, T, B: Backend> Clone for Slot<Key, T, B> {
113    fn clone(&self) -> Self {
114        Self {
115            key: self.key.clone(),
116            sk: self.sk.clone(),
117        }
118    }
119}
120
121struct Cache<Key: Clone + Hash, T, B: Backend> {
122    arena: Vec<Option<Slot<Key, T, B>>>,
123    map: HashMap<Key, usize>,
124}
125
126#[derive(Debug)]
127struct Link {
128    prev: usize,
129    next: usize,
130}
131
132/// A least-recently-used [`EvictionPolicy`]
133///
134/// Tracks access order in an intrusive doubly linked list and, once the cache is
135/// full, evicts the entry that has gone longest without a hit.
136///
137/// # Examples
138///
139/// ```
140/// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
141/// use candela::{Layout, Tensor};
142///
143/// // LRUPolicy is DynamicSkeleton's default; a size-1 cache drops the older shape.
144/// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(1, Box::new(|inputs: &[Layout]| {
145///     let a = SkeletonSlot::new(inputs[0].clone());
146///     (&a * 2.0).into_skeleton(&[a]).unwrap()
147/// }));
148///
149/// let a = Tensor::from_scalar(3.0, &[4]);
150/// sk.run(&[&a])?;
151/// sk.run(&[&Tensor::from_scalar(3.0, &[8])])?; // evicts the [4] entry
152/// assert!(!sk.contains_key(&[&a]));
153/// # Ok::<(), candela::OpError>(())
154/// ```
155#[derive(Debug)]
156pub struct LRUPolicy {
157    order: HashMap<usize, Link>,
158    head: usize,
159    tail: usize,
160}
161
162impl EvictionPolicy for LRUPolicy {
163    fn new(cache_size: usize) -> Self {
164        Self {
165            order: HashMap::with_capacity(cache_size),
166            head: usize::MAX,
167            tail: usize::MAX,
168        }
169    }
170
171    // The caller must ensure that the idx exist!
172    // Assumes that at least a single value is present (head and tail != usize::MAX)
173    fn on_get(&mut self, idx: usize) {
174        if idx == self.head {
175            return;
176        }
177
178        let [Some(recent), Some(head)] = self.order.get_disjoint_mut([&idx, &self.head]) else {
179            unreachable!("on_get should only be called if we are sure that the idx exists")
180        };
181
182        let recent_next = recent.next;
183        let recent_previous = recent.prev;
184
185        recent.next = self.head;
186        recent.prev = usize::MAX;
187        head.prev = idx;
188
189        if idx != self.tail {
190            let [Some(previous), Some(next)] = self
191                .order
192                .get_disjoint_mut([&recent_previous, &recent_next])
193            else {
194                unreachable!("on_get should only be called if we are sure that the idx exists")
195            };
196
197            previous.next = recent_next;
198            next.prev = recent_previous;
199        } else {
200            let previous = self.order.get_mut(&recent_previous).unwrap();
201
202            previous.next = usize::MAX;
203            self.tail = recent_previous;
204        }
205
206        self.head = idx;
207    }
208
209    fn on_insert(&mut self, idx: usize) {
210        self.order.insert(
211            idx,
212            Link {
213                prev: usize::MAX,
214                next: self.head,
215            },
216        );
217
218        if self.head != usize::MAX {
219            let older_head = self.order.get_mut(&self.head).unwrap();
220            older_head.prev = idx;
221        }
222
223        self.head = idx;
224
225        if self.tail == usize::MAX {
226            self.tail = idx;
227        }
228    }
229
230    fn on_remove(&mut self, idx: usize) {
231        if idx == self.head {
232            let next = self.order.remove(&idx).unwrap().next;
233
234            if next != usize::MAX {
235                self.order.get_mut(&next).unwrap().prev = usize::MAX;
236            } else {
237                self.tail = usize::MAX;
238            }
239
240            self.head = next;
241        } else if idx == self.tail {
242            let previous = self.order.remove(&idx).unwrap().prev;
243
244            if previous != usize::MAX {
245                self.order.get_mut(&previous).unwrap().next = usize::MAX;
246            }
247
248            self.tail = previous;
249        } else {
250            let recent = self.order.remove(&idx).unwrap();
251
252            let [Some(previous), Some(next)] =
253                self.order.get_disjoint_mut([&recent.prev, &recent.next])
254            else {
255                panic!()
256            };
257
258            previous.next = recent.next;
259            next.prev = recent.prev;
260        }
261    }
262
263    // Assumes that at least a single element is present
264    fn evict(&mut self) -> Option<usize> {
265        let tail_idx = self.tail;
266
267        let tail = self.order.remove(&tail_idx).unwrap();
268
269        if tail.prev != usize::MAX {
270            self.order.get_mut(&tail.prev).unwrap().next = usize::MAX;
271        } else {
272            self.head = usize::MAX;
273        }
274
275        self.tail = tail.prev;
276
277        Some(tail_idx)
278    }
279}
280
281//////////////////////////////////////////////////////////////
282
283struct SkeletonCacheInner<Key: Clone + Hash + Eq, P: EvictionPolicy, T, B: Backend> {
284    cache: Cache<Key, T, B>,
285    free: Vec<usize>,
286    policy: P,
287}
288
289/// A concurrent store of skeletons keyed by `Key`
290///
291/// Holds several skeletons at once and picks one by key. Eviction is delegated to
292/// the chosen [`EvictionPolicy`]. This is the primitive [`DynamicSkeleton`] is
293/// built on; reach for that first unless you need a custom key.
294///
295/// [`DynamicSkeleton`]: crate::skeleton::DynamicSkeleton
296///
297/// # Examples
298///
299/// ```
300/// use candela::skeleton::{BuildFunction, LRUPolicy, SkeletonCache, SkeletonSlot};
301/// use candela::{Layout, Tensor};
302///
303/// // Keyed by input layouts, evicting under an LRU policy.
304/// let cache: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
305/// let build: BuildFunction<f32> = Box::new(|inputs: &[Layout]| {
306///     let a = SkeletonSlot::new(inputs[0].clone());
307///     (&a * 2.0).into_skeleton(&[a]).unwrap()
308/// });
309///
310/// let out = cache.run(&[&Tensor::from_scalar(3.0, &[4])], &build)?;
311/// assert_eq!(out.data(), &[6.0; 4]);
312/// # Ok::<(), candela::OpError>(())
313/// ```
314pub struct SkeletonCache<Key: Clone + Hash + Eq, P: EvictionPolicy, T, B: Backend = DefaultBackend>(
315    Mutex<SkeletonCacheInner<Key, P, T, B>>,
316);
317
318impl<Key: Clone + Hash + Eq, P: EvictionPolicy, T, B: Backend> std::fmt::Debug
319    for SkeletonCache<Key, P, T, B>
320{
321    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322        let mut db_struct = f.debug_struct("SkeletonCache");
323        match self.0.try_lock() {
324            Ok(inner) => {
325                db_struct.field("cached", &inner.cache.map.len());
326                db_struct.field("arena_size", &inner.cache.arena.len());
327            }
328            Err(std::sync::TryLockError::Poisoned(poisoned)) => {
329                let inner = poisoned.get_ref();
330                db_struct.field("cached", &inner.cache.map.len());
331                db_struct.field("arena_size", &inner.cache.arena.len());
332                db_struct.field("state", &format_args!("<poisoned>"));
333            }
334            Err(std::sync::TryLockError::WouldBlock) => {
335                db_struct.field("state", &format_args!("<locked by another thread>"));
336            }
337        }
338        db_struct.finish_non_exhaustive()
339    }
340}
341
342impl<Key, P: EvictionPolicy, T, B> SkeletonCache<Key, P, T, B>
343where
344    Key: Clone + Hash + Eq,
345    T: Clone + PartialEq + ComputeFor<B>,
346    B: Backend,
347{
348    fn insert_pair(
349        &self,
350        mut lock: std::sync::MutexGuard<'_, SkeletonCacheInner<Key, P, T, B>>,
351        key: Key,
352        value: Arc<Skeleton<T, B>>,
353    ) {
354        if let Some(idx) = lock.free.pop() {
355            lock.cache.arena[idx] = Some(Slot {
356                key: key.clone(),
357                sk: value,
358            });
359
360            lock.cache.map.insert(key, idx);
361            lock.policy.on_insert(idx);
362        } else {
363            let idx = lock.policy.evict();
364
365            match idx {
366                Some(idx) => {
367                    let slot = lock.cache.arena[idx]
368                        .replace(Slot {
369                            key: key.clone(),
370                            sk: value,
371                        })
372                        .unwrap();
373
374                    lock.cache.map.remove(&slot.key);
375                    lock.cache.map.insert(key, idx);
376                    lock.policy.on_insert(idx);
377                }
378                None => {
379                    let idx = lock.cache.arena.len();
380
381                    lock.cache.arena.push(Some(Slot {
382                        key: key.clone(),
383                        sk: value,
384                    }));
385
386                    lock.cache.map.insert(key, idx);
387                    lock.policy.on_insert(idx);
388                }
389            }
390        }
391    }
392
393    /// Creates a new cache
394    ///
395    /// Reserves room for at least `cache_size` entries. The policy decides whether the
396    /// cache stays at that size or grows past it.
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// use candela::skeleton::{LRUPolicy, SkeletonCache};
402    /// use candela::Layout;
403    ///
404    /// let cache: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
405    /// # let _ = cache;
406    /// ```
407    pub fn new(cache_size: usize) -> Self {
408        let mut v: Vec<Option<Slot<Key, T, B>>> = Vec::with_capacity(cache_size);
409        v.resize(cache_size, None);
410
411        let free: Vec<usize> = (0..cache_size).collect();
412
413        Self(Mutex::new(SkeletonCacheInner {
414            cache: Cache {
415                arena: v,
416                map: HashMap::with_capacity(cache_size),
417            },
418            free,
419            policy: P::new(cache_size),
420        }))
421    }
422
423    /// Looks up `key`, building and inserting on a miss
424    ///
425    /// Returns the cached skeleton if `key` is present. Otherwise `build` is called,
426    /// the result is stored under `key`, and a handle to it is returned. `build` runs
427    /// at most once, and only on a miss.
428    ///
429    /// # Examples
430    ///
431    /// ```
432    /// use candela::skeleton::{LRUPolicy, SkeletonCache, SkeletonSlot};
433    /// use candela::{Layout, Tensor};
434    ///
435    /// let cache: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
436    /// let key: Box<[Layout]> = Box::new([Layout::new(&[4])]);
437    ///
438    /// // Built on the first call; a second call with the same key reuses it.
439    /// let sk = cache.get_or_insert_with(&key, || {
440    ///     let a = SkeletonSlot::from_shape(&[4]);
441    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
442    /// });
443    /// assert_eq!(sk.run(&[&Tensor::from_scalar(3.0, &[4])])?.data(), &[6.0; 4]);
444    /// # Ok::<(), candela::OpError>(())
445    /// ```
446    pub fn get_or_insert_with<F>(&self, key: &Key, build: F) -> Arc<Skeleton<T, B>>
447    where
448        F: FnOnce() -> Skeleton<T, B>,
449    {
450        let mut skeleton: Option<Arc<Skeleton<T, B>>> = None;
451
452        {
453            let mut lock = self.0.lock().unwrap();
454
455            if let Some(&idx) = lock.cache.map.get(key) {
456                skeleton = Some(lock.cache.arena[idx].as_ref().unwrap().sk.clone());
457                lock.policy.on_get(idx);
458            }
459        }
460
461        if let Some(sk) = skeleton {
462            return sk;
463        }
464
465        let sk: Arc<Skeleton<T, B>> = Arc::new(build());
466
467        {
468            let lock = self.0.lock().unwrap();
469
470            if !lock.cache.map.contains_key(key) {
471                self.insert_pair(lock, key.clone(), sk.clone());
472            }
473        }
474
475        sk
476    }
477
478    /// Removes the entry for `key`
479    ///
480    /// Returns the skeleton that was stored, or `None` if `key` was not present. The
481    /// freed slot is returned to the cache for reuse.
482    ///
483    /// # Examples
484    ///
485    /// ```
486    /// use candela::skeleton::{LRUPolicy, SkeletonCache, SkeletonSlot};
487    /// use candela::Layout;
488    ///
489    /// let cache: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
490    /// let key: Box<[Layout]> = Box::new([Layout::new(&[4])]);
491    /// cache.get_or_insert_with(&key, || {
492    ///     let a = SkeletonSlot::from_shape(&[4]);
493    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
494    /// });
495    ///
496    /// assert!(cache.remove(&key).is_some());
497    /// assert!(!cache.contains_key(&key));
498    /// ```
499    pub fn remove<Q>(&self, key: &Q) -> Option<Arc<Skeleton<T, B>>>
500    where
501        Key: Borrow<Q>,
502        Q: Hash + Eq + ?Sized,
503    {
504        let mut lock = self.0.lock().unwrap();
505
506        let idx = lock.cache.map.remove(key)?;
507        let slot = lock.cache.arena[idx].take();
508        lock.free.push(idx);
509        lock.policy.on_remove(idx);
510
511        Some(slot.unwrap().sk)
512    }
513
514    /// Returns whether `key` currently has an entry in the cache
515    ///
516    /// # Examples
517    ///
518    /// ```
519    /// use candela::skeleton::{LRUPolicy, SkeletonCache, SkeletonSlot};
520    /// use candela::Layout;
521    ///
522    /// let cache: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
523    /// let key: Box<[Layout]> = Box::new([Layout::new(&[4])]);
524    ///
525    /// assert!(!cache.contains_key(&key));
526    /// cache.get_or_insert_with(&key, || {
527    ///     let a = SkeletonSlot::from_shape(&[4]);
528    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
529    /// });
530    /// assert!(cache.contains_key(&key));
531    /// ```
532    pub fn contains_key<Q>(&self, key: &Q) -> bool
533    where
534        Key: Borrow<Q>,
535        Q: Hash + Eq + ?Sized,
536    {
537        let lock = self.0.lock().unwrap();
538
539        lock.cache.map.contains_key(key)
540    }
541}
542
543/// Builds a [`Skeleton`] for a given set of input layouts
544///
545/// Called on a cache miss with the layouts of the current inputs. It must create its
546/// slots from those layouts and bind them in the same order, so the resulting skeleton
547/// accepts exactly that shape.
548///
549/// # Examples
550///
551/// ```
552/// use candela::skeleton::{BuildFunction, SkeletonSlot};
553/// use candela::Layout;
554///
555/// // Doubles whatever single input it is handed, whatever its shape.
556/// let build: BuildFunction<f32> = Box::new(|inputs: &[Layout]| {
557///     let a = SkeletonSlot::new(inputs[0].clone());
558///     (&a * 2.0).into_skeleton(&[a]).unwrap()
559/// });
560/// # let _ = build;
561/// ```
562pub type BuildFunction<T, B = DefaultBackend> =
563    Box<dyn Fn(&[Layout]) -> Skeleton<T, B> + Send + Sync>;
564
565impl<P, T, B> SkeletonCache<Box<[Layout]>, P, T, B>
566where
567    P: EvictionPolicy,
568    T: Clone + PartialEq + ComputeFor<B>,
569    B: Backend,
570{
571    /// Runs the cached skeleton for the inputs' shapes, building one on a miss.
572    ///
573    /// Keys the cache by the inputs' layouts; on a miss `on_miss` builds the
574    /// [`Skeleton`], which is then cached and run. See [`Skeleton::run`].
575    ///
576    /// # Examples
577    ///
578    /// ```
579    /// use candela::skeleton::{BuildFunction, LRUPolicy, SkeletonCache, SkeletonSlot};
580    /// use candela::{Layout, Tensor};
581    ///
582    /// let cache: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
583    /// let build: BuildFunction<f32> = Box::new(|inputs: &[Layout]| {
584    ///     let a = SkeletonSlot::new(inputs[0].clone());
585    ///     (&a + 1.0).into_skeleton(&[a]).unwrap()
586    /// });
587    ///
588    /// let out = cache.run(&[&Tensor::from_scalar(3.0, &[4])], &build)?;
589    /// assert_eq!(out.data(), &[4.0; 4]);
590    /// # Ok::<(), candela::OpError>(())
591    /// ```
592    pub fn run(
593        &self,
594        inputs: &[&Tensor<T, B>],
595        on_miss: &BuildFunction<T, B>,
596    ) -> Result<Tensor<T, B>, OpError> {
597        // TODO: this clones every input layout on each call, even on a cache hit where
598        // nothing owned is needed. A raw_entry-based lookup could build the owned key only
599        // on a miss and skip the allocation entirely on hits.
600        let input_layouts: Box<[Layout]> = inputs.iter().map(|&t| t.layout().clone()).collect();
601
602        let sk: Arc<Skeleton<T, B>> =
603            self.get_or_insert_with(&input_layouts, || on_miss(&input_layouts));
604        sk.run(inputs)
605    }
606
607    /// Composes the cached skeleton for the inputs' shapes, building one on a miss.
608    ///
609    /// Like [`run`], but embeds the skeleton's plan into a [`BakedPromise`]
610    /// instead of executing it. See [`Skeleton::compose`].
611    ///
612    /// [`run`]: SkeletonCache::run
613    ///
614    /// # Examples
615    ///
616    /// ```
617    /// use candela::skeleton::{BuildFunction, LRUPolicy, SkeletonCache, SkeletonSlot};
618    /// use candela::{Layout, Tensor};
619    ///
620    /// let cache: SkeletonCache<Box<[Layout]>, LRUPolicy, f32> = SkeletonCache::new(4);
621    /// let build: BuildFunction<f32> = Box::new(|inputs: &[Layout]| {
622    ///     let a = SkeletonSlot::new(inputs[0].clone());
623    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
624    /// });
625    ///
626    /// // Compose over a lazy promise and fold the result into a larger graph.
627    /// let a = Tensor::from_scalar(1.0, &[4]) + 2.0;
628    /// let baked = cache.compose(&[&a], &build)?;
629    /// assert_eq!(baked.to_promise().materialize().data(), &[6.0; 4]);
630    /// # Ok::<(), candela::OpError>(())
631    /// ```
632    pub fn compose<C>(
633        &self,
634        inputs: &[&C],
635        on_miss: &BuildFunction<T, B>,
636    ) -> Result<BakedPromise<T, B>, OpError>
637    where
638        C: Composable<T, B>,
639    {
640        let input_layouts: Box<[Layout]> = inputs.iter().map(|&t| t.layout().clone()).collect();
641
642        let sk: Arc<Skeleton<T, B>> =
643            self.get_or_insert_with(&input_layouts, || on_miss(&input_layouts));
644        sk.compose(inputs)
645    }
646}