diff --git a/.ci/scripts/setup-samsung-linux-deps.sh b/.ci/scripts/setup-samsung-linux-deps.sh index 7e6024c584e..ed704b2bfbd 100644 --- a/.ci/scripts/setup-samsung-linux-deps.sh +++ b/.ci/scripts/setup-samsung-linux-deps.sh @@ -54,15 +54,6 @@ install_enn_backend() { rm -rf "${NDK_INSTALLATION_DIR}" && sudo mkdir -p "${NDK_INSTALLATION_DIR}" ANDROID_NDK_VERSION=r27b - pushd . - cd /tmp - curl -Os --retry 3 "https://ossci-android.s3.amazonaws.com/android-ndk-${ANDROID_NDK_VERSION}-linux.zip" - unzip -qo "android-ndk-${ANDROID_NDK_VERSION}-linux.zip" - - # Print the content for manual verification - ls -lah "android-ndk-${ANDROID_NDK_VERSION}" - sudo mv "android-ndk-${ANDROID_NDK_VERSION}"/* "${NDK_INSTALLATION_DIR}" - popd # build Exynos backend export ANDROID_NDK_ROOT=${ANDROID_NDK_ROOT:-/opt/ndk} bash backends/samsung/build.sh --build all diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index 7cb22d90f60..6e9169132e5 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -874,7 +874,7 @@ jobs: contents: read with: runner: linux.2xlarge - docker-image: ci-image:executorch-ubuntu-22.04-gcc9 + docker-image: ci-image:executorch-ubuntu-22.04-clang12-android submodules: 'recursive' ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} timeout: 90 @@ -892,7 +892,7 @@ jobs: source .ci/scripts/setup-samsung-linux-deps.sh # Test models serially - models="mv2 ic3 resnet18 resnet50" + models="mv2 ic3 resnet18 resnet50 mv3 ic4 dl3 edsr vit w2l" for model in $models; do python -m executorch.examples.samsung.aot_compiler --model_name=$model -c E9955 done diff --git a/backends/samsung/_passes/conv1d_to_conv2d.py b/backends/samsung/_passes/conv1d_to_conv2d.py new file mode 100644 index 00000000000..57f1074b348 --- /dev/null +++ b/backends/samsung/_passes/conv1d_to_conv2d.py @@ -0,0 +1,88 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import torch +from executorch.exir import ExportedProgram +from executorch.exir.dialects._ops import ops as exir_ops +from executorch.exir.pass_base import ExportPass, PassResult +from torch._export.utils import get_param + + +class Conv1dToConv2d(ExportPass): + + def __init__(self, edge_program: ExportedProgram): + super().__init__() + self.edge_program = edge_program + + def call(self, graph_module: torch.fx.GraphModule): + graph = graph_module.graph + node_list = list(graph.nodes) + for node in node_list: + if node.op == "call_function": + if node.target == exir_ops.edge.aten.convolution.default: + stride = list(node.args[3]) + if len(stride) != 1: + continue + + # convert 3dim weight to 4dim + weight_node = node.args[1] + weight_3dim = get_param(self.edge_program, weight_node) + weight_4dim = torch.nn.Parameter( + data=weight_3dim.data.contiguous().unsqueeze(dim=-1), + requires_grad=False, + ) + parameter_name = ( + self.edge_program.graph_signature.inputs_to_parameters[ + weight_node.name + ] + ) + self.edge_program.state_dict[parameter_name] = weight_4dim + weight_node.meta["val"] = weight_node.meta["val"].data.unsqueeze( + dim=-1 + ) + + # Extend stride, padding, and dilation + node.args = ( + node.args[0], + node.args[1], + node.args[2], + node.args[3] + [1], # stride + node.args[4] + [0], # padding + node.args[5] + [1], # dilation + node.args[6], + node.args[7], + node.args[8], + ) + + # unsqueeze -> conv2d -> squeeze + with graph.inserting_before(node): + input_node = node.args[0] + unsqueeze_before = graph.create_node( + "call_function", exir_ops.edge.aten.unsqueeze_copy.default + ) + unsqueeze_before.args = ( + input_node, + -1, + ) + node.replace_input_with(input_node, unsqueeze_before) + + with graph.inserting_after(node): + squeeze_after = graph.create_node( + "call_function", exir_ops.edge.aten.squeeze_copy.dims + ) + squeeze_after.args = ( + node, + [-1], + ) + original_users = [ + user for user in node.users if user != squeeze_after + ] + for user in original_users: + user.replace_input_with(node, squeeze_after) + + graph_module.recompile() + graph_module = super().call(graph_module).graph_module + return PassResult(graph_module, True) diff --git a/backends/samsung/_passes/customized_constant_prop.py b/backends/samsung/_passes/customized_constant_prop.py new file mode 100644 index 00000000000..fa5bad3a056 --- /dev/null +++ b/backends/samsung/_passes/customized_constant_prop.py @@ -0,0 +1,40 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import executorch.exir.passes.constant_prop_pass as constant_prop_module +from executorch.exir import ExportedProgram +from executorch.exir.pass_base import ExportPass, PassResult +from executorch.exir.passes.constant_prop_pass import constant_prop_pass +from torch.fx import GraphModule + + +class _constant_prop_context: + def __init__(self): + self.backup = constant_prop_module._DEFAULT_SKIP_TARGETS + + def __enter__(self): + constant_prop_module._DEFAULT_SKIP_TARGETS = ( + constant_prop_module._DEFAULT_SKIP_TARGETS_NO_QUANT + ) + + def __exit__(self, exc_type, exc_val, exc_tb): + constant_prop_module._DEFAULT_SKIP_TARGETS = self.backup + + +class ConstantPropPass(ExportPass): + """ + Official constant_prop_pass will not fold Q-DQ + But we need to fold quantized constant tensor as well as non-quantized one + """ + + def __init__(self, edge_program: ExportedProgram): + super().__init__() + self.edge_program = edge_program + + def call(self, graph_module: GraphModule): + with _constant_prop_context(): + _ = constant_prop_pass(self.edge_program) + return PassResult(graph_module, True) diff --git a/backends/samsung/_passes/replace_scalar_ops.py b/backends/samsung/_passes/replace_scalar_ops.py new file mode 100644 index 00000000000..8ae54b0dc98 --- /dev/null +++ b/backends/samsung/_passes/replace_scalar_ops.py @@ -0,0 +1,46 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict, Tuple + +import torch +from executorch.exir.dialects._ops import ops as exir_ops +from executorch.exir.pass_base import ExportPass +from torch._export.pass_base import Argument +from torch._export.pass_infra.node_metadata import NodeMetadata +from torch._export.pass_infra.proxy_value import ProxyValue + + +class ReplaceOpsWithScalar(ExportPass): + # Replace binary ops with scalar into binary ops with tensor. + # Ops list below. + _ops_with_scalar = { + exir_ops.edge.aten.add.Scalar: exir_ops.edge.aten.add.Tensor, + exir_ops.edge.aten.sub.Scalar: exir_ops.edge.aten.sub.Tensor, + exir_ops.edge.aten.div.Scalar: exir_ops.edge.aten.div.Tensor, + exir_ops.edge.aten.mul.Scalar: exir_ops.edge.aten.mul.Tensor, + exir_ops.edge.aten.pow.Tensor_Scalar: exir_ops.edge.aten.pow.Tensor_Tensor, + } + + def __init__(self): + super(ReplaceOpsWithScalar, self).__init__() + + def call_operator( + self, + op, + args: Tuple[Argument, ...], + kwargs: Dict[str, Argument], + meta: NodeMetadata, + ) -> ProxyValue: + if op not in self._ops_with_scalar: + return super().call_operator(op, args, kwargs, meta) + + return super().call_operator( + op=self._ops_with_scalar.get(op, op), + args=(args[0], torch.tensor(args[1])), + kwargs=kwargs, + meta=meta, + ) diff --git a/backends/samsung/builders/__init__.py b/backends/samsung/builders/__init__.py index b3e72da36c3..02a457fd06e 100644 --- a/backends/samsung/builders/__init__.py +++ b/backends/samsung/builders/__init__.py @@ -9,20 +9,42 @@ op_add, op_avg_pool2d, op_batch_norm, + op_bmm, op_cat, op_clamp, + op_constant_pad_nd, op_conv2d, + op_div, + op_embedding, + op_expand_copy, + op_gelu, op_getitem, + op_hardswish, op_hardtanh, + op_layer_norm, + op_leaky_relu, op_linear, + op_log_softmax, op_max_pool2d, + op_maximum, op_mean_dim, + op_minimum, op_mul, op_permute, + op_pixel_shuffle, op_relu, op_reshape, + op_rsqrt, op_select, + op_slice_copy, + op_softmax, + op_sqrt, + op_squeeze, + op_sub, + op_to_copy, op_unsqueeze, + op_upsample_bilinear2d, + op_upsample_nearest2d, ) __all__ = [ @@ -30,18 +52,40 @@ op_add, op_avg_pool2d, op_batch_norm, + op_bmm, op_cat, op_clamp, op_conv2d, + op_constant_pad_nd, + op_div, + op_embedding, + op_expand_copy, + op_gelu, op_getitem, + op_hardswish, op_hardtanh, + op_layer_norm, + op_leaky_relu, op_linear, + op_log_softmax, op_max_pool2d, + op_maximum, op_mean_dim, + op_minimum, op_mul, op_permute, + op_pixel_shuffle, op_relu, op_reshape, + op_rsqrt, op_select, + op_slice_copy, + op_softmax, + op_sqrt, + op_squeeze, + op_sub, + op_to_copy, op_unsqueeze, + op_upsample_bilinear2d, + op_upsample_nearest2d, ] diff --git a/backends/samsung/builders/op_bmm.py b/backends/samsung/builders/op_bmm.py new file mode 100644 index 00000000000..6ba8864ebb3 --- /dev/null +++ b/backends/samsung/builders/op_bmm.py @@ -0,0 +1,40 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class BMMVisitor(NodeVisitor): + target = "aten.bmm.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input1 = node.args[0] + input_id_1 = self.define_tensor(input1, enn_graph, vals_to_ids) + input2 = node.args[1] + input_id_2 = self.define_tensor(input2, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op( + node.name, "BATCH_MATMUL", [input_id_1, input_id_2], [output_id] + ) diff --git a/backends/samsung/builders/op_constant_pad_nd.py b/backends/samsung/builders/op_constant_pad_nd.py new file mode 100644 index 00000000000..cc7cdc5751b --- /dev/null +++ b/backends/samsung/builders/op_constant_pad_nd.py @@ -0,0 +1,56 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict, List + +import numpy as np + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph +from executorch.backends.transforms import get_shape + + +@register_node_visitor +class ConstantPadNDVisitor(NodeVisitor): + target = "aten.constant_pad_nd.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + # torch padding order starts from the last axis, change the order to fit samsung lite-core + paddings = np.reshape(cast(List[int], node.args[1]), (-1, 2))[::-1].astype( + np.uint32 + ) + in_shape = get_shape(input) + paddings = paddings.reshape(-1).tolist() + paddings = [0] * (2 * len(in_shape) - len(paddings)) + paddings + paddings = paddings[::2] + paddings[1::2] + + padding_value = node.args[2] + assert padding_value == 0.0, "Only Support pad constant 0 now." + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + params = { + "explicit_padding": paddings, + "padding": "EXPLICIT", + "padding_type": "CONSTANT", + } + + enn_graph.define_op(node.name, "PAD", [input_id], [output_id], params) diff --git a/backends/samsung/builders/op_div.py b/backends/samsung/builders/op_div.py new file mode 100644 index 00000000000..89d773ddb0e --- /dev/null +++ b/backends/samsung/builders/op_div.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class DivVisitor(NodeVisitor): + target = "aten.div.Tensor" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + # inputs + input1 = node.args[0] + input_id_1 = self.define_tensor(input1, enn_graph, vals_to_ids) + input2 = node.args[1] + input_id_2 = self.define_tensor(input2, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "ELTDIV", [input_id_1, input_id_2], [output_id]) diff --git a/backends/samsung/builders/op_embedding.py b/backends/samsung/builders/op_embedding.py new file mode 100644 index 00000000000..f37c46a56d6 --- /dev/null +++ b/backends/samsung/builders/op_embedding.py @@ -0,0 +1,41 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class EmbeddingVisitor(NodeVisitor): + target = "aten.embedding.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + weight_node = node.args[0] + weight_id = self.define_tensor(weight_node, enn_graph, vals_to_ids) + + input = node.args[1] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + params = {"axis": 0, "input_type": "indices"} + enn_graph.define_op( + node.name, "GATHER", [input_id, weight_id], [output_id], params + ) diff --git a/backends/samsung/builders/op_expand_copy.py b/backends/samsung/builders/op_expand_copy.py new file mode 100644 index 00000000000..f4c707b8e62 --- /dev/null +++ b/backends/samsung/builders/op_expand_copy.py @@ -0,0 +1,77 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict, List + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph +from executorch.backends.transforms import get_shape + + +@register_node_visitor +class ExpandVisitor(NodeVisitor): + target = "aten.expand_copy.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ): + # inputs + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + in_shape = get_shape(input) + sizes = cast(List[int], node.args[1]) + expand_dims = self.check_expand_dims(sizes, in_shape) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + if len(expand_dims) == 0: + params = {"new_shape": [*node.meta["val"].shape]} + enn_graph.define_op(node.name, "RESHAPE", [input_id], [output_id], params) + elif len(expand_dims) == 1: + expand_dim = expand_dims[0] + params = {"axis": expand_dim} + enn_graph.define_op( + node.name, + "CONCAT", + [input_id] * sizes[expand_dim], + [output_id], + params, + ) + else: + raise NotImplementedError("Don't support expanding at more than one axes.") + + def check_expand_dims(self, sizes, in_shape): + expand_dims = [] + new_size_index = len(sizes) + in_shape_index = len(in_shape) + + while in_shape_index > 0 and new_size_index > 0: + in_shape_index -= 1 + new_size_index -= 1 + if ( + sizes[new_size_index] == -1 + or sizes[new_size_index] == in_shape[in_shape_index] + ): + continue + expand_dims.append(in_shape_index) + + while new_size_index > 0: + new_size_index -= 1 + assert sizes[new_size_index] == 1, "Current expand is unsupported!" + + return expand_dims diff --git a/backends/samsung/builders/op_gelu.py b/backends/samsung/builders/op_gelu.py new file mode 100644 index 00000000000..059a3b77850 --- /dev/null +++ b/backends/samsung/builders/op_gelu.py @@ -0,0 +1,34 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class GeluVisitor(NodeVisitor): + target = "aten.gelu.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input_id = self.define_tensor(node.args[0], enn_graph, vals_to_ids) + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "GELU", [input_id], [output_id]) diff --git a/backends/samsung/builders/op_hardswish.py b/backends/samsung/builders/op_hardswish.py new file mode 100644 index 00000000000..72a99d17b83 --- /dev/null +++ b/backends/samsung/builders/op_hardswish.py @@ -0,0 +1,35 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class HardSwishVisitor(NodeVisitor): + target = "aten.hardswish.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "HARDSWISH", [input_id], [output_id]) diff --git a/backends/samsung/builders/op_layer_norm.py b/backends/samsung/builders/op_layer_norm.py new file mode 100644 index 00000000000..e6f853178d8 --- /dev/null +++ b/backends/samsung/builders/op_layer_norm.py @@ -0,0 +1,54 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + +from executorch.backends.transforms import get_shape + + +@register_node_visitor +class LayerNormVisitor(NodeVisitor): + target = ["aten.layer_norm.default"] + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + all_input_tensors = [] + input_node = node.args[0] + input_id = self.define_tensor(input_node, enn_graph, vals_to_ids) + all_input_tensors.append(input_id) + + normalized_shapes = node.args[1] + assert ( + len(normalized_shapes) == 1 + and normalized_shapes[0] == get_shape(input_node)[-1] + ), "Enn Backend only support norm at last axis." + + weight_node = node.args[2] + weight_id = self.define_tensor(weight_node, enn_graph, vals_to_ids) + all_input_tensors.append(weight_id) + bias_node = node.args[3] + bias_id = self.define_tensor(bias_node, enn_graph, vals_to_ids) + all_input_tensors.append(bias_id) + + epsilon = node.args[4] if len(node.args) > 4 else 1e-5 + params = {"epsilon": epsilon} + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op( + node.name, "LAYERNORM", all_input_tensors, [output_id], params + ) diff --git a/backends/samsung/builders/op_leaky_relu.py b/backends/samsung/builders/op_leaky_relu.py new file mode 100644 index 00000000000..c7ed37d12e5 --- /dev/null +++ b/backends/samsung/builders/op_leaky_relu.py @@ -0,0 +1,58 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.builders.utils import get_map_dtype +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class LeakyReluVisitor(NodeVisitor): + target = ["aten.leaky_relu.default", "aten.prelu.default"] + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + all_input_tensors = [] + input_id = self.define_tensor(node.args[0], enn_graph, vals_to_ids) + all_input_tensors.append(input_id) + + if node.target.__name__ == "aten.prelu.default": + negative_slope = node.args[1] + negative_slope_id = self.define_tensor( + negative_slope, enn_graph, vals_to_ids + ) + else: + negative_slope = cast(float, node.args[1]) if len(node.args) > 1 else 0.01 + negative_slope_tensor = torch.tensor(negative_slope).to(torch.float32) + negative_slope_node_name = node.name + "_slope" + dims = list(negative_slope_tensor.size()) + data_type = get_map_dtype(negative_slope_tensor.dtype) + negative_slope_id = enn_graph.define_tensor( + negative_slope_node_name, + dims, + data_type, + "CONSTANT", + negative_slope_tensor.detach().numpy(), + ) + + all_input_tensors.append(negative_slope_id) + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "PRELU", all_input_tensors, [output_id]) diff --git a/backends/samsung/builders/op_log_softmax.py b/backends/samsung/builders/op_log_softmax.py new file mode 100644 index 00000000000..f2d87601cbb --- /dev/null +++ b/backends/samsung/builders/op_log_softmax.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class LogSoftmaxVisitor(NodeVisitor): + target = "aten._log_softmax.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ): + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + axis = cast(int, node.args[1]) + meta_data = {"axis": axis} + + enn_graph.define_op(node.name, "LOGSOFTMAX", [input_id], [output_id], meta_data) diff --git a/backends/samsung/builders/op_maximum.py b/backends/samsung/builders/op_maximum.py new file mode 100644 index 00000000000..d3358d736f3 --- /dev/null +++ b/backends/samsung/builders/op_maximum.py @@ -0,0 +1,37 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class MaximumVisitor(NodeVisitor): + target = "aten.maximum.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + # inputs + input_id_1 = self.define_tensor(node.args[0], enn_graph, vals_to_ids) + input_id_2 = self.define_tensor(node.args[1], enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "MAXIMUM", [input_id_1, input_id_2], [output_id]) diff --git a/backends/samsung/builders/op_minimum.py b/backends/samsung/builders/op_minimum.py new file mode 100644 index 00000000000..a32b462d45f --- /dev/null +++ b/backends/samsung/builders/op_minimum.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class MinimumVisitor(NodeVisitor): + target = "aten.minimum.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + # inputs + input1 = node.args[0] + input_id_1 = self.define_tensor(input1, enn_graph, vals_to_ids) + input2 = node.args[1] + input_id_2 = self.define_tensor(input2, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "MIN", [input_id_1, input_id_2], [output_id]) diff --git a/backends/samsung/builders/op_pixel_shuffle.py b/backends/samsung/builders/op_pixel_shuffle.py new file mode 100644 index 00000000000..28259299c81 --- /dev/null +++ b/backends/samsung/builders/op_pixel_shuffle.py @@ -0,0 +1,38 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +from typing import cast, Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class PixelShuffleVisitor(NodeVisitor): + target = "aten.pixel_shuffle.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input_id = self.define_tensor(node.args[0], enn_graph, vals_to_ids) + + scale_factor = cast(int, node.args[1]) + params = {"block_size": scale_factor, "mode": "CRD"} + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op( + node.name, "DEPTH_TO_SPACE", [input_id], [output_id], params + ) diff --git a/backends/samsung/builders/op_rsqrt.py b/backends/samsung/builders/op_rsqrt.py new file mode 100644 index 00000000000..b3600d41ee2 --- /dev/null +++ b/backends/samsung/builders/op_rsqrt.py @@ -0,0 +1,35 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class LinearVisitor(NodeVisitor): + target = "aten.rsqrt.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "RSQRT", [input_id], [output_id]) diff --git a/backends/samsung/builders/op_slice_copy.py b/backends/samsung/builders/op_slice_copy.py new file mode 100644 index 00000000000..0d7a23118a0 --- /dev/null +++ b/backends/samsung/builders/op_slice_copy.py @@ -0,0 +1,59 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph +from executorch.backends.transforms import get_shape + + +@register_node_visitor +class SliceCopyVisitor(NodeVisitor): + target = "aten.slice_copy.Tensor" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ): + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + in_shape = get_shape(input) + dim = cast(int, node.args[1]) + if dim < 0: + dim = dim + len(in_shape) + start_val = cast(int, node.args[2]) + if start_val < 0: + start_val = start_val + in_shape[dim] + end_val = min(cast(int, node.args[3]), in_shape[dim]) + if end_val < 0: + end_val = end_val + in_shape[dim] + + step = cast(int, node.args[4]) if len(node.args) > 4 else 1 + + begin = [0] * len(in_shape) + begin[dim] = start_val + end = in_shape + end[dim] = end_val + strides = [1] * len(in_shape) + strides[dim] = step + + params = {"begin": begin, "end": end, "strides": strides} + + enn_graph.define_op(node.name, "STRIDEDSLICE", [input_id], [output_id], params) diff --git a/backends/samsung/builders/op_softmax.py b/backends/samsung/builders/op_softmax.py new file mode 100644 index 00000000000..1e2e4a378dc --- /dev/null +++ b/backends/samsung/builders/op_softmax.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class SoftmaxVisitor(NodeVisitor): + target = ["aten._softmax.default", "aten._safe_softmax.default"] + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ): + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + axis = cast(int, node.args[1]) + params = {"axis": axis} + + enn_graph.define_op(node.name, "SOFTMAX", [input_id], [output_id], params) diff --git a/backends/samsung/builders/op_sqrt.py b/backends/samsung/builders/op_sqrt.py new file mode 100644 index 00000000000..3560542a0bc --- /dev/null +++ b/backends/samsung/builders/op_sqrt.py @@ -0,0 +1,35 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class SqrtVisitor(NodeVisitor): + target = "aten.sqrt.default" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "SQRT", [input_id], [output_id]) diff --git a/backends/samsung/builders/op_squeeze.py b/backends/samsung/builders/op_squeeze.py new file mode 100644 index 00000000000..d165a22fcb3 --- /dev/null +++ b/backends/samsung/builders/op_squeeze.py @@ -0,0 +1,36 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class SqueezeVisitor(NodeVisitor): + target = "aten.squeeze_copy.dims" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "RESHAPE", [input_id], [output_id]) diff --git a/backends/samsung/builders/op_sub.py b/backends/samsung/builders/op_sub.py new file mode 100644 index 00000000000..af2931f298e --- /dev/null +++ b/backends/samsung/builders/op_sub.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class SubVisitor(NodeVisitor): + target = "aten.sub.Tensor" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + # inputs + input1 = node.args[0] + input_id_1 = self.define_tensor(input1, enn_graph, vals_to_ids) + input2 = node.args[1] + input_id_2 = self.define_tensor(input2, enn_graph, vals_to_ids) + + # output + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "SUB", [input_id_1, input_id_2], [output_id]) diff --git a/backends/samsung/builders/op_to_copy.py b/backends/samsung/builders/op_to_copy.py new file mode 100644 index 00000000000..545672ef6a3 --- /dev/null +++ b/backends/samsung/builders/op_to_copy.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Dict + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph + + +@register_node_visitor +class ToCopyVisitor(NodeVisitor): + target = ["aten._to_copy.default", "dim_order_ops._to_dim_order_copy.default"] + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + memory_format_target = node.kwargs.get("memory_format", torch.contiguous_format) + to_contiguous = bool(memory_format_target == torch.contiguous_format) + assert to_contiguous, "Don't support other param in _to_copy" + + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + + enn_graph.define_op(node.name, "CAST", [input_id], [output_id]) diff --git a/backends/samsung/builders/op_upsample_bilinear2d.py b/backends/samsung/builders/op_upsample_bilinear2d.py new file mode 100644 index 00000000000..a934b2789ba --- /dev/null +++ b/backends/samsung/builders/op_upsample_bilinear2d.py @@ -0,0 +1,52 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict, List + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph +from executorch.backends.transforms import get_shape + + +@register_node_visitor +class UpsampleBilinear2dVisitor(NodeVisitor): + target = "aten.upsample_bilinear2d.vec" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + in_shape = get_shape(input) + output_size = cast(List[int], node.args[1]) + scale_factor = [ + output_size[0] * 1.0 / in_shape[-2], + output_size[1] * 1.0 / in_shape[-1], + ] + + align_corners = cast(bool, node.args[2]) + if len(node.args) > 3 and node.args[3]: + scale_factor = cast(List[float], node.args[3]) + + params = { + "align_corners": align_corners, + "upsampling_factor": scale_factor, + "half_pixel_centers": True, + } + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + enn_graph.define_op( + node.name, "RESIZE_BILINEAR", [input_id], [output_id], params + ) diff --git a/backends/samsung/builders/op_upsample_nearest2d.py b/backends/samsung/builders/op_upsample_nearest2d.py new file mode 100644 index 00000000000..9859cd8f07e --- /dev/null +++ b/backends/samsung/builders/op_upsample_nearest2d.py @@ -0,0 +1,52 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from typing import cast, Dict, List + +import torch +from executorch.backends.samsung.builders.node_visitor import ( + NodeVisitor, + register_node_visitor, +) +from executorch.backends.samsung.serialization.enn_graph_schema import EnnGraph +from executorch.backends.transforms import get_shape + + +@register_node_visitor +class UpsampleNearest2dVisitor(NodeVisitor): + target = "aten.upsample_nearest2d.vec" + + def __init__(self, *args) -> None: + super().__init__(*args) + + def define_node( + self, + node: torch.fx.Node, + enn_graph: EnnGraph, + vals_to_ids: Dict[torch.Tensor, int], + ) -> None: + input = node.args[0] + input_id = self.define_tensor(input, enn_graph, vals_to_ids) + in_shape = get_shape(input) + output_size = cast(List[int], node.args[1]) + scale_factor = [ + output_size[0] * 1.0 / in_shape[-2], + output_size[1] * 1.0 / in_shape[-1], + ] + + if len(node.args) > 2 and node.args[2]: + scale_factor = cast(List[float], node.args[2]) + + params = { + "align_corners": False, + "upsampling_factor": scale_factor, + "half_pixel_centers": True, + } + + output_id = self.define_tensor(node, enn_graph, vals_to_ids) + enn_graph.define_op( + node.name, "RESIZE_NEAREST_NEIGHBOR", [input_id], [output_id], params + ) diff --git a/backends/samsung/enn_preprocess.py b/backends/samsung/enn_preprocess.py index ca95e5e8611..dde01bc09c7 100644 --- a/backends/samsung/enn_preprocess.py +++ b/backends/samsung/enn_preprocess.py @@ -9,6 +9,11 @@ import executorch.backends.samsung.python.PyEnnWrapperAdaptor as PyEnnWrapper import torch +from executorch.backends.samsung._passes.conv1d_to_conv2d import Conv1dToConv2d +from executorch.backends.samsung._passes.customized_constant_prop import ( + ConstantPropPass, +) +from executorch.backends.samsung._passes.replace_scalar_ops import ReplaceOpsWithScalar from executorch.backends.samsung.builders.node_visitor import get_node_visitors from executorch.backends.samsung.serialization.compile_options import ( ENN_COMPILE_OPTION_TITLE, @@ -48,8 +53,11 @@ def preprocess( enn_preprocess_passes = PassManager( passes=[ + ConstantPropPass(edge_program), + Conv1dToConv2d(edge_program), FuseBatchNormWithConvPass(edge_program), AddmmToLinearTransform(), + ReplaceOpsWithScalar(), RemoveGetItemPass(), ] ) diff --git a/backends/samsung/partition/enn_partitioner.py b/backends/samsung/partition/enn_partitioner.py index 466a7d13e08..952cb000429 100644 --- a/backends/samsung/partition/enn_partitioner.py +++ b/backends/samsung/partition/enn_partitioner.py @@ -4,7 +4,7 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. import logging -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List, Optional, Tuple import executorch.backends.samsung.builders.node_visitor as node_visitor @@ -32,7 +32,12 @@ from torch.fx.passes.operator_support import OperatorSupportBase SUPPORTED_OPS = [ + # support because preprocess in backend exir_ops.edge.aten.addmm.default, + exir_ops.edge.aten.add.Scalar, + exir_ops.edge.aten.sub.Scalar, + exir_ops.edge.aten.mul.Scalar, + exir_ops.edge.aten.div.Scalar, ] @@ -109,3 +114,20 @@ def partition(self, edge_program: torch.export.ExportedProgram) -> PartitionResu return PartitionResult( tagged_exported_program=edge_program, partition_tags=self.partition_tags ) + + # override + def ops_to_not_decompose( + self, ep: torch.export.ExportedProgram + ) -> Tuple[List[torch._ops.OpOverload], Optional[Callable[[torch.fx.Node], bool]]]: + ops_not_to_decompose = [ + torch.ops.aten.hardswish.default, + torch.ops.aten.max_pool2d.default, + torch.ops.aten.linear.default, + torch.ops.aten._safe_softmax.default, + torch.ops.aten.upsample_bilinear2d.vec, + torch.ops.aten.upsample_nearest2d.vec, + torch.ops.aten.prelu.default, + torch.ops.aten.layer_norm.default, + torch.ops.aten.pixel_shuffle.default, + ] + return (ops_not_to_decompose, None) diff --git a/backends/samsung/test/ops/test_batch_norm.py b/backends/samsung/test/ops/test_batch_norm.py new file mode 100644 index 00000000000..7cb9db0e47c --- /dev/null +++ b/backends/samsung/test/ops/test_batch_norm.py @@ -0,0 +1,51 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class BatchNorm(torch.nn.Module): + def __init__(self, num_features: int) -> None: + super().__init__() + self.num_features = num_features + self.bn = torch.nn.BatchNorm2d(num_features=self.num_features) + self.bn.weight.data.uniform_(-0.1, 0.1) + self.bn.bias.data.uniform_(-0.1, 0.1) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.bn(x) + + +class TestBatchNorm(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, inputs, [gen_samsung_backend_compile_spec("E9955")] + ) + ( + tester.export() + .to_edge_transform_and_lower() + .check_not( + [ + "executorch_exir_dialects_edge__ops_aten__native_batch_norm_legit_no_training_default" + ] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_batch_norm(self): + num_features = 16 + inputs = (torch.randn(4, num_features, 32, 32),) + self._test(BatchNorm(num_features), inputs) diff --git a/backends/samsung/test/ops/test_bmm.py b/backends/samsung/test/ops/test_bmm.py new file mode 100644 index 00000000000..7712a50a0c9 --- /dev/null +++ b/backends/samsung/test/ops/test_bmm.py @@ -0,0 +1,48 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class BatchMatmul(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def get_example_inputs(self) -> tuple[torch.Tensor]: + input_1 = torch.randn(2, 16, 56) + input_2 = torch.randn(2, 56, 32) + return (input_1, input_2) + + def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + return torch.bmm(x, y) + + +class TestBatchMatmul(unittest.TestCase): + def _test(self, module: torch.nn.Module): + inputs = module.get_example_inputs() + tester = SamsungTester( + module, inputs, [gen_samsung_backend_compile_spec("E9955")] + ) + ( + tester.export() + .check_count({"torch.ops.aten.bmm.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_bmm_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_bmm(self): + self._test(BatchMatmul()) diff --git a/backends/samsung/test/ops/test_cat.py b/backends/samsung/test/ops/test_cat.py new file mode 100644 index 00000000000..f744f9ca882 --- /dev/null +++ b/backends/samsung/test/ops/test_cat.py @@ -0,0 +1,48 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Concat(torch.nn.Module): + def __init__(self, axis) -> None: + super().__init__() + self.axis = axis + + def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + return torch.cat((x, y), dim=self.axis) + + +class TestConcat(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, inputs, [gen_samsung_backend_compile_spec("E9955")] + ) + ( + tester.export() + .check_count({"torch.ops.aten.cat.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_cat_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_concat_on_axis1(self): + inputs = (torch.randn(1, 3, 8, 8), torch.randn(1, 3, 8, 8)) + self._test(Concat(axis=1), inputs) + + def test_fp32_concat_on_axis3(self): + inputs = (torch.randn(1, 3, 8, 8), torch.randn(1, 3, 8, 8)) + self._test(Concat(axis=3), inputs) diff --git a/backends/samsung/test/ops/test_clamp.py b/backends/samsung/test/ops/test_clamp.py new file mode 100644 index 00000000000..00e3eb72690 --- /dev/null +++ b/backends/samsung/test/ops/test_clamp.py @@ -0,0 +1,49 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Clamp(torch.nn.Module): + def __init__( + self, + minimum=0.0, + maximum=0.0, + ) -> None: + super().__init__() + self.minimum = minimum + self.maximum = maximum + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.clamp(x, self.minimum, self.maximum) + + +class TestClamp(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, inputs, [gen_samsung_backend_compile_spec("E9955")] + ) + ( + tester.export() + .check_count({"torch.ops.aten.clamp.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_clamp_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_clamp(self): + inputs = (torch.randn(1, 16, 8, 8),) + self._test(Clamp(minimum=0, maximum=2.0), inputs) diff --git a/backends/samsung/test/ops/test_constant_pad_nd.py b/backends/samsung/test/ops/test_constant_pad_nd.py new file mode 100644 index 00000000000..dae24abb7d7 --- /dev/null +++ b/backends/samsung/test/ops/test_constant_pad_nd.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class ConstantPadND(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + pad = (0, 0, 1, 1) + return torch.nn.functional.pad(x, pad, mode="constant", value=0) + + +class TestConstantPadND(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, inputs, [gen_samsung_backend_compile_spec("E9955")] + ) + ( + tester.export() + .to_edge_transform_and_lower() + .check_not( + ["executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default"] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_constant_pad_nd(self): + inputs = (torch.randn(1, 6, 8, 16),) + self._test(ConstantPadND(), inputs) diff --git a/backends/samsung/test/ops/test_div.py b/backends/samsung/test/ops/test_div.py new file mode 100644 index 00000000000..31384afd896 --- /dev/null +++ b/backends/samsung/test/ops/test_div.py @@ -0,0 +1,49 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Div(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + return x / y + + +class TestDiv(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.div.Tensor": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_div_Tensor"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_simple_div(self): + inputs = (torch.randn(1, 3, 8, 8), torch.randn(1, 3, 8, 8).abs() + 1e-3) + self._test(Div(), inputs) + + def test_fp32_div_broadcast(self): + inputs = (torch.randn(1, 1, 8, 8), torch.randn(1, 3, 8, 8).abs() + 1e-3) + self._test(Div(), inputs) diff --git a/backends/samsung/test/ops/test_embedding.py b/backends/samsung/test/ops/test_embedding.py new file mode 100644 index 00000000000..fb6aaaf7766 --- /dev/null +++ b/backends/samsung/test/ops/test_embedding.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Embedding(torch.nn.Module): + def __init__(self, num_embeddings, embedding_dim=256) -> None: + super().__init__() + self.embedding = torch.nn.Embedding(num_embeddings, embedding_dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.embedding(x) + + +class TestEmbedding(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, inputs, [gen_samsung_backend_compile_spec("E9955")] + ) + ( + tester.export() + .check_count({"torch.ops.aten.embedding.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_embedding_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_embedding(self): + num_embeddings = 2048 + inputs = (torch.randint(0, num_embeddings, (1, 64), dtype=torch.int32),) + self._test(Embedding(num_embeddings=num_embeddings), inputs) diff --git a/backends/samsung/test/ops/test_expand_copy.py b/backends/samsung/test/ops/test_expand_copy.py new file mode 100644 index 00000000000..47df38be8e2 --- /dev/null +++ b/backends/samsung/test/ops/test_expand_copy.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class ExpandCopy(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.expand(1, 16, -1, -1) + + +class TestExpand(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.expand.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_expand_copy_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_expand_copy(self): + inputs = (torch.randn(1, 1, 8, 8),) + self._test(ExpandCopy(), inputs) diff --git a/backends/samsung/test/ops/test_gelu.py b/backends/samsung/test/ops/test_gelu.py new file mode 100644 index 00000000000..4e6f2d971ab --- /dev/null +++ b/backends/samsung/test/ops/test_gelu.py @@ -0,0 +1,64 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class GELU(torch.nn.Module): + def __init__(self, with_conv=False) -> None: + super().__init__() + self.module = ( + torch.nn.Sequential( + torch.nn.Conv2d( + in_channels=3, + out_channels=16, + kernel_size=3, + stride=(2, 2), + padding=(1, 1), + dilation=(1, 1), + ).to(torch.float), + torch.nn.GELU(), + ) + if with_conv + else torch.nn.GELU() + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.module(x) + + +class TestGELU(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.gelu.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_gelu_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_single_gelu(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(GELU(with_conv=False), inputs) + + def test_fp32_conv_gelu(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(GELU(with_conv=True), inputs) diff --git a/backends/samsung/test/ops/test_leaky_relu.py b/backends/samsung/test/ops/test_leaky_relu.py new file mode 100644 index 00000000000..0af6ea0da90 --- /dev/null +++ b/backends/samsung/test/ops/test_leaky_relu.py @@ -0,0 +1,46 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class LeakyReLU(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.module = torch.nn.LeakyReLU() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.module(x) + + +class TestLeakyReLU(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.leaky_relu.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_leaky_relu_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_leaky_relu(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(LeakyReLU(), inputs) diff --git a/backends/samsung/test/ops/test_linear.py b/backends/samsung/test/ops/test_linear.py new file mode 100644 index 00000000000..f327464fc0c --- /dev/null +++ b/backends/samsung/test/ops/test_linear.py @@ -0,0 +1,48 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Linear(torch.nn.Module): + def __init__(self, in_features) -> None: + super().__init__() + self.module = torch.nn.Linear(in_features, 8) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.module(x) + + +class TestLinear(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.linear.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_linear_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + .run_method_and_compare_outputs() + ) + + def test_fp32_linear(self): + in_num_features = 24 + inputs = (torch.randn(128, in_num_features),) + self._test(Linear(in_num_features), inputs) diff --git a/backends/samsung/test/ops/test_log_softmax.py b/backends/samsung/test/ops/test_log_softmax.py new file mode 100644 index 00000000000..2e2b3ff0604 --- /dev/null +++ b/backends/samsung/test/ops/test_log_softmax.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class LogSoftmax(torch.nn.Module): + def __init__(self, dim) -> None: + super().__init__() + self.module = torch.nn.LogSoftmax(dim=dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.module(x) + + +class TestLogSoftmax(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten__log_softmax_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_log_softmax(self): + inputs = (torch.randn(1, 16, 56, 56),) + self._test(LogSoftmax(dim=1), inputs) diff --git a/backends/samsung/test/ops/test_mean_dim.py b/backends/samsung/test/ops/test_mean_dim.py new file mode 100644 index 00000000000..113e26c45b2 --- /dev/null +++ b/backends/samsung/test/ops/test_mean_dim.py @@ -0,0 +1,50 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class MeanDim(torch.nn.Module): + def __init__(self, keep_dims=True) -> None: + super().__init__() + self.keep_dims = keep_dims + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.mean(x, dim=[2, 3], keepdim=self.keep_dims) + + +class TestMeanDim(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.mean.dim": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_mean_dim"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_mean_with_keep_dims(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(MeanDim(), inputs) + + def test_fp32_mean_without_keep_dims(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(MeanDim(keep_dims=False), inputs) diff --git a/backends/samsung/test/ops/test_minimum.py b/backends/samsung/test/ops/test_minimum.py new file mode 100644 index 00000000000..de275cc4d46 --- /dev/null +++ b/backends/samsung/test/ops/test_minimum.py @@ -0,0 +1,44 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Minimum(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + return torch.min(x, y) + + +class TestMinimum(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_minimum_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_minimum(self): + inputs = (torch.randn(1, 8, 16, 16), torch.randn(1, 8, 16, 16)) + self._test(Minimum(), inputs) diff --git a/backends/samsung/test/ops/test_permute.py b/backends/samsung/test/ops/test_permute.py new file mode 100644 index 00000000000..3889c803e85 --- /dev/null +++ b/backends/samsung/test/ops/test_permute.py @@ -0,0 +1,54 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Permute(torch.nn.Module): + def __init__(self, order=None) -> None: + super().__init__() + self.order = order + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.permute(x, self.order) + + +class TestPermute(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.permute.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_permute_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_permute_0231(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(Permute(order=[0, 2, 3, 1]), inputs) + + def test_fp32_permute_0312(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(Permute(order=[0, 3, 1, 2]), inputs) + + def test_fp32_permute_0321(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(Permute(order=[0, 3, 2, 1]), inputs) diff --git a/backends/samsung/test/ops/test_pixel_shuffle.py b/backends/samsung/test/ops/test_pixel_shuffle.py new file mode 100644 index 00000000000..bc7a53ff592 --- /dev/null +++ b/backends/samsung/test/ops/test_pixel_shuffle.py @@ -0,0 +1,48 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class PixelShuffle(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.module = torch.nn.PixelShuffle(upscale_factor=3) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.module(x) + + +class TestPixelShuffle(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.pixel_shuffle.default": 1}) + .to_edge_transform_and_lower() + .check_not( + ["executorch_exir_dialects_edge__ops_aten_pixel_shuffle_default"] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_pixel_shuffle(self): + inputs = (torch.randn(1, 9, 4, 4),) + self._test(PixelShuffle(), inputs) diff --git a/backends/samsung/test/ops/test_relu.py b/backends/samsung/test/ops/test_relu.py new file mode 100644 index 00000000000..386827109f0 --- /dev/null +++ b/backends/samsung/test/ops/test_relu.py @@ -0,0 +1,64 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class ReLU(torch.nn.Module): + def __init__(self, with_conv=False) -> None: + super().__init__() + self.module = ( + torch.nn.Sequential( + torch.nn.Conv2d( + in_channels=3, + out_channels=16, + kernel_size=3, + stride=(2, 2), + padding=(1, 1), + dilation=(1, 1), + ).to(torch.float), + torch.nn.ReLU(), + ) + if with_conv + else torch.nn.ReLU() + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.module(x) + + +class TestReLU(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.relu.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_relu_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_single_relu(self): + inputs = (torch.randn(1, 3, 56, 56),) + self._test(ReLU(with_conv=False), inputs) + + def test_fp32_conv_relu(self): + inputs = (torch.randn(1, 3, 56, 56),) + self._test(ReLU(with_conv=True), inputs) diff --git a/backends/samsung/test/ops/test_reshape.py b/backends/samsung/test/ops/test_reshape.py new file mode 100644 index 00000000000..8c89d946361 --- /dev/null +++ b/backends/samsung/test/ops/test_reshape.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Reshape(torch.nn.Module): + def __init__(self, new_shape) -> None: + super().__init__() + self.new_shape = new_shape + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.reshape(*self.new_shape) + + +class TestReshape(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_view_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_reshape(self): + inputs = (torch.randn(1, 16, 2, 8),) + self._test(Reshape(new_shape=[1, 32, 8]), inputs) diff --git a/backends/samsung/test/ops/test_rsqrt.py b/backends/samsung/test/ops/test_rsqrt.py new file mode 100644 index 00000000000..4bf302c867f --- /dev/null +++ b/backends/samsung/test/ops/test_rsqrt.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Rsqrt(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.rsqrt(x) + + +class TestRsqrt(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.rsqrt.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_rsqrt_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_rsqrt(self): + inputs = (torch.randn(16, 8).abs().add(1e-6),) + self._test(Rsqrt(), inputs) diff --git a/backends/samsung/test/ops/test_select.py b/backends/samsung/test/ops/test_select.py new file mode 100644 index 00000000000..dcb0667d036 --- /dev/null +++ b/backends/samsung/test/ops/test_select.py @@ -0,0 +1,51 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class SelectCopy(torch.nn.Module): + def __init__(self, axis, index) -> None: + super().__init__() + self.axis = axis + self.index = index + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.select(x, self.axis, self.index) + + +class TestSelectCopy(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.select.int": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_select_copy_int"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_select_on_axis1(self): + inputs = (torch.randn([1, 4, 16, 16]),) + self._test(SelectCopy(axis=1, index=0), inputs) + + def test_fp32_concat_on_axis3(self): + inputs = (torch.randn([1, 4, 16, 16]),) + self._test(SelectCopy(axis=3, index=6), inputs) diff --git a/backends/samsung/test/ops/test_slice_copy.py b/backends/samsung/test/ops/test_slice_copy.py new file mode 100644 index 00000000000..f31410b8a41 --- /dev/null +++ b/backends/samsung/test/ops/test_slice_copy.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class SliceCopy(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x[:, :8, :, :8] + + +class TestSliceCopy(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.slice.Tensor": 2}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_slice_copy_Tensor"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_slice_copy(self): + inputs = (torch.randn(1, 16, 16, 16),) + self._test(SliceCopy(), inputs) diff --git a/backends/samsung/test/ops/test_softmax.py b/backends/samsung/test/ops/test_softmax.py new file mode 100644 index 00000000000..a4c2f36acfc --- /dev/null +++ b/backends/samsung/test/ops/test_softmax.py @@ -0,0 +1,46 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Softmax(torch.nn.Module): + def __init__(self, dim=0) -> None: + super().__init__() + self.dim = dim + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.softmax(x, dim=self.dim) + + +class TestSoftmax(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten__softmax_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + .run_method_and_compare_outputs() + ) + + def test_fp32_softmax(self): + inputs = (torch.randn(1, 16, 8, 8),) + self._test(Softmax(dim=1), inputs) diff --git a/backends/samsung/test/ops/test_sqrt.py b/backends/samsung/test/ops/test_sqrt.py new file mode 100644 index 00000000000..e1a084c3611 --- /dev/null +++ b/backends/samsung/test/ops/test_sqrt.py @@ -0,0 +1,45 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Sqrt(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.sqrt(x) + + +class TestSqrt(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.sqrt.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_sqrt_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_sqrt(self): + inputs = (torch.randn(16, 8).abs(),) + self._test(Sqrt(), inputs) diff --git a/backends/samsung/test/ops/test_squeeze.py b/backends/samsung/test/ops/test_squeeze.py new file mode 100644 index 00000000000..ab93758f203 --- /dev/null +++ b/backends/samsung/test/ops/test_squeeze.py @@ -0,0 +1,46 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Squeeze(torch.nn.Module): + def __init__(self, dims) -> None: + super().__init__() + self.dims = dims + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.squeeze(x, self.dims) + + +class TestSqueeze(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.squeeze.dims": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_squeeze_dims"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_squeeze(self): + inputs = (torch.randn(1, 2, 1, 3, 1),) + self._test(Squeeze(dims=[2, 4]), inputs) diff --git a/backends/samsung/test/ops/test_sub.py b/backends/samsung/test/ops/test_sub.py new file mode 100644 index 00000000000..5541a52c80c --- /dev/null +++ b/backends/samsung/test/ops/test_sub.py @@ -0,0 +1,62 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class Sub(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + return x - y + + +class SubConstant(torch.nn.Module): + def __init__(self, constant) -> None: + super().__init__() + self.constant = constant + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x - self.constant + + +class TestSub(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.sub.Tensor": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_sub_Tensor"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_simple_sub(self): + inputs = (torch.randn(1, 3, 8, 8), torch.randn(1, 3, 8, 8)) + self._test(Sub(), inputs) + + def test_fp32_const_sub(self): + inputs = (torch.randn(1, 3, 8, 8),) + self._test(SubConstant(torch.randn(1, 3, 8, 8)), inputs) + + def test_fp32_sub_broadcast(self): + inputs = (torch.randn(1, 1, 8, 8), torch.randn(1, 3, 8, 8)) + self._test(Sub(), inputs) diff --git a/backends/samsung/test/ops/test_to_copy.py b/backends/samsung/test/ops/test_to_copy.py new file mode 100644 index 00000000000..d6917c9403f --- /dev/null +++ b/backends/samsung/test/ops/test_to_copy.py @@ -0,0 +1,48 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class ToCopy(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return x.to(torch.float16) + + +class TestToCopy(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .to_edge_transform_and_lower() + .check_not( + [ + "executorch_exir_dialects_edge__ops_dim_order_ops__to_dim_order_copy_default" + ] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_to_copy(self): + inputs = (torch.randn(1, 3, 56, 56),) + self._test(ToCopy(), inputs) diff --git a/backends/samsung/test/ops/test_unsqueeze.py b/backends/samsung/test/ops/test_unsqueeze.py new file mode 100644 index 00000000000..543fa0bc282 --- /dev/null +++ b/backends/samsung/test/ops/test_unsqueeze.py @@ -0,0 +1,50 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class UnSqueeze(torch.nn.Module): + def __init__(self, axis) -> None: + super().__init__() + self.axis = axis + + def get_example_inputs(self) -> tuple[torch.Tensor]: + input_1 = torch.randn(2, 3, 1, 4) # input should be positive + return (input_1,) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return torch.unsqueeze(x, dim=self.axis) + + +class TestSqueeze(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.unsqueeze.default": 1}) + .to_edge_transform_and_lower() + .check_not(["executorch_exir_dialects_edge__ops_aten_unsqueeze_default"]) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_unsqueeze(self): + inputs = (torch.randn(2, 3, 1, 4),) + self._test(UnSqueeze(axis=1), inputs) diff --git a/backends/samsung/test/ops/test_upsample_bilinear2d.py b/backends/samsung/test/ops/test_upsample_bilinear2d.py new file mode 100644 index 00000000000..37dcb28df83 --- /dev/null +++ b/backends/samsung/test/ops/test_upsample_bilinear2d.py @@ -0,0 +1,53 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class UpsampleBilinear2d(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + output_shape = [32, 32] + return torch.nn.functional.interpolate( + x, + size=output_shape, + mode="bilinear", + align_corners=False, + ) + + +class TestUpsampleBilinear2d(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.upsample_bilinear2d.vec": 1}) + .to_edge_transform_and_lower() + .check_not( + ["executorch_exir_dialects_edge__ops_aten_upsample_bilinear2d_vec"] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_upsample_bilinear2d(self): + inputs = (torch.randn(1, 16, 16, 16),) + self._test(UpsampleBilinear2d(), inputs) diff --git a/backends/samsung/test/ops/test_upsample_nearest2d.py b/backends/samsung/test/ops/test_upsample_nearest2d.py new file mode 100644 index 00000000000..e027ab23337 --- /dev/null +++ b/backends/samsung/test/ops/test_upsample_nearest2d.py @@ -0,0 +1,52 @@ +# Copyright (c) Samsung Electronics Co. LTD +# All rights reserved +# +# Licensed under the BSD License (the "License"); you may not use this file +# except in compliance with the License. See the license file in the root +# directory of this source tree for more details. + + +import unittest + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.test.tester import SamsungTester + + +class UpsampleNearest2d(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + output_shape = [32, 32] + return torch.nn.functional.interpolate( + x, + size=output_shape, + mode="nearest", + ) + + +class TestUpsampleNearest2d(unittest.TestCase): + def _test(self, module: torch.nn.Module, inputs): + tester = SamsungTester( + module, + inputs, + [gen_samsung_backend_compile_spec("E9955")], + ) + ( + tester.export() + .check_count({"torch.ops.aten.upsample_nearest2d.vec": 1}) + .to_edge_transform_and_lower() + .check_not( + ["executorch_exir_dialects_edge__ops_aten_upsample_nearest2d_vec"] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + ) + + def test_fp32_upsample_nearest2d(self): + inputs = (torch.randn(1, 4, 16, 16),) + self._test(UpsampleNearest2d(), inputs) diff --git a/backends/samsung/test/tester/samsung_tester.py b/backends/samsung/test/tester/samsung_tester.py index b750fb26a96..1a595d5d77a 100644 --- a/backends/samsung/test/tester/samsung_tester.py +++ b/backends/samsung/test/tester/samsung_tester.py @@ -9,6 +9,7 @@ import executorch.backends.test.harness.stages as BaseStages import torch from executorch.backends.samsung.partition.enn_partitioner import EnnPartitioner +from executorch.backends.samsung.utils.export_utils import get_edge_compile_config from executorch.backends.test.harness import Tester as TesterBase from executorch.exir import EdgeCompileConfig, to_edge_transform_and_lower @@ -33,9 +34,7 @@ def __init__( ): compile_specs = compile_specs or [] self.partitioners = [EnnPartitioner(compile_specs=compile_specs)] - self.edge_compile_config = edge_compile_config or EdgeCompileConfig( - _skip_dim_order=True, _check_ir_validity=False - ) + self.edge_compile_config = edge_compile_config or get_edge_compile_config() self.edge_dialect_program = None def run( diff --git a/backends/samsung/utils/export_utils.py b/backends/samsung/utils/export_utils.py index a9b7de7c5ae..aaf407ef0b3 100644 --- a/backends/samsung/utils/export_utils.py +++ b/backends/samsung/utils/export_utils.py @@ -12,9 +12,27 @@ from executorch.backends.transforms.remove_clone_ops import RemoveCloneOpsTransform from executorch.exir import EdgeCompileConfig from executorch.exir.backend.backend_details import CompileSpec + +from executorch.exir.dialects._ops import ops as exir_ops from executorch.exir.program._program import to_edge_transform_and_lower +def get_edge_compile_config(): + return EdgeCompileConfig( + _skip_dim_order=True, + _core_aten_ops_exception_list=[ + exir_ops.edge.aten.max_pool2d.default, + exir_ops.edge.aten.linear.default, + exir_ops.edge.aten.hardswish.default, + exir_ops.edge.aten.prelu.default, + exir_ops.edge.aten.pixel_shuffle.default, + exir_ops.edge.aten._safe_softmax.default, + exir_ops.edge.aten.layer_norm.default, + exir_ops.edge.aten.matmul.default, + ], + ) + + def to_edge_transform_and_lower_to_enn( module: torch.nn.Module, inputs: Tuple[torch.Tensor], @@ -30,5 +48,5 @@ def to_edge_transform_and_lower_to_enn( prog, ahead_pass_list, {"forward": [EnnPartitioner(compile_specs)]}, - compile_config=EdgeCompileConfig(_skip_dim_order=True), + compile_config=get_edge_compile_config(), ) diff --git a/examples/samsung/aot_compiler.py b/examples/samsung/aot_compiler.py index 5b092d3d9ac..771d74e0b45 100644 --- a/examples/samsung/aot_compiler.py +++ b/examples/samsung/aot_compiler.py @@ -5,11 +5,7 @@ # LICENSE file in the root directory of this source tree. import argparse -import collections import logging -import os - -import torch from executorch.backends.samsung.serialization.compile_options import ( gen_samsung_backend_compile_spec, @@ -17,6 +13,7 @@ from executorch.backends.samsung.utils.export_utils import ( to_edge_transform_and_lower_to_enn, ) +from executorch.examples.samsung.utils import save_tensors from executorch.exir import ExecutorchBackendConfig from executorch.extension.export_util.utils import save_pte_program @@ -26,22 +23,18 @@ FORMAT = "[%(levelname)s %(asctime)s %(filename)s:%(lineno)s] %(message)s" logging.basicConfig(level=logging.INFO, format=FORMAT) -SUPPORT_MODEL_NAMES = ["mv2", "ic3", "resnet18", "resnet50"] - - -def save_tensors(tensors, prefix, artifact_dir): - if isinstance(tensors, tuple): - for index, output in enumerate(tensors): - save_path = prefix + "_" + str(index) + ".bin" - output.detach().numpy().tofile(os.path.join(artifact_dir, save_path)) - elif isinstance(tensors, torch.Tensor): - tensors.detach().numpy().tofile(os.path.join(artifact_dir, prefix + ".bin")) - elif isinstance(tensors, collections.OrderedDict): - for index, output in enumerate(tensors.values()): - save_path = prefix + "_" + str(index) + ".bin" - output.detach().numpy().tofile(os.path.join(artifact_dir, save_path)) - else: - logging.warning("Unsupported type (", type(tensors), ") skip saving tensor. ") +SUPPORT_MODEL_NAMES = [ + "mv2", + "ic3", + "ic4", + "resnet18", + "resnet50", + "mv3", + "edsr", + "dl3", + "vit", + "w2l", +] if __name__ == "__main__": diff --git a/examples/samsung/scripts/mobilebert_finetune.py b/examples/samsung/scripts/mobilebert_finetune.py new file mode 100644 index 00000000000..e0c11df246a --- /dev/null +++ b/examples/samsung/scripts/mobilebert_finetune.py @@ -0,0 +1,261 @@ +# Copyright (c) 2025 Samsung Electronics Co. LTD +# All rights reserved +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import argparse +import os +from pathlib import Path + +import torch + +from executorch.backends.samsung.serialization.compile_options import ( + gen_samsung_backend_compile_spec, +) +from executorch.backends.samsung.utils.export_utils import ( + to_edge_transform_and_lower_to_enn, +) +from executorch.examples.samsung.utils import save_tensors +from executorch.exir import ExecutorchBackendConfig +from executorch.extension.export_util.utils import save_pte_program +from transformers import AutoTokenizer, MobileBertForSequenceClassification + + +# Output from pretrained model exceeds the representation scale of half-float. +# Finetune bert model on specific task and make output more reasonable for hardware. +# Here is an example. +class MobileBertFinetune: + def __init__(self): + self.tokenizer = self.load_tokenizer() + + def load_tokenizer(self): + return AutoTokenizer.from_pretrained("google/mobilebert-uncased") + + def get_example_inputs(self): + encoding = self.tokenizer.encode_plus( + "Hello, my dog is cute", + add_special_tokens=True, + max_length=128, + return_token_type_ids=False, + return_attention_mask=True, + truncation=True, + return_tensors="pt", + padding="max_length", + ) + + return ( + encoding["input_ids"], + encoding["attention_mask"].to(torch.float32), + ) + + def build_loader_from_dataset(self, dataset, batch_size, usage="train"): + """ + :param data: Provide dataset in pandas table type. The header names should be ['text', 'label'], + and label range from 0 (include) to total number of classification kinds (not include). + For example: + index text label + 0 despite its title , punch drunk love is never heavy handed 1 + 1 at once half baked and overheated 0 + 2 this is a shameless sham, ... 0 + ... + :param batch_size: Size of data fetch in one batch. + :param usage: The type of dataset which is used to build dataloader, like train, val. + :return: dataloader + """ + from torch.utils.data import ( + DataLoader, + RandomSampler, + SequentialSampler, + TensorDataset, + ) + + encoded_dataset = self.tokenizer.batch_encode_plus( + dataset.text.values.tolist(), + return_attention_mask=True, + truncation=True, + padding="max_length", + max_length=128, + return_tensors="pt", + ) + + labels = torch.tensor(dataset.label.values.tolist()) + + tensor_dataset = TensorDataset( + encoded_dataset["input_ids"], encoded_dataset["attention_mask"], labels + ) + data_loader = None + if usage == "train": + data_loader = DataLoader( + tensor_dataset, + sampler=RandomSampler(tensor_dataset), + batch_size=batch_size, + ) + elif usage == "val": + data_loader = DataLoader( + tensor_dataset, + sampler=SequentialSampler(tensor_dataset), + batch_size=batch_size, + drop_last=True, + ) + else: + raise NotImplementedError( + f"Unsupported `{usage}` dataset for building dataloader." + ) + + return data_loader + + def get_finetune_mobilebert(self, artifacts_dir): + # Pretrained bert's output ranges in a large scale. It is challenge for enn backend to support directly. + # Please finetune mobilebert on specific tasks, make sure that bert's output and hidden states are friendly + # to resource-constraint device. + from io import BytesIO + + import pandas as pd + import requests + + from tqdm import tqdm + from transformers import get_linear_schedule_with_warmup + + # sentiment classification + train_url = "https://raw.githubusercontent.com/clairett/pytorch-sentiment-classification/refs/heads/master/data/SST2/train.tsv" + content = requests.get(train_url, allow_redirects=True).content + train_data = pd.read_csv( + BytesIO(content), delimiter="\t", header=None, names=["text", "label"] + ) + labels_set = train_data.label.unique() + + train_data_loader = self.build_loader_from_dataset( + train_data, batch_size=64, usage="train" + ) + + val_url = "https://raw.githubusercontent.com/clairett/pytorch-sentiment-classification/refs/heads/master/data/SST2/test.tsv" + content = requests.get(val_url, allow_redirects=True).content + val_data = pd.read_csv( + BytesIO(content), delimiter="\t", header=None, names=["text", "label"] + ) + val_data_loader = self.build_loader_from_dataset( + val_data, batch_size=64, usage="val" + ) + + artifacts_dir = artifacts_dir if artifacts_dir is not None else "./mobilebert" + need_finetune = True + os.makedirs(artifacts_dir, exist_ok=True) + pretrained_required_files = ["config.json", "model.safetensors"] + path = Path(artifacts_dir) + if (path / pretrained_required_files[0]).exists() and ( + path / pretrained_required_files[1] + ).exists(): + need_finetune = False + + # get pre-trained mobilebert + model = MobileBertForSequenceClassification.from_pretrained( + "google/mobilebert-uncased" if need_finetune else artifacts_dir, + num_labels=len(labels_set), + return_dict=False, + ) + + if not need_finetune: + return model.eval(), val_data_loader + + num_epochs = 5 + num_train_steps = len(train_data_loader) * num_epochs + + optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5) + scheduler = get_linear_schedule_with_warmup( + optimizer, num_warmup_steps=0, num_training_steps=num_train_steps + ) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model.to(device) + for epoch in range(1, num_epochs + 1): + print(f"Epoch {epoch}") + model.train() + for batch in tqdm(train_data_loader): + texts, attention_mask, labels = batch + texts = texts.to(device) + labels = labels.to(device) + + loss = model(texts, attention_mask=attention_mask, labels=labels)[0] + # backward + optimizer.zero_grad() + loss.backward() + torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) + optimizer.step() + # update learning rate + scheduler.step() + model.to("cpu") + + model.save_pretrained(artifacts_dir) + + return model.eval(), val_data_loader + + def validate(self, model, val_data_loader): + model.eval() + total_loss = 0 + correct = 0 + total = 0 + with torch.no_grad(): + for batch in val_data_loader: + texts, attention_mask, labels = batch + + loss, output = model( + texts, attention_mask=attention_mask, labels=labels + ) + total_loss += loss.item() + predictions = torch.argmax(output, dim=1) + correct += (predictions == labels).sum().item() + total += labels.size(0) + + val_loss, val_accuracy = total_loss / len(val_data_loader), correct / total + print(f"Val Loss: {val_loss:.4f}, Val Accuracy: {val_accuracy:.4f}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "-c", + "--chipset", + required=True, + help="Samsung chipset, i.e. E9955, etc", + type=str, + ) + parser.add_argument( + "-a", + "--artifact", + help="path for storing generated artifacts by this example.", + default="./mobilebert", + type=str, + ) + parser.add_argument( + "--dump", + default=False, + action="store_true", + help=("Whether to dump all outputs. If not set, we only dump pte."), + ) + args = parser.parse_args() + # ensure the working directory exist. + os.makedirs(args.artifact, exist_ok=True) + + mobilebert_finetune = MobileBertFinetune() + model, val_dataset = mobilebert_finetune.get_finetune_mobilebert(args.artifact) + mobilebert_finetune.validate(model, val_dataset) + + example_inputs = mobilebert_finetune.get_example_inputs() + output = model(*example_inputs) + + compile_specs = [gen_samsung_backend_compile_spec(args.chipset)] + edge = to_edge_transform_and_lower_to_enn( + model, example_inputs, compile_specs=compile_specs + ) + model_name = "mobilebert_exynos_fp32" + exec_prog = edge.to_executorch( + config=ExecutorchBackendConfig(extract_delegate_segments=True) + ) + save_pte_program(exec_prog, model_name, args.artifact) + + if args.dump: + # Expect example inputs are tuple, including input ids and attn mask + save_tensors(example_inputs, prefix="float_input", artifact_dir=args.artifact) + save_tensors(output, prefix="float_output", artifact_dir=args.artifact) diff --git a/examples/samsung/utils.py b/examples/samsung/utils.py new file mode 100644 index 00000000000..4f2cc56086c --- /dev/null +++ b/examples/samsung/utils.py @@ -0,0 +1,20 @@ +import collections +import logging +import os + +import torch + + +def save_tensors(tensors, prefix, artifact_dir): + if isinstance(tensors, tuple): + for index, output in enumerate(tensors): + save_path = prefix + "_" + str(index) + ".bin" + output.detach().numpy().tofile(os.path.join(artifact_dir, save_path)) + elif isinstance(tensors, torch.Tensor): + tensors.detach().numpy().tofile(os.path.join(artifact_dir, prefix + ".bin")) + elif isinstance(tensors, collections.OrderedDict): + for index, output in enumerate(tensors.values()): + save_path = prefix + "_" + str(index) + ".bin" + output.detach().numpy().tofile(os.path.join(artifact_dir, save_path)) + else: + logging.warning("Unsupported type (", type(tensors), ") skip saving tensor. ")