Skip to content

Commit ee9107c

Browse files
committed
index select for composite input quantization and refactoring
Signed-off-by: Prathamesh Mandke <46148373+pkmandke@users.noreply.github.com>
1 parent 82eaa4f commit ee9107c

4 files changed

Lines changed: 138 additions & 48 deletions

File tree

tests/export/test_composite_op_externalize.py

Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
via a module-level config, by name and by type, on a bare composite, on a
1818
mixed model with other quantized ops, and on a multi-tensor (q / k / v) SDPA
1919
composite; a distinct dtype on the composite config proves it outranks the
20-
global spec at the boundary (``TestCompositeOpIOQuantization``).
20+
global spec at the boundary (``TestCompositeOpIOQuantization``). A proper
21+
subset of integer input indices selects exactly those positional args, which
22+
pins the index -> argument mapping the wildcard cannot
23+
(``test_composite_boundary_input_index_selects_those_args``).
2124
2225
End-to-end lowering and execution tests live in
2326
``tests/export/test_graph_mode_mlir_export.py::test_composite_externalize_export``.
@@ -29,7 +32,6 @@
2932
import torch
3033
import torch.nn as nn
3134
from coreai_torch import ExternalizeSpec, _patch_model_for_externalization
32-
from coreai_torch.composite_ops import SDPA, RMSNormImpl
3335

3436
from coreai_opt import ExportBackend
3537
from coreai_opt.quantization import (
@@ -49,6 +51,8 @@
4951
CompositeRMSNormModel,
5052
CompositeRMSNormOnlyModel,
5153
CompositeSDPAModel,
54+
rmsnorm_externalize_spec,
55+
sdpa_externalize_spec,
5256
)
5357
from tests.test_utils.general import (
5458
assert_single_call_function_node,
@@ -57,16 +61,8 @@
5761
is_coreai_quantize,
5862
)
5963

60-
_RMSNORM_SPEC = ExternalizeSpec(
61-
target_class=RMSNormImpl,
62-
composite_op_name="rms_norm",
63-
composite_attrs=["axes", "eps"],
64-
)
65-
_SDPA_SPEC = ExternalizeSpec(
66-
target_class=SDPA,
67-
composite_op_name="scaled_dot_product_attention",
68-
composite_attrs=["scale", "is_causal", "window_size"],
69-
)
64+
_RMSNORM_SPEC = rmsnorm_externalize_spec()
65+
_SDPA_SPEC = sdpa_externalize_spec()
7066

7167

