Skip to content

Commit 4201e88

Browse files
mcremon-metafacebook-github-bot
authored andcommitted
Make QuantAbsorption's input-side absorption opt-in (#24780)
Summary: This should have been part of #24720, which added `QuantAbsorptionPass` with the other fused_quant graph passes. It was left out of that PR by mistake. By default, `QuantAbsorptionPass` now only runs the rewrite that is never less accurate. The docstring explains why the other two modes are opt-in. - Output side (unchanged, always on): the producer writes its output directly in the downstream quantize's qparams. The value still ends up on that grid, rounded once instead of twice, so each element is at least as close to the float result. It isn't bit-exact with the q/dq graph, though. - Input side (now opt-in, `absorb_into_inputs`): the consumer reads values on the dequantize's grid instead of the quantize's. Dropping the second rounding is more accurate on average, but not for every value. - Forked dequantize (already opt-in, `absorb_with_fork`): its float users see values on the quantize's grid instead of the producer's, which can be coarser. Only the docstring changes. With input-side absorption off, a pair that only the input side would remove is left to `ReplaceDequantQuantWithRequantize`, which turns it into a `fused_quant.requantize`. Reviewed By: DrJessop Differential Revision: D130466673
1 parent ebdb936 commit 4201e88

2 files changed

Lines changed: 45 additions & 17 deletions

File tree

‎backends/transforms/fused_quant/optimization_passes/quant_absorption.py‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -567,11 +567,16 @@ class QuantAbsorptionPass(ExportedProgramPassBase):
567567
feeds into a dequant→quant pair, this pass composes the producer, dequantize,
568568
and downstream quantize qparams into the fused op's output qparams. When the
569569
producer and dequantize qparams match, this reduces to the downstream target.
570+
This is always on because it is never less accurate: the value still ends up
571+
on the downstream quantize's grid, rounded once instead of twice, so each
572+
element is at least as close to the float result. It is not bit-exact with
573+
the q/dq graph, though, and a QAT model was trained with both roundings.
570574
571575
On the input side, when a dequant→quant pair feeds any fused_quant input with
572576
per-tensor qparams, this pass composes the dequantize, quantize, and consumer
573577
input qparams, then bypasses the pair. This handles graph inputs, which have
574-
no fused producer into which the quantization can be absorbed.
578+
no fused producer into which the quantization can be absorbed. It is opt-in:
579+
see ``absorb_into_inputs`` below.
575580
576581
It also absorbs a *bare* downstream quantize into a fused op whose output is
577582
currently unquantized (null out qparams), turning that op into a directly
@@ -581,10 +586,20 @@ class QuantAbsorptionPass(ExportedProgramPassBase):
581586
Args:
582587
absorb_with_fork: When True, also absorb dequant→quant pairs where
583588
the dequant has additional users (a "forked" dequant). The dequant
584-
is kept for the other users but its qparams are updated to match
585-
the new output qparams. This changes (not necessarily worsens) the
586-
numerics of the float path through the dequant, but should improve
587-
numerics on the int path since it removes a redundant dq-q pair.
589+
is kept for the other users, with the downstream quantize's qparams.
590+
Off by default: those float users then see values on the quantize's
591+
grid instead of the producer's, which can be coarser.
592+
absorb_into_inputs: When True, also absorb a dequant→quant pair into
593+
the input qparams of the fused op it feeds. The consumer then reads
594+
the producer's values on the dequantize's grid, skipping a rounding.
595+
That is more accurate on average, but unlike the output side it can
596+
move a value further from the float result: with scales 0.1 then
597+
0.25 and zero points 0, a float 0.24 is stored as 0.2, which the
598+
q/dq graph rounds to 0.25 and the consumer now reads as 0.2. The
599+
consumer also takes the dequantize's zero point and integer range,
600+
which its kernel has to support. Off by default for both reasons; a
601+
backend that turns it on lists the ops whose kernels need symmetric
602+
inputs in ``symmetric_quant_input_ops``.
588603
symmetric_quant_input_ops: Ops whose inputs must stay symmetrically
589604
quantized (zero point 0) because their backend kernel ignores input
590605
zero points. An input absorption that would give them a nonzero input
@@ -594,10 +609,12 @@ class QuantAbsorptionPass(ExportedProgramPassBase):
594609
def __init__(
595610
self,
596611
absorb_with_fork: bool = False,
612+
absorb_into_inputs: bool = False,
597613
symmetric_quant_input_ops: frozenset[EdgeOpOverload] = frozenset(),
598614
) -> None:
599615
super().__init__()
600616
self._absorb_with_fork = absorb_with_fork
617+
self._absorb_into_inputs = absorb_into_inputs
601618
self._symmetric_quant_input_ops = symmetric_quant_input_ops
602619

603620
def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
@@ -615,6 +632,8 @@ def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
615632
modified |= _try_absorb_quant_into_unquantized_out(
616633
exported_program, node, graph
617634
)
635+
if not self._absorb_into_inputs:
636+
continue
618637
assert isinstance(node.target, EdgeOpOverload)
619638
input_names, _ = split_fused_arg_names(node.target)
620639
keep_symmetric = node.target in self._symmetric_quant_input_ops

‎backends/transforms/fused_quant/test/test_quant_absorption.py‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,10 +1416,17 @@ def _build_dequant_quant_add(self) -> tuple[ExportedProgram, torch.Tensor]:
14161416
builder.output([add])
14171417
return builder.get_program(), inp
14181418

