Skip to content

Commit 5cdb1f1

Browse files
authored
refactor: use dataclass for weight axis defaults table (#30)
* refactor: use dataclass for weight axis defaults table Signed-off-by: Prathamesh Mandke <46148373+pkmandke@users.noreply.github.com> * address comments * import privately Signed-off-by: Prathamesh Mandke <46148373+pkmandke@users.noreply.github.com> --------- Signed-off-by: Prathamesh Mandke <46148373+pkmandke@users.noreply.github.com>
1 parent 519f21c commit 5cdb1f1

3 files changed

Lines changed: 77 additions & 55 deletions

File tree

src/coreai_opt/_utils/torch_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@
3333
E8M0_EXPONENT_BIAS: Final[int] = 127
3434
F32_MIN_NORMAL: Final[float] = 2**-126 # ~1.175e-38
3535

36+
# Maps a graph-mode aten ``OpOverload`` to the ``nn.Module`` type it corresponds
37+
# to.
38+
ATEN_OP_TO_MODULE_TYPE: dict[torch._ops.OpOverload, type[torch.nn.Module]] = {
39+
torch.ops.aten.conv1d.default: torch.nn.Conv1d,
40+
torch.ops.aten.conv2d.default: torch.nn.Conv2d,
41+
torch.ops.aten.conv3d.default: torch.nn.Conv3d,
42+
torch.ops.aten.conv_transpose1d.default: torch.nn.ConvTranspose1d,
43+
torch.ops.aten.conv_transpose2d.input: torch.nn.ConvTranspose2d,
44+
torch.ops.aten.conv_transpose3d.input: torch.nn.ConvTranspose3d,
45+
torch.ops.aten.linear.default: torch.nn.Linear,
46+
torch.ops.aten.embedding.default: torch.nn.Embedding,
47+
}
48+
3649

3750
class NamedModule(NamedTuple):
3851
"""NamedTuple for holding name and module info"""

src/coreai_opt/quantization/_axis_defaults.py

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313

1414
import logging
1515
from collections import defaultdict
16+
from dataclasses import dataclass
1617

1718
import torch
1819
import torch.nn as nn
1920
import torch.nn.utils.parametrize as P
2021
from torch.fx import GraphModule
2122

23+
from coreai_opt._utils.torch_utils import (
24+
ATEN_OP_TO_MODULE_TYPE as _ATEN_OP_TO_MODULE_TYPE,
25+
)
2226
from coreai_opt.config.spec import CompressionTargetTensor
2327
from coreai_opt.quantization.spec.fake_quantize import FakeQuantizeImplBase
2428
from coreai_opt.quantization.spec.granularity import (
@@ -35,43 +39,38 @@
3539
_WeightFQMap = defaultdict[FakeQuantizeImplBase, list[_ConsumerInfo]]
3640

3741

38-
# Per-channel defaults: quantize along the OUTPUT channel axis.
39-
# Conv weights are [out_ch, in_ch, ...]; ConvTranspose weights are [in_ch, out_ch, ...].
40-
# Embedding weights are [num_embeddings, embedding_dim].
41-
_PER_CHANNEL_WEIGHT_AXIS_DEFAULTS: dict[type[nn.Module], int] = {
42-
nn.Conv1d: 0,
43-
nn.Conv2d: 0,
44-
nn.Conv3d: 0,
45-
nn.ConvTranspose1d: 1,
46-
nn.ConvTranspose2d: 1,
47-
nn.ConvTranspose3d: 1,
48-
nn.Linear: 0,
49-
nn.Embedding: 0,
50-
}
42+
@dataclass(frozen=True)
43+
class _WeightAxisSpec:
44+
"""Default axis indices for a single ``nn.Module`` type.
5145
52-
# Per-block defaults: quantize along the INPUT channel axis (reduction dimension)
53-
# Embedding weights are [num_embeddings, embedding_dim].
54-
_PER_BLOCK_WEIGHT_AXIS_DEFAULTS: dict[type[nn.Module], int] = {
55-
nn.Conv1d: 1,
56-
nn.Conv2d: 1,
57-
nn.Conv3d: 1,
58-
nn.ConvTranspose1d: 0,
59-
nn.ConvTranspose2d: 0,
60-
nn.ConvTranspose3d: 0,
61-
nn.Linear: 1,
62-
nn.Embedding: 1,
63-
}
46+
Attributes:
47+
per_channel_axis (int): Output-channel axis for per-channel granularity.
48+
per_block_axis (int): Input-channel (reduction) axis for per-block granularity.
49+
"""
50+
51+
per_channel_axis: int
52+
per_block_axis: int
6453

65-
# Maps aten OpOverload -> nn.Module type for graph-mode op to module type resolution.
66-
_ATEN_OP_TO_MODULE_TYPE: dict[torch._ops.OpOverload, type[nn.Module]] = {
67-
torch.ops.aten.conv1d.default: nn.Conv1d,
68-
torch.ops.aten.conv2d.default: nn.Conv2d,
69-
torch.ops.aten.conv3d.default: nn.Conv3d,
70-
torch.ops.aten.conv_transpose1d.default: nn.ConvTranspose1d,
71-
torch.ops.aten.conv_transpose2d.input: nn.ConvTranspose2d,
72-
torch.ops.aten.conv_transpose3d.input: nn.ConvTranspose3d,
73-
torch.ops.aten.linear.default: nn.Linear,
74-
torch.ops.aten.embedding.default: nn.Embedding,
54+
def default_axis_for(self, granularity: QuantizationGranularity) -> int | None:
55+
"""Return the default axis for the given granularity, or ``None`` if not applicable."""
56+
if isinstance(granularity, PerChannelGranularity):
57+
return self.per_channel_axis
58+
if isinstance(granularity, PerBlockGranularity):
59+
return self.per_block_axis
60+
return None
61+
62+
63+
# Conv weights are [out_ch, in_ch, ...]; ConvTranspose weights are [in_ch, out_ch, ...].
64+
# Embedding weights are [num_embeddings, embedding_dim].
65+
_WEIGHT_AXIS_SPECS: dict[type[nn.Module], _WeightAxisSpec] = {
66+
nn.Conv1d: _WeightAxisSpec(0, 1),
67+
nn.Conv2d: _WeightAxisSpec(0, 1),
68+
nn.Conv3d: _WeightAxisSpec(0, 1),
69+
nn.ConvTranspose1d: _WeightAxisSpec(1, 0),
70+
nn.ConvTranspose2d: _WeightAxisSpec(1, 0),
71+
nn.ConvTranspose3d: _WeightAxisSpec(1, 0),
72+
nn.Linear: _WeightAxisSpec(0, 1),
73+
nn.Embedding: _WeightAxisSpec(0, 1),
7574
}
7675

7776

@@ -220,12 +219,11 @@ def _collect_weight_fq_entries_eager(model: nn.Module) -> _WeightFQMap:
220219
def _apply_defaults(fq_map: _WeightFQMap) -> None:
221220
"""Apply default weight axes and raise on unresolved or conflicting entries.
222221
223-
For each FQ in the map, selects the per-channel or per-block defaults
224-
table based on the granularity type. Resolves each consumer's identifier
225-
to a module type (aten ``OpOverload`` entries from graph-mode are mapped via
226-
``_ATEN_OP_TO_MODULE_TYPE``), then looks up the default axis. When a
227-
single FQ has multiple consumers (shared weight), all consumers must
228-
agree on the same default axis.
222+
For each FQ in the map, resolves each consumer's identifier to a module
223+
type (aten ``OpOverload`` entries from graph-mode are mapped via
224+
``_ATEN_OP_TO_MODULE_TYPE``), then looks up the default axis from
225+
``_WEIGHT_AXIS_SPECS``. When a single FQ has multiple consumers (shared
226+
weight), all consumers must agree on the same default axis.
229227
230228
Args:
231229
fq_map (_WeightFQMap): Map from fake-quantize instance to its
@@ -243,14 +241,6 @@ def _apply_defaults(fq_map: _WeightFQMap) -> None:
243241
if not _granularity_needs_axis_default(fq.granularity):
244242
continue
245243

246-
# get appropriate defaults table
247-
if isinstance(fq.granularity, PerChannelGranularity):
248-
axis_defaults = _PER_CHANNEL_WEIGHT_AXIS_DEFAULTS
249-
elif isinstance(fq.granularity, PerBlockGranularity):
250-
axis_defaults = _PER_BLOCK_WEIGHT_AXIS_DEFAULTS
251-
else:
252-
continue
253-
254244
# Resolve each consumer to a module type and look up its default axis
255245
resolved_axes: set[int] = set()
256246
all_consumer_names: list[str] = []
@@ -261,8 +251,8 @@ def _apply_defaults(fq_map: _WeightFQMap) -> None:
261251
else:
262252
module_type = module_type_or_op
263253

264-
# pick a default based on this consumer
265-
default_axis = axis_defaults.get(module_type) if module_type is not None else None
254+
spec = _WEIGHT_AXIS_SPECS.get(module_type) if module_type is not None else None
255+
default_axis = spec.default_axis_for(fq.granularity) if spec is not None else None
266256
if default_axis is not None:
267257
resolved_axes.add(default_axis)
268258

tests/quantization/test_axis_defaults.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
QuantizerConfig,
2020
)
2121
from coreai_opt.quantization._axis_defaults import (
22-
_PER_BLOCK_WEIGHT_AXIS_DEFAULTS,
23-
_PER_CHANNEL_WEIGHT_AXIS_DEFAULTS,
22+
_WEIGHT_AXIS_SPECS,
2423
_apply_defaults,
24+
_WeightAxisSpec,
2525
_WeightFQMap,
2626
)
2727
from coreai_opt.quantization.spec import default_activation_quantization_spec
@@ -184,9 +184,9 @@ def test_axis_none_resolved(
184184
prepared = Quantizer(model, config).prepare((make_input(),))
185185

186186
if isinstance(granularity, PerChannelGranularity):
187-
expected_axis = _PER_CHANNEL_WEIGHT_AXIS_DEFAULTS[module_type]
187+
expected_axis = _WEIGHT_AXIS_SPECS[module_type].per_channel_axis
188188
else:
189-
expected_axis = _PER_BLOCK_WEIGHT_AXIS_DEFAULTS[module_type]
189+
expected_axis = _WEIGHT_AXIS_SPECS[module_type].per_block_axis
190190

191191
weight_fqs = _get_weight_fqs(prepared)
192192
assert len(weight_fqs) == 1
@@ -293,6 +293,25 @@ def _activation_only_config(
293293
)
294294

295295

296+
class TestWeightAxisSpec:
297+
"""Verify _WeightAxisSpec.default_axis_for maps granularity types to axes."""
298+
299+
def test_per_channel_returns_per_channel_axis(self):
300+
"""PerChannelGranularity resolves to the spec's per_channel_axis."""
301+
spec = _WeightAxisSpec(per_channel_axis=0, per_block_axis=1)
302+
assert spec.default_axis_for(PerChannelGranularity(axis=None)) == 0
303+
304+
def test_per_block_returns_per_block_axis(self):
305+
"""PerBlockGranularity resolves to the spec's per_block_axis."""
306+
spec = _WeightAxisSpec(per_channel_axis=0, per_block_axis=1)
307+
assert spec.default_axis_for(PerBlockGranularity(axis=None, block_size=2)) == 1
308+
309+
def test_other_granularity_returns_none(self):
310+
"""A granularity that is neither per-channel nor per-block returns None."""
311+
spec = _WeightAxisSpec(per_channel_axis=0, per_block_axis=1)
312+
assert spec.default_axis_for(PerTensorGranularity()) is None
313+
314+
296315
class TestActivationAxisValidation:
297316
"""Verify that activation FQs with unresolved axis=None are caught at prepare time."""
298317

0 commit comments

Comments
 (0)