This is an automated email from the ASF dual-hosted git repository.

mshr-h 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 0054a95abf [Relax][ONNX] Add Celu operator support (#20923)
0054a95abf is described below

commit 0054a95abf8f0358160f19834a5b8493a3ac4bce
Author: Anirudh Mittal <[email protected]>
AuthorDate: Fri Nov 6 18:03:49 2026 +0530

    [Relax][ONNX] Add Celu operator support (#20923)
    
    This commit adds support for the ONNX Celu (Continuously Differentiable
    Exponential Linear Unit) operator in the Relax ONNX frontend.
    
    Celu is computed element-wise as:
      celu(x) = max(0, x) + min(0, alpha * (exp(x / alpha) - 1))
              = -alpha * relu(1 - exp(x / alpha)) + relu(x)
    
    Changes include:
    - Added `Celu` converter class in
    `python/tvm/relax/frontend/onnx/onnx_frontend.py` with `_impl_v12`
    supporting the `alpha` attribute (defaulting to 1.0).
    - Registered "Celu" in the operator converter map.
    - Added structural IR equivalence tests (`test_celu_ir`) and numerical
    correctness tests against ONNX Runtime (`test_celu`) in
    `tests/python/relax/test_frontend_onnx.py`.
    
    Co-authored-by: Anirudh-65 <[email protected]>
---
 python/tvm/relax/frontend/onnx/onnx_frontend.py | 17 +++++++
 tests/python/relax/test_frontend_onnx.py        | 60 +++++++++++++++++++++++++
 2 files changed, 77 insertions(+)

diff --git a/python/tvm/relax/frontend/onnx/onnx_frontend.py 
b/python/tvm/relax/frontend/onnx/onnx_frontend.py
index 8866019c55..2f97ef839a 100644
--- a/python/tvm/relax/frontend/onnx/onnx_frontend.py
+++ b/python/tvm/relax/frontend/onnx/onnx_frontend.py
@@ -1961,6 +1961,22 @@ class Elu(OnnxOpConverter):
         ) + relax.op.nn.relu(inputs[0])
 
 
+class Celu(OnnxOpConverter):
+    """Converts an onnx Celu node into an equivalent Relax expression.
+
+    celu(x) = max(0, x) + min(0, alpha * (exp(x / alpha) - 1))
+            = relu(x) - alpha * relu(1 - exp(x / alpha))
+    """
+
+    @classmethod
+    def _impl_v12(cls, bb, inputs, attr, params):
+        alpha = float(attr.get("alpha", 1.0))
+        x = inputs[0]
+        return relax.expr.const(-alpha) * relax.op.nn.relu(
+            relax.expr.const(1.0) - relax.op.exp(x / relax.expr.const(alpha))
+        ) + relax.op.nn.relu(x)
+
+
 class Selu(OnnxOpConverter):
     """Converts an onnx Selu node into an equivalent Relax expression."""
 
@@ -6164,6 +6180,7 @@ def _get_convert_map():
         "LeakyRelu": LeakyRelu,
         "ThresholdedRelu": ThresholdedRelu,
         "Elu": Elu,
+        "Celu": Celu,
         "Gelu": Gelu,
         "FastGelu": FastGelu,
         "BiasGelu": BiasGelu,
diff --git a/tests/python/relax/test_frontend_onnx.py 
b/tests/python/relax/test_frontend_onnx.py
index bba6ef4faf..d34d208b00 100644
--- a/tests/python/relax/test_frontend_onnx.py
+++ b/tests/python/relax/test_frontend_onnx.py
@@ -4480,6 +4480,66 @@ def test_trilu_dynamic_k_ir(upper: bool):
     tvm_tirx.ir.assert_structural_equal(tvm_model, expected)
 
 
+def test_celu_ir():
+    """Check the lowered Relax IR for the Celu operator (opset 12, default 
alpha)."""
+    model = make_unary_model("Celu", [2, 3])
+    model.opset_import[0].version = 12
+    tvm_model = from_onnx(model, opset=12, keep_params_in_input=True)
+
+    @I.ir_module
+    class Expected:
+        @R.function
+        def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2, 3), 
dtype="float32"):
+            R.func_attr({"num_input": 1})
+            with R.dataflow():
+                lv: R.Tensor((2, 3), dtype="float32") = R.divide(x, 
R.const(1.0, "float32"))
+                lv1: R.Tensor((2, 3), dtype="float32") = R.exp(lv)
+                lv2: R.Tensor((2, 3), dtype="float32") = 
R.subtract(R.const(1.0, "float32"), lv1)
+                lv3: R.Tensor((2, 3), dtype="float32") = R.nn.relu(lv2)
+                lv4: R.Tensor((2, 3), dtype="float32") = 
R.multiply(R.const(-1.0, "float32"), lv3)
+                lv5: R.Tensor((2, 3), dtype="float32") = R.nn.relu(x)
+                gv: R.Tensor((2, 3), dtype="float32") = R.add(lv4, lv5)
+                R.output(gv)
+            return gv
+
+    tvm_tirx.ir.assert_structural_equal(tvm_model, Expected)
+
+    # Custom alpha=0.5
+    model = make_unary_model("Celu", [2, 3], attrs={"alpha": 0.5})
+    model.opset_import[0].version = 12
+    tvm_model = from_onnx(model, opset=12, keep_params_in_input=True)
+
+    @I.ir_module
+    class ExpectedCustomAlpha:
+        @R.function
+        def main(x: R.Tensor((2, 3), dtype="float32")) -> R.Tensor((2, 3), 
dtype="float32"):
+            R.func_attr({"num_input": 1})
+            with R.dataflow():
+                lv: R.Tensor((2, 3), dtype="float32") = R.divide(x, 
R.const(0.5, "float32"))
+                lv1: R.Tensor((2, 3), dtype="float32") = R.exp(lv)
+                lv2: R.Tensor((2, 3), dtype="float32") = 
R.subtract(R.const(1.0, "float32"), lv1)
+                lv3: R.Tensor((2, 3), dtype="float32") = R.nn.relu(lv2)
+                lv4: R.Tensor((2, 3), dtype="float32") = 
R.multiply(R.const(-0.5, "float32"), lv3)
+                lv5: R.Tensor((2, 3), dtype="float32") = R.nn.relu(x)
+                gv: R.Tensor((2, 3), dtype="float32") = R.add(lv4, lv5)
+                R.output(gv)
+            return gv
+
+    tvm_tirx.ir.assert_structural_equal(tvm_model, ExpectedCustomAlpha)
+
+
+def test_celu():
+    """Check numerical correctness of the Celu operator against onnxruntime."""
+    # Default alpha=1.0
+    check_correctness(make_unary_model("Celu", [2, 3]), opset=12)
+
+    # Custom alpha=0.5
+    check_correctness(make_unary_model("Celu", [2, 3], attrs={"alpha": 0.5}), 
opset=12)
+
+    # Custom alpha=2.0
+    check_correctness(make_unary_model("Celu", [2, 3], attrs={"alpha": 2.0}), 
opset=12)
+
+
 def test_selu():
     model = make_unary_model("Selu", [2, 3])
     tvm_model = from_onnx(model, keep_params_in_input=True)

Reply via email to