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 ba24a42400 [Relax][Frontend][ONNX] Support Shape start and end
attributes (#20050)
ba24a42400 is described below
commit ba24a424009b841513c7a225a775c2bc3815eccc
Author: Ronald Nap <[email protected]>
AuthorDate: Sun Jul 26 09:47:26 2026 -0700
[Relax][Frontend][ONNX] Support Shape start and end attributes (#20050)
## Summary
This adds support for the `start` and `end` attributes introduced for
ONNX `Shape` in opset 15.
The Relax ONNX frontend previously reused the opset 13 implementation,
which always returned the full input shape. As a result, models using
sliced shape values could construct an incorrect target shape and fail
in downstream operators such as `Reshape`:
```text
ValueError: Reshape expects the new shape to be convertible from the old
shape. However, the old shape is R.shape([12]), with product T.int64(12), while
the new shape is R.shape([2, 3, 4]), with product T.int64(24)
```
### Minimal reproduce
```python
import onnx
from tvm.relax.frontend.onnx import from_onnx
input_shape = [2, 3, 4]
data_shape = [12]
expected_shape = [3, 4]
start = 1
end = None
opset = 15
shape_attrs = {"start": start}
if end is not None:
shape_attrs["end"] = end
model = onnx.helper.make_model(
onnx.helper.make_graph(
[
onnx.helper.make_node("Shape", ["x"], ["shape"], **shape_attrs),
onnx.helper.make_node("Reshape", ["data", "shape"], ["y"]),
],
"shape_start_end_repro",
[
onnx.helper.make_tensor_value_info(
"x", onnx.TensorProto.FLOAT, input_shape
),
onnx.helper.make_tensor_value_info(
"data", onnx.TensorProto.FLOAT, data_shape
),
],
[
onnx.helper.make_tensor_value_info(
"y", onnx.TensorProto.FLOAT, expected_shape
)
],
),
opset_imports=[onnx.helper.make_opsetid("", opset)],
)
print(f"Shape attributes: start={start}, end={end}")
print(f"Expected Shape output: {input_shape[start:end]}")
print(from_onnx(model, opset=opset).script())
```
The new implementation applies `start` and `end` slicing to static and
symbolic shape expressions. It also handles runtime-produced shape
values by converting them to a tensor, applying `strided_slice`, and
converting the result back to a shape.
---
python/tvm/relax/frontend/onnx/onnx_frontend.py | 27 +++-
tests/python/relax/test_frontend_onnx.py | 170 ++++++++++++++++++++++++
2 files changed, 196 insertions(+), 1 deletion(-)
diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index 881789c7ed..806d16f5a8 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -1629,7 +1629,7 @@ class Clip(OnnxOpConverter):
class Shape(OnnxOpConverter):
- """Converts an onnx Equal node into an equivalent Relax expression."""
+ """Converts an onnx Shape node into an equivalent Relax expression."""
@classmethod
def _impl_v13(cls, bb, inputs, attr, params):
@@ -1647,6 +1647,31 @@ class Shape(OnnxOpConverter):
return data_info.shape
+ @classmethod
+ def _impl_v15(cls, bb, inputs, attr, params):
+ shape = cls._impl_v13(bb, inputs, attr, params)
+ start = attr.get("start", 0)
+ end = attr.get("end")
+
+ if start == 0 and end is None:
+ return shape
+
+ if isinstance(shape, relax.ShapeExpr):
+ return relax.ShapeExpr(list(shape.values)[start:end])
+
+ shape_tensor = bb.normalize(relax.op.shape_to_tensor(shape))
+ sliced_shape = bb.normalize(
+ relax.op.strided_slice(
+ shape_tensor,
+ axes=[0],
+ begin=[start],
+ end=[end if end is not None else 2**63 - 1],
+ strides=[1],
+ assume_inbound=False,
+ )
+ )
+ return bb.normalize(relax.op.tensor_to_shape(sliced_shape))
+
class Trilu(OnnxOpConverter):
"""Given a 2-D matrix or batches of 2-D matrices, returns the upper or
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index 7e3da83df0..3a0a4aa5b9 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -3067,6 +3067,176 @@ def test_shape():
tvm.ir.assert_structural_equal(tvm_model, Expected)
[email protected](
+ "attrs,expected_shape",
+ [
+ ({"start": 1}, (4, 5, 6)),
+ ({"end": -1}, (3, 4, 5)),
+ ({"start": -2}, (5, 6)),
+ ({"start": 1, "end": 3}, (4, 5)),
+ ({"start": -10, "end": 10}, (3, 4, 5, 6)),
+ ({"start": 3, "end": 2}, ()),
+ ],
+)
+def test_shape_start_end(attrs, expected_shape):
+ expected_shape = list(expected_shape)
+ shape_node = helper.make_node("Shape", ["data"], ["output"], **attrs)
+
+ graph = helper.make_graph(
+ [shape_node],
+ "shape_start_end_test",
+ inputs=[
+ helper.make_tensor_value_info(
+ "data",
+ TensorProto.FLOAT,
+ [3, 4, 5, 6],
+ ),
+ ],
+ outputs=[
+ helper.make_tensor_value_info(
+ "output",
+ TensorProto.INT64,
+ [len(expected_shape)],
+ )
+ ],
+ )
+
+ model = helper.make_model(
+ graph,
+ producer_name="shape_start_end_test",
+ opset_imports=[helper.make_opsetid("", 15)],
+ )
+ tvm_model = from_onnx(
+ model,
+ opset=15,
+ keep_params_in_input=True,
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(
+ data: R.Tensor((3, 4, 5, 6), dtype="float32"),
+ ) -> R.Shape(expected_shape):
+ R.func_attr({"num_input": 1})
+ with R.dataflow():
+ gv: R.Shape(expected_shape) = R.shape(expected_shape)
+ R.output(gv)
+ return gv
+
+ tvm.ir.assert_structural_equal(tvm_model, Expected)
+
+
+def test_shape_start_end_symbolic():
+ shape_node = helper.make_node(
+ "Shape",
+ ["data"],
+ ["output"],
+ start=1,
+ end=3,
+ )
+ graph = helper.make_graph(
+ [shape_node],
+ "shape_start_end_symbolic_test",
+ inputs=[
+ helper.make_tensor_value_info(
+ "data",
+ TensorProto.FLOAT,
+ [3, "B", 5, 6],
+ ),
+ ],
+ outputs=[
+ helper.make_tensor_value_info(
+ "output",
+ TensorProto.INT64,
+ [2],
+ )
+ ],
+ )
+
+ model = helper.make_model(
+ graph,
+ producer_name="shape_start_end_symbolic_test",
+ opset_imports=[helper.make_opsetid("", 15)],
+ )
+ tvm_model = from_onnx(
+ model,
+ opset=15,
+ keep_params_in_input=True,
+ )
+
+ @I.ir_module
+ class Expected:
+ @R.function
+ def main(
+ data: R.Tensor((3, "B", 5, 6), dtype="float32"),
+ ) -> R.Shape(ndim=2):
+ B = T.int64()
+ R.func_attr({"num_input": 1})
+ with R.dataflow():
+ gv: R.Shape([B, 5]) = R.shape([B, 5])
+ R.output(gv)
+ return gv
+
+ tvm.ir.assert_structural_equal(tvm_model, Expected)
+
+
+def test_shape_start_end_scalar():
+ shape_node = helper.make_node(
+ "Shape",
+ ["data"],
+ ["output"],
+ start=1,
+ )
+
+ graph = helper.make_graph(
+ [shape_node],
+ "shape_start_end_scalar_test",
+ inputs=[
+ helper.make_tensor_value_info(
+ "data",
+ TensorProto.FLOAT,
+ [],
+ ),
+ ],
+ outputs=[
+ helper.make_tensor_value_info(
+ "output",
+ TensorProto.INT64,
+ [0],
+ )
+ ],
+ )
+
+ model = helper.make_model(
+ graph,
+ producer_name="shape_start_end_scalar_test",
+ opset_imports=[helper.make_opsetid("", 15)],
+ )
+ tvm_model = from_onnx(
+ model,
+ opset=15,
+ keep_params_in_input=True,
+ )
+
+ assert relax.analysis.check_well_formed(tvm_model)
+
+ op_names = []
+
+ def collect_ops(expr):
+ if isinstance(expr, relax.Call) and isinstance(expr.op, tvm.ir.Op):
+ op_names.append(expr.op.name)
+
+ relax.analysis.post_order_visit(tvm_model["main"], collect_ops)
+
+ assert op_names == [
+ "relax.shape_of",
+ "relax.shape_to_tensor",
+ "relax.strided_slice",
+ "relax.tensor_to_shape",
+ ]
+
+
def test_trilu():
def verify_trilu(upper: bool):
node = helper.make_node("Trilu", ["x"], ["y"], upper=upper)