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 38fd68c3c2 [Fix][Relax][Frontend][ONNX] Support broadcastable
multi-axis PRelu slopes (#20149)
38fd68c3c2 is described below
commit 38fd68c3c25294c6f4e58dd4b189559abb51da02
Author: HuEnwei <[email protected]>
AuthorDate: Wed Aug 26 06:51:32 2026 +0800
[Fix][Relax][Frontend][ONNX] Support broadcastable multi-axis PRelu slopes
(#20149)
Fixes: #20148
## Summary
The Relax ONNX frontend `PRelu._impl_v1` rejected a valid `PRelu` whose
`slope`
is broadcastable to `X` across **multiple** non-broadcast axes (e.g. a
slope
shaped exactly like `X`, or with several non-1 dims) with a
`ValueError`, even
though onnxruntime and `onnx.reference` accept and run such models
correctly.
After #20115 added lower-rank and rank-0 slope support, this was the
remaining
gap for unidirectionally broadcastable slopes.
## Root cause
`PRelu._impl_v1` lowers a slope with a **single** non-broadcast axis to
`relax.op.nn.prelu(x, slope_vec, axis)`, which is the only shape Relax's
`nn.prelu` can express (one per-axis slope vector). A slope that
broadcasts
along several axes (`slope == X`, `(1, C, H, W)`, `(N, C, 1, 1)`, …)
cannot be
represented that way, so the code raised instead of lowering the op:
```python
# Must have only ONE non-broadcast axis
if len(non_one_axes) != 1:
raise ValueError(
f"Invalid PRelu slope shape (multiple non-broadcast dims):
{slope_shape}"
)
```
## Fix
For the multi-non-broadcast case, lower `PRelu` elementwise instead:
```python
dtype = x.ty.dtype.dtype
return relax.op.where(
relax.op.less(x, relax.const(0, dtype)),
relax.op.multiply(x, slope),
x,
)
```
This is exactly `PRelu(x, s) = where(x < 0, s * x, x)` with `s`
broadcast to
`x`. The all-ones / rank-1 / single-non-broadcast paths are unchanged
and still
use `nn.prelu`. Non-broadcastable slopes (rank > x, or a non-1 dim that
does not
match `x`) still fail at build time with a broadcast error, consistent
with
onnxruntime rejecting them at session creation.
## Validation
Differential test: Relax (build + `VirtualMachine`) vs onnxruntime on
the same
model, comparing output shapes and max |diff| (cross-checked with
`onnx.reference`), across 7 `X` shapes × every valid broadcastable
`slope`.
| Case | onnxruntime | TVM | max\|diff\| | Result |
|---|---|---|---|---|
| lower-rank `(64,1,1)` on `(1,64,128,128)` | `(1,64,128,128)` |
`(1,64,128,128)` | 0.00e+00 | OK |
| rank-0 scalar slope | `(2,3,4,5)` | `(2,3,4,5)` | 0.00e+00 | OK |
| same-rank multi non-broadcast, `slope == X` | `(2,3,4,5)` |
`(2,3,4,5)` | 0.00e+00 | OK |
| same-rank multi non-broadcast `(1,3,4,5)` | `(2,3,4,5)` | `(2,3,4,5)`
| 0.00e+00 | OK |
| lower-rank multi non-broadcast `(4,5)` | `(2,3,4,5)` | `(2,3,4,5)` |
0.00e+00 | OK |
| same-rank single non-broadcast `(1,1,4,1)` (regression) | `(2,3,4,5)`
| `(2,3,4,5)` | 0.00e+00 | OK |
| 1-D `(5,)` / all-ones (regression) | `(2,3,4,5)` | `(2,3,4,5)` |
0.00e+00 | OK |
Total: **113 valid cases (onnxruntime-accepted), 113 OK, 0 rejected, 0
numeric
mismatches** (pre-fix: 46 OK / 67 rejected). The motivating Real-ESRGAN
case
`X(1,64,128,128)` + `slope(64,1,1)` now imports and matches onnxruntime
exactly.
Run:
```bash
python results/TVM/deepseek-v4-flash/prove_hum/onnx_PRelu/5修复_差分验证.py
```
## Files changed
- `python/tvm/relax/frontend/onnx/onnx_frontend.py` — `PRelu._impl_v1`:
elementwise
`where(x < 0, s * x, x)` fallback for multi-non-broadcast slopes.
---
python/tvm/relax/frontend/onnx/onnx_frontend.py | 28 +++++++++------
tests/python/relax/test_frontend_onnx.py | 46 +++++++++++++++++++++++++
2 files changed, 64 insertions(+), 10 deletions(-)
diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index b63518f6c0..054efa9f0c 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -1802,16 +1802,24 @@ class PRelu(OnnxOpConverter):
if s_ndim <= ndim:
non_one_axes = [i for i, ss in enumerate(slope_shape) if ss != 1]
- # Must have only ONE non-broadcast axis
- if len(non_one_axes) != 1:
- raise ValueError(
- f"Invalid PRelu slope shape (multiple non-broadcast dims):
{slope_shape}"
- )
- relative_axis = non_one_axes[0]
- axis = ndim - s_ndim + relative_axis
-
- slope = relax.op.reshape(slope, (slope_shape[relative_axis],))
- return relax.op.nn.prelu(x, slope, axis)
+ # A single non-broadcast axis can be expressed directly as a
+ # per-axis slope of nn.prelu.
+ if len(non_one_axes) == 1:
+ relative_axis = non_one_axes[0]
+ axis = ndim - s_ndim + relative_axis
+
+ slope = relax.op.reshape(slope, (slope_shape[relative_axis],))
+ return relax.op.nn.prelu(x, slope, axis)
+
+ # Multiple non-broadcast axes (including a slope shaped like x):
+ # nn.prelu can only express a single per-axis slope, so lower
+ # PRelu(x, s) = where(x < 0, s * x, x) elementwise instead.
+ dtype = x.ty.dtype.dtype
+ return relax.op.where(
+ relax.op.less(x, relax.const(0, dtype)),
+ relax.op.multiply(x, slope),
+ x,
+ )
raise ValueError(f"Unsupported PRelu slope shape: {slope_shape}")
diff --git a/tests/python/relax/test_frontend_onnx.py
b/tests/python/relax/test_frontend_onnx.py
index 39e067d5d7..60046d44df 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -3727,12 +3727,28 @@ def test_prelu():
R.output(gv)
return gv
+ @I.ir_module
+ class ExpectedMultiAxisSlope:
+ @R.function
+ def main(
+ a: R.Tensor((2, 3, 4, 5), dtype="float32"),
+ b: R.Tensor((4, 5), dtype="float32"),
+ ) -> R.Tensor((2, 3, 4, 5), dtype="float32"):
+ R.func_attr({"num_input": 2})
+ with R.dataflow():
+ lv: R.Tensor((2, 3, 4, 5), dtype="bool") = R.less(a,
R.const(0.0, "float32"))
+ lv1: R.Tensor((2, 3, 4, 5), dtype="float32") = R.multiply(a, b)
+ gv: R.Tensor((2, 3, 4, 5), dtype="float32") = R.where(lv, lv1,
a)
+ R.output(gv)
+ return gv
+
_assert_prelu_ir([], ExpectedRankZeroSlope)
_assert_prelu_ir([1], ExpectedScalarSlope)
_assert_prelu_ir([1, 1], ExpectedTwoDimScalarSlope)
_assert_prelu_ir([32], ExpectedChannelSlope)
_assert_prelu_ir([3, 1, 1], ExpectedBatchSlope)
_assert_prelu_ir([32, 1, 1], ExpectedLowerRankChannelSlope,
input_shape=(1, 32, 16, 16))
+ _assert_prelu_ir([4, 5], ExpectedMultiAxisSlope, input_shape=(2, 3, 4, 5))
def test_prelu_lower_rank_slope():
@@ -3759,6 +3775,36 @@ def test_prelu_lower_rank_slope():
check_correctness(model, inputs=inputs, opset=16, check_dtypes=True)
+def test_prelu_multi_axis_slope():
+ """A slope broadcastable across multiple axes (incl. a slope shaped like
x) is
+ lowered elementwise to PRelu(x, s) = where(x < 0, s * x, x) since nn.prelu
can
+ only express a single per-axis slope."""
+ input_shape = (2, 3, 4, 5)
+ for slope_shape in [(4, 5), (1, 3, 4, 5), (2, 3, 4, 5)]:
+ graph = helper.make_graph(
+ [helper.make_node("PRelu", ["x", "slope"], ["y"])],
+ "prelu_multi_axis_slope_test",
+ inputs=[
+ helper.make_tensor_value_info("x", TensorProto.FLOAT,
input_shape),
+ helper.make_tensor_value_info("slope", TensorProto.FLOAT,
list(slope_shape)),
+ ],
+ outputs=[helper.make_tensor_value_info("y", TensorProto.FLOAT,
input_shape)],
+ )
+ model = helper.make_model(
+ graph,
+ producer_name="prelu_multi_axis_slope_test",
+ opset_imports=[helper.make_opsetid("", 16)],
+ )
+ inputs = {
+ "x": np.linspace(-2.0, 2.0, np.prod(input_shape),
dtype="float32").reshape(input_shape),
+ # negative slopes exercise the s * x path on both sides of the
sign.
+ "slope": np.linspace(-0.5, 0.8, np.prod(slope_shape),
dtype="float32").reshape(
+ slope_shape
+ ),
+ }
+ check_correctness(model, inputs=inputs, opset=16, check_dtypes=True)
+
+
def test_thresholded_relu():
model = make_unary_model("ThresholdedRelu", [2, 3])
tvm_model = from_onnx(model, keep_params_in_input=True)