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])
 

Reply via email to