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}