Skip to content

Commit 71cfbb0

Browse files
maxwbuckleyclaude
andauthored
[CoreML EP] Add Sin and Cos unary ops (#28596)
### Summary Lower ONNX `Sin` and `Cos` to the CoreML ML Program `sin` / `cos` elementwise ops via the existing `UnaryOpBuilder`, registered 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. ### Why `Sin` / `Cos` form the sinusoidal timestep embedding of diffusion UNets. Supporting them keeps that prologue on CoreML instead of splitting the graph — a tiny Stable-Diffusion UNet goes from **2 CoreML partitions → 1, zero graph breaks** with this change alone. This PR is **independent** of the rest of the series (it touches only the unary builder) and can be reviewed/merged in any order. ### 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`. ### Series — CoreML EP coverage for transformer / diffusion graphs - #28595 — Support bool Cast in ML Program *(prerequisite)* - **#28596 — Add Sin and Cos unary ops** *(this PR — independent)* - #28597 — Add Where and And builders *(depends on #28595)* - #28598 — Add GatherND builder *(depends on #28595)* Together with #28278 (scalar-`Gather`), the series takes BERT / GPT-2 / ViT / diffusion-UNet graphs — tiny and full-size — from 2 CoreML partitions to 1, with zero graph breaks. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b0924f4 commit 71cfbb0

4 files changed

Lines changed: 72 additions & 2 deletions

File tree

onnxruntime/core/providers/coreml/builders/impl/unary_op_builder.cc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Status UnaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const
4444
coreml_op_type = "round";
4545
} else if (op_type == "Exp") {
4646
coreml_op_type = "exp";
47+
} else if (op_type == "Sin") {
48+
coreml_op_type = "sin";
49+
} else if (op_type == "Cos") {
50+
coreml_op_type = "cos";
4751
} else if (op_type == "Ceil") {
4852
coreml_op_type = "ceil";
4953
} else {
@@ -89,8 +93,11 @@ Status UnaryOpBuilder::AddToModelBuilderImpl(ModelBuilder& model_builder, const
8993
bool UnaryOpBuilder::IsOpSupportedImpl(const Node& node, const OpBuilderInputParams& input_params,
9094
const logging::Logger& /*logger*/) const {
9195
if (!input_params.create_mlprogram) {
92-
if (node.OpType() == "Erf" || node.OpType() == "Round" || node.OpType() == "Exp" ||
93-
node.OpType() == "Ceil") {
96+
// These ops only have an ML Program lowering; the NeuralNetwork
97+
// UnaryFunctionLayerParams has no equivalent.
98+
const auto& op_type = node.OpType();
99+
if (op_type == "Erf" || op_type == "Round" || op_type == "Exp" ||
100+
op_type == "Sin" || op_type == "Cos" || op_type == "Ceil") {
94101
return false;
95102
}
96103
}

onnxruntime/core/providers/coreml/builders/op_builder_factory.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ static OpBuilderRegistrations CreateOpBuilderRegistrations() {
3838
CreateUnaryOpBuilder("Round", op_registrations);
3939
CreateUnaryOpBuilder("Sqrt", op_registrations);
4040
CreateUnaryOpBuilder("Exp", op_registrations);
41+
CreateUnaryOpBuilder("Sin", op_registrations);
42+
CreateUnaryOpBuilder("Cos", op_registrations);
4143
CreateUnaryOpBuilder("Ceil", op_registrations);
4244

4345
// Binary elementwise ops

onnxruntime/test/providers/coreml/coreml_basic_test.cc

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,6 +2360,55 @@ TEST(CoreMLExecutionProviderTest, Split11SingleOutputNotSupported) {
23602360
TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::None);
23612361
}
23622362

2363+
namespace {
2364+
// Single-input model with both Sin and Cos consuming `X`, used by the
2365+
// Sin/Cos tests below.
2366+
std::string MakeSinCosModelData() {
2367+
onnxruntime::Model model("sin_cos_test", false, DefaultLoggingManager().DefaultLogger());
2368+
auto& graph = model.MainGraph();
2369+
2370+
ONNX_NAMESPACE::TypeProto float_tensor;
2371+
float_tensor.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
2372+
auto* shape = float_tensor.mutable_tensor_type()->mutable_shape();
2373+
shape->add_dim()->set_dim_value(1);
2374+
shape->add_dim()->set_dim_value(6);
2375+
2376+
auto& x = graph.GetOrCreateNodeArg("X", &float_tensor);
2377+
auto& sin_out = graph.GetOrCreateNodeArg("Sin_out", &float_tensor);
2378+
auto& cos_out = graph.GetOrCreateNodeArg("Cos_out", &float_tensor);
2379+
graph.AddNode("sin", "Sin", "sin node", {&x}, {&sin_out});
2380+
graph.AddNode("cos", "Cos", "cos node", {&x}, {&cos_out});
2381+
2382+
ORT_THROW_IF_ERROR(graph.Resolve());
2383+
std::string model_data;
2384+
model.ToProto().SerializeToString(&model_data);
2385+
return model_data;
2386+
}
2387+
} // namespace
2388+
2389+
// Sin and Cos are lowered to the ML Program 'sin' / 'cos' ops.
2390+
TEST(CoreMLExecutionProviderTest, SinCos_MLProgram) {
2391+
const std::string model_data = MakeSinCosModelData();
2392+
gsl::span<const std::byte> model_span{reinterpret_cast<const std::byte*>(model_data.data()),
2393+
model_data.size()};
2394+
2395+
#if defined(__APPLE__)
2396+
std::vector<int64_t> dims = {1, 6};
2397+
std::vector<float> values = {-2.0f, -0.5f, 0.0f, 0.5f, 1.0f, 2.0f};
2398+
OrtValue x_val;
2399+
CreateMLValue<float>(CPUAllocator::DefaultInstance(), dims, values, &x_val);
2400+
NameMLValMap feeds;
2401+
feeds.insert(std::make_pair("X", x_val));
2402+
2403+
EPVerificationParams params{};
2404+
params.ep_node_assignment = ExpectedEPNodeAssignment::All;
2405+
RunAndVerifyOutputsWithEP(model_span, CurrentTestName(),
2406+
MakeCoreMLExecutionProvider("MLProgram"), feeds, params);
2407+
#else
2408+
TestModelLoad(model_span, MakeCoreMLExecutionProvider("MLProgram"), ExpectedEPNodeAssignment::All);
2409+
#endif
2410+
}
2411+
23632412
TEST(CoreMLExecutionProviderTest, GatherScalarIndicesAxis1) {
23642413
// ai.onnx:Gather with rank-0 (scalar) 'indices'. ONNX output rank =
23652414
// data_rank + indices_rank - 1 = 2. The CoreML builder internally promotes
@@ -2437,6 +2486,16 @@ TEST(CoreMLExecutionProviderTest, GatherScalarIndicesAxis1) {
24372486
#endif
24382487
}
24392488

2489+
// Sin/Cos only have an ML Program lowering (the NeuralNetwork
2490+
// UnaryFunctionLayerParams has no sin/cos), so on the NeuralNetwork format
2491+
// they must fall back to CPU rather than be claimed.
2492+
TEST(CoreMLExecutionProviderTest, SinCosNeuralNetworkNotSupported) {
2493+
const std::string model_data = MakeSinCosModelData();
2494+
gsl::span<const std::byte> model_span{reinterpret_cast<const std::byte*>(model_data.data()),
2495+
model_data.size()};
2496+
TestModelLoad(model_span, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::None);
2497+
}
2498+
24402499
TEST(CoreMLExecutionProviderTest, GatherScalarIndicesAxis0) {
24412500
// Scalar Gather along axis 0 — squeeze axis is 0; covers a different
24422501
// squeeze position than the axis=1 test.

tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Keep in sync with doco generated from /docs/execution-providers/CoreML-Execution
1111
|ai.onnx:Concat||
1212
|ai.onnx:Conv|Only 1D/2D Conv is supported.<br/>Bias if provided must be constant.|
1313
|ai.onnx:ConvTranspose|Weight and bias must be constant.<br/>padding_type of SAME_UPPER/SAME_LOWER is not supported.<br/>kernel_shape must have default values.<br/>output_shape is not supported.<br/>output_padding must have default values.|
14+
|ai.onnx:Cos||
1415
|ai.onnx:DepthToSpace|If 'mode' is 'CRD' the input must have a fixed shape.|
1516
|ai.onnx:Ceil||
1617
|ai.onnx:Div||
@@ -43,6 +44,7 @@ Keep in sync with doco generated from /docs/execution-providers/CoreML-Execution
4344
|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.|
4445
|ai.onnx:Round||
4546
|ai.onnx:Shape||
47+
|ai.onnx:Sin||
4648
|ai.onnx:Slice|starts/ends/axes/steps must be constant initializers.|
4749
|ai.onnx:Softplus||
4850
|ai.onnx:Split|If provided, `splits` must be constant.|

0 commit comments

Comments
 (0)