1use super::*;
5use rlx_cpu::{arena::Arena, thunk};
6use rlx_ir::{DType, NodeId, Op};
7use rlx_opt::memory::{self, MemoryPlan};
8use rlx_driver::arena::{read_typed_to_f32, write_typed_from_f32};
11
12#[cfg(unix)]
15unsafe extern "C" {
16 fn madvise(
17 addr: *mut core::ffi::c_void,
18 len: usize,
19 advice: core::ffi::c_int,
20 ) -> core::ffi::c_int;
21}
22
23pub struct CpuBackend;
24
25impl Backend for CpuBackend {
26 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
27 rlx_cpu::SUPPORTED_OPS
28 }
29
30 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
31 use rlx_opt::pass::Pass as _;
32 static ONNX_KERNELS: std::sync::Once = std::sync::Once::new();
33 ONNX_KERNELS.call_once(rlx_cpu::onnx_ref::register_onnx_reference_kernels);
34 let graph = rlx_opt::LowerControlFlow.run(graph);
38 let graph = rlx_opt::LowerSpectral.run(graph);
44 if let Err(errors) = rlx_opt::legalize_for_backend(&graph, rlx_cpu::SUPPORTED_OPS) {
48 panic!("{}", rlx_opt::format_legalize_error("cpu", &errors));
49 }
50 let policy = options.policy.clone();
51 let _precision = options.precision;
52 let cfg = rlx_cpu::config::RuntimeConfig::global();
53
54 let graph = crate::precompile::precompile_cleanup(graph, options);
55
56 let mut compile_opts = options.clone();
58 compile_opts.arena_alignment = cfg.arena_alignment;
59 let compile_result = crate::stages::compile_graph_stages_for_backend(
60 rlx_driver::Device::Cpu,
61 graph,
62 &compile_opts,
63 rlx_cpu::SUPPORTED_OPS,
64 );
65 crate::stages::maybe_log_fusion(&compile_result.fusion);
66 let fused = compile_result.lir.into_graph();
67 let fused = rlx_cpu::prepare_graph_for_thunks(fused);
71
72 let fused = match policy {
75 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(fused),
76 None => fused,
77 };
78
79 let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&fused);
80 let exec_graph = if cpu_low_precision::needs_f32_exec(&fused) {
81 cpu_low_precision::promote_to_f32(fused)
82 } else {
83 fused
84 };
85
86 let plan = memory::plan_memory_native_in_order(&exec_graph, cfg.arena_alignment);
103 if rlx_ir::env::flag("RLX_CPU_ARENA_REPORT") {
104 eprint!("[rlx-arena-report]\n{}", plan.report());
105 }
106 if cfg.verbose >= 1 {
107 eprintln!(
108 "[rlx] arena: {} bytes, {} buffers, alignment: {}",
109 plan.arena_size,
110 plan.assignments.len(),
111 cfg.arena_alignment
112 );
113 }
114 Box::new(build_cpu_executable(
115 exec_graph,
116 plan,
117 io_manifest,
118 options.rng,
119 ))
120 }
121
122 fn compile_lir(&self, lir: LirModule, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
123 #[cfg(not(target_arch = "wasm32"))]
125 let prof = std::env::var_os("RLX_PROFILE_COMPILE").is_some();
126 #[cfg(not(target_arch = "wasm32"))]
127 let tt = if prof {
128 Some(std::time::Instant::now())
129 } else {
130 None
131 };
132 let alignment = lir.buffers.alignment.max(options.arena_alignment);
133 let mut graph = lir.into_graph();
134 graph = rlx_cpu::prepare_graph_for_thunks(graph);
137 {
138 use rlx_opt::pass::Pass as _;
139 graph = rlx_opt::LegalizeBroadcast.run(graph);
140 }
141 if let Some(p) = options.policy.clone() {
142 use rlx_opt::pass::Pass;
143 graph = rlx_opt::AutoMixedPrecision::new(p).run(graph);
144 }
145 let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&graph);
146 let promote = cpu_low_precision::needs_f32_exec(&graph);
147 let exec_graph = if promote {
148 cpu_low_precision::promote_to_f32(graph)
149 } else {
150 graph
151 };
152 #[cfg(not(target_arch = "wasm32"))]
153 let t_prep = tt.map(|t| t.elapsed());
154 #[cfg(not(target_arch = "wasm32"))]
155 let t1 = if prof {
156 Some(std::time::Instant::now())
157 } else {
158 None
159 };
160 let plan = memory::plan_memory_native_in_order(&exec_graph, alignment);
166 #[cfg(not(target_arch = "wasm32"))]
167 let t_plan = t1.map(|t| t.elapsed());
168 #[cfg(not(target_arch = "wasm32"))]
169 if prof {
170 eprintln!(
171 "[compile_lir] {} nodes: into_graph+passes={:?} plan_memory={:?}",
172 exec_graph.nodes().len(),
173 t_prep.unwrap(),
174 t_plan.unwrap(),
175 );
176 }
177 let cfg = rlx_cpu::config::RuntimeConfig::global();
178 if cfg.verbose >= 1 {
179 eprintln!(
180 "[rlx] compile_lir: arena {} bytes ({} buffers, alignment {})",
181 plan.arena_size,
182 plan.assignments.len(),
183 alignment,
184 );
185 }
186 #[cfg(not(target_arch = "wasm32"))]
187 let t2 = if prof {
188 Some(std::time::Instant::now())
189 } else {
190 None
191 };
192 let exe = build_cpu_executable(exec_graph, plan, io_manifest, options.rng);
193 #[cfg(not(target_arch = "wasm32"))]
194 if let Some(t2) = t2 {
195 eprintln!("[compile_lir] build_thunks={:?}", t2.elapsed());
196 }
197 Box::new(exe)
198 }
199}
200
201fn build_cpu_executable(
202 graph: Graph,
203 plan: MemoryPlan,
204 io_manifest: cpu_low_precision::IoDtypeManifest,
205 rng: rlx_ir::RngOptions,
206) -> CpuExecutable {
207 let mut arena = Arena::from_plan(plan);
208 let mut input_ids = HashMap::new();
209 let mut param_ids = HashMap::new();
210 let mut node_dtypes: HashMap<NodeId, DType> = HashMap::new();
211 for node in graph.nodes() {
212 node_dtypes.insert(node.id, node.shape.dtype());
213 match &node.op {
214 Op::Input { name } => {
215 input_ids.insert(name.clone(), node.id);
216 }
217 Op::Param { name } => {
218 param_ids.insert(name.clone(), node.id);
219 }
220 _ => {}
221 }
222 }
223
224 let schedule = thunk::compile_thunks_with_rng(&graph, &arena, rng);
225
226 let mut input_slots = Vec::new();
227 for node in graph.nodes() {
228 if let Op::Input { name } = &node.op {
229 let off = arena.byte_offset(node.id);
230 let len = node.shape.num_elements().unwrap_or(0);
231 input_slots.push((name.clone(), off, len, node.shape.dtype()));
232 }
233 }
234
235 let output_slots: Vec<(usize, usize)> = graph
236 .outputs
237 .iter()
238 .map(|&id| {
239 let off = arena.byte_offset(id);
240 let len = graph.node(id).shape.num_elements().unwrap_or(0);
241 (off, len)
242 })
243 .collect();
244
245 for node in graph.nodes() {
246 if let Op::Constant { data } = &node.op
247 && arena.has_buffer(node.id)
248 && !data.is_empty()
249 {
250 match node.shape.dtype() {
251 DType::F64
261 | DType::F16
262 | DType::BF16
263 | DType::I64
264 | DType::I32
265 | DType::U32
266 | DType::Bool
267 | DType::U8
268 | DType::I8 => {
269 let off = arena.byte_offset(node.id);
270 let buf = arena.raw_buf_mut();
271 let n = buf.len().saturating_sub(off).min(data.len());
272 buf[off..off + n].copy_from_slice(&data[..n]);
273 }
274 _ => {
275 let buf = arena.slice_mut(node.id);
276 let n_floats = data.len() / 4;
277 let n = buf.len().min(n_floats);
278 for i in 0..n {
279 let bytes = [
280 data[i * 4],
281 data[i * 4 + 1],
282 data[i * 4 + 2],
283 data[i * 4 + 3],
284 ];
285 buf[i] = f32::from_le_bytes(bytes);
286 }
287 }
288 }
289 }
290 }
291
292 CpuExecutable {
293 graph,
294 arena,
295 input_ids,
296 param_ids,
297 node_dtypes,
298 io_manifest,
299 schedule,
300 input_slots,
301 output_slots,
302 handles: HashMap::new(),
303 active_extent: None,
304 moe_resident: None,
305 moe_resident_layers: None,
306 moe_topk_capture: None,
307 baseline_written: false,
308 }
309}
310
311#[derive(Clone)]
312struct CpuExecutable {
313 graph: Graph,
314 arena: Arena,
315 input_ids: HashMap<String, NodeId>,
316 param_ids: HashMap<String, NodeId>,
317 node_dtypes: HashMap<NodeId, DType>,
320 io_manifest: cpu_low_precision::IoDtypeManifest,
322 schedule: thunk::ThunkSchedule,
323 input_slots: Vec<(String, usize, usize, DType)>,
325 output_slots: Vec<(usize, usize)>,
327 handles: HashMap<String, Vec<f32>>,
332 active_extent: Option<(usize, usize)>,
338 moe_resident: Option<std::sync::Arc<[bool]>>,
339 moe_resident_layers: Option<std::sync::Arc<Vec<std::sync::Arc<[bool]>>>>,
340 moe_topk_capture: Option<std::sync::Arc<rlx_cpu::moe_topk_capture::MoeTopkCapture>>,
341 baseline_written: bool,
347}
348
349unsafe impl Send for CpuExecutable {}
350
351impl CpuExecutable {
352 fn arena_report_if_requested(&self) {
356 if !rlx_ir::env::flag("RLX_CPU_ARENA_REPORT") {
357 return;
358 }
359 let mut rows: Vec<(usize, NodeId, bool)> = Vec::new();
360 for node in self.graph.nodes() {
361 if !self.arena.has_buffer(node.id) {
362 continue;
363 }
364 let dt = self
365 .node_dtypes
366 .get(&node.id)
367 .copied()
368 .unwrap_or(DType::F32);
369 let bytes = node.shape.num_elements().unwrap_or(0) * dt.size_bytes();
370 let persistent = matches!(
371 node.op,
372 Op::Param { .. } | Op::Input { .. } | Op::Constant { .. }
373 );
374 rows.push((bytes, node.id, persistent));
375 }
376 rows.sort_by_key(|b| std::cmp::Reverse(b.0));
377 let persist: usize = rows.iter().filter(|r| r.2).map(|r| r.0).sum();
378 let scratch: usize = rows.iter().filter(|r| !r.2).map(|r| r.0).sum();
379 eprintln!(
380 "[arena-report] {} buffers, persistent={}MB scratch={}MB",
381 rows.len(),
382 persist >> 20,
383 scratch >> 20
384 );
385 for (b, id, p) in rows.iter().take(15) {
386 eprintln!(" {:>6}MB node {:>5} persistent={}", b >> 20, id, p);
387 }
388 }
389
390 fn dump_nodes_if_requested(&self) {
395 self.arena_report_if_requested();
396 if !rlx_ir::env::flag("RLX_CPU_DUMP_NODES") {
397 return;
398 }
399 let limit = rlx_ir::env::parse_or("RLX_CPU_DUMP_NODES_LIMIT", 2000usize);
400 let flat_probe = rlx_ir::env::parse_or::<usize>("RLX_CPU_DUMP_FLAT", usize::MAX);
401 eprintln!("[rlx-cpu-dump] per-node max |x| (topo order, limit={limit})");
402 let buf = self.arena.raw_buf();
403 let mut shown = 0usize;
404 for (i, node) in self.graph.nodes().iter().enumerate() {
405 if !self.arena.has_buffer(node.id) {
406 continue;
407 }
408 if matches!(
409 node.op,
410 rlx_ir::Op::Input { .. }
411 | rlx_ir::Op::Param { .. }
412 | rlx_ir::Op::Constant { .. }
413 | rlx_ir::Op::Reshape { .. }
414 | rlx_ir::Op::Cast { .. }
415 ) {
416 continue;
417 }
418 if self
419 .node_dtypes
420 .get(&node.id)
421 .copied()
422 .unwrap_or(DType::F32)
423 != DType::F32
424 {
425 continue;
426 }
427 let off = self.arena.byte_offset(node.id);
428 let n = node.shape.num_elements().unwrap_or(0);
429 let data: &[f32] =
430 unsafe { std::slice::from_raw_parts(buf.as_ptr().add(off) as *const f32, n) };
431 let max = data.iter().fold(0f32, |m, &v| m.max(v.abs()));
432 let nz = data.iter().filter(|&&v| v != 0.0).count();
433 let flat_s = if flat_probe < data.len() {
434 format!(" flat[{flat_probe}]={:.6}", data[flat_probe])
435 } else {
436 String::new()
437 };
438 eprintln!(
439 " [{i:>3}] id={:?} name={:?} {:?} shape={:?} max={max:.6} nonzero={nz}/{}{flat_s}",
440 node.id,
441 node.name,
442 node.op,
443 node.shape.dims(),
444 data.len()
445 );
446 shown += 1;
447 if shown >= limit {
448 break;
449 }
450 }
451 }
452
453 fn write_input(&mut self, id: NodeId, data: &[f32]) {
455 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
456 let off = self.arena.byte_offset(id);
457 let buf = self.arena.raw_buf_mut();
458 let elem_size = dtype.size_bytes();
459 let max_elems = (buf.len() - off) / elem_size;
460 unsafe {
461 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
462 }
463 }
464
465 fn read_output(&self, id: NodeId) -> Vec<f32> {
467 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
468 let off = self.arena.byte_offset(id);
469 let buf = self.arena.raw_buf();
470 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
471 unsafe { read_typed_to_f32(buf.as_ptr().add(off), dtype, n_elems) }
472 }
473}
474
475impl ExecutableGraph for CpuExecutable {
476 fn capabilities(&self) -> crate::ExecutableCapabilities {
477 crate::ExecutableCapabilities {
478 clone: true,
479 moe: true,
480 typed_io: true,
481 active_extent: true,
482 ..crate::ExecutableCapabilities::NONE
483 }
484 }
485
486 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
487 Box::new(self.clone())
488 }
489 fn set_param(&mut self, name: &str, data: &[f32]) {
490 if let Some(&id) = self.param_ids.get(name)
495 && self.arena.has_buffer(id)
496 {
497 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
498 let off = self.arena.byte_offset(id);
499 let buf = self.arena.raw_buf_mut();
500 let elem_size = dtype.size_bytes();
501 let max_elems = (buf.len() - off) / elem_size;
502 unsafe {
503 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
504 }
505 }
506 }
507
508 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
509 self.restore_arena_baseline();
510 let handle_names: Vec<String> = self.handles.keys().cloned().collect();
513 for name in &handle_names {
514 if let Some(&id) = self.input_ids.get(name)
515 && self.arena.has_buffer(id)
516 {
517 let data = self.handles.get(name).cloned().unwrap_or_default();
518 self.write_input(id, &data);
519 }
520 }
521 for &(name, data) in inputs {
523 if let Some(&id) = self.input_ids.get(name)
524 && self.arena.has_buffer(id)
525 {
526 self.write_input(id, data);
527 }
528 }
529
530 let active_used = if let Some((actual, upper)) = self.active_extent {
535 thunk::execute_thunks_active(&self.schedule, self.arena.raw_buf_mut(), actual, upper)
536 } else {
537 false
538 };
539 if !active_used {
540 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
542 }
543
544 self.dump_nodes_if_requested();
545
546 for (idx, &out_id) in self.graph.outputs.iter().enumerate() {
550 let name = format!("out{idx}");
551 if self.handles.contains_key(&name) {
552 let v = self.read_output(out_id);
553 self.handles.insert(name, v);
554 }
555 }
556
557 self.graph
558 .outputs
559 .iter()
560 .map(|&out_id| self.read_output(out_id))
561 .collect()
562 }
563
564 fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
565 self.restore_arena_baseline();
566 for &(name, data) in inputs {
568 if let Some(&id) = self.input_ids.get(name)
569 && self.arena.has_buffer(id)
570 {
571 self.write_input(id, data);
572 }
573 }
574 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
575 self.graph
579 .outputs
580 .iter()
581 .map(|&out_id| {
582 let (ptr, len) = self.arena.raw_ptr(out_id);
583 (ptr as *const f32, len)
584 })
585 .collect()
586 }
587
588 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
592 self.restore_arena_baseline();
593 let buf = self.arena.raw_buf_mut();
594 for (i, &data) in inputs.iter().enumerate() {
595 if i < self.input_slots.len() {
596 let (_, off, max_len, dtype) = &self.input_slots[i];
597 unsafe {
598 write_typed_from_f32(buf.as_mut_ptr().add(*off), *dtype, data, *max_len);
599 }
600 }
601 }
602 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
603 &self.output_slots
604 }
605
606 fn arena_ptr(&self) -> *const u8 {
607 self.arena.raw_buf_mut_ptr()
608 }
609
610 fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
611 self.handles.insert(name.to_string(), data.to_vec());
616 true
617 }
618
619 fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
620 self.handles.get(name).cloned()
621 }
622
623 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
624 self.active_extent = extent;
625 }
626
627 fn release_scratch(&mut self) {
628 self.release_scratch_pages();
629 }
630
631 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
632 *self.schedule.rng.write().unwrap() = rng;
633 }
634
635 fn rng(&self) -> rlx_ir::RngOptions {
636 *self.schedule.rng.read().unwrap()
637 }
638
639 fn set_moe_resident_experts(&mut self, mask: &[bool]) {
640 self.moe_resident_layers = None;
641 self.schedule.moe_resident_layers = None;
642 self.moe_resident = Some(Arc::from(mask));
643 self.schedule.moe_resident = self.moe_resident.clone();
644 }
645
646 fn set_moe_resident_experts_per_layer(&mut self, masks: &[&[bool]]) {
647 self.moe_resident = None;
648 self.schedule.moe_resident = None;
649 let layers: Vec<Arc<[bool]>> = masks.iter().map(|m| Arc::from(*m)).collect();
650 let arc = Arc::new(layers);
651 self.moe_resident_layers = Some(arc.clone());
652 self.schedule.moe_resident_layers = Some(arc);
653 }
654
655 fn enable_moe_topk_capture(&mut self, num_experts: usize) -> bool {
656 let cap = rlx_cpu::moe_topk_capture::MoeTopkCapture::new(num_experts);
657 self.moe_topk_capture = Some(cap.clone());
658 self.schedule.moe_topk_capture = Some(cap);
659 true
660 }
661
662 fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
663 let cap = self.moe_topk_capture.as_ref()?;
664 let layers = cap.take_layers();
665 if layers.is_empty() {
666 None
667 } else {
668 Some(layers)
669 }
670 }
671
672 fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
673 rlx_cpu::moe_residency::take_last_forward_stats()
674 }
675
676 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
682 if matches!(dtype, DType::F64 | DType::I64 | DType::I32 | DType::U32) {
683 self.set_param_bytes(name, data, dtype);
684 return;
685 }
686 if matches!(dtype, DType::U8 | DType::I8) {
690 self.set_param_bytes(name, data, dtype);
691 return;
692 }
693 if dtype == DType::F32 {
694 let n = data.len() / 4;
695 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
696 self.set_param(name, s);
697 } else {
698 let f32_buf = super::widen_bytes_to_f32(data, dtype);
699 self.set_param(name, &f32_buf);
700 }
701 }
702
703 fn run_typed(
715 &mut self,
716 inputs: &[(&str, &[u8], rlx_ir::DType)],
717 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
718 let all_f64 = !inputs.is_empty() && inputs.iter().all(|(_, _, dt)| *dt == DType::F64);
723
724 if all_f64 {
725 for (name, data, _) in inputs {
726 if let Some(&id) = self.input_ids.get(*name) {
727 if !self.arena.has_buffer(id) {
728 continue;
729 }
730 let off = self.arena.byte_offset(id);
731 let buf = self.arena.raw_buf_mut();
732 let n = data.len();
733 debug_assert!(
734 off + n <= buf.len(),
735 "run_typed: input '{name}' overflows arena slot"
736 );
737 buf[off..off + n].copy_from_slice(data);
738 }
739 }
740 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
741 } else {
742 let mut f32_owned: Vec<(String, Vec<f32>)> = Vec::new();
747 for (name, data, dt) in inputs {
748 let direct = matches!(
749 *dt,
750 DType::F64 | DType::I32 | DType::I64 | DType::U32 | DType::C64 | DType::C128
751 );
752 if direct {
753 if let Some(&id) = self.input_ids.get(*name) {
754 if !self.arena.has_buffer(id) {
755 continue;
756 }
757 let off = self.arena.byte_offset(id);
758 let buf = self.arena.raw_buf_mut();
759 buf[off..off + data.len()].copy_from_slice(data);
760 }
761 } else {
762 let v = super::widen_bytes_to_f32(data, *dt);
763 f32_owned.push((name.to_string(), v));
764 }
765 }
766 for (name, data) in &f32_owned {
767 if let Some(&id) = self.input_ids.get(name.as_str()) {
768 if self.arena.has_buffer(id) {
769 self.write_input(id, data);
770 }
771 }
772 }
773 let active_used = if let Some((actual, upper)) = self.active_extent {
774 thunk::execute_thunks_active(
775 &self.schedule,
776 self.arena.raw_buf_mut(),
777 actual,
778 upper,
779 )
780 } else {
781 false
782 };
783 if !active_used {
784 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
785 }
786 }
787
788 self.dump_nodes_if_requested();
789
790 self.graph
792 .outputs
793 .iter()
794 .enumerate()
795 .map(|(idx, &id)| {
796 let exec_dtype = self.graph.node(id).shape.dtype();
797 let declared = self.io_manifest.output_dtype(idx, exec_dtype);
798 if matches!(
799 exec_dtype,
800 DType::F64
801 | DType::F16
802 | DType::BF16
803 | DType::I32
804 | DType::I64
805 | DType::U32
806 | DType::C64
807 | DType::C128
808 ) {
809 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
810 let n_bytes = n_elems * exec_dtype.size_bytes();
811 let off = self.arena.byte_offset(id);
812 let bytes = self.arena.raw_buf()[off..off + n_bytes].to_vec();
813 return (bytes, declared);
814 }
815 let f32_vals = self.read_output(id);
816 if declared != exec_dtype {
817 return (super::narrow_f32_to_bytes(&f32_vals, declared), declared);
818 }
819 let bytes = f32_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
820 (bytes, declared)
821 })
822 .collect()
823 }
824}
825
826impl CpuExecutable {
827 #[cfg(unix)]
842 fn persistent_keep_ranges(&self) -> Vec<(usize, usize)> {
843 let persistent: std::collections::HashSet<NodeId> = {
844 let mut s: std::collections::HashSet<NodeId> =
845 self.param_ids.values().copied().collect();
846 for node in self.graph.nodes() {
847 if matches!(node.op, Op::Constant { .. }) {
848 s.insert(node.id);
849 }
850 }
851 s
852 };
853 let mut keep: Vec<(usize, usize)> = self
854 .graph
855 .nodes()
856 .iter()
857 .filter_map(|node| {
858 let id = node.id;
859 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
860 return None;
861 }
862 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
863 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
864 let off = self.arena.byte_offset(id);
865 Some((off, off + nbytes))
866 })
867 .collect();
868 keep.sort_unstable();
869 keep
870 }
871
872 #[allow(unused_variables)]
883 fn release_scratch_pages(&self) {
884 #[cfg(unix)]
885 {
886 const PAGE: usize = 16 * 1024;
889 let keep = self.persistent_keep_ranges();
890 let base = self.arena.raw_buf_mut_ptr() as usize;
891 let total = self.arena.size();
892 let mut cursor = 0usize;
894 let mut spans: Vec<(usize, usize)> = Vec::new();
895 for &(s, e) in &keep {
896 let s = s.min(total);
897 if cursor < s {
898 spans.push((cursor, s));
899 }
900 cursor = cursor.max(e.min(total));
901 }
902 if cursor < total {
903 spans.push((cursor, total));
904 }
905 for (s, e) in spans {
906 let a = (base + s).div_ceil(PAGE) * PAGE;
907 let b = (base + e) / PAGE * PAGE;
908 if b > a {
909 #[cfg(target_os = "macos")]
917 const ADVICE: i32 = 7;
918 #[cfg(not(target_os = "macos"))]
919 const ADVICE: i32 = 4;
920 let advice = std::env::var("RLX_MADV")
921 .ok()
922 .and_then(|s| s.parse().ok())
923 .unwrap_or(ADVICE);
924 unsafe { madvise(a as *mut core::ffi::c_void, b - a, advice) };
925 }
926 }
927 }
928 }
929
930 fn restore_arena_baseline(&mut self) {
931 let persistent: std::collections::HashSet<NodeId> = {
933 let mut s: std::collections::HashSet<NodeId> =
934 self.param_ids.values().copied().collect();
935 for node in self.graph.nodes() {
936 if matches!(node.op, Op::Constant { .. }) {
937 s.insert(node.id);
938 }
939 }
940 s
941 };
942
943 if !self.baseline_written {
946 let constants: Vec<(NodeId, DType, Vec<u8>)> = self
947 .graph
948 .nodes()
949 .iter()
950 .filter_map(|node| {
951 if let Op::Constant { data } = &node.op
952 && self.arena.has_buffer(node.id)
953 && !data.is_empty()
954 {
955 Some((node.id, node.shape.dtype(), data.clone()))
956 } else {
957 None
958 }
959 })
960 .collect();
961 for (id, dtype, data) in constants {
962 self.write_constant_to_arena(id, dtype, &data);
963 }
964 self.baseline_written = true;
965 }
966
967 let mut keep: Vec<(usize, usize)> = self
978 .graph
979 .nodes()
980 .iter()
981 .filter_map(|node| {
982 let id = node.id;
983 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
984 return None;
985 }
986 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
987 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
988 let off = self.arena.byte_offset(id);
989 Some((off, off + nbytes))
990 })
991 .collect();
992 keep.sort_unstable();
993
994 let buf = self.arena.raw_buf_mut();
995 let len = buf.len();
996 let mut cursor = 0usize;
997 for (start, end) in keep {
998 let start = start.min(len);
999 if cursor < start {
1000 buf[cursor..start].fill(0);
1001 }
1002 cursor = cursor.max(end.min(len));
1003 }
1004 if cursor < len {
1005 buf[cursor..len].fill(0);
1006 }
1007 }
1008
1009 fn write_constant_to_arena(&mut self, id: NodeId, dtype: DType, data: &[u8]) {
1010 match dtype {
1011 DType::F64
1012 | DType::F16
1013 | DType::BF16
1014 | DType::U8
1015 | DType::I8
1016 | DType::Bool
1017 | DType::I64
1018 | DType::I32
1019 | DType::U32 => {
1020 let off = self.arena.byte_offset(id);
1021 let buf = self.arena.raw_buf_mut();
1022 let n = buf.len().saturating_sub(off).min(data.len());
1023 buf[off..off + n].copy_from_slice(&data[..n]);
1024 }
1025 _ => {
1026 let buf = self.arena.slice_mut(id);
1027 let n_floats = data.len() / 4;
1028 let n = buf.len().min(n_floats);
1029 for i in 0..n {
1030 let bytes = [
1031 data[i * 4],
1032 data[i * 4 + 1],
1033 data[i * 4 + 2],
1034 data[i * 4 + 3],
1035 ];
1036 buf[i] = f32::from_le_bytes(bytes);
1037 }
1038 }
1039 }
1040 }
1041
1042 fn set_param_bytes(&mut self, name: &str, data: &[u8], _dtype: rlx_ir::DType) {
1048 self.write_param_bytes_to_arena(name, data);
1050 }
1051
1052 fn write_param_bytes_to_arena(&mut self, name: &str, data: &[u8]) {
1053 if let Some(&id) = self.param_ids.get(name)
1054 && self.arena.has_buffer(id)
1055 {
1056 let off = self.arena.byte_offset(id);
1057 let buf = self.arena.raw_buf_mut();
1058 debug_assert!(
1059 off + data.len() <= buf.len(),
1060 "set_param_bytes: '{name}' would overflow arena slot"
1061 );
1062 buf[off..off + data.len()].copy_from_slice(data);
1063 }
1064 }
1065}