1use crate::{CompiledGraph, Device, Session};
40use rlx_ir::DimBinding;
41use rlx_ir::Graph;
42use rlx_ir::hir::HirModule;
43use rlx_opt::CompileResult;
44use std::collections::HashMap;
45use std::collections::VecDeque;
46use std::ops::Range;
47
48pub struct CacheRunInput<'a> {
50 pub name: &'a str,
51 pub data: &'a [f32],
52 pub row_inner: Option<usize>,
54}
55
56pub struct CompileCache {
57 device: Device,
58 capacity: usize,
59 policy: Option<rlx_opt::PrecisionPolicy>,
62 entries: Vec<(u64, CompiledGraph)>,
66 order: VecDeque<u64>,
68}
69
70impl CompileCache {
71 pub fn new(device: Device, capacity: usize) -> Self {
72 Self::with_policy(device, capacity, None)
73 }
74
75 pub fn with_policy(
79 device: Device,
80 capacity: usize,
81 policy: Option<rlx_opt::PrecisionPolicy>,
82 ) -> Self {
83 assert!(capacity > 0, "CompileCache capacity must be ≥ 1");
84 Self {
85 device,
86 capacity,
87 policy,
88 entries: Vec::with_capacity(capacity),
89 order: VecDeque::with_capacity(capacity),
90 }
91 }
92
93 pub fn get_or_compile<F: FnOnce() -> Graph>(
97 &mut self,
98 key: u64,
99 build: F,
100 ) -> &mut CompiledGraph {
101 self.get_or_compile_with_options(key, build, &crate::CompileOptions::new())
102 }
103
104 pub fn get_or_compile_with_options<F: FnOnce() -> Graph>(
106 &mut self,
107 key: u64,
108 build: F,
109 options: &crate::CompileOptions,
110 ) -> &mut CompiledGraph {
111 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
112 return &mut self.entries[idx].1;
113 }
114 let mut session = Session::new(self.device);
115 if let Some(p) = &self.policy {
116 session = session.with_policy(p.clone());
117 }
118 let compiled = session.compile_with(build(), options);
119
120 if self.entries.len() >= self.capacity
122 && let Some(evict_key) = self.order.pop_front()
123 {
124 sync_evicted_entry(&mut self.entries, evict_key);
125 self.entries.retain(|(k, _)| *k != evict_key);
126 }
127 self.entries.push((key, compiled));
128 self.order.push_back(key);
129 &mut self.entries.last_mut().unwrap().1
130 }
131
132 pub fn get_or_compile_hir_with_options<F: FnOnce() -> rlx_ir::hir::HirModule>(
134 &mut self,
135 key: u64,
136 build: F,
137 options: &crate::CompileOptions,
138 ) -> &mut CompiledGraph {
139 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
140 return &mut self.entries[idx].1;
141 }
142 let mut session = Session::new(self.device);
143 if let Some(p) = &self.policy {
144 session = session.with_policy(p.clone());
145 }
146 let compiled = session
147 .compile_hir_with(build(), options)
148 .expect("HIR lower/compile in compile cache");
149
150 if self.entries.len() >= self.capacity
151 && let Some(evict_key) = self.order.pop_front()
152 {
153 sync_evicted_entry(&mut self.entries, evict_key);
154 self.entries.retain(|(k, _)| *k != evict_key);
155 }
156 self.entries.push((key, compiled));
157 self.order.push_back(key);
158 &mut self.entries.last_mut().unwrap().1
159 }
160
161 pub fn len(&self) -> usize {
163 self.entries.len()
164 }
165 pub fn is_empty(&self) -> bool {
166 self.entries.is_empty()
167 }
168 pub fn contains(&self, key: u64) -> bool {
170 self.entries.iter().any(|(k, _)| *k == key)
171 }
172
173 pub fn clear(&mut self) {
175 self.sync_all();
176 self.entries.clear();
177 self.order.clear();
178 }
179
180 pub fn sync_all(&mut self) {
182 for (_, compiled) in &mut self.entries {
183 compiled.sync_pending();
184 }
185 }
186}
187
188fn sync_evicted_entry(entries: &mut [(u64, CompiledGraph)], evict_key: u64) {
189 if let Some((_, compiled)) = entries.iter_mut().find(|(k, _)| *k == evict_key) {
190 compiled.sync_pending();
191 }
192}
193
194pub struct BucketedCompileCache {
241 device: Device,
242 policy: Option<rlx_opt::PrecisionPolicy>,
243 buckets: Vec<Bucket>,
244 weight_donor_upper: Option<u64>,
249 clock: u64,
251}
252
253struct Bucket {
254 range: Range<u64>,
255 compiled: Option<CompiledGraph>,
256 resident_bytes: usize,
259 last_used: u64,
261}
262
263const LARGE_BUCKET_BYTES: usize = 256 * 1024 * 1024;
269
270fn max_resident_large_buckets() -> usize {
280 std::env::var("RLX_KV_CACHE_MAX_RESIDENT")
281 .ok()
282 .and_then(|s| s.parse::<usize>().ok())
283 .filter(|&n| n >= 1)
284 .unwrap_or(1)
285}
286
287fn device_has_discrete_vram(dev: Device) -> bool {
292 matches!(
293 dev,
294 Device::Gpu
295 | Device::Cuda
296 | Device::Vulkan
297 | Device::Rocm
298 | Device::OneApi
299 | Device::DirectX
300 | Device::WebGpu
301 )
302}
303
304impl BucketedCompileCache {
305 pub fn new(device: Device, buckets: Vec<Range<u64>>) -> Self {
306 Self::with_policy(device, buckets, None)
307 }
308
309 pub fn power_of_two_ladder(device: Device, min: u64, max: u64) -> Self {
325 Self::power_of_two_ladder_with_policy(device, min, max, None)
326 }
327
328 pub fn power_of_two_ladder_with_policy(
329 device: Device,
330 min: u64,
331 max: u64,
332 policy: Option<rlx_opt::PrecisionPolicy>,
333 ) -> Self {
334 assert!(min >= 1, "power_of_two_ladder: min must be ≥ 1, got {min}");
335 assert!(
336 max >= min,
337 "power_of_two_ladder: max ({max}) must be ≥ min ({min})"
338 );
339 let mut buckets: Vec<Range<u64>> = Vec::new();
340 let mut start = 1u64;
341 let mut extent = min.next_power_of_two();
342 loop {
343 buckets.push(start..(extent + 1));
344 if extent >= max {
345 break;
346 }
347 start = extent + 1;
348 extent = extent
349 .checked_mul(2)
350 .expect("power_of_two_ladder: extent overflow");
351 }
352 Self::with_policy(device, buckets, policy)
353 }
354
355 pub fn with_policy(
356 device: Device,
357 buckets: Vec<Range<u64>>,
358 policy: Option<rlx_opt::PrecisionPolicy>,
359 ) -> Self {
360 assert!(!buckets.is_empty(), "BucketedCompileCache needs ≥1 bucket");
361 for (i, b) in buckets.iter().enumerate() {
362 assert!(b.start < b.end, "bucket {i} ({b:?}) is empty");
363 if i + 1 < buckets.len() {
364 assert!(
365 b.end <= buckets[i + 1].start,
366 "buckets {i} ({b:?}) and {} ({:?}) overlap",
367 i + 1,
368 buckets[i + 1],
369 );
370 }
371 }
372 let buckets = buckets
373 .into_iter()
374 .map(|range| Bucket {
375 range,
376 compiled: None,
377 resident_bytes: 0,
378 last_used: 0,
379 })
380 .collect();
381 Self {
382 device,
383 policy,
384 buckets,
385 weight_donor_upper: None,
386 clock: 0,
387 }
388 }
389
390 fn touch(&mut self, idx: usize) {
392 self.clock += 1;
393 self.buckets[idx].last_used = self.clock;
394 }
395
396 fn evict_for_incoming(&mut self, incoming_idx: usize, incoming_bytes: usize) {
403 if std::env::var("RLX_KV_CACHE_NO_EVICT").is_ok() {
404 return;
405 }
406 let unified_opt_in = std::env::var("RLX_KV_CACHE_MAX_RESIDENT").is_ok();
413 if !device_has_discrete_vram(self.device) && !unified_opt_in {
414 return;
415 }
416 if std::env::var("RLX_KV_CACHE_DBG").is_ok() {
417 let (n_large, resident): (usize, usize) = self
418 .buckets
419 .iter()
420 .filter(|b| b.compiled.is_some())
421 .fold((0, 0), |(n, r), b| {
422 let is_large = b.resident_bytes >= LARGE_BUCKET_BYTES;
423 (n + is_large as usize, r + b.resident_bytes)
424 });
425 eprintln!(
426 "[KVCACHE] compiling bucket idx={incoming_idx} incoming={:.2}GB \
427 resident_large={n_large} resident_total={:.2}GB cap={}",
428 incoming_bytes as f64 / 1e9,
429 resident as f64 / 1e9,
430 max_resident_large_buckets(),
431 );
432 }
433 if incoming_bytes < LARGE_BUCKET_BYTES {
434 return;
435 }
436 let cap = max_resident_large_buckets();
437 loop {
438 let large: Vec<usize> = (0..self.buckets.len())
439 .filter(|&j| {
440 j != incoming_idx
441 && self.buckets[j].compiled.is_some()
442 && self.buckets[j].resident_bytes >= LARGE_BUCKET_BYTES
443 })
444 .collect();
445 if large.len() < cap {
447 break;
448 }
449 let Some(&victim) = large.iter().min_by_key(|&&j| self.buckets[j].last_used) else {
450 break;
451 };
452 if let Some(c) = &mut self.buckets[victim].compiled {
453 c.sync_pending();
454 }
455 let victim_upper = self.buckets[victim].range.end.saturating_sub(1);
456 self.buckets[victim].compiled = None;
457 self.buckets[victim].resident_bytes = 0;
458 if self.weight_donor_upper == Some(victim_upper) {
459 self.weight_donor_upper = None;
460 }
461 }
462 }
463
464 pub fn get_or_compile<F: FnOnce(u64) -> Graph>(
473 &mut self,
474 key: u64,
475 build: F,
476 ) -> Option<(u64, &mut CompiledGraph)> {
477 self.get_or_compile_with_options(key, build, &crate::CompileOptions::new())
478 }
479
480 pub fn get_or_compile_with_options<F: FnOnce(u64) -> Graph>(
482 &mut self,
483 key: u64,
484 build: F,
485 options: &crate::CompileOptions,
486 ) -> Option<(u64, &mut CompiledGraph)> {
487 let idx = self.bucket_for(key)?;
488 let upper = self.buckets[idx].range.end - 1;
489 if self.buckets[idx].compiled.is_none() {
490 let mut session = Session::new(self.device);
491 if let Some(p) = &self.policy {
492 session = session.with_policy(p.clone());
493 }
494 self.buckets[idx].compiled = Some(session.compile_with(build(upper), options));
495 }
496 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
497 }
498
499 pub fn get_or_compile_hir<F: FnOnce(u64) -> HirModule>(
502 &mut self,
503 key: u64,
504 build: F,
505 ) -> Option<(u64, &mut CompiledGraph)> {
506 self.get_or_compile_hir_with_options(key, build, &crate::CompileOptions::new())
507 }
508
509 pub fn get_or_compile_hir_with_options<F: FnOnce(u64) -> HirModule>(
511 &mut self,
512 key: u64,
513 build: F,
514 options: &crate::CompileOptions,
515 ) -> Option<(u64, &mut CompiledGraph)> {
516 let idx = self.bucket_for(key)?;
517 let upper = self.buckets[idx].range.end - 1;
518 if self.buckets[idx].compiled.is_none() {
519 let mut session = Session::new(self.device);
520 if let Some(p) = &self.policy {
521 session = session.with_policy(p.clone());
522 }
523 let compiled = session
524 .compile_hir_with(build(upper), options)
525 .expect("HIR lower/compile in bucketed cache");
526 self.buckets[idx].compiled = Some(compiled);
527 }
528 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
529 }
530
531 pub fn bucket_for(&self, key: u64) -> Option<usize> {
534 self.buckets.iter().position(|b| b.range.contains(&key))
535 }
536
537 pub fn bucket_upper_for_key(&self, key: u64) -> Option<u64> {
539 let idx = self.bucket_for(key)?;
540 Some(self.buckets[idx].range.end - 1)
541 }
542
543 pub fn buckets(&self) -> impl Iterator<Item = &Range<u64>> {
544 self.buckets.iter().map(|b| &b.range)
545 }
546
547 pub fn compiled_count(&self) -> usize {
549 self.buckets.iter().filter(|b| b.compiled.is_some()).count()
550 }
551
552 pub fn compiled_for_key_mut(&mut self, key: u64) -> Option<&mut CompiledGraph> {
554 let idx = self.bucket_for(key)?;
555 self.buckets[idx].compiled.as_mut()
556 }
557
558 pub fn compiled_for_upper(&self, upper: u64) -> Option<&CompiledGraph> {
560 self.buckets
561 .iter()
562 .find(|b| b.range.end.saturating_sub(1) == upper)
563 .and_then(|b| b.compiled.as_ref())
564 }
565
566 pub fn try_copy_params_between_uppers(&mut self, dst_upper: u64, src_upper: u64) -> bool {
568 if dst_upper == src_upper {
569 return true;
570 }
571 let dst_idx = self
572 .buckets
573 .iter()
574 .position(|b| b.range.end.saturating_sub(1) == dst_upper);
575 let src_idx = self
576 .buckets
577 .iter()
578 .position(|b| b.range.end.saturating_sub(1) == src_upper);
579 let (Some(dst_idx), Some(src_idx)) = (dst_idx, src_idx) else {
580 return false;
581 };
582 if dst_idx == src_idx {
583 return true;
584 }
585 let (dst, src) = if dst_idx < src_idx {
586 let (left, right) = self.buckets.split_at_mut(src_idx);
587 (&mut left[dst_idx], &right[0])
588 } else {
589 let (left, right) = self.buckets.split_at_mut(dst_idx);
590 (&mut right[0], &left[src_idx])
591 };
592 let Some(dst_c) = dst.compiled.as_mut() else {
593 return false;
594 };
595 let Some(src_c) = src.compiled.as_ref() else {
596 return false;
597 };
598 dst_c.copy_params_from(src_c)
599 }
600
601 pub fn weight_donor(&self) -> Option<&CompiledGraph> {
603 self.compiled_for_upper(self.weight_donor_upper?)
604 }
605
606 pub fn set_weight_donor(&mut self, upper: u64) {
610 if self.weight_donor_upper.is_none() {
611 self.weight_donor_upper = Some(upper);
612 }
613 }
614
615 pub fn try_share_params_from_donor(&mut self, dst_upper: u64) -> bool {
622 if std::env::var("RLX_METAL_NO_SHARE").is_ok() {
623 return false;
624 }
625 let mut candidates: Vec<u64> = Vec::new();
626 if let Some(src) = self.weight_donor_upper {
627 candidates.push(src);
628 }
629 for b in &self.buckets {
630 if b.compiled.is_none() {
631 continue;
632 }
633 let u = b.range.end.saturating_sub(1);
634 if u != dst_upper && !candidates.contains(&u) {
635 candidates.push(u);
636 }
637 }
638 for src_upper in candidates {
639 if self.try_share_params_between_uppers(dst_upper, src_upper) {
640 self.weight_donor_upper = Some(src_upper);
641 return true;
642 }
643 }
644 false
645 }
646
647 fn try_share_params_between_uppers(&mut self, dst_upper: u64, src_upper: u64) -> bool {
648 if dst_upper == src_upper {
649 return true;
650 }
651 let dst_idx = self
652 .buckets
653 .iter()
654 .position(|b| b.range.end.saturating_sub(1) == dst_upper);
655 let src_idx = self
656 .buckets
657 .iter()
658 .position(|b| b.range.end.saturating_sub(1) == src_upper);
659 let (Some(dst_idx), Some(src_idx)) = (dst_idx, src_idx) else {
660 return false;
661 };
662 if dst_idx == src_idx {
663 return true;
664 }
665 let (dst, src) = if dst_idx < src_idx {
666 let (left, right) = self.buckets.split_at_mut(src_idx);
667 (&mut left[dst_idx], &right[0])
668 } else {
669 let (left, right) = self.buckets.split_at_mut(dst_idx);
670 (&mut right[0], &left[src_idx])
671 };
672 let Some(dst_c) = dst.compiled.as_mut() else {
673 return false;
674 };
675 let Some(src_c) = src.compiled.as_ref() else {
676 return false;
677 };
678 dst_c.share_params_from(src_c)
679 }
680
681 pub fn seed_resident_kv_prefix_from_keys(
685 &mut self,
686 src_key: u64,
687 dst_key: u64,
688 prefix_tokens: usize,
689 outgoing_upper: usize,
690 kv_dim: usize,
691 n_layers: usize,
692 ) -> bool {
693 let Some(src_idx) = self.bucket_for(src_key) else {
694 return false;
695 };
696 let Some(dst_idx) = self.bucket_for(dst_key) else {
697 return false;
698 };
699 if src_idx == dst_idx {
700 return true;
701 }
702 if src_idx < dst_idx {
703 let (left, right) = self.buckets.split_at_mut(dst_idx);
704 let Some(src) = left[src_idx].compiled.as_ref() else {
705 return false;
706 };
707 let Some(dst) = right[0].compiled.as_mut() else {
708 return false;
709 };
710 return dst.seed_resident_kv_prefix_from(
711 src,
712 prefix_tokens,
713 outgoing_upper,
714 kv_dim,
715 n_layers,
716 );
717 }
718 let (left, right) = self.buckets.split_at_mut(src_idx);
719 let Some(src) = right[0].compiled.as_ref() else {
720 return false;
721 };
722 let Some(dst) = left[dst_idx].compiled.as_mut() else {
723 return false;
724 };
725 dst.seed_resident_kv_prefix_from(src, prefix_tokens, outgoing_upper, kv_dim, n_layers)
726 }
727
728 pub fn rebind_resident_kv_hybrid_from_keys(
732 &mut self,
733 src_key: u64,
734 dst_key: u64,
735 host_k: &[Vec<f32>],
736 host_v: &[Vec<f32>],
737 prefix_tokens: usize,
738 outgoing_upper: usize,
739 upper: usize,
740 kv_dim: usize,
741 n_layers: usize,
742 ) -> bool {
743 let Some(src_idx) = self.bucket_for(src_key) else {
744 return false;
745 };
746 let Some(dst_idx) = self.bucket_for(dst_key) else {
747 return false;
748 };
749 if src_idx == dst_idx {
750 return true;
751 }
752 let host_rows = host_k.first().map(|k| k.len() / kv_dim.max(1)).unwrap_or(0);
753 let rebind = |src: &CompiledGraph, dst: &mut CompiledGraph| -> bool {
754 for i in 0..n_layers {
755 let mut kp = vec![0f32; upper * kv_dim];
756 let mut vp = vec![0f32; upper * kv_dim];
757 let nk = host_k[i].len().min(kp.len());
758 let nv = host_v[i].len().min(vp.len());
759 kp[..nk].copy_from_slice(&host_k[i][..nk]);
760 vp[..nv].copy_from_slice(&host_v[i][..nv]);
761 let k_name = format!("past_k_{i}");
762 let v_name = format!("past_v_{i}");
763 if !dst.bind_gpu_handle(&k_name, &kp) || !dst.bind_gpu_handle(&v_name, &vp) {
764 return false;
765 }
766 dst.register_kv_row_feed(&k_name, 1 + 2 * i);
767 dst.register_kv_row_feed(&v_name, 2 + 2 * i);
768 }
769 dst.stage_bound_gpu_handles_to_arena();
770 if host_rows < prefix_tokens {
771 return dst.copy_resident_kv_rows_from(
772 src,
773 host_rows,
774 prefix_tokens,
775 outgoing_upper,
776 kv_dim,
777 n_layers,
778 );
779 }
780 for i in 0..n_layers {
781 let k_name = format!("past_k_{i}");
782 let v_name = format!("past_v_{i}");
783 dst.prepare_resident_gpu_handle(&k_name);
784 dst.prepare_resident_gpu_handle(&v_name);
785 }
786 true
787 };
788 if src_idx < dst_idx {
789 let (left, right) = self.buckets.split_at_mut(dst_idx);
790 let Some(src) = left[src_idx].compiled.as_ref() else {
791 return false;
792 };
793 let Some(dst) = right[0].compiled.as_mut() else {
794 return false;
795 };
796 return rebind(src, dst);
797 }
798 let (left, right) = self.buckets.split_at_mut(src_idx);
799 let Some(src) = right[0].compiled.as_ref() else {
800 return false;
801 };
802 let Some(dst) = left[dst_idx].compiled.as_mut() else {
803 return false;
804 };
805 rebind(src, dst)
806 }
807
808 pub fn total_buckets(&self) -> usize {
809 self.buckets.len()
810 }
811
812 pub fn evict_except(&mut self, keep: usize) {
814 for (i, bucket) in self.buckets.iter_mut().enumerate() {
815 if i != keep {
816 bucket.compiled = None;
817 }
818 }
819 }
820
821 pub fn evict_one_except(&mut self, protect: usize) -> Option<usize> {
835 let victim = self
836 .buckets
837 .iter()
838 .enumerate()
839 .rev()
840 .find(|(i, b)| *i != protect && b.compiled.is_some())
841 .map(|(i, _)| i)?;
842 if let Some(compiled) = self.buckets[victim].compiled.as_mut() {
843 compiled.sync_pending();
844 }
845 self.buckets[victim].compiled = None;
846 Some(victim)
847 }
848
849 pub fn clear_compiled(&mut self) {
851 for bucket in &mut self.buckets {
852 bucket.compiled = None;
853 }
854 }
855
856 pub fn run_padded<F: FnOnce(u64) -> Graph>(
882 &mut self,
883 key: u64,
884 actual_rows: usize,
885 build: F,
886 inputs: &[(&str, &[f32], usize)],
887 output_inners: &[usize],
888 ) -> Option<(u64, Vec<Vec<f32>>)> {
889 let (upper, compiled) = self.get_or_compile(key, build)?;
890
891 let padded: Vec<(&str, Vec<f32>)> = inputs
893 .iter()
894 .map(|(name, data, inner)| (*name, pad_rows(data, *inner, upper)))
895 .collect();
896 let pairs: Vec<(&str, &[f32])> = padded.iter().map(|(n, d)| (*n, d.as_slice())).collect();
897
898 compiled.set_active_extent(Some((actual_rows, upper as usize)));
904 let raw_outputs = compiled.run(&pairs);
905 compiled.set_active_extent(None);
906 #[cfg(feature = "cpu")]
907 crate::onnx_active::set_active_token_count(None);
908
909 let outs = raw_outputs
910 .into_iter()
911 .enumerate()
912 .map(|(i, out)| match output_inners.get(i).copied() {
913 Some(0) | None => out,
914 Some(inner) => slice_rows(&out, inner, actual_rows),
915 })
916 .collect();
917
918 Some((upper, outs))
919 }
920
921 pub fn ensure_graph_with_params<F>(
923 &mut self,
924 key: u64,
925 build: F,
926 options: &crate::CompileOptions,
927 ) -> Option<(u64, &mut CompiledGraph)>
928 where
929 F: FnOnce(u64) -> (Graph, HashMap<String, Vec<f32>>),
930 {
931 let idx = self.bucket_for(key)?;
932 let upper = self.buckets[idx].range.end - 1;
933 if self.buckets[idx].compiled.is_none() {
934 let (graph, params) = build(upper);
935 let bytes: usize = params.values().map(|v| v.len() * 4).sum();
936 self.evict_for_incoming(idx, bytes);
937 let mut session = Session::new(self.device);
938 if let Some(p) = &self.policy {
939 session = session.with_policy(p.clone());
940 }
941 let mut compiled = session.compile_with(graph, options);
942 for (name, data) in params {
943 compiled.set_param(&name, &data);
944 }
945 self.buckets[idx].compiled = Some(compiled);
946 self.buckets[idx].resident_bytes = bytes;
947 }
948 self.touch(idx);
949 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
950 }
951
952 pub fn ensure_hir_with_params<F>(
954 &mut self,
955 key: u64,
956 build: F,
957 options: &crate::CompileOptions,
958 ) -> Option<(u64, &mut CompiledGraph)>
959 where
960 F: FnOnce(u64) -> (HirModule, HashMap<String, Vec<f32>>),
961 {
962 let idx = self.bucket_for(key)?;
963 let upper = self.buckets[idx].range.end - 1;
964 if self.buckets[idx].compiled.is_none() {
965 let (hir, params) = build(upper);
966 let bytes: usize = params.values().map(|v| v.len() * 4).sum();
967 let mut session = Session::new(self.device);
972 if let Some(p) = &self.policy {
973 session = session.with_policy(p.clone());
974 }
975 let mut compiled = session
976 .compile_hir_with(hir, options)
977 .expect("HIR lower/compile in ensure_hir_with_params");
978 for (name, data) in params {
979 compiled.set_param(&name, &data);
980 }
981 self.buckets[idx].compiled = Some(compiled);
982 self.buckets[idx].resident_bytes = bytes;
983 }
984 self.touch(idx);
985 Some((upper, self.buckets[idx].compiled.as_mut().unwrap()))
986 }
987
988 pub fn note_resident_bytes(&mut self, key: u64, extra_bytes: usize) {
994 if extra_bytes == 0 {
995 return;
996 }
997 let Some(idx) = self.bucket_for(key) else {
998 return;
999 };
1000 let before = self.buckets[idx].resident_bytes;
1001 let after = before.saturating_add(extra_bytes);
1002 if after >= LARGE_BUCKET_BYTES {
1004 self.evict_for_incoming(idx, after);
1005 }
1006 self.buckets[idx].resident_bytes = after;
1007 self.touch(idx);
1008 }
1009
1010 pub fn run_padded_mixed<F>(
1012 &mut self,
1013 key: u64,
1014 actual_rows: usize,
1015 build: F,
1016 inputs: &[CacheRunInput<'_>],
1017 output_inners: &[usize],
1018 ) -> Option<(u64, Vec<Vec<f32>>)>
1019 where
1020 F: FnOnce(u64) -> Graph,
1021 {
1022 let (upper, compiled) = self.get_or_compile(key, build)?;
1023
1024 let padded: Vec<(&str, Vec<f32>)> = inputs
1025 .iter()
1026 .map(|inp| match inp.row_inner {
1027 Some(inner) => (inp.name, pad_rows(inp.data, inner, upper)),
1028 None => (inp.name, inp.data.to_vec()),
1029 })
1030 .collect();
1031 let pairs: Vec<(&str, &[f32])> = padded.iter().map(|(n, d)| (*n, d.as_slice())).collect();
1032
1033 compiled.set_active_extent(Some((actual_rows, upper as usize)));
1034 let raw_outputs = compiled.run(&pairs);
1035 compiled.set_active_extent(None);
1036 #[cfg(feature = "cpu")]
1037 crate::onnx_active::set_active_token_count(None);
1038
1039 let outs = raw_outputs
1040 .into_iter()
1041 .enumerate()
1042 .map(|(i, out)| match output_inners.get(i).copied() {
1043 Some(0) | None => out,
1044 Some(inner) => slice_rows(&out, inner, actual_rows),
1045 })
1046 .collect();
1047
1048 Some((upper, outs))
1049 }
1050
1051 pub fn sync_all(&mut self) {
1053 for bucket in &mut self.buckets {
1054 if let Some(compiled) = &mut bucket.compiled {
1055 compiled.sync_pending();
1056 }
1057 }
1058 }
1059}
1060
1061pub struct DynamicDimCompileCache {
1069 device: Device,
1070 policy: Option<rlx_opt::PrecisionPolicy>,
1071 capacity: usize,
1072 template: Option<CompileResult>,
1073 entries: Vec<(u64, CompiledGraph)>,
1074 order: VecDeque<u64>,
1075}
1076
1077impl DynamicDimCompileCache {
1078 pub fn new(device: Device, capacity: usize) -> Self {
1079 Self::with_policy(device, capacity, None)
1080 }
1081
1082 pub fn with_policy(
1083 device: Device,
1084 capacity: usize,
1085 policy: Option<rlx_opt::PrecisionPolicy>,
1086 ) -> Self {
1087 assert!(capacity > 0, "DynamicDimCompileCache capacity must be ≥ 1");
1088 Self {
1089 device,
1090 policy,
1091 capacity,
1092 template: None,
1093 entries: Vec::with_capacity(capacity),
1094 order: VecDeque::with_capacity(capacity),
1095 }
1096 }
1097
1098 pub fn compile_device(&self) -> Device {
1099 self.device
1100 }
1101
1102 pub fn get_or_specialize<F: FnOnce() -> HirModule>(
1105 &mut self,
1106 key: u64,
1107 binding: &DimBinding,
1108 build_hir: F,
1109 options: &crate::CompileOptions,
1110 ) -> Result<&mut CompiledGraph, rlx_ir::hir::LowerError> {
1111 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
1112 return Ok(&mut self.entries[idx].1);
1113 }
1114 if self.template.is_none() {
1115 let mut template_opts = options.clone();
1116 template_opts.dim_binding = None;
1117 let pipe = crate::stages::pipeline_for(self.device, &template_opts);
1118 self.template = Some(pipe.compile_hir(build_hir())?);
1119 }
1120 let template = self.template.as_ref().expect("template just set");
1121 let mut spec_opts = options.clone();
1122 spec_opts.dim_binding = None;
1123 let pipe = crate::stages::pipeline_for(self.device, &spec_opts);
1124 let specialized = template.specialize(&pipe, binding);
1125 let backend = crate::registry::backend_for(self.device).expect("backend registered");
1126 let mut compile_opts = options.clone();
1127 compile_opts.dim_binding = None;
1128 if compile_opts.policy.is_none() {
1129 if let Some(p) = &self.policy {
1130 compile_opts = compile_opts.policy(p.clone());
1131 }
1132 }
1133 let executable = backend.compile_lir(specialized.lir, &compile_opts);
1134 let compiled = CompiledGraph::new(executable, self.device);
1135
1136 if self.entries.len() >= self.capacity
1137 && let Some(evict_key) = self.order.pop_front()
1138 {
1139 sync_evicted_entry(&mut self.entries, evict_key);
1140 self.entries.retain(|(k, _)| *k != evict_key);
1141 }
1142 self.entries.push((key, compiled));
1143 self.order.push_back(key);
1144 Ok(&mut self.entries.last_mut().unwrap().1)
1145 }
1146
1147 pub fn len(&self) -> usize {
1148 self.entries.len()
1149 }
1150
1151 pub fn is_empty(&self) -> bool {
1152 self.entries.is_empty()
1153 }
1154
1155 pub fn contains(&self, key: u64) -> bool {
1156 self.entries.iter().any(|(k, _)| *k == key)
1157 }
1158
1159 pub fn clear(&mut self) {
1160 self.sync_all();
1161 self.template = None;
1162 self.entries.clear();
1163 self.order.clear();
1164 }
1165
1166 pub fn has_template(&self) -> bool {
1167 self.template.is_some()
1168 }
1169
1170 pub fn sync_all(&mut self) {
1172 for (_, compiled) in &mut self.entries {
1173 compiled.sync_pending();
1174 }
1175 }
1176
1177 pub fn ensure_template<F: FnOnce() -> HirModule>(
1179 &mut self,
1180 build_hir: F,
1181 options: &crate::CompileOptions,
1182 ) -> Result<&CompileResult, rlx_ir::hir::LowerError> {
1183 if self.template.is_none() {
1184 let mut opts = options.clone();
1185 opts.dim_binding = None;
1186 let pipe = crate::stages::pipeline_for(self.device, &opts);
1187 self.template = Some(pipe.compile_hir(build_hir())?);
1188 }
1189 Ok(self.template.as_ref().expect("template set"))
1190 }
1191
1192 pub fn template_result(&self) -> Option<&CompileResult> {
1193 self.template.as_ref()
1194 }
1195
1196 pub fn get_or_specialize_aot<F: FnOnce() -> HirModule>(
1199 &mut self,
1200 aot: &crate::AotCache,
1201 disk_base: &str,
1202 key: u64,
1203 binding: &rlx_ir::DimBinding,
1204 build_hir: F,
1205 options: &crate::CompileOptions,
1206 ) -> Result<&mut CompiledGraph, crate::AotCacheError> {
1207 if let Some(idx) = self.entries.iter().position(|(k, _)| *k == key) {
1208 return Ok(&mut self.entries[idx].1);
1209 }
1210 let device = self.device;
1211 let template = self.ensure_template(build_hir, options)?;
1212 let compiled = aot.specialize_cached(disk_base, binding, device, template, options)?;
1213 if self.entries.len() >= self.capacity
1214 && let Some(evict_key) = self.order.pop_front()
1215 {
1216 sync_evicted_entry(&mut self.entries, evict_key);
1217 self.entries.retain(|(k, _)| *k != evict_key);
1218 }
1219 self.entries.push((key, compiled));
1220 self.order.push_back(key);
1221 Ok(&mut self.entries.last_mut().unwrap().1)
1222 }
1223}
1224
1225pub fn pad_rows(data: &[f32], inner: usize, upper: u64) -> Vec<f32> {
1233 assert!(inner > 0, "pad_rows: inner stride must be ≥ 1");
1234 assert_eq!(
1235 data.len() % inner,
1236 0,
1237 "pad_rows: data len {} not a multiple of inner {inner}",
1238 data.len(),
1239 );
1240 let upper = upper as usize;
1241 let actual = data.len() / inner;
1242 assert!(
1243 actual <= upper,
1244 "pad_rows: actual rows {actual} exceed upper bound {upper}",
1245 );
1246 let mut out = vec![0.0_f32; upper * inner];
1247 out[..actual * inner].copy_from_slice(data);
1248 out
1249}
1250
1251pub fn pad_rows_into(out: &mut [f32], data: &[f32], inner: usize) {
1253 assert!(inner > 0, "pad_rows_into: inner stride must be ≥ 1");
1254 assert_eq!(
1255 data.len() % inner,
1256 0,
1257 "pad_rows_into: data len {} not a multiple of inner {inner}",
1258 data.len(),
1259 );
1260 assert_eq!(
1261 out.len() % inner,
1262 0,
1263 "pad_rows_into: out len {} not a multiple of inner {inner}",
1264 out.len(),
1265 );
1266 let upper = out.len() / inner;
1267 let actual = data.len() / inner;
1268 assert!(
1269 actual <= upper,
1270 "pad_rows_into: actual rows {actual} exceed upper bound {upper}",
1271 );
1272 out.fill(0.0);
1273 out[..data.len()].copy_from_slice(data);
1274}
1275
1276pub fn slice_rows(data: &[f32], inner: usize, actual: usize) -> Vec<f32> {
1282 assert!(inner > 0, "slice_rows: inner stride must be ≥ 1");
1283 assert_eq!(
1284 data.len() % inner,
1285 0,
1286 "slice_rows: data len {} not a multiple of inner {inner}",
1287 data.len(),
1288 );
1289 let upper = data.len() / inner;
1290 assert!(
1291 actual <= upper,
1292 "slice_rows: actual rows {actual} exceed upper {upper}",
1293 );
1294 data[..actual * inner].to_vec()
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299 use super::*;
1300 use rlx_ir::infer::GraphExt;
1301 use rlx_ir::*;
1302 use std::cell::Cell;
1303
1304 fn tiny_graph(n: usize) -> Graph {
1305 let mut g = Graph::new("t");
1306 let f = DType::F32;
1307 let x = g.input("x", Shape::new(&[n], f));
1308 let y = g.activation(rlx_ir::op::Activation::Relu, x, Shape::new(&[n], f));
1309 g.set_outputs(vec![y]);
1310 g
1311 }
1312
1313 #[test]
1314 fn cache_hits_avoid_recompile() {
1315 let mut cache = CompileCache::new(Device::Cpu, 4);
1316 let calls = Cell::new(0);
1317
1318 let _ = cache.get_or_compile(1, || {
1319 calls.set(calls.get() + 1);
1320 tiny_graph(8)
1321 });
1322 let _ = cache.get_or_compile(1, || {
1323 calls.set(calls.get() + 1);
1324 tiny_graph(8)
1325 });
1326 let _ = cache.get_or_compile(1, || {
1327 calls.set(calls.get() + 1);
1328 tiny_graph(8)
1329 });
1330 assert_eq!(calls.get(), 1);
1332 assert_eq!(cache.len(), 1);
1333 }
1334
1335 #[test]
1336 fn fifo_evicts_oldest_at_capacity() {
1337 let mut cache = CompileCache::new(Device::Cpu, 2);
1338 let _ = cache.get_or_compile(1, || tiny_graph(4));
1339 let _ = cache.get_or_compile(2, || tiny_graph(8));
1340 assert!(cache.contains(1) && cache.contains(2));
1341 let _ = cache.get_or_compile(3, || tiny_graph(16));
1343 assert!(!cache.contains(1));
1344 assert!(cache.contains(2) && cache.contains(3));
1345 }
1346
1347 #[test]
1348 fn different_keys_keep_separate_compiles() {
1349 let mut cache = CompileCache::new(Device::Cpu, 4);
1350 let calls = Cell::new(0);
1351 let _ = cache.get_or_compile(1, || {
1352 calls.set(calls.get() + 1);
1353 tiny_graph(8)
1354 });
1355 let _ = cache.get_or_compile(2, || {
1356 calls.set(calls.get() + 1);
1357 tiny_graph(16)
1358 });
1359 let _ = cache.get_or_compile(1, || {
1360 calls.set(calls.get() + 1);
1361 tiny_graph(8)
1362 });
1363 assert_eq!(calls.get(), 2);
1365 assert_eq!(cache.len(), 2);
1366 }
1367
1368 #[test]
1371 fn bucket_amortizes_keys_within_range() {
1372 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16]);
1373 let calls = Cell::new(0);
1374 let uppers = Cell::new((0u64, 0u64));
1375
1376 let (u1, _) = cache
1378 .get_or_compile(2, |upper| {
1379 calls.set(calls.get() + 1);
1380 uppers.set((upper, uppers.get().1));
1381 tiny_graph(upper as usize)
1382 })
1383 .expect("key 2 in range");
1384 let (u2, _) = cache
1385 .get_or_compile(3, |upper| {
1386 calls.set(calls.get() + 1);
1387 uppers.set((uppers.get().0, upper));
1388 tiny_graph(upper as usize)
1389 })
1390 .expect("key 3 in range");
1391
1392 assert_eq!(calls.get(), 1);
1394 assert_eq!(u1, 3);
1395 assert_eq!(u2, 3);
1396 assert_eq!(uppers.get().0, 3);
1397 assert_eq!(cache.compiled_count(), 1);
1398 assert_eq!(cache.total_buckets(), 2);
1399 }
1400
1401 #[test]
1402 fn bucket_lookup_returns_none_outside_range() {
1403 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16]);
1404 assert!(cache.bucket_for(0).is_none());
1405 assert!(cache.bucket_for(16).is_none());
1406 assert!(cache.bucket_for(100).is_none());
1407 assert_eq!(cache.bucket_for(3), Some(0));
1408 assert_eq!(cache.bucket_for(4), Some(1));
1409 assert_eq!(cache.bucket_upper_for_key(3), Some(3));
1410 assert_eq!(cache.bucket_upper_for_key(4), Some(15));
1411 assert!(cache.bucket_upper_for_key(0).is_none());
1412
1413 let calls = Cell::new(0);
1414 let result = cache.get_or_compile(100, |u| {
1415 calls.set(calls.get() + 1);
1416 tiny_graph(u as usize)
1417 });
1418 assert!(result.is_none());
1419 assert_eq!(calls.get(), 0); assert_eq!(cache.compiled_count(), 0);
1421 }
1422
1423 #[test]
1424 fn bucket_compiles_lazily_per_bucket() {
1425 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..4, 4..16, 16..64]);
1426 let calls = Cell::new(0);
1427
1428 let _ = cache.get_or_compile(2, |u| {
1429 calls.set(calls.get() + 1);
1430 tiny_graph(u as usize)
1431 });
1432 let _ = cache.get_or_compile(8, |u| {
1433 calls.set(calls.get() + 1);
1434 tiny_graph(u as usize)
1435 });
1436 assert_eq!(calls.get(), 2);
1438 assert_eq!(cache.compiled_count(), 2);
1439 assert_eq!(cache.total_buckets(), 3);
1440 }
1441
1442 #[test]
1443 #[should_panic(expected = "overlap")]
1444 fn bucket_overlap_rejected() {
1445 let _ = BucketedCompileCache::new(Device::Cpu, vec![1..8, 4..16]);
1446 }
1447
1448 #[test]
1449 #[should_panic(expected = "≥1 bucket")]
1450 fn empty_bucket_list_rejected() {
1451 let _ = BucketedCompileCache::new(Device::Cpu, vec![]);
1452 }
1453
1454 #[test]
1457 fn pad_rows_appends_zeros() {
1458 let p = pad_rows(&[1.0, 2.0, 3.0], 1, 5);
1460 assert_eq!(p, vec![1.0, 2.0, 3.0, 0.0, 0.0]);
1461
1462 let p = pad_rows(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 4);
1464 assert_eq!(
1465 p,
1466 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],
1467 );
1468
1469 let p = pad_rows(&[7.0, 8.0], 1, 2);
1471 assert_eq!(p, vec![7.0, 8.0]);
1472 }
1473
1474 #[test]
1475 fn slice_rows_truncates_trailing() {
1476 let s = slice_rows(&[1.0, 2.0, 3.0, 0.0, 0.0], 1, 3);
1477 assert_eq!(s, vec![1.0, 2.0, 3.0]);
1478
1479 let s = slice_rows(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 0.0], 3, 2);
1480 assert_eq!(s, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1481 }
1482
1483 #[test]
1484 #[should_panic(expected = "exceed upper")]
1485 fn pad_rows_rejects_too_long_input() {
1486 let _ = pad_rows(&[1.0, 2.0, 3.0, 4.0], 1, 3);
1487 }
1488
1489 #[test]
1490 #[should_panic(expected = "exceed upper")]
1491 fn slice_rows_rejects_too_large_actual() {
1492 let _ = slice_rows(&[1.0, 2.0, 3.0], 1, 5);
1493 }
1494
1495 #[test]
1498 fn run_padded_pads_input_and_slices_output() {
1499 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1502 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];
1503
1504 let (upper, outs) = cache
1505 .run_padded(
1506 10, 10, |max| tiny_graph(max as usize),
1509 &[("x", &input, 1)], &[1], )
1512 .expect("key 10 in [1..16)");
1513
1514 assert_eq!(upper, 15);
1515 assert_eq!(outs.len(), 1);
1516 let out = &outs[0];
1517 assert_eq!(out.len(), 10, "output sliced back to actual_rows");
1518 let expected: Vec<f32> = input.iter().map(|x| x.max(0.0)).collect();
1519 assert_eq!(out, &expected);
1520 }
1521
1522 #[test]
1523 fn run_padded_reuses_bucket_across_actuals() {
1524 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1526 let calls = Cell::new(0);
1527
1528 let (u1, o1) = cache
1529 .run_padded(
1530 10,
1531 10,
1532 |max| {
1533 calls.set(calls.get() + 1);
1534 tiny_graph(max as usize)
1535 },
1536 &[(
1537 "x",
1538 &[1.0, -1.0, 2.0, -2.0, 3.0, -3.0, 4.0, -4.0, 5.0, -5.0],
1539 1,
1540 )],
1541 &[1],
1542 )
1543 .unwrap();
1544 assert_eq!(o1.len(), 1);
1545 assert_eq!(o1[0].len(), 10);
1546 assert_eq!(u1, 15);
1547
1548 let (u2, o2) = cache
1549 .run_padded(
1550 5,
1551 5,
1552 |max| {
1553 calls.set(calls.get() + 1);
1554 tiny_graph(max as usize)
1555 },
1556 &[("x", &[-1.0, 2.0, -3.0, 4.0, -5.0], 1)],
1557 &[1],
1558 )
1559 .unwrap();
1560 assert_eq!(o2.len(), 1);
1561 assert_eq!(o2[0].len(), 5);
1562 assert_eq!(u2, 15);
1563 assert_eq!(o2[0], vec![0.0, 2.0, 0.0, 4.0, 0.0]);
1564
1565 assert_eq!(calls.get(), 1, "bucket cached across actuals");
1566 assert_eq!(cache.compiled_count(), 1);
1567 }
1568
1569 #[test]
1570 fn run_padded_returns_none_out_of_range() {
1571 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1572 let calls = Cell::new(0);
1573 let result = cache.run_padded(
1574 100,
1575 5,
1576 |u| {
1577 calls.set(calls.get() + 1);
1578 tiny_graph(u as usize)
1579 },
1580 &[("x", &[1.0, 2.0, 3.0, 4.0, 5.0], 1)],
1581 &[1],
1582 );
1583 assert!(result.is_none());
1584 assert_eq!(calls.get(), 0);
1585 assert_eq!(cache.compiled_count(), 0);
1586 }
1587
1588 #[test]
1591 fn power_of_two_ladder_generates_log_buckets() {
1592 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 64);
1593 let ranges: Vec<_> = cache.buckets().cloned().collect();
1595 assert_eq!(ranges, vec![1..9, 9..17, 17..33, 33..65]);
1596 assert_eq!(cache.total_buckets(), 4);
1597 }
1598
1599 #[test]
1600 fn power_of_two_ladder_picks_smallest_extent_for_actual() {
1601 let mut cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 64);
1604 let captured_uppers: std::cell::RefCell<Vec<u64>> = Default::default();
1605
1606 let (u17, _) = cache
1607 .get_or_compile(17, |upper| {
1608 captured_uppers.borrow_mut().push(upper);
1609 tiny_graph(upper as usize)
1610 })
1611 .unwrap();
1612 let (u9, _) = cache
1613 .get_or_compile(9, |upper| {
1614 captured_uppers.borrow_mut().push(upper);
1615 tiny_graph(upper as usize)
1616 })
1617 .unwrap();
1618 let (u3, _) = cache
1619 .get_or_compile(3, |upper| {
1620 captured_uppers.borrow_mut().push(upper);
1621 tiny_graph(upper as usize)
1622 })
1623 .unwrap();
1624 let (u64_, _) = cache
1625 .get_or_compile(64, |upper| {
1626 captured_uppers.borrow_mut().push(upper);
1627 tiny_graph(upper as usize)
1628 })
1629 .unwrap();
1630
1631 assert_eq!(u17, 32, "key=17 → smallest extent ≥ 17 is 32");
1632 assert_eq!(u9, 16, "key=9 → smallest extent ≥ 9 is 16");
1633 assert_eq!(u3, 8, "key=3 → smallest extent ≥ 3 is 8");
1634 assert_eq!(u64_, 64, "key=64 → exact match at 64");
1635 assert_eq!(*captured_uppers.borrow(), vec![32, 16, 8, 64]);
1636 assert_eq!(cache.compiled_count(), 4);
1637 }
1638
1639 #[test]
1640 fn power_of_two_ladder_min_above_one_starts_at_one() {
1641 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 16, 32);
1644 let ranges: Vec<_> = cache.buckets().cloned().collect();
1645 assert_eq!(ranges, vec![1..17, 17..33]);
1647 }
1648
1649 #[test]
1650 fn power_of_two_ladder_non_pow2_min_rounds_up() {
1651 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 10, 64);
1653 let ranges: Vec<_> = cache.buckets().cloned().collect();
1654 assert_eq!(ranges, vec![1..17, 17..33, 33..65]);
1655 }
1656
1657 #[test]
1658 fn power_of_two_ladder_max_below_pow2_extends_up() {
1659 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 8, 20);
1661 let ranges: Vec<_> = cache.buckets().cloned().collect();
1662 assert_eq!(ranges, vec![1..9, 9..17, 17..33]);
1663 }
1664
1665 #[test]
1666 fn power_of_two_ladder_min_equals_max() {
1667 let cache = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 16, 16);
1668 let ranges: Vec<_> = cache.buckets().cloned().collect();
1669 assert_eq!(ranges, vec![1..17]);
1670 }
1671
1672 #[test]
1673 #[should_panic(expected = "min must be ≥ 1")]
1674 fn power_of_two_ladder_zero_min_rejected() {
1675 let _ = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 0, 16);
1676 }
1677
1678 #[test]
1679 #[should_panic(expected = "max")]
1680 fn power_of_two_ladder_max_below_min_rejected() {
1681 let _ = BucketedCompileCache::power_of_two_ladder(Device::Cpu, 32, 8);
1682 }
1683
1684 #[test]
1697 #[ignore = "active-extent execution is a stub on CPU (thunk.rs::execute_thunks_active)"]
1698 fn active_extent_skips_compute_on_cpu_activation() {
1699 let graph = tiny_graph(15);
1710 let mut compiled = Session::new(Device::Cpu).compile(graph);
1711
1712 let warm_input: Vec<f32> = vec![1.0; 15];
1714 let warm_outs = compiled.run(&[("x", &warm_input)]);
1715 assert_eq!(warm_outs[0], vec![1.0; 15], "warm-up sanity");
1716
1717 let neg_input: Vec<f32> = vec![-1.0; 15];
1720 compiled.set_active_extent(Some((5, 15)));
1721 let outs = compiled.run(&[("x", &neg_input)]);
1722 let out = &outs[0];
1723
1724 assert_eq!(out.len(), 15);
1725 assert_eq!(
1726 out[..5],
1727 [0.0; 5],
1728 "first 5 elements processed (relu of -1)"
1729 );
1730 assert_eq!(
1731 out[5..],
1732 [1.0; 10],
1733 "tail untouched — proves Copy + Activation skipped indices 5..15"
1734 );
1735
1736 compiled.set_active_extent(None);
1739 let outs = compiled.run(&[("x", &neg_input)]);
1740 assert_eq!(
1741 outs[0],
1742 vec![0.0; 15],
1743 "full-extent path must clip every negative"
1744 );
1745 }
1746
1747 #[test]
1748 #[ignore = "active-extent execution is a stub on CPU (thunk.rs::execute_thunks_active)"]
1749 fn active_extent_skips_compute_on_binary_full() {
1750 let mut g = Graph::new("add");
1754 let f = DType::F32;
1755 let a = g.input("a", Shape::new(&[4], f));
1756 let b = g.input("b", Shape::new(&[4], f));
1757 let c = g.add(a, b);
1758 g.set_outputs(vec![c]);
1759 let mut compiled = Session::new(Device::Cpu).compile(g);
1760
1761 let warm = compiled.run(&[("a", &[1.0f32; 4]), ("b", &[1.0f32; 4])]);
1763 assert_eq!(warm[0], vec![2.0; 4]);
1764
1765 compiled.set_active_extent(Some((2, 4)));
1768 let outs = compiled.run(&[("a", &[10.0f32; 4]), ("b", &[10.0f32; 4])]);
1769 let out = &outs[0];
1770 assert_eq!(out[..2], [20.0, 20.0], "first 2 = active sum");
1771 assert_eq!(
1772 out[2..],
1773 [2.0, 2.0],
1774 "tail untouched — proves BinaryFull skipped indices 2..4"
1775 );
1776
1777 compiled.set_active_extent(None);
1779 let outs = compiled.run(&[("a", &[10.0f32; 4]), ("b", &[10.0f32; 4])]);
1780 assert_eq!(outs[0], vec![20.0; 4]);
1781 }
1782
1783 #[test]
1784 #[ignore = "process-wide STATE; runs only in isolation via `cargo test perfetto -- --ignored`"]
1785 fn perfetto_trace_emits_per_thunk_events() {
1786 use std::env;
1793 use std::fs;
1794 let path = env::temp_dir().join(format!("rlx-perfetto-e2e-{}.json", std::process::id()));
1795 if path.exists() {
1796 let _ = fs::remove_file(&path);
1797 }
1798 unsafe {
1799 env::set_var("RLX_TRACE_PERFETTO", &path);
1800 }
1801
1802 let f = DType::F32;
1804 let mut g = Graph::new("perf");
1805 let a = g.input("a", Shape::new(&[4], f));
1806 let b = g.input("b", Shape::new(&[4], f));
1807 let s = g.add(a, b);
1808 let r = g.relu(s);
1809 g.set_outputs(vec![r]);
1810 let mut compiled = Session::new(Device::Cpu).compile(g);
1811 let _ = compiled.run(&[("a", &[1.0; 4]), ("b", &[1.0; 4])]);
1812
1813 crate::perfetto::flush_and_finalize();
1815
1816 let contents = fs::read_to_string(&path).expect("trace file");
1817 assert!(
1819 contents.contains("\"binary\"")
1820 || contents.contains("\"activation\"")
1821 || contents.contains("\"elementwise_region\""),
1822 "expected at least one thunk-name event in perfetto trace; got: {contents}"
1823 );
1824 assert!(contents.trim_start().starts_with('['));
1826 let _ = fs::remove_file(&path);
1827 }
1828
1829 #[test]
1830 fn elementwise_region_fused_matches_unfused() {
1831 let f = DType::F32;
1836 let mut g = Graph::new("ew_e2e");
1837 let a = g.input("a", Shape::new(&[8], f));
1838 let b = g.input("b", Shape::new(&[8], f));
1839 let c = g.input("c", Shape::new(&[8], f));
1840 let s = Shape::new(&[8], f);
1841 let add = g.add(a, b);
1842 let mul = g.mul(add, c);
1843 let relu = g.relu(mul);
1844 let _ = s;
1845 g.set_outputs(vec![relu]);
1846
1847 let mut compiled = Session::new(Device::Cpu).compile(g);
1848 let av: Vec<f32> = vec![1.0, -2.0, 3.0, -4.0, 0.5, -0.5, 1.5, -1.5];
1849 let bv: Vec<f32> = vec![0.5, 1.0, 2.0, 4.0, 0.5, 0.5, 0.5, 0.5];
1850 let cv: Vec<f32> = vec![1.0, 2.0, 1.0, 1.0, 2.0, 3.0, 0.5, 4.0];
1851 let outs = compiled.run(&[("a", &av), ("b", &bv), ("c", &cv)]);
1852 let out = &outs[0];
1853
1854 let expected: Vec<f32> = (0..8)
1855 .map(|i| {
1856 let v = (av[i] + bv[i]) * cv[i];
1857 v.max(0.0)
1858 })
1859 .collect();
1860 for (i, (got, exp)) in out.iter().zip(&expected).enumerate() {
1861 assert!(
1862 (got - exp).abs() < 1e-6,
1863 "mismatch at {i}: got {got}, expected {exp}"
1864 );
1865 }
1866 }
1867
1868 #[test]
1869 #[ignore = "active-extent execution is a stub on CPU (thunk.rs::execute_thunks_active)"]
1870 fn active_extent_skips_compute_on_attention() {
1871 use rlx_ir::op::MaskKind;
1874 let f = DType::F32;
1875 let mut g = Graph::new("attn");
1876 let q = g.input("q", Shape::new(&[1, 4, 8], f));
1877 let k = g.input("k", Shape::new(&[1, 4, 8], f));
1878 let v = g.input("v", Shape::new(&[1, 4, 8], f));
1879 let out = g.attention_kind(q, k, v, 2, 4, MaskKind::None, Shape::new(&[1, 4, 8], f));
1880 g.set_outputs(vec![out]);
1881 let mut compiled = Session::new(Device::Cpu).compile(g);
1882
1883 let warm = compiled.run(&[
1885 ("q", &[1.0f32; 32]),
1886 ("k", &[1.0f32; 32]),
1887 ("v", &[1.0f32; 32]),
1888 ]);
1889 let warm_out = warm[0].clone();
1890 assert_eq!(warm_out.len(), 32);
1891
1892 compiled.set_active_extent(Some((2, 4)));
1896 let outs = compiled.run(&[
1897 ("q", &[3.0f32; 32]),
1898 ("k", &[3.0f32; 32]),
1899 ("v", &[3.0f32; 32]),
1900 ]);
1901 let out = &outs[0];
1902 assert_eq!(out.len(), 32);
1903 assert_eq!(
1904 &out[16..],
1905 &warm_out[16..],
1906 "tail (positions 2,3) must be untouched — proves Attention skipped"
1907 );
1908 assert_ne!(
1910 &out[..16],
1911 &warm_out[..16],
1912 "first 2 positions should reflect new input"
1913 );
1914 }
1915
1916 #[test]
1917 fn active_extent_falls_back_when_unsupported_thunk_in_schedule() {
1918 }
1933
1934 #[test]
1935 fn run_padded_uses_active_extent_on_cpu() {
1936 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1939 let input: Vec<f32> = vec![
1940 1.0, -1.0, 2.0, -2.0, 3.0, -10.0, -20.0, -30.0, -40.0, -50.0, ];
1943 let (upper, outs) = cache
1949 .run_padded(
1950 5,
1951 5,
1952 |max| tiny_graph(max as usize),
1953 &[("x", &input[..5], 1)],
1954 &[1],
1955 )
1956 .unwrap();
1957 assert_eq!(upper, 15);
1958 assert_eq!(outs[0].len(), 5);
1959 assert_eq!(outs[0], vec![1.0, 0.0, 2.0, 0.0, 3.0]);
1965 }
1966
1967 #[test]
1968 fn run_padded_inner_zero_returns_output_unsliced() {
1969 let mut cache = BucketedCompileCache::new(Device::Cpu, vec![1..16]);
1972 let input: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 3.0];
1973
1974 let (upper, outs) = cache
1975 .run_padded(
1976 5,
1977 5,
1978 |max| tiny_graph(max as usize),
1979 &[("x", &input, 1)],
1980 &[0], )
1982 .unwrap();
1983
1984 assert_eq!(upper, 15);
1985 assert_eq!(
1986 outs[0].len(),
1987 15,
1988 "unsliced output preserves full upper extent"
1989 );
1990 assert_eq!(&outs[0][..5], &[1.0, 0.0, 2.0, 0.0, 3.0]);
1992 assert!(outs[0][5..].iter().all(|&v| v == 0.0));
1993 }
1994
1995 #[test]
1996 fn dynamic_dim_cache_specializes_per_key() {
1997 use rlx_ir::DType;
1998 use rlx_ir::Shape;
1999 use rlx_ir::hir::HirModule;
2000 use rlx_ir::sym;
2001
2002 let mut cache = DynamicDimCompileCache::new(Device::Cpu, 4);
2003 let opts = crate::CompileOptions::new();
2004 {
2005 let _short = cache
2006 .get_or_specialize(
2007 8,
2008 &rlx_ir::DimBinding::batch_seq(1, 8),
2009 || {
2010 let mut hir = HirModule::new("dyn_cache");
2011 let x = hir.input_batch_seq("x", sym::BATCH, sym::SEQ, 4, DType::F32);
2012 let w = hir.param("w", Shape::new(&[4, 2], DType::F32));
2013 let y = hir.linear(
2014 x,
2015 w,
2016 None,
2017 None,
2018 Shape::batch_seq(sym::BATCH, sym::SEQ, 2, DType::F32),
2019 );
2020 hir.set_outputs(vec![y]);
2021 hir
2022 },
2023 &opts,
2024 )
2025 .expect("specialize short");
2026 }
2027 assert!(cache.has_template());
2028 assert_eq!(cache.len(), 1);
2029 cache
2030 .get_or_specialize(
2031 128,
2032 &rlx_ir::DimBinding::batch_seq(1, 128),
2033 || panic!("HIR builder must not run twice"),
2034 &opts,
2035 )
2036 .expect("specialize long");
2037 assert_eq!(cache.len(), 2);
2038 }
2039}