1use std::mem::{size_of, size_of_val};
2use std::sync::Arc;
3
4use lru::LruCache;
5use smallvec::{Array, SmallVec};
6use strided_kernel::{
7 ErasedDynamicSlicePlan, ErasedDynamicUpdateSlicePlan, ErasedGatherPlan, ErasedScatterPlan,
8 KernelDType,
9};
10use tenferro_tensor::CacheStats;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub struct IndexedPlanCacheLimits {
28 max_entries: usize,
29 max_retained_bytes: usize,
30}
31
32impl IndexedPlanCacheLimits {
33 pub const fn new(max_entries: usize, max_retained_bytes: usize) -> Self {
44 Self {
45 max_entries,
46 max_retained_bytes,
47 }
48 }
49
50 pub const fn max_entries(self) -> usize {
60 self.max_entries
61 }
62
63 pub const fn max_retained_bytes(self) -> usize {
76 self.max_retained_bytes
77 }
78}
79
80pub(crate) const DEFAULT_INDEXED_PLAN_CACHE_LIMITS: IndexedPlanCacheLimits =
81 IndexedPlanCacheLimits::new(256, 8 * 1024 * 1024);
82
83#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
84pub(crate) enum IndexedPlanFamily {
85 Gather,
86 Scatter,
87 DynamicSlice,
88 DynamicUpdateSlice,
89}
90
91#[derive(Clone, Debug, Eq, Hash, PartialEq)]
92pub(crate) struct IndexedPlanKey {
93 family: IndexedPlanFamily,
94 dtype: KernelDType,
95 index_dtype: KernelDType,
96 dims: SmallVec<[SmallVec<[usize; 8]>; 4]>,
97 strides: SmallVec<[SmallVec<[isize; 8]>; 4]>,
98 config: SmallVec<[SmallVec<[usize; 8]>; 5]>,
99}
100
101impl IndexedPlanKey {
102 pub(crate) fn from_slices(
103 family: IndexedPlanFamily,
104 dtype: KernelDType,
105 index_dtype: KernelDType,
106 dims: &[&[usize]],
107 strides: &[&[isize]],
108 config: &[&[usize]],
109 ) -> Self {
110 Self {
111 family,
112 dtype,
113 index_dtype,
114 dims: dims
115 .iter()
116 .map(|values| values.iter().copied().collect())
117 .collect(),
118 strides: strides
119 .iter()
120 .map(|values| values.iter().copied().collect())
121 .collect(),
122 config: config
123 .iter()
124 .map(|values| values.iter().copied().collect())
125 .collect(),
126 }
127 }
128
129 fn retained_bytes(&self) -> usize {
130 nested_smallvec_retained_bytes(&self.dims)
131 .saturating_add(nested_smallvec_retained_bytes(&self.strides))
132 .saturating_add(nested_smallvec_retained_bytes(&self.config))
133 }
134
135 fn logical_payload_bytes(&self) -> usize {
136 nested_smallvec_logical_bytes(&self.dims)
137 .saturating_add(nested_smallvec_logical_bytes(&self.strides))
138 .saturating_add(nested_smallvec_logical_bytes(&self.config))
139 }
140}
141
142fn smallvec_retained_bytes<A: Array>(values: &SmallVec<A>) -> usize {
143 if values.spilled() {
144 values.capacity().saturating_mul(size_of::<A::Item>())
145 } else {
146 0
147 }
148}
149
150fn nested_smallvec_retained_bytes<A, B>(values: &SmallVec<A>) -> usize
151where
152 A: Array<Item = SmallVec<B>>,
153 B: Array,
154{
155 smallvec_retained_bytes(values).saturating_add(
156 values
157 .iter()
158 .map(smallvec_retained_bytes)
159 .fold(0usize, usize::saturating_add),
160 )
161}
162
163fn nested_smallvec_logical_bytes<A, B>(values: &SmallVec<A>) -> usize
164where
165 A: Array<Item = SmallVec<B>>,
166 B: Array,
167{
168 values.iter().fold(0usize, |total, inner| {
169 total.saturating_add(inner.len().saturating_mul(size_of::<B::Item>()))
170 })
171}
172
173#[derive(Clone, Debug)]
174enum IndexedPlan {
175 Gather(Arc<ErasedGatherPlan>),
176 Scatter(Arc<ErasedScatterPlan>),
177 DynamicSlice(Arc<ErasedDynamicSlicePlan>),
178 DynamicUpdateSlice(Arc<ErasedDynamicUpdateSlicePlan>),
179}
180
181#[derive(Debug)]
182struct IndexedPlanCacheEntry {
183 plan: IndexedPlan,
184 retained_bytes: usize,
185}
186
187#[derive(Debug)]
199pub(crate) struct IndexedPlanCache {
200 entries: LruCache<IndexedPlanKey, IndexedPlanCacheEntry>,
201 limits: IndexedPlanCacheLimits,
202 retained_bytes: usize,
203 hits: u64,
204 misses: u64,
205 evictions: u64,
206 clears: u64,
207}
208
209impl Default for IndexedPlanCache {
210 fn default() -> Self {
211 Self::new(DEFAULT_INDEXED_PLAN_CACHE_LIMITS)
212 }
213}
214
215impl IndexedPlanCache {
216 pub(crate) fn new(limits: IndexedPlanCacheLimits) -> Self {
217 Self {
218 entries: LruCache::unbounded(),
219 limits,
220 retained_bytes: 0,
221 hits: 0,
222 misses: 0,
223 evictions: 0,
224 clears: 0,
225 }
226 }
227
228 pub(crate) fn set_limits(&mut self, limits: IndexedPlanCacheLimits) {
229 self.limits = limits;
230 self.evict_to_limits();
231 }
232
233 pub(crate) fn clear(&mut self) {
234 self.entries.clear();
235 self.retained_bytes = 0;
236 self.clears = self.clears.saturating_add(1);
237 }
238
239 pub(crate) fn stats(&self) -> CacheStats {
240 CacheStats {
241 entries: self.entries.len(),
242 retained_bytes: self.retained_bytes,
243 hits: self.hits,
244 misses: self.misses,
245 evictions: self.evictions,
246 clears: self.clears,
247 }
248 }
249
250 pub(crate) fn gather<E>(
251 &mut self,
252 key: IndexedPlanKey,
253 compile: impl FnOnce() -> Result<ErasedGatherPlan, E>,
254 ) -> Result<Arc<ErasedGatherPlan>, E> {
255 if let Some(IndexedPlan::Gather(plan)) = self.lookup(&key) {
256 return Ok(plan);
257 }
258 let plan = Arc::new(compile()?);
259 self.insert(key, IndexedPlan::Gather(Arc::clone(&plan)));
260 Ok(plan)
261 }
262
263 pub(crate) fn scatter<E>(
264 &mut self,
265 key: IndexedPlanKey,
266 compile: impl FnOnce() -> Result<ErasedScatterPlan, E>,
267 ) -> Result<Arc<ErasedScatterPlan>, E> {
268 if let Some(IndexedPlan::Scatter(plan)) = self.lookup(&key) {
269 return Ok(plan);
270 }
271 let plan = Arc::new(compile()?);
272 self.insert(key, IndexedPlan::Scatter(Arc::clone(&plan)));
273 Ok(plan)
274 }
275
276 pub(crate) fn dynamic_slice<E>(
277 &mut self,
278 key: IndexedPlanKey,
279 compile: impl FnOnce() -> Result<ErasedDynamicSlicePlan, E>,
280 ) -> Result<Arc<ErasedDynamicSlicePlan>, E> {
281 if let Some(IndexedPlan::DynamicSlice(plan)) = self.lookup(&key) {
282 return Ok(plan);
283 }
284 let plan = Arc::new(compile()?);
285 self.insert(key, IndexedPlan::DynamicSlice(Arc::clone(&plan)));
286 Ok(plan)
287 }
288
289 pub(crate) fn dynamic_update_slice<E>(
290 &mut self,
291 key: IndexedPlanKey,
292 compile: impl FnOnce() -> Result<ErasedDynamicUpdateSlicePlan, E>,
293 ) -> Result<Arc<ErasedDynamicUpdateSlicePlan>, E> {
294 if let Some(IndexedPlan::DynamicUpdateSlice(plan)) = self.lookup(&key) {
295 return Ok(plan);
296 }
297 let plan = Arc::new(compile()?);
298 self.insert(key, IndexedPlan::DynamicUpdateSlice(Arc::clone(&plan)));
299 Ok(plan)
300 }
301
302 fn lookup(&mut self, key: &IndexedPlanKey) -> Option<IndexedPlan> {
303 let Some(entry) = self.entries.get(key) else {
304 self.misses = self.misses.saturating_add(1);
305 return None;
306 };
307 self.hits = self.hits.saturating_add(1);
308 Some(entry.plan.clone())
309 }
310
311 fn insert(&mut self, key: IndexedPlanKey, plan: IndexedPlan) {
312 if self.limits.max_entries == 0 || self.limits.max_retained_bytes == 0 {
313 return;
314 }
315 let retained_bytes = indexed_plan_retained_bytes(&key, &plan);
316 if retained_bytes > self.limits.max_retained_bytes {
317 return;
318 }
319 let entry = IndexedPlanCacheEntry {
320 plan,
321 retained_bytes,
322 };
323 if let Some(replaced) = self.entries.put(key, entry) {
324 self.retained_bytes = self.retained_bytes.saturating_sub(replaced.retained_bytes);
325 }
326 self.retained_bytes = self.retained_bytes.saturating_add(retained_bytes);
327 self.evict_to_limits();
328 }
329
330 fn evict_to_limits(&mut self) {
331 while self.entries.len() > self.limits.max_entries
332 || self.retained_bytes > self.limits.max_retained_bytes
333 {
334 let Some((_key, entry)) = self.entries.pop_lru() else {
335 break;
336 };
337 self.retained_bytes = self.retained_bytes.saturating_sub(entry.retained_bytes);
338 self.evictions = self.evictions.saturating_add(1);
339 }
340 }
341}
342
343fn indexed_plan_retained_bytes(key: &IndexedPlanKey, plan: &IndexedPlan) -> usize {
344 let plan_header = match plan {
345 IndexedPlan::Gather(plan) => size_of_val(plan.as_ref()),
346 IndexedPlan::Scatter(plan) => size_of_val(plan.as_ref()),
347 IndexedPlan::DynamicSlice(plan) => size_of_val(plan.as_ref()),
348 IndexedPlan::DynamicUpdateSlice(plan) => size_of_val(plan.as_ref()),
349 };
350 size_of::<IndexedPlanCacheEntry>()
355 .saturating_add(size_of::<IndexedPlanKey>())
356 .saturating_add(plan_header)
357 .saturating_add(key.retained_bytes())
358 .saturating_add(key.logical_payload_bytes().saturating_mul(2))
359}
360
361#[cfg(test)]
362mod tests;