Skip to content

Commit 8647e29

Browse files
mcremon-metafacebook-github-bot
authored andcommitted
Make QuantAbsorption's input-side absorption opt-in
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`. Differential Revision: D130466673
1 parent e0015e1 commit 8647e29

2 files changed

Lines changed: 39 additions & 17 deletions

File tree

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

Lines changed: 18 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+
(``absorb_into_inputs``) because it is not always more accurate: see 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,14 @@ 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. Off by default: the
594+
consumer then reads values on the dequantize's grid instead of the
595+
quantize's. Dropping the second rounding is more accurate on
596+
average, but not for every value.
588597
symmetric_quant_input_ops: Ops whose inputs must stay symmetrically
589598
quantized (zero point 0) because their backend kernel ignores input
590599
zero points. An input absorption that would give them a nonzero input
@@ -594,10 +603,12 @@ class QuantAbsorptionPass(ExportedProgramPassBase):
594603
def __init__(
595604
self,
596605
absorb_with_fork: bool = False,
606+
absorb_into_inputs: bool = False,
597607
symmetric_quant_input_ops: frozenset[EdgeOpOverload] = frozenset(),
598608
) -> None:
599609
super().__init__()
600610
self._absorb_with_fork = absorb_with_fork
611+
self._absorb_into_inputs = absorb_into_inputs
601612
self._symmetric_quant_input_ops = symmetric_quant_input_ops
602613

603614
def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
@@ -615,6 +626,8 @@ def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
615626
modified |= _try_absorb_quant_into_unquantized_out(
616627
exported_program, node, graph
617628
)
629+
if not self._absorb_into_inputs:
630+
continue
618631
assert isinstance(node.target, EdgeOpOverload)
619632
input_names, _ = split_fused_arg_names(node.target)
620633
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)