#![allow(unused_imports)]
use crate::arena::Arena;
use crate::device::metal_device;
use crate::kernels::kernels;
use crate::thunk::{Thunk, ThunkSchedule};
use rlx_ir::{Graph, NodeId, Op};
use rlx_opt::memory;
use std::collections::HashMap;
use super::*;
impl MetalExecutable {
pub fn compile_with_precision(graph: Graph, precision: MetalPrecision) -> Self {
let effective = if precision == MetalPrecision::F16 {
let verbose = rlx_ir::env::var("RLX_VERBOSE")
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or(0)
>= 1;
if verbose {
eprintln!(
"[rlx-metal] F16 requested but full-graph f16 kernels are WIP; using F32"
);
}
MetalPrecision::F32
} else {
precision
};
let mut exe = Self::compile(graph);
exe.precision = effective;
exe
}
pub fn compile(graph: Graph) -> Self {
Self::compile_inner(graph, None, None, false, rlx_ir::RngOptions::default())
}
pub fn compile_with_policy(
graph: Graph,
policy: Option<rlx_opt::PrecisionPolicy>,
supported_ops: Option<&'static [rlx_ir::OpKind]>,
rng: rlx_ir::RngOptions,
) -> Self {
Self::compile_inner(graph, policy, supported_ops, false, rng)
}
pub fn compile_from_fused(
graph: Graph,
policy: Option<rlx_opt::PrecisionPolicy>,
supported_ops: Option<&'static [rlx_ir::OpKind]>,
rng: rlx_ir::RngOptions,
) -> Self {
Self::compile_inner(graph, policy, supported_ops, true, rng)
}
pub(crate) fn compile_inner(
graph: Graph,
policy: Option<rlx_opt::PrecisionPolicy>,
supported_ops: Option<&'static [rlx_ir::OpKind]>,
skip_fusion: bool,
rng: rlx_ir::RngOptions,
) -> Self {
let verbose = rlx_ir::env::var("RLX_VERBOSE")
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or(0)
>= 1;
if verbose {
eprintln!("[rlx-metal] compiling graph: {} nodes", graph.len());
}
crate::mps_blas::invalidate_caches();
let fused = if skip_fusion {
graph
} else {
let mut pipe = rlx_opt::CompilePipeline::new(rlx_opt::FusionTarget::Metal)
.with_assert_fusion_clean(false);
if let Some(ops) = supported_ops {
pipe = pipe.with_supported_ops(ops);
}
let compile_result = pipe.compile_graph(graph);
if verbose {
eprintln!(
"[rlx-metal] fusion: {} → {} nodes",
compile_result.fusion.nodes_before, compile_result.fusion.nodes_after
);
}
compile_result.lir.into_graph()
};
let fused = match policy {
Some(p) => {
use rlx_opt::pass::Pass;
let g = rlx_opt::AutoMixedPrecision::new(p).run(fused);
if verbose {
eprintln!("[rlx-metal] after AutoMixedPrecision: {} nodes", g.len());
}
g
}
None => fused,
};
let fused = lower_fab_for_metal(fused);
let fused = widen_integer_activations_to_f32(fused);
let (fab_scratch_bytes, fab_scratch_rel) = fab_scratch_layout(&fused);
if verbose {
eprintln!("[rlx-metal] after fusion: {} nodes", fused.len());
}
let gdn_scratch = gdn_ephemeral_state_bytes(&fused);
let dequant_scratch = dequant_gguf_scratch_bytes(&fused);
let conv_bwd_scratch = conv_bwd_scratch_bytes(&fused);
let attn_bwd_scratch = crate::attention_bwd_gpu::scratch_bytes(&fused);
let rms_norm_bwd_scratch = rms_norm_bwd_scratch_bytes(&fused);
let onnx_qmatmul_act_scratch = if crate::onnx_qmatmul::ingraph_gpu_enabled() {
crate::onnx_qmatmul::act_scratch_bytes(&fused)
} else {
0
};
let mut plan = memory::plan_memory_with_options(
&fused,
128,
memory::MemoryPlanOptions {
arena_no_reuse: rlx_ir::env::flag("RLX_ARENA_NO_REUSE"),
..Default::default()
},
);
let max_buffer = crate::device::metal_device()
.map(|d| d.device.max_buffer_length() as usize)
.unwrap_or(usize::MAX);
if plan.arena_size > max_buffer {
if verbose {
eprintln!(
"[rlx-metal] arena {} B > maxBufferLength {} B with output-ancestor pin; \
re-planning without it to restore slot reuse",
plan.arena_size, max_buffer
);
}
plan = memory::plan_memory_with_options(
&fused,
128,
memory::MemoryPlanOptions {
pin_output_ancestors: false,
..Default::default()
},
);
}
let mut tail = plan.arena_size;
let gdn_scratch_off = if gdn_scratch > 0 {
tail = (tail + 127) & !127;
let off = tail;
tail = off + gdn_scratch;
off
} else {
0
};
let dequant_scratch_off = if dequant_scratch > 0 {
tail = (tail + 127) & !127;
let off = tail;
tail = off + dequant_scratch;
off
} else {
0
};
let conv_bwd_scratch_off = if conv_bwd_scratch > 0 {
tail = (tail + 127) & !127;
let off = tail;
tail = off + conv_bwd_scratch;
off
} else {
0
};
let attn_bwd_scratch_off = if attn_bwd_scratch > 0 {
tail = (tail + 127) & !127;
let off = tail;
tail = off + attn_bwd_scratch;
off
} else {
0
};
let rms_norm_bwd_scratch_off = if rms_norm_bwd_scratch > 0 {
tail = (tail + 127) & !127;
let off = tail;
tail = off + rms_norm_bwd_scratch;
off
} else {
0
};
let onnx_qmatmul_act_scratch_off = if onnx_qmatmul_act_scratch > 0 {
tail = (tail + 127) & !127;
let off = tail;
tail = off + onnx_qmatmul_act_scratch;
off
} else {
0
};
let fab_scratch_off = if fab_scratch_bytes > 0 {
tail = (tail + 127) & !127;
let off = tail;
tail = off + fab_scratch_bytes;
off
} else {
0
};
plan.arena_size = tail;
let fab_scratch: std::collections::HashMap<rlx_ir::NodeId, (usize, usize)> =
fab_scratch_rel
.iter()
.map(|(id, qkv_rel, attn_rel)| {
(*id, (fab_scratch_off + qkv_rel, fab_scratch_off + attn_rel))
})
.collect();
if verbose && gdn_scratch > 0 {
eprintln!(
"[rlx-metal] GatedDeltaNet scratch: {} bytes @ offset {}",
gdn_scratch, gdn_scratch_off
);
}
if verbose && dequant_scratch > 0 {
eprintln!(
"[rlx-metal] DequantMatMul scratch: {} bytes @ offset {}",
dequant_scratch, dequant_scratch_off
);
}
if verbose && conv_bwd_scratch > 0 {
eprintln!(
"[rlx-metal] Conv2dBackwardWeight scratch: {} bytes @ offset {}",
conv_bwd_scratch, conv_bwd_scratch_off
);
}
if verbose && attn_bwd_scratch > 0 {
eprintln!(
"[rlx-metal] AttentionBackward scratch: {} bytes @ offset {}",
attn_bwd_scratch, attn_bwd_scratch_off
);
}
if verbose && rms_norm_bwd_scratch > 0 {
eprintln!(
"[rlx-metal] RmsNormBackward param scratch: {} bytes @ offset {}",
rms_norm_bwd_scratch, rms_norm_bwd_scratch_off
);
}
if verbose && onnx_qmatmul_act_scratch > 0 {
eprintln!(
"[rlx-metal] onnx.QMatMul act scratch: {} bytes @ offset {}",
onnx_qmatmul_act_scratch, onnx_qmatmul_act_scratch_off
);
}
if verbose {
eprintln!(
"[rlx-metal] arena: {} bytes, {} buffers",
plan.arena_size,
plan.assignments.len()
);
}
if std::env::var_os("RLX_METAL_DEBUG").is_some() {
let mut sizes: Vec<(usize, usize)> = plan
.assignments
.values()
.map(|s| (s.offset, s.size))
.collect();
sizes.sort_by_key(|&(_, sz)| std::cmp::Reverse(sz));
let total: usize = plan.assignments.values().map(|s| s.size).sum();
let max_end = plan
.assignments
.values()
.map(|s| s.offset + s.size)
.max()
.unwrap_or(0);
eprintln!(
"[rlx-metal] arena_size={:.2} GB, {} buffers, sum_slot_bytes={:.2} GB, max_end={:.2} GB",
plan.arena_size as f64 / 1e9,
plan.assignments.len(),
total as f64 / 1e9,
max_end as f64 / 1e9,
);
for (off, sz) in sizes.iter().take(6) {
eprintln!(
" slot off={:.2}GB size={:.3}GB",
*off as f64 / 1e9,
*sz as f64 / 1e9
);
}
}
let arena = Arena::from_plan_with_graph(plan, Some(&fused));
for node in fused.nodes() {
if let Op::Constant { data } = &node.op
&& !data.is_empty()
&& arena.has_buffer(node.id)
{
let off = arena.byte_offset(node.id);
unsafe {
let dst = (arena.buffer.contents() as *mut u8).add(off);
std::ptr::copy_nonoverlapping(data.as_ptr(), dst, data.len());
}
}
}
let schedule = ThunkSchedule::compile_with_rng_fab(&fused, &arena, rng, &fab_scratch);
if verbose {
let nop_count = schedule
.thunks
.iter()
.filter(|t| matches!(t, crate::thunk::Thunk::Nop))
.count();
eprintln!(
"[rlx-metal] schedule: {} thunks ({} compute, {} nop)",
schedule.thunks.len(),
schedule.thunks.len() - nop_count,
nop_count
);
}
let mut input_ids = HashMap::new();
let mut param_ids = HashMap::new();
for node in fused.nodes() {
match &node.op {
Op::Input { name } => {
input_ids.insert(name.clone(), node.id);
}
Op::Param { name } => {
param_ids.insert(name.clone(), node.id);
}
_ => {}
}
}
let output_slots: Vec<(usize, usize)> = fused
.outputs
.iter()
.map(|&id| {
let off = if arena.has_buffer(id) {
arena.byte_offset(id)
} else {
0
};
let logical = fused.node(id).shape.num_elements().unwrap_or(0);
(off, logical)
})
.collect();
let mut input_slots = Vec::new();
for node in fused.nodes() {
if let Op::Input { name } = &node.op {
let off = if arena.has_buffer(node.id) {
arena.byte_offset(node.id)
} else {
0
};
let len = node.shape.num_elements().unwrap_or(0);
input_slots.push((name.clone(), off, len));
}
}
let mps_plan = if rlx_ir::env::flag("RLX_DISABLE_MPSGRAPH") {
None
} else {
let plan = crate::mps_graph_lower::try_lower(&fused);
if verbose {
match &plan {
Some(_) => eprintln!("[rlx-metal] MPSGraph lowering: success"),
None => eprintln!(
"[rlx-metal] MPSGraph lowering: unsupported op or dynamic shape; falling back to thunks"
),
}
}
plan
};
let mps_hybrid = if mps_plan.is_none()
&& rlx_ir::env::is_unset("RLX_DISABLE_MPSGRAPH")
&& rlx_ir::env::is_unset("RLX_DISABLE_MPSGRAPH_HYBRID")
{
crate::mps_graph_hybrid::build_hybrid_plan(&fused, None)
.filter(|steps| crate::mps_graph_hybrid::hybrid_has_mps(steps))
} else {
None
};
if verbose && mps_hybrid.is_some() {
eprintln!("[rlx-metal] MPSGraph hybrid lowering: enabled");
}
let icb_segments = if rlx_ir::env::flag("RLX_USE_ICB") {
let dev_ref = metal_device().expect("Metal device required");
let segs =
crate::icb::compile_segments(&schedule.thunks, &arena.buffer, &dev_ref.device);
if verbose {
let total_cmds: u64 = segs.iter().map(|r| r.segment.command_count).sum();
eprintln!(
"[rlx-metal] ICB pre-encoded {} segments / {} commands",
segs.len(),
total_cmds
);
}
segs
} else {
Vec::new()
};
let max_matmul_flops = max_matmul_flops_in(&fused);
let mut me = Self {
graph: fused,
arena,
schedule,
input_ids,
param_ids,
input_slots,
output_slots,
precision: MetalPrecision::F32,
mps_plan,
mps_hybrid,
icb_segments,
pending_cmd_bufs: Vec::new(),
active_extent: None,
max_matmul_flops,
mps_params_frozen: false,
gdn_scratch_off,
dequant_scratch_off,
conv_bwd_scratch_off,
attn_bwd_scratch_off,
rms_norm_bwd_scratch_off,
onnx_qmatmul_act_scratch_off,
qmatmul_weight_cache: std::cell::RefCell::new(
crate::onnx_qmatmul::QMatMulWeightCache::new(),
),
gpu_handles: HashMap::new(),
gpu_handle_feeds: HashMap::new(),
gpu_handle_resident: std::collections::HashSet::new(),
kv_row_feeds: HashMap::new(),
};
me.bind_mps_executable_to_arena();
me
}
}
#[inline]
fn metal_widened_dtype(dt: rlx_ir::DType) -> bool {
matches!(
dt,
rlx_ir::DType::I64 | rlx_ir::DType::I32 | rlx_ir::DType::U32 | rlx_ir::DType::Bool
)
}
fn int_bytes_to_f32_bytes(data: &[u8], dt: rlx_ir::DType) -> Vec<u8> {
use rlx_ir::DType;
match dt {
DType::I64 => data
.chunks_exact(8)
.flat_map(|c| (i64::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes())
.collect(),
DType::I32 => data
.chunks_exact(4)
.flat_map(|c| (i32::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes())
.collect(),
DType::U32 => data
.chunks_exact(4)
.flat_map(|c| (u32::from_le_bytes(c.try_into().unwrap()) as f32).to_le_bytes())
.collect(),
DType::Bool => data
.iter()
.flat_map(|&b| (b as f32).to_le_bytes())
.collect(),
_ => data.to_vec(),
}
}
fn widen_integer_activations_to_f32(mut graph: Graph) -> Graph {
use rlx_ir::DType;
let custom_operands: std::collections::HashSet<rlx_ir::NodeId> = graph
.nodes()
.iter()
.filter(|n| matches!(n.op, Op::Custom { .. }))
.flat_map(|n| n.inputs.iter().copied())
.collect();
for node in graph.nodes_mut() {
if matches!(node.op, Op::Param { .. }) {
continue;
}
if custom_operands.contains(&node.id) {
continue;
}
let old = node.shape.dtype();
if metal_widened_dtype(old)
&& let Op::Constant { data } = &mut node.op
{
*data = int_bytes_to_f32_bytes(data, old);
}
if let Op::Cast { to } = &mut node.op
&& metal_widened_dtype(*to)
{
*to = DType::F32;
}
if metal_widened_dtype(old) {
node.shape = node.shape.clone().with_dtype(DType::F32);
}
}
graph
}