Skip to content

Commit a4ec0d6

Browse files
committed
Update logic for blocking activations
1 parent 2f00734 commit a4ec0d6

10 files changed

Lines changed: 228 additions & 39 deletions

File tree

src/coreai_opt/quantization/_eager/_prepare_for_export.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,7 @@ def _import_coreai_torch_modules():
201201
if is_float4_dtype(module.dtype):
202202
raise ValueError("FP4 activation quantization is not supported for MLIR export.")
203203
if isinstance(module.granularity, PerBlockGranularity):
204-
raise ValueError(
205-
"MLIR export does not support per-block granularity for activations."
206-
)
204+
raise ValueError("MLIR export does not support PerBlockGranularity on activations.")
207205
modules_to_replace.append((name, module))
208206

209207
# Replace each FakeQuantizeImplBase module

src/coreai_opt/quantization/_graph/_prepare_for_export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ def _process_mlir_activation_quantization(
340340
raise ValueError("FP4 activation quantization is not supported for MLIR export.")
341341

342342
if isinstance(fake_quant_mod.granularity, PerBlockGranularity):
343-
raise ValueError("MLIR export does not support per-block granularity for activations.")
343+
raise ValueError("MLIR export does not support PerBlockGranularity on activations.")
344344

345345
def _import_coreai_custom_ops():
346346
import coreai_torch._compression.custom_layers # noqa: PLC0415, F401

src/coreai_opt/quantization/spec/factory.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,25 @@ class QuantizationComponentFactory(CompressionComponentFactoryBase):
3535
"""
3636

3737
@classmethod
38-
def create_range_calculator(cls, spec: QuantizationSpec) -> RangeCalculatorBase:
38+
def create_range_calculator(
39+
cls,
40+
spec: QuantizationSpec,
41+
quantization_target: CompressionTargetTensor = CompressionTargetTensor.WEIGHT,
42+
) -> RangeCalculatorBase:
3943
"""
4044
Create a RangeCalculatorBase instance from a QuantizationSpec.
4145
4246
Args:
4347
spec: QuantizationSpec instance containing configuration
48+
quantization_target: The target tensor for quantization (weight/activation).
4449
4550
Returns:
4651
RangeCalculatorBase instance configured from the spec
4752
"""
4853
# Standard arguments for range calculator
4954
common_args = {
5055
"granularity": spec.granularity,
56+
"quantization_target": quantization_target,
5157
}
5258

5359
# Automatically detect and include any extra arguments
@@ -96,7 +102,7 @@ def create_qparams_calculator(
96102
)
97103

98104
# Create range calculator first
99-
range_calculator = cls.create_range_calculator(spec)
105+
range_calculator = cls.create_range_calculator(spec, quantization_target)
100106

101107
# Standard arguments for qparams calculator
102108
common_args = {

src/coreai_opt/quantization/spec/fake_quantize.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def _quantize_int(
359359
360360
This function quantizes the values in tensor but keeps the quantized tensor dtype in FP.
361361
"""
362-
block_size = self.granularity.get_block_size(tensor.shape)
362+
block_size = self.granularity.get_block_size(tensor.shape, self.quantization_target)
363363
original_shape, blockwise_shape, reduced_shape = _get_quantization_shapes(
364364
tensor, block_size
365365
)
@@ -384,7 +384,7 @@ def _dequantize_int(
384384
output_dtype: torch.dtype,
385385
) -> torch.Tensor:
386386
"""Integer dequantization. See :func:`_dequantize_int` for the math."""
387-
block_size = self.granularity.get_block_size(tensor.shape)
387+
block_size = self.granularity.get_block_size(tensor.shape, self.quantization_target)
388388
original_shape, blockwise_shape, reduced_shape = _get_quantization_shapes(
389389
tensor, block_size
390390
)
@@ -406,7 +406,7 @@ def _quantize_float(
406406
"""
407407
Floating-point quantization: cast_to_low_precision(clamp(input / scale, min, max))
408408
"""
409-
block_size = self.granularity.get_block_size(tensor.shape)
409+
block_size = self.granularity.get_block_size(tensor.shape, self.quantization_target)
410410
original_shape, blockwise_shape, reduced_shape = _get_quantization_shapes(
411411
tensor, block_size
412412
)
@@ -427,7 +427,7 @@ def _dequantize_float(
427427
output_dtype: torch.dtype,
428428
) -> torch.Tensor:
429429
"""Floating-point dequantization: input * scale"""
430-
block_size = self.granularity.get_block_size(tensor.shape)
430+
block_size = self.granularity.get_block_size(tensor.shape, self.quantization_target)
431431
original_shape, blockwise_shape, reduced_shape = _get_quantization_shapes(
432432
tensor, block_size
433433
)
@@ -449,7 +449,7 @@ def _fused_fake_quant_dequant(
449449
450450
Dispatches to the int or float fused STE class based on self.dtype.
451451
"""
452-
block_size = self.granularity.get_block_size(tensor.shape)
452+
block_size = self.granularity.get_block_size(tensor.shape, self.quantization_target)
453453
original_shape, blockwise_shape, reduced_shape = _get_quantization_shapes(
454454
tensor, block_size
455455
)

src/coreai_opt/quantization/spec/granularity.py

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pydantic import BaseModel, ConfigDict, Field, model_serializer
1313

1414
from coreai_opt._utils.registry_utils import ConfigRegistryMixin as _ConfigRegistryMixin
15+
from coreai_opt.config.spec import CompressionTargetTensor as _CompressionTargetTensor
1516
from coreai_opt.quantization.spec.errors import _BlockSizeMismatchError
1617

1718

@@ -49,7 +50,11 @@ def _serialize_model(self) -> dict[str, Any]:
4950
return data
5051

5152
@abstractmethod
52-
def _get_block_size(self, block_sizes_list: list[int]) -> list[int]:
53+
def _get_block_size(
54+
self,
55+
block_sizes_list: list[int],
56+
quantization_target: _CompressionTargetTensor = _CompressionTargetTensor.WEIGHT,
57+
) -> list[int]:
5358
"""
5459
Given an initial list of the tensor shape, return a list of block sizes
5560
corresponding to each axis:
@@ -61,6 +66,11 @@ def _get_block_size(self, block_sizes_list: list[int]) -> list[int]:
6166
- if per-block structuring is being done for a certain axis, set the block
6267
size for that specific axis
6368
69+
``quantization_target`` distinguishes weight from activation tensors.
70+
Only per-block granularity uses it (see
71+
:meth:`PerBlockGranularity._handle_single_axis_block_size`); the other
72+
granularities ignore it.
73+
6474
Example:
6575
- ``[10, 5, 2]`` with per-channel structuring on axis 1 results in
6676
``[10, 1, 2]``
@@ -70,11 +80,20 @@ def _get_block_size(self, block_sizes_list: list[int]) -> list[int]:
7080
"""
7181
pass
7282

73-
def get_block_size(self, tensor_shape: torch.Size) -> tuple[int, ...]:
83+
def get_block_size(
84+
self,
85+
tensor_shape: torch.Size,
86+
quantization_target: _CompressionTargetTensor = _CompressionTargetTensor.WEIGHT,
87+
) -> tuple[int, ...]:
7488
"""
7589
Get a list of block sizes based on the granularity.
90+
91+
Args:
92+
tensor_shape: Shape of the tensor being quantized.
93+
quantization_target: Whether the tensor is a weight or an activation.
94+
Defaults to ``WEIGHT``, which preserves the historical behavior.
7695
"""
77-
return tuple(self._get_block_size(list(tensor_shape)))
96+
return tuple(self._get_block_size(list(tensor_shape), quantization_target))
7897

7998
# The axis resolution logic lives here because it is granularity-specific.
8099
# Currently only PerChannelGranularity has a meaningful axis to resolve, but
@@ -119,7 +138,11 @@ class PerTensorGranularity(QuantizationGranularity):
119138

120139
axis: Literal[None] = None
121140

122-
def _get_block_size(self, block_sizes_list: list[int]) -> list[int]:
141+
def _get_block_size(
142+
self,
143+
block_sizes_list: list[int],
144+
quantization_target: _CompressionTargetTensor = _CompressionTargetTensor.WEIGHT,
145+
) -> list[int]:
123146
return block_sizes_list
124147

125148

@@ -138,7 +161,11 @@ class PerChannelGranularity(QuantizationGranularity):
138161

139162
axis: int | None = None
140163

141-
def _get_block_size(self, block_sizes_list: list[int]) -> list[int]:
164+
def _get_block_size(
165+
self,
166+
block_sizes_list: list[int],
167+
quantization_target: _CompressionTargetTensor = _CompressionTargetTensor.WEIGHT,
168+
) -> list[int]:
142169

143170
if self.axis is None:
144171
raise ValueError(
@@ -185,39 +212,63 @@ class PerBlockGranularity(QuantizationGranularity):
185212
``Quantizer.prepare()`` automatically resolves the axis based on the module type
186213
for weight quantization.
187214
215+
Single-axis mode treats weights and activations differently. For weights only
216+
the two leading channel axes participate in blocking, so trailing kernel
217+
dimensions span a whole block. For activations every axis other than the block
218+
axis gets its own scale.
219+
188220
.. list-table::
189221
:header-rows: 1
190222
191-
* - Weight tensor shape (input)
223+
* - Tensor shape (input)
224+
- target
192225
- axis
193226
- block_size
194-
- Weight shape of each block (output)
227+
- Shape of each block (output)
195228
* - [C_out, C_in]
229+
- weight
196230
- 1
197231
- 32
198232
- [1, 32]
199233
* - [C_out, C_in]
234+
- weight
200235
- None
201236
- (4, 8)
202237
- [4, 8]
203238
* - [C_out, C_in, KH, KW]
239+
- weight
204240
- 0
205241
- 16
206242
- [16, 1, KH, KW]
207243
* - [C_out, C_in, KH, KW]
244+
- weight
208245
- None
209246
- (4, 16, 3, -1)
210247
- [4, 16, 3, KW]
248+
* - [B, S, D]
249+
- activation
250+
- -1
251+
- 16
252+
- [1, 1, 16]
253+
* - [B, C, H, W]
254+
- activation
255+
- 1
256+
- 16
257+
- [1, 16, 1, 1]
211258
"""
212259

213260
axis: int | None = None
214261
block_size: Annotated[int, Field(gt=0)] | tuple[Annotated[int, Field(gt=0)] | Literal[-1], ...]
215262

216-
def _get_block_size(self, block_sizes_list: list[int]) -> list[int]:
263+
def _get_block_size(
264+
self,
265+
block_sizes_list: list[int],
266+
quantization_target: _CompressionTargetTensor = _CompressionTargetTensor.WEIGHT,
267+
) -> list[int]:
217268
if isinstance(self.block_size, tuple):
218269
return self._handle_multi_axis_block_size(block_sizes_list)
219270
else:
220-
return self._handle_single_axis_block_size(block_sizes_list)
271+
return self._handle_single_axis_block_size(block_sizes_list, quantization_target)
221272

222273
def _handle_multi_axis_block_size(self, block_sizes_list: list[int]) -> list[int]:
223274
"""Handle blocking when self.block_size is a tuple"""
@@ -247,7 +298,11 @@ def _handle_multi_axis_block_size(self, block_sizes_list: list[int]) -> list[int
247298

248299
return block_sizes_list
249300

250-
def _handle_single_axis_block_size(self, block_sizes_list: list[int]) -> list[int]:
301+
def _handle_single_axis_block_size(
302+
self,
303+
block_sizes_list: list[int],
304+
quantization_target: _CompressionTargetTensor = _CompressionTargetTensor.WEIGHT,
305+
) -> list[int]:
251306
"""Handle blocking when self.block_size is an integer"""
252307
# TODO: Logic to be added where if self.axis is None,
253308
# we can figure out the optimal axis for the user
@@ -272,12 +327,25 @@ def _handle_single_axis_block_size(self, block_sizes_list: list[int]) -> list[in
272327
f"is not divisible by block size {self.block_size}"
273328
)
274329

275-
# Set the specified axis to block_size, and set the other channel axis
276-
# (the one of {0, 1} that is not the block axis) to 1 so that it is
277-
# quantized per-slice. Any remaining higher dimensions (index 2+) are
278-
# left unchanged.
330+
# How the non-block axes are treated depends on the quantization target,
331+
#
332+
# WEIGHT: only the two leading channel axes participate. The other
333+
# channel axis becomes 1 (one scale per slice) while any trailing
334+
# dimensions (index 2+, e.g. conv kernel dims) keep their full size so
335+
# each block spans the whole kernel.
336+
# [C_out, C_in, KH, KW], axis=0, block=16 -> [16, 1, KH, KW]
337+
#
338+
# ACTIVATION: blocking runs along a single axis and every other
339+
# dimension gets its own scale, so all non-block axes become 1.
340+
# [B, S, D], axis=-1, block=16 -> [1, 1, 16]
341+
# [B, C, H, W], axis=1, block=16 -> [1, 16, 1, 1]
342+
if quantization_target == _CompressionTargetTensor.ACTIVATION:
343+
collapse_upto = rank
344+
else:
345+
collapse_upto = 2
346+
279347
block_sizes_list[axis] = self.block_size
280-
for i, _ in enumerate(block_sizes_list[:2]):
348+
for i, _ in enumerate(block_sizes_list[:collapse_upto]):
281349
if i != axis:
282350
block_sizes_list[i] = 1
283351

src/coreai_opt/quantization/spec/qparams_calculator.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ def granularity(self) -> QuantizationGranularity:
8282
"""Getter for granularity."""
8383
return self._granularity
8484

85+
@property
86+
def quantization_target(self):
87+
"""Whether the quantized tensor is a weight or an activation."""
88+
return self.range_calculator.quantization_target
89+
8590
@granularity.setter
8691
def granularity(self, granularity: QuantizationGranularity) -> None:
8792
"""Update granularity for this calculator and its range calculator.
@@ -117,7 +122,9 @@ def _get_tensor_with_granularity_from_scalar(
117122
Return a tensor with dimensions equal to num blocks in each dimension, comprised
118123
of values equal to scalar.
119124
"""
120-
block_size_list = self.granularity.get_block_size(input_tensor.shape)
125+
block_size_list = self.granularity.get_block_size(
126+
input_tensor.shape, self.quantization_target
127+
)
121128
num_blocks_list = [
122129
inp_size // block_size
123130
for inp_size, block_size in zip(input_tensor.shape, block_size_list, strict=True)
@@ -229,7 +236,7 @@ def _compute_scale_zero_point_minval(
229236
min_val=min_val,
230237
max_val=max_val,
231238
mapping_type=QuantizationScheme._to_mapping_type(self.qscheme),
232-
block_size=self.granularity.get_block_size(tensor.shape),
239+
block_size=self.granularity.get_block_size(tensor.shape, self.quantization_target),
233240
target_dtype=self.target_dtype,
234241
quant_min=self.quant_min,
235242
quant_max=self.quant_max,

src/coreai_opt/quantization/spec/range_calculator.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from torchao.quantization.quant_primitives import _get_reduction_params
1111

1212
from coreai_opt._utils.registry_utils import ClassRegistryMixin as _ClassRegistryMixin
13+
from coreai_opt.config.spec import CompressionTargetTensor as _CompressionTargetTensor
1314

1415
from .granularity import QuantizationGranularity
1516

@@ -20,16 +21,22 @@ class RangeCalculatorBase(_ClassRegistryMixin, nn.Module):
2021
of a given tensor.
2122
"""
2223

23-
def __init__(self, granularity: QuantizationGranularity, **kwargs):
24+
def __init__(
25+
self,
26+
granularity: QuantizationGranularity,
27+
quantization_target: _CompressionTargetTensor = _CompressionTargetTensor.WEIGHT,
28+
**kwargs,
29+
):
2430
super().__init__()
2531
self.granularity = granularity
32+
self.quantization_target = quantization_target
2633

2734
def _reshape_min_max(self, range_tensor: torch.Tensor, input_shape: torch.Size):
2835
"""
2936
Reshape range_tensor to have the same number of dimensions as input shape,
3037
taking block size into account.
3138
"""
32-
block_size_list = self.granularity.get_block_size(input_shape)
39+
block_size_list = self.granularity.get_block_size(input_shape, self.quantization_target)
3340

3441
# While reducing, each dimension with block size other than 1 or the original
3542
# dimension size will be split into 2 dimensions of num_blocks and block_size.
@@ -77,7 +84,7 @@ class MinMaxRangeCalculator(RangeCalculatorBase):
7784
"""
7885

7986
def _generate_min_max(self, tensor: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
80-
block_size_list = self.granularity.get_block_size(tensor.shape)
87+
block_size_list = self.granularity.get_block_size(tensor.shape, self.quantization_target)
8188
shape_for_reduction, reduction_dims = _get_reduction_params(block_size_list, tensor.size())
8289

8390
# If tensor is already the shape required, no minmaxing is needed.

tests/quantization/test_factory.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,14 @@
3838
class TestQuantizationComponentFactory:
3939
"""Test the QuantizationComponentFactory class"""
4040

41-
def test_create_range_calculator(self):
41+
@pytest.mark.parametrize(
42+
"quantization_target",
43+
[
44+
CompressionTargetTensor.WEIGHT,
45+
CompressionTargetTensor.ACTIVATION,
46+
],
47+
)
48+
def test_create_range_calculator(self, quantization_target):
4249
"""Test creating range calculator from spec"""
4350
spec = QuantizationSpec(
4451
dtype=torch.int8,
@@ -49,10 +56,11 @@ def test_create_range_calculator(self):
4956
range_calculator_cls=MinMaxRangeCalculator,
5057
)
5158

52-
range_calc = QuantizationComponentFactory.create_range_calculator(spec)
59+
range_calc = QuantizationComponentFactory.create_range_calculator(spec, quantization_target)
5360

5461
assert isinstance(range_calc, MinMaxRangeCalculator)
5562
assert range_calc.granularity == spec.granularity
63+
assert range_calc.quantization_target == quantization_target
5664

5765
@pytest.mark.parametrize(
5866
"range",

0 commit comments

Comments
 (0)