Skip to content

Commit 37456c1

Browse files
RahulC7facebook-github-bot
authored andcommitted
Don't Fold In Negative Scalar Multiplies Into Quant/Dequant
Summary: ## Context The way we lowered the GDN model is we looked at the traced model, and then recreated "stages" of it(i.e created a graph for that stage), and lowered each one of those stages, making them go through the entire compiler pipeline. That’s usually fine for the ops/nodes in the middle, because they still have the surrounding nodes(producer consumer chain) and those get collapsed/changed in the same way, but in the boundaries, where there’s connections to other other nodes not in the subgraph, the lowering might end up producing a different result than what we expect in the actual graph. The quant/dequant insertion, calibration and FuseMul adjacency depend on the global context so it’ll look different. That's what happens here. ## Problem In D117100989, we do `N_c = -A_c` as `aten.full(-1.0) + aten.mul.Tensor`(elementwise kernel). However, before the multiply, there's a quantize, and so multiply gets combined with the quantize. The issue is that this is a negative, and so we're going to have a negative scale in the quantize/dequantize scales, because of `FuseMulTensorIntoDequantPass` and `FuseMulScalarIntoDequantPass` folding it in. ## What this diff does - Make all three multiply fusion passes stop when the derived scale is not positive. - Leave the original multiply in the graph when fusion is not valid. - Add focused tests for tensor-to-quant, tensor-from-dequant, and scalar-from-dequant with multipliers `0.0` and `-1.0`. - Give the chunk-32 GDN test a test-only gate offset of `-5.0`. This keeps the 32-step cumulative `Gamma_c` values in the useful int16 calibration range. Chunk size 2 stays unchanged. - Use measured tolerances for the three equivalent chunk-32 pre-trace transforms. ## Result The full chunk-32 graph keeps one explicit `aten.mul.Tensor` with `aten.full(-1.0)` for `N_c = -A_c`. All quantize, dequantize, requantize, and packed prep scales in the dumped graph are finite and positive. Reviewed By: ThomasJannaud Differential Revision: D120871768
1 parent bfdfe10 commit 37456c1

2 files changed

Lines changed: 136 additions & 2 deletions

File tree

‎backends/cadence/aot/fuse_ops.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,10 @@ def maybe_remove_or_replace(self, node: torch.fx.Node) -> bool:
839839
assert isinstance(dequant_node.args[1], Number)
840840
assert isinstance(mul_node.args[1], Number)
841841
# pyre-ignore[58]: Unsupported operand *
842-
new_deq_args[1] = dequant_node.args[1] * mul_node.args[1]
842+
new_scale = dequant_node.args[1] * mul_node.args[1]
843+
if new_scale <= 0:
844+
return False
845+
new_deq_args[1] = new_scale
843846

844847
# Replace all uses of mul with the dequant node
845848
mul_node.replace_all_uses_with(dequant_node)
@@ -925,6 +928,8 @@ def maybe_remove_or_replace(self, node: torch.fx.Node) -> bool:
925928
if mul_scalar == 0 or old_scale == 0:
926929
return False
927930
new_scale = float(old_scale) / float(mul_scalar)
931+
if new_scale <= 0:
932+
return False
928933

