Skip to content

Commit 4f95191

Browse files
authored
Cortex-M backend: Fix Cortex-M max pool fallback lowering (pytorch#20905)
- Lower unsupported quantized max_pool2d cases through max_pool2d_with_indices and extract the tensor result. - Add the fallback op to the Cortex-M test runner and cover Edge, ExecuTorch, and implementation fallback paths. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Xingguo Li <xingguo.li@arm.com>
1 parent 43bdb7b commit 4f95191

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

backends/cortex_m/passes/aten_to_cortex_m_pass.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# LICENSE file in the root directory of this source tree.
77

88
import math
9+
import operator
910
from typing import cast, Optional
1011

1112
import executorch.backends.cortex_m.ops.operators # noqa
@@ -65,11 +66,17 @@ def __init__(
6566

6667
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
6768
result = super().call(graph_module)
69+
# RemoveGetItemPass converts max_pool2d_with_indices to max_pool2d. Models
70+
# such as GoogleNet and SqueezeNet use configurations unsupported by the
71+
# Cortex-M kernel, so restore the portable with-indices fallback for them.
72+
max_pool_modified = _restore_max_pool2d_with_indices_fallback(
73+
result.graph_module
74+
)
6875

6976
for node in result.graph_module.graph.nodes:
7077
self._initialize_alloc_node_size(node)
7178

72-
return result
79+
return PassResult(result.graph_module, result.modified or max_pool_modified)
7380

7481
def _initialize_alloc_node_size(self, node: torch.fx.Node) -> None:
7582
"""Initialize trailing scratch alloc nodes for CMSIS-NN kernels."""
@@ -107,6 +114,76 @@ def _create_uninitialized_alloc_node(
107114
)
108115

109116

117+
def _first_meta_value(value):
118+
if isinstance(value, (tuple, list)):
119+
return value[0]
120+
return value
121+
122+
123+
def _indices_meta_value(output_value):
124+
return output_value.new_empty(output_value.shape, dtype=torch.int64)
125+
126+
127+
def _indices_tensor_meta(output_tensor_meta):
128+
if output_tensor_meta is None:
129+
return None
130+
return output_tensor_meta._replace(
131+
dtype=torch.int64, is_quantized=False, qparams={}
132+
)
133+
134+
135+
def _restore_max_pool2d_with_indices_fallback(
136+
graph_module: torch.fx.GraphModule,
137+
) -> bool:
138+
modified = False
139+
for node in list(graph_module.graph.nodes):
140+
if (
141+
node.op != "call_function"
142+
or node.target != exir_ops.edge.aten.max_pool2d.default
143+
):
144+
continue
145+
146+
output_value = _first_meta_value(node.meta.get("val"))
147+
output_tensor_meta = _first_meta_value(node.meta.get("tensor_meta"))
148+
149+
with graph_module.graph.inserting_before(node):
150+
max_pool_with_indices = graph_module.graph.create_node(
151+
"call_function",
152+
exir_ops.edge.aten.max_pool2d_with_indices.default,
153+
args=node.args,
154+
kwargs=node.kwargs,
155+
)
156+
getitem = graph_module.graph.create_node(
157+
"call_function",
158+
operator.getitem,
159+
args=(max_pool_with_indices, 0),
160+
kwargs={},
161+
)
162+
163+
max_pool_with_indices.meta = dict(node.meta)
164+
max_pool_with_indices.meta["val"] = (
165+
output_value,
166+
_indices_meta_value(output_value),
167+
)
168+
max_pool_with_indices.meta["tensor_meta"] = (
169+
output_tensor_meta,
170+
_indices_tensor_meta(output_tensor_meta),
171+
)
172+
getitem.meta = dict(node.meta)
173+
getitem.meta["val"] = output_value
174+
getitem.meta["tensor_meta"] = output_tensor_meta
175+
176+
node.replace_all_uses_with(getitem)
177+
graph_module.graph.erase_node(node)
178+
modified = True
179+
180+
if modified:
181+
graph_module.graph.eliminate_dead_code()
182+
graph_module.recompile()
183+
184+
return modified
185+
186+
110187
_SOFTMAX_INPUT_INTEGER_BITS = 5
111188

112189

backends/cortex_m/test/build_test_runner.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ aten::add.out,\
4040
aten::clamp.out,\
4141
aten::mul.out,\
4242
aten::convolution.out,\
43+
aten::max_pool2d_with_indices.out,\
4344
dim_order_ops::_clone_dim_order.out,\
4445
aten::cat.out,\
4546
aten::full.out,\

backends/cortex_m/test/ops/test_max_pool2d.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,28 @@ def test_dialect_max_pool2d_fallback(test_case, cortex_m_target):
116116
{
117117
"executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 1,
118118
"executorch_exir_dialects_edge__ops_cortex_m_dequantize_per_tensor_default": 1,
119-
"executorch_exir_dialects_edge__ops_aten_max_pool2d_default": 1,
119+
"executorch_exir_dialects_edge__ops_aten_max_pool2d_with_indices_default": 1,
120120
},
121121
qtol=1,
122122
)
123123

124124

125+
@parametrize("test_case", fallback_test_cases)
126+
def test_executorch_max_pool2d_fallback(test_case, cortex_m_target):
127+
tester = CortexMTester(
128+
test_case.model, test_case.example_inputs, target_config=cortex_m_target
129+
)
130+
tester.quantize().export().to_edge().run_passes().to_executorch()
131+
132+
133+
@parametrize("test_case", fallback_test_cases)
134+
def test_implementation_max_pool2d_fallback(test_case, cortex_m_target):
135+
tester = CortexMTester(
136+
test_case.model, test_case.example_inputs, target_config=cortex_m_target
137+
)
138+
tester.test_implementation(qtol=1)
139+
140+
125141
@parametrize("test_case", test_cases, xfails=xfails_max_pool2d)
126142
def test_implementation_max_pool2d(test_case, cortex_m_target):
127143
tester = CortexMTester(

0 commit comments

Comments
 (0)