This is an automated email from the ASF dual-hosted git repository.
tlopex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push:
new e55e3c3a5d [BugFix][Relax][ONNX] Honor auto_pad in ConvTranspose
converter (#19450)
e55e3c3a5d is described below
commit e55e3c3a5d12435ac18c15a4aa2a2298dd2fc9d3
Author: Soowon Jeong <[email protected]>
AuthorDate: Tue Apr 28 05:37:09 2026 +0900
[BugFix][Relax][ONNX] Honor auto_pad in ConvTranspose converter (#19450)
## Motivation
The `ConvTranspose` ONNX → Relax converter silently drops the `auto_pad`
attribute. `Conv` has dedicated handling (`onnx_frontend.py` lines
1383-1404), but `ConvTranspose` passes `pads` straight through,
defaulting to 0 when the attribute is absent. Models that rely on
`auto_pad=SAME_UPPER`/`SAME_LOWER`/`VALID` therefore produce the wrong
output shape after import.
Minimal repro (against onnxruntime):
```python
node = helper.make_node(
"ConvTranspose", ["X", "W"], ["Y"],
strides=[2, 2], auto_pad="SAME_UPPER",
)
# input X: [1, 1, 4, 4], W: [1, 1, 3, 3]
# ORT -> Y.shape = (1, 1, 8, 8) # input * stride
# TVM -> Y.shape = (1, 1, 9, 9) # auto_pad ignored, pads=0
```
## Fix
Compute `pads` from the ONNX spec equation before delegating to the
Relax conv-transpose op:
```
total_pad[i] = stride[i] * (in[i] - 1)
+ output_padding[i]
+ (kernel[i] - 1) * dilation[i] + 1
- in[i] * stride[i]
```
then split begin/end by `SAME_UPPER` vs `SAME_LOWER`. `VALID` becomes
`pads=0`; `NOTSET` keeps the user-supplied `pads`. We deliberately do
not reuse Conv's `autopad()` helper because it pads the input data,
whereas ConvTranspose subtracts pads from the output.
The `output_shape` attribute (which, when set, also overrides `pads` per
spec) remains unsupported; leaving that as a follow-up.
## Test plan
- [x] \`pytest
tests/python/relax/test_frontend_onnx.py::test_conv_transpose_auto_pad\`
— 6 new cases (3 modes × 2 strides) for 1D/2D/3D pass.
- [x] \`pytest
tests/python/relax/test_frontend_onnx.py::test_conv_transpose\` —
existing 8 parameterizations still pass.
---
python/tvm/relax/frontend/onnx/onnx_frontend.py | 58 +++++++++++++++++++++++--
tests/python/relax/test_frontend_onnx.py | 31 +++++++++++++
2 files changed, 86 insertions(+), 3 deletions(-)
diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index bf65434db0..98571634a7 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -1663,13 +1663,65 @@ class ConvTranspose(OnnxOpConverter):
else:
raise NotImplementedError("Ndim > 5 not supported for
convolution.")
+ spatial_dims = ndim - 2
+ strides = attr.get("strides", [1] * spatial_dims)
+ dilations = attr.get("dilations", [1] * spatial_dims)
+ output_padding = attr.get("output_padding", [0] * spatial_dims)
+ if "kernel_shape" in attr:
+ kernel_shape = list(attr["kernel_shape"])
+ else:
+ kernel_shape = [int(s) for s in
inputs[1].struct_info.shape.values[2:]]
+
+ # Resolve `auto_pad` per ONNX ConvTranspose spec. Unlike Conv, the spec
+ # derives `pads` from `output_shape`/`strides` when auto_pad is SAME_*,
+ # so we cannot reuse `autopad()` (which pads the input data instead).
+ if "auto_pad" in attr:
+ auto_pad = attr["auto_pad"]
+ if isinstance(auto_pad, bytes):
+ auto_pad = auto_pad.decode("utf-8")
+ if auto_pad in ("SAME_UPPER", "SAME_LOWER"):
+ # Per ONNX ConvTranspose spec, when output_shape is unspecified
+ # the target output size is `input_size * stride`. Substituting
+ # this into the spec's total_padding formula cancels the
+ # input-size term, leaving a value that depends only on the
+ # kernel/dilation/stride/output_padding attributes. Avoiding
the
+ # input shape keeps the converter usable when spatial dims are
+ # symbolic (`tir.Var`).
+ pads_begin: list[int] = []
+ pads_end: list[int] = []
+ for i in range(spatial_dims):
+ total_pad = (
+ (kernel_shape[i] - 1) * dilations[i]
+ + 1
+ + output_padding[i]
+ - strides[i]
+ )
+ total_pad = max(total_pad, 0)
+ if auto_pad == "SAME_UPPER":
+ pad_begin = total_pad // 2
+ else:
+ pad_begin = total_pad - total_pad // 2
+ pads_begin.append(pad_begin)
+ pads_end.append(total_pad - pad_begin)
+ attr["pads"] = pads_begin + pads_end
+ elif auto_pad == "VALID":
+ attr["pads"] = [0] * (2 * spatial_dims)
+ elif auto_pad == "NOTSET":
+ pass
+ else:
+ raise tvm.error.OpAttributeInvalid(
+ f'Value {auto_pad} in attribute "auto_pad" of operator '
+ "ConvTranspose is invalid."
+ )
+ attr.pop("auto_pad")
+
conv_out = op(
data=inputs[0],
weight=inputs[1],
- strides=attr.get("strides", 1),
+ strides=strides,
padding=attr.get("pads", 0),
- output_padding=attr.get("output_padding", 0),
- dilation=attr.get("dilations", 1),
+ output_padding=output_padding,
+ dilation=dilations,
groups=attr.get("group", 1),
data_layout=data_layout,
kernel_layout=kernel_layout,
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index 4e13e906d8..d67309d223 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -1619,6 +1619,37 @@ def test_conv_transpose(stride: int, dilation: int, pad:
int, bias: bool, output
_verify_conv_transpose([3, 4, 12, 12, 12], [4, 2, 3, 3, 3]) # group=2
[email protected]("auto_pad", ["SAME_UPPER", "SAME_LOWER", "VALID"])
[email protected]("stride", [1, 2])
+def test_conv_transpose_auto_pad(auto_pad: str, stride: int):
+ def _verify(input_shape, weight_shape):
+ nd = len(weight_shape) - 2
+ conv_node = helper.make_node(
+ "ConvTranspose",
+ inputs=["x", "w"],
+ outputs=["y"],
+ kernel_shape=weight_shape[2:],
+ strides=[stride] * nd,
+ auto_pad=auto_pad,
+ )
+ graph = helper.make_graph(
+ [conv_node],
+ "conv_transpose_auto_pad_test",
+ inputs=[
+ helper.make_tensor_value_info("x", TensorProto.FLOAT,
input_shape),
+ helper.make_tensor_value_info("w", TensorProto.FLOAT,
weight_shape),
+ ],
+ outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT,
None)],
+ )
+ model = helper.make_model(graph,
producer_name="conv_transpose_auto_pad_test")
+ check_correctness(model, atol=1e-4)
+
+ # ConvTranspose1D / 2D / 3D
+ _verify([1, 1, 8], [1, 1, 3])
+ _verify([1, 1, 8, 8], [1, 1, 3, 3])
+ _verify([1, 1, 4, 4, 4], [1, 1, 3, 3, 3])
+
+
def test_pow():
verify_binary("Pow", [32, 32], [32, 32], [32, 32])