Version
- Executed revision:
e269315c90e3a061c9e1c77b370ce883b1b223f4
- Current
main source rechecked: 0587e59a696cb435ad4d328726a07daa8d966f3c; the converter still defaults a missing iou_threshold to 0.5
- TVM:
0.26.dev0
- Host: Ubuntu 24.04 x86-64, LLVM CPU
Reproducer
Run the complete script below.
The two boxes have IoU 1/7, so the missing ONNX threshold of 0 suppresses the second box,
while a threshold of 0.5 keeps it.
Observed output:
explicit_iou=False ORT [[0, 0, 0]] TVM [[0, 0, 0], [0, 0, 1]]
explicit_iou=True ORT [[0, 0, 0], [0, 0, 1]] TVM [[0, 0, 0], [0, 0, 1]]
The first line omits the optional input and fails. The second line explicitly supplies 0.5
and is the passing control.
Expected behavior
The ONNX NonMaxSuppression schema says a missing iou_threshold defaults to 0. The imported
function should therefore match ONNX Runtime and suppress the second overlapping box.
Actual behavior and likely cause
The Relax ONNX converter assigns 0.5 whenever iou_threshold is absent, apparently copying
the native TVM NMS default rather than the ONNX contract. Current main retains that branch.
Duplicate check
GitHub issue, pull-request, and repository searches found no report for this default mismatch
as of 2026-09-30. Issue #17767 predates Relax support for NonMaxSuppression. PR #19547 fixed
the missing default for max_output_boxes_per_class, and PR #19843 fixed 1-D scalar inputs;
both retained iou_threshold=0.5 and do not cover this case.
Complete reproducer
import numpy as np
import onnx
import onnxruntime as ort
import tvm
from onnx import TensorProto, helper, numpy_helper
from tvm import relax
from tvm.relax.frontend.onnx import from_onnx
def make_model(explicit_iou):
boxes = helper.make_tensor_value_info("boxes", TensorProto.FLOAT, [1, 2, 4])
scores = helper.make_tensor_value_info("scores", TensorProto.FLOAT, [1, 1, 2])
initializers = [numpy_helper.from_array(np.array([10], dtype="int64"), name="max_out")]
node_inputs = ["boxes", "scores", "max_out"]
if explicit_iou:
initializers.append(
numpy_helper.from_array(np.array([0.5], dtype="float32"), name="iou")
)
node_inputs.append("iou")
output = helper.make_tensor_value_info("selected", TensorProto.INT64, [None, 3])
node = helper.make_node("NonMaxSuppression", node_inputs, ["selected"])
model = helper.make_model(
helper.make_graph([node], "nms_default_iou", [boxes, scores], [output], initializers),
opset_imports=[helper.make_opsetid("", 11)],
)
onnx.checker.check_model(model)
return model
def run(explicit_iou):
model = make_model(explicit_iou)
feeds = {
"boxes": np.array([[[0, 0, 2, 2], [1, 1, 3, 3]]], dtype="float32"),
"scores": np.array([[[0.9, 0.8]]], dtype="float32"),
}
expected = ort.InferenceSession(
model.SerializeToString(), providers=["CPUExecutionProvider"]
).run(None, feeds)[0]
mod = from_onnx(model, keep_params_in_input=False)
executable = relax.build(mod, target="llvm", relax_pipeline="default", exec_mode="bytecode")
actual = relax.VirtualMachine(executable, tvm.cpu())["main"](
tvm.runtime.tensor(feeds["boxes"]), tvm.runtime.tensor(feeds["scores"])
).numpy()
print("explicit_iou", explicit_iou, "ORT", expected.tolist(), "TVM", actual.tolist())
if explicit_iou:
np.testing.assert_array_equal(actual, expected)
run(False)
run(True)
Version
e269315c90e3a061c9e1c77b370ce883b1b223f4mainsource rechecked:0587e59a696cb435ad4d328726a07daa8d966f3c; the converter still defaults a missingiou_thresholdto0.50.26.dev0Reproducer
Run the complete script below.
The two boxes have IoU
1/7, so the missing ONNX threshold of 0 suppresses the second box,while a threshold of 0.5 keeps it.
Observed output:
The first line omits the optional input and fails. The second line explicitly supplies 0.5
and is the passing control.
Expected behavior
The ONNX NonMaxSuppression schema says a missing
iou_thresholddefaults to 0. The importedfunction should therefore match ONNX Runtime and suppress the second overlapping box.
Actual behavior and likely cause
The Relax ONNX converter assigns
0.5wheneveriou_thresholdis absent, apparently copyingthe native TVM NMS default rather than the ONNX contract. Current
mainretains that branch.Duplicate check
GitHub issue, pull-request, and repository searches found no report for this default mismatch
as of 2026-09-30. Issue #17767 predates Relax support for NonMaxSuppression. PR #19547 fixed
the missing default for
max_output_boxes_per_class, and PR #19843 fixed 1-D scalar inputs;both retained
iou_threshold=0.5and do not cover this case.Complete reproducer