Skip to content

Commit 734573b

Browse files
committed
Address review comments
1 parent bb16d77 commit 734573b

5 files changed

Lines changed: 120 additions & 50 deletions

File tree

src/coreai_opt/base_model_compressor.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from abc import ABC, abstractmethod
1313
from contextlib import contextmanager
14+
from enum import Enum
1415
from os import PathLike
1516

1617
import torch
@@ -21,12 +22,21 @@
2122
_COREAI_OPT_PREPARED_ATTR = "_coreai_opt_prepared"
2223

2324

25+
class _CompressorLifecycle(Enum):
26+
"""Lifecycle state of a model compressor."""
27+
28+
IDLE = "idle"
29+
TRAINING = "training"
30+
CALIBRATING = "calibrating"
31+
32+
2433
class _BaseModelCompressor(ABC):
2534
"""
2635
An abstract base class for implementing model compression techniques.
2736
"""
2837

2938
_supported_modules: tuple[type[torch.nn.Module]]
39+
_lifecycle: _CompressorLifecycle
3040

3141
def __init__(self, model: torch.nn.Module, config: CompressionConfig | None = None):
3242
"""
@@ -39,6 +49,7 @@ def __init__(self, model: torch.nn.Module, config: CompressionConfig | None = No
3949
"""
4050
self._model = model
4151
self._config = config
52+
self._lifecycle = _CompressorLifecycle.IDLE
4253

4354
@staticmethod
4455
def _is_model_prepared(model: torch.nn.Module) -> bool:

src/coreai_opt/palettization/kmeans/kmeans_fake_palettize.py

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -155,23 +155,19 @@ def sensitivities(self, value: torch.Tensor | None) -> None:
155155
self._centroids_initialized = False
156156
self._indices_stale = True
157157

158-
def forward(self, tensor: torch.Tensor) -> torch.Tensor:
159-
if self._disabled:
160-
return tensor
161-
162-
if not self._centroids_initialized:
163-
try:
164-
self._initialize(tensor.detach())
165-
except (_IncompatibleClusterDimError, _IncompatibleGranularityError) as e:
166-
logger.warning(
167-
f"Tensor incompatible with configured spec: {e}. Skipping palettization."
168-
)
169-
self._disabled = True
170-
return tensor
171-
172-
if self.fake_palett_enabled[0] == 0:
173-
return tensor
158+
def ensure_initialized(self, tensor: torch.Tensor) -> None:
159+
"""Cluster centroids on first use; disable on an incompatible tensor."""
160+
if self._centroids_initialized:
161+
return
162+
try:
163+
self._initialize(tensor.detach())
164+
except (_IncompatibleClusterDimError, _IncompatibleGranularityError) as e:
165+
logger.warning(
166+
f"Tensor incompatible with configured spec: {e}. Skipping palettization."
167+
)
168+
self._disabled = True
174169

170+
def forward_enabled(self, tensor: torch.Tensor) -> torch.Tensor:
175171
if self.training:
176172
return self._training_strategy.train_forward(self, tensor)
177173
return self.hard_assign(tensor)
@@ -223,14 +219,14 @@ def lut(self) -> torch.Tensor | None:
223219

224220
raw_lut = self._raw_lut(self.centroids)
225221
if self._lut_fake_quantizer is None:
226-
lut = raw_lut
227-
else:
228-
orig_dtype = raw_lut.dtype
229-
scale, zero_point, minval = self._lut_fake_quantizer.qparams_calculator.get_qparams()
230-
lut = self._lut_fake_quantizer._fused_fake_quant_dequant(
231-
raw_lut.to(torch.float32), scale, zero_point, minval
232-
).to(orig_dtype)
233-
return self._reshape_lut_tensor(lut)
222+
return self._reshape_lut_tensor(raw_lut)
223+
224+
orig_dtype = raw_lut.dtype
225+
scale, zero_point, minval = self._lut_fake_quantizer.qparams_calculator.get_qparams()
226+
fq_lut = self._lut_fake_quantizer._fused_fake_quant_dequant(
227+
raw_lut.to(torch.float32), scale, zero_point, minval
228+
).to(orig_dtype)
229+
return self._reshape_lut_tensor(fq_lut)
234230

235231
@property
236232
def quantized_lut(self) -> torch.Tensor | None:

