Skip to content

Commit 413106c

Browse files
williclaude
andcommitted
Fix ReplicationPad3d failing at CoreML ML Program runtime (#2571)
CoreML's ML Program runtime rejects non-constant padding modes (reflect, replicate) when more than 2 dimensions are padded, with: "Padding for more than two dimensions only supports constant mode". Previously, torch.nn.ReplicationPad3d converted silently but failed at prediction time. Add a new mil_backend pass `split_non_constant_pads` that decomposes such pad ops into sequential pads each covering at most 2 dimensions, which CoreML supports. The pass runs in _BACKEND_MIL_PASSES after merge_consecutive_paddings to prevent splits from being merged back. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f272aa3 commit 413106c

4 files changed

Lines changed: 133 additions & 1 deletion

File tree

coremltools/converters/mil/backend/mil/passes/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
# found in the LICENSE.txt file or at https://opensource.org/licenses/BSD-3-Clause
55

66
from . import (adjust_io_to_supported_types, fuse_activation_silu,
7-
insert_image_preprocessing_op, sanitize_name_strings)
7+
insert_image_preprocessing_op, sanitize_name_strings,
8+
split_non_constant_pads)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright (c) 2025, Apple Inc. All rights reserved.
2+
#
3+
# Use of this source code is governed by a BSD-3-clause license that can be
4+
# found in the LICENSE.txt file or at https://opensource.org/licenses/BSD-3-Clause
5+
6+
import numpy as np
7+
8+
from coremltools.converters.mil.mil import Builder as mb
9+
from coremltools.converters.mil.mil.passes.graph_pass import AbstractGraphPass
10+
from coremltools.converters.mil.mil.passes.helper import block_context_manager
11+
from coremltools.converters.mil.mil.passes.pass_registry import register_pass
12+
13+
14+
@register_pass(namespace="mil_backend")
15+
class split_non_constant_pads(AbstractGraphPass):
16+
"""
17+
Split ``pad`` ops that use non-constant modes (``reflect``, ``replicate``)
18+
and pad more than two dimensions, because the CoreML ML Program runtime
19+
rejects such ops with the error:
20+
"Padding for more than two dimensions only supports `constant` mode".
21+
22+
Each split step pads at most two dimensions, which CoreML supports for all
23+
padding modes.
24+
25+
.. code-block::
26+
27+
Input:
28+
x(1, 3, 4, 4, 4) -> pad([0,0, 0,0, 2,2, 2,2, 2,2], mode="replicate") -> (1, 3, 8, 8, 8)
29+
30+
Output:
31+
x(1, 3, 4, 4, 4) -> pad([0,0, 0,0, 2,2, 2,2, 0,0], mode="replicate") -> (1, 3, 8, 8, 4)
32+
-> pad([0,0, 0,0, 0,0, 0,0, 2,2], mode="replicate") -> (1, 3, 8, 8, 8)
33+
"""
34+
35+
def apply(self, prog):
36+
for f in prog.functions.values():
37+
self._split_pads_block(f)
38+
39+
@block_context_manager
40+
def _split_pads_block(self, block):
41+
for op in list(block.operations):
42+
for b in op.blocks:
43+
self._split_pads_block(b)
44+
45+
if op.op_type != "pad":
46+
continue
47+
48+
mode = op.inputs["mode"].val
49+
if mode == "constant":
50+
continue
51+
52+
pad = op.inputs["pad"].val
53+
if pad is None:
54+
continue
55+
56+
# Find dimensions with non-zero padding
57+
pad_pairs = pad.reshape(-1, 2)
58+
nonzero_dims = [
59+
i for i, (before, after) in enumerate(pad_pairs) if before != 0 or after != 0
60+
]
61+
62+
if len(nonzero_dims) <= 2:
63+
continue
64+
65+
# Split into sequential pads, each covering at most 2 dimensions
66+
x = op.inputs["x"]
67+
constant_val = op.inputs["constant_val"].val
68+
num_chunks = (len(nonzero_dims) + 1) // 2
69+
result = x
70+
for chunk_idx, chunk_start in enumerate(range(0, len(nonzero_dims), 2)):
71+
chunk_dims = nonzero_dims[chunk_start : chunk_start + 2]
72+
chunk_pad = np.zeros_like(pad)
73+
for dim in chunk_dims:
74+
chunk_pad[2 * dim] = pad_pairs[dim][0]
75+
chunk_pad[2 * dim + 1] = pad_pairs[dim][1]
76+
77+
is_last = chunk_idx == num_chunks - 1
78+
step_name = op.name if is_last else f"{op.name}_split_{chunk_idx}"
79+
result = mb.pad(
80+
x=result,
81+
pad=chunk_pad,
82+
mode=mode,
83+
constant_val=constant_val,
84+
before_op=op,
85+
name=step_name,
86+
)
87+
88+
op.enclosing_block.replace_uses_of_var_after_op(
89+
anchor_op=op, old_var=op.outputs[0], new_var=result
90+
)
91+
op.enclosing_block.remove_ops([op])

