1919import torch .nn .utils .parametrize as P
2020from pydantic import ValidationError
2121
22+ from coreai_opt .base_model_compressor import _CompressorLifecycle
2223from coreai_opt .palettization import (
2324 KMeansPalettizer ,
2425 KMeansPalettizerConfig ,
@@ -114,13 +115,31 @@ def test_frozen(self):
114115class 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
169188class TestStep :
@@ -298,10 +317,12 @@ def forward(self, x):
298317class 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