929934
logging.debug(
930935
f"Fused {node} and {full_node} into {quant_node}. Updated scale from {quant_node.args[1]} to {new_scale}"
@@ -984,7 +989,10 @@ def maybe_remove_or_replace(self, node: torch.fx.Node) -> bool:
984989
assert isinstance(deq_node.args[1], Number)
985990
assert isinstance(mplier_node.args[1], Number)
986991
# pyre-ignore[58]: Unsupported operand *
987-
new_deq_args[1] = deq_node.args[1] * mplier_node.args[1]
992+
new_scale = deq_node.args[1] * mplier_node.args[1]
993+
if new_scale <= 0:
994+
return False
995+
new_deq_args[1] = new_scale
988996

989997
logging.debug(
990998
f"Fused {node} and {mplier_node} into {deq_node}. Updated scale from {deq_node.args[1]} to {new_deq_args[1]}"

‎backends/cadence/aot/tests/test_fusion_ops_passes.py‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,51 @@ def test_fuse_mul_into_dequant(self) -> None:
792792
deq_scale = node.args[1]
793793
self.assertEqual(deq_scale, DEQUANT_SCALE * FULL_VALUE)
794794

795+
@parameterized.expand([("zero", 0.0), ("negative", -1.0)])
796+
def test_keep_non_positive_mul_tensor_after_dequant(
797+
self, _name: str, mul_value: float
798+
) -> None:
799+
input_shape: Final[List[int]] = [4, 32]
800+
dequant_scale = 1.5
801+
802+
builder = GraphBuilder()
803+
x = builder.placeholder(
804+
"x", torch.randint(0, 255, input_shape, dtype=torch.uint8)
805+
)
806+
dequant = builder.call_operator(
807+
op=exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
808+
args=(x, dequant_scale, 0, 0, 255, torch.uint8),
809+
)
810+
full = builder.call_operator(
811+
op=exir_ops.edge.aten.full.default,
812+
args=(input_shape, mul_value),
813+
)
814+
mul = builder.call_operator(
815+
op=exir_ops.edge.aten.mul.Tensor,
816+
args=(dequant, full),
817+
)
818+
builder.output([mul])
819+
820+
result = cast(
821+
PassResult,
822+
FuseMulTensorIntoDequantPass()(builder.get_graph_module()),
823+
)
824+
825+
self.assertFalse(result.modified)
826+
self.check_op_counts(
827+
result.graph_module,
828+
expected_op_counts={
829+
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default: 1,
830+
exir_ops.edge.aten.full.default: 1,
831+
exir_ops.edge.aten.mul.Tensor: 1,
832+
},
833+
)
834+
dequant_nodes = result.graph_module.graph.find_nodes(
835+
op="call_function",
836+
target=exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
837+
)
838+
self.assertEqual(dequant_nodes[0].args[1], dequant_scale)
839+
795840
def test_fuse_mul_into_dequant_no_match(self) -> None:
796841
"""
797842
Test that FuseMulTensorIntoDequantPass does NOT modify the graph
@@ -880,6 +925,45 @@ def test_fuse_mul_scalar_into_dequant(self) -> None:
880925
deq_scale = node.args[1]
881926
self.assertEqual(deq_scale, dequant_scale * mul_value)
882927

928+
@parameterized.expand([("zero", 0.0), ("negative", -1.0)])
929+
def test_keep_non_positive_mul_scalar_after_dequant(
930+
self, _name: str, mul_value: float
931+
) -> None:
932+
dequant_scale = 0.006
933+
934+
builder = GraphBuilder()
935+
x = builder.placeholder(
936+
"x", torch.randint(-128, 127, (2, 3, 4), dtype=torch.int8)
937+
)
938+
dequant = builder.call_operator(
939+
op=exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
940+
args=(x, dequant_scale, 5, -128, 127, torch.int8),
941+
)
942+
mul = builder.call_operator(
943+
op=exir_ops.edge.aten.mul.Scalar,
944+
args=(dequant, mul_value),
945+
)
946+
builder.output([mul])
947+
948+
result = cast(
949+
PassResult,
950+
FuseMulScalarIntoDequantPass()(builder.get_graph_module()),
951+
)
952+
953+
self.assertFalse(result.modified)
954+
self.check_op_counts(
955+
result.graph_module,
956+
expected_op_counts={
957+
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default: 1,
958+
exir_ops.edge.aten.mul.Scalar: 1,
959+
},
960+
)
961+
dequant_nodes = result.graph_module.graph.find_nodes(
962+
op="call_function",
963+
target=exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
964+
)
965+
self.assertEqual(dequant_nodes[0].args[1], dequant_scale)
966+
883967
def test_fuse_mul_into_quant(self) -> None:
884968
quant_scale = 5
885969
mul_value = 10
@@ -931,6 +1015,48 @@ def test_fuse_mul_into_quant(self) -> None:
9311015
new_quant_scale = node.args[1]
9321016
self.assertEqual(new_quant_scale, quant_scale / mul_value)
9331017

1018+
@parameterized.expand([("zero", 0.0), ("negative", -1.0)])
1019+
def test_keep_non_positive_mul_tensor_before_quant(
1020+
self, _name: str, mul_value: float
1021+
) -> None:
1022+
quant_scale = 5.0
1023+
1024+
builder = GraphBuilder()
1025+
x = builder.placeholder("x", torch.randn(4, 32, dtype=torch.float32))
1026+
full = builder.call_operator(
1027+
op=exir_ops.edge.aten.full.default,
1028+
args=([1], mul_value),
1029+
)
1030+
mul = builder.call_operator(
1031+
op=exir_ops.edge.aten.mul.Tensor,
1032+
args=(x, full),
1033+
)
1034+
quant = builder.call_operator(
1035+
op=exir_ops.edge.quantized_decomposed.quantize_per_tensor.default,
1036+
args=(mul, quant_scale, 7, 0, 255, torch.uint8),
1037+
)
1038+
builder.output([quant])
1039+
1040+
result = cast(
1041+
PassResult,
1042+
FuseMulTensorIntoQuantPass()(builder.get_graph_module()),
1043+
)
1044+
1045+
self.assertFalse(result.modified)
1046+
self.check_op_counts(
1047+
result.graph_module,
1048+
expected_op_counts={
1049+
exir_ops.edge.quantized_decomposed.quantize_per_tensor.default: 1,
1050+
exir_ops.edge.aten.full.default: 1,
1051+
exir_ops.edge.aten.mul.Tensor: 1,
1052+
},
1053+
)
1054+
quant_nodes = result.graph_module.graph.find_nodes(
1055+
op="call_function",
1056+
target=exir_ops.edge.quantized_decomposed.quantize_per_tensor.default,
1057+
)
1058+
self.assertEqual(quant_nodes[0].args[1], quant_scale)
1059+
9341060
def test_fuse_then_transpose_pass(self) -> None:
9351061
# Create a graph with full -> transpose -> permute -> view.
9361062
builder = GraphBuilder()

0 commit comments

Comments
 (0)