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}