Expected behavior
ONNX NonMaxSuppression declares max_output_boxes_per_class, iou_threshold, and score_threshold as scalar tensors. By long-standing convention (and ORT's behavior), both 0-D scalars (shape []) and 1-D single-element tensors (shape [1]) are accepted. Many ONNX exporters emit shape [1].
Actual behavior
The TVM frontend crashes during conversion:
TypeError: only 0-dimensional arrays can be converted to Python scalars
This is a NumPy 2.x stricter cast — calling int(np.array([3])) now raises TypeError, whereas NumPy 1.x silently accepted it.
Reproduction
import numpy as np
import onnx
from onnx import helper, TensorProto, numpy_helper
import onnxruntime as ort
from tvm.relax.frontend.onnx import from_onnx
boxes = helper.make_tensor_value_info("boxes", TensorProto.FLOAT, [1, 5, 4])
scores = helper.make_tensor_value_info("scores", TensorProto.FLOAT, [1, 1, 5])
Y = onnx.ValueInfoProto(); Y.name = "selected"
inits = [
numpy_helper.from_array(np.array([3], dtype=np.int64), "max_output"), # shape [1]
numpy_helper.from_array(np.array([0.5], dtype=np.float32), "iou"), # shape [1]
numpy_helper.from_array(np.array([0.0], dtype=np.float32), "score_thr"), # shape [1]
]
node = helper.make_node("NonMaxSuppression",
["boxes", "scores", "max_output", "iou", "score_thr"],
["selected"])
g = helper.make_graph([node], "g", [boxes, scores], [Y], initializer=inits)
m = helper.make_model(g, opset_imports=[helper.make_opsetid("", 18)])
boxes_v = np.array([[[0., 0., 1., 1.],
[0., 0.1, 1., 1.1],
[0., -0.1, 1., 0.9],
[0., 10., 1., 11.],
[0., 10.1, 1., 11.1]]], dtype=np.float32)
scores_v = np.array([[[0.9, 0.75, 0.6, 0.95, 0.5]]], dtype=np.float32)
print("ORT:", ort.InferenceSession(m.SerializeToString()).run(None, {"boxes": boxes_v, "scores": scores_v})[0])
# ORT: [[0 0 3] [0 0 0]]
inf = onnx.shape_inference.infer_shapes(m)
mod = from_onnx(inf) # TypeError: only 0-dimensional arrays can be converted to Python scalars
Root cause
python/tvm/relax/frontend/onnx/onnx_frontend.py, NonMaxSuppression._impl_v10:
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()) # NumPy 2.x: TypeError on shape [1]
...
if iou_threshold is not None and isinstance(iou_threshold, relax.Constant):
iou_threshold = float(iou_threshold.data.numpy()) # same
...
if score_threshold is not None and isinstance(score_threshold, relax.Constant):
score_threshold = float(score_threshold.data.numpy()) # same
int() / float() on a NumPy ndarray no longer auto-flattens; this raises TypeError on any non-0-D tensor.
Suggested fix
Use .item() (which accepts both 0-D and 1-element tensors of any rank):
max_output_boxes_per_class = int(max_output_boxes_per_class.data.numpy().item())
iou_threshold = float(iou_threshold.data.numpy().item())
score_threshold = float(score_threshold.data.numpy().item())
The same pattern (int(constant.data.numpy()) / float(...)) appears in several other converters and should be audited; e.g., TopK had a similar problem fixed by #19573.
Impact
Any ONNX model that emits NMS thresholds as shape-[1] tensors fails to import. ONNX exporters from torchvision, MMDetection, YOLO-family object-detection pipelines commonly produce shape-[1] here.
Unrelated to (but distinct from) #19544 which addresses the max_output_boxes_per_class = 0 semantics.
Environment
- TVM: latest
main (commit b172d5e)
- Python: 3.11
- NumPy: 2.4.4
- ONNX Runtime: 1.24.4
cc @KJlaccHoeUM9l @junrushao
Expected behavior
ONNX
NonMaxSuppressiondeclaresmax_output_boxes_per_class,iou_threshold, andscore_thresholdas scalar tensors. By long-standing convention (and ORT's behavior), both 0-D scalars (shape[]) and 1-D single-element tensors (shape[1]) are accepted. Many ONNX exporters emit shape[1].Actual behavior
The TVM frontend crashes during conversion:
This is a NumPy 2.x stricter cast — calling
int(np.array([3]))now raisesTypeError, whereas NumPy 1.x silently accepted it.Reproduction
Root cause
python/tvm/relax/frontend/onnx/onnx_frontend.py,NonMaxSuppression._impl_v10:int()/float()on a NumPy ndarray no longer auto-flattens; this raisesTypeErroron any non-0-D tensor.Suggested fix
Use
.item()(which accepts both 0-D and 1-element tensors of any rank):The same pattern (
int(constant.data.numpy())/float(...)) appears in several other converters and should be audited; e.g.,TopKhad a similar problem fixed by #19573.Impact
Any ONNX model that emits NMS thresholds as shape-
[1]tensors fails to import. ONNX exporters from torchvision, MMDetection, YOLO-family object-detection pipelines commonly produce shape-[1]here.Unrelated to (but distinct from) #19544 which addresses the
max_output_boxes_per_class = 0semantics.Environment
main(commit b172d5e)cc @KJlaccHoeUM9l @junrushao