From ac3268be72eebca1bee03c6ccb2a15518544ecf6 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Mon, 13 Jul 2026 16:53:53 +0200 Subject: [PATCH 01/34] Fix comparison/logical ops --- src/executors/coreml.rs | 145 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/src/executors/coreml.rs b/src/executors/coreml.rs index 19cc8e41..e11b3663 100644 --- a/src/executors/coreml.rs +++ b/src/executors/coreml.rs @@ -607,6 +607,9 @@ unsafe fn fill_multiarray_from_bytes( /// /// 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 +618,13 @@ 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); + 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 +643,139 @@ 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); + } + + // 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, } +} - Ok(unsafe { gather_strided_bytes(ptr, &shape, &strides, elem) }) +/// 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 | DataType::Int8 => src.iter().map(|&v| v 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 | DataType::Int8 => src + .iter() + .map(|&bits| half::f16::from_bits(bits).to_f32() 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`. From b5c31efaa5ca745b5eef36d2eaf37fd62ff1fad8 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Mon, 13 Jul 2026 16:59:08 +0200 Subject: [PATCH 02/34] Fix missing MIL params --- src/converters/coreml_mlprogram.rs | 117 +++++++++++++++++++++++------ src/executors/coreml.rs | 83 +++++++++++++++++--- 2 files changed, 166 insertions(+), 34 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index fc0c43d6..7a13634d 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1815,15 +1815,24 @@ impl CoremlMlProgramConverter { Self::create_immediate_string("custom"), ); } else { + // CoreML avg_pool/max_pool requires explicit pad even when zero. + inputs.insert( + "pad".to_string(), + Self::create_immediate_int_array(&[0u32; 0]), + ); inputs.insert( "pad_type".to_string(), - Self::create_immediate_string("same"), + Self::create_immediate_string("valid"), ); } } else { + inputs.insert( + "pad".to_string(), + Self::create_immediate_int_array(&[0u32; 0]), + ); inputs.insert( "pad_type".to_string(), - Self::create_immediate_string("same"), + Self::create_immediate_string("valid"), ); } } @@ -2193,29 +2202,45 @@ impl CoremlMlProgramConverter { .. } => { // 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])); } - 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" + + // 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: always emit (required even for non-constant modes) + let constant_val = options + .as_ref() + .and_then(|o| Self::parse_mlnumber_f64(o.value.as_ref())) + .unwrap_or(0.0); + inputs.insert( + "constant_val".to_string(), + Self::create_immediate_float(constant_val as f32), + ); } Operation::Gelu { .. } => { @@ -2307,7 +2332,8 @@ impl CoremlMlProgramConverter { } Operation::ScatterElements { options, .. } => { - // scatter: data, indices, updates, axis + // 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( @@ -2323,6 +2349,9 @@ impl CoremlMlProgramConverter { if let Some(opts) = options { inputs.insert("axis".to_string(), Self::create_immediate_int(opts.axis)); } + + // CoreML requires explicit mode even though "update" is the only supported value. + inputs.insert("mode".to_string(), Self::create_immediate_string("update")); } Operation::ScatterND { .. } @@ -3666,6 +3695,48 @@ impl super::GraphConverter for CoremlMlProgramConverter { continue; } + // `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; + } + } + } + let mil_op = self.convert_operation_with_overrides(graph_info, op, &operand_name_overrides)?; main_block.operations.push(mil_op); diff --git a/src/executors/coreml.rs b/src/executors/coreml.rs index e11b3663..5ba998e0 100644 --- a/src/executors/coreml.rs +++ b/src/executors/coreml.rs @@ -576,19 +576,27 @@ 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) + // 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); + if let Some(array_elem) = ml_dtype_code_element_size(canonical_code) && array_elem != elem { - 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 = 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,6 +611,59 @@ 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 | DataType::Int8, 32) => { + // bool/uint8 → float32: 0→0.0, 1→1.0 (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::Int32 | DataType::Uint32, 32) => { + // int32 as float32 bits (reinterpret, same size — shouldn't normally happen) + src.to_vec() + } + (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 From 32244b64b30d7a6156c9e50aaab544311cf2bc8d Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Mon, 13 Jul 2026 17:30:09 +0200 Subject: [PATCH 03/34] QDQ subgraph fixes --- src/converters/coreml_mlprogram.rs | 265 +++++++++++++++++++++++++---- src/executors/coreml.rs | 11 +- 2 files changed, 235 insertions(+), 41 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 7a13634d..9c7c9ef2 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,43 @@ 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 + } + _ => src.to_vec(), + } +} + /// MIL operation type names (matching Chromium's implementation) mod mil_ops { // Binary operations @@ -2660,6 +2697,50 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // 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(); + // Squeeze shape for both scale (index 1) and zero_point (index 2). + 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) { + if param_operand.descriptor.shape.len() > 1 { + scale_ids_to_squeeze.insert(param_id); + } + } + } + } + // Determine the expected zero_point data type. + // For quantize: zp type = output type. + // For dequantize: zp type = input type. + let zp_expected_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()) + }; + if let (Some(&zp_id), Some(expected_dt)) = (input_ids.get(2), zp_expected_type) { + zp_id_to_dtype.insert(zp_id, expected_dt); + } + } + } + // Add constant operands as const operations for (operand_id, constant_data) in &graph_info.constant_operand_ids_to_handles { let operand = @@ -2670,16 +2751,93 @@ impl super::GraphConverter for CoremlMlProgramConverter { reason: format!("Constant operand {} not found", 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); + // 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 { @@ -2729,10 +2887,6 @@ impl super::GraphConverter for CoremlMlProgramConverter { let filter_name = operand_name(graph_info, filter_operand_id); let transposed_filter_name = format!("{}_transposed", filter_name); - // Store the override mapping - operand_name_overrides - .insert(filter_operand_id, transposed_filter_name.clone()); - let mut transpose_inputs: HashMap = HashMap::new(); transpose_inputs .insert("x".to_string(), Self::create_name_argument(filter_name)); @@ -2772,7 +2926,29 @@ impl super::GraphConverter for CoremlMlProgramConverter { vec![transpose_output_type], ); - main_block.operations.push(transpose_op); + // 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); + } } } } @@ -2802,10 +2978,6 @@ impl super::GraphConverter for CoremlMlProgramConverter { let input_name = operand_name(graph_info, input_operand_id); let transposed_input_name = format!("{}_nchw", input_name); - // Store the override mapping - operand_name_overrides - .insert(input_operand_id, transposed_input_name.clone()); - let mut transpose_inputs: HashMap = HashMap::new(); transpose_inputs .insert("x".to_string(), Self::create_name_argument(input_name)); @@ -2845,7 +3017,23 @@ impl super::GraphConverter for CoremlMlProgramConverter { vec![transpose_output_type], ); - main_block.operations.push(transpose_op); + // 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); + } } } } @@ -3700,13 +3888,12 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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"), - })?; + 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, @@ -3720,17 +3907,16 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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")); + 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, - )?; + let mil_op = + self.convert_operation_with_overrides(graph_info, op, &overrides)?; main_block.operations.push(mil_op); continue; } @@ -3740,6 +3926,17 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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) diff --git a/src/executors/coreml.rs b/src/executors/coreml.rs index 5ba998e0..e3025a28 100644 --- a/src/executors/coreml.rs +++ b/src/executors/coreml.rs @@ -629,8 +629,7 @@ fn convert_input_bytes(src: &[u8], src_dtype: DataType, target_code: i32, count: } (DataType::Float32, 16) => { // float32 → float16 - let src_f32 = - unsafe { std::slice::from_raw_parts(src.as_ptr() as *const f32, count) }; + 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()); @@ -639,8 +638,7 @@ fn convert_input_bytes(src: &[u8], src_dtype: DataType, target_code: i32, count: } (DataType::Float16, 32) => { // float16 → float32 - let src_f16 = - unsafe { std::slice::from_raw_parts(src.as_ptr() as *const u16, count) }; + 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()); @@ -649,8 +647,7 @@ fn convert_input_bytes(src: &[u8], src_dtype: DataType, target_code: i32, count: } (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 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()); @@ -729,7 +726,7 @@ unsafe fn extract_multiarray_bytes( /// 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 + 65600 | 4 => 4, // Int64 / Double → treat as Int64 65568 | 131104 | 32 => 32, // Float32 variants 65552 | 16 => 16, // Float16 variants 3 => 3, // Int32 From a2a2b82045bde1991f0f25dc48096da00247447f Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Mon, 13 Jul 2026 17:58:17 +0200 Subject: [PATCH 04/34] Fix interface and logic issues --- src/converters/coreml_mlprogram.rs | 370 ++++++++++++----- .../coreml_expected_failures.txt | 392 ------------------ 2 files changed, 276 insertions(+), 486 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 9c7c9ef2..3e6c1992 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1672,9 +1672,14 @@ impl CoremlMlProgramConverter { 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])); + // 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) @@ -1876,73 +1881,46 @@ impl CoremlMlProgramConverter { // 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 + // Build sorted axes vector (CoreML requires sorted axes; empty axes are handled + // as a special case in the main convert loop before reaching here). + let mut 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(); + axes_vec.sort_unstable(); - 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(), - }); - } - - // Add input operand (only x) + // 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) 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(), - }); + // 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)), + ); } - inputs.insert("gamma".to_string(), Self::create_argument(&input_names[1])); - } - - // 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(), - }); + if let Some(bias_id) = opts.bias { + inputs.insert( + "beta".to_string(), + Self::create_argument(&operand_name(graph, bias_id)), + ); } - inputs.insert("beta".to_string(), Self::create_argument(&input_names[2])); + inputs.insert( + "epsilon".to_string(), + Self::create_immediate_float(opts.epsilon as f32), + ); } - // Add axes parameter (REQUIRED by CoreML, must not be empty) + // 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), ); - - if let Some(opts) = options { - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(opts.epsilon as f32), - ); - } } // Batch/instance normalization (have mean, variance inputs) @@ -2010,46 +1988,19 @@ impl CoremlMlProgramConverter { 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() - ), - }); + 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)), + ); } - 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 let Some(bias_id) = opts.bias { + inputs.insert( + "beta".to_string(), + Self::create_argument(&operand_name(graph, bias_id)), + ); } - 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), @@ -2515,6 +2466,23 @@ impl CoremlMlProgramConverter { ); } + 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, .. } @@ -2524,7 +2492,6 @@ impl CoremlMlProgramConverter { | 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() { @@ -3516,7 +3483,11 @@ impl super::GraphConverter for CoremlMlProgramConverter { _ => (1.0, 1.0), }; - let has_bias = op.input_operands().len() >= 3; + 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; @@ -3607,7 +3578,7 @@ impl super::GraphConverter for CoremlMlProgramConverter { } if has_bias { - let c_operand_id = op.input_operands()[2]; + 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) @@ -3883,6 +3854,217 @@ impl super::GraphConverter for CoremlMlProgramConverter { continue; } + // 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, .. } => { + 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(); + axes_vec.is_empty() + } + _ => false, + }; + + if axes_empty { + if op.input_operands().is_empty() || op.output_operand().is_none() { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "layerNormalization requires input and output operand" + .to_string(), + }); + } + + 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 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 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(), + }, + ), + ), + }; + + 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 { + 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( + "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 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), + }; + + 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(), + }); + } + + 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 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 band_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 band_name = format!("{}_triangular_band", output_name); + + // 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 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; + } + } + // `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" { diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 3541b648..a7dd13f8 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -88,15 +88,12 @@ coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_options_a 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 @@ -104,9 +101,6 @@ 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 @@ -116,11 +110,7 @@ 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 @@ -177,8 +167,6 @@ coreml::conv_transpose2d::convTranspose2d_float16_4D_input_and_filter_tensors_op 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 @@ -194,8 +182,6 @@ coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_odd_size_with_float 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 @@ -221,40 +207,6 @@ coreml::dequantizeLinear::quantizeLinear_then_dequantizeLinear_with_different_fl 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 @@ -262,11 +214,9 @@ 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 @@ -290,14 +240,9 @@ 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 @@ -316,91 +261,8 @@ 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_ @@ -440,9 +302,7 @@ coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_ 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 @@ -514,149 +374,19 @@ 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_ @@ -740,45 +470,9 @@ 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 @@ -789,19 +483,8 @@ 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 @@ -844,52 +527,23 @@ 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__ @@ -900,10 +554,8 @@ coreml::quantizeLinear::quantizeLinear_float16_tensor_with_int4_zeroPoint_which_ 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__ @@ -1056,48 +708,4 @@ 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 From ff7debdac6575edb18696d4abe6f76429ae1353f Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Mon, 13 Jul 2026 18:31:46 +0200 Subject: [PATCH 05/34] Fix dtype and constraint fixes --- src/converters/coreml_mlprogram.rs | 158 +++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 40 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 3e6c1992..78c54654 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -561,6 +561,13 @@ impl CoremlMlProgramConverter { values: constant_data.data.clone().into(), })), }, + crate::graph::DataType::Uint32 + | crate::graph::DataType::Int64 + | crate::graph::DataType::Uint64 => TensorValue { + value: Some(tensor_value::Value::Bytes(tensor_value::RepeatedBytes { + values: constant_data.data.clone().into(), + })), + }, _ => { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), @@ -1454,15 +1461,34 @@ impl CoremlMlProgramConverter { ); } - // Quantization operations: input, scale, zero_point - Operation::DequantizeLinear { .. } | Operation::QuantizeLinear { .. } - if input_names.len() >= 3 => { + // 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])); - inputs.insert( - "zero_point".to_string(), - Self::create_argument(&input_names[2]), - ); + if input_names.len() >= 3 { + inputs.insert( + "zero_point".to_string(), + Self::create_argument(&input_names[2]), + ); + } + // When scale is rank-1 (per-channel), CoreML requires an explicit axis. + // Find which dimension of the input tensor the scale elements correspond to. + if let Some(scale_op) = graph.operand(*scale_id) { + if scale_op.descriptor.shape.len() == 1 { + let scale_len = scale_op.descriptor.static_or_max_shape().first().copied().unwrap_or(0); + let axis = graph.operand(*inp_id) + .and_then(|inp| { + inp.descriptor.static_or_max_shape() + .iter() + .position(|&d| d == scale_len) + .map(|i| i as u32) + }) + .unwrap_or(1); + inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); + } + } } // Specialized activation: prelu - x, slope (two inputs) @@ -1831,12 +1857,15 @@ impl CoremlMlProgramConverter { Self::create_immediate_int_array(window_dimensions), ); } - if !opts.strides.is_empty() { - inputs.insert( - "strides".to_string(), - Self::create_immediate_int_array(&opts.strides), - ); - } + let strides = if opts.strides.is_empty() { + vec![1u32, 1] + } else { + opts.strides.clone() + }; + inputs.insert( + "strides".to_string(), + Self::create_immediate_int_array(&strides), + ); if !opts.dilations.is_empty() && opts.dilations.iter().any(|&d| d != 1) { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), @@ -1876,6 +1905,10 @@ impl CoremlMlProgramConverter { "pad_type".to_string(), Self::create_immediate_string("valid"), ); + inputs.insert( + "strides".to_string(), + Self::create_immediate_int_array(&[1u32, 1]), + ); } } @@ -1909,10 +1942,18 @@ impl CoremlMlProgramConverter { Self::create_argument(&operand_name(graph, bias_id)), ); } - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(opts.epsilon as f32), - ); + 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; @@ -1978,10 +2019,18 @@ impl CoremlMlProgramConverter { } 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), - ); + 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, .. } => { @@ -2001,10 +2050,18 @@ impl CoremlMlProgramConverter { Self::create_argument(&operand_name(graph, bias_id)), ); } - inputs.insert( - "epsilon".to_string(), - Self::create_immediate_float(opts.epsilon as f32), - ); + 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); } } @@ -2220,15 +2277,27 @@ impl CoremlMlProgramConverter { Self::create_immediate_string(coreml_mode), ); - // constant_val: always emit (required even for non-constant modes) + // 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); - inputs.insert( - "constant_val".to_string(), - Self::create_immediate_float(constant_val as f32), - ); + 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 { .. } => { @@ -2302,9 +2371,14 @@ impl CoremlMlProgramConverter { MLOperandDataType::Int32 => "int32", MLOperandDataType::Uint32 => "uint32", MLOperandDataType::Int8 => "int8", - MLOperandDataType::Uint8 => "int8", + MLOperandDataType::Uint8 => "uint8", MLOperandDataType::Int64 => "int64", - MLOperandDataType::Uint64 => "uint64", + 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(), @@ -2338,12 +2412,16 @@ impl CoremlMlProgramConverter { inputs.insert("axis".to_string(), Self::create_immediate_int(opts.axis)); } - // CoreML requires explicit mode even though "update" is the only supported value. + // 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 + // 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( @@ -2354,6 +2432,7 @@ impl CoremlMlProgramConverter { "updates".to_string(), Self::create_argument(&input_names[2]), ); + inputs.insert("mode".to_string(), Self::create_immediate_string("update")); } Operation::Tile { repetitions, .. } => { @@ -2362,12 +2441,11 @@ impl CoremlMlProgramConverter { inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); } - if !repetitions.is_empty() { - inputs.insert( - "reps".to_string(), - Self::create_immediate_int_array(repetitions), - ); - } + // 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, .. } => { From f297ca50854f81b8ae766d1a310b954f9739b9f2 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 09:23:56 +0200 Subject: [PATCH 06/34] Add missing operations --- src/converters/coreml_mlprogram.rs | 305 +++++++++++++++++- .../coreml_expected_failures.txt | 85 ----- 2 files changed, 298 insertions(+), 92 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 78c54654..aa4338d8 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -204,6 +204,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. @@ -561,9 +572,20 @@ impl CoremlMlProgramConverter { values: constant_data.data.clone().into(), })), }, - crate::graph::DataType::Uint32 - | crate::graph::DataType::Int64 - | crate::graph::DataType::Uint64 => TensorValue { + crate::graph::DataType::Int64 => { + // Reinterpret raw bytes as Vec using little-endian byte order. + let data = &constant_data.data; + let values: Vec = data + .chunks_exact(8) + .map(|chunk| i64::from_le_bytes(chunk.try_into().unwrap())) + .collect(); + TensorValue { + value: Some(tensor_value::Value::LongInts( + tensor_value::RepeatedLongInts { values }, + )), + } + } + crate::graph::DataType::Uint32 | crate::graph::DataType::Uint64 => TensorValue { value: Some(tensor_value::Value::Bytes(tensor_value::RepeatedBytes { values: constant_data.data.clone().into(), })), @@ -1290,6 +1312,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(), @@ -1485,7 +1514,7 @@ impl CoremlMlProgramConverter { .position(|&d| d == scale_len) .map(|i| i as u32) }) - .unwrap_or(1); + .unwrap_or(0); inputs.insert("axis".to_string(), Self::create_immediate_int(axis)); } } @@ -1856,6 +1885,21 @@ impl CoremlMlProgramConverter { "kernel_sizes".to_string(), Self::create_immediate_int_array(window_dimensions), ); + } else { + // window_dimensions is None or empty: infer kernel_sizes from input shape. + // For NCHW (guaranteed above), shape = [N, C, H, W], spatial dims at [2], [3]. + if let Some(&input_id) = op.input_operands().first() { + if let Some(input_op) = graph.operand(input_id) { + let shape = input_op.descriptor.static_or_max_shape(); + if shape.len() >= 4 { + let kernel_sizes = vec![shape[2], shape[3]]; + inputs.insert( + "kernel_sizes".to_string(), + Self::create_immediate_int_array(&kernel_sizes), + ); + } + } + } } let strides = if opts.strides.is_empty() { vec![1u32, 1] @@ -1886,10 +1930,11 @@ impl CoremlMlProgramConverter { Self::create_immediate_string("custom"), ); } else { - // CoreML avg_pool/max_pool requires explicit pad even when zero. + // CoreML avg_pool/max_pool requires pad to be exactly 4 elements + // (2 * spatial_rank) even when all zeros. inputs.insert( "pad".to_string(), - Self::create_immediate_int_array(&[0u32; 0]), + Self::create_immediate_int_array(&[0u32, 0, 0, 0]), ); inputs.insert( "pad_type".to_string(), @@ -1897,9 +1942,23 @@ impl CoremlMlProgramConverter { ); } } else { + // No opts: infer kernel_sizes from input shape (NCHW: spatial dims at [2], [3]). + if let Some(&input_id) = op.input_operands().first() { + if let Some(input_op) = graph.operand(input_id) { + let shape = input_op.descriptor.static_or_max_shape(); + if shape.len() >= 4 { + let kernel_sizes = vec![shape[2], shape[3]]; + inputs.insert( + "kernel_sizes".to_string(), + Self::create_immediate_int_array(&kernel_sizes), + ); + } + } + } + // CoreML requires pad to be exactly 4 elements even when all zeros. inputs.insert( "pad".to_string(), - Self::create_immediate_int_array(&[0u32; 0]), + Self::create_immediate_int_array(&[0u32, 0, 0, 0]), ); inputs.insert( "pad_type".to_string(), @@ -2433,6 +2492,10 @@ impl CoremlMlProgramConverter { 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, .. } => { @@ -2589,6 +2652,42 @@ impl CoremlMlProgramConverter { } } + 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)), + ); + } + _ => {} } @@ -3100,6 +3199,8 @@ impl super::GraphConverter for CoremlMlProgramConverter { | "logicalor" | "logicalxor" | "notequal" + | "isnan" + | "isinfinite" ) { use crate::protos::coreml::mil_spec::DataType as MilDataType; @@ -3203,6 +3304,74 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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 mut gt_inputs = HashMap::new(); + gt_inputs.insert("x".to_string(), Self::create_name_argument(abs_name)); + gt_inputs.insert("y".to_string(), Self::create_immediate_float(max_val)); + 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, @@ -4183,6 +4352,128 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // 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: "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(input_id) + .ok_or_else(|| GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!("resample2d input operand {input_id} not found"), + })?; + + let opts = match op { + Operation::Resample2d { options, .. } => options.clone().unwrap_or_default(), + _ => Default::default(), + }; + + 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() + ), + }); + } + + // Determine scale factors for height (dim 2) and width (dim 3). + let (scale_h, scale_w) = if !opts.scales.is_empty() { + let sh = if opts.scales.len() > 0 { + opts.scales[0] + } else { + 1.0 + }; + let sw = if opts.scales.len() > 1 { + opts.scales[1] + } else { + 1.0 + }; + (sh, sw) + } else if let Some(sizes) = &opts.sizes { + if sizes.len() >= 2 { + let input_h = input_shape[2] as f32; + let input_w = input_shape[3] as f32; + if input_h == 0.0 || input_w == 0.0 { + return Err(GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "resample2d: input spatial dimensions are zero".to_string(), + }); + } + (sizes[0] as f32 / input_h, sizes[1] as f32 / input_w) + } else { + (1.0, 1.0) + } + } 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 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 _ = output_name; + + let mut resample_inputs: HashMap = HashMap::new(); + resample_inputs.insert("x".to_string(), Self::create_name_argument(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") { + resample_inputs.insert( + "align_corners".to_string(), + Self::create_immediate_bool(false), + ); + resample_inputs.insert( + "half_pixel_centers".to_string(), + Self::create_immediate_bool(false), + ); + } + + main_block.operations.push(Self::create_mil_operation( + mil_op_name, + resample_inputs, + 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; + } + let mil_op = self.convert_operation_with_overrides(graph_info, op, &operand_name_overrides)?; main_block.operations.push(mil_op); diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index a7dd13f8..1f719abc 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -30,43 +30,27 @@ 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 @@ -89,12 +73,9 @@ coreml::batch_normalization_constant::batchNormalization_float16_2D_constant_ten 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_uint32 -coreml::cast::cast_float16_4D_tensor_to_uint8 coreml::cast::cast_float32_4D_tensor_to_int64 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_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 @@ -103,7 +84,6 @@ coreml::cast::cast_int64_4D_tensor_to_uint32 coreml::cast::cast_int64_4D_tensor_to_uint8 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 @@ -194,7 +174,6 @@ coreml::dequantizeLinear::dequantizeLinear_uint4_3D_tensor_with_float16_3D_scale 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 @@ -203,7 +182,6 @@ coreml::dequantizeLinear::dequantizeLinear_with_float32_3D_scale_as_an_intermedi 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 @@ -223,20 +201,12 @@ coreml::gatherElements::gatherElements_float32_1D_input_and_int32_out-of-bounds_ 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 @@ -308,32 +278,6 @@ 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 @@ -430,31 +374,19 @@ coreml::lstm_cell::lstmCell_float32_tensors_with_options_peepholeWeight_and_opti 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 @@ -474,15 +406,8 @@ coreml::neg::neg_int64_4D_tensor coreml::neg::neg_int8_4D_tensor coreml::not_equal::notEqual_float16_1D_constant_tensors 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_4D_tensor_options_mode__edge_ coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ coreml::pad::pad_int32_2D_tensor_default_options @@ -554,7 +479,6 @@ coreml::quantizeLinear::quantizeLinear_float16_tensor_with_int4_zeroPoint_which_ 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_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_with_int4_zeroPoint_which_has_even_size coreml::quantizeLinear::quantizeLinear_float32_2D_tensor_with_int4_zeroPoint_with_block_size____3__2_ @@ -634,17 +558,12 @@ 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 @@ -658,11 +577,7 @@ 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 From d6d1b9fe2b620267694be686f9fb70f6960036a4 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 09:50:49 +0200 Subject: [PATCH 07/34] Fix default axes and transposes --- src/converters/coreml_mlprogram.rs | 411 +++++++++++++++++- .../coreml_expected_failures.txt | 47 -- 2 files changed, 389 insertions(+), 69 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index aa4338d8..352ada52 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1815,9 +1815,10 @@ impl CoremlMlProgramConverter { | 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 + // 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, so we proceed unconditionally. + let _layout = pool_opts .as_ref() .map(|o| { if o.layout.is_empty() { @@ -1827,16 +1828,6 @@ impl CoremlMlProgramConverter { } }) .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 @@ -1975,11 +1966,24 @@ impl CoremlMlProgramConverter { 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). - let mut 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(); + // 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: default to last axis of the input operand + 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 > 0 { + vec![(input_rank as i32) - 1] + } else { + vec![0] + } + }; axes_vec.sort_unstable(); // Add input operand (only x; scale/bias come from options, not input_operands) @@ -4109,12 +4113,12 @@ impl super::GraphConverter for CoremlMlProgramConverter { if op_type_lower == "layernormalization" { let axes_empty = match &op { Operation::LayerNormalization { options, .. } => { - let axes_vec: Vec = options + // None means "default to last axis" (not empty) + options .as_ref() .and_then(|o| o.axes.as_ref()) - .map(|ax| ax.iter().map(|&u| u as i32).collect()) - .unwrap_or_default(); - axes_vec.is_empty() + .map(|ax| ax.is_empty()) + .unwrap_or(false) } _ => false, }; @@ -4474,6 +4478,369 @@ impl super::GraphConverter for CoremlMlProgramConverter { continue; } + // Special handling for argmax/argmin: CoreML outputs int32 but WebNN declares int64. + // Emit with int32 intermediate output and cast to int64 when needed. + 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), + })?; + if output_operand.descriptor.data_type == DataType::Int64 { + let (output_name, output_type) = + Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + 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 input_names = + Self::input_names_for_operation(graph_info, op, &operand_name_overrides); + 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, + "int64", + )); + continue; + } + } + + // 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" { + let pool_layout = match op { + Operation::AveragePool2d { options, .. } + | Operation::MaxPool2d { 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!("pool 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!("pool op '{}' has no output operand", op.op_type()), + })?; + + let input_operand = graph_info.operand(input_id).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + 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 = 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_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 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 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], + )); + + // 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); + + // 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], + )); + + // 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; + } + } + + // 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" + ); + 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)?; + + // 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(), + }, + ), + ), + }), + }; + + // 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], + )); + + // 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()); + + // 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(), + }, + ), + ), + }), + }; + + // 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); diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 1f719abc..4912dc77 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -50,8 +50,6 @@ coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRoundi 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::global_averagePool2d_float16_4D_tensor_options_layout_nhwc_and_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 @@ -307,7 +305,6 @@ coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputSizes_ignores_options 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 @@ -317,8 +314,6 @@ coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_epsilo 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_default_options coreml::layer_normalization::layerNormalization_float32_3D_tensor_default_options coreml::layer_normalization::layerNormalization_float32_4D_tensor_default_options coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_bias @@ -375,13 +370,11 @@ 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_options_dilations -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations_with_options_strides 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_float32_4D_tensor_options_dilations -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_dilations_with_options_strides 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 @@ -490,69 +483,29 @@ coreml::quantizeLinear::quantizeLinear_float32_tensor_with_int4_zeroPoint_which_ 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 From 6a68a5b21b1d319d73c368e9c3bfcce3965a4270 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 10:25:59 +0200 Subject: [PATCH 08/34] Fix prelu, is_inf and clamp --- src/converters/coreml_mlprogram.rs | 176 +++++++++++++++++- .../coreml_expected_failures.txt | 25 --- 2 files changed, 175 insertions(+), 26 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 352ada52..f85640c3 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -3368,9 +3368,14 @@ impl super::GraphConverter for CoremlMlProgramConverter { } 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(), Self::create_immediate_float(max_val)); + gt_inputs.insert("y".to_string(), max_val_arg); main_block.operations.push(Self::create_mil_operation( mil_ops::GREATER, gt_inputs, @@ -3395,6 +3400,98 @@ impl super::GraphConverter for CoremlMlProgramConverter { continue; } + // 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; + let mil_dtype = Self::mil_data_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, + ); + let alpha_name = Self::output_name_for_operand( + graph_info, + alpha_id, + &operand_name_overrides, + ); + + // 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, + )?; + let zero_arg = if is_float16 { + Self::create_immediate_float16(0.0) + } else { + Self::create_immediate_float(0.0) + }; + 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], + )); + + // 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], + )); + + // 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], + )); + + 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. @@ -3412,6 +3509,83 @@ impl super::GraphConverter for CoremlMlProgramConverter { _ => (f64::NEG_INFINITY, f64::INFINITY), }; + // 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. + { + 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; + let is_castable_int = matches!( + int_dtype, + DataType::Int8 | DataType::Uint8 | DataType::Int32 + ); + 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], + )); + + // Cast back to the original int type + let back_dtype = Self::cast_dtype_string_for_graph_type(int_dtype)?; + main_block.operations.push(Self::create_cast_operation( + clipped_name, + output_type, + back_dtype, + )); + continue; + } + } + } + } + if min_value == max_value { if op.input_operands().is_empty() || op.output_operand().is_none() { return Err(GraphError::ConversionFailed { diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 4912dc77..ab7fde37 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -98,7 +98,6 @@ 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 @@ -271,11 +270,6 @@ coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_ 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_layout__nhwc_ -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::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 @@ -411,9 +405,7 @@ coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no 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 @@ -426,21 +418,6 @@ 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 @@ -568,8 +545,6 @@ 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___ From ebfc082db1a85f4fe6dba447525e5bf64acc02a6 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 10:51:59 +0200 Subject: [PATCH 09/34] Cast and reshape fixes --- src/converters/coreml_mlprogram.rs | 471 +++++++++++++++++- .../coreml_expected_failures.txt | 13 - 2 files changed, 465 insertions(+), 19 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index f85640c3..099b4d04 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -3745,6 +3745,166 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // Relu with integer input: CoreML relu only accepts float (fp32/fp16). + // Cast int → fp32, apply relu, cast back. + 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 = matches!( + int_dtype, + DataType::Int8 | DataType::Uint8 | DataType::Int32 + ); + 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::cast_dtype_string_for_graph_type(&int_dtype)?; + main_block.operations.push(Self::create_cast_operation( + relu_name, + output_type, + back_dtype, + )); + continue; + } + } + } + } + + // 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 = matches!( + int_dtype, + DataType::Int8 | DataType::Uint8 | DataType::Int32 + ); + 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 original int type + let back_dtype = Self::cast_dtype_string_for_graph_type(&int_dtype)?; + main_block.operations.push(Self::create_cast_operation( + padded_name, + output_type, + back_dtype, + )); + 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 @@ -4652,8 +4812,265 @@ impl super::GraphConverter for CoremlMlProgramConverter { continue; } - // Special handling for argmax/argmin: CoreML outputs int32 but WebNN declares int64. - // Emit with int32 intermediate output and cast to int64 when needed. + // 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. + 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(); + if input_rank < 3 { + 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; + } + } + } + } + + // 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 = @@ -4669,9 +5086,50 @@ impl super::GraphConverter for CoremlMlProgramConverter { format: "coreml_mlprogram".to_string(), reason: format!("Output operand {} not found", output_id), })?; - if output_operand.descriptor.data_type == DataType::Int64 { + + 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 + ) + }) + .unwrap_or(false); + let output_is_int64 = output_operand.descriptor.data_type == DataType::Int64; + + if input_needs_float_cast || output_is_int64 { let (output_name, output_type) = Self::create_output_value(graph_info, output_id, &operand_name_overrides)?; + + // Optional: cast integer input 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; + } + } + } + + // Apply argmax/argmin (CoreML always produces int32) let int32_name = format!("{}_int32", output_name); let int32_type = Self::create_value_with_mil_type( graph_info, @@ -4680,8 +5138,6 @@ impl super::GraphConverter for CoremlMlProgramConverter { MilDataType::Int32 as i32, )?; let mil_op_type = self.get_mil_op_type(op.op_type())?; - let input_names = - Self::input_names_for_operation(graph_info, op, &operand_name_overrides); let arg_op = self.convert_operation_with_input_names_and_outputs( graph_info, op, @@ -4690,10 +5146,13 @@ impl super::GraphConverter for CoremlMlProgramConverter { mil_op_type, )?; main_block.operations.push(arg_op); + + // Cast int32 to the declared output type + let final_dtype = if output_is_int64 { "int64" } else { "int32" }; main_block.operations.push(Self::create_cast_operation( int32_name, output_type, - "int64", + final_dtype, )); continue; } diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index ab7fde37..f9be3518 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -17,7 +17,6 @@ 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_ @@ -28,7 +27,6 @@ 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_options_dilations coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_dilations_with_options_strides @@ -62,13 +60,10 @@ coreml::batch_normalization::batchNormalization_float16_4D_NHWC_tensor_all_optio 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_uint32 coreml::cast::cast_float32_4D_tensor_to_int64 @@ -157,8 +152,6 @@ coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_even_size_with_floa 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_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 @@ -397,10 +390,7 @@ coreml::pad::pad_float16_4D_tensor_options_mode__edge_ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ coreml::pad::pad_float32_4D_tensor_options_mode__edge_ coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ -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 @@ -436,7 +426,6 @@ coreml::qdq_subgraph::quantized_reduceSum coreml::qdq_subgraph::quantized_resample2d 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_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_with_int4_zeroPoint_which_has_even_size @@ -448,7 +437,6 @@ coreml::quantizeLinear::quantizeLinear_float16_tensor_with_int4_zeroPoint_which_ 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_1D_tensor_with_uint4_zeroPoint_with_block_size___3 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_ @@ -487,7 +475,6 @@ 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_upsample__float32_4D_tensor_explicit_options_axes__3__2_ coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_axes__0__1_ coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_axes__1__0_ From 4670dabc19f2912c1f0f3d78169d7af52edbcb1e Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 11:04:04 +0200 Subject: [PATCH 10/34] Upgrade weight file to blob format v2 --- src/converters/coreml_mlprogram.rs | 113 +++---- src/converters/weight_file_builder.rs | 297 +++++++++--------- .../coreml_expected_failures.txt | 185 ----------- 3 files changed, 198 insertions(+), 397 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 099b4d04..a66564dc 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -530,10 +530,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, )?; @@ -5765,11 +5764,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" @@ -5777,37 +5774,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] @@ -5822,11 +5814,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" @@ -5834,18 +5824,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] @@ -5931,23 +5913,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] 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/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index f9be3518..c3f7c9a8 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -1,13 +1,10 @@ # 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 @@ -17,7 +14,6 @@ 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::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 @@ -27,7 +23,6 @@ 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::averagePool2d::averagePool2d_float16_4D_constant_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_nhwc @@ -48,22 +43,12 @@ coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRoundi 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::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_2D_tensor__mean_and_variance_are_non-constant__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::cast::cast_float16_4D_tensor_to_int64 coreml::cast::cast_float16_4D_tensor_to_uint32 coreml::cast::cast_float32_4D_tensor_to_int64 @@ -85,8 +70,6 @@ coreml::cast::cast_uint32_4D_tensor_to_int8 coreml::cast::cast_uint32_4D_tensor_to_uint8 coreml::cast::cast_uint8_4D_tensor_to_int64 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 @@ -97,47 +80,19 @@ 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_options_inputLayout_nhwc coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors_options_inputLayout_nhwc_options_filterLayout_hwoi @@ -145,7 +100,6 @@ coreml::conv_transpose2d::convTranspose2d_float32_4D_input_and_filter_tensors_op 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 @@ -157,29 +111,18 @@ coreml::dequantizeLinear::dequantizeLinear_int8_2D_tensor_with_float32_2D_scale_ 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_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::div::div_float16_1D_constant_tensors -coreml::elu::elu_float16_1D_constant_tensor_default_options -coreml::equal::equal_float16_1D_constant_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_float32_1D_tensor_and_int64_0D_scalar_indices_default_options @@ -188,7 +131,6 @@ coreml::gather::gather_float32_2D_tensor_and_int32_0D_out-of-bound_positive_indi 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_4D_input_and_1D_int64_indices @@ -199,54 +141,14 @@ coreml::gatherND::gatherND_float32_2D_input_and_2D_out-of-bounds_indices coreml::gatherND::gatherND_float32_4D_input_and_1D_int64_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_2nd_float16_input_tensor -coreml::gemm::gemm_both_negative_options_alpha_and_3rd_float16_input_tensor__options_c_ -coreml::gemm::gemm_both_negative_options_beta_and_3rd_float16_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::greater::greater_float16_1D_constant_tensors -coreml::greater_or_equal::greaterOrEqual_float16_1D_constant_tensors 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_ @@ -255,12 +157,8 @@ 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_layout__nhwc_ coreml::l2Pool2d::l2Pool2d_float16_4D_constant_tensor_all_positive_default_options @@ -277,42 +175,25 @@ coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_outputSizes_ignores_options 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_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_3D_tensor_default_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_5D_tensor_default_options -coreml::leaky_relu::leakyRelu_float16_1D_constant_tensor_default_options -coreml::lesser::lesser_float16_1D_constant_tensors -coreml::lesser_or_equal::lesserOrEqual_float16_1D_constant_tensors -coreml::linear::linear_float16_1D_constant_tensor_default_options -coreml::log::log_float16_positive_1D_constant_tensor 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_ @@ -327,16 +208,6 @@ coreml::lstm::lstm_float16_tensors_steps_1_with_options_bias__options_recurrentB 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 @@ -353,9 +224,7 @@ coreml::lstm_cell::lstmCell_float32_tensors_with_options_bias__options_recurrent 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_options_dilations coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_layout_nhwc coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_outputShapeRounding_ceil @@ -367,7 +236,6 @@ 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_outputSizes_ignores_options_outputShapeRounding_ceil coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -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 @@ -379,35 +247,15 @@ 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_int64_4D_tensor coreml::neg::neg_int8_4D_tensor -coreml::not_equal::notEqual_float16_1D_constant_tensors -coreml::pad::pad_float16_1D_constant_tensor_default_options coreml::pad::pad_float16_4D_tensor_options_mode__edge_ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ coreml::pad::pad_float32_4D_tensor_options_mode__edge_ coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint 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_1D_constant_tensors -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_int64_2D_constant_tensors coreml::qdq_subgraph::dequantizeLinear_-__conv2d_-__clamp_-__quantizeLinear coreml::qdq_subgraph::dequantizeLinear_-__conv2d_-__relu_-__quantizeLinear @@ -417,24 +265,19 @@ coreml::qdq_subgraph::quantized_conv2d coreml::qdq_subgraph::quantized_conv2d_with_padding coreml::qdq_subgraph::quantized_convTranspose2d coreml::qdq_subgraph::quantized_gather -coreml::qdq_subgraph::quantized_gemm_with_bias coreml::qdq_subgraph::quantized_maxPool2d 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::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor 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_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_1D_tensor_with_uint4_zeroPoint_with_block_size___3 @@ -447,32 +290,20 @@ coreml::quantizeLinear::quantizeLinear_float32_tensor_with_int4_zeroPoint_which_ 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_1D_constant_tensor_all_positive_default_options 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_1D_constant_tensor_all_positive_default_options coreml::reduce_l2::reduceL2_float32_1D_constant_tensor_empty_axes -coreml::reduce_log_sum::reduceLogSum_float16_1D_constant_tensor_all_non-negative_default_options 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_1D_constant_tensor_all_positive_default_options coreml::reduce_log_sum_exp::reduceLogSumExp_float32_1D_constant_tensor_empty_axes -coreml::reduce_max::reduceMax_float16_1D_constant_tensor_default_options coreml::reduce_max::reduceMax_float32_1D_constant_tensor_empty_axes -coreml::reduce_mean::reduceMean_float16_1D_constant_tensor_all_positive_default_options coreml::reduce_mean::reduceMean_float32_1D_constant_tensor_empty_axes -coreml::reduce_min::reduceMin_float16_1D_constant_tensor_default_options coreml::reduce_min::reduceMin_float32_1D_constant_tensor_empty_axes -coreml::reduce_product::reduceProduct_float16_1D_constant_tensor_default_options coreml::reduce_product::reduceProduct_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum::reduceSum_float16_1D_constant_tensor_all_positive_default_options coreml::reduce_sum::reduceSum_float32_1D_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_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::resample2d::resample2d_upsample__float32_4D_tensor_explicit_options_axes__3__2_ @@ -485,7 +316,6 @@ coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_sizes_ignored 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 @@ -495,11 +325,8 @@ coreml::scatterElements::scatterElements_float32_tensors_along_axis_0_and_consta coreml::scatterElements::scatterElements_float32_tensors_along_axis_1 coreml::scatterElements::scatterElements_float32_tensors_along_axis_1_and_constant_indices coreml::scatterND::scatterND_2D_int8_tensors_with_index_out_of_bound -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 @@ -507,12 +334,6 @@ 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 @@ -526,16 +347,10 @@ 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::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::where::where_float16_1D_constant_tensors From 454f6cd11440907aa50e18ac37432d3bd5d6d8ec Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 11:29:43 +0200 Subject: [PATCH 11/34] Fix conv2d nhwc output shape --- src/converters/coreml_mlprogram.rs | 142 +++- .../coreml_expected_failures.txt | 722 +++++++++--------- 2 files changed, 503 insertions(+), 361 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index a66564dc..5c24b33c 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1501,11 +1501,15 @@ impl CoremlMlProgramConverter { Self::create_argument(&input_names[2]), ); } - // When scale is rank-1 (per-channel), CoreML requires an explicit axis. - // Find which dimension of the input tensor the scale elements correspond to. + // 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. if let Some(scale_op) = graph.operand(*scale_id) { - if scale_op.descriptor.shape.len() == 1 { - let scale_len = scale_op.descriptor.static_or_max_shape().first().copied().unwrap_or(0); + let scale_shape = scale_op.descriptor.static_or_max_shape(); + let scale_len = scale_shape.first().copied().unwrap_or(0); + let is_per_channel = scale_op.descriptor.shape.len() == 1 && scale_len > 1; + if is_per_channel { let axis = graph.operand(*inp_id) .and_then(|inp| { inp.descriptor.static_or_max_shape() @@ -2859,19 +2863,28 @@ impl super::GraphConverter for CoremlMlProgramConverter { if matches!(op_lower.as_str(), "quantizelinear" | "dequantizelinear") { let input_ids = op.input_operands(); // 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) { - if param_operand.descriptor.shape.len() > 1 { + 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. - // For dequantize: zp type = input type. - let zp_expected_type = if op_lower == "quantizelinear" { + // 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()) @@ -2882,9 +2895,26 @@ impl super::GraphConverter for CoremlMlProgramConverter { .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); + } + } + } + } } } @@ -5157,6 +5187,102 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // 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( + "transpose", + post_tp_inputs, + 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; + } + } + // 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). diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index c3f7c9a8..336b25c1 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -1,356 +1,372 @@ # 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_6D_tensor -coreml::abs::abs_int32_4D_tensor -coreml::abs::abs_int64_4D_tensor -coreml::abs::abs_int8_4D_tensor -coreml::add::add_float32_with_special_character_names -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::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::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_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_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_float32_4D_tensor_options_dilations -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_dilations_with_options_strides -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_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::batch_normalization::batchNormalization_float16_2D_tensor__mean_and_variance_are_non-constant__default_options -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_float32_2D_tensor__mean_and_variance_are_non-constant__default_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_all_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_options_axis_3 -coreml::cast::cast_float16_4D_tensor_to_int64 -coreml::cast::cast_float16_4D_tensor_to_uint32 -coreml::cast::cast_float32_4D_tensor_to_int64 -coreml::cast::cast_float32_4D_tensor_to_uint32 -coreml::cast::cast_int32_4D_tensor_to_int64 -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_int64 -coreml::cast::cast_int8_4D_tensor_to_uint32 -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_int64 -coreml::cast::cast_uint8_4D_tensor_to_uint32 -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::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_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_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::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_input_tensors_options_padding_is_the_same_upper_padding -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::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_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_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_tensor_with_float16_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::expand::expand_float32_0D_scalar_to_0D -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_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::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_3D_input_and_int32_negative_indices -coreml::gatherND::gatherND_float16_2D_input_and_2D_negative_indices -coreml::gatherND::gatherND_float16_4D_input_and_1D_int64_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_4D_input_and_1D_int64_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_uint32_indices -coreml::gatherND::gatherND_float32_rank-5_input_and_rank-5_indices -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_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_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::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_float32_with_special_character_names +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::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::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_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_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_float32_4D_tensor_options_dilations +coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_dilations_with_options_strides +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_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::batch_normalization::batchNormalization_float16_2D_tensor__mean_and_variance_are_non-constant__default_options +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_float32_2D_tensor__mean_and_variance_are_non-constant__default_options +coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_all_options +coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_options_axis_3 +coreml::cast::cast_float16_4D_tensor_to_int64 +coreml::cast::cast_float16_4D_tensor_to_uint32 +coreml::cast::cast_float32_4D_tensor_to_int64 +coreml::cast::cast_float32_4D_tensor_to_uint32 +coreml::cast::cast_int32_4D_tensor_to_int64 +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_int64 +coreml::cast::cast_int8_4D_tensor_to_uint32 +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_int64 +coreml::cast::cast_uint8_4D_tensor_to_uint32 +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::maxValue_as_-Infinity +coreml::clamp::minValue____maxValue +coreml::clamp::minValue_as_Infinity +coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 +coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ +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_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_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::expand::expand_float32_0D_scalar_to_0D +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_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::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_4D_input_and_1D_int64_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_4D_input_and_1D_int64_indices +coreml::gatherND::gatherND_float32_4D_input_and_1D_uint32_indices +coreml::gatherND::gatherND_float32_rank-5_input_and_rank-5_indices +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_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::instance_normalization::instanceNormalization_float16_4D_tensor_all_options -coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_layout__nhwc_ -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_all_options -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_options_layout__nhwc_ -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_tensor_options_dilations_with_options_strides -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_layout_nhwc -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::layer_normalization::layerNormalization_float16_3D_tensor_default_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_5D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_3D_tensor_default_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_scale -coreml::layer_normalization::layerNormalization_float32_5D_tensor_default_options -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_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_uint8_4D_tensors -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations -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_float32_4D_tensor_options_dilations -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_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -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 -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_uint32_4D_tensors -coreml::neg::neg_int64_4D_tensor -coreml::neg::neg_int8_4D_tensor -coreml::pad::pad_float16_4D_tensor_options_mode__edge_ -coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ -coreml::pad::pad_float32_4D_tensor_options_mode__edge_ -coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ -coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint -coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op -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_averagePool2d -coreml::qdq_subgraph::quantized_conv2d -coreml::qdq_subgraph::quantized_conv2d_with_padding -coreml::qdq_subgraph::quantized_convTranspose2d -coreml::qdq_subgraph::quantized_gather -coreml::qdq_subgraph::quantized_maxPool2d -coreml::qdq_subgraph::quantized_reduceMax -coreml::qdq_subgraph::quantized_reduceMean -coreml::qdq_subgraph::quantized_reduceMin -coreml::qdq_subgraph::quantized_resample2d -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor -coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -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_uint4_zeroPoint_which_has_even_size -coreml::quantizeLinear::quantizeLinear_float16_tensor_with_uint4_zeroPoint_which_has_odd_size -coreml::quantizeLinear::quantizeLinear_float32_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -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::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_float32_1D_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_float32_1D_constant_tensor_empty_axes -coreml::reduce_max::reduceMax_float32_1D_constant_tensor_empty_axes -coreml::reduce_mean::reduceMean_float32_1D_constant_tensor_empty_axes -coreml::reduce_min::reduceMin_float32_1D_constant_tensor_empty_axes -coreml::reduce_product::reduceProduct_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum::reduceSum_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float16_1D_tensor_with_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float32_1D_tensor_with_empty_axes -coreml::relu::relu_int32_4D_tensor -coreml::relu::relu_int64_4D_tensor -coreml::resample2d::resample2d_upsample__float32_4D_tensor_explicit_options_axes__3__2_ -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_options_mode__linear_ -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::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_2D_int8_tensors_with_index_out_of_bound -coreml::sign::sign_int64_3D_tensor -coreml::sign::sign_int8_4D_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::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___softsign -coreml::subgraph::batchNormalization_options_axis_0____softmax -coreml::subgraph::batchNormalization_options_axis_0___gelu -coreml::subgraph::batchNormalization_options_axis_0___softplus -coreml::tile::tile_float32_0D_scalar_tensor_by_repetitions___ -coreml::tile::tile_uint32_2D_tensor -coreml::transpose::transpose_float32_0D_constant_tensor_default_options +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::instance_normalization::instanceNormalization_float16_4D_tensor_all_options +coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_layout__nhwc_ +coreml::instance_normalization::instanceNormalization_float32_4D_tensor_all_options +coreml::instance_normalization::instanceNormalization_float32_4D_tensor_options_layout__nhwc_ +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_3D_tensor_default_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_5D_tensor_default_options +coreml::layer_normalization::layerNormalization_float32_3D_tensor_default_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_5D_tensor_default_options +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_uint8_4D_tensors +coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations +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_float32_4D_tensor_options_dilations +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_outputSizes_ignores_options_outputShapeRounding_ceil +coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor +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 +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_uint32_4D_tensors +coreml::neg::neg_int64_4D_tensor +coreml::neg::neg_int8_4D_tensor +coreml::pad::pad_float16_4D_tensor_options_mode__edge_ +coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ +coreml::pad::pad_float32_4D_tensor_options_mode__edge_ +coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ +coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint +coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op +coreml::prelu::prelu_int64_2D_constant_tensors +coreml::qdq_subgraph::per-channel_quantized_gemm_with_non-zero_quantized_dimension_of_the_filter +coreml::qdq_subgraph::quantized_averagePool2d +coreml::qdq_subgraph::quantized_convTranspose2d +coreml::qdq_subgraph::quantized_maxPool2d +coreml::qdq_subgraph::quantized_resample2d +coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor +coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor +coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 +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_1D_tensor_with_uint4_zeroPoint_with_block_size___3 +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::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_float32_1D_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_float32_1D_constant_tensor_empty_axes +coreml::reduce_max::reduceMax_float32_1D_constant_tensor_empty_axes +coreml::reduce_mean::reduceMean_float32_1D_constant_tensor_empty_axes +coreml::reduce_min::reduceMin_float32_1D_constant_tensor_empty_axes +coreml::reduce_product::reduceProduct_float32_1D_constant_tensor_empty_axes +coreml::reduce_sum::reduceSum_float32_1D_constant_tensor_empty_axes +coreml::reduce_sum_square::reduceSumSquare_float16_1D_tensor_with_empty_axes +coreml::reduce_sum_square::reduceSumSquare_float32_1D_tensor_with_empty_axes +coreml::relu::relu_int32_4D_tensor +coreml::relu::relu_int64_4D_tensor +coreml::resample2d::resample2d_upsample__float32_4D_tensor_explicit_options_axes__3__2_ +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_options_mode__linear_ +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::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_2D_int8_tensors_with_index_out_of_bound +coreml::sign::sign_int64_3D_tensor +coreml::sign::sign_int8_4D_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::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::tile::tile_float32_0D_scalar_tensor_by_repetitions___ +coreml::tile::tile_uint32_2D_tensor +coreml::transpose::transpose_float32_0D_constant_tensor_default_options From a51121061b9bd62584400a5f5d3a4d59ae2f780c Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 11:51:34 +0200 Subject: [PATCH 12/34] Fix slice and axis mismatch --- src/converters/coreml_mlprogram.rs | 442 ++++++++++- .../coreml_expected_failures.txt | 722 +++++++++--------- 2 files changed, 757 insertions(+), 407 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 5c24b33c..ece188b7 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -160,6 +160,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"; @@ -1026,6 +1027,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); @@ -2151,27 +2167,55 @@ impl CoremlMlProgramConverter { 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])); } - 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 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, .. } => { @@ -3776,6 +3820,75 @@ impl super::GraphConverter for CoremlMlProgramConverter { // Relu with integer input: CoreML relu only accepts float (fp32/fp16). // Cast int → fp32, apply relu, cast back. + // sub / sign with int8/uint8: CoreML only supports int32/fp32/fp16. + // Cast inputs to int32, apply op, cast back to the original int type. + if matches!(op_type_lower.as_str(), "sub" | "sign") { + 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); + } + + // 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; + } + } + } + } + if op_type_lower == "relu" { if let (Some(&input_id), Some(output_id)) = (op.input_operands().first(), op.output_operand()) @@ -4756,30 +4869,129 @@ impl super::GraphConverter for CoremlMlProgramConverter { }); } - // Determine scale factors for height (dim 2) and width (dim 3). - let (scale_h, scale_w) = if !opts.scales.is_empty() { - let sh = if opts.scales.len() > 0 { - opts.scales[0] - } else { - 1.0 + // 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 sw = if opts.scales.len() > 1 { - opts.scales[1] - } else { - 1.0 + 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], + )); + + // 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; + + let (scale_h, scale_w) = 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 if let Some(sizes) = &opts.sizes { if sizes.len() >= 2 { - let input_h = input_shape[2] as f32; - let input_w = input_shape[3] as f32; - if input_h == 0.0 || input_w == 0.0 { + 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(), }); } - (sizes[0] as f32 / input_h, sizes[1] as f32 / input_w) + 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 { (1.0, 1.0) } @@ -4799,14 +5011,11 @@ impl super::GraphConverter for CoremlMlProgramConverter { mil_ops::UPSAMPLE_NEAREST_NEIGHBOR }; - 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 _ = output_name; - let mut resample_inputs: HashMap = HashMap::new(); - resample_inputs.insert("x".to_string(), Self::create_name_argument(input_name)); + 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), @@ -4829,9 +5038,28 @@ impl super::GraphConverter for CoremlMlProgramConverter { main_block.operations.push(Self::create_mil_operation( mil_op_name, resample_inputs, - vec![output_type], + 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) { @@ -5187,6 +5415,142 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // 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 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( + "transpose", + pre_tp, + vec![nchw_in_type], + )); + + // 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); + + // 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], + )); + + 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; + } + } + // 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 diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 336b25c1..61dc563d 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -1,372 +1,358 @@ # 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_6D_tensor -coreml::abs::abs_int32_4D_tensor -coreml::abs::abs_int64_4D_tensor -coreml::abs::abs_int8_4D_tensor -coreml::add::add_float32_with_special_character_names -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::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::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_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_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_float32_4D_tensor_options_dilations -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_dilations_with_options_strides -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_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::batch_normalization::batchNormalization_float16_2D_tensor__mean_and_variance_are_non-constant__default_options -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_float32_2D_tensor__mean_and_variance_are_non-constant__default_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_all_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_options_axis_3 -coreml::cast::cast_float16_4D_tensor_to_int64 -coreml::cast::cast_float16_4D_tensor_to_uint32 -coreml::cast::cast_float32_4D_tensor_to_int64 -coreml::cast::cast_float32_4D_tensor_to_uint32 -coreml::cast::cast_int32_4D_tensor_to_int64 -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_int64 -coreml::cast::cast_int8_4D_tensor_to_uint32 -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_int64 -coreml::cast::cast_uint8_4D_tensor_to_uint32 -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::maxValue_as_-Infinity -coreml::clamp::minValue____maxValue -coreml::clamp::minValue_as_Infinity -coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 -coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ -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_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_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::expand::expand_float32_0D_scalar_to_0D -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_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::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_4D_input_and_1D_int64_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_4D_input_and_1D_int64_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_uint32_indices -coreml::gatherND::gatherND_float32_rank-5_input_and_rank-5_indices -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::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_float32_with_special_character_names +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::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::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_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_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_float32_4D_tensor_options_dilations +coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_dilations_with_options_strides +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_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::batch_normalization::batchNormalization_float16_2D_tensor__mean_and_variance_are_non-constant__default_options +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_float32_2D_tensor__mean_and_variance_are_non-constant__default_options +coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_all_options +coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_options_axis_3 +coreml::cast::cast_float16_4D_tensor_to_int64 +coreml::cast::cast_float16_4D_tensor_to_uint32 +coreml::cast::cast_float32_4D_tensor_to_int64 +coreml::cast::cast_float32_4D_tensor_to_uint32 +coreml::cast::cast_int32_4D_tensor_to_int64 +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_int64 +coreml::cast::cast_int8_4D_tensor_to_uint32 +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_int64 +coreml::cast::cast_uint8_4D_tensor_to_uint32 +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::maxValue_as_-Infinity +coreml::clamp::minValue____maxValue +coreml::clamp::minValue_as_Infinity +coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 +coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ +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_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_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::expand::expand_float32_0D_scalar_to_0D +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_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::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_4D_input_and_1D_int64_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_4D_input_and_1D_int64_indices +coreml::gatherND::gatherND_float32_4D_input_and_1D_uint32_indices +coreml::gatherND::gatherND_float32_rank-5_input_and_rank-5_indices +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_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::instance_normalization::instanceNormalization_float16_4D_tensor_all_options -coreml::instance_normalization::instanceNormalization_float16_4D_tensor_options_layout__nhwc_ -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_all_options -coreml::instance_normalization::instanceNormalization_float32_4D_tensor_options_layout__nhwc_ -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_3D_tensor_default_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_5D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_3D_tensor_default_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_5D_tensor_default_options -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_uint8_4D_tensors -coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations -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_float32_4D_tensor_options_dilations -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_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor -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 -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_uint32_4D_tensors -coreml::neg::neg_int64_4D_tensor -coreml::neg::neg_int8_4D_tensor -coreml::pad::pad_float16_4D_tensor_options_mode__edge_ -coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ -coreml::pad::pad_float32_4D_tensor_options_mode__edge_ -coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ -coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint -coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op -coreml::prelu::prelu_int64_2D_constant_tensors -coreml::qdq_subgraph::per-channel_quantized_gemm_with_non-zero_quantized_dimension_of_the_filter -coreml::qdq_subgraph::quantized_averagePool2d -coreml::qdq_subgraph::quantized_convTranspose2d -coreml::qdq_subgraph::quantized_maxPool2d -coreml::qdq_subgraph::quantized_resample2d -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor -coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -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_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -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::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_float32_1D_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_float32_1D_constant_tensor_empty_axes -coreml::reduce_max::reduceMax_float32_1D_constant_tensor_empty_axes -coreml::reduce_mean::reduceMean_float32_1D_constant_tensor_empty_axes -coreml::reduce_min::reduceMin_float32_1D_constant_tensor_empty_axes -coreml::reduce_product::reduceProduct_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum::reduceSum_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float16_1D_tensor_with_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float32_1D_tensor_with_empty_axes -coreml::relu::relu_int32_4D_tensor -coreml::relu::relu_int64_4D_tensor -coreml::resample2d::resample2d_upsample__float32_4D_tensor_explicit_options_axes__3__2_ -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_options_mode__linear_ -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::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_2D_int8_tensors_with_index_out_of_bound -coreml::sign::sign_int64_3D_tensor -coreml::sign::sign_int8_4D_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::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::tile::tile_float32_0D_scalar_tensor_by_repetitions___ -coreml::tile::tile_uint32_2D_tensor -coreml::transpose::transpose_float32_0D_constant_tensor_default_options +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::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_3D_tensor_default_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_5D_tensor_default_options +coreml::layer_normalization::layerNormalization_float32_3D_tensor_default_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_5D_tensor_default_options +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_uint8_4D_tensors +coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations +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_float32_4D_tensor_options_dilations +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_outputSizes_ignores_options_outputShapeRounding_ceil +coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor +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 +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_uint32_4D_tensors +coreml::neg::neg_int64_4D_tensor +coreml::neg::neg_int8_4D_tensor +coreml::pad::pad_float16_4D_tensor_options_mode__edge_ +coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ +coreml::pad::pad_float32_4D_tensor_options_mode__edge_ +coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ +coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint +coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op +coreml::prelu::prelu_int64_2D_constant_tensors +coreml::qdq_subgraph::per-channel_quantized_gemm_with_non-zero_quantized_dimension_of_the_filter +coreml::qdq_subgraph::quantized_averagePool2d +coreml::qdq_subgraph::quantized_convTranspose2d +coreml::qdq_subgraph::quantized_maxPool2d +coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor +coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor +coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 +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_1D_tensor_with_uint4_zeroPoint_with_block_size___3 +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::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_float32_1D_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_float32_1D_constant_tensor_empty_axes +coreml::reduce_max::reduceMax_float32_1D_constant_tensor_empty_axes +coreml::reduce_mean::reduceMean_float32_1D_constant_tensor_empty_axes +coreml::reduce_min::reduceMin_float32_1D_constant_tensor_empty_axes +coreml::reduce_product::reduceProduct_float32_1D_constant_tensor_empty_axes +coreml::reduce_sum::reduceSum_float32_1D_constant_tensor_empty_axes +coreml::reduce_sum_square::reduceSumSquare_float16_1D_tensor_with_empty_axes +coreml::reduce_sum_square::reduceSumSquare_float32_1D_tensor_with_empty_axes +coreml::relu::relu_int32_4D_tensor +coreml::relu::relu_int64_4D_tensor +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__options_mode__linear_ +coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_scales_options_mode__linear_ +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::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_2D_int8_tensors_with_index_out_of_bound +coreml::sign::sign_int64_3D_tensor +coreml::sign::sign_int8_4D_tensor +coreml::slice::slicing_float32_0D_constant_tensor_with_empty_starts_and_sizes_should_be_a_no-op +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::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::tile::tile_float32_0D_scalar_tensor_by_repetitions___ +coreml::tile::tile_uint32_2D_tensor +coreml::transpose::transpose_float32_0D_constant_tensor_default_options From 4c59c2628ac864e493fe092ced5558a8e29dc49e Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 12:22:35 +0200 Subject: [PATCH 13/34] Fix pooling and normalization --- src/converters/coreml_mlprogram.rs | 534 +++++++++++++++++- .../coreml_expected_failures.txt | 16 - 2 files changed, 503 insertions(+), 47 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index ece188b7..42e871b1 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -72,6 +72,17 @@ fn convert_zp_bytes(src: &[u8], src_dtype: &DataType, tgt_dtype: &DataType) -> V } 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(), } } @@ -1897,12 +1908,19 @@ impl CoremlMlProgramConverter { ); } else { // window_dimensions is None or empty: infer kernel_sizes from input shape. - // For NCHW (guaranteed above), shape = [N, C, H, W], spatial dims at [2], [3]. + // Layout determines which dims are H and W: + // NCHW → [N, C, H, W]: H=dim2, W=dim3 + // NHWC → [N, H, W, C]: H=dim1, W=dim2 + let is_nhwc = opts.layout.eq_ignore_ascii_case("nhwc"); if let Some(&input_id) = op.input_operands().first() { if let Some(input_op) = graph.operand(input_id) { let shape = input_op.descriptor.static_or_max_shape(); if shape.len() >= 4 { - let kernel_sizes = vec![shape[2], shape[3]]; + let kernel_sizes = if is_nhwc { + vec![shape[1], shape[2]] + } else { + vec![shape[2], shape[3]] + }; inputs.insert( "kernel_sizes".to_string(), Self::create_immediate_int_array(&kernel_sizes), @@ -2962,6 +2980,25 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // 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); + } + } + } + } + } + // Add constant operands as const operations for (operand_id, constant_data) in &graph_info.constant_operand_ids_to_handles { let operand = @@ -5350,17 +5387,39 @@ impl super::GraphConverter for CoremlMlProgramConverter { .map(|inp| { matches!( inp.descriptor.data_type, - DataType::Int8 | DataType::Uint8 | DataType::Int32 + DataType::Int8 + | DataType::Uint8 + | DataType::Int32 + | DataType::Uint32 + | DataType::Int64 ) }) .unwrap_or(false); - let output_is_int64 = output_operand.descriptor.data_type == DataType::Int64; + // 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 + ); - if input_needs_float_cast || output_is_int64 { + 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)?; - // Optional: cast integer input to float32 + // 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() + }; + + // 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 { @@ -5386,35 +5445,448 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } - // Apply argmax/argmin (CoreML always produces 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); - - // Cast int32 to the declared output type - let final_dtype = if output_is_int64 { "int64" } else { "int32" }; - main_block.operations.push(Self::create_cast_operation( - int32_name, - output_type, - final_dtype, - )); + if output_needs_int32_proxy { + // 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; } } + // Gather: cast uint32/int64 indices to int32 (CoreML gather only accepts int32 and smaller). + if op_type_lower == "gather" { + use crate::protos::coreml::mil_spec::DataType as MilDataType; + if let Operation::Gather { .. } = op { + let indices_id = op.input_operands().get(1).copied(); + if let Some(idx_id) = indices_id { + if let Some(idx_op) = graph_info.operand(idx_id) { + let needs_cast = matches!( + idx_op.descriptor.data_type, + DataType::Int64 | DataType::Uint32 + ); + if needs_cast { + let output_id = op.output_operand().ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: "gather has no output".to_string(), + } + })?; + 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, + op.input_operands()[0], + &operand_name_overrides, + ); + let idx_name = Self::output_name_for_operand( + graph_info, + idx_id, + &operand_name_overrides, + ); + let casted_idx_name = format!("{}_idx_i32", output_name); + let casted_idx_type = Self::create_value_with_mil_type( + graph_info, + idx_id, + casted_idx_name.clone(), + MilDataType::Int32 as i32, + )?; + main_block.operations.push(Self::create_cast_operation( + idx_name, + casted_idx_type, + "int32", + )); + let axis = if let Operation::Gather { options, .. } = op { + options.as_ref().map(|o| o.axis).unwrap_or(0) + } else { + 0 + }; + 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(casted_idx_name), + ); + 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_ops::GATHER, + gather_inputs, + vec![output_type], + )); + continue; + } + } + } + } + } + + // 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 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. + let (mean_for_sub, var_for_div) = 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 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) + } else { + (mean_name, var_name) + }; + + // 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(), + }, + ), + ), + }), + } + }; + + // 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())], + )); + + // var_plus_eps = variance + epsilon + // variance has shape [C], result is also [C] + let var_op = graph_info.operand(variance_id_v).ok_or_else(|| { + GraphError::ConversionFailed { + format: "coreml_mlprogram".to_string(), + reason: format!( + "batchNorm variance {} not found", + variance_id_v + ), + } + })?; + let var_shape = &var_op.descriptor.shape; + let var_dims = Self::mil_dimensions_from_graph_shape(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())], + )); + + // 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())], + )); + + // 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())], + )); + + // 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 + }; + + 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. diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 61dc563d..9c990afa 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -25,7 +25,6 @@ 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::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_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 @@ -35,7 +34,6 @@ coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputSizes_ignor coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_padding 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_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 @@ -44,11 +42,7 @@ coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputSizes_ignor coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_padding coreml::batch_normalization::batchNormalization_float16_2D_tensor__mean_and_variance_are_non-constant__default_options -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_float32_2D_tensor__mean_and_variance_are_non-constant__default_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_all_options -coreml::batch_normalization::batchNormalization_float32_4D_NHWC_tensor_options_axis_3 coreml::cast::cast_float16_4D_tensor_to_int64 coreml::cast::cast_float16_4D_tensor_to_uint32 coreml::cast::cast_float32_4D_tensor_to_int64 @@ -106,15 +100,9 @@ coreml::dequantizeLinear::dequantizeLinear_with_float32_3D_scale_as_an_intermedi 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::expand::expand_float32_0D_scalar_to_0D -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_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::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_4D_input_and_1D_int64_indices @@ -242,12 +230,10 @@ coreml::lstm_cell::lstmCell_float32_tensors_with_options_bias__options_recurrent coreml::lstm_cell::lstmCell_float32_tensors_with_options_peepholeWeight_and_options_layout__ifgo_ coreml::max::max_uint8_4D_tensors coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations -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_float32_4D_tensor_options_dilations -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_outputSizes_ignores_options_outputShapeRounding_ceil @@ -274,9 +260,7 @@ coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op coreml::prelu::prelu_int64_2D_constant_tensors coreml::qdq_subgraph::per-channel_quantized_gemm_with_non-zero_quantized_dimension_of_the_filter -coreml::qdq_subgraph::quantized_averagePool2d coreml::qdq_subgraph::quantized_convTranspose2d -coreml::qdq_subgraph::quantized_maxPool2d coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 From d5d3a0007075f22a2016659c30518bcb1c836a66 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 13:15:20 +0200 Subject: [PATCH 14/34] Fix non-const mean --- src/backends/coreml.rs | 23 ++++- src/converters/coreml_mlprogram.rs | 96 ++++++++++++++----- .../coreml_expected_failures.txt | 32 ------- 3 files changed, 94 insertions(+), 57 deletions(-) diff --git a/src/backends/coreml.rs b/src/backends/coreml.rs index 319484b7..ff099957 100644 --- a/src/backends/coreml.rs +++ b/src/backends/coreml.rs @@ -267,7 +267,26 @@ 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 but CoreML produced int32 bytes + // (e.g. argmin/argmax proxy), zero-extend each 4-byte value to 8 bytes. + // Index values are always non-negative so zero-extension is correct. + use crate::operator_enums::MLOperandDataType; + let expanded: Option> = if data.len() * 2 == logical + && matches!(ml_tensor.descriptor().data_type(), MLOperandDataType::Int64) + { + let count = data.len() / 4; + let mut buf = vec![0u8; count * 8]; + for i in 0..count { + buf[i * 8..i * 8 + 4].copy_from_slice(&data[i * 4..i * 4 + 4]); + } + 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 +305,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 42e871b1..eaae5ef9 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1532,16 +1532,23 @@ impl CoremlMlProgramConverter { // 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(); - let scale_len = scale_shape.first().copied().unwrap_or(0); - let is_per_channel = scale_op.descriptor.shape.len() == 1 && scale_len > 1; + // 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 == scale_len) + .position(|&d| d == effective_len) .map(|i| i as u32) }) .unwrap_or(0); @@ -2881,6 +2888,10 @@ impl super::GraphConverter for CoremlMlProgramConverter { // 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 = @@ -5186,11 +5197,18 @@ impl super::GraphConverter for CoremlMlProgramConverter { // 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(); - if input_rank < 3 { + 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) @@ -5446,6 +5464,9 @@ impl super::GraphConverter for CoremlMlProgramConverter { } 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( @@ -5629,7 +5650,8 @@ impl super::GraphConverter for CoremlMlProgramConverter { // When axis is not the last dimension, reshape mean/variance for // correct broadcasting: [C] → [1, C, 1, 1, ...] with 1s elsewhere. - let (mean_for_sub, var_for_div) = if axis + // 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 { @@ -5644,6 +5666,10 @@ impl super::GraphConverter for CoremlMlProgramConverter { .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 { @@ -5714,9 +5740,13 @@ impl super::GraphConverter for CoremlMlProgramConverter { rs_var, vec![var_rs_type], )); - (mean_rs_name, var_rs_name) + (mean_rs_name, var_rs_name, effective_var_shape) } else { - (mean_name, var_name) + 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) }; // Build an intermediate value type matching the output shape. @@ -5753,18 +5783,9 @@ impl super::GraphConverter for CoremlMlProgramConverter { )); // var_plus_eps = variance + epsilon - // variance has shape [C], result is also [C] - let var_op = graph_info.operand(variance_id_v).ok_or_else(|| { - GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "batchNorm variance {} not found", - variance_id_v - ), - } - })?; - let var_shape = &var_op.descriptor.shape; - let var_dims = Self::mil_dimensions_from_graph_shape(var_shape, false); + // 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, @@ -6465,17 +6486,25 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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 interface_mil_type = Self::interface_mil_data_type(&operand.descriptor.data_type); - if graph_mil_type != interface_mil_type { + // For argmin/argmax int32 proxy outputs, use int32 at the interface (not float32). + // The executor zero-extends int32 → int64 when the WebNN descriptor is int64. + let effective_interface_type = if int32_proxy_output_names.contains(&output_name) { + 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); @@ -6518,9 +6547,30 @@ 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 (e.g. argmin/argmax with int64 WebNN type), + // use Int32 at the model interface so it matches the function's emit type. + let feature_type = if int32_proxy_output_names.contains(&output_name) { + 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() }); } diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 9c990afa..1633d98b 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -5,22 +5,10 @@ coreml::abs::abs_int32_4D_tensor coreml::abs::abs_int64_4D_tensor coreml::abs::abs_int8_4D_tensor coreml::add::add_float32_with_special_character_names -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::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::averagePool2d::averagePool2d_float16_4D_tensor_options_dilations @@ -41,8 +29,6 @@ coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRoundi 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::batch_normalization::batchNormalization_float16_2D_tensor__mean_and_variance_are_non-constant__default_options -coreml::batch_normalization::batchNormalization_float32_2D_tensor__mean_and_variance_are_non-constant__default_options coreml::cast::cast_float16_4D_tensor_to_int64 coreml::cast::cast_float16_4D_tensor_to_uint32 coreml::cast::cast_float32_4D_tensor_to_int64 @@ -83,8 +69,6 @@ coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_odd_size_with_float coreml::dequantizeLinear::dequantizeLinear_int4_1D_tensor_of_odd_size_with_float32_1D_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 @@ -259,7 +243,6 @@ coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op coreml::prelu::prelu_int64_2D_constant_tensors -coreml::qdq_subgraph::per-channel_quantized_gemm_with_non-zero_quantized_dimension_of_the_filter coreml::qdq_subgraph::quantized_convTranspose2d coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor @@ -267,7 +250,6 @@ coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_wi 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 @@ -277,7 +259,6 @@ coreml::quantizeLinear::quantizeLinear_float32_1D_tensor_with_uint4_zeroPoint_wi 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 @@ -324,19 +305,6 @@ coreml::sub::sub_int8_4D_tensors coreml::sub::sub_uint32_4D_tensors coreml::sub::sub_uint64_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::tile::tile_float32_0D_scalar_tensor_by_repetitions___ coreml::tile::tile_uint32_2D_tensor coreml::transpose::transpose_float32_0D_constant_tensor_default_options From 159b42e3b30292f9d03add0464ea07056bd2d5fb Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 13:32:37 +0200 Subject: [PATCH 15/34] Fix explicit empty axes --- src/converters/coreml_mlprogram.rs | 110 ++++++++++++++++++ .../coreml_expected_failures.txt | 11 -- 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index eaae5ef9..b43a5338 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -6306,6 +6306,116 @@ impl super::GraphConverter for CoremlMlProgramConverter { | "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!("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)?; + + // 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 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 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], + )); + + 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], + )); + + // 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; + } + } if is_reduce_op { let maybe_0d_input = op.input_operands().first().and_then(|&id| { graph_info diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 1633d98b..73ffb821 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -264,20 +264,9 @@ coreml::quantizeLinear::quantizeLinear_float32_tensor_with_int4_zeroPoint_which_ 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::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_float32_1D_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_float32_1D_constant_tensor_empty_axes -coreml::reduce_max::reduceMax_float32_1D_constant_tensor_empty_axes -coreml::reduce_mean::reduceMean_float32_1D_constant_tensor_empty_axes -coreml::reduce_min::reduceMin_float32_1D_constant_tensor_empty_axes -coreml::reduce_product::reduceProduct_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum::reduceSum_float32_1D_constant_tensor_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float16_1D_tensor_with_empty_axes -coreml::reduce_sum_square::reduceSumSquare_float32_1D_tensor_with_empty_axes coreml::relu::relu_int32_4D_tensor coreml::relu::relu_int64_4D_tensor coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_axes__0__1_ From e5245410861e313439c9c365a59b963b0feed000 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 13:40:45 +0200 Subject: [PATCH 16/34] Fix int8 correctness --- src/converters/coreml_mlprogram.rs | 62 ++++++++++++++++++- src/executors/coreml.rs | 25 ++++++-- .../coreml_expected_failures.txt | 5 -- 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index b43a5338..83035e45 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -3868,9 +3868,9 @@ impl super::GraphConverter for CoremlMlProgramConverter { // Relu with integer input: CoreML relu only accepts float (fp32/fp16). // Cast int → fp32, apply relu, cast back. - // sub / sign with int8/uint8: CoreML only supports int32/fp32/fp16. + // sub / sign / abs with int8/uint8: CoreML only supports int32/fp32/fp16. // Cast inputs to int32, apply op, cast back to the original int type. - if matches!(op_type_lower.as_str(), "sub" | "sign") { + if matches!(op_type_lower.as_str(), "sub" | "sign" | "abs") { 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) { @@ -3937,6 +3937,64 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // 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; + } + } + } + } + if op_type_lower == "relu" { if let (Some(&input_id), Some(output_id)) = (op.input_operands().first(), op.output_operand()) diff --git a/src/executors/coreml.rs b/src/executors/coreml.rs index e3025a28..583defd3 100644 --- a/src/executors/coreml.rs +++ b/src/executors/coreml.rs @@ -615,14 +615,23 @@ unsafe fn fill_multiarray_from_bytes( /// 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 | DataType::Int8, 32) => { - // bool/uint8 → float32: 0→0.0, 1→1.0 (used for boolean condition inputs) + (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 | DataType::Uint32, 32) => { // int32 as float32 bits (reinterpret, same size — shouldn't normally happen) src.to_vec() @@ -752,7 +761,10 @@ fn convert_multiarray_bytes(actual_bytes: Vec, actual_code: i32, target: Dat let src = unsafe { std::slice::from_raw_parts(actual_bytes.as_ptr() as *const f32, count) }; match target { - DataType::Uint8 | DataType::Int8 => src.iter().map(|&v| v as u8).collect(), + 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 { @@ -775,10 +787,15 @@ fn convert_multiarray_bytes(actual_bytes: Vec, actual_code: i32, target: Dat let src = unsafe { std::slice::from_raw_parts(actual_bytes.as_ptr() as *const u16, count) }; match target { - DataType::Uint8 | DataType::Int8 => src + 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 { diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 73ffb821..41e085da 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -3,7 +3,6 @@ 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_float32_with_special_character_names coreml::arg_min_max::argMax_int64_4D_tensor__axis_0__all_options coreml::arg_min_max::argMax_uint32_4D_tensor__axis_1__all_options @@ -52,7 +51,6 @@ coreml::cast::cast_uint8_4D_tensor_to_int64 coreml::cast::cast_uint8_4D_tensor_to_uint32 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 @@ -235,7 +233,6 @@ coreml::mlNumber::cast_float_to_integer_underflows coreml::mlNumber::cast_fractional_float_to_integer coreml::mul::mul_uint32_4D_tensors coreml::neg::neg_int64_4D_tensor -coreml::neg::neg_int8_4D_tensor coreml::pad::pad_float16_4D_tensor_options_mode__edge_ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ coreml::pad::pad_float32_4D_tensor_options_mode__edge_ @@ -287,10 +284,8 @@ coreml::scatterElements::scatterElements_float32_tensors_along_axis_1 coreml::scatterElements::scatterElements_float32_tensors_along_axis_1_and_constant_indices coreml::scatterND::scatterND_2D_int8_tensors_with_index_out_of_bound coreml::sign::sign_int64_3D_tensor -coreml::sign::sign_int8_4D_tensor coreml::slice::slicing_float32_0D_constant_tensor_with_empty_starts_and_sizes_should_be_a_no-op 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::subgraph::add___sub___mul___gather_default From 5788eeca8726c9725db281dd360736005bdee27e Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 13:45:08 +0200 Subject: [PATCH 17/34] Fix l2pool --- src/converters/coreml_mlprogram.rs | 13 ++++++++++-- .../coreml_expected_failures.txt | 21 ------------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 83035e45..351ac8de 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -112,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 @@ -1237,6 +1238,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, @@ -1851,6 +1853,9 @@ impl CoremlMlProgramConverter { } | 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 @@ -6201,10 +6206,14 @@ impl super::GraphConverter for CoremlMlProgramConverter { // 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" { + 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::MaxPool2d { options, .. } + | Operation::L2Pool2d { options, .. } => { options.as_ref().map(|o| o.layout.as_str()).unwrap_or("") } _ => "", diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 41e085da..157985d2 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -127,35 +127,14 @@ 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::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_3D_tensor_default_options coreml::layer_normalization::layerNormalization_float16_4D_tensor_default_options coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_bias From 196b598e2c7fc3765a06dfe8130687686b758fed Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 13:50:17 +0200 Subject: [PATCH 18/34] Fix average pooling padding --- src/converters/coreml_mlprogram.rs | 7 +++++-- tests/wpt_conformance/coreml_expected_failures.txt | 8 -------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 351ac8de..de954fdf 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1901,11 +1901,14 @@ impl CoremlMlProgramConverter { Self::create_immediate_bool(ceil_mode), ); - // Only average pooling accepts this parameter. + // Only average pooling accepts this parameter. WebNN averagePool2d + // averages over the true window size, excluding padded elements from + // the divisor (a boundary window covering N real elements divides by + // N, not by the full kernel area). if matches!(&op, Operation::AveragePool2d { .. }) { inputs.insert( "exclude_padding_from_average".to_string(), - Self::create_immediate_bool(false), + Self::create_immediate_bool(true), ); } diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 157985d2..e548648d 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -11,23 +11,15 @@ coreml::arg_min_max::argMin_int64_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::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_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_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_float32_4D_tensor_options_dilations -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_dilations_with_options_strides 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_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::cast::cast_float16_4D_tensor_to_int64 coreml::cast::cast_float16_4D_tensor_to_uint32 coreml::cast::cast_float32_4D_tensor_to_int64 From f57ebf3017d5a3f7057ac8875d03fbdc5170869b Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 14:05:54 +0200 Subject: [PATCH 19/34] Int boundary proxy --- src/backends/coreml.rs | 19 ++++-- src/converters/coreml_mlprogram.rs | 66 ++++++++++++++++++- src/executors/coreml.rs | 27 +++++++- .../coreml_expected_failures.txt | 27 -------- 4 files changed, 103 insertions(+), 36 deletions(-) diff --git a/src/backends/coreml.rs b/src/backends/coreml.rs index ff099957..33ba07b0 100644 --- a/src/backends/coreml.rs +++ b/src/backends/coreml.rs @@ -268,17 +268,26 @@ impl<'context> MLBackendContext<'context> for CoremlContext { })?; let logical = tensor_byte_len(ml_tensor.descriptor()); - // When the graph output type is int64 but CoreML produced int32 bytes - // (e.g. argmin/argmax proxy), zero-extend each 4-byte value to 8 bytes. - // Index values are always non-negative so zero-extension is correct. + // 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!(ml_tensor.descriptor().data_type(), MLOperandDataType::Int64) + && 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 { - buf[i * 8..i * 8 + 4].copy_from_slice(&data[i * 4..i * 4 + 4]); + 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 { diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index de954fdf..c447a41e 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -2909,7 +2909,18 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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)?; + // 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 = matches!( + operand.descriptor.data_type, + DataType::Uint32 | DataType::Int64 | DataType::Uint64 + ); + 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); @@ -2921,10 +2932,15 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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)? + }; main_block.operations.push(Self::create_cast_operation( input_name, graph_input_type, - Self::cast_dtype_string_for_graph_type(&operand.descriptor.data_type)?, + cast_str, )); } } @@ -5445,6 +5461,52 @@ impl super::GraphConverter for CoremlMlProgramConverter { } } + // 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. diff --git a/src/executors/coreml.rs b/src/executors/coreml.rs index 583defd3..6bd5b030 100644 --- a/src/executors/coreml.rs +++ b/src/executors/coreml.rs @@ -580,8 +580,13 @@ unsafe fn fill_multiarray_from_bytes( // 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 + && (array_elem != elem || same_width_mismatch) { if count == 0 { return Ok(()); @@ -632,10 +637,28 @@ fn convert_input_bytes(src: &[u8], src_dtype: DataType, target_code: i32, count: } out } - (DataType::Int32 | DataType::Uint32, 32) => { + (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) }; diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index e548648d..38e041a7 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -4,12 +4,6 @@ coreml::abs::abs_float16_6D_tensor coreml::abs::abs_int32_4D_tensor coreml::abs::abs_int64_4D_tensor coreml::add::add_float32_with_special_character_names -coreml::arg_min_max::argMax_int64_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::argMin_int64_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::averagePool2d::averagePool2d_float16_4D_tensor_options_dilations coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_layout_nhwc_and_options_outputShapeRounding_ceil coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputShapeRounding_ceil @@ -20,27 +14,6 @@ coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_layout_nhwc_and_o coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRounding_ceil 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::cast::cast_float16_4D_tensor_to_int64 -coreml::cast::cast_float16_4D_tensor_to_uint32 -coreml::cast::cast_float32_4D_tensor_to_int64 -coreml::cast::cast_float32_4D_tensor_to_uint32 -coreml::cast::cast_int32_4D_tensor_to_int64 -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_int64 -coreml::cast::cast_int8_4D_tensor_to_uint32 -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_int64 -coreml::cast::cast_uint8_4D_tensor_to_uint32 coreml::clamp::clamp_int32_1D_tensor coreml::clamp::clamp_int64_1D_tensor_with_bigint_max coreml::clamp::clamp_uint32_1D_tensor From 4b5c4b456288b94ca0581d67755fed9ee252e048 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 14:33:52 +0200 Subject: [PATCH 20/34] Generalize int32 proxy --- src/converters/coreml_mlprogram.rs | 153 +++++++++++++----- .../coreml_expected_failures.txt | 12 -- 2 files changed, 115 insertions(+), 50 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index c447a41e..fe0d0697 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -384,7 +384,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 { @@ -448,6 +448,49 @@ impl CoremlMlProgramConverter { } } + /// MIL type used for an operand's value *inside* the graph. CoreML MIL has no + /// int64/uint32/uint64 tensor type, so those are represented as int32 (a proxy); + /// the model interface and executor reconcile the width/sign at the boundary. + fn graph_value_mil_type(data_type: &DataType) -> Result { + if matches!( + data_type, + DataType::Uint32 | DataType::Int64 | DataType::Uint64 + ) { + 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. + fn is_wide_int(data_type: &DataType) -> bool { + matches!( + data_type, + 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; @@ -482,7 +525,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 = @@ -585,23 +629,44 @@ impl CoremlMlProgramConverter { })), }, crate::graph::DataType::Int64 => { - // Reinterpret raw bytes as Vec using little-endian byte order. - let data = &constant_data.data; - let values: Vec = data + // 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())) + .map(|chunk| i64::from_le_bytes(chunk.try_into().unwrap()) as i32) .collect(); TensorValue { - value: Some(tensor_value::Value::LongInts( - tensor_value::RepeatedLongInts { values }, - )), + 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::Uint32 | crate::graph::DataType::Uint64 => TensorValue { - value: Some(tensor_value::Value::Bytes(tensor_value::RepeatedBytes { - values: constant_data.data.clone().into(), - })), - }, _ => { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), @@ -2865,9 +2930,14 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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); + 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)); @@ -3663,10 +3733,10 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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; - let is_castable_int = matches!( - int_dtype, - DataType::Int8 | DataType::Uint8 | DataType::Int32 - ); + // 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, @@ -3718,8 +3788,8 @@ impl super::GraphConverter for CoremlMlProgramConverter { vec![clipped_type], )); - // Cast back to the original int type - let back_dtype = Self::cast_dtype_string_for_graph_type(int_dtype)?; + // 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( clipped_name, output_type, @@ -4025,10 +4095,7 @@ impl super::GraphConverter for CoremlMlProgramConverter { { if let Some(input_op) = graph_info.operand(input_id) { let int_dtype = input_op.descriptor.data_type.clone(); - let is_castable_int = matches!( - int_dtype, - DataType::Int8 | DataType::Uint8 | DataType::Int32 - ); + let is_castable_int = Self::is_castable_int(&int_dtype); if is_castable_int { let int_name = Self::output_name_for_operand( graph_info, @@ -4065,7 +4132,7 @@ impl super::GraphConverter for CoremlMlProgramConverter { relu_inputs, vec![relu_type], )); - let back_dtype = Self::cast_dtype_string_for_graph_type(&int_dtype)?; + let back_dtype = Self::int_back_cast_dtype(&int_dtype)?; main_block.operations.push(Self::create_cast_operation( relu_name, output_type, @@ -4604,7 +4671,11 @@ impl super::GraphConverter for CoremlMlProgramConverter { } })?; - let input_name = operand_name(graph_info, op.input_operands()[0]); + let input_name = Self::output_name_for_operand( + graph_info, + op.input_operands()[0], + &operand_name_overrides, + ); // Create typed -1 constant matching input dtype. MIL `mul` // preserves rank rather than broadcasting scalars, so when @@ -4618,7 +4689,8 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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 => { + // 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::{ @@ -4679,7 +4751,8 @@ impl super::GraphConverter for CoremlMlProgramConverter { } })?; - let output_dtype = Self::mil_data_type(&output_operand.descriptor.data_type)?; + 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 @@ -6726,11 +6799,13 @@ 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); - // For argmin/argmax int32 proxy outputs, use int32 at the interface (not float32). - // The executor zero-extends int32 → int64 when the WebNN descriptor is int64. - let effective_interface_type = if int32_proxy_output_names.contains(&output_name) { + // 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 { @@ -6789,9 +6864,11 @@ 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 (e.g. argmin/argmax with int64 WebNN type), - // use Int32 at the model interface so it matches the function's emit type. - let feature_type = if int32_proxy_output_names.contains(&output_name) { + // 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, }; diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 38e041a7..bfeadf4b 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -52,13 +52,9 @@ coreml::gatherElements::gatherElements_float16_3D_input_and_int32_negative_indic coreml::gatherElements::gatherElements_float32_1D_input_and_int32_out-of-bounds_indices coreml::gatherElements::gatherElements_float32_3D_input_and_int32_negative_indices coreml::gatherND::gatherND_float16_2D_input_and_2D_negative_indices -coreml::gatherND::gatherND_float16_4D_input_and_1D_int64_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_4D_input_and_1D_int64_indices -coreml::gatherND::gatherND_float32_4D_input_and_1D_uint32_indices coreml::gatherND::gatherND_float32_rank-5_input_and_rank-5_indices 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_ @@ -174,8 +170,6 @@ 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_uint32_4D_tensors coreml::neg::neg_int64_4D_tensor coreml::pad::pad_float16_4D_tensor_options_mode__edge_ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ @@ -205,7 +199,6 @@ coreml::quantizeLinear::quantizeLinear_float32_tensor_with_int4_zeroPoint_which_ 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::reduce_l1::reduceL1_uint32_4D_tensor_options_axes_with_options_keepDimensions_false 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::relu::relu_int32_4D_tensor @@ -227,12 +220,7 @@ coreml::scatterElements::scatterElements_float32_tensors_along_axis_0_and_consta coreml::scatterElements::scatterElements_float32_tensors_along_axis_1 coreml::scatterElements::scatterElements_float32_tensors_along_axis_1_and_constant_indices coreml::scatterND::scatterND_2D_int8_tensors_with_index_out_of_bound -coreml::sign::sign_int64_3D_tensor coreml::slice::slicing_float32_0D_constant_tensor_with_empty_starts_and_sizes_should_be_a_no-op -coreml::sub::sub_int64_4D_tensors -coreml::sub::sub_uint32_4D_tensors -coreml::sub::sub_uint64_4D_tensors coreml::subgraph::add___sub___mul___gather_default coreml::tile::tile_float32_0D_scalar_tensor_by_repetitions___ -coreml::tile::tile_uint32_2D_tensor coreml::transpose::transpose_float32_0D_constant_tensor_default_options From 9a9a04e198cfec4bfd3fba8f8e5f2d89f664ebdb Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 15:25:49 +0200 Subject: [PATCH 21/34] Fix scatterElements --- src/converters/coreml_mlprogram.rs | 4 +++- tests/wpt_conformance/coreml_expected_failures.txt | 8 -------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index fe0d0697..fe15b2f3 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -204,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 diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index bfeadf4b..9ad44c6b 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -211,14 +211,6 @@ coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_sizes_ignored 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::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_2D_int8_tensors_with_index_out_of_bound coreml::slice::slicing_float32_0D_constant_tensor_with_empty_starts_and_sizes_should_be_a_no-op coreml::subgraph::add___sub___mul___gather_default From 198d2e2f06169d621f69de9b8bfa0881bf92c7a3 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 15:37:29 +0200 Subject: [PATCH 22/34] Fix layernorm default axes --- src/converters/coreml_mlprogram.rs | 7 ++++--- tests/wpt_conformance/coreml_expected_failures.txt | 12 ------------ 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index fe15b2f3..d470b3dd 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -2090,15 +2090,16 @@ impl CoremlMlProgramConverter { 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: default to last axis of the input operand + // 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 > 0 { - vec![(input_rank as i32) - 1] + if input_rank > 1 { + (1..input_rank as i32).collect() } else { vec![0] } diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 9ad44c6b..2ae31021 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -96,20 +96,8 @@ coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_dilations coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputShapeRounding_ceil 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::layer_normalization::layerNormalization_float16_3D_tensor_default_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_5D_tensor_default_options -coreml::layer_normalization::layerNormalization_float32_3D_tensor_default_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_5D_tensor_default_options 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_ From 59942533a7ef387339d56c46cdfab5f824baa105 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 15:49:11 +0200 Subject: [PATCH 23/34] Improve pooling coverage --- src/converters/coreml_mlprogram.rs | 249 +++++++++--------- .../coreml_expected_failures.txt | 20 -- 2 files changed, 118 insertions(+), 151 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index d470b3dd..8c8c2952 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1425,6 +1425,59 @@ impl CoremlMlProgramConverter { Ok(mil_type) } + /// 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, + 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) + } + /// Create inputs map for MIL operation fn create_operation_inputs( &self, @@ -1926,159 +1979,93 @@ impl CoremlMlProgramConverter { } => { // 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, so we proceed unconditionally. - let _layout = pool_opts + // is already in NCHW form, but the graph operands remain in WebNN layout. + let is_nhwc = pool_opts .as_ref() - .map(|o| { - if o.layout.is_empty() { - "nchw" - } else { - o.layout.as_str() - } - }) - .unwrap_or("nchw"); + .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])); + } - // 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() + // 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 with outputSizes is not supported yet; got {:?} for {}", - output_sizes, + "CoreML pooling does not support non-default dilations; got {:?} for {}", + dil, op.op_type() ), }); } - if !input_names.is_empty() { - inputs.insert("x".to_string(), Self::create_argument(&input_names[0])); - } + // 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), + ); - // outputShapeRounding: "floor" (default) or "ceil" - let ceil_mode = pool_opts + let strides = pool_opts .as_ref() - .map(|o| o.output_shape_rounding.eq_ignore_ascii_case("ceil")) - .unwrap_or(false); + .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( - "ceil_mode".to_string(), - Self::create_immediate_bool(ceil_mode), + "pad_type".to_string(), + Self::create_immediate_string("custom"), ); + inputs.insert("ceil_mode".to_string(), Self::create_immediate_bool(false)); - // Only average pooling accepts this parameter. WebNN averagePool2d - // averages over the true window size, excluding padded elements from - // the divisor (a boundary window covering N real elements divides by - // N, not by the full kernel area). + // 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), ); } - - // 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), - ); - } else { - // window_dimensions is None or empty: infer kernel_sizes from input shape. - // Layout determines which dims are H and W: - // NCHW → [N, C, H, W]: H=dim2, W=dim3 - // NHWC → [N, H, W, C]: H=dim1, W=dim2 - let is_nhwc = opts.layout.eq_ignore_ascii_case("nhwc"); - if let Some(&input_id) = op.input_operands().first() { - if let Some(input_op) = graph.operand(input_id) { - let shape = input_op.descriptor.static_or_max_shape(); - if shape.len() >= 4 { - let kernel_sizes = if is_nhwc { - vec![shape[1], shape[2]] - } else { - vec![shape[2], shape[3]] - }; - inputs.insert( - "kernel_sizes".to_string(), - Self::create_immediate_int_array(&kernel_sizes), - ); - } - } - } - } - let strides = if opts.strides.is_empty() { - vec![1u32, 1] - } else { - opts.strides.clone() - }; - inputs.insert( - "strides".to_string(), - Self::create_immediate_int_array(&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 { - // CoreML avg_pool/max_pool requires pad to be exactly 4 elements - // (2 * spatial_rank) even when all zeros. - inputs.insert( - "pad".to_string(), - Self::create_immediate_int_array(&[0u32, 0, 0, 0]), - ); - inputs.insert( - "pad_type".to_string(), - Self::create_immediate_string("valid"), - ); - } - } else { - // No opts: infer kernel_sizes from input shape (NCHW: spatial dims at [2], [3]). - if let Some(&input_id) = op.input_operands().first() { - if let Some(input_op) = graph.operand(input_id) { - let shape = input_op.descriptor.static_or_max_shape(); - if shape.len() >= 4 { - let kernel_sizes = vec![shape[2], shape[3]]; - inputs.insert( - "kernel_sizes".to_string(), - Self::create_immediate_int_array(&kernel_sizes), - ); - } - } - } - // CoreML requires pad to be exactly 4 elements even when all zeros. - inputs.insert( - "pad".to_string(), - Self::create_immediate_int_array(&[0u32, 0, 0, 0]), - ); - inputs.insert( - "pad_type".to_string(), - Self::create_immediate_string("valid"), - ); - inputs.insert( - "strides".to_string(), - Self::create_immediate_int_array(&[1u32, 1]), - ); - } } // Layer normalization (different from batch/instance normalization) diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 2ae31021..dc7c2f16 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -5,15 +5,7 @@ coreml::abs::abs_int32_4D_tensor coreml::abs::abs_int64_4D_tensor coreml::add::add_float32_with_special_character_names coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_dilations -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_layout_nhwc_and_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float16_4D_tensor_options_outputShapeRounding_ceil -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_float32_4D_tensor_options_dilations -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_layout_nhwc_and_options_outputShapeRounding_ceil -coreml::averagePool2d::averagePool2d_float32_4D_tensor_options_outputShapeRounding_ceil -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::clamp::clamp_int32_1D_tensor coreml::clamp::clamp_int64_1D_tensor_with_bigint_max coreml::clamp::clamp_uint32_1D_tensor @@ -89,13 +81,7 @@ coreml::gru_cell::gruCell_float32_tensors_with_options_bias__options_recurrentBi 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::l2Pool2d::l2Pool2d_float16_4D_tensor_options_dilations -coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_outputShapeRounding_ceil -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_float32_4D_tensor_options_dilations -coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_outputShapeRounding_ceil -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::layer_normalization::layerNormalization_float16_4D_tensor_options_bias_and_options_axes__3__1__2_ coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_bias_and_options_axes__3__1__2_ coreml::lstm::lstm_float16_tensors_steps_1_with_all_options @@ -140,14 +126,8 @@ coreml::lstm_cell::lstmCell_float32_tensors_with_options_bias__options_recurrent coreml::lstm_cell::lstmCell_float32_tensors_with_options_peepholeWeight_and_options_layout__ifgo_ coreml::max::max_uint8_4D_tensors coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations -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_float32_4D_tensor_options_dilations -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_outputSizes_ignores_options_outputShapeRounding_ceil -coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputSizes_ignores_options_outputShapeRounding_floor coreml::min::min_uint8_4D_tensors coreml::mlNumber::cast_BigInt_to_int64 coreml::mlNumber::cast_BigInt_to_int64_overflow From a5d2f396bf169985f0de43ca9231210801cc70d4 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 16:20:16 +0200 Subject: [PATCH 24/34] Fix quantization --- src/converters/coreml_mlprogram.rs | 537 ++++++++++++++++++ .../coreml_expected_failures.txt | 14 - 2 files changed, 537 insertions(+), 14 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 8c8c2952..8596a922 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1478,6 +1478,514 @@ impl CoremlMlProgramConverter { Some(pad) } + /// 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])) + } + + /// 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 matches!(quant_dt, DataType::Int4 | DataType::Uint4) { + return false; + } + if tensor_op.descriptor.shape.is_empty() { + return false; + } + 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) + } + + /// 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) + } + + /// 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: format!( + "quantize/dequantize: input dim {} not divisible by scale dim {}", + input_shape[i], nb + ), + }); + } + 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)) + } + + /// 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 = input_op.descriptor.static_or_max_shape(); + let scale_shape = scale_op.descriptor.static_or_max_shape(); + + // 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], + )); + + // 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], + )); + + // 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 + }; + + // 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], + )); + + 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(()) + } + + /// 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 = 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)?; + let out_int_str = Self::cast_dtype_string_for_graph_type(&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 (int8/uint8; int32 needs no clamp). + let clamped_name = match quant_dt { + DataType::Int8 | DataType::Uint8 => { + let (qmin, qmax) = if matches!(quant_dt, DataType::Uint8) { + (0.0f32, 255.0f32) + } else { + (-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(()) + } + /// Create inputs map for MIL operation fn create_operation_inputs( &self, @@ -3019,6 +3527,12 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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]. @@ -5262,6 +5776,29 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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; + } + // 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. diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index dc7c2f16..3f6830eb 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -16,14 +16,10 @@ coreml::clamp::minValue____maxValue coreml::clamp::minValue_as_Infinity coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ -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_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_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 @@ -32,10 +28,6 @@ coreml::dequantizeLinear::dequantizeLinear_uint4_3D_tensor_with_float16_3D_scale 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_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::expand::expand_float32_0D_scalar_to_0D @@ -147,13 +139,9 @@ coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op coreml::prelu::prelu_int64_2D_constant_tensors coreml::qdq_subgraph::quantized_convTranspose2d -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float16_4D_tensor -coreml::quantizeLinear::per-tensor_quantizeLinear_for_float32_4D_tensor coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 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_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 @@ -161,8 +149,6 @@ coreml::quantizeLinear::quantizeLinear_float16_tensor_with_uint4_zeroPoint_which coreml::quantizeLinear::quantizeLinear_float32_1D_tensor_with_uint4_zeroPoint_with_block_size___3 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_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 From bb81f7bcb0b579bd3959179f2624fd19ba1d90e4 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 16:32:02 +0200 Subject: [PATCH 25/34] Decompose lstm and gru --- src/converters/coreml_mlprogram.rs | 475 ++++++++++++++++++ .../coreml_expected_failures.txt | 20 - 2 files changed, 475 insertions(+), 20 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 8596a922..3cfe0cac 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1535,6 +1535,105 @@ impl CoremlMlProgramConverter { Self::create_named_value_type(name, dtype, &dims, 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), + }); + } + }) + } + + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 + } + /// 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; @@ -1986,6 +2085,360 @@ impl CoremlMlProgramConverter { 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 mut 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 mut 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(()) + } + /// Create inputs map for MIL operation fn create_operation_inputs( &self, @@ -5799,6 +6252,28 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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; + } + // 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. diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 3f6830eb..4a563b83 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -64,14 +64,6 @@ coreml::gru::gru_float32_tensors_steps_2_with_options_bias__options_recurrentBia 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::l2Pool2d::l2Pool2d_float16_4D_tensor_options_dilations coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_dilations coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_bias_and_options_axes__3__1__2_ @@ -104,18 +96,6 @@ coreml::lstm::lstm_float32_tensors_steps_2__batchSize_1_with_options_bias__optio 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_uint8_4D_tensors coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_dilations From 7b207f8f1b48177ece71a40f91d02a1c0c75b7e0 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 16:41:30 +0200 Subject: [PATCH 26/34] Decompose sequence gru and lstm --- src/converters/coreml_mlprogram.rs | 759 ++++++++++++++++++ .../coreml_expected_failures.txt | 52 -- 2 files changed, 759 insertions(+), 52 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 3cfe0cac..6b0c361c 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1634,6 +1634,276 @@ impl CoremlMlProgramConverter { out_name } + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 mut 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) + } + + /// 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 mut 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 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) + } + /// 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; @@ -2439,6 +2709,477 @@ impl CoremlMlProgramConverter { 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, @@ -6273,6 +7014,24 @@ impl super::GraphConverter for CoremlMlProgramConverter { )?; 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 diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 4a563b83..e7204725 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -40,62 +40,10 @@ 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_rank-5_input_and_rank-5_indices -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::l2Pool2d::l2Pool2d_float16_4D_tensor_options_dilations coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_dilations coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_bias_and_options_axes__3__1__2_ coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_bias_and_options_axes__3__1__2_ -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::max::max_uint8_4D_tensors coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_dilations From b67a2d031698249cc4b65c07669fc914e1c07d9b Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 17:14:43 +0200 Subject: [PATCH 27/34] Add int4 quantization support --- src/converters/coreml_mlprogram.rs | 148 +++++++++++------- src/executors/coreml.rs | 73 +++++++++ .../coreml_expected_failures.txt | 28 ---- 3 files changed, 163 insertions(+), 86 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 6b0c361c..312d2f97 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -451,13 +451,10 @@ impl CoremlMlProgramConverter { } /// MIL type used for an operand's value *inside* the graph. CoreML MIL has no - /// int64/uint32/uint64 tensor type, so those are represented as int32 (a proxy); - /// the model interface and executor reconcile the width/sign at the boundary. + /// 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 matches!( - data_type, - DataType::Uint32 | DataType::Int64 | DataType::Uint64 - ) { + if Self::is_wide_int(data_type) { Ok(crate::protos::coreml::mil_spec::DataType::Int32 as i32) } else { Self::mil_data_type(data_type) @@ -465,11 +462,15 @@ impl CoremlMlProgramConverter { } /// Whether a WebNN type has no native MIL tensor representation and is proxied - /// through int32 inside the graph. + /// 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::Uint32 | DataType::Int64 | DataType::Uint64 + DataType::Int4 + | DataType::Uint4 + | DataType::Uint32 + | DataType::Int64 + | DataType::Uint64 ) } @@ -669,6 +670,39 @@ impl CoremlMlProgramConverter { })), } } + 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, + })), + } + } _ => { return Err(GraphError::ConversionFailed { format: "coreml_mlprogram".to_string(), @@ -1518,11 +1552,10 @@ impl CoremlMlProgramConverter { return false; }; let quant_dt = quant_op.descriptor.data_type.clone(); - if matches!(quant_dt, DataType::Int4 | DataType::Uint4) { - return false; - } if tensor_op.descriptor.shape.is_empty() { - return false; + // 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(); @@ -1995,7 +2028,11 @@ impl CoremlMlProgramConverter { format: "coreml_mlprogram".to_string(), reason: format!("dequantize scale operand {} not found", scale_id), })?; - let input_shape = input_op.descriptor.static_or_max_shape(); + 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(); // Output MIL dtype is the (float) scale/output type. @@ -2169,7 +2206,11 @@ impl CoremlMlProgramConverter { format: "coreml_mlprogram".to_string(), reason: format!("quantize output operand {} not found", output_id), })?; - let input_shape = input_op.descriptor.static_or_max_shape(); + 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(); @@ -2178,7 +2219,10 @@ impl CoremlMlProgramConverter { // 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)?; - let out_int_str = Self::cast_dtype_string_for_graph_type(&quant_dt)?; + // 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)?; @@ -2304,13 +2348,14 @@ impl CoremlMlProgramConverter { round_name }; - // 5. clamp to the quantized type's range (int8/uint8; int32 needs no clamp). + // 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 => { - let (qmin, qmax) = if matches!(quant_dt, DataType::Uint8) { - (0.0f32, 255.0f32) - } else { - (-128.0f32, 127.0f32) + 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( @@ -4671,13 +4716,10 @@ impl super::GraphConverter for CoremlMlProgramConverter { format: "coreml_mlprogram".to_string(), reason: format!("Input operand {} not found", input_id), })?; - // 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 = matches!( - operand.descriptor.data_type, - DataType::Uint32 | DataType::Int64 | DataType::Uint64 - ); + // 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 { @@ -9093,7 +9135,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], @@ -9135,23 +9177,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], @@ -9197,23 +9230,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], @@ -9255,14 +9279,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], @@ -9304,10 +9332,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/executors/coreml.rs b/src/executors/coreml.rs index 6bd5b030..31dd9ff7 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 = 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 { @@ -713,6 +742,50 @@ unsafe fn extract_multiarray_bytes( // 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 = { diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index e7204725..c8d51e75 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -16,20 +16,6 @@ coreml::clamp::minValue____maxValue coreml::clamp::minValue_as_Infinity coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ -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_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::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::expand::expand_float32_0D_scalar_to_0D coreml::gather::gather_float32_2D_tensor_and_int32_0D_out-of-bound_positive_indices_default_options coreml::gatherElements::gatherElements_float16_3D_input_and_int32_negative_indices @@ -67,20 +53,6 @@ coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op coreml::prelu::prelu_int64_2D_constant_tensors coreml::qdq_subgraph::quantized_convTranspose2d -coreml::quantizeLinear::quantizeLinear_float16_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -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_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_1D_tensor_with_uint4_zeroPoint_with_block_size___3 -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_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::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::relu::relu_int32_4D_tensor From 62f3969e33a108a222fcd053530bd0670e5ab186 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 17:53:33 +0200 Subject: [PATCH 28/34] Residual fixes --- src/converters/coreml_mlprogram.rs | 525 +++++++++++++----- .../coreml_expected_failures.txt | 22 - 2 files changed, 394 insertions(+), 153 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 312d2f97..d6d00195 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1667,6 +1667,133 @@ impl CoremlMlProgramConverter { out_name } + /// 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 + } + + /// 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, + ) + } + + /// 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 + } + /// Emit `reshape(x, shape)`. Returns `out_name`. fn rnn_reshape( block: &mut Block, @@ -5146,6 +5273,58 @@ impl super::GraphConverter for CoremlMlProgramConverter { for op in &graph_info.operations { let op_type_lower = op.op_type().to_lowercase(); + // 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; + } + } + } + if matches!( op_type_lower.as_str(), "equal" @@ -5372,7 +5551,9 @@ impl super::GraphConverter for CoremlMlProgramConverter { })?; let input_dtype = &x_operand.descriptor.data_type; let is_float16 = *input_dtype == DataType::Float16; - let mil_dtype = Self::mil_data_type(input_dtype)?; + // 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, @@ -5397,11 +5578,16 @@ impl super::GraphConverter for CoremlMlProgramConverter { cond_name.clone(), crate::protos::coreml::mil_spec::DataType::Bool as i32, )?; - let zero_arg = if is_float16 { + // 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 { + } 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())); @@ -5560,62 +5746,52 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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 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(), + 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: 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( - "mul", - mul_inputs, - vec![zeroed_type], + mil_ops::MAXIMUM, + mx_inputs, + vec![make_type(mx_name.clone())], )); - - // 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), - ); - } else { - add_inputs.insert( - "y".to_string(), - Self::create_immediate_float(min_value as f32), - ); - } + 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( - "add", - add_inputs, + mil_ops::MINIMUM, + mn_inputs, vec![output_type], )); @@ -5702,9 +5878,12 @@ impl super::GraphConverter for CoremlMlProgramConverter { // Relu with integer input: CoreML relu only accepts float (fp32/fp16). // Cast int → fp32, apply relu, cast back. - // sub / sign / abs with int8/uint8: CoreML only supports int32/fp32/fp16. - // Cast inputs to int32, apply op, cast back to the original int type. - if matches!(op_type_lower.as_str(), "sub" | "sign" | "abs") { + // 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) { @@ -5892,10 +6071,7 @@ impl super::GraphConverter for CoremlMlProgramConverter { { if let Some(input_op) = graph_info.operand(input_id) { let int_dtype = input_op.descriptor.data_type.clone(); - let is_castable_int = matches!( - int_dtype, - DataType::Int8 | DataType::Uint8 | DataType::Int32 - ); + let is_castable_int = Self::is_castable_int(&int_dtype); if is_castable_int { let (beginning_padding, ending_padding, options) = match op { Operation::Pad { @@ -5971,8 +6147,8 @@ impl super::GraphConverter for CoremlMlProgramConverter { pad_inputs, vec![padded_type], )); - // Cast back to the original int type - let back_dtype = Self::cast_dtype_string_for_graph_type(&int_dtype)?; + // 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, @@ -6920,24 +7096,23 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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; - let (scale_h, scale_w) = if !opts.scales.is_empty() { + // 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 if let Some(sizes) = &opts.sizes { - if sizes.len() >= 2 { - 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 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 { - (1.0, 1.0) - } } else { (1.0, 1.0) }; @@ -6968,13 +7143,15 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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(false), + Self::create_immediate_bool(true), ); } @@ -7510,76 +7687,162 @@ impl super::GraphConverter for CoremlMlProgramConverter { } // Gather: cast uint32/int64 indices to int32 (CoreML gather only accepts int32 and smaller). - if op_type_lower == "gather" { + // 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; - if let Operation::Gather { .. } = op { - let indices_id = op.input_operands().get(1).copied(); - if let Some(idx_id) = indices_id { - if let Some(idx_op) = graph_info.operand(idx_id) { - let needs_cast = matches!( - idx_op.descriptor.data_type, - DataType::Int64 | DataType::Uint32 - ); - if needs_cast { - let output_id = op.output_operand().ok_or_else(|| { - GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: "gather has no output".to_string(), - } - })?; - 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, - op.input_operands()[0], - &operand_name_overrides, - ); - let idx_name = Self::output_name_for_operand( - graph_info, - idx_id, - &operand_name_overrides, - ); - let casted_idx_name = format!("{}_idx_i32", output_name); - let casted_idx_type = Self::create_value_with_mil_type( - graph_info, - idx_id, - casted_idx_name.clone(), - MilDataType::Int32 as i32, - )?; - main_block.operations.push(Self::create_cast_operation( - idx_name, - casted_idx_type, - "int32", - )); - let axis = if let Operation::Gather { options, .. } = op { - options.as_ref().map(|o| o.axis).unwrap_or(0) - } else { - 0 - }; - 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(casted_idx_name), - ); - 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_ops::GATHER, - gather_inputs, - vec![output_type], - )); - continue; - } + 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 (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, + ); + + 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; + } + } + + // 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; } } } diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index c8d51e75..0a6346d9 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -11,30 +11,17 @@ coreml::clamp::clamp_int64_1D_tensor_with_bigint_max 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::maxValue_as_-Infinity -coreml::clamp::minValue____maxValue -coreml::clamp::minValue_as_Infinity coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ -coreml::expand::expand_float32_0D_scalar_to_0D coreml::gather::gather_float32_2D_tensor_and_int32_0D_out-of-bound_positive_indices_default_options -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_3D_input_and_int32_negative_indices -coreml::gatherND::gatherND_float16_2D_input_and_2D_negative_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_rank-5_input_and_rank-5_indices coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_dilations coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_dilations coreml::layer_normalization::layerNormalization_float16_4D_tensor_options_bias_and_options_axes__3__1__2_ coreml::layer_normalization::layerNormalization_float32_4D_tensor_options_bias_and_options_axes__3__1__2_ -coreml::max::max_uint8_4D_tensors coreml::maxPool2d::maxPool2d_float16_4D_tensor_options_dilations coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_dilations coreml::maxPool2d::maxPool2d_float32_4D_tensor_options_outputShapeRounding_ceil_and_symmetric_padding -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 @@ -50,8 +37,6 @@ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ coreml::pad::pad_float32_4D_tensor_options_mode__edge_ coreml::pad::pad_float32_4D_tensor_options_mode__reflection_ coreml::pad::pad_int64_2D_tensor_with_options_value_as_bigint -coreml::pad::padding_float32_0D_constant_tensor_with_empty_paddings_should_be_no-op -coreml::prelu::prelu_int64_2D_constant_tensors coreml::qdq_subgraph::quantized_convTranspose2d 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 @@ -59,14 +44,7 @@ coreml::relu::relu_int32_4D_tensor coreml::relu::relu_int64_4D_tensor 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__options_mode__linear_ -coreml::resample2d::resample2d_upsample__float32_4D_tensor_options_scales_options_mode__linear_ -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::scatterND::scatterND_2D_int8_tensors_with_index_out_of_bound -coreml::slice::slicing_float32_0D_constant_tensor_with_empty_starts_and_sizes_should_be_a_no-op coreml::subgraph::add___sub___mul___gather_default -coreml::tile::tile_float32_0D_scalar_tensor_by_repetitions___ -coreml::transpose::transpose_float32_0D_constant_tensor_default_options From 93f8a13c4d03cf775f97149a821b92008ffb508c Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 18:11:58 +0200 Subject: [PATCH 29/34] Fix clamp with a workaround --- src/converters/coreml_mlprogram.rs | 39 ++++++++++++++++++- .../coreml_expected_failures.txt | 2 - 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index d6d00195..585afd2b 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -5714,10 +5714,47 @@ impl super::GraphConverter for CoremlMlProgramConverter { 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( - clipped_name, + sat_name, output_type, back_dtype, )); diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 0a6346d9..4f087f9a 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -29,8 +29,6 @@ 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::neg::neg_int64_4D_tensor coreml::pad::pad_float16_4D_tensor_options_mode__edge_ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ From da547a6cd2fac197f3c3d385f779df8d906f0bd3 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 18:14:33 +0200 Subject: [PATCH 30/34] Fix cargo check warnings --- src/converters/coreml_mlprogram.rs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 585afd2b..5463b058 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -703,15 +703,6 @@ impl CoremlMlProgramConverter { })), } } - _ => { - return Err(GraphError::ConversionFailed { - format: "coreml_mlprogram".to_string(), - reason: format!( - "Unsupported constant data type: {:?}", - operand.descriptor.data_type - ), - }); - } }; // Create immediate value @@ -1906,7 +1897,7 @@ impl CoremlMlProgramConverter { _ => (0, h, 2 * h), }; let p = |s: &str| format!("{}_{}", prefix, s); - let mut gate = |block: &mut Block, off: u32, act: &str, tag: &str| -> String { + let gate = |block: &mut Block, off: u32, act: &str, tag: &str| -> String { let wg = Self::rnn_slice( block, w_name, @@ -2000,7 +1991,7 @@ impl CoremlMlProgramConverter { }; let (pi_off, po_off, pf_off) = (0u32, h, 2 * h); let p = |s: &str| format!("{}_{}", prefix, s); - let mut gate = |block: &mut Block, + let gate = |block: &mut Block, off: u32, act: &str, tag: &str, @@ -2616,7 +2607,7 @@ impl CoremlMlProgramConverter { let p = |s: &str| format!("{}_{}", output_name, s); // Compute a "reset/update"-style gate: activation(X·Wg^T + bg + H·Rg^T + rbg). - let mut gate = |block: &mut Block, off: u32, act: &str, tag: &str| -> String { + let gate = |block: &mut Block, off: u32, act: &str, tag: &str| -> String { let wg = Self::rnn_slice( block, &w_name, @@ -2801,7 +2792,7 @@ impl CoremlMlProgramConverter { let p = |s: &str| format!("{}_{}", h_name, s); // gate = activation(X·Wg^T + bg + H·Rg^T + rbg [+ pg ⊙ cstate]) - let mut gate = |block: &mut Block, + let gate = |block: &mut Block, off: u32, act: &str, tag: &str, From 4aee702757a368e118f8ab25f772d5dd96fb752d Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 18:18:13 +0200 Subject: [PATCH 31/34] Fix formatting --- src/converters/coreml_mlprogram.rs | 16 ++++++++-------- src/executors/coreml.rs | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/converters/coreml_mlprogram.rs b/src/converters/coreml_mlprogram.rs index 5463b058..638d722f 100644 --- a/src/converters/coreml_mlprogram.rs +++ b/src/converters/coreml_mlprogram.rs @@ -1992,10 +1992,10 @@ impl CoremlMlProgramConverter { 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)>| + off: u32, + act: &str, + tag: &str, + peep: Option<(u32, &str)>| -> String { let wg = Self::rnn_slice( block, @@ -2793,10 +2793,10 @@ impl CoremlMlProgramConverter { // 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)>| + off: u32, + act: &str, + tag: &str, + peep: Option<(u32, &str)>| -> String { let wg = Self::rnn_slice( block, diff --git a/src/executors/coreml.rs b/src/executors/coreml.rs index 31dd9ff7..400495be 100644 --- a/src/executors/coreml.rs +++ b/src/executors/coreml.rs @@ -589,7 +589,7 @@ unsafe fn fill_multiarray_from_bytes( reason: format!("MLMultiArray has no backing buffer for data type {dtype:?}"), }); } - let dst = std::slice::from_raw_parts_mut(ptr as *mut u8, count * 4); + 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()); } @@ -628,7 +628,7 @@ unsafe fn fill_multiarray_from_bytes( } // Convert src bytes (dtype) → array bytes (canonical_code). let converted = convert_input_bytes(src, dtype, canonical_code, count); - let dst = std::slice::from_raw_parts_mut(ptr as *mut u8, count * array_elem); + let dst = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, count * array_elem) }; dst.copy_from_slice(&converted); return Ok(()); } From a8018d3a920f8023ebb16a4d56a6b9753a19705f Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 18:22:50 +0200 Subject: [PATCH 32/34] Update CoreML operator support doc --- docs/development/backend-operator-support.md | 126 ++++++++++--------- 1 file changed, 64 insertions(+), 62 deletions(-) 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 From 75cf1083798f4e21b79ce9189c19291696e99350 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 18:26:08 +0200 Subject: [PATCH 33/34] Match CI behavior --- tests/wpt_conformance/coreml_expected_failures.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/wpt_conformance/coreml_expected_failures.txt b/tests/wpt_conformance/coreml_expected_failures.txt index 4f087f9a..ea07cdd8 100644 --- a/tests/wpt_conformance/coreml_expected_failures.txt +++ b/tests/wpt_conformance/coreml_expected_failures.txt @@ -13,7 +13,9 @@ coreml::clamp::clamp_uint64_1D_tensor_with_Number_min_and_max coreml::clamp::clamp_uint64_1D_tensor_with_bigint_max coreml::constant_reshape_optimization::reshape___reshape___reshape___instanceNormalization_float32 coreml::conv_transpose2d::convTranspose2d_same_output_size_different_padding__padding_2__outputPadding_2__ +coreml::gather::gather_float16_5D_tensor_and_0D_scalar_indices_options_axis_4 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::gatherND::gatherND_float32_rank-5_input_and_rank-5_indices coreml::l2Pool2d::l2Pool2d_float16_4D_tensor_options_dilations coreml::l2Pool2d::l2Pool2d_float32_4D_tensor_options_dilations @@ -29,6 +31,7 @@ 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::neg::neg_int32_4D_tensor coreml::neg::neg_int64_4D_tensor coreml::pad::pad_float16_4D_tensor_options_mode__edge_ coreml::pad::pad_float16_4D_tensor_options_mode__reflection_ From 9c35d159b6851c0cd0046e8f85ae5c8f598ed2c5 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Tue, 14 Jul 2026 18:36:01 +0200 Subject: [PATCH 34/34] Use all logical cores for test-wpt-coreml --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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