Skip to content

Commit d7157b7

Browse files
pssrawatfacebook-github-bot
authored andcommitted
Precompute quantized constant ops after Q/DQ folding (#24258)
Summary: Q/DQ folding (`FoldAndAnnotateQParamsPass`) always folds the quantize/dequantize pair around a constant-producing op (`full`, `full_like`, `arange`, `eye`, `linspace`, `scalar_tensor`) into the op: it removes the Q node and records the output qparams, but the op itself keeps producing float. The Arm backend follows folding with `ComputeConstantOpsAOTPass`, which evaluates such ops in float and quantizes the result. Cortex-M did not, so in `x + torch.full(shape, 0.5)` the int8 `quantized_add` kernel read a float32 constant, and the output was silently wrong. Add `ComputeQuantizedConstantOpsPass` and run it right after folding on both Cortex-M pass lists. - a. It only touches nodes with output qparams, so unquantized constant ops are unchanged. A quantized `full` stays a runtime fill, with its fill value quantized and its dtype set to the qparams' dtype, so a large fill does not turn into a large constant; - b.`full_like` is first converted to `full` with the Arm backend's `ConvertFullLikeToFullPass`. - c. Since `ComputeConstantOpsAOTPass` only logs a failed precompute, the new pass raises if a quantized constant op is still in the graph afterwards, as it would feed float data to an int8 kernel. Differential Revision: D128002647
1 parent 42331d0 commit d7157b7

4 files changed

Lines changed: 157 additions & 3 deletions

File tree

‎backends/cortex_m/passes/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fbcode_target(_kind = runtime.python_library,
3131
"activation_fusion_pass.py",
3232
"aten_to_cortex_m_pass.py",
3333
"clamp_hardswish_pass.py",
34+
"compute_quantized_constant_ops_pass.py",
3435
"cortex_m_pass.py",
3536
"cortex_m_pass_manager.py",
3637
"decompose_hardswish_pass.py",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import torch
8+
from executorch.backends.arm._passes import (
9+
ComputeConstantOpsAOTPass,
10+
ConvertFullLikeToFullPass,
11+
)
12+
from executorch.exir.dialects._ops import ops as exir_ops
13+
from executorch.exir.pass_base import PassResult
14+
from torch.fx import GraphModule
15+
16+
17+
def _is_quantized_fill(node: torch.fx.Node) -> bool:
18+
return (
19+
node.target == exir_ops.edge.aten.full.default
20+
and node.kwargs.get("dtype") == node.meta["output_qparams"][0].dtype
21+
)
22+
23+
24+
class ComputeQuantizedConstantOpsPass(ComputeConstantOpsAOTPass):
25+
"""Give the quantized constant ops their int8 values.
26+
27+
Folding removes the Q node after these ops but leaves them producing float,
28+
so the int8 kernels they feed would read float data. A ``full`` stays a
29+
runtime fill with its fill value quantized, so it adds no constant data; the
30+
other ops are computed in float and stored as quantized constants.
31+
``full_like`` is converted to ``full`` first and handled the same way.
32+
"""
33+
34+
def compute_node_aot(self, node: torch.fx.Node) -> bool:
35+
output_qparams = node.meta.get("output_qparams")
36+
if not output_qparams:
37+
return False
38+
if node.target == exir_ops.edge.aten.full.default:
39+
qparams = output_qparams[0]
40+
fill = qparams.quantize_value(torch.tensor(float(node.args[1])))
41+
node.args = (node.args[0], int(fill.item()))
42+
node.kwargs = {"dtype": qparams.dtype}
43+
return True
44+
return super().compute_node_aot(node)
45+
46+
def call(self, graph_module: GraphModule) -> PassResult:
47+
converted = ConvertFullLikeToFullPass()(graph_module)
48+
result = super().call(converted.graph_module)
49+
# The base pass only logs a failure; a quantized constant left behind
50+
# would feed float data to an int8 kernel without any error.
51+
remaining = [
52+
node.name
53+
for node in result.graph_module.graph.nodes
54+
if node.op == "call_function"
55+
and node.target in self.targeted_ops
56+
and node.meta.get("output_qparams")
57+
and not _is_quantized_fill(node)
58+
]
59+
if remaining:
60+
raise RuntimeError(
61+
f"Could not precompute quantized constant ops: {remaining}"
62+
)
63+
return PassResult(result.graph_module, converted.modified or result.modified)

‎backends/cortex_m/passes/cortex_m_pass_manager.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from .activation_fusion_pass import ActivationFusionPass
4949
from .aten_to_cortex_m_pass import AtenToCortexMPass
5050
from .clamp_hardswish_pass import ClampHardswishPass
51+
from .compute_quantized_constant_ops_pass import ComputeQuantizedConstantOpsPass
5152
from .decompose_hardswish_pass import DecomposeHardswishPass
5253
from .decompose_mean_pass import DecomposeMeanPass
5354
from .decompose_sdpa_pass import DecomposeSDPAPass
@@ -149,6 +150,7 @@ class CortexMPassManager(ExportedProgramPassManager):
149150
# Retrace scalar ops while the graph still has floating-point Q/DQ inputs.
150151
ReplaceScalarWithTensorArgPass,
151152
FoldAndAnnotateQParamsPass,
153+
ComputeQuantizedConstantOpsPass,
152154
ActivationFusionPass,
153155
QuantizedClampActivationPass,
154156
DecomposeHardswishPass,
@@ -164,6 +166,7 @@ class CortexMPassManager(ExportedProgramPassManager):
164166
explicit_layout_pass_list: list[PassClass] = [
165167
RemoveGetItemPass,
166168
FoldAndAnnotateQParamsPass,
169+
ComputeQuantizedConstantOpsPass,
167170
ReplaceScalarWithTensorArgPass,
168171
ActivationFusionPass,
169172
QuantizedClampActivationPass,

‎backends/cortex_m/test/test_explicit_layout_pipeline.py‎

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import pytest
1111
import torch
12+
from executorch.backends.arm._passes import ComputeConstantOpsAOTPass
1213
from executorch.backends.cortex_m.passes.cortex_m_pass_manager import (
1314
CortexMPassManager,
1415
LiftConstantTensorsPass,
@@ -552,14 +553,100 @@ def test_convs_around_a_reshape_behind_lut_activation_lower_to_cmsis_nn():
552553
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
553554

554555

555-
def test_explicit_layout_rejects_float_input_to_int8_conv():
556+
def test_explicit_layout_lowers_conv_on_a_quantized_constant():
557+
inputs = (torch.randn(1, 8, 16),)
556558
model = ThenConv1d(lambda x: torch.full_like(x, 0.5, dtype=torch.float32))
557-
tester = CortexMTester(model.eval(), (torch.randn(1, 8, 16),))
559+
tester = _run_explicit_layout_passes(CortexMTester(model.eval(), inputs))
560+
program = tester.get_artifact(StageType.RUN_PASSES).exported_program()
561+
562+
assert _count(program, exir_ops.edge.aten.full_like.default) == 0
563+
assert _count(program, exir_ops.edge.aten.full.default) == 1
564+
assert _count(program, exir_ops.edge.cortex_m.quantized_conv2d_nhwc.default) == 1
565+
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
566+
567+
568+
@pytest.mark.parametrize("use_explicit_layout", [False, True])
569+
@pytest.mark.parametrize(
570+
"constant,constant_op,shape",
571+
[
572+
(
573+
lambda: torch.arange(-1.0, 1.0, 0.125),
574+
exir_ops.edge.aten.arange.start_step,
575+
(16,),
576+
),
577+
(
578+
lambda: torch.scalar_tensor(0.5),
579+
exir_ops.edge.aten.scalar_tensor.default,
580+
(),
581+
),
582+
],
583+
ids=["arange", "scalar_tensor"],
584+
)
585+
def test_quantized_constant_is_computed_before_quantizing(
586+
use_explicit_layout, constant, constant_op, shape
587+
):
588+
torch.manual_seed(0)
589+
inputs = (torch.randn(shape),)
590+
tester = CortexMTester(Apply(lambda x: x + constant()).eval(), inputs)
591+
tester.quantize(Quantize(CortexMQuantizer(use_explicit_layout=use_explicit_layout)))
592+
tester.export().to_edge()
593+
program = _run_passes(tester, use_explicit_layout)
594+
595+
assert _count(program, constant_op) == 0
596+
assert _count(program, exir_ops.edge.cortex_m.quantized_add.default) == 1
597+
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
598+
599+
600+
@pytest.mark.parametrize("use_explicit_layout", [False, True])
601+
@pytest.mark.parametrize(
602+
"fill",
603+
[lambda x: torch.full((1, 8, 16), 0.5), lambda x: torch.full_like(x, 0.5)],
604+
ids=["full", "full_like"],
605+
)
606+
def test_quantized_fill_stays_a_runtime_int8_full(use_explicit_layout, fill):
607+
torch.manual_seed(0)
608+
inputs = (torch.randn(1, 8, 16),)
609+
tester = CortexMTester(Apply(lambda x: x + fill(x)).eval(), inputs)
610+
tester.quantize(Quantize(CortexMQuantizer(use_explicit_layout=use_explicit_layout)))
611+
tester.export().to_edge()
612+
program = _run_passes(tester, use_explicit_layout)
613+
614+
(full,) = program.graph.find_nodes(
615+
op="call_function", target=exir_ops.edge.aten.full.default
616+
)
617+
assert full.meta["val"].dtype == torch.int8
618+
assert _count(program, exir_ops.edge.aten.full_like.default) == 0
619+
assert _count(program, exir_ops.edge.cortex_m.quantized_add.default) == 1
620+
tester.run_method_and_compare_outputs(inputs=inputs, qtol=1)
621+
622+
623+
def test_failed_constant_precompute_is_an_error(monkeypatch):
624+
def fail(self, node):
625+
raise ValueError("cannot evaluate")
626+
627+
monkeypatch.setattr(ComputeConstantOpsAOTPass, "compute_node_aot", fail)
628+
tester = CortexMTester(
629+
Apply(lambda x: x + torch.arange(-1.0, 1.0, 0.125)).eval(),
630+
(torch.randn(16),),
631+
)
558632

559633
with pytest.raises(Exception) as caught:
560634
_run_explicit_layout_passes(tester)
561635

562636
error = caught.value
563637
while error.__cause__ is not None:
564638
error = error.__cause__
565-
assert "must move an int8 tensor" in str(error)
639+
assert "Could not precompute quantized constant ops" in str(error)
640+
641+
642+
@pytest.mark.parametrize("use_explicit_layout", [False, True])
643+
def test_unquantized_constant_is_not_precomputed(use_explicit_layout):
644+
inputs = (torch.randn(1, 8, 16),)
645+
tester = CortexMTester(
646+
Apply(lambda x: x + torch.full((1, 8, 16), 0.5)).eval(), inputs
647+
)
648+
tester.export().to_edge()
649+
program = _run_passes(tester, use_explicit_layout)
650+
651+
assert _count(program, exir_ops.edge.aten.full.default) == 1
652+
tester.run_method_and_compare_outputs(inputs=inputs)

0 commit comments

Comments
 (0)