1use super::*;
2use rlx_cpu::{arena::Arena, thunk};
3use rlx_ir::{DType, NodeId, Op};
4use rlx_opt::memory::{self, MemoryPlan};
5use rlx_driver::arena::{read_typed_to_f32, write_typed_from_f32};
8
9pub struct CpuBackend;
10
11impl Backend for CpuBackend {
12 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
13 rlx_cpu::SUPPORTED_OPS
14 }
15
16 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
17 use rlx_opt::pass::Pass as _;
18 static ONNX_KERNELS: std::sync::Once = std::sync::Once::new();
19 ONNX_KERNELS.call_once(rlx_cpu::onnx_ref::register_onnx_reference_kernels);
20 let graph = rlx_opt::LowerControlFlow.run(graph);
26 let graph = rlx_opt::LowerSpectral.run(graph);
32 if let Err(errors) = rlx_opt::legalize_for_backend(&graph, rlx_cpu::SUPPORTED_OPS) {
36 panic!("{}", rlx_opt::format_legalize_error("cpu", &errors));
37 }
38 let policy = options.policy.clone();
39 let _precision = options.precision;
40 let cfg = rlx_cpu::config::RuntimeConfig::global();
41
42 let graph = crate::precompile::precompile_cleanup(graph, options);
43
44 let mut compile_opts = options.clone();
46 compile_opts.arena_alignment = cfg.arena_alignment;
47 let compile_result = crate::stages::compile_graph_stages_for_backend(
48 rlx_driver::Device::Cpu,
49 graph,
50 &compile_opts,
51 rlx_cpu::SUPPORTED_OPS,
52 );
53 crate::stages::maybe_log_fusion(&compile_result.fusion);
54 let fused = compile_result.lir.into_graph();
55
56 let fused = match policy {
59 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(fused),
60 None => fused,
61 };
62
63 let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&fused);
64 let exec_graph = if cpu_low_precision::needs_f32_exec(&fused) {
65 cpu_low_precision::promote_to_f32(fused)
66 } else {
67 fused
68 };
69
70 let plan = memory::plan_memory_aligned(&exec_graph, cfg.arena_alignment);
72 if cfg.verbose >= 1 {
73 eprintln!(
74 "[rlx] arena: {} bytes, {} buffers, alignment: {}",
75 plan.arena_size,
76 plan.assignments.len(),
77 cfg.arena_alignment
78 );
79 }
80 Box::new(build_cpu_executable(
81 exec_graph,
82 plan,
83 io_manifest,
84 options.rng,
85 ))
86 }
87
88 fn compile_lir(&self, lir: LirModule, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
89 #[cfg(not(target_arch = "wasm32"))]
91 let prof = std::env::var_os("RLX_PROFILE_COMPILE").is_some();
92 #[cfg(not(target_arch = "wasm32"))]
93 let tt = if prof {
94 Some(std::time::Instant::now())
95 } else {
96 None
97 };
98 let alignment = lir.buffers.alignment.max(options.arena_alignment);
99 let mut graph = lir.into_graph();
100 {
101 use rlx_opt::pass::Pass as _;
102 graph = rlx_opt::LegalizeBroadcast.run(graph);
103 }
104 if let Some(p) = options.policy.clone() {
105 use rlx_opt::pass::Pass;
106 graph = rlx_opt::AutoMixedPrecision::new(p).run(graph);
107 }
108 let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&graph);
109 let promote = cpu_low_precision::needs_f32_exec(&graph);
110 let exec_graph = if promote {
111 cpu_low_precision::promote_to_f32(graph)
112 } else {
113 graph
114 };
115 #[cfg(not(target_arch = "wasm32"))]
116 let t_prep = tt.map(|t| t.elapsed());
117 #[cfg(not(target_arch = "wasm32"))]
118 let t1 = if prof {
119 Some(std::time::Instant::now())
120 } else {
121 None
122 };
123 let plan = memory::plan_memory_aligned(&exec_graph, alignment);
126 #[cfg(not(target_arch = "wasm32"))]
127 let t_plan = t1.map(|t| t.elapsed());
128 #[cfg(not(target_arch = "wasm32"))]
129 if prof {
130 eprintln!(
131 "[compile_lir] {} nodes: into_graph+passes={:?} plan_memory={:?}",
132 exec_graph.nodes().len(),
133 t_prep.unwrap(),
134 t_plan.unwrap(),
135 );
136 }
137 let cfg = rlx_cpu::config::RuntimeConfig::global();
138 if cfg.verbose >= 1 {
139 eprintln!(
140 "[rlx] compile_lir: arena {} bytes ({} buffers, alignment {})",
141 plan.arena_size,
142 plan.assignments.len(),
143 alignment,
144 );
145 }
146 #[cfg(not(target_arch = "wasm32"))]
147 let t2 = if prof {
148 Some(std::time::Instant::now())
149 } else {
150 None
151 };
152 let exe = build_cpu_executable(exec_graph, plan, io_manifest, options.rng);
153 #[cfg(not(target_arch = "wasm32"))]
154 if let Some(t2) = t2 {
155 eprintln!("[compile_lir] build_thunks={:?}", t2.elapsed());
156 }
157 Box::new(exe)
158 }
159}
160
161fn build_cpu_executable(
162 graph: Graph,
163 plan: MemoryPlan,
164 io_manifest: cpu_low_precision::IoDtypeManifest,
165 rng: rlx_ir::RngOptions,
166) -> CpuExecutable {
167 let mut arena = Arena::from_plan(plan);
168 let mut input_ids = HashMap::new();
169 let mut param_ids = HashMap::new();
170 let mut node_dtypes: HashMap<NodeId, DType> = HashMap::new();
171 for node in graph.nodes() {
172 node_dtypes.insert(node.id, node.shape.dtype());
173 match &node.op {
174 Op::Input { name } => {
175 input_ids.insert(name.clone(), node.id);
176 }
177 Op::Param { name } => {
178 param_ids.insert(name.clone(), node.id);
179 }
180 _ => {}
181 }
182 }
183
184 let schedule = thunk::compile_thunks_with_rng(&graph, &arena, rng);
185
186 let mut input_slots = Vec::new();
187 for node in graph.nodes() {
188 if let Op::Input { name } = &node.op {
189 let off = arena.byte_offset(node.id);
190 let len = node.shape.num_elements().unwrap_or(0);
191 input_slots.push((name.clone(), off, len, node.shape.dtype()));
192 }
193 }
194
195 let output_slots: Vec<(usize, usize)> = graph
196 .outputs
197 .iter()
198 .map(|&id| {
199 let off = arena.byte_offset(id);
200 let len = graph.node(id).shape.num_elements().unwrap_or(0);
201 (off, len)
202 })
203 .collect();
204
205 for node in graph.nodes() {
206 if let Op::Constant { data } = &node.op
207 && arena.has_buffer(node.id)
208 && !data.is_empty()
209 {
210 match node.shape.dtype() {
211 DType::F64
221 | DType::F16
222 | DType::BF16
223 | DType::I64
224 | DType::I32
225 | DType::U32
226 | DType::Bool
227 | DType::U8
228 | DType::I8 => {
229 let off = arena.byte_offset(node.id);
230 let buf = arena.raw_buf_mut();
231 let n = buf.len().saturating_sub(off).min(data.len());
232 buf[off..off + n].copy_from_slice(&data[..n]);
233 }
234 _ => {
235 let buf = arena.slice_mut(node.id);
236 let n_floats = data.len() / 4;
237 let n = buf.len().min(n_floats);
238 for i in 0..n {
239 let bytes = [
240 data[i * 4],
241 data[i * 4 + 1],
242 data[i * 4 + 2],
243 data[i * 4 + 3],
244 ];
245 buf[i] = f32::from_le_bytes(bytes);
246 }
247 }
248 }
249 }
250 }
251
252 CpuExecutable {
253 graph,
254 arena,
255 input_ids,
256 param_ids,
257 node_dtypes,
258 io_manifest,
259 schedule,
260 input_slots,
261 output_slots,
262 handles: HashMap::new(),
263 active_extent: None,
264 moe_resident: None,
265 moe_resident_layers: None,
266 moe_topk_capture: None,
267 baseline_written: false,
268 }
269}
270
271#[derive(Clone)]
272struct CpuExecutable {
273 graph: Graph,
274 arena: Arena,
275 input_ids: HashMap<String, NodeId>,
276 param_ids: HashMap<String, NodeId>,
277 node_dtypes: HashMap<NodeId, DType>,
280 io_manifest: cpu_low_precision::IoDtypeManifest,
282 schedule: thunk::ThunkSchedule,
283 input_slots: Vec<(String, usize, usize, DType)>,
285 output_slots: Vec<(usize, usize)>,
287 handles: HashMap<String, Vec<f32>>,
292 active_extent: Option<(usize, usize)>,
298 moe_resident: Option<std::sync::Arc<[bool]>>,
299 moe_resident_layers: Option<std::sync::Arc<Vec<std::sync::Arc<[bool]>>>>,
300 moe_topk_capture: Option<std::sync::Arc<rlx_cpu::moe_topk_capture::MoeTopkCapture>>,
301 baseline_written: bool,
307}
308
309unsafe impl Send for CpuExecutable {}
310
311impl CpuExecutable {
312 fn dump_nodes_if_requested(&self) {
317 if !rlx_ir::env::flag("RLX_CPU_DUMP_NODES") {
318 return;
319 }
320 let limit = rlx_ir::env::parse_or("RLX_CPU_DUMP_NODES_LIMIT", 2000usize);
321 let flat_probe = rlx_ir::env::parse_or::<usize>("RLX_CPU_DUMP_FLAT", usize::MAX);
322 eprintln!("[rlx-cpu-dump] per-node max |x| (topo order, limit={limit})");
323 let buf = self.arena.raw_buf();
324 let mut shown = 0usize;
325 for (i, node) in self.graph.nodes().iter().enumerate() {
326 if !self.arena.has_buffer(node.id) {
327 continue;
328 }
329 if matches!(
330 node.op,
331 rlx_ir::Op::Input { .. }
332 | rlx_ir::Op::Param { .. }
333 | rlx_ir::Op::Constant { .. }
334 | rlx_ir::Op::Reshape { .. }
335 | rlx_ir::Op::Cast { .. }
336 ) {
337 continue;
338 }
339 if self
340 .node_dtypes
341 .get(&node.id)
342 .copied()
343 .unwrap_or(DType::F32)
344 != DType::F32
345 {
346 continue;
347 }
348 let off = self.arena.byte_offset(node.id);
349 let n = node.shape.num_elements().unwrap_or(0);
350 let data: &[f32] =
351 unsafe { std::slice::from_raw_parts(buf.as_ptr().add(off) as *const f32, n) };
352 let max = data.iter().fold(0f32, |m, &v| m.max(v.abs()));
353 let nz = data.iter().filter(|&&v| v != 0.0).count();
354 let flat_s = if flat_probe < data.len() {
355 format!(" flat[{flat_probe}]={:.6}", data[flat_probe])
356 } else {
357 String::new()
358 };
359 eprintln!(
360 " [{i:>3}] {:?} shape={:?} max={max:.6} nonzero={nz}/{}{flat_s}",
361 node.op,
362 node.shape.dims(),
363 data.len()
364 );
365 shown += 1;
366 if shown >= limit {
367 break;
368 }
369 }
370 }
371
372 fn write_input(&mut self, id: NodeId, data: &[f32]) {
374 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
375 let off = self.arena.byte_offset(id);
376 let buf = self.arena.raw_buf_mut();
377 let elem_size = dtype.size_bytes();
378 let max_elems = (buf.len() - off) / elem_size;
379 unsafe {
380 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
381 }
382 }
383
384 fn read_output(&self, id: NodeId) -> Vec<f32> {
386 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
387 let off = self.arena.byte_offset(id);
388 let buf = self.arena.raw_buf();
389 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
390 unsafe { read_typed_to_f32(buf.as_ptr().add(off), dtype, n_elems) }
391 }
392}
393
394impl ExecutableGraph for CpuExecutable {
395 fn capabilities(&self) -> crate::ExecutableCapabilities {
396 crate::ExecutableCapabilities {
397 clone: true,
398 moe: true,
399 typed_io: true,
400 active_extent: true,
401 ..crate::ExecutableCapabilities::NONE
402 }
403 }
404
405 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
406 Box::new(self.clone())
407 }
408 fn set_param(&mut self, name: &str, data: &[f32]) {
409 if let Some(&id) = self.param_ids.get(name)
414 && self.arena.has_buffer(id)
415 {
416 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
417 let off = self.arena.byte_offset(id);
418 let buf = self.arena.raw_buf_mut();
419 let elem_size = dtype.size_bytes();
420 let max_elems = (buf.len() - off) / elem_size;
421 unsafe {
422 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
423 }
424 }
425 }
426
427 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
428 self.restore_arena_baseline();
429 let handle_names: Vec<String> = self.handles.keys().cloned().collect();
432 for name in &handle_names {
433 if let Some(&id) = self.input_ids.get(name)
434 && self.arena.has_buffer(id)
435 {
436 let data = self.handles.get(name).cloned().unwrap_or_default();
437 self.write_input(id, &data);
438 }
439 }
440 for &(name, data) in inputs {
442 if let Some(&id) = self.input_ids.get(name)
443 && self.arena.has_buffer(id)
444 {
445 self.write_input(id, data);
446 }
447 }
448
449 let active_used = if let Some((actual, upper)) = self.active_extent {
454 thunk::execute_thunks_active(&self.schedule, self.arena.raw_buf_mut(), actual, upper)
455 } else {
456 false
457 };
458 if !active_used {
459 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
461 }
462
463 self.dump_nodes_if_requested();
464
465 for (idx, &out_id) in self.graph.outputs.iter().enumerate() {
469 let name = format!("out{idx}");
470 if self.handles.contains_key(&name) {
471 let v = self.read_output(out_id);
472 self.handles.insert(name, v);
473 }
474 }
475
476 self.graph
477 .outputs
478 .iter()
479 .map(|&out_id| self.read_output(out_id))
480 .collect()
481 }
482
483 fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
484 self.restore_arena_baseline();
485 for &(name, data) in inputs {
487 if let Some(&id) = self.input_ids.get(name)
488 && self.arena.has_buffer(id)
489 {
490 self.write_input(id, data);
491 }
492 }
493 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
494 self.graph
498 .outputs
499 .iter()
500 .map(|&out_id| {
501 let (ptr, len) = self.arena.raw_ptr(out_id);
502 (ptr as *const f32, len)
503 })
504 .collect()
505 }
506
507 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
511 self.restore_arena_baseline();
512 let buf = self.arena.raw_buf_mut();
513 for (i, &data) in inputs.iter().enumerate() {
514 if i < self.input_slots.len() {
515 let (_, off, max_len, dtype) = &self.input_slots[i];
516 unsafe {
517 write_typed_from_f32(buf.as_mut_ptr().add(*off), *dtype, data, *max_len);
518 }
519 }
520 }
521 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
522 &self.output_slots
523 }
524
525 fn arena_ptr(&self) -> *const u8 {
526 self.arena.raw_buf_mut_ptr()
527 }
528
529 fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
530 self.handles.insert(name.to_string(), data.to_vec());
535 true
536 }
537
538 fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
539 self.handles.get(name).cloned()
540 }
541
542 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
543 self.active_extent = extent;
544 }
545
546 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
547 *self.schedule.rng.write().unwrap() = rng;
548 }
549
550 fn rng(&self) -> rlx_ir::RngOptions {
551 *self.schedule.rng.read().unwrap()
552 }
553
554 fn set_moe_resident_experts(&mut self, mask: &[bool]) {
555 self.moe_resident_layers = None;
556 self.schedule.moe_resident_layers = None;
557 self.moe_resident = Some(Arc::from(mask));
558 self.schedule.moe_resident = self.moe_resident.clone();
559 }
560
561 fn set_moe_resident_experts_per_layer(&mut self, masks: &[&[bool]]) {
562 self.moe_resident = None;
563 self.schedule.moe_resident = None;
564 let layers: Vec<Arc<[bool]>> = masks.iter().map(|m| Arc::from(*m)).collect();
565 let arc = Arc::new(layers);
566 self.moe_resident_layers = Some(arc.clone());
567 self.schedule.moe_resident_layers = Some(arc);
568 }
569
570 fn enable_moe_topk_capture(&mut self, num_experts: usize) -> bool {
571 let cap = rlx_cpu::moe_topk_capture::MoeTopkCapture::new(num_experts);
572 self.moe_topk_capture = Some(cap.clone());
573 self.schedule.moe_topk_capture = Some(cap);
574 true
575 }
576
577 fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
578 let cap = self.moe_topk_capture.as_ref()?;
579 let layers = cap.take_layers();
580 if layers.is_empty() {
581 None
582 } else {
583 Some(layers)
584 }
585 }
586
587 fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
588 rlx_cpu::moe_residency::take_last_forward_stats()
589 }
590
591 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
597 if matches!(dtype, DType::F64 | DType::I64 | DType::I32 | DType::U32) {
598 self.set_param_bytes(name, data, dtype);
599 return;
600 }
601 if matches!(dtype, DType::U8 | DType::I8) {
605 self.set_param_bytes(name, data, dtype);
606 return;
607 }
608 if dtype == DType::F32 {
609 let n = data.len() / 4;
610 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
611 self.set_param(name, s);
612 } else {
613 let f32_buf = super::widen_bytes_to_f32(data, dtype);
614 self.set_param(name, &f32_buf);
615 }
616 }
617
618 fn run_typed(
630 &mut self,
631 inputs: &[(&str, &[u8], rlx_ir::DType)],
632 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
633 let all_f64 = !inputs.is_empty() && inputs.iter().all(|(_, _, dt)| *dt == DType::F64);
638
639 if all_f64 {
640 for (name, data, _) in inputs {
641 if let Some(&id) = self.input_ids.get(*name) {
642 if !self.arena.has_buffer(id) {
643 continue;
644 }
645 let off = self.arena.byte_offset(id);
646 let buf = self.arena.raw_buf_mut();
647 let n = data.len();
648 debug_assert!(
649 off + n <= buf.len(),
650 "run_typed: input '{name}' overflows arena slot"
651 );
652 buf[off..off + n].copy_from_slice(data);
653 }
654 }
655 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
656 } else {
657 let mut f32_owned: Vec<(String, Vec<f32>)> = Vec::new();
662 for (name, data, dt) in inputs {
663 let direct = matches!(
664 *dt,
665 DType::F64 | DType::I32 | DType::I64 | DType::U32 | DType::C64
666 );
667 if direct {
668 if let Some(&id) = self.input_ids.get(*name) {
669 if !self.arena.has_buffer(id) {
670 continue;
671 }
672 let off = self.arena.byte_offset(id);
673 let buf = self.arena.raw_buf_mut();
674 buf[off..off + data.len()].copy_from_slice(data);
675 }
676 } else {
677 let v = super::widen_bytes_to_f32(data, *dt);
678 f32_owned.push((name.to_string(), v));
679 }
680 }
681 for (name, data) in &f32_owned {
682 if let Some(&id) = self.input_ids.get(name.as_str()) {
683 if self.arena.has_buffer(id) {
684 self.write_input(id, data);
685 }
686 }
687 }
688 let active_used = if let Some((actual, upper)) = self.active_extent {
689 thunk::execute_thunks_active(
690 &self.schedule,
691 self.arena.raw_buf_mut(),
692 actual,
693 upper,
694 )
695 } else {
696 false
697 };
698 if !active_used {
699 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
700 }
701 }
702
703 self.dump_nodes_if_requested();
704
705 self.graph
707 .outputs
708 .iter()
709 .enumerate()
710 .map(|(idx, &id)| {
711 let exec_dtype = self.graph.node(id).shape.dtype();
712 let declared = self.io_manifest.output_dtype(idx, exec_dtype);
713 if matches!(
714 exec_dtype,
715 DType::F64
716 | DType::F16
717 | DType::BF16
718 | DType::I32
719 | DType::I64
720 | DType::U32
721 | DType::C64
722 ) {
723 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
724 let n_bytes = n_elems * exec_dtype.size_bytes();
725 let off = self.arena.byte_offset(id);
726 let bytes = self.arena.raw_buf()[off..off + n_bytes].to_vec();
727 return (bytes, declared);
728 }
729 let f32_vals = self.read_output(id);
730 if declared != exec_dtype {
731 return (super::narrow_f32_to_bytes(&f32_vals, declared), declared);
732 }
733 let bytes = f32_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
734 (bytes, declared)
735 })
736 .collect()
737 }
738}
739
740impl CpuExecutable {
741 fn restore_arena_baseline(&mut self) {
751 let persistent: std::collections::HashSet<NodeId> = {
753 let mut s: std::collections::HashSet<NodeId> =
754 self.param_ids.values().copied().collect();
755 for node in self.graph.nodes() {
756 if matches!(node.op, Op::Constant { .. }) {
757 s.insert(node.id);
758 }
759 }
760 s
761 };
762
763 if !self.baseline_written {
766 let constants: Vec<(NodeId, DType, Vec<u8>)> = self
767 .graph
768 .nodes()
769 .iter()
770 .filter_map(|node| {
771 if let Op::Constant { data } = &node.op
772 && self.arena.has_buffer(node.id)
773 && !data.is_empty()
774 {
775 Some((node.id, node.shape.dtype(), data.clone()))
776 } else {
777 None
778 }
779 })
780 .collect();
781 for (id, dtype, data) in constants {
782 self.write_constant_to_arena(id, dtype, &data);
783 }
784 self.baseline_written = true;
785 }
786
787 let mut keep: Vec<(usize, usize)> = self
798 .graph
799 .nodes()
800 .iter()
801 .filter_map(|node| {
802 let id = node.id;
803 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
804 return None;
805 }
806 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
807 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
808 let off = self.arena.byte_offset(id);
809 Some((off, off + nbytes))
810 })
811 .collect();
812 keep.sort_unstable();
813
814 let buf = self.arena.raw_buf_mut();
815 let len = buf.len();
816 let mut cursor = 0usize;
817 for (start, end) in keep {
818 let start = start.min(len);
819 if cursor < start {
820 buf[cursor..start].fill(0);
821 }
822 cursor = cursor.max(end.min(len));
823 }
824 if cursor < len {
825 buf[cursor..len].fill(0);
826 }
827 }
828
829 fn write_constant_to_arena(&mut self, id: NodeId, dtype: DType, data: &[u8]) {
830 match dtype {
831 DType::F64
832 | DType::F16
833 | DType::BF16
834 | DType::U8
835 | DType::I8
836 | DType::Bool
837 | DType::I64
838 | DType::I32
839 | DType::U32 => {
840 let off = self.arena.byte_offset(id);
841 let buf = self.arena.raw_buf_mut();
842 let n = buf.len().saturating_sub(off).min(data.len());
843 buf[off..off + n].copy_from_slice(&data[..n]);
844 }
845 _ => {
846 let buf = self.arena.slice_mut(id);
847 let n_floats = data.len() / 4;
848 let n = buf.len().min(n_floats);
849 for i in 0..n {
850 let bytes = [
851 data[i * 4],
852 data[i * 4 + 1],
853 data[i * 4 + 2],
854 data[i * 4 + 3],
855 ];
856 buf[i] = f32::from_le_bytes(bytes);
857 }
858 }
859 }
860 }
861
862 fn set_param_bytes(&mut self, name: &str, data: &[u8], _dtype: rlx_ir::DType) {
868 self.write_param_bytes_to_arena(name, data);
870 }
871
872 fn write_param_bytes_to_arena(&mut self, name: &str, data: &[u8]) {
873 if let Some(&id) = self.param_ids.get(name)
874 && self.arena.has_buffer(id)
875 {
876 let off = self.arena.byte_offset(id);
877 let buf = self.arena.raw_buf_mut();
878 debug_assert!(
879 off + data.len() <= buf.len(),
880 "set_param_bytes: '{name}' would overflow arena slot"
881 );
882 buf[off..off + data.len()].copy_from_slice(data);
883 }
884 }
885}