Skip to content

Commit 5c3e15f

Browse files
committed
composite ops boundary quantization coverage
Signed-off-by: Prathamesh Mandke <46148373+pkmandke@users.noreply.github.com>
1 parent 2e94595 commit 5c3e15f

3 files changed

Lines changed: 110 additions & 48 deletions

File tree

tests/export/test_composite_op_externalize.py

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,19 @@
2020

2121
from coreai_opt import ExportBackend
2222
from coreai_opt.quantization import (
23-
ModuleQuantizerConfig,
2423
Quantizer,
2524
QuantizerConfig,
2625
)
2726
from coreai_opt.quantization.spec import (
2827
PerTensorGranularity,
2928
QuantizationScheme,
3029
QuantizationSpec,
31-
default_activation_quantization_spec,
32-
default_weight_quantization_spec,
3330
)
34-
from tests.fixtures.quantization import make_graph_mode_ptq_config
31+
from tests.fixtures.quantization import (
32+
COMPOSITE_BOUNDARY_ACT_DTYPE,
33+
make_graph_mode_composite_boundary_config,
34+
make_graph_mode_ptq_config,
35+
)
3536
from tests.models.composite import (
3637
CompositeRMSNormModel,
3738
CompositeRMSNormOnlyModel,
@@ -104,15 +105,10 @@ class TestCompositeOpIOQuantization:
104105
while every other quantized edge carries the global dtype.
105106
"""
106107

107-
_COMPOSITE_ACT_DTYPE = torch.uint8
108+
_COMPOSITE_ACT_DTYPE = COMPOSITE_BOUNDARY_ACT_DTYPE
108109

109110
@classmethod
110111
def _composite_act_spec(cls) -> QuantizationSpec:
111-
# The composite config must use a dtype DISTINCT from the global
112-
# (default) activation dtype: a matching dtype collapses via observer
113-
# sharing into a vacuous no-op, so the composite's effect at the
114-
# boundary would not be observable.
115-
assert cls._COMPOSITE_ACT_DTYPE != default_activation_quantization_spec().dtype
116112
return QuantizationSpec(
117113
dtype=cls._COMPOSITE_ACT_DTYPE,
118114
qscheme=QuantizationScheme.SYMMETRIC,
@@ -127,21 +123,11 @@ def _config(
127123
target_by: str,
128124
module_input_spec: dict | None = None,
129125
) -> QuantizerConfig:
130-
composite_act = cls._composite_act_spec()
131-
global_config = ModuleQuantizerConfig(
132-
op_state_spec={"weight": default_weight_quantization_spec()},
133-
op_input_spec={"*": default_activation_quantization_spec()},
134-
op_output_spec={"*": default_activation_quantization_spec()},
135-
)
136-
composite_config = ModuleQuantizerConfig(
137-
module_input_spec=module_input_spec or {"*": composite_act},
138-
module_output_spec={"*": composite_act},
126+
return make_graph_mode_composite_boundary_config(
127+
module_name=module_name if target_by == "name" else None,
128+
module_type=spec.target_class if target_by == "type" else None,
129+
module_input_spec=module_input_spec,
139130
)
140-
if target_by == "name":
141-
scope = {"module_name_configs": {module_name: composite_config}}
142-
else:
143-
scope = {"module_type_configs": {spec.target_class: composite_config}}
144-
return QuantizerConfig(global_config=global_config, execution_mode="graph", **scope)
145131

146132
def _finalize(
147133
self,

tests/export/test_graph_mode_mlir_export.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from tests.fixtures.fp8 import ParametrizedFP8Configs
3232
from tests.fixtures.quantization import (
3333
ParametrizedQuantConfigs,
34+
make_graph_mode_composite_boundary_config,
3435
make_graph_mode_ptq_config,
3536
)
3637
from tests.models.composite import (
@@ -452,47 +453,66 @@ def test_integer_quant_minval_export(
452453

453454
# Composite-op externalize export coverage
454455

456+
# (model, externalize spec, composite submodule path, expected coreai.quantize count
457+
# per config kind)
458+
_EXTERNALIZE_EXPORT_CASES = [
459+
pytest.param(
460+
CompositeRMSNormModel,
461+
rmsnorm_externalize_spec(),
462+
"norm",
463+
{"w8": 0, "w8a8": 4, "w8a8-boundary": 6},
464+
id="rmsnorm",
465+
),
466+
pytest.param(
467+
CompositeSDPAModel,
468+
sdpa_externalize_spec(),
469+
"composite",
470+
{"w8": 0, "w8a8": 4, "w8a8-boundary": 8},
471+
id="sdpa",
472+
),
473+
]
455474

475+
476+
@pytest.mark.parametrize("config_kind", ["w8", "w8a8", "w8a8-boundary"])
456477
@pytest.mark.parametrize(
457-
"quantize_activations, expected_quantize_count",
458-
[
459-
pytest.param(False, 0, id="w8-weight-only"),
460-
pytest.param(True, 4, id="w8a8"),
461-
],
462-
)
463-
@pytest.mark.parametrize(
464-
"model_cls, externalize_spec",
465-
[
466-
pytest.param(CompositeRMSNormModel, rmsnorm_externalize_spec(), id="rmsnorm"),
467-
pytest.param(CompositeSDPAModel, sdpa_externalize_spec(), id="sdpa"),
468-
],
478+
"model_cls, externalize_spec, composite_module, expected_quantize_counts",
479+
_EXTERNALIZE_EXPORT_CASES,
469480
)
470481
def test_composite_externalize_export(
471482
model_cls: type[torch.nn.Module],
472483
externalize_spec: ExternalizeSpec,
473-
quantize_activations: bool,
474-
expected_quantize_count: int,
484+
composite_module: str,
485+
expected_quantize_counts: Mapping[str, int],
486+
config_kind: str,
475487
) -> None:
476488
"""End-to-end CoreAI export of a model with an externalized composite op.
477489
478-
Marks the composite op for externalization, runs graph-mode PTQ
479-
(w8 weight-only or w8a8), then lowers the finalized graph to a
480-
.aimodel and runs it. ``convert_and_verify`` handles SNR / PSNR
481-
on the runtime output and op-count verification on the exported
482-
program (``constexpr_blockwise_shift_scale`` for weight quantizers
483-
and ``quantize`` / ``dequantize`` for activation quantizers).
490+
Marks the composite op for externalization, runs graph-mode PTQ, then lowers the
491+
finalized graph to a .aimodel and runs it. ``convert_and_verify`` handles SNR /
492+
PSNR on the runtime output and op-count verification on the exported program
493+
(``constexpr_blockwise_shift_scale`` for weight quantizers and ``quantize`` /
494+
``dequantize`` for activation quantizers).
495+
496+
Three configs:
497+
498+
- ``w8`` / ``w8a8``: global config only.
499+
- ``w8a8-boundary``: adds a module-scoped ``module_input_spec`` /
500+
``module_output_spec`` so the composite op's i/o edges are quantized
484501
"""
485502
model = model_cls().eval().half()
486503
input_data = torch.randn(2, 4, 32, dtype=torch.float16)
487504

488505
_patch_model_for_externalization(model, [externalize_spec])
489506

490-
quantizer = Quantizer(
491-
model, make_graph_mode_ptq_config(quantize_activations=quantize_activations)
492-
)
507+
if config_kind == "w8a8-boundary":
508+
config = make_graph_mode_composite_boundary_config(module_name=composite_module)
509+
else:
510+
config = make_graph_mode_ptq_config(quantize_activations=config_kind == "w8a8")
511+
512+
quantizer = Quantizer(model, config)
493513
prepared_model = quantizer.prepare((input_data,))
494514

495-
if quantize_activations:
515+
if config_kind != "w8":
496516
with quantizer.calibration_mode(), torch.no_grad():
497517
prepared_model(input_data)
498518

@@ -501,6 +521,7 @@ def test_composite_externalize_export(
501521

502522
finalized_model = quantizer.finalize(backend=ExportBackend.CoreAI)
503523

524+
expected_quantize_count = expected_quantize_counts[config_kind]
504525
export_utils.convert_and_verify(
505526
finalized_model=finalized_model,
506527
input_data=input_data,

tests/fixtures/quantization.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,61 @@ def make_graph_mode_ptq_config(*, quantize_activations: bool) -> QuantizerConfig
104104
)
105105

106106

107+
COMPOSITE_BOUNDARY_ACT_DTYPE = torch.uint8
108+
109+
110+
def make_graph_mode_composite_boundary_config(
111+
*,
112+
module_name: str | None = None,
113+
module_type: type | None = None,
114+
module_input_spec: dict | None = None,
115+
) -> QuantizerConfig:
116+
"""Build a graph-mode w8a8 config that also quantizes the composite op module's
117+
own i/o boundary.
118+
119+
Args:
120+
module_name: Target the module at this path (``module_name_configs``).
121+
module_type: Target modules of this type (``module_type_configs``).
122+
Exactly one of module_name / module_type must be given.
123+
module_input_spec: Override the boundary input spec, e.g.
124+
``{0: spec, 2: spec}`` to select individual positional args.
125+
Defaults to the ``"*"`` wildcard over every boundary input.
126+
127+
Returns:
128+
QuantizerConfig: global w8a8 plus a module-scoped boundary spec.
129+
"""
130+
if (module_name is None) == (module_type is None):
131+
msg = "pass exactly one of module_name / module_type"
132+
raise ValueError(msg)
133+
assert COMPOSITE_BOUNDARY_ACT_DTYPE != default_activation_quantization_spec().dtype
134+
135+
def _boundary_spec() -> QuantizationSpec:
136+
return QuantizationSpec(
137+
dtype=COMPOSITE_BOUNDARY_ACT_DTYPE,
138+
qscheme=QuantizationScheme.SYMMETRIC,
139+
granularity=PerTensorGranularity(),
140+
)
141+
142+
boundary_config = ModuleQuantizerConfig(
143+
module_input_spec=module_input_spec or {"*": _boundary_spec()},
144+
module_output_spec={"*": _boundary_spec()},
145+
)
146+
scope = (
147+
{"module_name_configs": {module_name: boundary_config}}
148+
if module_name is not None
149+
else {"module_type_configs": {module_type: boundary_config}}
150+
)
151+
return QuantizerConfig(
152+
global_config=ModuleQuantizerConfig(
153+
op_state_spec={"weight": default_weight_quantization_spec()},
154+
op_input_spec={"*": default_activation_quantization_spec()},
155+
op_output_spec={"*": default_activation_quantization_spec()},
156+
),
157+
execution_mode="graph",
158+
**scope,
159+
)
160+
161+
107162
@dataclass
108163
class ParametrizedQuantConfigs:
109164
"""Container for parametrized Eager and PT2E quantization configs.

0 commit comments

Comments
 (0)