Skip to content

Commit 5be88d0

Browse files
authored
Claim getitem when its producer is claimed
Summary: NativeSupportedOperators now claims operator.getitem, so a supported multi-output op and the getitems that unpack it stay in one partition. Without this every getitem is a partition boundary, and fused-projection models (chunked gate_up / qkv) fragment into many delegates. The claim is conditional on the producer also being claimed. partition() passes allows_single_node_partition=True, which disables the capability partitioner's filter that would otherwise drop getitem-only partitions -- getitem is in its default non-compute set, so such a partition normally scores zero compute nodes and is deleted (see torch/fx/passes/infra/partitioner.py). With that filter off, claiming getitem unconditionally can emit a delegate that holds no computation and is fed by a tuple crossing the delegate boundary, where boundaries carry tensors. Backends that claim getitem unconditionally (Qualcomm, OpenVINO) leave the filter enabled and rely on it. Reviewed By: digantdesai Differential Revision: D117907800 Pull Request resolved: pytorch#22974
1 parent 3b065a6 commit 5be88d0

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎backends/native/partitioner.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ExecuTorch forbids a partitioner from mutating the graph module.
1414
"""
1515

16+
import operator
1617
from typing import Callable, final, List, Mapping, Optional, Tuple
1718

1819
# Registers torch.ops.torchao.dequantize_gguf, referenced in _SUPPORTED_NON_CORE_OPS.
@@ -71,6 +72,23 @@ def is_node_supported(
7172
return False
7273
if node.op != "call_function":
7374
return False
75+
# getitem only unpacks a multi-output node's result (e.g. split/chunk); it
76+
# is not a real op. Claim it so a supported multi-output op and its
77+
# unpackers stay in one partition -- otherwise every getitem is a partition
78+
# boundary and fused-projection models (chunked gate_up / qkv) fragment into
79+
# many delegates.
80+
#
81+
# Only claim it when the producer is claimed too. partition() passes
82+
# allows_single_node_partition=True, which disables the capability
83+
# partitioner's filter that would otherwise drop getitem-only partitions
84+
# (getitem counts as non-compute there). Claiming unconditionally can
85+
# therefore emit a delegate holding no computation, fed by a tuple crossing
86+
# the delegate boundary.
87+
if node.target is operator.getitem:
88+
producer = node.args[0] if node.args else None
89+
return isinstance(producer, Node) and self.is_node_supported(
90+
submodules, producer
91+
)
7492
if isinstance(node.target, torch._ops.HigherOrderOperator):
7593
return False
7694

‎backends/native/test/test_partitioner.py‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import operator
78
import unittest
89
from unittest.mock import MagicMock
910

@@ -32,10 +33,13 @@
3233
)
3334

3435

35-
def _make_node(op, target):
36-
node = MagicMock()
36+
def _make_node(op, target, args=()):
37+
# spec=Node so isinstance(node, Node) holds; the getitem support check
38+
# resolves its producer through node.args and requires a real Node.
39+
node = MagicMock(spec=torch.fx.Node)
3740
node.op = op
3841
node.target = target
42+
node.args = args
3943
return node
4044

4145

@@ -88,6 +92,34 @@ def test_accepts_view_copy_tagged_op(self):
8892
self.assertIn(torch.Tag.view_copy, op.tags)
8993
self.assertTrue(self.sup.is_node_supported({}, _make_node("call_function", op)))
9094

95+
def test_accepts_getitem_on_supported_producer(self):
96+
# getitem unpacks a multi-output result; it must be claimed alongside a
97+
# producer the backend claims, so the two stay in one partition.
98+
producer = _make_node("call_function", torch.ops.aten.split_copy.Tensor)
99+
self.assertTrue(self.sup.is_node_supported({}, producer))
100+
getitem = _make_node("call_function", operator.getitem, args=(producer, 0))
101+
self.assertTrue(self.sup.is_node_supported({}, getitem))
102+
103+
def test_rejects_getitem_on_unsupported_producer(self):
104+
# partition() sets allows_single_node_partition=True, which disables the
105+
# capability partitioner's getitem-only partition filter. Claiming a
106+
# getitem whose producer is not claimed would strand it in a delegate
107+
# containing no computation, fed by a tuple crossing the boundary.
108+
producer = _make_node(
109+
"call_function", torch.ops.aten.linalg_solve_triangular.default
110+
)
111+
self.assertFalse(self.sup.is_node_supported({}, producer))
112+
getitem = _make_node("call_function", operator.getitem, args=(producer, 0))
113+
self.assertFalse(self.sup.is_node_supported({}, getitem))
114+
115+
def test_rejects_getitem_without_node_producer(self):
116+
# A getitem whose first arg is not a Node (or which has no args) has no
117+
# producer to validate, so it is not claimed.
118+
for args in ((), (None, 0), ("not_a_node", 0)):
119+
with self.subTest(args=args):
120+
node = _make_node("call_function", operator.getitem, args=args)
121+
self.assertFalse(self.sup.is_node_supported({}, node))
122+
91123
def test_rejects_non_core_unsupported_op(self):
92124
op = torch.ops.aten.linalg_solve_triangular.default
93125
self.assertNotIn(torch.Tag.core, op.tags)

0 commit comments

Comments
 (0)