From d2f5f47df0d29d00bdd58255485aacb24c9f4016 Mon Sep 17 00:00:00 2001 From: Michiel Olieslagers Date: Wed, 13 Nov 2024 10:31:29 +0000 Subject: [PATCH 1/2] Added 8 new unit tests for testing various passes. Refactored tests cases to use TestPassPipeline instead of ArmTester (Removed all rights reserved) Signed-off-by: Michiel Olieslagers Change-Id: Ib5e9051dc6299a499423bfc73f1c74d3e4a1225f --- .../test_convert_expand_copy_to_repeat.py | 51 +++++++++ .../passes/test_convert_split_to_slice.py | 67 +++++++++++ .../test/passes/test_decompose_div_pass.py | 65 +++++++++++ .../passes/test_decompose_layernorm_pass.py | 69 ++++++++++++ .../passes/test_decompose_meandim_pass.py | 73 ++++++++++++ .../passes/test_decompose_softmaxes_pass.py | 105 ++++++++++++++++++ .../test/passes/test_decompose_var_pass.py | 84 ++++++++++++++ .../arm/test/passes/test_remove_clone_pass.py | 43 +++++++ 8 files changed, 557 insertions(+) create mode 100644 backends/arm/test/passes/test_convert_expand_copy_to_repeat.py create mode 100644 backends/arm/test/passes/test_convert_split_to_slice.py create mode 100644 backends/arm/test/passes/test_decompose_div_pass.py create mode 100644 backends/arm/test/passes/test_decompose_layernorm_pass.py create mode 100644 backends/arm/test/passes/test_decompose_meandim_pass.py create mode 100644 backends/arm/test/passes/test_decompose_softmaxes_pass.py create mode 100644 backends/arm/test/passes/test_decompose_var_pass.py create mode 100755 backends/arm/test/passes/test_remove_clone_pass.py diff --git a/backends/arm/test/passes/test_convert_expand_copy_to_repeat.py b/backends/arm/test/passes/test_convert_expand_copy_to_repeat.py new file mode 100644 index 00000000000..5d83bc82f22 --- /dev/null +++ b/backends/arm/test/passes/test_convert_expand_copy_to_repeat.py @@ -0,0 +1,51 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.convert_expand_copy_to_repeat import ( + ConvertExpandCopyToRepeatPass, +) + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class Expand(torch.nn.Module): + """ + Basic expand model using torch.Tensor.expand function + """ + + def __init__(self): + super(Expand, self).__init__() + + def forward(self, x): + return x.expand(3, 4) + + def get_inputs(self) -> input_t: + return (torch.rand(3, 1),) + + +def test_expand_to_repeat_tosa_BI(): + module = Expand() + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+BI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten_expand_copy_default": 1, + }, + ops_not_before_pass=["executorch_exir_dialects_edge__ops_aten_repeat_default"], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_repeat_default": 1, + }, + ops_not_after_pass=[ + "executorch_exir_dialects_edge__ops_aten_expand_copy_default" + ], + pass_list=[ConvertExpandCopyToRepeatPass], + ) + pipeline.run() diff --git a/backends/arm/test/passes/test_convert_split_to_slice.py b/backends/arm/test/passes/test_convert_split_to_slice.py new file mode 100644 index 00000000000..d4fdffe3b01 --- /dev/null +++ b/backends/arm/test/passes/test_convert_split_to_slice.py @@ -0,0 +1,67 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.convert_split_to_slice import ( + ConvertSplitToSlicePass, +) + +from executorch.backends.arm.test import common + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class Split(torch.nn.Module): + """ + Basic split model using torch.split function + """ + + def get_inputs(self) -> input_t: + return (torch.rand(10),) + + def forward(self, x): + return torch.split(x, 2) + + +class SplitTensor(torch.nn.Module): + """ + Basic split model using torch.Tensor.split function + """ + + def get_inputs(self) -> input_t: + return (torch.rand(10),) + + def forward(self, x): + return x.split(2) + + +modules = {"split_basic": Split(), "split_tensor": SplitTensor()} + + +@common.parametrize("module", modules) +def test_split_to_slice_tosa_BI(module): + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+BI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten_split_with_sizes_copy_default": 1, + }, + ops_not_before_pass=[ + "executorch_exir_dialects_edge__ops_aten_slice_copy_Tensor" + ], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_slice_copy_Tensor": 5, + }, + ops_not_after_pass=[ + "executorch_exir_dialects_edge__ops_aten_split_with_sizes_copy_default" + ], + pass_list=[ConvertSplitToSlicePass], + ) + pipeline.run() diff --git a/backends/arm/test/passes/test_decompose_div_pass.py b/backends/arm/test/passes/test_decompose_div_pass.py new file mode 100644 index 00000000000..71d586c0029 --- /dev/null +++ b/backends/arm/test/passes/test_decompose_div_pass.py @@ -0,0 +1,65 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.decompose_div_pass import DecomposeDivPass + +from executorch.backends.arm.test import common + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class Div(torch.nn.Module): + """ + Basic div model using torch.div + """ + + def get_inputs(self) -> input_t: + return (torch.rand(10),) + + def forward(self, x): + return torch.div(x, 2) + + +class DivTensor(torch.nn.Module): + """ + Basic div model using torch.Tensor.div + """ + + def get_inputs(self) -> input_t: + return (torch.rand(10),) + + def forward(self, x): + return x.div(2) + + +modules = {"div_basic": Div(), "div_tensor": DivTensor()} + + +@common.parametrize("module", modules) +def test_decompose_div_tosa_MI(module): + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+MI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten_div_Tensor": 1, + }, + ops_not_before_pass=[ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor", + "executorch_exir_dialects_edge__ops_aten_reciprocal_default", + ], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor": 1, + "executorch_exir_dialects_edge__ops_aten_reciprocal_default": 1, + }, + ops_not_after_pass=["executorch_exir_dialects_edge__ops_aten_div_Tensor"], + pass_list=[DecomposeDivPass], + ) + pipeline.run() diff --git a/backends/arm/test/passes/test_decompose_layernorm_pass.py b/backends/arm/test/passes/test_decompose_layernorm_pass.py new file mode 100644 index 00000000000..40e49e15bc5 --- /dev/null +++ b/backends/arm/test/passes/test_decompose_layernorm_pass.py @@ -0,0 +1,69 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.decompose_layernorm_pass import ( + DecomposeLayerNormPass, +) + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class LayerNorm(torch.nn.Module): + """ + Basic layer_norm model using torch.nn.layer_norm layer + """ + + def __init__(self): + super(LayerNorm, self).__init__() + self.layer_norm = torch.nn.LayerNorm(10) + + def forward(self, x): + x = self.layer_norm(x) + return x + + def get_inputs(self) -> input_t: + return (torch.rand(10),) + + +def test_decompose_layernorm_tosa_MI(): + module = LayerNorm() + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+MI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten_native_layer_norm_default": 1, + }, + ops_not_before_pass=[ + "executorch_exir_dialects_edge__ops_aten_add_Tensor", + "executorch_exir_dialects_edge__ops_aten_view_copy_default", + "executorch_exir_dialects_edge__ops_aten_mul_Tensor", + "executorch_exir_dialects_edge__ops_aten_full_default", + "executorch_exir_dialects_edge__ops_aten_rsqrt_default", + "executorch_exir_dialects_edge__ops_aten_var_correction", + "executorch_exir_dialects_edge__ops_aten_sub_Tensor", + "executorch_exir_dialects_edge__ops_aten_mean_dim", + ], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_add_Tensor": 2, + "executorch_exir_dialects_edge__ops_aten_view_copy_default": 2, + "executorch_exir_dialects_edge__ops_aten_mul_Tensor": 2, + "executorch_exir_dialects_edge__ops_aten_full_default": 1, + "executorch_exir_dialects_edge__ops_aten_rsqrt_default": 1, + "executorch_exir_dialects_edge__ops_aten_var_correction": 1, + "executorch_exir_dialects_edge__ops_aten_sub_Tensor": 1, + "executorch_exir_dialects_edge__ops_aten_mean_dim": 1, + }, + ops_not_after_pass=[ + "executorch_exir_dialects_edge__ops_aten_expand_copy_default" + ], + pass_list=[DecomposeLayerNormPass], + ) + pipeline.run() diff --git a/backends/arm/test/passes/test_decompose_meandim_pass.py b/backends/arm/test/passes/test_decompose_meandim_pass.py new file mode 100644 index 00000000000..6ba9ceff3a7 --- /dev/null +++ b/backends/arm/test/passes/test_decompose_meandim_pass.py @@ -0,0 +1,73 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.decompose_meandim_pass import DecomposeMeanDimPass + +from executorch.backends.arm.test import common + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class MeanDim(torch.nn.Module): + """ + Basic mean model using torch.mean function making sure keepdim=True (keepdim=False doesnt work for this pass for some reason) + """ + + def __init__(self): + super(MeanDim, self).__init__() + + def forward(self, x): + return torch.mean(x, 1, True) + + def get_inputs(self) -> input_t: + return (torch.rand(4, 4),) + + +class MeanDimTensor(torch.nn.Module): + """ + Basic mean model using torch.Tensor.mean function making sure keepdim=True (keepdim=False doesnt work for this pass for some reason) + """ + + def __init__(self): + super(MeanDimTensor, self).__init__() + + def forward(self, x): + return x.mean(1, True) + + def get_inputs(self) -> input_t: + return (torch.rand(4, 4),) + + +modules = {"meandim_basic": MeanDim(), "meandim_tensor": MeanDimTensor()} + + +@common.parametrize("module", modules) +def test_decompose_meandim_tosa_MI(module): + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+MI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten_mean_dim": 1, + }, + ops_not_before_pass=[ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor", + "executorch_exir_dialects_edge__ops_aten_full_default", + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList", + ], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor": 1, + "executorch_exir_dialects_edge__ops_aten_full_default": 1, + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList": 1, + }, + ops_not_after_pass=["executorch_exir_dialects_edge__ops_aten_mean_dim"], + pass_list=[DecomposeMeanDimPass], + ) + pipeline.run() diff --git a/backends/arm/test/passes/test_decompose_softmaxes_pass.py b/backends/arm/test/passes/test_decompose_softmaxes_pass.py new file mode 100644 index 00000000000..565a40aeff0 --- /dev/null +++ b/backends/arm/test/passes/test_decompose_softmaxes_pass.py @@ -0,0 +1,105 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.decompose_softmaxes_pass import ( + DecomposeSoftmaxesPass, +) + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class Softmax(torch.nn.Module): + """ + Basic torch.nn.softmax layer model + """ + + def __init__(self): + super(Softmax, self).__init__() + self.softmax = torch.nn.Softmax(dim=1) + + def forward(self, x): + x = self.softmax(x) + return x + + def get_inputs(self) -> input_t: + return (torch.rand(2, 3),) + + +class SoftmaxLog(torch.nn.Module): + """ + Basic torch.nn.log_softmax layer model + """ + + def __init__(self): + super(SoftmaxLog, self).__init__() + self.softmax = torch.nn.LogSoftmax(dim=1) + + def forward(self, x): + x = self.softmax(x) + return x + + def get_inputs(self) -> input_t: + return (torch.rand(2, 3),) + + +def test_softmax_basic_tosa_MI(): + module = Softmax() + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+MI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten__softmax_default": 1, + }, + ops_not_before_pass=[ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor", + "executorch_exir_dialects_edge__ops_aten_reciprocal_default", + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList", + "executorch_exir_dialects_edge__ops_aten_exp_default", + ], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor": 1, + "executorch_exir_dialects_edge__ops_aten_exp_default": 1, + "executorch_exir_dialects_edge__ops_aten_reciprocal_default": 1, + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList": 1, + }, + ops_not_after_pass=["executorch_exir_dialects_edge__ops_aten__softmax_default"], + pass_list=[DecomposeSoftmaxesPass], + ) + pipeline.run() + + +def test_softmax_log_tosa_MI(): + module = SoftmaxLog() + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+MI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten__log_softmax_default": 1, + }, + ops_not_before_pass=[ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor", + "executorch_exir_dialects_edge__ops_aten_reciprocal_default", + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList", + "executorch_exir_dialects_edge__ops_aten_exp_default", + ], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor": 1, + "executorch_exir_dialects_edge__ops_aten_exp_default": 1, + "executorch_exir_dialects_edge__ops_aten_reciprocal_default": 1, + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList": 1, + }, + ops_not_after_pass=[ + "executorch_exir_dialects_edge__ops_aten__log_softmax_default" + ], + pass_list=[DecomposeSoftmaxesPass], + ) + pipeline.run() diff --git a/backends/arm/test/passes/test_decompose_var_pass.py b/backends/arm/test/passes/test_decompose_var_pass.py new file mode 100644 index 00000000000..fe793dba14b --- /dev/null +++ b/backends/arm/test/passes/test_decompose_var_pass.py @@ -0,0 +1,84 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.decompose_var_pass import DecomposeVarPass + +from executorch.backends.arm.test import common + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class VarDim(torch.nn.Module): + """ + Basic variance model using torch.Tensor.var function. + """ + + def __init__(self, keepdim): + super(VarDim, self).__init__() + self.keepdim = keepdim + + def forward(self, x): + return x.var(dim=-1, keepdim=self.keepdim) + + def get_inputs(self) -> input_t: + return (torch.rand(4, 4),) + + +class VarCorrection(torch.nn.Module): + """ + Basic variance model using torch.var function. + """ + + def __init__(self, keepdim): + super(VarCorrection, self).__init__() + self.keepdim = keepdim + + def forward(self, x): + return torch.var(x, -1, keepdim=self.keepdim) + + def get_inputs(self) -> input_t: + return (torch.rand(4, 4),) + + +modules = { + "vardim_keepdim": VarDim(True), + "vardim_no_keepdim": VarDim(False), + "varcorrection_keepdim": VarCorrection(True), + "varcorrection_no_keepdim": VarCorrection(False), +} + + +@common.parametrize("module", modules) +def test_decompose_var_tosa_MI(module): + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+MI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten_var_correction": 1, + }, + ops_not_before_pass=[ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor", + "executorch_exir_dialects_edge__ops_aten_full_default", + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList", + "executorch_exir_dialects_edge__ops_aten_mean_dim", + "executorch_exir_dialects_edge__ops_aten_sub_Tensor", + ], + ops_after_pass={ + "executorch_exir_dialects_edge__ops_aten_mul_Tensor": 2, + "executorch_exir_dialects_edge__ops_aten_mean_dim": 1, + "executorch_exir_dialects_edge__ops_aten_sub_Tensor": 1, + "executorch_exir_dialects_edge__ops_aten_full_default": 1, + "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList": 1, + }, + ops_not_after_pass=["executorch_exir_dialects_edge__ops_aten_var_correction"], + pass_list=[DecomposeVarPass], + ) + pipeline.run() diff --git a/backends/arm/test/passes/test_remove_clone_pass.py b/backends/arm/test/passes/test_remove_clone_pass.py new file mode 100755 index 00000000000..e586edd323d --- /dev/null +++ b/backends/arm/test/passes/test_remove_clone_pass.py @@ -0,0 +1,43 @@ +# Copyright 2025 Arm Limited and/or its affiliates. +# +# 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 Tuple + +import torch +from executorch.backends.arm._passes.remove_clone_pass import RemoveClonePass + +from executorch.backends.arm.test.tester.test_pipeline import PassPipeline + +input_t = Tuple[torch.Tensor] # Input x + + +class Clone(torch.nn.Module): + """ + Basic remove layer model to test RemoveClonePass + """ + + def __init__(self): + super(Clone, self).__init__() + + def forward(self, x): + return torch.clone(x) + + def get_inputs(self) -> input_t: + return (torch.rand(3, 1),) + + +def test_remove_clone_tosa_BI(): + module = Clone() + pipeline = PassPipeline[input_t]( + module, + module.get_inputs(), + tosa_version="TOSA-0.80+BI", + ops_before_pass={ + "executorch_exir_dialects_edge__ops_aten_clone_default": 1, + }, + ops_not_after_pass=["executorch_exir_dialects_edge__ops_aten_clone_default"], + pass_list=[RemoveClonePass], + ) + pipeline.run() From a01db2120ee6baf9aaa8f3ebb607e36368a3c949 Mon Sep 17 00:00:00 2001 From: Michiel Olieslagers Date: Tue, 8 Apr 2025 17:53:09 +0100 Subject: [PATCH 2/2] Changed name of pass used in test. Change-Id: If6bf02bcf5aec4da3d2c92bc51f8efd1337647a6 --- ...e_softmaxes_pass.py => test_decompose_softmax_pass.py} | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) rename backends/arm/test/passes/{test_decompose_softmaxes_pass.py => test_decompose_softmax_pass.py} (94%) diff --git a/backends/arm/test/passes/test_decompose_softmaxes_pass.py b/backends/arm/test/passes/test_decompose_softmax_pass.py similarity index 94% rename from backends/arm/test/passes/test_decompose_softmaxes_pass.py rename to backends/arm/test/passes/test_decompose_softmax_pass.py index 565a40aeff0..efb911f03aa 100644 --- a/backends/arm/test/passes/test_decompose_softmaxes_pass.py +++ b/backends/arm/test/passes/test_decompose_softmax_pass.py @@ -6,9 +6,7 @@ from typing import Tuple import torch -from executorch.backends.arm._passes.decompose_softmaxes_pass import ( - DecomposeSoftmaxesPass, -) +from executorch.backends.arm._passes.decompose_softmax_pass import DecomposeSoftmaxPass from executorch.backends.arm.test.tester.test_pipeline import PassPipeline @@ -71,7 +69,7 @@ def test_softmax_basic_tosa_MI(): "executorch_exir_dialects_edge__ops_aten_sum_dim_IntList": 1, }, ops_not_after_pass=["executorch_exir_dialects_edge__ops_aten__softmax_default"], - pass_list=[DecomposeSoftmaxesPass], + pass_list=[DecomposeSoftmaxPass], ) pipeline.run() @@ -100,6 +98,6 @@ def test_softmax_log_tosa_MI(): ops_not_after_pass=[ "executorch_exir_dialects_edge__ops_aten__log_softmax_default" ], - pass_list=[DecomposeSoftmaxesPass], + pass_list=[DecomposeSoftmaxPass], ) pipeline.run()