Skip to content

Commit b1c3ec8

Browse files
authored
per-tensor-granularity-for-hgq (#44)
* Use config quantization parameter named granularity to determine HGQ granularity, either per-tensor or per-weight
1 parent 158f4e8 commit b1c3ec8

13 files changed

Lines changed: 814 additions & 38 deletions

src/pquant/core/keras/activations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def build(self, input_shape):
102102
hgq_gamma=self.hgq_gamma,
103103
place="datalane",
104104
dynamic_data=self.dynamic_data,
105+
granularity=self.config.quantization_parameters.granularity,
105106
)
106107
if self.quantize_output:
107108
self.output_quantizer = Quantizer(
@@ -115,6 +116,7 @@ def build(self, input_shape):
115116
hgq_gamma=self.hgq_gamma,
116117
place="datalane",
117118
dynamic_data=self.dynamic_data,
119+
granularity=self.config.quantization_parameters.granularity,
118120
)
119121

120122
if self.use_multiplier:

src/pquant/core/keras/layers.py

Lines changed: 111 additions & 3 deletions
Large diffs are not rendered by default.

src/pquant/core/keras/quantizer.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ def __init__(
3737
self.place = place
3838
self.granularity = granularity.value if isinstance(granularity, Enum) else granularity
3939
self.quantizer = create_quantizer(
40-
self.k_init, self.i_init, self.f_init, self.overflow, self.round_mode, self.use_hgq, self.is_data, place
40+
self.k_init,
41+
self.i_init,
42+
self.f_init,
43+
self.overflow,
44+
self.round_mode,
45+
self.use_hgq,
46+
self.is_data,
47+
place,
48+
granularity=self.granularity,
4149
)
4250
self.is_pretraining = True
4351
self.hgq_gamma = hgq_gamma
@@ -205,14 +213,41 @@ def get_config(self):
205213
return config
206214

207215

208-
def create_hgq_parameters_quantizer(k, i, f, overflow, round_mode, place, gamma=1e-8):
216+
def axis_kwargs_for_granularity(granularity, is_data):
217+
"""Translate a granularity into HGQ's (mutually exclusive) homogeneous/heterogeneous axis spec.
218+
219+
HGQ only supports per_tensor and per_weight from the granularity enum:
220+
- per_tensor: nothing varies -> heterogeneous_axis=() (one bitwidth for the whole tensor)
221+
- per_weight: every element varies. For data we keep the batch axis (0) homogeneous via
222+
homogeneous_axis=(0,); for weights nothing is shared via homogeneous_axis=().
223+
224+
per_channel is intentionally NOT supported for HGQ (the channel axis is layout-dependent, so we
225+
don't guess it).
226+
"""
227+
if granularity == "per_tensor":
228+
return {"heterogeneous_axis": ()}
229+
if granularity == "per_weight":
230+
return {"homogeneous_axis": (0,) if is_data else ()}
231+
if granularity == "per_channel":
232+
raise ValueError("per_channel granularity is not supported for HGQ. Use 'per_tensor' or 'per_weight'.")
233+
raise ValueError(f"Unsupported granularity: {granularity}")
234+
235+
236+
def create_hgq_parameters_quantizer(k, i, f, overflow, round_mode, place, axis_kwargs, gamma=1e-8):
209237
quantizer_config = QuantizerConfig(
210-
q_type="kif", place=place, k0=k, i0=i, f0=f, overflow_mode=overflow, round_mode=round_mode, homogeneous_axis=()
238+
q_type="kif",
239+
place=place,
240+
k0=k,
241+
i0=i,
242+
f0=f,
243+
overflow_mode=overflow,
244+
round_mode=round_mode,
245+
**axis_kwargs,
211246
)
212247
return HGQQuantizer(config=quantizer_config)
213248

214249

215-
def create_hgq_data_quantizer(k, i, f, overflow, round_mode, gamma=1e-8):
250+
def create_hgq_data_quantizer(k, i, f, overflow, round_mode, axis_kwargs, gamma=1e-8):
216251
quantizer_config = QuantizerConfig(
217252
q_type="kif",
218253
place="datalane",
@@ -221,16 +256,19 @@ def create_hgq_data_quantizer(k, i, f, overflow, round_mode, gamma=1e-8):
221256
f0=f,
222257
overflow_mode=overflow,
223258
round_mode=round_mode,
224-
homogeneous_axis=(0,),
259+
**axis_kwargs,
225260
)
226261
return HGQQuantizer(config=quantizer_config)
227262

228263

229-
def create_quantizer(k, i, f, overflow, round_mode, is_heterogeneous, is_data, place="datalane", gamma=1e-8):
264+
def create_quantizer(
265+
k, i, f, overflow, round_mode, is_heterogeneous, is_data, place="datalane", granularity="per_weight", gamma=1e-8
266+
):
230267
if is_heterogeneous:
268+
axis_kwargs = axis_kwargs_for_granularity(granularity, is_data)
231269
if is_data:
232-
return create_hgq_data_quantizer(k, i, f, overflow, round_mode, gamma=gamma)
270+
return create_hgq_data_quantizer(k, i, f, overflow, round_mode, axis_kwargs, gamma=gamma)
233271
else:
234-
return create_hgq_parameters_quantizer(k, i, f, overflow, round_mode, place, gamma=gamma)
272+
return create_hgq_parameters_quantizer(k, i, f, overflow, round_mode, place, axis_kwargs, gamma=gamma)
235273
else:
236274
return get_fixed_quantizer(round_mode=round_mode, overflow_mode=overflow)

src/pquant/core/torch/activations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def check_is_built(self, input_shape):
107107
hgq_gamma=self.hgq_gamma,
108108
place="datalane",
109109
dynamic_data=self.dynamic_data,
110+
granularity=self.config.quantization_parameters.granularity,
110111
)
111112
self.input_quantizer = Quantizer(
112113
k=self.k_input,
@@ -119,6 +120,7 @@ def check_is_built(self, input_shape):
119120
hgq_gamma=self.hgq_gamma,
120121
place="datalane",
121122
dynamic_data=self.dynamic_data,
123+
granularity=self.config.quantization_parameters.granularity,
122124
)
123125
if self.use_hgq:
124126
self.input_quantizer.quantizer.build(input_shape)