1419+
def test_does_not_absorb_into_consumer_input_by_default(self) -> None:
1420+
ep, _ = self._build_dequant_quant_pool()
1421+
1422+
result = QuantAbsorptionPass()(ep)
1423+
1424+
self.assertFalse(result.modified)
1425+
14191426
def test_absorbs_dequant_quant_into_fused_consumer_input(self) -> None:
14201427
ep, inp = self._build_dequant_quant_pool()
14211428

1422-
result = QuantAbsorptionPass()(ep)
1429+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
14231430

14241431
self.assertTrue(result.modified)
14251432
(actual,) = result.exported_program.module()(inp)
@@ -1458,7 +1465,7 @@ def test_absorbs_dequant_quant_into_fused_consumer_input(self) -> None:
14581465
def test_absorbs_consumer_input_through_view_copy(self) -> None:
14591466
ep, inp = self._build_dequant_quant_pool(input_passthrough="view")
14601467

1461-
result = QuantAbsorptionPass()(ep)
1468+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
14621469

14631470
self.assertTrue(result.modified)
14641471
(actual,) = result.exported_program.module()(inp)
@@ -1501,7 +1508,7 @@ def test_absorbs_consumer_input_through_view_copy(self) -> None:
15011508
def test_absorbs_consumer_input_through_permute_copy(self) -> None:
15021509
ep, inp = self._build_dequant_quant_pool(input_passthrough="permute")
15031510

1504-
result = QuantAbsorptionPass()(ep)
1511+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
15051512

15061513
self.assertTrue(result.modified)
15071514
(actual,) = result.exported_program.module()(inp)
@@ -1546,7 +1553,7 @@ def test_does_not_absorb_through_shared_input_passthrough(self) -> None:
15461553
input_passthrough="view", expose_passthrough=True
15471554
)
15481555

1549-
result = QuantAbsorptionPass()(ep)
1556+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
15501557

15511558
self.assertFalse(result.modified)
15521559
self.assertEqual(
@@ -1562,7 +1569,7 @@ def test_does_not_absorb_through_shared_input_passthrough(self) -> None:
15621569
def test_absorbs_all_quantized_inputs_of_binary_op(self) -> None:
15631570
ep, inp = self._build_dequant_quant_add()
15641571

1565-
result = QuantAbsorptionPass()(ep)
1572+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
15661573

15671574
self.assertTrue(result.modified)
15681575
(actual,) = result.exported_program.module()(inp)
@@ -1628,7 +1635,7 @@ def test_composes_mismatched_consumer_input_qparams(self) -> None:
16281635
)
16291636
expected = ep.module()(inp)
16301637

1631-
result = QuantAbsorptionPass()(ep)
1638+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
16321639

16331640
self.assertTrue(result.modified)
16341641
actual = result.exported_program.module()(inp)
@@ -1668,7 +1675,7 @@ def test_does_not_absorb_unrepresentable_consumer_input_qparams(self) -> None:
16681675
consumer_input_zero_point=-4,
16691676
)
16701677

1671-
result = QuantAbsorptionPass()(ep)
1678+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
16721679

16731680
self.assertFalse(result.modified)
16741681

@@ -1683,9 +1690,10 @@ def test_symmetric_quant_input_op_keeps_requantize_for_nonzero_zero_point(
16831690
)
16841691

16851692
result = QuantAbsorptionPass(
1693+
absorb_into_inputs=True,
16861694
symmetric_quant_input_ops=frozenset(
16871695
{exir_ops.edge.fused_quant.avg_pool2d_channels_last.default}
1688-
)
1696+
),
16891697
)(ep)
16901698

16911699
self.assertFalse(result.modified)
@@ -1696,9 +1704,10 @@ def test_symmetric_quant_input_op_absorbs_when_zero_point_stays_zero(
16961704
ep, inp = self._build_dequant_quant_pool()
16971705

16981706
result = QuantAbsorptionPass(
1707+
absorb_into_inputs=True,
16991708
symmetric_quant_input_ops=frozenset(
17001709
{exir_ops.edge.fused_quant.avg_pool2d_channels_last.default}
1701-
)
1710+
),
17021711
)(ep)
17031712

17041713
self.assertTrue(result.modified)
@@ -1746,7 +1755,7 @@ def test_does_not_absorb_per_channel_consumer_input(self) -> None:
17461755
builder.output([relu])
17471756
ep = builder.get_program()
17481757

1749-
result = QuantAbsorptionPass()(ep)
1758+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
17501759

17511760
self.assertFalse(result.modified)
17521761
self.assertEqual(
@@ -1762,7 +1771,7 @@ def test_does_not_absorb_per_channel_consumer_input(self) -> None:
17621771
def test_does_not_absorb_shared_quantize(self) -> None:
17631772
ep, _ = self._build_dequant_quant_pool(expose_quant=True)
17641773

1765-
result = QuantAbsorptionPass()(ep)
1774+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
17661775

17671776
self.assertFalse(result.modified)
17681777
self.assertEqual(
@@ -1779,7 +1788,7 @@ def test_absorbs_quantize_and_preserves_shared_dequantize(self) -> None:
17791788
ep, inp = self._build_dequant_quant_pool(expose_dequant=True)
17801789
_, expected_dequant = ep.module()(inp)
17811790

1782-
result = QuantAbsorptionPass()(ep)
1791+
result = QuantAbsorptionPass(absorb_into_inputs=True)(ep)
17831792

17841793
self.assertTrue(result.modified)
17851794
actual_pool, actual_dequant = result.exported_program.module()(inp)

0 commit comments

Comments
 (0)