From 5d8d2c19b55be20955ae968b617988515fe44e8e Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sun, 17 May 2026 18:51:10 -0700 Subject: [PATCH 1/2] Register __ior__ as alias of bitwise_or `sanitize_op_kind` strips the dunder wrapper from torch op names, so `aten::__or__` becomes `or` and matches `bitwise_or`'s alias. But the in-place form `aten::__ior__` sanitizes to `ior` -- the leading `i` is preserved because `unify_inplace_and_functional` only strips a trailing `_`, and `sanitize_op_kind` only strips the dunder wrapper. The result is that `tensor |= other` fails with "PyTorch convert function for op '__ior__' not implemented", which is the gemma-3-1b-it failure reported in #2584. Add `ior` to the `bitwise_or` alias list (matching how `or`/`xor` are already registered for the post-sanitize form of `__or__`/`__xor__`) and a regression test in `TestBitwiseOr`. Fixes #2584 --- .../converters/mil/frontend/torch/ops.py | 6 ++++- .../mil/frontend/torch/test/test_torch_ops.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/coremltools/converters/mil/frontend/torch/ops.py b/coremltools/converters/mil/frontend/torch/ops.py index d2470423c..3e8a6694d 100644 --- a/coremltools/converters/mil/frontend/torch/ops.py +++ b/coremltools/converters/mil/frontend/torch/ops.py @@ -5796,7 +5796,11 @@ def bitwise_and(context, node): # which torch.export emits for `tensor | tensor` / `tensor ^ tensor`. These are # common when building boolean attention masks (e.g. Gemma combines a causal # mask with a padding mask via __or__). -@register_torch_op(torch_alias=["or"]) +# +# "ior" is the post-sanitize form of "aten::__ior__", the in-place `|=`. +# `sanitize_op_kind` only strips a trailing `_`, so the leading `i` in the +# `__i__` family is preserved -- the alias has to be listed explicitly. +@register_torch_op(torch_alias=["or", "ior"]) def bitwise_or(context, node): _bitwise_as_logical_if_boolean(context, node, "bitwise_or", logical_or) diff --git a/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py b/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py index 00dd7f5f9..d5d9b98dc 100644 --- a/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py +++ b/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py @@ -13754,6 +13754,32 @@ def forward(self, x, y): input_as_shape=False, ) + @pytest.mark.parametrize( + "compute_unit, backend, frontend", + itertools.product(compute_units, backends, frontends), + ) + def test_ior_operator(self, compute_unit, backend, frontend): + # Regression test for issue #2584: tensor.__ior__ (i.e. `x |= y`) + # sanitizes to "ior", which must be registered as an alias of + # bitwise_or. Previously gemma-3-1b-it conversion failed with + # "PyTorch convert function for op '__ior__' not implemented." + class TestModel(torch.nn.Module): + def forward(self, x, y): + x |= y + return x + + input_shape = (2, 3) + input_data_x = torch.rand(*input_shape) > 0.2 + input_data_y = torch.rand(*input_shape) < 0.8 + self.run_compare_torch( + [input_data_x, input_data_y], + TestModel(), + frontend=frontend, + backend=backend, + compute_unit=compute_unit, + input_as_shape=False, + ) + class TestBitwiseXor(TorchBaseTest): @pytest.mark.parametrize( From 0f894fc874eae549e3f3412ecc7581969035681e Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Fri, 22 May 2026 21:46:26 -0700 Subject: [PATCH 2/2] Address review: rewrite test to clone-before-mutate and scope to TorchScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test mutated a user input directly (`x |= y`), which Core ML rejects with a separate "user input mutation" guard regardless of whether the `__ior__` alias is registered -- so the test failed even with the fix. Two changes: * Clone the input before the in-place op (`z = x.clone(); z |= y`), so the model only mutates an intermediate. Confirmed locally that this produces the expected `logical_or` MIL graph and that `run_compare_torch` passes end-to-end (verified after hot-patching the native BlobWriter shim into the dev tree from the 9.0 wheel). * Drop the TorchFrontend parametrization: only TorchScript actually records `aten::__ior__` (which is where the new "ior" alias matters); torch.export decomposes `__ior__` into `clone + bitwise_or.Tensor` before the converter sees it, so testing both frontends just duplicates the `__or__` path. Local regression coverage confirmed: - With the alias removed, the test fails as expected with "PyTorch convert function for op 'ior' not implemented." - With the alias in place, both backend×compute_unit parameterizations pass via `run_compare_torch`. --- .../mil/frontend/torch/test/test_torch_ops.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py b/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py index d5d9b98dc..4b7bb38c8 100644 --- a/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py +++ b/coremltools/converters/mil/frontend/torch/test/test_torch_ops.py @@ -13755,18 +13755,27 @@ def forward(self, x, y): ) @pytest.mark.parametrize( - "compute_unit, backend, frontend", - itertools.product(compute_units, backends, frontends), + "compute_unit, backend", + itertools.product(compute_units, backends), ) - def test_ior_operator(self, compute_unit, backend, frontend): - # Regression test for issue #2584: tensor.__ior__ (i.e. `x |= y`) - # sanitizes to "ior", which must be registered as an alias of - # bitwise_or. Previously gemma-3-1b-it conversion failed with + def test_ior_operator(self, compute_unit, backend): + # Regression test for issue #2584: TorchScript trace of `z |= y` + # records `aten::__ior__`, which sanitizes to "ior" and must be + # registered as an alias of bitwise_or. Previously gemma-3-1b-it + # conversion failed with # "PyTorch convert function for op '__ior__' not implemented." + # + # Notes on scope: + # * Core ML inputs are immutable, so the model clones an input + # before mutating it; without the clone, `run_compare_torch` + # would raise the unrelated user-input-mutation guard. + # * torch.export decomposes `__ior__` into `clone + bitwise_or`, + # so the new alias is only reachable via the TorchScript path. class TestModel(torch.nn.Module): def forward(self, x, y): - x |= y - return x + z = x.clone() + z |= y + return z input_shape = (2, 3) input_data_x = torch.rand(*input_shape) > 0.2 @@ -13774,7 +13783,7 @@ def forward(self, x, y): self.run_compare_torch( [input_data_x, input_data_y], TestModel(), - frontend=frontend, + frontend=TorchFrontend.TORCHSCRIPT, backend=backend, compute_unit=compute_unit, input_as_shape=False,