7268
@pytest.mark.parametrize(
@@ -139,24 +135,34 @@ class TestCompositeOpIOQuantization:
139135
_COMPOSITE_ACT_DTYPE = torch.uint8
140136

141137
@classmethod
142-
def _config(cls, spec: ExternalizeSpec, module_name: str, target_by: str) -> QuantizerConfig:
138+
def _composite_act_spec(cls) -> QuantizationSpec:
143139
# The composite config must use a dtype DISTINCT from the global
144140
# (default) activation dtype: a matching dtype collapses via observer
145141
# sharing into a vacuous no-op, so the composite's effect at the
146142
# boundary would not be observable.
147143
assert cls._COMPOSITE_ACT_DTYPE != default_activation_quantization_spec().dtype
148-
composite_act = QuantizationSpec(
144+
return QuantizationSpec(
149145
dtype=cls._COMPOSITE_ACT_DTYPE,
150146
qscheme=QuantizationScheme.SYMMETRIC,
151147
granularity=PerTensorGranularity(),
152148
)
149+
150+
@classmethod
151+
def _config(
152+
cls,
153+
spec: ExternalizeSpec,
154+
module_name: str,
155+
target_by: str,
156+
module_input_spec: dict | None = None,
157+
) -> QuantizerConfig:
158+
composite_act = cls._composite_act_spec()
153159
global_config = ModuleQuantizerConfig(
154160
op_state_spec={"weight": default_weight_quantization_spec()},
155161
op_input_spec={"*": default_activation_quantization_spec()},
156162
op_output_spec={"*": default_activation_quantization_spec()},
157163
)
158164
composite_config = ModuleQuantizerConfig(
159-
module_input_spec={"*": composite_act},
165+
module_input_spec=module_input_spec or {"*": composite_act},
160166
module_output_spec={"*": composite_act},
161167
)
162168
if target_by == "name":
@@ -172,12 +178,13 @@ def _finalize(
172178
spec: ExternalizeSpec,
173179
module_name: str,
174180
target_by: str,
181+
module_input_spec: dict | None = None,
175182
) -> tuple[torch.fx.GraphModule, str]:
176183
_patch_model_for_externalization(model, [spec])
177184
op_name = model.get_submodule(module_name)._externalize_op_name
178185
target_substr = f"coreai_torch_ext.{op_name}"
179186

180-
quantizer = Quantizer(model, self._config(spec, module_name, target_by))
187+
quantizer = Quantizer(model, self._config(spec, module_name, target_by, module_input_spec))
181188
prepared = quantizer.prepare((sample,))
182189
assert_single_call_function_node(prepared, target_substr, stage="prepared")
183190

@@ -234,3 +241,72 @@ def test_composite_boundary_quantized(
234241
sample = torch.randn(2, 4, 32, dtype=torch.float16)
235242
finalized, target_substr = self._finalize(model, sample, spec, module_name, target_by)
236243
self._assert_boundary_quantized(finalized, target_substr, num_tensor_inputs)
244+
245+
@pytest.mark.parametrize("target_by", ["name", "type"])
246+
def test_composite_boundary_input_index_selects_those_args(self, target_by: str) -> None:
247+
"""Integer keys in ``module_input_spec`` quantize exactly those positional args
248+
for composite ops.
249+
250+
The unselected input is left unquantized rather than falling
251+
back to the global spec, because the composite is opaque to the
252+
op-pattern annotator and only a module-level config reaches its edges.
253+
"""
254+
quantized_indices = (0, 2)
255+
num_tensor_inputs = 3
256+
model = CompositeSDPAModel().eval().half()
257+
sample = torch.randn(2, 4, 32, dtype=torch.float16)
258+
259+
finalized, target_substr = self._finalize(
260+
model,
261+
sample,
262+
_SDPA_SPEC,
263+
"composite",
264+
target_by,
265+
module_input_spec={i: self._composite_act_spec() for i in quantized_indices},
266+
)
267+
268+
composite = assert_single_call_function_node(finalized, target_substr, stage="finalized")
269+
tensor_inputs = [
270+
a for a in composite.args if isinstance(a, torch.fx.Node) and a.op != "get_attr"
271+
]
272+
assert len(tensor_inputs) == num_tensor_inputs, (
273+
f"Expected {num_tensor_inputs} tensor inputs to {composite.name}, "
274+
f"got {[n.name for n in tensor_inputs]}"
275+
)
276+
277+
# Each selected index must be fed by a dequantize whose producing
278+
# quantize carries the composite dtype; each unselected index must not
279+
# be quantized at all.
280+
for index, act_input in enumerate(tensor_inputs):
281+
if index in quantized_indices:
282+
assert is_coreai_dequantize(act_input.target), (
283+
f"input {index} was selected by module_input_spec but is not fed by "
284+
f"a dequantize: {act_input.target}"
285+
)
286+
input_dtype = get_quantize_dtype(act_input.args[0])
287+
assert input_dtype == self._COMPOSITE_ACT_DTYPE, (
288+
f"input {index} was selected by module_input_spec but is quantized as "
289+
f"{input_dtype}, expected the composite dtype {self._COMPOSITE_ACT_DTYPE}"
290+
)
291+
else:
292+
assert not is_coreai_dequantize(act_input.target), (
293+
f"input {index} was not selected by module_input_spec but is fed by "
294+
f"a dequantize: {act_input.target}"
295+
)
296+
297+
# module_output_spec stays the wildcard, so the composite's consumer is
298+
# quantized with the composite dtype regardless of which inputs were selected.
299+
consumers = list(composite.users)
300+
assert len(consumers) == 1, (
301+
f"expected the composite to have exactly one consumer, got "
302+
f"{[n.name for n in consumers]}"
303+
)
304+
consumer = consumers[0]
305+
assert is_coreai_quantize(consumer.target), (
306+
f"the composite's consumer is not a quantize node: {consumer.target}"
307+
)
308+
consumer_dtype = get_quantize_dtype(consumer)
309+
assert consumer_dtype == self._COMPOSITE_ACT_DTYPE, (
310+
f"the composite's output is quantized as {consumer_dtype}, expected the "
311+
f"composite dtype {self._COMPOSITE_ACT_DTYPE}"
312+
)

tests/export/test_graph_mode_mlir_export.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import pytest
1212
import torch
1313
from coreai_torch import ExternalizeSpec, _patch_model_for_externalization
14-
from coreai_torch.composite_ops import SDPA, RMSNormImpl
1514

1615
from coreai_opt import ExportBackend
1716
from coreai_opt.palettization.kmeans import KMeansPalettizer
@@ -34,7 +33,12 @@
3433
ParametrizedQuantConfigs,
3534
make_graph_mode_ptq_config,
3635
)
37-
from tests.models.composite import CompositeRMSNormModel, CompositeSDPAModel
36+
from tests.models.composite import (
37+
CompositeRMSNormModel,
38+
CompositeSDPAModel,
39+
rmsnorm_externalize_spec,
40+
sdpa_externalize_spec,
41+
)
3842

3943
from . import export_utils
4044

@@ -459,24 +463,8 @@ def test_integer_quant_minval_export(
459463
@pytest.mark.parametrize(
460464
"model_cls, externalize_spec",
461465
[
462-
pytest.param(
463-
CompositeRMSNormModel,
464-
ExternalizeSpec(
465-
target_class=RMSNormImpl,
466-
composite_op_name="rms_norm",
467-
composite_attrs=["axes", "eps"],
468-
),
469-
id="rmsnorm",
470-
),
471-
pytest.param(
472-
CompositeSDPAModel,
473-
ExternalizeSpec(
474-
target_class=SDPA,
475-
composite_op_name="scaled_dot_product_attention",
476-
composite_attrs=["scale", "is_causal", "window_size"],
477-
),
478-
id="sdpa",
479-
),
466+
pytest.param(CompositeRMSNormModel, rmsnorm_externalize_spec(), id="rmsnorm"),
467+
pytest.param(CompositeSDPAModel, sdpa_externalize_spec(), id="sdpa"),
480468
],
481469
)
482470
def test_composite_externalize_export(

tests/models/composite.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,40 @@
1818
_BATCH = 2
1919

2020

21+
# Externalize specs shared by the externalization test modules.
22+
#
23+
# These are factories rather than module-level constants on purpose: this module
24+
# is registered as a pytest plugin in tests/conftest.py, so it is imported for
25+
# every test session, while ``coreai-torch`` is an optional extra. Importing it
26+
# at module scope would break collection for anyone without the extra, so the
27+
# imports stay function-local (same reason the model classes import inside
28+
# ``__init__``).
29+
30+
31+
def rmsnorm_externalize_spec():
32+
"""ExternalizeSpec targeting the RMSNormImpl composite."""
33+
from coreai_torch import ExternalizeSpec # noqa: PLC0415
34+
from coreai_torch.composite_ops import RMSNormImpl # noqa: PLC0415
35+
36+
return ExternalizeSpec(
37+
target_class=RMSNormImpl,
38+
composite_op_name="rms_norm",
39+
composite_attrs=["axes", "eps"],
40+
)
41+
42+
43+
def sdpa_externalize_spec():
44+
"""ExternalizeSpec targeting the SDPA composite."""
45+
from coreai_torch import ExternalizeSpec # noqa: PLC0415
46+
from coreai_torch.composite_ops import SDPA # noqa: PLC0415
47+
48+
return ExternalizeSpec(
49+
target_class=SDPA,
50+
composite_op_name="scaled_dot_product_attention",
51+
composite_attrs=["scale", "is_causal", "window_size"],
52+
)
53+
54+
2155
class CompositeRMSNormModel(nn.Module):
2256
"""Linear -> RMSNormImpl (composite) -> Linear over rank-3 activations."""
2357

tests/quantization/test_graph_mode_quantizer_mnist.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
import pytest
99
import torch
10-
from coreai_torch import ExternalizeSpec, _patch_model_for_externalization
11-
from coreai_torch.composite_ops import RMSNormImpl
10+
from coreai_torch import _patch_model_for_externalization
1211

1312
import tests.utils as utils
1413
from coreai_opt import ExportBackend
@@ -23,6 +22,7 @@
2322
PerTensorGranularity,
2423
)
2524
from tests.export import export_utils
25+
from tests.models.composite import rmsnorm_externalize_spec
2626
from tests.test_utils.general import assert_single_call_function_node
2727

2828
image_size = 28
@@ -333,6 +333,7 @@ def test_weight_and_activation_qat_mnist(mnist_pretrained_model, mnist_dataset,
333333
assert post_qat_accuracy == finalized_accuracy
334334

335335

336+
@pytest.mark.slow
336337
@pytest.mark.seed
337338
def test_weight_and_activation_qat_mnist_with_externalized_composite(
338339
mnist_composite_rmsnorm_pretrained_model,
@@ -369,16 +370,7 @@ def test_weight_and_activation_qat_mnist_with_externalized_composite(
369370
f"expect pretrained MNIST-composite model accuracy > 92%, got {accuracy:.2f}%"
370371
)
371372

372-
_patch_model_for_externalization(
373-
model,
374-
[
375-
ExternalizeSpec(
376-
target_class=RMSNormImpl,
377-
composite_op_name="rms_norm",
378-
composite_attrs=["axes", "eps"],
379-
),
380-
],
381-
)
373+
_patch_model_for_externalization(model, [rmsnorm_externalize_spec()])
382374
op_name = model.norm._externalize_op_name
383375
target_substr = f"coreai_torch_ext.{op_name}"
384376

0 commit comments

Comments
 (0)