Skip to content

Commit 74ebdfc

Browse files
Fix int16 TOSA.TABLE LUT zeroed when output range uses <16 bits (#20401)
Summary: `InsertTableOpsPass.generate_16_bit_table_values` builds the int16 `TOSA.TABLE` lookup for unary ops (sigmoid, tanh, ...). It computes `rshift = ceil(log2(max_table_value)) + 1 - 16` to fit the table into 16 signed bits, then does `lut_values >> rshift`, assuming the table fills ~16 bits (its own comment notes "for int16, rshift == 0"). When the op's output range uses fewer than 16 bits this breaks. A sigmoid output is in `[0, 1]`; quantized with a small scale (e.g. `1/4096`), the largest table value is `4096` (13 bits), so `rshift = 13 - 16 = -3`. `lut_values >> -3` is an undefined negative right-shift; on the host the shift count is masked and the entire table is zeroed, so the activation returns 0 for every input. This makes any int16 `TABLE` op with a small output range (e.g. a sigmoid in a Squeeze-and-Excitation block) degenerate. Fix: clamp `rshift` to >= 0. When it would be negative the values already fit in int16, so no shift is needed; this restores the documented `rshift == 0` / `rescale_lshift == -7` case. The fix is general -- it covers any int16 `TABLE` op whose output range is small. Differential Revision: D107331163
1 parent 6f8a889 commit 74ebdfc

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

backends/arm/_passes/insert_table_ops.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ def f(x: torch.Tensor) -> torch.Tensor:
176176
)
177177
return (f(effective_codes).to(dtype=torch.int8), 0)
178178

179+
@staticmethod
179180
def generate_16_bit_table_values(
180-
self,
181181
torch_op: Callable[[torch.Tensor], torch.Tensor],
182182
in_quantargs: QuantArgs,
183183
out_quantargs: QuantArgs,
@@ -224,6 +224,15 @@ def f(x: torch.Tensor) -> torch.Tensor:
224224
# but due to signedness this is a negative number! So we need to shift it one more bit.
225225
# Note: for out_quantargs.dtype=torch.int16, rshift == 0 and rescale_lshift = -7.
226226
rshift = int(torch.ceil(torch.log2(lut_values.abs().max()))) + 1 - 16
227+
# When the table values use fewer than 16 bits (e.g. a sigmoid output
228+
# quantized with a small scale, so the max table value is well below
229+
# 2**15), the formula above yields a negative rshift. The values already
230+
# fit in signed int16, and a negative right-shift is undefined (on host it
231+
# masks the shift count and zeroes the table, giving a degenerate
232+
# step-function LUT on device). Clamp to 0 so no shift is applied; this is
233+
# the documented int16 case (rshift == 0, rescale_lshift == -7) and keeps
234+
# rescale_lshift consistent with the shift actually performed below.
235+
rshift = max(rshift, 0)
227236
# The 7 fractional bits are equivalent to a lshift of 7, so subtract 7 from the lshift we do.
228237
rescale_lshift = rshift - 7
229238
lut_values = lut_values >> rshift

backends/arm/test/ops/test_sigmoid_32bit.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# LICENSE file in the root directory of this source tree.
55

66
import torch
7+
from executorch.backends.arm._passes.insert_table_ops import InsertTableOpsPass
8+
from executorch.backends.arm._passes.quant_args import QuantArgs
79
from executorch.backends.arm.quantizer.quantization_config import QuantizationConfig
810
from executorch.backends.arm.test import common
911
from executorch.backends.arm.test.tester.test_pipeline import (
@@ -175,3 +177,42 @@ def test_sigmoid_u85_INT_add_sigmoid(test_data):
175177
)
176178
configure_32bit_sigmoid_quantizer(pipeline)
177179
pipeline.run()
180+
181+
182+
def test_int16_table_small_output_range_is_not_degenerate():
183+
"""Regression for the int16 TABLE negative-rshift bug.
184+
185+
A TABLE op whose output uses fewer than 16 bits -- e.g. a sigmoid output in
186+
[0, 1] quantized with a small scale, so the max table value (here 4096) is
187+
well below 2**15 -- yields ``rshift < 0``. The generator then did
188+
``lut_values >> rshift``, an undefined negative right-shift that zeroed the
189+
whole table on the host, turning the on-device activation into a constant.
190+
The table must remain a non-degenerate, monotonic ramp.
191+
"""
192+
# qparams for a small-output-range sigmoid (input spans ~+-22.4,
193+
# output in [0, 1] quantized at 1/4096 -> max table value 4096 = 13 bits).
194+
in_quantargs = QuantArgs(
195+
scale=0.0006833122461102903, zp=0, qmin=-32767, qmax=32767, dtype=torch.int16
196+
)
197+
out_quantargs = QuantArgs(
198+
scale=1.0 / 4096, zp=0, qmin=-32767, qmax=32767, dtype=torch.int16
199+
)
200+
201+
# generate_16_bit_table_values is a @staticmethod; call it on the class.
202+
lut, rescale_lshift = InsertTableOpsPass.generate_16_bit_table_values(
203+
torch.sigmoid, in_quantargs, out_quantargs
204+
)
205+
206+
assert (
207+
torch.unique(lut).numel() > 1
208+
), "int16 sigmoid table collapsed to a constant (negative-rshift bug)"
209+
assert bool(
210+
(lut[1:] >= lut[:-1]).all()
211+
), "sigmoid table must be monotonically non-decreasing"
212+
assert int(lut.min()) == 0 and int(lut.max()) == 4096, (
213+
f"unexpected table range [{int(lut.min())}, {int(lut.max())}], "
214+
"expected a full [0, 4096] sigmoid ramp"
215+
)
216+
# Values already fit in int16, so no shift is applied: this is the documented
217+
# int16 case (rshift == 0 -> rescale_lshift == -7), not the buggy -10.
218+
assert rescale_lshift == -7, f"expected rescale_lshift == -7, got {rescale_lshift}"

0 commit comments

Comments
 (0)