use std::borrow::Cow;
use std::os::raw::c_char;
use std::sync::LazyLock;
use half::{bf16, f16};
use crate::engine::{MlxError, NodeDesc, TranslationContext};
use crate::sys::ort;
pub type OpHandler = fn(&mut TranslationContext, &NodeDesc) -> Result<(), MlxError>;
pub type ClaimResult = Result<(), Cow<'static, str>>;
pub type ClaimPredicate = fn(&NodeView) -> ClaimResult;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompileShapeSafety {
Shapeless,
ShapeKeyedOnly { reason: &'static str },
EagerOnly { reason: &'static str },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompilePartitionClass {
Shapeless,
ShapeKeyed,
Eager,
}
pub const MLX_PAD_SHAPE_REASON: &str =
"emits MLX Pad, which lacks Primitive::output_shapes in MLX 0.32.1";
pub const MLX_RANDOM_BITS_SHAPE_REASON: &str =
"emits MLX RandomBits, which lacks Primitive::output_shapes in MLX 0.32.1";
pub const MLX_SCAN_SHAPE_REASON: &str =
"emits MLX Scan, which lacks Primitive::output_shapes in MLX 0.32.1";
pub const MLX_SCATTER_SHAPE_REASON: &str =
"emits MLX Scatter, which lacks Primitive::output_shapes in MLX 0.32.1";
pub const MLX_SLICE_SHAPE_REASON: &str =
"emits MLX Slice, which lacks Primitive::output_shapes in MLX 0.32.1";
pub const MLX_SPLIT_SHAPE_REASON: &str =
"emits MLX Split, which lacks Primitive::output_shapes in MLX 0.32.1";
pub const MLX_VIEW_SHAPE_REASON: &str =
"emits MLX View, which lacks Primitive::output_shapes in MLX 0.32.1";
impl CompileShapeSafety {
pub const fn allows_shapeless(self) -> bool {
matches!(self, Self::Shapeless)
}
pub const fn allows_shape_keyed(self) -> bool {
!matches!(self, Self::EagerOnly { .. })
}
pub const fn partition_class(self) -> CompilePartitionClass {
match self {
Self::Shapeless => CompilePartitionClass::Shapeless,
Self::ShapeKeyedOnly { .. } => CompilePartitionClass::ShapeKeyed,
Self::EagerOnly { .. } => CompilePartitionClass::Eager,
}
}
}
pub type CompileShapeClassifier = fn(&NodeView) -> CompileShapeSafety;
#[macro_export]
macro_rules! deny {
($($fmt:tt)+) => {
return ::core::result::Result::Err(::std::borrow::Cow::Owned(format!($($fmt)+)))
};
}
#[macro_export]
macro_rules! require {
($cond:expr, $($fmt:tt)+) => {
{
let ok: bool = $cond;
if !ok {
return ::core::result::Result::Err(::std::borrow::Cow::Owned(format!($($fmt)+)));
}
}
};
}
pub const K_ANY_OPSET: i32 = -1;
pub struct OpRegistration {
pub domain: &'static str,
pub op_type: &'static str,
pub min_opset: i32,
pub max_opset: i32,
pub handler: OpHandler,
pub claim: ClaimPredicate,
}
pub struct OpRegistry {
table: Vec<RegisteredOp>,
}
enum CompileShapeRule {
Always(CompileShapeSafety),
Classified(CompileShapeClassifier),
}
struct RegisteredOp {
entry: OpRegistration,
compile_shape_rule: CompileShapeRule,
}
impl OpRegistry {
fn new() -> Self {
OpRegistry { table: Vec::new() }
}
pub fn register_shapeless(&mut self, entry: OpRegistration) {
self.table.push(RegisteredOp {
entry,
compile_shape_rule: CompileShapeRule::Always(CompileShapeSafety::Shapeless),
});
}
pub fn register_shape_keyed(&mut self, entry: OpRegistration, reason: &'static str) {
assert!(
!reason.is_empty(),
"shape-keyed registration for {}::{} requires a reason",
entry.domain,
entry.op_type
);
self.table.push(RegisteredOp {
entry,
compile_shape_rule: CompileShapeRule::Always(CompileShapeSafety::ShapeKeyedOnly {
reason,
}),
});
}
pub fn register_shape_classified(
&mut self,
entry: OpRegistration,
classifier: CompileShapeClassifier,
) {
self.table.push(RegisteredOp {
entry,
compile_shape_rule: CompileShapeRule::Classified(classifier),
});
}
fn find_entry_index(&self, domain: &str, op_type: &str, since_version: i32) -> Option<usize> {
self.table.iter().position(|registered| {
let e = ®istered.entry;
e.domain == domain
&& e.op_type == op_type
&& (e.min_opset == K_ANY_OPSET || since_version >= e.min_opset)
&& (e.max_opset == K_ANY_OPSET || since_version <= e.max_opset)
})
}
pub fn find_entry(
&self,
domain: &str,
op_type: &str,
since_version: i32,
) -> Option<&OpRegistration> {
self.find_entry_index(domain, op_type, since_version)
.map(|index| &self.table[index].entry)
}
fn compile_shape_safety(&self, node: &NodeView) -> CompileShapeSafety {
let domain = node.domain();
let domain = if domain == "ai.onnx" { "" } else { &domain };
let op_type = node.op_type();
let Some(index) = self.find_entry_index(domain, &op_type, node.since_version()) else {
return CompileShapeSafety::ShapeKeyedOnly {
reason: "no matching registered lowering; shapeless compile denied conservatively",
};
};
let safety = match self.table[index].compile_shape_rule {
CompileShapeRule::Always(safety) => safety,
CompileShapeRule::Classified(classifier) => classifier(node),
};
if let CompileShapeSafety::ShapeKeyedOnly { reason }
| CompileShapeSafety::EagerOnly { reason } = safety
{
assert!(
!reason.is_empty(),
"shape classifier for {}::{} returned an empty reason",
domain,
op_type
);
}
safety
}
}
static REGISTRY: LazyLock<OpRegistry> = LazyLock::new(|| {
let mut r = OpRegistry::new();
register_builtin_ops(&mut r);
r
});
fn registry() -> &'static OpRegistry {
®ISTRY
}
pub fn compile_shape_safety(node: &NodeView) -> CompileShapeSafety {
registry().compile_shape_safety(node)
}
fn register_builtin_ops(registry: &mut OpRegistry) {
crate::ops::elementwise::register(registry);
crate::ops::image::register(registry);
crate::ops::math::register(registry);
crate::ops::reduction::register(registry);
crate::ops::shape::register(registry);
crate::ops::matmul::register(registry);
crate::ops::signal::register(registry);
crate::ops::random::register(registry);
crate::ops::recurrent::register(registry);
crate::ops::ssm::register(registry);
crate::ops::misc::register(registry);
crate::ops::controlflow::register(registry);
crate::ops::norm::register_norm(registry);
crate::ops::onnx_ml_linear::register(registry);
crate::ops::onnx_ml_preprocess::register(registry);
crate::ops::attention::register_attention(registry);
crate::ops::conv::register_conv(registry);
crate::ops::vision::register_vision(registry);
crate::ops::quant::register(registry); crate::ops::stragglers::register(registry); }
pub fn translate(ctx: &mut TranslationContext, n: &NodeDesc) -> Result<(), MlxError> {
let handler = registry()
.find_entry(&n.domain, &n.op_type, n.since_version)
.map(|e| e.handler)
.ok_or_else(|| {
format!(
"MLX: no translation for op {}::{}",
if n.domain.is_empty() {
"ai.onnx"
} else {
&n.domain
},
n.op_type
)
})?;
let tr = crate::trace::tracer();
let start = tr.op_timer_start();
ctx.reset_path_mark();
let r = handler(ctx, n);
if r.is_ok() {
let mark = ctx.take_path_mark();
tr.record_op_path(n, start, mark);
ctx.trace_node(&n.op_type, n, start);
}
r
}
pub fn claim_decision(node: &NodeView) -> ClaimResult {
match registry().find_entry(&node.domain(), &node.op_type(), node.since_version()) {
Some(entry) => (entry.claim)(node),
None => {
let op = node.op_type();
let dom = node.domain();
let where_ = if dom.is_empty() {
String::new()
} else {
format!(" (domain {dom})")
};
Err(Cow::Owned(format!(
"no MLX handler for {op}{where_} at opset {} — add a claim+handler in rust/src/ops/ and register it",
node.since_version()
)))
}
}
}
pub fn qualified_op_name(node: &NodeView) -> String {
let op = node.op_type();
let domain = node.domain();
if domain.is_empty() || domain == "ai.onnx" {
op
} else {
format!("{domain}.{op}")
}
}
pub fn claimable(node: &NodeView) -> bool {
claim_decision(node).is_ok()
}
pub fn ort_dtype_name(t: ort::ONNXTensorElementDataType) -> &'static str {
#[allow(non_upper_case_globals)]
match t {
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT => "fp32",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16 => "fp16",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16 => "bf16",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE => "fp64",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8 => "int8",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16 => "int16",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32 => "int32",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64 => "int64",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8 => "uint8",
ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BOOL => "bool",
_ => "other",
}
}
pub struct NodeView {
api: *const ort::OrtApi,
node: *const ort::OrtNode,
}
pub struct SlotInfo {
pub dtype: ort::ONNXTensorElementDataType,
pub shape: Vec<i64>,
}
impl NodeView {
pub fn new(api: *const ort::OrtApi, node: *const ort::OrtNode) -> Self {
NodeView { api, node }
}
fn api(&self) -> &ort::OrtApi {
unsafe { &*self.api }
}
fn cstr(&self, p: *const c_char) -> String {
if p.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
}
}
pub fn op_type(&self) -> String {
unsafe {
let mut p: *const c_char = std::ptr::null();
(self.api().Node_GetOperatorType.unwrap())(self.node, &mut p);
self.cstr(p)
}
}
pub fn name(&self) -> String {
unsafe {
let mut p: *const c_char = std::ptr::null();
(self.api().Node_GetName.unwrap())(self.node, &mut p);
self.cstr(p)
}
}
pub fn domain(&self) -> String {
unsafe {
let mut p: *const c_char = std::ptr::null();
(self.api().Node_GetDomain.unwrap())(self.node, &mut p);
self.cstr(p)
}
}
pub fn since_version(&self) -> i32 {
unsafe {
let mut v: i32 = 0;
(self.api().Node_GetSinceVersion.unwrap())(self.node, &mut v);
v
}
}
pub fn num_inputs(&self) -> usize {
unsafe {
let mut n: usize = 0;
(self.api().Node_GetNumInputs.unwrap())(self.node, &mut n);
n
}
}
pub fn num_outputs(&self) -> usize {
unsafe {
let mut n: usize = 0;
(self.api().Node_GetNumOutputs.unwrap())(self.node, &mut n);
n
}
}
fn inputs_raw(&self) -> Vec<*const ort::OrtValueInfo> {
let n = self.num_inputs();
let mut v: Vec<*const ort::OrtValueInfo> = vec![std::ptr::null(); n];
if n > 0 {
unsafe { (self.api().Node_GetInputs.unwrap())(self.node, v.as_mut_ptr(), n) };
}
v
}
fn outputs_raw(&self) -> Vec<*const ort::OrtValueInfo> {
let n = self.num_outputs();
let mut v: Vec<*const ort::OrtValueInfo> = vec![std::ptr::null(); n];
if n > 0 {
unsafe { (self.api().Node_GetOutputs.unwrap())(self.node, v.as_mut_ptr(), n) };
}
v
}
fn slot_info(&self, vi: *const ort::OrtValueInfo) -> Option<SlotInfo> {
if vi.is_null() {
return None;
}
unsafe {
let api = self.api();
let mut ti: *const ort::OrtTypeInfo = std::ptr::null();
let st = (api.GetValueInfoTypeInfo.unwrap())(vi, &mut ti);
if !st.is_null() || ti.is_null() {
return None;
}
let mut onnx_type: ort::ONNXType = 0;
(api.GetOnnxTypeFromTypeInfo.unwrap())(ti, &mut onnx_type);
if onnx_type != ort::ONNXType_ONNX_TYPE_TENSOR {
return None;
}
let mut tsi: *const ort::OrtTensorTypeAndShapeInfo = std::ptr::null();
(api.CastTypeInfoToTensorInfo.unwrap())(ti, &mut tsi);
if tsi.is_null() {
return None;
}
let mut dtype: ort::ONNXTensorElementDataType = 0;
(api.GetTensorElementType.unwrap())(tsi, &mut dtype);
let mut nd: usize = 0;
(api.GetDimensionsCount.unwrap())(tsi, &mut nd);
let mut dims = vec![0i64; nd];
if nd > 0 {
(api.GetDimensions.unwrap())(tsi, dims.as_mut_ptr(), nd);
}
Some(SlotInfo { dtype, shape: dims })
}
}
pub fn input_info(&self, i: usize) -> Option<SlotInfo> {
let ins = self.inputs_raw();
ins.get(i).and_then(|&vi| self.slot_info(vi))
}
pub fn output_info(&self, i: usize) -> Option<SlotInfo> {
let outs = self.outputs_raw();
outs.get(i).and_then(|&vi| self.slot_info(vi))
}
#[inline]
unsafe fn release_status(&self, st: *mut ort::OrtStatus) {
if !st.is_null() {
unsafe { (self.api().ReleaseStatus.unwrap())(st) };
}
}
pub fn int_attr(&self, name: &str, default: i64) -> i64 {
unsafe {
let api = self.api();
let cname = match std::ffi::CString::new(name) {
Ok(c) => c,
Err(_) => return default,
};
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return default;
}
if attr.is_null() {
return default;
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
if atype != ort::OrtOpAttrType_ORT_OP_ATTR_INT {
return default;
}
let mut value: i64 = default;
let mut out_len: usize = 0;
let st = (api.ReadOpAttr.unwrap())(
attr,
ort::OrtOpAttrType_ORT_OP_ATTR_INT,
&mut value as *mut i64 as *mut std::os::raw::c_void,
std::mem::size_of::<i64>(),
&mut out_len,
);
if !st.is_null() {
self.release_status(st);
return default;
}
value
}
}
pub fn float_attr(&self, name: &str, default: f32) -> f32 {
unsafe {
let api = self.api();
let cname = match std::ffi::CString::new(name) {
Ok(c) => c,
Err(_) => return default,
};
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return default;
}
if attr.is_null() {
return default;
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
if atype != ort::OrtOpAttrType_ORT_OP_ATTR_FLOAT {
return default;
}
let mut value: f32 = default;
let mut out_len: usize = 0;
let st = (api.ReadOpAttr.unwrap())(
attr,
ort::OrtOpAttrType_ORT_OP_ATTR_FLOAT,
&mut value as *mut f32 as *mut std::os::raw::c_void,
std::mem::size_of::<f32>(),
&mut out_len,
);
if !st.is_null() {
self.release_status(st);
return default;
}
value
}
}
pub fn output_present(&self, i: usize) -> bool {
let outs = self.outputs_raw();
match outs.get(i) {
Some(&vi) if !vi.is_null() => {
let mut p: *const c_char = std::ptr::null();
unsafe { (self.api().GetValueInfoName.unwrap())(vi, &mut p) };
!p.is_null() && unsafe { !std::ffi::CStr::from_ptr(p).to_bytes().is_empty() }
}
_ => false,
}
}
pub fn ints_attr(&self, name: &str) -> (bool, Vec<i64>) {
unsafe {
let api = self.api();
let cname = match std::ffi::CString::new(name) {
Ok(c) => c,
Err(_) => return (false, Vec::new()),
};
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return (false, Vec::new());
}
if attr.is_null() {
return (false, Vec::new());
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
if atype != ort::OrtOpAttrType_ORT_OP_ATTR_INTS {
return (false, Vec::new());
}
let read = api.ReadOpAttr.unwrap();
let mut needed: usize = 0;
let st0 = read(attr, atype, std::ptr::null_mut(), 0, &mut needed);
self.release_status(st0);
if needed == 0 {
return (true, Vec::new());
}
let count = needed / std::mem::size_of::<i64>();
let mut buf = vec![0i64; count];
let mut out: usize = 0;
let st = read(
attr,
atype,
buf.as_mut_ptr() as *mut std::os::raw::c_void,
needed,
&mut out,
);
if st.is_null() {
(true, buf)
} else {
self.release_status(st);
(false, Vec::new())
}
}
}
pub fn string_attr(&self, name: &str, default: &str) -> String {
unsafe {
let api = self.api();
let cname = match std::ffi::CString::new(name) {
Ok(c) => c,
Err(_) => return default.to_string(),
};
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return default.to_string();
}
if attr.is_null() {
return default.to_string();
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
if atype != ort::OrtOpAttrType_ORT_OP_ATTR_STRING {
return default.to_string();
}
let read = api.ReadOpAttr.unwrap();
let mut needed: usize = 0;
let st0 = read(attr, atype, std::ptr::null_mut(), 0, &mut needed);
self.release_status(st0);
if needed == 0 {
return String::new();
}
let mut buf = vec![0u8; needed];
let mut out: usize = 0;
let st = read(
attr,
atype,
buf.as_mut_ptr() as *mut std::os::raw::c_void,
needed,
&mut out,
);
if !st.is_null() {
self.release_status(st);
return default.to_string();
}
buf.truncate(out.min(needed));
String::from_utf8(buf).unwrap_or_else(|_| default.to_string())
}
}
pub fn float_attr_opt(&self, name: &str) -> Option<f32> {
unsafe {
let api = self.api();
let cname = std::ffi::CString::new(name).ok()?;
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return None;
}
if attr.is_null() {
return None;
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
if atype != ort::OrtOpAttrType_ORT_OP_ATTR_FLOAT {
return None;
}
let mut value: f32 = 0.0;
let mut out_len: usize = 0;
let st = (api.ReadOpAttr.unwrap())(
attr,
ort::OrtOpAttrType_ORT_OP_ATTR_FLOAT,
&mut value as *mut f32 as *mut std::os::raw::c_void,
std::mem::size_of::<f32>(),
&mut out_len,
);
if !st.is_null() {
self.release_status(st);
return None;
}
Some(value)
}
}
pub fn strings_attr(&self, name: &str) -> Option<Vec<String>> {
unsafe {
let api = self.api();
let cname = std::ffi::CString::new(name).ok()?;
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return None;
}
if attr.is_null() {
return None;
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
if atype != ort::OrtOpAttrType_ORT_OP_ATTR_STRINGS {
return None;
}
let read = api.ReadOpAttr.unwrap();
let mut needed: usize = 0;
let st0 = read(attr, atype, std::ptr::null_mut(), 0, &mut needed);
self.release_status(st0);
if needed == 0 {
return Some(Vec::new());
}
let mut buf = vec![0u8; needed];
let mut out: usize = 0;
let st = read(
attr,
atype,
buf.as_mut_ptr() as *mut std::os::raw::c_void,
needed,
&mut out,
);
if !st.is_null() {
self.release_status(st);
return None;
}
buf.truncate(out.min(needed));
let mut result = Vec::new();
for part in buf.split(|&b| b == 0) {
if !part.is_empty() {
result.push(String::from_utf8_lossy(part).into_owned());
}
}
Some(result)
}
}
pub fn has_attr(&self, name: &str) -> bool {
unsafe {
let api = self.api();
let cname = match std::ffi::CString::new(name) {
Ok(c) => c,
Err(_) => return false,
};
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return false;
}
if attr.is_null() {
return false;
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
atype != ort::OrtOpAttrType_ORT_OP_ATTR_UNDEFINED
}
}
pub fn attr_tensor_dtype(&self, name: &str) -> Option<ort::ONNXTensorElementDataType> {
unsafe {
let api = self.api();
let cname = std::ffi::CString::new(name).ok()?;
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return None;
}
if attr.is_null() {
return None;
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
if atype != ort::OrtOpAttrType_ORT_OP_ATTR_TENSOR {
return None;
}
let mut value: *mut ort::OrtValue = std::ptr::null_mut();
let st = (api.OpAttr_GetTensorAttributeAsOrtValue.unwrap())(attr, &mut value);
if !st.is_null() {
self.release_status(st);
return None;
}
if value.is_null() {
return None;
}
let mut info: *mut ort::OrtTensorTypeAndShapeInfo = std::ptr::null_mut();
let st = (api.GetTensorTypeAndShape.unwrap())(value, &mut info);
if !st.is_null() {
self.release_status(st);
(api.ReleaseValue.unwrap())(value);
return None;
}
if info.is_null() {
(api.ReleaseValue.unwrap())(value);
return None;
}
let mut etype: ort::ONNXTensorElementDataType = 0;
let st = (api.GetTensorElementType.unwrap())(info, &mut etype);
let ok = st.is_null();
if !ok {
self.release_status(st);
}
(api.ReleaseTensorTypeAndShapeInfo.unwrap())(info);
(api.ReleaseValue.unwrap())(value);
ok.then_some(etype)
}
}
pub fn attr_type(&self, name: &str) -> ort::OrtOpAttrType {
unsafe {
let api = self.api();
let cname = match std::ffi::CString::new(name) {
Ok(c) => c,
Err(_) => return ort::OrtOpAttrType_ORT_OP_ATTR_UNDEFINED,
};
let mut attr: *const ort::OrtOpAttr = std::ptr::null();
let st = (api.Node_GetAttributeByName.unwrap())(self.node, cname.as_ptr(), &mut attr);
if !st.is_null() {
self.release_status(st);
return ort::OrtOpAttrType_ORT_OP_ATTR_UNDEFINED;
}
if attr.is_null() {
return ort::OrtOpAttrType_ORT_OP_ATTR_UNDEFINED;
}
let mut atype: ort::OrtOpAttrType = 0;
(api.OpAttr_GetType.unwrap())(attr, &mut atype);
atype
}
}
pub fn const_scalar_i64(&self, i: usize) -> Option<i64> {
let info = self.input_info(i)?;
if info.dtype != ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64 {
return None;
}
if !(info.shape.is_empty() || info.shape == [1]) {
return None;
}
if !self.is_constant_initializer(i) {
return None;
}
let ins = self.inputs_raw();
let vi = *ins.get(i)?;
if vi.is_null() {
return None;
}
unsafe {
let api = self.api();
let mut val: *const ort::OrtValue = std::ptr::null();
let st = (api.ValueInfo_GetInitializerValue.unwrap())(vi, &mut val);
if !st.is_null() || val.is_null() {
if !st.is_null() {
self.release_status(st);
}
return None;
}
let mut data: *mut std::os::raw::c_void = std::ptr::null_mut();
let st = (api.GetTensorMutableData.unwrap())(val as *mut ort::OrtValue, &mut data);
if !st.is_null() || data.is_null() {
if !st.is_null() {
self.release_status(st);
}
return None;
}
Some(*(data as *const i64))
}
}
pub fn input_present(&self, i: usize) -> bool {
let ins = self.inputs_raw();
match ins.get(i) {
Some(&vi) if !vi.is_null() => {
let mut p: *const c_char = std::ptr::null();
unsafe { (self.api().GetValueInfoName.unwrap())(vi, &mut p) };
!p.is_null() && unsafe { !std::ffi::CStr::from_ptr(p).to_bytes().is_empty() }
}
_ => false,
}
}
pub fn is_constant_initializer(&self, i: usize) -> bool {
let ins = self.inputs_raw();
let vi = match ins.get(i) {
Some(&vi) if !vi.is_null() => vi,
_ => return false,
};
unsafe {
let mut is_const = false;
let st = (self.api().ValueInfo_IsConstantInitializer.unwrap())(vi, &mut is_const);
if !st.is_null() {
self.release_status(st);
return false;
}
is_const
}
}
pub fn is_const_int64(&self, i: usize) -> bool {
matches!(self.input_info(i), Some(info)
if info.dtype == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64)
&& self.is_constant_initializer(i)
}
pub fn input_is_host_readable(&self, i: usize) -> bool {
let ins = self.inputs_raw();
let vi = match ins.get(i) {
Some(&vi) if !vi.is_null() => vi,
_ => return false,
};
unsafe {
let mut producer: *const ort::OrtNode = std::ptr::null();
let st = (self.api().ValueInfo_GetValueProducer.unwrap())(
vi,
&mut producer,
std::ptr::null_mut(),
);
if !st.is_null() {
self.release_status(st);
return false;
}
producer.is_null()
}
}
pub fn read_const_int64(&self, i: usize) -> Option<Vec<i64>> {
if !self.is_const_int64(i) {
return None;
}
let ins = self.inputs_raw();
let vi = *ins.get(i)?;
unsafe {
let api = self.api();
let mut value: *const ort::OrtValue = std::ptr::null();
let st = (api.ValueInfo_GetInitializerValue.unwrap())(vi, &mut value);
if !st.is_null() {
self.release_status(st);
return None;
}
if value.is_null() {
return None;
}
let mut info: *mut ort::OrtTensorTypeAndShapeInfo = std::ptr::null_mut();
(api.GetTensorTypeAndShape.unwrap())(value, &mut info);
let mut count: usize = 0;
(api.GetTensorShapeElementCount.unwrap())(info, &mut count);
(api.ReleaseTensorTypeAndShapeInfo.unwrap())(info);
let mut data: *const std::os::raw::c_void = std::ptr::null();
(api.GetTensorData.unwrap())(value, &mut data);
if data.is_null() {
return if count == 0 { Some(Vec::new()) } else { None };
}
Some(std::slice::from_raw_parts(data as *const i64, count).to_vec())
}
}
pub fn read_const_f32(&self, i: usize) -> Option<Vec<f32>> {
if !matches!(self.input_info(i), Some(info)
if info.dtype == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT)
|| !self.is_constant_initializer(i)
{
return None;
}
let ins = self.inputs_raw();
let vi = *ins.get(i)?;
unsafe {
let api = self.api();
let mut value: *const ort::OrtValue = std::ptr::null();
let st = (api.ValueInfo_GetInitializerValue.unwrap())(vi, &mut value);
if !st.is_null() {
self.release_status(st);
return None;
}
if value.is_null() {
return None;
}
let mut info: *mut ort::OrtTensorTypeAndShapeInfo = std::ptr::null_mut();
(api.GetTensorTypeAndShape.unwrap())(value, &mut info);
let mut count: usize = 0;
(api.GetTensorShapeElementCount.unwrap())(info, &mut count);
(api.ReleaseTensorTypeAndShapeInfo.unwrap())(info);
let mut data: *const std::os::raw::c_void = std::ptr::null();
(api.GetTensorData.unwrap())(value, &mut data);
if data.is_null() {
return if count == 0 { Some(Vec::new()) } else { None };
}
Some(std::slice::from_raw_parts(data as *const f32, count).to_vec())
}
}
pub fn input_names(&self) -> Vec<String> {
self.inputs_raw()
.iter()
.map(|&vi| {
if vi.is_null() {
return String::new();
}
let mut p: *const c_char = std::ptr::null();
unsafe { (self.api().GetValueInfoName.unwrap())(vi, &mut p) };
self.cstr(p)
})
.collect()
}
pub fn output_names(&self) -> Vec<String> {
self.outputs_raw()
.iter()
.map(|&vi| {
if vi.is_null() {
return String::new();
}
let mut p: *const c_char = std::ptr::null();
unsafe { (self.api().GetValueInfoName.unwrap())(vi, &mut p) };
self.cstr(p)
})
.collect()
}
pub fn subgraphs(&self) -> Vec<(String, GraphView)> {
unsafe {
let api = self.api();
let mut num: usize = 0;
let st = (api.Node_GetNumSubgraphs.unwrap())(self.node, &mut num);
if !st.is_null() {
self.release_status(st);
return Vec::new();
}
if num == 0 {
return Vec::new();
}
let mut graphs: Vec<*const ort::OrtGraph> = vec![std::ptr::null(); num];
let mut names: Vec<*const c_char> = vec![std::ptr::null(); num];
let st = (api.Node_GetSubgraphs.unwrap())(
self.node,
graphs.as_mut_ptr(),
num,
names.as_mut_ptr(),
);
if !st.is_null() {
self.release_status(st);
return Vec::new();
}
(0..num)
.map(|i| (self.cstr(names[i]), GraphView::new(self.api, graphs[i])))
.collect()
}
}
pub fn is_const_int_tensor(&self, i: usize) -> bool {
matches!(self.input_info(i), Some(info) if is_int_index(info.dtype))
&& self.is_constant_initializer(i)
}
pub fn read_const_ints_any(&self, i: usize) -> Option<Vec<i64>> {
let dtype = self.input_info(i)?.dtype;
if !self.is_const_int_tensor(i) {
return None;
}
let ins = self.inputs_raw();
let vi = *ins.get(i)?;
unsafe {
let api = self.api();
let mut value: *const ort::OrtValue = std::ptr::null();
let st = (api.ValueInfo_GetInitializerValue.unwrap())(vi, &mut value);
if !st.is_null() {
self.release_status(st);
return None;
}
if value.is_null() {
return None;
}
let mut info: *mut ort::OrtTensorTypeAndShapeInfo = std::ptr::null_mut();
(api.GetTensorTypeAndShape.unwrap())(value, &mut info);
let mut count: usize = 0;
(api.GetTensorShapeElementCount.unwrap())(info, &mut count);
(api.ReleaseTensorTypeAndShapeInfo.unwrap())(info);
let mut data: *const std::os::raw::c_void = std::ptr::null();
(api.GetTensorData.unwrap())(value, &mut data);
if data.is_null() {
return if count == 0 { Some(Vec::new()) } else { None };
}
if dtype == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64 {
Some(std::slice::from_raw_parts(data as *const i64, count).to_vec())
} else {
Some(
std::slice::from_raw_parts(data as *const i32, count)
.iter()
.map(|&v| v as i64)
.collect(),
)
}
}
}
pub fn read_const_scalar_f64(&self, i: usize) -> Option<f64> {
if !self.is_constant_initializer(i) {
return None;
}
let dtype = self.input_info(i)?.dtype;
let ins = self.inputs_raw();
let vi = *ins.get(i)?;
unsafe {
let api = self.api();
let mut value: *const ort::OrtValue = std::ptr::null();
let st = (api.ValueInfo_GetInitializerValue.unwrap())(vi, &mut value);
if !st.is_null() {
self.release_status(st);
return None;
}
if value.is_null() {
return None;
}
let mut info: *mut ort::OrtTensorTypeAndShapeInfo = std::ptr::null_mut();
(api.GetTensorTypeAndShape.unwrap())(value, &mut info);
let mut count: usize = 0;
(api.GetTensorShapeElementCount.unwrap())(info, &mut count);
(api.ReleaseTensorTypeAndShapeInfo.unwrap())(info);
if count != 1 {
return None;
}
let mut data: *const std::os::raw::c_void = std::ptr::null();
(api.GetTensorData.unwrap())(value, &mut data);
if data.is_null() {
return None;
}
match dtype {
t if t == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16 => {
Some(*(data as *const i16) as f64)
}
t if t == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32 => {
Some(*(data as *const i32) as f64)
}
t if t == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64 => {
Some(*(data as *const i64) as f64)
}
t if t == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT => {
Some(*(data as *const f32) as f64)
}
t if t == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16 => {
Some(f16::from_bits(*(data as *const u16)).to_f64())
}
t if t == ort::ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16 => {
Some(bf16::from_bits(*(data as *const u16)).to_f64())
}
_ => None,
}
}
}
}
pub struct GraphView {
api: *const ort::OrtApi,
graph: *const ort::OrtGraph,
}
impl GraphView {
pub fn new(api: *const ort::OrtApi, graph: *const ort::OrtGraph) -> Self {
GraphView { api, graph }
}
fn api(&self) -> &ort::OrtApi {
unsafe { &*self.api }
}
fn name(&self, p: *const c_char) -> String {
if p.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }
}
}
pub fn nodes(&self) -> Vec<NodeView> {
unsafe {
let api = self.api();
let mut num: usize = 0;
(api.Graph_GetNumNodes.unwrap())(self.graph, &mut num);
if num == 0 {
return Vec::new();
}
let mut nodes: Vec<*const ort::OrtNode> = vec![std::ptr::null(); num];
(api.Graph_GetNodes.unwrap())(self.graph, nodes.as_mut_ptr(), num);
nodes
.into_iter()
.map(|n| NodeView::new(self.api, n))
.collect()
}
}
fn value_names(
&self,
count_fn: unsafe extern "C" fn(*const ort::OrtGraph, *mut usize) -> *mut ort::OrtStatus,
get_fn: unsafe extern "C" fn(
*const ort::OrtGraph,
*mut *const ort::OrtValueInfo,
usize,
) -> *mut ort::OrtStatus,
) -> Vec<String> {
unsafe {
let mut num: usize = 0;
count_fn(self.graph, &mut num);
if num == 0 {
return Vec::new();
}
let mut vis: Vec<*const ort::OrtValueInfo> = vec![std::ptr::null(); num];
get_fn(self.graph, vis.as_mut_ptr(), num);
vis.into_iter()
.map(|vi| {
if vi.is_null() {
return String::new();
}
let mut p: *const c_char = std::ptr::null();
(self.api().GetValueInfoName.unwrap())(vi, &mut p);
self.name(p)
})
.collect()
}
}
pub fn input_names(&self) -> Vec<String> {
self.value_names(
self.api().Graph_GetNumInputs.unwrap(),
self.api().Graph_GetInputs.unwrap(),
)
}
pub fn output_names(&self) -> Vec<String> {
self.value_names(
self.api().Graph_GetNumOutputs.unwrap(),
self.api().Graph_GetOutputs.unwrap(),
)
}
pub fn all_nodes_claimable(&self) -> bool {
self.first_unclaimable_node().is_none()
}
pub fn first_unclaimable_node(&self) -> Option<(String, String, String)> {
self.nodes().iter().find_map(|n| match claim_decision(n) {
Ok(()) => None,
Err(reason) => Some((n.op_type(), n.name(), reason.into_owned())),
})
}
pub fn body_uses_float64(&self) -> bool {
self.nodes().iter().any(|n| {
node_uses_float64(n)
|| n.subgraphs()
.iter()
.any(|(_, body)| body.body_uses_float64())
})
}
}
use ort::*;
pub fn is_mlx_float(t: ort::ONNXTensorElementDataType) -> bool {
t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16
}
pub fn is_float64(t: ort::ONNXTensorElementDataType) -> bool {
t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE
}
pub fn is_mlx_cpu_float(t: ort::ONNXTensorElementDataType) -> bool {
is_mlx_float(t) || is_float64(t)
}
pub fn node_uses_float64(node: &NodeView) -> bool {
(0..node.num_inputs()).any(|i| node.input_info(i).is_some_and(|s| is_float64(s.dtype)))
|| (0..node.num_outputs()).any(|i| node.output_info(i).is_some_and(|s| is_float64(s.dtype)))
}
pub fn is_signed_integer(t: ort::ONNXTensorElementDataType) -> bool {
t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64
}
pub fn is_unsigned_integer(t: ort::ONNXTensorElementDataType) -> bool {
t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT16
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64
}
pub fn is_mlx_supported(t: ort::ONNXTensorElementDataType) -> bool {
is_mlx_float(t)
|| is_signed_integer(t)
|| is_unsigned_integer(t)
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BOOL
}
pub fn is_mlx_numeric(t: ort::ONNXTensorElementDataType) -> bool {
is_mlx_float(t) || is_signed_integer(t) || is_unsigned_integer(t)
}
pub fn is_movable(t: ort::ONNXTensorElementDataType) -> bool {
is_mlx_float(t)
|| is_float64(t)
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT16
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BOOL
}
pub fn is_int_index(t: ort::ONNXTensorElementDataType) -> bool {
t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64
}
pub fn is_range_type(t: ort::ONNXTensorElementDataType) -> bool {
t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16
|| t == ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16
}
pub fn suffix_broadcast(a: &[i64], b: &[i64]) -> bool {
if a.is_empty() || b.is_empty() {
return false;
}
let (long, short) = if a.len() >= b.len() { (a, b) } else { (b, a) };
let off = long.len() - short.len();
for i in 0..short.len() {
let l = long[off + i];
let s = short[i];
if l < 0 || s < 0 {
continue;
}
if l != s && s != 1 && l != 1 {
return false;
}
}
true
}
pub fn scalar_or_suffix_broadcast(a: &[i64], b: &[i64]) -> bool {
if a.is_empty() || b.is_empty() {
return true;
}
suffix_broadcast(a, b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suffix_broadcast_treats_unknown_extents_as_compatible() {
assert!(suffix_broadcast(&[-1, -1, 64], &[-1, -1, -1]));
assert!(suffix_broadcast(&[-1, 256, 1], &[1, 256, 16]));
assert!(suffix_broadcast(&[-1], &[8]));
assert!(scalar_or_suffix_broadcast(&[-1, -1, 64], &[-1, -1, -1]));
}
#[test]
fn float64_is_a_separate_opt_in_from_the_gpu_float_set() {
use ort::*;
const F64: ort::ONNXTensorElementDataType =
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE;
const F32: ort::ONNXTensorElementDataType =
ONNXTensorElementDataType_ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT;
assert!(!is_mlx_float(F64));
assert!(!is_mlx_supported(F64));
assert!(!is_mlx_numeric(F64));
assert!(is_mlx_cpu_float(F64));
assert!(is_mlx_cpu_float(F32));
assert!(is_float64(F64));
assert!(!is_float64(F32));
assert!(is_movable(F64));
}
#[test]
fn suffix_broadcast_still_rejects_known_mismatches() {
assert!(!suffix_broadcast(&[2, 3], &[2, 4]));
assert!(!suffix_broadcast(&[-1, 3], &[-1, 4]));
assert!(suffix_broadcast(&[2, 3], &[3]));
assert!(suffix_broadcast(&[2, 3], &[1, 3]));
assert!(!suffix_broadcast(&[2, 3], &[]));
assert!(scalar_or_suffix_broadcast(&[2, 3], &[]));
}
fn compile_shape_rule(
domain: &str,
op_type: &str,
since_version: i32,
) -> &'static CompileShapeRule {
let index = registry()
.find_entry_index(domain, op_type, since_version)
.unwrap_or_else(|| panic!("missing registration for {domain}::{op_type}"));
®istry().table[index].compile_shape_rule
}
#[test]
fn every_shape_keyed_registration_carries_a_reason() {
for registered in ®istry().table {
if let CompileShapeRule::Always(CompileShapeSafety::ShapeKeyedOnly { reason }) =
registered.compile_shape_rule
{
assert!(
!reason.is_empty(),
"{}::{} omitted its shape-specialization reason",
registered.entry.domain,
registered.entry.op_type
);
assert!(
reason.contains("output_shapes") || reason.contains("conservatively"),
"{}::{} has an unactionable shape-specialization reason: {reason}",
registered.entry.domain,
registered.entry.op_type
);
}
}
}
#[test]
fn known_missing_output_shape_emitters_cannot_be_shapeless() {
let always_shape_keyed = [
("com.microsoft", "PagedAttention", 1),
("com.microsoft", "Attention", 1),
("com.microsoft", "PackedMultiHeadAttention", 1),
("", "RotaryEmbedding", 23),
("com.microsoft", "RotaryEmbedding", 1),
("com.microsoft", "MRotaryEmbedding", 1),
("", "Scan", 24),
("", "DeformConv", 22),
("", "AveragePool", 24),
("", "MaxPool", 24),
("", "LpPool", 24),
("", "BitCast", 26),
("", "Scatter", 9),
("", "LRN", 24),
("ai.onnx.ml", "SVMClassifier", 1),
("ai.onnx.ml", "FeatureVectorizer", 1),
("com.microsoft", "QMoE", 1),
("", "RandomNormal", 1),
("", "RandomNormalLike", 1),
("", "RandomUniform", 1),
("", "RandomUniformLike", 1),
("", "Bernoulli", 15),
("", "Multinomial", 7),
("", "RNN", 24),
("", "GRU", 24),
("", "LSTM", 24),
("", "CumSum", 14),
("", "ScatterElements", 24),
("", "CenterCropPad", 24),
("", "ScatterND", 24),
("", "Slice", 24),
("", "Split", 24),
("", "Pad", 24),
("", "DFT", 17),
("", "STFT", 17),
("com.microsoft", "CausalConvWithState", 1),
("", "CausalConvWithState", 27),
("com.microsoft", "LinearAttention", 1),
("", "LinearAttention", 27),
("", "GridSample", 24),
("", "Col2Im", 24),
("", "RoiAlign", 24),
("", "MaxRoiPool", 24),
("", "MaxUnpool", 24),
];
for (domain, op_type, since_version) in always_shape_keyed {
assert!(
matches!(
compile_shape_rule(domain, op_type, since_version),
CompileShapeRule::Always(CompileShapeSafety::ShapeKeyedOnly { .. })
),
"{domain}::{op_type} can emit an MLX primitive without output_shapes"
);
}
for op_type in [
"MatMulBlockQuantizedFp4Weight",
"MatMulBlockQuantizedFp8Weight",
"MatMulNBits",
"GatherBlockQuantized",
] {
assert!(
matches!(
compile_shape_rule("com.microsoft", op_type, 1),
CompileShapeRule::Classified(_)
),
"com.microsoft::{op_type} needs a per-node quant geometry classifier"
);
}
assert!(matches!(
compile_shape_rule("com.microsoft", "GroupQueryAttention", 1),
CompileShapeRule::Classified(_)
));
}
}