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

ekalda 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 1a70083711 [microNPU][ETHOSU] Softmax int8 legalization support 
(#14629)
1a70083711 is described below

commit 1a70083711d0f3a315485e9618b0553c31bec864
Author: Aleksei-grovety <[email protected]>
AuthorDate: Wed Apr 26 13:08:40 2023 +0400

    [microNPU][ETHOSU] Softmax int8 legalization support (#14629)
    
    Add support for legalizing Softmax int8 to list NPU operations as 
implemented in Vela.
---
 python/tvm/autotvm/task/topi_integration.py        |   4 +
 .../tvm/relay/backend/contrib/ethosu/__init__.py   |   1 +
 .../tvm/relay/backend/contrib/ethosu/legalize.py   |   2 +
 .../backend/contrib/ethosu/softmax_rewriter.py     | 516 +++++++++++++++++++++
 .../backend/contrib/ethosu/te/unary_elementwise.py |   3 +-
 .../tvm/relay/backend/contrib/ethosu/tir/passes.py |   4 +-
 .../backend/contrib/ethosu/tir_to_cs_translator.py |  16 +-
 python/tvm/relay/op/contrib/ethosu.py              |  62 +++
 tests/python/contrib/test_ethosu/test_codegen.py   |  18 +
 tests/python/contrib/test_ethosu/test_legalize.py  | 120 +++++
 10 files changed, 738 insertions(+), 8 deletions(-)

diff --git a/python/tvm/autotvm/task/topi_integration.py 
b/python/tvm/autotvm/task/topi_integration.py
index 307d44810c..a4f3636edb 100644
--- a/python/tvm/autotvm/task/topi_integration.py
+++ b/python/tvm/autotvm/task/topi_integration.py
@@ -250,11 +250,15 @@ def register_topi_schedule(task_name, func=None):
 
 def get_workload(outs, task_name=None):
     """Retrieve the workload from outputs"""
+    visited = set()
 
     def traverse(tensors):
         """traverse all ops to find attached workload"""
         for t in tensors:
             op = t.op
+            if op in visited:
+                continue
+            visited.add(op)
             wkl = traverse(op.input_tensors)
             if wkl is not None:
                 return wkl
diff --git a/python/tvm/relay/backend/contrib/ethosu/__init__.py 
b/python/tvm/relay/backend/contrib/ethosu/__init__.py
index c4948d54dc..be77a81e4e 100644
--- a/python/tvm/relay/backend/contrib/ethosu/__init__.py
+++ b/python/tvm/relay/backend/contrib/ethosu/__init__.py
@@ -21,3 +21,4 @@ from . import preprocess
 from . import codegen
 from . import vela_api
 from . import tir_to_cs_translator
+from . import softmax_rewriter
diff --git a/python/tvm/relay/backend/contrib/ethosu/legalize.py 
b/python/tvm/relay/backend/contrib/ethosu/legalize.py
index 5aaa1417ae..3e69b409a3 100644
--- a/python/tvm/relay/backend/contrib/ethosu/legalize.py
+++ b/python/tvm/relay/backend/contrib/ethosu/legalize.py
@@ -32,6 +32,7 @@ from tvm.relay.dataflow_pattern import CallPattern
 from tvm.relay.backend.contrib.ethosu import op as ethosu_ops  # type: ignore
 from tvm.relay.backend.contrib.ethosu import vela_api
 from tvm.relay.backend.contrib.ethosu import util
+from tvm.relay.backend.contrib.ethosu.softmax_rewriter import SoftmaxRewriter
 from tvm.relay.op.contrib import ethosu as ethosu_patterns  # type: ignore
 
 
@@ -1479,6 +1480,7 @@ class LegalizeEthosU:
             LeakyReLURewriter(),
             MeanRewriter(),
             SumRewriter(),
+            SoftmaxRewriter(),
             ConcatRewriter(),
             SigmoidRewriter(),
             RequantizeRewriter(),
diff --git a/python/tvm/relay/backend/contrib/ethosu/softmax_rewriter.py 
b/python/tvm/relay/backend/contrib/ethosu/softmax_rewriter.py
new file mode 100644
index 0000000000..16067fed95
--- /dev/null
+++ b/python/tvm/relay/backend/contrib/ethosu/softmax_rewriter.py
@@ -0,0 +1,516 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""SoftmaxRewriter for legalization Softmax operation."""
+import math
+
+import numpy as np
+from ethosu.vela import fp_math, scaling
+
+import tvm
+from tvm import relay
+from tvm.relay.backend.contrib.ethosu import op as ethosu_ops
+from tvm.relay.dataflow_pattern import DFPatternCallback, wildcard
+from tvm.relay.op.contrib import ethosu as ethosu_patterns
+
+
+class SoftmaxRewriter(DFPatternCallback):
+    """This rewriting converts Softmax operation into a sequence of operations 
as in Vela."""
+
+    def __init__(self):
+        super().__init__(require_type=True, rewrite_once=True)
+        self.params_class = ethosu_patterns.SoftMaxParams
+        self.pattern = (
+            wildcard().has_attr({"Composite": 
ethosu_patterns.SoftMaxParams.composite_name})
+        )(None)
+
+    def generate_exp_table(self, input_scale):
+        """Generate a LUT table for exponential function.
+
+        Parameters
+        ----------
+        input_scale : float
+            The scale for input.
+
+        Returns
+        -------
+        lut : tvm.relay.expr.Constant
+            LUT table for exponential function.
+        """
+        beta = 1.0
+        integer_bits = 5
+        total_signed_bits = 31
+        # Calculate scaling
+        real_beta = min(
+            np.double(beta) * np.double(input_scale) * (1 << (31 - 
integer_bits)),
+            np.double((1 << 31) - 1.0),
+        )
+        scale, shift = scaling.quantise_scale(real_beta)
+        shift = 31 - shift
+        diff_min = -1.0 * math.floor(
+            1.0
+            * ((1 << integer_bits) - 1)
+            * (1 << (total_signed_bits - integer_bits))
+            / (1 << shift)
+        )
+        # Generate the exp LUT
+        lut = []
+        for x in range(256):
+            input_diff = x - 255
+            if input_diff >= diff_min:
+                rescale = fp_math.saturating_rounding_mul32(input_diff * (1 << 
shift), scale)
+                lut.append(fp_math.exp_on_negative_values(rescale))
+            else:
+                lut.append(0)
+        res = np.array(lut, dtype="int32")
+        return relay.const(res)
+
+    def callback(
+        self, pre: tvm.relay.Expr, post: tvm.relay.Expr, node_map: 
tvm.ir.container.Map
+    ) -> tvm.relay.Expr:
+        params = self.params_class(post.op.body)
+
+        ifm = post.args[0]
+        ifm_dtype = ifm.checked_type.dtype
+        bhw = np.prod(params.ifm.shape[:-1])
+        depth = params.ifm.shape[-1]
+
+        # The calculation of Softmax is similar to that in Vela
+        # 
https://review.mlplatform.org/plugins/gitiles/ml/ethos-u/ethos-u-vela/+/refs/tags/3.7.0/ethosu/vela/softmax.py#230
+        # PASS 0 - Depthwise Maxpool
+        # reshape for depthwise maxpool
+        ifm = relay.reshape(ifm, (1, bhw, depth, 1))
+        lut = relay.const([], dtype="int32")
+        depthwise_maxpool = ethosu_ops.ethosu_pooling(
+            ifm=ifm,
+            lut=lut,
+            pooling_type="MAX",
+            ifm_scale=float(params.ifm.q_params.scale_f32),
+            ifm_zero_point=int(params.ifm.q_params.zero_point),
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            pool_shape=(1, depth),
+            ofm_channels=1,
+            ofm_dtype=ifm_dtype,
+        )
+
+        # PASS 1 - Sub+LUT(exp)
+        # move all data along the height axis, except channels
+        ifm = relay.reshape(ifm, (1, bhw, 1, depth))
+        exp_lut = self.generate_exp_table(float(params.ifm.q_params.scale_f32))
+        ifm_exp = ethosu_ops.ethosu_binary_elementwise(
+            ifm=ifm,
+            ifm2=depthwise_maxpool,
+            lut=exp_lut,
+            operator_type="SUB",
+            ifm_scale=float(params.ifm.q_params.scale_f32),
+            ifm_zero_point=int(params.ifm.q_params.zero_point),
+            ifm2_scale=0.0,
+            ifm2_zero_point=int(params.ifm.q_params.zero_point),
+            ofm_scale=1.0,
+            ofm_zero_point=127,
+            ifm_channels=depth,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="LUT",
+        )
+
+        # PASS 2 - SHR
+        shr_const = relay.const(np.full([1, 1, 1, 1], 12, dtype="int32"))
+        shr = ethosu_ops.ethosu_binary_elementwise(
+            ifm=ifm_exp,
+            ifm2=shr_const,
+            lut=lut,
+            operator_type="SHR",
+            ifm_scale=1.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ifm_channels=params.ifm.shape[-1],
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128,
+            clip_max=127,
+            rounding_mode="NATURAL",
+        )
+
+        # PASS 3 - Reduce sum
+        sum_of_exp = ethosu_ops.ethosu_pooling(
+            ifm=shr,
+            lut=lut,
+            pooling_type="SUM",
+            ifm_scale=0.0,
+            ifm_zero_point=0,
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            pool_shape=(1, 1),
+            ofm_channels=1,
+            upscale="NONE",
+            ofm_dtype="int32",
+        )
+
+        # PASS 4 - CLZ
+        headroom_plus_one = ethosu_ops.ethosu_unary_elementwise(
+            ifm=sum_of_exp,
+            lut=lut,
+            operator_type="CLZ",
+            ifm_scale=0.0,
+            ifm_zero_point=0,
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ofm_channels=1,
+        )
+
+        # PASS 5 - Sub
+        headroom_offset_const = relay.const(np.full([1, bhw, 1, 1], 35, 
dtype="int32"))
+        right_shift = ethosu_ops.ethosu_binary_elementwise(
+            ifm=headroom_offset_const,
+            ifm2=headroom_plus_one,
+            lut=lut,
+            operator_type="SUB",
+            ifm_scale=1.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=1.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ifm_channels=1,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+        )
+
+        # PASS 6 - Sub
+        one_const = relay.const(np.full([1, 1, 1, 1], 1, dtype="int32"))
+        headroom = ethosu_ops.ethosu_binary_elementwise(
+            ifm=headroom_plus_one,
+            ifm2=one_const,
+            lut=lut,
+            operator_type="SUB",
+            ifm_scale=0.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ifm_channels=1,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+        )
+
+        # PASS 7 - SHL
+        shifted_sum = ethosu_ops.ethosu_binary_elementwise(
+            ifm=sum_of_exp,
+            ifm2=headroom,
+            lut=lut,
+            operator_type="SHL",
+            ifm_scale=0.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ifm_channels=depth,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128,
+            clip_max=127,
+        )
+
+        # PASS 8 - Sub
+        shifted_one_const = relay.const(np.full([1, 1, 1, 1], 1 << 30, 
dtype="int32"))
+        shifted_sum_minus_one = ethosu_ops.ethosu_binary_elementwise(
+            ifm=shifted_sum,
+            ifm2=shifted_one_const,
+            lut=lut,
+            operator_type="SUB",
+            ifm_scale=0.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ifm_channels=1,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+        )
+
+        # PASS 9 - SHL
+        shifted_sum_minus_one = ethosu_ops.ethosu_binary_elementwise(
+            ifm=shifted_sum_minus_one,
+            ifm2=one_const,
+            lut=lut,
+            operator_type="SHL",
+            ifm_scale=0.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=0.0,
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ifm_channels=1,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128,
+            clip_max=127,
+        )
+
+        # PASS 10 - Add
+        f0_one_const = relay.const(np.full([1, 1, 1, 1], (1 << 31) - 1, 
dtype="int32"))
+        half_denominator = ethosu_ops.ethosu_binary_elementwise(
+            ifm=shifted_sum_minus_one,
+            ifm2=f0_one_const,
+            lut=lut,
+            operator_type="ADD",
+            ifm_scale=0.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=1.0,
+            ofm_zero_point=0,
+            ifm_channels=1,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128,
+            clip_max=127,
+            use_rescale=True,
+            rescale_scale=1,
+            rescale_shift=1,
+        )
+
+        # PASS 11 - Mul
+        neg_32_over_17_const = relay.const(np.full([1, 1, 1, 1], -1010580540, 
dtype="int32"))
+        rescaled = ethosu_ops.ethosu_binary_elementwise(
+            ifm=half_denominator,
+            ifm2=neg_32_over_17_const,
+            lut=lut,
+            operator_type="MUL",
+            ifm_scale=1.0,
+            ifm_zero_point=0,
+            ifm2_scale=1.0,
+            ifm2_zero_point=0,
+            ofm_scale=2.0,
+            ofm_zero_point=0,
+            ifm_channels=depth,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128 * 2,
+            clip_max=127 * 2,
+        )
+
+        # PASS 12 - Add
+        const_48_over_17_const = relay.const(np.full([1, 1, 1, 1], 1515870810, 
dtype="int32"))
+        rescale_w_offset = ethosu_ops.ethosu_binary_elementwise(
+            ifm=rescaled,
+            ifm2=const_48_over_17_const,
+            lut=lut,
+            operator_type="ADD",
+            ifm_scale=2.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=1.0,
+            ofm_zero_point=0,
+            ifm_channels=1,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128,
+            clip_max=127,
+        )
+
+        nr_x = rescale_w_offset
+        f2_one_const = relay.const(np.full([1, bhw, 1, 1], 1 << 29, 
dtype="int32"))
+        four_const = relay.const(np.full([1, 1, 1, 1], 4, dtype="int32"))
+        for _ in range(3):
+            # PASS 13, 18, 23 - Mul
+            half_denominator_times_x = ethosu_ops.ethosu_binary_elementwise(
+                ifm=nr_x,
+                ifm2=half_denominator,
+                lut=lut,
+                operator_type="MUL",
+                ifm_scale=1.0,
+                ifm_zero_point=0,
+                ifm2_scale=1.0,
+                ifm2_zero_point=0,
+                ofm_scale=2.0,
+                ofm_zero_point=0,
+                ifm_channels=1,
+                ifm2_channels=1,
+                reversed_operands=False,
+                ofm_dtype="int32",
+                activation="CLIP",
+                clip_min=-128 * 2,
+                clip_max=127 * 2,
+            )
+
+            # PASS 14, 19, 24 - Sub
+            one_minus_half_denomin_times_x = 
ethosu_ops.ethosu_binary_elementwise(
+                ifm=f2_one_const,
+                ifm2=half_denominator_times_x,
+                lut=lut,
+                operator_type="SUB",
+                ifm_scale=0.0,
+                ifm_zero_point=0,
+                ifm2_scale=2.0,
+                ifm2_zero_point=0,
+                ofm_scale=1.0,
+                ofm_zero_point=0,
+                ifm_channels=1,
+                ifm2_channels=1,
+                reversed_operands=False,
+                ofm_dtype="int32",
+            )
+
+            # PASS 15, 20, 25 - Mul
+            to_rescale = ethosu_ops.ethosu_binary_elementwise(
+                ifm=nr_x,
+                ifm2=one_minus_half_denomin_times_x,
+                lut=lut,
+                operator_type="MUL",
+                ifm_scale=1.0,
+                ifm_zero_point=0,
+                ifm2_scale=1.0,
+                ifm2_zero_point=0,
+                ofm_scale=2.0,
+                ofm_zero_point=0,
+                ifm_channels=1,
+                ifm2_channels=1,
+                reversed_operands=False,
+                ofm_dtype="int32",
+                activation="CLIP",
+                clip_min=-128 * 2,
+                clip_max=127 * 2,
+            )
+
+            # PASS 16, 21, 26 - Mul
+            to_add = ethosu_ops.ethosu_binary_elementwise(
+                ifm=to_rescale,
+                ifm2=four_const,
+                lut=lut,
+                operator_type="MUL",
+                ifm_scale=2.0,
+                ifm_zero_point=0,
+                ifm2_scale=0.0,
+                ifm2_zero_point=0,
+                ofm_scale=0.0,
+                ofm_zero_point=int(params.ofm.q_params.zero_point),
+                ifm_channels=1,
+                ifm2_channels=1,
+                reversed_operands=False,
+                ofm_dtype="int32",
+                activation="CLIP",
+                clip_min=-128,
+                clip_max=127,
+            )
+
+            # PASS 17, 22, 27 - Add
+            nr_x = ethosu_ops.ethosu_binary_elementwise(
+                ifm=nr_x,
+                ifm2=to_add,
+                lut=lut,
+                operator_type="ADD",
+                ifm_scale=1.0,
+                ifm_zero_point=0,
+                ifm2_scale=0.0,
+                ifm2_zero_point=0,
+                ofm_scale=1.0,
+                ofm_zero_point=0,
+                ifm_channels=1,
+                ifm2_channels=1,
+                reversed_operands=False,
+                ofm_dtype="int32",
+            )
+
+        # PASS 28 - Mul
+        two_const = relay.const(np.full([1, 1, 1, 1], 2, dtype="int32"))
+        scale_factor = ethosu_ops.ethosu_binary_elementwise(
+            ifm=nr_x,
+            ifm2=two_const,
+            lut=lut,
+            operator_type="MUL",
+            ifm_scale=1.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=1.0,
+            ofm_zero_point=0,
+            ifm_channels=1,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128,
+            clip_max=127,
+        )
+
+        # PASS 29 - Mul
+        scaled_exp = ethosu_ops.ethosu_binary_elementwise(
+            ifm=ifm_exp,
+            ifm2=scale_factor,
+            lut=lut,
+            operator_type="MUL",
+            ifm_scale=1.0,
+            ifm_zero_point=0,
+            ifm2_scale=1.0,
+            ifm2_zero_point=0,
+            ofm_scale=2.0,
+            ofm_zero_point=0,
+            ifm_channels=depth,
+            ifm2_channels=1,
+            reversed_operands=False,
+            ofm_dtype="int32",
+            activation="CLIP",
+            clip_min=-128 * 2,
+            clip_max=127 * 2,
+        )
+
+        # PASS 30 - SHR
+        shr30_op = ethosu_ops.ethosu_binary_elementwise(
+            ifm=scaled_exp,
+            ifm2=right_shift,
+            lut=lut,
+            operator_type="SHR",
+            ifm_scale=2.0,
+            ifm_zero_point=0,
+            ifm2_scale=0.0,
+            ifm2_zero_point=0,
+            ofm_scale=float(params.ofm.q_params.scale_f32),
+            ofm_zero_point=int(params.ofm.q_params.zero_point),
+            ifm_channels=depth,
+            ifm2_channels=1,
+            reversed_operands=False,
+            rounding_mode="NATURAL",
+            ofm_dtype=ifm_dtype,
+        )
+
+        reshape = relay.reshape(shr30_op, params.ofm.shape)
+        return reshape
diff --git a/python/tvm/relay/backend/contrib/ethosu/te/unary_elementwise.py 
b/python/tvm/relay/backend/contrib/ethosu/te/unary_elementwise.py
index 50bbd36d98..dde3133b56 100644
--- a/python/tvm/relay/backend/contrib/ethosu/te/unary_elementwise.py
+++ b/python/tvm/relay/backend/contrib/ethosu/te/unary_elementwise.py
@@ -94,7 +94,8 @@ def unary_elementwise_compute(
     assert ofm_layout in {"NHWC", "NHCWB16"}
 
     # Changing the ifm and ofm scale to conform with that expected by Vela API
-    ofm_scale = ifm_scale / ofm_scale
+    if ofm_scale != 0:
+        ofm_scale = ifm_scale / ofm_scale
     ifm_scale = 1.0
 
     # Compute operation for the IFM DMA pipeline
diff --git a/python/tvm/relay/backend/contrib/ethosu/tir/passes.py 
b/python/tvm/relay/backend/contrib/ethosu/tir/passes.py
index c721efb471..0f6105277f 100644
--- a/python/tvm/relay/backend/contrib/ethosu/tir/passes.py
+++ b/python/tvm/relay/backend/contrib/ethosu/tir/passes.py
@@ -250,7 +250,9 @@ def DivideConstants(const_dict):
                     # Note by convention the arg after a constant read is the 
length of the read
                     length = int(stmt.args[i + 1])
                     # If it's anything other than a full read, create a new 
buffer
-                    if (offset != 0 or flattened_const_shape != length) and 
not is_u65_conv2d:
+                    if (
+                        offset != 0 or flattened_const_shape != length and 
length > 0
+                    ) and not is_u65_conv2d:
                         out_channels = const.shape[0]
                         offset_channels = int((offset * out_channels) / 
flattened_const_shape)
                         length_channels = int((length * out_channels) / 
flattened_const_shape)
diff --git a/python/tvm/relay/backend/contrib/ethosu/tir_to_cs_translator.py 
b/python/tvm/relay/backend/contrib/ethosu/tir_to_cs_translator.py
index 50268f5f87..e2ebfd0d1c 100644
--- a/python/tvm/relay/backend/contrib/ethosu/tir_to_cs_translator.py
+++ b/python/tvm/relay/backend/contrib/ethosu/tir_to_cs_translator.py
@@ -582,12 +582,16 @@ def _convert_clip_bounds(npu_op: vapi.NpuBlockOperation):
     """
     clip_min_quant = npu_op.activation.min
     clip_max_quant = npu_op.activation.max
-    clip_min_actual = (
-        clip_min_quant - npu_op.ofm.quantization.zero_point
-    ) * npu_op.ofm.quantization.scale_f32
-    clip_max_actual = (
-        clip_max_quant - npu_op.ofm.quantization.zero_point
-    ) * npu_op.ofm.quantization.scale_f32
+    if npu_op.ofm.quantization.scale_f32:
+        clip_min_actual = (
+            clip_min_quant - npu_op.ofm.quantization.zero_point
+        ) * npu_op.ofm.quantization.scale_f32
+        clip_max_actual = (
+            clip_max_quant - npu_op.ofm.quantization.zero_point
+        ) * npu_op.ofm.quantization.scale_f32
+    else:
+        clip_min_actual = clip_min_quant
+        clip_max_actual = clip_max_quant
     npu_op.activation.min = clip_min_actual
     npu_op.activation.max = clip_max_actual
 
diff --git a/python/tvm/relay/op/contrib/ethosu.py 
b/python/tvm/relay/op/contrib/ethosu.py
index 8ec06d3a92..744c15987b 100644
--- a/python/tvm/relay/op/contrib/ethosu.py
+++ b/python/tvm/relay/op/contrib/ethosu.py
@@ -2006,6 +2006,63 @@ def pad_pattern():
     return pattern
 
 
+class SoftMaxParams:
+    """
+    This class will parse a call to a ethos-u.softmax composite function
+    and extract the parameter information.
+    """
+
+    composite_name = "ethos-u.softmax"
+
+    def __init__(self, func_body: Call):
+        from tvm.relay.backend.contrib.ethosu.util import QuantizeArgs
+        from tvm.relay.backend.contrib.ethosu.util import DequantizeArgs
+
+        quantize = func_body
+        softmax_op = quantize.args[0]
+        dequantize = softmax_op.args[0]
+
+        layout = "NHWC"
+
+        self.ifm = TensorParams(
+            dequantize.args[DequantizeArgs.IFM.value],
+            layout,
+            dequantize.args[DequantizeArgs.IFM_SCALE.value],
+            dequantize.args[DequantizeArgs.IFM_ZERO_POINT.value],
+        )
+        self.ofm = TensorParams(
+            quantize,
+            layout,
+            quantize.args[QuantizeArgs.OFM_SCALE.value],
+            quantize.args[QuantizeArgs.OFM_ZERO_POINT.value],
+        )
+
+        self.operator_type = "SOFTMAX"
+
+    def is_valid(self):
+        """Checks whether Softmax has compatible attributes with HW"""
+        tensor_params = [self.ifm, self.ofm]
+        if not check_valid_dtypes(tensor_params, supported_dtypes=[np.int8]):
+            return False
+        if self.ifm.dtype != self.ofm.dtype:
+            return False
+        if not check_dimensions(self.ifm):
+            return False
+        if self.ifm.shape != self.ofm.shape:
+            return False
+        return True
+
+
+def softmax_pattern() -> tvm.relay.dataflow_pattern.DFPattern:
+    """
+    This function creates the pattern for Softmax.
+    """
+    pattern = is_op("qnn.dequantize")(wildcard(), is_constant(), is_constant())
+    pattern = is_op("nn.softmax")(pattern)
+    pattern = is_op("qnn.quantize")(pattern, is_constant(), is_constant())
+    return pattern
+
+
 @register_pattern_table("ethos-u")
 def pattern_table() -> List[Tuple[str, tvm.relay.dataflow_pattern.DFPattern, 
Callable]]:
     return [
@@ -2110,6 +2167,11 @@ def pattern_table() -> List[Tuple[str, 
tvm.relay.dataflow_pattern.DFPattern, Cal
             sum_pattern(),
             lambda pat: SumParams(pat).is_valid(),
         ),
+        (
+            SoftMaxParams.composite_name,
+            softmax_pattern(),
+            lambda pat: SoftMaxParams(pat).is_valid(),
+        ),
         (
             LeakyReLUParams.composite_name,
             leaky_relu_pattern(),
diff --git a/tests/python/contrib/test_ethosu/test_codegen.py 
b/tests/python/contrib/test_ethosu/test_codegen.py
index 14441d8e93..86f64d6483 100644
--- a/tests/python/contrib/test_ethosu/test_codegen.py
+++ b/tests/python/contrib/test_ethosu/test_codegen.py
@@ -315,6 +315,24 @@ def test_ethosu_pooling(
     infra.compare_tvm_with_tflite(pooling, [ifm_shape], accel_type)
 
 
[email protected](
+    "accel_type",
+    ["ethos-u55-256", "ethos-u65-256"],
+)
[email protected]("ifm_shape", [[1, 148, 29], [4, 148, 29], [1, 12], 
[8, 12]])
+def test_ethosu_softmax(
+    accel_type,
+    ifm_shape,
+):
+    np.random.seed(0)
+
+    @tf.function
+    def softmax(x):
+        return tf.nn.softmax(x)
+
+    infra.compare_tvm_with_tflite(softmax, [ifm_shape], accel_type)
+
+
 @pytest.mark.parametrize("accel_type", ACCEL_TYPES)
 @pytest.mark.parametrize("operator_type", ["ADD", "SUB", "MUL", "MIN", "MAX"])
 @pytest.mark.parametrize(
diff --git a/tests/python/contrib/test_ethosu/test_legalize.py 
b/tests/python/contrib/test_ethosu/test_legalize.py
index 6330930fa5..d1d0befcee 100644
--- a/tests/python/contrib/test_ethosu/test_legalize.py
+++ b/tests/python/contrib/test_ethosu/test_legalize.py
@@ -3076,5 +3076,125 @@ def test_tflite_hard_swish(ifm_shape):
     assert tuple(func_body.args[1].checked_type.shape) == (256,)
 
 
[email protected]("ifm_shape", [(1, 12), (1, 12, 32)])
+def test_tflite_softmax(ifm_shape):
+    dtype = "int8"
+
+    def create_tflite_graph():
+        @tf.function
+        def softmax(x):
+            return tf.nn.softmax(x)
+
+        concrete_func = softmax.get_concrete_function(tf.TensorSpec(ifm_shape, 
dtype=tf.float32))
+        # Convert the model
+        def representative_dataset():
+            for _ in range(100):
+                data = np.random.rand(*tuple(ifm_shape))
+                yield [data.astype(np.float32)]
+
+        converter = 
tf.lite.TFLiteConverter.from_concrete_functions([concrete_func])
+        converter.optimizations = [tf.lite.Optimize.DEFAULT]
+        converter.representative_dataset = representative_dataset
+        converter.target_spec.supported_ops = 
[tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
+        converter.inference_input_type = tf.int8
+        converter.inference_output_type = tf.int8
+        tflite_model = converter.convert()
+        return tflite_model
+
+    def verify(ext_func):
+        out_op = ext_func.body
+        ops = []
+        # List of expected operations and their type if it exists
+        expected_ops = [
+            ("reshape", None),
+            ("reshape", None),
+            ("contrib.ethosu.pooling", "MAX"),
+            ("contrib.ethosu.binary_elementwise", "SUB"),
+            ("contrib.ethosu.binary_elementwise", "SHR"),
+            ("contrib.ethosu.pooling", "SUM"),
+            ("contrib.ethosu.unary_elementwise", "CLZ"),
+            ("contrib.ethosu.binary_elementwise", "SUB"),
+            ("contrib.ethosu.binary_elementwise", "SHL"),
+            ("contrib.ethosu.binary_elementwise", "SUB"),
+            ("contrib.ethosu.binary_elementwise", "SHL"),
+            ("contrib.ethosu.binary_elementwise", "ADD"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "ADD"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "SUB"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "ADD"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "SUB"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "ADD"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "SUB"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "ADD"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "MUL"),
+            ("contrib.ethosu.binary_elementwise", "SUB"),
+            ("contrib.ethosu.binary_elementwise", "SHR"),
+            ("reshape", None),
+        ]
+
+        def get_op_type(op):
+            if hasattr(op.attrs, "pooling_type"):
+                return op.attrs.pooling_type
+            elif hasattr(op.attrs, "operator_type"):
+                return op.attrs.operator_type
+            return None
+
+        def _visit(stmt):
+            if isinstance(stmt, relay.expr.Call):
+                ops.append(stmt)
+
+        relay.analysis.post_order_visit(out_op, _visit)
+
+        # check IFM
+        ifm = ops[0].args[0].checked_type
+        assert list(ifm.shape) == list(ifm_shape)
+        assert str(ifm.dtype) == dtype
+
+        # check OFM
+        ofm = out_op.checked_type
+        assert list(ofm.shape) == list(ifm_shape)
+        assert ofm.dtype == dtype
+
+        # check operations
+
+        ops = [(op.op.name, get_op_type(op)) for op in ops]
+        assert expected_ops == ops
+
+    softmax_pattern_table = [
+        (
+            ethosu.SoftMaxParams.composite_name,
+            ethosu.softmax_pattern(),
+            lambda pat: ethosu.SoftMaxParams(pat).is_valid(),
+        )
+    ]
+
+    tflite_graph = create_tflite_graph()
+    tflite_model = tflite.Model.Model.GetRootAsModel(tflite_graph, 0)
+
+    mod, params = relay.frontend.from_tflite(
+        tflite_model,
+        shape_dict={"input": ifm_shape},
+        dtype_dict={"input": dtype},
+    )
+    mod["main"] = bind_params_by_name(mod["main"], params)
+    mod = partition_ethosu_by_table(mod, softmax_pattern_table)
+    mod["tvmgen_default_ethos_u_main_0"] = dataflow_pattern.rewrite(
+        legalize.SoftmaxRewriter(), mod["tvmgen_default_ethos_u_main_0"]
+    )
+    mod = relay.transform.InferType()(mod)
+
+    verify(mod["tvmgen_default_ethos_u_main_0"])
+
+
 if __name__ == "__main__":
     tvm.testing.main()

Reply via email to