src/pquant/core/torch/hgq_quantizer.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ class HGQQuantizer(nn.Module):
4444
round_mode : str
4545
One of 'RND', 'RND_CONV', 'TRN', etc.
4646
is_data : bool
47-
True → data/activation quantizer (homogeneous over batch axis 0).
48-
False → weight/bias quantizer (fully heterogeneous, per-element).
47+
True → data/activation quantizer (batch axis 0 always homogeneous;
48+
per_channel keys on channel axis 1).
49+
False → weight/bias quantizer (per_channel keys on output-channel axis 0).
50+
granularity : str
51+
One of 'per_tensor' (one shared bit-width) or 'per_weight' (one per element).
52+
Controls which axes are shared. per_channel is not supported.
4953
gamma : float
5054
L1 regularisation coefficient on bit-widths.
5155
i_decay_speed : float
@@ -73,6 +77,7 @@ def __init__(
7377
overflow_mode: str,
7478
round_mode: str,
7579
is_data: bool,
80+
granularity: str = "per_weight",
7681
gamma: float = 1e-8,
7782
i_decay_speed: float = float("inf"),
7883
i_min: float = -23.0,
@@ -92,6 +97,7 @@ def __init__(
9297
self.overflow_mode = overflow_mode.upper()
9398
self.round_mode = round_mode.upper()
9499
self.is_data = is_data
100+
self.granularity = granularity
95101
self.gamma = gamma
96102
self.i_decay_speed = i_decay_speed
97103
self.i_min = i_min
@@ -129,10 +135,9 @@ def build(self, input_shape: tuple) -> None:
129135
parameters, not the scalar placeholders from __init__.
130136
"""
131137
device = self._k.device
138+
self.homogeneous_axis = self._homogeneous_axis(len(input_shape))
132139
bw_shape = self._infer_bw_shape(input_shape)
133140

134-
self.homogeneous_axis = (0,) if self.is_data else ()
135-
136141
# k: non-trainable sign-bit buffer
137142
self.register_buffer("_k", torch.full(bw_shape, self.k0, device=device))
138143

@@ -146,15 +151,23 @@ def build(self, input_shape: tuple) -> None:
146151

147152
self._built = True
148153

154+
def _homogeneous_axis(self, ndim: int) -> tuple[int, ...]:
155+
"""Axes shared (collapsed to 1 in the bit-width tensor), derived from `granularity`.
156+
157+
HGQ supports only per_tensor and per_weight (data always shares the batch axis 0).
158+
per_channel is intentionally unsupported (the channel axis is layout-dependent).
159+
"""
160+
if self.granularity == "per_tensor":
161+
return tuple(range(ndim))
162+
if self.granularity == "per_weight":
163+
return (0,) if self.is_data else ()
164+
if self.granularity == "per_channel":
165+
raise ValueError("per_channel granularity is not supported for HGQ. Use 'per_tensor' or 'per_weight'.")
166+
raise ValueError(f"Unsupported granularity: {self.granularity}")
167+
149168
def _infer_bw_shape(self, input_shape: tuple) -> tuple:
150-
"""Shape of bit-width parameter tensors given input tensor shape."""
151-
if self.is_data:
152-
# Batch axis (0) is homogeneous → dimension 0 collapses to 1.
153-
shape = list(input_shape)
154-
shape[0] = 1
155-
return tuple(shape)
156-
# Fully heterogeneous (per-parameter): same shape as input.
157-
return tuple(input_shape)
169+
"""Shape of bit-width parameter tensors: homogeneous axes collapse to 1."""
170+
return tuple(1 if ax in self.homogeneous_axis else d for ax, d in enumerate(input_shape))
158171

159172
# ------------------------------------------------------------------
160173
# Properties

0 commit comments

Comments
 (0)