Skip to content

Commit 00e393e

Browse files
committed
pnnx: fix Conv2d padding tuple normalization
Normalize 4-element aten pad values from (left, right, top, bottom) to nn.Conv2d padding order (height, width) when fusing Conv2d modules. Also add regression coverage for asymmetric ZeroPad2d + depthwise Conv2d to keep the output shape and ncnn pad params correct.
1 parent d0d5063 commit 00e393e

4 files changed

Lines changed: 90 additions & 42 deletions

File tree

tools/pnnx/src/pass_level1/nn_Conv2d.cpp

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@
66

77
namespace pnnx {
88

9+
static void normalize_conv2d_padding(Parameter& padding_param)
10+
{
11+
std::vector<int>& padding = padding_param.ai;
12+
if (padding.size() != 4)
13+
return;
14+
15+
// aten::pad uses (left, right, top, bottom), while nn.Conv2d
16+
// stores padding as (top/bottom, left/right).
17+
if (padding[0] == padding[1] && padding[2] == padding[3])
18+
{
19+
padding = {padding[2], padding[0]};
20+
return;
21+
}
22+
23+
if (padding[0] == padding[2] && padding[1] == padding[3] && padding[0] != padding[1])
24+
{
25+
padding.resize(0);
26+
padding_param.s = "same";
27+
}
28+
}
29+
930
class Conv2d : public FuseModulePass
1031
{
1132
public:
@@ -53,58 +74,19 @@ class Conv2d : public FuseModulePass
5374
{
5475
op->params["padding_mode"] = pad->namedInput("mode");
5576
op->params["padding"] = pad->namedInput("pad");
56-
std::vector<int>& padding = op->params["padding"].ai;
57-
if (padding.size() == 4)
58-
{
59-
// Conv2d only accepts tuple of two integers
60-
if (padding[0] == padding[1] && padding[1] == padding[2] && padding[2] == padding[3])
61-
{
62-
padding.resize(2);
63-
}
64-
else if (padding[0] == padding[2] && padding[1] == padding[3] && padding[0] != padding[1])
65-
{
66-
padding.resize(0);
67-
op->params["padding"].s = "same";
68-
}
69-
}
77+
normalize_conv2d_padding(op->params["padding"]);
7078
}
7179
else if (reflection_pad2d)
7280
{
7381
op->params["padding_mode"] = "reflect";
7482
op->params["padding"] = reflection_pad2d->namedInput("padding");
75-
std::vector<int>& padding = op->params["padding"].ai;
76-
if (padding.size() == 4)
77-
{
78-
// Conv2d only accepts tuple of two integers
79-
if (padding[0] == padding[1] && padding[1] == padding[2] && padding[2] == padding[3])
80-
{
81-
padding.resize(2);
82-
}
83-
else if (padding[0] == padding[2] && padding[1] == padding[3] && padding[0] != padding[1])
84-
{
85-
padding.resize(0);
86-
op->params["padding"].s = "same";
87-
}
88-
}
83+
normalize_conv2d_padding(op->params["padding"]);
8984
}
9085
else if (replication_pad2d)
9186
{
9287
op->params["padding_mode"] = "replicate";
9388
op->params["padding"] = replication_pad2d->namedInput("padding");
94-
std::vector<int>& padding = op->params["padding"].ai;
95-
if (padding.size() == 4)
96-
{
97-
// Conv2d only accepts tuple of two integers
98-
if (padding[0] == padding[1] && padding[1] == padding[2] && padding[2] == padding[3])
99-
{
100-
padding.resize(2);
101-
}
102-
else if (padding[0] == padding[2] && padding[1] == padding[3] && padding[0] != padding[1])
103-
{
104-
padding.resize(0);
105-
op->params["padding"].s = "same";
106-
}
107-
}
89+
normalize_conv2d_padding(op->params["padding"]);
10890
}
10991
else
11092
{

tools/pnnx/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ pnnx_add_test(pnnx_fuse_slice_to_tensor_split)
360360
pnnx_add_test(pnnx_fuse_adjacent_permute)
361361
pnnx_add_test(pnnx_fuse_adjacent_reshape)
362362
pnnx_add_test(pnnx_fuse_pad_conv1d)
363+
pnnx_add_test(pnnx_fuse_asymmetric_pad_conv2d)
363364
pnnx_add_test(pnnx_fuse_pad_conv2d)
364365
pnnx_add_test(pnnx_fuse_pixel_shuffle)
365366
pnnx_add_test(pnnx_fuse_pixel_unshuffle)

tools/pnnx/tests/test_nn_Conv2d.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def __init__(self):
2121
self.conv_4 = nn.Conv2d(in_channels=28, out_channels=32, kernel_size=3, stride=1, padding='same', dilation=(1,2), groups=2, bias=False, padding_mode='zeros')
2222
self.conv_5 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=2, stride=2, padding=3, dilation=1, groups=32, bias=True, padding_mode='reflect')
2323
self.conv_6 = nn.Conv2d(in_channels=32, out_channels=28, kernel_size=2, stride=1, padding=2, dilation=1, groups=1, bias=False, padding_mode='replicate')
24+
self.conv_7 = nn.Conv2d(in_channels=28, out_channels=28, kernel_size=3, stride=1, padding=(1,2), dilation=1, groups=1, bias=False, padding_mode='reflect')
2425
#self.conv_7 = nn.Conv2d(in_channels=28, out_channels=24, kernel_size=3, stride=2, padding=(5,6), dilation=2, groups=1, bias=True, padding_mode='circular')
2526

2627
self.conv_8 = nn.Conv2d(in_channels=28, out_channels=24, kernel_size=3, stride=2, padding=(5,6), dilation=2, groups=1, bias=True)
@@ -37,6 +38,7 @@ def forward(self, x):
3738
x = self.conv_4(x)
3839
x = self.conv_5(x)
3940
x = self.conv_6(x)
41+
x = self.conv_7(x)
4042
#x = self.conv_7(x)
4143
x = self.conv_8(x)
4244

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2026 Tencent
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
import os
5+
6+
import torch
7+
import torch.nn as nn
8+
9+
10+
class Model(nn.Module):
11+
def __init__(self):
12+
super(Model, self).__init__()
13+
14+
self.pad = nn.ZeroPad2d((1, 2, 1, 2))
15+
self.conv = nn.Conv2d(4, 4, kernel_size=5, stride=2, groups=4, bias=False)
16+
17+
def forward(self, x):
18+
x = self.pad(x)
19+
x = self.conv(x)
20+
return x
21+
22+
23+
def test():
24+
net = Model()
25+
net.eval()
26+
27+
torch.manual_seed(0)
28+
x = torch.rand(1, 4, 56, 56)
29+
30+
a = net(x)
31+
if a.shape != (1, 4, 28, 28):
32+
return False
33+
34+
mod = torch.jit.trace(net, x)
35+
mod.save("test_pnnx_fuse_asymmetric_pad_conv2d.pt")
36+
37+
os.system("../src/pnnx test_pnnx_fuse_asymmetric_pad_conv2d.pt inputshape=[1,4,56,56]")
38+
39+
import test_pnnx_fuse_asymmetric_pad_conv2d_pnnx
40+
b = test_pnnx_fuse_asymmetric_pad_conv2d_pnnx.test_inference()
41+
42+
if not torch.equal(a, b):
43+
return False
44+
45+
with open("test_pnnx_fuse_asymmetric_pad_conv2d.pnnx.param", "r") as f:
46+
pnnx_param = f.read()
47+
if "#2=(1,4,28,28)f32" not in pnnx_param:
48+
return False
49+
50+
with open("test_pnnx_fuse_asymmetric_pad_conv2d.ncnn.param", "r") as f:
51+
ncnn_param = f.read()
52+
53+
if "ConvolutionDepthWise" in ncnn_param:
54+
return " 4=1 " in ncnn_param and " 14=1 " in ncnn_param and " 15=2 " in ncnn_param and " 16=2 " in ncnn_param
55+
56+
return "Padding" in ncnn_param and " 0=1 " in ncnn_param and " 1=2 " in ncnn_param and " 2=1 " in ncnn_param and " 3=2 " in ncnn_param
57+
58+
59+
if __name__ == "__main__":
60+
if test():
61+
exit(0)
62+
else:
63+
exit(1)

0 commit comments

Comments
 (0)