Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
eebd74a
Catch missing scatter and index_put kernels when lowering to AOTI bac…
shoumikhin Oct 28, 2026
58ebe32
Keep the OpenVINO blob alive while its compiled model is in use
shoumikhin Oct 28, 2026
b0f7f53
Qualcomm AI Engine Direct - Fix DecomposeWrapWithAutocast for real re…
shoumikhin Oct 28, 2026
44991dd
Lower detach in AOTI backends
shoumikhin Oct 28, 2026
80aa633
Copy a strided CUDA delegate output into its ETensor
shoumikhin Oct 28, 2026
7e5178f
Do not fold a tensor that an in-place op writes in constant_prop_pass
shoumikhin Oct 28, 2026
d48cbd1
[MLX] Check tensor list inputs for unsupported dtypes
shoumikhin Oct 28, 2026
d1a5fc5
[MLX] Share one constant slot between placeholders of the same weight
shoumikhin Oct 28, 2026
5e7f3e8
Skip CoreML ops on bfloat16 tensors
shoumikhin Oct 28, 2026
87a0991
Skip CoreML ops on tensors above rank 5
shoumikhin Oct 28, 2026
34059c9
Vulkan: load programs with a constant of more than 4 dims
shoumikhin Oct 28, 2026
f869d87
Arm backend: Keep unused delegate inputs in the TOSA graph
shoumikhin Oct 28, 2026
2bdf12d
NXP backend: quantize programs from a non-strict torch.export
shoumikhin Oct 28, 2026
31a6348
Do not quantize bfloat16 tensors in XNNPACKQuantizer
shoumikhin Oct 28, 2026
5dc180d
Keep the output node last when to_backend fuses a partition
shoumikhin Oct 28, 2026
401a564
Lower torch._check_tensor_all and torch.sym_ite through to_edge
shoumikhin Oct 28, 2026
1078f5a
Add the aoti_torch_as_strided shim to the CUDA backend
shoumikhin Oct 28, 2026
8363aae
Stop GroupBasedPartitioner from making partitions that depend on each…
shoumikhin Oct 28, 2026
3dc94ab
Cortex-M: Handle a constant operand in MatmulToBmmPass
shoumikhin Oct 28, 2026
5350e76
exir: Give tensor constants static shapes in ExportPass
shoumikhin Oct 28, 2026
43849be
[MLX] Read the base of an out-of-place slice update as an input
shoumikhin Oct 28, 2026
41052c8
Qualcomm AI Engine Direct - Decompose a stack that HTP Pack cannot take
shoumikhin Oct 28, 2026
f1b70d3
Qualcomm AI Engine Direct - Keep partitions with no runtime input on CPU
shoumikhin Oct 28, 2026
c112a14
Qualcomm AI Engine Direct - Remove a 0-d select only when its input c…
shoumikhin Oct 28, 2026
27993e9
Qualcomm AI Engine Direct - Recompose RMSNorm only over the last dim …
shoumikhin Oct 28, 2026
2db708f
Qualcomm AI Engine Direct - Leave bfloat16 nodes on CPU
shoumikhin Oct 28, 2026
cb58a4b
Qualcomm AI Engine Direct - Leave nodes with data-dependent shapes on…
shoumikhin Oct 28, 2026
593b908
Qualcomm AI Engine Direct - Cast a top-of-graph int64 constant before…
shoumikhin Oct 28, 2026
c9c90f6
Delegate ELU to XNNPACK
shoumikhin Oct 28, 2026
3ee64c6
Stop XNNPACK from dropping the scales of weight-only quantized linears
shoumikhin Oct 28, 2026
91894de
Add a portable kernel for cummin.out
shoumikhin Oct 28, 2026
4ec7b9f
OpenVINO backend: convert partitions with their input types
shoumikhin Oct 28, 2026
d0cf5b0
CUDA backend: lower transposed convolution without ATen
shoumikhin Oct 28, 2026
3185752
Fix constant data of duplicated constant placeholders
shoumikhin Oct 28, 2026
b03f8bf
Lower torch._check_tensor_all in the CUDA backend
shoumikhin Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion backends/aoti/aoti_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import contextlib
import hashlib
import os
import re
import typing
from abc import ABC, abstractmethod
from enum import Enum
Expand Down Expand Up @@ -168,7 +169,8 @@ def release_moved_tensors(
def collect_unsupported_fallback_kernels(cls, missing_fallback_kernels: Set[str]):
"""
Context manager to collect unsupported fallback kernels during compilation.
Monitors both extern kernel calls and runtime lookup.
Monitors extern kernel calls, runtime lookup, and the scatter and
index_put fallbacks, which call their kernels directly.
"""
supported_kernels = cls.get_supported_fallback_kernels()

Expand All @@ -178,6 +180,10 @@ def collect_unsupported_fallback_kernels(cls, missing_fallback_kernels: Set[str]
original_generate_fallback_kernel_with_runtime_lookup_aot = (
CppWrapperCpu.generate_fallback_kernel_with_runtime_lookup_aot
)
original_generate_scatter_fallback = CppWrapperCpu._generate_scatter_fallback
original_generate_index_put_fallback = (
CppWrapperCpu._generate_index_put_fallback
)

def generate_c_shim_extern_kernel_call_and_collect_unsupported_kernels(
self, kernel: str, *args: Any, **kwargs: Any
Expand All @@ -200,10 +206,36 @@ def generate_fallback_kernel_with_runtime_lookup_aot_and_collect_unsupported_ker
self, op_overload, *args, **kwargs
)

def collect_kernels_it_writes(original: Any) -> Any:
# These fallbacks write their shim call directly, so read the
# kernel names from the lines they write.
def generate_and_collect_unsupported_kernels(
self, *args: Any, **kwargs: Any
) -> None:
lines: List[str] = []
writeline = self.writeline
self.writeline = lambda line: (lines.append(str(line)), writeline(line))
try:
return original(self, *args, **kwargs)
finally:
self.writeline = writeline
for line in lines:
for kernel in re.findall(r"\b(aoti_torch_\w+)\(", line):
if kernel not in supported_kernels:
missing_fallback_kernels.add(kernel)

return generate_and_collect_unsupported_kernels

CppWrapperCpu.generate_c_shim_extern_kernel_call = (
generate_c_shim_extern_kernel_call_and_collect_unsupported_kernels
)
CppWrapperCpu.generate_fallback_kernel_with_runtime_lookup_aot = generate_fallback_kernel_with_runtime_lookup_aot_and_collect_unsupported_kernels
CppWrapperCpu._generate_scatter_fallback = collect_kernels_it_writes(
original_generate_scatter_fallback
)
CppWrapperCpu._generate_index_put_fallback = collect_kernels_it_writes(
original_generate_index_put_fallback
)

try:
yield
Expand All @@ -214,6 +246,12 @@ def generate_fallback_kernel_with_runtime_lookup_aot_and_collect_unsupported_ker
CppWrapperCpu.generate_fallback_kernel_with_runtime_lookup_aot = (
original_generate_fallback_kernel_with_runtime_lookup_aot
)
CppWrapperCpu._generate_scatter_fallback = (
original_generate_scatter_fallback
)
CppWrapperCpu._generate_index_put_fallback = (
original_generate_index_put_fallback
)

@classmethod
def preprocess(
Expand Down
5 changes: 5 additions & 0 deletions backends/aoti/passes/replace_view_copy_with_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
# natively by inductor, so map it here like the other view-copy ops.
torch.ops.aten.split_copy.Tensor: torch.ops.aten.split.Tensor,
ops.edge.aten.split_copy.Tensor: ops.edge.aten.split.Tensor,
# Edge turns ``detach`` into ``detach_copy``, which has no c-shim either.
# Without autograd ``detach`` is ``alias``, which unlike ``detach`` is core
# ATen and so passes Edge verification.
torch.ops.aten.detach_copy.default: torch.ops.aten.alias.default,
ops.edge.aten.detach_copy.default: ops.edge.aten.alias.default,
}


Expand Down
98 changes: 91 additions & 7 deletions backends/apple/coreml/partition/coreml_partitioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,59 @@
)


# Ops that coremltools can carry through a tensor above rank 5, by rewriting a
# reshape -> transpose -> reshape chain into rank 4 steps.
_HIGH_RANK_LAYOUT_TARGETS = frozenset(
[
exir_ops.edge.aten.view_copy.default,
exir_ops.edge.aten.permute_copy.default,
exir_ops.edge.aten.clone.default,
exir_ops.edge.dim_order_ops._clone_dim_order.default,
]
)


def _produces_tensor_above_rank_5(node: torch.fx.Node) -> bool:
val = node.meta.get("val")
vals = list(val) if isinstance(val, (list, tuple)) else [val]
return any(isinstance(v, torch.Tensor) and v.dim() > 5 for v in vals)


def _touches_unsupported_high_rank_tensor(node: torch.fx.Node) -> bool:
"""Core ML tensors have at most rank 5.

coremltools 9.0 raises "Core ML only supports tensors with rank <= 5" for
any tensor above rank 5 that it cannot rewrite away. It only rewrites a
static, unbranched layout chain, so such a tensor is fine only when every
node connected to it through tensors above rank 5 is a view, permute or
clone, and every node producing such a tensor has a static shape and a
single user.
"""
seen, todo = set(), [node]
while todo:
n = todo.pop()
if n in seen:
continue
seen.add(n)
high_rank_inputs = [
arg for arg in n.all_input_nodes if _produces_tensor_above_rank_5(arg)
]
high_rank_output = _produces_tensor_above_rank_5(n)
if not high_rank_inputs and not high_rank_output:
continue
if n.op != "call_function" or n.target not in _HIGH_RANK_LAYOUT_TARGETS:
return True
if high_rank_output and (
len(n.users) != 1
or any(isinstance(d, torch.SymInt) for d in n.meta["val"].shape)
):
return True
todo += high_rank_inputs
if high_rank_output:
todo += list(n.users)
return False


_ARG_MIN_MAX_TARGETS = (
torch.ops.aten.argmax.default,
torch.ops.aten.argmin.default,
Expand All @@ -83,6 +136,18 @@ def _is_arg_min_max_over_flattened_input(node: torch.fx.Node) -> bool:
return dim is None


def _reads_or_produces_bfloat16(node: torch.fx.Node) -> bool:
"""coremltools 9.0 has no bfloat16 type.

It raises ``KeyError: torch.bfloat16`` on any op that reads or produces a
bfloat16 tensor, instead of reporting the op as unsupported.
"""
val = node.meta.get("val")
vals = list(val) if isinstance(val, (list, tuple)) else [val]
vals += [arg.meta.get("val") for arg in node.all_input_nodes]
return any(isinstance(v, torch.Tensor) and v.dtype == torch.bfloat16 for v in vals)


def _is_view_op(op: torch._ops.OpOverload) -> bool:
schema = op._schema
if len(schema.arguments) == 0:
Expand Down Expand Up @@ -152,13 +217,27 @@ def should_override_support(self, node) -> bool:
)
return True

if _touches_unsupported_high_rank_tensor(node):
self.log_once(
f"{node.target} on a tensor above rank 5 is not supported by "
"CoreML. Overriding op support."
)
return True

if _is_arg_min_max_over_flattened_input(node):
self.log_once(
"torch.ops.aten.{argmax, argmin}.default with dim=None is "
"not supported by CoreML. Overriding op support."
)
return True

if _reads_or_produces_bfloat16(node):
self.log_once(
f"{node.target} on a bfloat16 tensor is not supported by CoreML. "
"Overriding op support."
)
return True

# TODO: enable this after bugs in ExecuTorch's partitioner are fixed
# # If lower_full_graph=False, do not partition nodes with symbolic args because it can result in symbolic args
# # in the placeholders due to partitioning, which CoreML does not support
Expand Down Expand Up @@ -362,21 +441,26 @@ def ops_to_not_decompose(
torch.ops.aten.repeat_interleave.self_int,
torch.ops.aten.repeat_interleave.self_Tensor,
]
rejected = set()
for node in ep.graph.nodes:
if node.op == "call_function" and isinstance(
node.target, torch._ops.OpOverload
):
try:
if (
op_support.is_node_supported(None, node)
and node.target not in do_not_decompose_blocklist
and not _is_view_op(node.target)
):
do_not_decompose.append(node.target)
supported = op_support.is_node_supported(None, node)
except Exception as e:
# CoreML's op_support.is_node_supported will sometimes throw
# for unsupported ops, rather than returning False
self.log_once(
f"Encountered exception when checking node support, treating node as unsupported: {e}"
)
return do_not_decompose, None
supported = False
if not supported:
rejected.add(node.target)
elif node.target not in do_not_decompose_blocklist and not _is_view_op(
node.target
):
do_not_decompose.append(node.target)
# Without a per-node filter, a preserved op stays whole at every call, so
# one rejected call would be left with no kernel.
return [op for op in do_not_decompose if op not in rejected], None
72 changes: 72 additions & 0 deletions backends/apple/coreml/test/test_coreml_partitioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,45 @@ def forward(self, x):
self.assertIn("executorch_call_delegate", op_names)
self.assertNotIn("aten.randn.default", op_names)

def test_ops_above_rank_5_are_skipped(self):
"""
Core ML tensors have at most rank 5. coremltools only rewrites an
unbranched view, permute, view chain through a higher rank (a window
partition), and raises on any other op above rank 5. The partitioner
must leave those ops to the portable backend and keep such a layout
chain delegated.
"""

class Model(torch.nn.Module):
def forward(self, x):
windows = x.view(2, 2, 2, 2, 2, 8).permute(0, 1, 3, 2, 4, 5)
windows = windows.reshape(-1, 4, 8).relu()
branched = x.view(2, 2, 2, 2, 2, 8).permute(0, 1, 3, 2, 4, 5)
branched = (
branched.reshape(-1, 4, 8).sum() + branched.reshape(32, 8).sum()
)
stacked = torch.stack([x.unsqueeze(0), x.unsqueeze(0) * 2])
return windows.sum() + branched + stacked.sum(0).relu().sum()

model = Model().eval()
x = torch.randn(2, 4, 4, 8)
ep = torch.export.export(model, (x,), strict=True)
edge = executorch.exir.to_edge_transform_and_lower(
ep, partitioner=[CoreMLPartitioner()]
)
op_names = [
n.target.__name__
for n in edge.exported_program().graph.nodes
if n.op == "call_function"
]
self.assertIn("executorch_call_delegate", op_names)
self.assertIn("aten.sum.dim_IntList", op_names)
self.assertEqual(op_names.count("aten.permute_copy.default"), 1)
if _TEST_RUNTIME:
program = Runtime.get().load_program(edge.to_executorch().buffer)
out = program.load_method("forward").execute((x,))[0]
self.assertTrue(torch.allclose(out, model(x), atol=5e-2, rtol=5e-2))

def test_argmax_argmin_dim_none_is_skipped(self):
"""
Regression test for https://github.com/pytorch/executorch/issues/11715.
Expand Down Expand Up @@ -433,6 +472,39 @@ def forward(self, x):
self.assertIn("executorch_call_delegate", op_names)
self.assertNotIn("aten.argmax.default", op_names)

def test_bfloat16_ops_are_skipped(self):
"""
coremltools has no bfloat16 type and raises on ops that use it. The
partitioner must leave those ops to the portable backend and still
delegate the rest, also when the same op has a float32 call that Core
ML takes (as under bfloat16 autocast).
"""

class Model(torch.nn.Module):
def forward(self, q):
a = torch.nn.functional.scaled_dot_product_attention(q, q, q)
b = q.to(torch.bfloat16)
c = torch.nn.functional.scaled_dot_product_attention(b, b, b)
return a + c.float()

model = Model().eval()
x = torch.randn(1, 2, 4, 8)
ep = torch.export.export(model, (x,), strict=True)
edge = executorch.exir.to_edge_transform_and_lower(
ep, partitioner=[CoreMLPartitioner()]
)
op_names = [
n.target.__name__
for n in edge.exported_program().graph.nodes
if n.op == "call_function"
]
self.assertIn("executorch_call_delegate", op_names)
self.assertNotIn("aten.scaled_dot_product_attention.default", op_names)
if _TEST_RUNTIME:
program = Runtime.get().load_program(edge.to_executorch().buffer)
out = program.load_method("forward").execute((x,))[0]
self.assertTrue(torch.allclose(out, model(x), atol=5e-2, rtol=5e-2))

def test_tied_embedding_is_quantized_with_its_linear(self):
"""A weight that is both an embedding table and an output projection lowers.

Expand Down
26 changes: 23 additions & 3 deletions backends/arm/test/ops/test_copy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Arm Limited and/or its affiliates.
# Copyright 2025-2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
Expand Down Expand Up @@ -55,6 +55,11 @@ def forward(self, x):
return x * 2 + y.copy_(x)


class CopyOverUnusedInput(torch.nn.Module):
def forward(self, x, y):
return y.clone().copy_(x) + x


test_suite = {
"copy_output": lambda: (
CopyOutput,
Expand All @@ -80,9 +85,24 @@ def forward(self, x):
CopyParallelToOtherOp,
(torch.rand(1, 2, 3, 4, dtype=torch.float32),),
),
"copy_over_unused_input": lambda: (
CopyOverUnusedInput,
(
torch.rand(1, 2, 3, 4, dtype=torch.float32),
torch.rand(1, 2, 3, 4, dtype=torch.float32),
),
),
}


ethos_u_xfails = {
"copy_over_unused_input": (
"Vela drops unused inputs, but the Ethos-U runtime binds delegate "
"inputs by position",
AssertionError,
),
}

aten_op = "torch.ops.aten.copy_.default"
exir_op = "executorch_exir_dialects_edge__ops_aten_copy_default"

Expand Down Expand Up @@ -114,7 +134,7 @@ def test_copy_tosa_INT(input_data):
pipeline.run()


@common.parametrize("input_data", test_suite)
@common.parametrize("input_data", test_suite, xfails=ethos_u_xfails)
@common.XfailIfNoCorstone300
def test_copy_u55_INT(input_data):
module, input_tensor = input_data()
Expand All @@ -128,7 +148,7 @@ def test_copy_u55_INT(input_data):
pipeline.run()


@common.parametrize("input_data", test_suite)
@common.parametrize("input_data", test_suite, xfails=ethos_u_xfails)
@common.XfailIfNoCorstone320
def test_copy_u85_INT(input_data):
module, input_tensor = input_data()
Expand Down
12 changes: 9 additions & 3 deletions backends/arm/tosa/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,15 @@ def _annotate_control_flow_region_names(
if node.op == "call_function":
process_call_function(node, tosa_graph, node_visitors, tosa_spec)
elif node.op == "placeholder":
if len(node.users) == 0 and submodule_name is None:
# In top level module, we don't need to handle unused placeholders.
# In submodules, we do need to handle them to preserve call signature.
if (
len(node.users) == 0
and submodule_name is None
and node.name not in edge_program.graph_signature.user_inputs
):
# Unused delegate inputs are kept because the runtime
# binds delegate arguments by position. In submodules,
# all placeholders are kept to preserve the call
# signature.
continue
process_placeholder(
node,
Expand Down
Loading
Loading