#![allow(unused_imports)]
use std::collections::{HashMap, HashSet};
use rlx_ir::RegionPrologue;
use rlx_ir::op::{
Activation, AdaNormKind, BinaryOp, ChainOperand, ChainStep, CmpOp, MaskKind, ReduceOp,
RopeStyle, ScaleMode, SteKind, TransformStep,
};
use rlx_ir::shape::{Dim, DimBinding, Shape};
use rlx_ir::{DType, Graph, NodeId, Op};
use crate::array::{Array, MlxError, async_eval, eval};
use crate::ffi::{MlxMask, MlxReduce, MlxUnary};
use crate::ops;
mod env;
mod helpers;
mod subgraph;
pub use env::lower_with_env;
pub(crate) use helpers::*;
pub(crate) use subgraph::broadcast_leaf_data;
pub use subgraph::{build_leaf_for, expand_leaf_env, lower_subgraph};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MlxMode {
Eager,
#[default]
Lazy,
AsyncCommit,
Compiled,
}
#[derive(Debug, Clone)]
pub enum LeafKey {
Input(String),
Param(String),
Constant, }
pub fn leaf_order(graph: &Graph) -> Vec<(NodeId, LeafKey)> {
let mut out = Vec::new();
for node in graph.nodes() {
match &node.op {
Op::Input { name } => out.push((node.id, LeafKey::Input(name.clone()))),
Op::Param { name } => out.push((node.id, LeafKey::Param(name.clone()))),
Op::Constant { .. } => out.push((node.id, LeafKey::Constant)),
_ => {}
}
}
out
}
pub fn compile_leaf_order(graph: &Graph) -> Vec<(NodeId, LeafKey)> {
let mut seen_input = HashSet::new();
let mut seen_param = HashSet::new();
let mut out = Vec::new();
for node in graph.nodes() {
match &node.op {
Op::Input { name } if seen_input.insert(name.clone()) => {
out.push((node.id, LeafKey::Input(name.clone())));
}
Op::Param { name } if seen_param.insert(name.clone()) => {
out.push((node.id, LeafKey::Param(name.clone())));
}
Op::Constant { .. } => out.push((node.id, LeafKey::Constant)),
_ => {}
}
}
out
}
pub fn first_host_eval_op(graph: &Graph) -> Option<&'static str> {
for node in graph.nodes() {
match &node.op {
Op::DequantMatMul { scheme }
if (scheme.is_gguf() || matches!(scheme, rlx_ir::QuantScheme::Nvfp4Block)) =>
{
return Some("DequantMatMul[GGUF|NVFP4] (host dequant)");
}
Op::DequantGroupedMatMul { scheme } if scheme.is_gguf() => {
return Some("DequantGroupedMatMul[GGUF] (host dequant)");
}
Op::GaussianSplatRender { .. } => return Some("GaussianSplatRender (host kernel)"),
Op::GaussianSplatRenderBackward { .. } => {
return Some("GaussianSplatRenderBackward (host kernel)");
}
Op::LogMel | Op::LogMelBackward => return Some("LogMel (host filterbank)"),
Op::WelchPeaks { .. } => return Some("WelchPeaks (host PSD top-K)"),
Op::Custom { .. } => return Some("Custom (host kernel)"),
Op::RngNormal { .. } | Op::RngUniform { .. } => {
return Some("RngNormal/RngUniform (host fill)");
}
Op::Scan { .. } => return Some("Scan (host packed body)"),
Op::ScanBackward { .. } | Op::ScanBackwardXs { .. } => {
return Some("ScanBackward (host VJP loop)");
}
Op::ScatterNd { .. } => return Some("ScatterNd (host reference)"),
Op::ScatterElements { .. } => return Some("ScatterElements (host reference)"),
Op::GatherNd { .. } => return Some("GatherNd (host reference)"),
Op::GatherElements { .. } => return Some("GatherElements (host reference)"),
Op::ConvTranspose2d { .. } => return Some("ConvTranspose2d (host reference)"),
Op::Conv {
kernel_size,
groups,
..
} if mlx_conv_im2col_too_large(graph, node, kernel_size, *groups) => {
return Some("Conv (oversized im2col → host naive)");
}
Op::Lstm { carry: true, .. } => return Some("Lstm carry (host execute_lstm_f32)"),
Op::Gru { carry: true, .. } => return Some("Gru carry (host execute_gru_f32)"),
Op::Rnn { carry: true, .. } => return Some("Rnn carry (host execute_rnn_f32)"),
_ => {}
}
}
None
}
static MLX_PROFILE: std::sync::Mutex<Option<std::collections::BTreeMap<&'static str, (u64, u64)>>> =
std::sync::Mutex::new(None);
fn mlx_profile_enabled() -> bool {
std::env::var_os("RLX_MLX_PROFILE").is_some()
}
fn mlx_profile_kind(op: &Op) -> &'static str {
match op.kind() {
rlx_ir::OpKind::MatMul => "MatMul",
rlx_ir::OpKind::Binary => "Binary",
rlx_ir::OpKind::Activation => "Activation",
rlx_ir::OpKind::Reduce => "Reduce",
rlx_ir::OpKind::Scan => "Scan",
rlx_ir::OpKind::ScanBackward | rlx_ir::OpKind::ScanBackwardXs => "ScanBackward",
rlx_ir::OpKind::SelectiveScan => "SelectiveScan",
rlx_ir::OpKind::Fft => "Fft",
rlx_ir::OpKind::Conv => "Conv",
_ => "Other",
}
}
fn mlx_profile_record(kind: &'static str, ns: u64) {
if !mlx_profile_enabled() {
return;
}
let mut guard = MLX_PROFILE.lock().expect("mlx profile lock");
let map = guard.get_or_insert_with(std::collections::BTreeMap::new);
let e = map.entry(kind).or_insert((0, 0));
e.0 += 1;
e.1 = e.1.saturating_add(ns);
}
pub fn mlx_profile_report() {
if !mlx_profile_enabled() {
return;
}
let guard = MLX_PROFILE.lock().expect("mlx profile lock");
let Some(map) = guard.as_ref() else {
eprintln!("[rlx-mlx] RLX_MLX_PROFILE: no lower samples recorded");
return;
};
let mut rows: Vec<_> = map.iter().collect();
rows.sort_by_key(|(_, (_, ns))| std::cmp::Reverse(*ns));
eprintln!("[rlx-mlx] RLX_MLX_PROFILE (lower_with_env wall time):");
for (kind, (n, ns)) in rows {
eprintln!(
" {kind:16} calls={n:6} total_ms={:.3} mean_us={:.1}",
*ns as f64 / 1e6,
(*ns as f64 / (*n as f64).max(1.0)) / 1e3
);
}
}
pub fn is_fusable(op: &Op) -> bool {
matches!(
op,
Op::Binary(_)
| Op::Activation(_)
| Op::Compare(_)
| Op::Cast { .. }
| Op::Where
| Op::Expand { .. }
| Op::Fma
| Op::Reshape { .. }
| Op::Transpose { .. }
)
}
pub fn lower_and_run(
graph: &Graph,
params: &HashMap<String, Vec<f32>>,
inputs: &HashMap<String, Vec<f32>>,
mode: MlxMode,
) -> Result<Vec<Array>, MlxError> {
let _perf = rlx_ir::perfetto::TraceSpan::new("lower_and_run", "mlx");
lower_and_run_typed(
graph,
params,
&HashMap::new(),
inputs,
&HashMap::new(),
mode,
)
}
pub fn lower_and_run_typed(
graph: &Graph,
params: &HashMap<String, Vec<f32>>,
params_typed: &HashMap<String, (Vec<u8>, DType)>,
inputs: &HashMap<String, Vec<f32>>,
inputs_typed: &HashMap<String, (Vec<u8>, DType)>,
mode: MlxMode,
) -> Result<Vec<Array>, MlxError> {
lower_and_run_typed_with_extent(
graph,
params,
params_typed,
inputs,
inputs_typed,
mode,
None,
None,
rlx_ir::RngOptions::default(),
)
}
pub fn lower_and_run_typed_with_extent(
graph: &Graph,
params: &HashMap<String, Vec<f32>>,
params_typed: &HashMap<String, (Vec<u8>, DType)>,
inputs: &HashMap<String, Vec<f32>>,
inputs_typed: &HashMap<String, (Vec<u8>, DType)>,
mode: MlxMode,
active_extent: Option<(usize, usize)>,
gpu_inputs: Option<&HashMap<String, Array>>,
rng: rlx_ir::RngOptions,
) -> Result<Vec<Array>, MlxError> {
let resolved_owner;
let graph: &Graph = if has_dynamic_dims(graph) {
let binding = collect_bindings(graph, inputs, inputs_typed)?;
resolved_owner = resolve_graph(graph, &binding);
&resolved_owner
} else {
graph
};
let order = compile_leaf_order(graph);
let mut env: HashMap<NodeId, Array> = HashMap::with_capacity(graph.nodes().len());
for (id, _key) in &order {
env.insert(
*id,
build_leaf_for(
graph,
*id,
params,
inputs,
params_typed,
inputs_typed,
gpu_inputs,
)?,
);
}
env = expand_leaf_env(graph, env)?;
if let Some((actual, upper)) = active_extent
&& actual < upper
&& is_safe_for_active_extent(graph, upper)
{
for (id, _key) in &order {
let node = graph.node(*id);
if !matches!(node.op, Op::Input { .. }) {
continue;
}
let dims = node.shape.dims();
if dims.is_empty() {
continue;
}
let outer = match dims[0] {
Dim::Static(d) => d,
_ => continue,
};
if outer != upper {
continue;
}
let leaf = env.get(id).unwrap();
let in_shape: Vec<usize> = dims.iter().map(|d| d.unwrap_static()).collect();
let mut start = vec![0i32; in_shape.len()];
let mut stop: Vec<i32> = in_shape.iter().map(|&d| d as i32).collect();
start[0] = 0;
stop[0] = actual as i32;
let sliced = ops::slice(leaf, &start, &stop)?;
env.insert(*id, sliced);
}
}
let outs = lower_with_env(graph, env, params, params_typed, rng, true)?;
let refs: Vec<&Array> = outs.iter().collect();
match mode {
MlxMode::Eager => {
for o in &outs {
eval(&[o])?;
}
}
MlxMode::Lazy => {
for (i, o) in refs.iter().enumerate() {
let oid = graph.outputs.get(i).copied();
let name = oid
.and_then(|id| graph.node(id).name.clone())
.unwrap_or_else(|| format!("{oid:?}"));
eval(&[*o]).map_err(|e| MlxError(format!("eval output[{i}] {name}: {e}")))?;
}
}
MlxMode::AsyncCommit => {
async_eval(&refs)?;
}
MlxMode::Compiled => {
eval(&refs)?;
}
}
Ok(outs)
}
pub fn is_safe_for_active_extent(graph: &Graph, upper: usize) -> bool {
let upper_i64 = upper as i64;
for node in graph.nodes() {
match &node.op {
Op::Input { .. } | Op::Param { .. } | Op::Constant { .. } => {}
Op::Activation(_)
| Op::Cast { .. }
| Op::Binary(_)
| Op::Compare(_)
| Op::Where
| Op::ElementwiseRegion { .. }
| Op::BatchElementwiseRegion { .. }
| Op::TransformRegion { .. } => {}
Op::Softmax { axis: _ }
| Op::LayerNorm { .. }
| Op::LayerNorm2d { .. }
| Op::GroupNorm { .. }
| Op::RmsNorm { .. }
| Op::ResizeNearest2x => {}
Op::Rope { .. }
| Op::Attention { .. }
| Op::MatMul
| Op::DotGeneral { .. }
| Op::FusedMatMulBiasAct { .. }
| Op::FusedSwiGLU { .. }
| Op::FusedResidualLN { .. }
| Op::FusedResidualRmsNorm { .. }
| Op::AdaLayerNorm { .. }
| Op::GatedResidual
| Op::AdaLayerNormBackward { .. }
| Op::GatedResidualBackward
| Op::FusedAttentionBlock { .. } => {}
Op::DequantMatMul { .. } | Op::LoraMatMul { .. } => {}
Op::QMatMul { .. } | Op::QConv2d { .. } => return false,
Op::Reduce { axes, .. } => {
if axes.contains(&0) {
return false;
}
}
Op::Cumsum { axis, .. } => {
if *axis == 0 {
return false;
}
}
Op::Concat { axis } => {
if *axis == 0 {
return false;
}
}
Op::Narrow { axis, .. } => {
if *axis == 0 {
return false;
}
}
Op::Transpose { perm } => {
if perm.first().copied() != Some(0) {
return false;
}
}
Op::Reshape { new_shape } => {
if new_shape.contains(&upper_i64) {
return false;
}
}
Op::Expand { target_shape } => {
if target_shape.contains(&upper_i64) {
return false;
}
}
Op::Gather { .. } => return false,
Op::ScatterAdd
| Op::ScatterNd { .. }
| Op::ScatterElements { .. }
| Op::GatherNd { .. }
| Op::GatherElements { .. }
| Op::Sample { .. }
| Op::RngNormal { .. }
| Op::RngUniform { .. }
| Op::TopK { .. }
| Op::SelectiveScan { .. }
| Op::GatedDeltaNet { .. }
| Op::GroupedMatMul
| Op::Pool { .. }
| Op::Conv { .. }
| Op::ConvTranspose2d { .. }
| Op::FusedTransformerLayer { .. }
| Op::DenseSolve
| Op::Custom { .. }
| Op::If { .. }
| Op::While { .. } => return false,
Op::Quantize { .. }
| Op::Dequantize { .. }
| Op::FakeQuantize { .. }
| Op::FakeQuantizeBackward { .. }
| Op::FakeQuantizeLSQ { .. }
| Op::FakeQuantizeLSQBackwardX { .. }
| Op::FakeQuantizeLSQBackwardScale { .. } => return false,
Op::ReluBackward
| Op::ActivationBackward { .. }
| Op::MaxPool2dBackward { .. }
| Op::Conv2dBackwardInput { .. }
| Op::Conv2dBackwardWeight { .. }
| Op::SoftmaxCrossEntropyWithLogits
| Op::SoftmaxCrossEntropyBackward
| Op::LayerNormBackwardInput { .. }
| Op::LayerNormBackwardGamma { .. }
| Op::RmsNormBackwardInput { .. }
| Op::RmsNormBackwardGamma { .. }
| Op::RmsNormBackwardBeta { .. }
| Op::RopeBackward { .. }
| Op::CumsumBackward { .. }
| Op::GatherBackward { .. }
| Op::GroupNormBackwardInput { .. }
| Op::GroupNormBackwardGamma { .. }
| Op::GroupNormBackwardBeta { .. } => return false,
Op::Scan { .. }
| Op::ScanBackward { .. }
| Op::ScanBackwardXs { .. }
| Op::BatchedDenseSolve => return false,
Op::CustomFn { .. } => return false,
Op::Fft { .. } => return true,
Op::ComplexNormSq | Op::ComplexNormSqBackward | Op::Conjugate => return false,
_ => return false,
}
}
true
}
fn has_dynamic_dims(graph: &Graph) -> bool {
graph
.nodes()
.iter()
.any(|n| n.shape.dims().iter().any(|d| !d.is_static()))
}
fn collect_bindings(
graph: &Graph,
inputs: &HashMap<String, Vec<f32>>,
inputs_typed: &HashMap<String, (Vec<u8>, DType)>,
) -> Result<DimBinding, MlxError> {
let mut binding = DimBinding::new();
for node in graph.nodes() {
if let Op::Input { name } = &node.op {
let n_elems = if let Some((bytes, dt)) = inputs_typed.get(name) {
let elem_size = dt.size_bytes();
if elem_size == 0 || bytes.len() % elem_size != 0 {
return Err(MlxError(format!(
"Input '{name}': typed bytes len {} not aligned to dtype size",
bytes.len()
)));
}
bytes.len() / elem_size
} else if let Some(data) = inputs.get(name) {
data.len()
} else {
continue;
};
let mut static_prod: usize = 1;
let mut dynamic_sym: Option<u32> = None;
for d in node.shape.dims().iter() {
match d {
Dim::Static(n) => {
static_prod = static_prod.checked_mul(*n).ok_or_else(|| {
MlxError(format!("Input '{name}': static dim product overflow"))
})?;
}
Dim::Dynamic(sym) => {
if dynamic_sym.is_some() {
return Err(MlxError(format!(
"Input '{name}' has multiple dynamic dims; \
explicit DimBinding required"
)));
}
dynamic_sym = Some(*sym);
}
}
}
if let Some(sym) = dynamic_sym {
if static_prod == 0 {
return Err(MlxError(format!(
"Input '{name}': can't infer dynamic dim against zero \
static product"
)));
}
if n_elems % static_prod != 0 {
return Err(MlxError(format!(
"Input '{name}': nelems {n_elems} not divisible by \
static dim product {static_prod}"
)));
}
let dim_size = n_elems / static_prod;
if let Some(prev) = binding.get(sym) {
if prev != dim_size {
return Err(MlxError(format!(
"Dynamic dim ?{sym}: inconsistent values across \
inputs ({prev} vs {dim_size})"
)));
}
} else {
binding.set(sym, dim_size);
}
}
}
}
Ok(binding)
}
fn resolve_graph(graph: &Graph, binding: &DimBinding) -> Graph {
let mut fresh = Graph::new(&graph.name);
for node in graph.nodes() {
let bound: Shape = node.shape.bind(binding);
fresh.add_node(node.op.clone(), node.inputs.clone(), bound);
}
fresh.set_outputs(graph.outputs.clone());
fresh
}