use crate::CompileOptions;
use rlx_ir::Graph;
use rlx_ir::hir::HirModule;
use rlx_ir::lir::LirModule;
use std::collections::HashMap;
use std::sync::Arc;
use crate::cpu_low_precision;
#[allow(dead_code)]
pub(crate) fn widen_bytes_to_f32(data: &[u8], dtype: rlx_ir::DType) -> Vec<f32> {
use rlx_ir::DType;
if data.is_empty() {
return Vec::new();
}
match dtype {
DType::F32 => {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
s.to_vec()
}
DType::F64 => {
let n = data.len() / 8;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f64, n) };
s.iter().map(|&x| x as f32).collect()
}
DType::F16 => {
let n = data.len() / 2;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const half::f16, n) };
s.iter().map(|h| h.to_f32()).collect()
}
DType::BF16 => {
let n = data.len() / 2;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const half::bf16, n) };
s.iter().map(|h| h.to_f32()).collect()
}
DType::I64 => {
let n = data.len() / 8;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const i64, n) };
s.iter().map(|&x| x as f32).collect()
}
DType::I32 => {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const i32, n) };
s.iter().map(|&x| x as f32).collect()
}
DType::I8 => data.iter().map(|&b| b as i8 as f32).collect(),
DType::U8 | DType::Bool => data.iter().map(|&b| b as f32).collect(),
other => panic!(
"widen_bytes_to_f32: dtype {other:?} unsupported on f32-arena backends \
(only F32/F64/F16/BF16/I64/I32/I8/U8/Bool are accepted on the host I/O surface)"
),
}
}
#[allow(dead_code)]
pub(crate) fn narrow_f32_to_bytes(v: &[f32], dt: rlx_ir::DType) -> Vec<u8> {
use rlx_ir::DType;
match dt {
DType::F32 => {
let mut bytes = Vec::with_capacity(v.len() * 4);
for &x in v {
bytes.extend_from_slice(&x.to_le_bytes());
}
bytes
}
DType::F16 => {
let mut bytes = Vec::with_capacity(v.len() * 2);
for &x in v {
bytes.extend_from_slice(&half::f16::from_f32(x).to_le_bytes());
}
bytes
}
DType::BF16 => {
let mut bytes = Vec::with_capacity(v.len() * 2);
for &x in v {
bytes.extend_from_slice(&half::bf16::from_f32(x).to_le_bytes());
}
bytes
}
DType::F64 => {
let mut bytes = Vec::with_capacity(v.len() * 8);
for &x in v {
bytes.extend_from_slice(&(x as f64).to_le_bytes());
}
bytes
}
DType::I8 => v.iter().map(|&x| x as i8 as u8).collect(),
DType::U8 => v.iter().map(|&x| x as u8).collect(),
DType::I16 => {
let mut bytes = Vec::with_capacity(v.len() * 2);
for &x in v {
bytes.extend_from_slice(&(x as i16).to_le_bytes());
}
bytes
}
DType::I32 => {
let mut bytes = Vec::with_capacity(v.len() * 4);
for &x in v {
bytes.extend_from_slice(&(x as i32).to_le_bytes());
}
bytes
}
DType::U32 => {
let mut bytes = Vec::with_capacity(v.len() * 4);
for &x in v {
bytes.extend_from_slice(&(x as u32).to_le_bytes());
}
bytes
}
DType::I64 => {
let mut bytes = Vec::with_capacity(v.len() * 8);
for &x in v {
bytes.extend_from_slice(&(x as i64).to_le_bytes());
}
bytes
}
DType::Bool => v
.iter()
.map(|&x| if x != 0.0 { 1u8 } else { 0u8 })
.collect(),
DType::C64 => {
let mut bytes = Vec::with_capacity(v.len() * 8);
for &x in v {
bytes.extend_from_slice(&x.to_le_bytes());
bytes.extend_from_slice(&0.0_f32.to_le_bytes());
}
bytes
}
}
}
#[cfg(test)]
mod f64_boundary_tests {
use super::{narrow_f32_to_bytes, widen_bytes_to_f32};
use rlx_ir::DType;
#[test]
fn widen_f64_narrows_without_panic() {
let vals = [1.5f64, -2.25, 3.0, 4.5];
let bytes: Vec<u8> = vals.iter().flat_map(|x| x.to_le_bytes()).collect();
let out = widen_bytes_to_f32(&bytes, DType::F64);
assert_eq!(out, vec![1.5f32, -2.25, 3.0, 4.5]);
}
#[test]
fn f64_boundary_round_trips() {
let v = [0.5f32, -1.0, 2.0];
let bytes = narrow_f32_to_bytes(&v, DType::F64);
assert_eq!(bytes.len(), v.len() * 8);
assert_eq!(widen_bytes_to_f32(&bytes, DType::F64), v.to_vec());
}
}
#[cfg(all(test, feature = "cpu"))]
mod capabilities_tests {
use super::*;
use rlx_ir::{DType, Graph, Shape};
#[test]
fn cpu_executable_declares_core_capabilities() {
let mut g = Graph::new("caps");
let x = g.input("x", Shape::new(&[2], DType::F32));
g.set_outputs(vec![x]);
let be = cpu_backend::CpuBackend;
let exec = be.compile(g, &CompileOptions::default());
let caps = exec.capabilities();
assert!(caps.clone);
assert!(caps.moe);
assert!(caps.typed_io);
assert!(caps.active_extent);
assert!(!caps.gpu_handles);
assert_eq!(
caps.enabled_names(),
vec!["clone", "moe", "typed_io", "active_extent"]
);
}
}
pub trait ExecutableGraph: Send {
fn capabilities(&self) -> crate::ExecutableCapabilities {
crate::ExecutableCapabilities::NONE
}
fn set_param(&mut self, name: &str, data: &[f32]);
fn finalize_params(&mut self) {}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
panic!("clone_box not implemented for this backend");
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>>;
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
match read_indices {
None => self.run(inputs),
Some(ix) => {
let all = self.run(inputs);
ix.iter().filter_map(|&i| all.get(i).cloned()).collect()
}
}
}
fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
let vecs = self.run(inputs);
vecs.iter().map(|v| (v.as_ptr(), v.len())).collect()
}
fn run_slots(&mut self, _inputs: &[&[f32]]) -> &[(usize, usize)] {
&[] }
fn arena_ptr(&self) -> *const u8 {
std::ptr::null()
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
let _ = extent;
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
let _ = rng;
}
fn rng(&self) -> rlx_ir::RngOptions {
rlx_ir::RngOptions::default()
}
fn set_moe_resident_experts(&mut self, _mask: &[bool]) {}
fn set_moe_resident_experts_per_layer(&mut self, _masks: &[&[bool]]) {}
fn enable_moe_topk_capture(&mut self, _num_experts: usize) -> bool {
false
}
fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
None
}
fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
None
}
fn bind_handle(&mut self, _name: &str, _data: &[f32]) -> bool {
false
}
fn read_handle(&self, _name: &str) -> Option<Vec<f32>> {
None
}
fn bind_gpu_handle(&mut self, _name: &str, _data: &[f32]) -> bool {
false
}
fn has_gpu_handle(&self, _name: &str) -> bool {
false
}
fn set_gpu_handle_feed(&mut self, _handle_name: &str, _output_index: usize) -> bool {
false
}
fn read_gpu_handle(&self, _name: &str) -> Option<Vec<f32>> {
None
}
fn read_gpu_handle_row(&self, _name: &str, _row: usize, _row_inner: usize) -> Option<Vec<f32>> {
None
}
fn register_kv_row_feed(&mut self, _handle_name: &str, _output_index: usize) -> bool {
false
}
fn feed_kv_row(&mut self, _src_row: usize, _dst_row: usize, _row_elems: usize) -> bool {
false
}
fn feed_kv_batch_major(
&mut self,
dst_row: usize,
batch: usize,
seq_cap: usize,
row_elems: usize,
) -> bool {
if batch == 1 {
return self.feed_kv_row(0, dst_row, row_elems);
}
let _ = (dst_row, seq_cap, row_elems);
false
}
fn prepare_resident_gpu_handle(&mut self, _name: &str) -> bool {
false
}
fn stage_bound_gpu_handles_to_arena(&mut self) {}
fn seed_resident_kv_prefix_from(
&mut self,
_src: &dyn ExecutableGraph,
_prefix_tokens: usize,
_outgoing_upper: usize,
_kv_dim: usize,
_n_layers: usize,
) -> bool {
false
}
fn copy_resident_kv_rows_from(
&mut self,
_src: &dyn ExecutableGraph,
_from_row: usize,
_to_row: usize,
_outgoing_upper: usize,
_kv_dim: usize,
_n_layers: usize,
) -> bool {
false
}
fn copy_params_from(&mut self, src: &dyn ExecutableGraph) -> bool {
let _ = src;
false
}
fn share_params_from(&mut self, src: &dyn ExecutableGraph) -> bool {
let _ = src;
false
}
fn executable_as_any(&self) -> Option<&dyn std::any::Any> {
None
}
fn executable_as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
None
}
#[cfg(feature = "cuda")]
fn cuda_executable_for_kv_seed(&mut self) -> Option<&mut rlx_cuda::backend::CudaExecutable> {
let _ = self;
None
}
#[cfg(feature = "cuda")]
fn cuda_executable_for_kv_seed_ref(&self) -> Option<&rlx_cuda::backend::CudaExecutable> {
None
}
fn read_output_row(&self, _out_idx: usize, _row: usize, _row_inner: usize) -> Option<Vec<f32>> {
None
}
fn run_feed_gpu_handle(
&mut self,
inputs: &[(&str, &[f32])],
_handle_name: &str,
_output_index: usize,
) -> Option<Vec<f32>> {
let _ = inputs;
None
}
fn commit_no_wait(&mut self, inputs: &[(&str, &[f32])]) {
let _ = self.run(inputs);
}
fn sync_pending(&mut self) {}
fn run_pipelined(&mut self, input_sets: &[Vec<(&str, &[f32])>]) -> Vec<Vec<Vec<f32>>> {
input_sets.iter().map(|inputs| self.run(inputs)).collect()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
if dtype != rlx_ir::DType::F32 {
panic!(
"backend's default set_param_typed only handles F32; \
got {dtype:?}. Override on the backend for typed support."
);
}
if !data.len().is_multiple_of(4) {
panic!(
"set_param_typed F32: data length {} not a multiple of 4",
data.len()
);
}
let n = data.len() / 4;
let f32_slice = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.set_param(name, f32_slice);
}
fn run_typed(
&mut self,
inputs: &[(&str, &[u8], rlx_ir::DType)],
) -> Vec<(Vec<u8>, rlx_ir::DType)> {
let mut owned: Vec<(String, Vec<f32>)> = Vec::with_capacity(inputs.len());
for (name, data, dt) in inputs {
if *dt != rlx_ir::DType::F32 {
panic!(
"backend's default run_typed only handles F32 inputs; \
got {dt:?} for input '{name}'"
);
}
if data.len() % 4 != 0 {
panic!(
"run_typed F32 input '{name}': len {} not multiple of 4",
data.len()
);
}
let n = data.len() / 4;
let v: Vec<f32> =
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }.to_vec();
owned.push((name.to_string(), v));
}
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let outs = self.run(&refs);
outs.into_iter()
.map(|v| {
let bytes =
unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, v.len() * 4) }
.to_vec();
(bytes, rlx_ir::DType::F32)
})
.collect()
}
#[cfg(target_arch = "wasm32")]
fn wgpu_requires_host(&self) -> bool {
false
}
#[cfg(target_arch = "wasm32")]
fn wgpu_run_async<'a>(
&'a mut self,
_inputs: &'a [(&'a str, &'a [f32])],
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<Vec<f32>>, String>> + 'a>>
{
Box::pin(async move { Err("wgpu_run_async not implemented for this backend".into()) })
}
}
pub trait Backend: Send + Sync {
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph>;
fn compile_lir(&self, lir: LirModule, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
self.compile(lir.into_graph(), options)
}
fn compile_hir(
&self,
hir: HirModule,
device: rlx_driver::Device,
options: &CompileOptions,
) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
let result = crate::stages::compile_hir_stages(device, hir, options)?;
crate::stages::maybe_log_fusion(&result.fusion);
Ok(self.compile_lir(result.lir, options))
}
fn compile_module(
&self,
module: rlx_ir::GraphModule,
device: rlx_driver::Device,
options: &CompileOptions,
) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
let result = crate::stages::compile_module_stages(device, module, options)?;
crate::stages::maybe_log_fusion(&result.fusion);
Ok(self.compile_lir(result.lir, options))
}
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
&[]
}
}
#[allow(dead_code)]
fn prepare_fused_graph(
graph: Graph,
options: &CompileOptions,
supported_ops: &[rlx_ir::OpKind],
backend_name: &str,
) -> Graph {
let (mut graph, report) = rlx_opt::prepare_graph_for_backend_with_report(
graph,
backend_name,
supported_ops,
options.kernel_dispatch,
);
rlx_opt::maybe_log_dispatch_report(&report);
if !report.compile_ready {
panic!(
"{}\n{}",
rlx_opt::format_legalize_error(backend_name, &report.still_unsupported),
rlx_opt::format_dispatch_report(&report)
);
}
if supported_ops.contains(&rlx_ir::OpKind::Scan) {
graph = apply_scan_device_preference(graph, options);
}
graph = crate::precompile::post_fusion_cleanup(graph, options);
if let Some(p) = options.policy.clone() {
use rlx_opt::pass::Pass as _;
graph = rlx_opt::AutoMixedPrecision::new(p).run(graph);
}
graph
}
fn apply_scan_device_preference(graph: Graph, options: &CompileOptions) -> Graph {
let g = rlx_cpu::rlx_maybe_unroll_scans!(graph, options.scan_unroll_max_length);
rlx_opt::maybe_unroll_scans_budget(g, 4096)
}
#[allow(dead_code)]
fn declared_output_dtypes(
manifest: &cpu_low_precision::IoDtypeManifest,
exec_dtypes: Vec<rlx_ir::DType>,
) -> Vec<rlx_ir::DType> {
exec_dtypes
.into_iter()
.enumerate()
.map(|(i, exec)| manifest.output_dtype(i, exec))
.collect()
}
pub fn compile(backend: &dyn Backend, graph: Graph) -> Box<dyn ExecutableGraph> {
backend.compile(graph, &CompileOptions::default())
}
pub fn compile_hir(
backend: &dyn Backend,
hir: HirModule,
device: rlx_driver::Device,
options: &CompileOptions,
) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
backend.compile_hir(hir, device, options)
}
pub fn compile_module(
backend: &dyn Backend,
module: rlx_ir::GraphModule,
device: rlx_driver::Device,
options: &CompileOptions,
) -> Result<Box<dyn ExecutableGraph>, rlx_ir::hir::LowerError> {
backend.compile_module(module, device, options)
}
pub fn compile_with_precision(
backend: &dyn Backend,
graph: Graph,
precision: crate::Precision,
) -> Box<dyn ExecutableGraph> {
backend.compile(graph, &CompileOptions::new().precision(precision))
}
fn _legacy_apply_policy(graph: Graph, policy: Option<rlx_opt::PrecisionPolicy>) -> Graph {
use rlx_opt::pass::Pass as _;
match policy {
Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
None => graph,
}
}
#[cfg(feature = "cpu")]
pub mod cpu_backend;
#[cfg(feature = "gpu")]
pub mod wgpu_backend;
#[cfg(feature = "opengl")]
pub mod webgl_backend;
#[cfg(feature = "vulkan")]
pub mod vulkan_backend;
#[cfg(feature = "oneapi")]
pub mod oneapi_backend;
#[cfg(all(feature = "mlx", rlx_mlx_host))]
pub mod mlx_backend;
#[cfg(all(
feature = "coreml",
target_vendor = "apple",
not(target_os = "watchos")
))]
pub mod coreml_backend;
#[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
pub mod metal_backend;
#[cfg(feature = "cuda")]
pub mod cuda_backend;
#[cfg(feature = "rocm")]
pub mod rocm_backend;
#[cfg(feature = "tpu")]
pub mod tpu_backend;
#[cfg(feature = "qnn")]
pub mod qnn_backend;