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>;