1use crate::{CompiledGraph, Device, Session};
29use rlx_ir::DimBinding;
30use rlx_ir::Graph;
31use rlx_ir::hir::HirModule;
32use rlx_opt::CompileResult;
33use std::collections::HashMap;
34use std::collections::VecDeque;
35use std::ops::Range;
36
37pub struct CacheRunInput<'a> {
39 pub name: &'a str,
40 pub data: &'a [f32],
41 pub row_inner: Option<usize>,
43}
44
45pub struct CompileCache {
46 device: Device,
47 capacity: usize,
48 policy: Option<rlx_opt::PrecisionPolicy>,
51 entries: Vec<(u64, CompiledGraph)>,
55 order: VecDeque<u64>,
57}
58
59impl CompileCache {
60 pub fn new(device: Device, capacity: usize) -> Self {
61 Self::with_policy(device, capacity, None)
62 }
63
64 pub fn with_policy(
68 device: Device,
69 capacity: usize,
70 policy: Option<rlx_opt::PrecisionPolicy>,
71 ) -> Self {
72 assert!(capacity > 0, "CompileCache capacity must be ≥ 1");
73 Self {
74 device,
75 capacity,
76 policy,
77 entries: Vec::with_capacity(capacity),
78 order: VecDeque::with_capacity(capacity),
79 }
80 }
81
82 pub fn get_or_compile<F: FnOnce() -> Graph>(
86 &mut self,
87 key: u64,
88 build: F,
89 ) -> &mut CompiledGraph {
90 self.get_or_compile_with_options(key, build, &crate::CompileOptions::new())
91 }
92
93 pub fn get_or_compile_with_options<F: FnOnce() -> Graph>(
95 &mut self,
96 key: u64,
97 build: F,
98 options: &crate::CompileOptions,
99 ) -> &mut CompiledGraph {
100 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
101 return &mut self.entries[idx].1;
102 }
103 let mut session = Session::new(self.device);
104 if let Some(p) = &self.policy {
105 session = session.with_policy(p.clone());
106 }
107 let compiled = session.compile_with(build(), options);
108
109 if self.entries.len() >= self.capacity
111 && let Some(evict_key) = self.order.pop_front()
112 {
113 sync_evicted_entry(&mut self.entries, evict_key);
114 self.entries.retain(|(k, _)| *k != evict_key);
115 }
116 self.entries.push((key, compiled));
117 self.order.push_back(key);
118 &mut self.entries.last_mut().unwrap().1
119 }
120
121 pub fn get_or_compile_hir_with_options<F: FnOnce() -> rlx_ir::hir::HirModule>(
123 &mut self,
124 key: u64,
125 build: F,
126 options: &crate::CompileOptions,
127 ) -> &mut CompiledGraph {
128 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
129 return &mut self.entries[idx].1;
130 }
131 let mut session = Session::new(self.device);
132 if let Some(p) = &self.policy {
133 session = session.with_policy(p.clone());
134 }
135 let compiled = session
136 .compile_hir_with(build(), options)
137 .expect("HIR lower/compile in compile cache");
138
139 if self.entries.len() >= self.capacity
140 && let Some(evict_key) = self.order.pop_front()
141 {
142 sync_evicted_entry(&mut self.entries, evict_key);
143 self.entries.retain(|(k, _)| *k != evict_key);
144 }
145 self.entries.push((key, compiled));
146 self.order.push_back(key);
147 &mut self.entries.last_mut().unwrap().1
148 }
149
150 pub fn len(&self) -> usize {
152 self.entries.len()
153 }
154 pub fn is_empty(&self) -> bool {
155 self.entries.is_empty()
156 }
157 pub fn contains(&self, key: u64) -> bool {
159 self.entries.iter().any(|(k, _)| *k == key)
160 }
161
162 pub fn clear(&mut self) {
164 self.sync_all();
165 self.entries.clear();
166 self.order.clear();
167 }
168
169 pub fn sync_all(&mut self) {
171 for (_, compiled) in &mut self.entries {
172 compiled.sync_pending();
173 }
174 }
175}
176
177fn sync_evicted_entry(entries: &mut [(u64, CompiledGraph)], evict_key: u64) {
178 if let Some((_, compiled)) = entries.iter_mut().find(|(k, _)| *k == evict_key) {
179 compiled.sync_pending();
180 }
181}
182
183pub struct BucketedCompileCache {
230 device: Device,
231 policy: Option<rlx_opt::PrecisionPolicy>,
232 buckets: Vec<Bucket>,
233 weight_donor_upper: Option<u64>,
238 clock: u64,
240}
241
242struct Bucket {
243 range: Range<u64>,
244 compiled: Option<CompiledGraph>,
245 resident_bytes: usize,
248 last_used: u64,
250}
251
252const LARGE_BUCKET_BYTES: usize = 256 * 1024 * 1024;
258
259fn max_resident_large_buckets() -> usize {
269 rlx_ir::env::var("RLX_KV_CACHE_MAX_RESIDENT")
270 .and_then(|s| s.parse::<usize>().ok())
271 .filter(|&n| n >= 1)
272 .unwrap_or(1)
273}
274
275fn device_has_discrete_vram(dev: Device) -> bool {
280 matches!(
281 dev,
282 Device::Gpu
283 | Device::Cuda
284 | Device::Vulkan
285 | Device::Rocm
286 | Device::OneApi
287 | Device::DirectX
288 | Device::WebGpu
289 )
290}
291
292impl BucketedCompileCache {
293 pub fn new(device: Device, buckets: Vec<Range<u64>>) -> Self {
294 Self::with_policy(device, buckets, None)
295 }
296
297 pub fn power_of_two_ladder(device: Device, min: u64, max: u64) -> Self {
313 Self::power_of_two_ladder_with_policy(device, min, max, None)
314 }
315
316 pub fn power_of_two_ladder_with_policy(
317 device: Device,
318 min: u64,
319 max: u64,
320 policy: Option<rlx_opt::PrecisionPolicy>,
321 ) -> Self {
322 assert!(min >= 1, "power_of_two_ladder: min must be ≥ 1, got {min}");
323 assert!(
324 max >= min,
325 "power_of_two_ladder: max ({max}) must be ≥ min ({min})"
326 );
327 let mut buckets: Vec<Range<u64>> = Vec::new();
328 let mut start = 1u64;
329 let mut extent = min.next_power_of_two();
330 loop {
331 buckets.push(start..(extent + 1));
332 if extent >= max {
333 break;
334 }
335 start = extent + 1;
336 extent = extent
337 .checked_mul(2)
338 .expect("power_of_two_ladder: extent overflow");
339 }
340 Self::with_policy(device, buckets, policy)
341 }
342
343 pub fn with_policy(
344 device: Device,
345 buckets: Vec<Range<u64>>,
346 policy: Option<rlx_opt::PrecisionPolicy>,
347 ) -> Self {
348 assert!(!buckets.is_empty(), "BucketedCompileCache needs ≥1 bucket");
349 for (i, b) in buckets.iter().enumerate() {
350 assert!(b.start < b.end, "bucket {i} ({b:?}) is empty");
351 if i + 1 < buckets.len() {
352 assert!(
353 b.end <= buckets[i + 1].start,
354 "buckets {i} ({b:?}) and {} ({:?}) overlap",
355 i + 1,
356 buckets[i + 1],
357 );
358 }
359 }
360 let buckets = buckets
361 .into_iter()
362 .map(|range| Bucket {
363 range,
364 compiled: None,
365 resident_bytes: 0,
366 last_used: 0,
367 })
368 .collect();
369 Self {
370 device,
371 policy,
372 buckets,
373 weight_donor_upper: None,
374 clock: 0,
375 }
376 }
377
378 fn touch(&mut self, idx: usize) {
380 self.clock += 1;
381 self.buckets[idx].last_used = self.clock;
382 }
383
384 fn evict_for_incoming(&mut self, incoming_idx: usize, incoming_bytes: usize) {
391 if rlx_ir::env::var("RLX_KV_CACHE_NO_EVICT").is_some() {
392 return;
393 }
394 let unified_opt_in = rlx_ir::env::var("RLX_KV_CACHE_MAX_RESIDENT").is_some();
401 if !device_has_discrete_vram(self.device) && !unified_opt_in {
402 return;
403 }
404 if rlx_ir::env::var("RLX_KV_CACHE_DBG").is_some() {
405 let (n_large, resident): (usize, usize) = self
406 .buckets
407 .iter()
408 .filter(|b| b.compiled.is_some())
409 .fold((0, 0), |(n, r), b| {
410 let is_large = b.resident_bytes >= LARGE_BUCKET_BYTES;
411 (n + is_large as usize, r + b.resident_bytes)
412 });
413 eprintln!(
414 "[KVCACHE] compiling bucket idx={incoming_idx} incoming={:.2}GB \
415 resident_large={n_large} resident_total={:.2}GB cap={}",
416 incoming_bytes as f64 / 1e9,
417 resident as f64 / 1e9,
418 max_resident_large_buckets(),
419 );
420 }
421 if incoming_bytes < LARGE_BUCKET_BYTES {
422 return;
423 }
424 let cap = max_resident_large_buckets();
425 loop {
426 let large: Vec<usize> = (0..self.buckets.len())
427 .filter(|&j| {
428 j != incoming_idx
429 && self.buckets[j].compiled.is_some()
430 && self.buckets[j].resident_bytes >= LARGE_BUCKET_BYTES
431 })
432 .collect();
433 if large.len() < cap {
435 break;
436 }
437 let Some(&victim) = large.iter().min_by_key(|&&j| self.buckets[j].last_used) else {
438 break;
439 };
440 if let Some(c) = &mut self.buckets[victim].compiled {
441 c.sync_pending();
442 }
443 let victim_upper = self.buckets[victim].range.end.saturating_sub(1);
444 self.buckets[victim].compiled = None;
445 self.buckets[victim].resident_bytes = 0;
446 if self.weight_donor_upper == Some(victim_upper) {
447 self.weight_donor_upper = None;
448 }
449 }
450 }
451
452 pub fn get_or_compile<F: FnOnce(u64) -> Graph>(
461 &mut self,
462 key: u64,
463 build: F,
464 ) -> Option<(u64, &mut CompiledGraph)> {
465 self.get_or_compile_with_options(key, build, &crate::CompileOptions::new())
466 }
467
468 pub fn get_or_compile_with_options<F: FnOnce(u64) -> Graph>(
470 &mut self,
471 key: u64,
472 build: F,
473 options: &crate::CompileOptions,
474 ) -> Option<(u64, &mut CompiledGraph)> {
475 let idx = self.bucket_for(key)?;
476 let upper = self.buckets[idx].range.end - 1;
477 if self.buckets[idx].compiled.is_none() {
478 let mut session = Session::new(self.device);
479 if let Some(p) = &self.policy {
480 session = session.with_policy(p.clone());
481 }
482 self.buckets[idx].compiled = Some(session.compile_with(build(upper), options));
483 }
484 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
485 }
486
487 pub fn get_or_compile_hir<F: FnOnce(u64) -> HirModule>(
490 &mut self,
491 key: u64,
492 build: F,
493 ) -> Option<(u64, &mut CompiledGraph)> {
494 self.get_or_compile_hir_with_options(key, build, &crate::CompileOptions::new())
495 }
496
497 pub fn get_or_compile_hir_with_options<F: FnOnce(u64) -> HirModule>(
499 &mut self,
500 key: u64,
501 build: F,
502 options: &crate::CompileOptions,
503 ) -> Option<(u64, &mut CompiledGraph)> {
504 let idx = self.bucket_for(key)?;
505 let upper = self.buckets[idx].range.end - 1;
506 if self.buckets[idx].compiled.is_none() {
507 let mut session = Session::new(self.device);
508 if let Some(p) = &self.policy {
509 session = session.with_policy(p.clone());
510 }
511 let compiled = session
512 .compile_hir_with(build(upper), options)
513 .expect("HIR lower/compile in bucketed cache");
514 self.buckets[idx].compiled = Some(compiled);
515 }
516 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
517 }
518
519 pub fn bucket_for(&self, key: u64) -> Option<usize> {
522 self.buckets.iter().position(|b| b.range.contains(&key))
523 }
524
525 pub fn bucket_upper_for_key(&self, key: u64) -> Option<u64> {
527 let idx = self.bucket_for(key)?;
528 Some(self.buckets[idx].range.end - 1)
529 }
530
531 pub fn buckets(&self) -> impl Iterator<Item = &Range<u64>> {
532 self.buckets.iter().map(|b| &b.range)
533 }
534
535 pub fn compiled_count(&self) -> usize {
537 self.buckets.iter().filter(|b| b.compiled.is_some()).count()
538 }
539
540 pub fn compiled_for_key_mut(&mut self, key: u64) -> Option<&mut CompiledGraph> {
542 let idx = self.bucket_for(key)?;
543 self.buckets[idx].compiled.as_mut()
544 }
545
546 pub fn compiled_for_upper(&self, upper: u64) -> Option<&CompiledGraph> {
548 self.buckets
549 .iter()
550 .find(|b| b.range.end.saturating_sub(1) == upper)
551 .and_then(|b| b.compiled.as_ref())
552 }
553
554 pub fn try_copy_params_between_uppers(&mut self, dst_upper: u64, src_upper: u64) -> bool {
556 if dst_upper == src_upper {
557 return true;
558 }
559 let dst_idx = self
560 .buckets
561 .iter()
562 .position(|b| b.range.end.saturating_sub(1) == dst_upper);
563 let src_idx = self
564 .buckets
565 .iter()
566 .position(|b| b.range.end.saturating_sub(1) == src_upper);
567 let (Some(dst_idx), Some(src_idx)) = (dst_idx, src_idx) else {
568 return false;
569 };
570 if dst_idx == src_idx {
571 return true;
572 }
573 let (dst, src) = if dst_idx < src_idx {
574 let (left, right) = self.buckets.split_at_mut(src_idx);
575 (&mut left[dst_idx], &right[0])
576 } else {
577 let (left, right) = self.buckets.split_at_mut(dst_idx);
578 (&mut right[0], &left[src_idx])
579 };
580 let Some(dst_c) = dst.compiled.as_mut() else {
581 return false;
582 };
583 let Some(src_c) = src.compiled.as_ref() else {
584 return false;
585 };
586 dst_c.copy_params_from(src_c)
587 }
588
589 pub fn weight_donor(&self) -> Option<&CompiledGraph> {
591 self.compiled_for_upper(self.weight_donor_upper?)
592 }
593
594 pub fn set_weight_donor(&mut self, upper: u64) {
598 if self.weight_donor_upper.is_none() {
599 self.weight_donor_upper = Some(upper);
600 }
601 }
602
603 pub fn try_share_params_from_donor(&mut self, dst_upper: u64) -> bool {
610 if rlx_ir::env::var("RLX_METAL_NO_SHARE").is_some() {
611 return false;
612 }
613 let mut candidates: Vec<u64> = Vec::new();
614 if let Some(src) = self.weight_donor_upper {
615 candidates.push(src);
616 }
617 for b in &self.buckets {
618 if b.compiled.is_none() {
619 continue;
620 }
621 let u = b.range.end.saturating_sub(1);
622 if u != dst_upper && !candidates.contains(&u) {
623 candidates.push(u);
624 }
625 }
626 for src_upper in candidates {
627 if self.try_share_params_between_uppers(dst_upper, src_upper) {
628 self.weight_donor_upper = Some(src_upper);
629 return true;
630 }
631 }
632 false
633 }
634
635 fn try_share_params_between_uppers(&mut self, dst_upper: u64, src_upper: u64) -> bool {
636 if dst_upper == src_upper {
637 return true;
638 }
639 let dst_idx = self
640 .buckets
641 .iter()
642 .position(|b| b.range.end.saturating_sub(1) == dst_upper);
643 let src_idx = self
644 .buckets
645 .iter()
646 .position(|b| b.range.end.saturating_sub(1) == src_upper);
647 let (Some(dst_idx), Some(src_idx)) = (dst_idx, src_idx) else {
648 return false;
649 };
650 if dst_idx == src_idx {
651 return true;
652 }
653 let (dst, src) = if dst_idx < src_idx {
654 let (left, right) = self.buckets.split_at_mut(src_idx);
655 (&mut left[dst_idx], &right[0])
656 } else {
657 let (left, right) = self.buckets.split_at_mut(dst_idx);
658 (&mut right[0], &left[src_idx])
659 };
660 let Some(dst_c) = dst.compiled.as_mut() else {
661 return false;
662 };
663 let Some(src_c) = src.compiled.as_ref() else {
664 return false;
665 };
666 dst_c.share_params_from(src_c)
667 }
668
669 pub fn seed_resident_kv_prefix_from_keys(
673 &mut self,
674 src_key: u64,
675 dst_key: u64,
676 prefix_tokens: usize,
677 outgoing_upper: usize,
678 kv_dim: usize,
679 n_layers: usize,
680 ) -> bool {
681 let Some(src_idx) = self.bucket_for(src_key) else {
682 return false;
683 };
684 let Some(dst_idx) = self.bucket_for(dst_key) else {
685 return false;
686 };
687 if src_idx == dst_idx {
688 return true;
689 }
690 if src_idx < dst_idx {
691 let (left, right) = self.buckets.split_at_mut(dst_idx);
692 let Some(src) = left[src_idx].compiled.as_ref() else {
693 return false;
694 };
695 let Some(dst) = right[0].compiled.as_mut() else {
696 return false;
697 };
698 return dst.seed_resident_kv_prefix_from(
699 src,
700 prefix_tokens,
701 outgoing_upper,
702 kv_dim,
703 n_layers,
704 );
705 }
706 let (left, right) = self.buckets.split_at_mut(src_idx);
707 let Some(src) = right[0].compiled.as_ref() else {
708 return false;
709 };
710 let Some(dst) = left[dst_idx].compiled.as_mut() else {
711 return false;
712 };
713 dst.seed_resident_kv_prefix_from(src, prefix_tokens, outgoing_upper, kv_dim, n_layers)
714 }
715
716 pub fn rebind_resident_kv_hybrid_from_keys(
720 &mut self,
721 src_key: u64,
722 dst_key: u64,
723 host_k: &[Vec<f32>],
724 host_v: &[Vec<f32>],
725 prefix_tokens: usize,
726 outgoing_upper: usize,
727 upper: usize,
728 kv_dim: usize,
729 n_layers: usize,
730 ) -> bool {
731 let Some(src_idx) = self.bucket_for(src_key) else {
732 return false;
733 };
734 let Some(dst_idx) = self.bucket_for(dst_key) else {
735 return false;
736 };
737 if src_idx == dst_idx {
738 return true;
739 }
740 let host_rows = host_k.first().map(|k| k.len() / kv_dim.max(1)).unwrap_or(0);
741 let rebind = |src: &CompiledGraph, dst: &mut CompiledGraph| -> bool {
742 for i in 0..n_layers {
743 let mut kp = vec![0f32; upper * kv_dim];
744 let mut vp = vec![0f32; upper * kv_dim];
745 let nk = host_k[i].len().min(kp.len());
746 let nv = host_v[i].len().min(vp.len());
747 kp[..nk].copy_from_slice(&host_k[i][..nk]);
748 vp[..nv].copy_from_slice(&host_v[i][..nv]);
749 let k_name = format!("past_k_{i}");
750 let v_name = format!("past_v_{i}");
751 if !dst.bind_gpu_handle(&k_name, &kp) || !dst.bind_gpu_handle(&v_name, &vp) {
752 return false;
753 }
754 dst.register_kv_row_feed(&k_name, 1 + 2 * i);
755 dst.register_kv_row_feed(&v_name, 2 + 2 * i);
756 }
757 dst.stage_bound_gpu_handles_to_arena();
758 if host_rows < prefix_tokens {
759 return dst.copy_resident_kv_rows_from(
760 src,
761 host_rows,
762 prefix_tokens,
763 outgoing_upper,
764 kv_dim,
765 n_layers,
766 );
767 }
768 for i in 0..n_layers {
769 let k_name = format!("past_k_{i}");
770 let v_name = format!("past_v_{i}");
771 dst.prepare_resident_gpu_handle(&k_name);
772 dst.prepare_resident_gpu_handle(&v_name);
773 }
774 true
775 };
776 if src_idx < dst_idx {
777 let (left, right) = self.buckets.split_at_mut(dst_idx);
778 let Some(src) = left[src_idx].compiled.as_ref() else {
779 return false;
780 };
781 let Some(dst) = right[0].compiled.as_mut() else {
782 return false;
783 };
784 return rebind(src, dst);
785 }
786 let (left, right) = self.buckets.split_at_mut(src_idx);
787 let Some(src) = right[0].compiled.as_ref() else {
788 return false;
789 };
790 let Some(dst) = left[dst_idx].compiled.as_mut() else {
791 return false;
792 };
793 rebind(src, dst)
794 }
795
796 pub fn total_buckets(&self) -> usize {
797 self.buckets.len()
798 }
799
800 pub fn evict_except(&mut self, keep: usize) {
802 for (i, bucket) in self.buckets.iter_mut().enumerate() {
803 if i != keep {
804 bucket.compiled = None;
805 }
806 }
807 }
808
809 pub fn evict_one_except(&mut self, protect: usize) -> Option<usize> {
823 let victim = self
824 .buckets
825 .iter()
826 .enumerate()
827 .rev()
828 .find(|(i, b)| *i != protect && b.compiled.is_some())
829 .map(|(i, _)| i)?;
830 if let Some(compiled) = self.buckets[victim].compiled.as_mut() {
831 compiled.sync_pending();
832 }
833 self.buckets[victim].compiled = None;
834 Some(victim)
835 }
836
837 pub fn clear_compiled(&mut self) {
839 for bucket in &mut self.buckets {
840 bucket.compiled = None;
841 }
842 }
843
844 pub fn run_padded<F: FnOnce(u64) -> Graph>(
870 &mut self,
871 key: u64,
872 actual_rows: usize,
873 build: F,
874 inputs: &[(&str, &[f32], usize)],
875 output_inners: &[usize],
876 ) -> Option<(u64, Vec<Vec<f32>>)> {
877 let (upper, compiled) = self.get_or_compile(key, build)?;
878
879 let padded: Vec<(&str, Vec<f32>)> = inputs
881 .iter()
882 .map(|(name, data, inner)| (*name, pad_rows(data, *inner, upper)))
883 .collect();
884 let pairs: Vec<(&str, &[f32])> = padded.iter().map(|(n, d)| (*n, d.as_slice())).collect();
885
886 compiled.set_active_extent(Some((actual_rows, upper as usize)));
892 let raw_outputs = compiled.run(&pairs);
893 compiled.set_active_extent(None);
894 #[cfg(feature = "cpu")]
895 crate::onnx_active::set_active_token_count(None);
896
897 let outs = raw_outputs
898 .into_iter()
899 .enumerate()
900 .map(|(i, out)| match output_inners.get(i).copied() {
901 Some(0) | None => out,
902 Some(inner) => slice_rows(&out, inner, actual_rows),
903 })
904 .collect();
905
906 Some((upper, outs))
907 }
908
909 pub fn ensure_graph_with_params<F>(
911 &mut self,
912 key: u64,
913 build: F,
914 options: &crate::CompileOptions,
915 ) -> Option<(u64, &mut CompiledGraph)>
916 where
917 F: FnOnce(u64) -> (Graph, HashMap<String, Vec<f32>>),
918 {
919 let idx = self.bucket_for(key)?;
920 let upper = self.buckets[idx].range.end - 1;
921 if self.buckets[idx].compiled.is_none() {
922 let (graph, params) = build(upper);
923 let bytes: usize = params.values().map(|v| v.len() * 4).sum();
924 self.evict_for_incoming(idx, bytes);
925 let mut session = Session::new(self.device);
926 if let Some(p) = &self.policy {
927 session = session.with_policy(p.clone());
928 }
929 let mut compiled = session.compile_with(graph, options);
930 for (name, data) in params {
931 compiled.set_param(&name, &data);
932 }
933 self.buckets[idx].compiled = Some(compiled);
934 self.buckets[idx].resident_bytes = bytes;
935 }
936 self.touch(idx);
937 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
938 }
939
940 pub fn ensure_graph_with_packed<F>(
948 &mut self,
949 key: u64,
950 build: F,
951 options: &crate::CompileOptions,
952 ) -> Option<(u64, &mut CompiledGraph)>
953 where
954 F: FnOnce(u64) -> (Graph, HashMap<String, Vec<f32>>, Vec<(String, Vec<u8>)>),
955 {
956 let idx = self.bucket_for(key)?;
957 let upper = self.buckets[idx].range.end - 1;
958 if self.buckets[idx].compiled.is_none() {
959 let (graph, params, packed) = build(upper);
960 let bytes: usize = params.values().map(|v| v.len() * 4).sum::<usize>()
961 + packed.iter().map(|(_, b)| b.len()).sum::<usize>();
962 self.evict_for_incoming(idx, bytes);
963 let mut session = Session::new(self.device);
964 if let Some(p) = &self.policy {
965 session = session.with_policy(p.clone());
966 }
967 let mut compiled = session.compile_with(graph, options);
968 for (name, data) in params {
969 compiled.set_param(&name, &data);
970 }
971 for (name, data) in &packed {
972 compiled.set_param_typed(name, data, rlx_ir::DType::U8);
973 }
974 self.buckets[idx].compiled = Some(compiled);
975 self.buckets[idx].resident_bytes = bytes;
976 }
977 self.touch(idx);
978 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
979 }
980
981 pub fn ensure_hir_with_params<F>(
983 &mut self,
984 key: u64,
985 build: F,
986 options: &crate::CompileOptions,
987 ) -> Option<(u64, &mut CompiledGraph)>
988 where
989 F: FnOnce(u64) -> (HirModule, HashMap<String, Vec<f32>>),
990 {
991 let idx = self.bucket_for(key)?;
992 let upper = self.buckets[idx].range.end - 1;
993 if self.buckets[idx].compiled.is_none() {
994 let (hir, params) = build(upper);
995 let bytes: usize = params.values().map(|v| v.len() * 4).sum();
996 let mut session = Session::new(self.device);
1001 if let Some(p) = &self.policy {
1002 session = session.with_policy(p.clone());
1003 }
1004 let mut compiled = session
1005 .compile_hir_with(hir, options)
1006 .expect("HIR lower/compile in ensure_hir_with_params");
1007 for (name, data) in params {
1008 compiled.set_param(&name, &data);
1009 }
1010 self.buckets[idx].compiled = Some(compiled);
1011 self.buckets[idx].resident_bytes = bytes;
1012 }
1013 self.touch(idx);
1014 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
1015 }
1016
1017 pub fn note_resident_bytes(&mut self, key: u64, extra_bytes: usize) {
1023 if extra_bytes == 0 {
1024 return;
1025 }
1026 let Some(idx) = self.bucket_for(key) else {
1027 return;
1028 };
1029 let before = self.buckets[idx].resident_bytes;
1030 let after = before.saturating_add(extra_bytes);
1031 if after >= LARGE_BUCKET_BYTES {
1033 self.evict_for_incoming(idx, after);
1034 }
1035 self.buckets[idx].resident_bytes = after;
1036 self.touch(idx);
1037 }
1038
1039 pub fn run_padded_mixed<F>(
1041 &mut self,
1042 key: u64,
1043 actual_rows: usize,
1044 build: F,
1045 inputs: &[CacheRunInput<'_>],
1046 output_inners: &[usize],
1047 ) -> Option<(u64, Vec<Vec<f32>>)>
1048 where
1049 F: FnOnce(u64) -> Graph,
1050 {
1051 let (upper, compiled) = self.get_or_compile(key, build)?;
1052
1053 let padded: Vec<(&str, Vec<f32>)> = inputs
1054 .iter()
1055 .map(|inp| match inp.row_inner {
1056 Some(inner) => (inp.name, pad_rows(inp.data, inner, upper)),
1057 None => (inp.name, inp.data.to_vec()),
1058 })
1059 .collect();
1060 let pairs: Vec<(&str, &[f32])> = padded.iter().map(|(n, d)| (*n, d.as_slice())).collect();
1061
1062 compiled.set_active_extent(Some((actual_rows, upper as usize)));
1063 let raw_outputs = compiled.run(&pairs);
1064 compiled.set_active_extent(None);
1065 #[cfg(feature = "cpu")]
1066 crate::onnx_active::set_active_token_count(None);
1067
1068 let outs = raw_outputs
1069 .into_iter()
1070 .enumerate()
1071 .map(|(i, out)| match output_inners.get(i).copied() {
1072 Some(0) | None => out,
1073 Some(inner) => slice_rows(&out, inner, actual_rows),
1074 })
1075 .collect();
1076
1077 Some((upper, outs))
1078 }
1079
1080 pub fn sync_all(&mut self) {
1082 for bucket in &mut self.buckets {
1083 if let Some(compiled) = &mut bucket.compiled {
1084 compiled.sync_pending();
1085 }
1086 }
1087 }
1088}
1089
1090pub struct DynamicDimCompileCache {
1098 device: Device,
1099 policy: Option<rlx_opt::PrecisionPolicy>,
1100 capacity: usize,
1101 template: Option<CompileResult>,
1102 entries: Vec<(u64, CompiledGraph)>,
1103 order: VecDeque<u64>,
1104}
1105
1106impl DynamicDimCompileCache {
1107 pub fn new(device: Device, capacity: usize) -> Self {
1108 Self::with_policy(device, capacity, None)
1109 }
1110
1111 pub fn with_policy(
1112 device: Device,
1113 capacity: usize,
1114 policy: Option<rlx_opt::PrecisionPolicy>,
1115 ) -> Self {
1116 assert!(capacity > 0, "DynamicDimCompileCache capacity must be ≥ 1");
1117 Self {
1118 device,
1119 policy,
1120 capacity,
1121 template: None,
1122 entries: Vec::with_capacity(capacity),
1123 order: VecDeque::with_capacity(capacity),
1124 }
1125 }
1126
1127 pub fn compile_device(&self) -> Device {
1128 self.device
1129 }
1130
1131 pub fn get_or_specialize<F: FnOnce() -> HirModule>(
1134 &mut self,
1135 key: u64,
1136 binding: &DimBinding,
1137 build_hir: F,
1138 options: &crate::CompileOptions,
1139 ) -> Result<&mut CompiledGraph, rlx_ir::hir::LowerError> {
1140 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
1141 return Ok(&mut self.entries[idx].1);
1142 }
1143 if self.template.is_none() {
1144 let mut template_opts = options.clone();
1145 template_opts.dim_binding = None;
1146 let pipe = crate::stages::pipeline_for(self.device, &template_opts);
1147 self.template = Some(pipe.compile_hir(build_hir())?);
1148 }
1149 let template = self.template.as_ref().expect("template just set");
1150 let mut spec_opts = options.clone();
1151 spec_opts.dim_binding = None;
1152 let pipe = crate::stages::pipeline_for(self.device, &spec_opts);
1153 let specialized = template.specialize(&pipe, binding);
1154 let backend = crate::registry::backend_for(self.device).expect("backend registered");
1155 let mut compile_opts = options.clone();
1156 compile_opts.dim_binding = None;
1157 if compile_opts.policy.is_none() {
1158 if let Some(p) = &self.policy {
1159 compile_opts = compile_opts.policy(p.clone());
1160 }
1161 }
1162 let executable = backend.compile_lir(specialized.lir, &compile_opts);
1163 let compiled = CompiledGraph::new(executable, self.device);
1164
1165 if self.entries.len() >= self.capacity
1166 && let Some(evict_key) = self.order.pop_front()
1167 {
1168 sync_evicted_entry(&mut self.entries, evict_key);
1169 self.entries.retain(|(k, _)| *k != evict_key);
1170 }
1171 self.entries.push((key, compiled));
1172 self.order.push_back(key);
1173 Ok(&mut self.entries.last_mut().unwrap().1)
1174 }
1175
1176 pub fn len(&self) -> usize {
1177 self.entries.len()
1178 }
1179
1180 pub fn is_empty(&self) -> bool {
1181 self.entries.is_empty()
1182 }
1183
1184 pub fn contains(&self, key: u64) -> bool {
1185 self.entries.iter().any(|(k, _)| *k == key)
1186 }
1187
1188 pub fn clear(&mut self) {
1189 self.sync_all();
1190 self.template = None;
1191 self.entries.clear();
1192 self.order.clear();
1193 }
1194
1195 pub fn has_template(&self) -> bool {
1196 self.template.is_some()
1197 }
1198
1199 pub fn sync_all(&mut self) {
1201 for (_, compiled) in &mut self.entries {
1202 compiled.sync_pending();
1203 }
1204 }
1205
1206 pub fn ensure_template<F: FnOnce() -> HirModule>(
1208 &mut self,
1209 build_hir: F,
1210 options: &crate::CompileOptions,
1211 ) -> Result<&CompileResult, rlx_ir::hir::LowerError> {
1212 if self.template.is_none() {
1213 let mut opts = options.clone();
1214 opts.dim_binding = None;
1215 let pipe = crate::stages::pipeline_for(self.device, &opts);
1216 self.template = Some(pipe.compile_hir(build_hir())?);
1217 }
1218 Ok(self.template.as_ref().expect("template set"))
1219 }
1220
1221 pub fn template_result(&self) -> Option<&CompileResult> {
1222 self.template.as_ref()
1223 }
1224
1225 pub fn get_or_specialize_aot<F: FnOnce() -> HirModule>(
1228 &mut self,
1229 aot: &crate::AotCache,
1230 disk_base: &str,
1231 key: u64,
1232 binding: &rlx_ir::DimBinding,
1233 build_hir: F,
1234 options: &crate::CompileOptions,
1235 ) -> Result<&mut CompiledGraph, crate::AotCacheError> {
1236 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
1237 return Ok(&mut self.entries[idx].1);
1238 }
1239 let device = self.device;
1240 let template = self.ensure_template(build_hir, options)?;
1241 let compiled = aot.specialize_cached(disk_base, binding, device, template, options)?;
1242 if self.entries.len() >= self.capacity
1243 && let Some(evict_key) = self.order.pop_front()
1244 {
1245 sync_evicted_entry(&mut self.entries, evict_key);
1246 self.entries.retain(|(k, _)| *k != evict_key);
1247 }
1248 self.entries.push((key, compiled));
1249 self.order.push_back(key);
1250 Ok(&mut self.entries.last_mut().unwrap().1)
1251 }
1252}
1253
1254pub fn pad_rows(data: &[f32], inner: usize, upper: u64) -> Vec<f32> {
1262 assert!(inner > 0, "pad_rows: inner stride must be ≥ 1");
1263 assert_eq!(
1264 data.len() % inner,
1265 0,
1266 "pad_rows: data len {} not a multiple of inner {inner}",
1267 data.len(),
1268 );
1269 let upper = upper as usize;
1270 let actual = data.len() / inner;
1271 assert!(
1272 actual <= upper,
1273 "pad_rows: actual rows {actual} exceed upper bound {upper}",
1274 );
1275 let mut out = vec![0.0_f32; upper * inner];
1276 out[..actual * inner].copy_from_slice(data);
1277 out
1278}
1279
1280pub fn pad_rows_into(out: &mut [f32], data: &[f32], inner: usize) {
1282 assert!(inner > 0, "pad_rows_into: inner stride must be ≥ 1");
1283 assert_eq!(
1284 data.len() % inner,
1285 0,
1286 "pad_rows_into: data len {} not a multiple of inner {inner}",
1287 data.len(),
1288 );
1289 assert_eq!(
1290 out.len() % inner,
1291 0,
1292 "pad_rows_into: out len {} not a multiple of inner {inner}",
1293 out.len(),
1294 );
1295 let upper = out.len() / inner;
1296 let actual = data.len() / inner;
1297 assert!(
1298 actual <= upper,
1299 "pad_rows_into: actual rows {actual} exceed upper bound {upper}",
1300 );
1301 out.fill(0.0);
1302 out[..data.len()].copy_from_slice(data);
1303}
1304
1305pub fn slice_rows(data: &[f32], inner: usize, actual: usize) -> Vec<f32> {
1311 assert!(inner > 0, "slice_rows: inner stride must be ≥ 1");
1312 assert_eq!(
1313 data.len() % inner,
1314 0,
1315 "slice_rows: data len {} not a multiple of inner {inner}",
1316 data.len(),
1317 );
1318 let upper = data.len() / inner;
1319 assert!(
1320 actual <= upper,
1321 "slice_rows: actual rows {actual} exceed upper {upper}",
1322 );
1323 data[..actual * inner].to_vec()
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328 use super::*;
1329 use rlx_ir::infer::GraphExt;
1330 use rlx_ir::*;
1331 use std::cell::Cell;
1332
1333 fn tiny_graph(n: usize) -> Graph {
1334 let mut g = Graph::new("t");
1335 let f = DType::F32;
1336 let x = g.input("x", Shape::new(&[n], f));
1337 let y = g.activation(rlx_ir::op::Activation::Relu, x, Shape::new(&[n], f));
1338 g.set_outputs(vec![y]);
1339 g
1340 }
1341
1342 #[test]
1343 fn cache_hits_avoid_recompile() {
1344 let mut cache = CompileCache::new(Device::Cpu, 4);
1345 let calls = Cell::new(0);
1346
1347 let _ = cache.get_or_compile(1, || {
1348 calls.set(calls.get() + 1);
1349 tiny_graph(8)
1350 });
1351 let _ = cache.get_or_compile(1, || {
1352 calls.set(calls.get() + 1);
1353 tiny_graph(8)
1354 });
1355 let _ = cache.get_or_compile(1, || {
1356 calls.set(calls.get() + 1);
1357 tiny_graph(8)
1358 });
1359 assert_eq!(calls.get(), 1);
1361 assert_eq!(cache.len(), 1);
1362 }
1363
1364 #[test]
1365 fn fifo_evicts_oldest_at_capacity() {
1366 let mut cache = CompileCache::new(Device::Cpu, 2);
1367 let _ = cache.get_or_compile(1, || tiny_graph(4));
1368 let _ = cache.get_or_compile(2, || tiny_graph(8));
1369 assert!(cache.contains(1) && cache.contains(2));
1370 let _ = cache.get_or_compile(3, || tiny_graph(16));
1372 assert!(!cache.contains(1));
1373 assert!(cache.contains(2) && cache.contains(3));
1374 }
1375
1376 #[test]
1377 fn different_keys_keep_separate_compiles() {
1378 let mut cache = CompileCache::new(Device::Cpu, 4);
1379 let calls = Cell::new(0);
1380 let _ = cache.get_or_compile(1, || {
1381 calls.set(calls.get() + 1);
1382 tiny_graph(8)
1383 });
1384 let _ = cache.get_or_compile(2, || {
1385 calls.set(calls.get() + 1);
1386 tiny_graph(16)
1387 });
1388 let _ = cache.get_or_compile(1, || {
1389 calls.set(calls.get() + 1);
1390 tiny_graph(8)
1391 });
1392 assert_eq!(calls.get(), 2);
1394 assert_eq!(cache.len(), 2);
1395 }
1396
1397 #[test]
1400 fn bucket_amortizes_keys_within_range() {
1401 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16]);
1402 let calls = Cell::new(0);
1403 let uppers = Cell::new((0u64, 0u64));
1404
1405 let (u1, _) = cache
1407 .get_or_compile(2, |upper| {
1408 calls.set(calls.get() + 1);
1409 uppers.set((upper, uppers.get().1));
1410 tiny_graph(upper as usize)
1411 })
1412 .expect("key 2 in range");
1413 let (u2, _) = cache
1414 .get_or_compile(3, |upper| {
1415 calls.set(calls.get() + 1);
1416 uppers.set((uppers.get().0, upper));
1417 tiny_graph(upper as usize)
1418 })
1419 .expect("key 3 in range");
1420
1421 assert_eq!(calls.get(), 1);
1423 assert_eq!(u1, 3);
1424 assert_eq!(u2, 3);
1425 assert_eq!(uppers.get().0, 3);
1426 assert_eq!(cache.compiled_count(), 1);
1427 assert_eq!(cache.total_buckets(), 2);
1428 }
1429
1430 #[test]
1431 fn bucket_lookup_returns_none_outside_range() {
1432 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16]);
1433 assert!(cache.bucket_for(0).is_none());
1434 assert!(cache.bucket_for(16).is_none());
1435 assert!(cache.bucket_for(100).is_none());
1436 assert_eq!(cache.bucket_for(3), Some(0));
1437 assert_eq!(cache.bucket_for(4), Some(1));
1438 assert_eq!(cache.bucket_upper_for_key(3), Some(3));
1439 assert_eq!(cache.bucket_upper_for_key(4), Some(15));
1440 assert!(cache.bucket_upper_for_key(0).is_none());
1441
1442 let calls = Cell::new(0);
1443 let result = cache.get_or_compile(100, |u| {
1444 calls.set(calls.get() + 1);
1445 tiny_graph(u as usize)
1446 });
1447 assert!(result.is_none());
1448 assert_eq!(calls.get(), 0); assert_eq!(cache.compiled_count(), 0);
1450 }
1451
1452 #[test]
1453 fn bucket_compiles_lazily_per_bucket() {
1454 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16, 16..64]);
1455 let calls = Cell::new(0);
1456
1457 let _ = cache.get_or_compile(2, |u| {
1458 calls.set(calls.get() + 1);
1459 tiny_graph(u as usize)
1460 });
1461 let _ = cache.get_or_compile(8, |u| {
1462 calls.set(calls.get() + 1);
1463 tiny_graph(u as usize)
1464 });
1465 assert_eq!(calls.get(), 2);
1467 assert_eq!(cache.compiled_count(), 2);
1468 assert_eq!(cache.total_buckets(), 3);
1469 }
1470
1471 #[test]
1472 #[should_panic(expected = "overlap")]
1473 fn bucket_overlap_rejected() {
1474 let _ = BucketedCompileCache::new(Device::Cpu, vec![1..8, 4..16]);
1475 }
1476
1477 #[test]
1478 #[should_panic(expected = "≥1 bucket")]
1479 fn empty_bucket_list_rejected() {
1480 let _ = BucketedCompileCache::new(Device::Cpu, vec![]);
1481 }
1482
1483 #[test]
1486 fn pad_rows_appends_zeros() {
1487 let p = pad_rows(&[1.0, 2.0, 3.0], 1, 5);
1489 assert_eq!(p, vec![1.0, 2.0, 3.0, 0.0, 0.0]);
1490
1491 let p = pad_rows(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 4);
1493 assert_eq!(
1494 p,
1495 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1496 );
1497
1498 let p = pad_rows(&[7.0, 8.0], 1, 2);
1500 assert_eq!(p, vec![7.0, 8.0]);
1501 }
1502
1503 #[test]
1504 fn slice_rows_truncates_trailing() {
1505 let s = slice_rows(&[1.0, 2.0, 3.0, 0.0, 0.0], 1, 3);
1506 assert_eq!(s, vec![1.0, 2.0, 3.0]);
1507
1508 let s = slice_rows(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 0.0], 3, 2);
1509 assert_eq!(s, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1510 }
1511
1512 #[test]
1513 #[should_panic(expected = "exceed upper")]
1514 fn pad_rows_rejects_too_long_input() {
1515 let _ = pad_rows(&[1.0, 2.0, 3.0, 4.0], 1, 3);
1516 }
1517
1518 #[test]
1519 #[should_panic(expected = "exceed upper")]
1520 fn slice_rows_rejects_too_large_actual() {
1521 let _ = slice_rows(&[1.0, 2.0, 3.0], 1, 5);
1522 }
1523
1524 #[test]
1527 fn run_padded_pads_input_and_slices_output() {
1528 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1531 let input: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 3.0, -3.0, 4.0, -4.0, 5.0, -5.0];
1532
1533 let (upper, outs) = cache
1534 .run_padded(
1535 10, 10, |max| tiny_graph(max as usize),
1538 &[("x", &input, 1)], &[1], )
1541 .expect("key 10 in [1..16)");
1542
1543 assert_eq!(upper, 15);
1544 assert_eq!(outs.len(), 1);
1545 let out = &outs[0];
1546 assert_eq!(out.len(), 10, "output sliced back to actual_rows");
1547 let expected: Vec<f32> = input.iter().map(|x| x.max(0.0)).collect();
1548 assert_eq!(out, &expected);
1549 }
1550
1551 #[test]
1552 fn run_padded_reuses_bucket_across_actuals() {
1553 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1555 let calls = Cell::new(0);
1556
1557 let (u1, o1) = cache
1558 .run_padded(
1559 10,
1560 10,
1561 |max| {
1562 calls.set(calls.get() + 1);
1563 tiny_graph(max as usize)
1564 },
1565 &[(
1566 "x",
1567 &[1.0, -1.0, 2.0, -2.0, 3.0, -3.0, 4.0, -4.0, 5.0, -5.0],
1568 1,
1569 )],
1570 &[1],
1571 )
1572 .unwrap();
1573 assert_eq!(o1.len(), 1);
1574 assert_eq!(o1[0].len(), 10);
1575 assert_eq!(u1, 15);
1576
1577 let (u2, o2) = cache
1578 .run_padded(
1579 5,
1580 5,
1581 |max| {
1582 calls.set(calls.get() + 1);
1583 tiny_graph(max as usize)
1584 },
1585 &[("x", &[-1.0, 2.0, -3.0, 4.0, -5.0], 1)],
1586 &[1],
1587 )
1588 .unwrap();
1589 assert_eq!(o2.len(), 1);
1590 assert_eq!(o2[0].len(), 5);
1591 assert_eq!(u2, 15);
1592 assert_eq!(o2[0], vec![0.0, 2.0, 0.0, 4.0, 0.0]);
1593
1594 assert_eq!(calls.get(), 1, "bucket cached across actuals");
1595 assert_eq!(cache.compiled_count(), 1);
1596 }
1597
1598 #[test]
1599 fn run_padded_returns_none_out_of_range() {
1600 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1601 let calls = Cell::new(0);
1602 let result = cache.run_padded(
1603 100,
1604 5,
1605 |u| {
1606 calls.set(calls.get() + 1);
1607 tiny_graph(u as usize)
1608 },
1609 &[("x", &[1.0, 2.0, 3.0, 4.0, 5.0], 1)],
1610 &[1],
1611 );
1612 assert!(result.is_none());
1613 assert_eq!(calls.get(), 0);
1614 assert_eq!(cache.compiled_count(), 0);
1615 }
1616
1617 #[test]
1620 fn power_of_two_ladder_generates_log_buckets() {
1621 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 64);
1622 let ranges: Vec<_> = cache.buckets().cloned().collect();
1624 assert_eq!(ranges, vec![1..9, 9..17, 17..33, 33..65]);
1625 assert_eq!(cache.total_buckets(), 4);
1626 }
1627
1628 #[test]
1629 fn power_of_two_ladder_picks_smallest_extent_for_actual() {
1630 let mut cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 64);
1633 let captured_uppers: std::cell::RefCell<Vec<u64>> = Default::default();
1634
1635 let (u17, _) = cache
1636 .get_or_compile(17, |upper| {
1637 captured_uppers.borrow_mut().push(upper);
1638 tiny_graph(upper as usize)
1639 })
1640 .unwrap();
1641 let (u9, _) = cache
1642 .get_or_compile(9, |upper| {
1643 captured_uppers.borrow_mut().push(upper);
1644 tiny_graph(upper as usize)
1645 })
1646 .unwrap();
1647 let (u3, _) = cache
1648 .get_or_compile(3, |upper| {
1649 captured_uppers.borrow_mut().push(upper);
1650 tiny_graph(upper as usize)
1651 })
1652 .unwrap();
1653 let (u64_, _) = cache
1654 .get_or_compile(64, |upper| {
1655 captured_uppers.borrow_mut().push(upper);
1656 tiny_graph(upper as usize)
1657 })
1658 .unwrap();
1659
1660 assert_eq!(u17, 32, "key=17 → smallest extent ≥ 17 is 32");
1661 assert_eq!(u9, 16, "key=9 → smallest extent ≥ 9 is 16");
1662 assert_eq!(u3, 8, "key=3 → smallest extent ≥ 3 is 8");
1663 assert_eq!(u64_, 64, "key=64 → exact match at 64");
1664 assert_eq!(*captured_uppers.borrow(), vec![32, 16, 8, 64]);
1665 assert_eq!(cache.compiled_count(), 4);
1666 }
1667
1668 #[test]
1669 fn power_of_two_ladder_min_above_one_starts_at_one() {
1670 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 16, 32);
1673 let ranges: Vec<_> = cache.buckets().cloned().collect();
1674 assert_eq!(ranges, vec![1..17, 17..33]);
1676 }
1677
1678 #[test]
1679 fn power_of_two_ladder_non_pow2_min_rounds_up() {
1680 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 10, 64);
1682 let ranges: Vec<_> = cache.buckets().cloned().collect();
1683 assert_eq!(ranges, vec![1..17, 17..33, 33..65]);
1684 }
1685
1686 #[test]
1687 fn power_of_two_ladder_max_below_pow2_extends_up() {
1688 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 20);
1690 let ranges: Vec<_> = cache.buckets().cloned().collect();
1691 assert_eq!(ranges, vec![1..9, 9..17, 17..33]);
1692 }
1693
1694 #[test]
1695 fn power_of_two_ladder_min_equals_max() {
1696 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 16, 16);
1697 let ranges: Vec<_> = cache.buckets().cloned().collect();
1698 assert_eq!(ranges, vec![1..17]);
1699 }
1700
1701 #[test]
1702 #[should_panic(expected = "min must be ≥ 1")]
1703 fn power_of_two_ladder_zero_min_rejected() {
1704 let _ = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 0, 16);
1705 }
1706
1707 #[test]
1708 #[should_panic(expected = "max")]
1709 fn power_of_two_ladder_max_below_min_rejected() {
1710 let _ = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 32, 8);
1711 }
1712
1713 #[test]
1726 #[ignore = "active-extent execution is a stub on CPU (rlx-cpu/src/thunk/exec_dispatch.rs::execute_thunks_active returns false unconditionally)"]
1727 fn active_extent_skips_compute_on_cpu_activation() {
1728 let graph = tiny_graph(15);
1739 let mut compiled = Session::new(Device::Cpu).compile(graph);
1740
1741 let warm_input: Vec<f32> = vec![1.0; 15];
1743 let warm_outs = compiled.run(&[("x", &warm_input)]);
1744 assert_eq!(warm_outs[0], vec![1.0; 15], "warm-up sanity");
1745
1746 let neg_input: Vec<f32> = vec![-1.0; 15];
1749 compiled.set_active_extent(Some((5, 15)));
1750 let outs = compiled.run(&[("x", &neg_input)]);
1751 let out = &outs[0];
1752
1753 assert_eq!(out.len(), 15);
1754 assert_eq!(
1755 out[..5],
1756 [0.0; 5],
1757 "first 5 elements processed (relu of -1)"
1758 );
1759 assert_eq!(
1760 out[5..],
1761 [1.0; 10],
1762 "tail untouched — proves Copy + Activation skipped indices 5..15"
1763 );
1764
1765 compiled.set_active_extent(None);
1768 let outs = compiled.run(&[("x", &neg_input)]);
1769 assert_eq!(
1770 outs[0],
1771 vec![0.0; 15],
1772 "full-extent path must clip every negative"
1773 );
1774 }
1775
1776 #[test]
1777 #[ignore = "active-extent execution is a stub on CPU (rlx-cpu/src/thunk/exec_dispatch.rs::execute_thunks_active returns false unconditionally)"]
1778 fn active_extent_skips_compute_on_binary_full() {
1779 let mut g = Graph::new("add");
1783 let f = DType::F32;
1784 let a = g.input("a", Shape::new(&[4], f));
1785 let b = g.input("b", Shape::new(&[4], f));
1786 let c = g.add(a, b);
1787 g.set_outputs(vec![c]);
1788 let mut compiled = Session::new(Device::Cpu).compile(g);
1789
1790 let warm = compiled.run(&[("a", &[1.0f32; 4]), ("b", &[1.0f32; 4])]);
1792 assert_eq!(warm[0], vec![2.0; 4]);
1793
1794 compiled.set_active_extent(Some((2, 4)));
1797 let outs = compiled.run(&[("a", &[10.0f32; 4]), ("b", &[10.0f32; 4])]);
1798 let out = &outs[0];
1799 assert_eq!(out[..2], [20.0, 20.0], "first 2 = active sum");
1800 assert_eq!(
1801 out[2..],
1802 [2.0, 2.0],
1803 "tail untouched — proves BinaryFull skipped indices 2..4"
1804 );
1805
1806 compiled.set_active_extent(None);
1808 let outs = compiled.run(&[("a", &[10.0f32; 4]), ("b", &[10.0f32; 4])]);
1809 assert_eq!(outs[0], vec![20.0; 4]);
1810 }
1811
1812 #[test]
1813 #[ignore = "process-wide STATE; runs only in isolation via `cargo test perfetto -- --ignored`"]
1814 fn perfetto_trace_emits_per_thunk_events() {
1815 use std::env;
1822 use std::fs;
1823 let path = env::temp_dir().join(format!("rlx-perfetto-e2e-{}.json", std::process::id()));
1824 if path.exists() {
1825 let _ = fs::remove_file(&path);
1826 }
1827 unsafe {
1828 env::set_var("RLX_TRACE_PERFETTO", &path);
1829 }
1830
1831 let f = DType::F32;
1833 let mut g = Graph::new("perf");
1834 let a = g.input("a", Shape::new(&[4], f));
1835 let b = g.input("b", Shape::new(&[4], f));
1836 let s = g.add(a, b);
1837 let r = g.relu(s);
1838 g.set_outputs(vec![r]);
1839 let mut compiled = Session::new(Device::Cpu).compile(g);
1840 let _ = compiled.run(&[("a", &[1.0; 4]), ("b", &[1.0; 4])]);
1841
1842 crate::perfetto::flush_and_finalize();
1844
1845 let contents = fs::read_to_string(&path).expect("trace file");
1846 assert!(
1848 contents.contains("\"binary\"")
1849 || contents.contains("\"activation\"")
1850 || contents.contains("\"elementwise_region\""),
1851 "expected at least one thunk-name event in perfetto trace; got: {contents}"
1852 );
1853 assert!(contents.trim_start().starts_with('['));
1855 let _ = fs::remove_file(&path);
1856 }
1857
1858 #[test]
1859 fn elementwise_region_fused_matches_unfused() {
1860 let f = DType::F32;
1865 let mut g = Graph::new("ew_e2e");
1866 let a = g.input("a", Shape::new(&[8], f));
1867 let b = g.input("b", Shape::new(&[8], f));
1868 let c = g.input("c", Shape::new(&[8], f));
1869 let s = Shape::new(&[8], f);
1870 let add = g.add(a, b);
1871 let mul = g.mul(add, c);
1872 let relu = g.relu(mul);
1873 let _ = s;
1874 g.set_outputs(vec![relu]);
1875
1876 let mut compiled = Session::new(Device::Cpu).compile(g);
1877 let av: Vec<f32> = vec![1.0, -2.0, 3.0, -4.0, 0.5, -0.5, 1.5, -1.5];
1878 let bv: Vec<f32> = vec![0.5, 1.0, 2.0, 4.0, 0.5, 0.5, 0.5, 0.5];
1879 let cv: Vec<f32> = vec![1.0, 2.0, 1.0, 1.0, 2.0, 3.0, 0.5, 4.0];
1880 let outs = compiled.run(&[("a", &av), ("b", &bv), ("c", &cv)]);
1881 let out = &outs[0];
1882
1883 let expected: Vec<f32> = (0..8)
1884 .map(|i| {
1885 let v = (av[i] + bv[i]) * cv[i];
1886 v.max(0.0)
1887 })
1888 .collect();
1889 for (i, (got, exp)) in out.iter().zip(&expected).enumerate() {
1890 assert!(
1891 (got - exp).abs() < 1e-6,
1892 "mismatch at {i}: got {got}, expected {exp}"
1893 );
1894 }
1895 }
1896
1897 #[test]
1898 #[ignore = "active-extent execution is a stub on CPU (rlx-cpu/src/thunk/exec_dispatch.rs::execute_thunks_active returns false unconditionally)"]
1899 fn active_extent_skips_compute_on_attention() {
1900 use rlx_ir::op::MaskKind;
1903 let f = DType::F32;
1904 let mut g = Graph::new("attn");
1905 let q = g.input("q", Shape::new(&[1, 4, 8], f));
1906 let k = g.input("k", Shape::new(&[1, 4, 8], f));
1907 let v = g.input("v", Shape::new(&[1, 4, 8], f));
1908 let out = g.attention_kind(q, k, v, 2, 4, MaskKind::None, Shape::new(&[1, 4, 8], f));
1909 g.set_outputs(vec![out]);
1910 let mut compiled = Session::new(Device::Cpu).compile(g);
1911
1912 let warm = compiled.run(&[
1914 ("q", &[1.0f32; 32]),
1915 ("k", &[1.0f32; 32]),
1916 ("v", &[1.0f32; 32]),
1917 ]);
1918 let warm_out = warm[0].clone();
1919 assert_eq!(warm_out.len(), 32);
1920
1921 compiled.set_active_extent(Some((2, 4)));
1925 let outs = compiled.run(&[
1926 ("q", &[3.0f32; 32]),
1927 ("k", &[3.0f32; 32]),
1928 ("v", &[3.0f32; 32]),
1929 ]);
1930 let out = &outs[0];
1931 assert_eq!(out.len(), 32);
1932 assert_eq!(
1933 &out[16..],
1934 &warm_out[16..],
1935 "tail (positions 2,3) must be untouched — proves Attention skipped"
1936 );
1937 assert_ne!(
1939 &out[..16],
1940 &warm_out[..16],
1941 "first 2 positions should reflect new input"
1942 );
1943 }
1944
1945 #[test]
1946 fn active_extent_falls_back_when_unsupported_thunk_in_schedule() {
1947 }
1962
1963 #[test]
1964 fn run_padded_uses_active_extent_on_cpu() {
1965 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1968 let input: Vec<f32> = vec![
1969 1.0, -1.0, 2.0, -2.0, 3.0, -10.0, -20.0, -30.0, -40.0, -50.0, ];
1972 let (upper, outs) = cache
1978 .run_padded(
1979 5,
1980 5,
1981 |max| tiny_graph(max as usize),
1982 &[("x", &input[..5], 1)],
1983 &[1],
1984 )
1985 .unwrap();
1986 assert_eq!(upper, 15);
1987 assert_eq!(outs[0].len(), 5);
1988 assert_eq!(outs[0], vec![1.0, 0.0, 2.0, 0.0, 3.0]);
1994 }
1995
1996 #[test]
1997 fn run_padded_inner_zero_returns_output_unsliced() {
1998 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
2001 let input: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 3.0];
2002
2003 let (upper, outs) = cache
2004 .run_padded(
2005 5,
2006 5,
2007 |max| tiny_graph(max as usize),
2008 &[("x", &input, 1)],
2009 &[0], )
2011 .unwrap();
2012
2013 assert_eq!(upper, 15);
2014 assert_eq!(
2015 outs[0].len(),
2016 15,
2017 "unsliced output preserves full upper extent"
2018 );
2019 assert_eq!(&outs[0][..5], &[1.0, 0.0, 2.0, 0.0, 3.0]);
2021 assert!(outs[0][5..].iter().all(|&v| v == 0.0));
2022 }
2023
2024 #[test]
2025 fn dynamic_dim_cache_specializes_per_key() {
2026 use rlx_ir::DType;
2027 use rlx_ir::Shape;
2028 use rlx_ir::hir::HirModule;
2029 use rlx_ir::sym;
2030
2031 let mut cache = DynamicDimCompileCache::new(Device::Cpu, 4);
2032 let opts = crate::CompileOptions::new();
2033 {
2034 let _short = cache
2035 .get_or_specialize(
2036 8,
2037 &rlx_ir::DimBinding::batch_seq(1, 8),
2038 || {
2039 let mut hir = HirModule::new("dyn_cache");
2040 let x = hir.input_batch_seq("x", sym::BATCH, sym::SEQ, 4, DType::F32);
2041 let w = hir.param("w", Shape::new(&[4, 2], DType::F32));
2042 let y = hir.linear(
2043 x,
2044 w,
2045 None,
2046 None,
2047 Shape::batch_seq(sym::BATCH, sym::SEQ, 2, DType::F32),
2048 );
2049 hir.set_outputs(vec![y]);
2050 hir
2051 },
2052 &opts,
2053 )
2054 .expect("specialize short");
2055 }
2056 assert!(cache.has_template());
2057 assert_eq!(cache.len(), 1);
2058 cache
2059 .get_or_specialize(
2060 128,
2061 &rlx_ir::DimBinding::batch_seq(1, 128),
2062 || panic!("HIR builder must not run twice"),
2063 &opts,
2064 )
2065 .expect("specialize long");
2066 assert_eq!(cache.len(), 2);
2067 }
2068}