Skip to content

Commit 12684ef

Browse files
Add MLX op handler for aten.bitwise_xor (#18931)
## Summary - Add `BitwiseXorNode` to the MLX delegate schema, C++ runtime, Python op handler, and tests - Enables element-wise bitwise XOR for boolean and integer tensors via `mlx::core::bitwise_xor` Closes #18927 ## Test plan - [ ] `python -m executorch.backends.mlx.test.run_all_tests -k bitwise_xor` passes both bool and int variants - [ ] Existing MLX op tests remain passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) cc @metascroy --------- Co-authored-by: Nanook <nanookclaw@users.noreply.github.com> Co-authored-by: Scott Roy <161522778+metascroy@users.noreply.github.com>
1 parent 5c6938e commit 12684ef

4 files changed

Lines changed: 74 additions & 1 deletion

File tree

backends/mlx/ops.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
BitwiseAndNode,
5454
BitwiseInvertNode,
5555
BitwiseOrNode,
56+
BitwiseXorNode,
5657
BroadcastToNode,
5758
CeilNode,
5859
ClipNode,
@@ -497,6 +498,12 @@ def _isnan_handler(P: MLXProgramBuilder, n: Node) -> Slot:
497498
"aten.bitwise_or",
498499
True,
499500
),
501+
(
502+
[torch.ops.aten.bitwise_xor.Tensor, torch.ops.aten.bitwise_xor.Scalar],
503+
BitwiseXorNode,
504+
"aten.bitwise_xor",
505+
True,
506+
),
500507
(
501508
[torch.ops.aten.lt.Tensor, torch.ops.aten.lt.Scalar],
502509
LessNode,

backends/mlx/runtime/MLXInterpreter.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,15 @@ exec_bitwise_or(const BitwiseOrNode& n, ExecutionState& st, StreamOrDevice s) {
14221422
n.out, bitwise_or(st.const_tensor_ref(n.a), st.const_tensor_ref(n.b), s));
14231423
}
14241424

