Skip to content

Commit abd2b54

Browse files
Arm backend: Add LUT-based leaky_relu lowering (pytorch#20837)
Add option to lower quantized leaky_relu through TOSA TABLE instead of decomposing it into clamp, mul, and add. Add an Arm pass pipeline config switch so callers can enable the table-based lowering rather than the old decomposition. cc @digantdesai @freddan80 @per @zingo @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Oscar Andersson <oscar.andersson@arm.com>
1 parent 3efa3b3 commit abd2b54

10 files changed

Lines changed: 167 additions & 17 deletions

backends/arm/_passes/arm_pass.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,16 @@ def get_name(pass_) -> str:
8282
)
8383

8484
def call_operator(self, op, args, kwargs, meta, updated: Optional[bool] = False):
85+
ops_without_quantized_fake_kernel = {
86+
exir_ops.edge.aten.bmm.default,
87+
exir_ops.edge.aten.leaky_relu.default,
88+
}
8589
if (
86-
op == exir_ops.edge.aten.bmm.default
90+
op in ops_without_quantized_fake_kernel
8791
and isinstance(meta, NodeMetadata)
8892
and len(meta.data.get("input_qparams", {})) > 0
8993
):
90-
return self._call_quantized_bmm_without_fake_kernel(op, args, kwargs, meta)
94+
return self._call_quantized_op_without_fake_kernel(op, args, kwargs, meta)
9195

9296
if not updated:
9397
return super().call_operator(op, args, kwargs, meta)
@@ -101,7 +105,7 @@ def call_operator(self, op, args, kwargs, meta, updated: Optional[bool] = False)
101105
new_meta["stack_trace"] = f"{old_stack_trace}\n{traceback.format_stack()[-2]}"
102106
return super().call_operator(op, args, kwargs, NodeMetadata(new_meta))
103107

104-
def _call_quantized_bmm_without_fake_kernel(
108+
def _call_quantized_op_without_fake_kernel(
105109
self,
106110
op,
107111
args: tuple[ProxyValue, ...],

backends/arm/_passes/arm_pass_manager.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,10 @@
164164
)
165165
from executorch.backends.arm._passes.arm_pass import ArmPass
166166
from executorch.backends.arm.common.arm_compile_spec import ArmCompileSpec
167-
from executorch.backends.arm.common.pipeline_config import SoftmaxDecompositionConfig
167+
from executorch.backends.arm.common.pipeline_config import (
168+
LeakyReLULoweringConfig,
169+
SoftmaxDecompositionConfig,
170+
)
168171
from executorch.backends.arm.tosa.specification import (
169172
tosa_spec_in_set,
170173
TosaLoweringContext,
@@ -292,7 +295,8 @@ def configure_skip_passes(self) -> tuple[type, ...]:
292295
pass
293296
case SoftmaxDecompositionConfig.STABLE:
294297
skip_set.add(DecomposeMaskedFillPass)
295-
298+
if config.leaky_relu == LeakyReLULoweringConfig.TABLE:
299+
skip_set.add(DecomposeLeakyReLUPass)
296300
self._skip_pass_types = tuple(skip_set)
297301
skip_names = [skipped_pass.__name__ for skipped_pass in self._skip_pass_types]
298302
logger.debug(f"Passes in skip list: {skip_names}")
@@ -704,7 +708,6 @@ def transform_for_annotation_pipeline(self, graph_module: GraphModule):
704708
DecomposeEinsumPass(tfa_pass=True),
705709
RewriteInplaceArithmeticPass(tfa_pass=True),
706710
DecomposeAddSubAlphaPass(tfa_pass=True),
707-
DecomposeLeakyReLUPass(tfa_pass=True),
708711
ConvertEluFamilyToEluPass(tfa_pass=True),
709712
DecomposeGroupNormPass(tfa_pass=True),
710713
DecomposeLayerNormPass(tfa_pass=True),
@@ -715,6 +718,12 @@ def transform_for_annotation_pipeline(self, graph_module: GraphModule):
715718
]
716719
)
717720

