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