From 92c3e648b107da24f6ae9dd20fa93ae54979490e Mon Sep 17 00:00:00 2001 From: Youngsik Yang Date: Wed, 29 Jul 2026 03:16:20 +0900 Subject: [PATCH] Arm backend: give SiLU its own output quantization spec Issue: 1. SiLU's quantized output was forced to reuse its input's quantization scale, wasting resolution since SiLU's output range is always narrower than its input's. 2. The composable quantizer and `silu_.default` was separately misclassified in the legacy annotator since #17202. Fix: Give SiLU its own output qspec. Also fixes an unrelated latent bug that this change exposed. Signed-off-by: Youngsik Yang --- .../arm/quantizer/quantization_annotator.py | 2 +- backends/arm/quantizer/quantization_config.py | 4 +- backends/arm/test/misc/test_shared_qspecs.py | 47 +++++++++++++++++++ backends/arm/test/ops/test_silu.py | 4 +- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/backends/arm/quantizer/quantization_annotator.py b/backends/arm/quantizer/quantization_annotator.py index 4a699e9f3eb..423b49729c5 100644 --- a/backends/arm/quantizer/quantization_annotator.py +++ b/backends/arm/quantizer/quantization_annotator.py @@ -556,6 +556,7 @@ def _get_fixed_qparams_qspec( torch.ops.aten.pow.Tensor_Scalar, torch.ops.aten.gelu.default, torch.ops.aten.silu.default, + torch.ops.aten.silu_.default, torch.ops.aten.sinh.default, torch.ops.aten.atan.default, torch.ops.aten.log1p.default, @@ -654,7 +655,6 @@ def _get_fixed_qparams_qspec( torch.ops.aten.hardtanh_.default, torch.ops.aten.relu.default, torch.ops.aten.relu_.default, - torch.ops.aten.silu_.default, torch.ops.aten.mean.default, torch.ops.aten.mean.dim, torch.ops.aten.permute.default, diff --git a/backends/arm/quantizer/quantization_config.py b/backends/arm/quantizer/quantization_config.py index a4aac89928b..956049c70c4 100644 --- a/backends/arm/quantizer/quantization_config.py +++ b/backends/arm/quantizer/quantization_config.py @@ -261,6 +261,8 @@ class TOSAQuantizationConfig(QuantizationConfig): quantize_grid_sampler_grid = False + # Ops whose output range matches their input's, so reusing the input qspec + # costs no resolution. Ops that compress the value range do not belong here. SHARED_OUTPUT_ACT_QSPEC_PATTERNS = { torch.ops.aten.adaptive_avg_pool2d.default, torch.ops.aten.upsample_bilinear2d.vec, @@ -269,8 +271,6 @@ class TOSAQuantizationConfig(QuantizationConfig): torch.ops.aten.max_pool2d.default, torch.ops.aten.mean.default, torch.ops.aten.mean.dim, - torch.ops.aten.silu.default, - torch.ops.aten.silu_.default, } SHARED_INPUT_ACT_QSPEC_PATTERNS = { diff --git a/backends/arm/test/misc/test_shared_qspecs.py b/backends/arm/test/misc/test_shared_qspecs.py index 93129633418..7287aefb8af 100644 --- a/backends/arm/test/misc/test_shared_qspecs.py +++ b/backends/arm/test/misc/test_shared_qspecs.py @@ -567,6 +567,33 @@ def forward(self, x): return torch.maximum(self.int16(x), self.int8(x)) +class NonSharedQspecSilu(torch.nn.Module): + """SiLU's output range is always narrower than its input's, so reusing the + input scale misaligns the quantization bins with the output distribution and + wastes resolution. + """ + + qspecs = { + "quantized_decomposed.quantize_per_tensor.default": {None: 2}, + "quantized_decomposed.dequantize_per_tensor.default": {None: 2}, + } + input_qspecs = {_INT8_QSPEC: 1} + output_qspecs = {_INT8_QSPEC: 1} + quant_params = { + "quantized_decomposed.quantize_per_tensor.default": { + (0.03919654, 76, -128, 127, torch.int8): 1, + (0.007980779, -94, -128, 127, torch.int8): 1, + }, + "quantized_decomposed.dequantize_per_tensor.default": { + (0.03919654, 76, -128, 127, torch.int8): 1, + (0.007980779, -94, -128, 127, torch.int8): 1, + }, + } + + def forward(self, x): + return torch.nn.functional.silu(x) + + test_cases = { "multiple_clusters": McuTestCase( SharedQspecMulipleClusters(), @@ -687,3 +714,23 @@ def test_maximum_mixed_int8_int16_inputs(): ) pipeline.run() _check_quant_params(pipeline, model.quant_params) + + +def test_silu_does_not_share_input_qspec(): + """Sharing saves nothing in return for that lost resolution: SiLU runs as a + lookup table whose entries are precomputed from both the input and the output + qparams, so a distinct output scale costs nothing at runtime. + """ + model = NonSharedQspecSilu() + inputs = (ramp_tensor(-8, 2, (2, 3, 4)),) + + pipeline = QuantizationPipeline( + model, + inputs, + quantizer=_get_quantizer(), + qspecs=model.qspecs, + input_qspecs=model.input_qspecs, + output_qspecs=model.output_qspecs, + ) + pipeline.run() + _check_quant_params(pipeline, model.quant_params) diff --git a/backends/arm/test/ops/test_silu.py b/backends/arm/test/ops/test_silu.py index 31ea99d5957..08f40d83659 100644 --- a/backends/arm/test/ops/test_silu.py +++ b/backends/arm/test/ops/test_silu.py @@ -28,7 +28,9 @@ def forward( _input: torch.Tensor, _inplace: Optional[bool] = False, ): - return torch.nn.SiLU(inplace=_inplace)(_input) + # Clone so the in-place variant does not overwrite the caller's tensor, + # which the pipeline reuses for the comparison run. + return torch.nn.SiLU(inplace=_inplace)(_input.clone()) test_data: list[input_t] = { "op_silu_rank1_ones": lambda: torch.ones(5),