siyiweigeHEW opened a new issue, #20183:
URL: https://github.com/apache/tvm/issues/20183

   
   ### Expected behavior
   
   A valid ONNX `Softmax` node must follow the version-specific semantics:
   
   - **opset ≤ 12** (`Softmax-1` / `Softmax-11`): the input is coerced into a 
2-D tensor of shape `[a_0 * ... * a_{k-1}, a_k * ... * a_{n-1}]` (where `k` is 
`axis`), softmax is computed along the second dimension of the coerced tensor, 
and the result is reshaped back to the original shape. The default `axis` is 
**1**.
   - **opset ≥ 13** (`Softmax-13`): softmax is computed directly along `axis`; 
the default `axis` is **-1**.
   
   For example, with input `X` of shape `(2, 3, 4)` at opset 11 and `axis = 1`, 
the output must be `exp(x) / sum(exp(x), axis=(1, 2), keepdims=True)` (12 
elements normalized per row), because dims `1, 2` are flattened into the 
coerced second dimension. The model passes `onnx.checker` and runs correctly in 
onnxruntime.
   
   ### Actual behavior
   
   `tvm.relax.frontend.onnx.from_onnx` silently returns numerically **wrong** 
output for any opset ≤ 12 model whose input has rank > 2 with a non-last axis 
(or no `axis` attribute).
   
   The `Softmax` converter only registers `_impl_v13` 
([onnx_frontend.py:613-615](https://github.com/apache/tvm/blob/262c6d2e04/python/tvm/relax/frontend/onnx/onnx_frontend.py#L613-L615)):
   
   ```python
   class Softmax(OnnxOpConverter):
       @classmethod
       def _impl_v13(cls, bb, inputs, attr, params):
           axis = attr.get("axis", -1)
           return relax.op.nn.softmax(inputs[0], axis=axis)
   ```
   
   Since `OnnxOpConverter.get_converter` 
([onnx_frontend.py:288-308](https://github.com/apache/tvm/blob/262c6d2e04/python/tvm/relax/frontend/onnx/onnx_frontend.py#L288-L308))
 picks the largest registered `_impl_v*` ≤ the model opset, *every* opset (1, 
11, 12, …) is routed to `_impl_v13`. That implementation:
   1. never performs the required coerce-to-2D — it runs direct-axis softmax 
over the single `axis` dimension, and
   2. defaults `axis` to **-1** instead of the spec default **1** for opset ≤ 
12 (confirmed by `onnx.defs`: `Softmax-1`/`Softmax-11` default `axis=1`, 
`Softmax-13` default `axis=-1`).
   
   As a result valid opset ≤ 12 models are **silently** (no error) computed 
with the wrong normalization. For a `(2,3,4)` input at opset 11:
   
   | case | onnxruntime (spec: coerce-to-2D, default axis=1) | TVM relax 
(direct axis) | max \|diff\| |
   
|------|---------------------------------------------------|--------------------------|---------------|
   | no `axis` | softmax over coerced `[2,12]` (12 elems) | softmax over last 4 
elems | 0.644 |
   | `axis=1` | softmax over coerced `[2,12]` (12 elems) | softmax over 
`axis=1` (3 elems) | 0.950 |
   | `axis=0` | softmax over coerced `[1,24]` (24 elems) | softmax over 
`axis=0` (2 elems) | 1.000 |
   
   `LogSoftmax` and `Hardmax` are affected identically (same `_impl_v13`-only 
pattern).
   
   This was already recognized upstream: apache/tvm PR #19428 
("[Relax][FRONTEND][ONNX] Support Softmax, LogSoftmax and Hardmax when opset 
version ≤12", commit `7eea6df1b6`) fixes it by adding `_impl_v1`/`_impl_v11` 
with flatten-to-2D + reshape-back. The version below (built 2026-02-11) 
predates that fix.
   
   ### Environment
   
   - OS: Linux
   - TVM: v0.24.dev0 (main branch, commit `262c6d2e0`, built 2026-02-11)
   - Python: 3.11
   - onnx: 1.20.1
   - onnxruntime: 1.24.1
   
   ### Steps to reproduce
   
   ```python
   """Repro: ONNX Softmax opset<=12 coerce-to-2D semantics silently wrong in 
TVM relax frontend."""
   import numpy as np
   import onnx, onnxruntime
   from onnx import helper, TensorProto
   import tvm
   from tvm import relax
   from tvm.relax.frontend.onnx import from_onnx
   
   
   def build(shape, axis, opset):
       node = helper.make_node("Softmax", ["X"], ["Y"])
       if axis is not None:
           node.attribute.append(helper.make_attribute("axis", axis))
       X = helper.make_tensor_value_info("X", TensorProto.FLOAT, list(shape))
       Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, None)
       g = helper.make_graph([node], "g", [X], [Y])
       m = helper.make_model(g, opset_imports=[helper.make_opsetid("", opset)])
       m.ir_version = 8
       return m
   
   
   def run_tvm(model, shape):
       mod = from_onnx(model, shape_dict={"X": list(shape)})
       ex = relax.build(mod, target="llvm")
       vm = relax.VirtualMachine(ex, tvm.cpu())
       return vm["main"](x).numpy()
   
   
   def show(label, shape, axis, opset):
       m = build(shape, axis, opset)
       o = onnxruntime.InferenceSession(m.SerializeToString(), 
providers=["CPUExecutionProvider"]).run(None, {"X": x})[0]
       t = run_tvm(m, shape)
       d = float(np.abs(t.astype(np.float64) - o.astype(np.float64)).max())
       print(f"{label} onnxruntime shape={list(o.shape)} 
max|diff|(tvm-vs-ort)={d:.3e}")
   
   
   x = (np.arange(24).reshape(2, 3, 4) + 1).astype(np.float32)
   show("Case1 (op11, no axis)", (2, 3, 4), None, 11)   # ONNX default axis=1
   show("Case2 (op11, axis=1) ", (2, 3, 4), 1, 11)
   ```
   
   Actual output:
   
   ```
   Case1 (op11, no axis) onnxruntime shape=[2, 3, 4] 
max|diff|(tvm-vs-ort)=6.437e-01
   Case2 (op11, axis=1)  onnxruntime shape=[2, 3, 4] 
max|diff|(tvm-vs-ort)=9.502e-01
   ```
   
   ### Triage
   
   * needs-triage
   * bug
   * relax
   * frontend/onnx
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to