Skip to content

Commit 62903fa

Browse files
authored
IterativePassGroup: warn when the step cap is hit (#24759)
Summary: `IterativePassGroup` re-runs its passes until they stop changing the graph, up to `steps` rounds. Until now it handed `steps` to `ExportedProgramPassManager`, which stops quietly at the cap, so a group that never converged looked the same as one that did. The group now loops itself, one manager step per round, and returns as soon as a round leaves the graph unchanged. If a multi-step group is still changing the graph after its last allowed round, it logs a warning naming the group and the cap. Single-step groups run once by design and never warn. `AdvanceQuantizeOpAboveDefInBranchPass` now reports whether it changed the graph. It returned `ExportPass.call`'s result, which always says modified, so a group containing it could never converge: it ran all of its rounds and hit this warning. Like `AdvanceQuantizeOpAboveDefChainPass`, it now re-traces and reports a change only when it advanced a quantize. The rewrite itself is unchanged. Differential Revision: D129194624 Pull Request resolved: #24759
1 parent 4411b8d commit 62903fa

3 files changed

Lines changed: 57 additions & 11 deletions

File tree

‎backends/cadence/aot/tests/test_reorder_ops_passes.py‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def test_advance_branched_quantize(self) -> None:
288288
0,
289289
)
290290

291-
def _advance_two_branch_quantize(
291+
def _two_branch_quantize_graph(
292292
self,
293293
first_qparams: Tuple[float, int],
294294
second_qparams: Tuple[float, int],
@@ -311,13 +311,34 @@ def _advance_two_branch_quantize(
311311
)
312312
)
313313
builder.output(quantized)
314+
return builder.get_graph_module()
315+
316+
def _advance_two_branch_quantize(
317+
self,
318+
first_qparams: Tuple[float, int],
319+
second_qparams: Tuple[float, int],
320+
) -> torch.fx.GraphModule:
314321
graph_module = cast(
315322
PassResult,
316-
AdvanceQuantizeOpAboveDefInBranchPass()(builder.get_graph_module()),
323+
AdvanceQuantizeOpAboveDefInBranchPass()(
324+
self._two_branch_quantize_graph(first_qparams, second_qparams)
325+
),
317326
).graph_module
318327
graph_module.graph.eliminate_dead_code()
319328
return graph_module
320329

330+
def test_reports_whether_it_advanced_a_quantize(self) -> None:
331+
for second_qparams, advanced in (((0.1, 10), True), ((2.0, 4), False)):
332+
with self.subTest(advanced=advanced):
333+
result = cast(
334+
PassResult,
335+
AdvanceQuantizeOpAboveDefInBranchPass()(
336+
self._two_branch_quantize_graph((0.1, 10), second_qparams)
337+
),
338+
)
339+
340+
self.assertEqual(result.modified, advanced)
341+
321342
def test_does_not_advance_quantize_when_qparams_differ(self) -> None:
322343
# Requantizing from one hoisted quantize can clip or coarsen a branch, so
323344
# the branches are left alone unless they already share qparams

‎backends/transforms/advance_quantize_op_above_def_in_branch.py‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ def get_descendent_quant_ops(self, node: torch.fx.Node) -> List[torch.fx.Node]:
107107

108108
return descendent_quant_ops
109109

110-
def advance_quantize_op(self, graph_module: torch.fx.GraphModule):
110+
def advance_quantize_op(self, graph_module: torch.fx.GraphModule) -> bool:
111+
modified = False
111112
graph = graph_module.graph
112113
for node in graph.nodes:
113114
# We are only interested in call functions and placeholders
@@ -150,6 +151,7 @@ def advance_quantize_op(self, graph_module: torch.fx.GraphModule):
150151
node.replace_all_uses_with(dom_quant_node)
151152
dom_quant_node.args = dom_quant_args
152153
graph.erase_node(quant_node)
154+
modified = True
153155
continue
154156

155157
# Otherwise we have multiple quant descendents. Requantizing them from
@@ -185,12 +187,16 @@ def advance_quantize_op(self, graph_module: torch.fx.GraphModule):
185187
)
186188
dequant_node.args = (quant_node.args[0],) + rep_args
187189
quant_node.args = (dequant_node,) + quant_node.args[1:]
190+
modified = True
188191

189-
graph_module.recompile()
190-
graph_module.graph.eliminate_dead_code()
192+
return modified
191193

192194
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
193195
self.graph_module = graph_module
194-
self.advance_quantize_op(graph_module)
195-
result = super().call(graph_module)
196-
return result
196+
modified = self.advance_quantize_op(graph_module)
197+
if modified:
198+
graph_module.recompile()
199+
graph_module.graph.eliminate_dead_code()
200+
return super().call(graph_module)
201+
202+
return PassResult(graph_module, False)

‎backends/transforms/fused_quant/pass_base.py‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
# LICENSE file in the root directory of this source tree.
66

77

8+
import logging
9+
810
from executorch.exir.pass_base import ExportedProgramPassBase, ExportedProgramPassResult
911
from executorch.exir.pass_manager import ExportedProgramPassManager, PassType
1012
from torch.export import ExportedProgram
1113

14+
logger: logging.Logger = logging.getLogger(__name__)
15+
1216

1317
class IterativePassGroup(ExportedProgramPassBase):
1418
"""Runs a group of passes as a single ExportedProgramPassBase.
@@ -22,8 +26,23 @@ def __init__(self, passes: list[PassType], steps: int) -> None:
2226
# rebuild this group with wrapped leaf passes.
2327
self._passes = passes
2428
self._steps = steps
25-
self._pm = ExportedProgramPassManager(passes, steps=steps)
29+
self._pm = ExportedProgramPassManager(passes, steps=1)
2630

2731
def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
28-
result = self._pm(exported_program)
29-
return ExportedProgramPassResult(result.exported_program, result.modified)
32+
modified = False
33+
for _ in range(self._steps):
34+
result = self._pm(exported_program)
35+
exported_program = result.exported_program
36+
if not result.modified:
37+
return ExportedProgramPassResult(exported_program, modified)
38+
modified = True
39+
# A single-step group runs its passes once by design; only a group meant
40+
# to iterate to a fixed point can fail to converge.
41+
if self._steps > 1:
42+
logger.warning(
43+
"%s did not converge within %d steps; the graph was still changing "
44+
"on the last step",
45+
type(self).__name__,
46+
self._steps,
47+
)
48+
return ExportedProgramPassResult(exported_program, modified)

0 commit comments

Comments
 (0)