coremltools/converters/mil/frontend/torch/test/test_torch_ops.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11257,6 +11257,45 @@ def test_constant_pad_3d(self, compute_unit, backend, frontend):
1125711257
input_shape, model, backend=backend, compute_unit=compute_unit, frontend=frontend
1125811258
)
1125911259

11260+
@pytest.mark.parametrize(
11261+
"compute_unit, backend, frontend",
11262+
itertools.product(
11263+
compute_units,
11264+
backends,
11265+
frontends,
11266+
),
11267+
)
11268+
def test_replication_pad3d(self, compute_unit, backend, frontend):
11269+
"""Regression test for https://github.com/apple/coremltools/issues/2571.
11270+
ReplicationPad3d should work correctly rather than producing a model that
11271+
fails at CoreML runtime with 'Padding for more than two dimensions only
11272+
supports constant mode'."""
11273+
if backend[0] == "neuralnetwork":
11274+
pytest.skip("NeuralNetwork backend does not support replicate padding for >2 spatial dims")
11275+
input_shape = (1, 3, 4, 4, 4)
11276+
model = torch.nn.ReplicationPad3d(2).eval()
11277+
self.run_compare_torch(
11278+
input_shape, model, backend=backend, compute_unit=compute_unit, frontend=frontend
11279+
)
11280+
11281+
@pytest.mark.parametrize(
11282+
"compute_unit, backend, frontend",
11283+
itertools.product(
11284+
compute_units,
11285+
backends,
11286+
frontends,
11287+
),
11288+
)
11289+
def test_replication_pad3d_asymmetric(self, compute_unit, backend, frontend):
11290+
"""Test ReplicationPad3d with asymmetric padding (different values per side)."""
11291+
if backend[0] == "neuralnetwork":
11292+
pytest.skip("NeuralNetwork backend does not support replicate padding for >2 spatial dims")
11293+
input_shape = (1, 3, 4, 5, 6)
11294+
model = torch.nn.ReplicationPad3d((1, 2, 3, 1, 2, 3)).eval()
11295+
self.run_compare_torch(
11296+
input_shape, model, backend=backend, compute_unit=compute_unit, frontend=frontend
11297+
)
11298+
1126011299

1126111300
class TestMaskedFill(TorchBaseTest):
1126211301
@pytest.mark.parametrize(

coremltools/converters/mil/mil/passes/pass_pipeline.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@
200200
"common::const_deduplication", # after all consts have been settled
201201
"common::cast_optimization",
202202
"common::dead_code_elimination",
203+
"mil_backend::split_non_constant_pads", # must come before sanitize_name_strings
203204
"mil_backend::sanitize_name_strings",
204205
"common::dedup_op_and_var_names",
205206
"nn_backend::handle_unused_inputs", # must come after dce.

0 commit comments

Comments
 (0)