This is an automated email from the ASF dual-hosted git repository.
cbalint13 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 626a3537e6 [Relax][ONNX] Accept 1-D scalar inputs in NonMaxSuppression
(#19843)
626a3537e6 is described below
commit 626a3537e64b3b00ec403ca3ea4c11e93ce45442
Author: Guan-Ming (Wesley) Chiu <[email protected]>
AuthorDate: Sun Jun 21 00:21:39 2026 +0800
[Relax][ONNX] Accept 1-D scalar inputs in NonMaxSuppression (#19843)
## Related Issue
closes #19693
## Why
NumPy 2.x raises TypeError on int(np.array([3])), so importing an ONNX
NonMaxSuppression whose scalar params are 1-D single-element tensors
(shape[1], common from exporters) crashed.
## How
- Cast the constant max_output_boxes_per_class / iou_threshold /
score_threshold via .numpy().item(), which accepts both 0-D and
1-element tensors (matching the existing param-Var path).
- Add an import test feeding 1-D scalar constants (the default folding
path that the existing test_nms misses).
---
python/tvm/relax/frontend/onnx/onnx_frontend.py | 26 +++++++++++++++++++------
tests/python/relax/test_frontend_onnx.py | 26 +++++++++++++++++++++++++
2 files changed, 46 insertions(+), 6 deletions(-)
diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index cdb213f10d..430e7cef7a 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -4884,7 +4884,7 @@ class NonMaxSuppression(OnnxOpConverter):
if max_output_boxes_per_class is not None and isinstance(
max_output_boxes_per_class, relax.Constant
):
- max_output_boxes_per_class =
int(max_output_boxes_per_class.data.numpy())
+ max_output_boxes_per_class =
int(max_output_boxes_per_class.data.numpy().item())
elif max_output_boxes_per_class is not None and isinstance(
max_output_boxes_per_class, relax.Var
):
@@ -4898,12 +4898,19 @@ class NonMaxSuppression(OnnxOpConverter):
max_output_boxes_per_class = 0 # Default value
if iou_threshold is not None and isinstance(iou_threshold,
relax.Constant):
- iou_threshold = float(iou_threshold.data.numpy())
+ iou_threshold = float(iou_threshold.data.numpy().item())
+ elif iou_threshold is not None and isinstance(iou_threshold,
relax.Var):
+ var_name = iou_threshold.name_hint
+ if var_name in params[1]:
+ _, param_value = params[1][var_name]
+ iou_threshold = float(param_value.numpy().item())
+ else:
+ iou_threshold = 0.5 # Default value
else:
iou_threshold = 0.5 # Default value
if score_threshold is not None and isinstance(score_threshold,
relax.Constant):
- score_threshold = float(score_threshold.data.numpy())
+ score_threshold = float(score_threshold.data.numpy().item())
elif score_threshold is not None and isinstance(score_threshold,
relax.Var):
var_name = score_threshold.name_hint
if var_name in params[1]:
@@ -4973,7 +4980,7 @@ class AllClassNMS(OnnxOpConverter):
if max_output_boxes_per_class is not None and isinstance(
max_output_boxes_per_class, relax.Constant
):
- max_output_boxes_per_class =
int(max_output_boxes_per_class.data.numpy())
+ max_output_boxes_per_class =
int(max_output_boxes_per_class.data.numpy().item())
elif max_output_boxes_per_class is not None and isinstance(
max_output_boxes_per_class, relax.Var
):
@@ -4987,12 +4994,19 @@ class AllClassNMS(OnnxOpConverter):
max_output_boxes_per_class = 0 # Default value
if iou_threshold is not None and isinstance(iou_threshold,
relax.Constant):
- iou_threshold = float(iou_threshold.data.numpy())
+ iou_threshold = float(iou_threshold.data.numpy().item())
+ elif iou_threshold is not None and isinstance(iou_threshold,
relax.Var):
+ var_name = iou_threshold.name_hint
+ if var_name in params[1]:
+ _, param_value = params[1][var_name]
+ iou_threshold = float(param_value.numpy().item())
+ else:
+ iou_threshold = 0.5 # Default value
else:
iou_threshold = 0.5 # Default value
if score_threshold is not None and isinstance(score_threshold,
relax.Constant):
- score_threshold = float(score_threshold.data.numpy())
+ score_threshold = float(score_threshold.data.numpy().item())
elif score_threshold is not None and isinstance(score_threshold,
relax.Var):
var_name = score_threshold.name_hint
if var_name in params[1]:
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index 6e9d4c9d95..58004b80ad 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -5120,6 +5120,32 @@ def test_nms():
)
+def test_nms_scalar_shape1_constants():
+ """Scalar params given as 1-D single-element constants must import (NumPy
2.x cast)."""
+ nms_node = helper.make_node(
+ "NonMaxSuppression",
+ ["boxes", "scores", "max_output_boxes_per_class", "iou_threshold",
"score_threshold"],
+ ["selected_indices"],
+ )
+ graph = helper.make_graph(
+ [nms_node],
+ "nms_scalar_shape1",
+ inputs=[
+ helper.make_tensor_value_info("boxes", TensorProto.FLOAT, [1, 5,
4]),
+ helper.make_tensor_value_info("scores", TensorProto.FLOAT, [1, 1,
5]),
+ ],
+ initializer=[
+ helper.make_tensor("max_output_boxes_per_class",
TensorProto.INT64, [1], [3]),
+ helper.make_tensor("iou_threshold", TensorProto.FLOAT, [1], [0.5]),
+ helper.make_tensor("score_threshold", TensorProto.FLOAT, [1],
[0.0]),
+ ],
+ outputs=[helper.make_tensor_value_info("selected_indices",
TensorProto.INT64, [0, 3])],
+ )
+ model = helper.make_model(graph, opset_imports=[helper.make_opsetid("",
18)])
+ # Default import folds initializers to relax.Constant, exercising the
scalar-cast path.
+ from_onnx(model)
+
+
@pytest.mark.parametrize("with_explicit_max", [False, True])
def test_nms_max_output_boxes_per_class_zero(with_explicit_max: bool):
"""ONNX default for max_output_boxes_per_class is 0, yielding empty
output."""