src/coreai_opt/palettization/kmeans/palettizer.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
from coreai_opt._utils.spec_utils import PartialConstructor as _PartialConstructor
3030
from coreai_opt._utils.torch_utils import (
3131
move_model_to_eval as _move_model_to_eval,
32+
move_model_to_train as _move_model_to_train,
3233
remove_compression_parametrizations as _remove_compression_parametrizations,
3334
)
35+
from coreai_opt.base_model_compressor import _CompressorLifecycle
3436
from coreai_opt.common import ExportBackend
3537
from coreai_opt.config.compression_config import ModuleCompressionConfig, ModuleConfigDict
3638
from coreai_opt.config.spec import CompressionTargetTensor
@@ -146,7 +148,6 @@ def __init__(self, model: torch.nn.Module, config: KMeansPalettizerConfig | None
146148

147149
self._num_workers = 1
148150

149-
self._mode: str = "idle" # "idle" | "training" | "calibrating"
150151
self._step_count: int = 0
151152
self._fp_to_schedule: dict[_KMeansFakePalettize, PATSchedule] = {}
152153
self._module_config_dict: ModuleConfigDict[ModuleCompressionConfig] = {}
@@ -288,9 +289,11 @@ def calibration_mode(
288289
"Model must be prepared before entering calibration mode. Call prepare() first."
289290
)
290291

291-
if self._mode != "idle":
292-
raise RuntimeError(f"Cannot enter calibration_mode() while palettizer is {self._mode}")
293-
self._mode = "calibrating"
292+
if self._lifecycle is not _CompressorLifecycle.IDLE:
293+
raise RuntimeError(
294+
f"Cannot enter calibration_mode() while palettizer is {self._lifecycle.value}"
295+
)
296+
self._lifecycle = _CompressorLifecycle.CALIBRATING
294297
try:
295298
# Save model checkpoint before modifying gradients
296299
checkpoint_path = self._save_model_checkpoint(self._model)
@@ -346,30 +349,34 @@ def step(self, output: torch.Tensor, target: torch.Tensor):
346349
# Restore normal operation
347350
self._model.apply(_enable_fake_palett)
348351
finally:
349-
self._mode = "idle"
352+
self._lifecycle = _CompressorLifecycle.IDLE
350353

351354
@contextmanager
352355
def training_mode(self):
353356
"""Context manager wrapping a training loop. Mutually exclusive with
354357
calibration_mode().
355358
"""
356-
if self._mode != "idle":
357-
raise RuntimeError(f"Cannot enter training_mode() while palettizer is {self._mode}")
358-
self._mode = "training"
359-
try:
360-
self._model.train()
361-
self._build_fp_to_schedule()
362-
self._apply_schedule()
359+
if not self._is_model_prepared(self._model):
360+
raise RuntimeError(
361+
"Model must be prepared before entering training mode. Call prepare() first."
362+
)
363+
364+
if self._lifecycle is not _CompressorLifecycle.IDLE:
365+
raise RuntimeError(
366+
f"Cannot enter training_mode() while palettizer is {self._lifecycle.value}"
367+
)
368+
self._lifecycle = _CompressorLifecycle.TRAINING
369+
with _move_model_to_train(self._model):
363370
try:
371+
self._build_fp_to_schedule()
372+
self._apply_schedule()
364373
yield self
365374
finally:
366-
self._model.eval()
367-
finally:
368-
self._mode = "idle"
375+
self._lifecycle = _CompressorLifecycle.IDLE
369376

370377
def step(self) -> None:
371378
"""Advance the schedule by one step. Must be called inside training_mode()."""
372-
if self._mode != "training":
379+
if self._lifecycle is not _CompressorLifecycle.TRAINING:
373380
raise RuntimeError("step() must be called inside a training_mode() context.")
374381
self._step_count += 1
375382
self._apply_schedule()

src/coreai_opt/palettization/spec/fake_palettize.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,36 @@ def is_disabled(self) -> bool:
5656
"""Return True if fake palettization has been disabled."""
5757
return self._disabled
5858

59-
@abstractmethod
6059
def forward(self, tensor: torch.Tensor) -> torch.Tensor:
61-
"""Apply fake palettization to input tensor."""
60+
"""Fake-palettize ``tensor`` through the enable/disable lifecycle.
61+
62+
Delegates representation-specific work to ``ensure_initialized`` and
63+
``forward_enabled``.
64+
"""
65+
if self._disabled:
66+
return tensor
67+
68+
self.ensure_initialized(tensor)
69+
70+
# Check for self._disabled again in case ensure_initialized disabled the palettizer.
71+
if self._disabled:
72+
return tensor
73+
if self.fake_palett_enabled[0] == 0:
74+
return tensor
75+
return self.forward_enabled(tensor)
76+
77+
@abstractmethod
78+
def ensure_initialized(self, tensor: torch.Tensor) -> None:
79+
"""Initialize compression parameters from ``tensor`` on first use.
80+
81+
Set ``self._disabled = True`` if ``tensor`` is incompatible with the
82+
configured spec.
83+
"""
84+
raise NotImplementedError()
85+
86+
@abstractmethod
87+
def forward_enabled(self, tensor: torch.Tensor) -> torch.Tensor:
88+
"""Return the palettized output for ``tensor`` when enabled."""
6289
raise NotImplementedError()
6390

6491
@abstractmethod

tests/palettization/test_pat_schedule.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import torch.nn.utils.parametrize as P
2020
from pydantic import ValidationError
2121

22+
from coreai_opt.base_model_compressor import _CompressorLifecycle
2223
from coreai_opt.palettization import (
2324
KMeansPalettizer,
2425
KMeansPalettizerConfig,
@@ -114,13 +115,31 @@ def test_frozen(self):
114115
class TestTrainingMode:
115116
"""Runtime behavior of KMeansPalettizer.training_mode()."""
116117

118+
def test_training_mode_requires_prepared_model(self):
119+
config = KMeansPalettizerConfig(
120+
global_config=ModuleKMeansPalettizerConfig(
121+
op_state_spec={"weight": default_weight_palettization_spec()},
122+
)
123+
)
124+
palettizer = KMeansPalettizer(ToyModel(), config)
125+
with pytest.raises(RuntimeError, match="Model must be prepared"):
126+
with palettizer.training_mode():
127+
pass
128+
117129
def test_entry_trains_exit_evals(self):
118130
palettizer, prepared = _prepared_palettizer()
119131
prepared.eval()
120132
with palettizer.training_mode():
121133
assert prepared.training is True
122134
assert prepared.training is False
123135

136+
def test_entry_from_train_stays_train_on_exit(self):
137+
palettizer, prepared = _prepared_palettizer()
138+
prepared.train()
139+
with palettizer.training_mode():
140+
assert prepared.training is True
141+
assert prepared.training is True
142+
124143
def test_default_no_schedule_stays_enabled(self):
125144
palettizer, prepared = _prepared_palettizer()
126145
with palettizer.training_mode():
@@ -161,9 +180,9 @@ def test_mode_restored_to_idle_on_exception(self):
161180
palettizer, _ = _prepared_palettizer()
162181
with pytest.raises(ValueError, match="boom"):
163182
with palettizer.training_mode():
164-
assert palettizer._mode == "training"
183+
assert palettizer._lifecycle is _CompressorLifecycle.TRAINING
165184
raise ValueError("boom")
166-
assert palettizer._mode == "idle"
185+
assert palettizer._lifecycle is _CompressorLifecycle.IDLE
167186

168187

169188
class TestStep:
@@ -298,10 +317,12 @@ def forward(self, x):
298317
class TestDefaultStrategyTraining:
299318
"""The default training strategy's behavior inside a training_mode() loop."""
300319

301-
def test_freezes_palettized_weight_but_trains_the_rest(self):
302-
"""The default strategy reconstructs from frozen centroids, so a
303-
palettized weight receives no gradient during training, while
304-
non-palettized parameters still train against the palettized values.
320+
@pytest.mark.parametrize("use_training_mode_ctx", [True, False])
321+
def test_gradient_flow_with_and_without_training_mode_context(self, use_training_mode_ctx):
322+
"""Training-time gradients must flow through the palettized layer whether
323+
or not the training_mode() context is active: the input and downstream
324+
params receive gradients while the frozen palettized weight receives none
325+
(no unintended gradient path).
305326
"""
306327
config = KMeansPalettizerConfig(
307328
module_name_configs={
@@ -314,8 +335,16 @@ def test_freezes_palettized_weight_but_trains_the_rest(self):
314335
palettizer = KMeansPalettizer(MixedModel(), config)
315336
prepared = palettizer.prepare((torch.randn(2, 16),))
316337

317-
with palettizer.training_mode():
318-
prepared(torch.randn(2, 16)).sum().backward()
338+
x = torch.randn(2, 16, requires_grad=True)
339+
if use_training_mode_ctx:
340+
with palettizer.training_mode():
341+
prepared(x).sum().backward()
342+
else:
343+
prepared.train()
344+
prepared(x).sum().backward()
319345

346+
# frozen palettized weight gets no gradient (no unintended path)
320347
assert prepared.palettized.parametrizations.weight.original.grad is None
348+
# graph intact: gradients reach the input (through the palettized layer) and downstream
349+
assert x.grad is not None
321350
assert prepared.head.weight.grad is not None

0 commit comments

Comments
 (0)