Skip to content

Commit cafbb3a

Browse files
committed
Arm backend: Support quantizing no-op fp casts.
If a int to fp cast goes directly into a quantized operator, the cast is a noop if it is quantized with unit quantization parameters (scale=1, zp=0). This means it can be handled, even if the backend doesn't support fp. - Add Fixed unit qparam annotation to such a cast. - Allow it in the partitioner. - Finally, make sure the cast dtype is correct after folding quant nodes. This pattern can appear in the wild in a model, or in a decomposition where you want to quantize an integer tensor. Signed-off-by: Erik Lundell <erik.lundell@arm.com> Change-Id: I93bfd17ba51e25f121f61cbe3003e9c6b9891401
1 parent 3f3b6e7 commit cafbb3a

6 files changed

Lines changed: 185 additions & 17 deletions

File tree

backends/arm/_passes/decompose_div_tensor_mode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"logical_xor": torch.ops.aten.logical_xor.default,
4747
"mul": torch.ops.aten.mul.Tensor,
4848
"sub": torch.ops.aten.sub.Tensor,
49-
"to": torch.ops.dim_order_ops._to_dim_order_copy.default,
49+
"to": torch.ops.aten.to.dtype,
5050
"where": torch.ops.aten.where.self,
5151
}
5252

backends/arm/_passes/fold_qdq_with_annotated_qparams_pass.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,25 @@ def is_foldable(node: Node) -> bool:
305305
return False
306306
return True
307307

308+
@staticmethod
309+
def _correct_output_dtype(node: torch.fx.Node):
310+
if node.target not in {
311+
exir_ops.edge.aten.sum.dim_IntList,
312+
exir_ops.edge.dim_order_ops._to_dim_order_copy.default,
313+
}:
314+
return
315+
if len(node.meta["output_qparams"]) == 0:
316+
return
317+
output_qparams = cast(QuantArgs, node.meta["output_qparams"][0])
318+
319+
if node.target == exir_ops.edge.dim_order_ops._to_dim_order_copy.default:
320+
if output_qparams.scale != 1.0 or output_qparams.zp != 0.0:
321+
raise ValueError(
322+
f"Expected quantized {node.target} '{node.name}' to have unit scale and zero point."
323+
)
324+
325+
set_node_arg(node, "dtype", output_qparams.dtype)
326+
308327
def call(self, graph_module: GraphModule) -> PassResult: # noqa: C901
309328

310329
# Loop over the graph nodes and find any node in the 'targeted_ops' list.
@@ -355,13 +374,7 @@ def call(self, graph_module: GraphModule) -> PassResult: # noqa: C901
355374

356375
# Some op(s) contain a "dtype" key in their node kwargs. Set this
357376
# to the type of output qparams.
358-
output_qparams = n.meta["output_qparams"]
359-
if (
360-
n.target in {exir_ops.edge.aten.sum.dim_IntList}
361-
and len(output_qparams) > 0
362-
):
363-
output_dtype = output_qparams[0].dtype
364-
set_node_arg(n, "dtype", output_dtype)
377+
FoldAndAnnotateQParamsPass._correct_output_dtype(n)
365378

