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 = rlx_ir::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 if let Some(spec) = rlx_ir::env::var("RLX_CPU_DUMP_NODE_DATA") {
409 let dir = rlx_ir::env::var("RLX_CPU_DUMP_DIR").unwrap_or_else(|| "/tmp".into());
410 for want in spec.split(',').filter_map(|t| t.trim().parse::<u32>().ok()) {
411 let Some(node) = self.graph.nodes().iter().find(|n| n.id.0 == want) else {
412 continue;
413 };
414 if !self.arena.has_buffer(node.id) {
415 continue;
416 }
417 let off = self.arena.byte_offset(node.id);
418 let n = node.shape.num_elements().unwrap_or(0);
419 let data: &[f32] =
420 unsafe { std::slice::from_raw_parts(buf.as_ptr().add(off) as *const f32, n) };
421 let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_le_bytes()).collect();
422 let path = format!("{dir}/node_{want}.f32");
423 match std::fs::write(&path, &bytes) {
424 Ok(()) => eprintln!(
425 "[rlx-cpu-dump] wrote {path} ({n} f32) byte_range=[{off}, {})",
426 off + n * 4
427 ),
428 Err(e) => eprintln!("[rlx-cpu-dump] {path}: {e}"),
429 }
430 }
431 }
432 let mut shown = 0usize;
433 let (mut no_buf, mut skipped_op, mut not_f32) = (0usize, 0usize, 0usize);
434 for (i, node) in self.graph.nodes().iter().enumerate() {
435 if !self.arena.has_buffer(node.id) {
436 no_buf += 1;
437 continue;
438 }
439 if matches!(
440 node.op,
441 rlx_ir::Op::Input { .. }
442 | rlx_ir::Op::Param { .. }
443 | rlx_ir::Op::Constant { .. }
444 | rlx_ir::Op::Reshape { .. }
445 | rlx_ir::Op::Cast { .. }
446 ) {
447 skipped_op += 1;
448 continue;
449 }
450 if self
451 .node_dtypes
452 .get(&node.id)
453 .copied()
454 .unwrap_or(DType::F32)
455 != DType::F32
456 {
457 not_f32 += 1;
458 continue;
459 }
460 let off = self.arena.byte_offset(node.id);
461 let n = node.shape.num_elements().unwrap_or(0);
462 let data: &[f32] =
463 unsafe { std::slice::from_raw_parts(buf.as_ptr().add(off) as *const f32, n) };
464 let max = data.iter().fold(0f32, |m, &v| m.max(v.abs()));
465 let nz = data.iter().filter(|&&v| v != 0.0).count();
466 let sum: f64 = data.iter().map(|&v| v as f64).sum();
470 let row = node
474 .shape
475 .dims()
476 .last()
477 .and_then(|d| match d {
478 rlx_ir::Dim::Static(n) => Some(*n),
479 rlx_ir::Dim::Dynamic(_) => None,
480 })
481 .unwrap_or(data.len())
482 .max(1);
483 let tail = data
484 .len()
485 .checked_sub(row)
486 .map(|o| data[o..].iter().fold(0f32, |m, &v| m.max(v.abs())))
487 .unwrap_or(max);
488 let tabs: f64 = data
491 .len()
492 .checked_sub(row)
493 .map(|o| data[o..].iter().map(|&v| v.abs() as f64).sum())
494 .unwrap_or(0.0);
495 let flat_s = if flat_probe < data.len() {
496 format!(" flat[{flat_probe}]={:.6}", data[flat_probe])
497 } else {
498 String::new()
499 };
500 eprintln!(
501 " [{i:>3}] in={:?} id={:?} name={:?} {:?} shape={:?} max={max:.6} tail={tail:.6} sum={sum:.6} tabs={tabs:.6} nonzero={nz}/{}{flat_s}",
502 node.inputs.iter().map(|n| n.0).collect::<Vec<_>>(),
503 node.id,
504 node.name,
505 node.op,
506 node.shape.dims(),
507 data.len()
508 );
509 if rlx_ir::env::parse_or::<u32>("RLX_CPU_ROW_TABS", u32::MAX) == node.id.0 {
514 for (r, chunk) in data.chunks(row).enumerate() {
515 let t: f64 = chunk.iter().map(|&v| v.abs() as f64).sum();
516 eprintln!(" rowtabs node={} row={r} tabs={t:.6}", node.id.0);
517 }
518 }
519 shown += 1;
520 if shown >= limit {
521 break;
522 }
523 }
524 eprintln!(
525 "[rlx-cpu-dump] nodes={} shown={shown} no_buffer={no_buf} skipped_op={skipped_op} not_f32={not_f32}",
526 self.graph.nodes().len()
527 );
528 }
529
530 fn write_input(&mut self, id: NodeId, data: &[f32]) {
532 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
533 let off = self.arena.byte_offset(id);
534 let buf = self.arena.raw_buf_mut();
535 let elem_size = dtype.size_bytes();
536 let max_elems = (buf.len() - off) / elem_size;
537 unsafe {
538 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
539 }
540 }
541
542 fn read_output(&self, id: NodeId) -> Vec<f32> {
544 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
545 let off = self.arena.byte_offset(id);
546 let buf = self.arena.raw_buf();
547 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
548 unsafe { read_typed_to_f32(buf.as_ptr().add(off), dtype, n_elems) }
549 }
550}
551
552impl ExecutableGraph for CpuExecutable {
553 fn capabilities(&self) -> crate::ExecutableCapabilities {
554 crate::ExecutableCapabilities {
555 clone: true,
556 moe: true,
557 persistent_handles: true,
560 typed_io: true,
561 active_extent: true,
562 ..crate::ExecutableCapabilities::NONE
563 }
564 }
565
566 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
567 Box::new(self.clone())
568 }
569 fn set_param(&mut self, name: &str, data: &[f32]) {
570 if let Some(&id) = self.param_ids.get(name)
575 && self.arena.has_buffer(id)
576 {
577 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
578 let off = self.arena.byte_offset(id);
579 let buf = self.arena.raw_buf_mut();
580 let elem_size = dtype.size_bytes();
581 let max_elems = (buf.len() - off) / elem_size;
582 unsafe {
583 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
584 }
585 }
586 }
587
588 fn set_param_range(&mut self, name: &str, byte_offset: usize, data: &[u8]) -> bool {
589 let Some(&id) = self.param_ids.get(name) else {
596 return false;
597 };
598 if !self.arena.has_buffer(id) {
599 return false;
600 }
601 if self.node_dtypes.get(&id).copied().unwrap_or(DType::F32) != DType::F32 {
606 return false;
607 }
608 let size = self.arena.byte_size(id);
612 let Some(end) = byte_offset.checked_add(data.len()) else {
613 return false;
614 };
615 if end > size {
616 return false;
617 }
618 let off = self.arena.byte_offset(id);
619 self.arena.raw_buf_mut()[off + byte_offset..off + end].copy_from_slice(data);
620 true
621 }
622
623 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
624 self.restore_arena_baseline();
625 let handle_names: Vec<String> = self.handles.keys().cloned().collect();
628 for name in &handle_names {
629 if let Some(&id) = self.input_ids.get(name)
630 && self.arena.has_buffer(id)
631 {
632 let data = self.handles.get(name).cloned().unwrap_or_default();
633 self.write_input(id, &data);
634 }
635 }
636 for &(name, data) in inputs {
638 if let Some(&id) = self.input_ids.get(name)
639 && self.arena.has_buffer(id)
640 {
641 self.write_input(id, data);
642 }
643 }
644
645 let active_used = if let Some((actual, upper)) = self.active_extent {
650 thunk::execute_thunks_active(&self.schedule, self.arena.raw_buf_mut(), actual, upper)
651 } else {
652 false
653 };
654 if !active_used {
655 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
657 }
658
659 self.dump_nodes_if_requested();
660
661 for (idx, &out_id) in self.graph.outputs.iter().enumerate() {
665 let name = format!("out{idx}");
666 if self.handles.contains_key(&name) {
667 let v = self.read_output(out_id);
668 self.handles.insert(name, v);
669 }
670 }
671
672 self.graph
673 .outputs
674 .iter()
675 .map(|&out_id| self.read_output(out_id))
676 .collect()
677 }
678
679 fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
680 self.restore_arena_baseline();
681 for &(name, data) in inputs {
683 if let Some(&id) = self.input_ids.get(name)
684 && self.arena.has_buffer(id)
685 {
686 self.write_input(id, data);
687 }
688 }
689 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
690 self.graph
694 .outputs
695 .iter()
696 .map(|&out_id| {
697 let (ptr, len) = self.arena.raw_ptr(out_id);
698 (ptr as *const f32, len)
699 })
700 .collect()
701 }
702
703 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
707 self.restore_arena_baseline();
708 let buf = self.arena.raw_buf_mut();
709 for (i, &data) in inputs.iter().enumerate() {
710 if i < self.input_slots.len() {
711 let (_, off, max_len, dtype) = &self.input_slots[i];
712 unsafe {
713 write_typed_from_f32(buf.as_mut_ptr().add(*off), *dtype, data, *max_len);
714 }
715 }
716 }
717 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
718 &self.output_slots
719 }
720
721 fn arena_ptr(&self) -> *const u8 {
722 self.arena.raw_buf_mut_ptr()
723 }
724
725 fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
726 self.handles.insert(name.to_string(), data.to_vec());
731 true
732 }
733
734 fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
735 self.handles.get(name).cloned()
736 }
737
738 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
739 self.active_extent = extent;
740 }
741
742 fn release_scratch(&mut self) {
743 self.release_scratch_pages();
744 }
745
746 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
747 *self.schedule.rng.write().unwrap() = rng;
748 }
749
750 fn rng(&self) -> rlx_ir::RngOptions {
751 *self.schedule.rng.read().unwrap()
752 }
753
754 fn set_moe_resident_experts(&mut self, mask: &[bool]) {
755 self.moe_resident_layers = None;
756 self.schedule.moe_resident_layers = None;
757 self.moe_resident = Some(Arc::from(mask));
758 self.schedule.moe_resident = self.moe_resident.clone();
759 }
760
761 fn set_moe_resident_experts_per_layer(&mut self, masks: &[&[bool]]) {
762 self.moe_resident = None;
763 self.schedule.moe_resident = None;
764 let layers: Vec<Arc<[bool]>> = masks.iter().map(|m| Arc::from(*m)).collect();
765 let arc = Arc::new(layers);
766 self.moe_resident_layers = Some(arc.clone());
767 self.schedule.moe_resident_layers = Some(arc);
768 }
769
770 fn enable_moe_topk_capture(&mut self, num_experts: usize) -> bool {
771 let cap = rlx_cpu::moe_topk_capture::MoeTopkCapture::new(num_experts);
772 self.moe_topk_capture = Some(cap.clone());
773 self.schedule.moe_topk_capture = Some(cap);
774 true
775 }
776
777 fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
778 let cap = self.moe_topk_capture.as_ref()?;
779 let layers = cap.take_layers();
780 if layers.is_empty() {
781 None
782 } else {
783 Some(layers)
784 }
785 }
786
787 fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
788 rlx_cpu::moe_residency::take_last_forward_stats()
789 }
790
791 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
797 if matches!(dtype, DType::F64 | DType::I64 | DType::I32 | DType::U32) {
798 self.set_param_bytes(name, data, dtype);
799 return;
800 }
801 if matches!(dtype, DType::U8 | DType::I8) {
805 self.set_param_bytes(name, data, dtype);
806 return;
807 }
808 if dtype == DType::F32 {
809 let n = data.len() / 4;
810 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
811 self.set_param(name, s);
812 } else {
813 let f32_buf = super::widen_bytes_to_f32(data, dtype);
814 self.set_param(name, &f32_buf);
815 }
816 }
817
818 fn run_typed(
830 &mut self,
831 inputs: &[(&str, &[u8], rlx_ir::DType)],
832 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
833 let all_f64 = !inputs.is_empty() && inputs.iter().all(|(_, _, dt)| *dt == DType::F64);
838
839 if all_f64 {
840 for (name, data, _) in inputs {
841 if let Some(&id) = self.input_ids.get(*name) {
842 if !self.arena.has_buffer(id) {
843 continue;
844 }
845 let off = self.arena.byte_offset(id);
846 let buf = self.arena.raw_buf_mut();
847 let n = data.len();
848 debug_assert!(
849 off + n <= buf.len(),
850 "run_typed: input '{name}' overflows arena slot"
851 );
852 buf[off..off + n].copy_from_slice(data);
853 }
854 }
855 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
856 } else {
857 let mut f32_owned: Vec<(String, Vec<f32>)> = Vec::new();
862 for (name, data, dt) in inputs {
863 let direct = matches!(
864 *dt,
865 DType::F64 | DType::I32 | DType::I64 | DType::U32 | DType::C64 | DType::C128
866 );
867 if direct {
868 if let Some(&id) = self.input_ids.get(*name) {
869 if !self.arena.has_buffer(id) {
870 continue;
871 }
872 let off = self.arena.byte_offset(id);
873 let buf = self.arena.raw_buf_mut();
874 buf[off..off + data.len()].copy_from_slice(data);
875 }
876 } else {
877 let v = super::widen_bytes_to_f32(data, *dt);
878 f32_owned.push((name.to_string(), v));
879 }
880 }
881 for (name, data) in &f32_owned {
882 if let Some(&id) = self.input_ids.get(name.as_str()) {
883 if self.arena.has_buffer(id) {
884 self.write_input(id, data);
885 }
886 }
887 }
888 let active_used = if let Some((actual, upper)) = self.active_extent {
889 thunk::execute_thunks_active(
890 &self.schedule,
891 self.arena.raw_buf_mut(),
892 actual,
893 upper,
894 )
895 } else {
896 false
897 };
898 if !active_used {
899 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
900 }
901 }
902
903 self.dump_nodes_if_requested();
904
905 self.graph
907 .outputs
908 .iter()
909 .enumerate()
910 .map(|(idx, &id)| {
911 let exec_dtype = self.graph.node(id).shape.dtype();
912 let declared = self.io_manifest.output_dtype(idx, exec_dtype);
913 if matches!(
914 exec_dtype,
915 DType::F64
916 | DType::F16
917 | DType::BF16
918 | DType::I32
919 | DType::I64
920 | DType::U32
921 | DType::C64
922 | DType::C128
923 ) {
924 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
925 let n_bytes = n_elems * exec_dtype.size_bytes();
926 let off = self.arena.byte_offset(id);
927 let bytes = self.arena.raw_buf()[off..off + n_bytes].to_vec();
928 return (bytes, declared);
929 }
930 let f32_vals = self.read_output(id);
931 if declared != exec_dtype {
932 return (super::narrow_f32_to_bytes(&f32_vals, declared), declared);
933 }
934 let bytes = f32_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
935 (bytes, declared)
936 })
937 .collect()
938 }
939}
940
941impl CpuExecutable {
942 #[cfg(unix)]
957 fn persistent_keep_ranges(&self) -> Vec<(usize, usize)> {
958 let persistent: std::collections::HashSet<NodeId> = {
959 let mut s: std::collections::HashSet<NodeId> =
960 self.param_ids.values().copied().collect();
961 for node in self.graph.nodes() {
962 if matches!(node.op, Op::Constant { .. }) {
963 s.insert(node.id);
964 }
965 }
966 s
967 };
968 let mut keep: Vec<(usize, usize)> = self
969 .graph
970 .nodes()
971 .iter()
972 .filter_map(|node| {
973 let id = node.id;
974 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
975 return None;
976 }
977 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
978 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
979 let off = self.arena.byte_offset(id);
980 Some((off, off + nbytes))
981 })
982 .collect();
983 keep.sort_unstable();
984 keep
985 }
986
987 #[allow(unused_variables)]
998 fn release_scratch_pages(&self) {
999 #[cfg(unix)]
1000 {
1001 const PAGE: usize = 16 * 1024;
1004 let keep = self.persistent_keep_ranges();
1005 let base = self.arena.raw_buf_mut_ptr() as usize;
1006 let total = self.arena.size();
1007 let mut cursor = 0usize;
1009 let mut spans: Vec<(usize, usize)> = Vec::new();
1010 for &(s, e) in &keep {
1011 let s = s.min(total);
1012 if cursor < s {
1013 spans.push((cursor, s));
1014 }
1015 cursor = cursor.max(e.min(total));
1016 }
1017 if cursor < total {
1018 spans.push((cursor, total));
1019 }
1020 for (s, e) in spans {
1021 let a = (base + s).div_ceil(PAGE) * PAGE;
1022 let b = (base + e) / PAGE * PAGE;
1023 if b > a {
1024 #[cfg(target_os = "macos")]
1032 const ADVICE: i32 = 7;
1033 #[cfg(not(target_os = "macos"))]
1034 const ADVICE: i32 = 4;
1035 let advice = rlx_ir::env::var("RLX_MADV")
1036 .and_then(|s| s.parse().ok())
1037 .unwrap_or(ADVICE);
1038 unsafe { madvise(a as *mut core::ffi::c_void, b - a, advice) };
1039 }
1040 }
1041 }
1042 }
1043
1044 fn restore_arena_baseline(&mut self) {
1045 let persistent: std::collections::HashSet<NodeId> = {
1047 let mut s: std::collections::HashSet<NodeId> =
1048 self.param_ids.values().copied().collect();
1049 for node in self.graph.nodes() {
1050 if matches!(node.op, Op::Constant { .. }) {
1051 s.insert(node.id);
1052 }
1053 }
1054 s
1055 };
1056
1057 if !self.baseline_written {
1060 let constants: Vec<(NodeId, DType, Vec<u8>)> = self
1061 .graph
1062 .nodes()
1063 .iter()
1064 .filter_map(|node| {
1065 if let Op::Constant { data } = &node.op
1066 && self.arena.has_buffer(node.id)
1067 && !data.is_empty()
1068 {
1069 Some((node.id, node.shape.dtype(), data.clone()))
1070 } else {
1071 None
1072 }
1073 })
1074 .collect();
1075 for (id, dtype, data) in constants {
1076 self.write_constant_to_arena(id, dtype, &data);
1077 }
1078 self.baseline_written = true;
1079 }
1080
1081 let mut keep: Vec<(usize, usize)> = self
1092 .graph
1093 .nodes()
1094 .iter()
1095 .filter_map(|node| {
1096 let id = node.id;
1097 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
1098 return None;
1099 }
1100 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
1101 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
1102 let off = self.arena.byte_offset(id);
1103 Some((off, off + nbytes))
1104 })
1105 .collect();
1106 keep.sort_unstable();
1107
1108 let buf = self.arena.raw_buf_mut();
1109 let len = buf.len();
1110 let mut cursor = 0usize;
1111 for (start, end) in keep {
1112 let start = start.min(len);
1113 if cursor < start {
1114 buf[cursor..start].fill(0);
1115 }
1116 cursor = cursor.max(end.min(len));
1117 }
1118 if cursor < len {
1119 buf[cursor..len].fill(0);
1120 }
1121 }
1122
1123 fn write_constant_to_arena(&mut self, id: NodeId, dtype: DType, data: &[u8]) {
1124 match dtype {
1125 DType::F64
1126 | DType::F16
1127 | DType::BF16
1128 | DType::U8
1129 | DType::I8
1130 | DType::Bool
1131 | DType::I64
1132 | DType::I32
1133 | DType::U32 => {
1134 let off = self.arena.byte_offset(id);
1135 let buf = self.arena.raw_buf_mut();
1136 let n = buf.len().saturating_sub(off).min(data.len());
1137 buf[off..off + n].copy_from_slice(&data[..n]);
1138 }
1139 _ => {
1140 let buf = self.arena.slice_mut(id);
1141 let n_floats = data.len() / 4;
1142 let n = buf.len().min(n_floats);
1143 for i in 0..n {
1144 let bytes = [
1145 data[i * 4],
1146 data[i * 4 + 1],
1147 data[i * 4 + 2],
1148 data[i * 4 + 3],
1149 ];
1150 buf[i] = f32::from_le_bytes(bytes);
1151 }
1152 }
1153 }
1154 }
1155
1156 fn set_param_bytes(&mut self, name: &str, data: &[u8], _dtype: rlx_ir::DType) {
1162 self.write_param_bytes_to_arena(name, data);
1164 }
1165
1166 fn write_param_bytes_to_arena(&mut self, name: &str, data: &[u8]) {
1167 if let Some(&id) = self.param_ids.get(name)
1168 && self.arena.has_buffer(id)
1169 {
1170 let off = self.arena.byte_offset(id);
1171 let buf = self.arena.raw_buf_mut();
1172 debug_assert!(
1173 off + data.len() <= buf.len(),
1174 "set_param_bytes: '{name}' would overflow arena slot"
1175 );
1176 buf[off..off + data.len()].copy_from_slice(data);
1177 }
1178 }
1179}