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 5d6c972674 [BugFix][Relax] Fix ONNX `get_converter` index underflow
for old opsets (#19376)
5d6c972674 is described below
commit 5d6c9726748b0a5c0f19addbc183f18e994c1f6a
Author: Soowon Jeong <[email protected]>
AuthorDate: Fri Apr 10 07:31:19 2026 +0900
[BugFix][Relax] Fix ONNX `get_converter` index underflow for old opsets
(#19376)
## Description
`OnnxOpConverter.get_converter()` has an index underflow bug: when a
model's opset is below all implemented converter versions, the selection
index wraps to -1 (Python negative indexing), silently picking the
**latest** implementation instead of the earliest.
### Root cause
```python
versions = sorted(versions + [opset])
version = versions[max([i for i, v in enumerate(versions) if v == opset]) -
1]
#
^^^
# opset=11, versions=[11, 13, 18] → index=0 → 0-1=-1 →
versions[-1]=18
```
### Impact
14 operators affected. For opset 11-12 models, these ops silently
produce wrong results:
| Operator | Impl versions | Opset 11 dispatches to | Correct |
|----------|:---:|:---:|:---:|
| ReduceMean | [13, 18] | **v18** | v13 |
| ReduceL1/L2 | [13, 18] | **v18** | v13 |
| ReduceLogSum | [13, 18] | **v18** | v13 |
| ReduceLogSumExp | [13, 18] | **v18** | v13 |
| ReduceProd | [13, 18] | **v18** | v13 |
| ReduceSumSquare | [13, 18] | **v18** | v13 |
| ReduceMax/Min | [11, 18] | correct | correct |
| Pad, Scatter, ScatterND, RoiAlign | various | **wrong** | — |
The v18 implementations read `axes` from **inputs**, but opset 11-12
passes `axes` as **attributes** — so axes becomes None and the op
reduces over all dimensions.
Example: `ReduceMean(axes=[2,3])` on shape `(2,3,4,4)`:
- Before fix: output shape `(1,1,1,1)` (wrong, all-axis reduction)
- After fix: output shape `(2,3,1,1)` (correct)
### Fix
Replace the index arithmetic with an explicit filter:
```python
candidates = [v for v in impl_versions if v <= opset]
version = max(candidates) if candidates else impl_versions[0]
```
### Testing
Added opset 11 test cases for 7 Reduce operators in
`test_all_reduce_funcs_axes_attr`. All 20 new tests pass. Existing tests
unaffected (570 pass, 14 pre-existing failures in axes_input/topk
unrelated to this change).
```bash
pytest tests/python/relax/test_frontend_onnx.py -k "axes_attr and 11" -v
```
---
python/tvm/relax/frontend/onnx/onnx_frontend.py | 8 +++++---
tests/python/relax/test_frontend_onnx.py | 14 ++++++++++++--
2 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index 940022de41..2707f6ff1c 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -302,9 +302,11 @@ class OnnxOpConverter:
converter, which should be `_impl_vx`. Number x is the biggest
number smaller than or equal to opset belongs to all support
versions.
"""
- versions = [int(d.replace("_impl_v", "")) for d in dir(cls) if
"_impl_v" in d]
- versions = sorted(versions + [opset])
- version = versions[max([i for i, v in enumerate(versions) if v ==
opset]) - 1]
+ impl_versions = sorted(int(d.replace("_impl_v", "")) for d in dir(cls)
if "_impl_v" in d)
+ # Select the largest implemented version that is <= opset.
+ # If opset is below all implementations, fall back to the smallest.
+ candidates = [v for v in impl_versions if v <= opset]
+ version = max(candidates) if candidates else impl_versions[0]
if hasattr(cls, f"_impl_v{version}"):
return getattr(cls, f"_impl_v{version}")
raise NotImplementedError(f"opset version {version} of {cls.__name__}
not implemented")
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index 534161ce6d..db7c3da25a 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -2123,6 +2123,15 @@ def create_reduce_test_parameters_axes_attr():
output.append(("ReduceLogSumExp", value, 13))
output.append(("ReduceL1", value, 13))
output.append(("ReduceL2", value, 13))
+ # Opset 11-12 axes-as-attr: verifies get_converter does not
+ # underflow to the v18 (axes-as-input) implementation.
+ output.append(("ReduceMean", value, 11))
+ output.append(("ReduceProd", value, 11))
+ output.append(("ReduceSumSquare", value, 11))
+ output.append(("ReduceLogSum", value, 11))
+ output.append(("ReduceLogSumExp", value, 11))
+ output.append(("ReduceL1", value, 11))
+ output.append(("ReduceL2", value, 11))
return output
@@ -3837,7 +3846,7 @@ def test_concat_from_sequence_invalid_new_axis():
outputs=[helper.make_tensor_value_info("output", TensorProto.FLOAT,
[32, 8])],
)
model = helper.make_model(graph,
producer_name="test_concat_from_sequence_invalid_new_axis")
-
+
with pytest.raises(ValueError, match="ConcatFromSequence only supports
new_axis in"):
from_onnx(model, opset=11)
@@ -5528,7 +5537,7 @@ def test_split_to_sequence_keepdims_0(axis: int):
split_to_seq_node = helper.make_node(
"SplitToSequence",
- ["data"], # no split input — keepdims applies here
+ ["data"], # no split input — keepdims applies here
["output"],
axis=axis,
keepdims=0,
@@ -5568,6 +5577,7 @@ def
test_split_to_sequence_keepdims_ignored_when_split_provided():
model.ir_version = 8
# Cannot use check_correctness here as ORT deviates from the spec for this
case
from tvm.relax.frontend.onnx import from_onnx
+
tvm_model = from_onnx(model, opset=11, keep_params_in_input=True)
assert tvm_model is not None