diff --git a/Makefile b/Makefile index b6258103..4fe223b9 100644 --- a/Makefile +++ b/Makefile @@ -109,7 +109,7 @@ test-wpt-litert: $(CARGO) test --test run_wpt_conformance --features "litert-runtime" -- litert --test-threads=1 test-wpt-coreml: - $(CARGO) test --test run_wpt_conformance --features coreml-runtime -- coreml --test-threads 1 + $(CARGO) test --test run_wpt_conformance --features coreml-runtime -- coreml test-wpt-coreml-report: @mkdir -p reports diff --git a/docs/development/backend-operator-support.md b/docs/development/backend-operator-support.md index e3091d46..338ec39f 100644 --- a/docs/development/backend-operator-support.md +++ b/docs/development/backend-operator-support.md @@ -24,76 +24,78 @@ Executor-level operator coverage follows converter coverage for this backend. - Converter source: `src/converters/coreml_mlprogram.rs` - Executor source: `src/executors/coreml.rs` -- Converter operator count: **88** -- Executor operator count: **88** +- Converter operator count: **92** +- Executor operator count: **92** ### Converter Operators -- `abs`, `hardSigmoid`, `reducemin` -- `add`, `hardSwish`, `reduceproduct` -- `argMax`, `identity`, `reducesum` -- `argMin`, `instanceNormalization`, `reducesumsquare` -- `averagePool2d`, `layerNormalization`, `relu` -- `batchNormalization`, `leakyRelu`, `reshape` -- `cast`, `lesser`, `reverse` -- `ceil`, `lesserOrEqual`, `roundEven` -- `clamp`, `log`, `scatterElements` -- `concat`, `logicalAnd`, `scatterND` -- `conv2d`, `logicalNot`, `sigmoid` -- `convTranspose2d`, `logicalOr`, `sign` -- `cos`, `logicalXor`, `sin` -- `cumulativeSum`, `matmul`, `slice` -- `dequantizeLinear`, `max`, `softmax` -- `div`, `maxPool2d`, `softplus` -- `elu`, `min`, `softsign` -- `equal`, `mul`, `split` -- `erf`, `neg`, `sqrt` -- `exp`, `pad`, `squeeze` -- `expand`, `pow`, `sub` -- `floor`, `prelu`, `tan` -- `gather`, `quantizeLinear`, `tanh` -- `gatherElements`, `reciprocal`, `tile` -- `gelu`, `reducel1`, `transpose` -- `gemm`, `reducel2`, `triangular` -- `globalAveragePool`, `reducelogsum`, `unsqueeze` -- `globalMaxPool`, `reducelogsumexp`, `where` -- `greater`, `reducemax` -- `greaterOrEqual`, `reducemean` +- `abs`, `hardSigmoid`, `reducemax` +- `add`, `hardSwish`, `reducemean` +- `argMax`, `identity`, `reducemin` +- `argMin`, `instanceNormalization`, `reduceproduct` +- `averagePool2d`, `isInfinite`, `reducesum` +- `batchNormalization`, `isNaN`, `reducesumsquare` +- `cast`, `l2pool2d`, `relu` +- `ceil`, `layerNormalization`, `reshape` +- `clamp`, `leakyRelu`, `reverse` +- `concat`, `lesser`, `roundEven` +- `conv2d`, `lesserOrEqual`, `scatterElements` +- `convTranspose2d`, `log`, `scatterND` +- `cos`, `logicalAnd`, `sigmoid` +- `cumulativeSum`, `logicalNot`, `sign` +- `dequantizeLinear`, `logicalOr`, `sin` +- `div`, `logicalXor`, `slice` +- `elu`, `matmul`, `softmax` +- `equal`, `max`, `softplus` +- `erf`, `maxPool2d`, `softsign` +- `exp`, `min`, `split` +- `expand`, `mul`, `sqrt` +- `floor`, `neg`, `squeeze` +- `gather`, `pad`, `sub` +- `gatherElements`, `pow`, `tan` +- `gatherND`, `prelu`, `tanh` +- `gelu`, `quantizeLinear`, `tile` +- `gemm`, `reciprocal`, `transpose` +- `globalAveragePool`, `reducel1`, `triangular` +- `globalMaxPool`, `reducel2`, `unsqueeze` +- `greater`, `reducelogsum`, `where` +- `greaterOrEqual`, `reducelogsumexp` ### Executor Operators Executor-level operator coverage follows converter coverage for this backend. -- `abs`, `hardSigmoid`, `reducemin` -- `add`, `hardSwish`, `reduceproduct` -- `argMax`, `identity`, `reducesum` -- `argMin`, `instanceNormalization`, `reducesumsquare` -- `averagePool2d`, `layerNormalization`, `relu` -- `batchNormalization`, `leakyRelu`, `reshape` -- `cast`, `lesser`, `reverse` -- `ceil`, `lesserOrEqual`, `roundEven` -- `clamp`, `log`, `scatterElements` -- `concat`, `logicalAnd`, `scatterND` -- `conv2d`, `logicalNot`, `sigmoid` -- `convTranspose2d`, `logicalOr`, `sign` -- `cos`, `logicalXor`, `sin` -- `cumulativeSum`, `matmul`, `slice` -- `dequantizeLinear`, `max`, `softmax` -- `div`, `maxPool2d`, `softplus` -- `elu`, `min`, `softsign` -- `equal`, `mul`, `split` -- `erf`, `neg`, `sqrt` -- `exp`, `pad`, `squeeze` -- `expand`, `pow`, `sub` -- `floor`, `prelu`, `tan` -- `gather`, `quantizeLinear`, `tanh` -- `gatherElements`, `reciprocal`, `tile` -- `gelu`, `reducel1`, `transpose` -- `gemm`, `reducel2`, `triangular` -- `globalAveragePool`, `reducelogsum`, `unsqueeze` -- `globalMaxPool`, `reducelogsumexp`, `where` -- `greater`, `reducemax` -- `greaterOrEqual`, `reducemean` +- `abs`, `hardSigmoid`, `reducemax` +- `add`, `hardSwish`, `reducemean` +- `argMax`, `identity`, `reducemin` +- `argMin`, `instanceNormalization`, `reduceproduct` +- `averagePool2d`, `isInfinite`, `reducesum` +- `batchNormalization`, `isNaN`, `reducesumsquare` +- `cast`, `l2pool2d`, `relu` +- `ceil`, `layerNormalization`, `reshape` +- `clamp`, `leakyRelu`, `reverse` +- `concat`, `lesser`, `roundEven` +- `conv2d`, `lesserOrEqual`, `scatterElements` +- `convTranspose2d`, `log`, `scatterND` +- `cos`, `logicalAnd`, `sigmoid` +- `cumulativeSum`, `logicalNot`, `sign` +- `dequantizeLinear`, `logicalOr`, `sin` +- `div`, `logicalXor`, `slice` +- `elu`, `matmul`, `softmax` +- `equal`, `max`, `softplus` +- `erf`, `maxPool2d`, `softsign` +- `exp`, `min`, `split` +- `expand`, `mul`, `sqrt` +- `floor`, `neg`, `squeeze` +- `gather`, `pad`, `sub` +- `gatherElements`, `pow`, `tan` +- `gatherND`, `prelu`, `tanh` +- `gelu`, `quantizeLinear`, `tile` +- `gemm`, `reciprocal`, `transpose` +- `globalAveragePool`, `reducel1`, `triangular` +- `globalMaxPool`, `reducel2`, `unsqueeze` +- `greater`, `reducelogsum`, `where` +- `greaterOrEqual`, `reducelogsumexp` ## TensorRT Backend diff --git a/src/backends/coreml.rs b/src/backends/coreml.rs index 319484b7..33ba07b0 100644 --- a/src/backends/coreml.rs +++ b/src/backends/coreml.rs @@ -267,7 +267,35 @@ impl<'context> MLBackendContext<'context> for CoremlContext { source: format!("model did not produce output '{name}'").into(), })?; let logical = tensor_byte_len(ml_tensor.descriptor()); - if data.len() < logical { + + // When the graph output type is int64/uint64 but CoreML produced int32 + // bytes (argmin/argmax proxy, or a cast to int64/uint64), widen each + // 4-byte value to 8 bytes. int64 is sign-extended so negative results + // survive; uint64 is zero-extended. + use crate::operator_enums::MLOperandDataType; + let out_dt = ml_tensor.descriptor().data_type(); + let expanded: Option> = if data.len() * 2 == logical + && matches!(out_dt, MLOperandDataType::Int64 | MLOperandDataType::Uint64) + { + let sign_extend = matches!(out_dt, MLOperandDataType::Int64); + let count = data.len() / 4; + let mut buf = vec![0u8; count * 8]; + for i in 0..count { + let v = i32::from_le_bytes(data[i * 4..i * 4 + 4].try_into().unwrap()); + let widened: i64 = if sign_extend { + v as i64 + } else { + i64::from(v as u32) + }; + buf[i * 8..i * 8 + 8].copy_from_slice(&widened.to_le_bytes()); + } + Some(buf) + } else { + None + }; + let effective = expanded.as_deref().unwrap_or(data.as_slice()); + + if effective.len() < logical { return Err(Error::GraphDispatchError { source: format!( "output '{name}': CoreML produced {} bytes, descriptor expects {logical}", @@ -286,7 +314,7 @@ impl<'context> MLBackendContext<'context> for CoremlContext { .into(), }); } - dst[..logical].copy_from_slice(&data[..logical]); + dst[..logical].copy_from_slice(&effective[..logical]); } Ok(()) } diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index fc0c43d6..638d722f 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -27,7 +27,7 @@ /// This replaces the legacy NeuralNetwork format. use crate::converters::operand_name; use crate::error::GraphError; -use crate::graph::{DataType, Dimension as GraphDimension, GraphInfo}; +use crate::graph::{DataType, Dimension as GraphDimension, GraphInfo, OperandKind}; use crate::operator_enums::MLOperandDataType; use crate::operator_options::MLDimension; use crate::operators::Operation; @@ -39,6 +39,54 @@ use crate::protos::coreml::specification::Model; use prost::Message; use std::collections::HashMap; +/// Convert zero_point byte data from a source dtype to a target dtype. +/// Only Int32 → Uint8 and Int32 → Int8 are supported; all other pairs are returned as-is. +/// Values are clamped to the target range to avoid silent corruption. +fn convert_zp_bytes(src: &[u8], src_dtype: &DataType, tgt_dtype: &DataType) -> Vec { + match (src_dtype, tgt_dtype) { + (DataType::Int32, DataType::Uint8) => { + let count = src.len() / 4; + let mut out = Vec::with_capacity(count); + for i in 0..count { + let v = i32::from_le_bytes([ + src[i * 4], + src[i * 4 + 1], + src[i * 4 + 2], + src[i * 4 + 3], + ]); + out.push(v.clamp(0, 255) as u8); + } + out + } + (DataType::Int32, DataType::Int8) => { + let count = src.len() / 4; + let mut out = Vec::with_capacity(count); + for i in 0..count { + let v = i32::from_le_bytes([ + src[i * 4], + src[i * 4 + 1], + src[i * 4 + 2], + src[i * 4 + 3], + ]); + out.push(v.clamp(-128, 127) as i8 as u8); + } + out + } + // Uint32 → Int32: same 4 bytes, just reinterpreted as signed. + (DataType::Uint32, DataType::Int32) => src.to_vec(), + // Int64 → Int32: take the lower 4 bytes of each 8-byte element. + (DataType::Int64, DataType::Int32) => { + let count = src.len() / 8; + let mut out = Vec::with_capacity(count * 4); + for i in 0..count { + out.extend_from_slice(&src[i * 8..i * 8 + 4]); + } + out + } + _ => src.to_vec(), + } +} + /// MIL operation type names (matching Chromium's implementation) mod mil_ops { // Binary operations @@ -64,6 +112,7 @@ mod mil_ops { pub const CONV_TRANSPOSE: &str = "conv_transpose"; pub const AVG_POOL: &str = "avg_pool"; pub const MAX_POOL: &str = "max_pool"; + pub const L2_POOL: &str = "l2_pool"; pub const GLOBAL_AVG_POOL: &str = "reduce_mean"; // Global pooling via reduction pub const GLOBAL_MAX_POOL: &str = "reduce_max"; // Global pooling via reduction @@ -123,6 +172,7 @@ mod mil_ops { pub const TRANSPOSE: &str = "transpose"; pub const CONCAT: &str = "concat"; pub const SLICE: &str = "slice_by_size"; + pub const SLICE_BY_INDEX: &str = "slice_by_index"; pub const EXPAND: &str = "tile"; pub const GATHER: &str = "gather"; pub const GATHER_ALONG_AXIS: &str = "gather_along_axis"; @@ -154,7 +204,9 @@ mod mil_ops { pub const CAST: &str = "cast"; // Scatter operations - pub const SCATTER_ELEMENTS: &str = "scatter"; + // WebNN scatterElements (indices/updates share the data's rank, scattered along + // one axis) maps to MIL `scatter_along_axis`; MIL `scatter` expects rank-1 indices. + pub const SCATTER_ELEMENTS: &str = "scatter_along_axis"; pub const SCATTER_ND: &str = "scatter_nd"; // Tile operation @@ -167,6 +219,17 @@ mod mil_ops { // Clamp operation pub const CLIP: &str = "clip"; + + // NaN and infinity detection operations + pub const IS_NAN: &str = "is_nan"; + pub const IS_INF: &str = "is_inf"; + + // Gather N-dimensional + pub const GATHER_ND: &str = "gather_nd"; + + // Upsample/resample operations + pub const UPSAMPLE_NEAREST_NEIGHBOR: &str = "upsample_nearest_neighbor"; + pub const UPSAMPLE_BILINEAR: &str = "upsample_bilinear"; } // Default epsilon value used by several CoreML operations for numerical stability. @@ -323,7 +386,7 @@ impl CoremlMlProgramConverter { graph, operand_id, name.clone(), - Self::mil_data_type( + Self::graph_value_mil_type( &graph .operand(operand_id) .ok_or_else(|| GraphError::ConversionFailed { @@ -387,6 +450,50 @@ impl CoremlMlProgramConverter { } } + /// MIL type used for an operand's value *inside* the graph. CoreML MIL has no + /// int4/uint4/int64/uint32/uint64 tensor type, so those are represented as int32 (a + /// proxy); the model interface and executor reconcile width/sign/packing at the boundary. + fn graph_value_mil_type(data_type: &DataType) -> Result { + if Self::is_wide_int(data_type) { + Ok(crate::protos::coreml::mil_spec::DataType::Int32 as i32) + } else { + Self::mil_data_type(data_type) + } + } + + /// Whether a WebNN type has no native MIL tensor representation and is proxied + /// through int32 inside the graph (int4/uint4 sub-byte, and int64/uint32/uint64). + fn is_wide_int(data_type: &DataType) -> bool { + matches!( + data_type, + DataType::Int4 + | DataType::Uint4 + | DataType::Uint32 + | DataType::Int64 + | DataType::Uint64 + ) + } + + /// Whether an integer op that runs through fp32 can round-trip this input type + /// back to a MIL-representable integer (int8/uint8/int32 natively, or int32 for + /// the wide-int proxies). + fn is_castable_int(data_type: &DataType) -> bool { + matches!( + data_type, + DataType::Int8 | DataType::Uint8 | DataType::Int32 + ) || Self::is_wide_int(data_type) + } + + /// The MIL cast dtype string used to convert an fp32 intermediate back to an + /// operand's internal integer representation (int32 for the wide-int proxies). + fn int_back_cast_dtype(data_type: &DataType) -> Result<&'static str, GraphError> { + if Self::is_wide_int(data_type) { + Ok("int32") + } else { + Self::cast_dtype_string_for_graph_type(data_type) + } + } + /// Convert WebNN DataType to MIL DataType fn mil_data_type(data_type: &DataType) -> Result { use crate::protos::coreml::mil_spec::DataType as MilDataType; @@ -421,7 +528,8 @@ impl CoremlMlProgramConverter { use crate::protos::coreml::mil_spec::{TensorValue, Value, tensor_value, value}; let name = operand_name(graph, operand_id); - let dtype = Self::mil_data_type(&operand.descriptor.data_type)?; + // int64/uint32/uint64 constants have no MIL tensor type; emit them as int32. + let dtype = Self::graph_value_mil_type(&operand.descriptor.data_type)?; // Keep WebNN scalar constants at rank 0. Promoting them to [1] breaks // MIL ops such as `quantize` that distinguish scalars from vectors. let output_type = @@ -482,10 +590,9 @@ impl CoremlMlProgramConverter { if !is_scalar { // Non-scalar Float16: add to weight file and return BlobFileValue - let element_count = constant_data.data.len() / 2; // 2 bytes per f16 let offset = weight_builder.add_weight( operand_id, - element_count, + super::weight_file_builder::blob_data_type::FLOAT16, &constant_data.data, )?; @@ -524,14 +631,77 @@ impl CoremlMlProgramConverter { values: constant_data.data.clone().into(), })), }, - _ => { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "Unsupported constant data type: {:?}", - operand.descriptor.data_type - ), - }); + crate::graph::DataType::Int64 => { + // int64 has no MIL tensor type; emit as int32 values (narrowing). + let values: Vec = constant_data + .data + .chunks_exact(8) + .map(|chunk| i64::from_le_bytes(chunk.try_into().unwrap()) as i32) + .collect(); + TensorValue { + value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { + values, + })), + } + } + crate::graph::DataType::Uint32 => { + // uint32 has no MIL tensor type; emit as int32 (bit-preserving). + let values: Vec = constant_data + .data + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()) as i32) + .collect(); + TensorValue { + value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { + values, + })), + } + } + crate::graph::DataType::Uint64 => { + // uint64 has no MIL tensor type; emit as int32 (narrowing). + let values: Vec = constant_data + .data + .chunks_exact(8) + .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()) as i32) + .collect(); + TensorValue { + value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { + values, + })), + } + } + crate::graph::DataType::Int4 => { + // int4 is packed two values per byte; unpack (sign-extended) to int32. + let count: usize = operand + .descriptor + .static_or_max_shape() + .iter() + .map(|&d| d as usize) + .product(); + let values = crate::graph::unpack_int4(&constant_data.data, count); + TensorValue { + value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { + values, + })), + } + } + crate::graph::DataType::Uint4 => { + // uint4 is packed two values per byte; unpack to int32. + let count: usize = operand + .descriptor + .static_or_max_shape() + .iter() + .map(|&d| d as usize) + .product(); + let values: Vec = crate::graph::unpack_uint4(&constant_data.data, count) + .into_iter() + .map(|v| v as i32) + .collect(); + TensorValue { + value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { + values, + })), + } } }; @@ -961,6 +1131,21 @@ impl CoremlMlProgramConverter { let mil_op_type = self.get_mil_op_type(op.op_type())?; + // slice with non-unit strides must use slice_by_index instead of slice_by_size. + let mil_op_type = if let Operation::Slice { options, .. } = op { + let strides = options + .as_ref() + .map(|o| o.strides.as_slice()) + .unwrap_or(&[]); + if strides.iter().any(|&s| s != 1) && !strides.is_empty() { + mil_ops::SLICE_BY_INDEX + } else { + mil_op_type + } + } else { + mil_op_type + }; + // Get input operand names, using overrides if available let input_names = Self::input_names_for_operation(graph, op, operand_name_overrides); @@ -1145,6 +1330,7 @@ impl CoremlMlProgramConverter { "convtranspose2d" => mil_ops::CONV_TRANSPOSE, "averagepool2d" => mil_ops::AVG_POOL, "maxpool2d" => mil_ops::MAX_POOL, + "l2pool2d" => mil_ops::L2_POOL, "globalaveragepool" => mil_ops::GLOBAL_AVG_POOL, "globalmaxpool" => mil_ops::GLOBAL_MAX_POOL, @@ -1246,6 +1432,13 @@ impl CoremlMlProgramConverter { // Clamp operation "clamp" => mil_ops::CLIP, + // NaN and infinity detection + "isnan" => mil_ops::IS_NAN, + "isinfinite" => mil_ops::IS_INF, + + // Gather N-dimensional + "gathernd" => mil_ops::GATHER_ND, + _ => { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), @@ -1257,1731 +1450,5308 @@ impl CoremlMlProgramConverter { Ok(mil_type) } - /// Create inputs map for MIL operation - fn create_operation_inputs( - &self, + /// Compute the effective (possibly asymmetric) end-padding for a 2D pooling op. + /// + /// WebNN's `outputShapeRounding="ceil"` and explicit `outputSizes` both grow the + /// output beyond what floor rounding of the base padding yields. CoreML forbids + /// asymmetric padding under `ceil_mode` and does not take `outputSizes`, so instead + /// we fold the extra size into additional end-padding and pool with plain floor + /// rounding: for a target output `out`, the input must span `(out-1)*stride + kernel` + /// after padding, so `end_pad += needed - (input + begin_pad + end_pad)`. + /// + /// `kernel`/`strides` are `[H, W]`; `base_pad` is WebNN `[Hbegin, Hend, Wbegin, Wend]`. + /// Returns the adjusted `[Hbegin, Hend', Wbegin, Wend']`, or `None` if shapes are + /// unavailable (caller falls back to the base padding). + fn pool_effective_padding( graph: &GraphInfo, op: &Operation, - input_names: &[String], - ) -> Result, GraphError> { - let mut inputs = HashMap::new(); + kernel: &[u32], + strides: &[u32], + base_pad: &[u32], + is_nhwc: bool, + ) -> Option> { + if kernel.len() < 2 || strides.len() < 2 || base_pad.len() < 4 { + return None; + } + let input_shape = op + .input_operands() + .first() + .and_then(|&id| graph.operand(id)) + .map(|o| o.descriptor.static_or_max_shape())?; + let output_shape = op + .output_operand() + .and_then(|id| graph.operand(id)) + .map(|o| o.descriptor.static_or_max_shape())?; + if input_shape.len() < 4 || output_shape.len() < 4 { + return None; + } + // Spatial dimension indices for [H, W] under each layout. + let (h_idx, w_idx) = if is_nhwc { (1, 2) } else { (2, 3) }; + let mut pad = base_pad.to_vec(); + for (axis, &dim_idx) in [h_idx, w_idx].iter().enumerate() { + let input_dim = input_shape[dim_idx]; + let output_dim = output_shape[dim_idx]; + let k = kernel[axis]; + let s = strides[axis].max(1); + let begin = base_pad[axis * 2]; + let end = base_pad[axis * 2 + 1]; + let needed = output_dim.saturating_sub(1) * s + k; + let current = input_dim + begin + end; + let extra = needed.saturating_sub(current); + pad[axis * 2 + 1] = end + extra; + } + Some(pad) + } - match &op { - // Binary operations: x, y - Operation::Add { .. } - | Operation::Sub { .. } - | Operation::Mul { .. } - | Operation::Div { .. } - | Operation::Pow { .. } - | Operation::Max { .. } - | Operation::Min { .. } - | Operation::Equal { .. } - | Operation::Greater { .. } - | Operation::GreaterOrEqual { .. } - | Operation::Lesser { .. } - | Operation::LesserOrEqual { .. } - | Operation::LogicalAnd { .. } - | Operation::LogicalOr { .. } - | Operation::LogicalXor { .. } - if input_names.len() >= 2 => { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("y".to_string(), Self::create_argument(&input_names[1])); - } + /// Whether a quantize/dequantize with this quantized integer type and scale shape + /// can use CoreML's native `quantize`/`dequantize`. CoreML supports only int8/uint8 + /// quantized tensors with a scalar (per-tensor) or single-axis 1-D (per-channel) + /// scale; int32 tensors, block-wise scales, and multi-axis scales are not supported + /// and must be decomposed into elementwise arithmetic. + fn qdq_native_supported( + quant_dtype: &DataType, + input_shape: &[u32], + scale_shape: &[u32], + ) -> bool { + if !matches!(quant_dtype, DataType::Int8 | DataType::Uint8) { + return false; + } + let squeezed: Vec = scale_shape.iter().copied().filter(|&d| d != 1).collect(); + // per-tensor (scalar), or per-channel (one non-unit dim equal to some input dim). + squeezed.is_empty() || (squeezed.len() == 1 && input_shape.contains(&squeezed[0])) + } - // MatMul operation: x, y, transpose_x, transpose_y - // CoreML requires transpose parameters, WebNN doesn't have them so default to false - Operation::Matmul { .. } => { - if input_names.len() >= 2 { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("y".to_string(), Self::create_argument(&input_names[1])); - } + /// Whether a quantize/dequantize op must be lowered to elementwise arithmetic because + /// CoreML's native op can't express it. int4/uint4 tensors are excluded (they cannot + /// be materialized at all) and scalar tensors keep the native rank-0 fast path. + fn qdq_should_decompose(graph: &GraphInfo, op: &Operation) -> bool { + let (quant_id, tensor_shape_id, scale_id) = match op { + // dequantize: the quantized tensor is the input; its type/shape drive the check. + Operation::DequantizeLinear { input, scale, .. } => (*input, *input, *scale), + // quantize: the quantized tensor is the output; the float input carries the shape. + Operation::QuantizeLinear { input, scale, .. } => match op.output_operand() { + Some(out) => (out, *input, *scale), + None => return false, + }, + _ => return false, + }; + let (Some(quant_op), Some(tensor_op), Some(scale_op)) = ( + graph.operand(quant_id), + graph.operand(tensor_shape_id), + graph.operand(scale_id), + ) else { + return false; + }; + let quant_dt = quant_op.descriptor.data_type.clone(); + if tensor_op.descriptor.shape.is_empty() { + // Scalars normally use the native rank-0 fast path, but int4/uint4 have no + // native quantize/dequantize at all, so decompose them even when scalar. + return matches!(quant_dt, DataType::Int4 | DataType::Uint4); + } + let tensor_shape = tensor_op.descriptor.static_or_max_shape(); + let scale_shape = scale_op.descriptor.static_or_max_shape(); + !Self::qdq_native_supported(&quant_dt, &tensor_shape, &scale_shape) + } - // Add transpose_x parameter (required by CoreML, defaults to false) - inputs.insert( - "transpose_x".to_string(), - Self::create_immediate_bool(false), - ); + /// Build a value type for a concrete static `shape` with the given MIL dtype. + fn value_type_for_static_shape(name: String, dtype: i32, shape: &[u32]) -> NamedValueType { + let dims: Vec = shape.iter().map(|&d| GraphDimension::Static(d)).collect(); + Self::create_named_value_type(name, dtype, &dims, false) + } - // Add transpose_y parameter (required by CoreML, defaults to false) - inputs.insert( - "transpose_y".to_string(), - Self::create_immediate_bool(false), - ); + /// Map a WebNN recurrent-network activation name to its MIL op. + fn rnn_activation_op(name: &str) -> Result<&'static str, GraphError> { + Ok(match name.to_lowercase().as_str() { + "relu" => mil_ops::RELU, + "tanh" => mil_ops::TANH, + "sigmoid" => mil_ops::SIGMOID, + other => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("unsupported RNN activation '{}'", other), + }); } + }) + } - // Gemm operation: General Matrix Multiplication - // Y = alpha * op(A) * op(B) + beta * C - // CoreML matmul handles: Y = A * B (with transpose options) - // For now, we support transpose options and basic matmul - // TODO: Support alpha, beta, and bias (C) by decomposing into mul and add operations - Operation::Gemm { options, .. } => { - if input_names.len() >= 2 { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("y".to_string(), Self::create_argument(&input_names[1])); - } + /// Emit `out = x . y^T` (MIL matmul with transpose_y). Returns `out_name`. + fn rnn_matmul_ty( + block: &mut Block, + x: &str, + y: &str, + out_name: String, + dtype: i32, + out_shape: &[u32], + ) -> String { + let mut inputs = HashMap::new(); + inputs.insert("x".to_string(), Self::create_name_argument(x.to_string())); + inputs.insert("y".to_string(), Self::create_name_argument(y.to_string())); + inputs.insert( + "transpose_x".to_string(), + Self::create_immediate_bool(false), + ); + inputs.insert("transpose_y".to_string(), Self::create_immediate_bool(true)); + let ty = Self::value_type_for_static_shape(out_name.clone(), dtype, out_shape); + block.operations.push(Self::create_mil_operation( + mil_ops::MATMUL, + inputs, + vec![ty], + )); + out_name + } - // Add transpose parameters from operator options - if let Some(opts) = options { - inputs.insert( - "transpose_x".to_string(), - Self::create_immediate_bool(opts.a_transpose), - ); - inputs.insert( - "transpose_y".to_string(), - Self::create_immediate_bool(opts.b_transpose), - ); - } + /// Emit an elementwise binary op `out = f(x, y)`. Returns `out_name`. + fn rnn_binary( + block: &mut Block, + mil_op: &str, + x: &str, + y: &str, + out_name: String, + dtype: i32, + shape: &[u32], + ) -> String { + let mut inputs = HashMap::new(); + inputs.insert("x".to_string(), Self::create_name_argument(x.to_string())); + inputs.insert("y".to_string(), Self::create_name_argument(y.to_string())); + let ty = Self::value_type_for_static_shape(out_name.clone(), dtype, shape); + block + .operations + .push(Self::create_mil_operation(mil_op, inputs, vec![ty])); + out_name + } - // Note: alpha, beta, and bias (C) are not yet supported - // These would require decomposing gemm into multiple operations: - // 1. matmul(op(A), op(B)) - // 2. mul by alpha if != 1.0 - // 3. add beta * C if C is provided - } + /// Emit an elementwise unary op `out = f(x)`. Returns `out_name`. + fn rnn_unary( + block: &mut Block, + mil_op: &str, + x: &str, + out_name: String, + dtype: i32, + shape: &[u32], + ) -> String { + let mut inputs = HashMap::new(); + inputs.insert("x".to_string(), Self::create_name_argument(x.to_string())); + let ty = Self::value_type_for_static_shape(out_name.clone(), dtype, shape); + block + .operations + .push(Self::create_mil_operation(mil_op, inputs, vec![ty])); + out_name + } - // Global pooling operations (reduce over spatial dimensions) - Operation::GlobalAveragePool { .. } | Operation::GlobalMaxPool { .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - // Global pooling reduces over spatial dimensions (2, 3) for NCHW format - inputs.insert( - "axes".to_string(), - Self::create_immediate_int_array(&[2, 3]), - ); - // Keep dimensions to maintain rank - inputs.insert("keep_dims".to_string(), Self::create_immediate_bool(true)); - } + /// Emit `slice_by_size(x, begin, size)`. Returns `out_name`. + fn rnn_slice( + block: &mut Block, + x: &str, + begin: &[u32], + size: &[u32], + out_name: String, + dtype: i32, + ) -> String { + let mut inputs = HashMap::new(); + inputs.insert("x".to_string(), Self::create_name_argument(x.to_string())); + inputs.insert("begin".to_string(), Self::create_immediate_int_array(begin)); + inputs.insert("size".to_string(), Self::create_immediate_int_array(size)); + let ty = Self::value_type_for_static_shape(out_name.clone(), dtype, size); + block + .operations + .push(Self::create_mil_operation(mil_ops::SLICE, inputs, vec![ty])); + out_name + } - // Softmax operation (axis is required by WebNN spec) - Operation::Softmax { axis, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - inputs.insert("axis".to_string(), Self::create_immediate_int(*axis)); - } + /// Emit an int32 constant tensor with the given shape (scalar when `shape` is empty). + /// Returns `out_name`. + fn emit_int32_const( + block: &mut Block, + values: &[i32], + shape: &[u32], + out_name: String, + ) -> String { + use crate::protos::coreml::mil_spec::{TensorValue, Value, tensor_value, value}; + let int32 = crate::protos::coreml::mil_spec::DataType::Int32 as i32; + let ct = Self::value_type_for_static_shape(out_name.clone(), int32, shape); + let tv = TensorValue { + value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { + values: values.to_vec(), + })), + }; + let imm = Value { + doc_string: String::new(), + r#type: ct.r#type.clone(), + value: Some(value::Value::ImmediateValue(value::ImmediateValue { + value: Some(value::immediate_value::Value::Tensor(tv)), + })), + }; + let mut attrs = HashMap::new(); + attrs.insert("val".to_string(), imm); + block.operations.push(MilOperation { + r#type: "const".to_string(), + inputs: HashMap::new(), + outputs: vec![ct], + attributes: attrs, + ..Default::default() + }); + out_name + } - // Neg operation: implemented as mul by -1, requires x and y parameters - // CoreML neg is actually a mul operation, so we need both operands - Operation::Neg { .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - // Add -1.0 as the multiplier (y parameter required by CoreML mul) - inputs.insert("y".to_string(), Self::create_immediate_float(-1.0)); - } + /// Normalize gather-style indices to WebNN semantics: wrap negatives (`idx + size`) + /// then clamp out-of-bounds to `[0, size-1]`. `sizes` is the axis size per index + /// component (length 1 for gather/gatherElements, or `K` for gatherND's last axis), + /// broadcast against `idx_shape`. `idx_name` must already be int32. + fn emit_gather_index_norm( + block: &mut Block, + idx_name: &str, + idx_shape: &[u32], + sizes: &[u32], + prefix: &str, + ) -> String { + let int32 = crate::protos::coreml::mil_spec::DataType::Int32 as i32; + let bool_t = crate::protos::coreml::mil_spec::DataType::Bool as i32; + let p = |s: &str| format!("{prefix}_{s}"); + let sizes_i32: Vec = sizes.iter().map(|&s| s as i32).collect(); + let sizes_m1: Vec = sizes.iter().map(|&s| s as i32 - 1).collect(); + // Single-axis sizes are scalars (broadcast against any index shape); gatherND's + // per-component sizes are a rank-1 [K] vector broadcast over the last index axis. + let cshape: &[u32] = if sizes.len() == 1 { + &[] + } else { + &[sizes.len() as u32] + }; + let size_c = Self::emit_int32_const(block, &sizes_i32, cshape, p("gsz")); + let sizem1_c = Self::emit_int32_const(block, &sizes_m1, cshape, p("gszm1")); + let zero_c = Self::emit_int32_const(block, &[0], &[], p("gz")); + let is_neg = Self::rnn_binary( + block, + mil_ops::LESS, + idx_name, + &zero_c, + p("gneg"), + bool_t, + idx_shape, + ); + let is_neg_i = Self::rnn_unary_cast(block, &is_neg, p("gnegi"), int32, idx_shape); + let offset = Self::rnn_binary( + block, + mil_ops::MUL, + &is_neg_i, + &size_c, + p("goff"), + int32, + idx_shape, + ); + let wrapped = Self::rnn_binary( + block, + mil_ops::ADD, + idx_name, + &offset, + p("gwrap"), + int32, + idx_shape, + ); + let mx = Self::rnn_binary( + block, + mil_ops::MAXIMUM, + &wrapped, + &zero_c, + p("gmx"), + int32, + idx_shape, + ); + Self::rnn_binary( + block, + mil_ops::MINIMUM, + &mx, + &sizem1_c, + p("gcl"), + int32, + idx_shape, + ) + } - // Unary operations: x - Operation::Relu { .. } - | Operation::Sigmoid { .. } - | Operation::Tanh { .. } - | Operation::Abs { .. } - | Operation::Ceil { .. } - | Operation::Floor { .. } - | Operation::RoundEven { .. } - | Operation::Sign { .. } - | Operation::Identity { .. } - | Operation::Exp { .. } - | Operation::Sqrt { .. } - | Operation::Sin { .. } - | Operation::Cos { .. } - | Operation::Tan { .. } - | Operation::Erf { .. } - | Operation::LogicalNot { .. } - | Operation::Softplus { .. } - | Operation::Softsign { .. } - if !input_names.is_empty() => { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + /// Emit a `cast(x, dtype)` producing `out_name` with the given shape/dtype. + fn rnn_unary_cast( + block: &mut Block, + x: &str, + out_name: String, + dtype: i32, + shape: &[u32], + ) -> String { + let out_type = Self::value_type_for_static_shape(out_name.clone(), dtype, shape); + let dtype_str = Self::cast_dtype_string_for_mil_type(dtype).unwrap_or("int32"); + block.operations.push(Self::create_cast_operation( + x.to_string(), + out_type, + dtype_str, + )); + out_name + } - Operation::Reciprocal { .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - // Reciprocal requires epsilon parameter (default to 1e-45 for numerical stability) - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(DEFAULT_EPSILON), - ); - } + /// Emit `reshape(x, shape)`. Returns `out_name`. + fn rnn_reshape( + block: &mut Block, + x: &str, + shape: &[u32], + out_name: String, + dtype: i32, + ) -> String { + let mut inputs = HashMap::new(); + inputs.insert("x".to_string(), Self::create_name_argument(x.to_string())); + inputs.insert("shape".to_string(), Self::create_immediate_int_array(shape)); + let ty = Self::value_type_for_static_shape(out_name.clone(), dtype, shape); + block + .operations + .push(Self::create_mil_operation("reshape", inputs, vec![ty])); + out_name + } - // Log operation requires epsilon parameter - Operation::Log { .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - // CoreML log requires epsilon parameter (default to 1e-45 for numerical stability) - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(DEFAULT_EPSILON), - ); - } + /// Emit `concat(values, axis)`. Returns `out_name`. + fn rnn_concat( + block: &mut Block, + names: &[String], + axis: u32, + out_name: String, + dtype: i32, + out_shape: &[u32], + ) -> String { + let mut inputs = HashMap::new(); + inputs.insert("values".to_string(), Self::create_argument_tuple(names)); + inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); + inputs.insert("interleave".to_string(), Self::create_immediate_bool(false)); + let ty = Self::value_type_for_static_shape(out_name.clone(), dtype, out_shape); + block.operations.push(Self::create_mil_operation( + mil_ops::CONCAT, + inputs, + vec![ty], + )); + out_name + } - // Quantization operations: input, scale, zero_point - Operation::DequantizeLinear { .. } | Operation::QuantizeLinear { .. } - if input_names.len() >= 3 => { - inputs.insert("input".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("scale".to_string(), Self::create_argument(&input_names[1])); - inputs.insert( - "zero_point".to_string(), - Self::create_argument(&input_names[2]), - ); - } + /// Emit a constant zero tensor of the given shape/dtype. Returns `out_name`. + fn rnn_zeros(block: &mut Block, shape: &[u32], out_name: String, dtype: i32) -> String { + use crate::protos::coreml::mil_spec::{TensorValue, Value, tensor_value, value}; + let count: usize = shape.iter().map(|&d| d as usize).product(); + let f32_dtype = crate::protos::coreml::mil_spec::DataType::Float32 as i32; + let f32_name = if dtype == f32_dtype { + out_name.clone() + } else { + format!("{}_zf32", out_name) + }; + let const_type = Self::value_type_for_static_shape(f32_name.clone(), f32_dtype, shape); + let tensor_value = TensorValue { + value: Some(tensor_value::Value::Floats(tensor_value::RepeatedFloats { + values: vec![0.0f32; count], + })), + }; + let immediate = Value { + doc_string: String::new(), + r#type: const_type.r#type.clone(), + value: Some(value::Value::ImmediateValue(value::ImmediateValue { + value: Some(value::immediate_value::Value::Tensor(tensor_value)), + })), + }; + let mut attributes = HashMap::new(); + attributes.insert("val".to_string(), immediate); + block.operations.push(MilOperation { + r#type: "const".to_string(), + inputs: HashMap::new(), + outputs: vec![const_type], + attributes, + ..Default::default() + }); + if dtype == f32_dtype { + return out_name; + } + let casted = Self::value_type_for_static_shape(out_name.clone(), dtype, shape); + block.operations.push(Self::create_cast_operation( + f32_name, + casted, + Self::cast_dtype_string_for_mil_type(dtype).unwrap_or("fp16"), + )); + out_name + } - // Specialized activation: prelu - x, slope (two inputs) - Operation::Prelu { .. } - if input_names.len() >= 2 => { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("alpha".to_string(), Self::create_argument(&input_names[1])); - } + /// Emit one GRU time step and return the new hidden-state tensor name (shape [b, h]). + /// Same gate math as `emit_gru_cell_decomposition`, parameterized on tensor names so + /// the sequence `gru` can unroll it over time steps and directions. + #[allow(clippy::too_many_arguments)] + fn emit_gru_step( + block: &mut Block, + x_name: &str, + w_name: &str, + r_name: &str, + bias_name: Option<&str>, + rbias_name: Option<&str>, + hid_name: &str, + b: u32, + i: u32, + h: u32, + layout: &str, + act0: &str, + act1: &str, + reset_after: bool, + prefix: &str, + dtype: i32, + ) -> String { + let bh = [b, h]; + let (z_off, r_off, n_off) = match layout { + "rzn" => (h, 0, 2 * h), + _ => (0, h, 2 * h), + }; + let p = |s: &str| format!("{}_{}", prefix, s); + let gate = |block: &mut Block, off: u32, act: &str, tag: &str| -> String { + let wg = Self::rnn_slice( + block, + w_name, + &[off, 0], + &[h, i], + p(&format!("w{tag}")), + dtype, + ); + let mut xw = + Self::rnn_matmul_ty(block, x_name, &wg, p(&format!("xw{tag}")), dtype, &bh); + if let Some(bn) = bias_name { + let bg = Self::rnn_slice(block, bn, &[off], &[h], p(&format!("b{tag}")), dtype); + xw = Self::rnn_binary(block, "add", &xw, &bg, p(&format!("xwb{tag}")), dtype, &bh); + } + let rg = Self::rnn_slice( + block, + r_name, + &[off, 0], + &[h, h], + p(&format!("r{tag}")), + dtype, + ); + let mut hr = + Self::rnn_matmul_ty(block, hid_name, &rg, p(&format!("hr{tag}")), dtype, &bh); + if let Some(rbn) = rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[off], &[h], p(&format!("rb{tag}")), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p(&format!("hrb{tag}")), dtype, &bh); + } + let pre = Self::rnn_binary(block, "add", &xw, &hr, p(&format!("pre{tag}")), dtype, &bh); + Self::rnn_unary(block, act, &pre, p(&format!("g{tag}")), dtype, &bh) + }; + let z = gate(block, z_off, act0, "z"); + let r = gate(block, r_off, act0, "r"); + + let wn = Self::rnn_slice(block, w_name, &[n_off, 0], &[h, i], p("wn"), dtype); + let mut xwn = Self::rnn_matmul_ty(block, x_name, &wn, p("xwn"), dtype, &bh); + if let Some(bn) = bias_name { + let bg = Self::rnn_slice(block, bn, &[n_off], &[h], p("bn"), dtype); + xwn = Self::rnn_binary(block, "add", &xwn, &bg, p("xwbn"), dtype, &bh); + } + let rn = Self::rnn_slice(block, r_name, &[n_off, 0], &[h, h], p("rn"), dtype); + let hrn = if reset_after { + let mut hr = Self::rnn_matmul_ty(block, hid_name, &rn, p("hrn"), dtype, &bh); + if let Some(rbn) = rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[n_off], &[h], p("rbn"), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p("hrbn"), dtype, &bh); + } + Self::rnn_binary(block, "mul", &r, &hr, p("rhn"), dtype, &bh) + } else { + let rh = Self::rnn_binary(block, "mul", &r, hid_name, p("rh"), dtype, &bh); + let mut hr = Self::rnn_matmul_ty(block, &rh, &rn, p("hrn"), dtype, &bh); + if let Some(rbn) = rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[n_off], &[h], p("rbn"), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p("hrbn"), dtype, &bh); + } + hr + }; + let npre = Self::rnn_binary(block, "add", &xwn, &hrn, p("npre"), dtype, &bh); + let n = Self::rnn_unary(block, act1, &npre, p("n"), dtype, &bh); + let hsubn = Self::rnn_binary(block, "sub", hid_name, &n, p("hsubn"), dtype, &bh); + let zmul = Self::rnn_binary(block, "mul", &z, &hsubn, p("zmul"), dtype, &bh); + Self::rnn_binary(block, "add", &n, &zmul, p("h"), dtype, &bh) + } - // Specialized activations with alpha parameter: elu, leakyRelu - Operation::Elu { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - let alpha = options.as_ref().map(|o| o.alpha as f32).unwrap_or(1.0); - inputs.insert("alpha".to_string(), Self::create_immediate_float(alpha)); + /// Emit one LSTM time step; returns the new (hidden, cell) tensor names (shape [b, h]). + #[allow(clippy::too_many_arguments)] + fn emit_lstm_step( + block: &mut Block, + x_name: &str, + w_name: &str, + r_name: &str, + bias_name: Option<&str>, + rbias_name: Option<&str>, + peephole_name: Option<&str>, + hid_name: &str, + cell_name: &str, + b: u32, + i: u32, + h: u32, + layout: &str, + f0: &str, + f1: &str, + f2: &str, + prefix: &str, + dtype: i32, + ) -> (String, String) { + let bh = [b, h]; + let (i_off, o_off, f_off, g_off) = match layout { + "ifgo" => (0, 3 * h, h, 2 * h), + _ => (0, h, 2 * h, 3 * h), + }; + let (pi_off, po_off, pf_off) = (0u32, h, 2 * h); + let p = |s: &str| format!("{}_{}", prefix, s); + let gate = |block: &mut Block, + off: u32, + act: &str, + tag: &str, + peep: Option<(u32, &str)>| + -> String { + let wg = Self::rnn_slice( + block, + w_name, + &[off, 0], + &[h, i], + p(&format!("w{tag}")), + dtype, + ); + let mut xw = + Self::rnn_matmul_ty(block, x_name, &wg, p(&format!("xw{tag}")), dtype, &bh); + if let Some(bn) = bias_name { + let bg = Self::rnn_slice(block, bn, &[off], &[h], p(&format!("b{tag}")), dtype); + xw = Self::rnn_binary(block, "add", &xw, &bg, p(&format!("xwb{tag}")), dtype, &bh); } - Operation::LeakyRelu { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - let alpha = options.as_ref().map(|o| o.alpha as f32).unwrap_or(0.01); - inputs.insert("alpha".to_string(), Self::create_immediate_float(alpha)); + let rg = Self::rnn_slice( + block, + r_name, + &[off, 0], + &[h, h], + p(&format!("r{tag}")), + dtype, + ); + let mut hr = + Self::rnn_matmul_ty(block, hid_name, &rg, p(&format!("hr{tag}")), dtype, &bh); + if let Some(rbn) = rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[off], &[h], p(&format!("rb{tag}")), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p(&format!("hrb{tag}")), dtype, &bh); + } + let mut pre = + Self::rnn_binary(block, "add", &xw, &hr, p(&format!("pre{tag}")), dtype, &bh); + if let (Some((poff, cname)), Some(pw)) = (peep, peephole_name) { + let pg = Self::rnn_slice(block, pw, &[poff], &[h], p(&format!("p{tag}")), dtype); + let pc = + Self::rnn_binary(block, "mul", &pg, cname, p(&format!("pc{tag}")), dtype, &bh); + pre = Self::rnn_binary( + block, + "add", + &pre, + &pc, + p(&format!("pre2{tag}")), + dtype, + &bh, + ); } + Self::rnn_unary(block, act, &pre, p(&format!("g{tag}")), dtype, &bh) + }; + let gi = gate(block, i_off, f0, "i", Some((pi_off, cell_name))); + let gf = gate(block, f_off, f0, "f", Some((pf_off, cell_name))); + let gg = gate(block, g_off, f1, "g", None); + let fc = Self::rnn_binary(block, "mul", &gf, cell_name, p("fc"), dtype, &bh); + let ig = Self::rnn_binary(block, "mul", &gi, &gg, p("ig"), dtype, &bh); + let cnew = Self::rnn_binary(block, "add", &fc, &ig, p("c"), dtype, &bh); + let go = gate(block, o_off, f0, "o", Some((po_off, &cnew))); + let tanh_c = Self::rnn_unary(block, f2, &cnew, p("tanhc"), dtype, &bh); + let hnew = Self::rnn_binary(block, "mul", &go, &tanh_c, p("h"), dtype, &bh); + (hnew, cnew) + } - // HardSwish: decomposed in main loop (hardsigmoid + mul) - // This case should never be reached due to continue in main loop - Operation::HardSwish { .. } => { + /// Compute reshape targets that make a block-wise quantization scale broadcast against + /// the tensor. Each axis `i` where `1 < scale[i] < input[i]` is split into + /// `[scale[i], input[i]/scale[i]]` for the tensor and `[scale[i], 1]` for the scale; + /// per-tensor/per-element axes keep their single dim (ordinary broadcasting applies). + /// Splitting only genuine block axes keeps the rank low (CoreML reshape allows rank <= 5). + /// Returns `(interleaved_input_shape, interleaved_scale_shape)`. + fn qdq_interleave_shapes( + input_shape: &[u32], + scale_shape: &[u32], + ) -> Result<(Vec, Vec), GraphError> { + let rank = input_shape.len(); + // WebNN block dims right-align with the input. + let mut aligned = vec![1u32; rank]; + if scale_shape.len() <= rank { + let off = rank - scale_shape.len(); + for (i, &d) in scale_shape.iter().enumerate() { + aligned[off + i] = d; + } + } + let mut interleaved_input: Vec = Vec::with_capacity(rank * 2); + let mut interleaved_scale: Vec = Vec::with_capacity(rank * 2); + for i in 0..rank { + let nb = aligned[i].max(1); + if input_shape[i] % nb != 0 { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: "hardswish should be decomposed in main loop, not here".to_string(), + reason: format!( + "quantize/dequantize: input dim {} not divisible by scale dim {}", + input_shape[i], nb + ), }); } - - // HardSigmoid: x, alpha, beta parameters - Operation::HardSigmoid { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - if let Some(opts) = options { - inputs.insert( - "alpha".to_string(), - Self::create_immediate_float(opts.alpha as f32), - ); - inputs.insert( - "beta".to_string(), - Self::create_immediate_float(opts.beta as f32), - ); - } + let block = input_shape[i] / nb; + if nb > 1 && block > 1 { + interleaved_input.push(nb); + interleaved_input.push(block); + interleaved_scale.push(nb); + interleaved_scale.push(1); + } else { + interleaved_input.push(input_shape[i]); + interleaved_scale.push(nb); } + } + Ok((interleaved_input, interleaved_scale)) + } - // Clamp operation: x, alpha (min), beta (max) - Operation::Clamp { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - // CoreML clip operation requires BOTH alpha and beta parameters - // WebNN clamp defaults: minValue=-Infinity, maxValue=+Infinity - let (min_value, max_value) = options - .as_ref() - .map(|o| { - let min = - Self::parse_clamp_bound(o.min_value.as_ref(), f64::NEG_INFINITY) as f32; - let max = - Self::parse_clamp_bound(o.max_value.as_ref(), f64::INFINITY) as f32; - (min, max) - }) - .unwrap_or((f32::NEG_INFINITY, f32::INFINITY)); - - // Alpha and beta must match input type (CoreML requirement) - // Check first input operand type and use appropriate immediate value method - let use_float16 = if !op.input_operands().is_empty() { - if let Some(input_operand) = graph.operand(op.input_operands()[0]) { - input_operand.descriptor.data_type == DataType::Float16 - } else { - false - } - } else { - false - }; - - if use_float16 { - inputs.insert( - "alpha".to_string(), - Self::create_immediate_float16(min_value), - ); - inputs.insert( - "beta".to_string(), - Self::create_immediate_float16(max_value), - ); - } else { - inputs.insert("alpha".to_string(), Self::create_immediate_float(min_value)); - inputs.insert("beta".to_string(), Self::create_immediate_float(max_value)); - } + /// Lower `dequantizeLinear` as `(input - zeroPoint) * scale` in elementwise form. + /// + /// Handles quantized types and scale shapes CoreML's native `dequantize` cannot: + /// int32 tensors, block-wise scales, and multi-axis scales. Block quantization is + /// expressed by reshaping each axis `i` of length `input[i]` into `[scale[i], + /// block[i]]` (with `block[i] = input[i]/scale[i]`) and the scale/zeroPoint into + /// `[scale[i], 1]`, so ordinary broadcasting applies; the result is reshaped back. + fn emit_dequantize_decomposition( + graph: &GraphInfo, + op: &Operation, + overrides: &HashMap, + main_block: &mut Block, + ) -> Result<(), GraphError> { + let (input_id, scale_id, zp_id) = match op { + Operation::DequantizeLinear { + input, + scale, + zero_point, + .. + } => (*input, *scale, *zero_point), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "emit_dequantize_decomposition called on non-dequantize op".to_string(), + }); } + }; + let output_id = op + .output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "dequantizeLinear has no output operand".to_string(), + })?; + let input_op = graph + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("dequantize input operand {} not found", input_id), + })?; + let scale_op = graph + .operand(scale_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("dequantize scale operand {} not found", scale_id), + })?; + let input_shape: Vec = if input_op.descriptor.shape.is_empty() { + vec![1] + } else { + input_op.descriptor.static_or_max_shape() + }; + let scale_shape = scale_op.descriptor.static_or_max_shape(); - // Transpose operation: x, permutation - Operation::Transpose { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + // Output MIL dtype is the (float) scale/output type. + let out_dtype = Self::mil_data_type( + &graph + .operand(output_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("dequantize output operand {} not found", output_id), + })? + .descriptor + .data_type, + )?; + let float_str = Self::cast_dtype_string_for_mil_type(out_dtype)?; + let (output_name, output_type) = Self::create_output_value(graph, output_id, overrides)?; + + let (interleaved_input, interleaved_scale) = + Self::qdq_interleave_shapes(&input_shape, &scale_shape)?; + + // 1. cast input -> float, reshape to interleaved. + let input_name = Self::output_name_for_operand(graph, input_id, overrides); + let in_f_name = format!("{}_dq_in_f", output_name); + let in_f_type = + Self::value_type_for_static_shape(in_f_name.clone(), out_dtype, &input_shape); + main_block.operations.push(Self::create_cast_operation( + input_name, in_f_type, float_str, + )); + let in_r_name = format!("{}_dq_in_r", output_name); + let in_r_type = + Self::value_type_for_static_shape(in_r_name.clone(), out_dtype, &interleaved_input); + let mut in_reshape = HashMap::new(); + in_reshape.insert("x".to_string(), Self::create_name_argument(in_f_name)); + in_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&interleaved_input), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + in_reshape, + vec![in_r_type], + )); - // Add permutation parameter (required by CoreML) - // If not specified in WebNN, default is to reverse all dimensions - if let Some(opts) = options - && !opts.permutation.is_empty() - { - inputs.insert( - "perm".to_string(), - Self::create_immediate_int_array(&opts.permutation), - ); - } else if !op.input_operands().is_empty() - && let Some(input_operand) = graph.operand(op.input_operands()[0]) - { - let rank = input_operand.descriptor.shape.len(); - let default_perm: Vec = (0..rank).rev().map(|i| i as u32).collect(); - inputs.insert( - "perm".to_string(), - Self::create_immediate_int_array(&default_perm), - ); - } - } + // 2. scale -> reshape to interleaved. + let scale_name = operand_name(graph, scale_id); + let scale_r_name = format!("{}_dq_scale_r", output_name); + let scale_r_type = + Self::value_type_for_static_shape(scale_r_name.clone(), out_dtype, &interleaved_scale); + let mut scale_reshape = HashMap::new(); + scale_reshape.insert("x".to_string(), Self::create_name_argument(scale_name)); + scale_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&interleaved_scale), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + scale_reshape, + vec![scale_r_type], + )); - // Reshape: x, shape - Operation::Reshape { new_shape, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + // 3. (input - zeroPoint), if a zero_point is present. + let minus_zp_name = if let Some(zp_id) = zp_id { + let zp_name = operand_name(graph, zp_id); + let zp_f_name = format!("{}_dq_zp_f", output_name); + let zp_f_type = + Self::value_type_for_static_shape(zp_f_name.clone(), out_dtype, &scale_shape); + main_block + .operations + .push(Self::create_cast_operation(zp_name, zp_f_type, float_str)); + let zp_r_name = format!("{}_dq_zp_r", output_name); + let zp_r_type = + Self::value_type_for_static_shape(zp_r_name.clone(), out_dtype, &interleaved_scale); + let mut zp_reshape = HashMap::new(); + zp_reshape.insert("x".to_string(), Self::create_name_argument(zp_f_name)); + zp_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&interleaved_scale), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + zp_reshape, + vec![zp_r_type], + )); + let sub_name = format!("{}_dq_sub", output_name); + let sub_type = + Self::value_type_for_static_shape(sub_name.clone(), out_dtype, &interleaved_input); + let mut sub_inputs = HashMap::new(); + sub_inputs.insert("x".to_string(), Self::create_name_argument(in_r_name)); + sub_inputs.insert("y".to_string(), Self::create_name_argument(zp_r_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::SUB, + sub_inputs, + vec![sub_type], + )); + sub_name + } else { + in_r_name + }; - // MIL `reshape` declares `shape` as a required input. Always emit - // it, including the valid WebNN scalar-output case (`new_shape = []`) - let shape_values = if new_shape.is_empty() { - Vec::new() - } else { - crate::operator_options::mldimensions_static_or_max(new_shape) - }; - inputs.insert( - "shape".to_string(), - Self::create_immediate_int_array(&shape_values), - ); - } + // 4. multiply by scale, reshape back to the input shape. + let mul_name = format!("{}_dq_mul", output_name); + let mul_type = + Self::value_type_for_static_shape(mul_name.clone(), out_dtype, &interleaved_input); + let mut mul_inputs = HashMap::new(); + mul_inputs.insert("x".to_string(), Self::create_name_argument(minus_zp_name)); + mul_inputs.insert("y".to_string(), Self::create_name_argument(scale_r_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::MUL, + mul_inputs, + vec![mul_type], + )); - // Convolution operations: input, filter + parameters - Operation::Conv2d { options, .. } => { - if input_names.len() >= 2 { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("weight".to_string(), Self::create_argument(&input_names[1])); - } + let mut out_reshape = HashMap::new(); + out_reshape.insert("x".to_string(), Self::create_name_argument(mul_name)); + out_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&input_shape), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + out_reshape, + vec![output_type], + )); + Ok(()) + } - // Add optional bias if present (third input) - if input_names.len() >= 3 { - inputs.insert("bias".to_string(), Self::create_argument(&input_names[2])); + /// Lower `quantizeLinear` as `cast(clamp(round(input / scale) + zeroPoint, qmin, qmax))` + /// in elementwise form. Handles quantized types and scale shapes CoreML's native + /// `quantize` cannot: int32 outputs, block-wise scales, and multi-axis scales. Block + /// quantization uses the same reshape trick as the dequantize decomposition. + fn emit_quantize_decomposition( + graph: &GraphInfo, + op: &Operation, + overrides: &HashMap, + main_block: &mut Block, + ) -> Result<(), GraphError> { + let (input_id, scale_id, zp_id) = match op { + Operation::QuantizeLinear { + input, + scale, + zero_point, + .. + } => (*input, *scale, *zero_point), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "emit_quantize_decomposition called on non-quantize op".to_string(), + }); + } + }; + let output_id = op + .output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "quantizeLinear has no output operand".to_string(), + })?; + let input_op = graph + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("quantize input operand {} not found", input_id), + })?; + let scale_op = graph + .operand(scale_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("quantize scale operand {} not found", scale_id), + })?; + let output_op = graph + .operand(output_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("quantize output operand {} not found", output_id), + })?; + let input_shape: Vec = if input_op.descriptor.shape.is_empty() { + vec![1] + } else { + input_op.descriptor.static_or_max_shape() + }; + let scale_shape = scale_op.descriptor.static_or_max_shape(); + let quant_dt = output_op.descriptor.data_type.clone(); + + // Compute in fp32 regardless of the input's float type: the output is an integer, + // so upcasting is safe and avoids fp16 rounding (e.g. an exact 12347 that fp16 + // can't represent) and clip dtype-mismatch errors. + let float_dtype = crate::protos::coreml::mil_spec::DataType::Float32 as i32; + let (output_name, output_type) = Self::create_output_value(graph, output_id, overrides)?; + // The quantized output is represented in the graph as its native MIL int type + // (int8/uint8) or, for the sub-byte/wide proxies (int4/uint4/...), as int32; the + // executor packs/narrows it to the true width on readback. + let out_int_str = Self::int_back_cast_dtype(&quant_dt)?; + + let (interleaved_input, interleaved_scale) = + Self::qdq_interleave_shapes(&input_shape, &scale_shape)?; + + // 1. cast input -> fp32, reshape to interleaved form. + let input_name = Self::output_name_for_operand(graph, input_id, overrides); + let in_f_name = format!("{}_q_in_f", output_name); + let in_f_type = + Self::value_type_for_static_shape(in_f_name.clone(), float_dtype, &input_shape); + main_block + .operations + .push(Self::create_cast_operation(input_name, in_f_type, "fp32")); + let in_r_name = format!("{}_q_in_r", output_name); + let in_r_type = + Self::value_type_for_static_shape(in_r_name.clone(), float_dtype, &interleaved_input); + let mut in_reshape = HashMap::new(); + in_reshape.insert("x".to_string(), Self::create_name_argument(in_f_name)); + in_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&interleaved_input), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + in_reshape, + vec![in_r_type], + )); + + // 2. cast scale -> fp32, reshape to interleaved form. + let scale_name = operand_name(graph, scale_id); + let scale_f_name = format!("{}_q_scale_f", output_name); + let scale_f_type = + Self::value_type_for_static_shape(scale_f_name.clone(), float_dtype, &scale_shape); + main_block.operations.push(Self::create_cast_operation( + scale_name, + scale_f_type, + "fp32", + )); + let scale_r_name = format!("{}_q_scale_r", output_name); + let scale_r_type = Self::value_type_for_static_shape( + scale_r_name.clone(), + float_dtype, + &interleaved_scale, + ); + let mut scale_reshape = HashMap::new(); + scale_reshape.insert("x".to_string(), Self::create_name_argument(scale_f_name)); + scale_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&interleaved_scale), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + scale_reshape, + vec![scale_r_type], + )); + + // 3. div = input / scale, then round to nearest even. + let div_name = format!("{}_q_div", output_name); + let div_type = + Self::value_type_for_static_shape(div_name.clone(), float_dtype, &interleaved_input); + let mut div_inputs = HashMap::new(); + div_inputs.insert("x".to_string(), Self::create_name_argument(in_r_name)); + div_inputs.insert("y".to_string(), Self::create_name_argument(scale_r_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::DIV, + div_inputs, + vec![div_type], + )); + let round_name = format!("{}_q_round", output_name); + let round_type = + Self::value_type_for_static_shape(round_name.clone(), float_dtype, &interleaved_input); + let mut round_inputs = HashMap::new(); + round_inputs.insert("x".to_string(), Self::create_name_argument(div_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::ROUND_EVEN, + round_inputs, + vec![round_type], + )); + + // 4. add the zero_point (cast to float, reshaped), if present. + let biased_name = if let Some(zp_id) = zp_id { + let zp_name = operand_name(graph, zp_id); + let zp_f_name = format!("{}_q_zp_f", output_name); + let zp_f_type = + Self::value_type_for_static_shape(zp_f_name.clone(), float_dtype, &scale_shape); + main_block.operations.push(Self::create_cast_operation( + zp_name, + zp_f_type, + Self::cast_dtype_string_for_mil_type(float_dtype)?, + )); + let zp_r_name = format!("{}_q_zp_r", output_name); + let zp_r_type = Self::value_type_for_static_shape( + zp_r_name.clone(), + float_dtype, + &interleaved_scale, + ); + let mut zp_reshape = HashMap::new(); + zp_reshape.insert("x".to_string(), Self::create_name_argument(zp_f_name)); + zp_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&interleaved_scale), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + zp_reshape, + vec![zp_r_type], + )); + let add_name = format!("{}_q_add", output_name); + let add_type = Self::value_type_for_static_shape( + add_name.clone(), + float_dtype, + &interleaved_input, + ); + let mut add_inputs = HashMap::new(); + add_inputs.insert("x".to_string(), Self::create_name_argument(round_name)); + add_inputs.insert("y".to_string(), Self::create_name_argument(zp_r_name)); + main_block.operations.push(Self::create_mil_operation( + "add", + add_inputs, + vec![add_type], + )); + add_name + } else { + round_name + }; + + // 5. clamp to the quantized type's range (int32 spans the proxy and needs no clamp). + let clamped_name = match quant_dt { + DataType::Int8 | DataType::Uint8 | DataType::Int4 | DataType::Uint4 => { + let (qmin, qmax) = match quant_dt { + DataType::Uint8 => (0.0f32, 255.0f32), + DataType::Int4 => (-8.0f32, 7.0f32), + DataType::Uint4 => (0.0f32, 15.0f32), + _ => (-128.0f32, 127.0f32), + }; + let clip_name = format!("{}_q_clip", output_name); + let clip_type = Self::value_type_for_static_shape( + clip_name.clone(), + float_dtype, + &interleaved_input, + ); + let mut clip_inputs = HashMap::new(); + clip_inputs.insert("x".to_string(), Self::create_name_argument(biased_name)); + clip_inputs.insert("alpha".to_string(), Self::create_immediate_float(qmin)); + clip_inputs.insert("beta".to_string(), Self::create_immediate_float(qmax)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::CLIP, + clip_inputs, + vec![clip_type], + )); + clip_name + } + _ => biased_name, + }; + + // 6. reshape back to the output shape (still float), then cast to the int type. + let out_f_name = format!("{}_q_out_f", output_name); + let out_f_type = + Self::value_type_for_static_shape(out_f_name.clone(), float_dtype, &input_shape); + let mut out_reshape = HashMap::new(); + out_reshape.insert("x".to_string(), Self::create_name_argument(clamped_name)); + out_reshape.insert( + "shape".to_string(), + Self::create_immediate_int_array(&input_shape), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + out_reshape, + vec![out_f_type], + )); + main_block.operations.push(Self::create_cast_operation( + out_f_name, + output_type, + out_int_str, + )); + Ok(()) + } + + /// Lower a WebNN `gruCell` (single time step) into primitive MIL ops. + /// + /// For gate order z (update), r (reset), n (new) with per-gate weight/recurrence rows: + /// z = f0(X·Wz^T + bz + H·Rz^T + rbz) + /// r = f0(X·Wr^T + br + H·Rr^T + rbr) + /// n = f1(X·Wn^T + bn + (r ⊙ H)·Rn^T + rbn) (reset_after = false) + /// n = f1(X·Wn^T + bn + r ⊙ (H·Rn^T + rbn)) (reset_after = true) + /// Hnew = (1 - z) ⊙ n + z ⊙ H = n + z ⊙ (H - n) + /// Default activations f0=sigmoid, f1=tanh; default layout "zrn". + fn emit_gru_cell_decomposition( + graph: &GraphInfo, + op: &Operation, + overrides: &HashMap, + block: &mut Block, + ) -> Result<(), GraphError> { + let (input_id, weight_id, rec_id, hidden_id, hidden_size, opts) = match op { + Operation::GruCell { + input, + weight, + recurrence, + hidden_state, + hidden_size, + options, + .. + } => ( + *input, + *weight, + *recurrence, + *hidden_state, + *hidden_size, + options.as_ref(), + ), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "emit_gru_cell_decomposition called on non-gruCell op".to_string(), + }); + } + }; + let output_id = op + .output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "gruCell has no output operand".to_string(), + })?; + let input_op = graph + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "gruCell input operand not found".to_string(), + })?; + let dtype = Self::mil_data_type(&input_op.descriptor.data_type)?; + let in_shape = input_op.descriptor.static_or_max_shape(); + let b = in_shape[0]; + let i = in_shape[1]; + let h = hidden_size; + let bh = [b, h]; + + let x_name = Self::output_name_for_operand(graph, input_id, overrides); + let w_name = Self::output_name_for_operand(graph, weight_id, overrides); + let r_name = Self::output_name_for_operand(graph, rec_id, overrides); + let hid_name = Self::output_name_for_operand(graph, hidden_id, overrides); + let bias_name = opts + .and_then(|o| o.bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let rbias_name = opts + .and_then(|o| o.recurrent_bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let reset_after = opts.map(|o| o.reset_after).unwrap_or(false); + let layout = opts + .map(|o| o.layout.as_str()) + .filter(|s| !s.is_empty()) + .unwrap_or("zrn"); + let acts: Vec = opts + .and_then(|o| o.activations.clone()) + .unwrap_or_else(|| vec!["sigmoid".to_string(), "tanh".to_string()]); + let act0 = Self::rnn_activation_op(&acts[0])?; + let act1 = Self::rnn_activation_op(acts.get(1).map(|s| s.as_str()).unwrap_or("tanh"))?; + + // Row offsets of each gate within the 3*hidden weight/recurrence tensors. + let (z_off, r_off, n_off) = match layout { + "rzn" => (h, 0, 2 * h), + _ => (0, h, 2 * h), + }; + + let (output_name, output_type) = Self::create_output_value(graph, output_id, overrides)?; + let p = |s: &str| format!("{}_{}", output_name, s); + + // Compute a "reset/update"-style gate: activation(X·Wg^T + bg + H·Rg^T + rbg). + let gate = |block: &mut Block, off: u32, act: &str, tag: &str| -> String { + let wg = Self::rnn_slice( + block, + &w_name, + &[off, 0], + &[h, i], + p(&format!("w{tag}")), + dtype, + ); + let mut xw = + Self::rnn_matmul_ty(block, &x_name, &wg, p(&format!("xw{tag}")), dtype, &bh); + if let Some(bn) = &bias_name { + let bg = Self::rnn_slice(block, bn, &[off], &[h], p(&format!("b{tag}")), dtype); + xw = Self::rnn_binary(block, "add", &xw, &bg, p(&format!("xwb{tag}")), dtype, &bh); + } + let rg = Self::rnn_slice( + block, + &r_name, + &[off, 0], + &[h, h], + p(&format!("r{tag}")), + dtype, + ); + let mut hr = + Self::rnn_matmul_ty(block, &hid_name, &rg, p(&format!("hr{tag}")), dtype, &bh); + if let Some(rbn) = &rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[off], &[h], p(&format!("rb{tag}")), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p(&format!("hrb{tag}")), dtype, &bh); + } + let pre = Self::rnn_binary(block, "add", &xw, &hr, p(&format!("pre{tag}")), dtype, &bh); + Self::rnn_unary(block, act, &pre, p(&format!("g{tag}")), dtype, &bh) + }; + + let z = gate(block, z_off, act0, "z"); + let r = gate(block, r_off, act0, "r"); + + // New gate n, whose recurrent term depends on reset_after. + let wn = Self::rnn_slice(block, &w_name, &[n_off, 0], &[h, i], p("wn"), dtype); + let mut xwn = Self::rnn_matmul_ty(block, &x_name, &wn, p("xwn"), dtype, &bh); + if let Some(bn) = &bias_name { + let bg = Self::rnn_slice(block, bn, &[n_off], &[h], p("bn"), dtype); + xwn = Self::rnn_binary(block, "add", &xwn, &bg, p("xwbn"), dtype, &bh); + } + let rn = Self::rnn_slice(block, &r_name, &[n_off, 0], &[h, h], p("rn"), dtype); + let hrn = if reset_after { + // r ⊙ (H·Rn^T + rbn) + let mut hr = Self::rnn_matmul_ty(block, &hid_name, &rn, p("hrn"), dtype, &bh); + if let Some(rbn) = &rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[n_off], &[h], p("rbn"), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p("hrbn"), dtype, &bh); + } + Self::rnn_binary(block, "mul", &r, &hr, p("rhn"), dtype, &bh) + } else { + // (r ⊙ H)·Rn^T + rbn + let rh = Self::rnn_binary(block, "mul", &r, &hid_name, p("rh"), dtype, &bh); + let mut hr = Self::rnn_matmul_ty(block, &rh, &rn, p("hrn"), dtype, &bh); + if let Some(rbn) = &rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[n_off], &[h], p("rbn"), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p("hrbn"), dtype, &bh); + } + hr + }; + let npre = Self::rnn_binary(block, "add", &xwn, &hrn, p("npre"), dtype, &bh); + let n = Self::rnn_unary(block, act1, &npre, p("n"), dtype, &bh); + + // Hnew = n + z ⊙ (H - n) + let hsubn = Self::rnn_binary(block, "sub", &hid_name, &n, p("hsubn"), dtype, &bh); + let zmul = Self::rnn_binary(block, "mul", &z, &hsubn, p("zmul"), dtype, &bh); + let mut add_inputs = HashMap::new(); + add_inputs.insert("x".to_string(), Self::create_name_argument(n)); + add_inputs.insert("y".to_string(), Self::create_name_argument(zmul)); + block.operations.push(Self::create_mil_operation( + "add", + add_inputs, + vec![output_type], + )); + Ok(()) + } + + /// Lower a WebNN `lstmCell` (single time step) into primitive MIL ops. + /// + /// Gates i (input), o (output), f (forget), g (cell); optional peephole weights + /// [pi, po, pf]: + /// i = f0(X·Wi^T + bi + H·Ri^T + rbi + pi ⊙ C) + /// f = f0(X·Wf^T + bf + H·Rf^T + rbf + pf ⊙ C) + /// g = f1(X·Wg^T + bg + H·Rg^T + rbg) + /// Cnew = f ⊙ C + i ⊙ g + /// o = f0(X·Wo^T + bo + H·Ro^T + rbo + po ⊙ Cnew) + /// Hnew = o ⊙ f2(Cnew) + /// Default activations f0=sigmoid, f1=f2=tanh; default gate layout "iofg". + /// Outputs are [Hnew, Cnew]. + fn emit_lstm_cell_decomposition( + graph: &GraphInfo, + op: &Operation, + overrides: &HashMap, + block: &mut Block, + ) -> Result<(), GraphError> { + let (input_id, weight_id, rec_id, hidden_id, cell_id, hidden_size, opts) = match op { + Operation::LstmCell { + input, + weight, + recurrence, + hidden_state, + cell_state, + hidden_size, + options, + .. + } => ( + *input, + *weight, + *recurrence, + *hidden_state, + *cell_state, + *hidden_size, + options.as_ref(), + ), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "emit_lstm_cell_decomposition called on non-lstmCell op".to_string(), + }); + } + }; + let out_ids = op.output_operands(); + if out_ids.len() < 2 { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "lstmCell requires two outputs (hidden, cell)".to_string(), + }); + } + let (out_h_id, out_c_id) = (out_ids[0], out_ids[1]); + let input_op = graph + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "lstmCell input operand not found".to_string(), + })?; + let dtype = Self::mil_data_type(&input_op.descriptor.data_type)?; + let in_shape = input_op.descriptor.static_or_max_shape(); + let i = in_shape[1]; + let h = hidden_size; + let bh = [in_shape[0], h]; + + let x_name = Self::output_name_for_operand(graph, input_id, overrides); + let w_name = Self::output_name_for_operand(graph, weight_id, overrides); + let r_name = Self::output_name_for_operand(graph, rec_id, overrides); + let hid_name = Self::output_name_for_operand(graph, hidden_id, overrides); + let cell_name = Self::output_name_for_operand(graph, cell_id, overrides); + let bias_name = opts + .and_then(|o| o.bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let rbias_name = opts + .and_then(|o| o.recurrent_bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let peephole_name = opts + .and_then(|o| o.peephole_weight) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let layout = opts + .map(|o| o.layout.as_str()) + .filter(|s| !s.is_empty()) + .unwrap_or("iofg"); + let acts: Vec = opts.and_then(|o| o.activations.clone()).unwrap_or_else(|| { + vec![ + "sigmoid".to_string(), + "tanh".to_string(), + "tanh".to_string(), + ] + }); + let f0 = Self::rnn_activation_op(&acts[0])?; + let f1 = Self::rnn_activation_op(acts.get(1).map(|s| s.as_str()).unwrap_or("tanh"))?; + let f2 = Self::rnn_activation_op(acts.get(2).map(|s| s.as_str()).unwrap_or("tanh"))?; + + // Gate row offsets within the 4*hidden weight/recurrence tensors. + let (i_off, o_off, f_off, g_off) = match layout { + "ifgo" => (0, 3 * h, h, 2 * h), + _ => (0, h, 2 * h, 3 * h), + }; + // Peephole weights are ordered [i, o, f]. + let (pi_off, po_off, pf_off) = (0u32, h, 2 * h); + + let (h_name, h_type) = Self::create_output_value(graph, out_h_id, overrides)?; + let (c_name, c_type) = Self::create_output_value(graph, out_c_id, overrides)?; + let p = |s: &str| format!("{}_{}", h_name, s); + + // gate = activation(X·Wg^T + bg + H·Rg^T + rbg [+ pg ⊙ cstate]) + let gate = |block: &mut Block, + off: u32, + act: &str, + tag: &str, + peep: Option<(u32, &str)>| + -> String { + let wg = Self::rnn_slice( + block, + &w_name, + &[off, 0], + &[h, i], + p(&format!("w{tag}")), + dtype, + ); + let mut xw = + Self::rnn_matmul_ty(block, &x_name, &wg, p(&format!("xw{tag}")), dtype, &bh); + if let Some(bn) = &bias_name { + let bg = Self::rnn_slice(block, bn, &[off], &[h], p(&format!("b{tag}")), dtype); + xw = Self::rnn_binary(block, "add", &xw, &bg, p(&format!("xwb{tag}")), dtype, &bh); + } + let rg = Self::rnn_slice( + block, + &r_name, + &[off, 0], + &[h, h], + p(&format!("r{tag}")), + dtype, + ); + let mut hr = + Self::rnn_matmul_ty(block, &hid_name, &rg, p(&format!("hr{tag}")), dtype, &bh); + if let Some(rbn) = &rbias_name { + let rbg = Self::rnn_slice(block, rbn, &[off], &[h], p(&format!("rb{tag}")), dtype); + hr = Self::rnn_binary(block, "add", &hr, &rbg, p(&format!("hrb{tag}")), dtype, &bh); + } + let mut pre = + Self::rnn_binary(block, "add", &xw, &hr, p(&format!("pre{tag}")), dtype, &bh); + if let (Some((poff, cname)), Some(pw)) = (peep, &peephole_name) { + let pg = Self::rnn_slice(block, pw, &[poff], &[h], p(&format!("p{tag}")), dtype); + let pc = + Self::rnn_binary(block, "mul", &pg, cname, p(&format!("pc{tag}")), dtype, &bh); + pre = Self::rnn_binary( + block, + "add", + &pre, + &pc, + p(&format!("pre2{tag}")), + dtype, + &bh, + ); + } + Self::rnn_unary(block, act, &pre, p(&format!("g{tag}")), dtype, &bh) + }; + + let cell_owned = cell_name.clone(); + let gate_i = gate(block, i_off, f0, "i", Some((pi_off, &cell_owned))); + let gate_f = gate(block, f_off, f0, "f", Some((pf_off, &cell_owned))); + let gate_g = gate(block, g_off, f1, "g", None); + + // Cnew = f ⊙ C + i ⊙ g (emitted as the cell output). + let fc = Self::rnn_binary(block, "mul", &gate_f, &cell_name, p("fc"), dtype, &bh); + let ig = Self::rnn_binary(block, "mul", &gate_i, &gate_g, p("ig"), dtype, &bh); + let mut cnew_inputs = HashMap::new(); + cnew_inputs.insert("x".to_string(), Self::create_name_argument(fc)); + cnew_inputs.insert("y".to_string(), Self::create_name_argument(ig)); + block + .operations + .push(Self::create_mil_operation("add", cnew_inputs, vec![c_type])); + + // o depends on Cnew; Hnew = o ⊙ f2(Cnew). + let gate_o = gate(block, o_off, f0, "o", Some((po_off, &c_name))); + let tanh_c = Self::rnn_unary(block, f2, &c_name, p("tanhc"), dtype, &bh); + let mut h_inputs = HashMap::new(); + h_inputs.insert("x".to_string(), Self::create_name_argument(gate_o)); + h_inputs.insert("y".to_string(), Self::create_name_argument(tanh_c)); + block + .operations + .push(Self::create_mil_operation("mul", h_inputs, vec![h_type])); + Ok(()) + } + + /// Stack per-direction final states [b,h] into the WebNN output[0] shape + /// [num_dir, b, h], writing it to `out_name`. + fn rnn_pack_final( + block: &mut Block, + per_dir: &[String], + b: u32, + h: u32, + base: &str, + out_name: String, + dtype: i32, + ) { + let nd = per_dir.len() as u32; + if nd == 1 { + Self::rnn_reshape(block, &per_dir[0], &[1, b, h], out_name, dtype); + return; + } + let parts: Vec = per_dir + .iter() + .enumerate() + .map(|(d, name)| { + Self::rnn_reshape(block, name, &[1, b, h], format!("{base}_f3d{d}"), dtype) + }) + .collect(); + Self::rnn_concat(block, &parts, 0, out_name, dtype, &[nd, b, h]); + } + + /// Stack per-direction, per-time states into the WebNN sequence output shape + /// [steps, num_dir, b, h], writing it to `out_name`. `seq[d][t]` is the [b,h] hidden. + fn rnn_pack_sequence( + block: &mut Block, + seq: &[Vec], + steps: u32, + b: u32, + h: u32, + base: &str, + out_name: String, + dtype: i32, + ) { + let nd = seq.len() as u32; + // For each time step build a [1, nd, b, h] slab. + let mut time_slabs: Vec = Vec::with_capacity(steps as usize); + for t in 0..steps { + let dir_parts: Vec = (0..nd as usize) + .map(|d| { + Self::rnn_reshape( + block, + &seq[d][t as usize], + &[1, 1, b, h], + format!("{base}_s{t}_d{d}"), + dtype, + ) + }) + .collect(); + let slab = if nd == 1 { + dir_parts.into_iter().next().unwrap() + } else { + Self::rnn_concat( + block, + &dir_parts, + 1, + format!("{base}_s{t}"), + dtype, + &[1, nd, b, h], + ) + }; + time_slabs.push(slab); + } + if steps == 1 { + // Rename the single slab to the output by an identity reshape. + Self::rnn_reshape(block, &time_slabs[0], &[1, nd, b, h], out_name, dtype); + return; + } + Self::rnn_concat(block, &time_slabs, 0, out_name, dtype, &[steps, nd, b, h]); + } + + /// Lower a WebNN sequence `gru` by unrolling `emit_gru_step` over time steps and + /// directions (forward/backward/bidirectional), assembling output[0] = last hidden + /// [num_dir, b, h] and, when requested, output[1] = all steps [steps, num_dir, b, h]. + fn emit_gru_decomposition( + graph: &GraphInfo, + op: &Operation, + overrides: &HashMap, + block: &mut Block, + ) -> Result<(), GraphError> { + let (input_id, weight_id, rec_id, steps, hidden_size, opts) = match op { + Operation::Gru { + input, + weight, + recurrence, + steps, + hidden_size, + options, + .. + } => ( + *input, + *weight, + *recurrence, + *steps, + *hidden_size, + options.as_ref(), + ), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "emit_gru_decomposition called on non-gru op".to_string(), + }); + } + }; + let out_ids = op.output_operands().to_vec(); + let input_op = graph + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "gru input operand not found".to_string(), + })?; + let dtype = Self::mil_data_type(&input_op.descriptor.data_type)?; + let in_shape = input_op.descriptor.static_or_max_shape(); + let batch = in_shape[1]; + let i = in_shape[2]; + let h = hidden_size; + + let direction = opts.map(|o| o.direction.as_str()).unwrap_or("forward"); + let nd: u32 = if direction.eq_ignore_ascii_case("both") { + 2 + } else { + 1 + }; + let reset_after = opts.map(|o| o.reset_after).unwrap_or(false); + let layout = opts + .map(|o| o.layout.as_str()) + .filter(|s| !s.is_empty()) + .unwrap_or("zrn"); + let acts: Vec = opts + .and_then(|o| o.activations.clone()) + .unwrap_or_else(|| vec!["sigmoid".to_string(), "tanh".to_string()]); + let act0 = Self::rnn_activation_op(&acts[0])?; + let act1 = Self::rnn_activation_op(acts.get(1).map(|s| s.as_str()).unwrap_or("tanh"))?; + + let input_name = Self::output_name_for_operand(graph, input_id, overrides); + let weight_name = Self::output_name_for_operand(graph, weight_id, overrides); + let rec_name = Self::output_name_for_operand(graph, rec_id, overrides); + let bias_name = opts + .and_then(|o| o.bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let rbias_name = opts + .and_then(|o| o.recurrent_bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let init_name = opts + .and_then(|o| o.initial_hidden_state) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let base = operand_name(graph, out_ids[0]); + + let mut per_dir_final: Vec = Vec::with_capacity(nd as usize); + let mut per_dir_seq: Vec> = Vec::with_capacity(nd as usize); + for d in 0..nd { + let backward = direction.eq_ignore_ascii_case("backward") + || (direction.eq_ignore_ascii_case("both") && d == 1); + let dp = format!("{base}_d{d}"); + let wsl = Self::rnn_slice( + block, + &weight_name, + &[d, 0, 0], + &[1, 3 * h, i], + format!("{dp}_wsl"), + dtype, + ); + let wd = Self::rnn_reshape(block, &wsl, &[3 * h, i], format!("{dp}_w"), dtype); + let rsl = Self::rnn_slice( + block, + &rec_name, + &[d, 0, 0], + &[1, 3 * h, h], + format!("{dp}_rsl"), + dtype, + ); + let rd = Self::rnn_reshape(block, &rsl, &[3 * h, h], format!("{dp}_r"), dtype); + let bd = bias_name.as_ref().map(|bn| { + let s = + Self::rnn_slice(block, bn, &[d, 0], &[1, 3 * h], format!("{dp}_bsl"), dtype); + Self::rnn_reshape(block, &s, &[3 * h], format!("{dp}_b"), dtype) + }); + let rbd = rbias_name.as_ref().map(|bn| { + let s = + Self::rnn_slice(block, bn, &[d, 0], &[1, 3 * h], format!("{dp}_rbsl"), dtype); + Self::rnn_reshape(block, &s, &[3 * h], format!("{dp}_rb"), dtype) + }); + let mut hid = if let Some(inm) = &init_name { + let s = Self::rnn_slice( + block, + inm, + &[d, 0, 0], + &[1, batch, h], + format!("{dp}_h0sl"), + dtype, + ); + Self::rnn_reshape(block, &s, &[batch, h], format!("{dp}_h0"), dtype) + } else { + Self::rnn_zeros(block, &[batch, h], format!("{dp}_h0"), dtype) + }; + let mut seq_by_time: Vec = vec![String::new(); steps as usize]; + let order: Vec = if backward { + (0..steps).rev().collect() + } else { + (0..steps).collect() + }; + for t in order { + let xsl = Self::rnn_slice( + block, + &input_name, + &[t, 0, 0], + &[1, batch, i], + format!("{dp}_x{t}sl"), + dtype, + ); + let xt = Self::rnn_reshape(block, &xsl, &[batch, i], format!("{dp}_x{t}"), dtype); + hid = Self::emit_gru_step( + block, + &xt, + &wd, + &rd, + bd.as_deref(), + rbd.as_deref(), + &hid, + batch, + i, + h, + layout, + act0, + act1, + reset_after, + &format!("{dp}_t{t}"), + dtype, + ); + seq_by_time[t as usize] = hid.clone(); + } + per_dir_final.push(hid); + per_dir_seq.push(seq_by_time); + } + + let out0 = operand_name(graph, out_ids[0]); + Self::rnn_pack_final(block, &per_dir_final, batch, h, &base, out0, dtype); + if out_ids.len() > 1 { + let out1 = operand_name(graph, out_ids[1]); + Self::rnn_pack_sequence(block, &per_dir_seq, steps, batch, h, &base, out1, dtype); + } + Ok(()) + } + + /// Lower a WebNN sequence `lstm` by unrolling `emit_lstm_step` over time steps and + /// directions. Outputs: [0] last hidden [num_dir, b, h], [1] last cell [num_dir, b, h], + /// and (when requested) [2] all hidden steps [steps, num_dir, b, h]. + fn emit_lstm_decomposition( + graph: &GraphInfo, + op: &Operation, + overrides: &HashMap, + block: &mut Block, + ) -> Result<(), GraphError> { + let (input_id, weight_id, rec_id, steps, hidden_size, opts) = match op { + Operation::Lstm { + input, + weight, + recurrence, + steps, + hidden_size, + options, + .. + } => ( + *input, + *weight, + *recurrence, + *steps, + *hidden_size, + options.as_ref(), + ), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "emit_lstm_decomposition called on non-lstm op".to_string(), + }); + } + }; + let out_ids = op.output_operands().to_vec(); + if out_ids.len() < 2 { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "lstm requires hidden and cell outputs".to_string(), + }); + } + let input_op = graph + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "lstm input operand not found".to_string(), + })?; + let dtype = Self::mil_data_type(&input_op.descriptor.data_type)?; + let in_shape = input_op.descriptor.static_or_max_shape(); + let batch = in_shape[1]; + let i = in_shape[2]; + let h = hidden_size; + + let direction = opts.map(|o| o.direction.as_str()).unwrap_or("forward"); + let nd: u32 = if direction.eq_ignore_ascii_case("both") { + 2 + } else { + 1 + }; + let layout = opts + .map(|o| o.layout.as_str()) + .filter(|s| !s.is_empty()) + .unwrap_or("iofg"); + let acts: Vec = opts.and_then(|o| o.activations.clone()).unwrap_or_else(|| { + vec![ + "sigmoid".to_string(), + "tanh".to_string(), + "tanh".to_string(), + ] + }); + let f0 = Self::rnn_activation_op(&acts[0])?; + let f1 = Self::rnn_activation_op(acts.get(1).map(|s| s.as_str()).unwrap_or("tanh"))?; + let f2 = Self::rnn_activation_op(acts.get(2).map(|s| s.as_str()).unwrap_or("tanh"))?; + + let input_name = Self::output_name_for_operand(graph, input_id, overrides); + let weight_name = Self::output_name_for_operand(graph, weight_id, overrides); + let rec_name = Self::output_name_for_operand(graph, rec_id, overrides); + let bias_name = opts + .and_then(|o| o.bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let rbias_name = opts + .and_then(|o| o.recurrent_bias) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let peephole_name = opts + .and_then(|o| o.peephole_weight) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let init_h_name = opts + .and_then(|o| o.initial_hidden_state) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let init_c_name = opts + .and_then(|o| o.initial_cell_state) + .map(|id| Self::output_name_for_operand(graph, id, overrides)); + let base = operand_name(graph, out_ids[0]); + + let mut per_dir_h: Vec = Vec::with_capacity(nd as usize); + let mut per_dir_c: Vec = Vec::with_capacity(nd as usize); + let mut per_dir_seq: Vec> = Vec::with_capacity(nd as usize); + for d in 0..nd { + let backward = direction.eq_ignore_ascii_case("backward") + || (direction.eq_ignore_ascii_case("both") && d == 1); + let dp = format!("{base}_d{d}"); + let wsl = Self::rnn_slice( + block, + &weight_name, + &[d, 0, 0], + &[1, 4 * h, i], + format!("{dp}_wsl"), + dtype, + ); + let wd = Self::rnn_reshape(block, &wsl, &[4 * h, i], format!("{dp}_w"), dtype); + let rsl = Self::rnn_slice( + block, + &rec_name, + &[d, 0, 0], + &[1, 4 * h, h], + format!("{dp}_rsl"), + dtype, + ); + let rd = Self::rnn_reshape(block, &rsl, &[4 * h, h], format!("{dp}_r"), dtype); + let bd = bias_name.as_ref().map(|bn| { + let s = + Self::rnn_slice(block, bn, &[d, 0], &[1, 4 * h], format!("{dp}_bsl"), dtype); + Self::rnn_reshape(block, &s, &[4 * h], format!("{dp}_b"), dtype) + }); + let rbd = rbias_name.as_ref().map(|bn| { + let s = + Self::rnn_slice(block, bn, &[d, 0], &[1, 4 * h], format!("{dp}_rbsl"), dtype); + Self::rnn_reshape(block, &s, &[4 * h], format!("{dp}_rb"), dtype) + }); + let pd = peephole_name.as_ref().map(|pn| { + let s = + Self::rnn_slice(block, pn, &[d, 0], &[1, 3 * h], format!("{dp}_psl"), dtype); + Self::rnn_reshape(block, &s, &[3 * h], format!("{dp}_p"), dtype) + }); + let mut hid = if let Some(inm) = &init_h_name { + let s = Self::rnn_slice( + block, + inm, + &[d, 0, 0], + &[1, batch, h], + format!("{dp}_h0sl"), + dtype, + ); + Self::rnn_reshape(block, &s, &[batch, h], format!("{dp}_h0"), dtype) + } else { + Self::rnn_zeros(block, &[batch, h], format!("{dp}_h0"), dtype) + }; + let mut cell = if let Some(inm) = &init_c_name { + let s = Self::rnn_slice( + block, + inm, + &[d, 0, 0], + &[1, batch, h], + format!("{dp}_c0sl"), + dtype, + ); + Self::rnn_reshape(block, &s, &[batch, h], format!("{dp}_c0"), dtype) + } else { + Self::rnn_zeros(block, &[batch, h], format!("{dp}_c0"), dtype) + }; + let mut seq_by_time: Vec = vec![String::new(); steps as usize]; + let order: Vec = if backward { + (0..steps).rev().collect() + } else { + (0..steps).collect() + }; + for t in order { + let xsl = Self::rnn_slice( + block, + &input_name, + &[t, 0, 0], + &[1, batch, i], + format!("{dp}_x{t}sl"), + dtype, + ); + let xt = Self::rnn_reshape(block, &xsl, &[batch, i], format!("{dp}_x{t}"), dtype); + let (nh, nc) = Self::emit_lstm_step( + block, + &xt, + &wd, + &rd, + bd.as_deref(), + rbd.as_deref(), + pd.as_deref(), + &hid, + &cell, + batch, + i, + h, + layout, + f0, + f1, + f2, + &format!("{dp}_t{t}"), + dtype, + ); + hid = nh; + cell = nc; + seq_by_time[t as usize] = hid.clone(); + } + per_dir_h.push(hid); + per_dir_c.push(cell); + per_dir_seq.push(seq_by_time); + } + + let out_h = operand_name(graph, out_ids[0]); + Self::rnn_pack_final(block, &per_dir_h, batch, h, &base, out_h, dtype); + let out_c = operand_name(graph, out_ids[1]); + Self::rnn_pack_final( + block, + &per_dir_c, + batch, + h, + &format!("{base}_c"), + out_c, + dtype, + ); + if out_ids.len() > 2 { + let out_seq = operand_name(graph, out_ids[2]); + Self::rnn_pack_sequence(block, &per_dir_seq, steps, batch, h, &base, out_seq, dtype); + } + Ok(()) + } + + /// Create inputs map for MIL operation + fn create_operation_inputs( + &self, + graph: &GraphInfo, + op: &Operation, + input_names: &[String], + ) -> Result, GraphError> { + let mut inputs = HashMap::new(); + + match &op { + // Binary operations: x, y + Operation::Add { .. } + | Operation::Sub { .. } + | Operation::Mul { .. } + | Operation::Div { .. } + | Operation::Pow { .. } + | Operation::Max { .. } + | Operation::Min { .. } + | Operation::Equal { .. } + | Operation::Greater { .. } + | Operation::GreaterOrEqual { .. } + | Operation::Lesser { .. } + | Operation::LesserOrEqual { .. } + | Operation::LogicalAnd { .. } + | Operation::LogicalOr { .. } + | Operation::LogicalXor { .. } + if input_names.len() >= 2 => { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("y".to_string(), Self::create_argument(&input_names[1])); + } + + // MatMul operation: x, y, transpose_x, transpose_y + // CoreML requires transpose parameters, WebNN doesn't have them so default to false + Operation::Matmul { .. } => { + if input_names.len() >= 2 { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("y".to_string(), Self::create_argument(&input_names[1])); + } + + // Add transpose_x parameter (required by CoreML, defaults to false) + inputs.insert( + "transpose_x".to_string(), + Self::create_immediate_bool(false), + ); + + // Add transpose_y parameter (required by CoreML, defaults to false) + inputs.insert( + "transpose_y".to_string(), + Self::create_immediate_bool(false), + ); + } + + // Gemm operation: General Matrix Multiplication + // Y = alpha * op(A) * op(B) + beta * C + // CoreML matmul handles: Y = A * B (with transpose options) + // For now, we support transpose options and basic matmul + // TODO: Support alpha, beta, and bias (C) by decomposing into mul and add operations + Operation::Gemm { options, .. } => { + if input_names.len() >= 2 { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("y".to_string(), Self::create_argument(&input_names[1])); + } + + // Add transpose parameters from operator options + if let Some(opts) = options { + inputs.insert( + "transpose_x".to_string(), + Self::create_immediate_bool(opts.a_transpose), + ); + inputs.insert( + "transpose_y".to_string(), + Self::create_immediate_bool(opts.b_transpose), + ); + } + + // Note: alpha, beta, and bias (C) are not yet supported + // These would require decomposing gemm into multiple operations: + // 1. matmul(op(A), op(B)) + // 2. mul by alpha if != 1.0 + // 3. add beta * C if C is provided + } + + // Global pooling operations (reduce over spatial dimensions) + Operation::GlobalAveragePool { .. } | Operation::GlobalMaxPool { .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + // Global pooling reduces over spatial dimensions (2, 3) for NCHW format + inputs.insert( + "axes".to_string(), + Self::create_immediate_int_array(&[2, 3]), + ); + // Keep dimensions to maintain rank + inputs.insert("keep_dims".to_string(), Self::create_immediate_bool(true)); + } + + // Softmax operation (axis is required by WebNN spec) + Operation::Softmax { axis, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + inputs.insert("axis".to_string(), Self::create_immediate_int(*axis)); + } + + // Neg operation: implemented as mul by -1, requires x and y parameters + // CoreML neg is actually a mul operation, so we need both operands + Operation::Neg { .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + // Add -1.0 as the multiplier (y parameter required by CoreML mul) + inputs.insert("y".to_string(), Self::create_immediate_float(-1.0)); + } + + // Unary operations: x + Operation::Relu { .. } + | Operation::Sigmoid { .. } + | Operation::Tanh { .. } + | Operation::Abs { .. } + | Operation::Ceil { .. } + | Operation::Floor { .. } + | Operation::RoundEven { .. } + | Operation::Sign { .. } + | Operation::Identity { .. } + | Operation::Exp { .. } + | Operation::Sqrt { .. } + | Operation::Sin { .. } + | Operation::Cos { .. } + | Operation::Tan { .. } + | Operation::Erf { .. } + | Operation::LogicalNot { .. } + | Operation::Softplus { .. } + | Operation::Softsign { .. } + if !input_names.is_empty() => { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + Operation::Reciprocal { .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + // Reciprocal requires epsilon parameter (default to 1e-45 for numerical stability) + inputs.insert( + "epsilon".to_string(), + Self::create_immediate_float(DEFAULT_EPSILON), + ); + } + + // Log operation requires epsilon parameter + Operation::Log { .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + // CoreML log requires epsilon parameter (default to 1e-45 for numerical stability) + inputs.insert( + "epsilon".to_string(), + Self::create_immediate_float(DEFAULT_EPSILON), + ); + } + + // Quantization operations: input, scale, zero_point[, axis for per-channel] + Operation::DequantizeLinear { input: inp_id, scale: scale_id, .. } + | Operation::QuantizeLinear { input: inp_id, scale: scale_id, .. } + if input_names.len() >= 2 => { + inputs.insert("input".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("scale".to_string(), Self::create_argument(&input_names[1])); + if input_names.len() >= 3 { + inputs.insert( + "zero_point".to_string(), + Self::create_argument(&input_names[2]), + ); + } + // When scale is truly per-channel (rank-1 with >1 elements), CoreML requires + // an explicit axis. For per-tensor (scalar or single-element), omit axis. + // Note: single-element scales [1] are squeezed to scalar at constant emit + // time; emitting axis alongside a scalar scale causes a CoreML compile error. + // Multi-dimensional scales are squeezed to 1D at emission time (all size-1 + // dimensions removed). Compute the effective squeezed length here so we can + // detect the per-channel case even when the WebNN descriptor says rank > 1. + if let Some(scale_op) = graph.operand(*scale_id) { + let scale_shape = scale_op.descriptor.static_or_max_shape(); + // Effective shape after squeezing out all size-1 dims (mirrors the + // constant emission pre-scan for scale_ids_to_squeeze). + let squeezed: Vec = scale_shape.iter().copied().filter(|&d| d != 1).collect(); + let effective_rank = if squeezed.is_empty() { 0 } else { squeezed.len() }; + let effective_len = squeezed.first().copied().unwrap_or(scale_shape.first().copied().unwrap_or(0)); + let is_per_channel = effective_rank == 1 && effective_len > 1; + if is_per_channel { + let axis = graph.operand(*inp_id) + .and_then(|inp| { + inp.descriptor.static_or_max_shape() + .iter() + .position(|&d| d == effective_len) + .map(|i| i as u32) + }) + .unwrap_or(0); + inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); + } + } + } + + // Specialized activation: prelu - x, slope (two inputs) + Operation::Prelu { .. } + if input_names.len() >= 2 => { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("alpha".to_string(), Self::create_argument(&input_names[1])); + } + + // Specialized activations with alpha parameter: elu, leakyRelu + Operation::Elu { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + let alpha = options.as_ref().map(|o| o.alpha as f32).unwrap_or(1.0); + inputs.insert("alpha".to_string(), Self::create_immediate_float(alpha)); + } + Operation::LeakyRelu { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + let alpha = options.as_ref().map(|o| o.alpha as f32).unwrap_or(0.01); + inputs.insert("alpha".to_string(), Self::create_immediate_float(alpha)); + } + + // HardSwish: decomposed in main loop (hardsigmoid + mul) + // This case should never be reached due to continue in main loop + Operation::HardSwish { .. } => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "hardswish should be decomposed in main loop, not here".to_string(), + }); + } + + // HardSigmoid: x, alpha, beta parameters + Operation::HardSigmoid { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + if let Some(opts) = options { + inputs.insert( + "alpha".to_string(), + Self::create_immediate_float(opts.alpha as f32), + ); + inputs.insert( + "beta".to_string(), + Self::create_immediate_float(opts.beta as f32), + ); + } + } + + // Clamp operation: x, alpha (min), beta (max) + Operation::Clamp { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + // CoreML clip operation requires BOTH alpha and beta parameters + // WebNN clamp defaults: minValue=-Infinity, maxValue=+Infinity + let (min_value, max_value) = options + .as_ref() + .map(|o| { + let min = + Self::parse_clamp_bound(o.min_value.as_ref(), f64::NEG_INFINITY) as f32; + let max = + Self::parse_clamp_bound(o.max_value.as_ref(), f64::INFINITY) as f32; + (min, max) + }) + .unwrap_or((f32::NEG_INFINITY, f32::INFINITY)); + + // Alpha and beta must match input type (CoreML requirement) + // Check first input operand type and use appropriate immediate value method + let use_float16 = if !op.input_operands().is_empty() { + if let Some(input_operand) = graph.operand(op.input_operands()[0]) { + input_operand.descriptor.data_type == DataType::Float16 + } else { + false + } + } else { + false + }; + + if use_float16 { + inputs.insert( + "alpha".to_string(), + Self::create_immediate_float16(min_value), + ); + inputs.insert( + "beta".to_string(), + Self::create_immediate_float16(max_value), + ); + } else { + inputs.insert("alpha".to_string(), Self::create_immediate_float(min_value)); + inputs.insert("beta".to_string(), Self::create_immediate_float(max_value)); + } + } + + // Transpose operation: x, permutation + Operation::Transpose { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // Add permutation parameter (required by CoreML) + // If not specified in WebNN, default is to reverse all dimensions + if let Some(opts) = options + && !opts.permutation.is_empty() + { + inputs.insert( + "perm".to_string(), + Self::create_immediate_int_array(&opts.permutation), + ); + } else if !op.input_operands().is_empty() + && let Some(input_operand) = graph.operand(op.input_operands()[0]) + { + let rank = input_operand.descriptor.shape.len(); + let default_perm: Vec = (0..rank).rev().map(|i| i as u32).collect(); + inputs.insert( + "perm".to_string(), + Self::create_immediate_int_array(&default_perm), + ); + } + } + + // Reshape: x, shape + Operation::Reshape { new_shape, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // MIL `reshape` declares `shape` as a required input. Always emit + // it, including the valid WebNN scalar-output case (`new_shape = []`) + let shape_values = if new_shape.is_empty() { + Vec::new() + } else { + crate::operator_options::mldimensions_static_or_max(new_shape) + }; + inputs.insert( + "shape".to_string(), + Self::create_immediate_int_array(&shape_values), + ); + } + + // Convolution operations: input, filter + parameters + Operation::Conv2d { options, .. } => { + if input_names.len() >= 2 { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("weight".to_string(), Self::create_argument(&input_names[1])); + } + + // Add optional bias if present (third input) + if input_names.len() >= 3 { + inputs.insert("bias".to_string(), Self::create_argument(&input_names[2])); + } + + // MIL `conv` requires `strides`, `pad`, `dilations`, `groups` — all + // four are declared as required inputs in the MIL op schema, so + // Apple's CoreML loader rejects the model with + // "Required param '...' is missing" when any is omitted. Emit the + // WebNN defaults when the WebNN graph left them unset. + let (strides, dilations, padding, groups) = match options { + Some(o) => ( + if o.strides.is_empty() { + vec![1, 1] + } else { + o.strides.clone() + }, + if o.dilations.is_empty() { + vec![1, 1] + } else { + o.dilations.clone() + }, + if o.padding.is_empty() { + vec![0, 0, 0, 0] + } else { + o.padding.clone() + }, + o.groups, + ), + None => (vec![1, 1], vec![1, 1], vec![0, 0, 0, 0], 1), + }; + inputs.insert( + "strides".to_string(), + Self::create_immediate_int_array(&strides), + ); + inputs.insert( + "dilations".to_string(), + Self::create_immediate_int_array(&dilations), + ); + inputs.insert( + "pad".to_string(), + Self::create_immediate_int_array(&padding), + ); + inputs.insert("groups".to_string(), Self::create_immediate_int(groups)); + + // Add pad_type - required parameter in CoreML + // Use "custom" when explicit padding is provided + inputs.insert( + "pad_type".to_string(), + Self::create_immediate_string("custom"), + ); + } + + // Transposed convolution: input, filter + parameters + Operation::ConvTranspose2d { options, .. } => { + if input_names.len() >= 2 { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("weight".to_string(), Self::create_argument(&input_names[1])); + } + + // Add optional bias if present (from options, not input_names) + if let Some(opts) = options { + if let Some(bias_id) = opts.bias { + inputs.insert( + "bias".to_string(), + Self::create_argument(&operand_name(graph, bias_id)), + ); + } + } + + // CoreML requires pad_type parameter (defaults to "custom" for explicit padding) + inputs.insert( + "pad_type".to_string(), + Self::create_immediate_string("custom"), + ); + + // MIL `conv_transpose` requires `strides`, `pad`, `dilations`, + // `groups` — same as `conv` above. Apple's loader emits + // "Required param '...' is missing" when any is dropped, even + // when the WebNN graph left the attribute at its default. + let (strides, dilations, padding, groups) = match options { + Some(o) => ( + if o.strides.is_empty() { + vec![1, 1] + } else { + o.strides.clone() + }, + if o.dilations.is_empty() { + vec![1, 1] + } else { + o.dilations.clone() + }, + if o.padding.is_empty() { + vec![0, 0, 0, 0] + } else { + o.padding.clone() + }, + o.groups, + ), + None => (vec![1, 1], vec![1, 1], vec![0, 0, 0, 0], 1), + }; + inputs.insert( + "strides".to_string(), + Self::create_immediate_int_array(&strides), + ); + inputs.insert( + "dilations".to_string(), + Self::create_immediate_int_array(&dilations), + ); + inputs.insert( + "pad".to_string(), + Self::create_immediate_int_array(&padding), + ); + inputs.insert("groups".to_string(), Self::create_immediate_int(groups)); + + // MIL conv_transpose takes the complete output tensor shape, + // not only WebNN's two spatial outputSizes values. Chromium + // supplies this for every transposed convolution as well. + if let Some(output_id) = op.output_operand() + && let Some(output) = graph.operand(output_id) + { + let mut output_shape = output.descriptor.static_or_max_shape(); + let input_layout = options + .as_ref() + .map(|o| o.input_layout.as_str()) + .unwrap_or(""); + if input_layout.eq_ignore_ascii_case("nhwc") && output_shape.len() == 4 { + output_shape = vec![ + output_shape[0], + output_shape[3], + output_shape[1], + output_shape[2], + ]; + } + inputs.insert( + "output_shape".to_string(), + Self::create_immediate_int_array(&output_shape), + ); + } + } + + // Pooling operations: input + parameters + Operation::AveragePool2d { + options: pool_opts, .. + } + | Operation::MaxPool2d { + options: pool_opts, .. + } + | Operation::L2Pool2d { + options: pool_opts, .. + } => { + // NHWC pooling is handled by emitting NHWC→NCHW transpose wrappers in the + // main convert loop before reaching here. By the time we get here the input + // is already in NCHW form, but the graph operands remain in WebNN layout. + let is_nhwc = pool_opts + .as_ref() + .map(|o| o.layout.eq_ignore_ascii_case("nhwc")) + .unwrap_or(false); + + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // CoreML pooling has no dilation parameter. + if let Some(dil) = pool_opts.as_ref().map(|o| &o.dilations) + && !dil.is_empty() + && dil.iter().any(|&d| d != 1) + { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "CoreML pooling does not support non-default dilations; got {:?} for {}", + dil, + op.op_type() + ), + }); + } + + // Kernel sizes: explicit windowDimensions, else the full spatial extent. + let kernel: Vec = pool_opts + .as_ref() + .and_then(|o| o.window_dimensions.clone()) + .filter(|w| !w.is_empty()) + .or_else(|| { + op.input_operands() + .first() + .and_then(|&id| graph.operand(id)) + .map(|o| o.descriptor.static_or_max_shape()) + .filter(|s| s.len() >= 4) + .map(|s| { + if is_nhwc { + vec![s[1], s[2]] + } else { + vec![s[2], s[3]] + } + }) + }) + .unwrap_or_else(|| vec![1, 1]); + inputs.insert( + "kernel_sizes".to_string(), + Self::create_immediate_int_array(&kernel), + ); + + let strides = pool_opts + .as_ref() + .map(|o| o.strides.clone()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| vec![1, 1]); + inputs.insert( + "strides".to_string(), + Self::create_immediate_int_array(&strides), + ); + + // Fold outputShapeRounding="ceil"/outputSizes into extra end-padding and + // pool with floor rounding + custom padding (see pool_effective_padding). + let base_pad = pool_opts + .as_ref() + .map(|o| o.padding.clone()) + .filter(|p| !p.is_empty()) + .unwrap_or_else(|| vec![0, 0, 0, 0]); + let pad = Self::pool_effective_padding( + graph, &op, &kernel, &strides, &base_pad, is_nhwc, + ) + .unwrap_or(base_pad); + inputs.insert("pad".to_string(), Self::create_immediate_int_array(&pad)); + inputs.insert( + "pad_type".to_string(), + Self::create_immediate_string("custom"), + ); + inputs.insert("ceil_mode".to_string(), Self::create_immediate_bool(false)); + + // WebNN averagePool2d excludes padded elements from the divisor (a + // boundary window covering N real elements divides by N, not the kernel + // area). Only average pooling accepts this parameter. + if matches!(&op, Operation::AveragePool2d { .. }) { + inputs.insert( + "exclude_padding_from_average".to_string(), + Self::create_immediate_bool(true), + ); + } + } + + // Layer normalization (different from batch/instance normalization) + Operation::LayerNormalization { options, .. } => { + // Build sorted axes vector (CoreML requires sorted axes; empty axes are handled + // as a special case in the main convert loop before reaching here). + // axes=None means "last axis" per WebNN spec. + let mut axes_vec: Vec = + if let Some(ax) = options.as_ref().and_then(|o| o.axes.as_ref()) { + ax.iter().map(|&u| u as i32).collect() + } else { + // axes=None: WebNN defaults to all axes except the batch + // dimension, i.e. the sequence [1, 2, ..., N-1]. + let input_rank = op + .input_operands() + .first() + .and_then(|&id| graph.operand(id)) + .map(|o| o.descriptor.shape.len()) + .unwrap_or(1); + if input_rank > 1 { + (1..input_rank as i32).collect() + } else { + vec![0] + } + }; + axes_vec.sort_unstable(); + + // Add input operand (only x; scale/bias come from options, not input_operands) + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // Scale (gamma) and bias (beta) are stored in options, not in input_operands. + if let Some(opts) = options { + if let Some(scale_id) = opts.scale { + inputs.insert( + "gamma".to_string(), + Self::create_argument(&operand_name(graph, scale_id)), + ); + } + if let Some(bias_id) = opts.bias { + inputs.insert( + "beta".to_string(), + Self::create_argument(&operand_name(graph, bias_id)), + ); + } + let use_f16 = op + .input_operands() + .first() + .and_then(|&id| graph.operand(id)) + .map(|o| o.descriptor.data_type == DataType::Float16) + .unwrap_or(false); + let eps_arg = if use_f16 { + Self::create_immediate_float16(opts.epsilon as f32) + } else { + Self::create_immediate_float(opts.epsilon as f32) + }; + inputs.insert("epsilon".to_string(), eps_arg); + } + + // Add axes parameter (REQUIRED by CoreML, must not be empty; + // empty axes are caught before this point). + inputs.insert( + "axes".to_string(), + Self::create_int_array_argument(axes_vec), + ); + } + + // Batch/instance normalization (have mean, variance inputs) + Operation::BatchNormalization { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + if input_names.len() >= 2 && op.input_operands().len() >= 2 { + let mean_operand_id = op.input_operands()[1]; + if let Some(mean_operand) = graph.operand(mean_operand_id) + && mean_operand.kind != crate::graph::OperandKind::Constant + { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "CoreML {} requires mean parameter to be a constant tensor, not a graph input", + op.op_type() + ), + }); + } + inputs.insert("mean".to_string(), Self::create_argument(&input_names[1])); + } + if input_names.len() >= 3 && op.input_operands().len() >= 3 { + let variance_operand_id = op.input_operands()[2]; + if let Some(variance_operand) = graph.operand(variance_operand_id) + && variance_operand.kind != crate::graph::OperandKind::Constant + { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "CoreML {} requires variance parameter to be a constant tensor, not a graph input", + op.op_type() + ), + }); + } + inputs.insert( + "variance".to_string(), + Self::create_argument(&input_names[2]), + ); + } + if let Some(opts) = options { + if let Some(sid) = opts.scale { + inputs.insert( + "gamma".to_string(), + Self::create_argument(&operand_name(graph, sid)), + ); + } else if input_names.len() >= 4 { + inputs.insert("gamma".to_string(), Self::create_argument(&input_names[3])); + } + if let Some(bid) = opts.bias { + inputs.insert( + "beta".to_string(), + Self::create_argument(&operand_name(graph, bid)), + ); + } else if input_names.len() >= 5 { + inputs.insert("beta".to_string(), Self::create_argument(&input_names[4])); + } + let use_f16_bn = op + .input_operands() + .first() + .and_then(|&id| graph.operand(id)) + .map(|o| o.descriptor.data_type == DataType::Float16) + .unwrap_or(false); + let eps_bn = if use_f16_bn { + Self::create_immediate_float16(opts.epsilon as f32) + } else { + Self::create_immediate_float(opts.epsilon as f32) + }; + inputs.insert("epsilon".to_string(), eps_bn); + } + } + Operation::InstanceNormalization { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + if let Some(opts) = options { + if let Some(scale_id) = opts.scale { + inputs.insert( + "gamma".to_string(), + Self::create_argument(&operand_name(graph, scale_id)), + ); + } + if let Some(bias_id) = opts.bias { + inputs.insert( + "beta".to_string(), + Self::create_argument(&operand_name(graph, bias_id)), + ); + } + let use_f16_in = op + .input_operands() + .first() + .and_then(|&id| graph.operand(id)) + .map(|o| o.descriptor.data_type == DataType::Float16) + .unwrap_or(false); + let eps_in = if use_f16_in { + Self::create_immediate_float16(opts.epsilon as f32) + } else { + Self::create_immediate_float(opts.epsilon as f32) + }; + inputs.insert("epsilon".to_string(), eps_in); + } + } + + Operation::Concat { axis, .. } => { + // concat: values (variadic list of tensors), axis + // CoreML expects a single 'values' parameter containing a tuple of all inputs + if !input_names.is_empty() { + inputs.insert( + "values".to_string(), + Self::create_argument_tuple(input_names), + ); + } + + inputs.insert("axis".to_string(), Self::create_immediate_int(*axis)); + inputs.insert("interleave".to_string(), Self::create_immediate_bool(false)); + } + + Operation::Slice { + starts, + sizes, + options, + .. + } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + let strides = options + .as_ref() + .map(|o| o.strides.as_slice()) + .unwrap_or(&[]); + let has_nontrivial_strides = + strides.iter().any(|&s| s != 1) && !strides.is_empty(); + + if has_nontrivial_strides { + // slice_by_index: begin, end (exclusive), stride. + // CoreML produces ceil((end - begin) / stride) per axis. + // WebNN sizes[i] = window width (input elements spanned), so: + // end[i] = starts[i] + sizes[i] + // stride[i] = strides[i] + inputs.insert( + "begin".to_string(), + Self::create_immediate_int_array(starts), + ); + let ends: Vec = starts + .iter() + .zip(sizes.iter()) + .map(|(&s, d)| s + d.static_or_max()) + .collect(); + inputs.insert("end".to_string(), Self::create_immediate_int_array(&ends)); + inputs.insert( + "stride".to_string(), + Self::create_immediate_int_array(strides), + ); + } else { + // slice_by_size: x, begin, size. Both `begin` and `size` are + // declared required inputs in MIL's slice_by_size schema. Apple + // rejects the model with "Required param 'size' is missing" + // when an empty-shape no-op slice (0D scalar, WPT surfaces this) + // is emitted without the fields. Emit them as empty int32 arrays + // in that case so the MIL loader sees the param even though the + // tensor is rank-0. + inputs.insert( + "begin".to_string(), + Self::create_immediate_int_array(starts), + ); + let sizes_u32: Vec = sizes.iter().map(|d| d.static_or_max()).collect(); + inputs.insert( + "size".to_string(), + Self::create_immediate_int_array(&sizes_u32), + ); + } + } + + Operation::Expand { new_shape, .. } => { + // CoreML tile operation requires input rank to match reps length + // If reshape was added before this operation, use reshaped input name + // Otherwise use original input + + if let Some(new_shape_u32) = (!new_shape.is_empty()).then(|| { + new_shape + .iter() + .map(MLDimension::static_or_max) + .collect::>() + }) { + // Get input operand shape + if !op.input_operands().is_empty() + && let Some(input_operand) = graph.operand(op.input_operands()[0]) + { + let input_shape = input_operand.descriptor.static_or_max_shape(); + let input_rank = input_shape.len(); + let output_rank = new_shape_u32.len(); + + // Determine input name for tile operation + let tile_input_name = if input_rank < output_rank { + // A reshape was added, use the reshaped output name + // The reshape output name is: {input_name}_expand_reshaped + format!("{}_expand_reshaped", input_names[0]) + } else { + // No reshape, use original input + input_names[0].clone() + }; + + inputs.insert("x".to_string(), Self::create_name_argument(tile_input_name)); + + // Create reshaped dimensions (right-aligned, padded with 1s on left) + let mut reshaped_dims = vec![1u32; output_rank]; + for i in 0..input_rank { + reshaped_dims[output_rank - i - 1] = input_shape[input_rank - i - 1]; + } + + // Calculate reps: reps[i] = output_shape[i] / reshaped_input_shape[i] + let reps: Vec = new_shape_u32 + .iter() + .zip(reshaped_dims.iter()) + .map(|(&output_dim, &reshaped_dim)| { + if reshaped_dim == output_dim { + 1 + } else if reshaped_dim == 1 { + output_dim as i32 + } else { + // Should not happen - dimensions must match or input must be 1 + 1 + } + }) + .collect(); + + inputs.insert("reps".to_string(), Self::create_int_array_argument(reps)); + } + } + } + + Operation::Gather { options, .. } => { + // gather: x (data), indices, axis, validate_indices + // CoreML uses 'x' for the data input, not 'params' + if input_names.len() >= 2 { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert( + "indices".to_string(), + Self::create_argument(&input_names[1]), + ); + } + + // Add axis parameter (REQUIRED by CoreML, defaults to 0) + let axis = options.as_ref().map(|o| o.axis).unwrap_or(0); + inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); + + // Add validate_indices parameter (required by CoreML) + // Chromium sets this to false to avoid validation issues + // TODO: Handle negative and out-of-bounds indices properly + inputs.insert( + "validate_indices".to_string(), + Self::create_immediate_bool(false), + ); + } + + Operation::GatherElements { options, .. } => { + // gather_along_axis: x, indices, axis, validate_indices + if input_names.len() >= 2 { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + inputs.insert( + "indices".to_string(), + Self::create_argument(&input_names[1]), + ); + } + + let axis = options.as_ref().map(|o| o.axis).unwrap_or(0); + inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); + + inputs.insert( + "validate_indices".to_string(), + Self::create_immediate_bool(false), + ); + } + + Operation::Split { + splits, options, .. + } => { + // split: x, num_splits or split_sizes, axis + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + if let Some(opts) = options { + if splits.is_empty() { + inputs.insert( + "num_splits".to_string(), + Self::create_immediate_int(op.output_operands().len() as u32), + ); + } else { + inputs.insert( + "split_sizes".to_string(), + Self::create_immediate_int_array(splits), + ); + } + inputs.insert("axis".to_string(), Self::create_immediate_int(opts.axis)); + } + } + + Operation::Where { .. } + // select: cond, a (true_value), b (false_value) + if input_names.len() >= 3 => { + inputs.insert("cond".to_string(), Self::create_argument(&input_names[0])); + inputs.insert("a".to_string(), Self::create_argument(&input_names[1])); + inputs.insert("b".to_string(), Self::create_argument(&input_names[2])); + } + + Operation::Pad { + beginning_padding, + ending_padding, + options, + .. + } => { + // pad: x, pad, mode, constant_val + // All four params are required by CoreML (even when using defaults). + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // CoreML expects pad as [begin_0, end_0, begin_1, end_1, ...] + let pad: Vec = beginning_padding + .iter() + .zip(ending_padding.iter()) + .flat_map(|(a, b)| [*a, *b]) + .collect(); + inputs.insert("pad".to_string(), Self::create_immediate_int_array(&pad)); + + // Mode: WebNN → CoreML mapping + // WebNN: "constant", "edge", "reflection", "symmetric" + // CoreML: "constant", "replicate", "reflect" + let webnn_mode = options + .as_ref() + .map(|o| o.mode.as_str()) + .unwrap_or("constant"); + let coreml_mode = match webnn_mode { + "edge" => "replicate", + "reflection" | "symmetric" => "reflect", + _ => "constant", + }; + inputs.insert( + "mode".to_string(), + Self::create_immediate_string(coreml_mode), + ); + + // constant_val must match the dtype of the input tensor. + let constant_val = options + .as_ref() + .and_then(|o| Self::parse_mlnumber_f64(o.value.as_ref())) + .unwrap_or(0.0); + let input_dtype = op + .input_operands() + .first() + .and_then(|&id| graph.operand(id)) + .map(|o| &o.descriptor.data_type) + .cloned() + .unwrap_or(DataType::Float32); + let cval_arg = match input_dtype { + DataType::Float16 => Self::create_immediate_float16(constant_val as f32), + DataType::Int32 => Self::create_immediate_int(constant_val as u32), + DataType::Int8 | DataType::Uint8 => { + Self::create_immediate_int(constant_val as u32) + } + _ => Self::create_immediate_float(constant_val as f32), + }; + inputs.insert("constant_val".to_string(), cval_arg); + } + + Operation::Gelu { .. } => { + // gelu: x, mode + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + // watchOS MIL loader rejects gelu without an explicit mode ("Required + // param 'mode' is missing"); iOS/macOS loaders accept the default. + // WebNN spec has no mode parameter — exact (erf) is the implicit default. + inputs.insert("mode".to_string(), Self::create_immediate_string("EXACT")); + } + + Operation::Squeeze { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + if let Some(opts) = options + && !opts.axes.is_empty() + { + inputs.insert( + "axes".to_string(), + Self::create_immediate_int_array(&opts.axes), + ); + } + } + + Operation::Unsqueeze { options, .. } => { + // expand_dims: x, axes + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + if let Some(opts) = options + && !opts.axes.is_empty() + { + inputs.insert( + "axes".to_string(), + Self::create_immediate_int_array(&opts.axes), + ); + } + } + + Operation::ArgMax { axis, options, .. } | Operation::ArgMin { axis, options, .. } => { + // reduce_argmax/reduce_argmin: x, axis, keep_dims + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + inputs.insert("axis".to_string(), Self::create_immediate_int(*axis)); + if let Some(opts) = options { + inputs.insert( + "keep_dims".to_string(), + Self::create_immediate_bool(opts.keep_dimensions), + ); + } + // Note: outputDataType is handled by the output tensor's data type + } + + Operation::Cast { data_type: to, .. } => { + // cast: x, dtype + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // Add dtype parameter (required) + let to_type = to; + let dtype_string = match to_type { + MLOperandDataType::Float32 => "fp32", + MLOperandDataType::Float16 => "fp16", + MLOperandDataType::Int32 => "int32", + MLOperandDataType::Uint32 => "uint32", + MLOperandDataType::Int8 => "int8", + MLOperandDataType::Uint8 => "uint8", + MLOperandDataType::Int64 => "int64", + MLOperandDataType::Uint64 => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "Unsupported graph cast dtype Uint64".to_string(), + }); + } + MLOperandDataType::Int4 | MLOperandDataType::Uint4 => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "int4/uint4 cast targets are not supported in CoreML conversion" + .to_string(), + }); + } + }; + inputs.insert( + "dtype".to_string(), + Self::create_immediate_string(dtype_string), + ); + } + + Operation::ScatterElements { options, .. } => { + // scatter: data, indices, updates, axis, mode + // mode is required by CoreML (default "update"). + if input_names.len() >= 3 { + inputs.insert("data".to_string(), Self::create_argument(&input_names[0])); + inputs.insert( + "indices".to_string(), + Self::create_argument(&input_names[1]), + ); + inputs.insert( + "updates".to_string(), + Self::create_argument(&input_names[2]), + ); + } + + if let Some(opts) = options { + inputs.insert("axis".to_string(), Self::create_immediate_int(opts.axis)); + } + + // CoreML requires explicit mode and validate_indices. + inputs.insert("mode".to_string(), Self::create_immediate_string("update")); + inputs.insert( + "validate_indices".to_string(), + Self::create_immediate_bool(false), + ); + } + + Operation::ScatterND { .. } + // scatter_nd: data, indices, updates, mode (required by CoreML) + if input_names.len() >= 3 => { + inputs.insert("data".to_string(), Self::create_argument(&input_names[0])); + inputs.insert( + "indices".to_string(), + Self::create_argument(&input_names[1]), + ); + inputs.insert( + "updates".to_string(), + Self::create_argument(&input_names[2]), + ); + inputs.insert("mode".to_string(), Self::create_immediate_string("update")); + inputs.insert( + "validate_indices".to_string(), + Self::create_immediate_bool(false), + ); + } + + Operation::Tile { repetitions, .. } => { + // tile: x, reps + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // reps is required by CoreML even when all values are 1 + inputs.insert( + "reps".to_string(), + Self::create_immediate_int_array(repetitions), + ); + } + + Operation::CumulativeSum { axis, options, .. } => { + // cumsum: x, axis, exclusive, reverse + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); } - // MIL `conv` requires `strides`, `pad`, `dilations`, `groups` — all - // four are declared as required inputs in the MIL op schema, so - // Apple's CoreML loader rejects the model with - // "Required param '...' is missing" when any is omitted. Emit the - // WebNN defaults when the WebNN graph left them unset. - let (strides, dilations, padding, groups) = match options { - Some(o) => ( - if o.strides.is_empty() { - vec![1, 1] - } else { - o.strides.clone() - }, - if o.dilations.is_empty() { - vec![1, 1] - } else { - o.dilations.clone() - }, - if o.padding.is_empty() { - vec![0, 0, 0, 0] + inputs.insert("axis".to_string(), Self::create_int_argument(*axis as i32)); + if let Some(opts) = options { + inputs.insert( + "exclusive".to_string(), + Self::create_immediate_bool(opts.exclusive), + ); + inputs.insert( + "reverse".to_string(), + Self::create_immediate_bool(opts.reversed), + ); + } + } + + Operation::Reverse { options, .. } => { + // reverse: x, axes + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // Default behavior: reverse all axes when options.axes is omitted. + let axes_u32: Vec = match options.as_ref() { + Some(opts) => match opts.axes.as_ref() { + Some(axes) => axes.clone(), + None => { + if let Some(input_id) = op.input_operands().first() { + if let Some(input_operand) = graph.operand(*input_id) { + (0..input_operand.descriptor.shape.len()) + .map(|axis| axis as u32) + .collect() + } else { + Vec::new() + } + } else { + Vec::new() + } + } + }, + None => { + if let Some(input_id) = op.input_operands().first() { + if let Some(input_operand) = graph.operand(*input_id) { + (0..input_operand.descriptor.shape.len()) + .map(|axis| axis as u32) + .collect() + } else { + Vec::new() + } } else { - o.padding.clone() - }, - o.groups, - ), - None => (vec![1, 1], vec![1, 1], vec![0, 0, 0, 0], 1), + Vec::new() + } + } }; + + // Always provide axes, including empty arrays (explicit no-op). inputs.insert( - "strides".to_string(), - Self::create_immediate_int_array(&strides), + "axes".to_string(), + Self::create_immediate_int_array(&axes_u32), ); + } + + Operation::Triangular { options, .. } => { + // band_part: x, lower, upper + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + // CoreML band_part uses lower and upper bounds instead of upper/diagonal + let is_upper = options.as_ref().and_then(|o| o.upper).unwrap_or(true); + let diagonal = options.as_ref().map(|o| o.diagonal as i64).unwrap_or(0); + + // Convert WebNN (upper, diagonal) to CoreML (lower, upper) + // For upper triangle: keep diagonal and above + // For lower triangle: keep diagonal and below + let (lower_bound, upper_bound) = if is_upper { + // Upper triangle: remove elements below diagonal+k + (diagonal as i32, -1) // keep from diagonal+k upward + } else { + // Lower triangle: remove elements above diagonal+k + (-1, diagonal as i32) // keep from diagonal+k downward + }; + inputs.insert( - "dilations".to_string(), - Self::create_immediate_int_array(&dilations), + "lower".to_string(), + Self::create_immediate_int(lower_bound as u32), ); inputs.insert( - "pad".to_string(), - Self::create_immediate_int_array(&padding), + "upper".to_string(), + Self::create_immediate_int(upper_bound as u32), ); - inputs.insert("groups".to_string(), Self::create_immediate_int(groups)); + } + + Operation::ReduceLogSumExp { options, .. } => { + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + if let Some(opts) = options { + if let Some(axes) = opts.axes.as_ref() + && !axes.is_empty() + { + inputs.insert("axes".to_string(), Self::create_immediate_int_array(axes)); + } + inputs.insert( + "keep_dims".to_string(), + Self::create_immediate_bool(opts.keep_dimensions), + ); + } + } + + // Reduction operations: reduceSum, reduceMean, reduceMax, etc. + Operation::ReduceSum { options, .. } + | Operation::ReduceMean { options, .. } + | Operation::ReduceMax { options, .. } + | Operation::ReduceMin { options, .. } + | Operation::ReduceProduct { options, .. } + | Operation::ReduceL1 { options, .. } + | Operation::ReduceL2 { options, .. } + | Operation::ReduceLogSum { options, .. } + | Operation::ReduceSumSquare { options, .. } => { + // All reduce operations: x, axes, keep_dims + if !input_names.is_empty() { + inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + } + + if let Some(opts) = options { + if let Some(axes) = opts.axes.as_ref() + && !axes.is_empty() + { + inputs.insert("axes".to_string(), Self::create_immediate_int_array(axes)); + } + inputs.insert( + "keep_dims".to_string(), + Self::create_immediate_bool(opts.keep_dimensions), + ); + } + } + + Operation::GatherND { input, indices, .. } => { + // CoreML gather_nd crashes (SIGABRT) on rank-5+ inputs; guard against it. + if let Some(input_op) = graph.operand(*input) { + if input_op.descriptor.shape.len() > 4 { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "CoreML gather_nd does not support input rank > 4; got rank {}", + input_op.descriptor.shape.len() + ), + }); + } + } + // gather_nd: x (data), indices, validate_indices (required by CoreML) + inputs.insert( + "x".to_string(), + Self::create_argument(&operand_name(graph, *input)), + ); + inputs.insert( + "indices".to_string(), + Self::create_argument(&operand_name(graph, *indices)), + ); + inputs.insert( + "validate_indices".to_string(), + Self::create_immediate_bool(false), + ); + } + + // isNaN and isInfinite: single input 'x' + Operation::IsNaN { input, .. } | Operation::IsInfinite { input, .. } => { + inputs.insert( + "x".to_string(), + Self::create_argument(&operand_name(graph, *input)), + ); + } + + _ => {} + } + + Ok(inputs) + } + + /// Create a FeatureType for model description from an OperandDescriptor + fn create_feature_type( + descriptor: &crate::graph::OperandDescriptor, + ) -> Result { + use crate::protos::coreml::specification::{ArrayFeatureType, FeatureType, feature_type}; + + // Map WebNN data type to CoreML array data type + // CoreML feature descriptions (I/O) ONLY support: DOUBLE, FLOAT32, FLOAT16, INT32 + // Even though Int8 exists in protobuf enum, CoreML runtime rejects it + let array_data_type = match descriptor.data_type { + DataType::Float32 => { + crate::protos::coreml::specification::array_feature_type::ArrayDataType::Float32 + } + DataType::Float16 => { + crate::protos::coreml::specification::array_feature_type::ArrayDataType::Float16 + } + DataType::Int32 => { + crate::protos::coreml::specification::array_feature_type::ArrayDataType::Int32 + } + // Unsupported types - assume they have been converted to FLOAT32. + DataType::Int4 + | DataType::Uint4 + | DataType::Int8 + | DataType::Uint8 + | DataType::Uint32 + | DataType::Int64 + | DataType::Uint64 => { + crate::protos::coreml::specification::array_feature_type::ArrayDataType::Float32 + } + }; + + // Create array feature type with shape + let mut array_feature = ArrayFeatureType { + data_type: array_data_type as i32, + ..Default::default() + }; + + // Add shape dimensions + // CoreML requires explicit shape constraints - convert scalars (0D) to 1D [1] + // Following Chromium's approach for scalar handling + let shape_to_use = if descriptor.shape.is_empty() { + vec![1] // Scalar (0D) tensor -> [1] for CoreML compatibility + } else { + descriptor.static_or_max_shape() + }; + + for &dim in &shape_to_use { + array_feature.shape.push(dim as i64); + } + + Ok(FeatureType { + r#type: Some(feature_type::Type::MultiArrayType(array_feature)), + is_optional: false, + }) + } +} + +impl super::GraphConverter for CoremlMlProgramConverter { + fn format(&self) -> &'static str { + "coreml" + } - // Add pad_type - required parameter in CoreML - // Use "custom" when explicit padding is provided - inputs.insert( - "pad_type".to_string(), - Self::create_immediate_string("custom"), - ); - } + fn convert(&self, graph_info: &GraphInfo) -> Result { + if !crate::graph::dynamic_inputs_enabled() && graph_info.has_dynamic_dimensions() { + return Err(GraphError::DynamicInputsFeatureDisabled); + } - // Transposed convolution: input, filter + parameters - Operation::ConvTranspose2d { options, .. } => { - if input_names.len() >= 2 { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("weight".to_string(), Self::create_argument(&input_names[1])); - } + // Create weight file builder for Float16 constants + let mut weight_builder = super::WeightFileBuilder::new(); - // Add optional bias if present (third input) - if input_names.len() >= 3 { - inputs.insert("bias".to_string(), Self::create_argument(&input_names[2])); + // Create MLProgram + let mut program = Program { + version: 1, + ..Default::default() + }; + + // Create main function + let mut main_function = Function::default(); + + // Keep MLProgram boundary types aligned with CoreML feature-description + // restrictions. Unsupported WebNN I/O types (such as uint8) are exposed + // as float32 at the function boundary and cast to/from the internal + // graph representation inside the main block. + let mut operand_name_overrides: HashMap = HashMap::new(); + + for &output_id in &graph_info.output_operands { + if let Some(operand) = graph_info.operand(output_id) { + let graph_mil_type = Self::graph_value_mil_type(&operand.descriptor.data_type)?; + // Wide ints (int64/uint32/uint64) are int32 both inside the graph and + // at the interface (an int32 proxy), so they need no boundary cast. + let interface_mil_type = if Self::is_wide_int(&operand.descriptor.data_type) { + crate::protos::coreml::mil_spec::DataType::Int32 as i32 + } else { + Self::interface_mil_data_type(&operand.descriptor.data_type) + }; + if graph_mil_type != interface_mil_type { + let output_name = operand_name(graph_info, output_id); + operand_name_overrides.insert(output_id, format!("{}_graph", output_name)); } + } + } - // CoreML requires pad_type parameter (defaults to "custom" for explicit padding) - inputs.insert( - "pad_type".to_string(), - Self::create_immediate_string("custom"), - ); + // Add function inputs from graph inputs + for &input_id in &graph_info.input_operands { + let operand = + graph_info + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", input_id), + })?; + let input_name = operand_name(graph_info, input_id); + let value_type = Self::create_value_with_mil_type( + graph_info, + input_id, + input_name, + Self::interface_mil_data_type(&operand.descriptor.data_type), + )?; + main_function.inputs.push(value_type); + } - // MIL `conv_transpose` requires `strides`, `pad`, `dilations`, - // `groups` — same as `conv` above. Apple's loader emits - // "Required param '...' is missing" when any is dropped, even - // when the WebNN graph left the attribute at its default. - let (strides, dilations, padding, groups) = match options { - Some(o) => ( - if o.strides.is_empty() { - vec![1, 1] - } else { - o.strides.clone() - }, - if o.dilations.is_empty() { - vec![1, 1] - } else { - o.dilations.clone() - }, - if o.padding.is_empty() { - vec![0, 0, 0, 0] - } else { - o.padding.clone() - }, - o.groups, - ), - None => (vec![1, 1], vec![1, 1], vec![0, 0, 0, 0], 1), + // Create main block + let mut main_block = Block::default(); + // Tracks outputs that are proxied as int32 (e.g. argmin/argmax with int64 WebNN type). + // The final interface-cast loop uses this to cast to int32 rather than float32. + let mut int32_proxy_output_names: std::collections::HashSet = + std::collections::HashSet::new(); + + for &input_id in &graph_info.input_operands { + let operand = + graph_info + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", input_id), + })?; + // int4/uint4/int64/uint32/uint64 have no MIL tensor type; represent them + // internally as int32 (a proxy). The fp32 model input is cast to int32 here; + // the executor delivers the original integer values widened to float32. + let is_wide_int = Self::is_wide_int(&operand.descriptor.data_type); + let graph_mil_type = if is_wide_int { + crate::protos::coreml::mil_spec::DataType::Int32 as i32 + } else { + Self::mil_data_type(&operand.descriptor.data_type)? + }; + let interface_mil_type = Self::interface_mil_data_type(&operand.descriptor.data_type); + if graph_mil_type != interface_mil_type { + let input_name = operand_name(graph_info, input_id); + let graph_input_name = format!("{}_graph", input_name); + operand_name_overrides.insert(input_id, graph_input_name.clone()); + let graph_input_type = Self::create_value_with_mil_type( + graph_info, + input_id, + graph_input_name, + graph_mil_type, + )?; + let cast_str = if is_wide_int { + "int32" + } else { + Self::cast_dtype_string_for_graph_type(&operand.descriptor.data_type)? }; - inputs.insert( - "strides".to_string(), - Self::create_immediate_int_array(&strides), - ); - inputs.insert( - "dilations".to_string(), - Self::create_immediate_int_array(&dilations), - ); - inputs.insert( - "pad".to_string(), - Self::create_immediate_int_array(&padding), - ); - inputs.insert("groups".to_string(), Self::create_immediate_int(groups)); + main_block.operations.push(Self::create_cast_operation( + input_name, + graph_input_type, + cast_str, + )); + } + } - // MIL conv_transpose takes the complete output tensor shape, - // not only WebNN's two spatial outputSizes values. Chromium - // supplies this for every transposed convolution as well. - if let Some(output_id) = op.output_operand() - && let Some(output) = graph.operand(output_id) - { - let mut output_shape = output.descriptor.static_or_max_shape(); - let input_layout = options - .as_ref() - .map(|o| o.input_layout.as_str()) - .unwrap_or(""); - if input_layout.eq_ignore_ascii_case("nhwc") && output_shape.len() == 4 { - output_shape = vec![ - output_shape[0], - output_shape[3], - output_shape[1], - output_shape[2], - ]; + // MIL `quantize` / `dequantize` require: + // • `scale` (and `zero_point`) to be rank 0 (scalar) or rank 1 constants. + // • `zero_point` type to match the quantized tensor type (int8 or uint8). + // Some WebNN graphs store zero_point as Int32, which CoreML rejects. + // Pre-scan all quantize/dequantize operations to collect: + // scale_ids_to_squeeze — scale/zp operand IDs with rank > 1 (need shape squeezing) + // zp_id_to_dtype — zero_point operand IDs mapped to their required data type + let mut scale_ids_to_squeeze: std::collections::HashSet = + std::collections::HashSet::new(); + let mut zp_id_to_dtype: HashMap = HashMap::new(); + for op in &graph_info.operations { + let op_lower = op.op_type().to_lowercase(); + if matches!(op_lower.as_str(), "quantizelinear" | "dequantizelinear") { + let input_ids = op.input_operands(); + // Skip decomposed quantize/dequantize ops: they consume scale/zero_point in + // their native dtype and shape (see emit_*_decomposition), so the per-channel + // squeeze and int8 zero_point coercion below must not apply. + if Self::qdq_should_decompose(graph_info, op) { + continue; + } + // Squeeze shape for both scale (index 1) and zero_point (index 2). + // CoreML interprets a rank-1 scale as per-channel; for per-tensor semantics, + // scale/zp must be scalar. Squeeze rank > 1 AND rank-1 with single element [1]. + for ¶m_idx in &[1usize, 2usize] { + if let Some(¶m_id) = input_ids.get(param_idx) { + if let Some(param_operand) = graph_info.operand(param_id) { + let shape = ¶m_operand.descriptor.shape; + let needs_sq = shape.len() > 1 + || (shape.len() == 1 + && matches!(shape[0], GraphDimension::Static(1))); + if needs_sq { + scale_ids_to_squeeze.insert(param_id); + } + } + } + } + // Determine the expected zero_point data type. + // For quantize: zp type = output type (the quantized representation). + // For dequantize: zp type = input type (the quantized representation). + // CoreML only accepts int8 or uint8 for zero_point; coerce anything else + // (e.g. Int32 from some WebNN graphs) to the nearest compatible type. + // convert_zp_bytes handles Int32→Int8 and Int32→Uint8 byte reinterpretation. + let zp_webnn_type = if op_lower == "quantizelinear" { + op.output_operand() + .and_then(|id| graph_info.operand(id)) + .map(|o| o.descriptor.data_type.clone()) + } else { + // dequantize: first input is the quantized tensor + input_ids + .first() + .and_then(|&id| graph_info.operand(id)) + .map(|o| o.descriptor.data_type.clone()) + }; + let zp_expected_type = zp_webnn_type.map(|dt| match dt { + DataType::Uint8 => DataType::Uint8, + _ => DataType::Int8, + }); + if let (Some(&zp_id), Some(expected_dt)) = (input_ids.get(2), zp_expected_type) { + zp_id_to_dtype.insert(zp_id, expected_dt); + } + // For dequantize, CoreML also requires the INPUT (index 0) to be int8 or uint8. + // If the input is a constant Int32 tensor, coerce its bytes to int8/uint8 so + // CoreML accepts it. Non-constant Int32 inputs are handled with a cast op below. + if op_lower == "dequantizelinear" { + if let Some(&input_id) = input_ids.first() { + if let Some(input_op) = graph_info.operand(input_id) { + if matches!(input_op.descriptor.data_type, DataType::Int32) { + let target = DataType::Int8; + zp_id_to_dtype.insert(input_id, target); + } + } } - inputs.insert( - "output_shape".to_string(), - Self::create_immediate_int_array(&output_shape), - ); } } + } - // Pooling operations: input + parameters - Operation::AveragePool2d { - options: pool_opts, .. - } - | Operation::MaxPool2d { - options: pool_opts, .. - } => { - // CoreML MLProgram pooling path currently assumes NCHW input layout. - // Reject NHWC explicitly to avoid invalid model/runtime crashes. - let layout = pool_opts - .as_ref() - .map(|o| { - if o.layout.is_empty() { - "nchw" - } else { - o.layout.as_str() + // Gather/GatherElements: CoreML only accepts int32 (or smaller) for indices. + // If indices are a constant with uint32 or int64 type, coerce them to int32. + for op in &graph_info.operations { + let op_lower = op.op_type().to_lowercase(); + if op_lower == "gather" || op_lower == "gatherelements" { + if let Some(&idx_id) = op.input_operands().get(1) { + if let Some(idx_op) = graph_info.operand(idx_id) { + if matches!( + idx_op.descriptor.data_type, + DataType::Uint32 | DataType::Int64 + ) && idx_op.kind == crate::graph::OperandKind::Constant + { + zp_id_to_dtype.insert(idx_id, DataType::Int32); } - }) - .unwrap_or("nchw"); - if !layout.eq_ignore_ascii_case("nchw") { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML pooling currently supports only NCHW layout; got '{}' for {}", - layout, - op.op_type(), - ), - }); + } } + } + } - // WebNN `outputSizes` for pooling is not currently lowered to CoreML - // pooling parameters in this converter. Reject explicitly to avoid - // output-shape mismatches that can lead to runtime crashes. - if let Some(output_sizes) = pool_opts.as_ref().and_then(|o| o.output_sizes.as_ref()) - && !output_sizes.is_empty() - { - return Err(GraphError::ConversionFailed { + // Add constant operands as const operations + for (operand_id, constant_data) in &graph_info.constant_operand_ids_to_handles { + let operand = + graph_info + .operand(*operand_id) + .ok_or_else(|| GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML pooling with outputSizes is not supported yet; got {:?} for {}", - output_sizes, - op.op_type() - ), - }); - } + reason: format!("Constant operand {} not found", operand_id), + })?; + + // For quantize/dequantize scale and zero_point params, we may need to: + // (a) squeeze the shape to rank ≤ 1 (both scale and zp) + // (b) coerce the dtype to match the quantized tensor (zp only, e.g. Int32 → Uint8) + // Both may apply simultaneously; apply them together here. + let needs_squeeze = scale_ids_to_squeeze.contains(operand_id); + let needs_type_coerce = zp_id_to_dtype + .get(operand_id) + .filter(|expected| **expected != operand.descriptor.data_type) + .cloned(); + + if needs_squeeze || needs_type_coerce.is_some() { + use crate::graph::OperandDescriptor; + + // Compute the squeezed shape (or keep original if no squeezing needed). + let final_shape: Vec = if needs_squeeze { + let squeezed: Vec = operand + .descriptor + .shape + .iter() + .filter_map(|d| match d { + GraphDimension::Static(v) if *v != 1 => Some(*v), + _ => None, + }) + .collect(); + squeezed + .iter() + .map(|&v| GraphDimension::Static(v)) + .collect() + } else { + operand.descriptor.shape.clone() + }; + + // Compute the final dtype (and convert bytes if needed). + let (final_dtype, final_data_ref, _coerced_storage); + if let Some(expected_dtype) = needs_type_coerce { + let converted = convert_zp_bytes( + &constant_data.data, + &operand.descriptor.data_type, + &expected_dtype, + ); + _coerced_storage = crate::graph::ConstantData { + data: converted, + label: None, + }; + final_data_ref = &_coerced_storage; + final_dtype = expected_dtype; + } else { + _coerced_storage = crate::graph::ConstantData { + data: vec![], + label: None, + }; + final_data_ref = constant_data; + final_dtype = operand.descriptor.data_type.clone(); + }; + + let mut modified_operand = operand.clone(); + modified_operand.descriptor = OperandDescriptor { + data_type: final_dtype, + shape: final_shape, + pending_permutation: Vec::new(), + }; + let const_op = Self::create_const_operation( + graph_info, + *operand_id, + &modified_operand, + final_data_ref, + &mut weight_builder, + )?; + main_block.operations.push(const_op); + } else { + let const_op = Self::create_const_operation( + graph_info, + *operand_id, + operand, + constant_data, + &mut weight_builder, + )?; + main_block.operations.push(const_op); + } + } + + // Operations that must be inserted right after the op that produces their input operand. + // Keyed by the source operand_id. Value = (transpose ops, transposed_name). + // We intentionally do NOT set operand_name_overrides[id] until the deferred ops are + // emitted, so that the operation that *produces* the id still writes the original name. + let mut deferred_transposes: HashMap, String)> = HashMap::new(); + + // First pass: Handle filter layout transformations for conv operations + + for op in &graph_info.operations { + let op_type_lower = op.op_type().to_lowercase(); + + if (op_type_lower == "conv2d" || op_type_lower == "convtranspose2d") + && op.input_operands().len() >= 2 + { + let filter_layout = match &op { + Operation::Conv2d { options, .. } => options + .as_ref() + .map(|o| o.filter_layout.as_str()) + .unwrap_or(""), + Operation::ConvTranspose2d { options, .. } => options + .as_ref() + .map(|o| o.filter_layout.as_str()) + .unwrap_or(""), + _ => "", + }; + if !filter_layout.is_empty() { + let expected_layout = if op_type_lower == "conv2d" { + "oihw" + } else { + "iohw" + }; - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + if filter_layout != expected_layout { + let filter_operand_id = op.input_operands()[1]; - // outputShapeRounding: "floor" (default) or "ceil" - let ceil_mode = pool_opts - .as_ref() - .map(|o| o.output_shape_rounding.eq_ignore_ascii_case("ceil")) - .unwrap_or(false); - inputs.insert( - "ceil_mode".to_string(), - Self::create_immediate_bool(ceil_mode), - ); + if let Some(filter_operand) = graph_info.operand(filter_operand_id) { + // Calculate transpose permutation + let perm = match (op_type_lower.as_str(), filter_layout) { + // Conv2d conversions to oihw [O, I, H, W] + ("conv2d", "hwio") => vec![3, 2, 0, 1], // [H, W, I, O] -> [O, I, H, W] + ("conv2d", "ohwi") => vec![0, 3, 1, 2], // [O, H, W, I] -> [O, I, H, W] + ("conv2d", "ihwo") => vec![3, 0, 1, 2], // [I, H, W, O] -> [O, I, H, W] - // Only average pooling accepts this parameter. - if matches!(&op, Operation::AveragePool2d { .. }) { - inputs.insert( - "exclude_padding_from_average".to_string(), - Self::create_immediate_bool(false), - ); - } + // Conv_transpose2d conversions to iohw [I, O, H, W] + ("convtranspose2d", "hwoi") => vec![3, 2, 0, 1], // [H, W, O, I] -> [I, O, H, W] + ("convtranspose2d", "ohwi") => vec![3, 0, 1, 2], // [O, H, W, I] -> [I, O, H, W] + ("convtranspose2d", "hwio") => vec![2, 3, 0, 1], // [H, W, I, O] -> [I, O, H, W] - // Add parameters from operator options - if let Some(opts) = pool_opts.as_ref() { - if let Some(window_dimensions) = opts.window_dimensions.as_ref() - && !window_dimensions.is_empty() - { - inputs.insert( - "kernel_sizes".to_string(), - Self::create_immediate_int_array(window_dimensions), - ); - } - if !opts.strides.is_empty() { - inputs.insert( - "strides".to_string(), - Self::create_immediate_int_array(&opts.strides), - ); - } - if !opts.dilations.is_empty() && opts.dilations.iter().any(|&d| d != 1) { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML pooling does not support non-default dilations; got {:?} for {}", - opts.dilations, - op.op_type() - ), - }); - } - if !opts.padding.is_empty() { - inputs.insert( - "pad".to_string(), - Self::create_immediate_int_array(&opts.padding), - ); - inputs.insert( - "pad_type".to_string(), - Self::create_immediate_string("custom"), - ); - } else { - inputs.insert( - "pad_type".to_string(), - Self::create_immediate_string("same"), - ); - } - } else { - inputs.insert( - "pad_type".to_string(), - Self::create_immediate_string("same"), - ); - } - } + _ => continue, // Skip unsupported layouts + }; - // Layer normalization (different from batch/instance normalization) - Operation::LayerNormalization { options, .. } => { - // Check if axes is empty - CoreML doesn't support empty axes - // Following Chromium (graph_builder_coreml.cc:4000-4019): - // When axes is empty, mean equals input, so output = bias + (scale * 0) - let axes_vec: Vec = options - .as_ref() - .and_then(|o| o.axes.as_ref()) - .map(|ax| ax.iter().map(|&u| u as i32).collect()) - .unwrap_or_default(); - - if axes_vec.is_empty() { - // Empty axes case: use sub operation (input - input = 0) - // CoreML doesn't support empty axes, so we emulate it - // Note: This will be handled by inserting a sub operation in convert_operation - // For now, return error as this needs special multi-operation handling - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: "CoreML layer_norm with empty axes requires special handling (not yet implemented)".to_string(), - }); - } + // Create transpose operation for filter + let filter_name = operand_name(graph_info, filter_operand_id); + let transposed_filter_name = format!("{}_transposed", filter_name); - // Add input operand (only x) - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + let mut transpose_inputs: HashMap = HashMap::new(); + transpose_inputs + .insert("x".to_string(), Self::create_name_argument(filter_name)); + transpose_inputs.insert( + "perm".to_string(), + Self::create_immediate_int_array(&perm), + ); - // Scale (gamma) is optional (2nd input) - // CoreML requires scale/bias to be constant tensors (not graph inputs) - // Following Chromium: TODO(crbug.com/338529226) - these params must be constant - if input_names.len() >= 2 && op.input_operands().len() >= 2 { - let scale_operand_id = op.input_operands()[1]; - if let Some(scale_operand) = graph.operand(scale_operand_id) - && scale_operand.kind != crate::graph::OperandKind::Constant - { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: "CoreML layer_norm requires scale (gamma) parameter to be a constant tensor, not a graph input".to_string(), - }); - } - inputs.insert("gamma".to_string(), Self::create_argument(&input_names[1])); - } + // Create tensor type for transposed filter + let dtype = Self::mil_data_type(&filter_operand.descriptor.data_type)?; + let transposed_shape = + Self::permute_graph_shape(&filter_operand.descriptor.shape, &perm); + let dimensions = + Self::mil_dimensions_from_graph_shape(&transposed_shape, false); - // Bias (beta) is optional (3rd input) - if input_names.len() >= 3 && op.input_operands().len() >= 3 { - let bias_operand_id = op.input_operands()[2]; - if let Some(bias_operand) = graph.operand(bias_operand_id) - && bias_operand.kind != crate::graph::OperandKind::Constant - { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: "CoreML layer_norm requires bias (beta) parameter to be a constant tensor, not a graph input".to_string(), - }); - } - inputs.insert("beta".to_string(), Self::create_argument(&input_names[2])); - } + let value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: dimensions.len() as i64, + data_type: dtype, + dimensions, + attributes: HashMap::new(), + }, + ), + ), + }; - // Add axes parameter (REQUIRED by CoreML, must not be empty) - inputs.insert( - "axes".to_string(), - Self::create_int_array_argument(axes_vec), - ); + let transpose_output_type = NamedValueType { + name: transposed_filter_name.clone(), + r#type: Some(value_type), + }; - if let Some(opts) = options { - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(opts.epsilon as f32), - ); - } - } + let transpose_op = Self::create_mil_operation( + "transpose", + transpose_inputs, + vec![transpose_output_type], + ); - // Batch/instance normalization (have mean, variance inputs) - Operation::BatchNormalization { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - if input_names.len() >= 2 && op.input_operands().len() >= 2 { - let mean_operand_id = op.input_operands()[1]; - if let Some(mean_operand) = graph.operand(mean_operand_id) - && mean_operand.kind != crate::graph::OperandKind::Constant - { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML {} requires mean parameter to be a constant tensor, not a graph input", - op.op_type() - ), - }); - } - inputs.insert("mean".to_string(), Self::create_argument(&input_names[1])); - } - if input_names.len() >= 3 && op.input_operands().len() >= 3 { - let variance_operand_id = op.input_operands()[2]; - if let Some(variance_operand) = graph.operand(variance_operand_id) - && variance_operand.kind != crate::graph::OperandKind::Constant - { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML {} requires variance parameter to be a constant tensor, not a graph input", - op.op_type() - ), - }); - } - inputs.insert( - "variance".to_string(), - Self::create_argument(&input_names[2]), - ); - } - if let Some(opts) = options { - if let Some(sid) = opts.scale { - inputs.insert( - "gamma".to_string(), - Self::create_argument(&operand_name(graph, sid)), - ); - } else if input_names.len() >= 4 { - inputs.insert("gamma".to_string(), Self::create_argument(&input_names[3])); - } - if let Some(bid) = opts.bias { - inputs.insert( - "beta".to_string(), - Self::create_argument(&operand_name(graph, bid)), - ); - } else if input_names.len() >= 5 { - inputs.insert("beta".to_string(), Self::create_argument(&input_names[4])); - } - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(opts.epsilon as f32), - ); - } - } - Operation::InstanceNormalization { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - if input_names.len() >= 2 && op.input_operands().len() >= 2 { - let mean_operand_id = op.input_operands()[1]; - if let Some(mean_operand) = graph.operand(mean_operand_id) - && mean_operand.kind != crate::graph::OperandKind::Constant - { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML {} requires mean parameter to be a constant tensor, not a graph input", - op.op_type() - ), - }); - } - inputs.insert("mean".to_string(), Self::create_argument(&input_names[1])); - } - if input_names.len() >= 3 && op.input_operands().len() >= 3 { - let variance_operand_id = op.input_operands()[2]; - if let Some(variance_operand) = graph.operand(variance_operand_id) - && variance_operand.kind != crate::graph::OperandKind::Constant - { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML {} requires variance parameter to be a constant tensor, not a graph input", - op.op_type() - ), - }); + // If the filter operand is a constant or graph input it has already + // been emitted, so the transpose and the name override can go here. + // If it is an intermediate (e.g. output of dequantizeLinear in a QDQ + // graph) the producing operation hasn't been emitted yet — defer the + // transpose until right after that operation, and defer the override + // too (so the producing op writes the *original* name, not the + // transposed name). + if matches!( + filter_operand.kind, + OperandKind::Constant | OperandKind::Input + ) { + // Override can be set now; the filter has already been emitted. + operand_name_overrides + .insert(filter_operand_id, transposed_filter_name.clone()); + main_block.operations.push(transpose_op); + } else { + // Do NOT set the override yet; set it after the deferred op fires. + deferred_transposes + .entry(filter_operand_id) + .or_insert_with(|| (Vec::new(), transposed_filter_name.clone())) + .0 + .push(transpose_op); + } + } } - inputs.insert( - "variance".to_string(), - Self::create_argument(&input_names[2]), - ); - } - if input_names.len() >= 4 { - inputs.insert("gamma".to_string(), Self::create_argument(&input_names[3])); - } - if input_names.len() >= 5 { - inputs.insert("beta".to_string(), Self::create_argument(&input_names[4])); - } - if let Some(opts) = options { - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(opts.epsilon as f32), - ); } - } - Operation::Concat { axis, .. } => { - // concat: values (variadic list of tensors), axis - // CoreML expects a single 'values' parameter containing a tuple of all inputs - if !input_names.is_empty() { - inputs.insert( - "values".to_string(), - Self::create_argument_tuple(input_names), - ); - } + let input_layout = match &op { + Operation::Conv2d { options, .. } => options + .as_ref() + .map(|o| o.input_layout.as_str()) + .unwrap_or(""), + Operation::ConvTranspose2d { options, .. } => options + .as_ref() + .map(|o| o.input_layout.as_str()) + .unwrap_or(""), + _ => "", + }; + if input_layout == "nhwc" && !op.input_operands().is_empty() { + let input_operand_id = op.input_operands()[0]; - inputs.insert("axis".to_string(), Self::create_immediate_int(*axis)); - inputs.insert("interleave".to_string(), Self::create_immediate_bool(false)); - } + // Only transpose if not already transposed + if !operand_name_overrides.contains_key(&input_operand_id) + && let Some(input_operand) = graph_info.operand(input_operand_id) + { + // NHWC -> NCHW transposition: [0, 3, 1, 2] + let perm = [0, 3, 1, 2]; - Operation::Slice { - starts, - sizes, - options, - .. - } => { - // slice_by_size: x, begin, size. Both `begin` and `size` are - // declared required inputs in MIL's slice_by_size schema. Apple - // rejects the model with "Required param 'size' is missing" - // when an empty-shape no-op slice (0D scalar, WPT surfaces this) - // is emitted without the fields. Emit them as empty int32 arrays - // in that case so the MIL loader sees the param even though the - // tensor is rank-0. - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + // Create transpose operation for input + let input_name = operand_name(graph_info, input_operand_id); + let transposed_input_name = format!("{}_nchw", input_name); - inputs.insert( - "begin".to_string(), - Self::create_immediate_int_array(starts), - ); - let sizes_u32: Vec = sizes.iter().map(|d| d.static_or_max()).collect(); - inputs.insert( - "size".to_string(), - Self::create_immediate_int_array(&sizes_u32), - ); - let _ = options; - } + let mut transpose_inputs: HashMap = HashMap::new(); + transpose_inputs + .insert("x".to_string(), Self::create_name_argument(input_name)); + transpose_inputs.insert( + "perm".to_string(), + Self::create_immediate_int_array(perm.as_ref()), + ); - Operation::Expand { new_shape, .. } => { - // CoreML tile operation requires input rank to match reps length - // If reshape was added before this operation, use reshaped input name - // Otherwise use original input + // Create tensor type for transposed input + let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; + let transposed_shape = + Self::permute_graph_shape(&input_operand.descriptor.shape, &perm); + let dimensions = + Self::mil_dimensions_from_graph_shape(&transposed_shape, false); - if let Some(new_shape_u32) = (!new_shape.is_empty()).then(|| { - new_shape - .iter() - .map(MLDimension::static_or_max) - .collect::>() - }) { - // Get input operand shape - if !op.input_operands().is_empty() - && let Some(input_operand) = graph.operand(op.input_operands()[0]) - { - let input_shape = input_operand.descriptor.static_or_max_shape(); - let input_rank = input_shape.len(); - let output_rank = new_shape_u32.len(); + let value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: dimensions.len() as i64, + data_type: dtype, + dimensions, + attributes: HashMap::new(), + }, + ), + ), + }; - // Determine input name for tile operation - let tile_input_name = if input_rank < output_rank { - // A reshape was added, use the reshaped output name - // The reshape output name is: {input_name}_expand_reshaped - format!("{}_expand_reshaped", input_names[0]) - } else { - // No reshape, use original input - input_names[0].clone() + let transpose_output_type = NamedValueType { + name: transposed_input_name.clone(), + r#type: Some(value_type), }; - inputs.insert("x".to_string(), Self::create_name_argument(tile_input_name)); + let transpose_op = Self::create_mil_operation( + "transpose", + transpose_inputs, + vec![transpose_output_type], + ); - // Create reshaped dimensions (right-aligned, padded with 1s on left) - let mut reshaped_dims = vec![1u32; output_rank]; - for i in 0..input_rank { - reshaped_dims[output_rank - i - 1] = input_shape[input_rank - i - 1]; + // Same defer logic as for the filter: for constants/inputs the + // source has already been emitted so the override and transpose go now. + // For intermediates, defer both until the producing op is emitted. + if matches!( + input_operand.kind, + OperandKind::Constant | OperandKind::Input + ) { + operand_name_overrides + .insert(input_operand_id, transposed_input_name.clone()); + main_block.operations.push(transpose_op); + } else { + deferred_transposes + .entry(input_operand_id) + .or_insert_with(|| (Vec::new(), transposed_input_name.clone())) + .0 + .push(transpose_op); } + } + } + } + } - // Calculate reps: reps[i] = output_shape[i] / reshaped_input_shape[i] - let reps: Vec = new_shape_u32 - .iter() - .zip(reshaped_dims.iter()) - .map(|(&output_dim, &reshaped_dim)| { - if reshaped_dim == output_dim { - 1 - } else if reshaped_dim == 1 { - output_dim as i32 - } else { - // Should not happen - dimensions must match or input must be 1 - 1 - } - }) - .collect(); + // Convert all operations to MIL operations + for op in &graph_info.operations { + let op_type_lower = op.op_type().to_lowercase(); - inputs.insert("reps".to_string(), Self::create_int_array_argument(reps)); + // Rank-0 (scalar) no-ops: transpose/tile/slice/expand/pad/reshape that map a + // 0D scalar to a 0D scalar. CoreML rejects those ops on rank-0 tensors, so emit + // an identity (input * 1) instead. + if matches!( + op_type_lower.as_str(), + "transpose" | "tile" | "slice" | "expand" | "pad" | "reshape" + ) { + if let (Some(&in_id), Some(out_id)) = + (op.input_operands().first(), op.output_operand()) + { + let in_scalar = graph_info + .operand(in_id) + .map(|o| o.descriptor.shape.is_empty()) + .unwrap_or(false); + let out_op = graph_info.operand(out_id); + let out_scalar = out_op + .map(|o| o.descriptor.shape.is_empty()) + .unwrap_or(false); + let is_float = out_op + .map(|o| { + matches!( + o.descriptor.data_type, + DataType::Float32 | DataType::Float16 + ) + }) + .unwrap_or(false); + if in_scalar && out_scalar && is_float { + let in_name = Self::output_name_for_operand( + graph_info, + in_id, + &operand_name_overrides, + ); + let (_out_name, out_type) = + Self::create_output_value(graph_info, out_id, &operand_name_overrides)?; + // Reshape to [1] (create_output_value promotes 0D scalars to [1], + // matching the model's rank-1 boundary), which is a no-op copy. + let mut ri = HashMap::new(); + ri.insert("x".to_string(), Self::create_name_argument(in_name)); + ri.insert( + "shape".to_string(), + Self::create_immediate_int_array(&[1u32]), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + ri, + vec![out_type], + )); + continue; } } } - Operation::Gather { options, .. } => { - // gather: x (data), indices, axis, validate_indices - // CoreML uses 'x' for the data input, not 'params' - if input_names.len() >= 2 { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert( - "indices".to_string(), - Self::create_argument(&input_names[1]), - ); + if matches!( + op_type_lower.as_str(), + "equal" + | "greater" + | "greaterorequal" + | "lesser" + | "lesserorequal" + | "logicalnot" + | "logicaland" + | "logicalor" + | "logicalxor" + | "notequal" + | "isnan" + | "isinfinite" + ) { + use crate::protos::coreml::mil_spec::DataType as MilDataType; + + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "CoreML MLProgram".to_string(), + reason: format!("operation '{}' has no output operand", op.op_type()), + })?; + let output_operand = + graph_info + .operand(output_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Output operand {} not found", output_id), + })?; + if output_operand.descriptor.data_type != DataType::Uint8 { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "CoreML logical op '{}' expects uint8 graph output, got {:?}", + op.op_type(), + output_operand.descriptor.data_type + ), + }); } - // Add axis parameter (REQUIRED by CoreML, defaults to 0) - let axis = options.as_ref().map(|o| o.axis).unwrap_or(0); - inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + let bool_output_name = format!("{}_bool", output_name); + let bool_output_type = Self::create_value_with_mil_type( + graph_info, + output_id, + bool_output_name.clone(), + MilDataType::Bool as i32, + )?; - // Add validate_indices parameter (required by CoreML) - // Chromium sets this to false to avoid validation issues - // TODO: Handle negative and out-of-bounds indices properly - inputs.insert( - "validate_indices".to_string(), - Self::create_immediate_bool(false), - ); - } + let mut input_names = + Self::input_names_for_operation(graph_info, op, &operand_name_overrides); - Operation::GatherElements { options, .. } => { - // gather_along_axis: x, indices, axis, validate_indices - if input_names.len() >= 2 { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - inputs.insert( - "indices".to_string(), - Self::create_argument(&input_names[1]), + if matches!( + op_type_lower.as_str(), + "logicalnot" | "logicaland" | "logicalor" | "logicalxor" + ) { + for (index, &input_id) in op.input_operands().iter().enumerate() { + let input_operand = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", input_id), + } + })?; + if input_operand.descriptor.data_type == DataType::Uint8 { + let bool_input_name = format!("{}_bool", input_names[index]); + let bool_input_type = Self::create_value_with_mil_type( + graph_info, + input_id, + bool_input_name.clone(), + MilDataType::Bool as i32, + )?; + main_block.operations.push(Self::create_cast_operation( + input_names[index].clone(), + bool_input_type, + "bool", + )); + input_names[index] = bool_input_name; + } + } + } + + if op_type_lower == "notequal" { + let equal_output_name = format!("{}_equal", output_name); + let equal_output_type = Self::create_value_with_mil_type( + graph_info, + output_id, + equal_output_name.clone(), + MilDataType::Bool as i32, + )?; + + let mut equal_inputs = HashMap::new(); + equal_inputs.insert( + "x".to_string(), + Self::create_name_argument(input_names[0].clone()), + ); + equal_inputs.insert( + "y".to_string(), + Self::create_name_argument(input_names[1].clone()), + ); + main_block.operations.push(Self::create_mil_operation( + mil_ops::EQUAL, + equal_inputs, + vec![equal_output_type], + )); + + let mut not_inputs = HashMap::new(); + not_inputs.insert( + "x".to_string(), + Self::create_name_argument(equal_output_name), + ); + main_block.operations.push(Self::create_mil_operation( + mil_ops::LOGICAL_NOT, + not_inputs, + vec![bool_output_type], + )); + } else if op_type_lower == "isnan" { + // isNaN(x): NaN is the only value where x != x. Use equal(x,x) then not(). + let input_name = input_names.first().cloned().unwrap_or_default(); + let eq_name = format!("{}_isnan_eq", output_name); + let eq_type = Self::create_value_with_mil_type( + graph_info, + output_id, + eq_name.clone(), + MilDataType::Bool as i32, + )?; + let mut eq_inputs = HashMap::new(); + eq_inputs.insert( + "x".to_string(), + Self::create_name_argument(input_name.clone()), ); + eq_inputs.insert("y".to_string(), Self::create_name_argument(input_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::EQUAL, + eq_inputs, + vec![eq_type], + )); + let mut not_inputs = HashMap::new(); + not_inputs.insert("x".to_string(), Self::create_name_argument(eq_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::LOGICAL_NOT, + not_inputs, + vec![bool_output_type], + )); + } else if op_type_lower == "isinfinite" { + // isInfinite(x): abs(x) > f32::MAX is true only for ±inf; NaN > anything = false. + let input_name = input_names.first().cloned().unwrap_or_default(); + let abs_name = format!("{}_isinf_abs", output_name); + // Determine input dtype for abs output type + let input_dtype = op + .input_operands() + .first() + .and_then(|&id| graph_info.operand(id)) + .map(|o| &o.descriptor.data_type) + .cloned() + .unwrap_or(DataType::Float32); + let abs_mil_dtype = Self::mil_data_type(&input_dtype)?; + let abs_type = Self::create_value_with_mil_type( + graph_info, + output_id, + abs_name.clone(), + abs_mil_dtype, + )?; + let mut abs_inputs = HashMap::new(); + abs_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::ABS, + abs_inputs, + vec![abs_type], + )); + // f32::MAX is the largest finite float32; the only float32 > f32::MAX is +inf. + let max_val = if input_dtype == DataType::Float16 { + 65504.0_f32 // f16::MAX + } else { + f32::MAX + }; + let max_val_arg = if input_dtype == DataType::Float16 { + Self::create_immediate_float16(max_val) + } else { + Self::create_immediate_float(max_val) + }; + let mut gt_inputs = HashMap::new(); + gt_inputs.insert("x".to_string(), Self::create_name_argument(abs_name)); + gt_inputs.insert("y".to_string(), max_val_arg); + main_block.operations.push(Self::create_mil_operation( + mil_ops::GREATER, + gt_inputs, + vec![bool_output_type], + )); + } else { + let mil_op = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &input_names, + vec![bool_output_type], + self.get_mil_op_type(op.op_type())?, + )?; + main_block.operations.push(mil_op); } - let axis = options.as_ref().map(|o| o.axis).unwrap_or(0); - inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); - - inputs.insert( - "validate_indices".to_string(), - Self::create_immediate_bool(false), - ); + main_block.operations.push(Self::create_cast_operation( + bool_output_name, + output_type, + "uint8", + )); + continue; } - Operation::Split { - splits, options, .. - } => { - // split: x, num_splits or split_sizes, axis - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - if let Some(opts) = options { - if splits.is_empty() { - inputs.insert( - "num_splits".to_string(), - Self::create_immediate_int(op.output_operands().len() as u32), + // Prelu decomposition: select(x >= 0, x, x * alpha). + // CoreML native prelu has strict constraints: alpha must be 1D const, x rank >= 2. + // The element-wise decomposition handles all shapes, broadcasting, and non-constant + // alpha without any of those constraints. + if op_type_lower == "prelu" { + if let (Some(&x_id), Some(&alpha_id)) = + (op.input_operands().first(), op.input_operands().get(1)) + { + if let Some(output_id) = op.output_operand() { + let x_operand = graph_info.operand(x_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("prelu input operand {} not found", x_id), + } + })?; + let input_dtype = &x_operand.descriptor.data_type; + let is_float16 = *input_dtype == DataType::Float16; + // Use the graph proxy type (int32 for wide/int4 ints) so intermediate + // tensors and the zero comparand share a MIL-representable type. + let mil_dtype = Self::graph_value_mil_type(input_dtype)?; + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let x_name = Self::output_name_for_operand( + graph_info, + x_id, + &operand_name_overrides, ); - } else { - inputs.insert( - "split_sizes".to_string(), - Self::create_immediate_int_array(splits), + let alpha_name = Self::output_name_for_operand( + graph_info, + alpha_id, + &operand_name_overrides, ); - } - inputs.insert("axis".to_string(), Self::create_immediate_int(opts.axis)); - } - } - Operation::Where { .. } - // select: cond, a (true_value), b (false_value) - if input_names.len() >= 3 => { - inputs.insert("cond".to_string(), Self::create_argument(&input_names[0])); - inputs.insert("a".to_string(), Self::create_argument(&input_names[1])); - inputs.insert("b".to_string(), Self::create_argument(&input_names[2])); - } + // cond = greater_equal(x, 0): bool, same shape as x (not the broadcast output) + let cond_name = format!("{}_prelu_cond", output_name); + let cond_type = Self::create_value_with_mil_type( + graph_info, + x_id, + cond_name.clone(), + crate::protos::coreml::mil_spec::DataType::Bool as i32, + )?; + // The comparand must share x's MIL type (float vs int proxy). + use crate::protos::coreml::mil_spec::DataType as MilDt; + let zero_arg = if mil_dtype == MilDt::Float16 as i32 { + Self::create_immediate_float16(0.0) + } else if mil_dtype == MilDt::Float32 as i32 { + Self::create_immediate_float(0.0) + } else { + Self::create_immediate_int(0) + }; + let _ = is_float16; + let mut cond_inputs = HashMap::new(); + cond_inputs + .insert("x".to_string(), Self::create_name_argument(x_name.clone())); + cond_inputs.insert("y".to_string(), zero_arg); + main_block.operations.push(Self::create_mil_operation( + mil_ops::GREATER_EQUAL, + cond_inputs, + vec![cond_type], + )); - Operation::Pad { - beginning_padding, - ending_padding, - options, - .. - } => { - // pad: x, pad, mode, constant_val - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - if let Some(opts) = options { - // CoreML expects pad as [begin_0, end_0, begin_1, end_1, ...] - let pad: Vec = beginning_padding - .iter() - .zip(ending_padding.iter()) - .flat_map(|(a, b)| [*a, *b]) - .collect(); - if !pad.is_empty() { - inputs.insert("pad".to_string(), Self::create_immediate_int_array(&pad)); - } - if let Some(v) = Self::parse_mlnumber_f64(opts.value.as_ref()) { - inputs.insert( - "constant_val".to_string(), - Self::create_immediate_float(v as f32), - ); - } - } - // Add mode parameter (defaults to "constant") - // CoreML pad modes: "constant", "reflect", "replicate" - // WebNN modes: "constant", "edge", "reflection", "symmetric" - } + // neg_branch = mul(x, alpha): same dtype as x + let neg_name = format!("{}_prelu_neg", output_name); + let neg_type = Self::create_value_with_mil_type( + graph_info, + output_id, + neg_name.clone(), + mil_dtype, + )?; + let mut mul_inputs = HashMap::new(); + mul_inputs + .insert("x".to_string(), Self::create_name_argument(x_name.clone())); + mul_inputs.insert("y".to_string(), Self::create_name_argument(alpha_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::MUL, + mul_inputs, + vec![neg_type], + )); - Operation::Gelu { .. } => { - // gelu: x, mode - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - // watchOS MIL loader rejects gelu without an explicit mode ("Required - // param 'mode' is missing"); iOS/macOS loaders accept the default. - // WebNN spec has no mode parameter — exact (erf) is the implicit default. - inputs.insert("mode".to_string(), Self::create_immediate_string("EXACT")); - } + // output = select(cond, x, neg_branch) + let mut sel_inputs = HashMap::new(); + sel_inputs + .insert("cond".to_string(), Self::create_name_argument(cond_name)); + sel_inputs.insert("a".to_string(), Self::create_name_argument(x_name)); + sel_inputs.insert("b".to_string(), Self::create_name_argument(neg_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::WHERE, + sel_inputs, + vec![output_type], + )); - Operation::Squeeze { options, .. } => { - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - if let Some(opts) = options - && !opts.axes.is_empty() - { - inputs.insert( - "axes".to_string(), - Self::create_immediate_int_array(&opts.axes), - ); + continue; + } } } - Operation::Unsqueeze { options, .. } => { - // expand_dims: x, axes - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + // Special handling for clamp with equal bounds. + // CoreML clip rejects alpha == beta, while WebNN clamp(min==max) is valid and + // should produce a constant tensor. Lower as: output = input * 0 + bound. + if op_type_lower == "clamp" { + let (min_value, max_value) = match &op { + Operation::Clamp { options, .. } => options + .as_ref() + .map(|o| { + ( + Self::parse_clamp_bound(o.min_value.as_ref(), f64::NEG_INFINITY), + Self::parse_clamp_bound(o.max_value.as_ref(), f64::INFINITY), + ) + }) + .unwrap_or((f64::NEG_INFINITY, f64::INFINITY)), + _ => (f64::NEG_INFINITY, f64::INFINITY), + }; - if let Some(opts) = options - && !opts.axes.is_empty() + // For integer inputs (int8, uint8, int32), CoreML clip only accepts float. + // Cast: int → fp32, clip, cast back to int. + // Only int8/uint8/int32 are supported because CoreML cast cannot produce uint32/int64. { - inputs.insert( - "axes".to_string(), - Self::create_immediate_int_array(&opts.axes), - ); - } - } - - Operation::ArgMax { axis, options, .. } | Operation::ArgMin { axis, options, .. } => { - // reduce_argmax/reduce_argmin: x, axis, keep_dims - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } - - inputs.insert("axis".to_string(), Self::create_immediate_int(*axis)); - if let Some(opts) = options { - inputs.insert( - "keep_dims".to_string(), - Self::create_immediate_bool(opts.keep_dimensions), - ); - } - // Note: outputDataType is handled by the output tensor's data type - } - - Operation::Cast { data_type: to, .. } => { - // cast: x, dtype - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + let input_id = op.input_operands().first().copied(); + let output_id_opt = op.output_operand(); + if let (Some(input_id), Some(output_id)) = (input_id, output_id_opt) { + if let Some(input_op) = graph_info.operand(input_id) { + let int_dtype = &input_op.descriptor.data_type; + // Skip the fp32 clip path for equal bounds; that degenerate + // case is lowered separately (CoreML clip rejects alpha == beta). + let is_castable_int = + Self::is_castable_int(int_dtype) && min_value != max_value; + if is_castable_int { + let int_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + + // Cast int input to float32 + let float_name = format!("{}_clamp_float", output_name); + let float_type = Self::create_value_with_mil_type( + graph_info, + output_id, + float_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + main_block.operations.push(Self::create_cast_operation( + int_name, float_type, "fp32", + )); + + // Clip in float32 + let clipped_name = format!("{}_clamp_clipped", output_name); + let clipped_type = Self::create_value_with_mil_type( + graph_info, + output_id, + clipped_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + let mut clip_inputs = HashMap::new(); + clip_inputs.insert( + "x".to_string(), + Self::create_name_argument(float_name), + ); + clip_inputs.insert( + "alpha".to_string(), + Self::create_immediate_float(min_value as f32), + ); + clip_inputs.insert( + "beta".to_string(), + Self::create_immediate_float(max_value as f32), + ); + main_block.operations.push(Self::create_mil_operation( + mil_ops::CLIP, + clip_inputs, + vec![clipped_type], + )); + + // WebNN saturates the clamp result to the output type's + // range (a min/max bound outside the type range still yields + // an in-range value); CoreML's int cast would wrap instead. + // Clip to [type_min, type_max] before casting. (No-op when + // the bounds already lie inside the type range.) + let (sat_min, sat_max) = match int_dtype { + DataType::Int8 => (-128.0f32, 127.0f32), + DataType::Uint8 => (0.0f32, 255.0f32), + // int32 and the int32 proxies span the full int32 range. + _ => (i32::MIN as f32, i32::MAX as f32), + }; + let sat_name = format!("{}_clamp_sat", output_name); + let sat_type = Self::create_value_with_mil_type( + graph_info, + output_id, + sat_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + let mut sat_inputs = HashMap::new(); + sat_inputs.insert( + "x".to_string(), + Self::create_name_argument(clipped_name), + ); + sat_inputs.insert( + "alpha".to_string(), + Self::create_immediate_float(sat_min), + ); + sat_inputs.insert( + "beta".to_string(), + Self::create_immediate_float(sat_max), + ); + main_block.operations.push(Self::create_mil_operation( + mil_ops::CLIP, + sat_inputs, + vec![sat_type], + )); + + // Cast back to the operand's internal int representation. + let back_dtype = Self::int_back_cast_dtype(int_dtype)?; + main_block.operations.push(Self::create_cast_operation( + sat_name, + output_type, + back_dtype, + )); + continue; + } + } + } } - // Add dtype parameter (required) - let to_type = to; - let dtype_string = match to_type { - MLOperandDataType::Float32 => "fp32", - MLOperandDataType::Float16 => "fp16", - MLOperandDataType::Int32 => "int32", - MLOperandDataType::Uint32 => "uint32", - MLOperandDataType::Int8 => "int8", - MLOperandDataType::Uint8 => "int8", - MLOperandDataType::Int64 => "int64", - MLOperandDataType::Uint64 => "uint64", - MLOperandDataType::Int4 | MLOperandDataType::Uint4 => { + if min_value == max_value { + if op.input_operands().is_empty() || op.output_operand().is_none() { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: "int4/uint4 cast targets are not supported in CoreML conversion" - .to_string(), + reason: "clamp requires input and output operand".to_string(), }); } - }; - inputs.insert( - "dtype".to_string(), - Self::create_immediate_string(dtype_string), - ); - } - Operation::ScatterElements { options, .. } => { - // scatter: data, indices, updates, axis - if input_names.len() >= 3 { - inputs.insert("data".to_string(), Self::create_argument(&input_names[0])); - inputs.insert( - "indices".to_string(), - Self::create_argument(&input_names[1]), - ); - inputs.insert( - "updates".to_string(), - Self::create_argument(&input_names[2]), + let input_id = op.input_operands()[0]; + let input_operand = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", input_id), + } + })?; + let output_id = op.output_operand().expect("checked above"); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + let input_name = operand_name(graph_info, input_id); + let use_float16 = input_operand.descriptor.data_type == DataType::Float16; + let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; + let dimensions = Self::mil_dimensions_from_graph_shape( + &input_operand.descriptor.shape, + false, ); - } + let make_type = |name: String| NamedValueType { + name, + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: dimensions.len() as i64, + data_type: dtype, + dimensions: dimensions.clone(), + attributes: HashMap::new(), + }, + ), + ), + }), + }; + let bound_arg = |v: f64| { + if use_float16 { + Self::create_immediate_float16(v as f32) + } else { + Self::create_immediate_float(v as f32) + } + }; + // Equal bounds (incl. +/-Infinity): CoreML `clip` rejects alpha == beta, + // and `input*0 + bound` yields NaN for non-finite inputs. Use + // minimum(maximum(input, min), max), which is exact for infinities. + let mx_name = format!("{}_clamp_max", output_name); + let mut mx_inputs: HashMap = HashMap::new(); + mx_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + mx_inputs.insert("y".to_string(), bound_arg(min_value)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::MAXIMUM, + mx_inputs, + vec![make_type(mx_name.clone())], + )); + let mut mn_inputs: HashMap = HashMap::new(); + mn_inputs.insert("x".to_string(), Self::create_name_argument(mx_name)); + mn_inputs.insert("y".to_string(), bound_arg(max_value)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::MINIMUM, + mn_inputs, + vec![output_type], + )); - if let Some(opts) = options { - inputs.insert("axis".to_string(), Self::create_immediate_int(opts.axis)); + continue; } } - Operation::ScatterND { .. } - // scatter_nd: data, indices, updates - if input_names.len() >= 3 => { - inputs.insert("data".to_string(), Self::create_argument(&input_names[0])); - inputs.insert( - "indices".to_string(), - Self::create_argument(&input_names[1]), - ); - inputs.insert( - "updates".to_string(), - Self::create_argument(&input_names[2]), - ); - } + // Special handling for expand operation (may need reshape first) + if let Operation::Expand { + new_shape: expand_shape, + .. + } = &op + && !op.input_operands().is_empty() + && !expand_shape.is_empty() + && let Some(input_operand) = graph_info.operand(op.input_operands()[0]) + { + let new_shape_u32: Vec = expand_shape + .iter() + .map(MLDimension::static_or_max) + .collect(); + let input_shape = input_operand.descriptor.static_or_max_shape(); + let input_rank = input_shape.len(); + let output_rank = new_shape_u32.len(); - Operation::Tile { repetitions, .. } => { - // tile: x, reps - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + #[allow(clippy::collapsible_if)] + if input_rank < output_rank { + let mut reshaped_dims = vec![1u32; output_rank]; + for i in 0..input_rank { + reshaped_dims[output_rank - i - 1] = input_shape[input_rank - i - 1]; + } - if !repetitions.is_empty() { - inputs.insert( - "reps".to_string(), - Self::create_immediate_int_array(repetitions), + //Create reshape operation + let input_name = operand_name(graph_info, op.input_operands()[0]); + // Use input name to create unique intermediate name (don't rely on output_operands) + let reshape_output_name = format!("{}_expand_reshaped", input_name); + + let mut reshape_inputs: HashMap = HashMap::new(); + reshape_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + reshape_inputs.insert( + "shape".to_string(), + Self::create_int_array_argument( + reshaped_dims.iter().map(|&v| v as i32).collect(), + ), ); - } - } - Operation::CumulativeSum { axis, options, .. } => { - // cumsum: x, axis, exclusive, reverse - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + // Create tensor type for reshape output + let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; + let dimensions: Vec = reshaped_dims + .iter() + .map(|&d| Dimension { + dimension: Some(dimension::Dimension::Constant( + dimension::ConstantDimension { size: d as u64 }, + )), + }) + .collect(); - inputs.insert("axis".to_string(), Self::create_int_argument(*axis as i32)); - if let Some(opts) = options { - inputs.insert( - "exclusive".to_string(), - Self::create_immediate_bool(opts.exclusive), - ); - inputs.insert( - "reverse".to_string(), - Self::create_immediate_bool(opts.reversed), + let value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: dimensions.len() as i64, + data_type: dtype, + dimensions, + attributes: HashMap::new(), + }, + ), + ), + }; + + let reshape_output_type = NamedValueType { + name: reshape_output_name.clone(), + r#type: Some(value_type), + }; + + let reshape_mil_op = Self::create_mil_operation( + "reshape", + reshape_inputs, + vec![reshape_output_type], ); - } - } - Operation::Reverse { options, .. } => { - // reverse: x, axes - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + main_block.operations.push(reshape_mil_op); } + } - // Default behavior: reverse all axes when options.axes is omitted. - let axes_u32: Vec = match options.as_ref() { - Some(opts) => match opts.axes.as_ref() { - Some(axes) => axes.clone(), - None => { - if let Some(input_id) = op.input_operands().first() { - if let Some(input_operand) = graph.operand(*input_id) { - (0..input_operand.descriptor.shape.len()) - .map(|axis| axis as u32) - .collect() - } else { - Vec::new() - } - } else { - Vec::new() - } - } - }, - None => { - if let Some(input_id) = op.input_operands().first() { - if let Some(input_operand) = graph.operand(*input_id) { - (0..input_operand.descriptor.shape.len()) - .map(|axis| axis as u32) - .collect() - } else { - Vec::new() + // Relu with integer input: CoreML relu only accepts float (fp32/fp16). + // Cast int → fp32, apply relu, cast back. + // sub / sign / abs / max / min with int8/uint8: CoreML only supports + // int32/fp32/fp16. Cast inputs to int32, apply op, cast back to the int type. + if matches!( + op_type_lower.as_str(), + "sub" | "sign" | "abs" | "max" | "min" + ) { + let first_input_id = op.input_operands().first().copied(); + let output_id = op.output_operand(); + if let (Some(input_id), Some(output_id)) = (first_input_id, output_id) { + if let Some(input_op) = graph_info.operand(input_id) { + let int_dtype = input_op.descriptor.data_type.clone(); + if matches!(int_dtype, DataType::Int8 | DataType::Uint8) { + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let back_dtype = Self::cast_dtype_string_for_graph_type(&int_dtype)?; + + // Cast all inputs to int32 + let mut int32_names: Vec = Vec::new(); + for (idx, &in_id) in op.input_operands().iter().enumerate() { + let src_name = Self::output_name_for_operand( + graph_info, + in_id, + &operand_name_overrides, + ); + let cast32_name = format!("{}_op_cast32_{}", output_name, idx); + let cast32_type = Self::create_value_with_mil_type( + graph_info, + in_id, + cast32_name.clone(), + crate::protos::coreml::mil_spec::DataType::Int32 as i32, + )?; + main_block.operations.push(Self::create_cast_operation( + src_name, + cast32_type, + "int32", + )); + int32_names.push(cast32_name); } - } else { - Vec::new() + + // Apply the op on int32 values + let mil_type = self.get_mil_op_type(op.op_type())?; + let int32_out_name = format!("{}_op_int32", output_name); + let int32_out_type = Self::create_value_with_mil_type( + graph_info, + output_id, + int32_out_name.clone(), + crate::protos::coreml::mil_spec::DataType::Int32 as i32, + )?; + let op_mil = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &int32_names, + vec![int32_out_type], + mil_type, + )?; + main_block.operations.push(op_mil); + + // Cast result back to original int type + main_block.operations.push(Self::create_cast_operation( + int32_out_name, + output_type, + back_dtype, + )); + continue; } } - }; - - // Always provide axes, including empty arrays (explicit no-op). - inputs.insert( - "axes".to_string(), - Self::create_immediate_int_array(&axes_u32), - ); + } } - Operation::Triangular { options, .. } => { - // band_part: x, lower, upper - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + // neg with int8/uint8: emitted as mul-by-(-1), but the -1 multiplier is a + // float immediate, so route through fp32 (int8 values are exact in fp32), + // then cast back to the original int type. + if op_type_lower == "neg" { + if let (Some(&input_id), Some(output_id)) = + (op.input_operands().first(), op.output_operand()) + { + if let Some(input_op) = graph_info.operand(input_id) { + let int_dtype = input_op.descriptor.data_type.clone(); + if matches!(int_dtype, DataType::Int8 | DataType::Uint8) { + let int_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let float_name = format!("{}_neg_float", output_name); + let float_type = Self::create_value_with_mil_type( + graph_info, + output_id, + float_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + main_block + .operations + .push(Self::create_cast_operation(int_name, float_type, "fp32")); + let neg_name = format!("{}_neg_result", output_name); + let neg_type = Self::create_value_with_mil_type( + graph_info, + output_id, + neg_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + let mut neg_inputs = HashMap::new(); + neg_inputs + .insert("x".to_string(), Self::create_name_argument(float_name)); + neg_inputs.insert("y".to_string(), Self::create_immediate_float(-1.0)); + main_block.operations.push(Self::create_mil_operation( + self.get_mil_op_type("neg")?, + neg_inputs, + vec![neg_type], + )); + let back_dtype = Self::cast_dtype_string_for_graph_type(&int_dtype)?; + main_block.operations.push(Self::create_cast_operation( + neg_name, + output_type, + back_dtype, + )); + continue; + } + } } - - // CoreML band_part uses lower and upper bounds instead of upper/diagonal - let is_upper = options.as_ref().and_then(|o| o.upper).unwrap_or(true); - let diagonal = options.as_ref().map(|o| o.diagonal as i64).unwrap_or(0); - - // Convert WebNN (upper, diagonal) to CoreML (lower, upper) - // For upper triangle: keep diagonal and above - // For lower triangle: keep diagonal and below - let (lower_bound, upper_bound) = if is_upper { - // Upper triangle: remove elements below diagonal+k - (diagonal as i32, -1) // keep from diagonal+k upward - } else { - // Lower triangle: remove elements above diagonal+k - (-1, diagonal as i32) // keep from diagonal+k downward - }; - - inputs.insert( - "lower".to_string(), - Self::create_immediate_int(lower_bound as u32), - ); - inputs.insert( - "upper".to_string(), - Self::create_immediate_int(upper_bound as u32), - ); } - // Reduction operations: reduceSum, reduceMean, reduceMax, etc. - Operation::ReduceSum { options, .. } - | Operation::ReduceMean { options, .. } - | Operation::ReduceMax { options, .. } - | Operation::ReduceMin { options, .. } - | Operation::ReduceProduct { options, .. } - | Operation::ReduceL1 { options, .. } - | Operation::ReduceL2 { options, .. } - | Operation::ReduceLogSum { options, .. } - | Operation::ReduceLogSumExp { options, .. } - | Operation::ReduceSumSquare { options, .. } => { - // All reduce operations: x, axes, keep_dims - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); + if op_type_lower == "relu" { + if let (Some(&input_id), Some(output_id)) = + (op.input_operands().first(), op.output_operand()) + { + if let Some(input_op) = graph_info.operand(input_id) { + let int_dtype = input_op.descriptor.data_type.clone(); + let is_castable_int = Self::is_castable_int(&int_dtype); + if is_castable_int { + let int_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let float_name = format!("{}_relu_float", output_name); + let float_type = Self::create_value_with_mil_type( + graph_info, + output_id, + float_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + main_block + .operations + .push(Self::create_cast_operation(int_name, float_type, "fp32")); + let relu_name = format!("{}_relu_result", output_name); + let relu_type = Self::create_value_with_mil_type( + graph_info, + output_id, + relu_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + let mut relu_inputs = HashMap::new(); + relu_inputs + .insert("x".to_string(), Self::create_name_argument(float_name)); + main_block.operations.push(Self::create_mil_operation( + mil_ops::RELU, + relu_inputs, + vec![relu_type], + )); + let back_dtype = Self::int_back_cast_dtype(&int_dtype)?; + main_block.operations.push(Self::create_cast_operation( + relu_name, + output_type, + back_dtype, + )); + continue; + } + } } + } - if let Some(opts) = options { - if let Some(axes) = opts.axes.as_ref() - && !axes.is_empty() - { - inputs.insert("axes".to_string(), Self::create_immediate_int_array(axes)); + // Pad with integer input: CoreML pad only accepts float (fp32/fp16). + // Cast int → fp32, apply pad with float constant_val, cast back. + if op_type_lower == "pad" { + if let (Some(&input_id), Some(output_id)) = + (op.input_operands().first(), op.output_operand()) + { + if let Some(input_op) = graph_info.operand(input_id) { + let int_dtype = input_op.descriptor.data_type.clone(); + let is_castable_int = Self::is_castable_int(&int_dtype); + if is_castable_int { + let (beginning_padding, ending_padding, options) = match op { + Operation::Pad { + beginning_padding, + ending_padding, + options, + .. + } => (beginning_padding, ending_padding, options.as_ref()), + _ => { + continue; + } + }; + let int_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + // Cast input to float32 + let float_name = format!("{}_pad_float", output_name); + let float_type = Self::create_value_with_mil_type( + graph_info, + input_id, + float_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + main_block + .operations + .push(Self::create_cast_operation(int_name, float_type, "fp32")); + // Build pad inputs with float32 constant_val + let pad_vec: Vec = beginning_padding + .iter() + .zip(ending_padding.iter()) + .flat_map(|(a, b)| [*a, *b]) + .collect(); + let webnn_mode = options.map(|o| o.mode.as_str()).unwrap_or("constant"); + let coreml_mode = match webnn_mode { + "edge" => "replicate", + "reflection" | "symmetric" => "reflect", + _ => "constant", + }; + let constant_val = options + .and_then(|o| Self::parse_mlnumber_f64(o.value.as_ref())) + .unwrap_or(0.0); + let padded_name = format!("{}_pad_padded", output_name); + let padded_type = Self::create_value_with_mil_type( + graph_info, + output_id, + padded_name.clone(), + crate::protos::coreml::mil_spec::DataType::Float32 as i32, + )?; + let mut pad_inputs = HashMap::new(); + pad_inputs + .insert("x".to_string(), Self::create_name_argument(float_name)); + pad_inputs.insert( + "pad".to_string(), + Self::create_immediate_int_array(&pad_vec), + ); + pad_inputs.insert( + "mode".to_string(), + Self::create_immediate_string(coreml_mode), + ); + pad_inputs.insert( + "constant_val".to_string(), + Self::create_immediate_float(constant_val as f32), + ); + main_block.operations.push(Self::create_mil_operation( + mil_ops::PAD, + pad_inputs, + vec![padded_type], + )); + // Cast back to the operand's internal int representation. + let back_dtype = Self::int_back_cast_dtype(&int_dtype)?; + main_block.operations.push(Self::create_cast_operation( + padded_name, + output_type, + back_dtype, + )); + continue; + } } - inputs.insert( - "keep_dims".to_string(), - Self::create_immediate_bool(opts.keep_dimensions), - ); } } - _ => {} - } + // Special handling for hardswish (decompose into hardsigmoid + mul) + // Following Chromium: hardswish = x * hardsigmoid(x, alpha=1/6, beta=0.5) + // Note: op_type is "hardSwish" but we normalize to lowercase + if op_type_lower == "hardswish" { + // Validate inputs/outputs exist + // Note: hardswish uses output_operand (singular), not output_operands + if op.input_operands().is_empty() || op.output_operand().is_none() { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "hardswish requires input and output operand".to_string(), + }); + } - Ok(inputs) - } + let input_operand = + graph_info.operand(op.input_operands()[0]).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", op.input_operands()[0]), + } + })?; + { + let input_name = operand_name(graph_info, op.input_operands()[0]); + let hardsigmoid_output_name = format!("{}_hardswish_hardsigmoid", input_name); - /// Create a FeatureType for model description from an OperandDescriptor - fn create_feature_type( - descriptor: &crate::graph::OperandDescriptor, - ) -> Result { - use crate::protos::coreml::specification::{ArrayFeatureType, FeatureType, feature_type}; + // Create hardsigmoid operation with alpha=1/6, beta=0.5 + let mut hardsigmoid_inputs: HashMap = HashMap::new(); + hardsigmoid_inputs.insert( + "x".to_string(), + Self::create_name_argument(input_name.clone()), + ); + hardsigmoid_inputs + .insert("alpha".to_string(), Self::create_immediate_float(1.0 / 6.0)); + hardsigmoid_inputs + .insert("beta".to_string(), Self::create_immediate_float(0.5)); - // Map WebNN data type to CoreML array data type - // CoreML feature descriptions (I/O) ONLY support: DOUBLE, FLOAT32, FLOAT16, INT32 - // Even though Int8 exists in protobuf enum, CoreML runtime rejects it - let array_data_type = match descriptor.data_type { - DataType::Float32 => { - crate::protos::coreml::specification::array_feature_type::ArrayDataType::Float32 - } - DataType::Float16 => { - crate::protos::coreml::specification::array_feature_type::ArrayDataType::Float16 - } - DataType::Int32 => { - crate::protos::coreml::specification::array_feature_type::ArrayDataType::Int32 - } - // Unsupported types - assume they have been converted to FLOAT32. - DataType::Int4 - | DataType::Uint4 - | DataType::Int8 - | DataType::Uint8 - | DataType::Uint32 - | DataType::Int64 - | DataType::Uint64 => { - crate::protos::coreml::specification::array_feature_type::ArrayDataType::Float32 - } - }; + // Create tensor type for hardsigmoid output. + // Use `scalar_as_one_dim: true` to match the promotion the + // rest of the converter applies to rank-0 operands — with + // `false`, a rank-0 input gives hardsigmoid an rank-0 + // intermediate while the graph's declared output is + // `tensor`, and Apple's loader rejects the + // following `mul` with + // "Output '0' has unexpected type 'ios17.mul'. + // Expected tensor; got fp32." + // (surfaced by WPT "hardSwish float32 0D scalar default options"). + let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; + let dimensions = Self::mil_dimensions_from_graph_shape( + &input_operand.descriptor.shape, + true, + ); - // Create array feature type with shape - let mut array_feature = ArrayFeatureType { - data_type: array_data_type as i32, - ..Default::default() - }; + let value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: dimensions.len() as i64, + data_type: dtype, + dimensions, + attributes: HashMap::new(), + }, + ), + ), + }; - // Add shape dimensions - // CoreML requires explicit shape constraints - convert scalars (0D) to 1D [1] - // Following Chromium's approach for scalar handling - let shape_to_use = if descriptor.shape.is_empty() { - vec![1] // Scalar (0D) tensor -> [1] for CoreML compatibility - } else { - descriptor.static_or_max_shape() - }; + let hardsigmoid_output_type = NamedValueType { + name: hardsigmoid_output_name.clone(), + r#type: Some(value_type), + }; - for &dim in &shape_to_use { - array_feature.shape.push(dim as i64); - } + let hardsigmoid_op = Self::create_mil_operation( + "sigmoid_hard", + hardsigmoid_inputs, + vec![hardsigmoid_output_type], + ); - Ok(FeatureType { - r#type: Some(feature_type::Type::MultiArrayType(array_feature)), - is_optional: false, - }) - } -} + main_block.operations.push(hardsigmoid_op); -impl super::GraphConverter for CoremlMlProgramConverter { - fn format(&self) -> &'static str { - "coreml" - } + // Create mul operation: x * hardsigmoid_output + let mut mul_inputs: HashMap = HashMap::new(); + mul_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + mul_inputs.insert( + "y".to_string(), + Self::create_name_argument(hardsigmoid_output_name), + ); - fn convert(&self, graph_info: &GraphInfo) -> Result { - if !crate::graph::dynamic_inputs_enabled() && graph_info.has_dynamic_dimensions() { - return Err(GraphError::DynamicInputsFeatureDisabled); - } + // Get output name (using singular output_operand field) + let output_operand_id = op.output_operand().unwrap(); + let output_name = operand_name(graph_info, output_operand_id); + let output_operand = + graph_info.operand(output_operand_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Output operand {} not found", output_operand_id), + } + })?; - // Create weight file builder for Float16 constants - let mut weight_builder = super::WeightFileBuilder::new(); + let output_dtype = Self::mil_data_type(&output_operand.descriptor.data_type)?; + // Same promotion as the hardsigmoid intermediate above: the + // mul output must have the same rank as its inputs, and + // the graph's input/output operands already went through + // the `scalar_as_one_dim: true` pass when they were + // declared in `main` block — so rank-0 becomes [1] here too. + let output_dimensions = Self::mil_dimensions_from_graph_shape( + &output_operand.descriptor.shape, + true, + ); - // Create MLProgram - let mut program = Program { - version: 1, - ..Default::default() - }; + let output_value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: output_dimensions.len() as i64, + data_type: output_dtype, + dimensions: output_dimensions, + attributes: HashMap::new(), + }, + ), + ), + }; - // Create main function - let mut main_function = Function::default(); + let mul_output_type = NamedValueType { + name: output_name, + r#type: Some(output_value_type), + }; - // Keep MLProgram boundary types aligned with CoreML feature-description - // restrictions. Unsupported WebNN I/O types (such as uint8) are exposed - // as float32 at the function boundary and cast to/from the internal - // graph representation inside the main block. - let mut operand_name_overrides: HashMap = HashMap::new(); + let mul_op = + Self::create_mil_operation("mul", mul_inputs, vec![mul_output_type]); - for &output_id in &graph_info.output_operands { - if let Some(operand) = graph_info.operand(output_id) { - let graph_mil_type = Self::mil_data_type(&operand.descriptor.data_type)?; - let interface_mil_type = - Self::interface_mil_data_type(&operand.descriptor.data_type); - if graph_mil_type != interface_mil_type { - let output_name = operand_name(graph_info, output_id); - operand_name_overrides.insert(output_id, format!("{}_graph", output_name)); + main_block.operations.push(mul_op); } - } - } - - // Add function inputs from graph inputs - for &input_id in &graph_info.input_operands { - let operand = - graph_info - .operand(input_id) - .ok_or_else(|| GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!("Input operand {} not found", input_id), - })?; - let input_name = operand_name(graph_info, input_id); - let value_type = Self::create_value_with_mil_type( - graph_info, - input_id, - input_name, - Self::interface_mil_data_type(&operand.descriptor.data_type), - )?; - main_function.inputs.push(value_type); - } - // Create main block - let mut main_block = Block::default(); + // Skip normal operation conversion for hardswish + continue; + } - for &input_id in &graph_info.input_operands { - let operand = - graph_info - .operand(input_id) - .ok_or_else(|| GraphError::ConversionFailed { + // Special handling for gemm: y = alpha * op(a) * op(b) + beta * c + // Lower to matmul + optional mul(alpha) + optional mul(beta, c) + add. + if op_type_lower == "gemm" { + if op.input_operands().len() < 2 || op.output_operand().is_none() { + return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!("Input operand {} not found", input_id), - })?; - let graph_mil_type = Self::mil_data_type(&operand.descriptor.data_type)?; - let interface_mil_type = Self::interface_mil_data_type(&operand.descriptor.data_type); - if graph_mil_type != interface_mil_type { - let input_name = operand_name(graph_info, input_id); - let graph_input_name = format!("{}_graph", input_name); - operand_name_overrides.insert(input_id, graph_input_name.clone()); - let graph_input_type = Self::create_value_with_mil_type( - graph_info, - input_id, - graph_input_name, - graph_mil_type, - )?; - main_block.operations.push(Self::create_cast_operation( - input_name, - graph_input_type, - Self::cast_dtype_string_for_graph_type(&operand.descriptor.data_type)?, - )); - } - } + reason: "gemm requires at least 2 input operands and 1 output".to_string(), + }); + } - // Add constant operands as const operations - for (operand_id, constant_data) in &graph_info.constant_operand_ids_to_handles { - let operand = - graph_info - .operand(*operand_id) - .ok_or_else(|| GraphError::ConversionFailed { + let output_operand_id = op.output_operand().unwrap(); + let output_operand = graph_info.operand(output_operand_id).ok_or_else(|| { + GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!("Constant operand {} not found", operand_id), - })?; + reason: format!("Output operand {} not found", output_operand_id), + } + })?; - let const_op = Self::create_const_operation( - graph_info, - *operand_id, - operand, - constant_data, - &mut weight_builder, - )?; - main_block.operations.push(const_op); - } + let (output_name, output_type) = Self::create_value(graph_info, output_operand_id)?; - // First pass: Handle filter layout transformations for conv operations + let (alpha, beta) = match &op { + Operation::Gemm { options, .. } => ( + options.as_ref().map(|o| o.alpha as f32).unwrap_or(1.0), + options.as_ref().map(|o| o.beta as f32).unwrap_or(1.0), + ), + _ => (1.0, 1.0), + }; - for op in &graph_info.operations { - let op_type_lower = op.op_type().to_lowercase(); + let c_operand_id_opt = match &op { + Operation::Gemm { options, .. } => options.as_ref().and_then(|o| o.c), + _ => None, + }; + let has_bias = c_operand_id_opt.is_some(); + let needs_alpha_mul = (alpha - 1.0).abs() > f32::EPSILON; + let needs_beta_mul = has_bias && (beta - 1.0).abs() > f32::EPSILON; - if (op_type_lower == "conv2d" || op_type_lower == "convtranspose2d") - && op.input_operands().len() >= 2 - { - let filter_layout = match &op { - Operation::Conv2d { options, .. } => options - .as_ref() - .map(|o| o.filter_layout.as_str()) - .unwrap_or(""), - Operation::ConvTranspose2d { options, .. } => options - .as_ref() - .map(|o| o.filter_layout.as_str()) - .unwrap_or(""), - _ => "", + let (alpha_arg, beta_arg) = match output_operand.descriptor.data_type { + DataType::Float16 => ( + Self::create_immediate_float16(alpha), + Self::create_immediate_float16(beta), + ), + DataType::Float32 => ( + Self::create_immediate_float(alpha), + Self::create_immediate_float(beta), + ), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "gemm currently supports float32/float16 output, got {:?}", + output_operand.descriptor.data_type + ), + }); + } }; - if !filter_layout.is_empty() { - let expected_layout = if op_type_lower == "conv2d" { - "oihw" - } else { - "iohw" - }; - if filter_layout != expected_layout { - let filter_operand_id = op.input_operands()[1]; + let mut current_name: String; + let matmul_output_name = if needs_alpha_mul || has_bias { + format!("{}_gemm_matmul", output_name) + } else { + output_name.clone() + }; - if let Some(filter_operand) = graph_info.operand(filter_operand_id) { - // Calculate transpose permutation - let perm = match (op_type_lower.as_str(), filter_layout) { - // Conv2d conversions to oihw [O, I, H, W] - ("conv2d", "hwio") => vec![3, 2, 0, 1], // [H, W, I, O] -> [O, I, H, W] - ("conv2d", "ohwi") => vec![0, 3, 1, 2], // [O, H, W, I] -> [O, I, H, W] - ("conv2d", "ihwo") => vec![3, 0, 1, 2], // [I, H, W, O] -> [O, I, H, W] + let mut matmul_inputs: HashMap = HashMap::new(); + matmul_inputs.insert( + "x".to_string(), + Self::create_name_argument(operand_name(graph_info, op.input_operands()[0])), + ); + matmul_inputs.insert( + "y".to_string(), + Self::create_name_argument(operand_name(graph_info, op.input_operands()[1])), + ); + let (a_transpose, b_transpose) = match &op { + Operation::Gemm { options, .. } => ( + options.as_ref().map(|o| o.a_transpose).unwrap_or(false), + options.as_ref().map(|o| o.b_transpose).unwrap_or(false), + ), + _ => (false, false), + }; + matmul_inputs.insert( + "transpose_x".to_string(), + Self::create_immediate_bool(a_transpose), + ); + matmul_inputs.insert( + "transpose_y".to_string(), + Self::create_immediate_bool(b_transpose), + ); - // Conv_transpose2d conversions to iohw [I, O, H, W] - ("convtranspose2d", "hwoi") => vec![3, 2, 0, 1], // [H, W, O, I] -> [I, O, H, W] - ("convtranspose2d", "ohwi") => vec![3, 0, 1, 2], // [O, H, W, I] -> [I, O, H, W] - ("convtranspose2d", "hwio") => vec![2, 3, 0, 1], // [H, W, I, O] -> [I, O, H, W] + main_block.operations.push(Self::create_mil_operation( + "matmul", + matmul_inputs, + vec![NamedValueType { + name: matmul_output_name.clone(), + r#type: output_type.r#type.clone(), + }], + )); + current_name = matmul_output_name; - _ => continue, // Skip unsupported layouts - }; + if needs_alpha_mul { + let alpha_output_name = if has_bias { + format!("{}_gemm_alpha", output_name) + } else { + output_name.clone() + }; - // Create transpose operation for filter - let filter_name = operand_name(graph_info, filter_operand_id); - let transposed_filter_name = format!("{}_transposed", filter_name); + let mut alpha_mul_inputs: HashMap = HashMap::new(); + alpha_mul_inputs + .insert("x".to_string(), Self::create_name_argument(current_name)); + alpha_mul_inputs.insert("y".to_string(), alpha_arg); - // Store the override mapping - operand_name_overrides - .insert(filter_operand_id, transposed_filter_name.clone()); + main_block.operations.push(Self::create_mil_operation( + "mul", + alpha_mul_inputs, + vec![NamedValueType { + name: alpha_output_name.clone(), + r#type: output_type.r#type.clone(), + }], + )); - let mut transpose_inputs: HashMap = HashMap::new(); - transpose_inputs - .insert("x".to_string(), Self::create_name_argument(filter_name)); - transpose_inputs.insert( - "perm".to_string(), - Self::create_immediate_int_array(&perm), - ); + current_name = alpha_output_name; + } - // Create tensor type for transposed filter - let dtype = Self::mil_data_type(&filter_operand.descriptor.data_type)?; - let transposed_shape = - Self::permute_graph_shape(&filter_operand.descriptor.shape, &perm); - let dimensions = - Self::mil_dimensions_from_graph_shape(&transposed_shape, false); + if has_bias { + let c_operand_id = c_operand_id_opt.unwrap(); + let (c_name, c_type) = Self::create_value(graph_info, c_operand_id)?; + let scaled_c_name = if needs_beta_mul { + format!("{}_gemm_bias", output_name) + } else { + c_name.clone() + }; - let value_type = ValueType { - r#type: Some( - crate::protos::coreml::mil_spec::value_type::Type::TensorType( - TensorType { - rank: dimensions.len() as i64, - data_type: dtype, - dimensions, - attributes: HashMap::new(), - }, - ), - ), - }; + if needs_beta_mul { + let mut beta_mul_inputs: HashMap = HashMap::new(); + beta_mul_inputs.insert("x".to_string(), Self::create_name_argument(c_name)); + beta_mul_inputs.insert("y".to_string(), beta_arg); - let transpose_output_type = NamedValueType { - name: transposed_filter_name.clone(), - r#type: Some(value_type), - }; + main_block.operations.push(Self::create_mil_operation( + "mul", + beta_mul_inputs, + vec![NamedValueType { + name: scaled_c_name.clone(), + r#type: c_type.r#type, + }], + )); + } - let transpose_op = Self::create_mil_operation( - "transpose", - transpose_inputs, - vec![transpose_output_type], - ); + let mut add_inputs: HashMap = HashMap::new(); + add_inputs.insert("x".to_string(), Self::create_name_argument(current_name)); + add_inputs.insert("y".to_string(), Self::create_name_argument(scaled_c_name)); - main_block.operations.push(transpose_op); - } - } + main_block.operations.push(Self::create_mil_operation( + "add", + add_inputs, + vec![NamedValueType { + name: output_name, + r#type: output_type.r#type, + }], + )); } - let input_layout = match &op { - Operation::Conv2d { options, .. } => options - .as_ref() - .map(|o| o.input_layout.as_str()) - .unwrap_or(""), - Operation::ConvTranspose2d { options, .. } => options - .as_ref() - .map(|o| o.input_layout.as_str()) - .unwrap_or(""), - _ => "", - }; - if input_layout == "nhwc" && !op.input_operands().is_empty() { - let input_operand_id = op.input_operands()[0]; + continue; + } - // Only transpose if not already transposed - if !operand_name_overrides.contains_key(&input_operand_id) - && let Some(input_operand) = graph_info.operand(input_operand_id) - { - // NHWC -> NCHW transposition: [0, 3, 1, 2] - let perm = [0, 3, 1, 2]; + // Special handling for linear: y = alpha * x + beta + // Lower to mul + add primitives for backend parity. + if op_type_lower == "linear" { + if op.input_operands().is_empty() || op.output_operand().is_none() { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "linear requires input and output operand".to_string(), + }); + } + + let input_operand = + graph_info.operand(op.input_operands()[0]).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", op.input_operands()[0]), + } + })?; + let output_operand_id = op.output_operand().unwrap(); + let output_operand = graph_info.operand(output_operand_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Output operand {} not found", output_operand_id), + } + })?; - // Create transpose operation for input - let input_name = operand_name(graph_info, input_operand_id); - let transposed_input_name = format!("{}_nchw", input_name); + let (alpha, beta) = match &op { + Operation::Linear { options, .. } => options + .as_ref() + .map(|o| (o.alpha as f32, o.beta as f32)) + .unwrap_or((1.0, 0.0)), + _ => (1.0, 0.0), + }; - // Store the override mapping - operand_name_overrides - .insert(input_operand_id, transposed_input_name.clone()); + let (alpha_arg, beta_arg) = match input_operand.descriptor.data_type { + DataType::Float16 => ( + Self::create_immediate_float16(alpha), + Self::create_immediate_float16(beta), + ), + DataType::Float32 => ( + Self::create_immediate_float(alpha), + Self::create_immediate_float(beta), + ), + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "linear currently supports float32/float16, got {:?}", + input_operand.descriptor.data_type + ), + }); + } + }; - let mut transpose_inputs: HashMap = HashMap::new(); - transpose_inputs - .insert("x".to_string(), Self::create_name_argument(input_name)); - transpose_inputs.insert( - "perm".to_string(), - Self::create_immediate_int_array(perm.as_ref()), - ); + let input_name = operand_name(graph_info, op.input_operands()[0]); + let output_name = operand_name(graph_info, output_operand_id); + let mul_output_name = format!("{}_linear_mul", output_name); - // Create tensor type for transposed input - let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; - let transposed_shape = - Self::permute_graph_shape(&input_operand.descriptor.shape, &perm); - let dimensions = - Self::mil_dimensions_from_graph_shape(&transposed_shape, false); + let output_dtype = Self::mil_data_type(&output_operand.descriptor.data_type)?; + let output_shape = if output_operand.descriptor.shape.is_empty() { + vec![1u32] + } else { + output_operand.descriptor.static_or_max_shape() + }; + let output_dimensions: Vec = output_shape + .iter() + .map(|&d| Dimension { + dimension: Some(dimension::Dimension::Constant( + dimension::ConstantDimension { size: d as u64 }, + )), + }) + .collect(); - let value_type = ValueType { - r#type: Some( - crate::protos::coreml::mil_spec::value_type::Type::TensorType( - TensorType { - rank: dimensions.len() as i64, - data_type: dtype, - dimensions, - attributes: HashMap::new(), - }, - ), - ), - }; + let value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType(TensorType { + rank: output_dimensions.len() as i64, + data_type: output_dtype, + dimensions: output_dimensions.clone(), + attributes: HashMap::new(), + }), + ), + }; - let transpose_output_type = NamedValueType { - name: transposed_input_name.clone(), - r#type: Some(value_type), - }; + let mut mul_inputs: HashMap = HashMap::new(); + mul_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + mul_inputs.insert("y".to_string(), alpha_arg); + main_block.operations.push(Self::create_mil_operation( + "mul", + mul_inputs, + vec![NamedValueType { + name: mul_output_name.clone(), + r#type: Some(value_type.clone()), + }], + )); - let transpose_op = Self::create_mil_operation( - "transpose", - transpose_inputs, - vec![transpose_output_type], - ); + let mut add_inputs: HashMap = HashMap::new(); + add_inputs.insert("x".to_string(), Self::create_name_argument(mul_output_name)); + add_inputs.insert("y".to_string(), beta_arg); + main_block.operations.push(Self::create_mil_operation( + "add", + add_inputs, + vec![NamedValueType { + name: output_name, + r#type: Some(value_type), + }], + )); - main_block.operations.push(transpose_op); - } - } + continue; } - } - - // Convert all operations to MIL operations - for op in &graph_info.operations { - let op_type_lower = op.op_type().to_lowercase(); - - if matches!( - op_type_lower.as_str(), - "equal" - | "greater" - | "greaterorequal" - | "lesser" - | "lesserorequal" - | "logicalnot" - | "logicaland" - | "logicalor" - | "logicalxor" - | "notequal" - ) { - use crate::protos::coreml::mil_spec::DataType as MilDataType; - let output_id = - op.output_operand() - .ok_or_else(|| GraphError::ConversionFailed { - format: "CoreML MLProgram".to_string(), - reason: format!("operation '{}' has no output operand", op.op_type()), - })?; - let output_operand = - graph_info - .operand(output_id) - .ok_or_else(|| GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!("Output operand {} not found", output_id), - })?; - if output_operand.descriptor.data_type != DataType::Uint8 { + // Special handling for neg (decompose into mul(x, -1) with typed constant) + // Following Chromium: neg = mul(x, -1) with constant matching input dtype + if op_type_lower == "neg" { + // Validate inputs/outputs exist + if op.input_operands().is_empty() || op.output_operand().is_none() { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!( - "CoreML logical op '{}' expects uint8 graph output, got {:?}", - op.op_type(), - output_operand.descriptor.data_type - ), + reason: "neg requires input and output operand".to_string(), }); } - let (output_name, output_type) = - Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; - let bool_output_name = format!("{}_bool", output_name); - let bool_output_type = Self::create_value_with_mil_type( + let input_operand = + graph_info.operand(op.input_operands()[0]).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", op.input_operands()[0]), + } + })?; + + let input_name = Self::output_name_for_operand( graph_info, - output_id, - bool_output_name.clone(), - MilDataType::Bool as i32, - )?; + op.input_operands()[0], + &operand_name_overrides, + ); - let mut input_names = - Self::input_names_for_operation(graph_info, op, &operand_name_overrides); + // Create typed -1 constant matching input dtype. MIL `mul` + // preserves rank rather than broadcasting scalars, so when + // the input operand was promoted from rank-0 to rank-1 `[1]` + // (the `scalar_as_one_dim: true` rule applied to all graph + // inputs/outputs), the multiplier must be rank-1 `[1]` too. + // Otherwise Apple's loader rejects with + // "Output '0' has unexpected type 'ios17.mul'. + // Expected tensor; got fp32." + // (WPT "neg float32 positive 0D scalar" surfaces this.) + let neg_one_immediate = match input_operand.descriptor.data_type { + DataType::Float32 => Self::create_immediate_float_1d(-1.0f32), + DataType::Float16 => Self::create_immediate_float16(-1.0f32), + // Int32 and the int32-proxied wide ints all multiply by an int32 -1. + DataType::Int32 | DataType::Uint32 | DataType::Int64 | DataType::Uint64 => { + // create_immediate_int accepts u32 but converts to i32 internally + // We need to reimplement for -1 value + use crate::protos::coreml::mil_spec::{ + DataType as MilDataType, TensorType, TensorValue, Value, ValueType, + argument, tensor_value, value, value_type, + }; - if matches!( - op_type_lower.as_str(), - "logicalnot" | "logicaland" | "logicalor" | "logicalxor" - ) { - for (index, &input_id) in op.input_operands().iter().enumerate() { - let input_operand = graph_info.operand(input_id).ok_or_else(|| { - GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!("Input operand {} not found", input_id), - } - })?; - if input_operand.descriptor.data_type == DataType::Uint8 { - let bool_input_name = format!("{}_bool", input_names[index]); - let bool_input_type = Self::create_value_with_mil_type( - graph_info, - input_id, - bool_input_name.clone(), - MilDataType::Bool as i32, - )?; - main_block.operations.push(Self::create_cast_operation( - input_names[index].clone(), - bool_input_type, - "bool", - )); - input_names[index] = bool_input_name; + let tensor_value = TensorValue { + value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { + values: vec![-1i32], + })), + }; + + let val = Value { + doc_string: String::new(), + r#type: Some(ValueType { + r#type: Some(value_type::Type::TensorType(TensorType { + data_type: MilDataType::Int32 as i32, + rank: 0, // Scalar + dimensions: vec![], + attributes: HashMap::new(), + })), + }), + value: Some(value::Value::ImmediateValue(value::ImmediateValue { + value: Some(value::immediate_value::Value::Tensor(tensor_value)), + })), + }; + + Argument { + arguments: vec![argument::Binding { + binding: Some(argument::binding::Binding::Value(val)), + }], } } - } + _ => { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "Unsupported data type for neg: {:?}", + input_operand.descriptor.data_type + ), + }); + } + }; - if op_type_lower == "notequal" { - let equal_output_name = format!("{}_equal", output_name); - let equal_output_type = Self::create_value_with_mil_type( - graph_info, - output_id, - equal_output_name.clone(), - MilDataType::Bool as i32, - )?; + // Create mul operation: x * (-1) + let mut mul_inputs: HashMap = HashMap::new(); + mul_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + mul_inputs.insert("y".to_string(), neg_one_immediate); - let mut equal_inputs = HashMap::new(); - equal_inputs.insert( - "x".to_string(), - Self::create_name_argument(input_names[0].clone()), - ); - equal_inputs.insert( - "y".to_string(), - Self::create_name_argument(input_names[1].clone()), - ); - main_block.operations.push(Self::create_mil_operation( - mil_ops::EQUAL, - equal_inputs, - vec![equal_output_type], - )); + // Get output name + let output_operand_id = op.output_operand().unwrap(); + let output_name = operand_name(graph_info, output_operand_id); + let output_operand = graph_info.operand(output_operand_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Output operand {} not found", output_operand_id), + } + })?; - let mut not_inputs = HashMap::new(); - not_inputs.insert( - "x".to_string(), - Self::create_name_argument(equal_output_name), - ); - main_block.operations.push(Self::create_mil_operation( - mil_ops::LOGICAL_NOT, - not_inputs, - vec![bool_output_type], - )); - } else { - let mil_op = self.convert_operation_with_input_names_and_outputs( - graph_info, - op, - &input_names, - vec![bool_output_type], - self.get_mil_op_type(op.op_type())?, - )?; - main_block.operations.push(mil_op); - } + let output_dtype = + Self::graph_value_mil_type(&output_operand.descriptor.data_type)?; + // Use `scalar_as_one_dim: true` so a rank-0 output is promoted + // to `tensor`, matching the rank of the mul's + // (promoted) input/multiplier. See comment on the mul input + // above — this keeps the full op consistent. + let output_dimensions = + Self::mil_dimensions_from_graph_shape(&output_operand.descriptor.shape, true); - main_block.operations.push(Self::create_cast_operation( - bool_output_name, - output_type, - "uint8", - )); + let output_value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType(TensorType { + rank: output_dimensions.len() as i64, + data_type: output_dtype, + dimensions: output_dimensions, + attributes: HashMap::new(), + }), + ), + }; + + let mul_output_type = NamedValueType { + name: output_name, + r#type: Some(output_value_type), + }; + + let mul_op = Self::create_mil_operation("mul", mul_inputs, vec![mul_output_type]); + + main_block.operations.push(mul_op); + + // Skip normal operation conversion for neg continue; } - // Special handling for clamp with equal bounds. - // CoreML clip rejects alpha == beta, while WebNN clamp(min==max) is valid and - // should produce a constant tensor. Lower as: output = input * 0 + bound. - if op_type_lower == "clamp" { - let (min_value, max_value) = match &op { - Operation::Clamp { options, .. } => options - .as_ref() - .map(|o| { - ( - Self::parse_clamp_bound(o.min_value.as_ref(), f64::NEG_INFINITY), - Self::parse_clamp_bound(o.max_value.as_ref(), f64::INFINITY), - ) - }) - .unwrap_or((f64::NEG_INFINITY, f64::INFINITY)), - _ => (f64::NEG_INFINITY, f64::INFINITY), + // Special handling for layerNormalization with empty axes. + // When axes is empty (or not provided), the mean equals the input, so variance is 0 + // and the normalized output is 0 for every element. Following Chromium: + // out = sub(x, x) -> zeros shaped like input + // out = add(zeros, bias) -> if bias is present + if op_type_lower == "layernormalization" { + let axes_empty = match &op { + Operation::LayerNormalization { options, .. } => { + // None means "default to last axis" (not empty) + options + .as_ref() + .and_then(|o| o.axes.as_ref()) + .map(|ax| ax.is_empty()) + .unwrap_or(false) + } + _ => false, }; - if min_value == max_value { + if axes_empty { if op.input_operands().is_empty() || op.output_operand().is_none() { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: "clamp requires input and output operand".to_string(), + reason: "layerNormalization requires input and output operand" + .to_string(), }); } @@ -2992,123 +6762,125 @@ impl super::GraphConverter for CoremlMlProgramConverter { reason: format!("Input operand {} not found", input_id), } })?; - let output_id = op.output_operand().expect("checked above"); + let output_id = op.output_operand().unwrap(); let (output_name, output_type) = Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; - let input_name = operand_name(graph_info, input_id); - let zeroed_name = format!("{}_clamp_zeroed", output_name); - - let use_float16 = input_operand.descriptor.data_type == DataType::Float16; - - // zeroed = input * 0 - let mut mul_inputs: HashMap = HashMap::new(); - mul_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); - if use_float16 { - mul_inputs.insert("y".to_string(), Self::create_immediate_float16(0.0)); - } else { - mul_inputs.insert("y".to_string(), Self::create_immediate_float(0.0)); - } + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; let dimensions = Self::mil_dimensions_from_graph_shape( &input_operand.descriptor.shape, false, ); - let zeroed_type = NamedValueType { - name: zeroed_name.clone(), - r#type: Some(ValueType { - r#type: Some( - crate::protos::coreml::mil_spec::value_type::Type::TensorType( - TensorType { - rank: dimensions.len() as i64, - data_type: dtype, - dimensions, - attributes: HashMap::new(), - }, - ), + let zeros_value_type = ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: dimensions.len() as i64, + data_type: dtype, + dimensions, + attributes: HashMap::new(), + }, ), - }), + ), }; - main_block.operations.push(Self::create_mil_operation( - "mul", - mul_inputs, - vec![zeroed_type], - )); - // output = zeroed + min_value - let mut add_inputs: HashMap = HashMap::new(); - add_inputs.insert("x".to_string(), Self::create_name_argument(zeroed_name)); - if use_float16 { - add_inputs.insert( - "y".to_string(), - Self::create_immediate_float16(min_value as f32), - ); + let bias_id_opt = match &op { + Operation::LayerNormalization { options, .. } => { + options.as_ref().and_then(|o| o.bias) + } + _ => None, + }; + + let zeros_name = if bias_id_opt.is_some() { + format!("{}_ln_zeros", output_name) } else { - add_inputs.insert( - "y".to_string(), - Self::create_immediate_float(min_value as f32), - ); - } + output_name.clone() + }; + + // zeros = x - x + let mut sub_inputs: HashMap = HashMap::new(); + sub_inputs.insert( + "x".to_string(), + Self::create_name_argument(input_name.clone()), + ); + sub_inputs.insert("y".to_string(), Self::create_name_argument(input_name)); main_block.operations.push(Self::create_mil_operation( - "add", - add_inputs, - vec![output_type], + "sub", + sub_inputs, + vec![NamedValueType { + name: zeros_name.clone(), + r#type: Some(zeros_value_type), + }], )); + if let Some(bias_id) = bias_id_opt { + let bias_name = operand_name(graph_info, bias_id); + let mut add_inputs: HashMap = HashMap::new(); + add_inputs.insert("x".to_string(), Self::create_name_argument(zeros_name)); + add_inputs.insert("y".to_string(), Self::create_name_argument(bias_name)); + main_block.operations.push(Self::create_mil_operation( + "add", + add_inputs, + vec![output_type], + )); + } + continue; } } - // Special handling for expand operation (may need reshape first) - if let Operation::Expand { - new_shape: expand_shape, - .. - } = &op - && !op.input_operands().is_empty() - && !expand_shape.is_empty() - && let Some(input_operand) = graph_info.operand(op.input_operands()[0]) - { - let new_shape_u32: Vec = expand_shape - .iter() - .map(MLDimension::static_or_max) - .collect(); - let input_shape = input_operand.descriptor.static_or_max_shape(); - let input_rank = input_shape.len(); - let output_rank = new_shape_u32.len(); + // Special handling for triangular with non-zero diagonal k. + // CoreML band_part(x, lower, upper) keeps the band -lower <= row-col <= upper. + // For upper=true, k>0: the result is input minus the band up to diagonal k-1. + // result = input - band_part(input, -1, k-1) + // For upper=false, k<0: the result is input minus the band from diagonal -(|k|-1). + // result = input - band_part(input, |k|-1, -1) + if op_type_lower == "triangular" { + let (is_upper, diagonal) = match &op { + Operation::Triangular { options, .. } => ( + options.as_ref().and_then(|o| o.upper).unwrap_or(true), + options.as_ref().map(|o| o.diagonal as i64).unwrap_or(0), + ), + _ => (true, 0), + }; - #[allow(clippy::collapsible_if)] - if input_rank < output_rank { - let mut reshaped_dims = vec![1u32; output_rank]; - for i in 0..input_rank { - reshaped_dims[output_rank - i - 1] = input_shape[input_rank - i - 1]; + let needs_decompose = (is_upper && diagonal > 0) || (!is_upper && diagonal < 0); + + if needs_decompose { + if op.input_operands().is_empty() || op.output_operand().is_none() { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "triangular requires input and output operand".to_string(), + }); } - //Create reshape operation - let input_name = operand_name(graph_info, op.input_operands()[0]); - // Use input name to create unique intermediate name (don't rely on output_operands) - let reshape_output_name = format!("{}_expand_reshaped", input_name); + let input_id = op.input_operands()[0]; + let input_operand = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", input_id), + } + })?; + let output_id = op.output_operand().unwrap(); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; - let mut reshape_inputs: HashMap = HashMap::new(); - reshape_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); - reshape_inputs.insert( - "shape".to_string(), - Self::create_int_array_argument( - reshaped_dims.iter().map(|&v| v as i32).collect(), - ), + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, ); - - // Create tensor type for reshape output let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; - let dimensions: Vec = reshaped_dims - .iter() - .map(|&d| Dimension { - dimension: Some(dimension::Dimension::Constant( - dimension::ConstantDimension { size: d as u64 }, - )), - }) - .collect(); - - let value_type = ValueType { + let dimensions = Self::mil_dimensions_from_graph_shape( + &input_operand.descriptor.shape, + false, + ); + let band_value_type = ValueType { r#type: Some( crate::protos::coreml::mil_spec::value_type::Type::TensorType( TensorType { @@ -3120,555 +6892,1993 @@ impl super::GraphConverter for CoremlMlProgramConverter { ), ), }; + let band_name = format!("{}_triangular_band", output_name); - let reshape_output_type = NamedValueType { - name: reshape_output_name.clone(), - r#type: Some(value_type), + // Compute band_part bounds for the subtracted region. + // upper=true, k>0: subtract band_part(input, -1, k-1) + // upper=false, k<0: subtract band_part(input, |k|-1, -1) + let (band_lower, band_upper): (i64, i64) = if is_upper { + (-1, diagonal - 1) + } else { + ((-diagonal) - 1, -1) }; - let reshape_mil_op = Self::create_mil_operation( - "reshape", - reshape_inputs, - vec![reshape_output_type], + let mut band_inputs: HashMap = HashMap::new(); + band_inputs.insert( + "x".to_string(), + Self::create_name_argument(input_name.clone()), + ); + band_inputs.insert( + "lower".to_string(), + Self::create_immediate_int(band_lower as i32 as u32), ); + band_inputs.insert( + "upper".to_string(), + Self::create_immediate_int(band_upper as i32 as u32), + ); + main_block.operations.push(Self::create_mil_operation( + "band_part", + band_inputs, + vec![NamedValueType { + name: band_name.clone(), + r#type: Some(band_value_type), + }], + )); + + // result = input - band + let mut sub_inputs: HashMap = HashMap::new(); + sub_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + sub_inputs.insert("y".to_string(), Self::create_name_argument(band_name)); + main_block.operations.push(Self::create_mil_operation( + "sub", + sub_inputs, + vec![output_type], + )); + + continue; + } + } - main_block.operations.push(reshape_mil_op); + // `where` (MIL: select) — CoreML requires the condition to be bool, + // but WebNN encodes booleans as uint8. Insert a cast when needed. + if op_type_lower == "where" { + if let Operation::Where { condition, .. } = op { + let cond_id = *condition; + let cond_operand = graph_info.operand(cond_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("where condition operand {cond_id} not found"), + } + })?; + if cond_operand.descriptor.data_type == DataType::Uint8 { + let cond_name = Self::output_name_for_operand( + graph_info, + cond_id, + &operand_name_overrides, + ); + let bool_cond_name = format!("{cond_name}_bool"); + let bool_cond_type = Self::create_value_with_mil_type( + graph_info, + cond_id, + bool_cond_name.clone(), + crate::protos::coreml::mil_spec::DataType::Bool as i32, + )?; + main_block.operations.push(Self::create_cast_operation( + cond_name, + bool_cond_type, + "bool", + )); + + let mut overrides = operand_name_overrides.clone(); + overrides.insert(cond_id, bool_cond_name); + let mil_op = + self.convert_operation_with_overrides(graph_info, op, &overrides)?; + main_block.operations.push(mil_op); + continue; + } } } - // Special handling for hardswish (decompose into hardsigmoid + mul) - // Following Chromium: hardswish = x * hardsigmoid(x, alpha=1/6, beta=0.5) - // Note: op_type is "hardSwish" but we normalize to lowercase - if op_type_lower == "hardswish" { - // Validate inputs/outputs exist - // Note: hardswish uses output_operand (singular), not output_operands + // Special handling for resample2d: lower to CoreML upsample ops. + // Only handles 4D NCHW inputs (default WebNN layout) with scales or sizes. + if op_type_lower == "resample2d" { if op.input_operands().is_empty() || op.output_operand().is_none() { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: "hardswish requires input and output operand".to_string(), + reason: "resample2d requires input and output operand".to_string(), }); } + let input_id = op.input_operands()[0]; + let output_id = op.output_operand().unwrap(); + let input_operand = - graph_info.operand(op.input_operands()[0]).ok_or_else(|| { - GraphError::ConversionFailed { + graph_info + .operand(input_id) + .ok_or_else(|| GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!("Input operand {} not found", op.input_operands()[0]), - } - })?; - { - let input_name = operand_name(graph_info, op.input_operands()[0]); - let hardsigmoid_output_name = format!("{}_hardswish_hardsigmoid", input_name); + reason: format!("resample2d input operand {input_id} not found"), + })?; - // Create hardsigmoid operation with alpha=1/6, beta=0.5 - let mut hardsigmoid_inputs: HashMap = HashMap::new(); - hardsigmoid_inputs.insert( - "x".to_string(), - Self::create_name_argument(input_name.clone()), - ); - hardsigmoid_inputs - .insert("alpha".to_string(), Self::create_immediate_float(1.0 / 6.0)); - hardsigmoid_inputs - .insert("beta".to_string(), Self::create_immediate_float(0.5)); + let opts = match op { + Operation::Resample2d { options, .. } => options.clone().unwrap_or_default(), + _ => Default::default(), + }; - // Create tensor type for hardsigmoid output. - // Use `scalar_as_one_dim: true` to match the promotion the - // rest of the converter applies to rank-0 operands — with - // `false`, a rank-0 input gives hardsigmoid an rank-0 - // intermediate while the graph's declared output is - // `tensor`, and Apple's loader rejects the - // following `mul` with - // "Output '0' has unexpected type 'ios17.mul'. - // Expected tensor; got fp32." - // (surfaced by WPT "hardSwish float32 0D scalar default options"). - let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; - let dimensions = Self::mil_dimensions_from_graph_shape( - &input_operand.descriptor.shape, - true, - ); + let input_shape = input_operand.descriptor.static_or_max_shape(); + if input_shape.len() < 4 { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "resample2d requires 4D NCHW input, got {}D", + input_shape.len() + ), + }); + } - let value_type = ValueType { - r#type: Some( - crate::protos::coreml::mil_spec::value_type::Type::TensorType( - TensorType { - rank: dimensions.len() as i64, - data_type: dtype, - dimensions, - attributes: HashMap::new(), - }, + // Resolve axes: default to [2, 3] (NCHW H, W) when unspecified. + let axes: Vec = if opts.axes.is_empty() { + vec![2, 3] + } else { + opts.axes.clone() + }; + let is_nhwc = axes.len() == 2 && axes[0] == 1 && axes[1] == 2; + + // Determine input name and NCHW-relative H/W dims + their input sizes. + // For NHWC [N,H,W,C] (axes=[1,2]): transpose input to NCHW first. + // For NCHW (axes=[2,3] or [3,2]): use as-is. + let input_name_raw = + Self::output_name_for_operand(graph_info, input_id, &operand_name_overrides); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + + let output_operand = + graph_info + .operand(output_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("resample2d output operand {output_id} not found"), + })?; + let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; + + // For NHWC: emit pre-transpose (NHWC→NCHW) and record post-transpose. + let (upsample_input_name, upsample_output_name, upsample_output_type) = if is_nhwc { + let nchw_perm = [0u32, 3, 1, 2]; + let nchw_in_shape = + Self::permute_graph_shape(&input_operand.descriptor.shape, &nchw_perm); + let nchw_in_dims = Self::mil_dimensions_from_graph_shape(&nchw_in_shape, false); + let nchw_in_name = format!("{}_rs_in_nchw", output_name); + let nchw_in_type = NamedValueType { + name: nchw_in_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: nchw_in_dims.len() as i64, + data_type: dtype, + dimensions: nchw_in_dims, + attributes: HashMap::new(), + }, + ), ), - ), + }), }; + let mut pre_tp: HashMap = HashMap::new(); + pre_tp.insert("x".to_string(), Self::create_name_argument(input_name_raw)); + pre_tp.insert( + "perm".to_string(), + Self::create_immediate_int_array(&nchw_perm), + ); + main_block.operations.push(Self::create_mil_operation( + "transpose", + pre_tp, + vec![nchw_in_type], + )); - let hardsigmoid_output_type = NamedValueType { - name: hardsigmoid_output_name.clone(), - r#type: Some(value_type), + // Upsample intermediate is in NCHW; post-transpose to NHWC + let nchw_out_perm = [0u32, 3, 1, 2]; + let nchw_out_shape = + Self::permute_graph_shape(&output_operand.descriptor.shape, &nchw_out_perm); + let nchw_out_dims = + Self::mil_dimensions_from_graph_shape(&nchw_out_shape, false); + let nchw_out_name = format!("{}_rs_out_nchw", output_name); + let nchw_out_type = NamedValueType { + name: nchw_out_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: nchw_out_dims.len() as i64, + data_type: dtype, + dimensions: nchw_out_dims, + attributes: HashMap::new(), + }, + ), + ), + }), }; + (nchw_in_name, nchw_out_name, nchw_out_type) + } else { + (input_name_raw, output_name.clone(), output_type.clone()) + }; + + // Map scale factors using the resolved axes. + // For axes=[a0, a1]: sizes[0]/scales[0] applies to dim a0, + // sizes[1]/scales[1] applies to dim a1. + // For NHWC (axes=[1,2]): H=dim1, W=dim2 of original input. + // For NCHW (axes=[2,3]): H=dim2, W=dim3. + // For swapped NCHW (axes=[3,2]): sizes[0]→W(dim3), sizes[1]→H(dim2). + let (axis0, axis1) = if is_nhwc { + (1usize, 2usize) + } else if axes.len() >= 2 { + (axes[0] as usize, axes[1] as usize) + } else { + (2usize, 3usize) + }; + // H is the lower-numbered spatial dimension, W is the higher. + let (h_dim, w_dim, h_idx, w_idx) = if axis0 <= axis1 { + (axis0, axis1, 0usize, 1usize) + } else { + (axis1, axis0, 1usize, 0usize) + }; + let raw_input_h = input_shape.get(h_dim).copied().unwrap_or(1) as f32; + let raw_input_w = input_shape.get(w_dim).copied().unwrap_or(1) as f32; + + // WebNN: when `sizes` is provided it takes precedence and `scales` is ignored. + let sizes_valid = opts.sizes.as_ref().map(|s| s.len() >= 2).unwrap_or(false); + let (scale_h, scale_w) = if sizes_valid { + if raw_input_h == 0.0 || raw_input_w == 0.0 { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "resample2d: input spatial dimensions are zero".to_string(), + }); + } + let sizes = opts.sizes.as_ref().unwrap(); + let sh = sizes.get(h_idx).copied().unwrap_or(1) as f32 / raw_input_h; + let sw = sizes.get(w_idx).copied().unwrap_or(1) as f32 / raw_input_w; + (sh, sw) + } else if !opts.scales.is_empty() { + let sh = opts.scales.get(h_idx).copied().unwrap_or(1.0) as f32; + let sw = opts.scales.get(w_idx).copied().unwrap_or(1.0) as f32; + (sh, sw) + } else { + (1.0, 1.0) + }; + + let mode = if opts.mode.is_empty() { + "nearest-neighbor" + } else { + opts.mode.as_str() + }; + + let mil_op_name = if mode.eq_ignore_ascii_case("linear") { + mil_ops::UPSAMPLE_BILINEAR + } else { + mil_ops::UPSAMPLE_NEAREST_NEIGHBOR + }; + + let mut resample_inputs: HashMap = HashMap::new(); + resample_inputs.insert( + "x".to_string(), + Self::create_name_argument(upsample_input_name), + ); + resample_inputs.insert( + "scale_factor_height".to_string(), + Self::create_immediate_float(scale_h), + ); + resample_inputs.insert( + "scale_factor_width".to_string(), + Self::create_immediate_float(scale_w), + ); + if mode.eq_ignore_ascii_case("linear") { + // WebNN linear resample uses half-pixel coordinate centers + // (align_corners = false). + resample_inputs.insert( + "align_corners".to_string(), + Self::create_immediate_bool(false), + ); + resample_inputs.insert( + "half_pixel_centers".to_string(), + Self::create_immediate_bool(true), + ); + } + + main_block.operations.push(Self::create_mil_operation( + mil_op_name, + resample_inputs, + vec![upsample_output_type], + )); + + // For NHWC: post-transpose NCHW→NHWC + if is_nhwc { + let post_perm = [0u32, 2, 3, 1]; + let mut post_tp: HashMap = HashMap::new(); + post_tp.insert( + "x".to_string(), + Self::create_name_argument(upsample_output_name), + ); + post_tp.insert( + "perm".to_string(), + Self::create_immediate_int_array(&post_perm), + ); + main_block.operations.push(Self::create_mil_operation( + "transpose", + post_tp, + vec![output_type], + )); + } + + // Flush deferred transposes for this output + if let Some((pending_ops, transposed_name)) = deferred_transposes.remove(&output_id) + { + main_block.operations.extend(pending_ops); + operand_name_overrides.insert(output_id, transposed_name); + } + continue; + } + + // quantize/dequantize that CoreML's native op can't express (int32 tensors, + // block-wise or multi-axis scales) is lowered to elementwise arithmetic. + // int4/uint4 tensors can't be materialized at all, so leave those to the native + // path (which errors) rather than decomposing. + if op_type_lower == "dequantizelinear" && Self::qdq_should_decompose(graph_info, op) { + Self::emit_dequantize_decomposition( + graph_info, + op, + &operand_name_overrides, + &mut main_block, + )?; + continue; + } + if op_type_lower == "quantizelinear" && Self::qdq_should_decompose(graph_info, op) { + Self::emit_quantize_decomposition( + graph_info, + op, + &operand_name_overrides, + &mut main_block, + )?; + continue; + } + + // Recurrent networks are decomposed into primitive MIL ops (matmul, add, mul, + // sub, activations) because CoreML's native lstm/gru don't match WebNN's option + // set (custom activations, layouts, peephole, reset_after, ...). + if op_type_lower == "grucell" { + Self::emit_gru_cell_decomposition( + graph_info, + op, + &operand_name_overrides, + &mut main_block, + )?; + continue; + } + if op_type_lower == "lstmcell" { + Self::emit_lstm_cell_decomposition( + graph_info, + op, + &operand_name_overrides, + &mut main_block, + )?; + continue; + } + if op_type_lower == "gru" { + Self::emit_gru_decomposition( + graph_info, + op, + &operand_name_overrides, + &mut main_block, + )?; + continue; + } + if op_type_lower == "lstm" { + Self::emit_lstm_decomposition( + graph_info, + op, + &operand_name_overrides, + &mut main_block, + )?; + continue; + } + + // Dequantize/quantize with rank-0 (scalar) input: CoreML requires rank >= 1. + // Reshape [] → [1] before the op. The output type from create_output_value already + // promotes 0D to [1] (scalar_as_one_dim=true), so no reshape back is needed. + if op_type_lower == "dequantizelinear" || op_type_lower == "quantizelinear" { + if let Some(&input_id) = op.input_operands().first() { + if let Some(input_op) = graph_info.operand(input_id) { + if input_op.descriptor.shape.is_empty() { + let input_mil_type = + Self::mil_data_type(&input_op.descriptor.data_type)?; + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let output_id = op.output_operand().ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "dequantize/quantize op '{}' has no output", + op.op_type() + ), + } + })?; + let output_name = Self::output_name_for_operand( + graph_info, + output_id, + &operand_name_overrides, + ); + // Reshape [] → [1] + let reshaped_name = format!("{}_dq_in_1d", output_name); + let reshaped_type = NamedValueType { + name: reshaped_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: 1, + data_type: input_mil_type, + dimensions: vec![Dimension { + dimension: Some( + dimension::Dimension::Constant( + dimension::ConstantDimension { size: 1 }, + ), + ), + }], + attributes: HashMap::new(), + }, + ), + ), + }), + }; + let mut reshape_inputs = HashMap::new(); + reshape_inputs + .insert("x".to_string(), Self::create_name_argument(input_name)); + reshape_inputs.insert( + "shape".to_string(), + Self::create_immediate_int_array(&[1u32]), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + reshape_inputs, + vec![reshaped_type], + )); + // Apply dequantize/quantize with the [1] input override + let mut overrides_with_1d = operand_name_overrides.clone(); + overrides_with_1d.insert(input_id, reshaped_name); + let mil_op = self.convert_operation_with_overrides( + graph_info, + op, + &overrides_with_1d, + )?; + main_block.operations.push(mil_op); + continue; + } + } + } + } + + // BatchNormalization with rank < 3: CoreML batch_norm requires rank >= 3. + // Reshape input to 3D based on the axis parameter, apply batch_norm, reshape back. + // Skip when mean is non-constant — the decomposition path below handles that case. + if op_type_lower == "batchnormalization" { + if let Some(&input_id) = op.input_operands().first() { + if let Some(input_op) = graph_info.operand(input_id) { + let input_rank = input_op.descriptor.shape.len(); + let mean_is_nonconstant_for_rank_check = op + .input_operands() + .get(1) + .and_then(|&mid| graph_info.operand(mid)) + .map(|o| o.kind != crate::graph::OperandKind::Constant) + .unwrap_or(false); + if input_rank < 3 && !mean_is_nonconstant_for_rank_check { + let axis = match op { + Operation::BatchNormalization { options, .. } => { + options.as_ref().map(|o| o.axis as usize).unwrap_or(1) + } + _ => 1, + }; + let output_id = op.output_operand().ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("batchNorm op has no output operand"), + } + })?; + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let input_dtype = Self::mil_data_type(&input_op.descriptor.data_type)?; + + // Compute 3D shape: [batch_dims_product, C, spatial_dims_product] + let shape = &input_op.descriptor.shape; + let batch_size: u32 = shape[..axis] + .iter() + .map(|d| match d { + GraphDimension::Static(v) => *v, + GraphDimension::Dynamic(d) => d.max_size, + }) + .product::() + .max(1); + let channel_size: u32 = if axis < shape.len() { + match &shape[axis] { + GraphDimension::Static(v) => *v, + GraphDimension::Dynamic(d) => d.max_size, + } + } else { + 1 + }; + let spatial_size: u32 = shape[axis + 1..] + .iter() + .map(|d| match d { + GraphDimension::Static(v) => *v, + GraphDimension::Dynamic(d) => d.max_size, + }) + .product::() + .max(1); + let shape_3d = [batch_size, channel_size, spatial_size]; + + // Reshape input to 3D + let reshaped_input_name = format!("{}_bn_in_3d", output_name); + let reshaped_input_type = NamedValueType { + name: reshaped_input_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: 3, + data_type: input_dtype, + dimensions: shape_3d + .iter() + .map(|&d| Dimension { + dimension: Some( + dimension::Dimension::Constant( + dimension::ConstantDimension { + size: d as u64, + }, + ), + ), + }) + .collect(), + attributes: HashMap::new(), + }, + ), + ), + }), + }; + let mut reshape_in_inputs = HashMap::new(); + reshape_in_inputs + .insert("x".to_string(), Self::create_name_argument(input_name)); + reshape_in_inputs.insert( + "shape".to_string(), + Self::create_immediate_int_array(&shape_3d), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + reshape_in_inputs, + vec![reshaped_input_type], + )); + + // Apply batch_norm on the 3D tensor + let bn_result_name = format!("{}_bn_out_3d", output_name); + let bn_result_type = NamedValueType { + name: bn_result_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: 3, + data_type: input_dtype, + dimensions: shape_3d + .iter() + .map(|&d| Dimension { + dimension: Some( + dimension::Dimension::Constant( + dimension::ConstantDimension { + size: d as u64, + }, + ), + ), + }) + .collect(), + attributes: HashMap::new(), + }, + ), + ), + }), + }; + let mut overrides_with_3d = operand_name_overrides.clone(); + overrides_with_3d.insert(input_id, reshaped_input_name); + let bn_op = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &Self::input_names_for_operation( + graph_info, + op, + &overrides_with_3d, + ), + vec![bn_result_type], + self.get_mil_op_type(op.op_type())?, + )?; + main_block.operations.push(bn_op); + + // Reshape 3D result back to original output shape + let orig_output_shape: Vec = graph_info + .operand(output_id) + .map(|o| { + o.descriptor + .shape + .iter() + .map(|d| match d { + GraphDimension::Static(v) => *v, + GraphDimension::Dynamic(d) => d.max_size, + }) + .collect() + }) + .unwrap_or_default(); + let mut reshape_out_inputs = HashMap::new(); + reshape_out_inputs.insert( + "x".to_string(), + Self::create_name_argument(bn_result_name), + ); + reshape_out_inputs.insert( + "shape".to_string(), + Self::create_immediate_int_array(&orig_output_shape), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + reshape_out_inputs, + vec![output_type], + )); + continue; + } + } + } + } + + // cast targeting int64/uint32/uint64: CoreML MIL has no such tensor type. + // Emit the cast to int32 and expose the output as an int32 proxy at the + // model interface (the executor widens int32 -> int64/uint64 on readback, + // or reinterprets int32 -> uint32 for same-width types). + if op_type_lower == "cast" { + use crate::protos::coreml::mil_spec::DataType as MilDataType; + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "cast operation has no output operand".to_string(), + })?; + let out_dt = graph_info + .operand(output_id) + .map(|o| o.descriptor.data_type.clone()); + if matches!( + out_dt, + Some(DataType::Uint32 | DataType::Int64 | DataType::Uint64) + ) { + let input_names = + Self::input_names_for_operation(graph_info, op, &operand_name_overrides); + let (output_name, _) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + // Track the INTERFACE output name so the final cast loop and the model + // feature description expose this output as int32 rather than float32. + int32_proxy_output_names.insert(operand_name(graph_info, output_id)); + let out_type = Self::create_value_with_mil_type( + graph_info, + output_id, + output_name, + MilDataType::Int32 as i32, + )?; + let mut inputs: HashMap = HashMap::new(); + if let Some(first) = input_names.first() { + inputs.insert("x".to_string(), Self::create_argument(first)); + } + inputs.insert("dtype".to_string(), Self::create_immediate_string("int32")); + main_block.operations.push(Self::create_mil_operation( + mil_ops::CAST, + inputs, + vec![out_type], + )); + continue; + } + } + + // ArgMax/ArgMin: handle both int input types and int64 output type. + // CoreML reduce_argmax/reduce_argmin only accepts float input and produces int32. + // For int8/uint8/int32 inputs: cast to float32 first. + // For int64 output: cast the int32 result to int64. + if op_type_lower == "argmax" || op_type_lower == "argmin" { + use crate::protos::coreml::mil_spec::DataType as MilDataType; + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("operation '{}' has no output operand", op.op_type()), + })?; + let output_operand = + graph_info + .operand(output_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Output operand {} not found", output_id), + })?; - let hardsigmoid_op = Self::create_mil_operation( - "sigmoid_hard", - hardsigmoid_inputs, - vec![hardsigmoid_output_type], - ); + let int_input_id = op.input_operands().first().copied(); + let input_needs_float_cast = int_input_id + .and_then(|id| graph_info.operand(id)) + .map(|inp| { + matches!( + inp.descriptor.data_type, + DataType::Int8 + | DataType::Uint8 + | DataType::Int32 + | DataType::Uint32 + | DataType::Int64 + ) + }) + .unwrap_or(false); + // CoreML argMax/argMin always outputs int32. When WebNN declares the output as + // int64 or uint32 (unsupported by CoreML cast), proxy through int32. + let output_needs_int32_proxy = matches!( + output_operand.descriptor.data_type, + DataType::Int64 | DataType::Uint32 + ); - main_block.operations.push(hardsigmoid_op); + if input_needs_float_cast || output_needs_int32_proxy { + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; - // Create mul operation: x * hardsigmoid_output - let mut mul_inputs: HashMap = HashMap::new(); - mul_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); - mul_inputs.insert( - "y".to_string(), - Self::create_name_argument(hardsigmoid_output_name), - ); + // When the WebNN output type is unsupported by CoreML, produce int32 output + // directly instead of casting to an unsupported type. + let final_output_type = if output_needs_int32_proxy { + Self::create_value_with_mil_type( + graph_info, + output_id, + output_name.clone(), + MilDataType::Int32 as i32, + )? + } else { + output_type.clone() + }; - // Get output name (using singular output_operand field) - let output_operand_id = op.output_operand().unwrap(); - let output_name = operand_name(graph_info, output_operand_id); - let output_operand = - graph_info.operand(output_operand_id).ok_or_else(|| { - GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!("Output operand {} not found", output_operand_id), + // Optional: cast integer/unsupported input type to float32 + let mut input_names = + Self::input_names_for_operation(graph_info, op, &operand_name_overrides); + if input_needs_float_cast { + if let Some(input_id) = int_input_id { + let orig_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let float_name = format!("{}_arg_float", output_name); + let float_type = Self::create_value_with_mil_type( + graph_info, + input_id, + float_name.clone(), + MilDataType::Float32 as i32, + )?; + main_block + .operations + .push(Self::create_cast_operation(orig_name, float_type, "fp32")); + if !input_names.is_empty() { + input_names[0] = float_name; } - })?; - - let output_dtype = Self::mil_data_type(&output_operand.descriptor.data_type)?; - // Same promotion as the hardsigmoid intermediate above: the - // mul output must have the same rank as its inputs, and - // the graph's input/output operands already went through - // the `scalar_as_one_dim: true` pass when they were - // declared in `main` block — so rank-0 becomes [1] here too. - let output_dimensions = Self::mil_dimensions_from_graph_shape( - &output_operand.descriptor.shape, - true, - ); + } + } - let output_value_type = ValueType { - r#type: Some( - crate::protos::coreml::mil_spec::value_type::Type::TensorType( - TensorType { - rank: output_dimensions.len() as i64, - data_type: output_dtype, - dimensions: output_dimensions, - attributes: HashMap::new(), - }, - ), - ), - }; + if output_needs_int32_proxy { + // Track the INTERFACE output name (without _graph suffix) for the + // final cast loop, which uses int32 instead of the default float32. + int32_proxy_output_names.insert(operand_name(graph_info, output_id)); + // Emit argmax/argmin directly with int32 output (no cast needed). + let mil_op_type = self.get_mil_op_type(op.op_type())?; + let arg_op = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &input_names, + vec![final_output_type], + mil_op_type, + )?; + main_block.operations.push(arg_op); + } else { + // Apply argmax/argmin (CoreML always produces int32), then cast to int32. + let int32_name = format!("{}_int32", output_name); + let int32_type = Self::create_value_with_mil_type( + graph_info, + output_id, + int32_name.clone(), + MilDataType::Int32 as i32, + )?; + let mil_op_type = self.get_mil_op_type(op.op_type())?; + let arg_op = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &input_names, + vec![int32_type], + mil_op_type, + )?; + main_block.operations.push(arg_op); + main_block.operations.push(Self::create_cast_operation( + int32_name, + output_type, + "int32", + )); + } + continue; + } + } - let mul_output_type = NamedValueType { - name: output_name, - r#type: Some(output_value_type), + // Gather: cast uint32/int64 indices to int32 (CoreML gather only accepts int32 and smaller). + // gather / gatherElements: cast indices to int32 and normalize them to WebNN + // semantics (wrap negatives, clamp out-of-bounds), which CoreML gather does not + // do on its own (it would read out-of-range memory). + if matches!(op_type_lower.as_str(), "gather" | "gatherelements") { + use crate::protos::coreml::mil_spec::DataType as MilDataType; + let data_id = op.input_operands().first().copied(); + let idx_id = op.input_operands().get(1).copied(); + // CoreML promotes 0D scalars to rank-1, which changes the gather output + // rank; leave scalar-index gathers to the generic path. + let idx_is_scalar = idx_id + .and_then(|id| graph_info.operand(id)) + .map(|o| o.descriptor.shape.is_empty()) + .unwrap_or(true); + if let (Some(data_id), Some(idx_id), Some(output_id), false) = + (data_id, idx_id, op.output_operand(), idx_is_scalar) + { + let axis = match op { + Operation::Gather { options, .. } => { + options.as_ref().map(|o| o.axis).unwrap_or(0) + } + Operation::GatherElements { options, .. } => { + options.as_ref().map(|o| o.axis).unwrap_or(0) + } + _ => 0, }; + let data_shape = graph_info + .operand(data_id) + .map(|o| o.descriptor.static_or_max_shape()) + .unwrap_or_default(); + let idx_shape = graph_info + .operand(idx_id) + .map(|o| o.descriptor.static_or_max_shape()) + .unwrap_or_default(); + let axis_size = data_shape.get(axis as usize).copied().unwrap_or(1); - let mul_op = - Self::create_mil_operation("mul", mul_inputs, vec![mul_output_type]); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + let data_name = + Self::output_name_for_operand(graph_info, data_id, &operand_name_overrides); + let idx_name = + Self::output_name_for_operand(graph_info, idx_id, &operand_name_overrides); + + // Cast indices to int32 (idempotent when already int32). + let cast_idx = format!("{}_idx_i32", output_name); + let cast_idx_type = Self::create_value_with_mil_type( + graph_info, + idx_id, + cast_idx.clone(), + MilDataType::Int32 as i32, + )?; + main_block.operations.push(Self::create_cast_operation( + idx_name, + cast_idx_type, + "int32", + )); + let norm_idx = Self::emit_gather_index_norm( + &mut main_block, + &cast_idx, + &idx_shape, + &[axis_size], + &output_name, + ); - main_block.operations.push(mul_op); + let mil_op = if op_type_lower == "gather" { + mil_ops::GATHER + } else { + mil_ops::GATHER_ALONG_AXIS + }; + let mut gather_inputs: HashMap = HashMap::new(); + gather_inputs.insert("x".to_string(), Self::create_name_argument(data_name)); + gather_inputs + .insert("indices".to_string(), Self::create_name_argument(norm_idx)); + gather_inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); + gather_inputs.insert( + "validate_indices".to_string(), + Self::create_immediate_bool(false), + ); + main_block.operations.push(Self::create_mil_operation( + mil_op, + gather_inputs, + vec![output_type], + )); + continue; } - - // Skip normal operation conversion for hardswish - continue; } - // Special handling for gemm: y = alpha * op(a) * op(b) + beta * c - // Lower to matmul + optional mul(alpha) + optional mul(beta, c) + add. - if op_type_lower == "gemm" { - if op.input_operands().len() < 2 || op.output_operand().is_none() { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: "gemm requires at least 2 input operands and 1 output".to_string(), - }); + // gatherND: normalize the multi-component indices (wrap negatives / clamp OOB + // against each indexed input dimension) before CoreML's gather_nd. + if op_type_lower == "gathernd" { + use crate::protos::coreml::mil_spec::DataType as MilDataType; + let data_id = op.input_operands().first().copied(); + let idx_id = op.input_operands().get(1).copied(); + if let (Some(data_id), Some(idx_id), Some(output_id)) = + (data_id, idx_id, op.output_operand()) + { + let data_shape = graph_info + .operand(data_id) + .map(|o| o.descriptor.static_or_max_shape()) + .unwrap_or_default(); + let idx_shape = graph_info + .operand(idx_id) + .map(|o| o.descriptor.static_or_max_shape()) + .unwrap_or_default(); + // CoreML gather_nd crashes on rank-5+ data; leave those to the + // (guarded) generic path which reports the limitation. + let k = idx_shape.last().copied().unwrap_or(0) as usize; + if data_shape.len() <= 4 && k >= 1 && k <= data_shape.len() { + let sizes: Vec = data_shape[..k].to_vec(); + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let data_name = Self::output_name_for_operand( + graph_info, + data_id, + &operand_name_overrides, + ); + let idx_name = Self::output_name_for_operand( + graph_info, + idx_id, + &operand_name_overrides, + ); + let cast_idx = format!("{}_idx_i32", output_name); + let cast_idx_type = Self::create_value_with_mil_type( + graph_info, + idx_id, + cast_idx.clone(), + MilDataType::Int32 as i32, + )?; + main_block.operations.push(Self::create_cast_operation( + idx_name, + cast_idx_type, + "int32", + )); + let norm_idx = Self::emit_gather_index_norm( + &mut main_block, + &cast_idx, + &idx_shape, + &sizes, + &output_name, + ); + let mut gnd_inputs: HashMap = HashMap::new(); + gnd_inputs.insert("x".to_string(), Self::create_name_argument(data_name)); + gnd_inputs + .insert("indices".to_string(), Self::create_name_argument(norm_idx)); + gnd_inputs.insert( + "validate_indices".to_string(), + Self::create_immediate_bool(false), + ); + main_block.operations.push(Self::create_mil_operation( + mil_ops::GATHER_ND, + gnd_inputs, + vec![output_type], + )); + continue; + } } + } - let output_operand_id = op.output_operand().unwrap(); - let output_operand = graph_info.operand(output_operand_id).ok_or_else(|| { - GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!("Output operand {} not found", output_operand_id), - } - })?; + // BatchNormalization decomposition: when mean/variance are runtime inputs (non-constant) + // or when axis != 1 (NHWC or other non-standard layouts), CoreML's native batch_norm + // cannot be used. Decompose into element-wise ops: + // normalized = (x - mean) / sqrt(variance + epsilon) + // output = scale * normalized + bias (if scale/bias present) + // Mean/variance may need reshaping for broadcasting when axis is not the last dimension. + if op_type_lower == "batchnormalization" { + if let Operation::BatchNormalization { options, .. } = op { + let mean_id = op.input_operands().get(1).copied(); + let variance_id = op.input_operands().get(2).copied(); + let scale_id = options.as_ref().and_then(|o| o.scale); + let bias_id = options.as_ref().and_then(|o| o.bias); + let epsilon = options.as_ref().map(|o| o.epsilon as f32).unwrap_or(1e-5); + let axis = options.as_ref().map(|o| o.axis as usize).unwrap_or(1); + + let mean_is_nonconstant = mean_id + .and_then(|id| graph_info.operand(id)) + .map(|o| o.kind != crate::graph::OperandKind::Constant) + .unwrap_or(false); + + let input_id = op.input_operands().first().copied(); + let input_operand = input_id.and_then(|id| graph_info.operand(id)); + let input_rank = input_operand.map(|o| o.descriptor.shape.len()).unwrap_or(0); + + // Use decomposition when mean is non-constant or axis is non-standard (not 1). + // For axis==1 with constant mean, the native batch_norm path (fallthrough) works. + let use_decomposition = mean_is_nonconstant || (axis != 1 && input_rank >= 2); + + if use_decomposition { + if let (Some(input_id), Some(mean_id_v), Some(variance_id_v)) = + (input_id, mean_id, variance_id) + { + let output_id = op.output_operand().ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "batchNorm has no output".to_string(), + } + })?; + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let mean_name = Self::output_name_for_operand( + graph_info, + mean_id_v, + &operand_name_overrides, + ); + let var_name = Self::output_name_for_operand( + graph_info, + variance_id_v, + &operand_name_overrides, + ); - let (output_name, output_type) = Self::create_value(graph_info, output_operand_id)?; + let inp_op = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("batchNorm input {} not found", input_id), + } + })?; + let dtype = Self::mil_data_type(&inp_op.descriptor.data_type)?; + let is_f16 = inp_op.descriptor.data_type == DataType::Float16; + + // When axis is not the last dimension, reshape mean/variance for + // correct broadcasting: [C] → [1, C, 1, 1, ...] with 1s elsewhere. + // Also return the effective var shape so downstream add/sqrt types match. + let (mean_for_sub, var_for_div, effective_var_shape) = if axis + != input_rank.saturating_sub(1) + && input_rank > 1 + { + let c_size = inp_op + .descriptor + .shape + .get(axis) + .map(|d| match d { + GraphDimension::Static(v) => *v, + GraphDimension::Dynamic(d) => d.max_size, + }) + .unwrap_or(1); + let mut bcast_shape = vec![1u32; input_rank]; + bcast_shape[axis] = c_size; + let effective_var_shape: Vec = bcast_shape + .iter() + .map(|&d| GraphDimension::Static(d)) + .collect(); + let bcast_dims: Vec = bcast_shape + .iter() + .map(|&d| Dimension { + dimension: Some(dimension::Dimension::Constant( + dimension::ConstantDimension { size: d as u64 }, + )), + }) + .collect(); + + let mean_rs_name = format!("{}_bn_mean_rs", output_name); + let mean_rs_type = NamedValueType { + name: mean_rs_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: input_rank as i64, + data_type: dtype, + dimensions: bcast_dims.clone(), + attributes: HashMap::new(), + }, + ), + ), + }), + }; + let mut rs_in: HashMap = HashMap::new(); + rs_in.insert( + "x".to_string(), + Self::create_name_argument(mean_name.clone()), + ); + rs_in.insert( + "shape".to_string(), + Self::create_immediate_int_array(&bcast_shape), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + rs_in, + vec![mean_rs_type], + )); + + let var_rs_name = format!("{}_bn_var_rs", output_name); + let var_rs_type = NamedValueType { + name: var_rs_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: input_rank as i64, + data_type: dtype, + dimensions: bcast_dims, + attributes: HashMap::new(), + }, + ), + ), + }), + }; + let mut rs_var: HashMap = HashMap::new(); + rs_var.insert( + "x".to_string(), + Self::create_name_argument(var_name.clone()), + ); + rs_var.insert( + "shape".to_string(), + Self::create_immediate_int_array(&bcast_shape), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + rs_var, + vec![var_rs_type], + )); + (mean_rs_name, var_rs_name, effective_var_shape) + } else { + let orig_var_shape = graph_info + .operand(variance_id_v) + .map(|o| o.descriptor.shape.clone()) + .unwrap_or_default(); + (mean_name, var_name, orig_var_shape) + }; - let (alpha, beta) = match &op { - Operation::Gemm { options, .. } => ( - options.as_ref().map(|o| o.alpha as f32).unwrap_or(1.0), - options.as_ref().map(|o| o.beta as f32).unwrap_or(1.0), - ), - _ => (1.0, 1.0), - }; + // Build an intermediate value type matching the output shape. + let out_shape = &inp_op.descriptor.shape; + let out_dims = Self::mil_dimensions_from_graph_shape(out_shape, false); + let make_intermediate = |name: String| { + NamedValueType { + name, + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: out_dims.len() as i64, + data_type: dtype, + dimensions: out_dims.clone(), + attributes: HashMap::new(), + }, + ), + ), + }), + } + }; - let has_bias = op.input_operands().len() >= 3; - let needs_alpha_mul = (alpha - 1.0).abs() > f32::EPSILON; - let needs_beta_mul = has_bias && (beta - 1.0).abs() > f32::EPSILON; + // x_minus_mean = x - mean + let x_minus_mean = format!("{}_bn_xmm", output_name); + let mut sub_in: HashMap = HashMap::new(); + sub_in.insert("x".to_string(), Self::create_name_argument(input_name)); + sub_in + .insert("y".to_string(), Self::create_name_argument(mean_for_sub)); + main_block.operations.push(Self::create_mil_operation( + "sub", + sub_in, + vec![make_intermediate(x_minus_mean.clone())], + )); - let (alpha_arg, beta_arg) = match output_operand.descriptor.data_type { - DataType::Float16 => ( - Self::create_immediate_float16(alpha), - Self::create_immediate_float16(beta), - ), - DataType::Float32 => ( - Self::create_immediate_float(alpha), - Self::create_immediate_float(beta), - ), - _ => { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "gemm currently supports float32/float16 output, got {:?}", - output_operand.descriptor.data_type - ), - }); - } - }; + // var_plus_eps = variance + epsilon + // Use effective_var_shape (may be reshaped to bcast_shape) for type. + let var_dims = + Self::mil_dimensions_from_graph_shape(&effective_var_shape, false); + let make_var_type = |name: String| { + NamedValueType { + name, + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: var_dims.len() as i64, + data_type: dtype, + dimensions: var_dims.clone(), + attributes: HashMap::new(), + }, + ), + ), + }), + } + }; + let var_eps_name = format!("{}_bn_veps", output_name); + let eps_arg = if is_f16 { + Self::create_immediate_float16(epsilon) + } else { + Self::create_immediate_float(epsilon) + }; + let mut veps_in: HashMap = HashMap::new(); + veps_in.insert( + "x".to_string(), + Self::create_name_argument(var_for_div.clone()), + ); + veps_in.insert("y".to_string(), eps_arg); + main_block.operations.push(Self::create_mil_operation( + "add", + veps_in, + vec![make_var_type(var_eps_name.clone())], + )); - let mut current_name: String; - let matmul_output_name = if needs_alpha_mul || has_bias { - format!("{}_gemm_matmul", output_name) - } else { - output_name.clone() - }; + // std = sqrt(var_plus_eps) + let std_name = format!("{}_bn_std", output_name); + let mut sqrt_in: HashMap = HashMap::new(); + sqrt_in + .insert("x".to_string(), Self::create_name_argument(var_eps_name)); + main_block.operations.push(Self::create_mil_operation( + "sqrt", + sqrt_in, + vec![make_var_type(std_name.clone())], + )); - let mut matmul_inputs: HashMap = HashMap::new(); - matmul_inputs.insert( - "x".to_string(), - Self::create_name_argument(operand_name(graph_info, op.input_operands()[0])), - ); - matmul_inputs.insert( - "y".to_string(), - Self::create_name_argument(operand_name(graph_info, op.input_operands()[1])), - ); - let (a_transpose, b_transpose) = match &op { - Operation::Gemm { options, .. } => ( - options.as_ref().map(|o| o.a_transpose).unwrap_or(false), - options.as_ref().map(|o| o.b_transpose).unwrap_or(false), - ), - _ => (false, false), - }; - matmul_inputs.insert( - "transpose_x".to_string(), - Self::create_immediate_bool(a_transpose), - ); - matmul_inputs.insert( - "transpose_y".to_string(), - Self::create_immediate_bool(b_transpose), - ); + // normalized = x_minus_mean / std + let normed_name = format!("{}_bn_normed", output_name); + let mut div_in: HashMap = HashMap::new(); + div_in + .insert("x".to_string(), Self::create_name_argument(x_minus_mean)); + div_in.insert("y".to_string(), Self::create_name_argument(std_name)); + main_block.operations.push(Self::create_mil_operation( + "real_div", + div_in, + vec![make_intermediate(normed_name.clone())], + )); - main_block.operations.push(Self::create_mil_operation( - "matmul", - matmul_inputs, - vec![NamedValueType { - name: matmul_output_name.clone(), - r#type: output_type.r#type.clone(), - }], - )); - current_name = matmul_output_name; + // Apply scale (gamma) and bias (beta) if present + let after_scale = if let Some(sc_id) = scale_id { + let sc_name = Self::output_name_for_operand( + graph_info, + sc_id, + &operand_name_overrides, + ); + let scaled_name = format!("{}_bn_scaled", output_name); + let mut mul_in: HashMap = HashMap::new(); + mul_in.insert( + "x".to_string(), + Self::create_name_argument(normed_name), + ); + mul_in.insert("y".to_string(), Self::create_name_argument(sc_name)); + main_block.operations.push(Self::create_mil_operation( + "mul", + mul_in, + vec![make_intermediate(scaled_name.clone())], + )); + scaled_name + } else { + normed_name + }; - if needs_alpha_mul { - let alpha_output_name = if has_bias { - format!("{}_gemm_alpha", output_name) - } else { - output_name.clone() - }; + let final_name = if let Some(bi_id) = bias_id { + let bi_name = Self::output_name_for_operand( + graph_info, + bi_id, + &operand_name_overrides, + ); + let biased_name = output_name.clone(); + let mut add_in: HashMap = HashMap::new(); + add_in.insert( + "x".to_string(), + Self::create_name_argument(after_scale), + ); + add_in.insert("y".to_string(), Self::create_name_argument(bi_name)); + main_block.operations.push(Self::create_mil_operation( + "add", + add_in, + vec![output_type], + )); + biased_name + } else { + // No bias: rename final intermediate to output name via identity + let mut id_in: HashMap = HashMap::new(); + id_in.insert( + "x".to_string(), + Self::create_name_argument(after_scale), + ); + main_block.operations.push(Self::create_mil_operation( + "identity", + id_in, + vec![output_type], + )); + output_name.clone() + }; + let _ = final_name; + continue; + } + } + } + } + + // Special handling for instanceNormalization with NHWC layout. + // CoreML instance_norm requires NCHW [N,C,H,W]. For NHWC inputs: + // transpose NHWC→NCHW, instance_norm(NCHW), transpose NCHW→NHWC. + if op_type_lower == "instancenormalization" { + let inst_layout = match op { + Operation::InstanceNormalization { options, .. } => { + options.as_ref().map(|o| o.layout.as_str()).unwrap_or("") + } + _ => "", + }; + if inst_layout.eq_ignore_ascii_case("nhwc") { + let input_id = op.input_operands().first().copied().ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "instanceNorm op has no input operand".to_string(), + } + })?; + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "instanceNorm op has no output operand".to_string(), + })?; + let input_operand = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("instanceNorm input operand {} not found", input_id), + } + })?; + let output_operand = graph_info.operand(output_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("instanceNorm output operand {} not found", output_id), + } + })?; - let mut alpha_mul_inputs: HashMap = HashMap::new(); - alpha_mul_inputs - .insert("x".to_string(), Self::create_name_argument(current_name)); - alpha_mul_inputs.insert("y".to_string(), alpha_arg); + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; + // Pre-transpose: NHWC [N,H,W,C] -> NCHW [N,C,H,W], perm=[0,3,1,2] + let nchw_in_name = format!("{}_in_nchw", output_name); + let nchw_perm = [0u32, 3, 1, 2]; + let nchw_in_shape = + Self::permute_graph_shape(&input_operand.descriptor.shape, &nchw_perm); + let nchw_in_dims = Self::mil_dimensions_from_graph_shape(&nchw_in_shape, false); + let nchw_in_type = NamedValueType { + name: nchw_in_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: nchw_in_dims.len() as i64, + data_type: dtype, + dimensions: nchw_in_dims, + attributes: HashMap::new(), + }, + ), + ), + }), + }; + let mut pre_tp: HashMap = HashMap::new(); + pre_tp.insert("x".to_string(), Self::create_name_argument(input_name)); + pre_tp.insert( + "perm".to_string(), + Self::create_immediate_int_array(&nchw_perm), + ); main_block.operations.push(Self::create_mil_operation( - "mul", - alpha_mul_inputs, - vec![NamedValueType { - name: alpha_output_name.clone(), - r#type: output_type.r#type.clone(), - }], + "transpose", + pre_tp, + vec![nchw_in_type], )); - current_name = alpha_output_name; - } - - if has_bias { - let c_operand_id = op.input_operands()[2]; - let (c_name, c_type) = Self::create_value(graph_info, c_operand_id)?; - let scaled_c_name = if needs_beta_mul { - format!("{}_gemm_bias", output_name) - } else { - c_name.clone() + // instance_norm on NCHW intermediate + let nchw_out_name = format!("{}_out_nchw", output_name); + let nchw_out_perm = [0u32, 3, 1, 2]; + let nchw_out_shape = + Self::permute_graph_shape(&output_operand.descriptor.shape, &nchw_out_perm); + let nchw_out_dims = + Self::mil_dimensions_from_graph_shape(&nchw_out_shape, false); + let nchw_out_type = NamedValueType { + name: nchw_out_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: nchw_out_dims.len() as i64, + data_type: dtype, + dimensions: nchw_out_dims, + attributes: HashMap::new(), + }, + ), + ), + }), }; + let mut overrides_nchw = operand_name_overrides.clone(); + overrides_nchw.insert(input_id, nchw_in_name); + let in_names = Self::input_names_for_operation(graph_info, op, &overrides_nchw); + let norm_op = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &in_names, + vec![nchw_out_type], + mil_ops::INSTANCE_NORM, + )?; + main_block.operations.push(norm_op); - if needs_beta_mul { - let mut beta_mul_inputs: HashMap = HashMap::new(); - beta_mul_inputs.insert("x".to_string(), Self::create_name_argument(c_name)); - beta_mul_inputs.insert("y".to_string(), beta_arg); + // Post-transpose: NCHW [N,C,H,W] -> NHWC [N,H,W,C], perm=[0,2,3,1] + let post_perm = [0u32, 2, 3, 1]; + let mut post_tp: HashMap = HashMap::new(); + post_tp.insert("x".to_string(), Self::create_name_argument(nchw_out_name)); + post_tp.insert( + "perm".to_string(), + Self::create_immediate_int_array(&post_perm), + ); + main_block.operations.push(Self::create_mil_operation( + "transpose", + post_tp, + vec![output_type], + )); - main_block.operations.push(Self::create_mil_operation( - "mul", - beta_mul_inputs, - vec![NamedValueType { - name: scaled_c_name.clone(), - r#type: c_type.r#type, - }], - )); + if let Some((pending_ops, transposed_name)) = + deferred_transposes.remove(&output_id) + { + main_block.operations.extend(pending_ops); + operand_name_overrides.insert(output_id, transposed_name); } + continue; + } + } - let mut add_inputs: HashMap = HashMap::new(); - add_inputs.insert("x".to_string(), Self::create_name_argument(current_name)); - add_inputs.insert("y".to_string(), Self::create_name_argument(scaled_c_name)); + // Special handling for conv2d / convTranspose2d with NHWC layout. + // CoreML conv requires NCHW. The pre-scan (above) has already transposed input and + // filter operands to NCHW and recorded the overrides. Here we run the conv op with + // an intermediate NCHW output name, then post-transpose to restore NHWC. + if op_type_lower == "conv2d" || op_type_lower == "convtranspose2d" { + let conv_layout = match op { + Operation::Conv2d { options, .. } => options + .as_ref() + .map(|o| o.input_layout.as_str()) + .unwrap_or(""), + Operation::ConvTranspose2d { options, .. } => options + .as_ref() + .map(|o| o.input_layout.as_str()) + .unwrap_or(""), + _ => "", + }; + if conv_layout.eq_ignore_ascii_case("nhwc") { + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("{} op has no output operand", op.op_type()), + })?; + let output_operand = graph_info.operand(output_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("conv2d output operand {} not found", output_id), + } + })?; + + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + let dtype = Self::mil_data_type(&output_operand.descriptor.data_type)?; + + // Intermediate NCHW output: permute NHWC [N,H',W',C'] → [N,C',H',W'] + let nchw_perm = [0u32, 3, 1, 2]; + let nchw_out_shape = + Self::permute_graph_shape(&output_operand.descriptor.shape, &nchw_perm); + let nchw_out_dims = + Self::mil_dimensions_from_graph_shape(&nchw_out_shape, false); + let nchw_out_name = format!("{}_nchw_out", output_name); + let nchw_out_type = NamedValueType { + name: nchw_out_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: nchw_out_dims.len() as i64, + data_type: dtype, + dimensions: nchw_out_dims, + attributes: HashMap::new(), + }, + ), + ), + }), + }; + // Run conv with NCHW-transposed inputs (set up by pre-scan) and NCHW output + let input_names = + Self::input_names_for_operation(graph_info, op, &operand_name_overrides); + let mil_op_type = self.get_mil_op_type(op.op_type())?; + let conv_op = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &input_names, + vec![nchw_out_type], + mil_op_type, + )?; + main_block.operations.push(conv_op); + + // Post-transpose: NCHW [N,C',H',W'] → NHWC [N,H',W',C'], perm=[0,2,3,1] + let post_perm = [0u32, 2, 3, 1]; + let mut post_tp_inputs: HashMap = HashMap::new(); + post_tp_inputs + .insert("x".to_string(), Self::create_name_argument(nchw_out_name)); + post_tp_inputs.insert( + "perm".to_string(), + Self::create_immediate_int_array(&post_perm), + ); main_block.operations.push(Self::create_mil_operation( - "add", - add_inputs, - vec![NamedValueType { - name: output_name, - r#type: output_type.r#type, - }], + "transpose", + post_tp_inputs, + vec![output_type], )); - } - - continue; - } - // Special handling for linear: y = alpha * x + beta - // Lower to mul + add primitives for backend parity. - if op_type_lower == "linear" { - if op.input_operands().is_empty() || op.output_operand().is_none() { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: "linear requires input and output operand".to_string(), - }); + // Flush deferred transposes for this output + if let Some((pending_ops, transposed_name)) = + deferred_transposes.remove(&output_id) + { + main_block.operations.extend(pending_ops); + operand_name_overrides.insert(output_id, transposed_name); + } + continue; } + } - let input_operand = - graph_info.operand(op.input_operands()[0]).ok_or_else(|| { + // Special handling for averagePool2d / maxPool2d with NHWC layout. + // CoreML pooling only supports NCHW, so we wrap with transpose ops: + // transpose(NHWC→NCHW), pool(NCHW), transpose(NCHW→NHWC). + if op_type_lower == "averagepool2d" + || op_type_lower == "maxpool2d" + || op_type_lower == "l2pool2d" + { + let pool_layout = match op { + Operation::AveragePool2d { options, .. } + | Operation::MaxPool2d { options, .. } + | Operation::L2Pool2d { options, .. } => { + options.as_ref().map(|o| o.layout.as_str()).unwrap_or("") + } + _ => "", + }; + if pool_layout.eq_ignore_ascii_case("nhwc") { + let input_id = op.input_operands().first().copied().ok_or_else(|| { GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!("Input operand {} not found", op.input_operands()[0]), + reason: format!("pool op '{}' has no input operand", op.op_type()), } })?; - let output_operand_id = op.output_operand().unwrap(); - let output_operand = graph_info.operand(output_operand_id).ok_or_else(|| { - GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!("Output operand {} not found", output_operand_id), - } - })?; - - let (alpha, beta) = match &op { - Operation::Linear { options, .. } => options - .as_ref() - .map(|o| (o.alpha as f32, o.beta as f32)) - .unwrap_or((1.0, 0.0)), - _ => (1.0, 0.0), - }; + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("pool op '{}' has no output operand", op.op_type()), + })?; - let (alpha_arg, beta_arg) = match input_operand.descriptor.data_type { - DataType::Float16 => ( - Self::create_immediate_float16(alpha), - Self::create_immediate_float16(beta), - ), - DataType::Float32 => ( - Self::create_immediate_float(alpha), - Self::create_immediate_float(beta), - ), - _ => { - return Err(GraphError::ConversionFailed { + let input_operand = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!( - "linear currently supports float32/float16, got {:?}", - input_operand.descriptor.data_type - ), - }); - } - }; + reason: format!("pool input operand {} not found", input_id), + } + })?; + let output_operand = graph_info.operand(output_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("pool output operand {} not found", output_id), + } + })?; - let input_name = operand_name(graph_info, op.input_operands()[0]); - let output_name = operand_name(graph_info, output_operand_id); - let mul_output_name = format!("{}_linear_mul", output_name); + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + let dtype = Self::mil_data_type(&input_operand.descriptor.data_type)?; - let output_dtype = Self::mil_data_type(&output_operand.descriptor.data_type)?; - let output_shape = if output_operand.descriptor.shape.is_empty() { - vec![1u32] - } else { - output_operand.descriptor.static_or_max_shape() - }; - let output_dimensions: Vec = output_shape - .iter() - .map(|&d| Dimension { - dimension: Some(dimension::Dimension::Constant( - dimension::ConstantDimension { size: d as u64 }, - )), - }) - .collect(); + // Pre-transpose: NHWC [N,H,W,C] -> NCHW [N,C,H,W], perm=[0,3,1,2] + let nchw_input_name = format!("{}_pool_nchw_in", output_name); + let nchw_input_perm = [0u32, 3, 1, 2]; + let nchw_input_shape = Self::permute_graph_shape( + &input_operand.descriptor.shape, + &nchw_input_perm, + ); + let nchw_input_dims = + Self::mil_dimensions_from_graph_shape(&nchw_input_shape, false); - let value_type = ValueType { - r#type: Some( - crate::protos::coreml::mil_spec::value_type::Type::TensorType(TensorType { - rank: output_dimensions.len() as i64, - data_type: output_dtype, - dimensions: output_dimensions.clone(), - attributes: HashMap::new(), + let nchw_input_type = NamedValueType { + name: nchw_input_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: nchw_input_dims.len() as i64, + data_type: dtype, + dimensions: nchw_input_dims, + attributes: HashMap::new(), + }, + ), + ), }), - ), - }; - - let mut mul_inputs: HashMap = HashMap::new(); - mul_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); - mul_inputs.insert("y".to_string(), alpha_arg); - main_block.operations.push(Self::create_mil_operation( - "mul", - mul_inputs, - vec![NamedValueType { - name: mul_output_name.clone(), - r#type: Some(value_type.clone()), - }], - )); + }; + let mut pre_tp_inputs: HashMap = HashMap::new(); + pre_tp_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + pre_tp_inputs.insert( + "perm".to_string(), + Self::create_immediate_int_array(&nchw_input_perm), + ); + main_block.operations.push(Self::create_mil_operation( + "transpose", + pre_tp_inputs, + vec![nchw_input_type], + )); - let mut add_inputs: HashMap = HashMap::new(); - add_inputs.insert("x".to_string(), Self::create_name_argument(mul_output_name)); - add_inputs.insert("y".to_string(), beta_arg); - main_block.operations.push(Self::create_mil_operation( - "add", - add_inputs, - vec![NamedValueType { - name: output_name, - r#type: Some(value_type), - }], - )); + // Pool (NCHW): intermediate output shape is [N,C,H',W'] + let nchw_pool_output_name = format!("{}_pool_nchw_out", output_name); + // Compute NCHW output shape: permute NHWC output [N,H',W',C] -> [N,C,H',W'] + let nchw_out_perm = [0u32, 3, 1, 2]; + let nchw_pool_shape = + Self::permute_graph_shape(&output_operand.descriptor.shape, &nchw_out_perm); + let nchw_pool_dims = + Self::mil_dimensions_from_graph_shape(&nchw_pool_shape, false); + let nchw_pool_output_type = NamedValueType { + name: nchw_pool_output_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: nchw_pool_dims.len() as i64, + data_type: dtype, + dimensions: nchw_pool_dims, + attributes: HashMap::new(), + }, + ), + ), + }), + }; + // Build pool inputs using the NCHW-transposed input name + let mut overrides_for_pool = operand_name_overrides.clone(); + overrides_for_pool.insert(input_id, nchw_input_name); + let pool_input_names = + Self::input_names_for_operation(graph_info, op, &overrides_for_pool); + let mil_op_type = self.get_mil_op_type(op.op_type())?; + let pool_op = self.convert_operation_with_input_names_and_outputs( + graph_info, + op, + &pool_input_names, + vec![nchw_pool_output_type], + mil_op_type, + )?; + main_block.operations.push(pool_op); - continue; - } + // Post-transpose: NCHW [N,C,H',W'] -> NHWC [N,H',W',C], perm=[0,2,3,1] + let post_tp_perm = [0u32, 2, 3, 1]; + let mut post_tp_inputs: HashMap = HashMap::new(); + post_tp_inputs.insert( + "x".to_string(), + Self::create_name_argument(nchw_pool_output_name), + ); + post_tp_inputs.insert( + "perm".to_string(), + Self::create_immediate_int_array(&post_tp_perm), + ); + main_block.operations.push(Self::create_mil_operation( + "transpose", + post_tp_inputs, + vec![output_type], + )); - // Special handling for neg (decompose into mul(x, -1) with typed constant) - // Following Chromium: neg = mul(x, -1) with constant matching input dtype - if op_type_lower == "neg" { - // Validate inputs/outputs exist - if op.input_operands().is_empty() || op.output_operand().is_none() { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: "neg requires input and output operand".to_string(), - }); + // Flush deferred transposes for this output + if let Some((pending_ops, transposed_name)) = + deferred_transposes.remove(&output_id) + { + main_block.operations.extend(pending_ops); + operand_name_overrides.insert(output_id, transposed_name); + } + continue; } + } - let input_operand = - graph_info.operand(op.input_operands()[0]).ok_or_else(|| { + // Special handling for reduce ops on 0D (scalar) inputs. + // CoreML requires at least rank-1 inputs for reduce operations. + // Wrap: reshape([] -> [1]), reduce(axes=[0], keep_dims=False), reshape([1] -> []) if needed. + let is_reduce_op = matches!( + op_type_lower.as_str(), + "reducesum" + | "reducemean" + | "reducemax" + | "reducemin" + | "reduceproduct" + | "reducel1" + | "reducel2" + | "reducelogsum" + | "reducelogsumexp" + | "reducesumsquare" + ); + // WebNN reduce with an explicit empty `axes` list reduces over NO + // dimensions: the output shape equals the input shape, but the + // per-op element transform still applies (reduceL1 -> abs, + // reduceL2 -> abs, reduceLogSum -> log, reduceSumSquare -> square, + // reduceLogSumExp -> identity, etc.). CoreML's reduce with an + // omitted `axes` reduces ALL dimensions, so we instead append a + // singleton axis and reduce over it: reducing a one-element window + // yields exactly the WebNN empty-axes semantics for every reduce. + if is_reduce_op { + let axes_explicitly_empty = match op { + Operation::ReduceSum { options, .. } + | Operation::ReduceMean { options, .. } + | Operation::ReduceMax { options, .. } + | Operation::ReduceMin { options, .. } + | Operation::ReduceProduct { options, .. } + | Operation::ReduceL1 { options, .. } + | Operation::ReduceL2 { options, .. } + | Operation::ReduceLogSum { options, .. } + | Operation::ReduceLogSumExp { options, .. } + | Operation::ReduceSumSquare { options, .. } => options + .as_ref() + .and_then(|o| o.axes.as_ref()) + .map(|a| a.is_empty()) + .unwrap_or(false), + _ => false, + }; + if axes_explicitly_empty { + let input_id = op.input_operands().first().copied().ok_or_else(|| { GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), - reason: format!("Input operand {} not found", op.input_operands()[0]), + reason: format!("reduce op '{}' has no input operand", op.op_type()), } })?; + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "reduce op '{}' has no output operand", + op.op_type() + ), + })?; + let input_op = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Input operand {} not found", input_id), + } + })?; + let mil_dtype = Self::mil_data_type(&input_op.descriptor.data_type)?; - let input_name = operand_name(graph_info, op.input_operands()[0]); - - // Create typed -1 constant matching input dtype. MIL `mul` - // preserves rank rather than broadcasting scalars, so when - // the input operand was promoted from rank-0 to rank-1 `[1]` - // (the `scalar_as_one_dim: true` rule applied to all graph - // inputs/outputs), the multiplier must be rank-1 `[1]` too. - // Otherwise Apple's loader rejects with - // "Output '0' has unexpected type 'ios17.mul'. - // Expected tensor; got fp32." - // (WPT "neg float32 positive 0D scalar" surfaces this.) - let neg_one_immediate = match input_operand.descriptor.data_type { - DataType::Float32 => Self::create_immediate_float_1d(-1.0f32), - DataType::Float16 => Self::create_immediate_float16(-1.0f32), - DataType::Int32 => { - // create_immediate_int accepts u32 but converts to i32 internally - // We need to reimplement for -1 value - use crate::protos::coreml::mil_spec::{ - DataType as MilDataType, TensorType, TensorValue, Value, ValueType, - argument, tensor_value, value, value_type, - }; - - let tensor_value = TensorValue { - value: Some(tensor_value::Value::Ints(tensor_value::RepeatedInts { - values: vec![-1i32], - })), - }; + // Reshape input to [input_shape..., 1] and reduce over the new axis. + let mut reshaped_shape_vals = input_op.descriptor.static_or_max_shape(); + reshaped_shape_vals.push(1); + let reduce_axis = (reshaped_shape_vals.len() - 1) as u32; + let mut reshaped_dims = input_op.descriptor.shape.clone(); + reshaped_dims.push(GraphDimension::Static(1)); - let val = Value { - doc_string: String::new(), - r#type: Some(ValueType { - r#type: Some(value_type::Type::TensorType(TensorType { - data_type: MilDataType::Int32 as i32, - rank: 0, // Scalar - dimensions: vec![], - attributes: HashMap::new(), - })), - }), - value: Some(value::Value::ImmediateValue(value::ImmediateValue { - value: Some(value::immediate_value::Value::Tensor(tensor_value)), - })), - }; + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; - Argument { - arguments: vec![argument::Binding { - binding: Some(argument::binding::Binding::Value(val)), - }], - } - } - _ => { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "Unsupported data type for neg: {:?}", - input_operand.descriptor.data_type - ), - }); - } - }; + let reshaped_name = format!("{}_emptyaxes_rs", output_name); + let reshaped_type = Self::create_named_value_type( + reshaped_name.clone(), + mil_dtype, + &reshaped_dims, + false, + ); + let mut reshape_inputs: HashMap = HashMap::new(); + reshape_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + reshape_inputs.insert( + "shape".to_string(), + Self::create_immediate_int_array(&reshaped_shape_vals), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + reshape_inputs, + vec![reshaped_type], + )); - // Create mul operation: x * (-1) - let mut mul_inputs: HashMap = HashMap::new(); - mul_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); - mul_inputs.insert("y".to_string(), neg_one_immediate); + let mil_op_type = self.get_mil_op_type(op.op_type())?; + let mut reduce_inputs: HashMap = HashMap::new(); + reduce_inputs + .insert("x".to_string(), Self::create_name_argument(reshaped_name)); + reduce_inputs.insert( + "axes".to_string(), + Self::create_immediate_int_array(&[reduce_axis]), + ); + reduce_inputs + .insert("keep_dims".to_string(), Self::create_immediate_bool(false)); + main_block.operations.push(Self::create_mil_operation( + mil_op_type, + reduce_inputs, + vec![output_type], + )); - // Get output name - let output_operand_id = op.output_operand().unwrap(); - let output_name = operand_name(graph_info, output_operand_id); - let output_operand = graph_info.operand(output_operand_id).ok_or_else(|| { - GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!("Output operand {} not found", output_operand_id), + // Flush deferred transposes for this output. + if let Some((pending_ops, transposed_name)) = + deferred_transposes.remove(&output_id) + { + main_block.operations.extend(pending_ops); + operand_name_overrides.insert(output_id, transposed_name); } - })?; - - let output_dtype = Self::mil_data_type(&output_operand.descriptor.data_type)?; - // Use `scalar_as_one_dim: true` so a rank-0 output is promoted - // to `tensor`, matching the rank of the mul's - // (promoted) input/multiplier. See comment on the mul input - // above — this keeps the full op consistent. - let output_dimensions = - Self::mil_dimensions_from_graph_shape(&output_operand.descriptor.shape, true); + continue; + } + } + if is_reduce_op { + let maybe_0d_input = op.input_operands().first().and_then(|&id| { + graph_info + .operand(id) + .filter(|o| o.descriptor.shape.is_empty()) + .map(|o| (id, o.descriptor.data_type.clone())) + }); + if let Some((input_id, input_dtype)) = maybe_0d_input { + let input_name = Self::output_name_for_operand( + graph_info, + input_id, + &operand_name_overrides, + ); + let reshaped_input_name = format!("{}_reduce1d", input_name); + let mil_dtype = Self::mil_data_type(&input_dtype)?; - let output_value_type = ValueType { - r#type: Some( - crate::protos::coreml::mil_spec::value_type::Type::TensorType(TensorType { - rank: output_dimensions.len() as i64, - data_type: output_dtype, - dimensions: output_dimensions, - attributes: HashMap::new(), + // Build the [1] dimension for the reshaped input + let one_dim = Dimension { + dimension: Some(dimension::Dimension::Constant( + dimension::ConstantDimension { size: 1 }, + )), + }; + let reshaped_input_type = NamedValueType { + name: reshaped_input_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: 1, + data_type: mil_dtype, + dimensions: vec![one_dim], + attributes: HashMap::new(), + }, + ), + ), }), - ), - }; + }; - let mul_output_type = NamedValueType { - name: output_name, - r#type: Some(output_value_type), - }; + // Emit: reshape(x=input, shape=[1]) -> reshaped_input_name + let mut reshape_inputs: HashMap = HashMap::new(); + reshape_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + reshape_inputs.insert( + "shape".to_string(), + Self::create_immediate_int_array(&[1u32]), + ); + main_block.operations.push(Self::create_mil_operation( + "reshape", + reshape_inputs, + vec![reshaped_input_type], + )); - let mul_op = Self::create_mil_operation("mul", mul_inputs, vec![mul_output_type]); + // Override the input operand name so create_operation_inputs uses the 1D version + let mut overrides_with_reshape = operand_name_overrides.clone(); + overrides_with_reshape.insert(input_id, reshaped_input_name.clone()); - main_block.operations.push(mul_op); + // Get the output operand + let output_id = + op.output_operand() + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "reduce op '{}' has no output operand", + op.op_type() + ), + })?; + let output_operand = graph_info.operand(output_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("Output operand {} not found", output_id), + } + })?; + let webnn_output_is_0d = output_operand.descriptor.shape.is_empty(); + + let mil_op_type = self.get_mil_op_type(op.op_type())?; + let input_names = + Self::input_names_for_operation(graph_info, op, &overrides_with_reshape); + + if webnn_output_is_0d { + // The reduce will output [1] (axes=[0], keep_dims would give [1]) + // but we need [] — use a reduce-to-scalar intermediate then reshape. + let (output_name, output_type) = Self::create_output_value( + graph_info, + output_id, + &operand_name_overrides, + )?; + let reduce_intermediate_name = format!("{}_reduce_1d", output_name); + let reduce_intermediate_type = NamedValueType { + name: reduce_intermediate_name.clone(), + r#type: Some(ValueType { + r#type: Some( + crate::protos::coreml::mil_spec::value_type::Type::TensorType( + TensorType { + rank: 1, + data_type: mil_dtype, + dimensions: vec![Dimension { + dimension: Some(dimension::Dimension::Constant( + dimension::ConstantDimension { size: 1 }, + )), + }], + attributes: HashMap::new(), + }, + ), + ), + }), + }; - // Skip normal operation conversion for neg - continue; + // Build reduce inputs manually: x=reshaped, axes=[0], keep_dims=True + // so the output stays [1] (which we can reshape to []) + let mut reduce_inputs: HashMap = HashMap::new(); + if let Some(first) = input_names.first() { + reduce_inputs.insert("x".to_string(), Self::create_argument(first)); + } + reduce_inputs.insert( + "axes".to_string(), + Self::create_immediate_int_array(&[0u32]), + ); + reduce_inputs + .insert("keep_dims".to_string(), Self::create_immediate_bool(true)); + main_block.operations.push(Self::create_mil_operation( + mil_op_type, + reduce_inputs, + vec![reduce_intermediate_type], + )); + + // Reshape [1] -> [] + let mut reshape_back: HashMap = HashMap::new(); + reshape_back.insert( + "x".to_string(), + Self::create_name_argument(reduce_intermediate_name), + ); + reshape_back + .insert("shape".to_string(), Self::create_immediate_int_array(&[])); + main_block.operations.push(Self::create_mil_operation( + "reshape", + reshape_back, + vec![output_type], + )); + } else { + // Output is not 0D — just emit normally with the 1D input override + let mil_op = self.convert_operation_with_overrides( + graph_info, + op, + &overrides_with_reshape, + )?; + main_block.operations.push(mil_op); + } + continue; + } } let mil_op = self.convert_operation_with_overrides(graph_info, op, &operand_name_overrides)?; main_block.operations.push(mil_op); + + // Flush any transpose ops that were waiting for this operation's output, and + // activate the corresponding operand-name override so that later operations + // that consume this operand use the transposed name. + if let Some(output_id) = op.output_operand() { + if let Some((pending_ops, transposed_name)) = deferred_transposes.remove(&output_id) + { + main_block.operations.extend(pending_ops); + operand_name_overrides.insert(output_id, transposed_name); + } + } } // Add block outputs (output operand names) @@ -3683,19 +8893,29 @@ impl super::GraphConverter for CoremlMlProgramConverter { let output_name = operand_name(graph_info, output_id); let graph_output_name = Self::output_name_for_operand(graph_info, output_id, &operand_name_overrides); - let graph_mil_type = Self::mil_data_type(&operand.descriptor.data_type)?; + let graph_mil_type = Self::graph_value_mil_type(&operand.descriptor.data_type)?; let interface_mil_type = Self::interface_mil_data_type(&operand.descriptor.data_type); - if graph_mil_type != interface_mil_type { + // Wide ints and argmin/argmax proxy outputs use int32 at the interface + // (not float32); the executor widens int32 -> int64/uint64 on readback. + let effective_interface_type = if int32_proxy_output_names.contains(&output_name) + || Self::is_wide_int(&operand.descriptor.data_type) + { + use crate::protos::coreml::mil_spec::DataType as MilDt; + MilDt::Int32 as i32 + } else { + interface_mil_type + }; + if graph_mil_type != effective_interface_type { let output_type = Self::create_value_with_mil_type( graph_info, output_id, output_name.clone(), - interface_mil_type, + effective_interface_type, )?; main_block.operations.push(Self::create_cast_operation( graph_output_name, output_type, - Self::cast_dtype_string_for_mil_type(interface_mil_type)?, + Self::cast_dtype_string_for_mil_type(effective_interface_type)?, )); } main_block.outputs.push(output_name); @@ -3738,9 +8958,32 @@ impl super::GraphConverter for CoremlMlProgramConverter { for &output_id in &graph_info.output_operands { if let Some(operand) = graph_info.operand(output_id) { let output_name = operand_name(graph_info, output_id); + // For int32 proxy outputs (argmin/argmax, or any wide int64/uint32/uint64 + // output) use Int32 at the model interface to match the function emit type. + let feature_type = if int32_proxy_output_names.contains(&output_name) + || Self::is_wide_int(&operand.descriptor.data_type) + { + use crate::protos::coreml::specification::{ + ArrayFeatureType, FeatureType, feature_type, + }; + let mut af = ArrayFeatureType { + data_type: crate::protos::coreml::specification::array_feature_type::ArrayDataType::Int32 as i32, + ..Default::default() + }; + let shape = operand.descriptor.static_or_max_shape(); + for &d in &shape { + af.shape.push(d as i64); + } + FeatureType { + r#type: Some(feature_type::Type::MultiArrayType(af)), + is_optional: false, + } + } else { + Self::create_feature_type(&operand.descriptor)? + }; output_descriptions.push(FeatureDescription { name: output_name, - r#type: Some(Self::create_feature_type(&operand.descriptor)?), + r#type: Some(feature_type), ..Default::default() }); } @@ -3946,11 +9189,9 @@ mod tests { let graph = create_graph_with_float16_constant(s(&[3]), data.clone()); - // Convert the graph let converter = CoremlMlProgramConverter; let result = converter.convert(&graph).unwrap(); - // Verify weights_data is present assert!( result.weights_data.is_some(), "Non-scalar Float16 should use weight file" @@ -3958,37 +9199,32 @@ mod tests { let weights = result.weights_data.unwrap(); - // Verify weight file structure - // Expected structure: - // [0-3]: sentinel (0xDEADBEEF) - // [4-11]: count (3) - // [12-17]: data (6 bytes) - // [18-63]: padding (46 bytes) - assert_eq!(weights.len(), 64, "Weight file should be 64-byte aligned"); - - // Verify sentinel - let sentinel = u32::from_le_bytes([weights[0], weights[1], weights[2], weights[3]]); - assert_eq!(sentinel, 0xDEADBEEF, "Sentinel should be 0xDEADBEEF"); - - // Verify count - let count = u64::from_le_bytes([ - weights[4], - weights[5], - weights[6], - weights[7], - weights[8], - weights[9], - weights[10], - weights[11], - ]); - assert_eq!(count, 3, "Element count should be 3"); - - // Verify data + // v2 file layout: + // [0-63] 64-byte global header: count(u32)=1, version(u32)=2, 56 zero bytes + // [64-127] 64-byte WeightMetadata: sentinel, FLOAT16 type, size_in_bytes=6, payload_offset=128, zeros + // [128-133] 6 bytes payload + // [134-191] padding → total 192 bytes + + // Global header + assert_eq!(&weights[0..4], &1u32.to_le_bytes(), "Entry count = 1"); + assert_eq!(&weights[4..8], &2u32.to_le_bytes(), "File version = 2"); + + // WeightMetadata at offset 64 + assert_eq!(&weights[64..68], &0xDEADBEEFu32.to_le_bytes(), "Sentinel"); assert_eq!( - &weights[12..18], - &data[..], - "Weight data should match input" + &weights[68..72], + &1u32.to_le_bytes(), // FLOAT16 = 1 + "BlobDataType::FLOAT16" ); + assert_eq!(&weights[72..80], &6u64.to_le_bytes(), "size_in_bytes = 6"); + assert_eq!( + &weights[80..88], + &128u64.to_le_bytes(), + "payload at offset 128" + ); + + // Payload + assert_eq!(&weights[128..134], &data[..], "payload data"); } #[test] @@ -4003,11 +9239,9 @@ mod tests { let graph = create_graph_with_float16_constant(s(&[2, 2]), data.clone()); - // Convert the graph let converter = CoremlMlProgramConverter; let result = converter.convert(&graph).unwrap(); - // Verify weights_data is present assert!( result.weights_data.is_some(), "2D Float16 constant should use weight file" @@ -4015,18 +9249,10 @@ mod tests { let weights = result.weights_data.unwrap(); - // Verify count matches 2x2 = 4 elements - let count = u64::from_le_bytes([ - weights[4], - weights[5], - weights[6], - weights[7], - weights[8], - weights[9], - weights[10], - weights[11], - ]); - assert_eq!(count, 4, "Element count should be 4"); + // size_in_bytes = 8 (4 elements × 2 bytes each), located at [72..80] of metadata block + assert_eq!(&weights[72..80], &8u64.to_le_bytes(), "size_in_bytes = 8"); + // Payload at offset 128 + assert_eq!(&weights[128..136], &data[..], "payload data"); } #[test] @@ -4112,23 +9338,32 @@ mod tests { let weights = result.weights_data.unwrap(); - // Should have two entries: - // Entry 1: offset 0, 64 bytes - // Entry 2: offset 64, 64 bytes - // Total: 128 bytes + // v2 layout for two 2-element Float16 constants (4 bytes each): + // [0-63] global header (count=2, version=2, zeros) + // [64-127] metadata1 (sentinel, FLOAT16, size=4, payload_offset=128, zeros) + // [128-131] payload1 (4 bytes), padded to [192] + // [192-255] metadata2 (sentinel, FLOAT16, size=4, payload_offset=256, zeros) + // [256-259] payload2 (4 bytes), padded to [320] + // Total: 320 bytes + assert_eq!(weights.len(), 320, "Two Float16 constants layout"); + + // Global header + assert_eq!(&weights[0..4], &2u32.to_le_bytes(), "Entry count = 2"); + assert_eq!(&weights[4..8], &2u32.to_le_bytes(), "File version = 2"); + + // First entry sentinel at offset 64 assert_eq!( - weights.len(), - 128, - "Two Float16 constants should result in 128-byte weight file" + &weights[64..68], + &0xDEADBEEFu32.to_le_bytes(), + "First sentinel" ); - // Verify first entry sentinel at offset 0 - let sentinel1 = u32::from_le_bytes([weights[0], weights[1], weights[2], weights[3]]); - assert_eq!(sentinel1, 0xDEADBEEF, "First entry sentinel"); - - // Verify second entry sentinel at offset 64 - let sentinel2 = u32::from_le_bytes([weights[64], weights[65], weights[66], weights[67]]); - assert_eq!(sentinel2, 0xDEADBEEF, "Second entry sentinel"); + // Second entry sentinel at offset 192 + assert_eq!( + &weights[192..196], + &0xDEADBEEFu32.to_le_bytes(), + "Second sentinel" + ); } #[test] @@ -4191,7 +9426,7 @@ mod tests { } #[test] - fn test_int4_data_type_rejected() { + fn test_int4_data_type_supported() { let mut graph = GraphInfo { input_operands: vec![0], output_operands: vec![1], @@ -4233,23 +9468,14 @@ mod tests { OperatorOptions::default(), )); - // Convert should fail with Int4 + // int4 is now supported via the int32 proxy; conversion should succeed. let converter = CoremlMlProgramConverter; let result = converter.convert(&graph); - assert!(result.is_err()); - - match result.unwrap_err() { - crate::error::GraphError::ConversionFailed { format, reason } => { - assert_eq!(format, "coreml"); - assert!(reason.contains("int4/uint4")); - assert!(reason.contains("not supported")); - } - _ => panic!("Expected ConversionFailed error"), - } + assert!(result.is_ok(), "int4 should convert: {:?}", result.err()); } #[test] - fn test_uint4_data_type_rejected() { + fn test_uint4_data_type_supported() { let mut graph = GraphInfo { input_operands: vec![], output_operands: vec![1], @@ -4295,23 +9521,14 @@ mod tests { OperatorOptions::default(), )); - // Convert should fail with Uint4 + // uint4 is now supported via the int32 proxy; conversion should succeed. let converter = CoremlMlProgramConverter; let result = converter.convert(&graph); - assert!(result.is_err()); - - match result.unwrap_err() { - crate::error::GraphError::ConversionFailed { format, reason } => { - assert_eq!(format, "coreml"); - assert!(reason.contains("int4/uint4")); - assert!(reason.contains("not supported")); - } - _ => panic!("Expected ConversionFailed error"), - } + assert!(result.is_ok(), "uint4 should convert: {:?}", result.err()); } #[test] - fn test_int4_output_rejected() { + fn test_int4_output_supported() { let mut graph = GraphInfo { input_operands: vec![0], output_operands: vec![1], @@ -4353,14 +9570,18 @@ mod tests { OperatorOptions::default(), )); - // Convert should fail + // int4 output is now supported via the int32 proxy; conversion should succeed. let converter = CoremlMlProgramConverter; let result = converter.convert(&graph); - assert!(result.is_err()); + assert!( + result.is_ok(), + "int4 output should convert: {:?}", + result.err() + ); } #[test] - fn test_uint4_input_rejected() { + fn test_uint4_input_supported() { let mut graph = GraphInfo { input_operands: vec![0], output_operands: vec![1], @@ -4402,10 +9623,14 @@ mod tests { OperatorOptions::default(), )); - // Convert should fail + // uint4 input is now supported via the int32 proxy; conversion should succeed. let converter = CoremlMlProgramConverter; let result = converter.convert(&graph); - assert!(result.is_err()); + assert!( + result.is_ok(), + "uint4 input should convert: {:?}", + result.err() + ); } #[test] diff --git a/src/converters/weight_file_builder.rs b/src/converters/weight_file_builder.rs index fb3b6490..d2c23274 100644 --- a/src/converters/weight_file_builder.rs +++ b/src/converters/weight_file_builder.rs @@ -1,53 +1,72 @@ -use std::collections::HashMap; - use crate::error::GraphError; +use std::collections::HashMap; const SENTINEL: u32 = 0xDEADBEEF; const ALIGNMENT: usize = 64; +const FILE_VERSION: u32 = 2; -/// Builds a CoreML weight file with proper alignment and metadata. +/// BlobDataType values used in the per-entry metadata. +/// Matches the enum from Chromium's graph_builder_coreml.cc. +pub mod blob_data_type { + pub const FLOAT16: u32 = 1; + #[allow(dead_code)] + pub const FLOAT32: u32 = 2; + #[allow(dead_code)] + pub const UINT8: u32 = 3; + #[allow(dead_code)] + pub const INT8: u32 = 4; +} + +/// Builds a CoreML MLProgram blob weight file (format version 2). +/// +/// Structure: +/// +/// Global file header — exactly 64 bytes at offset 0: +/// [0-3] u32 entry count (written as 0, patched at finalize) +/// [4-7] u32 version = 2 +/// [8-63] u8[] zeros +/// +/// Per-entry — at a 64-byte aligned offset: +/// WeightMetadata block — 64 bytes: +/// [0-3] u32 sentinel = 0xDEADBEEF +/// [4-7] u32 mil_data_type (BlobDataType enum) +/// [8-15] u64 size_in_bytes (byte length of payload) +/// [16-23] u64 absolute file offset of payload (= metadata offset + 64) +/// [24-63] u8[] zeros +/// Raw payload — size_in_bytes bytes +/// Padding — zeros to next 64-byte boundary /// -/// CoreML MLProgram requires Float16 (and other) constants to be stored in -/// external weight files with specific formatting: -/// - 64-byte alignment for each entry -/// - Metadata header (sentinel + count) -/// - Raw data bytes -/// - Padding to next boundary +/// BlobFileValue.offset points to the WeightMetadata block (not the payload). /// -/// Reference: chromium/src/services/webnn/coreml/graph_builder_coreml.cc +/// Reference: Chromium services/webnn/coreml/graph_builder_coreml.cc pub struct WeightFileBuilder { - /// Binary weight data with alignment and metadata data: Vec, - - /// Maps operand ID to file offset for BlobFileValue references offsets: HashMap, + entry_count: u32, } impl WeightFileBuilder { - /// Creates a new empty weight file builder pub fn new() -> Self { - Self { + let mut builder = Self { data: Vec::new(), offsets: HashMap::new(), - } + entry_count: 0, + }; + // Write the 64-byte file header (count = 0 for now; patched at finalize) + builder.write_file_header(0); + builder } - /// Adds a weight entry for an operand with proper alignment and metadata. - /// - /// Format per entry: - /// - Sentinel (4 bytes): 0xDEADBEEF - /// - Count (8 bytes): number of elements - /// - Data (N bytes): raw bytes - /// - Padding (M bytes): zero padding to next 64-byte boundary + /// Adds a weight entry. Returns the absolute file offset of the WeightMetadata + /// block (this value goes into BlobFileValue.offset). /// - /// Returns the file offset where this weight begins (for BlobFileValue). + /// `mil_data_type` is a BlobDataType enum value (e.g. blob_data_type::FLOAT16). pub fn add_weight( &mut self, operand_id: u32, - element_count: usize, + mil_data_type: u32, data: &[u8], ) -> Result { - // Check if we already have this operand if self.offsets.contains_key(&operand_id) { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), @@ -55,63 +74,69 @@ impl WeightFileBuilder { }); } - // Align current position to 64-byte boundary - let current_len = self.data.len(); - let aligned_offset = align_to_64(current_len); - - // Add padding to reach alignment + // Align to 64-byte boundary before writing the metadata block + let aligned_offset = align_to(self.data.len(), ALIGNMENT); self.data.resize(aligned_offset, 0); - // Record offset for this weight (before writing metadata) - let offset = self.data.len() as u64; - self.offsets.insert(operand_id, offset); + // Record the offset of the metadata block (returned as BlobFileValue.offset) + let metadata_offset = self.data.len() as u64; + self.offsets.insert(operand_id, metadata_offset); - // Write sentinel (4 bytes, little-endian) - self.data.extend_from_slice(&SENTINEL.to_le_bytes()); + let size_in_bytes = data.len() as u64; + // Payload starts immediately after the 64-byte metadata block + let payload_offset = metadata_offset + ALIGNMENT as u64; - // Write element count (8 bytes, little-endian) - self.data - .extend_from_slice(&(element_count as u64).to_le_bytes()); + // Write 64-byte WeightMetadata block + self.data.extend_from_slice(&SENTINEL.to_le_bytes()); // [0-3] sentinel + self.data.extend_from_slice(&mil_data_type.to_le_bytes()); // [4-7] type + self.data.extend_from_slice(&size_in_bytes.to_le_bytes()); // [8-15] size + self.data.extend_from_slice(&payload_offset.to_le_bytes()); // [16-23] data offset + self.data.resize(aligned_offset + ALIGNMENT, 0); // [24-63] zeros - // Write actual data + // Write raw payload self.data.extend_from_slice(data); - Ok(offset) + self.entry_count += 1; + Ok(metadata_offset) } - /// Returns the file offset for a previously added weight + /// Returns the file offset for a previously added weight. #[allow(dead_code)] pub fn get_offset(&self, operand_id: u32) -> Option { self.offsets.get(&operand_id).copied() } - /// Finalizes the weight file and returns the complete binary data. - /// - /// Adds final padding to ensure the entire file is 64-byte aligned. + /// Finalizes the file: patches the entry count in the header and pads to alignment. pub fn finalize(mut self) -> Vec { - // Align final size to 64-byte boundary - let current_len = self.data.len(); - let aligned_len = align_to_64(current_len); - self.data.resize(aligned_len, 0); + // Pad to 64-byte boundary + let aligned = align_to(self.data.len(), ALIGNMENT); + self.data.resize(aligned, 0); + + // Patch entry count at offset 0 + let count_bytes = self.entry_count.to_le_bytes(); + self.data[0..4].copy_from_slice(&count_bytes); self.data } - /// Returns true if any weights have been added pub fn has_weights(&self) -> bool { - !self.offsets.is_empty() + self.entry_count > 0 } - /// Returns the current size of the weight data (may not be aligned) #[allow(dead_code)] pub fn size(&self) -> usize { self.data.len() } + + fn write_file_header(&mut self, count: u32) { + self.data.extend_from_slice(&count.to_le_bytes()); // [0-3] count + self.data.extend_from_slice(&FILE_VERSION.to_le_bytes()); // [4-7] version = 2 + self.data.resize(ALIGNMENT, 0); // [8-63] zeros + } } -/// Aligns an offset to the next 64-byte boundary -fn align_to_64(offset: usize) -> usize { - (offset + (ALIGNMENT - 1)) & !(ALIGNMENT - 1) +fn align_to(offset: usize, alignment: usize) -> usize { + (offset + (alignment - 1)) & !(alignment - 1) } #[cfg(test)] @@ -119,134 +144,104 @@ mod tests { use super::*; #[test] - fn test_align_to_64() { - assert_eq!(align_to_64(0), 0); - assert_eq!(align_to_64(1), 64); - assert_eq!(align_to_64(63), 64); - assert_eq!(align_to_64(64), 64); - assert_eq!(align_to_64(65), 128); - assert_eq!(align_to_64(127), 128); - assert_eq!(align_to_64(128), 128); + fn test_file_header_written_on_new() { + let builder = WeightFileBuilder::new(); + assert_eq!(builder.size(), 64, "Header must be exactly 64 bytes"); + // version at bytes [4-7] + let data = builder.finalize(); + assert_eq!(&data[4..8], &2u32.to_le_bytes(), "File version must be 2"); } #[test] - fn test_empty_builder() { + fn test_empty_builder_has_only_header() { let builder = WeightFileBuilder::new(); assert!(!builder.has_weights()); - assert_eq!(builder.size(), 0); - let data = builder.finalize(); - assert_eq!(data.len(), 0); + // Entry count = 0 + assert_eq!(&data[0..4], &0u32.to_le_bytes()); + // Version = 2 + assert_eq!(&data[4..8], &2u32.to_le_bytes()); + // File is 64 bytes (header only, aligned) + assert_eq!(data.len(), 64); } #[test] - fn test_single_weight() { + fn test_single_weight_layout() { let mut builder = WeightFileBuilder::new(); + let payload = vec![0x00u8, 0x3C, 0x00, 0x40, 0x00, 0x42]; // f16: 1.0, 2.0, 3.0 + let offset = builder + .add_weight(0, blob_data_type::FLOAT16, &payload) + .unwrap(); - // Add a small weight: 3 float16 values (6 bytes) - let data = vec![0x00, 0x3C, 0x00, 0x40, 0x00, 0x42]; // f16: 1.0, 2.0, 3.0 - let offset = builder.add_weight(0, 3, &data).unwrap(); - - // First weight starts at offset 0 - assert_eq!(offset, 0); + // Metadata block starts at byte 64 (right after header) + assert_eq!(offset, 64); assert!(builder.has_weights()); - assert_eq!(builder.get_offset(0), Some(0)); - - let result = builder.finalize(); - - // Check structure: - // [0-3]: sentinel (4 bytes) - // [4-11]: count (8 bytes) - // [12-17]: data (6 bytes) - // [18-63]: padding (46 bytes) - // Total: 64 bytes (aligned) - - assert_eq!(result.len(), 64); - - // Verify sentinel - assert_eq!(&result[0..4], &SENTINEL.to_le_bytes()); - // Verify count - assert_eq!(&result[4..12], &3u64.to_le_bytes()); - - // Verify data - assert_eq!(&result[12..18], &data[..]); + let data = builder.finalize(); - // Verify padding is zeros - assert!(result[18..64].iter().all(|&b| b == 0)); + // Header: entry count = 1 + assert_eq!(&data[0..4], &1u32.to_le_bytes()); + assert_eq!(&data[4..8], &2u32.to_le_bytes()); // version + + // Metadata block at offset 64: + // [64-67] sentinel + assert_eq!(&data[64..68], &SENTINEL.to_le_bytes()); + // [68-71] mil_data_type = FLOAT16 = 1 + assert_eq!(&data[68..72], &blob_data_type::FLOAT16.to_le_bytes()); + // [72-79] size_in_bytes = 6 + assert_eq!(&data[72..80], &6u64.to_le_bytes()); + // [80-87] payload absolute offset = 64 + 64 = 128 + assert_eq!(&data[80..88], &128u64.to_le_bytes()); + // [88-127] zeros + assert!(data[88..128].iter().all(|&b| b == 0)); + + // Payload at offset 128 + assert_eq!(&data[128..134], &payload[..]); } #[test] fn test_multiple_weights() { let mut builder = WeightFileBuilder::new(); - // Add first weight (operand 0): 2 bytes - let data1 = vec![0xAA, 0xBB]; - let offset1 = builder.add_weight(0, 1, &data1).unwrap(); - - // Add second weight (operand 1): 4 bytes - let data2 = vec![0x11, 0x22, 0x33, 0x44]; - let offset2 = builder.add_weight(1, 2, &data2).unwrap(); - - // First starts at 0, second starts at 64 (after alignment) - assert_eq!(offset1, 0); - assert_eq!(offset2, 64); - - // Verify offsets can be retrieved - assert_eq!(builder.get_offset(0), Some(0)); - assert_eq!(builder.get_offset(1), Some(64)); - assert_eq!(builder.get_offset(2), None); + let d1 = vec![0xAAu8, 0xBB]; // 2 bytes + let off1 = builder.add_weight(0, blob_data_type::FLOAT16, &d1).unwrap(); + assert_eq!(off1, 64); // right after 64-byte header - let result = builder.finalize(); + let d2 = vec![0x11u8, 0x22, 0x33, 0x44]; // 4 bytes + let off2 = builder.add_weight(1, blob_data_type::FLOAT16, &d2).unwrap(); - // Should be at least 128 bytes (2 aligned entries) - assert!(result.len() >= 128); - assert_eq!(result.len() % 64, 0); // Final size is aligned + // d1 metadata at 64, payload at 128 (2 bytes), padded to 192. + // d2 metadata at 192. + assert_eq!(off2, 192); - // Verify first entry sentinel - assert_eq!(&result[0..4], &SENTINEL.to_le_bytes()); - - // Verify second entry sentinel at offset 64 - assert_eq!(&result[64..68], &SENTINEL.to_le_bytes()); + let data = builder.finalize(); + assert_eq!(&data[0..4], &2u32.to_le_bytes()); // 2 entries + // d2 payload offset = 192 + 64 = 256 + assert_eq!(&data[256..260], &d2[..]); } #[test] fn test_duplicate_operand_error() { let mut builder = WeightFileBuilder::new(); - - let data = vec![0x00, 0x01]; - builder.add_weight(0, 1, &data).unwrap(); - - // Try to add same operand again - let result = builder.add_weight(0, 1, &data); - assert!(result.is_err()); - - match result { - Err(GraphError::ConversionFailed { reason, .. }) => { - assert!(reason.contains("Duplicate")); - } - _ => panic!("Expected ConversionFailed error"), - } + let d = vec![0x00u8, 0x01]; + builder.add_weight(0, blob_data_type::FLOAT16, &d).unwrap(); + assert!(builder.add_weight(0, blob_data_type::FLOAT16, &d).is_err()); } #[test] - fn test_large_weight() { + fn test_large_weight_alignment() { let mut builder = WeightFileBuilder::new(); + // 100 float16 values = 200 bytes + let payload = vec![0xABu8; 200]; + let offset = builder + .add_weight(0, blob_data_type::FLOAT16, &payload) + .unwrap(); + assert_eq!(offset, 64); - // Add a large weight: 100 float16 values (200 bytes) - let data = vec![0xAB; 200]; - let offset = builder.add_weight(0, 100, &data).unwrap(); - - assert_eq!(offset, 0); - - let result = builder.finalize(); - - // Metadata: 12 bytes (sentinel + count) - // Data: 200 bytes - // Total: 212 bytes -> aligns to 256 bytes (4 * 64) - assert_eq!(result.len(), 256); - - // Verify data is present - assert_eq!(&result[12..212], &data[..]); + let data = builder.finalize(); + // metadata at 64, payload at 128 (200 bytes), padded to 64-byte boundary + // 128 + 200 = 328 -> aligned to 384 + assert_eq!(data.len(), 384); + assert_eq!(&data[128..328], &payload[..]); } } diff --git a/src/executors/coreml.rs b/src/executors/coreml.rs index 19cc8e41..400495be 100644 --- a/src/executors/coreml.rs +++ b/src/executors/coreml.rs @@ -566,6 +566,35 @@ unsafe fn fill_multiarray_from_bytes( let count = usize::try_from(count_obj).map_err(|_| GraphError::CoremlRuntimeFailed { reason: format!("invalid element count: {count_obj}"), })?; + // int4/uint4 inputs arrive packed two-per-byte and are exposed as float32 at the + // model boundary; unpack the nibbles and write them into the float32 array. + if matches!(dtype, DataType::Int4 | DataType::Uint4) { + if count == 0 { + return Ok(()); + } + let floats: Vec = if matches!(dtype, DataType::Int4) { + crate::graph::unpack_int4(src, count) + .into_iter() + .map(|v| v as f32) + .collect() + } else { + crate::graph::unpack_uint4(src, count) + .into_iter() + .map(|v| v as f32) + .collect() + }; + let ptr: *mut c_void = msg_send![array, dataPointer]; + if ptr.is_null() { + return Err(GraphError::CoremlRuntimeFailed { + reason: format!("MLMultiArray has no backing buffer for data type {dtype:?}"), + }); + } + let dst = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, count * 4) }; + for (i, f) in floats.iter().enumerate() { + dst[i * 4..i * 4 + 4].copy_from_slice(&f.to_le_bytes()); + } + return Ok(()); + } let elem = dtype.bytes_per_element(); let expected = count.saturating_mul(elem); if src.len() != expected { @@ -576,19 +605,32 @@ unsafe fn fill_multiarray_from_bytes( ), }); } - // A raw byte copy is only valid when our source layout matches the array CoreML - // allocated. When the model boundary promotes the type (e.g. a WebNN int64 input - // exposed as Float32), the element sizes differ and copying `src` verbatim would - // overrun the array buffer -- fail cleanly instead of corrupting memory. - if let Some(array_elem) = ml_dtype_code_element_size(array_code) - && array_elem != elem + // When the model boundary promotes the input type (e.g. a WebNN uint8 boolean + // condition exposed as Float32 by CoreML), convert the source bytes to the + // required element size before writing. + let canonical_code = normalize_dtype_code(array_code); + // A different element size always needs conversion. So does a same-width + // dtype mismatch: uint32 (4 bytes) promoted into an fp32 (4 bytes) array must + // be converted by value, not copied as raw bits. + let same_width_mismatch = + canonical_code == 32 && !matches!(dtype, DataType::Float32 | DataType::Int32); + if let Some(array_elem) = ml_dtype_code_element_size(canonical_code) + && (array_elem != elem || same_width_mismatch) { - return Err(GraphError::CoremlRuntimeFailed { - reason: format!( - "input dtype mismatch: model expects {array_elem}-byte elements (code {array_code}), \ - but input is {dtype:?} ({elem}-byte); type conversion is not supported for this input" - ), - }); + if count == 0 { + return Ok(()); + } + let ptr: *mut c_void = msg_send![array, dataPointer]; + if ptr.is_null() { + return Err(GraphError::CoremlRuntimeFailed { + reason: format!("MLMultiArray has no backing buffer for data type {dtype:?}"), + }); + } + // Convert src bytes (dtype) → array bytes (canonical_code). + let converted = convert_input_bytes(src, dtype, canonical_code, count); + let dst = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, count * array_elem) }; + dst.copy_from_slice(&converted); + return Ok(()); } if expected == 0 { return Ok(()); @@ -603,10 +645,90 @@ unsafe fn fill_multiarray_from_bytes( Ok(()) } +/// Convert input bytes from `src_dtype` to a buffer compatible with `target_code`. +/// Used when CoreML promotes an input type (e.g. uint8 boolean → float32). +fn convert_input_bytes(src: &[u8], src_dtype: DataType, target_code: i32, count: usize) -> Vec { + match (src_dtype, target_code) { + (DataType::Uint8, 32) => { + // uint8/bool → float32: 0→0.0, 1→1.0 (also used for boolean condition inputs) + let mut out = Vec::with_capacity(count * 4); + for &b in src.iter().take(count) { + out.extend_from_slice(&(b as f32).to_le_bytes()); + } + out + } + (DataType::Int8, 32) => { + // int8 → float32: reinterpret the byte as signed so negatives are preserved + // (a raw `b as f32` would turn -128 into 128.0). + let mut out = Vec::with_capacity(count * 4); + for &b in src.iter().take(count) { + out.extend_from_slice(&((b as i8) as f32).to_le_bytes()); + } + out + } + (DataType::Int32, 32) => { + // int32 as float32 bits (reinterpret, same size — shouldn't normally happen) + src.to_vec() + } + (DataType::Uint32, 32) => { + // uint32 → float32 by value (the interface promotes uint32 to float32). + let src_u32 = unsafe { std::slice::from_raw_parts(src.as_ptr() as *const u32, count) }; + let mut out = Vec::with_capacity(count * 4); + for &v in src_u32 { + out.extend_from_slice(&(v as f32).to_le_bytes()); + } + out + } + (DataType::Uint64, 32) => { + // uint64 → float32 by value (the interface promotes uint64 to float32). + let src_u64 = unsafe { std::slice::from_raw_parts(src.as_ptr() as *const u64, count) }; + let mut out = Vec::with_capacity(count * 4); + for &v in src_u64 { + out.extend_from_slice(&(v as f32).to_le_bytes()); + } + out + } + (DataType::Float32, 16) => { + // float32 → float16 + let src_f32 = unsafe { std::slice::from_raw_parts(src.as_ptr() as *const f32, count) }; + let mut out = Vec::with_capacity(count * 2); + for &v in src_f32 { + out.extend_from_slice(&half::f16::from_f32(v).to_bits().to_le_bytes()); + } + out + } + (DataType::Float16, 32) => { + // float16 → float32 + let src_f16 = unsafe { std::slice::from_raw_parts(src.as_ptr() as *const u16, count) }; + let mut out = Vec::with_capacity(count * 4); + for &bits in src_f16 { + out.extend_from_slice(&half::f16::from_bits(bits).to_f32().to_le_bytes()); + } + out + } + (DataType::Int64, 32) => { + // int64 → float32: used for int64 inputs promoted to float32 by CoreML + let src_i64 = unsafe { std::slice::from_raw_parts(src.as_ptr() as *const i64, count) }; + let mut out = Vec::with_capacity(count * 4); + for &v in src_i64 { + out.extend_from_slice(&(v as f32).to_le_bytes()); + } + out + } + _ => { + // Fallback: pass bytes as-is (may be wrong but avoids panic) + src.to_vec() + } + } +} + /// Extract a `MLMultiArray` into raw bytes laid out per the output descriptor's dtype. /// /// Handles non-contiguous layouts (e.g. 64-byte aligned outputs from the Apple /// Neural Engine) by gathering elements according to the array's strides. +/// Also handles dtype mismatches: CoreML may return a different dtype than requested +/// (e.g. float32 for a uint8-declared output from a comparison op). In that case the +/// data is converted element-by-element to the expected dtype. unsafe fn extract_multiarray_bytes( array: *mut Object, descriptor: &OperandDescriptor, @@ -615,7 +737,57 @@ unsafe fn extract_multiarray_bytes( let count = usize::try_from(count_obj).map_err(|_| GraphError::CoremlRuntimeFailed { reason: format!("invalid element count: {count_obj}"), })?; - let elem = descriptor.data_type.bytes_per_element(); + + // Query the actual data type code from the MLMultiArray — CoreML may promote + // the dtype (e.g. uint8 cast result returned as float32). + let actual_dtype_code: i64 = msg_send![array, dataType]; + let actual_elem = ml_dtype_code_element_size(actual_dtype_code as i32).unwrap_or(4); + + // int4/uint4 outputs are produced as int32 (the proxy). Read the int32 values and + // re-pack them two-per-byte into the sub-byte layout the host tensor expects. + if matches!(descriptor.data_type, DataType::Int4 | DataType::Uint4) { + let ptr: *const u8 = { + let p: *mut c_void = msg_send![array, dataPointer]; + if p.is_null() { + return Err(GraphError::CoremlRuntimeFailed { + reason: "output MLMultiArray has no backing buffer for int4/uint4".to_string(), + }); + } + p as *const u8 + }; + let shape_ns: *mut Object = msg_send![array, shape]; + let shape = unsafe { nsarray_to_i64_vec(shape_ns)? }; + let strides_ns: *mut Object = msg_send![array, strides]; + let strides = unsafe { nsarray_to_i64_vec(strides_ns)? }; + let raw = if is_contiguous(&shape, &strides) { + unsafe { std::slice::from_raw_parts(ptr, count.saturating_mul(actual_elem)) }.to_vec() + } else { + unsafe { gather_strided_bytes(ptr, &shape, &strides, actual_elem) } + }; + // Float codes (Float32 = 32/0x10020, Float16 = 16/0x10010) must be read as + // floats; int codes (Int32 = 3/0x20020) as integers. NOTE: 0x20020 (131104) is + // Int32, not Float32 — do not route it through normalize_dtype_code here. + let is_float = matches!(actual_dtype_code as i32, 32 | 65568 | 16 | 65552); + let ints: Vec = if is_float { + raw.chunks_exact(4) + .map(|c| f32::from_le_bytes(c.try_into().unwrap()) as i32) + .collect() + } else { + raw.chunks_exact(4) + .map(|c| i32::from_le_bytes(c.try_into().unwrap())) + .collect() + }; + let packed = if matches!(descriptor.data_type, DataType::Int4) { + crate::graph::pack_int4(&ints) + } else { + let u: Vec = ints.iter().map(|&v| v as u8).collect(); + crate::graph::pack_uint4(&u) + }; + return Ok(packed); + } + + let expected_elem = descriptor.data_type.bytes_per_element(); + let ptr: *const u8 = { let p: *mut c_void = msg_send![array, dataPointer]; if p.is_null() { @@ -634,13 +806,147 @@ unsafe fn extract_multiarray_bytes( let strides_nsarray: *mut Object = msg_send![array, strides]; let strides = unsafe { nsarray_to_i64_vec(strides_nsarray)? }; - if is_contiguous(&shape, &strides) { - let total = count.saturating_mul(elem); + // Read raw bytes using the ACTUAL element size from the MLMultiArray. + let actual_bytes = if is_contiguous(&shape, &strides) { + let total = count.saturating_mul(actual_elem); let slice = unsafe { std::slice::from_raw_parts(ptr, total) }; - return Ok(slice.to_vec()); + slice.to_vec() + } else { + unsafe { gather_strided_bytes(ptr, &shape, &strides, actual_elem) } + }; + + if actual_elem == expected_elem { + return Ok(actual_bytes); } - Ok(unsafe { gather_strided_bytes(ptr, &shape, &strides, elem) }) + // Dtype mismatch: convert from the actual dtype to the expected dtype. + Ok(convert_multiarray_bytes( + actual_bytes, + actual_dtype_code as i32, + descriptor.data_type, + )) +} + +/// Normalize non-standard MLMultiArrayDataType codes to canonical ones. +/// CoreML sometimes returns vendor-specific codes (e.g. 65568 for Float32). +fn normalize_dtype_code(code: i32) -> i32 { + match code { + 65600 | 4 => 4, // Int64 / Double → treat as Int64 + 65568 | 131104 | 32 => 32, // Float32 variants + 65552 | 16 => 16, // Float16 variants + 3 => 3, // Int32 + 1 => 1, // Int8 + _ => code, + } +} + +/// Convert a byte buffer from `actual_code` dtype to the `target` dtype. +/// Used when CoreML promotes an output dtype (e.g. uint8 boolean result → float32). +fn convert_multiarray_bytes(actual_bytes: Vec, actual_code: i32, target: DataType) -> Vec { + let canonical_code = normalize_dtype_code(actual_code); + let actual_elem = ml_dtype_code_element_size(canonical_code).unwrap_or(4); + let count = if actual_elem > 0 { + actual_bytes.len() / actual_elem + } else { + 0 + }; + + match canonical_code { + 32 => { + // Source: Float32 + let src = + unsafe { std::slice::from_raw_parts(actual_bytes.as_ptr() as *const f32, count) }; + match target { + DataType::Uint8 => src.iter().map(|&v| v as u8).collect(), + // f32 -> int8: go through i8 so negatives keep their bit pattern + // (`v as u8` saturates a negative float to 0). + DataType::Int8 => src.iter().map(|&v| (v as i8) as u8).collect(), + DataType::Int32 | DataType::Uint32 => { + let mut out = Vec::with_capacity(count * 4); + for &v in src { + out.extend_from_slice(&(v as i32).to_le_bytes()); + } + out + } + DataType::Float16 => { + let mut out = Vec::with_capacity(count * 2); + for &v in src { + out.extend_from_slice(&half::f16::from_f32(v).to_bits().to_le_bytes()); + } + out + } + _ => actual_bytes, + } + } + 16 => { + // Source: Float16 + let src = + unsafe { std::slice::from_raw_parts(actual_bytes.as_ptr() as *const u16, count) }; + match target { + DataType::Uint8 => src + .iter() + .map(|&bits| half::f16::from_bits(bits).to_f32() as u8) + .collect(), + // f16 -> int8: go through i8 so negatives keep their bit pattern. + DataType::Int8 => src + .iter() + .map(|&bits| (half::f16::from_bits(bits).to_f32() as i8) as u8) + .collect(), + DataType::Float32 => { + let mut out = Vec::with_capacity(count * 4); + for &bits in src { + out.extend_from_slice(&half::f16::from_bits(bits).to_f32().to_le_bytes()); + } + out + } + _ => actual_bytes, + } + } + 3 => { + // Source: Int32 + let src = + unsafe { std::slice::from_raw_parts(actual_bytes.as_ptr() as *const i32, count) }; + match target { + DataType::Float32 => { + let mut out = Vec::with_capacity(count * 4); + for &v in src { + out.extend_from_slice(&(v as f32).to_le_bytes()); + } + out + } + DataType::Uint8 | DataType::Int8 => src.iter().map(|&v| v as u8).collect(), + _ => actual_bytes, + } + } + 1 => { + // Source: Int8 — compatible byte layout with Uint8 + actual_bytes + } + 4 => { + // Source: Int64 + let src = + unsafe { std::slice::from_raw_parts(actual_bytes.as_ptr() as *const i64, count) }; + match target { + DataType::Int32 | DataType::Uint32 => { + let mut out = Vec::with_capacity(count * 4); + for &v in src { + out.extend_from_slice(&(v as i32).to_le_bytes()); + } + out + } + DataType::Float32 => { + let mut out = Vec::with_capacity(count * 4); + for &v in src { + out.extend_from_slice(&(v as f32).to_le_bytes()); + } + out + } + DataType::Uint8 | DataType::Int8 => src.iter().map(|&v| v as u8).collect(), + _ => actual_bytes, + } + } + _ => actual_bytes, + } } /// Whether `strides` (in elements) describe a C-contiguous layout for `shape`. diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 3541b648..ea07cdd8 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -1,732 +1,29 @@ # CoreML WPT trials that must run but whose failures are currently non-fatal. # Keep full sanitized libtest trial IDs here; passing trials still count as passes. -coreml::abs::abs_float16_1D_constant_tensor coreml::abs::abs_float16_6D_tensor coreml::abs::abs_int32_4D_tensor coreml::abs::abs_int64_4D_tensor -coreml::abs::abs_int8_4D_tensor -coreml::add::add_float16_1D_constant_tensors coreml::add::add_float32_with_special_character_names -coreml::arg_min_max::argMax_float16_1D_constant_tensor__axis_0__default_options -coreml::arg_min_max::argMax_float16_4D_tensor__axis_3__all_options -coreml::arg_min_max::argMax_float16_4D_tensor__axis_3__options_outputDataType__int64_ -coreml::arg_min_max::argMax_float32_4D_tensor__axis_3__all_options -coreml::arg_min_max::argMax_float32_4D_tensor__axis_3__options_outputDataType__int64_ -coreml::arg_min_max::argMax_int32_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMax_int64_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMax_int8_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMax_uint32_4D_tensor__axis_1__all_options -coreml::arg_min_max::argMax_uint64_4D_tensor__axis_1__all_options -coreml::arg_min_max::argMax_uint8_4D_tensor__axis_1__all_options -coreml::arg_min_max::argMin_float16_1D_constant_tensor__axis_0__default_options -coreml::arg_min_max::argMin_float16_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMin_float16_4D_tensor__axis_0__options_outputDataType__int64_ -coreml::arg_min_max::argMin_float32_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMin_float32_4D_tensor__axis_0__options_outputDataType__int64_ -coreml::arg_min_max::argMin_int32_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMin_int64_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMin_int8_4D_tensor__axis_0__all_options -coreml::arg_min_max::argMin_uint32_4D_tensor__axis_1__all_options -coreml::arg_min_max::argMin_uint64_4D_tensor__axis_1__all_options -coreml::arg_min_max::argMin_uint8_4D_tensor__axis_1__all_options -coreml::averagePool2d::averagePool2d_float16_4D_constant_tensor_all_positive_default_options -coreml::averagePool2d::averagePool2d_float16_4D_tensor_all_negative_default_options -coreml::averagePool2d::averagePool2d_float16_4D_tensor_all_positive_default_options coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_dilations -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_dilations_with_options_strides -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_layout_nchw -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_layout_nhwc -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_layout_nhwc_and_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_layout_nhwc_and_options_outputShapeRounding_floor -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputShapeRounding_ceil_and_no_padding -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputShapeRounding_floor -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_padding -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_strides -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_windowDimensions -coreml::averagePool2d::averagePool2d_float32_4D_constant_tensor_all_positive_default_options -coreml::averagePool2d::averagePool2d_float32_4D_tensor_all_negative_default_options -coreml::averagePool2d::averagePool2d_float32_4D_tensor_all_positive_default_options coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_dilations -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_dilations_with_options_strides -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_layout_nchw -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_layout_nhwc -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_layout_nhwc_and_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_layout_nhwc_and_options_outputShapeRounding_floor -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRounding_ceil_and_no_padding -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRounding_ceil_with_asymmetric_window -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRounding_floor -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_padding -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_strides -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_windowDimensions -coreml::averagePool2d::global_averagePool2d_float16_4D_tensor_all_positive_options_windowDimensions -coreml::averagePool2d::global_averagePool2d_float16_4D_tensor_options_layout_nhwc_and_options_windowDimensions -coreml::averagePool2d::global_averagePool2d_float32_4D_tensor_all_positive_options_windowDimensions -coreml::averagePool2d::global_averagePool2d_float32_4D_tensor_options_layout_nhwc_and_options_windowDimensions -coreml::batch_normalization::batchNormalization_float16_1D_tensor_options_axis_0 -coreml::batch_normalization::batchNormalization_float16_2D_tensor__mean_and_variance_are_non-constant__default_options -coreml::batch_normalization::batchNormalization_float16_2D_tensor_default_options -coreml::batch_normalization::batchNormalization_float16_3D_tensor_default_options -coreml::batch_normalization::batchNormalization_float16_4D_NCHW_tensor_options_axis_1 -coreml::batch_normalization::batchNormalization_float16_4D_NCHW_tensor_options_bias -coreml::batch_normalization::batchNormalization_float16_4D_NCHW_tensor_options_epsilon -coreml::batch_normalization::batchNormalization_float16_4D_NCHW_tensor_options_scale -coreml::batch_normalization::batchNormalization_float16_4D_NHWC_tensor_all_options -coreml::batch_normalization::batchNormalization_float16_4D_NHWC_tensor_options_axis_3 -coreml::batch_normalization::batchNormalization_float16_4D_tensor_default_options -coreml::batch_normalization::batchNormalization_float16_5D_tensor_default_options -coreml::batch_normalization::batchNormalization_float32_1D_tensor_options_axis_0 -coreml::batch_normalization::batchNormalization_float32_2D_tensor__mean_and_variance_are_non-constant__default_options -coreml::batch_normalization::batchNormalization_float32_2D_tensor_default_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_all_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_options_axis_3 -coreml::batch_normalization_constant::batchNormalization_float16_2D_constant_tensors_default_options -coreml::batch_normalization_constant::batchNormalization_float32_2D_constant_tensors_default_options -coreml::cast::cast_float16_4D_tensor_to_int64 -coreml::cast::cast_float16_4D_tensor_to_int8 -coreml::cast::cast_float16_4D_tensor_to_uint32 -coreml::cast::cast_float16_4D_tensor_to_uint8 -coreml::cast::cast_float32_4D_tensor_to_int64 -coreml::cast::cast_float32_4D_tensor_to_int8 -coreml::cast::cast_float32_4D_tensor_to_uint32 -coreml::cast::cast_float32_4D_tensor_to_uint8 -coreml::cast::cast_int32_4D_tensor_to_int64 -coreml::cast::cast_int32_4D_tensor_to_int8 -coreml::cast::cast_int32_4D_tensor_to_uint8 -coreml::cast::cast_int64_4D_tensor_to_float16 -coreml::cast::cast_int64_4D_tensor_to_float32 -coreml::cast::cast_int64_4D_tensor_to_int32 -coreml::cast::cast_int64_4D_tensor_to_int8 -coreml::cast::cast_int64_4D_tensor_to_uint32 -coreml::cast::cast_int64_4D_tensor_to_uint8 -coreml::cast::cast_int8_4D_tensor_to_float16 -coreml::cast::cast_int8_4D_tensor_to_float32 -coreml::cast::cast_int8_4D_tensor_to_int32 -coreml::cast::cast_int8_4D_tensor_to_int64 -coreml::cast::cast_int8_4D_tensor_to_uint32 -coreml::cast::cast_int8_4D_tensor_to_uint8 -coreml::cast::cast_uint32_4D_tensor_to_float16 -coreml::cast::cast_uint32_4D_tensor_to_float32 -coreml::cast::cast_uint32_4D_tensor_to_int32 -coreml::cast::cast_uint32_4D_tensor_to_int64 -coreml::cast::cast_uint32_4D_tensor_to_int8 -coreml::cast::cast_uint32_4D_tensor_to_uint8 -coreml::cast::cast_uint8_4D_tensor_to_float16 -coreml::cast::cast_uint8_4D_tensor_to_float32 -coreml::cast::cast_uint8_4D_tensor_to_int32 -coreml::cast::cast_uint8_4D_tensor_to_int64 -coreml::cast::cast_uint8_4D_tensor_to_int8 -coreml::cast::cast_uint8_4D_tensor_to_uint32 -coreml::ceil::ceil_float16_1D_constant_tensor -coreml::clamp::clamp_float16_1D_constant_tensor_default_options coreml::clamp::clamp_int32_1D_tensor coreml::clamp::clamp_int64_1D_tensor_with_bigint_max -coreml::clamp::clamp_int8_1D_tensor coreml::clamp::clamp_uint32_1D_tensor coreml::clamp::clamp_uint64_1D_tensor_with_Number_min_and_max coreml::clamp::clamp_uint64_1D_tensor_with_bigint_max -coreml::clamp::clamp_uint8_1D_tensor -coreml::clamp::maxValue_as_-Infinity -coreml::clamp::minValue____maxValue -coreml::clamp::minValue_as_Infinity coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 -coreml::conv2d::conv2d_float16_4D_both_input_and_filter_constant_tensors_default_options -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_1D_options_bias -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors__both_negative_input_tensor_and_options_bias -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_all_options -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_default_options -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_dilations -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_filterLayout__hwio_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_filterLayout__ihwo_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_filterLayout__ohwi_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_filterLayout__oihw_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_inputLayout__nchw_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_inputLayout__nhwc_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__hwio_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__ihwo_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__ohwi_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__oihw_ -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_padding -coreml::conv2d::conv2d_float16_4D_input_and_filter_tensors_options_strides -coreml::conv2d::conv2d_float32_4D_input_and_filter_tensors_options_inputLayout__nhwc_ -coreml::conv2d::conv2d_float32_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__hwio_ -coreml::conv2d::conv2d_float32_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__ihwo_ -coreml::conv2d::conv2d_float32_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__ohwi_ -coreml::conv2d::conv2d_float32_4D_input_and_filter_tensors_options_inputLayout__nhwc__and_options_filterLayout__oihw_ -coreml::conv2d::depthwise_conv2d_float16_4D_input_and_filter_tensors_options_groups__input_channels -coreml::conv_transpose2d::convTranspose2d_float16_4D_both_input_and_filter_constant_tensors_default_options -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors__both_negative_input_tensor_and_options_bias -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_default_options -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_bias -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_dilations -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_filterLayout_hwoi -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_filterLayout_iohw -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_filterLayout_ohwi -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_inputLayout_nchw -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_inputLayout_nhwc -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_inputLayout_nhwc_options_filterLayout_hwoi -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_inputLayout_nhwc_options_filterLayout_iohw -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_inputLayout_nhwc_options_filterLayout_ohwi -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_outputPadding -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_outputSizes -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_padding -coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_options_strides -coreml::conv_transpose2d::convTranspose2d_float16_input_tensors_options_padding_is_the_same_upper_padding -coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors__both_negative_input_tensor_and_options_bias -coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors_options_bias -coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors_options_inputLayout_nhwc -coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors_options_inputLayout_nhwc_options_filterLayout_hwoi -coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors_options_inputLayout_nhwc_options_filterLayout_iohw -coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors_options_inputLayout_nhwc_options_filterLayout_ohwi -coreml::conv_transpose2d::convTranspose2d_options_padding_is_the_same_upper_padding coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ -coreml::cos::cos_float16_1D_constant_tensor -coreml::dequantizeLinear::dequantizeLinear_int32_1D_tensor_with_float16_1D_scale -coreml::dequantizeLinear::dequantizeLinear_int32_1D_tensor_with_float32_1D_scale -coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_even_size_with_float16_1D_scale -coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_even_size_with_float32_1D_scale -coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_odd_size_with_float16_1D_scale -coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_odd_size_with_float32_1D_scale -coreml::dequantizeLinear::dequantizeLinear_int8_0D_constant_tensor_with_float16_0D_scale -coreml::dequantizeLinear::dequantizeLinear_int8_0D_constant_tensor_with_float32_0D_scale -coreml::dequantizeLinear::dequantizeLinear_int8_0D_tensor_with_float16_0D_scale -coreml::dequantizeLinear::dequantizeLinear_int8_0D_tensor_with_float32_0D_scale -coreml::dequantizeLinear::dequantizeLinear_int8_2D_tensor_with_float16_2D_scale__block_size____3__2_ -coreml::dequantizeLinear::dequantizeLinear_int8_2D_tensor_with_float32_2D_scale__block_size____3__2_ -coreml::dequantizeLinear::dequantizeLinear_int8_4D_tensor_broadcasting_float16_4D_scale_and_int8_4D_zeroPoint -coreml::dequantizeLinear::dequantizeLinear_int8_4D_tensor_broadcasting_float32_4D_scale_and_int8_4D_zeroPoint -coreml::dequantizeLinear::dequantizeLinear_uint4_1D_tensor_of_even_size_with_float16_1D_scale -coreml::dequantizeLinear::dequantizeLinear_uint4_1D_tensor_of_even_size_with_float32_1D_scale -coreml::dequantizeLinear::dequantizeLinear_uint4_1D_tensor_of_odd_size_with_float16_1D_scale -coreml::dequantizeLinear::dequantizeLinear_uint4_1D_tensor_of_odd_size_with_float32_1D_scale -coreml::dequantizeLinear::dequantizeLinear_uint4_3D_tensor_with_float16_3D_scale__block_size____1__1__2_ -coreml::dequantizeLinear::dequantizeLinear_uint4_3D_tensor_with_float32_3D_scale__block_size____1__1__2_ -coreml::dequantizeLinear::dequantizeLinear_uint4_4D_tensor_with_broadcasting_float16_4D_scale_and_uint4_4D_zeroPoint -coreml::dequantizeLinear::dequantizeLinear_uint4_4D_tensor_with_broadcasting_float32_4D_scale_and_uint4_4D_zeroPoint -coreml::dequantizeLinear::dequantizeLinear_uint8_1D_constant_tensor_with_float32_1D_scale -coreml::dequantizeLinear::dequantizeLinear_uint8_1D_tensor_with_float16_1D_scale -coreml::dequantizeLinear::dequantizeLinear_uint8_1D_tensor_with_float16_1D_scale__implicit_block_size___2 -coreml::dequantizeLinear::dequantizeLinear_uint8_1D_tensor_with_float32_1D_scale__implicit_block_size___2 -coreml::dequantizeLinear::dequantizeLinear_with_float16_3D_scale_as_an_intermediate_node -coreml::dequantizeLinear::dequantizeLinear_with_float32_3D_scale_as_an_intermediate_node -coreml::dequantizeLinear::per-tensor_dequantizeLinear_for_int4_4D_tensor_with_float16_4D_scale -coreml::dequantizeLinear::per-tensor_dequantizeLinear_for_int4_4D_tensor_with_float32_4D_scale -coreml::dequantizeLinear::quantizeLinear_then_dequantizeLinear_with_different_float16_scale_and_int8_zeroPoint -coreml::dequantizeLinear::quantizeLinear_then_dequantizeLinear_with_different_float32_scale_and_int8_zeroPoint -coreml::div::div_float16_1D_constant_tensors -coreml::elu::elu_float16_1D_constant_tensor_default_options -coreml::equal::equal_float16_1D_constant_tensors -coreml::equal::equal_float16_1D_tensors -coreml::equal::equal_float16_2D_tensors -coreml::equal::equal_float16_3D_tensors -coreml::equal::equal_float16_4D_tensors -coreml::equal::equal_float16_5D_and_broadcastable_4D -coreml::equal::equal_float16_5D_inputs_with_alternating_broadcast_axes -coreml::equal::equal_float16_5D_tensor_and_broadcastable_0D_scalar -coreml::equal::equal_float16_5D_tensors -coreml::equal::equal_float16_broadcast_0D_to_4D -coreml::equal::equal_float16_broadcast_1D_to_4D -coreml::equal::equal_float16_broadcast_2D_to_4D -coreml::equal::equal_float16_broadcast_3D_to_4D -coreml::equal::equal_float16_broadcast_4D_to_4D -coreml::equal::equal_float16_broadcast_4D_to_5D -coreml::equal::equal_float16_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::equal::equal_float16_two_5D_inputs_with_identical_shapes -coreml::equal::equal_float32_1D_constant_tensors -coreml::equal::equal_float32_1D_tensors -coreml::equal::equal_float32_2D_tensors -coreml::equal::equal_float32_3D_tensors -coreml::equal::equal_float32_4D_tensors -coreml::equal::equal_float32_5D_and_broadcastable_4D -coreml::equal::equal_float32_5D_inputs_with_alternating_broadcast_axes -coreml::equal::equal_float32_5D_tensor_and_broadcastable_0D_scalar -coreml::equal::equal_float32_5D_tensors -coreml::equal::equal_float32_broadcast_0D_to_4D -coreml::equal::equal_float32_broadcast_1D_to_4D -coreml::equal::equal_float32_broadcast_2D_to_4D -coreml::equal::equal_float32_broadcast_3D_to_4D -coreml::equal::equal_float32_broadcast_4D_to_4D -coreml::equal::equal_float32_broadcast_4D_to_5D -coreml::equal::equal_float32_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::equal::equal_float32_two_5D_inputs_with_identical_shapes -coreml::equal::equal_int32_4D_tensors -coreml::erf::erf_float16_1D_constant_tensor -coreml::exp::exp_float16_1D_constant_tensor -coreml::expand::expand_float16_1D_constant_tensor_to_1D -coreml::expand::expand_float32_0D_scalar_to_0D -coreml::floor::floor_float16_1D_constant_tensor -coreml::gather::gather_float16_1D_tensor_and_int64_0D_scalar_indices_default_options -coreml::gather::gather_float16_1D_tensor_and_uint32_0D_scalar_indices_default_options coreml::gather::gather_float16_5D_tensor_and_0D_scalar_indices_options_axis_4 -coreml::gather::gather_float32_1D_tensor_and_int64_0D_scalar_indices_default_options -coreml::gather::gather_float32_1D_tensor_and_uint32_0D_scalar_indices_default_options coreml::gather::gather_float32_2D_tensor_and_int32_0D_out-of-bound_positive_indices_default_options coreml::gather::gather_float32_5D_tensor_and_0D_scalar_indices_options_axis_4 -coreml::gatherElements::gatherElements_float16_2D_input_and_uint32_indices_options_axis_1 -coreml::gatherElements::gatherElements_float16_3D_input_and_int32_negative_indices -coreml::gatherElements::gatherElements_float32_1D_input_and_int32_out-of-bounds_indices -coreml::gatherElements::gatherElements_float32_2D_input_and_uint32_indices_options_axis_1 -coreml::gatherElements::gatherElements_float32_3D_input_and_int32_negative_indices -coreml::gatherND::gatherND_float16_2D_input_and_2D_negative_indices -coreml::gatherND::gatherND_float16_3D_input_and_2D_indices -coreml::gatherND::gatherND_float16_4D_input_and_1D_int32_indices -coreml::gatherND::gatherND_float16_4D_input_and_1D_int64_indices -coreml::gatherND::gatherND_float16_4D_input_and_1D_maximum_indices -coreml::gatherND::gatherND_float16_4D_input_and_1D_minimum_indices -coreml::gatherND::gatherND_float16_4D_input_and_1D_uint32_indices -coreml::gatherND::gatherND_float32_1D_input_and_2D_out-of-bounds_indices -coreml::gatherND::gatherND_float32_2D_input_and_2D_negative_indices -coreml::gatherND::gatherND_float32_2D_input_and_2D_out-of-bounds_indices -coreml::gatherND::gatherND_float32_3D_input_and_2D_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_int32_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_int64_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_maximum_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_minimum_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_uint32_indices coreml::gatherND::gatherND_float32_rank-5_input_and_rank-5_indices -coreml::gemm::gemm_both_negative_options_alpha_and_1st_float16_input_tensor -coreml::gemm::gemm_both_negative_options_alpha_and_1st_float32_input_tensor -coreml::gemm::gemm_both_negative_options_alpha_and_2nd_float16_input_tensor -coreml::gemm::gemm_both_negative_options_alpha_and_2nd_float32_input_tensor -coreml::gemm::gemm_both_negative_options_alpha_and_3rd_float16_input_tensor__options_c_ -coreml::gemm::gemm_both_negative_options_alpha_and_3rd_float32_input_tensor__options_c_ -coreml::gemm::gemm_both_negative_options_alpha_and_options_beta -coreml::gemm::gemm_both_negative_options_beta_and_3rd_float16_input_tensor__options_c_ -coreml::gemm::gemm_both_negative_options_beta_and_3rd_float32_input_tensor__options_c_ -coreml::gemm::gemm_float16_input_tensors_with_both_negative_options_alpha_and_options_beta -coreml::gemm::gemm_two_float16_2D_constant_tensors_options_c -coreml::gemm::gemm_two_float16_2D_tensors_all_options -coreml::gemm::gemm_two_float16_2D_tensors_broadcast_options_c__1__1______3__5_ -coreml::gemm::gemm_two_float16_2D_tensors_broadcast_options_c__1__5______3__5_ -coreml::gemm::gemm_two_float16_2D_tensors_broadcast_options_c__1______3__5_ -coreml::gemm::gemm_two_float16_2D_tensors_broadcast_options_c__3__1______3__5_ -coreml::gemm::gemm_two_float16_2D_tensors_broadcast_options_c__5______3__5_ -coreml::gemm::gemm_two_float16_2D_tensors_default_options -coreml::gemm::gemm_two_float16_2D_tensors_options_aTranspose_being_explicit_false -coreml::gemm::gemm_two_float16_2D_tensors_options_aTranspose_being_true -coreml::gemm::gemm_two_float16_2D_tensors_options_alpha -coreml::gemm::gemm_two_float16_2D_tensors_options_bTranspose_being_explicit_false -coreml::gemm::gemm_two_float16_2D_tensors_options_bTranspose_being_true -coreml::gemm::gemm_two_float16_2D_tensors_options_beta -coreml::gemm::gemm_two_float16_2D_tensors_options_c -coreml::gemm::gemm_two_float16_2D_tensors_options_c_and_options_beta -coreml::gemm::gemm_two_float16_2D_tensors_scalar_options_c -coreml::gemm::gemm_two_float32_2D_constant_tensors_options_c -coreml::gemm::gemm_two_float32_2D_tensors_all_options -coreml::gemm::gemm_two_float32_2D_tensors_broadcast_options_c__1__1______3__5_ -coreml::gemm::gemm_two_float32_2D_tensors_broadcast_options_c__1__5______3__5_ -coreml::gemm::gemm_two_float32_2D_tensors_broadcast_options_c__1______3__5_ -coreml::gemm::gemm_two_float32_2D_tensors_broadcast_options_c__3__1______3__5_ -coreml::gemm::gemm_two_float32_2D_tensors_broadcast_options_c__5______3__5_ -coreml::gemm::gemm_two_float32_2D_tensors_options_alpha___0_0_and_1D_options_c -coreml::gemm::gemm_two_float32_2D_tensors_options_alpha___0_0_and_1D_options_c__beta___1 -coreml::gemm::gemm_two_float32_2D_tensors_options_alpha___0_0_and_2D_options_c -coreml::gemm::gemm_two_float32_2D_tensors_options_alpha___0_0_and_scalar_options_c -coreml::gemm::gemm_two_float32_2D_tensors_options_c -coreml::gemm::gemm_two_float32_2D_tensors_options_c_and_options_beta -coreml::gemm::gemm_two_float32_2D_tensors_scalar_options_c -coreml::greater::greater_float16_0D_scalar -coreml::greater::greater_float16_1D_constant_tensors -coreml::greater::greater_float16_1D_tensors -coreml::greater::greater_float16_2D_tensors -coreml::greater::greater_float16_3D_tensors -coreml::greater::greater_float16_4D_tensors -coreml::greater::greater_float16_5D_and_broadcastable_4D -coreml::greater::greater_float16_5D_inputs_with_alternating_broadcast_axes -coreml::greater::greater_float16_5D_tensor_and_broadcastable_0D_scalar -coreml::greater::greater_float16_5D_tensors -coreml::greater::greater_float16_broadcast_0D_to_4D -coreml::greater::greater_float16_broadcast_1D_to_4D -coreml::greater::greater_float16_broadcast_2D_to_4D -coreml::greater::greater_float16_broadcast_3D_to_4D -coreml::greater::greater_float16_broadcast_4D_to_4D -coreml::greater::greater_float16_broadcast_4D_to_5D -coreml::greater::greater_float16_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::greater::greater_float16_two_5D_inputs_with_identical_shapes -coreml::greater::greater_float32_0D_scalar -coreml::greater::greater_float32_1D_constant_tensors -coreml::greater::greater_float32_1D_tensors -coreml::greater::greater_float32_2D_tensors -coreml::greater::greater_float32_3D_tensors -coreml::greater::greater_float32_4D_tensors -coreml::greater::greater_float32_5D_and_broadcastable_4D -coreml::greater::greater_float32_5D_inputs_with_alternating_broadcast_axes -coreml::greater::greater_float32_5D_tensor_and_broadcastable_0D_scalar -coreml::greater::greater_float32_5D_tensors -coreml::greater::greater_float32_broadcast_0D_to_4D -coreml::greater::greater_float32_broadcast_1D_to_4D -coreml::greater::greater_float32_broadcast_2D_to_4D -coreml::greater::greater_float32_broadcast_3D_to_4D -coreml::greater::greater_float32_broadcast_4D_to_4D -coreml::greater::greater_float32_broadcast_4D_to_5D -coreml::greater::greater_float32_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::greater::greater_float32_two_5D_inputs_with_identical_shapes -coreml::greater::greater_int32_4D_tensors -coreml::greater_or_equal::greaterOrEqual_float16_1D_constant_tensors -coreml::greater_or_equal::greaterOrEqual_float16_1D_tensors -coreml::greater_or_equal::greaterOrEqual_float16_2D_tensors -coreml::greater_or_equal::greaterOrEqual_float16_3D_tensors -coreml::greater_or_equal::greaterOrEqual_float16_4D_tensors -coreml::greater_or_equal::greaterOrEqual_float16_5D_and_broadcastable_4D -coreml::greater_or_equal::greaterOrEqual_float16_5D_inputs_with_alternating_broadcast_axes -coreml::greater_or_equal::greaterOrEqual_float16_5D_tensor_and_broadcastable_0D_scalar -coreml::greater_or_equal::greaterOrEqual_float16_5D_tensors -coreml::greater_or_equal::greaterOrEqual_float16_broadcast_0D_to_4D -coreml::greater_or_equal::greaterOrEqual_float16_broadcast_1D_to_4D -coreml::greater_or_equal::greaterOrEqual_float16_broadcast_2D_to_4D -coreml::greater_or_equal::greaterOrEqual_float16_broadcast_3D_to_4D -coreml::greater_or_equal::greaterOrEqual_float16_broadcast_4D_to_4D -coreml::greater_or_equal::greaterOrEqual_float16_broadcast_4D_to_5D -coreml::greater_or_equal::greaterOrEqual_float16_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::greater_or_equal::greaterOrEqual_float16_two_5D_inputs_with_identical_shapes -coreml::greater_or_equal::greaterOrEqual_float32_1D_constant_tensors -coreml::greater_or_equal::greaterOrEqual_float32_1D_tensors -coreml::greater_or_equal::greaterOrEqual_float32_2D_tensors -coreml::greater_or_equal::greaterOrEqual_float32_3D_tensors -coreml::greater_or_equal::greaterOrEqual_float32_4D_tensors -coreml::greater_or_equal::greaterOrEqual_float32_5D_and_broadcastable_4D -coreml::greater_or_equal::greaterOrEqual_float32_5D_inputs_with_alternating_broadcast_axes -coreml::greater_or_equal::greaterOrEqual_float32_5D_tensor_and_broadcastable_0D_scalar -coreml::greater_or_equal::greaterOrEqual_float32_5D_tensors -coreml::greater_or_equal::greaterOrEqual_float32_broadcast_0D_to_4D -coreml::greater_or_equal::greaterOrEqual_float32_broadcast_1D_to_4D -coreml::greater_or_equal::greaterOrEqual_float32_broadcast_2D_to_4D -coreml::greater_or_equal::greaterOrEqual_float32_broadcast_3D_to_4D -coreml::greater_or_equal::greaterOrEqual_float32_broadcast_4D_to_4D -coreml::greater_or_equal::greaterOrEqual_float32_broadcast_4D_to_5D -coreml::greater_or_equal::greaterOrEqual_float32_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::greater_or_equal::greaterOrEqual_float32_two_5D_inputs_with_identical_shapes -coreml::gru::gru_float16_tensors_steps_1_all_options -coreml::gru::gru_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_explicit_options_direction__forward_ -coreml::gru::gru_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_explicit_options_layout__zrn_ -coreml::gru::gru_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_options_initialHiddenState -coreml::gru::gru_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_options_layout__rzn_ -coreml::gru::gru_float16_tensors_steps_1_with_options_bias__options_recurrentBias_and_options_activations___relu____relu__ -coreml::gru::gru_float16_tensors_steps_1_with_options_bias__options_recurrentBias_and_options_activations___relu____relu___and_resetAfter_true -coreml::gru::gru_float16_tensors_steps_2_with_all_options -coreml::gru::gru_float16_tensors_steps_2_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_options_direction__backward_ -coreml::gru::gru_float16_tensors_steps_2_with_options_bias__options_recurrentBias__options_direction__backward___options_activations___relu____relu___and_explicit_options_returnSequence_false -coreml::gru::gru_float16_tensors_steps_2_with_options_bias__options_recurrentBias__options_direction__backward___options_activations___relu____relu___and_options_returnSequence_true -coreml::gru::gru_float16_tensors_steps_2_with_options_bias__options_recurrentBias__options_direction__both__and_options_returnSequence_true -coreml::gru::gru_float32_tensors_steps_1_all_options -coreml::gru::gru_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_explicit_options_direction__forward_ -coreml::gru::gru_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_explicit_options_layout__zrn_ -coreml::gru::gru_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_options_initialHiddenState -coreml::gru::gru_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_options_layout__rzn_ -coreml::gru::gru_float32_tensors_steps_1_with_options_bias__options_recurrentBias_and_options_activations___relu____relu__ -coreml::gru::gru_float32_tensors_steps_1_with_options_bias__options_recurrentBias_and_options_activations___relu____relu___and_reset_after_true -coreml::gru::gru_float32_tensors_steps_2_with_all_options -coreml::gru::gru_float32_tensors_steps_2_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_options_direction__backward_ -coreml::gru::gru_float32_tensors_steps_2_with_options_bias__options_recurrentBias__options_direction__backward___options_activations___relu____relu___and_explicit_options_returnSequence_false -coreml::gru::gru_float32_tensors_steps_2_with_options_bias__options_recurrentBias__options_direction__backward___options_activations___relu____relu___and_options_returnSequence_true -coreml::gru::gru_float32_tensors_steps_2_with_options_bias__options_recurrentBias__options_direction__both__and_options_returnSequence_true -coreml::gru_cell::gruCell_float16_tensors_with_all_options -coreml::gru_cell::gruCell_float16_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_and_options_layout__rzn_ -coreml::gru_cell::gruCell_float16_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_explicit_options_layout__zrn_ -coreml::gru_cell::gruCell_float16_tensors_with_options_bias__options_recurrentBias_and_options_activations___relu____relu__ -coreml::gru_cell::gruCell_float32_tensors_with_all_options -coreml::gru_cell::gruCell_float32_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_and_options_layout__rzn_ -coreml::gru_cell::gruCell_float32_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu___and_explicit_options_layout__zrn_ -coreml::gru_cell::gruCell_float32_tensors_with_options_bias__options_recurrentBias_and_options_activations___relu____relu__ -coreml::hard_swish::hardSwish_float16_1D_constant_tensor -coreml::identity::identity_float16_1D_constant_tensor -coreml::instance_normalization::instanceNormalization_float16_4D_tensor_all_options -coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_bias -coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_layout__nhwc_ -coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_scale -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_all_options -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_options_bias -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_options_layout__nhwc_ -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_options_scale -coreml::is_infinite::isInfinite_float16_1D_tensor -coreml::is_infinite::isInfinite_float16_2D_tensor -coreml::is_infinite::isInfinite_float16_5D_tensor -coreml::is_infinite::isInfinite_float16_positive_0D_scalar -coreml::is_infinite::isInfinite_float16_special_values -coreml::is_infinite::isInfinite_float32_1D_tensor -coreml::is_infinite::isInfinite_float32_2D_tensor -coreml::is_infinite::isInfinite_float32_3D_tensor -coreml::is_infinite::isInfinite_float32_4D_tensor -coreml::is_infinite::isInfinite_float32_5D_tensor -coreml::is_infinite::isInfinite_float32_all_infinite_values -coreml::is_infinite::isInfinite_float32_large_finite_values -coreml::is_infinite::isInfinite_float32_negative_infinity_only -coreml::is_infinite::isInfinite_float32_no_infinite_values -coreml::is_infinite::isInfinite_float32_positive_0D_scalar -coreml::is_infinite::isInfinite_float32_positive_infinity_only -coreml::is_infinite::isInfinite_float32_special_values -coreml::is_nan::isNaN_float16_1D_tensor -coreml::is_nan::isNaN_float16_2D_tensor -coreml::is_nan::isNaN_float16_5D_tensor -coreml::is_nan::isNaN_float16_positive_0D_scalar -coreml::is_nan::isNaN_float16_special_values -coreml::is_nan::isNaN_float32_1D_tensor -coreml::is_nan::isNaN_float32_2D_tensor -coreml::is_nan::isNaN_float32_3D_tensor -coreml::is_nan::isNaN_float32_4D_tensor -coreml::is_nan::isNaN_float32_5D_tensor -coreml::is_nan::isNaN_float32_all_NaN_values -coreml::is_nan::isNaN_float32_no_NaN_values -coreml::is_nan::isNaN_float32_positive_0D_scalar -coreml::is_nan::isNaN_float32_special_values -coreml::l2Pool2d::l2Pool2d_float16_4D_constant_tensor_all_positive_default_options -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_all_negative_default_options -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_all_positive_default_options coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_dilations -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_dilations_with_options_strides -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_layout_nchw -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_layout_nhwc -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_outputShapeRounding_ceil -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_outputShapeRounding_floor -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_padding -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_strides -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_windowDimensions -coreml::l2Pool2d::l2Pool2d_float32_4D_constant_tensor_all_positive_default_options -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_all_negative_default_options -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_all_positive_default_options coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_dilations -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_dilations_with_options_strides -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_layout_nchw -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_layout_nhwc -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputShapeRounding_ceil -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputShapeRounding_ceil_with_asymmetric_window -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputShapeRounding_floor -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_padding -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_strides -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_windowDimensions -coreml::layer_normalization::layerNormalization_float16_2D_tensor_default_options -coreml::layer_normalization::layerNormalization_float16_3D_tensor_default_options -coreml::layer_normalization::layerNormalization_float16_4D_tensor_all_options -coreml::layer_normalization::layerNormalization_float16_4D_tensor_default_options -coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_bias coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_bias_and_options_axes__3__1__2_ -coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_epsilon -coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_scale -coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_scale_and_options_axes__0__2_ -coreml::layer_normalization::layerNormalization_float16_5D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_0D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_2D_tensor_axes___ -coreml::layer_normalization::layerNormalization_float32_2D_tensor_axes____and_options_bias -coreml::layer_normalization::layerNormalization_float32_2D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_3D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_4D_tensor_all_options -coreml::layer_normalization::layerNormalization_float32_4D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_bias coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_bias_and_options_axes__3__1__2_ -coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_epsilon -coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_scale -coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_scale_and_options_axes__0__2_ -coreml::layer_normalization::layerNormalization_float32_5D_tensor_default_options -coreml::leaky_relu::leakyRelu_float16_1D_constant_tensor_default_options -coreml::lesser::lesser_float16_0D_scalar -coreml::lesser::lesser_float16_1D_constant_tensors -coreml::lesser::lesser_float16_1D_tensors -coreml::lesser::lesser_float16_2D_tensors -coreml::lesser::lesser_float16_3D_tensors -coreml::lesser::lesser_float16_4D_tensors -coreml::lesser::lesser_float16_5D_and_broadcastable_4D -coreml::lesser::lesser_float16_5D_inputs_with_alternating_broadcast_axes -coreml::lesser::lesser_float16_5D_tensor_and_broadcastable_0D_scalar -coreml::lesser::lesser_float16_5D_tensors -coreml::lesser::lesser_float16_broadcast_0D_to_4D -coreml::lesser::lesser_float16_broadcast_1D_to_4D -coreml::lesser::lesser_float16_broadcast_2D_to_4D -coreml::lesser::lesser_float16_broadcast_3D_to_4D -coreml::lesser::lesser_float16_broadcast_4D_to_4D -coreml::lesser::lesser_float16_broadcast_4D_to_5D -coreml::lesser::lesser_float16_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::lesser::lesser_float16_two_5D_inputs_with_identical_shapes -coreml::lesser::lesser_float32_0D_scalar -coreml::lesser::lesser_float32_1D_constant_tensors -coreml::lesser::lesser_float32_1D_tensors -coreml::lesser::lesser_float32_2D_tensors -coreml::lesser::lesser_float32_3D_tensors -coreml::lesser::lesser_float32_4D_tensors -coreml::lesser::lesser_float32_5D_and_broadcastable_4D -coreml::lesser::lesser_float32_5D_inputs_with_alternating_broadcast_axes -coreml::lesser::lesser_float32_5D_tensor_and_broadcastable_0D_scalar -coreml::lesser::lesser_float32_5D_tensors -coreml::lesser::lesser_float32_broadcast_0D_to_4D -coreml::lesser::lesser_float32_broadcast_1D_to_4D -coreml::lesser::lesser_float32_broadcast_2D_to_4D -coreml::lesser::lesser_float32_broadcast_3D_to_4D -coreml::lesser::lesser_float32_broadcast_4D_to_4D -coreml::lesser::lesser_float32_broadcast_4D_to_5D -coreml::lesser::lesser_float32_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::lesser::lesser_float32_two_5D_inputs_with_identical_shapes -coreml::lesser::lesser_int32_4D_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_0D_scalar -coreml::lesser_or_equal::lesserOrEqual_float16_1D_constant_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_1D_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_2D_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_3D_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_4D_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_5D_and_broadcastable_4D -coreml::lesser_or_equal::lesserOrEqual_float16_5D_inputs_with_alternating_broadcast_axes -coreml::lesser_or_equal::lesserOrEqual_float16_5D_tensor_and_broadcastable_0D_scalar -coreml::lesser_or_equal::lesserOrEqual_float16_5D_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_broadcast_0D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float16_broadcast_1D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float16_broadcast_2D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float16_broadcast_3D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float16_broadcast_4D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float16_broadcast_4D_to_5D -coreml::lesser_or_equal::lesserOrEqual_float16_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::lesser_or_equal::lesserOrEqual_float16_two_5D_inputs_with_identical_shapes -coreml::lesser_or_equal::lesserOrEqual_float32_0D_scalar -coreml::lesser_or_equal::lesserOrEqual_float32_1D_constant_tensors -coreml::lesser_or_equal::lesserOrEqual_float32_1D_tensors -coreml::lesser_or_equal::lesserOrEqual_float32_2D_tensors -coreml::lesser_or_equal::lesserOrEqual_float32_3D_tensors -coreml::lesser_or_equal::lesserOrEqual_float32_4D_tensors -coreml::lesser_or_equal::lesserOrEqual_float32_5D_and_broadcastable_4D -coreml::lesser_or_equal::lesserOrEqual_float32_5D_inputs_with_alternating_broadcast_axes -coreml::lesser_or_equal::lesserOrEqual_float32_5D_tensor_and_broadcastable_0D_scalar -coreml::lesser_or_equal::lesserOrEqual_float32_5D_tensors -coreml::lesser_or_equal::lesserOrEqual_float32_broadcast_0D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float32_broadcast_1D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float32_broadcast_2D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float32_broadcast_3D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float32_broadcast_4D_to_4D -coreml::lesser_or_equal::lesserOrEqual_float32_broadcast_4D_to_5D -coreml::lesser_or_equal::lesserOrEqual_float32_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::lesser_or_equal::lesserOrEqual_float32_two_5D_inputs_with_identical_shapes -coreml::linear::linear_float16_1D_constant_tensor_default_options -coreml::log::log_float16_positive_1D_constant_tensor -coreml::logical_and::logicalAnd_uint8_0D_scalar -coreml::logical_and::logicalAnd_uint8_1D_constant_tensors -coreml::logical_and::logicalAnd_uint8_1D_tensors -coreml::logical_and::logicalAnd_uint8_2D_tensors -coreml::logical_and::logicalAnd_uint8_3D_tensors -coreml::logical_and::logicalAnd_uint8_4D_tensors -coreml::logical_and::logicalAnd_uint8_5D_and_broadcastable_4D -coreml::logical_and::logicalAnd_uint8_5D_inputs_with_alternating_broadcast_axes -coreml::logical_and::logicalAnd_uint8_5D_tensors -coreml::logical_and::logicalAnd_uint8_broadcast_0D_to_4D -coreml::logical_and::logicalAnd_uint8_broadcast_1D_to_4D -coreml::logical_and::logicalAnd_uint8_broadcast_2D_to_4D -coreml::logical_and::logicalAnd_uint8_broadcast_3D_to_4D -coreml::logical_and::logicalAnd_uint8_broadcast_4D_to_4D -coreml::logical_and::logicalAnd_uint8_broadcast_4D_to_5D -coreml::logical_and::logicalAnd_uint8_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::logical_not::logicalNot_uint8_0D_scalar -coreml::logical_not::logicalNot_uint8_1D_constant_tensor -coreml::logical_not::logicalNot_uint8_1D_tensor -coreml::logical_not::logicalNot_uint8_2D_tensor -coreml::logical_not::logicalNot_uint8_3D_tensor -coreml::logical_not::logicalNot_uint8_4D_tensor -coreml::logical_not::logicalNot_uint8_5D_tensor -coreml::logical_or::logicalOr_uint8_0D_scalar -coreml::logical_or::logicalOr_uint8_1D_constant_tensors -coreml::logical_or::logicalOr_uint8_1D_tensors -coreml::logical_or::logicalOr_uint8_2D_tensors -coreml::logical_or::logicalOr_uint8_3D_tensors -coreml::logical_or::logicalOr_uint8_4D_tensors -coreml::logical_or::logicalOr_uint8_5D_and_broadcastable_4D -coreml::logical_or::logicalOr_uint8_5D_inputs_with_alternating_broadcast_axes -coreml::logical_or::logicalOr_uint8_5D_tensors -coreml::logical_or::logicalOr_uint8_broadcast_0D_to_4D -coreml::logical_or::logicalOr_uint8_broadcast_1D_to_4D -coreml::logical_or::logicalOr_uint8_broadcast_2D_to_4D -coreml::logical_or::logicalOr_uint8_broadcast_3D_to_4D -coreml::logical_or::logicalOr_uint8_broadcast_4D_to_4D -coreml::logical_or::logicalOr_uint8_broadcast_4D_to_5D -coreml::logical_or::logicalOr_uint8_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::logical_xor::logicalXor_uint8_0D_scalar -coreml::logical_xor::logicalXor_uint8_1D_constant_tensors -coreml::logical_xor::logicalXor_uint8_1D_tensors -coreml::logical_xor::logicalXor_uint8_2D_tensors -coreml::logical_xor::logicalXor_uint8_3D_tensors -coreml::logical_xor::logicalXor_uint8_4D_tensors -coreml::logical_xor::logicalXor_uint8_5D_and_broadcastable_4D -coreml::logical_xor::logicalXor_uint8_5D_inputs_with_alternating_broadcast_axes -coreml::logical_xor::logicalXor_uint8_5D_tensors -coreml::logical_xor::logicalXor_uint8_broadcast_0D_to_4D -coreml::logical_xor::logicalXor_uint8_broadcast_1D_to_4D -coreml::logical_xor::logicalXor_uint8_broadcast_2D_to_4D -coreml::logical_xor::logicalXor_uint8_broadcast_3D_to_4D -coreml::logical_xor::logicalXor_uint8_broadcast_4D_to_4D -coreml::logical_xor::logicalXor_uint8_broadcast_4D_to_5D -coreml::logical_xor::logicalXor_uint8_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::lstm::lstm_float16_tensors_steps_1_with_all_options -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_direction__forward_ -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_layout__iofg_ -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_returnSequence_false -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_initialCellState -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_initialHiddenState -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_layout__ifgo_ -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_peepholeWeight -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_returnSequence_true -coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentBias_and_options_activations___relu____relu____relu__ -coreml::lstm::lstm_float16_tensors_steps_2_with_all_options -coreml::lstm::lstm_float16_tensors_steps_2_with_bidirections -coreml::lstm::lstm_float16_tensors_steps_2_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_direction__backward_ -coreml::lstm::lstm_float32_tensors_steps_1_with_all_options -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_direction__forward_ -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_layout__iofg_ -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_returnSequence_false -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_initialCellState -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_initialHiddenState -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_layout__ifgo_ -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_peepholeWeight -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_returnSequence_true -coreml::lstm::lstm_float32_tensors_steps_1_with_options_bias__options_recurrentBias_and_options_activations___relu____relu____relu__ -coreml::lstm::lstm_float32_tensors_steps_2__batchSize_1_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_direction__backward_ -coreml::lstm::lstm_float32_tensors_steps_2_with_all_options -coreml::lstm::lstm_float32_tensors_steps_2_with_bidirections -coreml::lstm::lstm_float32_tensors_steps_2_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_direction__backward_ -coreml::lstm_cell::lstmCell_float16_tensors_with_all_options -coreml::lstm_cell::lstmCell_float16_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_layout__iofg_ -coreml::lstm_cell::lstmCell_float16_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_layout__ifgo_ -coreml::lstm_cell::lstmCell_float16_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_peepholeWeight -coreml::lstm_cell::lstmCell_float16_tensors_with_options_bias__options_recurrentBias_and_options_activations___relu____relu____relu__ -coreml::lstm_cell::lstmCell_float16_tensors_with_options_peepholeWeight_and_options_layout__ifgo_ -coreml::lstm_cell::lstmCell_float32_tensors_with_all_options -coreml::lstm_cell::lstmCell_float32_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_explicit_options_layout__iofg_ -coreml::lstm_cell::lstmCell_float32_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_layout__ifgo_ -coreml::lstm_cell::lstmCell_float32_tensors_with_options_bias__options_recurrentBias__options_activations___relu____relu____relu___and_options_peepholeWeight -coreml::lstm_cell::lstmCell_float32_tensors_with_options_bias__options_recurrentBias_and_options_activations___relu____relu____relu__ -coreml::lstm_cell::lstmCell_float32_tensors_with_options_peepholeWeight_and_options_layout__ifgo_ -coreml::max::max_float16_1D_constant_tensors -coreml::max::max_uint8_4D_tensors -coreml::maxPool2d::maxPool2d_float16_4D_constant_tensor_default_options -coreml::maxPool2d::maxPool2d_float16_4D_tensor_default_options coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations_with_options_strides -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_layout_nchw -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_layout_nhwc -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_outputShapeRounding_ceil -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_padding -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_strides -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_windowDimensions -coreml::maxPool2d::maxPool2d_float32_4D_constant_tensor_default_options -coreml::maxPool2d::maxPool2d_float32_4D_tensor_default_options coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_dilations -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_dilations_with_options_strides -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_layout_nchw -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_layout_nhwc -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputShapeRounding_ceil coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputShapeRounding_ceil_and_symmetric_padding -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputShapeRounding_ceil_with_asymmetric_window -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_padding -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_strides -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_windowDimensions -coreml::min::min_float16_1D_constant_tensors -coreml::min::min_uint8_4D_tensors coreml::mlNumber::cast_BigInt_to_int64 coreml::mlNumber::cast_BigInt_to_int64_overflow coreml::mlNumber::cast_BigInt_to_int64_underflow @@ -734,370 +31,21 @@ coreml::mlNumber::cast_BigInt_to_uint64 coreml::mlNumber::cast_BigInt_to_uint64_overflow coreml::mlNumber::cast_BigInt_to_uint64_underflow coreml::mlNumber::cast_float_to_integer -coreml::mlNumber::cast_float_to_integer_overflows -coreml::mlNumber::cast_float_to_integer_underflows -coreml::mlNumber::cast_fractional_float_to_integer -coreml::mul::mul_float16_1D_constant_tensors -coreml::mul::mul_uint32_4D_tensors -coreml::neg::neg_float16_1D_constant_tensor coreml::neg::neg_int32_4D_tensor coreml::neg::neg_int64_4D_tensor -coreml::neg::neg_int8_4D_tensor -coreml::not_equal::notEqual_float16_0D_scalar -coreml::not_equal::notEqual_float16_1D_constant_tensors -coreml::not_equal::notEqual_float16_1D_tensors -coreml::not_equal::notEqual_float16_2D_tensors -coreml::not_equal::notEqual_float16_3D_tensors -coreml::not_equal::notEqual_float16_4D_tensors -coreml::not_equal::notEqual_float16_5D_and_broadcastable_4D -coreml::not_equal::notEqual_float16_5D_inputs_with_alternating_broadcast_axes -coreml::not_equal::notEqual_float16_5D_tensor_and_broadcastable_0D_scalar -coreml::not_equal::notEqual_float16_5D_tensors -coreml::not_equal::notEqual_float16_broadcast_0D_to_4D -coreml::not_equal::notEqual_float16_broadcast_1D_to_4D -coreml::not_equal::notEqual_float16_broadcast_2D_to_4D -coreml::not_equal::notEqual_float16_broadcast_3D_to_4D -coreml::not_equal::notEqual_float16_broadcast_4D_to_4D -coreml::not_equal::notEqual_float16_broadcast_4D_to_5D -coreml::not_equal::notEqual_float16_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::not_equal::notEqual_float16_two_5D_inputs_with_identical_shapes -coreml::not_equal::notEqual_float32_0D_scalar -coreml::not_equal::notEqual_float32_1D_constant_tensors -coreml::not_equal::notEqual_float32_1D_tensors -coreml::not_equal::notEqual_float32_2D_tensors -coreml::not_equal::notEqual_float32_3D_tensors -coreml::not_equal::notEqual_float32_4D_tensors -coreml::not_equal::notEqual_float32_5D_and_broadcastable_4D -coreml::not_equal::notEqual_float32_5D_inputs_with_alternating_broadcast_axes -coreml::not_equal::notEqual_float32_5D_tensor_and_broadcastable_0D_scalar -coreml::not_equal::notEqual_float32_5D_tensors -coreml::not_equal::notEqual_float32_broadcast_0D_to_4D -coreml::not_equal::notEqual_float32_broadcast_1D_to_4D -coreml::not_equal::notEqual_float32_broadcast_2D_to_4D -coreml::not_equal::notEqual_float32_broadcast_3D_to_4D -coreml::not_equal::notEqual_float32_broadcast_4D_to_4D -coreml::not_equal::notEqual_float32_broadcast_4D_to_5D -coreml::not_equal::notEqual_float32_broadcasting_two_5D_inputs_with_a_leading_batch_size_of_1 -coreml::not_equal::notEqual_float32_two_5D_inputs_with_identical_shapes -coreml::pad::pad_float16_1D_constant_tensor_default_options -coreml::pad::pad_float16_1D_tensor_default_options -coreml::pad::pad_float16_2D_tensor_default_options -coreml::pad::pad_float16_2D_tensor_explicit_options_mode__constant_ -coreml::pad::pad_float16_2D_tensor_options_value_default_constant_mode -coreml::pad::pad_float16_3D_tensor_default_options -coreml::pad::pad_float16_4D_tensor_default_options coreml::pad::pad_float16_4D_tensor_options_mode__edge_ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ -coreml::pad::pad_float16_5D_tensor_default_options -coreml::pad::pad_float32_1D_constant_tensor_default_options -coreml::pad::pad_float32_1D_tensor_default_options -coreml::pad::pad_float32_2D_tensor_default_options -coreml::pad::pad_float32_2D_tensor_explicit_options_mode__constant_ -coreml::pad::pad_float32_2D_tensor_options_value_default_constant_mode -coreml::pad::pad_float32_2D_tensor_with_options_value_as_-Infinity -coreml::pad::pad_float32_2D_tensor_with_options_value_as_Infinity -coreml::pad::pad_float32_2D_tensor_with_options_value_as_NaN -coreml::pad::pad_float32_3D_tensor_default_options -coreml::pad::pad_float32_4D_tensor_default_options coreml::pad::pad_float32_4D_tensor_options_mode__edge_ coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ -coreml::pad::pad_float32_5D_tensor_default_options -coreml::pad::pad_int32_2D_tensor_default_options -coreml::pad::pad_int32_2D_tensor_options_value coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint -coreml::pad::pad_uint8_2D_tensor_default_options -coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op -coreml::pow::pow__sqrt__float16_4D_positive_base_tensor_and_broadcastable_0D_integer_exponent_scalar -coreml::pow::pow__square__float16_4D_base_tensor_and_broadcastable_0D_integer_exponent_scalar -coreml::pow::pow_float16_constant_1D_base_tensor_and_1D_integer_exponent_tensor -coreml::prelu::prelu_float16_0D_scalar -coreml::prelu::prelu_float16_1D_constant_tensors -coreml::prelu::prelu_float16_1D_non-constant_slope -coreml::prelu::prelu_float16_1D_tensors -coreml::prelu::prelu_float16_2D_tensors -coreml::prelu::prelu_float16_3D_tensors -coreml::prelu::prelu_float16_4D_tensors -coreml::prelu::prelu_float16_5D_tensors -coreml::prelu::prelu_float16_broadcast_4D_x_1D_slope -coreml::prelu::prelu_float16_broadcast_4D_x_1D_slope_of_shape__1_ -coreml::prelu::prelu_float16_broadcast_4D_x_2D_slope -coreml::prelu::prelu_float16_broadcast_4D_x_3D_slope -coreml::prelu::prelu_float16_broadcast_4D_x_4D_slope -coreml::prelu::prelu_float16_broadcast_5D_x_1D_slope -coreml::prelu::prelu_float16_broadcast_5D_x_5D_slope -coreml::prelu::prelu_float32_0D_scalar -coreml::prelu::prelu_float32_1D_constant_tensors -coreml::prelu::prelu_float32_1D_non-constant_slope -coreml::prelu::prelu_float32_1D_tensors -coreml::prelu::prelu_float32_2D_tensors -coreml::prelu::prelu_float32_3D_tensors -coreml::prelu::prelu_float32_4D_tensors -coreml::prelu::prelu_float32_5D_tensors -coreml::prelu::prelu_float32_broadcast_4D_x_1D_slope -coreml::prelu::prelu_float32_broadcast_4D_x_2D_slope -coreml::prelu::prelu_float32_broadcast_4D_x_3D_slope -coreml::prelu::prelu_float32_broadcast_4D_x_4D_slope -coreml::prelu::prelu_float32_broadcast_5D_x_1D_slope -coreml::prelu::prelu_float32_broadcast_5D_x_5D_slope -coreml::prelu::prelu_float32_broadcast_5D_x_5D_slope_with_expanded_output_shape -coreml::prelu::prelu_int64_2D_constant_tensors -coreml::qdq_subgraph::dequantizeLinear_-__conv2d_-__clamp_-__quantizeLinear -coreml::qdq_subgraph::dequantizeLinear_-__conv2d_-__relu_-__quantizeLinear -coreml::qdq_subgraph::per-channel_quantized_gemm_with_non-zero_quantized_dimension_of_the_filter -coreml::qdq_subgraph::quantized_argMax -coreml::qdq_subgraph::quantized_averagePool2d -coreml::qdq_subgraph::quantized_clamp -coreml::qdq_subgraph::quantized_clamp_with_emulation -coreml::qdq_subgraph::quantized_concat -coreml::qdq_subgraph::quantized_conv2d -coreml::qdq_subgraph::quantized_conv2d_with_padding coreml::qdq_subgraph::quantized_convTranspose2d -coreml::qdq_subgraph::quantized_element-wise_binary_add -coreml::qdq_subgraph::quantized_element-wise_binary_max -coreml::qdq_subgraph::quantized_element-wise_binary_min -coreml::qdq_subgraph::quantized_element-wise_binary_mul -coreml::qdq_subgraph::quantized_element-wise_binary_sub -coreml::qdq_subgraph::quantized_element-wise_logical_equal -coreml::qdq_subgraph::quantized_element-wise_logical_greater -coreml::qdq_subgraph::quantized_element-wise_logical_greaterOrEqual -coreml::qdq_subgraph::quantized_element-wise_logical_lesser -coreml::qdq_subgraph::quantized_element-wise_logical_lesserOrEqual -coreml::qdq_subgraph::quantized_element-wise_logical_notEqual -coreml::qdq_subgraph::quantized_elu -coreml::qdq_subgraph::quantized_gather -coreml::qdq_subgraph::quantized_gemm_with_bias -coreml::qdq_subgraph::quantized_leaky_relu -coreml::qdq_subgraph::quantized_maxPool2d -coreml::qdq_subgraph::quantized_pad_with_constant_mode_for_Kernel_fusion -coreml::qdq_subgraph::quantized_pad_with_constant_mode_for_XNNPack_fusion -coreml::qdq_subgraph::quantized_pad_with_reflection_mode -coreml::qdq_subgraph::quantized_reduceMax -coreml::qdq_subgraph::quantized_reduceMean -coreml::qdq_subgraph::quantized_reduceMin -coreml::qdq_subgraph::quantized_reduceSum -coreml::qdq_subgraph::quantized_resample2d -coreml::qdq_subgraph::quantized_reshape -coreml::qdq_subgraph::quantized_sigmoid -coreml::qdq_subgraph::quantized_slice -coreml::qdq_subgraph::quantized_softmax -coreml::qdq_subgraph::quantized_split -coreml::qdq_subgraph::quantized_tanh -coreml::qdq_subgraph::quantized_transpose -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor -coreml::quantizeLinear::quantizeLinear_float16_0D_constant_tensor_with_int8_0D_zeroPoint -coreml::quantizeLinear::quantizeLinear_float16_0D_tensor_with_int8_0D_zeroPoint -coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint8_1D_zeroPoint -coreml::quantizeLinear::quantizeLinear_float16_2D_tensor_broadcasting_zeroPoint_and_scale -coreml::quantizeLinear::quantizeLinear_float16_2D_tensor_with_int4_zeroPoint_which_has_even_size -coreml::quantizeLinear::quantizeLinear_float16_2D_tensor_with_int4_zeroPoint_with_block_size____3__2_ -coreml::quantizeLinear::quantizeLinear_float16_3D_tensor_with_implicit_block_size____1__2__1__ -coreml::quantizeLinear::quantizeLinear_float16_4D_tensor_broadcasting_scale_and_zeroPoint -coreml::quantizeLinear::quantizeLinear_float16_tensor_with_int32_zeroPoint -coreml::quantizeLinear::quantizeLinear_float16_tensor_with_int4_zeroPoint_which_has_even_size -coreml::quantizeLinear::quantizeLinear_float16_tensor_with_int4_zeroPoint_which_has_odd_size -coreml::quantizeLinear::quantizeLinear_float16_tensor_with_uint4_zeroPoint_which_has_even_size -coreml::quantizeLinear::quantizeLinear_float16_tensor_with_uint4_zeroPoint_which_has_odd_size -coreml::quantizeLinear::quantizeLinear_float32_0D_constant_tensor_with_int8_0D_zeroPoint -coreml::quantizeLinear::quantizeLinear_float32_0D_tensor_with_int8_0D_zeroPoint -coreml::quantizeLinear::quantizeLinear_float32_1D_constant_tensor_with_uint8_1D_zeroPoint -coreml::quantizeLinear::quantizeLinear_float32_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -coreml::quantizeLinear::quantizeLinear_float32_2D_tensor_broadcasting_zeroPoint_and_scale -coreml::quantizeLinear::quantizeLinear_float32_2D_tensor_with_int4_zeroPoint_which_has_even_size -coreml::quantizeLinear::quantizeLinear_float32_2D_tensor_with_int4_zeroPoint_with_block_size____3__2_ -coreml::quantizeLinear::quantizeLinear_float32_3D_tensor_with_implicit_block_size____1__2__1__ -coreml::quantizeLinear::quantizeLinear_float32_4D_tensor_broadcasting_scale_and_zeroPoint -coreml::quantizeLinear::quantizeLinear_float32_tensor_with_int32_zeroPoint -coreml::quantizeLinear::quantizeLinear_float32_tensor_with_int4_zeroPoint_which_has_even_size -coreml::quantizeLinear::quantizeLinear_float32_tensor_with_int4_zeroPoint_which_has_odd_size -coreml::quantizeLinear::quantizeLinear_float32_tensor_with_uint4_zeroPoint_which_has_even_size -coreml::quantizeLinear::quantizeLinear_float32_tensor_with_uint4_zeroPoint_which_has_odd_size -coreml::reciprocal::reciprocal_float16_1D_constant_tensor -coreml::reduce_l1::reduceL1_float16_0D_constant_tensor_default_options -coreml::reduce_l1::reduceL1_float16_0D_constant_tensor_empty_axes -coreml::reduce_l1::reduceL1_float16_1D_constant_tensor_all_positive_default_options -coreml::reduce_l1::reduceL1_float32_0D_constant_tensor_default_options -coreml::reduce_l1::reduceL1_float32_0D_constant_tensor_empty_axes -coreml::reduce_l1::reduceL1_float32_1D_constant_tensor_empty_axes -coreml::reduce_l1::reduceL1_uint32_4D_tensor_options_axes_with_options_keepDimensions_false -coreml::reduce_l2::reduceL2_float16_0D_constant_tensor_default_options -coreml::reduce_l2::reduceL2_float16_0D_constant_tensor_empty_axes -coreml::reduce_l2::reduceL2_float16_1D_constant_tensor_all_positive_default_options -coreml::reduce_l2::reduceL2_float32_0D_constant_tensor_default_options -coreml::reduce_l2::reduceL2_float32_0D_constant_tensor_empty_axes -coreml::reduce_l2::reduceL2_float32_1D_constant_tensor_empty_axes -coreml::reduce_log_sum::reduceLogSum_float16_0D_constant_tensor_default_options -coreml::reduce_log_sum::reduceLogSum_float16_0D_constant_tensor_empty_axes -coreml::reduce_log_sum::reduceLogSum_float16_1D_constant_tensor_all_non-negative_default_options -coreml::reduce_log_sum::reduceLogSum_float32_0D_constant_tensor_default_options -coreml::reduce_log_sum::reduceLogSum_float32_0D_constant_tensor_empty_axes -coreml::reduce_log_sum::reduceLogSum_float32_1D_constant_tensor_empty_axes coreml::reduce_log_sum_exp::reduceLogSumExp_avoids_overflows_caused_by_taking_the_exp_of_large_inputs coreml::reduce_log_sum_exp::reduceLogSumExp_avoids_underflows_caused_by_taking_the_log_of_small_inputs -coreml::reduce_log_sum_exp::reduceLogSumExp_float16_0D_constant_tensor_default_options -coreml::reduce_log_sum_exp::reduceLogSumExp_float16_0D_constant_tensor_empty_axes -coreml::reduce_log_sum_exp::reduceLogSumExp_float16_1D_constant_tensor_all_positive_default_options -coreml::reduce_log_sum_exp::reduceLogSumExp_float32_0D_constant_tensor_default_options -coreml::reduce_log_sum_exp::reduceLogSumExp_float32_0D_constant_tensor_empty_axes -coreml::reduce_log_sum_exp::reduceLogSumExp_float32_1D_constant_tensor_empty_axes -coreml::reduce_max::reduceMax_float16_0D_constant_tensor_default_options -coreml::reduce_max::reduceMax_float16_0D_constant_tensor_empty_axes -coreml::reduce_max::reduceMax_float16_1D_constant_tensor_default_options -coreml::reduce_max::reduceMax_float32_0D_constant_tensor_default_options -coreml::reduce_max::reduceMax_float32_0D_constant_tensor_empty_axes -coreml::reduce_max::reduceMax_float32_1D_constant_tensor_empty_axes -coreml::reduce_mean::reduceMean_float16_0D_constant_tensor_default_options -coreml::reduce_mean::reduceMean_float16_0D_constant_tensor_empty_axes -coreml::reduce_mean::reduceMean_float16_1D_constant_tensor_all_positive_default_options -coreml::reduce_mean::reduceMean_float32_0D_constant_tensor_default_options -coreml::reduce_mean::reduceMean_float32_0D_constant_tensor_empty_axes -coreml::reduce_mean::reduceMean_float32_1D_constant_tensor_empty_axes -coreml::reduce_min::reduceMin_float16_0D_constant_tensor_default_options -coreml::reduce_min::reduceMin_float16_0D_constant_tensor_empty_axes -coreml::reduce_min::reduceMin_float16_1D_constant_tensor_default_options -coreml::reduce_min::reduceMin_float32_0D_constant_tensor_default_options -coreml::reduce_min::reduceMin_float32_0D_constant_tensor_empty_axes -coreml::reduce_min::reduceMin_float32_1D_constant_tensor_empty_axes -coreml::reduce_product::reduceProduct_float16_0D_constant_tensor_default_options -coreml::reduce_product::reduceProduct_float16_0D_constant_tensor_empty_axes -coreml::reduce_product::reduceProduct_float16_1D_constant_tensor_default_options -coreml::reduce_product::reduceProduct_float32_0D_constant_tensor_default_options -coreml::reduce_product::reduceProduct_float32_0D_constant_tensor_empty_axes -coreml::reduce_product::reduceProduct_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum::reduceSum_float16_0D_constant_tensor_default_options -coreml::reduce_sum::reduceSum_float16_0D_constant_tensor_empty_axes -coreml::reduce_sum::reduceSum_float16_1D_constant_tensor_all_positive_default_options -coreml::reduce_sum::reduceSum_float32_0D_constant_tensor_default_options -coreml::reduce_sum::reduceSum_float32_0D_constant_tensor_empty_axes -coreml::reduce_sum::reduceSum_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float16_0D_constant_tensor_default_options -coreml::reduce_sum_square::reduceSumSquare_float16_0D_constant_tensor_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float16_1D_constant_tensor_all_positive_default_options -coreml::reduce_sum_square::reduceSumSquare_float16_1D_tensor_with_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float32_0D_constant_tensor_default_options -coreml::reduce_sum_square::reduceSumSquare_float32_0D_constant_tensor_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float32_1D_tensor_with_empty_axes -coreml::relu::relu_float16_1D_constant_tensor coreml::relu::relu_int32_4D_tensor coreml::relu::relu_int64_4D_tensor -coreml::relu::relu_int8_4D_tensor -coreml::resample2d::resample2d_float32_4D_tensor_default_options -coreml::resample2d::resample2d_upsample__float32_4D_tensor_explicit_options_axes__2__3_ -coreml::resample2d::resample2d_upsample__float32_4D_tensor_explicit_options_axes__3__2_ -coreml::resample2d::resample2d_upsample__float32_4D_tensor_explicit_options_mode__nearest-neighbor_ coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_axes__0__1_ coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_axes__1__0_ -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_axes__1__2_ -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_axes__1__2__options_mode__linear_ -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_scales -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_scales_options_mode__linear_ -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_sizes -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_sizes_ignored_options_scales -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_sizes_options_mode__linear_ coreml::reshape::reshape__unsqueeze__float16_5D_tensor_by_adding_4th_dimension coreml::reshape::reshape__unsqueeze__float32_5D_tensor_by_adding_4th_dimension -coreml::reshape::reshape_float16_constant_tensor_to_a_new_shape__reorder_all_dimensions_ -coreml::scatterElements::scatterElements_float16_tensors_along_axis_0 -coreml::scatterElements::scatterElements_float16_tensors_along_axis_0_and_constant_indices -coreml::scatterElements::scatterElements_float16_tensors_along_axis_1 -coreml::scatterElements::scatterElements_float16_tensors_along_axis_1_and_constant_indices -coreml::scatterElements::scatterElements_float32_tensors_along_axis_0 -coreml::scatterElements::scatterElements_float32_tensors_along_axis_0_and_constant_indices -coreml::scatterElements::scatterElements_float32_tensors_along_axis_1 -coreml::scatterElements::scatterElements_float32_tensors_along_axis_1_and_constant_indices -coreml::scatterND::scatterND_1D_float16_tensors__Insert_individual_elements_in_a_tensor_by_index_ -coreml::scatterND::scatterND_1D_float32_tensors__Insert_individual_elements_in_a_tensor_by_index_ coreml::scatterND::scatterND_2D_int8_tensors_with_index_out_of_bound -coreml::scatterND::scatterND_3D_float16_tensors__Insert_entire_slices_of_a_higher_rank_tensor_ -coreml::scatterND::scatterND_3D_float32_tensors__Insert_entire_slices_of_a_higher_rank_tensor_ -coreml::sigmoid::sigmoid_float16_1D_constant_tensor -coreml::sign::sign_int64_3D_tensor -coreml::sign::sign_int8_4D_tensor -coreml::sin::sin_float16_1D_constant_tensor -coreml::slice::slice_float16_1D_constant_tensor -coreml::slice::slice_float16_2D_tensor_with_strides -coreml::slice::slice_float16_3D_tensor_with_strides -coreml::slice::slice_float16_4D_tensor_with_strides -coreml::slice::slice_float32_2D_tensor_with_strides -coreml::slice::slice_float32_3D_tensor_with_strides -coreml::slice::slice_float32_4D_tensor_with_strides -coreml::slice::slicing_float32_0D_constant_tensor_with_empty_starts_and_sizes_should_be_a_no-op -coreml::softmax::softmax_float16_2D_constant_tensor_all_positive -coreml::softplus::softplus_float16_1D_constant_tensor -coreml::softsign::softsign_positive_float16_1D_constant_tensor -coreml::split::split_float16_1D_constant_tensor_number_splits_default_options -coreml::sqrt::sqrt_float16_1D_constant_tensor -coreml::sub::sub_float16_1D_constant_tensors -coreml::sub::sub_int64_4D_tensors -coreml::sub::sub_int8_4D_tensors -coreml::sub::sub_uint32_4D_tensors -coreml::sub::sub_uint64_4D_tensors -coreml::sub::sub_uint8_4D_tensors coreml::subgraph::add___sub___mul___gather_default -coreml::subgraph::batchNormalization_default___clamp -coreml::subgraph::batchNormalization_default___elu -coreml::subgraph::batchNormalization_default___hardSigmoid -coreml::subgraph::batchNormalization_default___hardSwish -coreml::subgraph::batchNormalization_default___leakyRelu -coreml::subgraph::batchNormalization_default___linear -coreml::subgraph::batchNormalization_default___prelu -coreml::subgraph::batchNormalization_default___relu -coreml::subgraph::batchNormalization_default___sigmoid -coreml::subgraph::batchNormalization_default___softsign -coreml::subgraph::batchNormalization_options_axis_0____softmax -coreml::subgraph::batchNormalization_options_axis_0___gelu -coreml::subgraph::batchNormalization_options_axis_0___softplus -coreml::subgraph::conv2d_default___prelu -coreml::subgraph::convTranspose2d_default___prelu -coreml::tan::tan_float16_1D_constant_tensor -coreml::tanh::tanh_float16_1D_constant_tensor -coreml::tile::tile_float32_0D_scalar_tensor_by_repetitions___ -coreml::tile::tile_uint32_2D_tensor -coreml::transpose::transpose_float16_1D_constant_tensor_default_options -coreml::transpose::transpose_float32_0D_constant_tensor_default_options -coreml::triangular::triangular_float16_4D_tensor_fully_zero_options_diagonal_3 -coreml::triangular::triangular_float16_4D_tensor_fully_zero_options_upper_false_options_diagonal_-2 -coreml::triangular::triangular_float16_4D_tensor_options_diagonal_1 -coreml::triangular::triangular_float16_4D_tensor_options_upper_false_options_diagonal_-1 -coreml::triangular::triangular_float16_4D_tensor_options_upper_true_options_diagonal_1 -coreml::triangular::triangular_float32_4D_tensor_fully_zero_options_diagonal_3 -coreml::triangular::triangular_float32_4D_tensor_fully_zero_options_upper_false_options_diagonal_-2 -coreml::triangular::triangular_float32_4D_tensor_options_diagonal_1 -coreml::triangular::triangular_float32_4D_tensor_options_upper_false_options_diagonal_-1 -coreml::triangular::triangular_float32_4D_tensor_options_upper_true_options_diagonal_1 -coreml::where::where_float16_0D_scalars -coreml::where::where_float16_1D_constant_tensors -coreml::where::where_float16_1D_tensors -coreml::where::where_float16_2D_tensors -coreml::where::where_float16_3D_tensors -coreml::where::where_float16_4D_tensors -coreml::where::where_float16_4D_tensors_all_broadcast_4D -coreml::where::where_float16_4D_tensors_only_broadcast_condition_0D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_condition_1D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_condition_2D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_condition_3D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_condition_4D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_falseValues_3D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_falseValues_4D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_trueValues_2D_to_4D -coreml::where::where_float16_4D_tensors_only_broadcast_trueValues_4D_to_4D -coreml::where::where_float16_5D_tensors -coreml::where::where_float32_0D_scalars -coreml::where::where_float32_1D_constant_tensors -coreml::where::where_float32_1D_tensors -coreml::where::where_float32_2D_tensors -coreml::where::where_float32_3D_tensors -coreml::where::where_float32_4D_tensors -coreml::where::where_float32_4D_tensors_all_broadcast_4D -coreml::where::where_float32_4D_tensors_only_broadcast_condition_0D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_condition_1D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_condition_2D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_condition_3D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_condition_4D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_falseValues_3D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_falseValues_4D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_trueValues_2D_to_4D -coreml::where::where_float32_4D_tensors_only_broadcast_trueValues_4D_to_4D -coreml::where::where_float32_5D_tensors -coreml::where::where_int32_4D_tensors