Repository navigation
fix(reference): accept scalar and omitted optional inputs in NonMaxSuppression - #8655
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8655 +/- ##
==========================================
+ Coverage 84.96% 85.01% +0.05%
==========================================
Files 494 494
Lines 24280 24291 +11
Branches 2667 2665 -2
==========================================
+ Hits 20629 20651 +22
+ Misses 2883 2880 -3
+ Partials 768 760 -8 ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
🟢 Approval recommended
The focused fix addresses the reported failures with regression coverage, and no blocking issues remain; tests were not independently rerun.
No open issues 🎉
What changed in this PR
This PR fixes NonMaxSuppression in the reference evaluator to accept scalar and omitted optional inputs while preserving support for single-element 1-D tensors.
Changes:
- Normalize optional inputs and apply defaults when inputs are missing or empty.
- Return an empty tensor with shape
[0, 3]when the maximum output count is zero. - Add regression tests and update generated documentation with the scalar-input example.
| File | Description |
|---|---|
| tests/python/reference_evaluator_test.py | Tests scalar, 1-D, and omitted optional inputs. |
| onnx/reference/ops/op_non_max_suppression.py | Normalizes optional inputs and fixes the zero-output return. |
| onnx/backend/test/case/node/nonmaxsuppression.py | Adds a scalar-input backend test case. |
| docs/TestCoverage.md | Includes the new test and updates the case count. |
| docs/Operators.md | Adds the generated scalar-input example. |
Review effort: Balanced
…ppression The spec defines max_output_boxes_per_class, iou_threshold and score_threshold as optional scalars, with max_output_boxes_per_class and iou_threshold defaulting to 0. The reference implementation read them with [0], so 0-D inputs raised IndexError, and it called .size on the first two unconditionally, so omitting either of them raised AttributeError. The early return meant for max_output_boxes_per_class == 0 tested .size on the clamped value, which is a Python int when the input is empty or negative, so it raised AttributeError there and never returned otherwise. Flatten each present, non-empty optional input to 1-D so 0-D and single-element 1-D tensors are both accepted, keep the defaults for missing or empty ones, and return an empty [0, 3] result when no box can be selected. Add reference evaluator tests for both cases and a backend test case that passes the three inputs as 0-D tensors. Signed-off-by: Max Freedom Pollard <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
546c8ca to
7be8b12
Compare
|
Rebased on main to fix the conflict (kept both the new BitCast tests and these). The Check URLs failure is the link checker, which also failed on main at 0c8af06. |
Description
NonMaxSuppressionhas three optional inputs,max_output_boxes_per_class,iou_thresholdandscore_threshold. The spec calls each of them a scalar, and givesmax_output_boxes_per_classandiou_thresholda default of 0. The reference implementation inonnx/reference/ops/op_non_max_suppression.pyfails in both cases:[0](lines 137, 140 and 143 before this change), so a 0-D tensor raisesIndexError. Only 1-D tensors holding one element work, and those are what all the existing node tests use..sizeonmax_output_boxes_per_classandiou_thresholdwithout checking forNone(lines 159 and 161), so leaving out either input raisesAttributeError.score_thresholdalready had that check.max_output_boxes_per_class == 0tests.sizeon the clamped value (line 201). That value is a Python int when the input is empty or negative, so the check raisesAttributeErrorthere and never returns otherwise.On current main:
With this change the two calls return
[[0, 0, 3], [0, 0, 0]]and[[0, 0, 3], [0, 0, 0], [0, 0, 5]]. With no optional inputs at all, the result is an empty[0, 3]tensor.The fix adds a small helper that turns each present, non-empty optional input into a 1-D array and treats a missing or empty one as absent, so both 0-D and single-element 1-D tensors work and the defaults apply. The early return now compares the value with 0 and returns a
[0, 3]tensor, the documented output shape. Nothing changes for inputs that already worked.Motivation and Context
A model that passes these inputs as 0-D tensors, which is how the spec describes them, cannot run in
ReferenceEvaluator. The same is true of a model that leaves outiou_thresholdand relies on its default. The node tests did not catch this because every one of them passes all five inputs as 1-D tensors.Testing
Added to
tests/python/reference_evaluator_test.py:test_non_max_suppression_scalar_inputs, run with 0-D and with 1-D inputs. The 1-D case is a control that already passed.test_non_max_suppression_omitted_optional_inputs, which covers all three optional inputs omitted, both thresholds omitted, and onlyiou_thresholdomitted.Added the node test
test_nonmaxsuppression_scalar_inputs. It uses the same data astest_nonmaxsuppression_suppress_by_IOU_and_scores, with the three inputs as 0-D tensors.docs/Operators.mdanddocs/TestCoverage.mdwere regenerated withpython onnx/defs/gen_doc.pyandpython onnx/backend/test/stat_coverage.py(ONNX_ML=1). The only change in either file is the new example.On unmodified main, the four new reference evaluator cases fail: the 0-D case with
IndexError, the omitted-input cases withAttributeError. The 1-D control passes on both. The new node test fails on main with the sameIndexError.ruff checkandruff format --checkpass on the changed files.I also ran the new node test model through onnxruntime 1.31.0 as an independent implementation, and it returns the expected
[[0, 0, 3], [0, 0, 0]]. In 800 randomized cases (400 with 0-D inputs and 400 with 1-D inputs, covering both box formats and tied scores) the patched reference matches onnxruntime exactly.🤖 Generated with Claude Code