Skip to content

Commit f2ab082

Browse files
authored
Arm backend: Fix int64 argmax delegation rejection (#20765)
TOSA 1.0 ARGMAX returns int32 indices, while aten.argmax returns int64. If the graph consumes the raw aten.argmax result on CPU fallback, delegating ARGMAX changes the dtype and can fail with an Int-vs-Long mismatch. Keep delegation enabled only when every user immediately casts the argmax result to int32: aten.argmax -> _to_dim_order_copy(dtype=int32) -> users Otherwise reject the node during support checking so the PyTorch-compatible int64 result is preserved. Fixes the flow test: test_argmax_dim[arm_tosa_fp] Signed-off-by: Zingo Andersen <Zingo.Andersen@arm.com>
1 parent a87df42 commit f2ab082

2 files changed

Lines changed: 136 additions & 1 deletion

File tree

backends/arm/operator_support/tosa_supported_operators.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
1010
"""
1111

12-
1312
import operator
1413
import typing
1514
from typing import final, Optional, Sequence, Type
@@ -343,6 +342,7 @@ def _negative_checks(
343342
additional_checks: Optional[Sequence[OperatorSupportBase]],
344343
) -> list[OperatorSupportBase]:
345344
checks: list[OperatorSupportBase] = [RankCheck(reporter, MAX_RANK)]
345+
checks.append(CheckKnownUnsupportedTOSASemantics(reporter))
346346

347347
if not tosa_spec.support_extension("int64"):
348348
checks.append(CheckInt64InputsAndOutputs(exported_program, reporter, tosa_spec))
@@ -372,6 +372,52 @@ def _negative_checks(
372372
return checks
373373

374374

375+
class CheckKnownUnsupportedTOSASemantics(OperatorSupportBase):
376+
"""Reject ops whose TOSA lowering is known to differ from PyTorch."""
377+
378+
def __init__(self, reporter: WhyNoPartitionReporter):
379+
self.reporter = reporter
380+
381+
def _argmax_all_users_cast_to_int32(self, node: fx.Node) -> bool:
382+
# TOSA ARGMAX produces int32 indices. This is only a faithful lowering
383+
# when every user immediately narrows aten.argmax's int64 result:
384+
#
385+
# aten.argmax -> _to_dim_order_copy(dtype=int32) -> users
386+
cast_ops = (
387+
torch.ops.dim_order_ops._to_dim_order_copy.default,
388+
exir_ops.edge.dim_order_ops._to_dim_order_copy.default,
389+
)
390+
# A live argmax should normally have users. Treat a no-user node as not
391+
# proving the int64 result is intentionally narrowed to int32.
392+
if not node.users:
393+
return False
394+
for user in node.users:
395+
if user.target not in cast_ops:
396+
return False
397+
if user.kwargs.get("dtype") != torch.int32:
398+
return False
399+
return True
400+
401+
def _check_argmax(self, node: fx.Node) -> bool:
402+
if self._argmax_all_users_cast_to_int32(node):
403+
return True
404+
self.reporter.report_reject(
405+
node, "TOSA ARGMAX produces int32 but aten.argmax returns int64."
406+
)
407+
return False
408+
409+
def is_node_supported(
410+
self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node
411+
) -> bool:
412+
if node.target in (
413+
torch.ops.aten.argmax.default,
414+
exir_ops.edge.aten.argmax.default,
415+
):
416+
return self._check_argmax(node)
417+
418+
return True
419+
420+
375421
def tosa_support_factory(
376422
tosa_spec: TosaSpecification,
377423
exported_program: ExportedProgram,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
import torch
7+
from executorch.backends.arm.operator_support.tosa_supported_operators import (
8+
CheckKnownUnsupportedTOSASemantics,
9+
)
10+
from executorch.exir.backend.utils import WhyNoPartitionReporter
11+
from executorch.exir.dialects._ops import ops as exir_ops
12+
from torch._subclasses.fake_tensor import FakeTensorMode
13+
14+
15+
def _fake_tensor(shape: tuple[int, ...], dtype: torch.dtype = torch.float32):
16+
with FakeTensorMode() as mode:
17+
return mode.from_tensor(torch.empty(shape, dtype=dtype))
18+
19+
20+
def _placeholder(graph: torch.fx.Graph, name: str, shape, dtype=torch.float32):
21+
node = graph.placeholder(name)
22+
node.meta["val"] = _fake_tensor(shape, dtype)
23+
return node
24+
25+
26+
def _checker() -> CheckKnownUnsupportedTOSASemantics:
27+
return CheckKnownUnsupportedTOSASemantics(WhyNoPartitionReporter())
28+
29+
30+
def test_rejects_argmax_without_int32_cast_user() -> None:
31+
graph = torch.fx.Graph()
32+
x = _placeholder(graph, "x", (3, 4))
33+
node = graph.call_function(exir_ops.edge.aten.argmax.default, (x, 1, False))
34+
node.meta["val"] = _fake_tensor((3,), torch.int64)
35+
36+
assert not _checker().is_node_supported({}, node)
37+
38+
39+
def test_accepts_argmax_with_int32_cast_user() -> None:
40+
graph = torch.fx.Graph()
41+
x = _placeholder(graph, "x", (3, 4))
42+
node = graph.call_function(exir_ops.edge.aten.argmax.default, (x, 1, False))
43+
node.meta["val"] = _fake_tensor((3,), torch.int64)
44+
cast = graph.call_function(
45+
exir_ops.edge.dim_order_ops._to_dim_order_copy.default,
46+
(node,),
47+
{"dtype": torch.int32},
48+
)
49+
cast.meta["val"] = _fake_tensor((3,), torch.int32)
50+
51+
assert _checker().is_node_supported({}, node)
52+
53+
54+
def test_accepts_argmax_with_all_users_casting_to_int32() -> None:
55+
graph = torch.fx.Graph()
56+
x = _placeholder(graph, "x", (3, 4))
57+
node = graph.call_function(exir_ops.edge.aten.argmax.default, (x, 1, False))
58+
node.meta["val"] = _fake_tensor((3,), torch.int64)
59+
cast = graph.call_function(
60+
exir_ops.edge.dim_order_ops._to_dim_order_copy.default,
61+
(node,),
62+
{"dtype": torch.int32},
63+
)
64+
cast.meta["val"] = _fake_tensor((3,), torch.int32)
65+
cast_2 = graph.call_function(
66+
exir_ops.edge.dim_order_ops._to_dim_order_copy.default,
67+
(node,),
68+
{"dtype": torch.int32},
69+
)
70+
cast_2.meta["val"] = _fake_tensor((3,), torch.int32)
71+
72+
assert _checker().is_node_supported({}, node)
73+
74+
75+
def test_rejects_argmax_with_mixed_int32_cast_and_raw_user() -> None:
76+
graph = torch.fx.Graph()
77+
x = _placeholder(graph, "x", (3, 4))
78+
node = graph.call_function(exir_ops.edge.aten.argmax.default, (x, 1, False))
79+
node.meta["val"] = _fake_tensor((3,), torch.int64)
80+
cast = graph.call_function(
81+
exir_ops.edge.dim_order_ops._to_dim_order_copy.default,
82+
(node,),
83+
{"dtype": torch.int32},
84+
)
85+
cast.meta["val"] = _fake_tensor((3,), torch.int32)
86+
raw_user = graph.call_function(torch.ops.aten.clone.default, (node,))
87+
raw_user.meta["val"] = _fake_tensor((3,), torch.int64)
88+
89+
assert not _checker().is_node_supported({}, node)

0 commit comments

Comments
 (0)