Skip to content

Commit 45e1703

Browse files
authored
[TorchToLinalg] Fix avg_pool divisor to account for dilation. (#4671)
PoolSizeCalculator::getPoolSize() ignored dilation when computing the average pooling divisor. With dilation > 1, edge positions produced incorrect averages because the divisor did not reflect the actual number of valid dilated taps within bounds. Note adding an e2e test is not possible as PyTorch doesn't allow creating a pool op with dilation. The bug was found as part of stress testing onnx importer path by creating individual onnx ops.
1 parent e206607 commit 45e1703

2 files changed

Lines changed: 165 additions & 18 deletions

File tree

lib/Conversion/TorchToLinalg/Pooling.cpp

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,8 @@ template <int NumOfDims> class PoolSizeCalculator {
960960
// height and width labels in variables.
961961
Value getPoolSize(OpBuilder &b, SmallVectorImpl<Value> &kernelSizeIntValues,
962962
SmallVectorImpl<int64_t> &strideInts,
963-
SmallVectorImpl<int64_t> &paddingInts);
963+
SmallVectorImpl<int64_t> &paddingInts,
964+
SmallVectorImpl<int64_t> &dilationInts);
964965

965966
private:
966967
int64_t SumPoolTypeDimIndex[NumOfDims];
@@ -994,12 +995,14 @@ PoolSizeCalculator<NumOfDims>::PoolSizeCalculator(
994995
template <int NumOfDims>
995996
Value PoolSizeCalculator<NumOfDims>::getPoolSize(
996997
OpBuilder &b, SmallVectorImpl<Value> &kernelDimSizes,
997-
SmallVectorImpl<int64_t> &strideInts,
998-
SmallVectorImpl<int64_t> &paddingInts) {
998+
SmallVectorImpl<int64_t> &strideInts, SmallVectorImpl<int64_t> &paddingInts,
999+
SmallVectorImpl<int64_t> &dilationInts) {
9991000
Value poolSize;
10001001

10011002
Value cstZero =
10021003
b.createOrFold<arith::ConstantOp>(location, b.getI64IntegerAttr(0));
1004+
Value cstOne =
1005+
b.createOrFold<arith::ConstantOp>(location, b.getI64IntegerAttr(1));
10031006

10041007
for (int i = 0; i < NumOfDims; ++i) {
10051008
// See the link below for the PyTorch implementation where this is
@@ -1016,25 +1019,60 @@ Value PoolSizeCalculator<NumOfDims>::getPoolSize(
10161019
location, b.getI64IntegerAttr(strideInts[i]));
10171020
Value PadDim = b.createOrFold<arith::ConstantOp>(
10181021
location, b.getI64IntegerAttr(paddingInts[i]));
1022+
Value DilDim = b.createOrFold<arith::ConstantOp>(
1023+
location, b.getI64IntegerAttr(dilationInts[i]));
10191024
Value ODimDDim = b.createOrFold<arith::MulIOp>(location, ODim, DDim);
10201025
Value IDim0 = b.createOrFold<arith::SubIOp>(location, ODimDDim, PadDim);
10211026
Value IDim = castIndexToInt64(b, location, InputSpatialDimSizes[i]);
1027+
1028+
// Effective window end: IDim0 + (kernel - 1) * dilation + 1
1029+
Value KernelM1 =
1030+
b.createOrFold<arith::SubIOp>(location, kernelDimSizes[i], cstOne);
1031+
Value KernelM1Dil =
1032+
b.createOrFold<arith::MulIOp>(location, KernelM1, DilDim);
1033+
Value EffectiveKernel =
1034+
b.createOrFold<arith::AddIOp>(location, KernelM1Dil, cstOne);
10221035
Value IDim0KDim =
1023-
b.createOrFold<arith::AddIOp>(location, IDim0, kernelDimSizes[i]);
1036+
b.createOrFold<arith::AddIOp>(location, IDim0, EffectiveKernel);
10241037
Value IDimPadDim = b.createOrFold<arith::AddIOp>(location, IDim, PadDim);
10251038
Value IDim1 =
10261039
b.createOrFold<arith::MinSIOp>(location, IDim0KDim, IDimPadDim);
10271040

1028-
Value IDim0Clamped =
1029-
b.createOrFold<arith::MaxSIOp>(location, IDim0, cstZero);
10301041
Value IDim1Clamped = b.createOrFold<arith::MinSIOp>(location, IDim1, IDim);
1031-
Value IDim1_IDim0_Clamped =
1032-
b.createOrFold<arith::SubIOp>(location, IDim1Clamped, IDim0Clamped);
10331042

1034-
Value poolSizeDim =
1035-
!isCountIncludePad
1036-
? IDim1_IDim0_Clamped
1037-
: b.createOrFold<arith::SubIOp>(location, IDim1, IDim0);
1043+
// Count valid taps using k_min/k_max approach:
1044+
// Tap k is valid when IDim0 + k*dilation is in [0, IDim1Clamped), where
1045+
// IDim1Clamped = min(window end, IDim) is the input extent clamped to the
1046+
// window.
1047+
// k_min = ceil(max(-IDim0, 0) / dilation) -- first valid k
1048+
// k_max_excl = ceil((IDim1Clamped - IDim0) / dilation) -- first k past the
1049+
// end ValidTaps = max(k_max_excl - k_min, 0)
1050+
Value NegIDim0 = b.createOrFold<arith::SubIOp>(location, cstZero, IDim0);
1051+
Value KMinNumer =
1052+
b.createOrFold<arith::MaxSIOp>(location, NegIDim0, cstZero);
1053+
Value KMin =
1054+
b.createOrFold<arith::CeilDivSIOp>(location, KMinNumer, DilDim);
1055+
Value IDim1ClampedMinusIDim0 =
1056+
b.createOrFold<arith::SubIOp>(location, IDim1Clamped, IDim0);
1057+
Value KMaxExcl = b.createOrFold<arith::CeilDivSIOp>(
1058+
location, IDim1ClampedMinusIDim0, DilDim);
1059+
Value KDiff = b.createOrFold<arith::SubIOp>(location, KMaxExcl, KMin);
1060+
Value ValidTaps = b.createOrFold<arith::MaxSIOp>(location, KDiff, cstZero);
1061+
1062+
// For count_include_pad: count every dilated tap position in the full
1063+
// effective window [IDim0, IDim1), including padded positions. Same as
1064+
// ValidTaps but WITHOUT the low-side clamp (padded taps are counted), so
1065+
// it reduces to k_min == 0:
1066+
// FullTaps = max(ceil((IDim1 - IDim0) / dilation), 0)
1067+
// The max(.., 0) keeps this in agreement with ValidTaps on a degenerate
1068+
// empty window (range <= 0 => 0 taps).
1069+
Value FullRange = b.createOrFold<arith::SubIOp>(location, IDim1, IDim0);
1070+
Value FullTapsRaw =
1071+
b.createOrFold<arith::CeilDivSIOp>(location, FullRange, DilDim);
1072+
Value FullTaps =
1073+
b.createOrFold<arith::MaxSIOp>(location, FullTapsRaw, cstZero);
1074+
1075+
Value poolSizeDim = !isCountIncludePad ? ValidTaps : FullTaps;
10381076
if (i == 0) {
10391077
poolSize = poolSizeDim;
10401078
} else {
@@ -1060,7 +1098,8 @@ class ConvertAtenAvgPoolOp : public OpConversionPattern<OpTy> {
10601098
static bool
10611099
doesAvgPoolDivisorNeedsClamping(bool ceilMode, bool countIncludePad,
10621100
SmallVectorImpl<int64_t> &strideInts,
1063-
SmallVectorImpl<int64_t> &paddingInts);
1101+
SmallVectorImpl<int64_t> &paddingInts,
1102+
SmallVectorImpl<int64_t> &dilationInts);
10641103

10651104
// Creates the average pooling operation value with a clamped
10661105
// divisor. The clamped divisor is the product of kernel
@@ -1073,6 +1112,7 @@ class ConvertAtenAvgPoolOp : public OpConversionPattern<OpTy> {
10731112
SmallVectorImpl<Value> &kernelDimSizes,
10741113
SmallVectorImpl<int64_t> &strideInts,
10751114
SmallVectorImpl<int64_t> &paddingInts,
1115+
SmallVectorImpl<int64_t> &dilationInts,
10761116
SmallVector<AffineMap> &indexingMapsAvg,
10771117
SmallVector<utils::IteratorType> &iteratorTypesAvg);
10781118

@@ -1147,11 +1187,11 @@ LogicalResult ConvertAtenAvgPoolOp<OpTy, PoolingOpTy, Dim>::matchAndRewrite(
11471187
Dim + 2, utils::IteratorType::parallel);
11481188

11491189
if (doesAvgPoolDivisorNeedsClamping(ceilMode, countIncludePad, strideInts,
1150-
paddingInts)) {
1190+
paddingInts, dilationInts)) {
11511191
return createAveragePoolValueWithClampedDivisor(
11521192
ceilMode, countIncludePad, op, adaptor, rewriter, self, sumPool,
11531193
outputTensor, resultType, kernelSizeIntValues, strideInts, paddingInts,
1154-
indexingMapsAvg, iteratorTypesAvg);
1194+
dilationInts, indexingMapsAvg, iteratorTypesAvg);
11551195
}
11561196

11571197
return createAveragePoolValueWithRegularDivisor(
@@ -1163,7 +1203,8 @@ template <typename OpTy, typename PoolingOpTy, int Dim>
11631203
bool ConvertAtenAvgPoolOp<OpTy, PoolingOpTy, Dim>::
11641204
doesAvgPoolDivisorNeedsClamping(bool ceilMode, bool countIncludePad,
11651205
SmallVectorImpl<int64_t> &strideInts,
1166-
SmallVectorImpl<int64_t> &paddingInts) {
1206+
SmallVectorImpl<int64_t> &paddingInts,
1207+
SmallVectorImpl<int64_t> &dilationInts) {
11671208
// Determines whether the average pooling divisor needs to be clamped
11681209
// (i.e., adjusted to exclude padded or out-of-bounds elements).
11691210
//
@@ -1193,8 +1234,11 @@ bool ConvertAtenAvgPoolOp<OpTy, PoolingOpTy, Dim>::
11931234
!llvm::all_of(paddingInts, [](int64_t p) { return p == 0; });
11941235
bool allStridesUnitary =
11951236
llvm::all_of(strideInts, [](int64_t s) { return s == 1; });
1237+
bool allDilationsUnitary =
1238+
llvm::all_of(dilationInts, [](int64_t d) { return d == 1; });
11961239

1197-
return (!countIncludePad && hasPadding) || (ceilMode && !allStridesUnitary);
1240+
return (!countIncludePad && hasPadding) || (ceilMode && !allStridesUnitary) ||
1241+
!allDilationsUnitary;
11981242
}
11991243

12001244
template <typename OpTy, typename PoolingOpTy, int Dim>
@@ -1206,6 +1250,7 @@ LogicalResult ConvertAtenAvgPoolOp<OpTy, PoolingOpTy, Dim>::
12061250
SmallVectorImpl<Value> &kernelDimSizes,
12071251
SmallVectorImpl<int64_t> &strideInts,
12081252
SmallVectorImpl<int64_t> &paddingInts,
1253+
SmallVectorImpl<int64_t> &dilationInts,
12091254
SmallVector<AffineMap> &indexingMapsAvg,
12101255
SmallVector<utils::IteratorType> &iteratorTypesAvg) {
12111256
Location loc = op->getLoc();
@@ -1241,7 +1286,7 @@ LogicalResult ConvertAtenAvgPoolOp<OpTy, PoolingOpTy, Dim>::
12411286
[&](OpBuilder &b, Location loc, ValueRange args) {
12421287
if (!poolSize) {
12431288
poolSize = poolSizeCalculator.getPoolSize(
1244-
b, kernelDimSizes, strideInts, paddingInts);
1289+
b, kernelDimSizes, strideInts, paddingInts, dilationInts);
12451290
}
12461291
Value divisor =
12471292
convertScalarToDtype(b, loc, poolSize, resultElementType);

test/Conversion/TorchToLinalg/pooling.mlir

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,105 @@ func.func @forward_avgpool_2d_ceil(%arg0: !torch.vtensor<[1,1,4,4],f32>) -> !tor
291291
%3 = torch.aten.avg_pool2d %arg0, %0, %2, %1, %true, %false, %none : !torch.vtensor<[1,1,4,4],f32>, !torch.list<int>, !torch.list<int>, !torch.list<int>, !torch.bool, !torch.bool, !torch.none -> !torch.vtensor<[1,1,2,2],f32>
292292
return %3 : !torch.vtensor<[1,1,2,2],f32>
293293
}
294+
295+
// -----
296+
297+
// CHECK: #[[$MAP:.*]] = affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)>
298+
// CHECK-LABEL: func @forward_avgpool_2d_ceil_dilated
299+
func.func @forward_avgpool_2d_ceil_dilated(%arg0: !torch.vtensor<[1,1,7,7],f32>) -> !torch.vtensor<[1,1,2,2],f32> {
300+
// Sum pool uses dilation=2:
301+
// CHECK: linalg.pooling_nchw_sum {dilations = dense<2> : vector<2xi64>, strides = dense<3> : vector<2xi64>}
302+
303+
// The divisor generic uses dilation to count valid taps per dim:
304+
// effectiveKernel = (3-1)*2+1 = 5, then ceildivsi to count valid taps.
305+
// CHECK: linalg.generic {indexing_maps = [#[[$MAP]], #[[$MAP]]], iterator_types = ["parallel", "parallel", "parallel", "parallel"]}
306+
// CHECK: %[[C5:.*]] = arith.constant 5 : i64
307+
// CHECK: arith.addi %{{.*}}, %[[C5]] : i64
308+
// CHECK: arith.ceildivsi {{.*}} : i64
309+
// CHECK: arith.divf
310+
// CHECK: linalg.yield
311+
312+
%int3 = torch.constant.int 3
313+
%int3_0 = torch.constant.int 3
314+
%int0 = torch.constant.int 0
315+
%int0_1 = torch.constant.int 0
316+
%int3_s = torch.constant.int 3
317+
%int3_s2 = torch.constant.int 3
318+
%int2_d = torch.constant.int 2
319+
%int2_d2 = torch.constant.int 2
320+
// stride list = [stride_h, stride_w, dilation_h, dilation_w]
321+
%0 = torch.prim.ListConstruct %int3, %int3_0 : (!torch.int, !torch.int) -> !torch.list<int>
322+
%1 = torch.prim.ListConstruct %int0, %int0_1 : (!torch.int, !torch.int) -> !torch.list<int>
323+
%2 = torch.prim.ListConstruct %int3_s, %int3_s2, %int2_d, %int2_d2 : (!torch.int, !torch.int, !torch.int, !torch.int) -> !torch.list<int>
324+
%true = torch.constant.bool true
325+
%false = torch.constant.bool false
326+
%none = torch.constant.none
327+
%3 = torch.aten.avg_pool2d %arg0, %0, %2, %1, %true, %false, %none : !torch.vtensor<[1,1,7,7],f32>, !torch.list<int>, !torch.list<int>, !torch.list<int>, !torch.bool, !torch.bool, !torch.none -> !torch.vtensor<[1,1,2,2],f32>
328+
return %3 : !torch.vtensor<[1,1,2,2],f32>
329+
}
330+
331+
// -----
332+
333+
// CHECK: #[[$MAP:.*]] = affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)>
334+
// CHECK-LABEL: func @forward_avgpool_2d_count_include_pad_dilated
335+
func.func @forward_avgpool_2d_count_include_pad_dilated(%arg0: !torch.vtensor<[1,1,7,7],f32>) -> !torch.vtensor<[1,1,3,3],f32> {
336+
// CHECK: linalg.pooling_nchw_sum {dilations = dense<2> : vector<2xi64>, strides = dense<2> : vector<2xi64>}
337+
338+
// count_include_pad=true with dilation: ValidTaps uses ceildivsi; FullTaps uses divsi.
339+
// CHECK: linalg.generic {indexing_maps = [#[[$MAP]], #[[$MAP]]], iterator_types = ["parallel", "parallel", "parallel", "parallel"]}
340+
// CHECK: %[[C5:.*]] = arith.constant 5 : i64
341+
// CHECK: arith.addi %{{.*}}, %[[C5]] : i64
342+
// CHECK: arith.ceildivsi {{.*}} : i64
343+
// CHECK: arith.divf
344+
// CHECK: linalg.yield
345+
346+
%int3 = torch.constant.int 3
347+
%int3_0 = torch.constant.int 3
348+
%int1 = torch.constant.int 1
349+
%int1_1 = torch.constant.int 1
350+
%int2_s = torch.constant.int 2
351+
%int2_s2 = torch.constant.int 2
352+
%int2_d = torch.constant.int 2
353+
%int2_d2 = torch.constant.int 2
354+
// stride list = [stride_h, stride_w, dilation_h, dilation_w]
355+
%0 = torch.prim.ListConstruct %int3, %int3_0 : (!torch.int, !torch.int) -> !torch.list<int>
356+
%1 = torch.prim.ListConstruct %int1, %int1_1 : (!torch.int, !torch.int) -> !torch.list<int>
357+
%2 = torch.prim.ListConstruct %int2_s, %int2_s2, %int2_d, %int2_d2 : (!torch.int, !torch.int, !torch.int, !torch.int) -> !torch.list<int>
358+
%false = torch.constant.bool false
359+
%true = torch.constant.bool true
360+
%none = torch.constant.none
361+
%3 = torch.aten.avg_pool2d %arg0, %0, %2, %1, %false, %true, %none : !torch.vtensor<[1,1,7,7],f32>, !torch.list<int>, !torch.list<int>, !torch.list<int>, !torch.bool, !torch.bool, !torch.none -> !torch.vtensor<[1,1,3,3],f32>
362+
return %3 : !torch.vtensor<[1,1,3,3],f32>
363+
}
364+
365+
// -----
366+
367+
// CHECK-LABEL: func @forward_avgpool_2d_exclude_pad_dilated_edge
368+
func.func @forward_avgpool_2d_exclude_pad_dilated_edge(%arg0: !torch.vtensor<[1,1,11,11],f32>) -> !torch.vtensor<[1,1,1,1],f32> {
369+
// kernel=4, dilation=4, padding=1, stride=1, count_include_pad=false.
370+
// effective_kernel = (4-1)*4+1 = 13; output = (11+2-13)/1+1 = 1.
371+
// At output position 0: start_idx = 0*1 - 1 = -1. Taps land at -1, 3, 7, 11.
372+
// Valid taps (in [0,11)): 3 and 7 -> ValidTaps = 2.
373+
// Correct divisor is 2.
374+
// CHECK: linalg.pooling_nchw_sum {dilations = dense<4> : vector<2xi64>, strides = dense<1> : vector<2xi64>}
375+
// CHECK: linalg.generic
376+
// CHECK: arith.ceildivsi
377+
// CHECK: arith.divf
378+
// CHECK: linalg.yield
379+
%int4 = torch.constant.int 4
380+
%int4_0 = torch.constant.int 4
381+
%int1 = torch.constant.int 1
382+
%int1_1 = torch.constant.int 1
383+
%int1_s = torch.constant.int 1
384+
%int1_s2 = torch.constant.int 1
385+
%int4_d = torch.constant.int 4
386+
%int4_d2 = torch.constant.int 4
387+
%0 = torch.prim.ListConstruct %int4, %int4_0 : (!torch.int, !torch.int) -> !torch.list<int>
388+
%1 = torch.prim.ListConstruct %int1, %int1_1 : (!torch.int, !torch.int) -> !torch.list<int>
389+
%2 = torch.prim.ListConstruct %int1_s, %int1_s2, %int4_d, %int4_d2 : (!torch.int, !torch.int, !torch.int, !torch.int) -> !torch.list<int>
390+
%false = torch.constant.bool false
391+
%false_1 = torch.constant.bool false
392+
%none = torch.constant.none
393+
%3 = torch.aten.avg_pool2d %arg0, %0, %2, %1, %false, %false_1, %none : !torch.vtensor<[1,1,11,11],f32>, !torch.list<int>, !torch.list<int>, !torch.list<int>, !torch.bool, !torch.bool, !torch.none -> !torch.vtensor<[1,1,1,1],f32>
394+
return %3 : !torch.vtensor<[1,1,1,1],f32>
395+
}

0 commit comments

Comments
 (0)