721+
if (
722+
self.compile_spec._get_pass_pipeline_config().leaky_relu
723+
is LeakyReLULoweringConfig.DECOMPOSE
724+
):
725+
self.add_pass(DecomposeLeakyReLUPass(tfa_pass=True))
726+
718727
# Scalars -> tensors
719728
self.add_passes(
720729
[

backends/arm/_passes/decompose_leaky_relu_pass.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,30 @@ class DecomposeLeakyReLUPass(ArmOpTargetedPass):
5151
check_allowed_to_transform = True
5252

5353
def call_operator(self, op, args, kwargs, meta):
54-
if op not in self.target_ops or not self.allowed_to_transform(meta):
54+
if (
55+
op not in self.target_ops
56+
or not self.allowed_to_transform(meta)
57+
or (self._is_quantized_meta(meta))
58+
):
5559
return super().call_operator(op, args, kwargs, meta)
5660

5761
x = args[0]
5862
slope = args[1] if len(args) > 1 else 0.01
5963
clamp, mul, add = _get_leaky_relu_ops(op)
6064
op1 = super().call_operator(
61-
op=clamp, args=(x, 0, None), kwargs=kwargs, meta=meta
65+
op=clamp, args=(x, 0, None), kwargs=kwargs, meta=meta, updated=True
6266
)
6367
op2 = super().call_operator(
64-
op=clamp, args=(x, None, 0), kwargs=kwargs, meta=meta
68+
op=clamp, args=(x, None, 0), kwargs=kwargs, meta=meta, updated=True
6569
)
6670
op4 = super().call_operator(
6771
op=mul,
6872
args=(op2, super().call_scalar(slope, meta)),
6973
kwargs=kwargs,
7074
meta=meta,
75+
updated=True,
76+
)
77+
op5 = super().call_operator(
78+
op=add, args=(op1, op4), kwargs=kwargs, meta=meta, updated=True
7179
)
72-
op5 = super().call_operator(op=add, args=(op1, op4), kwargs=kwargs, meta=meta)
7380
return op5

backends/arm/_passes/insert_table_ops.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class TableOps:
6666
exir_ops.edge.aten.pow.Tensor_Scalar,
6767
exir_ops.edge.aten.gelu.default,
6868
exir_ops.edge.aten.elu.default,
69+
exir_ops.edge.aten.leaky_relu.default,
6970
exir_ops.edge.aten.remainder.Scalar,
7071
}
7172

@@ -107,6 +108,18 @@ def __getitem__(self, node: Node):
107108
return lambda x: torch.ops.aten.elu.default(
108109
x, input_alpha, scale, input_scale
109110
).flatten()
111+
case exir_ops.edge.aten.leaky_relu.default:
112+
negative_slope = cast(
113+
float,
114+
(
115+
node.args[1]
116+
if len(node.args) > 1
117+
else node.kwargs.get("negative_slope", 0.01)
118+
),
119+
)
120+
return lambda x: torch.nn.functional.leaky_relu(
121+
x, negative_slope=negative_slope
122+
).flatten()
110123
case exir_ops.edge.aten.remainder.Scalar:
111124
divisor = cast(float | int, node.args[1])
112125
return lambda x: torch.remainder(x, divisor).flatten()

backends/arm/common/pipeline_config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class SoftmaxDecompositionConfig(Enum):
1414
STABLE = auto() # Stable softmax, no masked fill decomposition
1515

1616

17+
class LeakyReLULoweringConfig(Enum):
18+
TABLE = auto() # Lower quantized leaky_relu with TOSA TABLE
19+
DECOMPOSE = auto() # Lower leaky_relu into clamp, mul, and add
20+
21+
1722
@dataclass
1823
class QuantizeInfConfig:
1924
"""Replacement values for infinities before quantization.
@@ -37,13 +42,16 @@ class ArmPassPipelineConfig:
3742
3843
Args:
3944
softmax (SoftmaxDecompositionConfig): Softmax decomposition mode.
45+
leaky_relu (LeakyReLULoweringConfig): Quantized leaky_relu lowering
46+
mode.
4047
quantize_inf (QuantizeInfConfig): Values used when replacing
4148
infinities before quantization.
4249
4350
Example:
4451
compile_spec.set_pass_pipeline_config(
4552
ArmPassPipelineConfig(
4653
softmax=SoftmaxDecompositionConfig.STABLE,
54+
leaky_relu=LeakyReLULoweringConfig.DECOMPOSE,
4755
quantize_inf=QuantizeInfConfig(
4856
neg_inf=-100.0,
4957
pos_inf=100.0,
@@ -54,11 +62,13 @@ class ArmPassPipelineConfig:
5462
"""
5563

5664
softmax: SoftmaxDecompositionConfig = SoftmaxDecompositionConfig.MASKED
65+
leaky_relu: LeakyReLULoweringConfig = LeakyReLULoweringConfig.DECOMPOSE
5766
quantize_inf: QuantizeInfConfig = field(default_factory=QuantizeInfConfig)
5867

5968
def is_default(self) -> bool:
6069
return (
6170
self.softmax is SoftmaxDecompositionConfig.MASKED
71+
and self.leaky_relu is LeakyReLULoweringConfig.DECOMPOSE
6272
and self.quantize_inf == QuantizeInfConfig()
6373
)
6474

backends/arm/operator_support/tosa_profile_supported_op_lists.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
exir_ops.edge.aten.gt.Scalar,
7171
exir_ops.edge.aten.le.Tensor,
7272
exir_ops.edge.aten.le.Scalar,
73+
exir_ops.edge.aten.leaky_relu.default,
7374
exir_ops.edge.aten.lt.Tensor,
7475
exir_ops.edge.aten.lt.Scalar,
7576
exir_ops.edge.aten.mul.Tensor,

backends/arm/quantizer/quantization_annotator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,8 @@ def _get_fixed_qparams_qspec(
545545
torch.ops.aten.hardsigmoid.default,
546546
torch.ops.aten.hardswish.default,
547547
torch.ops.aten.hardswish_.default,
548+
torch.ops.aten.leaky_relu.default,
549+
torch.ops.aten.leaky_relu_.default,
548550
torch.ops.aten.full_like.default,
549551
torch.ops.aten.zeros_like.default,
550552
torch.ops.aten.pow.Tensor_Scalar,

backends/arm/test/misc/test_compile_spec.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import warnings
77

8-
from executorch.backends.arm.common.pipeline_config import SoftmaxDecompositionConfig
8+
from executorch.backends.arm.common.pipeline_config import (
9+
LeakyReLULoweringConfig,
10+
SoftmaxDecompositionConfig,
11+
)
912
from executorch.backends.arm.ethosu import EthosUCompileSpec
1013
from executorch.backends.arm.tosa.compile_spec import TosaCompileSpec
1114
from executorch.backends.arm.vgf import VgfCompileSpec
@@ -72,11 +75,18 @@ def test_compile_spec_vgf_no_quant():
7275
EthosUCompileSpec._from_list(spec_list)
7376

7477

75-
def test_compile_spec_vgf_uses_default_pipeline_config():
78+
def test_compile_spec_vgf_defaults_leaky_relu_to_decompose():
7679
compile_spec = VgfCompileSpec()
7780
pipeline_config = compile_spec._get_pass_pipeline_config()
7881

79-
assert pipeline_config.is_default()
82+
assert pipeline_config.leaky_relu is LeakyReLULoweringConfig.DECOMPOSE
83+
84+
85+
def test_compile_spec_tosa_defaults_leaky_relu_to_decompose():
86+
compile_spec = TosaCompileSpec("TOSA-1.0+INT")
87+
pipeline_config = compile_spec._get_pass_pipeline_config()
88+
89+
assert pipeline_config.leaky_relu is LeakyReLULoweringConfig.DECOMPOSE
8090

8191

8292
def test_compile_spec_tosa_INT():

backends/arm/test/misc/test_pass_pipeline_config.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
from executorch.backends.arm._passes.arm_pass_manager import ArmPassManager
1414
from executorch.backends.arm.common.pipeline_config import (
1515
ArmPassPipelineConfig,
16+
LeakyReLULoweringConfig,
1617
QuantizeInfConfig,
1718
SoftmaxDecompositionConfig,
1819
)
20+
from executorch.backends.arm.test import common
21+
from executorch.backends.arm.test.tester.arm_tester import ArmTester
1922
from executorch.backends.arm.tosa.compile_spec import TosaCompileSpec
2023
from executorch.backends.arm.tosa.specification import TosaSpecification
2124
from torch.export import export
@@ -35,6 +38,17 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
3538
return x
3639

3740

41+
class ModuleWithLeakyReLU(torch.nn.Module):
42+
def forward(self, x: torch.Tensor) -> torch.Tensor:
43+
return torch.nn.functional.leaky_relu(x, negative_slope=0.2)
44+
45+
46+
def _call_function_targets(graph_module: torch.fx.GraphModule) -> list[object]:
47+
return [
48+
node.target for node in graph_module.graph.nodes if node.op == "call_function"
49+
]
50+
51+
3852
def test_pipeline_config_override_outside_compile_spec():
3953
compile_spec = TosaCompileSpec(
4054
TosaSpecification.create_from_string("TOSA-1.00+INT")
@@ -110,3 +124,87 @@ def test_quant_inf_config_reaches_annotation_pipeline():
110124
)
111125

112126
assert tensor_constant_values == [QUANT_NEG_INF, QUANT_POS_INF]
127+
128+
129+
def test_leaky_relu_config_serializes_roundtrip():
130+
config = ArmPassPipelineConfig(
131+
leaky_relu=LeakyReLULoweringConfig.DECOMPOSE,
132+
)
133+
assert config.is_default()
134+
135+
from_dict = ArmPassPipelineConfig.from_dict(config.to_dict())
136+
assert from_dict.leaky_relu is LeakyReLULoweringConfig.DECOMPOSE
137+
138+
compile_spec = TosaCompileSpec(
139+
TosaSpecification.create_from_string("TOSA-1.00+INT")
140+
)
141+
compile_spec.set_pass_pipeline_config(config)
142+
roundtripped = TosaCompileSpec._from_list(compile_spec._to_list())
143+
144+
assert (
145+
roundtripped._get_pass_pipeline_config().leaky_relu
146+
is LeakyReLULoweringConfig.DECOMPOSE
147+
)
148+
149+
150+
def test_leaky_relu_table_config_reaches_annotation_pipeline():
151+
config = ArmPassPipelineConfig(
152+
leaky_relu=LeakyReLULoweringConfig.TABLE,
153+
)
154+
compile_spec = TosaCompileSpec(
155+
TosaSpecification.create_from_string("TOSA-1.00+INT")
156+
)
157+
compile_spec.set_pass_pipeline_config(config)
158+
manager = ArmPassManager(compile_spec)
159+
exported = export(ModuleWithLeakyReLU(), (torch.randn(4),), strict=True)
160+
161+
transformed = manager.transform_for_annotation_pipeline(exported.graph_module)
162+
targets = _call_function_targets(transformed)
163+
164+
assert torch.ops.aten.leaky_relu.default in targets
165+
166+
167+
def test_leaky_relu_decompose_config_reaches_annotation_pipeline():
168+
config = ArmPassPipelineConfig(
169+
leaky_relu=LeakyReLULoweringConfig.DECOMPOSE,
170+
)
171+
compile_spec = TosaCompileSpec(
172+
TosaSpecification.create_from_string("TOSA-1.00+INT")
173+
)
174+
compile_spec.set_pass_pipeline_config(config)
175+
manager = ArmPassManager(compile_spec)
176+
exported = export(ModuleWithLeakyReLU(), (torch.randn(4),), strict=True)
177+
178+
transformed = manager.transform_for_annotation_pipeline(exported.graph_module)
179+
targets = _call_function_targets(transformed)
180+
181+
assert torch.ops.aten.leaky_relu.default not in targets
182+
assert torch.ops.aten.clamp.default in targets
183+
assert torch.ops.aten.mul.Tensor in targets
184+
assert torch.ops.aten.add.Tensor in targets
185+
186+
187+
def test_leaky_relu_decompose_config_reaches_backend_pipeline():
188+
config = ArmPassPipelineConfig(
189+
leaky_relu=LeakyReLULoweringConfig.DECOMPOSE,
190+
)
191+
compile_spec = common.get_tosa_compile_spec("TOSA-1.00+INT")
192+
compile_spec.set_pass_pipeline_config(config)
193+
tester = ArmTester(
194+
ModuleWithLeakyReLU(),
195+
example_inputs=(torch.linspace(-1.0, 1.0, 16),),
196+
compile_spec=compile_spec,
197+
)
198+
199+
tester.quantize().export()
200+
exported_program = tester.get_artifact()
201+
graph_module = ArmPassManager(compile_spec).transform_to_backend_pipeline(
202+
exported_program, exported_program.graph_module
203+
)
204+
graph_code = graph_module.code
205+
206+
assert "torch.ops.aten.leaky_relu.default" not in graph_code
207+
assert "torch.ops.backend.tosa.TABLE.default" not in graph_code
208+
assert "torch.ops.aten.clamp.default" in graph_code
209+
assert "torch.ops.aten.mul.Tensor" in graph_code
210+
assert "torch.ops.aten.add.Tensor" in graph_code

backends/arm/test/ops/test_leaky_relu.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ def test_leaky_relu_tosa_INT(test_data):
6161
[],
6262
use_to_edge_transform_and_lower=True,
6363
)
64-
pipeline.add_stage_after("quantize", pipeline.tester.check_not, [aten_op])
6564
pipeline.run()
6665

6766

@@ -75,7 +74,6 @@ def test_leaky_relu_u55_INT(test_data):
7574
[],
7675
use_to_edge_transform_and_lower=True,
7776
)
78-
pipeline.add_stage_after("quantize", pipeline.tester.check_not, [aten_op])
7977
pipeline.run()
8078

8179

@@ -89,7 +87,6 @@ def test_leaky_relu_u85_INT(test_data):
8987
[],
9088
use_to_edge_transform_and_lower=True,
9189
)
92-
pipeline.add_stage_after("quantize", pipeline.tester.check_not, [aten_op])
9390
pipeline.run()
9491

9592

@@ -121,5 +118,4 @@ def test_leaky_relu_vgf_quant(test_data):
121118
use_to_edge_transform_and_lower=True,
122119
quantize=True,
123120
)
124-
pipeline.add_stage_after("quantize", pipeline.tester.check_not, [aten_op])
125121
pipeline.run()

0 commit comments

Comments
 (0)