use std::collections::HashMap;
use std::os::raw::c_void;
use crate::mlx::{self, Array, VectorArray};
use crate::sys::mlx as mlxsys;
use crate::sys::ort;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Src {
CtxInput,
Initializer,
Intermediate,
Absent,
}
#[derive(Clone)]
pub struct InitData {
pub data: *const c_void,
pub shape: Vec<i64>,
pub dtype: ort::ONNXTensorElementDataType,
#[allow(dead_code)]
pub count: usize,
#[allow(dead_code)] pub owned: Option<std::sync::Arc<Vec<u8>>>,
}
#[derive(Clone)]
pub struct TensorRef {
pub name: String,
pub source: Src,
pub ctx_index: usize,
pub constant: bool,
pub init: Option<InitData>,
}
impl TensorRef {
pub fn absent() -> Self {
TensorRef {
name: String::new(),
source: Src::Absent,
ctx_index: 0,
constant: false,
init: None,
}
}
}
#[derive(Clone)]
pub struct OutRef {
pub name: String,
pub external: bool,
pub ctx_index: usize,
pub otype: ort::ONNXTensorElementDataType,
}
#[derive(Clone, Copy)]
pub struct DeltaWrite {
pub axis: usize,
pub offset: i64,
pub count: i64,
pub alias_ptr: usize,
}
#[derive(Clone)]
#[allow(dead_code)] pub struct ConstTensor {
pub data: Vec<u8>,
pub shape: Vec<i64>,
pub dtype: ort::ONNXTensorElementDataType,
pub count: usize,
}
#[derive(Clone)]
pub struct SubgraphDesc {
pub attr_name: String,
pub input_names: Vec<String>,
pub output_names: Vec<String>,
pub nodes: Vec<NodeDesc>,
}
#[derive(Clone)]
pub struct NodeDesc {
pub op_type: String,
pub domain: String,
pub since_version: i32,
pub ints: HashMap<String, i64>,
pub floats: HashMap<String, f32>,
pub int_arrays: HashMap<String, Vec<i64>>,
pub float_arrays: HashMap<String, Vec<f32>>,
pub strings: HashMap<String, String>,
#[allow(dead_code)] pub tensors: HashMap<String, ConstTensor>,
pub inputs: Vec<TensorRef>,
pub outputs: Vec<OutRef>,
pub subgraphs: Vec<SubgraphDesc>,
}
impl NodeDesc {
pub fn new(op_type: String, domain: String, since_version: i32) -> Self {
NodeDesc {
op_type,
domain,
since_version,
ints: HashMap::new(),
floats: HashMap::new(),
int_arrays: HashMap::new(),
float_arrays: HashMap::new(),
strings: HashMap::new(),
tensors: HashMap::new(),
inputs: Vec::new(),
outputs: Vec::new(),
subgraphs: Vec::new(),
}
}
}
#[derive(Clone)]
pub struct DynInput {
pub name: String,
pub ctx_index: usize,
}
#[derive(Clone)]
pub struct SynthRope {
pub key: String,
pub cache_name: String,
}
pub fn rope_row_key(cache_name: &str) -> String {
format!("__rope_row__{cache_name}")
}
pub const GQA_VALID_PAST_KEY: &str = "__gqa_valid_past";
pub struct TracePayload {
pub plan: *mut Plan,
pub ort_api: *const ort::OrtApi,
pub kctx: *mut ort::OrtKernelContext,
pub stream: mlxsys::mlx_stream,
pub slot: Slot,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Slot {
Decode,
Prefill,
General,
}
impl Slot {
#[inline]
pub fn get(self, plan: &Plan) -> &CompiledSubgraph {
match self {
Slot::Decode => &plan.compiled,
Slot::Prefill => &plan.prefill,
Slot::General => &plan.general,
}
}
#[inline]
pub fn get_mut(self, plan: &mut Plan) -> &mut CompiledSubgraph {
match self {
Slot::Decode => &mut plan.compiled,
Slot::Prefill => &mut plan.prefill,
Slot::General => &mut plan.general,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ShapeMode {
Shapeless,
ShapeKeyed,
}
#[derive(Clone, Copy)]
pub struct CompiledConfig {
pub shape_mode: ShapeMode,
pub kv_alias: bool,
pub rope_as_data: bool,
pub delta_copyout: bool,
pub contiguous_outputs: bool,
}
impl CompiledConfig {
pub const fn decode() -> Self {
CompiledConfig {
shape_mode: ShapeMode::Shapeless,
kv_alias: true,
rope_as_data: true,
delta_copyout: true,
contiguous_outputs: false,
}
}
pub const fn general() -> Self {
CompiledConfig {
shape_mode: ShapeMode::ShapeKeyed,
kv_alias: false,
rope_as_data: false,
delta_copyout: false,
contiguous_outputs: true,
}
}
pub const fn prefill() -> Self {
CompiledConfig {
shape_mode: ShapeMode::ShapeKeyed,
kv_alias: true,
rope_as_data: true,
delta_copyout: true,
contiguous_outputs: false,
}
}
}
pub struct CompiledSubgraph {
pub config: CompiledConfig,
pub enabled: bool,
pub attempted: bool,
pub valid: bool,
pub closure: Option<crate::mlx::Closure>,
pub dyn_inputs: Vec<DynInput>,
pub ext_outputs: Vec<OutRef>,
pub trace_transient: Vec<Array>,
pub payload: Option<Box<TracePayload>>,
pub synth_ropes: Vec<SynthRope>,
pub rope_past_ctx_index: i32,
pub rope_past_axis: i32,
pub shared_kv: bool,
pub mask_ctx_index: i32,
pub kv_present_names: Vec<(String, usize)>,
}
impl CompiledSubgraph {
pub fn new(config: CompiledConfig) -> Self {
CompiledSubgraph {
config,
enabled: false,
attempted: false,
valid: false,
closure: None,
dyn_inputs: Vec::new(),
ext_outputs: Vec::new(),
trace_transient: Vec::new(),
payload: None,
synth_ropes: Vec::new(),
rope_past_ctx_index: -1,
rope_past_axis: 2,
shared_kv: false,
mask_ctx_index: -1,
kv_present_names: Vec::new(),
}
}
}
pub struct Plan {
pub nodes: Vec<NodeDesc>,
pub cache: HashMap<String, Array>,
pub compiled: CompiledSubgraph,
pub prefill: CompiledSubgraph,
pub general: CompiledSubgraph,
}
impl Plan {
pub fn new(nodes: Vec<NodeDesc>) -> Self {
Plan {
nodes,
cache: HashMap::new(),
compiled: CompiledSubgraph::new(CompiledConfig::decode()),
prefill: CompiledSubgraph::new(CompiledConfig::prefill()),
general: CompiledSubgraph::new(CompiledConfig::general()),
}
}
}
pub fn mlx_dtype_from_onnx(t: ort::ONNXTensorElementDataType) -> mlxsys::mlx_dtype {
use ort::*;
#[allow(non_upper_case_globals)]
match t {
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT => mlxsys::mlx_dtype__MLX_FLOAT32,
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16 => {
mlxsys::mlx_dtype__MLX_FLOAT16
}
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16 => {
mlxsys::mlx_dtype__MLX_BFLOAT16
}
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE => {
mlxsys::mlx_dtype__MLX_FLOAT64
}
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8 => mlxsys::mlx_dtype__MLX_INT8,
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16 => mlxsys::mlx_dtype__MLX_INT16,
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32 => mlxsys::mlx_dtype__MLX_INT32,
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64 => mlxsys::mlx_dtype__MLX_INT64,
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8 => mlxsys::mlx_dtype__MLX_UINT8,
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT16 => {
mlxsys::mlx_dtype__MLX_UINT16
}
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32 => {
mlxsys::mlx_dtype__MLX_UINT32
}
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64 => {
mlxsys::mlx_dtype__MLX_UINT64
}
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BOOL => mlxsys::mlx_dtype__MLX_BOOL,
_ => mlxsys::mlx_dtype__MLX_FLOAT32,
}
}
pub type MlxError = String;
pub(crate) fn dim_i32(d: i64) -> Result<i32, MlxError> {
i32::try_from(d).map_err(|_| format!("MLX: dimension {d} does not fit MLX's i32 shape range"))
}
pub fn dtype_name(dt: mlxsys::mlx_dtype) -> &'static str {
#[allow(non_upper_case_globals)]
match dt {
mlxsys::mlx_dtype__MLX_BOOL => "bool",
mlxsys::mlx_dtype__MLX_UINT8 => "uint8",
mlxsys::mlx_dtype__MLX_UINT16 => "uint16",
mlxsys::mlx_dtype__MLX_UINT32 => "uint32",
mlxsys::mlx_dtype__MLX_UINT64 => "uint64",
mlxsys::mlx_dtype__MLX_INT8 => "int8",
mlxsys::mlx_dtype__MLX_INT16 => "int16",
mlxsys::mlx_dtype__MLX_INT32 => "int32",
mlxsys::mlx_dtype__MLX_INT64 => "int64",
mlxsys::mlx_dtype__MLX_FLOAT16 => "float16",
mlxsys::mlx_dtype__MLX_FLOAT32 => "float32",
mlxsys::mlx_dtype__MLX_FLOAT64 => "float64",
mlxsys::mlx_dtype__MLX_BFLOAT16 => "bfloat16",
mlxsys::mlx_dtype__MLX_COMPLEX64 => "complex64",
_ => "unknown",
}
}
pub struct HostBytes {
pub data: *const c_void,
#[allow(dead_code)] pub shape: Vec<i64>,
pub count: usize,
pub dtype: ort::ONNXTensorElementDataType,
}
pub struct TranslationContext<'a> {
plan: &'a mut Plan,
ort_api: *const ort::OrtApi,
kctx: *mut ort::OrtKernelContext,
stream: mlxsys::mlx_stream,
env: HashMap<String, mlxsys::mlx_array>,
arena: Vec<Array>,
path_mark: Option<crate::trace::PathMark>,
trace_enabled: bool,
rope_dynamic: bool,
shared_kv: bool,
compiled_shape_keyed: bool,
compiled_valid_past: i32,
kv_deltas: HashMap<String, DeltaWrite>,
in_general_trace: bool,
compiled_kv_present: Vec<(String, usize)>,
}
impl<'a> TranslationContext<'a> {
pub fn new(
plan: &'a mut Plan,
ort_api: *const ort::OrtApi,
kctx: *mut ort::OrtKernelContext,
stream: mlxsys::mlx_stream,
) -> Self {
TranslationContext {
plan,
ort_api,
kctx,
stream,
env: HashMap::new(),
arena: Vec::new(),
path_mark: None,
trace_enabled: crate::trace::tracer().is_enabled(),
rope_dynamic: false,
shared_kv: false,
compiled_shape_keyed: false,
compiled_valid_past: 0,
kv_deltas: HashMap::new(),
in_general_trace: false,
compiled_kv_present: Vec::new(),
}
}
#[inline]
pub fn mark_fast(&mut self, kernel: &'static str) {
if self.trace_enabled {
self.path_mark = Some(crate::trace::PathMark::Fast(kernel));
}
}
#[inline]
pub fn mark_composed(&mut self, reason: impl Into<String>) {
if self.trace_enabled {
self.path_mark = Some(crate::trace::PathMark::Composed(reason.into()));
}
}
#[inline]
pub fn reset_path_mark(&mut self) {
self.path_mark = None;
}
#[inline]
pub fn take_path_mark(&mut self) -> Option<crate::trace::PathMark> {
self.path_mark.take()
}
#[inline]
#[allow(dead_code)]
pub fn stream(&self) -> mlxsys::mlx_stream {
self.stream
}
pub fn keep(&mut self, a: Array) -> mlxsys::mlx_array {
let raw = a.as_raw();
self.arena.push(a);
raw
}
pub fn cache_get(&self, key: &str) -> Option<mlxsys::mlx_array> {
self.plan.cache.get(key).map(|a| a.as_raw())
}
pub fn cache_put(&mut self, key: String, a: Array) -> mlxsys::mlx_array {
let raw = a.as_raw();
self.plan.cache.insert(key, a);
raw
}
pub fn bind(&mut self, o: &OutRef, a: mlxsys::mlx_array) {
self.env.insert(o.name.clone(), a);
}
pub fn trace_node(&mut self, op_type: &str, node: &NodeDesc, start: Option<std::time::Instant>) {
if !self.trace_enabled {
return;
}
let Some(start) = start else {
return;
};
let tr = crate::trace::tracer();
let mut out_shapes: Vec<String> = Vec::new();
let mut dtype = "";
let mut elements: u64 = 0;
let mut bytes: u64 = 0;
for o in &node.outputs {
if o.name.is_empty() {
continue;
}
if let Some(&raw) = self.env.get(&o.name) {
let arr = std::mem::ManuallyDrop::new(Array::from_raw(raw));
let sh = arr.shape();
let cnt: u64 = sh.iter().map(|&d| d.max(0) as u64).product();
out_shapes.push(format!("{sh:?}"));
dtype = dtype_name(arr.dtype());
elements += cnt;
bytes += cnt * arr.itemsize() as u64;
}
}
let mut in_shapes: Vec<String> = Vec::new();
for inp in &node.inputs {
if inp.name.is_empty() {
continue;
}
if let Some(&raw) = self.env.get(&inp.name) {
let arr = std::mem::ManuallyDrop::new(Array::from_raw(raw));
in_shapes.push(format!("{:?}", arr.shape()));
}
}
let out_s = out_shapes.join(";");
let in_s = in_shapes.join(";");
tr.record_op_meta(op_type, start, start.elapsed(), &out_s, &in_s, dtype, elements, bytes);
}
pub fn resolve(&mut self, r: &TensorRef) -> Result<mlxsys::mlx_array, MlxError> {
match r.source {
Src::Intermediate => self
.env
.get(&r.name)
.copied()
.ok_or_else(|| format!("MLX: missing intermediate {}", r.name)),
Src::CtxInput => {
if r.constant {
if let Some(a) = self.plan.cache.get(&r.name) {
return Ok(a.as_raw());
}
} else if let Some(a) = self.env.get(&r.name) {
return Ok(*a);
}
let (data, shape, dtype) = self.read_ctx_input(r.ctx_index)?;
let ishape: Vec<i32> = shape.iter().map(|&d| dim_i32(d)).collect::<Result<_, _>>()?;
if r.constant {
let arr = Array::from_data(data, &ishape, mlx_dtype_from_onnx(dtype));
let raw = arr.as_raw();
self.plan.cache.insert(r.name.clone(), arr);
Ok(raw)
} else {
let arr = Array::from_data_managed(data, &ishape, mlx_dtype_from_onnx(dtype));
let raw = self.keep(arr);
self.env.insert(r.name.clone(), raw);
Ok(raw)
}
}
Src::Initializer => {
if let Some(a) = self.plan.cache.get(&r.name) {
return Ok(a.as_raw());
}
let init = r
.init
.as_ref()
.ok_or_else(|| format!("MLX: initializer {} has no data", r.name))?;
let ishape: Vec<i32> =
init.shape.iter().map(|&d| dim_i32(d)).collect::<Result<_, _>>()?;
let arr = Array::from_data(init.data, &ishape, mlx_dtype_from_onnx(init.dtype));
let raw = arr.as_raw();
self.plan.cache.insert(r.name.clone(), arr);
Ok(raw)
}
Src::Absent => Err("MLX: absent input".to_string()),
}
}
pub fn raw_host(&self, r: &TensorRef) -> Result<HostBytes, MlxError> {
match r.source {
Src::Initializer => {
let init = r
.init
.as_ref()
.ok_or_else(|| format!("MLX: initializer {} has no data", r.name))?;
Ok(HostBytes {
data: init.data,
shape: init.shape.clone(),
count: init.count,
dtype: init.dtype,
})
}
Src::CtxInput => {
let (data, shape, dtype) = self.read_ctx_input(r.ctx_index)?;
let count = shape.iter().map(|&d| d as usize).product::<usize>();
Ok(HostBytes {
data,
shape,
count,
dtype,
})
}
_ => Err(format!("MLX: RawHost on non-constant input {}", r.name)),
}
}
pub fn read_ints(&self, r: &TensorRef) -> Result<Vec<i64>, MlxError> {
let h = self.raw_host(r)?;
if h.data.is_null() {
return Ok(Vec::new());
}
let p = h.data as *const i64;
Ok(unsafe { std::slice::from_raw_parts(p, h.count) }.to_vec())
}
fn read_ctx_input(
&self,
index: usize,
) -> Result<(*const c_void, Vec<i64>, ort::ONNXTensorElementDataType), MlxError> {
unsafe {
let api = &*self.ort_api;
let mut val: *const ort::OrtValue = std::ptr::null();
let st = (api.KernelContext_GetInput.unwrap())(self.kctx, index, &mut val);
if !st.is_null() || val.is_null() {
return Err(format!("MLX: KernelContext_GetInput({index}) failed"));
}
let mut info: *mut ort::OrtTensorTypeAndShapeInfo = std::ptr::null_mut();
(api.GetTensorTypeAndShape.unwrap())(val, &mut info);
let mut nd: usize = 0;
(api.GetDimensionsCount.unwrap())(info, &mut nd);
let mut dims = vec![0i64; nd];
if nd > 0 {
(api.GetDimensions.unwrap())(info, dims.as_mut_ptr(), nd);
}
let mut etype: ort::ONNXTensorElementDataType = 0;
(api.GetTensorElementType.unwrap())(info, &mut etype);
(api.ReleaseTensorTypeAndShapeInfo.unwrap())(info);
let mut data: *const c_void = std::ptr::null();
(api.GetTensorData.unwrap())(val, &mut data);
Ok((data, dims, etype))
}
}
pub fn unary(
&mut self,
op: unsafe extern "C" fn(*mut mlxsys::mlx_array, mlxsys::mlx_array, mlxsys::mlx_stream) -> i32,
a: mlxsys::mlx_array,
) -> Result<mlxsys::mlx_array, MlxError> {
let mut res = Array::new();
let mut raw = res.as_raw();
let rc = unsafe { op(&mut raw, a, self.stream) };
res = Array::from_raw(raw);
if rc != 0 {
return Err("mlx unary op failed".to_string());
}
Ok(self.keep(res))
}
pub fn binary(
&mut self,
op: unsafe extern "C" fn(
*mut mlxsys::mlx_array,
mlxsys::mlx_array,
mlxsys::mlx_array,
mlxsys::mlx_stream,
) -> i32,
a: mlxsys::mlx_array,
b: mlxsys::mlx_array,
) -> Result<mlxsys::mlx_array, MlxError> {
let mut res = Array::new();
let mut raw = res.as_raw();
let rc = unsafe { op(&mut raw, a, b, self.stream) };
res = Array::from_raw(raw);
if rc != 0 {
return Err("mlx binary op failed".to_string());
}
Ok(self.keep(res))
}
pub fn astype(
&mut self,
a: mlxsys::mlx_array,
t: mlxsys::mlx_dtype,
) -> Result<mlxsys::mlx_array, MlxError> {
let mut res = Array::new();
let mut raw = res.as_raw();
let rc = unsafe { mlxsys::mlx_astype(&mut raw, a, t, self.stream) };
res = Array::from_raw(raw);
if rc != 0 {
return Err("mlx_astype failed".to_string());
}
Ok(self.keep(res))
}
pub fn zeros_like(&mut self, a: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
let mut res = Array::new();
let mut raw = res.as_raw();
let rc = unsafe { mlxsys::mlx_zeros_like(&mut raw, a, self.stream) };
res = Array::from_raw(raw);
if rc != 0 {
return Err("mlx_zeros_like failed".to_string());
}
Ok(self.keep(res))
}
pub fn emit<F>(&mut self, f: F) -> Result<mlxsys::mlx_array, MlxError>
where
F: FnOnce(*mut mlxsys::mlx_array, mlxsys::mlx_stream) -> i32,
{
let mut res = Array::new();
let mut raw = res.as_raw();
let rc = f(&mut raw, self.stream);
res = Array::from_raw(raw);
if rc != 0 {
return Err("mlx op failed".to_string());
}
Ok(self.keep(res))
}
pub fn shape_of(&self, a: mlxsys::mlx_array) -> Vec<i32> {
let nd = unsafe { mlxsys::mlx_array_ndim(a) };
let sh = unsafe { mlxsys::mlx_array_shape(a) };
(0..nd).map(|i| unsafe { *sh.add(i) }).collect()
}
pub fn ndim(&self, a: mlxsys::mlx_array) -> usize {
unsafe { mlxsys::mlx_array_ndim(a) }
}
pub fn dim(&self, a: mlxsys::mlx_array, i: i32) -> i32 {
unsafe { mlxsys::mlx_array_dim(a, i) }
}
pub fn size_of(&self, a: mlxsys::mlx_array) -> usize {
unsafe { mlxsys::mlx_array_size(a) }
}
pub fn dtype_of(&self, a: mlxsys::mlx_array) -> mlxsys::mlx_dtype {
unsafe { mlxsys::mlx_array_dtype(a) }
}
pub fn scalar_f32(&mut self, v: f32) -> mlxsys::mlx_array {
self.keep(Array::from_raw(unsafe { mlxsys::mlx_array_new_float32(v) }))
}
pub fn scalar_i32(&mut self, v: i32) -> mlxsys::mlx_array {
self.keep(Array::from_raw(unsafe { mlxsys::mlx_array_new_int(v) }))
}
pub fn scalar_i64(&mut self, v: i64) -> mlxsys::mlx_array {
let sh: [i32; 0] = [];
self.keep(Array::from_data(
&v as *const i64 as *const c_void,
&sh,
mlxsys::mlx_dtype__MLX_INT64,
))
}
pub fn from_host_i64(&mut self, data: &[i64], shape: &[i32]) -> mlxsys::mlx_array {
self.keep(Array::from_data(
data.as_ptr() as *const c_void,
shape,
mlxsys::mlx_dtype__MLX_INT64,
))
}
pub fn reshape(&mut self, a: mlxsys::mlx_array, shape: &[i32]) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_reshape(res, a, shape.as_ptr(), shape.len(), s) })
}
pub fn transpose(&mut self, a: mlxsys::mlx_array, axes: &[i32]) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe {
mlxsys::mlx_transpose_axes(res, a, axes.as_ptr(), axes.len(), s)
})
}
pub fn contiguous(&mut self, a: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_contiguous(res, a, false, s) })
}
pub fn zeros(&mut self, shape: &[i32], dtype: mlxsys::mlx_dtype) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_zeros(res, shape.as_ptr(), shape.len(), dtype, s) })
}
pub fn softmax_axis(
&mut self,
a: mlxsys::mlx_array,
axis: i32,
) -> Result<mlxsys::mlx_array, MlxError> {
let mut res = Array::new();
let mut raw = res.as_raw();
let rc = unsafe { mlxsys::mlx_softmax_axis(&mut raw, a, axis, true, self.stream) };
res = Array::from_raw(raw);
if rc != 0 {
return Err("mlx_softmax_axis failed".to_string());
}
Ok(self.keep(res))
}
pub fn mul(&mut self, a: mlxsys::mlx_array, b: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
self.binary(mlxsys::mlx_multiply, a, b)
}
pub fn add(&mut self, a: mlxsys::mlx_array, b: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
self.binary(mlxsys::mlx_add, a, b)
}
pub fn sub(&mut self, a: mlxsys::mlx_array, b: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
self.binary(mlxsys::mlx_subtract, a, b)
}
pub fn matmul(&mut self, a: mlxsys::mlx_array, b: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_matmul(res, a, b, s) })
}
pub fn concat2(&mut self, a: mlxsys::mlx_array, b: mlxsys::mlx_array, axis: i32) -> Result<mlxsys::mlx_array, MlxError> {
let mut vec = VectorArray::new();
vec.append(a);
vec.append(b);
self.emit(|res, s| unsafe { mlxsys::mlx_concatenate_axis(res, vec.as_raw(), axis, s) })
}
pub fn slice(&mut self, a: mlxsys::mlx_array, start: &[i32], stop: &[i32]) -> Result<mlxsys::mlx_array, MlxError> {
let stride = vec![1i32; start.len()];
self.emit(|res, s| unsafe {
mlxsys::mlx_slice(
res, a, start.as_ptr(), start.len(), stop.as_ptr(), stop.len(),
stride.as_ptr(), stride.len(), s,
)
})
}
pub fn expand_dims(&mut self, a: mlxsys::mlx_array, axis: i32) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_expand_dims(res, a, axis, s) })
}
pub fn arange(&mut self, start: f64, stop: f64, step: f64, dtype: mlxsys::mlx_dtype) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_arange(res, start, stop, step, dtype, s) })
}
pub fn less_equal(&mut self, a: mlxsys::mlx_array, b: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
self.binary(mlxsys::mlx_less_equal, a, b)
}
pub fn where_(&mut self, cond: mlxsys::mlx_array, x: mlxsys::mlx_array, y: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_where(res, cond, x, y, s) })
}
pub fn slice_update_dynamic(
&mut self,
src: mlxsys::mlx_array,
update: mlxsys::mlx_array,
start: mlxsys::mlx_array,
axes: &[i32],
) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe {
mlxsys::mlx_slice_update_dynamic(res, src, update, start, axes.as_ptr(), axes.len(), s)
})
}
pub fn squeeze(&mut self, a: mlxsys::mlx_array, axis: i32) -> Result<mlxsys::mlx_array, MlxError> {
self.emit(|res, s| unsafe { mlxsys::mlx_squeeze_axis(res, a, axis, s) })
}
pub fn stack(&mut self, parts: &[mlxsys::mlx_array], axis: i32) -> Result<mlxsys::mlx_array, MlxError> {
let mut vec = VectorArray::new();
for &p in parts {
vec.append(p);
}
self.emit(|res, s| unsafe { mlxsys::mlx_stack_axis(res, vec.as_raw(), axis, s) })
}
pub fn scalar_complex(&mut self, re: f32, im: f32) -> mlxsys::mlx_array {
self.keep(Array::from_raw(unsafe { mlxsys::mlx_array_new_complex(re, im) }))
}
pub fn scalar_bool(&mut self, v: bool) -> mlxsys::mlx_array {
self.keep(Array::from_raw(unsafe { mlxsys::mlx_array_new_bool(v) }))
}
pub fn from_host(&mut self, data: *const c_void, shape: &[i32], dtype: mlxsys::mlx_dtype) -> mlxsys::mlx_array {
self.keep(Array::from_data(data, shape, dtype))
}
pub fn contiguous_eval(&mut self, a: mlxsys::mlx_array) -> Result<mlxsys::mlx_array, MlxError> {
if self.in_general_trace {
return Err("MLX: host eval not permitted in general compiled trace".to_string());
}
let r = self.contiguous(a)?;
let mut v = VectorArray::new();
v.append(r);
mlx::eval(&v)?;
Ok(r)
}
pub fn host_ptr(&self, a: mlxsys::mlx_array) -> *const u8 {
unsafe { mlxsys::mlx_array_data_uint8(a) as *const u8 }
}
pub fn read_scalar_i64(&mut self, a: mlxsys::mlx_array) -> Result<i64, MlxError> {
let dt = self.dtype_of(a);
let a = if dt == mlxsys::mlx_dtype__MLX_INT64 {
a
} else {
self.astype(a, mlxsys::mlx_dtype__MLX_INT64)?
};
let a = self.contiguous_eval(a)?;
let mut v: i64 = 0;
let rc = unsafe { mlxsys::mlx_array_item_int64(&mut v, a) };
if rc != 0 {
return Err("mlx_array_item_int64 failed".to_string());
}
Ok(v)
}
pub fn slice_update(
&mut self,
src: mlxsys::mlx_array,
update: mlxsys::mlx_array,
start: &[i32],
stop: &[i32],
) -> Result<mlxsys::mlx_array, MlxError> {
let strides = vec![1i32; start.len()];
self.emit(|res, s| unsafe {
mlxsys::mlx_slice_update(
res,
src,
update,
start.as_ptr(),
start.len(),
stop.as_ptr(),
stop.len(),
strides.as_ptr(),
strides.len(),
s,
)
})
}
pub fn run_subgraph(
&mut self,
sg: &SubgraphDesc,
inputs: &[mlxsys::mlx_array],
) -> Result<Vec<mlxsys::mlx_array>, MlxError> {
if inputs.len() != sg.input_names.len() {
return Err(format!("MLX RunSubgraph: input arity mismatch for body '{}'", sg.attr_name));
}
let mut body_names: std::collections::HashSet<String> = std::collections::HashSet::new();
for nm in &sg.input_names {
if !nm.is_empty() {
body_names.insert(nm.clone());
}
}
for node in &sg.nodes {
for o in &node.outputs {
if !o.name.is_empty() {
body_names.insert(o.name.clone());
}
}
}
let mut saved: HashMap<String, mlxsys::mlx_array> = HashMap::new();
for nm in &body_names {
if let Some(&a) = self.env.get(nm) {
saved.insert(nm.clone(), a);
}
}
for (i, nm) in sg.input_names.iter().enumerate() {
if !nm.is_empty() {
self.env.insert(nm.clone(), inputs[i]);
}
}
for node in &sg.nodes {
crate::registry::translate(self, node)?;
}
let mut outs = Vec::with_capacity(sg.output_names.len());
for on in &sg.output_names {
match self.env.get(on) {
Some(&a) => outs.push(a),
None => {
return Err(format!(
"MLX RunSubgraph: body '{}' did not produce output {on}",
sg.attr_name
))
}
}
}
for nm in &body_names {
self.env.remove(nm);
}
for (k, v) in saved {
self.env.insert(k, v);
}
Ok(outs)
}
#[inline]
pub fn rope_dynamic(&self) -> bool {
self.rope_dynamic
}
#[inline]
pub fn shared_kv(&self) -> bool {
self.shared_kv
}
pub fn record_kv_present(&mut self, out_name: &str, offset: i64, count: i64, past_ctx_index: usize) {
if self.rope_dynamic {
if !self.compiled_kv_present.iter().any(|(n, _)| n == out_name) {
self.compiled_kv_present
.push((out_name.to_string(), past_ctx_index));
}
} else {
let alias_ptr = self
.read_ctx_input(past_ctx_index)
.map(|(p, _, _)| p as usize)
.unwrap_or(0);
self.kv_deltas.insert(
out_name.to_string(),
DeltaWrite { axis: 2, offset, count, alias_ptr },
);
}
}
#[inline]
pub fn shared_valid_past(&self) -> Option<mlxsys::mlx_array> {
self.env.get(GQA_VALID_PAST_KEY).copied()
}
pub(crate) fn set_compiled_trace(&mut self, shared_kv: bool, shape_keyed: bool, valid_past: i32) {
self.rope_dynamic = true;
self.shared_kv = shared_kv;
self.compiled_shape_keyed = shape_keyed;
self.compiled_valid_past = valid_past;
}
#[inline]
pub fn compiled_shape_keyed(&self) -> bool {
self.compiled_shape_keyed
}
#[inline]
pub fn compiled_valid_past(&self) -> i32 {
self.compiled_valid_past
}
pub(crate) fn set_general_trace(&mut self) {
self.in_general_trace = true;
}
pub(crate) fn take_compiled_kv_present(&mut self) -> Vec<(String, usize)> {
std::mem::take(&mut self.compiled_kv_present)
}
pub(crate) fn seed(&mut self, name: String, raw: mlxsys::mlx_array) {
self.env.insert(name, raw);
}
pub(crate) fn env_get(&self, name: &str) -> Option<mlxsys::mlx_array> {
self.env.get(name).copied()
}
pub(crate) fn take_arena(&mut self) -> Vec<Array> {
std::mem::take(&mut self.arena)
}
pub(crate) fn run_trace(
&mut self,
ext_outputs: &[OutRef],
contiguous: bool,
) -> Result<VectorArray, MlxError> {
let nodes = std::mem::take(&mut self.plan.nodes);
let mut result = Ok(());
for node in &nodes {
if let Err(e) = crate::registry::translate(self, node) {
result = Err(e);
break;
}
}
self.plan.nodes = nodes;
result?;
let mut res = VectorArray::new();
for o in ext_outputs {
let a = self
.env_get(&o.name)
.ok_or_else(|| format!("MLX: compiled trace missing output {}", o.name))?;
let casted = self.astype(a, mlx_dtype_from_onnx(o.otype))?;
let casted = if contiguous { self.contiguous(casted)? } else { casted };
res.append(casted);
}
Ok(res)
}
pub fn rope_row_full(
&mut self,
cache_name: &str,
seq: i32,
half: i32,
) -> Result<mlxsys::mlx_array, MlxError> {
let key = rope_row_key(cache_name);
let row = self
.env
.get(&key)
.copied()
.ok_or_else(|| format!("MLX: missing RoPE row placeholder for {cache_name}"))?;
self.reshape(row, &[1, 1, seq, 2 * half])
}
pub fn rotate_half_matrix(&mut self, hd: i32, half: i32) -> mlxsys::mlx_array {
let key = format!("__rope_rotate_half_{hd}");
if let Some(a) = self.plan.cache.get(&key) {
return a.as_raw();
}
let n = (hd as usize) * (hd as usize);
let mut m = vec![0.0f32; n];
for i in 0..(half as usize) {
let hd_u = hd as usize;
let half_u = half as usize;
m[(i + half_u) * hd_u + i] = -1.0; m[i * hd_u + (i + half_u)] = 1.0; }
let shp = [hd, hd];
let arr = Array::from_data(
m.as_ptr() as *const c_void,
&shp,
mlxsys::mlx_dtype__MLX_FLOAT32,
);
self.cache_put(key, arr)
}
pub fn execute(&mut self) -> Result<(), MlxError> {
let nodes = std::mem::take(&mut self.plan.nodes);
let mut result = Ok(());
let tr = crate::trace::tracer();
{
let _phase = tr.phase("translate");
for node in &nodes {
if let Err(e) = crate::registry::translate(self, node) {
result = Err(e);
break;
}
}
}
if result.is_ok() {
result = self.finish_boundary(&nodes);
}
self.plan.nodes = nodes;
result
}
fn finish_boundary(&mut self, nodes: &[NodeDesc]) -> Result<(), MlxError> {
let mut outs = VectorArray::new();
let mut ext: Vec<(OutRef, mlxsys::mlx_array)> = Vec::new();
for node in nodes {
for o in &node.outputs {
if o.external {
if let Some(&a) = self.env.get(&o.name) {
let casted = self.astype(a, mlx_dtype_from_onnx(o.otype))?;
let casted = self.contiguous(casted)?;
outs.append(casted);
ext.push((o.clone(), casted));
}
}
}
}
let tr = crate::trace::tracer();
tr.sample_gpu_counters();
let eval_t0 = if tr.active() { Some(std::time::Instant::now()) } else { None };
{
let _cap = tr.begin_gpu_capture();
let _eval = tr.eval_region();
mlx::eval(&outs)?;
}
if let Some(t0) = eval_t0 {
tr.record_phase("eval", t0.elapsed());
}
tr.sample_gpu_counters();
for (o, a) in &ext {
self.copy_out(o, *a)?;
}
Ok(())
}
fn copy_out(&self, o: &OutRef, a: mlxsys::mlx_array) -> Result<(), MlxError> {
let delta = self.kv_deltas.get(&o.name).copied();
copy_out_raw_delta(self.ort_api, self.kctx, o, a, delta)
}
}
pub(crate) fn read_ctx_input_raw(
ort_api: *const ort::OrtApi,
kctx: *mut ort::OrtKernelContext,
index: usize,
) -> Result<(*const c_void, Vec<i64>, ort::ONNXTensorElementDataType), MlxError> {
unsafe {
let api = &*ort_api;
let mut val: *const ort::OrtValue = std::ptr::null();
let st = (api.KernelContext_GetInput.unwrap())(kctx, index, &mut val);
if !st.is_null() || val.is_null() {
return Err(format!("MLX: KernelContext_GetInput({index}) failed"));
}
let mut info: *mut ort::OrtTensorTypeAndShapeInfo = std::ptr::null_mut();
(api.GetTensorTypeAndShape.unwrap())(val, &mut info);
let mut nd: usize = 0;
(api.GetDimensionsCount.unwrap())(info, &mut nd);
let mut dims = vec![0i64; nd];
if nd > 0 {
(api.GetDimensions.unwrap())(info, dims.as_mut_ptr(), nd);
}
let mut etype: ort::ONNXTensorElementDataType = 0;
(api.GetTensorElementType.unwrap())(info, &mut etype);
(api.ReleaseTensorTypeAndShapeInfo.unwrap())(info);
let mut data: *const c_void = std::ptr::null();
(api.GetTensorData.unwrap())(val, &mut data);
Ok((data, dims, etype))
}
}
pub(crate) fn copy_out_raw_delta(
ort_api: *const ort::OrtApi,
kctx: *mut ort::OrtKernelContext,
o: &OutRef,
a: mlxsys::mlx_array,
delta: Option<DeltaWrite>,
) -> Result<(), MlxError> {
let arr = std::mem::ManuallyDrop::new(Array::from_raw(a)); let shape = arr.shape();
let count: usize = shape.iter().map(|&d| d as usize).product::<usize>().max(0);
let itemsize = arr.itemsize();
unsafe {
let api = &*ort_api;
let mut out: *mut ort::OrtValue = std::ptr::null_mut();
let st = (api.KernelContext_GetOutput.unwrap())(
kctx,
o.ctx_index,
shape.as_ptr(),
shape.len(),
&mut out,
);
if !st.is_null() || out.is_null() {
return Err(format!("MLX: KernelContext_GetOutput({}) failed", o.ctx_index));
}
let mut dst: *mut c_void = std::ptr::null_mut();
(api.GetTensorMutableData.unwrap())(out, &mut dst);
let src = arr.data_bytes();
if src.is_null() || dst.is_null() {
return Ok(());
}
let tr = crate::trace::tracer();
let t0 = if tr.active() { Some(std::time::Instant::now()) } else { None };
let mut was_delta = false;
let mut moved_bytes: u64 = (count * itemsize) as u64;
match delta {
Some(d)
if d.count > 0
&& d.axis < shape.len()
&& d.alias_ptr != 0
&& d.alias_ptr == dst as usize =>
{
let axis_len = shape[d.axis].max(0) as usize;
let offset = d.offset.max(0) as usize;
let n_rows = (d.count as usize).min(axis_len.saturating_sub(offset));
if n_rows == 0 {
return Ok(());
}
let outer: usize =
shape[..d.axis].iter().map(|&x| x as usize).product::<usize>().max(1);
let inner: usize =
shape[d.axis + 1..].iter().map(|&x| x as usize).product::<usize>().max(1);
let run = n_rows * inner * itemsize; let stride = axis_len * inner * itemsize; let start = offset * inner * itemsize; for o_idx in 0..outer {
let byte_off = o_idx * stride + start;
std::ptr::copy_nonoverlapping(
src.add(byte_off),
(dst as *mut u8).add(byte_off),
run,
);
}
was_delta = true;
moved_bytes = (outer * run) as u64;
}
_ => {
std::ptr::copy_nonoverlapping(src, dst as *mut u8, count * itemsize);
}
}
if let Some(t0) = t0 {
tr.record_copyout(was_delta, moved_bytes, t0.elapsed());
}
}
Ok(())
}