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;
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::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()
}
other => panic!(
"widen_bytes_to_f32: dtype {other:?} unsupported on f32-arena backends \
(only F32/F16/BF16 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
}
}
}
pub trait ExecutableGraph: Send {
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 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 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()
}
}
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)
);
}
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
}
#[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 {
use super::*;
use rlx_cpu::{arena::Arena, thunk};
use rlx_ir::{DType, NodeId, Op};
use rlx_opt::memory::{self, MemoryPlan};
use rlx_driver::arena::{read_typed_to_f32, write_typed_from_f32};
pub struct CpuBackend;
const CPU_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
Input,
Param,
Constant,
Activation,
Cast,
StopGradient,
Binary,
Compare,
Where,
Fma,
ElementwiseRegion,
MatMul,
DotGeneral,
DenseSolve,
BatchedDenseSolve,
Scan,
ScanBackward,
ScanBackwardXs,
LayerNorm,
LayerNorm2d,
GroupNorm,
BatchNormInference,
RmsNorm,
ResizeNearest2x,
AxialRope2d,
Attention,
Rope,
Reshape,
Transpose,
Narrow,
Concat,
Expand,
Gather,
Reverse,
Reduce,
Softmax,
Cumsum,
ArgMax,
ArgMin,
TopK,
Sample,
RngNormal,
RngUniform,
Conv,
Im2Col,
ConvTranspose2d,
Pool,
GroupedMatMul,
DequantGroupedMatMul,
DequantMoEWeights,
ScatterAdd,
LoraMatMul,
DequantMatMul,
ScaledMatMul,
ScaledQuantize,
ScaledQuantScale,
ScaledDequantize,
SelectiveScan,
GatedDeltaNet,
Lstm,
Gru,
Rnn,
Mamba2,
FusedSwiGLU,
FusedMatMulBiasAct,
FusedResidualLN,
FusedResidualRmsNorm,
FusedAttentionBlock,
ReluBackward,
ActivationBackward,
FakeQuantize,
FakeQuantizeBackward,
FakeQuantizeLSQ,
FakeQuantizeLSQBackwardX,
FakeQuantizeLSQBackwardScale,
MaxPool2dBackward,
Conv2dBackwardInput,
Conv2dBackwardWeight,
SoftmaxCrossEntropy,
SoftmaxCrossEntropyWithLogits,
SoftmaxCrossEntropyBackward,
AttentionBackward,
LayerNormBackwardInput,
LayerNormBackwardGamma,
BatchNormInferenceBackwardInput,
BatchNormInferenceBackwardGamma,
BatchNormInferenceBackwardBeta,
GroupNormBackwardInput,
GroupNormBackwardGamma,
GroupNormBackwardBeta,
RmsNormBackwardInput,
RmsNormBackwardGamma,
RmsNormBackwardBeta,
RopeBackward,
CumsumBackward,
GatherBackward,
GaussianSplatRender,
GaussianSplatRenderBackward,
GaussianSplatPrepare,
GaussianSplatRasterize,
Custom,
CustomFn,
Fft,
FftButterflyStage,
LogMel,
LogMelBackward,
WelchPeaks,
ComplexNormSq,
ComplexNormSqBackward,
Conjugate,
]
};
impl Backend for CpuBackend {
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
CPU_SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
static ONNX_KERNELS: std::sync::Once = std::sync::Once::new();
ONNX_KERNELS.call_once(rlx_cpu::onnx_ref::register_onnx_reference_kernels);
let graph = rlx_opt::LowerControlFlow.run(graph);
if let Err(errors) = rlx_opt::legalize_for_backend(&graph, CPU_SUPPORTED_OPS) {
panic!("{}", rlx_opt::format_legalize_error("cpu", &errors));
}
let policy = options.policy.clone();
let _precision = options.precision;
let cfg = rlx_cpu::config::RuntimeConfig::global();
let graph = crate::precompile::precompile_cleanup(graph, options);
let mut compile_opts = options.clone();
compile_opts.arena_alignment = cfg.arena_alignment;
let compile_result = crate::stages::compile_graph_stages_for_backend(
rlx_driver::Device::Cpu,
graph,
&compile_opts,
CPU_SUPPORTED_OPS,
);
crate::stages::maybe_log_fusion(&compile_result.fusion);
let fused = compile_result.lir.into_graph();
let fused = match policy {
Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(fused),
None => fused,
};
let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&fused);
let exec_graph = if cpu_low_precision::needs_f32_exec(&fused) {
cpu_low_precision::promote_to_f32(fused)
} else {
fused
};
let plan = memory::plan_memory_aligned(&exec_graph, cfg.arena_alignment);
if cfg.verbose >= 1 {
eprintln!(
"[rlx] arena: {} bytes, {} buffers, alignment: {}",
plan.arena_size,
plan.assignments.len(),
cfg.arena_alignment
);
}
Box::new(build_cpu_executable(
exec_graph,
plan,
io_manifest,
options.rng,
))
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
let alignment = lir.buffers.alignment.max(options.arena_alignment);
let mut graph = lir.into_graph();
{
use rlx_opt::pass::Pass as _;
graph = rlx_opt::LegalizeBroadcast.run(graph);
}
if let Some(p) = options.policy.clone() {
use rlx_opt::pass::Pass;
graph = rlx_opt::AutoMixedPrecision::new(p).run(graph);
}
let io_manifest = cpu_low_precision::IoDtypeManifest::from_graph(&graph);
let promote = cpu_low_precision::needs_f32_exec(&graph);
let exec_graph = if promote {
cpu_low_precision::promote_to_f32(graph)
} else {
graph
};
let plan = memory::plan_memory_aligned(&exec_graph, alignment);
let cfg = rlx_cpu::config::RuntimeConfig::global();
if cfg.verbose >= 1 {
eprintln!(
"[rlx] compile_lir: arena {} bytes ({} buffers, alignment {})",
plan.arena_size,
plan.assignments.len(),
alignment,
);
}
Box::new(build_cpu_executable(
exec_graph,
plan,
io_manifest,
options.rng,
))
}
}
fn build_cpu_executable(
graph: Graph,
plan: MemoryPlan,
io_manifest: cpu_low_precision::IoDtypeManifest,
rng: rlx_ir::RngOptions,
) -> CpuExecutable {
let mut arena = Arena::from_plan(plan);
let mut input_ids = HashMap::new();
let mut param_ids = HashMap::new();
let mut node_dtypes: HashMap<NodeId, DType> = HashMap::new();
for node in graph.nodes() {
node_dtypes.insert(node.id, node.shape.dtype());
match &node.op {
Op::Input { name } => {
input_ids.insert(name.clone(), node.id);
}
Op::Param { name } => {
param_ids.insert(name.clone(), node.id);
}
_ => {}
}
}
let schedule = thunk::compile_thunks_with_rng(&graph, &arena, rng);
let mut input_slots = Vec::new();
for node in graph.nodes() {
if let Op::Input { name } = &node.op {
let off = arena.byte_offset(node.id);
let len = node.shape.num_elements().unwrap_or(0);
input_slots.push((name.clone(), off, len, node.shape.dtype()));
}
}
let output_slots: Vec<(usize, usize)> = graph
.outputs
.iter()
.map(|&id| {
let off = arena.byte_offset(id);
let len = graph.node(id).shape.num_elements().unwrap_or(0);
(off, len)
})
.collect();
for node in graph.nodes() {
if let Op::Constant { data } = &node.op
&& arena.has_buffer(node.id)
&& !data.is_empty()
{
match node.shape.dtype() {
DType::F64
| DType::F16
| DType::BF16
| DType::I64
| DType::I32
| DType::U32 => {
let off = arena.byte_offset(node.id);
let buf = arena.raw_buf_mut();
let n = buf.len().saturating_sub(off).min(data.len());
buf[off..off + n].copy_from_slice(&data[..n]);
}
_ => {
let buf = arena.slice_mut(node.id);
let n_floats = data.len() / 4;
let n = buf.len().min(n_floats);
for i in 0..n {
let bytes = [
data[i * 4],
data[i * 4 + 1],
data[i * 4 + 2],
data[i * 4 + 3],
];
buf[i] = f32::from_le_bytes(bytes);
}
}
}
}
}
CpuExecutable {
graph,
arena,
input_ids,
param_ids,
node_dtypes,
io_manifest,
schedule,
input_slots,
output_slots,
handles: HashMap::new(),
active_extent: None,
moe_resident: None,
moe_resident_layers: None,
moe_topk_capture: None,
baseline_written: false,
}
}
#[derive(Clone)]
struct CpuExecutable {
graph: Graph,
arena: Arena,
input_ids: HashMap<String, NodeId>,
param_ids: HashMap<String, NodeId>,
node_dtypes: HashMap<NodeId, DType>,
io_manifest: cpu_low_precision::IoDtypeManifest,
schedule: thunk::ThunkSchedule,
input_slots: Vec<(String, usize, usize, DType)>,
output_slots: Vec<(usize, usize)>,
handles: HashMap<String, Vec<f32>>,
active_extent: Option<(usize, usize)>,
moe_resident: Option<std::sync::Arc<[bool]>>,
moe_resident_layers: Option<std::sync::Arc<Vec<std::sync::Arc<[bool]>>>>,
moe_topk_capture: Option<std::sync::Arc<rlx_cpu::moe_topk_capture::MoeTopkCapture>>,
baseline_written: bool,
}
unsafe impl Send for CpuExecutable {}
impl CpuExecutable {
fn write_input(&mut self, id: NodeId, data: &[f32]) {
let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
let off = self.arena.byte_offset(id);
let buf = self.arena.raw_buf_mut();
let elem_size = dtype.size_bytes();
let max_elems = (buf.len() - off) / elem_size;
unsafe {
write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
}
}
fn read_output(&self, id: NodeId) -> Vec<f32> {
let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
let off = self.arena.byte_offset(id);
let buf = self.arena.raw_buf();
let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
unsafe { read_typed_to_f32(buf.as_ptr().add(off), dtype, n_elems) }
}
}
impl ExecutableGraph for CpuExecutable {
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(self.clone())
}
fn set_param(&mut self, name: &str, data: &[f32]) {
if let Some(&id) = self.param_ids.get(name)
&& self.arena.has_buffer(id)
{
let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
let off = self.arena.byte_offset(id);
let buf = self.arena.raw_buf_mut();
let elem_size = dtype.size_bytes();
let max_elems = (buf.len() - off) / elem_size;
unsafe {
write_typed_from_f32(buf.as_mut_ptr().add(off), dtype, data, max_elems);
}
}
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.restore_arena_baseline();
let handle_names: Vec<String> = self.handles.keys().cloned().collect();
for name in &handle_names {
if let Some(&id) = self.input_ids.get(name)
&& self.arena.has_buffer(id)
{
let data = self.handles.get(name).cloned().unwrap_or_default();
self.write_input(id, &data);
}
}
for &(name, data) in inputs {
if let Some(&id) = self.input_ids.get(name)
&& self.arena.has_buffer(id)
{
self.write_input(id, data);
}
}
let active_used = if let Some((actual, upper)) = self.active_extent {
thunk::execute_thunks_active(
&self.schedule,
self.arena.raw_buf_mut(),
actual,
upper,
)
} else {
false
};
if !active_used {
thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
}
for (idx, &out_id) in self.graph.outputs.iter().enumerate() {
let name = format!("out{idx}");
if self.handles.contains_key(&name) {
let v = self.read_output(out_id);
self.handles.insert(name, v);
}
}
self.graph
.outputs
.iter()
.map(|&out_id| self.read_output(out_id))
.collect()
}
fn run_raw(&mut self, inputs: &[(&str, &[f32])]) -> Vec<(*const f32, usize)> {
self.restore_arena_baseline();
for &(name, data) in inputs {
if let Some(&id) = self.input_ids.get(name)
&& self.arena.has_buffer(id)
{
self.write_input(id, data);
}
}
thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
self.graph
.outputs
.iter()
.map(|&out_id| {
let (ptr, len) = self.arena.raw_ptr(out_id);
(ptr as *const f32, len)
})
.collect()
}
fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
self.restore_arena_baseline();
let buf = self.arena.raw_buf_mut();
for (i, &data) in inputs.iter().enumerate() {
if i < self.input_slots.len() {
let (_, off, max_len, dtype) = &self.input_slots[i];
unsafe {
write_typed_from_f32(buf.as_mut_ptr().add(*off), *dtype, data, *max_len);
}
}
}
thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
&self.output_slots
}
fn arena_ptr(&self) -> *const u8 {
self.arena.raw_buf_mut_ptr()
}
fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.handles.insert(name.to_string(), data.to_vec());
true
}
fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
self.handles.get(name).cloned()
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.active_extent = extent;
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
*self.schedule.rng.write().unwrap() = rng;
}
fn rng(&self) -> rlx_ir::RngOptions {
*self.schedule.rng.read().unwrap()
}
fn set_moe_resident_experts(&mut self, mask: &[bool]) {
self.moe_resident_layers = None;
self.schedule.moe_resident_layers = None;
self.moe_resident = Some(Arc::from(mask));
self.schedule.moe_resident = self.moe_resident.clone();
}
fn set_moe_resident_experts_per_layer(&mut self, masks: &[&[bool]]) {
self.moe_resident = None;
self.schedule.moe_resident = None;
let layers: Vec<Arc<[bool]>> = masks.iter().map(|m| Arc::from(*m)).collect();
let arc = Arc::new(layers);
self.moe_resident_layers = Some(arc.clone());
self.schedule.moe_resident_layers = Some(arc);
}
fn enable_moe_topk_capture(&mut self, num_experts: usize) -> bool {
let cap = rlx_cpu::moe_topk_capture::MoeTopkCapture::new(num_experts);
self.moe_topk_capture = Some(cap.clone());
self.schedule.moe_topk_capture = Some(cap);
true
}
fn take_moe_topk_capture(&mut self) -> Option<Vec<Vec<u32>>> {
let cap = self.moe_topk_capture.as_ref()?;
let layers = cap.take_layers();
if layers.is_empty() {
None
} else {
Some(layers)
}
}
fn take_moe_residency_stats(&mut self) -> Option<crate::MoeResidencyStats> {
rlx_cpu::moe_residency::take_last_forward_stats()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
if matches!(dtype, DType::F64 | DType::I64 | DType::I32 | DType::U32) {
self.set_param_bytes(name, data, dtype);
return;
}
if matches!(dtype, DType::U8 | DType::I8) {
self.set_param_bytes(name, data, dtype);
return;
}
if dtype == DType::F32 {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.set_param(name, s);
} else {
let f32_buf = super::widen_bytes_to_f32(data, dtype);
self.set_param(name, &f32_buf);
}
}
fn run_typed(
&mut self,
inputs: &[(&str, &[u8], rlx_ir::DType)],
) -> Vec<(Vec<u8>, rlx_ir::DType)> {
let all_f64 = !inputs.is_empty() && inputs.iter().all(|(_, _, dt)| *dt == DType::F64);
if all_f64 {
for (name, data, _) in inputs {
if let Some(&id) = self.input_ids.get(*name) {
if !self.arena.has_buffer(id) {
continue;
}
let off = self.arena.byte_offset(id);
let buf = self.arena.raw_buf_mut();
let n = data.len();
debug_assert!(
off + n <= buf.len(),
"run_typed: input '{name}' overflows arena slot"
);
buf[off..off + n].copy_from_slice(data);
}
}
thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
} else {
let mut f32_owned: Vec<(String, Vec<f32>)> = Vec::new();
for (name, data, dt) in inputs {
let direct = matches!(
*dt,
DType::F64 | DType::I32 | DType::I64 | DType::U32 | DType::C64
);
if direct {
if let Some(&id) = self.input_ids.get(*name) {
if !self.arena.has_buffer(id) {
continue;
}
let off = self.arena.byte_offset(id);
let buf = self.arena.raw_buf_mut();
buf[off..off + data.len()].copy_from_slice(data);
}
} else {
let v = super::widen_bytes_to_f32(data, *dt);
f32_owned.push((name.to_string(), v));
}
}
for (name, data) in &f32_owned {
if let Some(&id) = self.input_ids.get(name.as_str()) {
if self.arena.has_buffer(id) {
self.write_input(id, data);
}
}
}
let active_used = if let Some((actual, upper)) = self.active_extent {
thunk::execute_thunks_active(
&self.schedule,
self.arena.raw_buf_mut(),
actual,
upper,
)
} else {
false
};
if !active_used {
thunk::execute_thunks(&self.schedule, self.arena.raw_buf_mut());
}
}
self.graph
.outputs
.iter()
.enumerate()
.map(|(idx, &id)| {
let exec_dtype = self.graph.node(id).shape.dtype();
let declared = self.io_manifest.output_dtype(idx, exec_dtype);
if matches!(
exec_dtype,
DType::F64
| DType::F16
| DType::BF16
| DType::I32
| DType::I64
| DType::U32
| DType::C64
) {
let n_elems = self.graph.node(id).shape.num_elements().unwrap_or(0);
let n_bytes = n_elems * exec_dtype.size_bytes();
let off = self.arena.byte_offset(id);
let bytes = self.arena.raw_buf()[off..off + n_bytes].to_vec();
return (bytes, declared);
}
let f32_vals = self.read_output(id);
if declared != exec_dtype {
return (super::narrow_f32_to_bytes(&f32_vals, declared), declared);
}
let bytes = f32_vals.iter().flat_map(|v| v.to_le_bytes()).collect();
(bytes, declared)
})
.collect()
}
}
impl CpuExecutable {
fn restore_arena_baseline(&mut self) {
let persistent: std::collections::HashSet<NodeId> = {
let mut s: std::collections::HashSet<NodeId> =
self.param_ids.values().copied().collect();
for node in self.graph.nodes() {
if matches!(node.op, Op::Constant { .. }) {
s.insert(node.id);
}
}
s
};
if !self.baseline_written {
let constants: Vec<(NodeId, DType, Vec<u8>)> = self
.graph
.nodes()
.iter()
.filter_map(|node| {
if let Op::Constant { data } = &node.op
&& self.arena.has_buffer(node.id)
&& !data.is_empty()
{
Some((node.id, node.shape.dtype(), data.clone()))
} else {
None
}
})
.collect();
for (id, dtype, data) in constants {
self.write_constant_to_arena(id, dtype, &data);
}
self.baseline_written = true;
}
let mut keep: Vec<(usize, usize)> = self
.graph
.nodes()
.iter()
.filter_map(|node| {
let id = node.id;
if !persistent.contains(&id) || !self.arena.has_buffer(id) {
return None;
}
let dtype = self.node_dtypes.get(&id).copied().unwrap_or(DType::F32);
let nbytes = node.shape.num_elements().unwrap_or(0) * dtype.size_bytes();
let off = self.arena.byte_offset(id);
Some((off, off + nbytes))
})
.collect();
keep.sort_unstable();
let buf = self.arena.raw_buf_mut();
let len = buf.len();
let mut cursor = 0usize;
for (start, end) in keep {
let start = start.min(len);
if cursor < start {
buf[cursor..start].fill(0);
}
cursor = cursor.max(end.min(len));
}
if cursor < len {
buf[cursor..len].fill(0);
}
}
fn write_constant_to_arena(&mut self, id: NodeId, dtype: DType, data: &[u8]) {
match dtype {
DType::F64 | DType::F16 | DType::BF16 | DType::U8 | DType::I8 => {
let off = self.arena.byte_offset(id);
let buf = self.arena.raw_buf_mut();
let n = buf.len().saturating_sub(off).min(data.len());
buf[off..off + n].copy_from_slice(&data[..n]);
}
_ => {
let buf = self.arena.slice_mut(id);
let n_floats = data.len() / 4;
let n = buf.len().min(n_floats);
for i in 0..n {
let bytes = [
data[i * 4],
data[i * 4 + 1],
data[i * 4 + 2],
data[i * 4 + 3],
];
buf[i] = f32::from_le_bytes(bytes);
}
}
}
}
fn set_param_bytes(&mut self, name: &str, data: &[u8], _dtype: rlx_ir::DType) {
self.write_param_bytes_to_arena(name, data);
}
fn write_param_bytes_to_arena(&mut self, name: &str, data: &[u8]) {
if let Some(&id) = self.param_ids.get(name)
&& self.arena.has_buffer(id)
{
let off = self.arena.byte_offset(id);
let buf = self.arena.raw_buf_mut();
debug_assert!(
off + data.len() <= buf.len(),
"set_param_bytes: '{name}' would overflow arena slot"
);
buf[off..off + data.len()].copy_from_slice(data);
}
}
}
}
#[cfg(feature = "gpu")]
pub mod wgpu_backend {
use super::*;
use rlx_ir::OpKind;
use rlx_wgpu::backend::WgpuExecutable;
pub struct WgpuBackend;
const WGPU_SUPPORTED_OPS: &[OpKind] = &[
OpKind::Input,
OpKind::Param,
OpKind::Constant,
OpKind::Activation,
OpKind::Cast,
OpKind::StopGradient,
OpKind::Binary,
OpKind::Compare,
OpKind::Where,
OpKind::Fma,
OpKind::ElementwiseRegion,
OpKind::TransformRegion,
OpKind::BatchElementwiseRegion,
OpKind::MatMul,
OpKind::DotGeneral,
OpKind::LayerNorm,
OpKind::LayerNorm2d,
OpKind::GroupNorm,
OpKind::ResizeNearest2x,
OpKind::RmsNorm,
OpKind::Attention,
OpKind::AttentionBackward,
OpKind::RmsNormBackwardInput,
OpKind::RmsNormBackwardGamma,
OpKind::RmsNormBackwardBeta,
OpKind::LayerNormBackwardInput,
OpKind::LayerNormBackwardGamma,
OpKind::RopeBackward,
OpKind::CumsumBackward,
OpKind::GatherBackward,
OpKind::Rope,
OpKind::Reshape,
OpKind::Transpose,
OpKind::Narrow,
OpKind::Concat,
OpKind::Expand,
OpKind::Gather,
OpKind::Reverse,
OpKind::Reduce,
OpKind::Softmax,
OpKind::SoftmaxCrossEntropy,
OpKind::ArgMax,
OpKind::ArgMin,
OpKind::Cumsum,
OpKind::TopK,
OpKind::Sample,
OpKind::Conv,
OpKind::Im2Col,
OpKind::Pool,
OpKind::GroupedMatMul,
OpKind::DequantGroupedMatMul,
OpKind::DequantMoEWeights,
OpKind::ScatterAdd,
OpKind::SelectiveScan,
OpKind::Lstm,
OpKind::Gru,
OpKind::Rnn,
OpKind::Mamba2,
OpKind::ConvTranspose2d,
OpKind::Conv3d,
OpKind::ConvTranspose3d,
OpKind::DequantMatMul,
OpKind::FusedMatMulBiasAct,
OpKind::FusedResidualLN,
OpKind::FusedResidualRmsNorm,
OpKind::FusedSwiGLU,
OpKind::FusedAttentionBlock,
OpKind::FusedTransformerLayer,
OpKind::Fft,
OpKind::Scan,
OpKind::LogMel,
OpKind::LogMelBackward,
OpKind::WelchPeaks,
OpKind::GaussianSplatRender,
OpKind::GaussianSplatRenderBackward,
OpKind::GaussianSplatPrepare,
OpKind::GaussianSplatRasterize,
OpKind::Custom,
OpKind::RngNormal,
OpKind::RngUniform,
];
impl Backend for WgpuBackend {
fn supported_ops(&self) -> &'static [OpKind] {
WGPU_SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let graph = rlx_opt::LowerControlFlow.run(graph);
let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, WGPU_SUPPORTED_OPS)
.unwrap_or_else(|errors| {
panic!("{}", rlx_opt::format_legalize_error("wgpu", &errors));
});
let graph = crate::precompile::precompile_cleanup(graph, options);
let graph = rlx_opt::LegalizeBroadcast.run(graph);
let compile_result = crate::stages::compile_graph_stages_for_backend(
rlx_driver::Device::Gpu,
graph,
options,
WGPU_SUPPORTED_OPS,
);
crate::stages::maybe_log_fusion(&compile_result.fusion);
let graph = compile_result.lir.into_graph();
let graph = match options.policy.clone() {
Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
None => graph,
};
let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
Box::new(WgpuExecutableWrapper {
inner: WgpuExecutable::compile_rng(graph, options.rng),
io_manifest,
})
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let graph = rlx_opt::LegalizeBroadcast.run(lir.into_graph());
let graph = prepare_fused_graph(graph, options, WGPU_SUPPORTED_OPS, "wgpu");
let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
Box::new(WgpuExecutableWrapper {
inner: WgpuExecutable::compile_rng(graph, options.rng),
io_manifest,
})
}
}
struct WgpuExecutableWrapper {
inner: WgpuExecutable,
io_manifest: cpu_low_precision::IoDtypeManifest,
}
unsafe impl Send for WgpuExecutableWrapper {}
impl ExecutableGraph for WgpuExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
self.inner.run_read_outputs(inputs, read_indices)
}
fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.inner.bind_gpu_handle(name, data)
}
fn has_gpu_handle(&self, name: &str) -> bool {
self.inner.has_gpu_handle(name)
}
fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.set_gpu_handle_feed(handle_name, output_index);
true
}
fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
self.inner.read_gpu_handle(name)
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.inner.set_active_extent(extent);
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
self.inner.set_rng(rng);
}
fn rng(&self) -> rlx_ir::RngOptions {
self.inner.rng()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
match dtype {
rlx_ir::DType::U8 | rlx_ir::DType::I8 => {
self.inner.set_param_bytes(name, data);
}
rlx_ir::DType::F32 => {
let n = data.len() / 4;
let f32_slice =
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.inner.set_param(name, f32_slice);
}
rlx_ir::DType::F16 => {
let n = data.len() / 2;
let f16_slice =
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const half::f16, n) };
let f32: Vec<f32> = f16_slice.iter().map(|h| h.to_f32()).collect();
self.inner.set_param(name, &f32);
}
rlx_ir::DType::BF16 => {
let n = data.len() / 2;
let bf16_slice = unsafe {
std::slice::from_raw_parts(data.as_ptr() as *const half::bf16, n)
};
let f32: Vec<f32> = bf16_slice.iter().map(|h| h.to_f32()).collect();
self.inner.set_param(name, &f32);
}
other => panic!(
"rlx-wgpu set_param_typed: dtype {other:?} unsupported \
(F32, F16, BF16 only — wgpu arena is f32-uniform)"
),
}
}
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 {
let v: Vec<f32> = match *dt {
rlx_ir::DType::F32 => {
let n = data.len() / 4;
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }
.to_vec()
}
rlx_ir::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()
}
rlx_ir::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()
}
rlx_ir::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()
}
rlx_ir::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()
}
rlx_ir::DType::U8 | rlx_ir::DType::I8 | rlx_ir::DType::Bool => {
data.iter().map(|&b| b as f32).collect()
}
other => {
panic!("rlx-wgpu run_typed: input '{name}' dtype {other:?} unsupported")
}
};
owned.push((name.to_string(), v));
}
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let dtypes =
super::declared_output_dtypes(&self.io_manifest, self.inner.output_dtypes());
let outs = self.inner.run(&refs);
outs.into_iter()
.zip(
dtypes
.into_iter()
.chain(std::iter::repeat(rlx_ir::DType::F32)),
)
.map(|(v, dt)| (narrow_to_dtype(&v, dt), dt))
.collect()
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(WgpuExecutableWrapper {
inner: self.inner.clone_for_cache(),
io_manifest: self.io_manifest.clone(),
})
}
}
fn narrow_to_dtype(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() * 4);
for &x in v {
bytes.extend_from_slice(&x.to_le_bytes());
}
bytes
}
}
}
}
#[cfg(feature = "vulkan")]
pub mod vulkan_backend {
use super::*;
use rlx_ir::OpKind;
use rlx_vulkan::backend::VulkanExecutable;
pub struct VulkanBackend;
impl Backend for VulkanBackend {
fn supported_ops(&self) -> &'static [OpKind] {
rlx_vulkan::backend::SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
Box::new(VulkanExecutableWrapper {
inner: VulkanExecutable::compile_rng(graph, options.rng),
})
}
}
struct VulkanExecutableWrapper {
inner: VulkanExecutable,
}
unsafe impl Send for VulkanExecutableWrapper {}
impl ExecutableGraph for VulkanExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
self.inner.run_read_outputs(inputs, read_indices)
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.inner.set_active_extent(extent);
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
self.inner.set_rng(rng);
}
fn rng(&self) -> rlx_ir::RngOptions {
self.inner.rng()
}
fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.inner.bind_gpu_handle(name, data)
}
fn has_gpu_handle(&self, name: &str) -> bool {
self.inner.has_gpu_handle(name)
}
fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.set_gpu_handle_feed(handle_name, output_index);
true
}
fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
self.inner.read_gpu_handle(name)
}
fn register_kv_row_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.register_kv_row_feed(handle_name, output_index);
true
}
fn feed_kv_row(&mut self, src_row: usize, dst_row: usize, row_elems: usize) -> bool {
self.inner.feed_kv_row(src_row, dst_row, row_elems);
true
}
fn read_output_row(
&self,
out_idx: usize,
row: usize,
row_inner: usize,
) -> Option<Vec<f32>> {
self.inner.read_output_row(out_idx, row, row_inner)
}
fn read_gpu_handle_row(
&self,
name: &str,
row: usize,
row_inner: usize,
) -> Option<Vec<f32>> {
self.inner.read_gpu_handle_row(name, row, row_inner)
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
match dtype {
rlx_ir::DType::U8 | rlx_ir::DType::I8 => self.inner.set_param_bytes(name, data),
rlx_ir::DType::F32 => {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.inner.set_param(name, s);
}
other => {
let f = super::widen_bytes_to_f32(data, other);
self.inner.set_param(name, &f);
}
}
}
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 {
let v = if *dt == rlx_ir::DType::F32 {
let n = data.len() / 4;
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }.to_vec()
} else {
super::widen_bytes_to_f32(data, *dt)
};
owned.push((name.to_string(), v));
}
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let dtypes = self.inner.output_dtypes();
let outs = self.inner.run(&refs);
outs.into_iter()
.zip(
dtypes
.into_iter()
.chain(std::iter::repeat(rlx_ir::DType::F32)),
)
.map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
.collect()
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(VulkanExecutableWrapper {
inner: self.inner.clone_for_cache(),
})
}
}
}
#[cfg(feature = "oneapi")]
pub mod oneapi_backend {
use super::*;
use rlx_ir::OpKind;
use rlx_oneapi::backend::OneApiExecutable;
pub struct OneApiBackend;
impl Backend for OneApiBackend {
fn supported_ops(&self) -> &'static [OpKind] {
rlx_oneapi::backend::SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
Box::new(OneApiExecutableWrapper {
inner: OneApiExecutable::compile_rng(graph, options.rng),
})
}
}
struct OneApiExecutableWrapper {
inner: OneApiExecutable,
}
unsafe impl Send for OneApiExecutableWrapper {}
impl ExecutableGraph for OneApiExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
self.inner.run_read_outputs(inputs, read_indices)
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.inner.set_active_extent(extent);
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
self.inner.set_rng(rng);
}
fn rng(&self) -> rlx_ir::RngOptions {
self.inner.rng()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
match dtype {
rlx_ir::DType::U8 | rlx_ir::DType::I8 => self.inner.set_param_bytes(name, data),
rlx_ir::DType::F32 => {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.inner.set_param(name, s);
}
other => {
let f = super::widen_bytes_to_f32(data, other);
self.inner.set_param(name, &f);
}
}
}
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 {
let v = if *dt == rlx_ir::DType::F32 {
let n = data.len() / 4;
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) }.to_vec()
} else {
super::widen_bytes_to_f32(data, *dt)
};
owned.push((name.to_string(), v));
}
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let dtypes = self.inner.output_dtypes();
let outs = self.inner.run(&refs);
outs.into_iter()
.zip(
dtypes
.into_iter()
.chain(std::iter::repeat(rlx_ir::DType::F32)),
)
.map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
.collect()
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(OneApiExecutableWrapper {
inner: self.inner.clone_for_cache(),
})
}
}
}
#[cfg(all(feature = "mlx", rlx_mlx_host))]
pub mod mlx_backend {
use super::*;
use rlx_mlx::MlxExecutable;
pub struct MlxBackend;
const MLX_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
Input,
Param,
Constant,
Activation,
Cast,
StopGradient,
Binary,
Compare,
Where,
ElementwiseRegion,
TransformRegion,
BatchElementwiseRegion,
MatMul,
DotGeneral,
DenseSolve,
BatchedDenseSolve,
LayerNorm,
LayerNorm2d,
GroupNorm,
ResizeNearest2x,
RmsNorm,
Attention,
Rope,
Reshape,
Transpose,
Narrow,
Concat,
Expand,
Gather,
Reverse,
Reduce,
Softmax,
Cumsum,
ArgMax,
ArgMin,
TopK,
RngNormal,
RngUniform,
Sample,
Conv,
Im2Col,
ConvTranspose2d,
Pool,
GroupedMatMul,
DequantGroupedMatMul,
DequantMoEWeights,
ScatterAdd,
LoraMatMul,
DequantMatMul,
SelectiveScan,
GatedDeltaNet,
FusedSwiGLU,
FusedMatMulBiasAct,
FusedResidualLN,
FusedResidualRmsNorm,
FusedAttentionBlock,
FusedTransformerLayer,
If,
While,
Scan,
ScanBackward,
ScanBackwardXs,
ReluBackward,
ActivationBackward,
SoftmaxCrossEntropy,
SoftmaxCrossEntropyWithLogits,
SoftmaxCrossEntropyBackward,
AttentionBackward,
LayerNormBackwardInput,
LayerNormBackwardGamma,
GroupNormBackwardInput,
GroupNormBackwardGamma,
GroupNormBackwardBeta,
Conv2dBackwardInput,
Conv2dBackwardWeight,
MaxPool2dBackward,
FakeQuantize,
FakeQuantizeBackward,
Custom,
Fft,
LogMel,
LogMelBackward,
WelchPeaks,
GaussianSplatRender,
GaussianSplatRenderBackward,
]
};
impl Backend for MlxBackend {
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
MLX_SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
let compile_result = crate::stages::compile_graph_stages_for_backend(
rlx_driver::Device::Mlx,
graph,
options,
MLX_SUPPORTED_OPS,
);
crate::stages::maybe_log_fusion(&compile_result.fusion);
self.compile_lir(compile_result.lir, options)
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let mut graph = lir.into_graph();
graph = rlx_opt::LowerControlFlow.run(graph);
let graph = prepare_fused_graph(graph, options, MLX_SUPPORTED_OPS, "mlx");
Box::new(build_mlx_executable(graph, options.rng))
}
}
fn build_mlx_executable(graph: Graph, rng: rlx_ir::RngOptions) -> MlxExecutableWrapper {
let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
let mode = mlx_mode_from_env();
let mut exe = MlxExecutable::compile_from_fused_with_rng(graph, mode, rng);
if mode == rlx_mlx::lower::MlxMode::Compiled {
if let Err(e) = exe.warm_compile() {
eprintln!(
"[rlx-runtime] MLX warm_compile failed ({e}); first run will pay the trace cost"
);
}
}
MlxExecutableWrapper {
inner: exe,
io_manifest,
}
}
fn mlx_mode_from_env() -> rlx_mlx::lower::MlxMode {
match rlx_ir::env::var("RLX_MLX_MODE").as_deref() {
Some(s) if s.eq_ignore_ascii_case("eager") => rlx_mlx::lower::MlxMode::Eager,
Some(s) if s.eq_ignore_ascii_case("lazy") => rlx_mlx::lower::MlxMode::Lazy,
Some(s) if s.eq_ignore_ascii_case("compiled") => rlx_mlx::lower::MlxMode::Compiled,
_ => rlx_mlx::lower::MlxMode::Compiled,
}
}
struct MlxExecutableWrapper {
inner: MlxExecutable,
io_manifest: cpu_low_precision::IoDtypeManifest,
}
unsafe impl Send for MlxExecutableWrapper {}
impl ExecutableGraph for MlxExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
self.inner
.run_read_outputs(inputs, read_indices)
.unwrap_or_else(|e| panic!("MLX run_read_outputs failed: {e}"))
}
fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
self.inner.run_slots(inputs)
}
fn arena_ptr(&self) -> *const u8 {
self.inner.arena_ptr()
}
fn commit_no_wait(&mut self, inputs: &[(&str, &[f32])]) {
self.inner.commit_no_wait(inputs);
}
fn sync_pending(&mut self) {
self.inner.sync_pending();
}
fn run_pipelined(&mut self, input_sets: &[Vec<(&str, &[f32])>]) -> Vec<Vec<Vec<f32>>> {
self.inner.run_pipelined(input_sets)
}
fn bind_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.inner.bind_handle(name, data)
}
fn read_handle(&self, name: &str) -> Option<Vec<f32>> {
self.inner.read_handle(name)
}
fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.inner.bind_gpu_handle(name, data).is_ok()
}
fn has_gpu_handle(&self, name: &str) -> bool {
self.inner.has_gpu_handle(name)
}
fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.set_gpu_handle_feed(handle_name, output_index);
true
}
fn register_kv_row_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.register_kv_row_feed(handle_name, output_index);
true
}
fn feed_kv_row(&mut self, src_row: usize, dst_row: usize, row_elems: usize) -> bool {
self.inner.feed_kv_row(src_row, dst_row, row_elems).is_ok()
}
fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
self.inner.read_gpu_handle(name).ok()
}
fn run_feed_gpu_handle(
&mut self,
inputs: &[(&str, &[f32])],
handle_name: &str,
output_index: usize,
) -> Option<Vec<f32>> {
self.inner
.run_feed_gpu(inputs, handle_name, output_index)
.ok()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
self.inner.set_param_typed(name, data, dtype);
}
fn run_typed(
&mut self,
inputs: &[(&str, &[u8], rlx_ir::DType)],
) -> Vec<(Vec<u8>, rlx_ir::DType)> {
self.inner.run_typed(inputs)
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.inner.set_active_extent(extent);
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
self.inner.set_rng(rng);
}
fn rng(&self) -> rlx_ir::RngOptions {
self.inner.rng()
}
fn copy_params_from(&mut self, src: &dyn ExecutableGraph) -> bool {
let Some(src_any) = src.executable_as_any() else {
return false;
};
let Some(src_wrap) = src_any.downcast_ref::<MlxExecutableWrapper>() else {
return false;
};
let Some(dst_any) = self.executable_as_any_mut() else {
return false;
};
let Some(dst_wrap) = dst_any.downcast_mut::<MlxExecutableWrapper>() else {
return false;
};
dst_wrap.inner.copy_params_from(&src_wrap.inner)
}
fn executable_as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn executable_as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(MlxExecutableWrapper {
inner: self.inner.clone_for_cache(),
io_manifest: self.io_manifest.clone(),
})
}
}
}
pub(crate) const COREML_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
Input,
Param,
Constant,
Activation,
Cast,
Binary,
MatMul,
LayerNorm,
RmsNorm,
Reduce,
Softmax,
Reshape,
Transpose,
Narrow,
Concat,
Gather,
Rope,
Attention,
FusedAttentionBlock,
Compare,
Where,
Expand,
Cumsum,
ScatterAdd,
BatchNormInference,
GroupNorm,
LayerNorm2d,
LoraMatMul,
Conv,
ConvTranspose2d,
Pool,
TopK,
AxialRope2d,
ResizeNearest2x,
StopGradient,
GroupedMatMul,
DequantMatMul,
DequantMoEWeights,
DequantGroupedMatMul,
Quantize,
Dequantize,
SelectiveScan,
GatedDeltaNet,
ArgMax,
ArgMin,
Reverse,
Fft,
LogMel,
Sample,
RngNormal,
Lstm,
Scan,
Gru,
Rnn,
Mamba2,
WelchPeaks,
Custom,
]
};
#[allow(dead_code)] pub(crate) const COREML_BACKWARD_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
ReluBackward,
ActivationBackward,
LayerNormBackwardInput,
LayerNormBackwardGamma,
GroupNormBackwardInput,
GroupNormBackwardGamma,
GroupNormBackwardBeta,
BatchNormInferenceBackwardInput,
BatchNormInferenceBackwardGamma,
BatchNormInferenceBackwardBeta,
RopeBackward,
AttentionBackward,
SoftmaxCrossEntropyBackward,
CumsumBackward,
GatherBackward,
FakeQuantizeBackward,
]
};
#[allow(dead_code)] pub(crate) const COREML_NATIVE_BACKWARD_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
RmsNormBackwardInput,
RmsNormBackwardGamma,
RmsNormBackwardBeta,
LayerNormBackwardInput,
LayerNormBackwardGamma,
GroupNormBackwardInput,
GroupNormBackwardGamma,
GroupNormBackwardBeta,
MaxPool2dBackward,
Conv2dBackwardInput,
Conv2dBackwardWeight,
AttentionBackward,
SoftmaxCrossEntropyWithLogits,
SoftmaxCrossEntropyBackward,
]
};
#[cfg(feature = "training")]
pub(crate) const COREML_SUPPORTED_OPS_TRAINING: [rlx_ir::OpKind;
COREML_SUPPORTED_OPS.len() + COREML_NATIVE_BACKWARD_OPS.len()] = {
let mut arr =
[rlx_ir::OpKind::Input; COREML_SUPPORTED_OPS.len() + COREML_NATIVE_BACKWARD_OPS.len()];
let mut i = 0;
while i < COREML_SUPPORTED_OPS.len() {
arr[i] = COREML_SUPPORTED_OPS[i];
i += 1;
}
let mut j = 0;
while j < COREML_NATIVE_BACKWARD_OPS.len() {
arr[COREML_SUPPORTED_OPS.len() + j] = COREML_NATIVE_BACKWARD_OPS[j];
j += 1;
}
arr
};
#[cfg(all(
feature = "coreml",
target_vendor = "apple",
not(target_os = "watchos")
))]
pub mod coreml_backend {
use super::*;
use crate::Precision;
use rlx_coreml::{CoremlExecutable, default_lower_options};
pub struct CoremlBackend;
impl Backend for CoremlBackend {
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
#[cfg(feature = "training")]
{
&super::COREML_SUPPORTED_OPS_TRAINING
}
#[cfg(not(feature = "training"))]
{
super::COREML_SUPPORTED_OPS
}
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, self.supported_ops())
.unwrap_or_else(|errors| {
panic!("{}", rlx_opt::format_legalize_error("coreml", &errors));
});
let (graph, mut lower_opts) = match options.policy.clone() {
Some(policy) => {
use rlx_opt::pass::Pass as _;
let g = rlx_opt::AutoMixedPrecision::new(policy).run(graph);
let opts = default_lower_options(&g);
(g, opts)
}
None => {
let mut opts = default_lower_options(&graph);
if options.precision == Precision::F16 {
opts.float_dtype = rlx_ir::DType::F16;
}
(graph, opts)
}
};
if let Some(binding) = &options.dim_binding {
let _ = binding;
lower_opts.flexible_inputs = false;
}
Box::new(CoremlExecutableWrapper {
inner: CoremlExecutable::compile_with_lower_opts(graph, lower_opts),
})
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
self.compile(lir.into_graph(), options)
}
}
struct CoremlExecutableWrapper {
inner: CoremlExecutable,
}
unsafe impl Send for CoremlExecutableWrapper {}
impl ExecutableGraph for CoremlExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
self.inner.set_param_typed(name, data, dtype);
}
fn finalize_params(&mut self) {
self.inner
.finalize()
.unwrap_or_else(|e| panic!("CoreML finalize failed: {e}"));
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner
.run(inputs)
.unwrap_or_else(|e| panic!("CoreML run failed: {e}"))
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(CoremlExecutableWrapper {
inner: self.inner.clone_for_cache(),
})
}
fn run_typed(
&mut self,
inputs: &[(&str, &[u8], rlx_ir::DType)],
) -> Vec<(Vec<u8>, rlx_ir::DType)> {
use rlx_ir::DType;
let owned: Vec<(String, Vec<f32>)> = inputs
.iter()
.map(|(name, data, dt)| {
let v: Vec<f32> = match dt {
DType::I64 => data
.chunks_exact(8)
.map(|c| i64::from_le_bytes(c.try_into().unwrap()) as f32)
.collect(),
DType::I32 => data
.chunks_exact(4)
.map(|c| i32::from_le_bytes(c.try_into().unwrap()) as f32)
.collect(),
DType::U8 | DType::Bool => data.iter().map(|&b| b as f32).collect(),
_ => super::widen_bytes_to_f32(data, *dt),
};
(name.to_string(), v)
})
.collect();
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
self.run(&refs)
.into_iter()
.map(|v| {
let bytes: Vec<u8> = v.iter().flat_map(|f| f.to_le_bytes()).collect();
(bytes, DType::F32)
})
.collect()
}
}
}
#[cfg(all(feature = "metal", target_vendor = "apple", not(target_os = "watchos")))]
pub mod metal_backend {
use super::*;
use rlx_metal::backend::MetalExecutable;
pub struct MetalBackend;
const METAL_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
Input,
Param,
Constant,
Activation,
Cast,
StopGradient,
Binary,
Compare,
Where,
Fma,
ElementwiseRegion,
TransformRegion,
BatchElementwiseRegion,
MatMul,
ScaledMatMul,
ScaledQuantize,
ScaledQuantScale,
ScaledDequantize,
DotGeneral,
LayerNorm,
LayerNorm2d,
GroupNorm,
RmsNorm,
ResizeNearest2x,
AxialRope2d,
Attention,
AttentionBackward,
RmsNormBackwardInput,
RmsNormBackwardGamma,
RmsNormBackwardBeta,
RopeBackward,
Cumsum,
CumsumBackward,
GatherBackward,
Conv2dBackwardInput,
Conv2dBackwardWeight,
MaxPool2dBackward,
Rope,
Reshape,
Transpose,
Narrow,
Concat,
Expand,
Gather,
Reverse,
Reduce,
Softmax,
SoftmaxCrossEntropy,
SoftmaxCrossEntropyWithLogits,
SoftmaxCrossEntropyBackward,
ArgMax,
ArgMin,
TopK,
Sample,
RngNormal,
RngUniform,
Conv,
Im2Col,
ConvTranspose2d,
Pool,
GroupedMatMul,
DequantGroupedMatMul,
DequantMoEWeights,
ScatterAdd,
DequantMatMul,
GatedDeltaNet,
SelectiveScan,
Lstm,
Gru,
Rnn,
Mamba2,
FusedSwiGLU,
FusedMatMulBiasAct,
FusedResidualLN,
FusedResidualRmsNorm,
FusedAttentionBlock,
Custom,
Fft,
Scan,
LogMel,
LogMelBackward,
WelchPeaks,
GaussianSplatRender,
GaussianSplatRenderBackward,
GaussianSplatPrepare,
GaussianSplatRasterize,
]
};
impl Backend for MetalBackend {
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
METAL_SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let graph = rlx_opt::LowerControlFlow.run(graph);
let dispatch = options.kernel_dispatch;
let graph = rlx_opt::legalize_or_rewrite_for_backend_with_config(
graph,
METAL_SUPPORTED_OPS,
dispatch,
)
.unwrap_or_else(|errors| {
panic!("{}", rlx_opt::format_legalize_error("metal", &errors));
});
let graph = crate::precompile::precompile_cleanup(graph, options);
let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
Box::new(MetalExecutableWrapper {
inner: MetalExecutable::compile_with_policy(
graph,
options.policy.clone(),
Some(METAL_SUPPORTED_OPS),
options.rng,
),
io_manifest,
})
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let mut graph = lir.into_graph();
graph = rlx_opt::LowerControlFlow.run(graph);
let dispatch = options.kernel_dispatch;
let mut graph = rlx_opt::legalize_or_rewrite_for_backend_with_config(
graph,
METAL_SUPPORTED_OPS,
dispatch,
)
.unwrap_or_else(|errors| {
panic!("{}", rlx_opt::format_legalize_error("metal", &errors));
});
graph = crate::precompile::precompile_cleanup(graph, options);
let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
Box::new(MetalExecutableWrapper {
inner: MetalExecutable::compile_from_fused(
graph,
options.policy.clone(),
Some(METAL_SUPPORTED_OPS),
options.rng,
),
io_manifest,
})
}
}
struct MetalExecutableWrapper {
inner: MetalExecutable,
io_manifest: cpu_low_precision::IoDtypeManifest,
}
unsafe impl Send for MetalExecutableWrapper {}
impl ExecutableGraph for MetalExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn finalize_params(&mut self) {
self.inner.preload_qmatmul_weights();
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
self.inner.run_read_outputs(inputs, read_indices)
}
fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.inner.bind_gpu_handle(name, data)
}
fn has_gpu_handle(&self, name: &str) -> bool {
self.inner.has_gpu_handle(name)
}
fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.set_gpu_handle_feed(handle_name, output_index);
true
}
fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
self.inner.read_gpu_handle(name)
}
fn register_kv_row_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.register_kv_row_feed(handle_name, output_index);
true
}
fn feed_kv_row(&mut self, src_row: usize, dst_row: usize, row_elems: usize) -> bool {
self.inner.feed_kv_row(src_row, dst_row, row_elems);
true
}
fn read_output_row(
&self,
out_idx: usize,
row: usize,
row_inner: usize,
) -> Option<Vec<f32>> {
Some(self.inner.read_graph_output_row(out_idx, row, row_inner))
}
fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
self.inner.run_slots(inputs)
}
fn arena_ptr(&self) -> *const u8 {
self.inner.arena_ptr()
}
fn commit_no_wait(&mut self, inputs: &[(&str, &[f32])]) {
self.inner.commit_no_wait(inputs);
}
fn sync_pending(&mut self) {
self.inner.sync_pending();
}
fn run_pipelined(&mut self, input_sets: &[Vec<(&str, &[f32])>]) -> Vec<Vec<Vec<f32>>> {
self.inner.run_pipelined(input_sets)
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.inner.set_active_extent(extent);
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
self.inner.set_rng(rng);
}
fn rng(&self) -> rlx_ir::RngOptions {
self.inner.rng()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
if matches!(
dtype,
rlx_ir::DType::U8
| rlx_ir::DType::I8
| rlx_ir::DType::I32
| rlx_ir::DType::I64
| rlx_ir::DType::U32
| rlx_ir::DType::F64
) {
self.inner.set_param_bytes(name, data);
return;
}
if dtype == rlx_ir::DType::F32 {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.inner.set_param(name, s);
} else {
let f32_buf = super::widen_bytes_to_f32(data, dtype);
self.inner.set_param(name, &f32_buf);
}
}
fn run_typed(
&mut self,
inputs: &[(&str, &[u8], rlx_ir::DType)],
) -> Vec<(Vec<u8>, rlx_ir::DType)> {
self.inner.run_typed(inputs)
}
fn copy_params_from(&mut self, src: &dyn ExecutableGraph) -> bool {
let Some(src_any) = src.executable_as_any() else {
return false;
};
let Some(src_wrap) = src_any.downcast_ref::<MetalExecutableWrapper>() else {
return false;
};
let Some(dst_any) = self.executable_as_any_mut() else {
return false;
};
let Some(dst_wrap) = dst_any.downcast_mut::<MetalExecutableWrapper>() else {
return false;
};
dst_wrap.inner.copy_params_from(&src_wrap.inner)
}
fn executable_as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn executable_as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(MetalExecutableWrapper {
inner: self.inner.clone_for_cache(),
io_manifest: self.io_manifest.clone(),
})
}
}
}
#[cfg(feature = "cuda")]
pub mod cuda_backend {
use super::*;
use rlx_cuda::backend::CudaExecutable;
pub struct CudaBackend;
const CUDA_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
Input,
Param,
Constant,
Activation,
Cast,
StopGradient,
Binary,
Compare,
Where,
ElementwiseRegion,
TransformRegion,
BatchElementwiseRegion,
MatMul,
ScaledMatMul,
ScaledQuantize,
ScaledQuantScale,
ScaledDequantize,
DotGeneral,
LayerNorm,
LayerNorm2d,
GroupNorm,
ResizeNearest2x,
AxialRope2d,
Reverse,
ArgMax,
ArgMin,
RmsNorm,
Attention,
AttentionBackward,
RmsNormBackwardInput,
RmsNormBackwardGamma,
RmsNormBackwardBeta,
RopeBackward,
CumsumBackward,
GatherBackward,
Conv2dBackwardInput,
Conv2dBackwardWeight,
MaxPool2dBackward,
Rope,
Reshape,
Transpose,
Narrow,
Concat,
Expand,
Gather,
Reduce,
Softmax,
Cumsum,
TopK,
Sample,
Conv,
ConvTranspose2d,
Pool,
GroupedMatMul,
DequantGroupedMatMul,
DequantMoEWeights,
ScatterAdd,
DequantMatMul,
SelectiveScan,
Lstm,
Scan,
FusedMatMulBiasAct,
FusedResidualLN,
FusedResidualRmsNorm,
FusedAttentionBlock,
GaussianSplatRender,
GaussianSplatRenderBackward,
GaussianSplatPrepare,
GaussianSplatRasterize,
Custom,
Fft,
LogMel,
LogMelBackward,
WelchPeaks,
Im2Col,
RngNormal,
RngUniform,
]
};
impl Backend for CudaBackend {
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
CUDA_SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let graph = rlx_cuda::unfuse::unfuse(graph);
let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, CUDA_SUPPORTED_OPS)
.unwrap_or_else(|errors| {
panic!("{}", rlx_opt::format_legalize_error("cuda", &errors));
});
let graph = crate::precompile::precompile_cleanup(graph, options);
let graph = rlx_opt::LegalizeBroadcast.run(graph);
let compile_result = crate::stages::compile_graph_stages_for_backend(
rlx_driver::Device::Cuda,
graph,
options,
CUDA_SUPPORTED_OPS,
);
crate::stages::maybe_log_fusion(&compile_result.fusion);
let graph = compile_result.lir.into_graph();
let graph = match options.policy.clone() {
Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
None => graph,
};
let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
Box::new(CudaExecutableWrapper {
inner: CudaExecutable::compile_rng(graph, options.rng),
io_manifest,
})
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let graph = rlx_opt::LegalizeBroadcast.run(lir.into_graph());
let (graph, io_manifest) =
cpu_low_precision::prepare_f32_exec_graph(prepare_fused_graph(
rlx_cuda::unfuse::unfuse(graph),
options,
CUDA_SUPPORTED_OPS,
"cuda",
));
Box::new(CudaExecutableWrapper {
inner: CudaExecutable::compile_rng(graph, options.rng),
io_manifest,
})
}
}
struct CudaExecutableWrapper {
inner: CudaExecutable,
io_manifest: cpu_low_precision::IoDtypeManifest,
}
unsafe impl Send for CudaExecutableWrapper {}
impl ExecutableGraph for CudaExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
self.inner.run_read_outputs(inputs, read_indices)
}
fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.inner.bind_gpu_handle(name, data)
}
fn has_gpu_handle(&self, name: &str) -> bool {
self.inner.has_gpu_handle(name)
}
fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.set_gpu_handle_feed(handle_name, output_index);
true
}
fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
self.inner.read_gpu_handle(name)
}
fn register_kv_row_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.register_kv_row_feed(handle_name, output_index);
true
}
fn feed_kv_row(&mut self, src_row: usize, dst_row: usize, row_elems: usize) -> bool {
self.inner.feed_kv_row(src_row, dst_row, row_elems);
true
}
fn read_output_row(
&self,
out_idx: usize,
row: usize,
row_inner: usize,
) -> Option<Vec<f32>> {
self.inner.read_output_row(out_idx, row, row_inner)
}
fn read_gpu_handle_row(
&self,
name: &str,
row: usize,
row_inner: usize,
) -> Option<Vec<f32>> {
self.inner.read_gpu_handle_row(name, row, row_inner)
}
fn prepare_resident_gpu_handle(&mut self, name: &str) -> bool {
self.inner.prepare_resident_gpu_handle(name)
}
fn stage_bound_gpu_handles_to_arena(&mut self) {
self.inner.stage_bound_gpu_handles_to_arena();
}
fn seed_resident_kv_prefix_from(
&mut self,
src: &dyn ExecutableGraph,
prefix_tokens: usize,
outgoing_upper: usize,
kv_dim: usize,
n_layers: usize,
) -> bool {
let Some(dst_exe) = self.cuda_executable_for_kv_seed() else {
return false;
};
let Some(src_exe) = src.cuda_executable_for_kv_seed_ref() else {
return false;
};
dst_exe.seed_resident_kv_prefix_from(
src_exe,
prefix_tokens,
outgoing_upper,
kv_dim,
n_layers,
)
}
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 {
let Some(dst_exe) = self.cuda_executable_for_kv_seed() else {
return false;
};
let Some(src_exe) = src.cuda_executable_for_kv_seed_ref() else {
return false;
};
dst_exe.copy_resident_kv_rows_from(
src_exe,
from_row,
to_row,
outgoing_upper,
kv_dim,
n_layers,
)
}
fn cuda_executable_for_kv_seed(
&mut self,
) -> Option<&mut rlx_cuda::backend::CudaExecutable> {
Some(&mut self.inner)
}
fn cuda_executable_for_kv_seed_ref(&self) -> Option<&rlx_cuda::backend::CudaExecutable> {
Some(&self.inner)
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.inner.set_active_extent(extent);
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
self.inner.set_rng(rng);
}
fn rng(&self) -> rlx_ir::RngOptions {
self.inner.rng()
}
fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
self.inner.run_slots(inputs)
}
fn arena_ptr(&self) -> *const u8 {
self.inner.arena_ptr()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
if matches!(dtype, rlx_ir::DType::U8 | rlx_ir::DType::I8) {
self.inner.set_param_bytes(name, data);
return;
}
if dtype == rlx_ir::DType::F32 {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.inner.set_param(name, s);
} else {
let f32_buf = super::widen_bytes_to_f32(data, dtype);
self.inner.set_param(name, &f32_buf);
}
}
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 {
let v = super::widen_bytes_to_f32(data, *dt);
owned.push((name.to_string(), v));
}
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let dtypes =
super::declared_output_dtypes(&self.io_manifest, self.inner.output_dtypes());
let outs = self.inner.run(&refs);
outs.into_iter()
.zip(
dtypes
.into_iter()
.chain(std::iter::repeat(rlx_ir::DType::F32)),
)
.map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
.collect()
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(CudaExecutableWrapper {
inner: self.inner.clone_for_cache(),
io_manifest: self.io_manifest.clone(),
})
}
}
}
#[cfg(feature = "rocm")]
pub mod rocm_backend {
use super::*;
use rlx_rocm::backend::RocmExecutable;
pub struct RocmBackend;
const ROCM_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
Input,
Param,
Constant,
Activation,
Cast,
StopGradient,
Binary,
Compare,
Where,
ElementwiseRegion,
TransformRegion,
BatchElementwiseRegion,
MatMul,
ScaledMatMul,
ScaledQuantize,
ScaledQuantScale,
ScaledDequantize,
DotGeneral,
LayerNorm,
LayerNorm2d,
GroupNorm,
ResizeNearest2x,
AxialRope2d,
Reverse,
ArgMax,
ArgMin,
RmsNorm,
Attention,
AttentionBackward,
RmsNormBackwardInput,
RmsNormBackwardGamma,
RmsNormBackwardBeta,
RopeBackward,
CumsumBackward,
GatherBackward,
Rope,
Reshape,
Transpose,
Narrow,
Concat,
Expand,
Gather,
Reduce,
Softmax,
Cumsum,
TopK,
Sample,
Conv,
ConvTranspose2d,
Pool,
GroupedMatMul,
DequantGroupedMatMul,
DequantMoEWeights,
ScatterAdd,
DequantMatMul,
SelectiveScan,
Lstm,
Scan,
FusedMatMulBiasAct,
FusedResidualLN,
FusedResidualRmsNorm,
FusedAttentionBlock,
GaussianSplatRender,
GaussianSplatRenderBackward,
GaussianSplatPrepare,
GaussianSplatRasterize,
Custom,
Fft,
LogMel,
LogMelBackward,
WelchPeaks,
Im2Col,
RngNormal,
RngUniform,
]
};
impl Backend for RocmBackend {
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
ROCM_SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
use rlx_opt::pass::Pass as _;
let graph = rlx_rocm::unfuse::unfuse(graph);
let graph = rlx_opt::legalize_or_rewrite_for_backend(graph, ROCM_SUPPORTED_OPS)
.unwrap_or_else(|errors| {
panic!("{}", rlx_opt::format_legalize_error("rocm", &errors));
});
let graph = crate::precompile::precompile_cleanup(graph, options);
let graph = rlx_opt::LegalizeBroadcast.run(graph);
let compile_result = crate::stages::compile_graph_stages_for_backend(
rlx_driver::Device::Rocm,
graph,
options,
ROCM_SUPPORTED_OPS,
);
crate::stages::maybe_log_fusion(&compile_result.fusion);
let graph = compile_result.lir.into_graph();
let graph = match options.policy.clone() {
Some(p) => rlx_opt::AutoMixedPrecision::new(p).run(graph),
None => graph,
};
let (graph, io_manifest) = cpu_low_precision::prepare_f32_exec_graph(graph);
Box::new(RocmExecutableWrapper {
inner: RocmExecutable::compile_rng(graph, options.rng),
io_manifest,
})
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
let (graph, io_manifest) =
cpu_low_precision::prepare_f32_exec_graph(prepare_fused_graph(
rlx_rocm::unfuse::unfuse(lir.into_graph()),
options,
ROCM_SUPPORTED_OPS,
"rocm",
));
Box::new(RocmExecutableWrapper {
inner: RocmExecutable::compile_rng(graph, options.rng),
io_manifest,
})
}
}
struct RocmExecutableWrapper {
inner: RocmExecutable,
io_manifest: cpu_low_precision::IoDtypeManifest,
}
unsafe impl Send for RocmExecutableWrapper {}
impl ExecutableGraph for RocmExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn run_read_outputs(
&mut self,
inputs: &[(&str, &[f32])],
read_indices: Option<&[usize]>,
) -> Vec<Vec<f32>> {
self.inner.run_read_outputs(inputs, read_indices)
}
fn bind_gpu_handle(&mut self, name: &str, data: &[f32]) -> bool {
self.inner.bind_gpu_handle(name, data)
}
fn has_gpu_handle(&self, name: &str) -> bool {
self.inner.has_gpu_handle(name)
}
fn set_gpu_handle_feed(&mut self, handle_name: &str, output_index: usize) -> bool {
self.inner.set_gpu_handle_feed(handle_name, output_index);
true
}
fn read_gpu_handle(&self, name: &str) -> Option<Vec<f32>> {
self.inner.read_gpu_handle(name)
}
fn run_slots(&mut self, inputs: &[&[f32]]) -> &[(usize, usize)] {
self.inner.run_slots(inputs)
}
fn arena_ptr(&self) -> *const u8 {
self.inner.arena_ptr()
}
fn set_active_extent(&mut self, extent: Option<(usize, usize)>) {
self.inner.set_active_extent(extent);
}
fn set_rng(&mut self, rng: rlx_ir::RngOptions) {
self.inner.set_rng(rng);
}
fn rng(&self) -> rlx_ir::RngOptions {
self.inner.rng()
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
if matches!(dtype, rlx_ir::DType::U8 | rlx_ir::DType::I8) {
self.inner.set_param_bytes(name, data);
return;
}
if dtype == rlx_ir::DType::F32 {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.inner.set_param(name, s);
} else {
let f32_buf = super::widen_bytes_to_f32(data, dtype);
self.inner.set_param(name, &f32_buf);
}
}
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 {
let v = super::widen_bytes_to_f32(data, *dt);
owned.push((name.to_string(), v));
}
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let dtypes =
super::declared_output_dtypes(&self.io_manifest, self.inner.output_dtypes());
let outs = self.inner.run(&refs);
outs.into_iter()
.zip(
dtypes
.into_iter()
.chain(std::iter::repeat(rlx_ir::DType::F32)),
)
.map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
.collect()
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(RocmExecutableWrapper {
inner: self.inner.clone_for_cache(),
io_manifest: self.io_manifest.clone(),
})
}
}
}
#[cfg(feature = "tpu")]
pub mod tpu_backend {
use super::*;
use rlx_tpu::TpuExecutable;
pub struct TpuBackend;
const TPU_SUPPORTED_OPS: &[rlx_ir::OpKind] = {
use rlx_ir::OpKind::*;
&[
Input,
Param,
Constant,
Activation,
Cast,
StopGradient,
Binary,
Compare,
Where,
ElementwiseRegion,
TransformRegion,
BatchElementwiseRegion,
MatMul,
DotGeneral,
LayerNorm,
RmsNorm,
Attention,
Rope,
Reshape,
Transpose,
Narrow,
Concat,
Expand,
Gather,
Reduce,
Softmax,
Cumsum,
TopK,
Sample,
Conv,
Pool,
GroupedMatMul,
DequantGroupedMatMul,
DequantMoEWeights,
ScatterAdd,
DequantMatMul,
SelectiveScan,
QMatMul,
QConv2d,
Quantize,
Dequantize,
FusedMatMulBiasAct,
FusedResidualLN,
FusedResidualRmsNorm,
FusedAttentionBlock,
Fft,
LogMel,
LogMelBackward,
WelchPeaks,
RngNormal,
RngUniform,
]
};
impl Backend for TpuBackend {
fn supported_ops(&self) -> &'static [rlx_ir::OpKind] {
TPU_SUPPORTED_OPS
}
fn compile(&self, graph: Graph, options: &CompileOptions) -> Box<dyn ExecutableGraph> {
let graph = rlx_opt::legalize_or_rewrite_for_backend_with_config(
graph,
TPU_SUPPORTED_OPS,
options.kernel_dispatch,
)
.unwrap_or_else(|errors| {
panic!("{}", rlx_opt::format_legalize_error("tpu", &errors));
});
use rlx_opt::pass::Pass as _;
let policy = options
.policy
.clone()
.unwrap_or(rlx_opt::PrecisionPolicy::AutoMixedBf16);
let graph = rlx_opt::AutoMixedPrecision::new(policy).run(graph);
let _ = options.dce;
let _ = options.constant_folding;
Box::new(TpuExecutableWrapper {
inner: TpuExecutable::compile_rng_with_param_bytes(
graph,
options.rng,
options.quant_param_bindings.as_ref(),
),
})
}
}
struct TpuExecutableWrapper {
inner: TpuExecutable,
}
unsafe impl Send for TpuExecutableWrapper {}
impl ExecutableGraph for TpuExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner.run(inputs)
}
fn set_param_typed(&mut self, name: &str, data: &[u8], dtype: rlx_ir::DType) {
if dtype == rlx_ir::DType::F32 {
let n = data.len() / 4;
let s = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const f32, n) };
self.inner.set_param(name, s);
} else {
let f32_buf = super::widen_bytes_to_f32(data, dtype);
self.inner.set_param(name, &f32_buf);
}
}
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 {
let v = super::widen_bytes_to_f32(data, *dt);
owned.push((name.to_string(), v));
}
let refs: Vec<(&str, &[f32])> = owned
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let dtypes = self.inner.output_dtypes();
let outs = self.inner.run(&refs);
outs.into_iter()
.zip(
dtypes
.into_iter()
.chain(std::iter::repeat(rlx_ir::DType::F32)),
)
.map(|(v, dt)| (super::narrow_f32_to_bytes(&v, dt), dt))
.collect()
}
fn clone_box(&self) -> Box<dyn ExecutableGraph> {
Box::new(TpuExecutableWrapper {
inner: self.inner.clone_for_cache(),
})
}
}
}
#[cfg(feature = "qnn")]
pub mod qnn_backend {
use super::*;
use rlx_qnn::runtime::QnnExecutable;
pub struct QnnBackend;
impl Backend for QnnBackend {
fn compile(&self, graph: Graph, _options: &CompileOptions) -> Box<dyn ExecutableGraph> {
let exec = QnnExecutable::compile_graph(&graph)
.unwrap_or_else(|e| panic!("rlx-qnn compile failed: {e}"));
Box::new(QnnExecutableWrapper { inner: exec })
}
fn compile_lir(
&self,
lir: LirModule,
options: &CompileOptions,
) -> Box<dyn ExecutableGraph> {
self.compile(lir.into_graph(), options)
}
}
struct QnnExecutableWrapper {
inner: QnnExecutable,
}
impl ExecutableGraph for QnnExecutableWrapper {
fn set_param(&mut self, name: &str, data: &[f32]) {
self.inner.set_param(name, data);
}
fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.inner
.run(inputs)
.unwrap_or_else(|e| panic!("rlx-qnn run failed: {e}"))
}
}
}