1425+
inline void exec_bitwise_xor(
1426+
const BitwiseXorNode& n,
1427+
ExecutionState& st,
1428+
StreamOrDevice s) {
1429+
st.set_tensor(
1430+
n.out,
1431+
bitwise_xor(st.const_tensor_ref(n.a), st.const_tensor_ref(n.b), s));
1432+
}
1433+
14251434
inline void exec_tri(const TriNode& n, ExecutionState& st, StreamOrDevice s) {
14261435
int rows = resolve_int(n.n, st);
14271436
int cols = resolve_int(n.m, st);
@@ -2078,6 +2087,9 @@ class Interpreter {
20782087
case OpCode::BITWISE_OR:
20792088
ops::exec_bitwise_or(std::get<BitwiseOrNode>(instr.node), st, s);
20802089
break;
2090+
case OpCode::BITWISE_XOR:
2091+
ops::exec_bitwise_xor(std::get<BitwiseXorNode>(instr.node), st, s);
2092+
break;
20812093
case OpCode::TRI:
20822094
ops::exec_tri(std::get<TriNode>(instr.node), st, s);
20832095
break;

backends/mlx/serialization/schema.fbs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,12 @@ table BitwiseOrNode {
591591
out: Tid (required);
592592
}
593593

594+
table BitwiseXorNode {
595+
a: Tid (required);
596+
b: Tid (required);
597+
out: Tid (required);
598+
}
599+
594600
// Triangular matrix ops
595601
table TriNode {
596602
out: Tid (required);
@@ -1144,7 +1150,8 @@ union OpNode {
11441150
BitwiseInvertNode,
11451151
RollNode,
11461152
BitwiseAndNode,
1147-
BitwiseOrNode
1153+
BitwiseOrNode,
1154+
BitwiseXorNode
11481155
// BC: Add new op nodes here (append only)
11491156
}
11501157

backends/mlx/test/test_ops.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4808,6 +4808,8 @@ def create_model(self) -> nn.Module:
48084808
{"op_name": "bitwise_and_int", "op_fn": torch.bitwise_and, "shapes": _SHAPES_3, "dtypes": [torch.int32, torch.int64], "input_fn_a": _int_input_fn(0, 256), "input_fn_b": _int_input_fn(0, 256)},
48094809
{"op_name": "bitwise_or_bool", "op_fn": torch.bitwise_or, "shapes": _SHAPES_3, "dtypes": [torch.bool], "input_fn_a": _bool_input_fn(), "input_fn_b": _bool_input_fn()},
48104810
{"op_name": "bitwise_or_int", "op_fn": torch.bitwise_or, "shapes": _SHAPES_3, "dtypes": [torch.int32, torch.int64], "input_fn_a": _int_input_fn(0, 256), "input_fn_b": _int_input_fn(0, 256)},
4811+
{"op_name": "bitwise_xor_bool", "op_fn": torch.bitwise_xor, "shapes": _SHAPES_3, "dtypes": [torch.bool], "input_fn_a": _bool_input_fn(), "input_fn_b": _bool_input_fn()},
4812+
{"op_name": "bitwise_xor_int", "op_fn": torch.bitwise_xor, "shapes": _SHAPES_3, "dtypes": [torch.int32, torch.int64], "input_fn_a": _int_input_fn(0, 256), "input_fn_b": _int_input_fn(0, 256)},
48114813
{"op_name": "logical_and", "op_fn": torch.logical_and, "shapes": [(2, 3, 4), (10,), (4, 8)], "dtypes": [torch.bool], "input_fn_a": _bool_input_fn(), "input_fn_b": _bool_input_fn()},
48124814
{"op_name": "logical_or", "op_fn": torch.logical_or, "shapes": [(2, 3, 4), (10,), (4, 8)], "dtypes": [torch.bool], "input_fn_a": _bool_input_fn(), "input_fn_b": _bool_input_fn()},
48134815
]
@@ -4910,6 +4912,51 @@ def create_model(self) -> nn.Module:
49104912
return BitwiseOrScalarModel(self.scalar)
49114913

49124914

4915+
class BitwiseXorScalarModel(nn.Module):
4916+
def __init__(self, scalar):
4917+
super().__init__()
4918+
self.scalar = scalar
4919+
4920+
def forward(self, a: torch.Tensor) -> torch.Tensor:
4921+
return torch.bitwise_xor(a, self.scalar)
4922+
4923+
4924+
@register_test
4925+
class BitwiseXorScalarTest(OpTestCase):
4926+
"""Test case for aten.bitwise_xor op (Tensor_Scalar variant)."""
4927+
4928+
name = "bitwise_xor_scalar"
4929+
4930+
def __init__(
4931+
self,
4932+
shape: Tuple[int, ...],
4933+
dtype: torch.dtype,
4934+
scalar,
4935+
):
4936+
self.shape = shape
4937+
self.dtype = dtype
4938+
self.scalar = scalar
4939+
shape_str = "x".join(str(s) for s in shape)
4940+
dtype_str = str(dtype).replace("torch.", "")
4941+
self.name = f"bitwise_xor_scalar_{shape_str}_{dtype_str}"
4942+
4943+
@classmethod
4944+
def get_test_configs(cls) -> List["BitwiseXorScalarTest"]:
4945+
return [
4946+
cls(shape=(16,), dtype=torch.bool, scalar=True),
4947+
cls(shape=(4, 4), dtype=torch.int32, scalar=7),
4948+
cls(shape=(2, 3, 4), dtype=torch.int64, scalar=13),
4949+
]
4950+
4951+
def create_inputs(self) -> Tuple[torch.Tensor, ...]:
4952+
if self.dtype == torch.bool:
4953+
return _bool_input_fn()(self.shape, self.dtype)
4954+
return _int_input_fn(0, 256)(self.shape, self.dtype)
4955+
4956+
def create_model(self) -> nn.Module:
4957+
return BitwiseXorScalarModel(self.scalar)
4958+
4959+
49134960
@register_test
49144961
class PowerScalarTest(OpTestCase):
49154962
"""Test case for aten.pow op (Tensor_Scalar variant)."""

0 commit comments

Comments
 (0)