Skip to main content

candela/tensor/skeleton/
dynamic.rs

1use std::sync::Arc;
2
3use crate::skeleton::Skeleton;
4use crate::tensor::backend::{Backend, ComputeFor, DefaultBackend};
5use crate::{Composable, Dimension, Layout, OpError, Tensor};
6
7use super::cache::{BuildFunction, EvictionPolicy, LRUPolicy, SkeletonCache, UnboundedPolicy};
8use super::frame::BakedPromise;
9
10/// A cache (group) of skeletons with different shapes
11///
12/// This is a hashmap abstraction on top of a [`Skeleton`] to enable dynamic shapes.
13/// It calls the [`BuildFunction`] every time a group of tensors with never-before-seen
14/// layouts arrives, and stores the result in the cache. The cache size and eviction
15/// behavior are determined by the chosen policy, which must implement [`EvictionPolicy`].
16///
17/// The build function must bind its slots in the same order as the layouts it receives,
18/// returning a [`Skeleton`] that supports that shape.
19///
20/// [`Skeleton`]: super::Skeleton
21///
22/// # Examples
23///
24/// ```
25/// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
26/// use candela::{Layout, Tensor};
27///
28/// // One build rule, reused for whatever shape shows up.
29/// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(8, Box::new(|inputs: &[Layout]| {
30///     let a = SkeletonSlot::new(inputs[0].clone());
31///     (&a * 2.0).into_skeleton(&[a]).unwrap()
32/// }));
33///
34/// // Two different shapes build (and cache) two different skeletons.
35/// let out4 = sk.run(&[&Tensor::from_scalar(3.0, &[4])])?;
36/// let out8 = sk.run(&[&Tensor::from_scalar(3.0, &[8])])?;
37/// assert_eq!(out4.data(), &[6.0; 4]);
38/// assert_eq!(out8.data(), &[6.0; 8]);
39/// # Ok::<(), candela::OpError>(())
40/// ```
41pub struct DynamicSkeleton<T, B: Backend = DefaultBackend, P: EvictionPolicy = LRUPolicy> {
42    cache: SkeletonCache<Box<[Layout]>, P, T, B>,
43    build: BuildFunction<T, B>,
44}
45
46impl<T, B: Backend, P: EvictionPolicy> std::fmt::Debug for DynamicSkeleton<T, B, P> {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("DynamicSkeleton")
49            .field("cache", &self.cache)
50            .finish_non_exhaustive()
51    }
52}
53
54impl<P: EvictionPolicy, T, B: Backend> DynamicSkeleton<T, B, P>
55where
56    T: ComputeFor<B>,
57    B: Backend,
58{
59    /// Creates a new dynamic skeleton
60    ///
61    /// Creates a cache of at least `cache_size` items, where each entry maps a
62    /// `Layout` to a [`Skeleton`].
63    ///
64    /// On a miss it calls `build` to create and cache a new skeleton; the slots bound
65    /// in `build` must be in the same order as its `inputs` argument.
66    ///
67    /// [`Skeleton`]: super::Skeleton
68    ///
69    /// # Examples
70    /// ```
71    /// use candela::skeleton::{DynamicSkeleton, Skeleton, SkeletonSlot};
72    /// use candela::{Layout, Tensor};
73    /// use std::error::Error;
74    ///
75    /// fn build(inputs: &[Layout]) -> Skeleton<f32> {
76    ///     let a = SkeletonSlot::new(inputs[0].clone());
77    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
78    /// }
79    ///
80    /// fn main() -> Result<(), Box<dyn Error>> {
81    ///     let a = Tensor::from_scalar(0.3, &[4]);
82    ///     let b = Tensor::from_scalar(0.3, &[8]);
83    ///
84    ///     let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(12, Box::new(build));
85    ///     let out_a = sk.run(&[&a])?;
86    ///     let out_b = sk.run(&[&b])?;
87    ///
88    ///     println!("{out_a}");
89    ///     println!("{out_b}");
90    ///
91    ///     Ok(())
92    /// }
93    /// ```
94    #[inline]
95    pub fn new(cache_size: usize, build: BuildFunction<T, B>) -> Self {
96        Self {
97            cache: SkeletonCache::new(cache_size),
98            build,
99        }
100    }
101
102    /// Runs the cached skeleton for the inputs' shapes, building one on a miss.
103    ///
104    /// Looks up the [`Skeleton`] keyed by the inputs' layouts and runs it,
105    /// calling the build function first if no entry exists yet. See
106    /// [`Skeleton::run`].
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
112    /// use candela::{Layout, Tensor};
113    ///
114    /// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(8, Box::new(|inputs: &[Layout]| {
115    ///     let a = SkeletonSlot::new(inputs[0].clone());
116    ///     (&a + 1.0).into_skeleton(&[a]).unwrap()
117    /// }));
118    ///
119    /// let out = sk.run(&[&Tensor::from_scalar(3.0, &[4])])?;
120    /// assert_eq!(out.data(), &[4.0; 4]);
121    /// # Ok::<(), candela::OpError>(())
122    /// ```
123    #[inline]
124    pub fn run(&self, inputs: &[&Tensor<T, B>]) -> Result<Tensor<T, B>, OpError> {
125        self.cache.run(inputs, &self.build)
126    }
127
128    /// Composes the cached skeleton for the inputs' shapes, building one on a miss.
129    ///
130    /// Like [`run`], but embeds the skeleton's plan into a [`BakedPromise`]
131    /// instead of executing it. See [`Skeleton::compose`].
132    ///
133    /// [`run`]: DynamicSkeleton::run
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
139    /// use candela::{Layout, Tensor};
140    ///
141    /// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(8, Box::new(|inputs: &[Layout]| {
142    ///     let a = SkeletonSlot::new(inputs[0].clone());
143    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
144    /// }));
145    ///
146    /// // Compose over a lazy promise, not a materialized tensor.
147    /// let a = Tensor::from_scalar(1.0, &[4]) + 2.0; // TensorPromise, still unevaluated
148    /// let baked = sk.compose(&[&a])?;
149    /// assert_eq!(baked.to_promise().materialize().data(), &[6.0; 4]);
150    /// # Ok::<(), candela::OpError>(())
151    /// ```
152    #[inline]
153    pub fn compose<C>(&self, inputs: &[&C]) -> Result<BakedPromise<T, B>, OpError>
154    where
155        C: Composable<T, B>,
156    {
157        self.cache.compose(inputs, &self.build)
158    }
159
160    /// Removes the entry for `key`
161    ///
162    /// Returns the skeleton that was stored, or `None` if `key` was not present. The
163    /// freed slot is returned to the cache for reuse.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
169    /// use candela::{Layout, Tensor};
170    ///
171    /// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(8, Box::new(|inputs: &[Layout]| {
172    ///     let a = SkeletonSlot::new(inputs[0].clone());
173    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
174    /// }));
175    ///
176    /// let a = Tensor::from_scalar(3.0, &[4]);
177    /// sk.run(&[&a])?;                     // builds and caches an entry
178    /// assert!(sk.remove(&[&a]).is_some());
179    /// assert!(!sk.contains_key(&[&a]));   // gone now
180    /// # Ok::<(), candela::OpError>(())
181    /// ```
182    #[inline]
183    pub fn remove(&self, key: &[&Tensor<T, B>]) -> Option<Arc<Skeleton<T, B>>> {
184        let layouts: Box<[Layout]> = key.iter().map(|&x| x.layout().clone()).collect();
185
186        self.cache.remove(&layouts)
187    }
188
189    /// Removes the entry for `key` via layout
190    ///
191    /// Same as [`Self::remove`] but using layouts instead.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
197    /// use candela::{Layout, Tensor};
198    ///
199    /// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(8, Box::new(|inputs: &[Layout]| {
200    ///     let a = SkeletonSlot::new(inputs[0].clone());
201    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
202    /// }));
203    ///
204    /// sk.run(&[&Tensor::from_scalar(3.0, &[4])])?;
205    /// assert!(sk.remove_by_layout(&[Layout::new(&[4])]).is_some());
206    /// # Ok::<(), candela::OpError>(())
207    /// ```
208    #[inline]
209    pub fn remove_by_layout(&self, key: &[Layout]) -> Option<Arc<Skeleton<T, B>>> {
210        self.cache.remove(key)
211    }
212
213    /// Returns whether `key` currently has an entry in the cache
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
219    /// use candela::{Layout, Tensor};
220    ///
221    /// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(8, Box::new(|inputs: &[Layout]| {
222    ///     let a = SkeletonSlot::new(inputs[0].clone());
223    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
224    /// }));
225    ///
226    /// let a = Tensor::from_scalar(3.0, &[4]);
227    /// assert!(!sk.contains_key(&[&a])); // nothing built yet
228    /// sk.run(&[&a])?;
229    /// assert!(sk.contains_key(&[&a]));  // now cached
230    /// # Ok::<(), candela::OpError>(())
231    /// ```
232    #[inline]
233    pub fn contains_key(&self, key: &[&Tensor<T, B>]) -> bool {
234        let layouts: Box<[Layout]> = key.iter().map(|&x| x.layout().clone()).collect();
235
236        self.cache.contains_key(&layouts)
237    }
238
239    /// Returns whether `key` currently has an entry in the cache
240    /// by layout
241    ///
242    /// # Examples
243    ///
244    /// ```
245    /// use candela::skeleton::{DynamicSkeleton, SkeletonSlot};
246    /// use candela::{Layout, Tensor};
247    ///
248    /// let sk: DynamicSkeleton<f32> = DynamicSkeleton::new(8, Box::new(|inputs: &[Layout]| {
249    ///     let a = SkeletonSlot::new(inputs[0].clone());
250    ///     (&a * 2.0).into_skeleton(&[a]).unwrap()
251    /// }));
252    ///
253    /// sk.run(&[&Tensor::from_scalar(3.0, &[4])])?;
254    /// assert!(sk.contains_key_by_layout(&[Layout::new(&[4])]));
255    /// # Ok::<(), candela::OpError>(())
256    /// ```
257    #[inline]
258    pub fn contains_key_by_layout(&self, key: &[Layout]) -> bool {
259        self.cache.contains_key(key)
260    }
261}
262
263/// A [`DynamicSkeleton`] whose cache never evicts (uses [`UnboundedPolicy`]).
264///
265/// # Examples
266///
267/// ```
268/// use candela::skeleton::{SkeletonSlot, UnboundedDynamicSkeleton};
269/// use candela::{Layout, Tensor};
270///
271/// // Like DynamicSkeleton, but every skeleton it builds is kept forever.
272/// let sk: UnboundedDynamicSkeleton<f32> =
273///     UnboundedDynamicSkeleton::new(0, Box::new(|inputs: &[Layout]| {
274///         let a = SkeletonSlot::new(inputs[0].clone());
275///         (&a * 2.0).into_skeleton(&[a]).unwrap()
276///     }));
277///
278/// let out = sk.run(&[&Tensor::from_scalar(3.0, &[4])])?;
279/// assert_eq!(out.data(), &[6.0; 4]);
280/// # Ok::<(), candela::OpError>(())
281/// ```
282pub type UnboundedDynamicSkeleton<T, B = DefaultBackend> = DynamicSkeleton<T, B, UnboundedPolicy>;