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 storage_dtype(&self, id: NodeId) -> DType {
547 let declared = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
548 if !matches!(declared, DType::F16 | DType::BF16) {
549 return declared;
550 }
551 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
552 if n_elems == 0 {
553 return declared;
554 }
555 match self.arena.byte_size(id) / n_elems {
556 2 => declared, _ => DType::F32, }
559 }
560
561 fn write_input(&mut self, id: NodeId, data: &[f32]) {
562 let dtype = self.storage_dtype(id);
563 let off = self.arena.byte_offset(id);
564 let buf = self.arena.raw_buf_mut();
565 let elem_size = dtype.size_bytes();
566 let max_elems = (buf.len() - off) / elem_size;
567 unsafe {
568 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
569 }
570 }
571
572 fn read_output(&self, id: NodeId) -> Vec<f32> {
575 let dtype = self.storage_dtype(id);
576 let off = self.arena.byte_offset(id);
577 let buf = self.arena.raw_buf();
578 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
579 unsafe { read_typed_to_f32(buf.as_ptr().add(off), dtype, n_elems) }
580 }
581}
582
583impl ExecutableGraph for CpuExecutable {
584 fn capabilities(&self) -> crate::ExecutableCapabilities {
585 crate::ExecutableCapabilities {
586 clone: true,
587 moe: true,
588 persistent_handles: true,
591 typed_io: true,
592 active_extent: true,
593 ..crate::ExecutableCapabilities::NONE
594 }
595 }
596
597 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
598 Box::new(self.clone())
599 }
600 fn set_param(&mut self, name: &str, data: &[f32]) {
601 if let Some(&id) = self.param_ids.get(name)
606 && self.arena.has_buffer(id)
607 {
608 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
609 let off = self.arena.byte_offset(id);
610 let buf = self.arena.raw_buf_mut();
611 let elem_size = dtype.size_bytes();
612 let max_elems = (buf.len() - off) / elem_size;
613 unsafe {
614 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
615 }
616 }
617 }
618
619 fn set_param_range(&mut self, name: &str, byte_offset: usize, data: &[u8]) -> bool {
620 let Some(&id) = self.param_ids.get(name) else {
627 return false;
628 };
629 if !self.arena.has_buffer(id) {
630 return false;
631 }
632 if self.node_dtypes.get(&id).copied().unwrap_or(DType::F32) != DType::F32 {
637 return false;
638 }
639 let size = self.arena.byte_size(id);
643 let Some(end) = byte_offset.checked_add(data.len()) else {
644 return false;
645 };
646 if end > size {
647 return false;
648 }
649 let off = self.arena.byte_offset(id);
650 self.arena.raw_buf_mut()[off + byte_offset..off + end].copy_from_slice(data);
651 true
652 }
653
654 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
655 self.restore_arena_baseline();
656 let handle_names: Vec<String> = self.handles.keys().cloned().collect();
659 for name in &handle_names {
660 if let Some(&id) = self.input_ids.get(name)
661 && self.arena.has_buffer(id)
662 {
663 let data = self.handles.get(name).cloned().unwrap_or_default();
664 self.write_input(id, &data);
665 }
666 }
667 for &(name, data) in inputs {
669 if let Some(&id) = self.input_ids.get(name)
670 && self.arena.has_buffer(id)
671 {
672 self.write_input(id, data);
673 }
674 }
675
676 let active_used = if let Some((actual, upper)) = self.active_extent {
681 thunk::execute_thunks_active(&self.schedule, self.arena.raw_buf_mut(), actual, upper)
682 } else {
683 false
684 };
685 if !active_used {
686 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
688 }
689
690 self.dump_nodes_if_requested();
691
692 for (idx, &out_id) in self.graph.outputs.iter().enumerate() {
696 let name = format!("out{idx}");
697 if self.handles.contains_key(&name) {
698 let v = self.read_output(out_id);
699 self.handles.insert(name, v);
700 }
701 }
702
703 self.graph
704 .outputs
705 .iter()
706 .map(|&out_id| self.read_output(out_id))
707 .collect()
708 }
709
710 fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
711 self.restore_arena_baseline();
712 for &(name, data) in inputs {
714 if let Some(&id) = self.input_ids.get(name)
715 && self.arena.has_buffer(id)
716 {
717 self.write_input(id, data);
718 }
719 }
720 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
721 self.graph
725 .outputs
726 .iter()
727 .map(|&out_id| {
728 let (ptr, len) = self.arena.raw_ptr(out_id);
729 (ptr as *const f32, len)
730 })
731 .collect()
732 }
733
734 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
738 self.restore_arena_baseline();
739 let buf = self.arena.raw_buf_mut();
740 for (i, &data) in inputs.iter().enumerate() {
741 if i < self.input_slots.len() {
742 let (_, off, max_len, dtype) = &self.input_slots[i];
743 unsafe {
744 write_typed_from_f32(buf.as_mut_ptr().add(*off), *dtype, data, *max_len);
745 }
746 }
747 }
748 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
749 &self.output_slots
750 }
751
752 fn arena_ptr(&self) -> *const u8 {
753 self.arena.raw_buf_mut_ptr()
754 }
755
756 fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
757 self.handles.insert(name.to_string(), data.to_vec());
762 true
763 }
764
765 fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
766 self.handles.get(name).cloned()
767 }
768
769 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
770 self.active_extent = extent;
771 }
772
773 fn release_scratch(&mut self) {
774 self.release_scratch_pages();
775 }
776
777 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
778 *self.schedule.rng.write().unwrap() = rng;
779 }
780
781 fn rng(&self) -> rlx_ir::RngOptions {
782 *self.schedule.rng.read().unwrap()
783 }
784
785 fn set_moe_resident_experts(&mut self, mask: &[bool]) {
786 self.moe_resident_layers = None;
787 self.schedule.moe_resident_layers = None;
788 self.moe_resident = Some(Arc::from(mask));
789 self.schedule.moe_resident = self.moe_resident.clone();
790 }
791
792 fn set_moe_resident_experts_per_layer(&mut self, masks: &[&[bool]]) {
793 self.moe_resident = None;
794 self.schedule.moe_resident = None;
795 let layers: Vec<Arc<[bool]>> = masks.iter().map(|m| Arc::from(*m)).collect();
796 let arc = Arc::new(layers);
797 self.moe_resident_layers = Some(arc.clone());
798 self.schedule.moe_resident_layers = Some(arc);
799 }
800
801 fn enable_moe_topk_capture(&mut self, num_experts: usize) -> bool {
802 let cap = rlx_cpu::moe_topk_capture::MoeTopkCapture::new(num_experts);
803 self.moe_topk_capture = Some(cap.clone());
804 self.schedule.moe_topk_capture = Some(cap);
805 true
806 }
807
808 fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
809 let cap = self.moe_topk_capture.as_ref()?;
810 let layers = cap.take_layers();
811 if layers.is_empty() {
812 None
813 } else {
814 Some(layers)
815 }
816 }
817
818 fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
819 rlx_cpu::moe_residency::take_last_forward_stats()
820 }
821
822 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
828 if matches!(dtype, DType::F64 | DType::I64 | DType::I32 | DType::U32) {
829 self.set_param_bytes(name, data, dtype);
830 return;
831 }
832 if matches!(dtype, DType::U8 | DType::I8) {
836 self.set_param_bytes(name, data, dtype);
837 return;
838 }
839 if dtype == DType::F32 {
840 let s = rlx_ir::bytes::decode_le::<f32>(data);
841 self.set_param(name, s.as_ref());
842 } else {
843 let f32_buf = super::widen_bytes_to_f32(data, dtype);
844 self.set_param(name, &f32_buf);
845 }
846 }
847
848 fn run_typed(
860 &mut self,
861 inputs: &[(&str, &[u8], rlx_ir::DType)],
862 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
863 let all_f64 = !inputs.is_empty() && inputs.iter().all(|(_, _, dt)| *dt == DType::F64);
868
869 if all_f64 {
870 for (name, data, _) in inputs {
871 if let Some(&id) = self.input_ids.get(*name) {
872 if !self.arena.has_buffer(id) {
873 continue;
874 }
875 let off = self.arena.byte_offset(id);
876 let buf = self.arena.raw_buf_mut();
877 let n = data.len();
878 debug_assert!(
879 off + n <= buf.len(),
880 "run_typed: input '{name}' overflows arena slot"
881 );
882 buf[off..off + n].copy_from_slice(data);
883 }
884 }
885 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
886 } else {
887 let mut f32_owned: Vec<(String, Vec<f32>)> = Vec::new();
891 for (name, data, dt) in inputs {
892 let direct = matches!(
893 *dt,
894 DType::F64 | DType::I32 | DType::I64 | DType::U32 | DType::C64 | DType::C128
895 );
896 if direct {
897 if let Some(&id) = self.input_ids.get(*name) {
898 if !self.arena.has_buffer(id) {
899 continue;
900 }
901 let off = self.arena.byte_offset(id);
902 let buf = self.arena.raw_buf_mut();
903 buf[off..off + data.len()].copy_from_slice(data);
904 }
905 } else {
906 let v = super::widen_bytes_to_f32(data, *dt);
907 f32_owned.push((name.to_string(), v));
908 }
909 }
910 for (name, data) in &f32_owned {
911 if let Some(&id) = self.input_ids.get(name.as_str()) {
912 if self.arena.has_buffer(id) {
913 self.write_input(id, data);
914 }
915 }
916 }
917 let active_used = if let Some((actual, upper)) = self.active_extent {
918 thunk::execute_thunks_active(
919 &self.schedule,
920 self.arena.raw_buf_mut(),
921 actual,
922 upper,
923 )
924 } else {
925 false
926 };
927 if !active_used {
928 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
929 }
930 }
931
932 self.dump_nodes_if_requested();
933
934 self.graph
936 .outputs
937 .iter()
938 .enumerate()
939 .map(|(idx, &id)| {
940 let exec_dtype = self.graph.node(id).shape.dtype();
941 let declared = self.io_manifest.output_dtype(idx, exec_dtype);
942 if matches!(
950 exec_dtype,
951 DType::F64 | DType::I32 | DType::I64 | DType::U32 | DType::C64 | DType::C128
952 ) {
953 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
954 let n_bytes = n_elems * exec_dtype.size_bytes();
955 let off = self.arena.byte_offset(id);
956 let bytes = self.arena.raw_buf()[off..off + n_bytes].to_vec();
957 return (bytes, declared);
958 }
959 let f32_vals = self.read_output(id);
960 if declared != exec_dtype || matches!(declared, DType::F16 | DType::BF16) {
961 return (super::narrow_f32_to_bytes(&f32_vals, declared), declared);
962 }
963 let bytes = f32_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
964 (bytes, declared)
965 })
966 .collect()
967 }
968}
969
970impl CpuExecutable {
971 #[cfg(unix)]
986 fn persistent_keep_ranges(&self) -> Vec<(usize, usize)> {
987 let persistent: std::collections::HashSet<NodeId> = {
988 let mut s: std::collections::HashSet<NodeId> =
989 self.param_ids.values().copied().collect();
990 for node in self.graph.nodes() {
991 if matches!(node.op, Op::Constant { .. }) {
992 s.insert(node.id);
993 }
994 }
995 s
996 };
997 let mut keep: Vec<(usize, usize)> = self
998 .graph
999 .nodes()
1000 .iter()
1001 .filter_map(|node| {
1002 let id = node.id;
1003 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
1004 return None;
1005 }
1006 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
1007 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
1008 let off = self.arena.byte_offset(id);
1009 Some((off, off + nbytes))
1010 })
1011 .collect();
1012 keep.sort_unstable();
1013 keep
1014 }
1015
1016 #[allow(unused_variables)]
1027 fn release_scratch_pages(&self) {
1028 #[cfg(unix)]
1029 {
1030 const PAGE: usize = 16 * 1024;
1033 let keep = self.persistent_keep_ranges();
1034 let base = self.arena.raw_buf_mut_ptr() as usize;
1035 let total = self.arena.size();
1036 let mut cursor = 0usize;
1038 let mut spans: Vec<(usize, usize)> = Vec::new();
1039 for &(s, e) in &keep {
1040 let s = s.min(total);
1041 if cursor < s {
1042 spans.push((cursor, s));
1043 }
1044 cursor = cursor.max(e.min(total));
1045 }
1046 if cursor < total {
1047 spans.push((cursor, total));
1048 }
1049 for (s, e) in spans {
1050 let a = (base + s).div_ceil(PAGE) * PAGE;
1051 let b = (base + e) / PAGE * PAGE;
1052 if b > a {
1053 #[cfg(target_os = "macos")]
1061 const ADVICE: i32 = 7;
1062 #[cfg(not(target_os = "macos"))]
1063 const ADVICE: i32 = 4;
1064 let advice = rlx_ir::env::var("RLX_MADV")
1065 .and_then(|s| s.parse().ok())
1066 .unwrap_or(ADVICE);
1067 unsafe { madvise(a as *mut core::ffi::c_void, b - a, advice) };
1068 }
1069 }
1070 }
1071 }
1072
1073 fn restore_arena_baseline(&mut self) {
1074 let persistent: std::collections::HashSet<NodeId> = {
1076 let mut s: std::collections::HashSet<NodeId> =
1077 self.param_ids.values().copied().collect();
1078 for node in self.graph.nodes() {
1079 if matches!(node.op, Op::Constant { .. }) {
1080 s.insert(node.id);
1081 }
1082 }
1083 s
1084 };
1085
1086 if !self.baseline_written {
1089 let constants: Vec<(NodeId, DType, Vec<u8>)> = self
1090 .graph
1091 .nodes()
1092 .iter()
1093 .filter_map(|node| {
1094 if let Op::Constant { data } = &node.op
1095 && self.arena.has_buffer(node.id)
1096 && !data.is_empty()
1097 {
1098 Some((node.id, node.shape.dtype(), data.clone()))
1099 } else {
1100 None
1101 }
1102 })
1103 .collect();
1104 for (id, dtype, data) in constants {
1105 self.write_constant_to_arena(id, dtype, &data);
1106 }
1107 self.baseline_written = true;
1108 }
1109
1110 let mut keep: Vec<(usize, usize)> = self
1121 .graph
1122 .nodes()
1123 .iter()
1124 .filter_map(|node| {
1125 let id = node.id;
1126 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
1127 return None;
1128 }
1129 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
1130 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
1131 let off = self.arena.byte_offset(id);
1132 Some((off, off + nbytes))
1133 })
1134 .collect();
1135 keep.sort_unstable();
1136
1137 let buf = self.arena.raw_buf_mut();
1138 let len = buf.len();
1139 let mut cursor = 0usize;
1140 for (start, end) in keep {
1141 let start = start.min(len);
1142 if cursor < start {
1143 buf[cursor..start].fill(0);
1144 }
1145 cursor = cursor.max(end.min(len));
1146 }
1147 if cursor < len {
1148 buf[cursor..len].fill(0);
1149 }
1150 }
1151
1152 fn write_constant_to_arena(&mut self, id: NodeId, dtype: DType, data: &[u8]) {
1153 match dtype {
1154 DType::F64
1155 | DType::F16
1156 | DType::BF16
1157 | DType::U8
1158 | DType::I8
1159 | DType::Bool
1160 | DType::I64
1161 | DType::I32
1162 | DType::U32 => {
1163 let off = self.arena.byte_offset(id);
1164 let buf = self.arena.raw_buf_mut();
1165 let n = buf.len().saturating_sub(off).min(data.len());
1166 buf[off..off + n].copy_from_slice(&data[..n]);
1167 }
1168 _ => {
1169 let buf = self.arena.slice_mut(id);
1170 let n_floats = data.len() / 4;
1171 let n = buf.len().min(n_floats);
1172 for i in 0..n {
1173 let bytes = [
1174 data[i * 4],
1175 data[i * 4 + 1],
1176 data[i * 4 + 2],
1177 data[i * 4 + 3],
1178 ];
1179 buf[i] = f32::from_le_bytes(bytes);
1180 }
1181 }
1182 }
1183 }
1184
1185 fn set_param_bytes(&mut self, name: &str, data: &[u8], _dtype: rlx_ir::DType) {
1191 self.write_param_bytes_to_arena(name, data);
1193 }
1194
1195 fn write_param_bytes_to_arena(&mut self, name: &str, data: &[u8]) {
1196 if let Some(&id) = self.param_ids.get(name)
1197 && self.arena.has_buffer(id)
1198 {
1199 let off = self.arena.byte_offset(id);
1200 let buf = self.arena.raw_buf_mut();
1201 debug_assert!(
1202 off + data.len() <= buf.len(),
1203 "set_param_bytes: '{name}' would overflow arena slot"
1204 );
1205 buf[off..off + data.len()].copy_from_slice(data);
1206 }
1207 }
1208}