From d4bea39ad846c4761bfdb109991c8fbaba6d57dd Mon Sep 17 00:00:00 2001 From: Max Buckley Date: Wed, 20 May 2026 16:12:44 +0200 Subject: [PATCH] [CoreML EP] Add Sin and Cos unary ops Lower ONNX Sin and Cos to the CoreML ML Program `sin` / `cos` elementwise ops via the existing UnaryOpBuilder, and register them in the op builder factory. Like Erf/Round/Exp, these have no NeuralNetwork lowering (UnaryFunctionLayerParams has no sin/cos), so IsOpSupportedImpl rejects them on the NeuralNetwork format. These appear in the timestep (sinusoidal position) embedding of diffusion UNets; supporting them lets that prologue stay on CoreML instead of splitting the graph into separate partitions. Tests (coreml_basic_test.cc): - SinCos_MLProgram: a Sin+Cos graph runs fully on CoreML and matches the CPU reference. - SinCosNeuralNetworkNotSupported: the same graph falls back to CPU on the NeuralNetwork format. Doc: coreml_supported_mlprogram_ops.md lists Sin and Cos. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../coreml/builders/impl/unary_op_builder.cc | 10 +++- .../coreml/builders/op_builder_factory.cc | 2 + .../providers/coreml/coreml_basic_test.cc | 59 +++++++++++++++++++ .../apple/coreml_supported_mlprogram_ops.md | 2 + 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/onnxruntime/core/providers/coreml/builders/impl/unary_op_builder.cc b/onnxruntime/core/providers/coreml/builders/impl/unary_op_builder.cc index 09ce25fd29778..df3fcd5ba3aec 100644 --- a/onnxruntime/core/providers/coreml/builders/impl/unary_op_builder.cc +++ b/onnxruntime/core/providers/coreml/builders/impl/unary_op_builder.cc @@ -39,6 +39,10 @@ Status UnaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const coreml_op_type = "round"; } else if (op_type == "Exp") { coreml_op_type = "exp"; + } else if (op_type == "Sin") { + coreml_op_type = "sin"; + } else if (op_type == "Cos") { + coreml_op_type = "cos"; } else { return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "UnaryOpBuilder::AddToModelBuilderImpl, unexpected op: ", op_type); @@ -82,7 +86,11 @@ Status UnaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const bool UnaryOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params, const logging::Logger& /*logger*/) const { if (!input_params.create_mlprogram) { - if (node.OpType() == "Erf" || node.OpType() == "Round" || node.OpType() == "Exp") { + // These ops only have an ML Program lowering; the NeuralNetwork + // UnaryFunctionLayerParams has no equivalent. + const auto& op_type = node.OpType(); + if (op_type == "Erf" || op_type == "Round" || op_type == "Exp" || + op_type == "Sin" || op_type == "Cos") { return false; } } diff --git a/onnxruntime/core/providers/coreml/builders/op_builder_factory.cc b/onnxruntime/core/providers/coreml/builders/op_builder_factory.cc index 6f465774a3c3c..52363567dd957 100644 --- a/onnxruntime/core/providers/coreml/builders/op_builder_factory.cc +++ b/onnxruntime/core/providers/coreml/builders/op_builder_factory.cc @@ -38,6 +38,8 @@ static OpBuilderRegistrations CreateOpBuilderRegistrations() { CreateUnaryOpBuilder("Round", op_registrations); CreateUnaryOpBuilder("Sqrt", op_registrations); CreateUnaryOpBuilder("Exp", op_registrations); + CreateUnaryOpBuilder("Sin", op_registrations); + CreateUnaryOpBuilder("Cos", op_registrations); // Binary elementwise ops CreateBinaryOpBuilder("Add", op_registrations); diff --git a/onnxruntime/test/providers/coreml/coreml_basic_test.cc b/onnxruntime/test/providers/coreml/coreml_basic_test.cc index b6e1545d6f319..31a89f0d908eb 100644 --- a/onnxruntime/test/providers/coreml/coreml_basic_test.cc +++ b/onnxruntime/test/providers/coreml/coreml_basic_test.cc @@ -1911,6 +1911,65 @@ TEST(CoreMLExecutionProviderTest, Split11SingleOutputNotSupported) { TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::None); } +namespace { +// Single-input model with both Sin and Cos consuming `X`, used by the +// Sin/Cos tests below. +std::string MakeSinCosModelData() { + onnxruntime::Model model("sin_cos_test", false, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto float_tensor; + float_tensor.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto* shape = float_tensor.mutable_tensor_type()->mutable_shape(); + shape->add_dim()->set_dim_value(1); + shape->add_dim()->set_dim_value(6); + + auto& x = graph.GetOrCreateNodeArg("X", &float_tensor); + auto& sin_out = graph.GetOrCreateNodeArg("Sin_out", &float_tensor); + auto& cos_out = graph.GetOrCreateNodeArg("Cos_out", &float_tensor); + graph.AddNode("sin", "Sin", "sin node", {&x}, {&sin_out}); + graph.AddNode("cos", "Cos", "cos node", {&x}, {&cos_out}); + + ORT_THROW_IF_ERROR(graph.Resolve()); + std::string model_data; + model.ToProto().SerializeToString(&model_data); + return model_data; +} +} // namespace + +// Sin and Cos are lowered to the ML Program 'sin' / 'cos' ops. +TEST(CoreMLExecutionProviderTest, SinCos_MLProgram) { + const std::string model_data = MakeSinCosModelData(); + gsl::span model_span{reinterpret_cast(model_data.data()), + model_data.size()}; + +#if defined(__APPLE__) + std::vector dims = {1, 6}; + std::vector values = {-2.0f, -0.5f, 0.0f, 0.5f, 1.0f, 2.0f}; + OrtValue x_val; + CreateMLValue(CPUAllocator::DefaultInstance(), dims, values, &x_val); + NameMLValMap feeds; + feeds.insert(std::make_pair("X", x_val)); + + EPVerificationParams params{}; + params.ep_node_assignment = ExpectedEPNodeAssignment::All; + RunAndVerifyOutputsWithEP(model_span, CurrentTestName(), + MakeCoreMLExecutionProvider("MLProgram"), feeds, params); +#else + TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All); +#endif +} + +// Sin/Cos only have an ML Program lowering (the NeuralNetwork +// UnaryFunctionLayerParams has no sin/cos), so on the NeuralNetwork format +// they must fall back to CPU rather than be claimed. +TEST(CoreMLExecutionProviderTest, SinCosNeuralNetworkNotSupported) { + const std::string model_data = MakeSinCosModelData(); + gsl::span model_span{reinterpret_cast(model_data.data()), + model_data.size()}; + TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::None); +} + #endif // !(ORT_MINIMAL_BUILD) } // namespace test } // namespace onnxruntime diff --git a/tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md b/tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md index 395813844906a..a13908fbae00e 100644 --- a/tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md +++ b/tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md @@ -11,6 +11,7 @@ Keep in sync with doco generated from /docs/execution-providers/CoreML-Execution |ai.onnx:Concat|| |ai.onnx:Conv|Only 1D/2D Conv is supported.
Bias if provided must be constant.| |ai.onnx:ConvTranspose|Weight and bias must be constant.
padding_type of SAME_UPPER/SAME_LOWER is not supported.
kernel_shape must have default values.
output_shape is not supported.
output_padding must have default values.| +|ai.onnx:Cos|| |ai.onnx:DepthToSpace|If 'mode' is 'CRD' the input must have a fixed shape.| |ai.onnx:Div|| |ai.onnx:Elu|| @@ -41,6 +42,7 @@ Keep in sync with doco generated from /docs/execution-providers/CoreML-Execution |ai.onnx:Resize|See [resize_op_builder.cc](https://github.com/microsoft/onnxruntime/blob/main/onnxruntime/core/providers/coreml/builders/impl/resize_op_builder.cc) implementation. There are too many permutations to describe the valid combinations.| |ai.onnx:Round|| |ai.onnx:Shape|| +|ai.onnx:Sin|| |ai.onnx:Slice|starts/ends/axes/steps must be constant initializers.| |ai.onnx:Softplus|| |ai.onnx:Split|If provided, `splits` must be constant.|