Skip to content

Commit 2cce41f

Browse files
authored
Cortex-M: Support avg_pool2d ceil_mode lowering (#21039)
Cortex-M avg_pool2d quantized lowering rejected ceil_mode=True and left the graph on the fallback aten.avg_pool2d path. That preserved correctness but prevented optimized Cortex-M lowering for cases that CMSIS-NN can execute from statically known output shapes. This patch threads ceil_mode through the cortex_m::quantized_avg_pool2d schema, fake implementation, lowering, and C++ kernel wrapper. The wrapper validates the static output shape against ceil-mode pooling semantics before dispatching to CMSIS-NN. The avg_pool2d tests now cover optimized ceil_mode lowering, padded ceil_mode, count_include_pad with ceil_mode, non-square multi-channel ceil_mode, and divisor_override fallback behavior. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani --------- Signed-off-by: Baris Demir <baris.demir@arm.com>
1 parent ec6a111 commit 2cce41f

9 files changed

Lines changed: 144 additions & 27 deletions

File tree

backends/arm/_passes/decompose_avg_pool2d_pass.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ def _compute_post_pad(
4848

4949
if pad == 0:
5050
return pad
51-
if ceil_mode and divisor_override is None:
52-
return pad
53-
5451
pad_adjust = adjust_pooling_pad_if_needed(size, kernel, stride, pad, ceil_mode)
5552

56-
# Padding must always be above 0, the above adjustment may return -1
57-
if pad_adjust > 0:
53+
if ceil_mode and divisor_override is None and pad_adjust > pad:
54+
return pad
55+
56+
# Padding must never be below 0; the above adjustment may return -1.
57+
if pad_adjust >= 0:
5858
return pad_adjust
5959
return pad
6060

backends/arm/operators/operator_validation_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ def adjust_pooling_pad_if_needed(
199199
numerator = input_size - kernel_size + 2 * pad
200200
if ceil_mode:
201201
output_size = (numerator + stride - 1) // stride + 1
202+
if (output_size - 1) * stride >= input_size + pad:
203+
output_size -= 1
202204
else:
203205
output_size = numerator // stride + 1
204206

backends/arm/test/ops/test_avg_pool2d.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ def forward(self, x: torch.Tensor):
9191
AvgPool2d(3, (1, 2), 1, True, True),
9292
(torch.rand(1, 1, 14, 14),),
9393
),
94+
"ceil_mode_count_include_pad_kernel_stride": lambda: (
95+
AvgPool2d(3, 3, 1, True, True),
96+
(torch.rand(1, 1, 5, 5),),
97+
),
9498
"divisor_override": lambda: (
9599
AvgPool2d(3, 2, 1, False, False, divisor_override=2),
96100
(torch.rand(1, 1, 14, 14),),

backends/cortex_m/ops/op_quantized_avg_pool2d.cpp

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,67 @@ namespace native {
1212

1313
using KernelRuntimeContext = torch::executor::KernelRuntimeContext;
1414

15+
namespace {
16+
17+
int32_t pooling_output_size(
18+
int32_t input,
19+
int32_t kernel,
20+
int32_t stride,
21+
int32_t padding,
22+
bool ceil_mode) {
23+
const int32_t numerator = input + 2 * padding - kernel;
24+
int32_t output = numerator / stride + 1;
25+
if (ceil_mode && numerator % stride != 0) {
26+
output += 1;
27+
}
28+
if (ceil_mode && (output - 1) * stride >= input + padding) {
29+
output -= 1;
30+
}
31+
return output;
32+
}
33+
34+
bool validate_avg_pool2d_output_size(
35+
KernelRuntimeContext& context,
36+
const CmsisPool2DConfig& pool_config,
37+
bool ceil_mode) {
38+
const int32_t expected_h = pooling_output_size(
39+
pool_config.input_dims.h,
40+
pool_config.filter_dims.h,
41+
pool_config.pool_params.stride.h,
42+
pool_config.pool_params.padding.h,
43+
ceil_mode);
44+
const int32_t expected_w = pooling_output_size(
45+
pool_config.input_dims.w,
46+
pool_config.filter_dims.w,
47+
pool_config.pool_params.stride.w,
48+
pool_config.pool_params.padding.w,
49+
ceil_mode);
50+
51+
if (pool_config.output_dims.h != expected_h ||
52+
pool_config.output_dims.w != expected_w) {
53+
ET_LOG(
54+
Error,
55+
"quantized_avg_pool2d_out: output shape mismatch - actual: (%d, %d) expected: (%d, %d)",
56+
pool_config.output_dims.h,
57+
pool_config.output_dims.w,
58+
expected_h,
59+
expected_w);
60+
context.fail(Error::InvalidArgument);
61+
return false;
62+
}
63+
return true;
64+
}
65+
66+
} // namespace
67+
1568
// cppcheck-suppress unusedFunction
1669
Tensor& quantized_avg_pool2d_out(
1770
KernelRuntimeContext& context,
1871
const Tensor& input,
1972
const Int64ArrayRef kernel_size,
2073
const Int64ArrayRef stride,
2174
const Int64ArrayRef padding,
75+
const bool ceil_mode,
2276
const int64_t zero_point,
2377
const int64_t multiplier,
2478
const int64_t shift,
@@ -39,10 +93,16 @@ Tensor& quantized_avg_pool2d_out(
3993
stride,
4094
padding,
4195
dilation,
42-
false,
96+
ceil_mode,
4397
activation_min,
4498
activation_max,
45-
pool_config)) {
99+
pool_config,
100+
true,
101+
true)) {
102+
return out;
103+
}
104+
105+
if (!validate_avg_pool2d_output_size(context, pool_config, ceil_mode)) {
46106
return out;
47107
}
48108

backends/cortex_m/ops/operators.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,7 @@ def quantized_transpose_conv2d_impl(
12151215
"int[] kernel_size, "
12161216
"int[] stride, "
12171217
"int[] padding, "
1218+
"bool ceil_mode, "
12181219
"int zero_point, "
12191220
"int multiplier, "
12201221
"int shift, "
@@ -1227,6 +1228,7 @@ def quantized_transpose_conv2d_impl(
12271228
"int[] kernel_size, "
12281229
"int[] stride, "
12291230
"int[] padding, "
1231+
"bool ceil_mode, "
12301232
"int zero_point, "
12311233
"int multiplier, "
12321234
"int shift, "
@@ -1241,6 +1243,7 @@ def quantized_avg_pool2d_meta(
12411243
kernel_size: Sequence[int],
12421244
stride: Sequence[int],
12431245
padding: Sequence[int],
1246+
ceil_mode: bool,
12441247
zero_point: int,
12451248
multiplier: int,
12461249
shift: int,
@@ -1254,7 +1257,7 @@ def quantized_avg_pool2d_meta(
12541257
kernel,
12551258
stride=stride_vals,
12561259
padding=padding_vals,
1257-
ceil_mode=False,
1260+
ceil_mode=ceil_mode,
12581261
count_include_pad=False,
12591262
)
12601263
return torch.empty(
@@ -1271,6 +1274,7 @@ def quantized_avg_pool2d_impl(
12711274
kernel_size: Sequence[int],
12721275
stride: Sequence[int],
12731276
padding: Sequence[int],
1277+
ceil_mode: bool,
12741278
zero_point: int,
12751279
multiplier: int,
12761280
shift: int,
@@ -1288,7 +1292,7 @@ def quantized_avg_pool2d_impl(
12881292
kernel,
12891293
stride=stride_vals,
12901294
padding=padding_vals,
1291-
ceil_mode=False,
1295+
ceil_mode=ceil_mode,
12921296
count_include_pad=False,
12931297
)
12941298
result = quantize_per_tensor_cmsis(result, zero_point, multiplier, shift)

backends/cortex_m/ops/operators.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
- arg_meta: null
9191
kernel_name: cortex_m::quantized_transpose_conv2d_out
9292

93-
- func: cortex_m::quantized_avg_pool2d.out(Tensor input, int[] kernel_size, int[] stride, int[] padding, int zero_point, int multiplier, int shift, Tensor scratch, *, Tensor(a!) out) -> Tensor(a!)
93+
- func: cortex_m::quantized_avg_pool2d.out(Tensor input, int[] kernel_size, int[] stride, int[] padding, bool ceil_mode, int zero_point, int multiplier, int shift, Tensor scratch, *, Tensor(a!) out) -> Tensor(a!)
9494
variants: function
9595
kernels:
9696
- arg_meta: null

backends/cortex_m/passes/aten_to_cortex_m_pass.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ def _get_avg_pool2d_replacement(
820820
count_include_pad = cast(bool, pool_args[5]) if len(pool_args) > 5 else True
821821
divisor_override = pool_args[6] if len(pool_args) > 6 else None
822822

823-
if ceil_mode or divisor_override is not None:
823+
if divisor_override is not None:
824824
return None
825825

826826
input_node = cast(Node, pool_args[0])
@@ -848,6 +848,7 @@ def _get_avg_pool2d_replacement(
848848
kernel_size,
849849
stride,
850850
avg_padding,
851+
ceil_mode,
851852
int(input_zp),
852853
int(output_mult),
853854
int(output_shift),

backends/cortex_m/quantizer/pattern_checkers.py

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

6-
from typing import cast
7-
86
import torch
97
from executorch.backends.arm._passes.arm_pass_utils import get_first_fake_tensor
108
from executorch.backends.arm.quantizer.arm_quantizer_utils import PatternCheck
@@ -250,8 +248,8 @@ def check_pattern(cls, pattern):
250248
if not pattern:
251249
return False
252250
node = pattern[0]
253-
ceil_mode = cast(bool, node.args[4]) if len(node.args) > 4 else False
254-
return not ceil_mode
251+
divisor_override = node.args[6] if len(node.args) > 6 else None
252+
return divisor_override is None
255253

256254
@classmethod
257255
def check_quantization_config(

backends/cortex_m/test/ops/test_avg_pool2d.py

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ class CortexMAvgPool2d(torch.nn.Module):
2828
}
2929

3030
def __init__(
31-
self, kernel_size, stride, padding=0, ceil_mode=False, count_include_pad=False
31+
self,
32+
kernel_size,
33+
stride,
34+
padding=0,
35+
ceil_mode=False,
36+
count_include_pad=False,
37+
divisor_override=None,
3238
):
3339
super().__init__()
3440
self.pool = torch.nn.AvgPool2d(
@@ -37,6 +43,7 @@ def __init__(
3743
padding,
3844
ceil_mode=ceil_mode,
3945
count_include_pad=count_include_pad,
46+
divisor_override=divisor_override,
4047
)
4148

4249
def forward(self, x): # noqa: D102
@@ -68,14 +75,37 @@ def forward(self, x): # noqa: D102
6875
}
6976

7077

71-
# ceil_mode=True is not supported by the CMSIS-NN avg_pool kernel; the convert
72-
# pass leaves aten.avg_pool2d in the graph for a portable kernel to handle. The
73-
# Cortex-M runner does not register aten.avg_pool2d, so this is dialect-only.
74-
fallback_test_cases = {
78+
ceil_mode_test_cases = {
7579
"avgpool_2x2_ceil_mode": McuTestCase(
7680
CortexMAvgPool2d(kernel_size=2, stride=2, ceil_mode=True),
7781
(ramp_tensor(0, 24, (1, 1, 5, 5)),),
7882
),
83+
"avgpool_3x3_s2_pad1_ceil_mode": McuTestCase(
84+
CortexMAvgPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=True),
85+
(ramp_tensor(0, 15, (1, 1, 4, 4)),),
86+
),
87+
"avgpool_3x3_s2_pad1_countinc_ceil_mode": McuTestCase(
88+
CortexMAvgPool2d(
89+
kernel_size=3,
90+
stride=2,
91+
padding=1,
92+
ceil_mode=True,
93+
count_include_pad=True,
94+
),
95+
(ramp_tensor(0, 15, (1, 1, 4, 4)),),
96+
),
97+
"avgpool_2x3_s2x3_ceil_mode": McuTestCase(
98+
CortexMAvgPool2d(kernel_size=(2, 3), stride=(2, 3), ceil_mode=True),
99+
(ramp_tensor(0, 79, (1, 2, 5, 8)).to(memory_format=torch.channels_last),),
100+
),
101+
}
102+
103+
104+
fallback_test_cases = {
105+
"avgpool_2x2_divisor_override": McuTestCase(
106+
CortexMAvgPool2d(kernel_size=2, stride=2, divisor_override=2),
107+
(ramp_tensor(0, 15, (1, 1, 4, 4)),),
108+
),
79109
}
80110

81111

@@ -119,15 +149,33 @@ def test_dialect_avg_pool2d(test_case, cortex_m_target):
119149
), f"scratch buffer size mismatch: got {scratch_size}, expected {expected_size}"
120150

121151

152+
@parametrize("test_case", ceil_mode_test_cases)
153+
def test_dialect_avg_pool2d_ceil_mode(test_case, cortex_m_target):
154+
tester = CortexMTester(
155+
test_case.model, test_case.example_inputs, target_config=cortex_m_target
156+
)
157+
tester.test_dialect(
158+
test_case.model.ops_before_transforms,
159+
test_case.model.ops_after_transforms,
160+
qtol=1,
161+
)
162+
163+
164+
@parametrize("test_case", ceil_mode_test_cases)
165+
def test_implementation_avg_pool2d_ceil_mode(test_case, cortex_m_target):
166+
tester = CortexMTester(
167+
test_case.model, test_case.example_inputs, target_config=cortex_m_target
168+
)
169+
tester.test_implementation(qtol=1)
170+
171+
122172
@parametrize("test_case", fallback_test_cases)
123-
def test_dialect_avg_pool2d_fallback(test_case):
124-
tester = CortexMTester(test_case.model, test_case.example_inputs)
173+
def test_dialect_avg_pool2d_fallback(test_case, cortex_m_target):
174+
tester = CortexMTester(
175+
test_case.model, test_case.example_inputs, target_config=cortex_m_target
176+
)
125177
tester.test_dialect(
126-
{
127-
"executorch_exir_dialects_edge__ops_aten_avg_pool2d_default": 1,
128-
"executorch_exir_dialects_edge__ops_quantized_decomposed_quantize_per_tensor_default": 2,
129-
"executorch_exir_dialects_edge__ops_quantized_decomposed_dequantize_per_tensor_default": 2,
130-
},
178+
test_case.model.ops_before_transforms,
131179
{
132180
"executorch_exir_dialects_edge__ops_aten_avg_pool2d_default": 1,
133181
"executorch_exir_dialects_edge__ops_cortex_m_quantize_per_tensor_default": 2,

0 commit comments

Comments
 (0)