1212from pydantic import BaseModel , ConfigDict , Field , model_serializer
1313
1414from coreai_opt ._utils .registry_utils import ConfigRegistryMixin as _ConfigRegistryMixin
15+ from coreai_opt .config .spec import CompressionTargetTensor as _CompressionTargetTensor
1516from 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
0 commit comments