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 weight_donor: Option<u64>,
64}
65
66impl CompileCache {
67 pub fn new(device: Device, capacity: usize) -> Self {
68 Self::with_policy(device, capacity, None)
69 }
70
71 pub fn with_policy(
75 device: Device,
76 capacity: usize,
77 policy: Option<rlx_opt::PrecisionPolicy>,
78 ) -> Self {
79 assert!(capacity > 0, "CompileCache capacity must be ≥ 1");
80 Self {
81 device,
82 capacity,
83 policy,
84 entries: Vec::with_capacity(capacity),
85 order: VecDeque::with_capacity(capacity),
86 weight_donor: None,
87 }
88 }
89
90 pub fn get_or_compile<F: FnOnce() -> Graph>(
94 &mut self,
95 key: u64,
96 build: F,
97 ) -> &mut CompiledGraph {
98 self.get_or_compile_with_options(key, build, &crate::CompileOptions::new())
99 }
100
101 pub fn get_or_compile_with_options<F: FnOnce() -> Graph>(
103 &mut self,
104 key: u64,
105 build: F,
106 options: &crate::CompileOptions,
107 ) -> &mut CompiledGraph {
108 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
109 return &mut self.entries[idx].1;
110 }
111 let mut session = Session::new(self.device);
112 if let Some(p) = &self.policy {
113 session = session.with_policy(p.clone());
114 }
115 let compiled = session.compile_with(build(), options);
116
117 if self.entries.len() >= self.capacity
119 && let Some(evict_key) = self.order.pop_front()
120 {
121 sync_evicted_entry(&mut self.entries, evict_key);
122 self.entries.retain(|(k, _)| *k != evict_key);
123 if self.weight_donor == Some(evict_key) {
124 self.weight_donor = None;
128 }
129 }
130 self.entries.push((key, compiled));
131 self.order.push_back(key);
132 &mut self.entries.last_mut().unwrap().1
133 }
134
135 pub fn ensure_with_params(
143 &mut self,
144 key: u64,
145 graph: Graph,
146 params: &HashMap<String, Vec<f32>>,
147 options: &crate::CompileOptions,
148 ) -> &mut CompiledGraph {
149 if self.contains(key) {
150 return self.get_or_compile_with_options(
151 key,
152 || unreachable!("checked present"),
153 options,
154 );
155 }
156 self.get_or_compile_with_options(key, || graph, options);
157 let has_other = self.entries.iter().any(|(k, _)| *k != key);
160 if has_other && self.try_share_params_from_donor(key) {
161 self.weight_donor.get_or_insert(key);
162 return self.get_or_compile_with_options(
163 key,
164 || unreachable!("compiled above"),
165 options,
166 );
167 }
168 let compiled =
169 self.get_or_compile_with_options(key, || unreachable!("compiled above"), options);
170 for (name, data) in params {
171 compiled.set_param(name, data);
172 }
173 self.weight_donor.get_or_insert(key);
174 self.get_or_compile_with_options(key, || unreachable!("compiled above"), options)
175 }
176
177 pub fn try_share_params_from_donor(&mut self, dst_key: u64) -> bool {
180 if rlx_ir::env::var("RLX_METAL_NO_SHARE").is_some() {
181 return false;
182 }
183 let mut candidates: Vec<u64> = Vec::new();
184 if let Some(d) = self.weight_donor.filter(|d| *d != dst_key) {
185 candidates.push(d);
186 }
187 for (k, _) in &self.entries {
188 if *k != dst_key && !candidates.contains(k) {
189 candidates.push(*k);
190 }
191 }
192 for src_key in candidates {
193 if self.try_share_params_between(dst_key, src_key) {
194 self.weight_donor = Some(src_key);
195 return true;
196 }
197 }
198 false
199 }
200
201 fn try_share_params_between(&mut self, dst_key: u64, src_key: u64) -> bool {
202 if dst_key == src_key {
203 return false;
204 }
205 let dst_idx = self.entries.iter().position(|(k, _)| *k == dst_key);
206 let src_idx = self.entries.iter().position(|(k, _)| *k == src_key);
207 let (Some(dst_idx), Some(src_idx)) = (dst_idx, src_idx) else {
208 return false;
209 };
210 if dst_idx == src_idx {
211 return false;
212 }
213 let (dst, src) = if dst_idx < src_idx {
214 let (left, right) = self.entries.split_at_mut(src_idx);
215 (&mut left[dst_idx].1, &right[0].1)
216 } else {
217 let (left, right) = self.entries.split_at_mut(dst_idx);
218 (&mut right[0].1, &left[src_idx].1)
219 };
220 dst.share_params_from(src)
221 }
222
223 pub fn get_or_compile_hir_with_options<F: FnOnce() -> rlx_ir::hir::HirModule>(
225 &mut self,
226 key: u64,
227 build: F,
228 options: &crate::CompileOptions,
229 ) -> &mut CompiledGraph {
230 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
231 return &mut self.entries[idx].1;
232 }
233 let mut session = Session::new(self.device);
234 if let Some(p) = &self.policy {
235 session = session.with_policy(p.clone());
236 }
237 let compiled = session
238 .compile_hir_with(build(), options)
239 .expect("HIR lower/compile in compile cache");
240
241 if self.entries.len() >= self.capacity
242 && let Some(evict_key) = self.order.pop_front()
243 {
244 sync_evicted_entry(&mut self.entries, evict_key);
245 self.entries.retain(|(k, _)| *k != evict_key);
246 if self.weight_donor == Some(evict_key) {
247 self.weight_donor = None;
251 }
252 }
253 self.entries.push((key, compiled));
254 self.order.push_back(key);
255 &mut self.entries.last_mut().unwrap().1
256 }
257
258 pub fn len(&self) -> usize {
260 self.entries.len()
261 }
262 pub fn is_empty(&self) -> bool {
263 self.entries.is_empty()
264 }
265 pub fn contains(&self, key: u64) -> bool {
267 self.entries.iter().any(|(k, _)| *k == key)
268 }
269
270 pub fn clear(&mut self) {
272 self.sync_all();
273 self.entries.clear();
274 self.order.clear();
275 }
276
277 pub fn sync_all(&mut self) {
279 for (_, compiled) in &mut self.entries {
280 compiled.sync_pending();
281 }
282 }
283}
284
285fn sync_evicted_entry(entries: &mut [(u64, CompiledGraph)], evict_key: u64) {
286 if let Some((_, compiled)) = entries.iter_mut().find(|(k, _)| *k == evict_key) {
287 compiled.sync_pending();
288 }
289}
290
291pub struct BucketedCompileCache {
338 device: Device,
339 policy: Option<rlx_opt::PrecisionPolicy>,
340 buckets: Vec<Bucket>,
341 weight_donor_upper: Option<u64>,
346 clock: u64,
348}
349
350struct Bucket {
351 range: Range<u64>,
352 compiled: Option<CompiledGraph>,
353 resident_bytes: usize,
356 last_used: u64,
358}
359
360const LARGE_BUCKET_BYTES: usize = 256 * 1024 * 1024;
366
367fn max_resident_large_buckets() -> usize {
377 rlx_ir::env::var("RLX_KV_CACHE_MAX_RESIDENT")
378 .and_then(|s| s.parse::<usize>().ok())
379 .filter(|&n| n >= 1)
380 .unwrap_or(1)
381}
382
383fn device_has_discrete_vram(dev: Device) -> bool {
388 matches!(
389 dev,
390 Device::Gpu
391 | Device::Cuda
392 | Device::Vulkan
393 | Device::Rocm
394 | Device::OneApi
395 | Device::DirectX
396 | Device::WebGpu
397 )
398}
399
400impl BucketedCompileCache {
401 pub fn new(device: Device, buckets: Vec<Range<u64>>) -> Self {
402 Self::with_policy(device, buckets, None)
403 }
404
405 pub fn power_of_two_ladder(device: Device, min: u64, max: u64) -> Self {
421 Self::power_of_two_ladder_with_policy(device, min, max, None)
422 }
423
424 pub fn power_of_two_ladder_with_policy(
425 device: Device,
426 min: u64,
427 max: u64,
428 policy: Option<rlx_opt::PrecisionPolicy>,
429 ) -> Self {
430 assert!(min >= 1, "power_of_two_ladder: min must be ≥ 1, got {min}");
431 assert!(
432 max >= min,
433 "power_of_two_ladder: max ({max}) must be ≥ min ({min})"
434 );
435 let mut buckets: Vec<Range<u64>> = Vec::new();
436 let mut start = 1u64;
437 let mut extent = min.next_power_of_two();
438 loop {
439 buckets.push(start..(extent + 1));
440 if extent >= max {
441 break;
442 }
443 start = extent + 1;
444 extent = extent
445 .checked_mul(2)
446 .expect("power_of_two_ladder: extent overflow");
447 }
448 Self::with_policy(device, buckets, policy)
449 }
450
451 pub fn with_policy(
452 device: Device,
453 buckets: Vec<Range<u64>>,
454 policy: Option<rlx_opt::PrecisionPolicy>,
455 ) -> Self {
456 assert!(!buckets.is_empty(), "BucketedCompileCache needs ≥1 bucket");
457 for (i, b) in buckets.iter().enumerate() {
458 assert!(b.start < b.end, "bucket {i} ({b:?}) is empty");
459 if i + 1 < buckets.len() {
460 assert!(
461 b.end <= buckets[i + 1].start,
462 "buckets {i} ({b:?}) and {} ({:?}) overlap",
463 i + 1,
464 buckets[i + 1],
465 );
466 }
467 }
468 let buckets = buckets
469 .into_iter()
470 .map(|range| Bucket {
471 range,
472 compiled: None,
473 resident_bytes: 0,
474 last_used: 0,
475 })
476 .collect();
477 Self {
478 device,
479 policy,
480 buckets,
481 weight_donor_upper: None,
482 clock: 0,
483 }
484 }
485
486 fn touch(&mut self, idx: usize) {
488 self.clock += 1;
489 self.buckets[idx].last_used = self.clock;
490 }
491
492 fn evict_for_incoming(&mut self, incoming_idx: usize, incoming_bytes: usize) {
499 if rlx_ir::env::var("RLX_KV_CACHE_NO_EVICT").is_some() {
500 return;
501 }
502 let unified_opt_in = rlx_ir::env::var("RLX_KV_CACHE_MAX_RESIDENT").is_some();
509 if !device_has_discrete_vram(self.device) && !unified_opt_in {
510 return;
511 }
512 if rlx_ir::env::var("RLX_KV_CACHE_DBG").is_some() {
513 let (n_large, resident): (usize, usize) = self
514 .buckets
515 .iter()
516 .filter(|b| b.compiled.is_some())
517 .fold((0, 0), |(n, r), b| {
518 let is_large = b.resident_bytes >= LARGE_BUCKET_BYTES;
519 (n + is_large as usize, r + b.resident_bytes)
520 });
521 eprintln!(
522 "[KVCACHE] compiling bucket idx={incoming_idx} incoming={:.2}GB \
523 resident_large={n_large} resident_total={:.2}GB cap={}",
524 incoming_bytes as f64 / 1e9,
525 resident as f64 / 1e9,
526 max_resident_large_buckets(),
527 );
528 }
529 if incoming_bytes < LARGE_BUCKET_BYTES {
530 return;
531 }
532 let cap = max_resident_large_buckets();
533 loop {
534 let large: Vec<usize> = (0..self.buckets.len())
535 .filter(|&j| {
536 j != incoming_idx
537 && self.buckets[j].compiled.is_some()
538 && self.buckets[j].resident_bytes >= LARGE_BUCKET_BYTES
539 })
540 .collect();
541 if large.len() < cap {
543 break;
544 }
545 let Some(&victim) = large.iter().min_by_key(|&&j| self.buckets[j].last_used) else {
546 break;
547 };
548 if let Some(c) = &mut self.buckets[victim].compiled {
549 c.sync_pending();
550 }
551 let victim_upper = self.buckets[victim].range.end.saturating_sub(1);
552 self.buckets[victim].compiled = None;
553 self.buckets[victim].resident_bytes = 0;
554 if self.weight_donor_upper == Some(victim_upper) {
555 self.weight_donor_upper = None;
556 }
557 }
558 }
559
560 pub fn get_or_compile<F: FnOnce(u64) -> Graph>(
569 &mut self,
570 key: u64,
571 build: F,
572 ) -> Option<(u64, &mut CompiledGraph)> {
573 self.get_or_compile_with_options(key, build, &crate::CompileOptions::new())
574 }
575
576 pub fn get_or_compile_with_options<F: FnOnce(u64) -> Graph>(
578 &mut self,
579 key: u64,
580 build: F,
581 options: &crate::CompileOptions,
582 ) -> Option<(u64, &mut CompiledGraph)> {
583 let idx = self.bucket_for(key)?;
584 let upper = self.buckets[idx].range.end - 1;
585 if self.buckets[idx].compiled.is_none() {
586 let mut session = Session::new(self.device);
587 if let Some(p) = &self.policy {
588 session = session.with_policy(p.clone());
589 }
590 self.buckets[idx].compiled = Some(session.compile_with(build(upper), options));
591 }
592 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
593 }
594
595 pub fn get_or_compile_hir<F: FnOnce(u64) -> HirModule>(
598 &mut self,
599 key: u64,
600 build: F,
601 ) -> Option<(u64, &mut CompiledGraph)> {
602 self.get_or_compile_hir_with_options(key, build, &crate::CompileOptions::new())
603 }
604
605 pub fn get_or_compile_hir_with_options<F: FnOnce(u64) -> HirModule>(
607 &mut self,
608 key: u64,
609 build: F,
610 options: &crate::CompileOptions,
611 ) -> Option<(u64, &mut CompiledGraph)> {
612 let idx = self.bucket_for(key)?;
613 let upper = self.buckets[idx].range.end - 1;
614 if self.buckets[idx].compiled.is_none() {
615 let mut session = Session::new(self.device);
616 if let Some(p) = &self.policy {
617 session = session.with_policy(p.clone());
618 }
619 let compiled = session
620 .compile_hir_with(build(upper), options)
621 .expect("HIR lower/compile in bucketed cache");
622 self.buckets[idx].compiled = Some(compiled);
623 }
624 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
625 }
626
627 pub fn bucket_for(&self, key: u64) -> Option<usize> {
630 self.buckets.iter().position(|b| b.range.contains(&key))
631 }
632
633 pub fn bucket_upper_for_key(&self, key: u64) -> Option<u64> {
635 let idx = self.bucket_for(key)?;
636 Some(self.buckets[idx].range.end - 1)
637 }
638
639 pub fn buckets(&self) -> impl Iterator<Item = &Range<u64>> {
640 self.buckets.iter().map(|b| &b.range)
641 }
642
643 pub fn compiled_count(&self) -> usize {
645 self.buckets.iter().filter(|b| b.compiled.is_some()).count()
646 }
647
648 pub fn compiled_for_key_mut(&mut self, key: u64) -> Option<&mut CompiledGraph> {
650 let idx = self.bucket_for(key)?;
651 self.buckets[idx].compiled.as_mut()
652 }
653
654 pub fn compiled_for_upper(&self, upper: u64) -> Option<&CompiledGraph> {
656 self.buckets
657 .iter()
658 .find(|b| b.range.end.saturating_sub(1) == upper)
659 .and_then(|b| b.compiled.as_ref())
660 }
661
662 pub fn try_copy_params_between_uppers(&mut self, dst_upper: u64, src_upper: u64) -> bool {
664 if dst_upper == src_upper {
665 return true;
666 }
667 let dst_idx = self
668 .buckets
669 .iter()
670 .position(|b| b.range.end.saturating_sub(1) == dst_upper);
671 let src_idx = self
672 .buckets
673 .iter()
674 .position(|b| b.range.end.saturating_sub(1) == src_upper);
675 let (Some(dst_idx), Some(src_idx)) = (dst_idx, src_idx) else {
676 return false;
677 };
678 if dst_idx == src_idx {
679 return true;
680 }
681 let (dst, src) = if dst_idx < src_idx {
682 let (left, right) = self.buckets.split_at_mut(src_idx);
683 (&mut left[dst_idx], &right[0])
684 } else {
685 let (left, right) = self.buckets.split_at_mut(dst_idx);
686 (&mut right[0], &left[src_idx])
687 };
688 let Some(dst_c) = dst.compiled.as_mut() else {
689 return false;
690 };
691 let Some(src_c) = src.compiled.as_ref() else {
692 return false;
693 };
694 dst_c.copy_params_from(src_c)
695 }
696
697 pub fn weight_donor(&self) -> Option<&CompiledGraph> {
699 self.compiled_for_upper(self.weight_donor_upper?)
700 }
701
702 pub fn set_weight_donor(&mut self, upper: u64) {
706 if self.weight_donor_upper.is_none() {
707 self.weight_donor_upper = Some(upper);
708 }
709 }
710
711 pub fn try_share_params_from_donor(&mut self, dst_upper: u64) -> bool {
718 if rlx_ir::env::var("RLX_METAL_NO_SHARE").is_some() {
719 return false;
720 }
721 let mut candidates: Vec<u64> = Vec::new();
722 if let Some(src) = self.weight_donor_upper {
723 candidates.push(src);
724 }
725 for b in &self.buckets {
726 if b.compiled.is_none() {
727 continue;
728 }
729 let u = b.range.end.saturating_sub(1);
730 if u != dst_upper && !candidates.contains(&u) {
731 candidates.push(u);
732 }
733 }
734 for src_upper in candidates {
735 if self.try_share_params_between_uppers(dst_upper, src_upper) {
736 self.weight_donor_upper = Some(src_upper);
737 return true;
738 }
739 }
740 false
741 }
742
743 fn try_share_params_between_uppers(&mut self, dst_upper: u64, src_upper: u64) -> bool {
744 if dst_upper == src_upper {
745 return true;
746 }
747 let dst_idx = self
748 .buckets
749 .iter()
750 .position(|b| b.range.end.saturating_sub(1) == dst_upper);
751 let src_idx = self
752 .buckets
753 .iter()
754 .position(|b| b.range.end.saturating_sub(1) == src_upper);
755 let (Some(dst_idx), Some(src_idx)) = (dst_idx, src_idx) else {
756 return false;
757 };
758 if dst_idx == src_idx {
759 return true;
760 }
761 let (dst, src) = if dst_idx < src_idx {
762 let (left, right) = self.buckets.split_at_mut(src_idx);
763 (&mut left[dst_idx], &right[0])
764 } else {
765 let (left, right) = self.buckets.split_at_mut(dst_idx);
766 (&mut right[0], &left[src_idx])
767 };
768 let Some(dst_c) = dst.compiled.as_mut() else {
769 return false;
770 };
771 let Some(src_c) = src.compiled.as_ref() else {
772 return false;
773 };
774 dst_c.share_params_from(src_c)
775 }
776
777 pub fn seed_resident_kv_prefix_from_keys(
781 &mut self,
782 src_key: u64,
783 dst_key: u64,
784 prefix_tokens: usize,
785 outgoing_upper: usize,
786 kv_dim: usize,
787 n_layers: usize,
788 ) -> bool {
789 let Some(src_idx) = self.bucket_for(src_key) else {
790 return false;
791 };
792 let Some(dst_idx) = self.bucket_for(dst_key) else {
793 return false;
794 };
795 if src_idx == dst_idx {
796 return true;
797 }
798 if src_idx < dst_idx {
799 let (left, right) = self.buckets.split_at_mut(dst_idx);
800 let Some(src) = left[src_idx].compiled.as_ref() else {
801 return false;
802 };
803 let Some(dst) = right[0].compiled.as_mut() else {
804 return false;
805 };
806 return dst.seed_resident_kv_prefix_from(
807 src,
808 prefix_tokens,
809 outgoing_upper,
810 kv_dim,
811 n_layers,
812 );
813 }
814 let (left, right) = self.buckets.split_at_mut(src_idx);
815 let Some(src) = right[0].compiled.as_ref() else {
816 return false;
817 };
818 let Some(dst) = left[dst_idx].compiled.as_mut() else {
819 return false;
820 };
821 dst.seed_resident_kv_prefix_from(src, prefix_tokens, outgoing_upper, kv_dim, n_layers)
822 }
823
824 pub fn rebind_resident_kv_hybrid_from_keys(
828 &mut self,
829 src_key: u64,
830 dst_key: u64,
831 host_k: &[Vec<f32>],
832 host_v: &[Vec<f32>],
833 prefix_tokens: usize,
834 outgoing_upper: usize,
835 upper: usize,
836 kv_dim: usize,
837 n_layers: usize,
838 ) -> bool {
839 let Some(src_idx) = self.bucket_for(src_key) else {
840 return false;
841 };
842 let Some(dst_idx) = self.bucket_for(dst_key) else {
843 return false;
844 };
845 if src_idx == dst_idx {
846 return true;
847 }
848 let host_rows = host_k.first().map(|k| k.len() / kv_dim.max(1)).unwrap_or(0);
849 let rebind = |src: &CompiledGraph, dst: &mut CompiledGraph| -> bool {
850 for i in 0..n_layers {
851 let mut kp = vec![0f32; upper * kv_dim];
852 let mut vp = vec![0f32; upper * kv_dim];
853 let nk = host_k[i].len().min(kp.len());
854 let nv = host_v[i].len().min(vp.len());
855 kp[..nk].copy_from_slice(&host_k[i][..nk]);
856 vp[..nv].copy_from_slice(&host_v[i][..nv]);
857 let k_name = format!("past_k_{i}");
858 let v_name = format!("past_v_{i}");
859 if !dst.bind_gpu_handle(&k_name, &kp) || !dst.bind_gpu_handle(&v_name, &vp) {
860 return false;
861 }
862 dst.register_kv_row_feed(&k_name, 1 + 2 * i);
863 dst.register_kv_row_feed(&v_name, 2 + 2 * i);
864 }
865 dst.stage_bound_gpu_handles_to_arena();
866 if host_rows < prefix_tokens {
867 return dst.copy_resident_kv_rows_from(
868 src,
869 host_rows,
870 prefix_tokens,
871 outgoing_upper,
872 kv_dim,
873 n_layers,
874 );
875 }
876 for i in 0..n_layers {
877 let k_name = format!("past_k_{i}");
878 let v_name = format!("past_v_{i}");
879 dst.prepare_resident_gpu_handle(&k_name);
880 dst.prepare_resident_gpu_handle(&v_name);
881 }
882 true
883 };
884 if src_idx < dst_idx {
885 let (left, right) = self.buckets.split_at_mut(dst_idx);
886 let Some(src) = left[src_idx].compiled.as_ref() else {
887 return false;
888 };
889 let Some(dst) = right[0].compiled.as_mut() else {
890 return false;
891 };
892 return rebind(src, dst);
893 }
894 let (left, right) = self.buckets.split_at_mut(src_idx);
895 let Some(src) = right[0].compiled.as_ref() else {
896 return false;
897 };
898 let Some(dst) = left[dst_idx].compiled.as_mut() else {
899 return false;
900 };
901 rebind(src, dst)
902 }
903
904 pub fn total_buckets(&self) -> usize {
905 self.buckets.len()
906 }
907
908 pub fn evict_except(&mut self, keep: usize) {
910 for (i, bucket) in self.buckets.iter_mut().enumerate() {
911 if i != keep {
912 bucket.compiled = None;
913 }
914 }
915 }
916
917 pub fn evict_one_except(&mut self, protect: usize) -> Option<usize> {
931 let victim = self
932 .buckets
933 .iter()
934 .enumerate()
935 .rev()
936 .find(|(i, b)| *i != protect && b.compiled.is_some())
937 .map(|(i, _)| i)?;
938 if let Some(compiled) = self.buckets[victim].compiled.as_mut() {
939 compiled.sync_pending();
940 }
941 self.buckets[victim].compiled = None;
942 Some(victim)
943 }
944
945 pub fn clear_compiled(&mut self) {
947 for bucket in &mut self.buckets {
948 bucket.compiled = None;
949 }
950 }
951
952 pub fn run_padded<F: FnOnce(u64) -> Graph>(
978 &mut self,
979 key: u64,
980 actual_rows: usize,
981 build: F,
982 inputs: &[(&str, &[f32], usize)],
983 output_inners: &[usize],
984 ) -> Option<(u64, Vec<Vec<f32>>)> {
985 let (upper, compiled) = self.get_or_compile(key, build)?;
986
987 let padded: Vec<(&str, Vec<f32>)> = inputs
989 .iter()
990 .map(|(name, data, inner)| (*name, pad_rows(data, *inner, upper)))
991 .collect();
992 let pairs: Vec<(&str, &[f32])> = padded.iter().map(|(n, d)| (*n, d.as_slice())).collect();
993
994 compiled.set_active_extent(Some((actual_rows, upper as usize)));
1000 let raw_outputs = compiled.run(&pairs);
1001 compiled.set_active_extent(None);
1002 #[cfg(feature = "cpu")]
1003 crate::onnx_active::set_active_token_count(None);
1004
1005 let outs = raw_outputs
1006 .into_iter()
1007 .enumerate()
1008 .map(|(i, out)| match output_inners.get(i).copied() {
1009 Some(0) | None => out,
1010 Some(inner) => slice_rows(&out, inner, actual_rows),
1011 })
1012 .collect();
1013
1014 Some((upper, outs))
1015 }
1016
1017 pub fn ensure_graph_with_params<F>(
1019 &mut self,
1020 key: u64,
1021 build: F,
1022 options: &crate::CompileOptions,
1023 ) -> Option<(u64, &mut CompiledGraph)>
1024 where
1025 F: FnOnce(u64) -> (Graph, HashMap<String, Vec<f32>>),
1026 {
1027 let idx = self.bucket_for(key)?;
1028 let upper = self.buckets[idx].range.end - 1;
1029 if self.buckets[idx].compiled.is_none() {
1030 let (graph, params) = build(upper);
1031 let bytes: usize = params.values().map(|v| v.len() * 4).sum();
1032 self.evict_for_incoming(idx, bytes);
1033 let mut session = Session::new(self.device);
1034 if let Some(p) = &self.policy {
1035 session = session.with_policy(p.clone());
1036 }
1037 let compiled = session.compile_with(graph, options);
1038 self.buckets[idx].compiled = Some(compiled);
1039 let has_other_donor = self
1054 .buckets
1055 .iter()
1056 .enumerate()
1057 .any(|(i, b)| i != idx && b.compiled.is_some());
1058 if has_other_donor && self.try_share_params_from_donor(upper) {
1059 self.buckets[idx].resident_bytes = 0;
1063 } else {
1064 let compiled = self.buckets[idx].compiled.as_mut().expect("just inserted");
1065 for (name, data) in params {
1066 compiled.set_param(&name, &data);
1067 }
1068 self.buckets[idx].resident_bytes = bytes;
1069 }
1070 self.set_weight_donor(upper);
1071 }
1072 self.touch(idx);
1073 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
1074 }
1075
1076 pub fn ensure_graph_with_packed<F>(
1084 &mut self,
1085 key: u64,
1086 build: F,
1087 options: &crate::CompileOptions,
1088 ) -> Option<(u64, &mut CompiledGraph)>
1089 where
1090 F: FnOnce(u64) -> (Graph, HashMap<String, Vec<f32>>, Vec<(String, Vec<u8>)>),
1091 {
1092 let idx = self.bucket_for(key)?;
1093 let upper = self.buckets[idx].range.end - 1;
1094 if self.buckets[idx].compiled.is_none() {
1095 let (graph, params, packed) = build(upper);
1096 let bytes: usize = params.values().map(|v| v.len() * 4).sum::<usize>()
1097 + packed.iter().map(|(_, b)| b.len()).sum::<usize>();
1098 self.evict_for_incoming(idx, bytes);
1099 let mut session = Session::new(self.device);
1100 if let Some(p) = &self.policy {
1101 session = session.with_policy(p.clone());
1102 }
1103 let mut compiled = session.compile_with(graph, options);
1104 for (name, data) in params {
1105 compiled.set_param(&name, &data);
1106 }
1107 for (name, data) in &packed {
1108 compiled.set_param_typed(name, data, rlx_ir::DType::U8);
1109 }
1110 self.buckets[idx].compiled = Some(compiled);
1111 self.buckets[idx].resident_bytes = bytes;
1112 }
1113 self.touch(idx);
1114 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
1115 }
1116
1117 pub fn ensure_hir_with_params<F>(
1119 &mut self,
1120 key: u64,
1121 build: F,
1122 options: &crate::CompileOptions,
1123 ) -> Option<(u64, &mut CompiledGraph)>
1124 where
1125 F: FnOnce(u64) -> (HirModule, HashMap<String, Vec<f32>>),
1126 {
1127 let idx = self.bucket_for(key)?;
1128 let upper = self.buckets[idx].range.end - 1;
1129 if self.buckets[idx].compiled.is_none() {
1130 let (hir, params) = build(upper);
1131 let bytes: usize = params.values().map(|v| v.len() * 4).sum();
1132 let mut session = Session::new(self.device);
1137 if let Some(p) = &self.policy {
1138 session = session.with_policy(p.clone());
1139 }
1140 let mut compiled = session
1141 .compile_hir_with(hir, options)
1142 .expect("HIR lower/compile in ensure_hir_with_params");
1143 for (name, data) in params {
1144 compiled.set_param(&name, &data);
1145 }
1146 self.buckets[idx].compiled = Some(compiled);
1147 self.buckets[idx].resident_bytes = bytes;
1148 }
1149 self.touch(idx);
1150 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
1151 }
1152
1153 pub fn note_resident_bytes(&mut self, key: u64, extra_bytes: usize) {
1159 if extra_bytes == 0 {
1160 return;
1161 }
1162 let Some(idx) = self.bucket_for(key) else {
1163 return;
1164 };
1165 let before = self.buckets[idx].resident_bytes;
1166 let after = before.saturating_add(extra_bytes);
1167 if after >= LARGE_BUCKET_BYTES {
1169 self.evict_for_incoming(idx, after);
1170 }
1171 self.buckets[idx].resident_bytes = after;
1172 self.touch(idx);
1173 }
1174
1175 pub fn run_padded_mixed<F>(
1177 &mut self,
1178 key: u64,
1179 actual_rows: usize,
1180 build: F,
1181 inputs: &[CacheRunInput<'_>],
1182 output_inners: &[usize],
1183 ) -> Option<(u64, Vec<Vec<f32>>)>
1184 where
1185 F: FnOnce(u64) -> Graph,
1186 {
1187 let (upper, compiled) = self.get_or_compile(key, build)?;
1188
1189 let padded: Vec<(&str, Vec<f32>)> = inputs
1190 .iter()
1191 .map(|inp| match inp.row_inner {
1192 Some(inner) => (inp.name, pad_rows(inp.data, inner, upper)),
1193 None => (inp.name, inp.data.to_vec()),
1194 })
1195 .collect();
1196 let pairs: Vec<(&str, &[f32])> = padded.iter().map(|(n, d)| (*n, d.as_slice())).collect();
1197
1198 compiled.set_active_extent(Some((actual_rows, upper as usize)));
1199 let raw_outputs = compiled.run(&pairs);
1200 compiled.set_active_extent(None);
1201 #[cfg(feature = "cpu")]
1202 crate::onnx_active::set_active_token_count(None);
1203
1204 let outs = raw_outputs
1205 .into_iter()
1206 .enumerate()
1207 .map(|(i, out)| match output_inners.get(i).copied() {
1208 Some(0) | None => out,
1209 Some(inner) => slice_rows(&out, inner, actual_rows),
1210 })
1211 .collect();
1212
1213 Some((upper, outs))
1214 }
1215
1216 pub fn sync_all(&mut self) {
1218 for bucket in &mut self.buckets {
1219 if let Some(compiled) = &mut bucket.compiled {
1220 compiled.sync_pending();
1221 }
1222 }
1223 }
1224}
1225
1226pub struct DynamicDimCompileCache {
1234 device: Device,
1235 policy: Option<rlx_opt::PrecisionPolicy>,
1236 capacity: usize,
1237 template: Option<CompileResult>,
1238 entries: Vec<(u64, CompiledGraph)>,
1239 order: VecDeque<u64>,
1240}
1241
1242impl DynamicDimCompileCache {
1243 pub fn new(device: Device, capacity: usize) -> Self {
1244 Self::with_policy(device, capacity, None)
1245 }
1246
1247 pub fn with_policy(
1248 device: Device,
1249 capacity: usize,
1250 policy: Option<rlx_opt::PrecisionPolicy>,
1251 ) -> Self {
1252 assert!(capacity > 0, "DynamicDimCompileCache capacity must be ≥ 1");
1253 Self {
1254 device,
1255 policy,
1256 capacity,
1257 template: None,
1258 entries: Vec::with_capacity(capacity),
1259 order: VecDeque::with_capacity(capacity),
1260 }
1261 }
1262
1263 pub fn compile_device(&self) -> Device {
1264 self.device
1265 }
1266
1267 pub fn get_or_specialize<F: FnOnce() -> HirModule>(
1270 &mut self,
1271 key: u64,
1272 binding: &DimBinding,
1273 build_hir: F,
1274 options: &crate::CompileOptions,
1275 ) -> Result<&mut CompiledGraph, rlx_ir::hir::LowerError> {
1276 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
1277 return Ok(&mut self.entries[idx].1);
1278 }
1279 if self.template.is_none() {
1280 let mut template_opts = options.clone();
1281 template_opts.dim_binding = None;
1282 let pipe = crate::stages::pipeline_for(self.device, &template_opts);
1283 self.template = Some(pipe.compile_hir(build_hir())?);
1284 }
1285 let template = self.template.as_ref().expect("template just set");
1286 let mut spec_opts = options.clone();
1287 spec_opts.dim_binding = None;
1288 let pipe = crate::stages::pipeline_for(self.device, &spec_opts);
1289 let specialized = template.specialize(&pipe, binding);
1290 let backend = crate::registry::backend_for(self.device).expect("backend registered");
1291 let mut compile_opts = options.clone();
1292 compile_opts.dim_binding = None;
1293 if compile_opts.policy.is_none() {
1294 if let Some(p) = &self.policy {
1295 compile_opts = compile_opts.policy(p.clone());
1296 }
1297 }
1298 let executable = backend.compile_lir(specialized.lir, &compile_opts);
1299 let compiled = CompiledGraph::new(executable, self.device);
1300
1301 if self.entries.len() >= self.capacity
1302 && let Some(evict_key) = self.order.pop_front()
1303 {
1304 sync_evicted_entry(&mut self.entries, evict_key);
1305 self.entries.retain(|(k, _)| *k != evict_key);
1306 }
1307 self.entries.push((key, compiled));
1308 self.order.push_back(key);
1309 Ok(&mut self.entries.last_mut().unwrap().1)
1310 }
1311
1312 pub fn len(&self) -> usize {
1313 self.entries.len()
1314 }
1315
1316 pub fn is_empty(&self) -> bool {
1317 self.entries.is_empty()
1318 }
1319
1320 pub fn contains(&self, key: u64) -> bool {
1321 self.entries.iter().any(|(k, _)| *k == key)
1322 }
1323
1324 pub fn clear(&mut self) {
1325 self.sync_all();
1326 self.template = None;
1327 self.entries.clear();
1328 self.order.clear();
1329 }
1330
1331 pub fn has_template(&self) -> bool {
1332 self.template.is_some()
1333 }
1334
1335 pub fn sync_all(&mut self) {
1337 for (_, compiled) in &mut self.entries {
1338 compiled.sync_pending();
1339 }
1340 }
1341
1342 pub fn ensure_template<F: FnOnce() -> HirModule>(
1344 &mut self,
1345 build_hir: F,
1346 options: &crate::CompileOptions,
1347 ) -> Result<&CompileResult, rlx_ir::hir::LowerError> {
1348 if self.template.is_none() {
1349 let mut opts = options.clone();
1350 opts.dim_binding = None;
1351 let pipe = crate::stages::pipeline_for(self.device, &opts);
1352 self.template = Some(pipe.compile_hir(build_hir())?);
1353 }
1354 Ok(self.template.as_ref().expect("template set"))
1355 }
1356
1357 pub fn template_result(&self) -> Option<&CompileResult> {
1358 self.template.as_ref()
1359 }
1360
1361 pub fn get_or_specialize_aot<F: FnOnce() -> HirModule>(
1364 &mut self,
1365 aot: &crate::AotCache,
1366 disk_base: &str,
1367 key: u64,
1368 binding: &rlx_ir::DimBinding,
1369 build_hir: F,
1370 options: &crate::CompileOptions,
1371 ) -> Result<&mut CompiledGraph, crate::AotCacheError> {
1372 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
1373 return Ok(&mut self.entries[idx].1);
1374 }
1375 let device = self.device;
1376 let template = self.ensure_template(build_hir, options)?;
1377 let compiled = aot.specialize_cached(disk_base, binding, device, template, options)?;
1378 if self.entries.len() >= self.capacity
1379 && let Some(evict_key) = self.order.pop_front()
1380 {
1381 sync_evicted_entry(&mut self.entries, evict_key);
1382 self.entries.retain(|(k, _)| *k != evict_key);
1383 }
1384 self.entries.push((key, compiled));
1385 self.order.push_back(key);
1386 Ok(&mut self.entries.last_mut().unwrap().1)
1387 }
1388}
1389
1390pub fn pad_rows(data: &[f32], inner: usize, upper: u64) -> Vec<f32> {
1398 assert!(inner > 0, "pad_rows: inner stride must be ≥ 1");
1399 assert_eq!(
1400 data.len() % inner,
1401 0,
1402 "pad_rows: data len {} not a multiple of inner {inner}",
1403 data.len(),
1404 );
1405 let upper = upper as usize;
1406 let actual = data.len() / inner;
1407 assert!(
1408 actual <= upper,
1409 "pad_rows: actual rows {actual} exceed upper bound {upper}",
1410 );
1411 let mut out = vec![0.0_f32; upper * inner];
1412 out[..actual * inner].copy_from_slice(data);
1413 out
1414}
1415
1416pub fn pad_rows_into(out: &mut [f32], data: &[f32], inner: usize) {
1418 assert!(inner > 0, "pad_rows_into: inner stride must be ≥ 1");
1419 assert_eq!(
1420 data.len() % inner,
1421 0,
1422 "pad_rows_into: data len {} not a multiple of inner {inner}",
1423 data.len(),
1424 );
1425 assert_eq!(
1426 out.len() % inner,
1427 0,
1428 "pad_rows_into: out len {} not a multiple of inner {inner}",
1429 out.len(),
1430 );
1431 let upper = out.len() / inner;
1432 let actual = data.len() / inner;
1433 assert!(
1434 actual <= upper,
1435 "pad_rows_into: actual rows {actual} exceed upper bound {upper}",
1436 );
1437 out.fill(0.0);
1438 out[..data.len()].copy_from_slice(data);
1439}
1440
1441pub fn slice_rows(data: &[f32], inner: usize, actual: usize) -> Vec<f32> {
1447 assert!(inner > 0, "slice_rows: inner stride must be ≥ 1");
1448 assert_eq!(
1449 data.len() % inner,
1450 0,
1451 "slice_rows: data len {} not a multiple of inner {inner}",
1452 data.len(),
1453 );
1454 let upper = data.len() / inner;
1455 assert!(
1456 actual <= upper,
1457 "slice_rows: actual rows {actual} exceed upper {upper}",
1458 );
1459 data[..actual * inner].to_vec()
1460}
1461
1462#[cfg(test)]
1463mod tests {
1464 use super::*;
1465 use rlx_ir::infer::GraphExt;
1466 use rlx_ir::*;
1467 use std::cell::Cell;
1468
1469 fn tiny_graph(n: usize) -> Graph {
1470 let mut g = Graph::new("t");
1471 let f = DType::F32;
1472 let x = g.input("x", Shape::new(&[n], f));
1473 let y = g.activation(rlx_ir::op::Activation::Relu, x, Shape::new(&[n], f));
1474 g.set_outputs(vec![y]);
1475 g
1476 }
1477
1478 #[test]
1479 fn cache_hits_avoid_recompile() {
1480 let mut cache = CompileCache::new(Device::Cpu, 4);
1481 let calls = Cell::new(0);
1482
1483 let _ = cache.get_or_compile(1, || {
1484 calls.set(calls.get() + 1);
1485 tiny_graph(8)
1486 });
1487 let _ = cache.get_or_compile(1, || {
1488 calls.set(calls.get() + 1);
1489 tiny_graph(8)
1490 });
1491 let _ = cache.get_or_compile(1, || {
1492 calls.set(calls.get() + 1);
1493 tiny_graph(8)
1494 });
1495 assert_eq!(calls.get(), 1);
1497 assert_eq!(cache.len(), 1);
1498 }
1499
1500 #[test]
1501 fn fifo_evicts_oldest_at_capacity() {
1502 let mut cache = CompileCache::new(Device::Cpu, 2);
1503 let _ = cache.get_or_compile(1, || tiny_graph(4));
1504 let _ = cache.get_or_compile(2, || tiny_graph(8));
1505 assert!(cache.contains(1) && cache.contains(2));
1506 let _ = cache.get_or_compile(3, || tiny_graph(16));
1508 assert!(!cache.contains(1));
1509 assert!(cache.contains(2) && cache.contains(3));
1510 }
1511
1512 #[test]
1513 fn different_keys_keep_separate_compiles() {
1514 let mut cache = CompileCache::new(Device::Cpu, 4);
1515 let calls = Cell::new(0);
1516 let _ = cache.get_or_compile(1, || {
1517 calls.set(calls.get() + 1);
1518 tiny_graph(8)
1519 });
1520 let _ = cache.get_or_compile(2, || {
1521 calls.set(calls.get() + 1);
1522 tiny_graph(16)
1523 });
1524 let _ = cache.get_or_compile(1, || {
1525 calls.set(calls.get() + 1);
1526 tiny_graph(8)
1527 });
1528 assert_eq!(calls.get(), 2);
1530 assert_eq!(cache.len(), 2);
1531 }
1532
1533 #[test]
1536 fn bucket_amortizes_keys_within_range() {
1537 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16]);
1538 let calls = Cell::new(0);
1539 let uppers = Cell::new((0u64, 0u64));
1540
1541 let (u1, _) = cache
1543 .get_or_compile(2, |upper| {
1544 calls.set(calls.get() + 1);
1545 uppers.set((upper, uppers.get().1));
1546 tiny_graph(upper as usize)
1547 })
1548 .expect("key 2 in range");
1549 let (u2, _) = cache
1550 .get_or_compile(3, |upper| {
1551 calls.set(calls.get() + 1);
1552 uppers.set((uppers.get().0, upper));
1553 tiny_graph(upper as usize)
1554 })
1555 .expect("key 3 in range");
1556
1557 assert_eq!(calls.get(), 1);
1559 assert_eq!(u1, 3);
1560 assert_eq!(u2, 3);
1561 assert_eq!(uppers.get().0, 3);
1562 assert_eq!(cache.compiled_count(), 1);
1563 assert_eq!(cache.total_buckets(), 2);
1564 }
1565
1566 #[test]
1567 fn bucket_lookup_returns_none_outside_range() {
1568 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16]);
1569 assert!(cache.bucket_for(0).is_none());
1570 assert!(cache.bucket_for(16).is_none());
1571 assert!(cache.bucket_for(100).is_none());
1572 assert_eq!(cache.bucket_for(3), Some(0));
1573 assert_eq!(cache.bucket_for(4), Some(1));
1574 assert_eq!(cache.bucket_upper_for_key(3), Some(3));
1575 assert_eq!(cache.bucket_upper_for_key(4), Some(15));
1576 assert!(cache.bucket_upper_for_key(0).is_none());
1577
1578 let calls = Cell::new(0);
1579 let result = cache.get_or_compile(100, |u| {
1580 calls.set(calls.get() + 1);
1581 tiny_graph(u as usize)
1582 });
1583 assert!(result.is_none());
1584 assert_eq!(calls.get(), 0); assert_eq!(cache.compiled_count(), 0);
1586 }
1587
1588 #[test]
1589 fn bucket_compiles_lazily_per_bucket() {
1590 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16, 16..64]);
1591 let calls = Cell::new(0);
1592
1593 let _ = cache.get_or_compile(2, |u| {
1594 calls.set(calls.get() + 1);
1595 tiny_graph(u as usize)
1596 });
1597 let _ = cache.get_or_compile(8, |u| {
1598 calls.set(calls.get() + 1);
1599 tiny_graph(u as usize)
1600 });
1601 assert_eq!(calls.get(), 2);
1603 assert_eq!(cache.compiled_count(), 2);
1604 assert_eq!(cache.total_buckets(), 3);
1605 }
1606
1607 #[test]
1608 #[should_panic(expected = "overlap")]
1609 fn bucket_overlap_rejected() {
1610 let _ = BucketedCompileCache::new(Device::Cpu, vec![1..8, 4..16]);
1611 }
1612
1613 #[test]
1614 #[should_panic(expected = "≥1 bucket")]
1615 fn empty_bucket_list_rejected() {
1616 let _ = BucketedCompileCache::new(Device::Cpu, vec![]);
1617 }
1618
1619 #[test]
1622 fn pad_rows_appends_zeros() {
1623 let p = pad_rows(&[1.0, 2.0, 3.0], 1, 5);
1625 assert_eq!(p, vec![1.0, 2.0, 3.0, 0.0, 0.0]);
1626
1627 let p = pad_rows(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 4);
1629 assert_eq!(
1630 p,
1631 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],
1632 );
1633
1634 let p = pad_rows(&[7.0, 8.0], 1, 2);
1636 assert_eq!(p, vec![7.0, 8.0]);
1637 }
1638
1639 #[test]
1640 fn slice_rows_truncates_trailing() {
1641 let s = slice_rows(&[1.0, 2.0, 3.0, 0.0, 0.0], 1, 3);
1642 assert_eq!(s, vec![1.0, 2.0, 3.0]);
1643
1644 let s = slice_rows(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 0.0], 3, 2);
1645 assert_eq!(s, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1646 }
1647
1648 #[test]
1649 #[should_panic(expected = "exceed upper")]
1650 fn pad_rows_rejects_too_long_input() {
1651 let _ = pad_rows(&[1.0, 2.0, 3.0, 4.0], 1, 3);
1652 }
1653
1654 #[test]
1655 #[should_panic(expected = "exceed upper")]
1656 fn slice_rows_rejects_too_large_actual() {
1657 let _ = slice_rows(&[1.0, 2.0, 3.0], 1, 5);
1658 }
1659
1660 #[test]
1663 fn run_padded_pads_input_and_slices_output() {
1664 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1667 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];
1668
1669 let (upper, outs) = cache
1670 .run_padded(
1671 10, 10, |max| tiny_graph(max as usize),
1674 &[("x", &input, 1)], &[1], )
1677 .expect("key 10 in [1..16)");
1678
1679 assert_eq!(upper, 15);
1680 assert_eq!(outs.len(), 1);
1681 let out = &outs[0];
1682 assert_eq!(out.len(), 10, "output sliced back to actual_rows");
1683 let expected: Vec<f32> = input.iter().map(|x| x.max(0.0)).collect();
1684 assert_eq!(out, &expected);
1685 }
1686
1687 #[test]
1688 fn run_padded_reuses_bucket_across_actuals() {
1689 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1691 let calls = Cell::new(0);
1692
1693 let (u1, o1) = cache
1694 .run_padded(
1695 10,
1696 10,
1697 |max| {
1698 calls.set(calls.get() + 1);
1699 tiny_graph(max as usize)
1700 },
1701 &[(
1702 "x",
1703 &[1.0, -1.0, 2.0, -2.0, 3.0, -3.0, 4.0, -4.0, 5.0, -5.0],
1704 1,
1705 )],
1706 &[1],
1707 )
1708 .unwrap();
1709 assert_eq!(o1.len(), 1);
1710 assert_eq!(o1[0].len(), 10);
1711 assert_eq!(u1, 15);
1712
1713 let (u2, o2) = cache
1714 .run_padded(
1715 5,
1716 5,
1717 |max| {
1718 calls.set(calls.get() + 1);
1719 tiny_graph(max as usize)
1720 },
1721 &[("x", &[-1.0, 2.0, -3.0, 4.0, -5.0], 1)],
1722 &[1],
1723 )
1724 .unwrap();
1725 assert_eq!(o2.len(), 1);
1726 assert_eq!(o2[0].len(), 5);
1727 assert_eq!(u2, 15);
1728 assert_eq!(o2[0], vec![0.0, 2.0, 0.0, 4.0, 0.0]);
1729
1730 assert_eq!(calls.get(), 1, "bucket cached across actuals");
1731 assert_eq!(cache.compiled_count(), 1);
1732 }
1733
1734 #[test]
1735 fn run_padded_returns_none_out_of_range() {
1736 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1737 let calls = Cell::new(0);
1738 let result = cache.run_padded(
1739 100,
1740 5,
1741 |u| {
1742 calls.set(calls.get() + 1);
1743 tiny_graph(u as usize)
1744 },
1745 &[("x", &[1.0, 2.0, 3.0, 4.0, 5.0], 1)],
1746 &[1],
1747 );
1748 assert!(result.is_none());
1749 assert_eq!(calls.get(), 0);
1750 assert_eq!(cache.compiled_count(), 0);
1751 }
1752
1753 #[test]
1756 fn power_of_two_ladder_generates_log_buckets() {
1757 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 64);
1758 let ranges: Vec<_> = cache.buckets().cloned().collect();
1760 assert_eq!(ranges, vec![1..9, 9..17, 17..33, 33..65]);
1761 assert_eq!(cache.total_buckets(), 4);
1762 }
1763
1764 #[test]
1765 fn power_of_two_ladder_picks_smallest_extent_for_actual() {
1766 let mut cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 64);
1769 let captured_uppers: std::cell::RefCell<Vec<u64>> = Default::default();
1770
1771 let (u17, _) = cache
1772 .get_or_compile(17, |upper| {
1773 captured_uppers.borrow_mut().push(upper);
1774 tiny_graph(upper as usize)
1775 })
1776 .unwrap();
1777 let (u9, _) = cache
1778 .get_or_compile(9, |upper| {
1779 captured_uppers.borrow_mut().push(upper);
1780 tiny_graph(upper as usize)
1781 })
1782 .unwrap();
1783 let (u3, _) = cache
1784 .get_or_compile(3, |upper| {
1785 captured_uppers.borrow_mut().push(upper);
1786 tiny_graph(upper as usize)
1787 })
1788 .unwrap();
1789 let (u64_, _) = cache
1790 .get_or_compile(64, |upper| {
1791 captured_uppers.borrow_mut().push(upper);
1792 tiny_graph(upper as usize)
1793 })
1794 .unwrap();
1795
1796 assert_eq!(u17, 32, "key=17 → smallest extent ≥ 17 is 32");
1797 assert_eq!(u9, 16, "key=9 → smallest extent ≥ 9 is 16");
1798 assert_eq!(u3, 8, "key=3 → smallest extent ≥ 3 is 8");
1799 assert_eq!(u64_, 64, "key=64 → exact match at 64");
1800 assert_eq!(*captured_uppers.borrow(), vec![32, 16, 8, 64]);
1801 assert_eq!(cache.compiled_count(), 4);
1802 }
1803
1804 #[test]
1805 fn power_of_two_ladder_min_above_one_starts_at_one() {
1806 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 16, 32);
1809 let ranges: Vec<_> = cache.buckets().cloned().collect();
1810 assert_eq!(ranges, vec![1..17, 17..33]);
1812 }
1813
1814 #[test]
1815 fn power_of_two_ladder_non_pow2_min_rounds_up() {
1816 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 10, 64);
1818 let ranges: Vec<_> = cache.buckets().cloned().collect();
1819 assert_eq!(ranges, vec![1..17, 17..33, 33..65]);
1820 }
1821
1822 #[test]
1823 fn power_of_two_ladder_max_below_pow2_extends_up() {
1824 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 20);
1826 let ranges: Vec<_> = cache.buckets().cloned().collect();
1827 assert_eq!(ranges, vec![1..9, 9..17, 17..33]);
1828 }
1829
1830 #[test]
1831 fn power_of_two_ladder_min_equals_max() {
1832 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 16, 16);
1833 let ranges: Vec<_> = cache.buckets().cloned().collect();
1834 assert_eq!(ranges, vec![1..17]);
1835 }
1836
1837 #[test]
1838 #[should_panic(expected = "min must be ≥ 1")]
1839 fn power_of_two_ladder_zero_min_rejected() {
1840 let _ = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 0, 16);
1841 }
1842
1843 #[test]
1844 #[should_panic(expected = "max")]
1845 fn power_of_two_ladder_max_below_min_rejected() {
1846 let _ = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 32, 8);
1847 }
1848
1849 #[test]
1862 #[ignore = "active-extent execution is a stub on CPU (rlx-cpu/src/thunk/exec_dispatch.rs::execute_thunks_active returns false unconditionally)"]
1863 fn active_extent_skips_compute_on_cpu_activation() {
1864 let graph = tiny_graph(15);
1875 let mut compiled = Session::new(Device::Cpu).compile(graph);
1876
1877 let warm_input: Vec<f32> = vec![1.0; 15];
1879 let warm_outs = compiled.run(&[("x", &warm_input)]);
1880 assert_eq!(warm_outs[0], vec![1.0; 15], "warm-up sanity");
1881
1882 let neg_input: Vec<f32> = vec![-1.0; 15];
1885 compiled.set_active_extent(Some((5, 15)));
1886 let outs = compiled.run(&[("x", &neg_input)]);
1887 let out = &outs[0];
1888
1889 assert_eq!(out.len(), 15);
1890 assert_eq!(
1891 out[..5],
1892 [0.0; 5],
1893 "first 5 elements processed (relu of -1)"
1894 );
1895 assert_eq!(
1896 out[5..],
1897 [1.0; 10],
1898 "tail untouched — proves Copy + Activation skipped indices 5..15"
1899 );
1900
1901 compiled.set_active_extent(None);
1904 let outs = compiled.run(&[("x", &neg_input)]);
1905 assert_eq!(
1906 outs[0],
1907 vec![0.0; 15],
1908 "full-extent path must clip every negative"
1909 );
1910 }
1911
1912 #[test]
1913 #[ignore = "active-extent execution is a stub on CPU (rlx-cpu/src/thunk/exec_dispatch.rs::execute_thunks_active returns false unconditionally)"]
1914 fn active_extent_skips_compute_on_binary_full() {
1915 let mut g = Graph::new("add");
1919 let f = DType::F32;
1920 let a = g.input("a", Shape::new(&[4], f));
1921 let b = g.input("b", Shape::new(&[4], f));
1922 let c = g.add(a, b);
1923 g.set_outputs(vec![c]);
1924 let mut compiled = Session::new(Device::Cpu).compile(g);
1925
1926 let warm = compiled.run(&[("a", &[1.0f32; 4]), ("b", &[1.0f32; 4])]);
1928 assert_eq!(warm[0], vec![2.0; 4]);
1929
1930 compiled.set_active_extent(Some((2, 4)));
1933 let outs = compiled.run(&[("a", &[10.0f32; 4]), ("b", &[10.0f32; 4])]);
1934 let out = &outs[0];
1935 assert_eq!(out[..2], [20.0, 20.0], "first 2 = active sum");
1936 assert_eq!(
1937 out[2..],
1938 [2.0, 2.0],
1939 "tail untouched — proves BinaryFull skipped indices 2..4"
1940 );
1941
1942 compiled.set_active_extent(None);
1944 let outs = compiled.run(&[("a", &[10.0f32; 4]), ("b", &[10.0f32; 4])]);
1945 assert_eq!(outs[0], vec![20.0; 4]);
1946 }
1947
1948 #[test]
1949 #[ignore = "process-wide STATE; runs only in isolation via `cargo test perfetto -- --ignored`"]
1950 fn perfetto_trace_emits_per_thunk_events() {
1951 use std::env;
1958 use std::fs;
1959 let path = env::temp_dir().join(format!("rlx-perfetto-e2e-{}.json", std::process::id()));
1960 if path.exists() {
1961 let _ = fs::remove_file(&path);
1962 }
1963 unsafe {
1964 env::set_var("RLX_TRACE_PERFETTO", &path);
1965 }
1966
1967 let f = DType::F32;
1969 let mut g = Graph::new("perf");
1970 let a = g.input("a", Shape::new(&[4], f));
1971 let b = g.input("b", Shape::new(&[4], f));
1972 let s = g.add(a, b);
1973 let r = g.relu(s);
1974 g.set_outputs(vec![r]);
1975 let mut compiled = Session::new(Device::Cpu).compile(g);
1976 let _ = compiled.run(&[("a", &[1.0; 4]), ("b", &[1.0; 4])]);
1977
1978 crate::perfetto::flush_and_finalize();
1980
1981 let contents = fs::read_to_string(&path).expect("trace file");
1982 assert!(
1984 contents.contains("\"binary\"")
1985 || contents.contains("\"activation\"")
1986 || contents.contains("\"elementwise_region\""),
1987 "expected at least one thunk-name event in perfetto trace; got: {contents}"
1988 );
1989 assert!(contents.trim_start().starts_with('['));
1991 let _ = fs::remove_file(&path);
1992 }
1993
1994 #[test]
1995 fn elementwise_region_fused_matches_unfused() {
1996 let f = DType::F32;
2001 let mut g = Graph::new("ew_e2e");
2002 let a = g.input("a", Shape::new(&[8], f));
2003 let b = g.input("b", Shape::new(&[8], f));
2004 let c = g.input("c", Shape::new(&[8], f));
2005 let s = Shape::new(&[8], f);
2006 let add = g.add(a, b);
2007 let mul = g.mul(add, c);
2008 let relu = g.relu(mul);
2009 let _ = s;
2010 g.set_outputs(vec![relu]);
2011
2012 let mut compiled = Session::new(Device::Cpu).compile(g);
2013 let av: Vec<f32> = vec![1.0, -2.0, 3.0, -4.0, 0.5, -0.5, 1.5, -1.5];
2014 let bv: Vec<f32> = vec![0.5, 1.0, 2.0, 4.0, 0.5, 0.5, 0.5, 0.5];
2015 let cv: Vec<f32> = vec![1.0, 2.0, 1.0, 1.0, 2.0, 3.0, 0.5, 4.0];
2016 let outs = compiled.run(&[("a", &av), ("b", &bv), ("c", &cv)]);
2017 let out = &outs[0];
2018
2019 let expected: Vec<f32> = (0..8)
2020 .map(|i| {
2021 let v = (av[i] + bv[i]) * cv[i];
2022 v.max(0.0)
2023 })
2024 .collect();
2025 for (i, (got, exp)) in out.iter().zip(&expected).enumerate() {
2026 assert!(
2027 (got - exp).abs() < 1e-6,
2028 "mismatch at {i}: got {got}, expected {exp}"
2029 );
2030 }
2031 }
2032
2033 #[test]
2034 #[ignore = "active-extent execution is a stub on CPU (rlx-cpu/src/thunk/exec_dispatch.rs::execute_thunks_active returns false unconditionally)"]
2035 fn active_extent_skips_compute_on_attention() {
2036 use rlx_ir::op::MaskKind;
2039 let f = DType::F32;
2040 let mut g = Graph::new("attn");
2041 let q = g.input("q", Shape::new(&[1, 4, 8], f));
2042 let k = g.input("k", Shape::new(&[1, 4, 8], f));
2043 let v = g.input("v", Shape::new(&[1, 4, 8], f));
2044 let out = g.attention_kind(q, k, v, 2, 4, MaskKind::None, Shape::new(&[1, 4, 8], f));
2045 g.set_outputs(vec![out]);
2046 let mut compiled = Session::new(Device::Cpu).compile(g);
2047
2048 let warm = compiled.run(&[
2050 ("q", &[1.0f32; 32]),
2051 ("k", &[1.0f32; 32]),
2052 ("v", &[1.0f32; 32]),
2053 ]);
2054 let warm_out = warm[0].clone();
2055 assert_eq!(warm_out.len(), 32);
2056
2057 compiled.set_active_extent(Some((2, 4)));
2061 let outs = compiled.run(&[
2062 ("q", &[3.0f32; 32]),
2063 ("k", &[3.0f32; 32]),
2064 ("v", &[3.0f32; 32]),
2065 ]);
2066 let out = &outs[0];
2067 assert_eq!(out.len(), 32);
2068 assert_eq!(
2069 &out[16..],
2070 &warm_out[16..],
2071 "tail (positions 2,3) must be untouched — proves Attention skipped"
2072 );
2073 assert_ne!(
2075 &out[..16],
2076 &warm_out[..16],
2077 "first 2 positions should reflect new input"
2078 );
2079 }
2080
2081 #[test]
2082 fn active_extent_falls_back_when_unsupported_thunk_in_schedule() {
2083 }
2098
2099 #[test]
2100 fn run_padded_uses_active_extent_on_cpu() {
2101 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
2104 let input: Vec<f32> = vec![
2105 1.0, -1.0, 2.0, -2.0, 3.0, -10.0, -20.0, -30.0, -40.0, -50.0, ];
2108 let (upper, outs) = cache
2114 .run_padded(
2115 5,
2116 5,
2117 |max| tiny_graph(max as usize),
2118 &[("x", &input[..5], 1)],
2119 &[1],
2120 )
2121 .unwrap();
2122 assert_eq!(upper, 15);
2123 assert_eq!(outs[0].len(), 5);
2124 assert_eq!(outs[0], vec![1.0, 0.0, 2.0, 0.0, 3.0]);
2130 }
2131
2132 #[test]
2133 fn run_padded_inner_zero_returns_output_unsliced() {
2134 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
2137 let input: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 3.0];
2138
2139 let (upper, outs) = cache
2140 .run_padded(
2141 5,
2142 5,
2143 |max| tiny_graph(max as usize),
2144 &[("x", &input, 1)],
2145 &[0], )
2147 .unwrap();
2148
2149 assert_eq!(upper, 15);
2150 assert_eq!(
2151 outs[0].len(),
2152 15,
2153 "unsliced output preserves full upper extent"
2154 );
2155 assert_eq!(&outs[0][..5], &[1.0, 0.0, 2.0, 0.0, 3.0]);
2157 assert!(outs[0][5..].iter().all(|&v| v == 0.0));
2158 }
2159
2160 #[test]
2161 fn dynamic_dim_cache_specializes_per_key() {
2162 use rlx_ir::DType;
2163 use rlx_ir::Shape;
2164 use rlx_ir::hir::HirModule;
2165 use rlx_ir::sym;
2166
2167 let mut cache = DynamicDimCompileCache::new(Device::Cpu, 4);
2168 let opts = crate::CompileOptions::new();
2169 {
2170 let _short = cache
2171 .get_or_specialize(
2172 8,
2173 &rlx_ir::DimBinding::batch_seq(1, 8),
2174 || {
2175 let mut hir = HirModule::new("dyn_cache");
2176 let x = hir.input_batch_seq("x", sym::BATCH, sym::SEQ, 4, DType::F32);
2177 let w = hir.param("w", Shape::new(&[4, 2], DType::F32));
2178 let y = hir.linear(
2179 x,
2180 w,
2181 None,
2182 None,
2183 Shape::batch_seq(sym::BATCH, sym::SEQ, 2, DType::F32),
2184 );
2185 hir.set_outputs(vec![y]);
2186 hir
2187 },
2188 &opts,
2189 )
2190 .expect("specialize short");
2191 }
2192 assert!(cache.has_template());
2193 assert_eq!(cache.len(), 1);
2194 cache
2195 .get_or_specialize(
2196 128,
2197 &rlx_ir::DimBinding::batch_seq(1, 128),
2198 || panic!("HIR builder must not run twice"),
2199 &opts,
2200 )
2201 .expect("specialize long");
2202 assert_eq!(cache.len(), 2);
2203 }
2204}