onnxruntime
cf8e4f55 - [CoreML EP] Support Gather with scalar 'indices' (#28278)

Commit
171 days ago
[CoreML EP] Support Gather with scalar 'indices' (#28278) ### Description The CoreML \`GatherOpBuilder\` rejected rank-0 (scalar) \`indices\` because CoreML's \`gather\` requires rank-1+ indices and the obvious workaround would change the output rank — see the \`// Don't allow scalar 'indices' input.\` comment at \`gather_op_builder.cc:90\`. This PR performs the workaround internally: \`\`\` reshape(indices, shape=[1]) -> indices_1d gather(data, indices_1d, axis) -> data_shape with the gather axis = 1 squeeze(., axes=[axis]) -> ONNX gather output shape \`\`\` …in both the MLProgram and NeuralNetwork emitters. The squeeze restores the original ONNX output rank, so caller-visible Gather semantics are unchanged. \`reshape\` is used rather than \`expand_dims\` because CoreML internally pads scalars and \`expand_dims\` on the padded tensor can push the apparent rank past the rank-5 limit on high-rank \`data\`. Restrictions: - \`data\` must have a fully static shape — we claim a static intermediate shape between gather and squeeze. - \`data\` rank capped at 4. The rank-5 case still trips CoreML's compiler with \`Invalid rank: 6\`, so we keep the conservative bound. Dynamic-shape and rank-5+ scalar Gather still falls back to CPU (preserves the existing \`GatherWithScalarIndices\` test, whose data is dynamic-shape). Fixes #28180. ### Motivation StyleGAN-family generators (StyleGAN, StyleGAN2, GFPGAN, …) select per-layer style codes with a scalar-index Gather. The resulting graph alternates between Gather and the rest of the generator, splitting the CoreML subgraph repeatedly. On GFPGAN-1024 (`[1, 3, 512, 512]`), this PR moves all 16 scalar Gathers off CPU and the model lands on a single CoreML partition. **M3 Max, MLProgram, batch 1, 3 × 100-iter steady-state runs (n=300):** | | Partitions | Mean | StdDev | P50 | P95 | P99 | Max | |---|---|---|---|---|---|---|---| | origin/main | 2 | 89.68 ms | 3.67 | 87.82 | 96.71 | 105.00 | 108.01 | | **this PR** | **1** | **81.77 ms** | **1.85** | **80.97** | **85.98** | **87.24** | **88.03** | **Mean −8.8%, stddev −50%, P99 −17%, max −18.5%** — eliminating the CPU↔CoreML round-trip on every scalar Gather both speeds up the steady state and tightens the tail. Striking secondary effect: the worst-case run with the fix (**88.03 ms**) is faster than the *mean* run without it (**89.68 ms**). Every single fixed inference over n=300 lands below the unfixed average. ### Tests Six new tests in \`onnxruntime/test/providers/coreml/coreml_basic_test.cc\` covering distinct code paths, exercised on both NeuralNetwork and MLProgram emitters where the dtype is supported: - \`GatherScalarIndicesAxis1\` — axis=1, mid-rank squeeze. - \`GatherScalarIndicesAxis0\` — axis=0, leading-axis squeeze. - \`GatherScalarIndicesNegativeAxis\` — axis=-1, exercises \`HandleNegativeAxis\`. - \`GatherScalarIndicesFloat16\` — fp16 data (MLProgram only, as per \`HasSupportedInputsImpl\`). - \`GatherScalarIndicesInt64Data\` — int64 data, both formats. - \`GatherScalarIndicesRank4Data\` — rank-4 data, exercises the supported maximum. Each verifies CoreML output against the CPU EP reference and asserts \`ExpectedEPNodeAssignment::All\`. The existing \`GatherWithScalarIndices\` test (dynamic-shape data) is updated only in its comment to reflect the new precise condition; it still exercises the CPU fall-back as before. All pass locally on macOS 26.3 / M3 Max. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Parents
Loading