Expected behavior
A valid ONNX Reshape node should follow the ONNX spec's 0-dimension semantics:
- With default
allowzero=0 (and opset < 14), a 0 in shape means "copy the corresponding dimension from the input". For constant data with shape (2, 3) and shape = [0, 3], the output must be (2, 3), unchanged.
- With
allowzero=1 (opset ≥ 14), a 0 in shape is a literal zero dimension. For input (2, 0) and shape = [0, 2], the output must be (0, 2).
Both models pass onnx.checker and run correctly in onnxruntime and onnx.reference.
Actual behavior
tvm.relax.frontend.onnx.from_onnx mishandles both cases:
-
Constant-fold path (Reshape._impl_v13, python/tvm/relax/frontend/onnx/onnx_frontend.py:979-981): when both data and shape are constants, the frontend calls np.reshape(data, shape), which treats 0 as a literal zero element (numpy semantics) instead of "copy dim from input" (ONNX default). A fully-constant model data=(2,3), shape=[0,3] (default allowzero=0) — which onnxruntime and onnx.reference both accept and return (2,3) — raises:
ValueError: cannot reshape array of size 6 into shape (0,3)
-
allowzero attribute ignored (Reshape._impl_v13, python/tvm/relax/frontend/onnx/onnx_frontend.py:968-985): the converter never reads attr["allowzero"]. It always passes 0-copy semantics to relax.op.reshape. A valid empty-tensor model data=(2,0), shape=[0,2], allowzero=1 (opset 14) — which onnxruntime and onnx.reference return (0,2) — is rejected:
InternalError: Reshape expects the new shape to be convertible from the old shape. However, the old shape ...
Both are valid, runnable ONNX models.
Environment
- OS: Linux
- TVM: v0.24.dev0 (main branch, commit
262c6d2e0, built 2026-02-11)
- Python: 3.11
- onnx: 1.20.1
- onnxruntime: 1.24.1
Steps to reproduce
"""Repro: ONNX Reshape 0-dim semantics mishandled by TVM relax frontend."""
import numpy as np
import onnx, onnxruntime
from onnx import helper, TensorProto
from onnx.reference import ReferenceEvaluator
from tvm.relax.frontend.onnx import from_onnx
def build(data_shape, shape_vals, data_const, out_shape, allowzero=None, opset=13):
inits, inputs = [], []
if data_const:
d = (np.arange(int(np.prod(data_shape))).reshape(data_shape) + 1).astype("float32")
inits.append(helper.make_tensor("data", TensorProto.FLOAT, list(data_shape), d.flatten().tolist()))
else:
inputs.append(helper.make_tensor_value_info("data", TensorProto.FLOAT, list(data_shape)))
inits.append(helper.make_tensor("shape", TensorProto.INT64, [len(shape_vals)], list(shape_vals)))
node = helper.make_node("Reshape", ["data", "shape"], ["Y"])
if allowzero is not None:
node.attribute.append(helper.make_attribute("allowzero", allowzero))
graph = helper.make_graph(
[node], "g", inputs,
[helper.make_tensor_value_info("Y", TensorProto.FLOAT, list(out_shape))], inits)
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", opset)])
model.ir_version = 8
return model
def try_tvm(m, shape_dict, label):
try:
from_onnx(m, shape_dict=shape_dict)
print(f"{label} TVM: OK")
except Exception as e:
print(f"{label} TVM: {type(e).__name__}: {e}")
# (1) Constant path, default allowzero=0: 0 must copy the input dim
m1 = build((2, 3), [0, 3], data_const=True, out_shape=(2, 3))
onnx.checker.check_model(m1) # valid ONNX model
print("m1 onnxruntime:", onnxruntime.InferenceSession(m1.SerializeToString()).run(None, {})[0].shape)
print("m1 onnx.reference:", ReferenceEvaluator(m1).run(None, {})[0].shape)
try_tvm(m1, {}, "m1") # TVM rejects
# (2) allowzero=1 (opset 14): 0 is a literal zero dim
m2 = build((2, 0), [0, 2], data_const=False, out_shape=(0, 2), allowzero=1, opset=14)
onnx.checker.check_model(m2) # valid ONNX model
x = np.zeros((2, 0), dtype="float32")
print("m2 onnxruntime:", onnxruntime.InferenceSession(m2.SerializeToString()).run(None, {"data": x})[0].shape)
print("m2 onnx.reference:", ReferenceEvaluator(m2).run(None, {"data": x})[0].shape)
try_tvm(m2, {"data": [2, 0]}, "m2") # TVM rejects
Actual output:
m1 onnxruntime: (2, 3)
m1 onnx.reference: (2, 3)
Error converting operator Reshape, with inputs: [metadata["relax.expr.Constant"][0], metadata["relax.expr.Constant"][0]]
m1 TVM: ValueError: cannot reshape array of size 6 into shape (0,3)
File "tvm/relax/frontend/onnx/onnx_frontend.py", line 980, in _impl_v13
out = _np.reshape(data.data.numpy(), new_shape.data.numpy().tolist())
m2 onnxruntime: (0, 2)
m2 onnx.reference: (0, 2)
Error converting operator Reshape, with inputs: [data, metadata["relax.expr.Constant"][0]]
m2 TVM: InternalError: Reshape expects the new shape to be convertible from the old shape. However, the old shape is R.shape([2, 0]), with product T.int64(0), while the new shape is R.shape([2, 2]), with product T.int64(4)
Triage
- needs-triage
- bug
- relax
- frontend/onnx
Expected behavior
A valid ONNX
Reshapenode should follow the ONNX spec's 0-dimension semantics:allowzero=0(and opset < 14), a0inshapemeans "copy the corresponding dimension from the input". For constantdatawith shape(2, 3)andshape = [0, 3], the output must be(2, 3), unchanged.allowzero=1(opset ≥ 14), a0inshapeis a literal zero dimension. For input(2, 0)andshape = [0, 2], the output must be(0, 2).Both models pass
onnx.checkerand run correctly in onnxruntime andonnx.reference.Actual behavior
tvm.relax.frontend.onnx.from_onnxmishandles both cases:Constant-fold path (
Reshape._impl_v13,python/tvm/relax/frontend/onnx/onnx_frontend.py:979-981): when bothdataandshapeare constants, the frontend callsnp.reshape(data, shape), which treats0as a literal zero element (numpy semantics) instead of "copy dim from input" (ONNX default). A fully-constant modeldata=(2,3),shape=[0,3](defaultallowzero=0) — which onnxruntime andonnx.referenceboth accept and return(2,3)— raises:allowzeroattribute ignored (Reshape._impl_v13,python/tvm/relax/frontend/onnx/onnx_frontend.py:968-985): the converter never readsattr["allowzero"]. It always passes0-copy semantics torelax.op.reshape. A valid empty-tensor modeldata=(2,0),shape=[0,2],allowzero=1(opset 14) — which onnxruntime andonnx.referencereturn(0,2)— is rejected:Both are valid, runnable ONNX models.
Environment
262c6d2e0, built 2026-02-11)Steps to reproduce
Actual output:
Triage