Skip to content

Commit 0cddbc7

Browse files
authored
Arm backend: Fix to ensure nested control flow graphs are delegated (pytorch#18010)
- Added function to ensure True/False come in the right order. - Update to quantizer to ensure submodules are converted first. - Update to scalar pass for nested conditions. Signed-off-by: Saoirse Stewart <saoirse.stewart@arm.com>
1 parent 3eb1726 commit 0cddbc7

8 files changed

Lines changed: 177 additions & 103 deletions

File tree

‎backends/arm/_passes/arm_pass_utils.py‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import traceback
1010
from inspect import isclass
11-
from typing import Optional, Sequence
11+
from typing import List, Optional, Sequence, Tuple
1212

1313
import torch
1414
import torch.fx
@@ -17,6 +17,10 @@
1717
from executorch.exir import ExportedProgram
1818
from executorch.exir.dialects._ops import ops as exir_ops
1919
from executorch.exir.dialects.edge._ops import EdgeOpOverload
20+
from executorch.exir.graph_module import (
21+
_get_control_flow_submodules,
22+
get_control_flow_submodules,
23+
)
2024

2125
from torch._export.utils import (
2226
get_buffer,
@@ -29,6 +33,7 @@
2933
from torch._ops import OpOverload
3034
from torch._subclasses.fake_tensor import FakeTensor
3135
from torch.export.graph_signature import InputKind
36+
from torch.fx import GraphModule, Node
3237

3338

3439
def is_submodule_node(node: torch.fx.Node):
@@ -284,3 +289,45 @@ def set_node_arg(node: torch.fx.Node, i: int | str, value):
284289
def get_output_dim_orders(graph_module):
285290
output_node = graph_module.graph.output_node()
286291
return [get_first_fake_tensor(node).dim_order() for node in output_node.args[0]]
292+
293+
294+
def is_nested_control_flow_graph(graph_module: GraphModule) -> bool:
295+
"""Returns True if graph_module is a nested control-flow graph."""
296+
297+
# Find all top-level control-flow submodules
298+
top_cf = get_control_flow_submodules(graph_module)
299+
# For each submodule, see if it itself has control-flow inside
300+
for _, submod, _ in top_cf:
301+
if get_control_flow_submodules(submod):
302+
return True
303+
return False
304+
305+
306+
def get_cond_while_submodules_nested(
307+
graph_module: GraphModule,
308+
apply_quantization: bool = False,
309+
) -> List[Tuple[str, GraphModule, Node]]:
310+
"""Recursively find cond/while_loop submodules in an GraphModule.
311+
312+
In nested control flow graphs, FX records the submodule functions
313+
(true/false or cond/body) in reverse order compared to top-level graphs. We
314+
must swap the indices when nested so that cond (first) and body/true_fn
315+
(second) are consistently identified across all nesting levels.
316+
317+
"""
318+
319+
# Determine arg indices based on nesting and whether only cond branch is needed
320+
nested = is_nested_control_flow_graph(graph_module)
321+
# cond: [true_fn, false_fn] or swapped if nested
322+
cond_indices = [2, 1] if nested else [1, 2]
323+
# while_loop: [cond_fn, body_fn] or swapped if nested
324+
while_indices = [1, 0] if nested else [0, 1]
325+
if apply_quantization:
326+
# only keep the cond_fn for while_loop (first index) when quantizing.
327+
while_indices = [while_indices[0]]
328+
mapping = {
329+
torch.ops.higher_order.cond: cond_indices,
330+
torch.ops.higher_order.while_loop: while_indices,
331+
}
332+
# collect cond/while submodules (using mapping indices)
333+
return _get_control_flow_submodules(graph_module, mapping)

‎backends/arm/_passes/control_flow_const_inline.py‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55

66
from typing import Set, Type
77

8+
import torch
89
from executorch.backends.arm._passes.arm_pass import ArmPass
10+
from executorch.backends.arm._passes.arm_pass_utils import (
11+
get_cond_while_submodules_nested,
12+
is_submodule_node,
13+
)
914
from executorch.backends.transforms.utils import is_get_attr_node
1015
from executorch.exir.dialects._ops import ops as exir_ops
11-
from executorch.exir.graph_module import get_cond_while_submodules
12-
1316
from executorch.exir.pass_base import ExportPass, PassResult
1417
from torch.fx import GraphModule
1518

@@ -27,15 +30,23 @@ class ControlFlowConstInlinePass(ArmPass):
2730

2831
_passes_required_after: Set[Type[ExportPass]] = set()
2932

30-
def __init__(self, *args, **kwargs):
31-
super().__init__(*args, **kwargs)
33+
_targeted_ops = {
34+
torch.ops.higher_order.cond,
35+
torch.ops.higher_order.while_loop,
36+
}
3237

33-
def call(self, graph_module: GraphModule) -> PassResult:
38+
def _convert_getattr(self, graph_module):
3439
modified = False
35-
36-
for _, submodule, _ in get_cond_while_submodules(graph_module):
40+
for _, submodule, _ in get_cond_while_submodules_nested(graph_module):
3741
for submodule_node in submodule.graph.nodes:
38-
if is_get_attr_node(submodule_node):
42+
if submodule_node.target in self._targeted_ops:
43+
self._convert_getattr(submodule)
44+
45+
# For nested control flow, a "node" may be may actually be GraphModule.
46+
# Enure we are only checking for nodes here.
47+
if is_get_attr_node(submodule_node) and not is_submodule_node(
48+
submodule_node
49+
):
3950
val = getattr(
4051
submodule_node.graph.owning_module, submodule_node.target
4152
)
@@ -53,6 +64,14 @@ def call(self, graph_module: GraphModule) -> PassResult:
5364
submodule_node.replace_all_uses_with(const_node)
5465
submodule.graph.erase_node(submodule_node)
5566
modified = True
67+
return modified
68+
69+
def __init__(self, *args, **kwargs):
70+
super().__init__(*args, **kwargs)
71+
72+
def call(self, graph_module: GraphModule) -> PassResult:
73+
74+
modified = self._convert_getattr(graph_module)
5675

5776
if modified:
5877
graph_module.recompile()

‎backends/arm/_passes/scalars_to_attribute_pass.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88

99
import torch
1010
from executorch.backends.arm._passes import ArmPass
11-
from executorch.backends.arm._passes.arm_pass_utils import get_first_fake_tensor
11+
from executorch.backends.arm._passes.arm_pass_utils import (
12+
get_cond_while_submodules_nested,
13+
get_first_fake_tensor,
14+
)
1215
from executorch.backends.arm._passes.match_arg_ranks_pass import MatchArgRanksPass
13-
from executorch.exir.graph_module import get_cond_while_submodules
14-
1516
from executorch.exir.pass_base import ExportPass, PassResult
1617
from torch.fx import GraphModule, Node
1718
from torchao.quantization.pt2e.utils import get_new_attr_name_with_prefix
@@ -99,7 +100,7 @@ def handle_control_nodes(self, node: Node, graph_module: GraphModule) -> None:
99100
"""Apply scalar argument conversion on subgraphs of control-flow
100101
nodes.
101102
"""
102-
for _, submodule, _ in get_cond_while_submodules(graph_module):
103+
for _, submodule, _ in get_cond_while_submodules_nested(graph_module):
103104
for submodule_node in submodule.graph.nodes:
104105
# use aten.full.default for scalar constants in control subgraphs
105106
self._convert_scalar_args(submodule, submodule_node)

‎backends/arm/operator_support/control_flow_support.py‎

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,43 @@
2020

2121

2222
def _fully_partitioned(submodule: fx.GraphModule) -> bool:
23+
"""Check that all nested control-flow ops within this submodule are also
24+
fully partitioned.
25+
"""
2326
partition_tag = None
27+
2428
for submodule_node in submodule.graph.nodes:
25-
if submodule_node.op == "call_function":
26-
# Input Q ops and output DQ ops will be de-tagged even if the submodule is fully supported.
27-
if (
28-
submodule_node.target in Q_OPS
29-
and list(submodule_node.all_input_nodes)[0].op == "placeholder"
30-
):
31-
continue
32-
if (
33-
submodule_node.target in DQ_OPS
34-
and list(submodule_node.users)[0].op == "output"
35-
):
36-
continue
37-
if "delegation_tag" not in submodule_node.meta:
38-
return False
39-
if partition_tag is None:
40-
partition_tag = submodule_node.meta["delegation_tag"]
41-
elif submodule_node.meta["delegation_tag"] != partition_tag:
42-
return False
29+
if submodule_node.target in ControlFlowOpSupported._targeted_ops:
30+
if _submodules_fully_partitioned(submodule_node, submodule):
31+
return True
32+
33+
if submodule_node.op != "call_function":
34+
continue
35+
# skip no-op quantize/dequantize boundary
36+
if (
37+
submodule_node.target in Q_OPS
38+
and list(submodule_node.all_input_nodes)[0].op == "placeholder"
39+
):
40+
continue
41+
if (
42+
submodule_node.target in DQ_OPS
43+
and list(submodule_node.users)[0].op == "output"
44+
):
45+
continue
46+
47+
if "delegation_tag" not in submodule_node.meta:
48+
return False
49+
50+
if partition_tag is None:
51+
partition_tag = submodule_node.meta["delegation_tag"]
52+
53+
elif submodule_node.meta["delegation_tag"] != partition_tag:
54+
return False
55+
4356
return True
4457

4558

46-
def _submodules_fully_partitioned(
47-
node: fx.Node, exported_program: ExportedProgram
48-
) -> bool:
59+
def _submodules_fully_partitioned(node: fx.Node, graph_module: fx.GraphModule) -> bool:
4960
"""Returns whether the submodule arguments to a cond node were fully
5061
partitioned.
5162
@@ -61,20 +72,19 @@ def _submodules_fully_partitioned(
6172
raise ValueError(f"Unexpected target: {node.target}")
6273
cond_submodules = (
6374
(
64-
exported_program.graph_module.get_submodule(
65-
str(cast(torch.fx.Node, submodule_node).target)
66-
),
75+
graph_module.get_submodule(str(cast(torch.fx.Node, submodule_node).target)),
6776
cast(torch.fx.Node, submodule_node),
6877
)
6978
for submodule_node in submodule_args
7079
)
7180
for submodule, submodule_node in cond_submodules:
7281
submodule = cast(torch.fx.GraphModule, submodule)
7382

74-
if _fully_partitioned(submodule):
75-
submodule_node.meta["val"] = submodule.graph.output_node().meta["val"]
76-
else:
83+
if not _fully_partitioned(submodule):
7784
return False
85+
else:
86+
submodule_node.meta["val"] = submodule.graph.output_node().meta["val"]
87+
7888
return True
7989

8090

@@ -105,6 +115,7 @@ def __init__(
105115
def is_node_supported(
106116
self, submodules: typing.Mapping[str, torch.nn.Module], node: fx.Node
107117
) -> bool:
118+
108119
if is_submodule_node(node):
109120
if not _tosa_spec_supports_cf(self.tosa_spec):
110121
self.reporter.report_reject(
@@ -118,7 +129,9 @@ def is_node_supported(
118129
node, f"Submodule had unsupported user {user}"
119130
)
120131
return False
121-
if not _submodules_fully_partitioned(user, self.exported_program):
132+
if not _submodules_fully_partitioned(
133+
user, self.exported_program.graph_module
134+
):
122135
self.reporter.report_reject(
123136
node, "One submodule was not fully partitioned"
124137
)
@@ -161,7 +174,9 @@ def is_node_supported(
161174
)
162175
return False
163176

164-
if not _submodules_fully_partitioned(node, self.exported_program):
177+
if not _submodules_fully_partitioned(
178+
node, self.exported_program.graph_module
179+
):
165180
self.reporter.report_reject(
166181
node, "Submodule was not fully partitioned."
167182
)

‎backends/arm/quantizer/arm_quantizer.py‎

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
from executorch.backends.arm.common.arm_compile_spec import (
2525
ArmCompileSpec,
2626
) # isort: skip
27+
from executorch.backends.arm._passes.arm_pass_utils import (
28+
get_cond_while_submodules_nested,
29+
is_submodule_node,
30+
)
2731
from executorch.backends.arm.vgf import VgfCompileSpec
28-
from executorch.exir.graph_module import _get_control_flow_submodules
2932

3033
from torch.fx import GraphModule, Node
3134
from torchao.quantization.pt2e import (
@@ -639,27 +642,6 @@ def validate(self, model: GraphModule) -> None:
639642
f"Quantizer detected operator {node.name} with different device inputs: {devices}."
640643
)
641644

642-
@staticmethod
643-
def _get_submodules_not_handled_by_torchao(
644-
graph_module: GraphModule,
645-
):
646-
"""Returns control flow submodules that torchao's
647-
prepare_pt2e/convert_pt2e do not handle natively. torchao now
648-
recursively handles while_loop body_fn.
649-
650-
(arg 1), so we only need to manually handle:
651-
- cond true/false branches (args 1, 2)
652-
- while_loop cond_fn (arg 0)
653-
654-
"""
655-
return _get_control_flow_submodules(
656-
graph_module,
657-
{
658-
torch.ops.higher_order.cond: [1, 2],
659-
torch.ops.higher_order.while_loop: [0],
660-
},
661-
)
662-
663645
def quantize_with_submodules(
664646
self,
665647
model: GraphModule,
@@ -691,18 +673,40 @@ def quantize_with_submodules(
691673
prepare_fn = prepare_qat_pt2e if is_qat else prepare_pt2e
692674

693675
prepared = prepare_fn(model, self)
694-
for name, submodule, _ in self._get_submodules_not_handled_by_torchao(prepared):
676+
# Prepare conditional submodules (e.g., if/while bodies)
677+
# prepare only cond branches and while_loop cond_fn
678+
for name, submodule, _ in get_cond_while_submodules_nested(
679+
prepared, apply_quantization=True
680+
):
695681
prepared.set_submodule(name, prepare_fn(submodule, self), strict=True)
682+
for submodule_node in submodule.graph.nodes:
683+
if is_submodule_node(submodule_node):
684+
for nested_name, nested_sub, _ in get_cond_while_submodules_nested(
685+
submodule, apply_quantization=True
686+
):
687+
prepared.set_submodule(
688+
nested_name, prepare_fn(nested_sub, self), strict=True
689+
)
690+
696691
for inp in calibration_samples:
697692
prepared(*inp)
698693

699-
for name, submodule, _ in self._get_submodules_not_handled_by_torchao(prepared):
700-
prepared.set_submodule(
701-
name, convert_pt2e(submodule, fold_quantize=fold_quantize), strict=True
702-
)
703-
converted = convert_pt2e(prepared, fold_quantize=fold_quantize)
694+
# Prepare conditional submodules (e.g., if/while bodies)
695+
# convert only cond branches and while_loop cond_fn
696+
for _, submodule, _ in get_cond_while_submodules_nested(
697+
prepared, apply_quantization=True
698+
):
699+
converted = convert_pt2e(submodule)
700+
for submodule_node in submodule.graph.nodes:
701+
if is_submodule_node(submodule_node):
702+
for nested_name, nested_sub, _ in get_cond_while_submodules_nested(
703+
submodule, apply_quantization=True
704+
):
705+
converted.set_submodule(
706+
nested_name, convert_pt2e(nested_sub), strict=True
707+
)
704708

705-
return converted
709+
return convert_pt2e(prepared)
706710

707711

708712
class EthosUQuantizer(TOSAQuantizer):

0 commit comments

Comments
 (0)