This is an automated email from the ASF dual-hosted git repository.
lukhut 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 ee26ecf1d5 [microNPU] Add transform matrices and part matcher to
identity op (#11453)
ee26ecf1d5 is described below
commit ee26ecf1d516af3c7693f6cb53901b4a055ef9d4
Author: Nicola Lancellotti <[email protected]>
AuthorDate: Wed Jun 1 15:51:56 2022 +0100
[microNPU] Add transform matrices and part matcher to identity op (#11453)
* [microNPU] Add transform matrices and part matcher to identity op
* Address comments
* Enable cascader in identity tests
* Address comments
---
.../tvm/contrib/ethosu/cascader/device_config.py | 46 +++++++----
.../relay/backend/contrib/ethosu/te/identity.py | 87 ++++++++++++++++++++-
.../cascader/test_ethosu_identity_matcher.py | 58 ++++++++++++++
tests/python/contrib/test_ethosu/test_codegen.py | 89 ++++++++++++----------
4 files changed, 223 insertions(+), 57 deletions(-)
diff --git a/python/tvm/contrib/ethosu/cascader/device_config.py
b/python/tvm/contrib/ethosu/cascader/device_config.py
index 27aa8b8c78..f654a2598b 100644
--- a/python/tvm/contrib/ethosu/cascader/device_config.py
+++ b/python/tvm/contrib/ethosu/cascader/device_config.py
@@ -48,9 +48,24 @@ class _Shape:
self.width = int(shape[3])
self.depth = int(shape[2]) * int(shape[4])
else:
- self.height = int(shape[1])
- self.width = int(shape[2])
- self.depth = int(shape[3])
+ # identity layout is NHWC but the shape is not always 4
+ length = len(shape)
+ if length == 4:
+ self.height = int(shape[1])
+ self.width = int(shape[2])
+ self.depth = int(shape[3])
+ elif length == 3:
+ self.height = int(shape[0])
+ self.width = int(shape[1])
+ self.depth = int(shape[2])
+ elif length == 2:
+ self.height = int(shape[0])
+ self.width = int(shape[1])
+ self.depth = 1
+ elif length == 1:
+ self.height = int(shape[0])
+ self.width = 1
+ self.depth = 1
def round_up(self, other: "_Shape"):
self.height = _round_up(self.height, other.height)
@@ -627,18 +642,19 @@ class EthosuDeviceConfig:
stride_w = int(op_attrs.get("stride_w", 1))
transform = ifm_propagator.transform
- if input_layout == "NHCWB16":
- transform[1][-1] = min(transform[1][-1], self._subkernel_limits[0]
- stride_h)
- transform[3][-1] = min(transform[3][-1], self._subkernel_limits[1]
- stride_w)
- else:
- transform[1][-1] = min(transform[1][-1], self._subkernel_limits[0]
- stride_h)
- transform[2][-1] = min(transform[2][-1], self._subkernel_limits[1]
- stride_w)
-
- if op_type in ("ethosu_pooling", "ethosu_depthwise_conv2d"):
- if output_layout == "NHCWB16" and input_layout == "NHWC":
- transform[3][-1] = depth
- elif output_layout == "NHCWB16" and input_layout == "NHCWB16":
- transform[2][-1] = 1 + ((depth - 1) // 16)
+ if op_type != "ethosu_identity":
+ if input_layout == "NHCWB16":
+ transform[1][-1] = min(transform[1][-1],
self._subkernel_limits[0] - stride_h)
+ transform[3][-1] = min(transform[3][-1],
self._subkernel_limits[1] - stride_w)
+ else:
+ transform[1][-1] = min(transform[1][-1],
self._subkernel_limits[0] - stride_h)
+ transform[2][-1] = min(transform[2][-1],
self._subkernel_limits[1] - stride_w)
+
+ if op_type in ("ethosu_pooling", "ethosu_depthwise_conv2d"):
+ if output_layout == "NHCWB16" and input_layout == "NHWC":
+ transform[3][-1] = depth
+ elif output_layout == "NHCWB16" and input_layout == "NHCWB16":
+ transform[2][-1] = 1 + ((depth - 1) // 16)
return Propagator(transform, ifm_propagator.offset)
diff --git a/python/tvm/relay/backend/contrib/ethosu/te/identity.py
b/python/tvm/relay/backend/contrib/ethosu/te/identity.py
index 271ca1542f..0b61e0c28b 100644
--- a/python/tvm/relay/backend/contrib/ethosu/te/identity.py
+++ b/python/tvm/relay/backend/contrib/ethosu/te/identity.py
@@ -16,7 +16,10 @@
# under the License.
# pylint: disable=invalid-name,unused-argument
"""Tensor Expression for identity"""
+import numpy as np
from tvm import te
+from tvm.contrib.ethosu.cascader import TESubgraph, EthosuPart, Propagator,
register_matcher
+
from .dma import read_compute, write_compute
@@ -56,7 +59,6 @@ def identity_compute(
-------
te.Tensor
The Output Feature Map tensor.
-
"""
dmaed_ifm = read_compute(ifm, ifm_zero_point, ifm_scale)
id_attrs = {"op": "ethosu_identity", "activation": activation}
@@ -76,7 +78,86 @@ def identity_compute(
name="ethosu_identity",
attrs=id_attrs,
)
+ length = len(ifm.shape)
+ ifm_matrix = np.identity(length + 1)
+ offset = np.zeros(length, dtype="int64")
+ ifm_propagator = Propagator(
+ ifm_matrix,
+ offset.tolist(),
+ )
+ propagator_attrs = {
+ "ifm_propagator": ifm_propagator,
+ }
+ return write_compute(identity, ofm_zero_point, ofm_scale,
attrs=propagator_attrs)
+
+
+@register_matcher
+def match_ethosu_identity(output_tensor, device_config):
+ """Match a Tensor Expression corresponding to an NPU identity.
- dmaed_ofm = write_compute(identity, ofm_zero_point, ofm_scale)
+ If the Tensor Expression matches, an EthosuPart will be created that
models the
+ matched Tensor Expression. Otherwise, None will be returned.
- return dmaed_ofm
+ Parameters
+ ----------
+ output_tensor : tvm.te.Tensor
+ The tensor to attempt to match with.
+ device_config : EthosuDeviceConfig
+ Target device configuration
+
+ Returns
+ -------
+ Union[None, EthosuPart]
+ The created EthosuPart if there was a match, otherwise None.
+ """
+ write = output_tensor
+ if write.op.name != "ethosu_write":
+ return None
+ identity = write.op.input_tensors[0]
+ if identity.op.name != "ethosu_identity":
+ return None
+ read = identity.op.input_tensors[0]
+ if read.op.name != "ethosu_read":
+ return None
+
+ input_tensors = [
+ read.op.input_tensors[0],
+ ]
+ subgraph = TESubgraph(input_tensors, output_tensor)
+ propagators = [
+ write.op.attrs["ifm_propagator"],
+ ]
+ ifm_dtype = input_tensors[0].dtype
+ ofm_dtype = output_tensor.dtype
+
+ input_tensors_shape = input_tensors[0].shape
+ length = len(input_tensors_shape)
+ assert length <= 4
+ channels = int(input_tensors_shape[length - 1]) if length >= 3 else 1
+
+ subkernels = len(device_config.get_kernel_steps(identity.op.name, 1, 1,
ifm_dtype))
+
+ input_layout = output_layout = "NHWC"
+ output_quantum = device_config.get_output_quantum(output_layout)
+
+ valid_block_configs = device_config.get_valid_block_configs(
+ propagators[0],
+ identity.op.attrs,
+ output_tensor.shape,
+ channels,
+ channels,
+ output_layout,
+ input_layout,
+ ifm_dtype,
+ ofm_dtype,
+ 1,
+ 1,
+ )
+
+ return EthosuPart(
+ subgraph,
+ propagators,
+ output_quantum,
+ subkernels,
+ valid_block_configs,
+ )
diff --git
a/tests/python/contrib/test_ethosu/cascader/test_ethosu_identity_matcher.py
b/tests/python/contrib/test_ethosu/cascader/test_ethosu_identity_matcher.py
new file mode 100644
index 0000000000..4609a5bc37
--- /dev/null
+++ b/tests/python/contrib/test_ethosu/cascader/test_ethosu_identity_matcher.py
@@ -0,0 +1,58 @@
+# 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.
+import pytest
+
+pytest.importorskip("ethosu.vela")
+
+import numpy as np
+
+from tvm import te
+import tvm.contrib.ethosu.cascader as cs
+from tvm.relay.backend.contrib.ethosu.te.identity import
match_ethosu_identity, identity_compute
+from .infra import make_matrices
+
+
+def test_ethosu_identity_matcher():
+ ofm_channels = 21
+ ifm_shape = (1, 12, 15, ofm_channels)
+ ifm = te.placeholder(ifm_shape, dtype="int8")
+ lut = te.placeholder((), dtype="uint8")
+ out = identity_compute(
+ ifm=ifm,
+ lut=lut,
+ ifm_scale=1,
+ ifm_zero_point=0,
+ ofm_scale=1,
+ ofm_zero_point=0,
+ activation="NONE",
+ )
+
+ length = len(ifm.shape)
+ ifm_transform = np.identity(length + 1).tolist()
+ ifm_offset = np.zeros(length, dtype="int64").tolist()
+
+ device_config = cs.EthosuDeviceConfig("ethos-u55-256")
+ part = match_ethosu_identity(out, device_config)
+
+ assert isinstance(part, cs.EthosuPart)
+ assert len(part.propagators) == 1
+ assert part.propagators[0].transform == ifm_transform
+ assert part.propagators[0].offset == ifm_offset
+
+
+if __name__ == "__main__":
+ pytest.main([__file__])
diff --git a/tests/python/contrib/test_ethosu/test_codegen.py
b/tests/python/contrib/test_ethosu/test_codegen.py
index ce617d14fa..b6b78c3357 100644
--- a/tests/python/contrib/test_ethosu/test_codegen.py
+++ b/tests/python/contrib/test_ethosu/test_codegen.py
@@ -37,6 +37,10 @@ from . import infra
ACCEL_TYPES = ["ethos-u55-256", "ethos-u55-128", "ethos-u55-64",
"ethos-u55-32", "ethos-u65-256"]
+def is_u55_accel_type(accel_type):
+ return "u55" in accel_type
+
+
@pytest.mark.parametrize("accel_type", ACCEL_TYPES + ["ethos-u65-512"])
@pytest.mark.parametrize("ifm_shape", [(1, 299, 299, 2), (1, 55, 55, 3)])
@pytest.mark.parametrize("kernel_shape", [(3, 2), (1, 3)])
@@ -270,9 +274,7 @@ def test_ethosu_binary_elementwise(
shapes=[ifm_shape, ifm2_shape],
ranges=[(0, 1), (0, 2)],
accel_type=accel_type,
- # non 4D ops legalize into identity op that is not currently supported
in the cascader
- enable_cascader=(len(ifm_shape) == 4 and len(ifm2_shape) == 4)
- and ("u65" not in accel_type),
+ enable_cascader=is_u55_accel_type(accel_type),
)
@@ -301,8 +303,7 @@ def test_binary_add_with_non_4d_shapes(
shapes=[ifm_shape, ifm2_shape],
ranges=[(0, 1), (0, 2)],
accel_type=accel_type,
- # non 4D ops legalize into identity op that is not currently supported
in the cascader
- enable_cascader=False,
+ enable_cascader=is_u55_accel_type(accel_type),
)
@@ -567,13 +568,12 @@ def test_ethosu_identity_codegen(ifm_shape, ifm_scale,
ifm_zp, ofm_scale, ofm_zp
ethosu_mod = infra.create_ethosu_partition(cpu_mod)
infra.compare_ethosu_with_reference(
- # identity op is not supported in cascader
ethosu_mod,
input_data,
output_data,
accel_type,
output_tolerance=1,
- enable_cascader=False,
+ enable_cascader=is_u55_accel_type(accel_type),
)
@@ -603,9 +603,12 @@ def test_relay_reshape_codegen(ifm_shape, new_shape,
accel_type):
output_data = generate_ref_data(cpu_mod, input_data)
ethosu_mod = infra.create_ethosu_partition(cpu_mod)
- # reshape ops legalize into identity op that is not currently supported in
the cascader
infra.compare_ethosu_with_reference(
- ethosu_mod, input_data, output_data, accel_type, enable_cascader=False
+ ethosu_mod,
+ input_data,
+ output_data,
+ accel_type,
+ enable_cascader=is_u55_accel_type(accel_type),
)
@@ -626,8 +629,9 @@ def test_tflite_slice(accel_type, ifm_shape, begin, size):
def slice_func(x):
return tf.slice(x, begin, size)
- # Ops that get legalized to identity is currently not supported by the
cascader
- infra.compare_tvm_with_tflite(slice_func, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ slice_func, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
@pytest.mark.parametrize("accel_type", ACCEL_TYPES)
@@ -642,9 +646,8 @@ def test_tflite_strided_slice(accel_type, ifm_shape, begin,
end):
def strided_slice_func(x):
return tf.strided_slice(x, begin, end)
- # Ops that get legalized to identity are currently not supported by the
cascader
infra.compare_tvm_with_tflite(
- strided_slice_func, [ifm_shape], accel_type, enable_cascader=False
+ strided_slice_func, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
)
@@ -667,12 +670,11 @@ def test_ethosu_unary_elementwise(
op = tf.math.abs(x)
return op
- # non-4D tensors are legalized to identity which are not supported by the
cascader
infra.compare_tvm_with_tflite(
abs_func,
[ifm_shape],
accel_type,
- enable_cascader=(len(ifm_shape) == 4) and ("u65" not in accel_type),
+ enable_cascader=is_u55_accel_type(accel_type),
)
@@ -752,8 +754,9 @@ def test_tflite_tanh(accel_type):
op = tf.nn.tanh(x)
return op
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(tanh_func, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ tanh_func, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
@pytest.mark.parametrize("accel_type", ACCEL_TYPES)
@@ -774,7 +777,6 @@ def test_tflite_concat(shapes, axis, accel_type):
op = tf.concat(list(inputs), axis)
return op
- # Ops that get legalized to identity are currently not supported by the
cascader
infra.compare_tvm_with_tflite(concat_func, shapes, accel_type,
enable_cascader=False)
@@ -788,8 +790,9 @@ def test_tflite_sigmoid(accel_type):
op = tf.nn.sigmoid(x)
return op
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(sigmoid_function, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ sigmoid_function, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
# This codegen test checks both, split and split_v
@@ -813,7 +816,6 @@ def test_tflite_split(accel_type, ifm_shape,
num_or_size_splits, axis):
op = tf.split(x, num_or_size_splits, axis=axis)
return op
- # Ops that get legalized to identity are currently not supported by the
cascader
infra.compare_tvm_with_tflite(split_func, [ifm_shape], accel_type,
enable_cascader=False)
@@ -845,9 +847,12 @@ def test_ethosu_requantize(accel_type, ifm_shape,
ifm_scale, ifm_zp, ofm_scale,
output_data = generate_ref_data(cpu_mod, input_data)
ethosu_mod = partition_for_ethosu(cpu_mod)
- # Ops that get legalized to identity are currently not supported by the
cascader
infra.compare_ethosu_with_reference(
- ethosu_mod, input_data, output_data, accel_type, enable_cascader=False
+ ethosu_mod,
+ input_data,
+ output_data,
+ accel_type,
+ enable_cascader=is_u55_accel_type(accel_type),
)
@@ -860,8 +865,9 @@ def test_tflite_expand_dims(accel_type, ifm_shape, axis):
def expand_dims_func(x):
return tf.expand_dims(x, axis=axis)
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(expand_dims_func, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ expand_dims_func, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
@pytest.mark.parametrize("accel_type", ACCEL_TYPES)
@@ -875,8 +881,9 @@ def test_tflite_squeeze(accel_type, ifm_shape, axis):
def squeeze_func(x):
return tf.squeeze(x, axis=axis)
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(squeeze_func, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ squeeze_func, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
@pytest.mark.parametrize("accel_type", ACCEL_TYPES)
@@ -894,8 +901,9 @@ def test_tflite_resize2d_nearest_neighbor(accel_type,
ifm_shape, size):
x, size, align_corners=align_corners, half_pixel_centers=False
)
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(resize_model, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ resize_model, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
@pytest.mark.parametrize("accel_type", ACCEL_TYPES)
@@ -918,8 +926,9 @@ def test_tflite_resize2d_bilinear(accel_type, ifm_shape,
size, align_corners):
x, size, align_corners=align_corners, half_pixel_centers=False
)
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(resize_model, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ resize_model, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
@pytest.mark.parametrize("accel_type", ACCEL_TYPES)
@@ -959,9 +968,11 @@ def test_tflite_transpose_convolution(
op = tf.nn.bias_add(op, bias)
return op
- # Ops that get legalized to identity are currently not supported by the
cascader
infra.compare_tvm_with_tflite(
- conv2d_transpose, [ifm_shape], accel_type=accel_type,
enable_cascader=False
+ conv2d_transpose,
+ [ifm_shape],
+ accel_type=accel_type,
+ enable_cascader=is_u55_accel_type(accel_type),
)
@@ -982,7 +993,6 @@ def test_tflite_pack(accel_type, ifm_shapes, axis):
def pack_func(*inputs):
return tf.stack(inputs, axis=axis)
- # Ops that get legalized to identity are currently not supported by the
cascader
infra.compare_tvm_with_tflite(pack_func, ifm_shapes, accel_type,
enable_cascader=False)
@@ -998,7 +1008,6 @@ def test_tflite_unpack(accel_type, ifm_shape, axis):
def unpack_func(x):
return tf.unstack(x, axis=axis)
- # Ops that get legalized to identity are currently not supported by the
cascader
infra.compare_tvm_with_tflite(unpack_func, [ifm_shape], accel_type,
enable_cascader=False)
@@ -1012,8 +1021,9 @@ def test_tflite_leaky_relu(accel_type, ifm_shape, alpha):
def leaky_relu_func(x):
return tf.nn.leaky_relu(x, alpha=alpha)
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(leaky_relu_func, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ leaky_relu_func, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
@pytest.mark.parametrize("accel_type", ACCEL_TYPES)
@@ -1045,8 +1055,9 @@ def test_tflite_fully_connected(
x = tf.nn.relu(x)
return x
- # Ops that get legalized to identity are currently not supported by the
cascader
- infra.compare_tvm_with_tflite(fully_connected, [ifm_shape], accel_type,
enable_cascader=False)
+ infra.compare_tvm_with_tflite(
+ fully_connected, [ifm_shape], accel_type,
enable_cascader=is_u55_accel_type(accel_type)
+ )
if __name__ == "__main__":