Skip to content

Commit e182e8b

Browse files
Arm backend: Update composable_quantizer to latest changes (#20912)
Updates the composable_quantizer to include corresponding changes as in the original one to allow a simple transition. - Adds constant ops support - Adds moveaxis/movedims support - Adds special to handling support - Update conv-relu annotation logic to match old behavior Signed-off-by: Adrian Lundell <adrian.lundell@arm.com>
1 parent 430ae39 commit e182e8b

3 files changed

Lines changed: 199 additions & 15 deletions

File tree

backends/arm/quantizer/quantization_config.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from torchao.quantization.pt2e.quantizer import (
2323
DerivedQuantizationSpec,
24+
FixedQParamsQuantizationSpec,
2425
QuantizationSpec,
2526
QuantizationSpecBase,
2627
SharedQuantizationSpec,
@@ -355,6 +356,7 @@ def get_output_act_qspec(
355356
356357
If node is a pooling or upsample operator, returns a shared quantization spec.
357358
If no weight spec is configured, return ``None``.
359+
If node is a `to.dtype` operator, returns a fixed quantization spec if the input is integer and the output is floating-point.
358360
359361
"""
360362

@@ -373,6 +375,39 @@ def get_output_act_qspec(
373375
return _get_fixed_qparams_qspec(
374376
node.target, _fixed_output_qspec_ops, output_act_qspec
375377
)
378+
if node.target == torch.ops.aten.to.dtype:
379+
from executorch.backends.arm.quantizer.quantizer_support import CastCheck
380+
381+
input_node = node.all_input_nodes[0]
382+
input_val = input_node.meta.get("val", None)
383+
output_val = node.meta.get("val", None)
384+
if (
385+
isinstance(input_val, torch.Tensor)
386+
and isinstance(output_val, torch.Tensor)
387+
and CastCheck.is_integer_to_integer(input_val.dtype, output_val.dtype)
388+
):
389+
return None
390+
if (
391+
isinstance(input_val, torch.Tensor)
392+
and isinstance(output_val, torch.Tensor)
393+
and CastCheck.is_integer_to_float(input_val.dtype, output_val.dtype)
394+
):
395+
return FixedQParamsQuantizationSpec(
396+
dtype=input_val.dtype,
397+
scale=1.0,
398+
zero_point=0,
399+
quant_min=torch.iinfo(input_val.dtype).min,
400+
quant_max=torch.iinfo(input_val.dtype).max,
401+
qscheme=torch.per_tensor_symmetric,
402+
is_dynamic=False,
403+
)
404+
if (
405+
isinstance(input_val, torch.Tensor)
406+
and isinstance(output_val, torch.Tensor)
407+
and CastCheck.is_float_identity(input_val.dtype, output_val.dtype)
408+
):
409+
return SharedQuantizationSpec((input_node, node))
410+
return super().get_output_act_qspec(node)
376411
if node.target not in self.SHARED_OUTPUT_ACT_QSPEC_PATTERNS:
377412
return super().get_output_act_qspec()
378413
if len(node.args) == 0:

backends/arm/quantizer/quantizer_support.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ def combo_pattern(*pattern_lists):
2121

2222

2323
class ReluFusedPatternCheck(PatternCheck):
24+
@classmethod
25+
def check_pattern(cls, pattern):
26+
output_node = pattern[-1] if pattern else None
27+
if output_node is None:
28+
return False
29+
30+
min_val = float(output_node.args[1]) if len(output_node.args) > 1 else -1.0
31+
return (
32+
output_node.target
33+
not in (torch.ops.aten.hardtanh.default, torch.ops.aten.hardtanh_.default)
34+
or min_val == 0
35+
)
36+
2437
@classmethod
2538
def check_quantization_config(cls, pattern, quantization_config):
2639
if quantization_config is None:
@@ -55,6 +68,47 @@ def check_pattern(cls, pattern):
5568
return True
5669

5770

71+
class CastCheck(PatternCheck):
72+
INTEGER_CAST_DTYPES = (torch.int8, torch.int16, torch.int32)
73+
74+
@classmethod
75+
def is_integer_to_integer(cls, input_dtype: torch.dtype, output_dtype: torch.dtype):
76+
return (
77+
input_dtype in cls.INTEGER_CAST_DTYPES
78+
and output_dtype in cls.INTEGER_CAST_DTYPES
79+
)
80+
81+
@classmethod
82+
def is_integer_to_float(cls, input_dtype: torch.dtype, output_dtype: torch.dtype):
83+
return input_dtype in cls.INTEGER_CAST_DTYPES and output_dtype.is_floating_point
84+
85+
@classmethod
86+
def is_float_identity(cls, input_dtype: torch.dtype, output_dtype: torch.dtype):
87+
return input_dtype.is_floating_point and input_dtype == output_dtype
88+
89+
@classmethod
90+
def is_supported_cast(cls, input_dtype: torch.dtype, output_dtype: torch.dtype):
91+
return (
92+
cls.is_integer_to_integer(input_dtype, output_dtype)
93+
or cls.is_integer_to_float(input_dtype, output_dtype)
94+
or cls.is_float_identity(input_dtype, output_dtype)
95+
)
96+
97+
@classmethod
98+
def check_pattern(cls, pattern):
99+
node = pattern[0]
100+
if len(node.all_input_nodes) == 0:
101+
return False
102+
103+
try:
104+
input_tensor = get_first_fake_tensor(node.all_input_nodes[0])
105+
output_tensor = get_first_fake_tensor(node)
106+
except Exception:
107+
return False
108+
109+
return cls.is_supported_cast(input_tensor.dtype, output_tensor.dtype)
110+
111+
58112
BINARY_OP_PATTERNS = [
59113
(torch.ops.aten.add.Tensor,),
60114
(torch.ops.aten.add_.Tensor,),
@@ -77,8 +131,6 @@ def check_pattern(cls, pattern):
77131
torch.ops.aten.relu_.default,
78132
torch.ops.aten.hardtanh.default,
79133
torch.ops.aten.hardtanh_.default,
80-
torch.ops.aten.clamp.default,
81-
torch.ops.aten.clamp_.default,
82134
]
83135
BATCH_NORM_OPS = [torch.ops.aten.batch_norm.default]
84136
LINEAR_OP_PATTERNS = (
@@ -175,6 +227,13 @@ def check_pattern(cls, pattern):
175227
(torch.ops.aten.atanh.default,),
176228
(torch.ops.aten.einsum.default,),
177229
(torch.ops.aten.grid_sampler.default,),
230+
(torch.ops.aten.linspace.default,),
231+
(torch.ops.aten.eye.default,),
232+
(torch.ops.aten.moveaxis.int,),
233+
(torch.ops.aten.moveaxis.intlist,),
234+
(torch.ops.aten.movedim.int,),
235+
(torch.ops.aten.movedim.intlist,),
236+
(torch.ops.aten.to.dtype,),
178237
]
179238
)
180239
TOSA_QUANTIZER_SUPPORT_DICT: dict[tuple[OpOverload, ...], type[PatternCheck] | None] = {
@@ -184,3 +243,4 @@ def check_pattern(cls, pattern):
184243
TOSA_QUANTIZER_SUPPORT_DICT[pattern] = ReluFusedPatternCheck
185244
for pattern in BINARY_OP_PATTERNS:
186245
TOSA_QUANTIZER_SUPPORT_DICT[pattern] = ArithmeticFloatInputsCheck
246+
TOSA_QUANTIZER_SUPPORT_DICT[(torch.ops.aten.to.dtype,)] = CastCheck

backends/arm/test/ops/test_to_copy.py

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -334,33 +334,33 @@ def test_to_tosa_INT_not_delegated(test_data: Tuple):
334334
pipeline.run()
335335

336336

337-
_TO_COPY_QUANTIZED_IDENTITY_CAST_DATA = {
338-
"int8_cast_add": lambda: (
337+
_TO_COPY_QUANTIZED_INT_TO_FLOAT_CAST_DATA = {
338+
"int8_to_fp32_add": lambda: (
339339
(torch.randn(1, 3, 4, 4) * 10).to(dtype=torch.int8),
340340
torch.randn(1, 3, 4, 4),
341341
torch.float32,
342342
),
343-
"int16_cast_add": lambda: (
343+
"int16_to_fp32_add": lambda: (
344344
(torch.randn(1, 3, 4, 4) * 10).to(dtype=torch.int16),
345345
torch.randn(1, 3, 4, 4),
346346
torch.float32,
347347
),
348-
"int32_cast_add": lambda: (
348+
"int32_to_fp32_add": lambda: (
349349
(torch.randn(1, 3, 4, 4) * 10).to(dtype=torch.int32),
350350
torch.randn(1, 3, 4, 4),
351351
torch.float32,
352352
),
353353
}
354354

355355

356-
_TO_COPY_QUANTIZED_IDENTITY_CAST_CAT_DATA = {
357-
"int8_cast_cat": lambda: (
356+
_TO_COPY_QUANTIZED_INT_TO_FLOAT_CAST_CAT_DATA = {
357+
"int8_to_fp32_cat": lambda: (
358358
(torch.randn(1, 2, 4, 4) * 10).to(dtype=torch.int8),
359359
torch.randn(1, 2, 4, 1),
360360
torch.float32,
361361
3,
362362
),
363-
"int16_cast_cat": lambda: (
363+
"int16_to_fp32_cat": lambda: (
364364
(torch.randn(1, 2, 4, 4) * 10).to(dtype=torch.int16),
365365
torch.randn(1, 2, 4, 1),
366366
torch.float32,
@@ -369,8 +369,8 @@ def test_to_tosa_INT_not_delegated(test_data: Tuple):
369369
}
370370

371371

372-
@common.parametrize("test_data", _TO_COPY_QUANTIZED_IDENTITY_CAST_DATA)
373-
def test_to_tosa_INT_quantized_identity_cast_add(test_data: Tuple):
372+
@common.parametrize("test_data", _TO_COPY_QUANTIZED_INT_TO_FLOAT_CAST_DATA)
373+
def test_to_tosa_INT_quantized_int_to_float_cast_add(test_data: Tuple):
374374
x, y, new_dtype = test_data()
375375
pipeline = TosaPipelineINT[input_t2](
376376
CastAddTensor(new_dtype),
@@ -388,8 +388,8 @@ def test_to_tosa_INT_quantized_identity_cast_add(test_data: Tuple):
388388
pipeline.run()
389389

390390

391-
@common.parametrize("test_data", _TO_COPY_QUANTIZED_IDENTITY_CAST_CAT_DATA)
392-
def test_to_tosa_INT_quantized_identity_cast_cat(test_data: Tuple):
391+
@common.parametrize("test_data", _TO_COPY_QUANTIZED_INT_TO_FLOAT_CAST_CAT_DATA)
392+
def test_to_tosa_INT_quantized_int_to_float_cast_cat(test_data: Tuple):
393393
x, y, new_dtype, dim = test_data()
394394
pipeline = TosaPipelineINT[input_t2](
395395
CastCatTensor(new_dtype, dim),
@@ -400,8 +400,8 @@ def test_to_tosa_INT_quantized_identity_cast_cat(test_data: Tuple):
400400
pipeline.run()
401401

402402

403-
@common.parametrize("test_data", _TO_COPY_QUANTIZED_IDENTITY_CAST_DATA)
404-
def test_to_tosa_INT_quantized_identity_cast_to_unquantized_add_delegated(
403+
@common.parametrize("test_data", _TO_COPY_QUANTIZED_INT_TO_FLOAT_CAST_DATA)
404+
def test_to_tosa_INT_quantized_int_to_float_cast_to_unquantized_add_delegated(
405405
test_data: Tuple,
406406
):
407407
x, y, new_dtype = test_data()
@@ -423,6 +423,46 @@ def test_to_tosa_INT_quantized_identity_cast_to_unquantized_add_delegated(
423423
pipeline.run()
424424

425425

426+
_TO_COPY_INT_TO_INT_CAST_DATA = {
427+
"int8_to_int16": lambda: (
428+
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int8),
429+
torch.int16,
430+
),
431+
"int8_to_int32": lambda: (
432+
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int8),
433+
torch.int32,
434+
),
435+
"int16_to_int8": lambda: (
436+
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int16),
437+
torch.int8,
438+
),
439+
"int16_to_int32": lambda: (
440+
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int16),
441+
torch.int32,
442+
),
443+
"int32_to_int8": lambda: (
444+
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int32),
445+
torch.int8,
446+
),
447+
"int32_to_int16": lambda: (
448+
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int32),
449+
torch.int16,
450+
),
451+
}
452+
453+
454+
@common.parametrize("test_data", _TO_COPY_INT_TO_INT_CAST_DATA)
455+
def test_to_tosa_INT_int_to_int_cast(test_data: Tuple):
456+
test_tensor, new_dtype = test_data()
457+
pipeline = TosaPipelineINT[input_t1](
458+
Cast(new_dtype),
459+
(test_tensor,),
460+
aten_op=[],
461+
exir_op=[],
462+
)
463+
pipeline.run()
464+
465+
426466
@common.parametrize("test_data", _TO_COPY_TEST_DATA_INT)
427467
@common.SkipIfNoModelConverter
428468
def test_to_vgf_quant(test_data: Tuple):
@@ -462,6 +502,25 @@ def test_to_vgf_quant(test_data: Tuple):
462502
"rand_fp16_fp16": "FP16 is not supported",
463503
}
464504

505+
_TO_COPY_FLOAT_IDENTITY_CAST_DATA = {
506+
"fp32_to_fp32": lambda: (
507+
torch.rand((1, 2, 3, 4), dtype=torch.float32),
508+
torch.float32,
509+
),
510+
}
511+
512+
513+
@common.parametrize("test_data", _TO_COPY_FLOAT_IDENTITY_CAST_DATA)
514+
def test_to_tosa_INT_float_to_same_dtype_cast(test_data: Tuple):
515+
test_tensor, new_dtype = test_data()
516+
pipeline = TosaPipelineINT[input_t1](
517+
CastAdd(new_dtype),
518+
(test_tensor,),
519+
aten_op=[],
520+
exir_op=[],
521+
)
522+
pipeline.run()
523+
465524

466525
@common.parametrize(
467526
"test_data", _TO_COPY_TEST_DATA_REDUNDANT_CAST, xfails=redundant_xfails_FP
@@ -504,6 +563,36 @@ def test_to_tosa_INT_not_delegated_REDUNDANT_CAST(test_data: Tuple):
504563
pipeline.run()
505564

506565

566+
_TO_COPY_UNSUPPORTED_QUANTIZED_CAST_DATA = {
567+
"fp32_to_fp16": lambda: (
568+
torch.rand((1, 2, 3, 4), dtype=torch.float32),
569+
torch.float16,
570+
),
571+
"fp32_to_int32": lambda: (
572+
torch.rand((1, 2, 3, 4), dtype=torch.float32),
573+
torch.int32,
574+
),
575+
"bool_to_fp32": lambda: (
576+
torch.randint(0, 2, (1, 2, 3, 4), dtype=torch.bool),
577+
torch.float32,
578+
),
579+
}
580+
581+
582+
@common.parametrize("test_data", _TO_COPY_UNSUPPORTED_QUANTIZED_CAST_DATA)
583+
def test_to_tosa_INT_unsupported_cast_not_delegated(test_data: Tuple):
584+
test_tensor, new_dtype = test_data()
585+
pipeline = OpNotSupportedPipeline[input_t1](
586+
Cast(new_dtype),
587+
(test_tensor,),
588+
{
589+
"executorch_exir_dialects_edge__ops_dim_order_ops__to_dim_order_copy_default": 1
590+
},
591+
quantize=True,
592+
)
593+
pipeline.run()
594+
595+
507596
_TO_COPY_DATA_INT_U55_REJECT = {
508597
"rand_bool_int8": lambda: (
509598
torch.randint(0, 2, (1, 2, 3, 4), dtype=torch.bool),

0 commit comments

Comments
 (0)