1use std::mem::{size_of, size_of_val};
2use std::sync::Arc;
3
4use lru::LruCache;
5use strided_kernel::{
6 ErasedDynamicSlicePlan, ErasedDynamicUpdateSlicePlan, ErasedGatherPlan, ErasedScatterPlan,
7 KernelDType,
8};
9use tenferro_tensor::CacheStats;
10use tinyvec::{Array, TinyVec};
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: TinyVec<[TinyVec<[usize; 8]>; 4]>,
97 strides: TinyVec<[TinyVec<[isize; 8]>; 4]>,
98 config: TinyVec<[TinyVec<[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_inline_vec_retained_bytes(&self.dims)
131 .saturating_add(nested_inline_vec_retained_bytes(&self.strides))
132 .saturating_add(nested_inline_vec_retained_bytes(&self.config))
133 }
134
135 fn logical_payload_bytes(&self) -> usize {
136 nested_inline_vec_logical_bytes(&self.dims)
137 .saturating_add(nested_inline_vec_logical_bytes(&self.strides))
138 .saturating_add(nested_inline_vec_logical_bytes(&self.config))
139 }
140}
141
142fn inline_vec_retained_bytes<A: Array>(values: &TinyVec<A>) -> usize {
143 if values.is_heap() {
144 values.capacity().saturating_mul(size_of::<A::Item>())
145 } else {
146 0
147 }
148}
149
150fn nested_inline_vec_retained_bytes<A, B>(values: &TinyVec<A>) -> usize
151where
152 A: Array<Item = TinyVec<B>>,
153 B: Array,
154{
155 inline_vec_retained_bytes(values).saturating_add(
156 values
157 .iter()
158 .map(inline_vec_retained_bytes)
159 .fold(0usize, usize::saturating_add),
160 )
161}
162
163fn nested_inline_vec_logical_bytes<A, B>(values: &TinyVec<A>) -> usize
164where
165 A: Array<Item = TinyVec<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)]
198pub(crate) struct IndexedPlanCache {
199 entries: LruCache<IndexedPlanKey, IndexedPlanCacheEntry>,
200 limits: IndexedPlanCacheLimits,
201 retained_bytes: usize,
202 hits: u64,
203 misses: u64,
204 evictions: u64,
205 clears: u64,
206}
207
208impl Default for IndexedPlanCache {
209 fn default() -> Self {
210 Self::new(DEFAULT_INDEXED_PLAN_CACHE_LIMITS)
211 }
212}
213
214impl IndexedPlanCache {
215 pub(crate) fn new(limits: IndexedPlanCacheLimits) -> Self {
216 Self {
217 entries: LruCache::unbounded(),
218 limits,
219 retained_bytes: 0,
220 hits: 0,
221 misses: 0,
222 evictions: 0,
223 clears: 0,
224 }
225 }
226
227 pub(crate) fn limits(&self) -> IndexedPlanCacheLimits {
228 self.limits
229 }
230
231 pub(crate) fn set_limits(&mut self, limits: IndexedPlanCacheLimits) {
232 self.limits = limits;
233 self.evict_to_limits();
234 }
235
236 pub(crate) fn clear(&mut self) {
237 self.entries.clear();
238 self.retained_bytes = 0;
239 self.clears = self.clears.saturating_add(1);
240 }
241
242 pub(crate) fn stats(&self) -> CacheStats {
243 CacheStats {
244 entries: self.entries.len(),
245 retained_bytes: self.retained_bytes,
246 hits: self.hits,
247 misses: self.misses,
248 evictions: self.evictions,
249 clears: self.clears,
250 }
251 }
252
253 pub(crate) fn gather<E>(
254 &mut self,
255 key: IndexedPlanKey,
256 compile: impl FnOnce() -> Result<ErasedGatherPlan, E>,
257 ) -> Result<Arc<ErasedGatherPlan>, E> {
258 if let Some(IndexedPlan::Gather(plan)) = self.lookup(&key) {
259 return Ok(plan);
260 }
261 let plan = Arc::new(compile()?);
262 self.insert(key, IndexedPlan::Gather(Arc::clone(&plan)));
263 Ok(plan)
264 }
265
266 pub(crate) fn scatter<E>(
267 &mut self,
268 key: IndexedPlanKey,
269 compile: impl FnOnce() -> Result<ErasedScatterPlan, E>,
270 ) -> Result<Arc<ErasedScatterPlan>, E> {
271 if let Some(IndexedPlan::Scatter(plan)) = self.lookup(&key) {
272 return Ok(plan);
273 }
274 let plan = Arc::new(compile()?);
275 self.insert(key, IndexedPlan::Scatter(Arc::clone(&plan)));
276 Ok(plan)
277 }
278
279 pub(crate) fn dynamic_slice<E>(
280 &mut self,
281 key: IndexedPlanKey,
282 compile: impl FnOnce() -> Result<ErasedDynamicSlicePlan, E>,
283 ) -> Result<Arc<ErasedDynamicSlicePlan>, E> {
284 if let Some(IndexedPlan::DynamicSlice(plan)) = self.lookup(&key) {
285 return Ok(plan);
286 }
287 let plan = Arc::new(compile()?);
288 self.insert(key, IndexedPlan::DynamicSlice(Arc::clone(&plan)));
289 Ok(plan)
290 }
291
292 pub(crate) fn dynamic_update_slice<E>(
293 &mut self,
294 key: IndexedPlanKey,
295 compile: impl FnOnce() -> Result<ErasedDynamicUpdateSlicePlan, E>,
296 ) -> Result<Arc<ErasedDynamicUpdateSlicePlan>, E> {
297 if let Some(IndexedPlan::DynamicUpdateSlice(plan)) = self.lookup(&key) {
298 return Ok(plan);
299 }
300 let plan = Arc::new(compile()?);
301 self.insert(key, IndexedPlan::DynamicUpdateSlice(Arc::clone(&plan)));
302 Ok(plan)
303 }
304
305 fn lookup(&mut self, key: &IndexedPlanKey) -> Option<IndexedPlan> {
306 let Some(entry) = self.entries.get(key) else {
307 self.misses = self.misses.saturating_add(1);
308 return None;
309 };
310 self.hits = self.hits.saturating_add(1);
311 Some(entry.plan.clone())
312 }
313
314 fn insert(&mut self, key: IndexedPlanKey, plan: IndexedPlan) {
315 if self.limits.max_entries == 0 || self.limits.max_retained_bytes == 0 {
316 return;
317 }
318 let retained_bytes = indexed_plan_retained_bytes(&key, &plan);
319 if retained_bytes > self.limits.max_retained_bytes {
320 return;
321 }
322 let entry = IndexedPlanCacheEntry {
323 plan,
324 retained_bytes,
325 };
326 if let Some(replaced) = self.entries.put(key, entry) {
327 self.retained_bytes = self.retained_bytes.saturating_sub(replaced.retained_bytes);
328 }
329 self.retained_bytes = self.retained_bytes.saturating_add(retained_bytes);
330 self.evict_to_limits();
331 }
332
333 fn evict_to_limits(&mut self) {
334 while self.entries.len() > self.limits.max_entries
335 || self.retained_bytes > self.limits.max_retained_bytes
336 {
337 let Some((_key, entry)) = self.entries.pop_lru() else {
338 break;
339 };
340 self.retained_bytes = self.retained_bytes.saturating_sub(entry.retained_bytes);
341 self.evictions = self.evictions.saturating_add(1);
342 }
343 }
344}
345
346fn indexed_plan_retained_bytes(key: &IndexedPlanKey, plan: &IndexedPlan) -> usize {
347 let plan_header = match plan {
348 IndexedPlan::Gather(plan) => size_of_val(plan.as_ref()),
349 IndexedPlan::Scatter(plan) => size_of_val(plan.as_ref()),
350 IndexedPlan::DynamicSlice(plan) => size_of_val(plan.as_ref()),
351 IndexedPlan::DynamicUpdateSlice(plan) => size_of_val(plan.as_ref()),
352 };
353 size_of::<IndexedPlanCacheEntry>()
358 .saturating_add(size_of::<IndexedPlanKey>())
359 .saturating_add(plan_header)
360 .saturating_add(key.retained_bytes())
361 .saturating_add(key.logical_payload_bytes().saturating_mul(2))
362}
363
364#[cfg(test)]
365mod tests;