diff --git a/onnxruntime/core/providers/coreml/builders/impl/split_op_builder.cc b/onnxruntime/core/providers/coreml/builders/impl/split_op_builder.cc index 4ee9b54cebd16..875754138e408 100644 --- a/onnxruntime/core/providers/coreml/builders/impl/split_op_builder.cc +++ b/onnxruntime/core/providers/coreml/builders/impl/split_op_builder.cc @@ -23,8 +23,7 @@ class SplitOpBuilder : public BaseOpBuilder { bool IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params, const logging::Logger& logger) const override; - // Split opset 13- uses "split" as attribute. Currently it's not supported. - int GetMinSupportedOpSet(const Node& /* node */) const override { return 13; } + int GetMinSupportedOpSet(const Node& /* node */) const override { return 1; } bool SupportsMLProgram() const override { return true; } }; @@ -56,6 +55,9 @@ Status SplitOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, return std::make_tuple(remainder, chunk_size); }; + // Pre-opset-13 'split' is an INTS attribute. If present, it overrides even splitting. + const auto split_attr = helper.GetInt64s("split"); + if (model_builder.CreateMLProgram()) { using namespace CoreML::Specification::MILSpec; std::unique_ptr split_op = model_builder.CreateOperation(node, "split"); @@ -68,6 +70,10 @@ Status SplitOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, auto split_span = unpacked_tensor.DataAsSpan(); AddOperationInput(*split_op, "split_sizes", model_builder.AddConstant(split_op->type(), "split_sizes", split_span)); + } else if (split_attr) { + // pre-opset-13 'split' attribute + AddOperationInput(*split_op, "split_sizes", + model_builder.AddConstant(split_op->type(), "split_sizes", *split_attr)); } else if (node.SinceVersion() < 18) { int64_t num_outputs = narrow(node.OutputDefs().size()); AddOperationInput(*split_op, "num_splits", @@ -109,6 +115,11 @@ Status SplitOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, for (const auto& split_size : split_span) { coreml_splitnd->add_splitsizes(split_size); } + } else if (split_attr) { + // pre-opset-13 'split' attribute + for (const auto& split_size : *split_attr) { + coreml_splitnd->add_splitsizes(split_size); + } } else if (node.SinceVersion() < 18) { int64_t num_outputs = narrow(node.OutputDefs().size()); coreml_splitnd->set_numsplits(num_outputs); @@ -166,6 +177,10 @@ bool SplitOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputPar return false; } + if (split_dims_at_axis == -1) { + LOGS(logger, VERBOSE) << "Dim at the splitting axis is not allowed to be dynamic."; + return false; + } Initializer unpacked_tensor(input_params.graph_viewer.GetGraph(), *splits_tensor, input_params.graph_viewer.ModelPath()); auto splits_span = unpacked_tensor.DataAsSpan(); @@ -182,10 +197,27 @@ bool SplitOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputPar LOGS(logger, VERBOSE) << "Invalid value in 'splits' input."; return false; } + } else if (const auto split_attr = helper.GetInt64s("split"); split_attr) { + // pre-opset-13: 'split' is an INTS attribute. Validate the same way we + // validate the input form above. + if (split_attr->size() < 2) { + LOGS(logger, VERBOSE) << "CoreML Split must produce at least 2 outputs."; + return false; + } if (split_dims_at_axis == -1) { LOGS(logger, VERBOSE) << "Dim at the splitting axis is not allowed to be dynamic."; return false; } + int64_t sum_of_splits = std::accumulate(split_attr->begin(), split_attr->end(), int64_t{0}); + if (sum_of_splits != split_dims_at_axis) { + LOGS(logger, VERBOSE) << "Mismatch between sum of 'split' attribute and split-axis size. Expected: " + << split_dims_at_axis << " Actual: " << sum_of_splits; + return false; + } + if (std::any_of(split_attr->begin(), split_attr->end(), [](int64_t v) { return v <= 0; })) { + LOGS(logger, VERBOSE) << "Invalid value in 'split' attribute (sizes must be positive)."; + return false; + } } else { if (node.SinceVersion() >= 18) { const auto num_outputs = helper.GetInt64("num_outputs"); @@ -205,6 +237,20 @@ bool SplitOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputPar << num_outputs.value(); return false; } + } else if (node.OutputDefs().size() < 2) { + LOGS(logger, VERBOSE) << "CoreML Split must produce at least 2 outputs."; + return false; + } else if (split_dims_at_axis == -1) { + // No 'split' attr or input: ONNX spec says split evenly, but we cannot + // verify divisibility without a known axis size. + LOGS(logger, VERBOSE) << "Dim at the splitting axis is not allowed to be dynamic when 'split' is omitted."; + return false; + } else if (split_dims_at_axis % static_cast(node.OutputDefs().size()) != 0) { + // No 'split' attr or input: ONNX spec says split evenly. CoreML's + // num_splits requires the axis size be evenly divisible. + LOGS(logger, VERBOSE) << "Even split required when 'split' is omitted; axis size " + << split_dims_at_axis << " not divisible by num outputs " << node.OutputDefs().size(); + return false; } } return true; diff --git a/onnxruntime/test/providers/coreml/coreml_basic_test.cc b/onnxruntime/test/providers/coreml/coreml_basic_test.cc index f56c81d2e89de..61c3297a43118 100644 --- a/onnxruntime/test/providers/coreml/coreml_basic_test.cc +++ b/onnxruntime/test/providers/coreml/coreml_basic_test.cc @@ -1164,6 +1164,577 @@ TEST(CoreMLExecutionProviderTest, QuickGeluTestFp16) { #endif } +TEST(CoreMLExecutionProviderTest, Split11UnevenAttribute) { + // ai.onnx:Split-11 with `split` attribute carrying non-uniform sizes. + // This is the form used by DWPose (`dw-ll_ucoco_384.onnx`); without + // attribute support the node falls back to CPU and fragments the CoreML + // partition. + std::unordered_map domain_to_version{{kOnnxDomain, 11}}; + onnxruntime::Model model("split11_uneven_attribute", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + // Input X: {1, 9} float + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(9); + + // Outputs along axis=1 with split=[4, 3, 2]: {1,4}, {1,3}, {1,2} + auto make_output_type = [](int64_t split_size) { + ONNX_NAMESPACE::TypeProto t; + t.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* s = t.mutable_tensor_type()->mutable_shape(); + s->add_dim()->set_dim_value(1); + s->add_dim()->set_dim_value(split_size); + return t; + }; + ONNX_NAMESPACE::TypeProto out0_type = make_output_type(4); + ONNX_NAMESPACE::TypeProto out1_type = make_output_type(3); + ONNX_NAMESPACE::TypeProto out2_type = make_output_type(2); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &out0_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &out1_type); + auto& out2_arg = graph.GetOrCreateNodeArg("Y2", &out2_type); + + auto& node = graph.AddNode("split11_uneven", "Split", "Split-11 with uneven 'split' attribute", + {&input_arg}, {&out0_arg, &out1_arg, &out2_arg}); + node.AddAttribute("axis", static_cast(1)); + node.AddAttribute("split", std::vector{4, 3, 2}); + + ASSERT_STATUS_OK(graph.Resolve()); + +#if defined(__APPLE__) + std::vector dims = {1, 9}; + std::vector input_data = {0.5f, -1.0f, 2.25f, -3.5f, 4.0f, -0.125f, 7.5f, -8.0f, 0.0f}; + OrtValue ml_value_x; + AllocatorPtr allocator = CPUAllocator::DefaultInstance(); + CreateMLValue(allocator, dims, input_data, &ml_value_x); + + NameMLValMap feeds; + feeds.insert(std::make_pair("X", ml_value_x)); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + + RunAndVerifyOutputsWithEP(model_span, "Split11UnevenAttribute_NN", + MakeCoreMLExecutionProvider(), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); + RunAndVerifyOutputsWithEP(model_span, "Split11UnevenAttribute_MLProgram", + MakeCoreMLExecutionProvider("MLProgram"), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); +#else + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +TEST(CoreMLExecutionProviderTest, Split11EvenAttribute) { + // Even sizes via attribute — exercises the split_sizes path with uniform + // values rather than the fall-through num_splits path. + std::unordered_map domain_to_version{{kOnnxDomain, 11}}; + onnxruntime::Model model("split11_even_attribute", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(6); + + ONNX_NAMESPACE::TypeProto output_type; + output_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* output_shape = output_type.mutable_tensor_type()->mutable_shape(); + output_shape->add_dim()->set_dim_value(1); + output_shape->add_dim()->set_dim_value(3); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &output_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &output_type); + + auto& node = graph.AddNode("split11_even", "Split", "Split-11 with even 'split' attribute", + {&input_arg}, {&out0_arg, &out1_arg}); + node.AddAttribute("axis", static_cast(1)); + node.AddAttribute("split", std::vector{3, 3}); + + ASSERT_STATUS_OK(graph.Resolve()); + +#if defined(__APPLE__) + std::vector dims = {1, 6}; + std::vector input_data = {1.0f, -2.0f, 3.0f, -4.0f, 5.0f, -6.0f}; + OrtValue ml_value_x; + AllocatorPtr allocator = CPUAllocator::DefaultInstance(); + CreateMLValue(allocator, dims, input_data, &ml_value_x); + + NameMLValMap feeds; + feeds.insert(std::make_pair("X", ml_value_x)); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + + RunAndVerifyOutputsWithEP(model_span, "Split11EvenAttribute_NN", + MakeCoreMLExecutionProvider(), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); + RunAndVerifyOutputsWithEP(model_span, "Split11EvenAttribute_MLProgram", + MakeCoreMLExecutionProvider("MLProgram"), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); +#else + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +TEST(CoreMLExecutionProviderTest, Split11NoAttributeEven) { + // No `split` attribute, axis size divides evenly: must fall through to the + // num_splits = num_outputs branch. + std::unordered_map domain_to_version{{kOnnxDomain, 11}}; + onnxruntime::Model model("split11_no_attribute_even", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(8); + + ONNX_NAMESPACE::TypeProto output_type; + output_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* output_shape = output_type.mutable_tensor_type()->mutable_shape(); + output_shape->add_dim()->set_dim_value(1); + output_shape->add_dim()->set_dim_value(4); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &output_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &output_type); + + auto& node = graph.AddNode("split11_no_attr", "Split", "Split-11 with no 'split' attribute", + {&input_arg}, {&out0_arg, &out1_arg}); + node.AddAttribute("axis", static_cast(1)); + + ASSERT_STATUS_OK(graph.Resolve()); + +#if defined(__APPLE__) + std::vector dims = {1, 8}; + std::vector input_data = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f}; + OrtValue ml_value_x; + AllocatorPtr allocator = CPUAllocator::DefaultInstance(); + CreateMLValue(allocator, dims, input_data, &ml_value_x); + + NameMLValMap feeds; + feeds.insert(std::make_pair("X", ml_value_x)); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + + RunAndVerifyOutputsWithEP(model_span, "Split11NoAttributeEven_NN", + MakeCoreMLExecutionProvider(), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); + RunAndVerifyOutputsWithEP(model_span, "Split11NoAttributeEven_MLProgram", + MakeCoreMLExecutionProvider("MLProgram"), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); +#else + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +TEST(CoreMLExecutionProviderTest, Split13UnevenInput) { + // Parity with Split11UnevenAttribute: same shapes and split sizes, but using + // the opset-13 input form ('split' as a constant initializer) instead of the + // pre-13 attribute form. Locks in that the existing input path still works. + std::unordered_map domain_to_version{{kOnnxDomain, 13}}; + onnxruntime::Model model("split13_uneven_input", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(9); + + auto make_output_type = [](int64_t split_size) { + ONNX_NAMESPACE::TypeProto t; + t.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* s = t.mutable_tensor_type()->mutable_shape(); + s->add_dim()->set_dim_value(1); + s->add_dim()->set_dim_value(split_size); + return t; + }; + ONNX_NAMESPACE::TypeProto out0_type = make_output_type(4); + ONNX_NAMESPACE::TypeProto out1_type = make_output_type(3); + ONNX_NAMESPACE::TypeProto out2_type = make_output_type(2); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &out0_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &out1_type); + auto& out2_arg = graph.GetOrCreateNodeArg("Y2", &out2_type); + + ONNX_NAMESPACE::TensorProto split_init; + split_init.set_name("split_sizes"); + split_init.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + split_init.add_dims(3); + for (auto v : std::vector{4, 3, 2}) { + split_init.add_int64_data(v); + } + graph.AddInitializedTensor(split_init); + auto& split_arg = graph.GetOrCreateNodeArg("split_sizes", nullptr); + + auto& node = graph.AddNode("split13_uneven", "Split", "Split-13 with uneven 'split' input", + {&input_arg, &split_arg}, {&out0_arg, &out1_arg, &out2_arg}); + node.AddAttribute("axis", static_cast(1)); + + ASSERT_STATUS_OK(graph.Resolve()); + +#if defined(__APPLE__) + std::vector dims = {1, 9}; + std::vector input_data = {0.5f, -1.0f, 2.25f, -3.5f, 4.0f, -0.125f, 7.5f, -8.0f, 0.0f}; + OrtValue ml_value_x; + AllocatorPtr allocator = CPUAllocator::DefaultInstance(); + CreateMLValue(allocator, dims, input_data, &ml_value_x); + + NameMLValMap feeds; + feeds.insert(std::make_pair("X", ml_value_x)); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + + RunAndVerifyOutputsWithEP(model_span, "Split13UnevenInput_NN", + MakeCoreMLExecutionProvider(), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); + RunAndVerifyOutputsWithEP(model_span, "Split13UnevenInput_MLProgram", + MakeCoreMLExecutionProvider("MLProgram"), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); +#else + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +TEST(CoreMLExecutionProviderTest, Split13EvenInput) { + // Parity with Split11EvenAttribute via the opset-13 input form. + std::unordered_map domain_to_version{{kOnnxDomain, 13}}; + onnxruntime::Model model("split13_even_input", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(6); + + ONNX_NAMESPACE::TypeProto output_type; + output_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* output_shape = output_type.mutable_tensor_type()->mutable_shape(); + output_shape->add_dim()->set_dim_value(1); + output_shape->add_dim()->set_dim_value(3); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &output_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &output_type); + + ONNX_NAMESPACE::TensorProto split_init; + split_init.set_name("split_sizes"); + split_init.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + split_init.add_dims(2); + for (auto v : std::vector{3, 3}) { + split_init.add_int64_data(v); + } + graph.AddInitializedTensor(split_init); + auto& split_arg = graph.GetOrCreateNodeArg("split_sizes", nullptr); + + auto& node = graph.AddNode("split13_even", "Split", "Split-13 with even 'split' input", + {&input_arg, &split_arg}, {&out0_arg, &out1_arg}); + node.AddAttribute("axis", static_cast(1)); + + ASSERT_STATUS_OK(graph.Resolve()); + +#if defined(__APPLE__) + std::vector dims = {1, 6}; + std::vector input_data = {1.0f, -2.0f, 3.0f, -4.0f, 5.0f, -6.0f}; + OrtValue ml_value_x; + AllocatorPtr allocator = CPUAllocator::DefaultInstance(); + CreateMLValue(allocator, dims, input_data, &ml_value_x); + + NameMLValMap feeds; + feeds.insert(std::make_pair("X", ml_value_x)); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + + RunAndVerifyOutputsWithEP(model_span, "Split13EvenInput_NN", + MakeCoreMLExecutionProvider(), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); + RunAndVerifyOutputsWithEP(model_span, "Split13EvenInput_MLProgram", + MakeCoreMLExecutionProvider("MLProgram"), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); +#else + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +TEST(CoreMLExecutionProviderTest, Split13NoSplitInputEven) { + // Parity with Split11NoAttributeEven: opset 13 with no 'split' input must + // fall through to the SinceVersion() < 18 even-split branch (num_splits = + // num_outputs) for both emitters. + std::unordered_map domain_to_version{{kOnnxDomain, 13}}; + onnxruntime::Model model("split13_no_split_input_even", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(8); + + ONNX_NAMESPACE::TypeProto output_type; + output_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* output_shape = output_type.mutable_tensor_type()->mutable_shape(); + output_shape->add_dim()->set_dim_value(1); + output_shape->add_dim()->set_dim_value(4); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &output_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &output_type); + + auto& node = graph.AddNode("split13_no_split_input", "Split", "Split-13 with no 'split' input", + {&input_arg}, {&out0_arg, &out1_arg}); + node.AddAttribute("axis", static_cast(1)); + + ASSERT_STATUS_OK(graph.Resolve()); + +#if defined(__APPLE__) + std::vector dims = {1, 8}; + std::vector input_data = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f}; + OrtValue ml_value_x; + AllocatorPtr allocator = CPUAllocator::DefaultInstance(); + CreateMLValue(allocator, dims, input_data, &ml_value_x); + + NameMLValMap feeds; + feeds.insert(std::make_pair("X", ml_value_x)); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + + RunAndVerifyOutputsWithEP(model_span, "Split13NoSplitInputEven_NN", + MakeCoreMLExecutionProvider(), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); + RunAndVerifyOutputsWithEP(model_span, "Split13NoSplitInputEven_MLProgram", + MakeCoreMLExecutionProvider("MLProgram"), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); +#else + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +TEST(CoreMLExecutionProviderTest, Split7UnevenAttribute) { + // Opset 7 (≤ 10) parity check. The builder advertises support from opset 1 + // and reads the 'split' attribute; the Split11* tests cover opset 11, this + // test covers the opset 7-10 range explicitly. + std::unordered_map domain_to_version{{kOnnxDomain, 7}}; + onnxruntime::Model model("split7_uneven_attribute", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(9); + + auto make_output_type = [](int64_t split_size) { + ONNX_NAMESPACE::TypeProto t; + t.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* s = t.mutable_tensor_type()->mutable_shape(); + s->add_dim()->set_dim_value(1); + s->add_dim()->set_dim_value(split_size); + return t; + }; + ONNX_NAMESPACE::TypeProto out0_type = make_output_type(4); + ONNX_NAMESPACE::TypeProto out1_type = make_output_type(3); + ONNX_NAMESPACE::TypeProto out2_type = make_output_type(2); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &out0_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &out1_type); + auto& out2_arg = graph.GetOrCreateNodeArg("Y2", &out2_type); + + auto& node = graph.AddNode("split7_uneven", "Split", "Split-7 with uneven 'split' attribute", + {&input_arg}, {&out0_arg, &out1_arg, &out2_arg}); + node.AddAttribute("axis", static_cast(1)); + node.AddAttribute("split", std::vector{4, 3, 2}); + + ASSERT_STATUS_OK(graph.Resolve()); + +#if defined(__APPLE__) + std::vector dims = {1, 9}; + std::vector input_data = {0.5f, -1.0f, 2.25f, -3.5f, 4.0f, -0.125f, 7.5f, -8.0f, 0.0f}; + OrtValue ml_value_x; + AllocatorPtr allocator = CPUAllocator::DefaultInstance(); + CreateMLValue(allocator, dims, input_data, &ml_value_x); + + NameMLValMap feeds; + feeds.insert(std::make_pair("X", ml_value_x)); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + + RunAndVerifyOutputsWithEP(model_span, "Split7UnevenAttribute_NN", + MakeCoreMLExecutionProvider(), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); + RunAndVerifyOutputsWithEP(model_span, "Split7UnevenAttribute_MLProgram", + MakeCoreMLExecutionProvider("MLProgram"), + feeds, + EPVerificationParams{ExpectedEPNodeAssignment::All}); +#else + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +TEST(CoreMLExecutionProviderTest, Split11ZeroSplitValueNotSupported) { + // Negative: a zero entry in the 'split' attribute must be rejected so the + // node falls back to CPU. Sum still equals the axis size, so this exercises + // the non-positive value check specifically. + std::unordered_map domain_to_version{{kOnnxDomain, 11}}; + onnxruntime::Model model("split11_zero_split_value", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(9); + + auto make_output_type = [](int64_t split_size) { + ONNX_NAMESPACE::TypeProto t; + t.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* s = t.mutable_tensor_type()->mutable_shape(); + s->add_dim()->set_dim_value(1); + s->add_dim()->set_dim_value(split_size); + return t; + }; + ONNX_NAMESPACE::TypeProto out0_type = make_output_type(3); + ONNX_NAMESPACE::TypeProto out1_type = make_output_type(0); + ONNX_NAMESPACE::TypeProto out2_type = make_output_type(6); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &out0_type); + auto& out1_arg = graph.GetOrCreateNodeArg("Y1", &out1_type); + auto& out2_arg = graph.GetOrCreateNodeArg("Y2", &out2_type); + + auto& node = graph.AddNode("split11_zero", "Split", "Split-11 with a zero 'split' entry", + {&input_arg}, {&out0_arg, &out1_arg, &out2_arg}); + node.AddAttribute("axis", static_cast(1)); + node.AddAttribute("split", std::vector{3, 0, 6}); + + ASSERT_STATUS_OK(graph.Resolve()); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::None); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::None); +} + +TEST(CoreMLExecutionProviderTest, Split11SingleOutputNotSupported) { + // Negative: a Split node with only 1 output. CoreML SplitND requires ≥2, + // so the attribute-form path's split_attr->size() < 2 check rejects it. + // ONNX schema allows variadic ≥1 outputs and CPU's Split kernel accepts + // a single output, so this case can be observed via partition assertion. + std::unordered_map domain_to_version{{kOnnxDomain, 11}}; + onnxruntime::Model model("split11_single_output", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto input_type; + input_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* input_shape = input_type.mutable_tensor_type()->mutable_shape(); + input_shape->add_dim()->set_dim_value(1); + input_shape->add_dim()->set_dim_value(5); + + ONNX_NAMESPACE::TypeProto output_type; + output_type.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + output_type.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(1); + output_type.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(5); + + auto& input_arg = graph.GetOrCreateNodeArg("X", &input_type); + auto& out0_arg = graph.GetOrCreateNodeArg("Y0", &output_type); + + auto& node = graph.AddNode("split11_single_output", "Split", + "Split-11 with a single output", + {&input_arg}, {&out0_arg}); + node.AddAttribute("axis", static_cast(1)); + node.AddAttribute("split", std::vector{5}); + + ASSERT_STATUS_OK(graph.Resolve()); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + gsl::span model_span{reinterpret_cast(model_data.data()), model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::None); + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::None); +} + #endif // !(ORT_MINIMAL_BUILD) } // namespace test } // namespace onnxruntime