Skip to content

Commit 052abb1

Browse files
ethansfngfacebook-github-bot
authored andcommitted
Add fuse() to remaining QuantizationPatterns (pytorch#19727)
Summary: Add `fuse()` implementations to the remaining Cadence `QuantizationPattern` subclasses: - `MaxPool2dPattern`, `MaxPool2dWithoutIndicesPattern` — order-preserving pool on quantized values - `ReluBasePattern` (inherited by `ReluPattern0`/`1`) — relu with requantization - `ConvReluBasePattern` (inherited by `Conv1d`/`2dReluPattern0`/`1`) — conv+relu fusion with `anchor_ops()` override to match only the conv op - `SoftmaxPattern` — softmax with dummy mask/pos tensors and fake_mode metadata - `MixedW8A32LinearPattern` — weight-only quantized linear (no input/output quant) - `MixedW8A32ConvPattern` — weight-only quantized conv1d with NCL→NLC permutation - `MixedW8A32GruPattern` — weight-only quantized GRU with 4 dequantized params Differential Revision: D105728177
1 parent 646c9b0 commit 052abb1

1 file changed

Lines changed: 298 additions & 10 deletions

File tree

backends/cadence/aot/quantizer/patterns.py

Lines changed: 298 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import List, Optional, Tuple, Union
1313

1414
import torch
15+
from executorch.backends.cadence.aot.compiler_utils import get_shape
1516
from executorch.backends.cadence.aot.pass_utils import get_arg
1617
from executorch.backends.cadence.aot.quantizer.pattern_utils import (
1718
DQ_PER_TENSOR,
@@ -25,6 +26,7 @@
2526
check_out_zero_point_is_min_range,
2627
copy_node_metadata,
2728
get_bias_qparams,
29+
quantize_tensor_multiplier,
2830
)
2931
from torch import fx
3032
from torch._ops import OpOverload
@@ -321,9 +323,8 @@ def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
321323
quant_node = find_quant_user(relu_node)
322324
if quant_node is None:
323325
return None
324-
check_out_zero_point_is_min_range(
325-
quant_node.args[2], quant_node.args[5] # pyre-ignore[6]
326-
)
326+
# pyre-ignore[6]: Argument -> int/dtype narrowing
327+
check_out_zero_point_is_min_range(quant_node.args[2], quant_node.args[5])
327328
args = (
328329
dq0.args[0],
329330
dq0.args[1],
@@ -724,13 +725,11 @@ def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
724725
quant_node = find_quant_user(anchor_node)
725726
if quant_node is None:
726727
return None
727-
dq_bias = (
728-
anchor_node.args[2]
729-
if len(anchor_node.args) > 2
730-
and isinstance(anchor_node.args[2], fx.Node)
731-
and anchor_node.args[2].target == DQ_PER_TENSOR
732-
else None
733-
)
728+
dq_bias: Optional[fx.Node] = None
729+
if len(anchor_node.args) > 2:
730+
bias_arg = anchor_node.args[2]
731+
if isinstance(bias_arg, fx.Node) and bias_arg.target == DQ_PER_TENSOR:
732+
dq_bias = bias_arg
734733
return fuse_linear(
735734
gm,
736735
dq_input,
@@ -836,6 +835,30 @@ def get_anchors(
836835
def replacement_op(self) -> OpOverload:
837836
return torch.ops.cadence.quantized_max_pool2d_nchw.default
838837

838+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
839+
dq_input = anchor_node.args[0]
840+
if not isinstance(dq_input, fx.Node) or dq_input.target != DQ_PER_TENSOR:
841+
return None
842+
quant_node = find_quant_user(anchor_node)
843+
if quant_node is None:
844+
return None
845+
kernel_size = get_arg(anchor_node, "kernel_size", Optional[list[int]]) or [1, 1]
846+
stride = get_arg(anchor_node, "stride", Optional[list[int]]) or kernel_size
847+
padding = get_arg(anchor_node, "padding", Optional[list[int]]) or [0, 0]
848+
dilation = get_arg(anchor_node, "dilation", Optional[list[int]]) or [1, 1]
849+
ceil_mode = get_arg(anchor_node, "ceil_mode", Optional[bool]) or False
850+
args = (dq_input.args[0],)
851+
kwargs = {
852+
"kernel_size": kernel_size,
853+
"stride": stride,
854+
"padding": padding,
855+
"dilation": dilation,
856+
"ceil_mode": ceil_mode,
857+
}
858+
return replace_with_op(
859+
gm, anchor_node, self.replacement_op(), args, kwargs, quant_node
860+
)
861+
839862

840863
class MaxPool2dWithoutIndicesPattern(QuantizationPattern):
841864
"""
@@ -875,6 +898,30 @@ def get_anchors(
875898
def replacement_op(self) -> OpOverload:
876899
return torch.ops.cadence.quantized_max_pool2d_nchw.default
877900

901+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
902+
dq_input = anchor_node.args[0]
903+
if not isinstance(dq_input, fx.Node) or dq_input.target != DQ_PER_TENSOR:
904+
return None
905+
quant_node = find_quant_user(anchor_node)
906+
if quant_node is None:
907+
return None
908+
kernel_size = get_arg(anchor_node, "kernel_size", Optional[list[int]]) or [1, 1]
909+
stride = get_arg(anchor_node, "stride", Optional[list[int]]) or kernel_size
910+
padding = get_arg(anchor_node, "padding", Optional[list[int]]) or [0, 0]
911+
dilation = get_arg(anchor_node, "dilation", Optional[list[int]]) or [1, 1]
912+
ceil_mode = get_arg(anchor_node, "ceil_mode", Optional[bool]) or False
913+
args = (dq_input.args[0],)
914+
kwargs = {
915+
"kernel_size": kernel_size,
916+
"stride": stride,
917+
"padding": padding,
918+
"dilation": dilation,
919+
"ceil_mode": ceil_mode,
920+
}
921+
return replace_with_op(
922+
gm, anchor_node, self.replacement_op(), args, kwargs, quant_node
923+
)
924+
878925

879926
# This is a base class for ReLU
880927

@@ -904,6 +951,29 @@ def get_anchors(
904951
def replacement_op(self) -> OpOverload:
905952
return torch.ops.cadence.quantized_relu.per_tensor
906953

954+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
955+
dq_input = anchor_node.args[0]
956+
if not isinstance(dq_input, fx.Node) or dq_input.target != DQ_PER_TENSOR:
957+
return None
958+
quant_node = find_quant_user(anchor_node)
959+
if quant_node is None:
960+
return None
961+
input_scale = dq_input.args[1]
962+
# pyre-fixme[58]
963+
requantize_scale = input_scale / quant_node.args[1]
964+
requantize_scale_t = torch.tensor([requantize_scale])
965+
out_multiplier, out_shift = quantize_tensor_multiplier(requantize_scale_t)
966+
args = (dq_input.args[0],)
967+
kwargs = {
968+
"X_zero_point": dq_input.args[2],
969+
"out_zero_point": quant_node.args[2],
970+
"out_multiplier": out_multiplier[0].item(),
971+
"out_shift": out_shift[0].item(),
972+
}
973+
return replace_with_op(
974+
gm, anchor_node, self.replacement_op(), args, kwargs, quant_node
975+
)
976+
907977

908978
# Regular relu op
909979
class ReluPattern0(ReluBasePattern):
@@ -963,6 +1033,37 @@ def get_anchors(
9631033
def replacement_op(self) -> OpOverload:
9641034
return torch.ops.cadence.quantized_conv2d_nchw.per_tensor
9651035

1036+
def anchor_ops(self) -> tuple[OpOverload, ...]:
1037+
return (self.partition_types()[0],)
1038+
1039+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
1040+
conv_users = list(anchor_node.users)
1041+
if len(conv_users) != 1:
1042+
return None
1043+
relu_node = conv_users[0]
1044+
if relu_node.target != self.partition_types()[1]:
1045+
return None
1046+
_arg0 = anchor_node.args[0]
1047+
dq_input = (
1048+
_arg0
1049+
if isinstance(_arg0, fx.Node) and _arg0.target == DQ_PER_TENSOR
1050+
else None
1051+
)
1052+
_arg1 = anchor_node.args[1]
1053+
dq_weight = (
1054+
_arg1
1055+
if isinstance(_arg1, fx.Node) and _arg1.target == DQ_PER_TENSOR
1056+
else None
1057+
)
1058+
if dq_input is None or dq_weight is None:
1059+
return None
1060+
quant_node = find_quant_user(relu_node)
1061+
if quant_node is None:
1062+
return None
1063+
# pyre-ignore[6]: Argument -> int/dtype narrowing
1064+
check_out_zero_point_is_min_range(quant_node.args[2], quant_node.args[5])
1065+
return fuse_conv(self, gm, anchor_node, dq_input, dq_weight, quant_node)
1066+
9661067

9671068
# Conv1d + regular relu op fusion
9681069
class Conv1dReluPattern0(ConvReluBasePattern):
@@ -1017,6 +1118,52 @@ def get_anchors(
10171118
def replacement_op(self) -> OpOverload:
10181119
return torch.ops.cadence.quantized_softmax.per_tensor
10191120

1121+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
1122+
dq_input = anchor_node.args[0]
1123+
if not isinstance(dq_input, fx.Node) or dq_input.target != DQ_PER_TENSOR:
1124+
return None
1125+
quant_node = find_quant_user(anchor_node)
1126+
if quant_node is None:
1127+
return None
1128+
input_q = dq_input.args[0]
1129+
assert isinstance(input_q, fx.Node)
1130+
quant_input = quant_node.args[0]
1131+
assert isinstance(quant_input, fx.Node)
1132+
mask_shape = get_shape(gm, quant_input)
1133+
mask_shape = list(mask_shape) if mask_shape else []
1134+
mask_shape[-1] = mask_shape[-1] // 16
1135+
with gm.graph.inserting_before(anchor_node):
1136+
mask_tensor = gm.graph.call_function(
1137+
torch.ops.aten.full.default, (mask_shape, 0.0), {"dtype": torch.int32}
1138+
)
1139+
assert "val" in input_q.meta
1140+
fake_mode = input_q.meta["val"].fake_mode
1141+
assert fake_mode is not None
1142+
with fake_mode:
1143+
mask_tensor.meta["val"] = torch.full(mask_shape, 0.0, dtype=torch.int32)
1144+
copy_node_metadata(mask_tensor, input_q)
1145+
with gm.graph.inserting_before(anchor_node):
1146+
pos_tensor = gm.graph.call_function(
1147+
torch.ops.aten.full.default, ([1], 0), {"dtype": torch.int64}
1148+
)
1149+
with fake_mode:
1150+
pos_tensor.meta["val"] = torch.full([1], 0, dtype=torch.int64)
1151+
copy_node_metadata(pos_tensor, input_q)
1152+
args = (
1153+
input_q,
1154+
mask_tensor,
1155+
get_arg(anchor_node, "dim", int),
1156+
0,
1157+
pos_tensor,
1158+
dq_input.args[1],
1159+
dq_input.args[2],
1160+
quant_node.args[1],
1161+
quant_node.args[2],
1162+
)
1163+
return replace_with_op(
1164+
gm, anchor_node, self.replacement_op(), args, {}, quant_node
1165+
)
1166+
10201167

10211168
class MixedW8A32LinearPattern(QuantizationPattern):
10221169
def partition_types(self) -> List[OpOverload]:
@@ -1071,6 +1218,38 @@ def get_anchors(
10711218
def replacement_op(self) -> OpOverload:
10721219
return torch.ops.cadence.quantized_w8a32_linear.default
10731220

1221+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
1222+
if len(anchor_node.args) != 3 or len(anchor_node.kwargs) > 0:
1223+
return None
1224+
_arg1 = anchor_node.args[1]
1225+
dq_weight = (
1226+
_arg1
1227+
if isinstance(_arg1, fx.Node) and _arg1.target == DQ_PER_TENSOR
1228+
else None
1229+
)
1230+
_arg2 = anchor_node.args[2]
1231+
dq_bias = (
1232+
_arg2
1233+
if isinstance(_arg2, fx.Node) and _arg2.target == DQ_PER_TENSOR
1234+
else None
1235+
)
1236+
if dq_weight is None or dq_bias is None:
1237+
return None
1238+
input_node = anchor_node.args[0]
1239+
assert isinstance(input_node, fx.Node)
1240+
args = (
1241+
input_node,
1242+
dq_weight.args[0],
1243+
dq_weight.args[1],
1244+
dq_bias.args[0],
1245+
dq_bias.args[1],
1246+
)
1247+
with gm.graph.inserting_after(anchor_node):
1248+
fused = gm.graph.call_function(self.replacement_op(), args, {})
1249+
fused.meta = anchor_node.meta
1250+
anchor_node.replace_all_uses_with(fused)
1251+
return fused
1252+
10741253

10751254
class MixedW8A32ConvPattern(QuantizationPattern):
10761255
def partition_types(self) -> List[OpOverload]:
@@ -1145,6 +1324,78 @@ def get_anchors(
11451324
def replacement_op(self) -> OpOverload:
11461325
return torch.ops.cadence.quantized_w8a32_conv.default
11471326

1327+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
1328+
if len(anchor_node.args) != 3 or len(anchor_node.kwargs) > 0:
1329+
return None
1330+
_arg1 = anchor_node.args[1]
1331+
dq_weight = (
1332+
_arg1
1333+
if isinstance(_arg1, fx.Node) and _arg1.target == DQ_PER_TENSOR
1334+
else None
1335+
)
1336+
_arg2 = anchor_node.args[2]
1337+
dq_bias = (
1338+
_arg2
1339+
if isinstance(_arg2, fx.Node) and _arg2.target == DQ_PER_TENSOR
1340+
else None
1341+
)
1342+
if dq_weight is None or dq_bias is None:
1343+
return None
1344+
input_node = anchor_node.args[0]
1345+
assert isinstance(input_node, fx.Node)
1346+
assert get_arg(anchor_node, "stride", list[int]) == [1]
1347+
assert get_arg(anchor_node, "padding", list[int]) == [0]
1348+
assert get_arg(anchor_node, "dilation", list[int]) == [1]
1349+
assert get_arg(anchor_node, "groups", int) == 1
1350+
weight_q = dq_weight.args[0]
1351+
assert isinstance(weight_q, fx.Node)
1352+
with gm.graph.inserting_before(anchor_node):
1353+
transposed_inputs = gm.graph.call_function(
1354+
torch.ops.aten.permute.default, (input_node, [0, 2, 1])
1355+
)
1356+
if "val" in input_node.meta:
1357+
original_val = input_node.meta["val"]
1358+
fake_mode = original_val.fake_mode
1359+
if fake_mode is not None:
1360+
with fake_mode:
1361+
transposed_inputs.meta["val"] = torch.ops.aten.permute.default(
1362+
original_val, [0, 2, 1]
1363+
)
1364+
else:
1365+
transposed_inputs.meta["val"] = torch.ops.aten.permute.default(
1366+
original_val, [0, 2, 1]
1367+
)
1368+
copy_node_metadata(transposed_inputs, input_node)
1369+
with gm.graph.inserting_before(anchor_node):
1370+
transposed_weights = gm.graph.call_function(
1371+
torch.ops.aten.permute.default, (weight_q, [2, 0, 1])
1372+
)
1373+
if "val" in weight_q.meta:
1374+
original_val = weight_q.meta["val"]
1375+
fake_mode = original_val.fake_mode
1376+
if fake_mode is not None:
1377+
with fake_mode:
1378+
transposed_weights.meta["val"] = torch.ops.aten.permute.default(
1379+
original_val, [2, 0, 1]
1380+
)
1381+
else:
1382+
transposed_weights.meta["val"] = torch.ops.aten.permute.default(
1383+
original_val, [2, 0, 1]
1384+
)
1385+
copy_node_metadata(transposed_weights, weight_q)
1386+
args = (
1387+
transposed_inputs,
1388+
transposed_weights,
1389+
dq_weight.args[1],
1390+
dq_bias.args[0],
1391+
dq_bias.args[1],
1392+
)
1393+
with gm.graph.inserting_after(anchor_node):
1394+
fused = gm.graph.call_function(self.replacement_op(), args, {})
1395+
fused.meta = anchor_node.meta
1396+
anchor_node.replace_all_uses_with(fused)
1397+
return fused
1398+
11481399

11491400
class MixedW8A32GruPattern(QuantizationPattern):
11501401
def partition_types(self) -> List[OpOverload]:
@@ -1217,6 +1468,43 @@ def __init__(self, args, meta):
12171468
def replacement_op(self) -> OpOverload:
12181469
return torch.ops.cadence.quantized_w8a32_gru.default
12191470

1471+
def fuse(self, gm: fx.GraphModule, anchor_node: fx.Node) -> Optional[fx.Node]:
1472+
if len(anchor_node.kwargs) > 0:
1473+
return None
1474+
params = anchor_node.args[2]
1475+
if not isinstance(params, (list, tuple)) or len(params) < 4:
1476+
return None
1477+
dq_w_ih = params[0]
1478+
if not isinstance(dq_w_ih, fx.Node) or dq_w_ih.target != DQ_PER_TENSOR:
1479+
return None
1480+
dq_w_hh = params[1]
1481+
if not isinstance(dq_w_hh, fx.Node) or dq_w_hh.target != DQ_PER_TENSOR:
1482+
return None
1483+
dq_b_ih = params[2]
1484+
if not isinstance(dq_b_ih, fx.Node) or dq_b_ih.target != DQ_PER_TENSOR:
1485+
return None
1486+
dq_b_hh = params[3]
1487+
if not isinstance(dq_b_hh, fx.Node) or dq_b_hh.target != DQ_PER_TENSOR:
1488+
return None
1489+
input_node = anchor_node.args[0]
1490+
hidden_node = anchor_node.args[1]
1491+
args = (
1492+
input_node,
1493+
hidden_node,
1494+
dq_w_ih.args[0],
1495+
dq_w_ih.args[1],
1496+
dq_w_hh.args[0],
1497+
dq_w_hh.args[1],
1498+
dq_b_ih.args[0],
1499+
dq_b_ih.args[1],
1500+
dq_b_hh.args[0],
1501+
)
1502+
with gm.graph.inserting_after(anchor_node):
1503+
fused = gm.graph.call_function(self.replacement_op(), args, {})
1504+
fused.meta = anchor_node.meta
1505+
anchor_node.replace_all_uses_with(fused)
1506+
return fused
1507+
12201508

12211509
class RmsNormPattern(QuantizationPattern):
12221510
"""Pattern that preserves rms_norm from decomposition without matching anything."""

0 commit comments

Comments
 (0)