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_softmax_pass.py b/backends/arm/test/passes/test_decompose_softmax_pass.py new file mode 100644 index 00000000000..efb911f03aa --- /dev/null +++ b/backends/arm/test/passes/test_decompose_softmax_pass.py @@ -0,0 +1,103 @@ +# 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_softmax_pass import DecomposeSoftmaxPass + +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=[DecomposeSoftmaxPass], + ) + 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=[DecomposeSoftmaxPass], + ) + 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()