Skip to main content

tenferro_cpu/
plan_cache.rs

1//! Shareable cache of immutable CPU numerical plans.
2//!
3//! A plan (a contraction plan, an indexed-traversal plan) is worked out once
4//! and never changed afterwards, so any number of computations may read it at
5//! the same time. [`CpuPlanCache`] holds those plans behind one lock that is
6//! taken only to look a plan up or insert it; the computation runs on the
7//! shared plan after the lock is released. Engines own a private cache by
8//! default and may be given a shared one, so several engines plan once.
9
10use std::fmt;
11use std::sync::{Arc, Mutex, MutexGuard};
12
13use crate::gemm::GemmAnalysisCache;
14use crate::indexed_plan_cache::{IndexedPlanCache, IndexedPlanCacheLimits, IndexedPlanKey};
15use crate::CacheStats;
16use strided_kernel::{
17    ErasedDynamicSlicePlan, ErasedDynamicUpdateSlicePlan, ErasedGatherPlan, ErasedScatterPlan,
18};
19
20/// A shareable, bounded cache of immutable CPU numerical plans.
21///
22/// Cloning the handle shares the cache. The lock inside is held only while a
23/// plan is looked up or inserted, never while a computation runs on it.
24///
25/// # Examples
26///
27/// ```
28/// use tenferro_cpu::{CpuEngine, CpuPlanCache};
29///
30/// let plans = CpuPlanCache::new();
31/// let first = CpuEngine::serial().with_plan_cache(plans.clone());
32/// let second = CpuEngine::serial().with_plan_cache(plans.clone());
33/// assert!(first.plan_cache().shares_with(second.plan_cache()));
34/// assert_eq!(plans.stats().entries, 0);
35/// ```
36#[derive(Clone)]
37pub struct CpuPlanCache {
38    inner: Arc<Mutex<PlanCacheState>>,
39}
40
41pub(crate) struct PlanCacheState {
42    pub(crate) gemm: GemmAnalysisCache,
43    pub(crate) indexed: IndexedPlanCache,
44}
45
46impl fmt::Debug for CpuPlanCache {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        let stats = self.stats();
49        f.debug_struct("CpuPlanCache")
50            .field("entries", &stats.entries)
51            .field("retained_bytes", &stats.retained_bytes)
52            .finish_non_exhaustive()
53    }
54}
55
56impl Default for CpuPlanCache {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl CpuPlanCache {
63    /// An empty cache with the default bounds.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// let plans = tenferro_cpu::CpuPlanCache::new();
69    /// assert_eq!(plans.stats().entries, 0);
70    /// ```
71    pub fn new() -> Self {
72        Self {
73            inner: Arc::new(Mutex::new(PlanCacheState {
74                gemm: GemmAnalysisCache::default(),
75                indexed: IndexedPlanCache::default(),
76            })),
77        }
78    }
79
80    pub(crate) fn lock(&self) -> MutexGuard<'_, PlanCacheState> {
81        // INVARIANT: the lock guards only lookup and insertion of immutable
82        // plans; a panic while it is held leaves complete entries only.
83        self.inner
84            .lock()
85            .unwrap_or_else(std::sync::PoisonError::into_inner)
86    }
87
88    /// Whether `other` is a handle to the same cache.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// let plans = tenferro_cpu::CpuPlanCache::new();
94    /// assert!(plans.shares_with(&plans.clone()));
95    /// assert!(!plans.shares_with(&tenferro_cpu::CpuPlanCache::new()));
96    /// ```
97    pub fn shares_with(&self, other: &Self) -> bool {
98        Arc::ptr_eq(&self.inner, &other.inner)
99    }
100
101    /// Combined statistics of every plan family.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// assert_eq!(tenferro_cpu::CpuPlanCache::new().stats().hits, 0);
107    /// ```
108    pub fn stats(&self) -> CacheStats {
109        let state = self.lock();
110        let gemm = tenferro_tensor::RuntimeCacheControl::stats(&state.gemm);
111        let indexed = state.indexed.stats();
112        CacheStats {
113            entries: gemm.entries.saturating_add(indexed.entries),
114            retained_bytes: gemm.retained_bytes.saturating_add(indexed.retained_bytes),
115            hits: gemm.hits.saturating_add(indexed.hits),
116            misses: gemm.misses.saturating_add(indexed.misses),
117            evictions: gemm.evictions.saturating_add(indexed.evictions),
118            clears: gemm.clears.saturating_add(indexed.clears),
119        }
120    }
121
122    /// Statistics of the contraction-plan family alone.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// assert_eq!(tenferro_cpu::CpuPlanCache::new().contraction_stats().entries, 0);
128    /// ```
129    pub fn contraction_stats(&self) -> CacheStats {
130        tenferro_tensor::RuntimeCacheControl::stats(&self.lock().gemm)
131    }
132
133    /// Statistics of the indexed-traversal family alone.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// assert_eq!(tenferro_cpu::CpuPlanCache::new().indexed_stats().entries, 0);
139    /// ```
140    pub fn indexed_stats(&self) -> CacheStats {
141        self.lock().indexed.stats()
142    }
143
144    /// Drop every retained plan.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// let plans = tenferro_cpu::CpuPlanCache::new();
150    /// plans.clear();
151    /// assert_eq!(plans.stats().entries, 0);
152    /// ```
153    pub fn clear(&self) {
154        let mut state = self.lock();
155        tenferro_tensor::RuntimeCacheControl::clear(&mut state.gemm);
156        state.indexed.clear();
157    }
158
159    /// Drop every retained indexed-traversal plan.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// let plans = tenferro_cpu::CpuPlanCache::new();
165    /// plans.clear_indexed();
166    /// assert_eq!(plans.indexed_stats().entries, 0);
167    /// ```
168    pub fn clear_indexed(&self) {
169        self.lock().indexed.clear();
170    }
171
172    /// Maximum number of retained contraction plans.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// let plans = tenferro_cpu::CpuPlanCache::new();
178    /// plans.set_contraction_capacity(8);
179    /// assert_eq!(plans.contraction_capacity(), 8);
180    /// ```
181    pub fn contraction_capacity(&self) -> usize {
182        self.lock().gemm.capacity()
183    }
184
185    /// Bound the number of retained contraction plans.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// let plans = tenferro_cpu::CpuPlanCache::new();
191    /// plans.set_contraction_capacity(0);
192    /// assert_eq!(plans.contraction_capacity(), 0);
193    /// ```
194    pub fn set_contraction_capacity(&self, max_plans: usize) {
195        self.lock().gemm.set_capacity(max_plans);
196    }
197
198    /// Bounds of the indexed-traversal family.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// let plans = tenferro_cpu::CpuPlanCache::new();
204    /// let limits = plans.indexed_limits();
205    /// plans.set_indexed_limits(limits);
206    /// assert_eq!(plans.indexed_limits(), limits);
207    /// ```
208    pub fn indexed_limits(&self) -> IndexedPlanCacheLimits {
209        self.lock().indexed.limits()
210    }
211
212    /// Replace the bounds of the indexed-traversal family, evicting as needed.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// let plans = tenferro_cpu::CpuPlanCache::new();
218    /// plans.set_indexed_limits(plans.indexed_limits());
219    /// ```
220    pub fn set_indexed_limits(&self, limits: IndexedPlanCacheLimits) {
221        self.lock().indexed.set_limits(limits);
222    }
223
224    pub(crate) fn gather<E>(
225        &self,
226        key: IndexedPlanKey,
227        compile: impl FnOnce() -> Result<ErasedGatherPlan, E>,
228    ) -> Result<Arc<ErasedGatherPlan>, E> {
229        self.lock().indexed.gather(key, compile)
230    }
231
232    pub(crate) fn scatter<E>(
233        &self,
234        key: IndexedPlanKey,
235        compile: impl FnOnce() -> Result<ErasedScatterPlan, E>,
236    ) -> Result<Arc<ErasedScatterPlan>, E> {
237        self.lock().indexed.scatter(key, compile)
238    }
239
240    pub(crate) fn dynamic_slice<E>(
241        &self,
242        key: IndexedPlanKey,
243        compile: impl FnOnce() -> Result<ErasedDynamicSlicePlan, E>,
244    ) -> Result<Arc<ErasedDynamicSlicePlan>, E> {
245        self.lock().indexed.dynamic_slice(key, compile)
246    }
247
248    pub(crate) fn dynamic_update_slice<E>(
249        &self,
250        key: IndexedPlanKey,
251        compile: impl FnOnce() -> Result<ErasedDynamicUpdateSlicePlan, E>,
252    ) -> Result<Arc<ErasedDynamicUpdateSlicePlan>, E> {
253        self.lock().indexed.dynamic_update_slice(key, compile)
254    }
255}