366379
if n.target in (
367380
torch.ops.higher_order.cond,

backends/arm/operator_support/to_dim_order_copy_support.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,20 @@ def _merge_supported_types(
139139
torch.float8_e5m2: [torch.bfloat16],
140140
}
141141

142+
@staticmethod
143+
def _is_quantized_identity_cast(node: torch.fx.Node) -> bool:
144+
for user in node.users:
145+
if (
146+
not user.target
147+
== exir_ops.edge.quantized_decomposed.quantize_per_tensor.default
148+
):
149+
return False
150+
scale = user.args[1]
151+
zp = user.args[2]
152+
if scale != 1.0 or zp != 0.0:
153+
return False
154+
return True
155+
142156
def is_node_tosa_supported( # noqa: C901
143157
self, node: fx.Node, tosa_spec: TosaSpecification
144158
) -> bool:
@@ -228,6 +242,11 @@ def is_node_tosa_supported( # noqa: C901
228242
)
229243
return False
230244
if output_val.dtype not in supported_dtypes[input_dtype]:
245+
if (
246+
tosa_spec.support_integer()
247+
and ToCopySupported._is_quantized_identity_cast(node)
248+
):
249+
return True
231250
self.reporter.report_reject(
232251
node,
233252
(

backends/arm/quantizer/quantization_annotator.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,27 @@ def any_or_hardtanh_min_zero(n: Node):
956956
shared_qspec = SharedQuantizationSpec(input_node)
957957
quant_properties.quant_inputs = [_QuantProperty(0, shared_qspec)]
958958
quant_properties.quant_output = _QuantProperty(0, shared_qspec)
959+
elif node.target == torch.ops.aten.to.dtype:
960+
# If we quantize a cast(fp32) with unit scale and same dtype as input, we can handle it as a no-op in the backend.
961+
input_val = node.all_input_nodes[0].meta.get("val", None)
962+
if input_val is None:
963+
return None
964+
965+
if input_val.dtype not in (torch.int8, torch.int16, torch.int32):
966+
return None
967+
968+
quant_properties.quant_output = _QuantProperty(
969+
0,
970+
FixedQParamsQuantizationSpec(
971+
dtype=input_val.dtype,
972+
scale=1.0,
973+
zero_point=0,
974+
quant_max=torch.iinfo(input_val.dtype).max,
975+
quant_min=torch.iinfo(input_val.dtype).min,
976+
qscheme=torch.per_tensor_symmetric,
977+
is_dynamic=False,
978+
),
979+
)
959980
elif node.target in (
960981
torch.ops.higher_order.cond,
961982
torch.ops.higher_order.while_loop,

backends/arm/test/ops/test_div_tensor_mode.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ def test_div_tensor_mode_u85_INT(data):
172172
exir_ops=[],
173173
use_to_edge_transform_and_lower=True,
174174
)
175-
pipeline.tester.use_portable_ops = True
176175
pipeline.run()
177176

178177

backends/arm/test/ops/test_to_copy.py

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121

2222
input_t1 = Tuple[torch.Tensor] # Input x
23+
input_t2 = Tuple[torch.Tensor, torch.Tensor] # Input x, y
2324

2425

2526
class Cast(torch.nn.Module):
@@ -40,6 +41,40 @@ def forward(self, x: torch.Tensor):
4041
return x.to(dtype=self.target_dtype) + x.to(dtype=self.target_dtype)
4142

4243

44+
class CastAddTensor(torch.nn.Module):
45+
def __init__(self, target_dtype):
46+
super().__init__()
47+
self.target_dtype = target_dtype
48+
49+
def forward(self, x: torch.Tensor, y: torch.Tensor):
50+
return x.to(dtype=self.target_dtype) + y
51+
52+
53+
class AddModule(torch.nn.Module):
54+
def forward(self, x: torch.Tensor, y: torch.Tensor):
55+
return x + y
56+
57+
58+
class CastToAddModule(torch.nn.Module):
59+
def __init__(self, target_dtype):
60+
super().__init__()
61+
self.target_dtype = target_dtype
62+
self.add = AddModule()
63+
64+
def forward(self, x: torch.Tensor, y: torch.Tensor):
65+
return self.add(x.to(dtype=self.target_dtype), y)
66+
67+
68+
class CastCatTensor(torch.nn.Module):
69+
def __init__(self, target_dtype, dim: int):
70+
super().__init__()
71+
self.target_dtype = target_dtype
72+
self.dim = dim
73+
74+
def forward(self, x: torch.Tensor, y: torch.Tensor):
75+
return torch.cat((x.to(dtype=self.target_dtype), y), dim=self.dim)
76+
77+
4378
"""
4479
Tests the _to_copy operation.
4580
@@ -262,14 +297,6 @@ def test_to_vgf_no_quant(test_data: Tuple):
262297
in ToCopySupported::is_node_tosa_supported() before it goes into the delegated graph.
263298
"""
264299
_TO_COPY_TEST_DATA_INT = {
265-
"rand_int8_fp32": lambda: (
266-
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int8),
267-
torch.float32,
268-
),
269-
"rand_int16_fp32": lambda: (
270-
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int16),
271-
torch.float32,
272-
),
273300
"rand_int32_fp32": lambda: (
274301
torch.randint(-127, 128, (1, 2, 3, 4), dtype=torch.int32),
275302
torch.float32,
@@ -300,6 +327,95 @@ def test_to_tosa_INT_not_delegated(test_data: Tuple):
300327
pipeline.run()
301328

302329

330+
_TO_COPY_QUANTIZED_IDENTITY_CAST_DATA = {
331+
"int8_cast_add": lambda: (
332+
(torch.randn(1, 3, 4, 4) * 10).to(dtype=torch.int8),
333+
torch.randn(1, 3, 4, 4),
334+
torch.float32,
335+
),
336+
"int16_cast_add": lambda: (
337+
(torch.randn(1, 3, 4, 4) * 10).to(dtype=torch.int16),
338+
torch.randn(1, 3, 4, 4),
339+
torch.float32,
340+
),
341+
"int32_cast_add": lambda: (
342+
(torch.randn(1, 3, 4, 4) * 10).to(dtype=torch.int32),
343+
torch.randn(1, 3, 4, 4),
344+
torch.float32,
345+
),
346+
}
347+
348+
349+
_TO_COPY_QUANTIZED_IDENTITY_CAST_CAT_DATA = {
350+
"int8_cast_cat": lambda: (
351+
(torch.randn(1, 2, 4, 4) * 10).to(dtype=torch.int8),
352+
torch.randn(1, 2, 4, 1),
353+
torch.float32,
354+
3,
355+
),
356+
"int16_cast_cat": lambda: (
357+
(torch.randn(1, 2, 4, 4) * 10).to(dtype=torch.int16),
358+
torch.randn(1, 2, 4, 1),
359+
torch.float32,
360+
3,
361+
),
362+
}
363+
364+
365+
@common.parametrize("test_data", _TO_COPY_QUANTIZED_IDENTITY_CAST_DATA)
366+
def test_to_tosa_INT_quantized_identity_cast_add(test_data: Tuple):
367+
x, y, new_dtype = test_data()
368+
pipeline = TosaPipelineINT[input_t2](
369+
CastAddTensor(new_dtype),
370+
(x, y),
371+
aten_op=["torch.ops.aten.add.Tensor"],
372+
exir_op=["executorch_exir_dialects_edge__ops_aten_add_Tensor"],
373+
qtol=1,
374+
)
375+
pipeline.change_args(
376+
"check_count.exir",
377+
{
378+
"torch.ops.higher_order.executorch_call_delegate": 1,
379+
},
380+
)
381+
pipeline.run()
382+
383+
384+
@common.parametrize("test_data", _TO_COPY_QUANTIZED_IDENTITY_CAST_CAT_DATA)
385+
def test_to_tosa_INT_quantized_identity_cast_cat(test_data: Tuple):
386+
x, y, new_dtype, dim = test_data()
387+
pipeline = TosaPipelineINT[input_t2](
388+
CastCatTensor(new_dtype, dim),
389+
(x, y),
390+
aten_op=["torch.ops.aten.cat.default"],
391+
exir_op=["executorch_exir_dialects_edge__ops_aten_cat_default"],
392+
)
393+
pipeline.run()
394+
395+
396+
@common.parametrize("test_data", _TO_COPY_QUANTIZED_IDENTITY_CAST_DATA)
397+
def test_to_tosa_INT_quantized_identity_cast_to_unquantized_add_delegated(
398+
test_data: Tuple,
399+
):
400+
x, y, new_dtype = test_data()
401+
pipeline = TosaPipelineINT[input_t2](
402+
CastToAddModule(new_dtype),
403+
(x, y),
404+
aten_op=["torch.ops.aten.add.Tensor"],
405+
exir_op=["executorch_exir_dialects_edge__ops_aten_add_Tensor"],
406+
)
407+
pipeline.quantizer.set_module_name("add", None)
408+
pipeline.pop_stage("check_not.exir")
409+
pipeline.change_args(
410+
"check_count.exir",
411+
{
412+
"torch.ops.higher_order.executorch_call_delegate": 1,
413+
"executorch_exir_dialects_edge__ops_dim_order_ops__to_dim_order_copy_default": 0,
414+
},
415+
)
416+
pipeline.run()
417+
418+
303419
@common.parametrize("test_data", _TO_COPY_TEST_DATA_INT)
304420
@common.SkipIfNoModelConverter
305421
def test_to_vgf_quant(test_data: Tuple):

0 commit comments

Comments
 (0)