1use crate::CompileOptions;
23use rlx_ir::Graph;
24use rlx_ir::hir::HirModule;
25use rlx_ir::lir::LirModule;
26use std::collections::HashMap;
27use std::sync::Arc;
28
29use crate::cpu_low_precision;
30
31#[allow(dead_code)]
38pub(crate) fn widen_bytes_to_f32(data: &[u8], dtype: rlx_ir::DType) -> Vec<f32> {
39 use rlx_ir::DType;
40 match dtype {
41 DType::F32 => {
42 let n = data.len() / 4;
43 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
44 s.to_vec()
45 }
46 DType::F16 => {
47 let n = data.len() / 2;
48 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const half::f16, n) };
49 s.iter().map(|h| h.to_f32()).collect()
50 }
51 DType::BF16 => {
52 let n = data.len() / 2;
53 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const half::bf16, n) };
54 s.iter().map(|h| h.to_f32()).collect()
55 }
56 other => panic!(
57 "widen_bytes_to_f32: dtype {other:?} unsupported on f32-arena backends \
58 (only F32/F16/BF16 are accepted on the host I/O surface)"
59 ),
60 }
61}
62
63#[allow(dead_code)]
68pub(crate) fn narrow_f32_to_bytes(v: &[f32], dt: rlx_ir::DType) -> Vec<u8> {
69 use rlx_ir::DType;
70 match dt {
71 DType::F32 => {
72 let mut bytes = Vec::with_capacity(v.len() * 4);
73 for &x in v {
74 bytes.extend_from_slice(&x.to_le_bytes());
75 }
76 bytes
77 }
78 DType::F16 => {
79 let mut bytes = Vec::with_capacity(v.len() * 2);
80 for &x in v {
81 bytes.extend_from_slice(&half::f16::from_f32(x).to_le_bytes());
82 }
83 bytes
84 }
85 DType::BF16 => {
86 let mut bytes = Vec::with_capacity(v.len() * 2);
87 for &x in v {
88 bytes.extend_from_slice(&half::bf16::from_f32(x).to_le_bytes());
89 }
90 bytes
91 }
92 DType::F64 => {
93 let mut bytes = Vec::with_capacity(v.len() * 8);
94 for &x in v {
95 bytes.extend_from_slice(&(x as f64).to_le_bytes());
96 }
97 bytes
98 }
99 DType::I8 => v.iter().map(|&x| x as i8 as u8).collect(),
100 DType::U8 => v.iter().map(|&x| x as u8).collect(),
101 DType::I16 => {
102 let mut bytes = Vec::with_capacity(v.len() * 2);
103 for &x in v {
104 bytes.extend_from_slice(&(x as i16).to_le_bytes());
105 }
106 bytes
107 }
108 DType::I32 => {
109 let mut bytes = Vec::with_capacity(v.len() * 4);
110 for &x in v {
111 bytes.extend_from_slice(&(x as i32).to_le_bytes());
112 }
113 bytes
114 }
115 DType::U32 => {
116 let mut bytes = Vec::with_capacity(v.len() * 4);
117 for &x in v {
118 bytes.extend_from_slice(&(x as u32).to_le_bytes());
119 }
120 bytes
121 }
122 DType::I64 => {
123 let mut bytes = Vec::with_capacity(v.len() * 8);
124 for &x in v {
125 bytes.extend_from_slice(&(x as i64).to_le_bytes());
126 }
127 bytes
128 }
129 DType::Bool => v
130 .iter()
131 .map(|&x| if x != 0.0 { 1u8 } else { 0u8 })
132 .collect(),
133 DType::C64 => {
134 let mut bytes = Vec::with_capacity(v.len() * 8);
138 for &x in v {
139 bytes.extend_from_slice(&x.to_le_bytes());
140 bytes.extend_from_slice(&0.0_f32.to_le_bytes());
141 }
142 bytes
143 }
144 }
145}
146
147pub trait ExecutableGraph: Send {
149 fn set_param(&mut self, name: &str, data: &[f32]);
151
152 fn finalize_params(&mut self) {}
155
156 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
163 panic!("clone_box not implemented for this backend");
164 }
165
166 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>>;
168
169 fn run_read_outputs(
172 &mut self,
173 inputs: &[(&str, &[f32])],
174 read_indices: Option<&[usize]>,
175 ) -> Vec<Vec<f32>> {
176 match read_indices {
177 None => self.run(inputs),
178 Some(ix) => {
179 let all = self.run(inputs);
182 ix.iter().filter_map(|&i| all.get(i).cloned()).collect()
183 }
184 }
185 }
186
187 fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
189 let vecs = self.run(inputs);
190 vecs.iter().map(|v| (v.as_ptr(), v.len())).collect()
191 }
192
193 fn run_slots(&mut self, _inputs: &[&[f32]]) -> &[(usize, usize)] {
196 &[] }
198
199 fn arena_ptr(&self) -> *const u8 {
201 std::ptr::null()
202 }
203
204 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
221 let _ = extent;
222 }
223
224 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
226 let _ = rng;
227 }
228
229 fn rng(&self) -> rlx_ir::RngOptions {
231 rlx_ir::RngOptions::default()
232 }
233
234 fn set_moe_resident_experts(&mut self, _mask: &[bool]) {}
236
237 fn set_moe_resident_experts_per_layer(&mut self, _masks: &[&[bool]]) {}
239
240 fn enable_moe_topk_capture(&mut self, _num_experts: usize) -> bool {
242 false
243 }
244
245 fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
247 None
248 }
249
250 fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
252 None
253 }
254
255 fn bind_handle(&mut self, _name: &str, _data: &[f32]) -> bool {
259 false
260 }
261
262 fn read_handle(&self, _name: &str) -> Option<Vec<f32>> {
264 None
265 }
266
267 fn bind_gpu_handle(&mut self, _name: &str, _data: &[f32]) -> bool {
269 false
270 }
271
272 fn has_gpu_handle(&self, _name: &str) -> bool {
273 false
274 }
275
276 fn set_gpu_handle_feed(&mut self, _handle_name: &str, _output_index: usize) -> bool {
277 false
278 }
279
280 fn read_gpu_handle(&self, _name: &str) -> Option<Vec<f32>> {
281 None
282 }
283
284 fn register_kv_row_feed(&mut self, _handle_name: &str, _output_index: usize) -> bool {
288 false
289 }
290
291 fn feed_kv_row(&mut self, _src_row: usize, _dst_row: usize, _row_elems: usize) -> bool {
296 false
297 }
298
299 fn read_output_row(&self, _out_idx: usize, _row: usize, _row_inner: usize) -> Option<Vec<f32>> {
302 None
303 }
304
305 fn run_feed_gpu_handle(
307 &mut self,
308 inputs: &[(&str, &[f32])],
309 _handle_name: &str,
310 _output_index: usize,
311 ) -> Option<Vec<f32>> {
312 let _ = inputs;
313 None
314 }
315
316 fn commit_no_wait(&mut self, inputs: &[(&str, &[f32])]) {
331 let _ = self.run(inputs);
332 }
333
334 fn sync_pending(&mut self) {}
337
338 fn run_pipelined(&mut self, input_sets: &[Vec<(&str, &[f32])>]) -> Vec<Vec<Vec<f32>>> {
347 input_sets.iter().map(|inputs| self.run(inputs)).collect()
348 }
349
350 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
363 if dtype != rlx_ir::DType::F32 {
364 panic!(
365 "backend's default set_param_typed only handles F32; \
366 got {dtype:?}. Override on the backend for typed support."
367 );
368 }
369 if !data.len().is_multiple_of(4) {
370 panic!(
371 "set_param_typed F32: data length {} not a multiple of 4",
372 data.len()
373 );
374 }
375 let n = data.len() / 4;
380 let f32_slice = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
381 self.set_param(name, f32_slice);
382 }
383
384 fn run_typed(
388 &mut self,
389 inputs: &[(&str, &[u8], rlx_ir::DType)],
390 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
391 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
394 for (name, data, dt) in inputs {
395 if *dt != rlx_ir::DType::F32 {
396 panic!(
397 "backend's default run_typed only handles F32 inputs; \
398 got {dt:?} for input '{name}'"
399 );
400 }
401 if data.len() % 4 != 0 {
402 panic!(
403 "run_typed F32 input '{name}': len {} not multiple of 4",
404 data.len()
405 );
406 }
407 let n = data.len() / 4;
408 let v: Vec<f32> =
409 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }.to_vec();
410 owned.push((name.to_string(), v));
411 }
412 let refs: Vec<(&str, &[f32])> = owned
413 .iter()
414 .map(|(n, d)| (n.as_str(), d.as_slice()))
415 .collect();
416 let outs = self.run(&refs);
417 outs.into_iter()
418 .map(|v| {
419 let bytes =
420 unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4) }
421 .to_vec();
422 (bytes, rlx_ir::DType::F32)
423 })
424 .collect()
425 }
426}
427
428pub trait Backend: Send + Sync {
438 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph>;
440
441 fn compile_lir(&self, lir: LirModule, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
445 self.compile(lir.into_graph(), options)
446 }
447
448 fn compile_hir(
450 &self,
451 hir: HirModule,
452 device: rlx_driver::Device,
453 options: &CompileOptions,
454 ) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
455 let result = crate::stages::compile_hir_stages(device, hir, options)?;
456 crate::stages::maybe_log_fusion(&result.fusion);
457 Ok(self.compile_lir(result.lir, options))
458 }
459
460 fn compile_module(
462 &self,
463 module: rlx_ir::GraphModule,
464 device: rlx_driver::Device,
465 options: &CompileOptions,
466 ) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
467 let result = crate::stages::compile_module_stages(device, module, options)?;
468 crate::stages::maybe_log_fusion(&result.fusion);
469 Ok(self.compile_lir(result.lir, options))
470 }
471
472 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
479 &[]
480 }
481}
482
483#[allow(dead_code)]
486fn prepare_fused_graph(
487 graph: Graph,
488 options: &CompileOptions,
489 supported_ops: &[rlx_ir::OpKind],
490 backend_name: &str,
491) -> Graph {
492 let (mut graph, report) = rlx_opt::prepare_graph_for_backend_with_report(
493 graph,
494 backend_name,
495 supported_ops,
496 options.kernel_dispatch,
497 );
498 rlx_opt::maybe_log_dispatch_report(&report);
499 if !report.compile_ready {
500 panic!(
501 "{}\n{}",
502 rlx_opt::format_legalize_error(backend_name, &report.still_unsupported),
503 rlx_opt::format_dispatch_report(&report)
504 );
505 }
506 graph = crate::precompile::post_fusion_cleanup(graph, options);
507 if let Some(p) = options.policy.clone() {
508 use rlx_opt::pass::Pass as _;
509 graph = rlx_opt::AutoMixedPrecision::new(p).run(graph);
510 }
511 graph
512}
513
514#[allow(dead_code)]
515fn declared_output_dtypes(
516 manifest: &cpu_low_precision::IoDtypeManifest,
517 exec_dtypes: Vec<rlx_ir::DType>,
518) -> Vec<rlx_ir::DType> {
519 exec_dtypes
520 .into_iter()
521 .enumerate()
522 .map(|(i, exec)| manifest.output_dtype(i, exec))
523 .collect()
524}
525
526pub fn compile(backend: &dyn Backend, graph: Graph) -> Box<dyn ExecutableGraph> {
534 backend.compile(graph, &CompileOptions::default())
535}
536
537pub fn compile_hir(
539 backend: &dyn Backend,
540 hir: HirModule,
541 device: rlx_driver::Device,
542 options: &CompileOptions,
543) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
544 backend.compile_hir(hir, device, options)
545}
546
547pub fn compile_module(
549 backend: &dyn Backend,
550 module: rlx_ir::GraphModule,
551 device: rlx_driver::Device,
552 options: &CompileOptions,
553) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
554 backend.compile_module(module, device, options)
555}
556
557pub fn compile_with_precision(
559 backend: &dyn Backend,
560 graph: Graph,
561 precision: crate::Precision,
562) -> Box<dyn ExecutableGraph> {
563 backend.compile(graph, &CompileOptions::new().precision(precision))
564}
565
566fn _legacy_apply_policy(graph: Graph, policy: Option<rlx_opt::PrecisionPolicy>) -> Graph {
571 use rlx_opt::pass::Pass as _;
572 match policy {
573 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
574 None => graph,
575 }
576}
577
578#[cfg(feature = "cpu")]
581pub mod cpu_backend {
582 use super::*;
583 use rlx_cpu::{arena::Arena, thunk};
584 use rlx_ir::{DType, NodeId, Op};
585 use rlx_opt::memory::{self, MemoryPlan};
586 use rlx_driver::arena::{read_typed_to_f32, write_typed_from_f32};
589
590 pub struct CpuBackend;
591
592 const CPU_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
599 use rlx_ir::OpKind::*;
600 &[
601 Input,
602 Param,
603 Constant,
604 Activation,
605 Cast,
606 StopGradient,
607 Binary,
608 Compare,
609 Where,
610 Fma,
611 ElementwiseRegion,
612 MatMul,
613 DotGeneral,
614 DenseSolve,
615 BatchedDenseSolve,
616 Scan,
617 ScanBackward,
618 ScanBackwardXs,
619 LayerNorm,
620 LayerNorm2d,
621 GroupNorm,
622 BatchNormInference,
623 RmsNorm,
624 ResizeNearest2x,
625 AxialRope2d,
626 Attention,
627 Rope,
628 Reshape,
629 Transpose,
630 Narrow,
631 Concat,
632 Expand,
633 Gather,
634 Reverse,
635 Reduce,
636 Softmax,
637 Cumsum,
638 ArgMax,
639 ArgMin,
640 TopK,
641 Sample,
642 RngNormal,
643 RngUniform,
644 Conv,
645 Im2Col,
646 ConvTranspose2d,
647 Pool,
648 GroupedMatMul,
649 DequantGroupedMatMul,
650 DequantMoEWeights,
651 ScatterAdd,
652 LoraMatMul,
653 DequantMatMul,
654 ScaledMatMul,
655 ScaledQuantize,
656 ScaledQuantScale,
657 ScaledDequantize,
658 SelectiveScan,
659 GatedDeltaNet,
660 Lstm,
661 Gru,
662 Rnn,
663 Mamba2,
664 FusedSwiGLU,
665 FusedMatMulBiasAct,
666 FusedResidualLN,
667 FusedResidualRmsNorm,
668 FusedAttentionBlock,
669 ReluBackward,
674 ActivationBackward,
675 FakeQuantize,
676 FakeQuantizeBackward,
677 FakeQuantizeLSQ,
679 FakeQuantizeLSQBackwardX,
680 FakeQuantizeLSQBackwardScale,
681 MaxPool2dBackward,
682 Conv2dBackwardInput,
683 Conv2dBackwardWeight,
684 SoftmaxCrossEntropy,
685 SoftmaxCrossEntropyWithLogits,
686 SoftmaxCrossEntropyBackward,
687 AttentionBackward,
688 LayerNormBackwardInput,
689 LayerNormBackwardGamma,
690 BatchNormInferenceBackwardInput,
691 BatchNormInferenceBackwardGamma,
692 BatchNormInferenceBackwardBeta,
693 GroupNormBackwardInput,
695 GroupNormBackwardGamma,
696 GroupNormBackwardBeta,
697 RmsNormBackwardInput,
698 RmsNormBackwardGamma,
699 RmsNormBackwardBeta,
700 RopeBackward,
701 CumsumBackward,
702 GatherBackward,
703 GaussianSplatRender,
705 GaussianSplatRenderBackward,
706 GaussianSplatPrepare,
707 GaussianSplatRasterize,
708 Custom,
712 CustomFn,
716 Fft,
720 FftButterflyStage,
721 LogMel,
722 LogMelBackward,
723 WelchPeaks,
724 ComplexNormSq,
729 ComplexNormSqBackward,
730 Conjugate,
731 ]
732 };
733
734 impl Backend for CpuBackend {
735 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
736 CPU_SUPPORTED_OPS
737 }
738
739 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
740 use rlx_opt::pass::Pass as _;
741 static ONNX_KERNELS: std::sync::Once = std::sync::Once::new();
742 ONNX_KERNELS.call_once(rlx_cpu::onnx_ref::register_onnx_reference_kernels);
743 let graph = rlx_opt::LowerControlFlow.run(graph);
749 if let Err(errors) = rlx_opt::legalize_for_backend(&graph, CPU_SUPPORTED_OPS) {
753 panic!("{}", rlx_opt::format_legalize_error("cpu", &errors));
754 }
755 let policy = options.policy.clone();
756 let _precision = options.precision;
757 let cfg = rlx_cpu::config::RuntimeConfig::global();
758
759 let graph = crate::precompile::precompile_cleanup(graph, options);
760
761 let mut compile_opts = options.clone();
763 compile_opts.arena_alignment = cfg.arena_alignment;
764 let compile_result = crate::stages::compile_graph_stages_for_backend(
765 rlx_driver::Device::Cpu,
766 graph,
767 &compile_opts,
768 CPU_SUPPORTED_OPS,
769 );
770 crate::stages::maybe_log_fusion(&compile_result.fusion);
771 let fused = compile_result.lir.into_graph();
772
773 let fused = match policy {
776 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(fused),
777 None => fused,
778 };
779
780 let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&fused);
781 let exec_graph = if cpu_low_precision::needs_f32_exec(&fused) {
782 cpu_low_precision::promote_to_f32(fused)
783 } else {
784 fused
785 };
786
787 let plan = memory::plan_memory_aligned(&exec_graph, cfg.arena_alignment);
789 if cfg.verbose >= 1 {
790 eprintln!(
791 "[rlx] arena: {} bytes, {} buffers, alignment: {}",
792 plan.arena_size,
793 plan.assignments.len(),
794 cfg.arena_alignment
795 );
796 }
797 Box::new(build_cpu_executable(
798 exec_graph,
799 plan,
800 io_manifest,
801 options.rng,
802 ))
803 }
804
805 fn compile_lir(
806 &self,
807 lir: LirModule,
808 options: &CompileOptions,
809 ) -> Box<dyn ExecutableGraph> {
810 let alignment = lir.buffers.alignment.max(options.arena_alignment);
811 let mut graph = lir.into_graph();
812 {
813 use rlx_opt::pass::Pass as _;
814 graph = rlx_opt::LegalizeBroadcast.run(graph);
815 }
816 if let Some(p) = options.policy.clone() {
817 use rlx_opt::pass::Pass;
818 graph = rlx_opt::AutoMixedPrecision::new(p).run(graph);
819 }
820 let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&graph);
821 let promote = cpu_low_precision::needs_f32_exec(&graph);
822 let exec_graph = if promote {
823 cpu_low_precision::promote_to_f32(graph)
824 } else {
825 graph
826 };
827 let plan = memory::plan_memory_aligned(&exec_graph, alignment);
830 let cfg = rlx_cpu::config::RuntimeConfig::global();
831 if cfg.verbose >= 1 {
832 eprintln!(
833 "[rlx] compile_lir: arena {} bytes ({} buffers, alignment {})",
834 plan.arena_size,
835 plan.assignments.len(),
836 alignment,
837 );
838 }
839 Box::new(build_cpu_executable(
840 exec_graph,
841 plan,
842 io_manifest,
843 options.rng,
844 ))
845 }
846 }
847
848 fn build_cpu_executable(
849 graph: Graph,
850 plan: MemoryPlan,
851 io_manifest: cpu_low_precision::IoDtypeManifest,
852 rng: rlx_ir::RngOptions,
853 ) -> CpuExecutable {
854 let mut arena = Arena::from_plan(plan);
855 let mut input_ids = HashMap::new();
856 let mut param_ids = HashMap::new();
857 let mut node_dtypes: HashMap<NodeId, DType> = HashMap::new();
858 for node in graph.nodes() {
859 node_dtypes.insert(node.id, node.shape.dtype());
860 match &node.op {
861 Op::Input { name } => {
862 input_ids.insert(name.clone(), node.id);
863 }
864 Op::Param { name } => {
865 param_ids.insert(name.clone(), node.id);
866 }
867 _ => {}
868 }
869 }
870
871 let schedule = thunk::compile_thunks_with_rng(&graph, &arena, rng);
872
873 let mut input_slots = Vec::new();
874 for node in graph.nodes() {
875 if let Op::Input { name } = &node.op {
876 let off = arena.byte_offset(node.id);
877 let len = node.shape.num_elements().unwrap_or(0);
878 input_slots.push((name.clone(), off, len, node.shape.dtype()));
879 }
880 }
881
882 let output_slots: Vec<(usize, usize)> = graph
883 .outputs
884 .iter()
885 .map(|&id| {
886 let off = arena.byte_offset(id);
887 let len = graph.node(id).shape.num_elements().unwrap_or(0);
888 (off, len)
889 })
890 .collect();
891
892 for node in graph.nodes() {
893 if let Op::Constant { data } = &node.op
894 && arena.has_buffer(node.id)
895 && !data.is_empty()
896 {
897 match node.shape.dtype() {
898 DType::F64
905 | DType::F16
906 | DType::BF16
907 | DType::I64
908 | DType::I32
909 | DType::U32 => {
910 let off = arena.byte_offset(node.id);
911 let buf = arena.raw_buf_mut();
912 let n = buf.len().saturating_sub(off).min(data.len());
913 buf[off..off + n].copy_from_slice(&data[..n]);
914 }
915 _ => {
916 let buf = arena.slice_mut(node.id);
917 let n_floats = data.len() / 4;
918 let n = buf.len().min(n_floats);
919 for i in 0..n {
920 let bytes = [
921 data[i * 4],
922 data[i * 4 + 1],
923 data[i * 4 + 2],
924 data[i * 4 + 3],
925 ];
926 buf[i] = f32::from_le_bytes(bytes);
927 }
928 }
929 }
930 }
931 }
932
933 CpuExecutable {
934 graph,
935 arena,
936 input_ids,
937 param_ids,
938 node_dtypes,
939 io_manifest,
940 schedule,
941 input_slots,
942 output_slots,
943 handles: HashMap::new(),
944 active_extent: None,
945 moe_resident: None,
946 moe_resident_layers: None,
947 moe_topk_capture: None,
948 baseline_written: false,
949 }
950 }
951
952 #[derive(Clone)]
953 struct CpuExecutable {
954 graph: Graph,
955 arena: Arena,
956 input_ids: HashMap<String, NodeId>,
957 param_ids: HashMap<String, NodeId>,
958 node_dtypes: HashMap<NodeId, DType>,
961 io_manifest: cpu_low_precision::IoDtypeManifest,
963 schedule: thunk::ThunkSchedule,
964 input_slots: Vec<(String, usize, usize, DType)>,
966 output_slots: Vec<(usize, usize)>,
968 handles: HashMap<String, Vec<f32>>,
973 active_extent: Option<(usize, usize)>,
979 moe_resident: Option<std::sync::Arc<[bool]>>,
980 moe_resident_layers: Option<std::sync::Arc<Vec<std::sync::Arc<[bool]>>>>,
981 moe_topk_capture: Option<std::sync::Arc<rlx_cpu::moe_topk_capture::MoeTopkCapture>>,
982 baseline_written: bool,
988 }
989
990 unsafe impl Send for CpuExecutable {}
991
992 impl CpuExecutable {
993 fn write_input(&mut self, id: NodeId, data: &[f32]) {
995 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
996 let off = self.arena.byte_offset(id);
997 let buf = self.arena.raw_buf_mut();
998 let elem_size = dtype.size_bytes();
999 let max_elems = (buf.len() - off) / elem_size;
1000 unsafe {
1001 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
1002 }
1003 }
1004
1005 fn read_output(&self, id: NodeId) -> Vec<f32> {
1007 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
1008 let off = self.arena.byte_offset(id);
1009 let buf = self.arena.raw_buf();
1010 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
1011 unsafe { read_typed_to_f32(buf.as_ptr().add(off), dtype, n_elems) }
1012 }
1013 }
1014
1015 impl ExecutableGraph for CpuExecutable {
1016 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
1017 Box::new(self.clone())
1018 }
1019 fn set_param(&mut self, name: &str, data: &[f32]) {
1020 if let Some(&id) = self.param_ids.get(name)
1025 && self.arena.has_buffer(id)
1026 {
1027 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
1028 let off = self.arena.byte_offset(id);
1029 let buf = self.arena.raw_buf_mut();
1030 let elem_size = dtype.size_bytes();
1031 let max_elems = (buf.len() - off) / elem_size;
1032 unsafe {
1033 write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
1034 }
1035 }
1036 }
1037
1038 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
1039 self.restore_arena_baseline();
1040 let handle_names: Vec<String> = self.handles.keys().cloned().collect();
1043 for name in &handle_names {
1044 if let Some(&id) = self.input_ids.get(name)
1045 && self.arena.has_buffer(id)
1046 {
1047 let data = self.handles.get(name).cloned().unwrap_or_default();
1048 self.write_input(id, &data);
1049 }
1050 }
1051 for &(name, data) in inputs {
1053 if let Some(&id) = self.input_ids.get(name)
1054 && self.arena.has_buffer(id)
1055 {
1056 self.write_input(id, data);
1057 }
1058 }
1059
1060 let active_used = if let Some((actual, upper)) = self.active_extent {
1065 thunk::execute_thunks_active(
1066 &self.schedule,
1067 self.arena.raw_buf_mut(),
1068 actual,
1069 upper,
1070 )
1071 } else {
1072 false
1073 };
1074 if !active_used {
1075 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
1077 }
1078
1079 for (idx, &out_id) in self.graph.outputs.iter().enumerate() {
1083 let name = format!("out{idx}");
1084 if self.handles.contains_key(&name) {
1085 let v = self.read_output(out_id);
1086 self.handles.insert(name, v);
1087 }
1088 }
1089
1090 self.graph
1091 .outputs
1092 .iter()
1093 .map(|&out_id| self.read_output(out_id))
1094 .collect()
1095 }
1096
1097 fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
1098 self.restore_arena_baseline();
1099 for &(name, data) in inputs {
1101 if let Some(&id) = self.input_ids.get(name)
1102 && self.arena.has_buffer(id)
1103 {
1104 self.write_input(id, data);
1105 }
1106 }
1107 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
1108 self.graph
1112 .outputs
1113 .iter()
1114 .map(|&out_id| {
1115 let (ptr, len) = self.arena.raw_ptr(out_id);
1116 (ptr as *const f32, len)
1117 })
1118 .collect()
1119 }
1120
1121 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
1125 self.restore_arena_baseline();
1126 let buf = self.arena.raw_buf_mut();
1127 for (i, &data) in inputs.iter().enumerate() {
1128 if i < self.input_slots.len() {
1129 let (_, off, max_len, dtype) = &self.input_slots[i];
1130 unsafe {
1131 write_typed_from_f32(buf.as_mut_ptr().add(*off), *dtype, data, *max_len);
1132 }
1133 }
1134 }
1135 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
1136 &self.output_slots
1137 }
1138
1139 fn arena_ptr(&self) -> *const u8 {
1140 self.arena.raw_buf_mut_ptr()
1141 }
1142
1143 fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
1144 self.handles.insert(name.to_string(), data.to_vec());
1149 true
1150 }
1151
1152 fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
1153 self.handles.get(name).cloned()
1154 }
1155
1156 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
1157 self.active_extent = extent;
1158 }
1159
1160 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
1161 *self.schedule.rng.write().unwrap() = rng;
1162 }
1163
1164 fn rng(&self) -> rlx_ir::RngOptions {
1165 *self.schedule.rng.read().unwrap()
1166 }
1167
1168 fn set_moe_resident_experts(&mut self, mask: &[bool]) {
1169 self.moe_resident_layers = None;
1170 self.schedule.moe_resident_layers = None;
1171 self.moe_resident = Some(Arc::from(mask));
1172 self.schedule.moe_resident = self.moe_resident.clone();
1173 }
1174
1175 fn set_moe_resident_experts_per_layer(&mut self, masks: &[&[bool]]) {
1176 self.moe_resident = None;
1177 self.schedule.moe_resident = None;
1178 let layers: Vec<Arc<[bool]>> = masks.iter().map(|m| Arc::from(*m)).collect();
1179 let arc = Arc::new(layers);
1180 self.moe_resident_layers = Some(arc.clone());
1181 self.schedule.moe_resident_layers = Some(arc);
1182 }
1183
1184 fn enable_moe_topk_capture(&mut self, num_experts: usize) -> bool {
1185 let cap = rlx_cpu::moe_topk_capture::MoeTopkCapture::new(num_experts);
1186 self.moe_topk_capture = Some(cap.clone());
1187 self.schedule.moe_topk_capture = Some(cap);
1188 true
1189 }
1190
1191 fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
1192 let cap = self.moe_topk_capture.as_ref()?;
1193 let layers = cap.take_layers();
1194 if layers.is_empty() {
1195 None
1196 } else {
1197 Some(layers)
1198 }
1199 }
1200
1201 fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
1202 rlx_cpu::moe_residency::take_last_forward_stats()
1203 }
1204
1205 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
1211 if matches!(dtype, DType::F64 | DType::I64 | DType::I32 | DType::U32) {
1212 self.set_param_bytes(name, data, dtype);
1213 return;
1214 }
1215 if matches!(dtype, DType::U8 | DType::I8) {
1219 self.set_param_bytes(name, data, dtype);
1220 return;
1221 }
1222 if dtype == DType::F32 {
1223 let n = data.len() / 4;
1224 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
1225 self.set_param(name, s);
1226 } else {
1227 let f32_buf = super::widen_bytes_to_f32(data, dtype);
1228 self.set_param(name, &f32_buf);
1229 }
1230 }
1231
1232 fn run_typed(
1244 &mut self,
1245 inputs: &[(&str, &[u8], rlx_ir::DType)],
1246 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
1247 let all_f64 = !inputs.is_empty() && inputs.iter().all(|(_, _, dt)| *dt == DType::F64);
1252
1253 if all_f64 {
1254 for (name, data, _) in inputs {
1255 if let Some(&id) = self.input_ids.get(*name) {
1256 if !self.arena.has_buffer(id) {
1257 continue;
1258 }
1259 let off = self.arena.byte_offset(id);
1260 let buf = self.arena.raw_buf_mut();
1261 let n = data.len();
1262 debug_assert!(
1263 off + n <= buf.len(),
1264 "run_typed: input '{name}' overflows arena slot"
1265 );
1266 buf[off..off + n].copy_from_slice(data);
1267 }
1268 }
1269 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
1270 } else {
1271 let mut f32_owned: Vec<(String, Vec<f32>)> = Vec::new();
1276 for (name, data, dt) in inputs {
1277 let direct = matches!(
1278 *dt,
1279 DType::F64 | DType::I32 | DType::I64 | DType::U32 | DType::C64
1280 );
1281 if direct {
1282 if let Some(&id) = self.input_ids.get(*name) {
1283 if !self.arena.has_buffer(id) {
1284 continue;
1285 }
1286 let off = self.arena.byte_offset(id);
1287 let buf = self.arena.raw_buf_mut();
1288 buf[off..off + data.len()].copy_from_slice(data);
1289 }
1290 } else {
1291 let v = super::widen_bytes_to_f32(data, *dt);
1292 f32_owned.push((name.to_string(), v));
1293 }
1294 }
1295 for (name, data) in &f32_owned {
1296 if let Some(&id) = self.input_ids.get(name.as_str()) {
1297 if self.arena.has_buffer(id) {
1298 self.write_input(id, data);
1299 }
1300 }
1301 }
1302 let active_used = if let Some((actual, upper)) = self.active_extent {
1303 thunk::execute_thunks_active(
1304 &self.schedule,
1305 self.arena.raw_buf_mut(),
1306 actual,
1307 upper,
1308 )
1309 } else {
1310 false
1311 };
1312 if !active_used {
1313 thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
1314 }
1315 }
1316
1317 self.graph
1319 .outputs
1320 .iter()
1321 .enumerate()
1322 .map(|(idx, &id)| {
1323 let exec_dtype = self.graph.node(id).shape.dtype();
1324 let declared = self.io_manifest.output_dtype(idx, exec_dtype);
1325 if matches!(
1326 exec_dtype,
1327 DType::F64
1328 | DType::F16
1329 | DType::BF16
1330 | DType::I32
1331 | DType::I64
1332 | DType::U32
1333 | DType::C64
1334 ) {
1335 let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
1336 let n_bytes = n_elems * exec_dtype.size_bytes();
1337 let off = self.arena.byte_offset(id);
1338 let bytes = self.arena.raw_buf()[off..off + n_bytes].to_vec();
1339 return (bytes, declared);
1340 }
1341 let f32_vals = self.read_output(id);
1342 if declared != exec_dtype {
1343 return (super::narrow_f32_to_bytes(&f32_vals, declared), declared);
1344 }
1345 let bytes = f32_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
1346 (bytes, declared)
1347 })
1348 .collect()
1349 }
1350 }
1351
1352 impl CpuExecutable {
1353 fn restore_arena_baseline(&mut self) {
1363 let persistent: std::collections::HashSet<NodeId> = {
1365 let mut s: std::collections::HashSet<NodeId> =
1366 self.param_ids.values().copied().collect();
1367 for node in self.graph.nodes() {
1368 if matches!(node.op, Op::Constant { .. }) {
1369 s.insert(node.id);
1370 }
1371 }
1372 s
1373 };
1374
1375 if !self.baseline_written {
1378 let constants: Vec<(NodeId, DType, Vec<u8>)> = self
1379 .graph
1380 .nodes()
1381 .iter()
1382 .filter_map(|node| {
1383 if let Op::Constant { data } = &node.op
1384 && self.arena.has_buffer(node.id)
1385 && !data.is_empty()
1386 {
1387 Some((node.id, node.shape.dtype(), data.clone()))
1388 } else {
1389 None
1390 }
1391 })
1392 .collect();
1393 for (id, dtype, data) in constants {
1394 self.write_constant_to_arena(id, dtype, &data);
1395 }
1396 self.baseline_written = true;
1397 }
1398
1399 let mut keep: Vec<(usize, usize)> = self
1410 .graph
1411 .nodes()
1412 .iter()
1413 .filter_map(|node| {
1414 let id = node.id;
1415 if !persistent.contains(&id) || !self.arena.has_buffer(id) {
1416 return None;
1417 }
1418 let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
1419 let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
1420 let off = self.arena.byte_offset(id);
1421 Some((off, off + nbytes))
1422 })
1423 .collect();
1424 keep.sort_unstable();
1425
1426 let buf = self.arena.raw_buf_mut();
1427 let len = buf.len();
1428 let mut cursor = 0usize;
1429 for (start, end) in keep {
1430 let start = start.min(len);
1431 if cursor < start {
1432 buf[cursor..start].fill(0);
1433 }
1434 cursor = cursor.max(end.min(len));
1435 }
1436 if cursor < len {
1437 buf[cursor..len].fill(0);
1438 }
1439 }
1440
1441 fn write_constant_to_arena(&mut self, id: NodeId, dtype: DType, data: &[u8]) {
1442 match dtype {
1443 DType::F64 | DType::F16 | DType::BF16 | DType::U8 | DType::I8 => {
1444 let off = self.arena.byte_offset(id);
1445 let buf = self.arena.raw_buf_mut();
1446 let n = buf.len().saturating_sub(off).min(data.len());
1447 buf[off..off + n].copy_from_slice(&data[..n]);
1448 }
1449 _ => {
1450 let buf = self.arena.slice_mut(id);
1451 let n_floats = data.len() / 4;
1452 let n = buf.len().min(n_floats);
1453 for i in 0..n {
1454 let bytes = [
1455 data[i * 4],
1456 data[i * 4 + 1],
1457 data[i * 4 + 2],
1458 data[i * 4 + 3],
1459 ];
1460 buf[i] = f32::from_le_bytes(bytes);
1461 }
1462 }
1463 }
1464 }
1465
1466 fn set_param_bytes(&mut self, name: &str, data: &[u8], _dtype: rlx_ir::DType) {
1472 self.write_param_bytes_to_arena(name, data);
1474 }
1475
1476 fn write_param_bytes_to_arena(&mut self, name: &str, data: &[u8]) {
1477 if let Some(&id) = self.param_ids.get(name)
1478 && self.arena.has_buffer(id)
1479 {
1480 let off = self.arena.byte_offset(id);
1481 let buf = self.arena.raw_buf_mut();
1482 debug_assert!(
1483 off + data.len() <= buf.len(),
1484 "set_param_bytes: '{name}' would overflow arena slot"
1485 );
1486 buf[off..off + data.len()].copy_from_slice(data);
1487 }
1488 }
1489 }
1490}
1491
1492#[cfg(feature = "gpu")]
1497pub mod wgpu_backend {
1498 use super::*;
1499 use rlx_ir::OpKind;
1500 use rlx_wgpu::backend::WgpuExecutable;
1501
1502 pub struct WgpuBackend;
1503
1504 const WGPU_SUPPORTED_OPS: &[OpKind] = &[
1510 OpKind::Input,
1511 OpKind::Param,
1512 OpKind::Constant,
1513 OpKind::Activation,
1514 OpKind::Cast,
1515 OpKind::StopGradient,
1516 OpKind::Binary,
1517 OpKind::Compare,
1518 OpKind::Where,
1519 OpKind::Fma,
1520 OpKind::ElementwiseRegion,
1521 OpKind::TransformRegion,
1522 OpKind::BatchElementwiseRegion,
1523 OpKind::MatMul,
1524 OpKind::DotGeneral,
1525 OpKind::LayerNorm,
1526 OpKind::LayerNorm2d,
1527 OpKind::GroupNorm,
1528 OpKind::ResizeNearest2x,
1529 OpKind::RmsNorm,
1530 OpKind::Attention,
1531 OpKind::AttentionBackward,
1532 OpKind::RmsNormBackwardInput,
1533 OpKind::RmsNormBackwardGamma,
1534 OpKind::RmsNormBackwardBeta,
1535 OpKind::LayerNormBackwardInput,
1542 OpKind::LayerNormBackwardGamma,
1543 OpKind::RopeBackward,
1544 OpKind::CumsumBackward,
1545 OpKind::GatherBackward,
1546 OpKind::Rope,
1547 OpKind::Reshape,
1548 OpKind::Transpose,
1549 OpKind::Narrow,
1550 OpKind::Concat,
1551 OpKind::Expand,
1552 OpKind::Gather,
1553 OpKind::Reverse,
1554 OpKind::Reduce,
1555 OpKind::Softmax,
1556 OpKind::SoftmaxCrossEntropy,
1557 OpKind::ArgMax,
1558 OpKind::ArgMin,
1559 OpKind::Cumsum,
1560 OpKind::TopK,
1561 OpKind::Sample,
1562 OpKind::Conv,
1563 OpKind::Im2Col,
1564 OpKind::Pool,
1565 OpKind::GroupedMatMul,
1566 OpKind::DequantGroupedMatMul,
1567 OpKind::DequantMoEWeights,
1568 OpKind::ScatterAdd,
1569 OpKind::SelectiveScan,
1570 OpKind::Lstm,
1571 OpKind::Gru,
1572 OpKind::Rnn,
1573 OpKind::Mamba2,
1574 OpKind::ConvTranspose2d,
1576 OpKind::DequantMatMul,
1577 OpKind::FusedMatMulBiasAct,
1578 OpKind::FusedResidualLN,
1579 OpKind::FusedResidualRmsNorm,
1580 OpKind::FusedSwiGLU,
1581 OpKind::FusedAttentionBlock,
1582 OpKind::FusedTransformerLayer,
1583 OpKind::Fft,
1589 OpKind::LogMel,
1590 OpKind::LogMelBackward,
1591 OpKind::WelchPeaks,
1592 OpKind::GaussianSplatRender,
1594 OpKind::GaussianSplatRenderBackward,
1595 OpKind::GaussianSplatPrepare,
1596 OpKind::GaussianSplatRasterize,
1597 OpKind::Custom,
1598 OpKind::RngNormal,
1599 OpKind::RngUniform,
1600 ];
1602
1603 impl Backend for WgpuBackend {
1604 fn supported_ops(&self) -> &'static [OpKind] {
1605 WGPU_SUPPORTED_OPS
1606 }
1607
1608 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
1609 use rlx_opt::pass::Pass as _;
1610 let graph = rlx_opt::LowerControlFlow.run(graph);
1611 let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, WGPU_SUPPORTED_OPS)
1612 .unwrap_or_else(|errors| {
1613 panic!("{}", rlx_opt::format_legalize_error("wgpu", &errors));
1614 });
1615 let graph = crate::precompile::precompile_cleanup(graph, options);
1616 let graph = rlx_opt::LegalizeBroadcast.run(graph);
1620 let compile_result = crate::stages::compile_graph_stages_for_backend(
1629 rlx_driver::Device::Gpu,
1630 graph,
1631 options,
1632 WGPU_SUPPORTED_OPS,
1633 );
1634 crate::stages::maybe_log_fusion(&compile_result.fusion);
1635 let graph = compile_result.lir.into_graph();
1636 let graph = match options.policy.clone() {
1637 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
1638 None => graph,
1639 };
1640 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
1641 Box::new(WgpuExecutableWrapper {
1642 inner: WgpuExecutable::compile_rng(graph, options.rng),
1643 io_manifest,
1644 })
1645 }
1646
1647 fn compile_lir(
1648 &self,
1649 lir: LirModule,
1650 options: &CompileOptions,
1651 ) -> Box<dyn ExecutableGraph> {
1652 use rlx_opt::pass::Pass as _;
1653 let graph = rlx_opt::LegalizeBroadcast.run(lir.into_graph());
1656 let graph = prepare_fused_graph(graph, options, WGPU_SUPPORTED_OPS, "wgpu");
1657 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
1658 Box::new(WgpuExecutableWrapper {
1659 inner: WgpuExecutable::compile_rng(graph, options.rng),
1660 io_manifest,
1661 })
1662 }
1663 }
1664
1665 struct WgpuExecutableWrapper {
1666 inner: WgpuExecutable,
1667 io_manifest: cpu_low_precision::IoDtypeManifest,
1668 }
1669
1670 unsafe impl Send for WgpuExecutableWrapper {}
1671
1672 impl ExecutableGraph for WgpuExecutableWrapper {
1673 fn set_param(&mut self, name: &str, data: &[f32]) {
1674 self.inner.set_param(name, data);
1675 }
1676 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
1677 self.inner.run(inputs)
1678 }
1679 fn run_read_outputs(
1680 &mut self,
1681 inputs: &[(&str, &[f32])],
1682 read_indices: Option<&[usize]>,
1683 ) -> Vec<Vec<f32>> {
1684 self.inner.run_read_outputs(inputs, read_indices)
1685 }
1686 fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
1687 self.inner.bind_gpu_handle(name, data)
1688 }
1689 fn has_gpu_handle(&self, name: &str) -> bool {
1690 self.inner.has_gpu_handle(name)
1691 }
1692 fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
1693 self.inner.set_gpu_handle_feed(handle_name, output_index);
1694 true
1695 }
1696 fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
1697 self.inner.read_gpu_handle(name)
1698 }
1699 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
1700 self.inner.set_active_extent(extent);
1701 }
1702
1703 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
1704 self.inner.set_rng(rng);
1705 }
1706
1707 fn rng(&self) -> rlx_ir::RngOptions {
1708 self.inner.rng()
1709 }
1710
1711 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
1714 match dtype {
1715 rlx_ir::DType::U8 | rlx_ir::DType::I8 => {
1716 self.inner.set_param_bytes(name, data);
1717 }
1718 rlx_ir::DType::F32 => {
1719 let n = data.len() / 4;
1720 let f32_slice =
1721 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
1722 self.inner.set_param(name, f32_slice);
1723 }
1724 rlx_ir::DType::F16 => {
1725 let n = data.len() / 2;
1726 let f16_slice =
1727 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const half::f16, n) };
1728 let f32: Vec<f32> = f16_slice.iter().map(|h| h.to_f32()).collect();
1729 self.inner.set_param(name, &f32);
1730 }
1731 rlx_ir::DType::BF16 => {
1732 let n = data.len() / 2;
1733 let bf16_slice = unsafe {
1734 std::slice::from_raw_parts(data.as_ptr() as *const half::bf16, n)
1735 };
1736 let f32: Vec<f32> = bf16_slice.iter().map(|h| h.to_f32()).collect();
1737 self.inner.set_param(name, &f32);
1738 }
1739 other => panic!(
1740 "rlx-wgpu set_param_typed: dtype {other:?} unsupported \
1741 (F32, F16, BF16 only — wgpu arena is f32-uniform)"
1742 ),
1743 }
1744 }
1745
1746 fn run_typed(
1749 &mut self,
1750 inputs: &[(&str, &[u8], rlx_ir::DType)],
1751 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
1752 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
1753 for (name, data, dt) in inputs {
1754 let v: Vec<f32> = match *dt {
1755 rlx_ir::DType::F32 => {
1756 let n = data.len() / 4;
1757 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }
1758 .to_vec()
1759 }
1760 rlx_ir::DType::F16 => {
1761 let n = data.len() / 2;
1762 let s = unsafe {
1763 std::slice::from_raw_parts(data.as_ptr() as *const half::f16, n)
1764 };
1765 s.iter().map(|h| h.to_f32()).collect()
1766 }
1767 rlx_ir::DType::BF16 => {
1768 let n = data.len() / 2;
1769 let s = unsafe {
1770 std::slice::from_raw_parts(data.as_ptr() as *const half::bf16, n)
1771 };
1772 s.iter().map(|h| h.to_f32()).collect()
1773 }
1774 rlx_ir::DType::I64 => {
1778 let n = data.len() / 8;
1779 let s =
1780 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const i64, n) };
1781 s.iter().map(|&x| x as f32).collect()
1782 }
1783 rlx_ir::DType::I32 => {
1784 let n = data.len() / 4;
1785 let s =
1786 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const i32, n) };
1787 s.iter().map(|&x| x as f32).collect()
1788 }
1789 rlx_ir::DType::U8 | rlx_ir::DType::I8 | rlx_ir::DType::Bool => {
1790 data.iter().map(|&b| b as f32).collect()
1791 }
1792 other => {
1793 panic!("rlx-wgpu run_typed: input '{name}' dtype {other:?} unsupported")
1794 }
1795 };
1796 owned.push((name.to_string(), v));
1797 }
1798 let refs: Vec<(&str, &[f32])> = owned
1799 .iter()
1800 .map(|(n, d)| (n.as_str(), d.as_slice()))
1801 .collect();
1802 let dtypes =
1803 super::declared_output_dtypes(&self.io_manifest, self.inner.output_dtypes());
1804 let outs = self.inner.run(&refs);
1805 outs.into_iter()
1806 .zip(
1807 dtypes
1808 .into_iter()
1809 .chain(std::iter::repeat(rlx_ir::DType::F32)),
1810 )
1811 .map(|(v, dt)| (narrow_to_dtype(&v, dt), dt))
1812 .collect()
1813 }
1814
1815 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
1816 Box::new(WgpuExecutableWrapper {
1817 inner: self.inner.clone_for_cache(),
1818 io_manifest: self.io_manifest.clone(),
1819 })
1820 }
1821 }
1822
1823 fn narrow_to_dtype(v: &[f32], dt: rlx_ir::DType) -> Vec<u8> {
1829 use rlx_ir::DType;
1830 match dt {
1831 DType::F32 => {
1832 let mut bytes = Vec::with_capacity(v.len() * 4);
1833 for &x in v {
1834 bytes.extend_from_slice(&x.to_le_bytes());
1835 }
1836 bytes
1837 }
1838 DType::F16 => {
1839 let mut bytes = Vec::with_capacity(v.len() * 2);
1840 for &x in v {
1841 bytes.extend_from_slice(&half::f16::from_f32(x).to_le_bytes());
1842 }
1843 bytes
1844 }
1845 DType::BF16 => {
1846 let mut bytes = Vec::with_capacity(v.len() * 2);
1847 for &x in v {
1848 bytes.extend_from_slice(&half::bf16::from_f32(x).to_le_bytes());
1849 }
1850 bytes
1851 }
1852 DType::F64 => {
1853 let mut bytes = Vec::with_capacity(v.len() * 8);
1854 for &x in v {
1855 bytes.extend_from_slice(&(x as f64).to_le_bytes());
1856 }
1857 bytes
1858 }
1859 DType::I8 => v.iter().map(|&x| x as i8 as u8).collect(),
1860 DType::U8 => v.iter().map(|&x| x as u8).collect(),
1861 DType::I16 => {
1862 let mut bytes = Vec::with_capacity(v.len() * 2);
1863 for &x in v {
1864 bytes.extend_from_slice(&(x as i16).to_le_bytes());
1865 }
1866 bytes
1867 }
1868 DType::I32 => {
1869 let mut bytes = Vec::with_capacity(v.len() * 4);
1870 for &x in v {
1871 bytes.extend_from_slice(&(x as i32).to_le_bytes());
1872 }
1873 bytes
1874 }
1875 DType::U32 => {
1876 let mut bytes = Vec::with_capacity(v.len() * 4);
1877 for &x in v {
1878 bytes.extend_from_slice(&(x as u32).to_le_bytes());
1879 }
1880 bytes
1881 }
1882 DType::I64 => {
1883 let mut bytes = Vec::with_capacity(v.len() * 8);
1884 for &x in v {
1885 bytes.extend_from_slice(&(x as i64).to_le_bytes());
1886 }
1887 bytes
1888 }
1889 DType::Bool => v
1890 .iter()
1891 .map(|&x| if x != 0.0 { 1u8 } else { 0u8 })
1892 .collect(),
1893 DType::C64 => {
1900 let mut bytes = Vec::with_capacity(v.len() * 4);
1901 for &x in v {
1902 bytes.extend_from_slice(&x.to_le_bytes());
1903 }
1904 bytes
1905 }
1906 }
1907 }
1908}
1909
1910#[cfg(feature = "vulkan")]
1913pub mod vulkan_backend {
1914 use super::*;
1915 use rlx_ir::OpKind;
1916 use rlx_vulkan::backend::VulkanExecutable;
1917
1918 pub struct VulkanBackend;
1919
1920 impl Backend for VulkanBackend {
1921 fn supported_ops(&self) -> &'static [OpKind] {
1922 rlx_vulkan::backend::SUPPORTED_OPS
1923 }
1924
1925 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
1926 Box::new(VulkanExecutableWrapper {
1931 inner: VulkanExecutable::compile_rng(graph, options.rng),
1932 })
1933 }
1934 }
1935
1936 struct VulkanExecutableWrapper {
1937 inner: VulkanExecutable,
1938 }
1939
1940 unsafe impl Send for VulkanExecutableWrapper {}
1941
1942 impl ExecutableGraph for VulkanExecutableWrapper {
1943 fn set_param(&mut self, name: &str, data: &[f32]) {
1944 self.inner.set_param(name, data);
1945 }
1946
1947 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
1948 self.inner.run(inputs)
1949 }
1950
1951 fn run_read_outputs(
1952 &mut self,
1953 inputs: &[(&str, &[f32])],
1954 read_indices: Option<&[usize]>,
1955 ) -> Vec<Vec<f32>> {
1956 self.inner.run_read_outputs(inputs, read_indices)
1957 }
1958
1959 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
1960 self.inner.set_active_extent(extent);
1961 }
1962
1963 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
1964 self.inner.set_rng(rng);
1965 }
1966
1967 fn rng(&self) -> rlx_ir::RngOptions {
1968 self.inner.rng()
1969 }
1970
1971 fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
1972 self.inner.bind_gpu_handle(name, data)
1973 }
1974
1975 fn has_gpu_handle(&self, name: &str) -> bool {
1976 self.inner.has_gpu_handle(name)
1977 }
1978
1979 fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
1980 self.inner.set_gpu_handle_feed(handle_name, output_index);
1981 true
1982 }
1983
1984 fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
1985 self.inner.read_gpu_handle(name)
1986 }
1987
1988 fn register_kv_row_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
1989 self.inner.register_kv_row_feed(handle_name, output_index);
1990 true
1991 }
1992
1993 fn feed_kv_row(&mut self, src_row: usize, dst_row: usize, row_elems: usize) -> bool {
1994 self.inner.feed_kv_row(src_row, dst_row, row_elems);
1995 true
1996 }
1997
1998 fn read_output_row(
1999 &self,
2000 out_idx: usize,
2001 row: usize,
2002 row_inner: usize,
2003 ) -> Option<Vec<f32>> {
2004 self.inner.read_output_row(out_idx, row, row_inner)
2005 }
2006
2007 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
2009 match dtype {
2010 rlx_ir::DType::U8 | rlx_ir::DType::I8 => self.inner.set_param_bytes(name, data),
2011 rlx_ir::DType::F32 => {
2012 let n = data.len() / 4;
2013 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
2014 self.inner.set_param(name, s);
2015 }
2016 other => {
2017 let f = super::widen_bytes_to_f32(data, other);
2018 self.inner.set_param(name, &f);
2019 }
2020 }
2021 }
2022
2023 fn run_typed(
2026 &mut self,
2027 inputs: &[(&str, &[u8], rlx_ir::DType)],
2028 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
2029 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
2030 for (name, data, dt) in inputs {
2031 let v = if *dt == rlx_ir::DType::F32 {
2032 let n = data.len() / 4;
2033 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }.to_vec()
2034 } else {
2035 super::widen_bytes_to_f32(data, *dt)
2036 };
2037 owned.push((name.to_string(), v));
2038 }
2039 let refs: Vec<(&str, &[f32])> = owned
2040 .iter()
2041 .map(|(n, d)| (n.as_str(), d.as_slice()))
2042 .collect();
2043 let dtypes = self.inner.output_dtypes();
2044 let outs = self.inner.run(&refs);
2045 outs.into_iter()
2046 .zip(
2047 dtypes
2048 .into_iter()
2049 .chain(std::iter::repeat(rlx_ir::DType::F32)),
2050 )
2051 .map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
2052 .collect()
2053 }
2054
2055 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
2056 Box::new(VulkanExecutableWrapper {
2057 inner: self.inner.clone_for_cache(),
2058 })
2059 }
2060 }
2061}
2062
2063#[cfg(feature = "oneapi")]
2066pub mod oneapi_backend {
2067 use super::*;
2068 use rlx_ir::OpKind;
2069 use rlx_oneapi::backend::OneApiExecutable;
2070
2071 pub struct OneApiBackend;
2072
2073 impl Backend for OneApiBackend {
2074 fn supported_ops(&self) -> &'static [OpKind] {
2075 rlx_oneapi::backend::SUPPORTED_OPS
2076 }
2077
2078 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
2079 Box::new(OneApiExecutableWrapper {
2083 inner: OneApiExecutable::compile_rng(graph, options.rng),
2084 })
2085 }
2086 }
2087
2088 struct OneApiExecutableWrapper {
2089 inner: OneApiExecutable,
2090 }
2091
2092 unsafe impl Send for OneApiExecutableWrapper {}
2093
2094 impl ExecutableGraph for OneApiExecutableWrapper {
2095 fn set_param(&mut self, name: &str, data: &[f32]) {
2096 self.inner.set_param(name, data);
2097 }
2098
2099 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
2100 self.inner.run(inputs)
2101 }
2102
2103 fn run_read_outputs(
2104 &mut self,
2105 inputs: &[(&str, &[f32])],
2106 read_indices: Option<&[usize]>,
2107 ) -> Vec<Vec<f32>> {
2108 self.inner.run_read_outputs(inputs, read_indices)
2109 }
2110
2111 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
2112 self.inner.set_active_extent(extent);
2113 }
2114
2115 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
2116 self.inner.set_rng(rng);
2117 }
2118
2119 fn rng(&self) -> rlx_ir::RngOptions {
2120 self.inner.rng()
2121 }
2122
2123 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
2125 match dtype {
2126 rlx_ir::DType::U8 | rlx_ir::DType::I8 => self.inner.set_param_bytes(name, data),
2127 rlx_ir::DType::F32 => {
2128 let n = data.len() / 4;
2129 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
2130 self.inner.set_param(name, s);
2131 }
2132 other => {
2133 let f = super::widen_bytes_to_f32(data, other);
2134 self.inner.set_param(name, &f);
2135 }
2136 }
2137 }
2138
2139 fn run_typed(
2142 &mut self,
2143 inputs: &[(&str, &[u8], rlx_ir::DType)],
2144 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
2145 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
2146 for (name, data, dt) in inputs {
2147 let v = if *dt == rlx_ir::DType::F32 {
2148 let n = data.len() / 4;
2149 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }.to_vec()
2150 } else {
2151 super::widen_bytes_to_f32(data, *dt)
2152 };
2153 owned.push((name.to_string(), v));
2154 }
2155 let refs: Vec<(&str, &[f32])> = owned
2156 .iter()
2157 .map(|(n, d)| (n.as_str(), d.as_slice()))
2158 .collect();
2159 let dtypes = self.inner.output_dtypes();
2160 let outs = self.inner.run(&refs);
2161 outs.into_iter()
2162 .zip(
2163 dtypes
2164 .into_iter()
2165 .chain(std::iter::repeat(rlx_ir::DType::F32)),
2166 )
2167 .map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
2168 .collect()
2169 }
2170
2171 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
2172 Box::new(OneApiExecutableWrapper {
2173 inner: self.inner.clone_for_cache(),
2174 })
2175 }
2176 }
2177}
2178
2179#[cfg(all(feature = "mlx", rlx_mlx_host))]
2182pub mod mlx_backend {
2183 use super::*;
2184 use rlx_mlx::MlxExecutable;
2185
2186 pub struct MlxBackend;
2187
2188 const MLX_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
2198 use rlx_ir::OpKind::*;
2199 &[
2200 Input,
2201 Param,
2202 Constant,
2203 Activation,
2204 Cast,
2205 StopGradient,
2206 Binary,
2207 Compare,
2208 Where,
2209 ElementwiseRegion,
2210 TransformRegion,
2211 BatchElementwiseRegion,
2212 MatMul,
2213 DotGeneral,
2214 DenseSolve,
2215 BatchedDenseSolve,
2216 LayerNorm,
2217 LayerNorm2d,
2218 GroupNorm,
2219 ResizeNearest2x,
2220 RmsNorm,
2221 Attention,
2222 Rope,
2223 Reshape,
2224 Transpose,
2225 Narrow,
2226 Concat,
2227 Expand,
2228 Gather,
2229 Reverse,
2230 Reduce,
2231 Softmax,
2232 Cumsum,
2233 ArgMax,
2234 ArgMin,
2235 TopK,
2236 RngNormal,
2237 RngUniform,
2238 Sample,
2239 Conv,
2240 Im2Col,
2241 ConvTranspose2d,
2242 Pool,
2243 GroupedMatMul,
2244 DequantGroupedMatMul,
2245 DequantMoEWeights,
2246 ScatterAdd,
2247 LoraMatMul,
2248 DequantMatMul,
2249 SelectiveScan,
2250 GatedDeltaNet,
2251 FusedSwiGLU,
2252 FusedMatMulBiasAct,
2253 FusedResidualLN,
2254 FusedResidualRmsNorm,
2255 FusedAttentionBlock,
2256 FusedTransformerLayer,
2257 If,
2258 While,
2259 Scan,
2264 ScanBackward,
2265 ScanBackwardXs,
2266 ReluBackward,
2269 ActivationBackward,
2270 SoftmaxCrossEntropy,
2271 SoftmaxCrossEntropyWithLogits,
2272 SoftmaxCrossEntropyBackward,
2273 AttentionBackward,
2274 LayerNormBackwardInput,
2275 LayerNormBackwardGamma,
2276 GroupNormBackwardInput,
2279 GroupNormBackwardGamma,
2280 GroupNormBackwardBeta,
2281 Conv2dBackwardInput,
2286 Conv2dBackwardWeight,
2287 MaxPool2dBackward,
2291 FakeQuantize,
2296 FakeQuantizeBackward,
2297 Custom,
2302 Fft,
2303 LogMel,
2304 LogMelBackward,
2305 WelchPeaks,
2306 GaussianSplatRender,
2307 GaussianSplatRenderBackward,
2308 ]
2311 };
2312
2313 impl Backend for MlxBackend {
2314 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
2315 MLX_SUPPORTED_OPS
2316 }
2317
2318 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
2319 let compile_result = crate::stages::compile_graph_stages_for_backend(
2320 rlx_driver::Device::Mlx,
2321 graph,
2322 options,
2323 MLX_SUPPORTED_OPS,
2324 );
2325 crate::stages::maybe_log_fusion(&compile_result.fusion);
2326 self.compile_lir(compile_result.lir, options)
2327 }
2328
2329 fn compile_lir(
2330 &self,
2331 lir: LirModule,
2332 options: &CompileOptions,
2333 ) -> Box<dyn ExecutableGraph> {
2334 use rlx_opt::pass::Pass as _;
2335 let mut graph = lir.into_graph();
2336 graph = rlx_opt::LowerControlFlow.run(graph);
2337 let graph = prepare_fused_graph(graph, options, MLX_SUPPORTED_OPS, "mlx");
2338 Box::new(build_mlx_executable(graph, options.rng))
2339 }
2340 }
2341
2342 fn build_mlx_executable(graph: Graph, rng: rlx_ir::RngOptions) -> MlxExecutableWrapper {
2343 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
2344 let mode = mlx_mode_from_env();
2345 let mut exe = MlxExecutable::compile_from_fused_with_rng(graph, mode, rng);
2346 if mode == rlx_mlx::lower::MlxMode::Compiled {
2347 if let Err(e) = exe.warm_compile() {
2348 eprintln!(
2349 "[rlx-runtime] MLX warm_compile failed ({e}); first run will pay the trace cost"
2350 );
2351 }
2352 }
2353 MlxExecutableWrapper {
2354 inner: exe,
2355 io_manifest,
2356 }
2357 }
2358
2359 fn mlx_mode_from_env() -> rlx_mlx::lower::MlxMode {
2360 match rlx_ir::env::var("RLX_MLX_MODE").as_deref() {
2361 Some(s) if s.eq_ignore_ascii_case("eager") => rlx_mlx::lower::MlxMode::Eager,
2362 Some(s) if s.eq_ignore_ascii_case("lazy") => rlx_mlx::lower::MlxMode::Lazy,
2363 Some(s) if s.eq_ignore_ascii_case("compiled") => rlx_mlx::lower::MlxMode::Compiled,
2364 _ => rlx_mlx::lower::MlxMode::Compiled,
2365 }
2366 }
2367
2368 struct MlxExecutableWrapper {
2369 inner: MlxExecutable,
2370 io_manifest: cpu_low_precision::IoDtypeManifest,
2371 }
2372
2373 unsafe impl Send for MlxExecutableWrapper {}
2374
2375 impl ExecutableGraph for MlxExecutableWrapper {
2376 fn set_param(&mut self, name: &str, data: &[f32]) {
2377 self.inner.set_param(name, data);
2378 }
2379 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
2380 self.inner.run(inputs)
2381 }
2382 fn run_read_outputs(
2383 &mut self,
2384 inputs: &[(&str, &[f32])],
2385 read_indices: Option<&[usize]>,
2386 ) -> Vec<Vec<f32>> {
2387 self.inner
2388 .run_read_outputs(inputs, read_indices)
2389 .unwrap_or_else(|e| panic!("MLX run_read_outputs failed: {e}"))
2390 }
2391 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
2392 self.inner.run_slots(inputs)
2393 }
2394 fn arena_ptr(&self) -> *const u8 {
2395 self.inner.arena_ptr()
2396 }
2397 fn commit_no_wait(&mut self, inputs: &[(&str, &[f32])]) {
2398 self.inner.commit_no_wait(inputs);
2399 }
2400 fn sync_pending(&mut self) {
2401 self.inner.sync_pending();
2402 }
2403 fn run_pipelined(&mut self, input_sets: &[Vec<(&str, &[f32])>]) -> Vec<Vec<Vec<f32>>> {
2404 self.inner.run_pipelined(input_sets)
2405 }
2406 fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
2407 self.inner.bind_handle(name, data)
2408 }
2409 fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
2410 self.inner.read_handle(name)
2411 }
2412 fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
2413 self.inner.bind_gpu_handle(name, data).is_ok()
2414 }
2415 fn has_gpu_handle(&self, name: &str) -> bool {
2416 self.inner.has_gpu_handle(name)
2417 }
2418 fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
2419 self.inner.set_gpu_handle_feed(handle_name, output_index);
2420 true
2421 }
2422 fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
2423 self.inner.read_gpu_handle(name).ok()
2424 }
2425 fn run_feed_gpu_handle(
2426 &mut self,
2427 inputs: &[(&str, &[f32])],
2428 handle_name: &str,
2429 output_index: usize,
2430 ) -> Option<Vec<f32>> {
2431 self.inner
2432 .run_feed_gpu(inputs, handle_name, output_index)
2433 .ok()
2434 }
2435 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
2436 self.inner.set_param_typed(name, data, dtype);
2437 }
2438 fn run_typed(
2439 &mut self,
2440 inputs: &[(&str, &[u8], rlx_ir::DType)],
2441 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
2442 self.inner.run_typed(inputs)
2443 }
2444 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
2445 self.inner.set_active_extent(extent);
2446 }
2447
2448 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
2449 self.inner.set_rng(rng);
2450 }
2451
2452 fn rng(&self) -> rlx_ir::RngOptions {
2453 self.inner.rng()
2454 }
2455
2456 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
2457 Box::new(MlxExecutableWrapper {
2458 inner: self.inner.clone_for_cache(),
2459 io_manifest: self.io_manifest.clone(),
2460 })
2461 }
2462 }
2463}
2464
2465pub(crate) const COREML_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
2472 use rlx_ir::OpKind::*;
2473 &[
2474 Input,
2475 Param,
2476 Constant,
2477 Activation,
2478 Cast,
2479 Binary,
2480 MatMul,
2481 LayerNorm,
2482 RmsNorm,
2483 Reduce,
2484 Softmax,
2485 Reshape,
2486 Transpose,
2487 Narrow,
2488 Concat,
2489 Gather,
2490 Rope,
2491 Attention,
2492 FusedAttentionBlock,
2497 Compare,
2498 Where,
2499 Expand,
2500 Cumsum,
2501 ScatterAdd,
2502 BatchNormInference,
2503 GroupNorm,
2504 LayerNorm2d,
2505 LoraMatMul,
2506 Conv,
2507 ConvTranspose2d,
2508 Pool,
2509 TopK,
2510 AxialRope2d,
2511 ResizeNearest2x,
2512 StopGradient,
2513 GroupedMatMul,
2514 DequantMatMul,
2515 DequantMoEWeights,
2516 DequantGroupedMatMul,
2517 Quantize,
2518 Dequantize,
2519 SelectiveScan,
2520 GatedDeltaNet,
2521 ArgMax,
2522 ArgMin,
2523 Reverse,
2524 Fft,
2525 LogMel,
2526 Sample,
2527 RngNormal,
2528 Lstm,
2529 Gru,
2530 Rnn,
2531 Mamba2,
2532 WelchPeaks,
2533 Custom,
2534 ]
2535};
2536
2537#[allow(dead_code)] pub(crate) const COREML_BACKWARD_OPS: &[rlx_ir::OpKind] = {
2556 use rlx_ir::OpKind::*;
2557 &[
2558 ReluBackward,
2559 ActivationBackward,
2560 LayerNormBackwardInput,
2561 LayerNormBackwardGamma,
2562 GroupNormBackwardInput,
2563 GroupNormBackwardGamma,
2564 GroupNormBackwardBeta,
2565 BatchNormInferenceBackwardInput,
2566 BatchNormInferenceBackwardGamma,
2567 BatchNormInferenceBackwardBeta,
2568 RopeBackward,
2569 AttentionBackward,
2570 SoftmaxCrossEntropyBackward,
2571 CumsumBackward,
2572 GatherBackward,
2573 FakeQuantizeBackward,
2574 ]
2575};
2576
2577#[allow(dead_code)] pub(crate) const COREML_NATIVE_BACKWARD_OPS: &[rlx_ir::OpKind] = {
2611 use rlx_ir::OpKind::*;
2612 &[
2613 RmsNormBackwardInput,
2614 RmsNormBackwardGamma,
2615 RmsNormBackwardBeta,
2616 LayerNormBackwardInput,
2617 LayerNormBackwardGamma,
2618 GroupNormBackwardInput,
2619 GroupNormBackwardGamma,
2620 GroupNormBackwardBeta,
2621 MaxPool2dBackward,
2622 Conv2dBackwardInput,
2623 Conv2dBackwardWeight,
2624 AttentionBackward,
2625 SoftmaxCrossEntropyWithLogits,
2631 SoftmaxCrossEntropyBackward,
2632 ]
2633};
2634
2635#[cfg(feature = "training")]
2646pub(crate) const COREML_SUPPORTED_OPS_TRAINING: [rlx_ir::OpKind;
2647 COREML_SUPPORTED_OPS.len() + COREML_NATIVE_BACKWARD_OPS.len()] = {
2648 let mut arr =
2649 [rlx_ir::OpKind::Input; COREML_SUPPORTED_OPS.len() + COREML_NATIVE_BACKWARD_OPS.len()];
2650 let mut i = 0;
2651 while i < COREML_SUPPORTED_OPS.len() {
2652 arr[i] = COREML_SUPPORTED_OPS[i];
2653 i += 1;
2654 }
2655 let mut j = 0;
2656 while j < COREML_NATIVE_BACKWARD_OPS.len() {
2657 arr[COREML_SUPPORTED_OPS.len() + j] = COREML_NATIVE_BACKWARD_OPS[j];
2658 j += 1;
2659 }
2660 arr
2661};
2662
2663#[cfg(all(
2671 feature = "coreml",
2672 target_vendor = "apple",
2673 not(target_os = "watchos")
2674))]
2675pub mod coreml_backend {
2676 use super::*;
2677 use crate::Precision;
2678 use rlx_coreml::{CoremlExecutable, default_lower_options};
2679
2680 pub struct CoremlBackend;
2681
2682 impl Backend for CoremlBackend {
2683 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
2684 #[cfg(feature = "training")]
2690 {
2691 &super::COREML_SUPPORTED_OPS_TRAINING
2692 }
2693 #[cfg(not(feature = "training"))]
2694 {
2695 super::COREML_SUPPORTED_OPS
2696 }
2697 }
2698
2699 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
2700 let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, self.supported_ops())
2704 .unwrap_or_else(|errors| {
2705 panic!("{}", rlx_opt::format_legalize_error("coreml", &errors));
2706 });
2707 let (graph, mut lower_opts) = match options.policy.clone() {
2714 Some(policy) => {
2715 use rlx_opt::pass::Pass as _;
2716 let g = rlx_opt::AutoMixedPrecision::new(policy).run(graph);
2717 let opts = default_lower_options(&g);
2718 (g, opts)
2719 }
2720 None => {
2721 let mut opts = default_lower_options(&graph);
2722 if options.precision == Precision::F16 {
2724 opts.float_dtype = rlx_ir::DType::F16;
2725 }
2726 (graph, opts)
2727 }
2728 };
2729 if let Some(binding) = &options.dim_binding {
2730 let _ = binding;
2731 lower_opts.flexible_inputs = false;
2732 }
2733 Box::new(CoremlExecutableWrapper {
2734 inner: CoremlExecutable::compile_with_lower_opts(graph, lower_opts),
2735 })
2736 }
2737
2738 fn compile_lir(
2739 &self,
2740 lir: LirModule,
2741 options: &CompileOptions,
2742 ) -> Box<dyn ExecutableGraph> {
2743 self.compile(lir.into_graph(), options)
2746 }
2747 }
2748
2749 struct CoremlExecutableWrapper {
2750 inner: CoremlExecutable,
2751 }
2752
2753 unsafe impl Send for CoremlExecutableWrapper {}
2755
2756 impl ExecutableGraph for CoremlExecutableWrapper {
2757 fn set_param(&mut self, name: &str, data: &[f32]) {
2758 self.inner.set_param(name, data);
2759 }
2760
2761 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
2762 self.inner.set_param_typed(name, data, dtype);
2765 }
2766
2767 fn finalize_params(&mut self) {
2768 self.inner
2769 .finalize()
2770 .unwrap_or_else(|e| panic!("CoreML finalize failed: {e}"));
2771 }
2772
2773 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
2774 self.inner
2775 .run(inputs)
2776 .unwrap_or_else(|e| panic!("CoreML run failed: {e}"))
2777 }
2778
2779 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
2780 Box::new(CoremlExecutableWrapper {
2781 inner: self.inner.clone_for_cache(),
2782 })
2783 }
2784
2785 fn run_typed(
2786 &mut self,
2787 inputs: &[(&str, &[u8], rlx_ir::DType)],
2788 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
2789 use rlx_ir::DType;
2790 let owned: Vec<(String, Vec<f32>)> = inputs
2793 .iter()
2794 .map(|(name, data, dt)| {
2795 let v: Vec<f32> = match dt {
2796 DType::I64 => data
2797 .chunks_exact(8)
2798 .map(|c| i64::from_le_bytes(c.try_into().unwrap()) as f32)
2799 .collect(),
2800 DType::I32 => data
2801 .chunks_exact(4)
2802 .map(|c| i32::from_le_bytes(c.try_into().unwrap()) as f32)
2803 .collect(),
2804 DType::U8 | DType::Bool => data.iter().map(|&b| b as f32).collect(),
2805 _ => super::widen_bytes_to_f32(data, *dt),
2806 };
2807 (name.to_string(), v)
2808 })
2809 .collect();
2810 let refs: Vec<(&str, &[f32])> = owned
2811 .iter()
2812 .map(|(n, d)| (n.as_str(), d.as_slice()))
2813 .collect();
2814 self.run(&refs)
2815 .into_iter()
2816 .map(|v| {
2817 let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
2818 (bytes, DType::F32)
2819 })
2820 .collect()
2821 }
2822 }
2823}
2824
2825#[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
2826pub mod metal_backend {
2827 use super::*;
2828 use rlx_metal::backend::MetalExecutable;
2829
2830 pub struct MetalBackend;
2831
2832 const METAL_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
2846 use rlx_ir::OpKind::*;
2847 &[
2848 Input,
2849 Param,
2850 Constant,
2851 Activation,
2852 Cast,
2853 StopGradient,
2854 Binary,
2855 Compare,
2856 Where,
2857 Fma,
2858 ElementwiseRegion,
2859 TransformRegion,
2860 BatchElementwiseRegion,
2861 MatMul,
2862 ScaledMatMul,
2863 ScaledQuantize,
2864 ScaledQuantScale,
2865 ScaledDequantize,
2866 DotGeneral,
2867 LayerNorm,
2868 LayerNorm2d,
2869 GroupNorm,
2870 RmsNorm,
2871 ResizeNearest2x,
2872 AxialRope2d,
2873 Attention,
2874 AttentionBackward,
2875 RmsNormBackwardInput,
2876 RmsNormBackwardGamma,
2877 RmsNormBackwardBeta,
2878 RopeBackward,
2879 Cumsum,
2880 CumsumBackward,
2881 GatherBackward,
2882 Conv2dBackwardInput,
2883 Conv2dBackwardWeight,
2884 MaxPool2dBackward,
2885 Rope,
2886 Reshape,
2887 Transpose,
2888 Narrow,
2889 Concat,
2890 Expand,
2891 Gather,
2892 Reverse,
2893 Reduce,
2894 Softmax,
2895 SoftmaxCrossEntropy,
2896 SoftmaxCrossEntropyWithLogits,
2897 SoftmaxCrossEntropyBackward,
2898 ArgMax,
2899 ArgMin,
2900 TopK,
2901 Sample,
2902 RngNormal,
2903 RngUniform,
2904 Conv,
2905 Im2Col,
2906 ConvTranspose2d,
2907 Pool,
2908 GroupedMatMul,
2909 DequantGroupedMatMul,
2910 DequantMoEWeights,
2911 ScatterAdd,
2912 DequantMatMul,
2913 GatedDeltaNet,
2914 SelectiveScan,
2915 Lstm,
2916 Gru,
2917 Rnn,
2918 Mamba2,
2919 FusedSwiGLU,
2920 FusedMatMulBiasAct,
2921 FusedResidualLN,
2922 FusedResidualRmsNorm,
2923 FusedAttentionBlock,
2929 Custom,
2935 Fft,
2941 LogMel,
2942 LogMelBackward,
2943 WelchPeaks,
2944 GaussianSplatRender,
2946 GaussianSplatRenderBackward,
2947 GaussianSplatPrepare,
2948 GaussianSplatRasterize,
2949 ]
2950 };
2951
2952 impl Backend for MetalBackend {
2953 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
2954 METAL_SUPPORTED_OPS
2955 }
2956
2957 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
2958 use rlx_opt::pass::Pass as _;
2959 let graph = rlx_opt::LowerControlFlow.run(graph);
2963 let dispatch = options.kernel_dispatch;
2964 let graph = rlx_opt::legalize_or_rewrite_for_backend_with_config(
2965 graph,
2966 METAL_SUPPORTED_OPS,
2967 dispatch,
2968 )
2969 .unwrap_or_else(|errors| {
2970 panic!("{}", rlx_opt::format_legalize_error("metal", &errors));
2971 });
2972 let graph = crate::precompile::precompile_cleanup(graph, options);
2973
2974 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
2977 Box::new(MetalExecutableWrapper {
2978 inner: MetalExecutable::compile_with_policy(
2979 graph,
2980 options.policy.clone(),
2981 Some(METAL_SUPPORTED_OPS),
2982 options.rng,
2983 ),
2984 io_manifest,
2985 })
2986 }
2987
2988 fn compile_lir(
2989 &self,
2990 lir: LirModule,
2991 options: &CompileOptions,
2992 ) -> Box<dyn ExecutableGraph> {
2993 use rlx_opt::pass::Pass as _;
2994 let mut graph = lir.into_graph();
2995 graph = rlx_opt::LowerControlFlow.run(graph);
2996 let dispatch = options.kernel_dispatch;
2997 let mut graph = rlx_opt::legalize_or_rewrite_for_backend_with_config(
2998 graph,
2999 METAL_SUPPORTED_OPS,
3000 dispatch,
3001 )
3002 .unwrap_or_else(|errors| {
3003 panic!("{}", rlx_opt::format_legalize_error("metal", &errors));
3004 });
3005 graph = crate::precompile::precompile_cleanup(graph, options);
3006 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
3007 Box::new(MetalExecutableWrapper {
3008 inner: MetalExecutable::compile_from_fused(
3009 graph,
3010 options.policy.clone(),
3011 Some(METAL_SUPPORTED_OPS),
3012 options.rng,
3013 ),
3014 io_manifest,
3015 })
3016 }
3017 }
3018
3019 struct MetalExecutableWrapper {
3020 inner: MetalExecutable,
3021 io_manifest: cpu_low_precision::IoDtypeManifest,
3022 }
3023
3024 unsafe impl Send for MetalExecutableWrapper {}
3025
3026 impl ExecutableGraph for MetalExecutableWrapper {
3027 fn set_param(&mut self, name: &str, data: &[f32]) {
3028 self.inner.set_param(name, data);
3029 }
3030
3031 fn finalize_params(&mut self) {
3032 self.inner.preload_qmatmul_weights();
3033 }
3034
3035 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
3036 self.inner.run(inputs)
3037 }
3038 fn run_read_outputs(
3039 &mut self,
3040 inputs: &[(&str, &[f32])],
3041 read_indices: Option<&[usize]>,
3042 ) -> Vec<Vec<f32>> {
3043 self.inner.run_read_outputs(inputs, read_indices)
3044 }
3045 fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
3046 self.inner.bind_gpu_handle(name, data)
3047 }
3048 fn has_gpu_handle(&self, name: &str) -> bool {
3049 self.inner.has_gpu_handle(name)
3050 }
3051 fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
3052 self.inner.set_gpu_handle_feed(handle_name, output_index);
3053 true
3054 }
3055 fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
3056 self.inner.read_gpu_handle(name)
3057 }
3058 fn register_kv_row_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
3059 self.inner.register_kv_row_feed(handle_name, output_index);
3060 true
3061 }
3062 fn feed_kv_row(&mut self, src_row: usize, dst_row: usize, row_elems: usize) -> bool {
3063 self.inner.feed_kv_row(src_row, dst_row, row_elems);
3064 true
3065 }
3066 fn read_output_row(
3067 &self,
3068 out_idx: usize,
3069 row: usize,
3070 row_inner: usize,
3071 ) -> Option<Vec<f32>> {
3072 Some(self.inner.read_graph_output_row(out_idx, row, row_inner))
3073 }
3074 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
3075 self.inner.run_slots(inputs)
3076 }
3077 fn arena_ptr(&self) -> *const u8 {
3078 self.inner.arena_ptr()
3079 }
3080 fn commit_no_wait(&mut self, inputs: &[(&str, &[f32])]) {
3081 self.inner.commit_no_wait(inputs);
3082 }
3083 fn sync_pending(&mut self) {
3084 self.inner.sync_pending();
3085 }
3086 fn run_pipelined(&mut self, input_sets: &[Vec<(&str, &[f32])>]) -> Vec<Vec<Vec<f32>>> {
3087 self.inner.run_pipelined(input_sets)
3088 }
3089 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
3090 self.inner.set_active_extent(extent);
3091 }
3092
3093 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
3094 self.inner.set_rng(rng);
3095 }
3096
3097 fn rng(&self) -> rlx_ir::RngOptions {
3098 self.inner.rng()
3099 }
3100
3101 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
3107 if matches!(
3108 dtype,
3109 rlx_ir::DType::U8
3110 | rlx_ir::DType::I8
3111 | rlx_ir::DType::I32
3112 | rlx_ir::DType::I64
3113 | rlx_ir::DType::U32
3114 | rlx_ir::DType::F64
3115 ) {
3116 self.inner.set_param_bytes(name, data);
3117 return;
3118 }
3119 if dtype == rlx_ir::DType::F32 {
3120 let n = data.len() / 4;
3121 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
3122 self.inner.set_param(name, s);
3123 } else {
3124 let f32_buf = super::widen_bytes_to_f32(data, dtype);
3125 self.inner.set_param(name, &f32_buf);
3126 }
3127 }
3128
3129 fn run_typed(
3133 &mut self,
3134 inputs: &[(&str, &[u8], rlx_ir::DType)],
3135 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
3136 self.inner.run_typed(inputs)
3137 }
3138
3139 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
3140 Box::new(MetalExecutableWrapper {
3141 inner: self.inner.clone_for_cache(),
3142 io_manifest: self.io_manifest.clone(),
3143 })
3144 }
3145 }
3146}
3147
3148#[cfg(feature = "cuda")]
3151pub mod cuda_backend {
3152 use super::*;
3153 use rlx_cuda::backend::CudaExecutable;
3154
3155 pub struct CudaBackend;
3156
3157 const CUDA_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
3166 use rlx_ir::OpKind::*;
3167 &[
3168 Input,
3169 Param,
3170 Constant,
3171 Activation,
3172 Cast,
3173 StopGradient,
3174 Binary,
3175 Compare,
3176 Where,
3177 ElementwiseRegion,
3178 TransformRegion,
3179 BatchElementwiseRegion,
3180 MatMul,
3181 ScaledMatMul,
3182 ScaledQuantize,
3183 ScaledQuantScale,
3184 ScaledDequantize,
3185 DotGeneral,
3186 LayerNorm,
3187 LayerNorm2d,
3188 GroupNorm,
3189 ResizeNearest2x,
3190 AxialRope2d,
3191 Reverse,
3192 ArgMax,
3193 ArgMin,
3194 RmsNorm,
3195 Attention,
3196 AttentionBackward,
3197 RmsNormBackwardInput,
3198 RmsNormBackwardGamma,
3199 RmsNormBackwardBeta,
3200 RopeBackward,
3201 CumsumBackward,
3202 GatherBackward,
3203 Conv2dBackwardInput,
3204 Conv2dBackwardWeight,
3205 MaxPool2dBackward,
3206 Rope,
3207 Reshape,
3208 Transpose,
3209 Narrow,
3210 Concat,
3211 Expand,
3212 Gather,
3213 Reduce,
3214 Softmax,
3215 Cumsum,
3216 TopK,
3217 Sample,
3218 Conv,
3219 ConvTranspose2d,
3220 Pool,
3221 GroupedMatMul,
3222 DequantGroupedMatMul,
3223 DequantMoEWeights,
3224 ScatterAdd,
3225 DequantMatMul,
3226 SelectiveScan,
3227 Lstm,
3228 FusedMatMulBiasAct,
3229 FusedResidualLN,
3230 FusedResidualRmsNorm,
3231 FusedAttentionBlock,
3235 GaussianSplatRender,
3236 GaussianSplatRenderBackward,
3237 GaussianSplatPrepare,
3238 GaussianSplatRasterize,
3239 Custom,
3240 Fft,
3241 LogMel,
3242 LogMelBackward,
3243 WelchPeaks,
3244 Im2Col,
3245 RngNormal,
3246 RngUniform,
3247 ]
3248 };
3249
3250 impl Backend for CudaBackend {
3251 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
3252 CUDA_SUPPORTED_OPS
3253 }
3254
3255 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
3256 use rlx_opt::pass::Pass as _;
3257 let graph = rlx_cuda::unfuse::unfuse(graph);
3260 let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, CUDA_SUPPORTED_OPS)
3261 .unwrap_or_else(|errors| {
3262 panic!("{}", rlx_opt::format_legalize_error("cuda", &errors));
3263 });
3264 let graph = crate::precompile::precompile_cleanup(graph, options);
3265 let graph = rlx_opt::LegalizeBroadcast.run(graph);
3267 let compile_result = crate::stages::compile_graph_stages_for_backend(
3269 rlx_driver::Device::Cuda,
3270 graph,
3271 options,
3272 CUDA_SUPPORTED_OPS,
3273 );
3274 crate::stages::maybe_log_fusion(&compile_result.fusion);
3275 let graph = compile_result.lir.into_graph();
3276 let graph = match options.policy.clone() {
3277 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
3278 None => graph,
3279 };
3280 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
3281 Box::new(CudaExecutableWrapper {
3282 inner: CudaExecutable::compile_rng(graph, options.rng),
3283 io_manifest,
3284 })
3285 }
3286
3287 fn compile_lir(
3288 &self,
3289 lir: LirModule,
3290 options: &CompileOptions,
3291 ) -> Box<dyn ExecutableGraph> {
3292 use rlx_opt::pass::Pass as _;
3293 let graph = rlx_opt::LegalizeBroadcast.run(lir.into_graph());
3294 let (graph, io_manifest) =
3295 cpu_low_precision::prepare_f32_exec_graph(prepare_fused_graph(
3296 rlx_cuda::unfuse::unfuse(graph),
3297 options,
3298 CUDA_SUPPORTED_OPS,
3299 "cuda",
3300 ));
3301 Box::new(CudaExecutableWrapper {
3302 inner: CudaExecutable::compile_rng(graph, options.rng),
3303 io_manifest,
3304 })
3305 }
3306 }
3307
3308 struct CudaExecutableWrapper {
3309 inner: CudaExecutable,
3310 io_manifest: cpu_low_precision::IoDtypeManifest,
3311 }
3312
3313 unsafe impl Send for CudaExecutableWrapper {}
3318
3319 impl ExecutableGraph for CudaExecutableWrapper {
3320 fn set_param(&mut self, name: &str, data: &[f32]) {
3321 self.inner.set_param(name, data);
3322 }
3323 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
3324 self.inner.run(inputs)
3325 }
3326 fn run_read_outputs(
3327 &mut self,
3328 inputs: &[(&str, &[f32])],
3329 read_indices: Option<&[usize]>,
3330 ) -> Vec<Vec<f32>> {
3331 self.inner.run_read_outputs(inputs, read_indices)
3332 }
3333 fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
3334 self.inner.bind_gpu_handle(name, data)
3335 }
3336 fn has_gpu_handle(&self, name: &str) -> bool {
3337 self.inner.has_gpu_handle(name)
3338 }
3339 fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
3340 self.inner.set_gpu_handle_feed(handle_name, output_index);
3341 true
3342 }
3343 fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
3344 self.inner.read_gpu_handle(name)
3345 }
3346 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
3347 self.inner.set_active_extent(extent);
3348 }
3349
3350 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
3351 self.inner.set_rng(rng);
3352 }
3353
3354 fn rng(&self) -> rlx_ir::RngOptions {
3355 self.inner.rng()
3356 }
3357
3358 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
3359 self.inner.run_slots(inputs)
3360 }
3361
3362 fn arena_ptr(&self) -> *const u8 {
3363 self.inner.arena_ptr()
3364 }
3365
3366 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
3371 if matches!(dtype, rlx_ir::DType::U8 | rlx_ir::DType::I8) {
3372 self.inner.set_param_bytes(name, data);
3373 return;
3374 }
3375 if dtype == rlx_ir::DType::F32 {
3376 let n = data.len() / 4;
3377 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
3378 self.inner.set_param(name, s);
3379 } else {
3380 let f32_buf = super::widen_bytes_to_f32(data, dtype);
3381 self.inner.set_param(name, &f32_buf);
3382 }
3383 }
3384
3385 fn run_typed(
3388 &mut self,
3389 inputs: &[(&str, &[u8], rlx_ir::DType)],
3390 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
3391 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
3392 for (name, data, dt) in inputs {
3393 let v = super::widen_bytes_to_f32(data, *dt);
3394 owned.push((name.to_string(), v));
3395 }
3396 let refs: Vec<(&str, &[f32])> = owned
3397 .iter()
3398 .map(|(n, d)| (n.as_str(), d.as_slice()))
3399 .collect();
3400 let dtypes =
3401 super::declared_output_dtypes(&self.io_manifest, self.inner.output_dtypes());
3402 let outs = self.inner.run(&refs);
3403 outs.into_iter()
3404 .zip(
3405 dtypes
3406 .into_iter()
3407 .chain(std::iter::repeat(rlx_ir::DType::F32)),
3408 )
3409 .map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
3410 .collect()
3411 }
3412
3413 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
3414 Box::new(CudaExecutableWrapper {
3415 inner: self.inner.clone_for_cache(),
3416 io_manifest: self.io_manifest.clone(),
3417 })
3418 }
3419 }
3420}
3421
3422#[cfg(feature = "rocm")]
3425pub mod rocm_backend {
3426 use super::*;
3427 use rlx_rocm::backend::RocmExecutable;
3428
3429 pub struct RocmBackend;
3430
3431 const ROCM_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
3434 use rlx_ir::OpKind::*;
3435 &[
3436 Input,
3437 Param,
3438 Constant,
3439 Activation,
3440 Cast,
3441 StopGradient,
3442 Binary,
3443 Compare,
3444 Where,
3445 ElementwiseRegion,
3446 TransformRegion,
3447 BatchElementwiseRegion,
3448 MatMul,
3449 ScaledMatMul,
3450 ScaledQuantize,
3451 ScaledQuantScale,
3452 ScaledDequantize,
3453 DotGeneral,
3454 LayerNorm,
3455 LayerNorm2d,
3456 GroupNorm,
3457 ResizeNearest2x,
3458 AxialRope2d,
3459 Reverse,
3460 ArgMax,
3461 ArgMin,
3462 RmsNorm,
3463 Attention,
3464 AttentionBackward,
3465 RmsNormBackwardInput,
3466 RmsNormBackwardGamma,
3467 RmsNormBackwardBeta,
3468 RopeBackward,
3469 CumsumBackward,
3470 GatherBackward,
3471 Rope,
3472 Reshape,
3473 Transpose,
3474 Narrow,
3475 Concat,
3476 Expand,
3477 Gather,
3478 Reduce,
3479 Softmax,
3480 Cumsum,
3481 TopK,
3482 Sample,
3483 Conv,
3484 ConvTranspose2d,
3485 Pool,
3486 GroupedMatMul,
3487 DequantGroupedMatMul,
3488 DequantMoEWeights,
3489 ScatterAdd,
3490 DequantMatMul,
3491 SelectiveScan,
3492 Lstm,
3493 FusedMatMulBiasAct,
3494 FusedResidualLN,
3495 FusedResidualRmsNorm,
3496 FusedAttentionBlock,
3500 GaussianSplatRender,
3501 GaussianSplatRenderBackward,
3502 GaussianSplatPrepare,
3503 GaussianSplatRasterize,
3504 Custom,
3505 Fft,
3506 LogMel,
3507 LogMelBackward,
3508 WelchPeaks,
3509 Im2Col,
3510 RngNormal,
3511 RngUniform,
3512 ]
3513 };
3514
3515 impl Backend for RocmBackend {
3516 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
3517 ROCM_SUPPORTED_OPS
3518 }
3519
3520 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
3521 use rlx_opt::pass::Pass as _;
3522 let graph = rlx_rocm::unfuse::unfuse(graph);
3523 let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, ROCM_SUPPORTED_OPS)
3524 .unwrap_or_else(|errors| {
3525 panic!("{}", rlx_opt::format_legalize_error("rocm", &errors));
3526 });
3527 let graph = crate::precompile::precompile_cleanup(graph, options);
3528 let graph = rlx_opt::LegalizeBroadcast.run(graph);
3529 let compile_result = crate::stages::compile_graph_stages_for_backend(
3530 rlx_driver::Device::Rocm,
3531 graph,
3532 options,
3533 ROCM_SUPPORTED_OPS,
3534 );
3535 crate::stages::maybe_log_fusion(&compile_result.fusion);
3536 let graph = compile_result.lir.into_graph();
3537 let graph = match options.policy.clone() {
3538 Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
3539 None => graph,
3540 };
3541 let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
3542 Box::new(RocmExecutableWrapper {
3543 inner: RocmExecutable::compile_rng(graph, options.rng),
3544 io_manifest,
3545 })
3546 }
3547
3548 fn compile_lir(
3549 &self,
3550 lir: LirModule,
3551 options: &CompileOptions,
3552 ) -> Box<dyn ExecutableGraph> {
3553 let (graph, io_manifest) =
3554 cpu_low_precision::prepare_f32_exec_graph(prepare_fused_graph(
3555 rlx_rocm::unfuse::unfuse(lir.into_graph()),
3556 options,
3557 ROCM_SUPPORTED_OPS,
3558 "rocm",
3559 ));
3560 Box::new(RocmExecutableWrapper {
3561 inner: RocmExecutable::compile_rng(graph, options.rng),
3562 io_manifest,
3563 })
3564 }
3565 }
3566
3567 struct RocmExecutableWrapper {
3568 inner: RocmExecutable,
3569 io_manifest: cpu_low_precision::IoDtypeManifest,
3570 }
3571
3572 unsafe impl Send for RocmExecutableWrapper {}
3576
3577 impl ExecutableGraph for RocmExecutableWrapper {
3578 fn set_param(&mut self, name: &str, data: &[f32]) {
3579 self.inner.set_param(name, data);
3580 }
3581 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
3582 self.inner.run(inputs)
3583 }
3584 fn run_read_outputs(
3585 &mut self,
3586 inputs: &[(&str, &[f32])],
3587 read_indices: Option<&[usize]>,
3588 ) -> Vec<Vec<f32>> {
3589 self.inner.run_read_outputs(inputs, read_indices)
3590 }
3591 fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
3592 self.inner.bind_gpu_handle(name, data)
3593 }
3594 fn has_gpu_handle(&self, name: &str) -> bool {
3595 self.inner.has_gpu_handle(name)
3596 }
3597 fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
3598 self.inner.set_gpu_handle_feed(handle_name, output_index);
3599 true
3600 }
3601 fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
3602 self.inner.read_gpu_handle(name)
3603 }
3604 fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
3605 self.inner.run_slots(inputs)
3606 }
3607 fn arena_ptr(&self) -> *const u8 {
3608 self.inner.arena_ptr()
3609 }
3610 fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
3611 self.inner.set_active_extent(extent);
3612 }
3613
3614 fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
3615 self.inner.set_rng(rng);
3616 }
3617
3618 fn rng(&self) -> rlx_ir::RngOptions {
3619 self.inner.rng()
3620 }
3621
3622 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
3627 if matches!(dtype, rlx_ir::DType::U8 | rlx_ir::DType::I8) {
3628 self.inner.set_param_bytes(name, data);
3629 return;
3630 }
3631 if dtype == rlx_ir::DType::F32 {
3632 let n = data.len() / 4;
3633 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
3634 self.inner.set_param(name, s);
3635 } else {
3636 let f32_buf = super::widen_bytes_to_f32(data, dtype);
3637 self.inner.set_param(name, &f32_buf);
3638 }
3639 }
3640
3641 fn run_typed(
3644 &mut self,
3645 inputs: &[(&str, &[u8], rlx_ir::DType)],
3646 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
3647 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
3648 for (name, data, dt) in inputs {
3649 let v = super::widen_bytes_to_f32(data, *dt);
3650 owned.push((name.to_string(), v));
3651 }
3652 let refs: Vec<(&str, &[f32])> = owned
3653 .iter()
3654 .map(|(n, d)| (n.as_str(), d.as_slice()))
3655 .collect();
3656 let dtypes =
3657 super::declared_output_dtypes(&self.io_manifest, self.inner.output_dtypes());
3658 let outs = self.inner.run(&refs);
3659 outs.into_iter()
3660 .zip(
3661 dtypes
3662 .into_iter()
3663 .chain(std::iter::repeat(rlx_ir::DType::F32)),
3664 )
3665 .map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
3666 .collect()
3667 }
3668
3669 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
3670 Box::new(RocmExecutableWrapper {
3671 inner: self.inner.clone_for_cache(),
3672 io_manifest: self.io_manifest.clone(),
3673 })
3674 }
3675 }
3676}
3677
3678#[cfg(feature = "tpu")]
3681pub mod tpu_backend {
3682 use super::*;
3683 use rlx_tpu::TpuExecutable;
3684
3685 pub struct TpuBackend;
3686
3687 const TPU_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
3695 use rlx_ir::OpKind::*;
3696 &[
3697 Input,
3698 Param,
3699 Constant,
3700 Activation,
3701 Cast,
3702 StopGradient,
3703 Binary,
3704 Compare,
3705 Where,
3706 ElementwiseRegion,
3707 TransformRegion,
3708 BatchElementwiseRegion,
3709 MatMul,
3710 DotGeneral,
3711 LayerNorm,
3712 RmsNorm,
3713 Attention,
3714 Rope,
3715 Reshape,
3716 Transpose,
3717 Narrow,
3718 Concat,
3719 Expand,
3720 Gather,
3721 Reduce,
3722 Softmax,
3723 Cumsum,
3724 TopK,
3725 Sample,
3726 Conv,
3727 Pool,
3728 GroupedMatMul,
3729 DequantGroupedMatMul,
3730 DequantMoEWeights,
3731 ScatterAdd,
3732 DequantMatMul,
3733 SelectiveScan,
3734 QMatMul,
3736 QConv2d,
3737 Quantize,
3738 Dequantize,
3739 FusedMatMulBiasAct,
3740 FusedResidualLN,
3741 FusedResidualRmsNorm,
3742 FusedAttentionBlock,
3745 Fft,
3746 LogMel,
3747 LogMelBackward,
3748 WelchPeaks,
3749 RngNormal,
3750 RngUniform,
3751 ]
3753 };
3754
3755 impl Backend for TpuBackend {
3756 fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
3757 TPU_SUPPORTED_OPS
3758 }
3759
3760 fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
3761 let graph = rlx_opt::legalize_or_rewrite_for_backend_with_config(
3762 graph,
3763 TPU_SUPPORTED_OPS,
3764 options.kernel_dispatch,
3765 )
3766 .unwrap_or_else(|errors| {
3767 panic!("{}", rlx_opt::format_legalize_error("tpu", &errors));
3768 });
3769 use rlx_opt::pass::Pass as _;
3785 let policy = options
3786 .policy
3787 .clone()
3788 .unwrap_or(rlx_opt::PrecisionPolicy::AutoMixedBf16);
3789 let graph = rlx_opt::AutoMixedPrecision::new(policy).run(graph);
3790 let _ = options.dce;
3791 let _ = options.constant_folding;
3792 Box::new(TpuExecutableWrapper {
3793 inner: TpuExecutable::compile_rng_with_param_bytes(
3794 graph,
3795 options.rng,
3796 options.quant_param_bindings.as_ref(),
3797 ),
3798 })
3799 }
3800 }
3801
3802 struct TpuExecutableWrapper {
3803 inner: TpuExecutable,
3804 }
3805
3806 unsafe impl Send for TpuExecutableWrapper {}
3810
3811 impl ExecutableGraph for TpuExecutableWrapper {
3812 fn set_param(&mut self, name: &str, data: &[f32]) {
3813 self.inner.set_param(name, data);
3814 }
3815 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
3816 self.inner.run(inputs)
3817 }
3818
3819 fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
3824 if dtype == rlx_ir::DType::F32 {
3825 let n = data.len() / 4;
3826 let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
3827 self.inner.set_param(name, s);
3828 } else {
3829 let f32_buf = super::widen_bytes_to_f32(data, dtype);
3830 self.inner.set_param(name, &f32_buf);
3831 }
3832 }
3833
3834 fn run_typed(
3835 &mut self,
3836 inputs: &[(&str, &[u8], rlx_ir::DType)],
3837 ) -> Vec<(Vec<u8>, rlx_ir::DType)> {
3838 let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
3839 for (name, data, dt) in inputs {
3840 let v = super::widen_bytes_to_f32(data, *dt);
3841 owned.push((name.to_string(), v));
3842 }
3843 let refs: Vec<(&str, &[f32])> = owned
3844 .iter()
3845 .map(|(n, d)| (n.as_str(), d.as_slice()))
3846 .collect();
3847 let dtypes = self.inner.output_dtypes();
3848 let outs = self.inner.run(&refs);
3849 outs.into_iter()
3850 .zip(
3851 dtypes
3852 .into_iter()
3853 .chain(std::iter::repeat(rlx_ir::DType::F32)),
3854 )
3855 .map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
3856 .collect()
3857 }
3858
3859 fn clone_box(&self) -> Box<dyn ExecutableGraph> {
3860 Box::new(TpuExecutableWrapper {
3861 inner: self.inner.clone_for_cache(),
3862 })
3863 }
3864 }
3865}
3866
3867#[cfg(feature = "qnn")]
3872pub mod qnn_backend {
3873 use super::*;
3874 use rlx_qnn::runtime::QnnExecutable;
3875
3876 pub struct QnnBackend;
3877
3878 impl Backend for QnnBackend {
3879 fn compile(&self, graph: Graph, _options: &CompileOptions) -> Box<dyn ExecutableGraph> {
3886 let exec = QnnExecutable::compile_graph(&graph)
3887 .unwrap_or_else(|e| panic!("rlx-qnn compile failed: {e}"));
3888 Box::new(QnnExecutableWrapper { inner: exec })
3889 }
3890
3891 fn compile_lir(
3892 &self,
3893 lir: LirModule,
3894 options: &CompileOptions,
3895 ) -> Box<dyn ExecutableGraph> {
3896 self.compile(lir.into_graph(), options)
3899 }
3900 }
3901
3902 struct QnnExecutableWrapper {
3903 inner: QnnExecutable,
3904 }
3905
3906 impl ExecutableGraph for QnnExecutableWrapper {
3907 fn set_param(&mut self, name: &str, data: &[f32]) {
3908 self.inner.set_param(name, data);
3911 }
3912
3913 fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
3914 self.inner
3915 .run(inputs)
3916 .unwrap_or_else(|e| panic!("rlx-qnn run failed: {e}"))
3917 }
3918 }
3919}