Skip to content

[Bug][Relax][ONNX] ConvTranspose ignores output_shape #20505

Description

@Yuhx141

Version

  • Executed revision: e269315c90e3a061c9e1c77b370ce883b1b223f4
  • Current main source rechecked: 0587e59a696cb435ad4d328726a07daa8d966f3c; ConvTranspose still does not read output_shape
  • TVM: 0.26.dev0
  • Host: Ubuntu 24.04 x86-64, LLVM CPU

Reproducer

Run the complete script below
from a TVM source environment. It creates a valid opset-11 model whose transposed
convolution requests spatial output_shape=[4,4], then compares ONNX Runtime with the
compiled Relax function.

Observed output:

output_shape [4, 4] ORT (1, 1, 4, 4) TVM (1, 1, 5, 5)
output_shape None ORT (1, 1, 5, 5) TVM (1, 1, 5, 5)

The first line is the failing case. The second line removes only output_shape and is
the passing control.

Expected behavior

ONNX defines output_shape as the requested spatial output shape and derives the needed
padding from the input, stride, dilation, kernel, and output padding. The imported
function should therefore return shape (1,1,4,4).

Actual behavior and likely cause

The converter reads strides, dilations, output_padding, kernel_shape, auto_pad,
pads, and group, but never reads output_shape. It lowers this model with zero
padding, producing the default (1,1,5,5) output instead. Current main retains the
same omission.

Duplicate check

GitHub searches for TVM Relax, ONNX ConvTranspose, and output_shape found no report of
the current Relax converter ignoring this attribute as of 2026-09-30. TVM PR #19450
fixed auto_pad, a separate attribute. ONNX issues #4159 and #4527 discuss ONNX shape
inference/documentation and pad distribution; this reproducer is accepted by ONNX
checker and ONNX Runtime and isolates the missing Relax frontend handling.

Complete reproducer

import numpy as np
import onnx
import onnxruntime as ort
import tvm
from onnx import TensorProto, helper
from tvm import relax
from tvm.relax.frontend.onnx import from_onnx


def run(output_shape):
    inputs = [
        helper.make_tensor_value_info("x", TensorProto.FLOAT, [1, 1, 2, 2]),
        helper.make_tensor_value_info("w", TensorProto.FLOAT, [1, 1, 3, 3]),
    ]
    expected_side = 4 if output_shape else 5
    output = helper.make_tensor_value_info(
        "y", TensorProto.FLOAT, [1, 1, expected_side, expected_side]
    )
    attrs = {"strides": [2, 2]}
    if output_shape:
        attrs["output_shape"] = output_shape
    node = helper.make_node("ConvTranspose", ["x", "w"], ["y"], **attrs)
    model = helper.make_model(
        helper.make_graph([node], "convtranspose_output_shape", inputs, [output]),
        opset_imports=[helper.make_opsetid("", 11)],
    )
    onnx.checker.check_model(model)
    feeds = {
        "x": np.arange(1, 5, dtype="float32").reshape(1, 1, 2, 2),
        "w": np.ones((1, 1, 3, 3), dtype="float32"),
    }
    expected = ort.InferenceSession(
        model.SerializeToString(), providers=["CPUExecutionProvider"]
    ).run(None, feeds)[0]

    mod = from_onnx(model, keep_params_in_input=True)
    executable = relax.build(mod, target="llvm", relax_pipeline="default", exec_mode="bytecode")
    vm = relax.VirtualMachine(executable, tvm.cpu())
    actual = vm["main"](*(tvm.runtime.tensor(feeds[x.name]) for x in inputs)).numpy()
    print("output_shape", output_shape, "ORT", expected.shape, "TVM", actual.shape)
    return actual, expected


risk_actual, risk_expected = run([4, 4])
control_actual, control_expected = run(None)
assert risk_actual.shape != risk_expected.shape
np.testing.assert_allclose(control_actual, control_expected)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions