Refactor KMeansPalettizer, add extendable training strategy capability - #60
Refactor KMeansPalettizer, add extendable training strategy capability#60crowbat wants to merge 3 commits into
Conversation
67ade25 to
2a9325e
Compare
| raise NotImplementedError | ||
|
|
||
|
|
||
| @TrainingStrategy.register("default") |
There was a problem hiding this comment.
Now that we have TrainingStrategyConfig being the main object users use when configuring PalettizationSpec, associating TrainingStrategy with a registry is not needed anymore (this was based on a previous design). I will remove the registry association.
2a9325e to
44fbc11
Compare
u-simha
left a comment
There was a problem hiding this comment.
Mostly looks good, have left some comments.
One thing to note, I think this might be backward-incompatible for an older palettization checkpoint, since we have added / modified buffers?
| def _refresh_indices(self, weight: torch.Tensor) -> None: | ||
| """Recompute indices from the current centroids, without re-clustering.""" | ||
| self.indices = self._assign_indices(weight, self.centroids).detach() | ||
| self._indices_stale = False |
There was a problem hiding this comment.
Should we re-assign only if the indices are stale? And have a flag to force re-assignment
There was a problem hiding this comment.
self._indices_stale is meant to be the flag for forcing re-assignment which is checked in the forward pass during hard_assign. _refresh_indices is only ever called there if self._indices_stale is True, so I think this covers your concern?
| else: | ||
| orig_dtype = raw_lut.dtype | ||
| scale, zero_point, minval = self._lut_fake_quantizer.qparams_calculator.get_qparams() | ||
| lut = self._lut_fake_quantizer._fused_fake_quant_dequant( |
| ) -> tuple[torch.Tensor, torch.Tensor]: | ||
| weight = original_weights.cpu() | ||
| @property | ||
| def quantized_lut(self) -> torch.Tensor | None: |
There was a problem hiding this comment.
Minor suggestion: this can be reused in the lut function with a fq flag
There was a problem hiding this comment.
This part, along with lut_quantization_scale and lut_quantization_zero_point were made into properties to try to preserve some backwards comaptibleness with the previous implementation which stored all 3 as separate buffers. Though there is still incompatibility when it comes to being able to save and load actual buffer values so perhaps we can consider deprecating them instead
| if self.enable_per_channel_scale: | ||
| weight = self._scale_by_per_channel_scale(weight) | ||
| @property | ||
| def lut_quantization_scale(self) -> torch.Tensor | None: |
There was a problem hiding this comment.
Can we have lut_quantization_qparams and return both the scale & zero point together? Any reason this is separated?
| with palettizer.training_mode(): | ||
| prepared(torch.randn(2, 16)).sum().backward() | ||
|
|
||
| assert prepared.palettized.parametrizations.weight.original.grad is None | ||
| assert prepared.head.weight.grad is not None |
There was a problem hiding this comment.
Can we have a bit more extensive test that tests that we don't break gradients within and outside of the training mode context:
I saw a couple places we call .detach() which sometimes breaks the gradients if it is part of the backward pass (or can short circuit and have the gradients flowing through unintended variables - this happened in quantization at some point)
There was a problem hiding this comment.
Sounds good, I'll add some coverage for this too
| self._fp_to_schedule[param] = schedule | ||
| break | ||
|
|
||
| def _resolve_schedule(self, module_name: str) -> PATSchedule | None: |
There was a problem hiding this comment.
Would this follow the module priority while applying the PAT schedule? I think I should use this for the QAT schedule too
There was a problem hiding this comment.
This part of the code is pretty much a mirror of what we have in quantizer: https://github.com/apple/coreai-optimization/blob/main/src/coreai_opt/quantization/quantizer.py#L184
so it may inherently have the same issue as QAT
|
|
||
| def _apply_schedule(self) -> None: | ||
| for fp_module, schedule in self._fp_to_schedule.items(): | ||
| fp_module.enable_fake_palett(schedule._compute_state(self._step_count)) |
There was a problem hiding this comment.
Would it be better to do fp_module.apply(enable_fake_palett) that way even children modules have the schedule applied?
There was a problem hiding this comment.
Given that fp_to_schedule is supposed to carry only leaf level modules mapping to schedules, it may be better to keep it as is to ensure we are setting the schedule for exactly the module we intend and no more accidentally
|
|
||
| @abstractmethod | ||
| def forward(self, tensor: torch.Tensor) -> torch.Tensor: | ||
| """Apply fake palettization to input tensor""" |
There was a problem hiding this comment.
I don't fully follow the reason for this logic to be moved to the downstream class. Is it because of the training strategy? I would think that would be generic across all fake palletize, and not specific to KMeans
There was a problem hiding this comment.
At first, due to the removal of observer_enabled, most of this function simply got deleted and what was left seemed to get overridden anyways by much of the newly added things in kmeans_fake_palettizer, so it felt like this function wasn't lifting any weight.
But I think we can reframe some of what is in kmeans_fake_palettizer to be generic. I'll try some alternatives to see
| self._model.apply(_enable_observer) | ||
| @contextmanager | ||
| def training_mode(self): | ||
| """Context manager wrapping a training loop. Mutually exclusive with |
There was a problem hiding this comment.
We should check that the model is prepared, similar to what we do for quantization
| self._mode = "training" | ||
| try: | ||
| self._model.train() | ||
| self._build_fp_to_schedule() |
There was a problem hiding this comment.
Can we cache the fp_to_schedule since it shouldn't change across entering the training mode context?
I would say a common way to call this code would be to enter it on every batch step / epoch step and exit it while doing eval
There was a problem hiding this comment.
Internally _build_fp_to_schedule() does check whether self._fp_to_schedule exists and returns that if it exists already. I can rename the function to _get_fp_to_schedule() to be less misleading
| def _refresh_indices(self, weight: torch.Tensor) -> None: | ||
| """Recompute indices from the current centroids, without re-clustering.""" | ||
| self.indices = self._assign_indices(weight, self.centroids).detach() | ||
| self._indices_stale = False |
There was a problem hiding this comment.
self._indices_stale is meant to be the flag for forcing re-assignment which is checked in the forward pass during hard_assign. _refresh_indices is only ever called there if self._indices_stale is True, so I think this covers your concern?
| self._mode = "training" | ||
| try: | ||
| self._model.train() | ||
| self._build_fp_to_schedule() |
There was a problem hiding this comment.
Internally _build_fp_to_schedule() does check whether self._fp_to_schedule exists and returns that if it exists already. I can rename the function to _get_fp_to_schedule() to be less misleading
|
|
||
| @abstractmethod | ||
| def forward(self, tensor: torch.Tensor) -> torch.Tensor: | ||
| """Apply fake palettization to input tensor""" |
There was a problem hiding this comment.
At first, due to the removal of observer_enabled, most of this function simply got deleted and what was left seemed to get overridden anyways by much of the newly added things in kmeans_fake_palettizer, so it felt like this function wasn't lifting any weight.
But I think we can reframe some of what is in kmeans_fake_palettizer to be generic. I'll try some alternatives to see
| ) -> tuple[torch.Tensor, torch.Tensor]: | ||
| weight = original_weights.cpu() | ||
| @property | ||
| def quantized_lut(self) -> torch.Tensor | None: |
There was a problem hiding this comment.
This part, along with lut_quantization_scale and lut_quantization_zero_point were made into properties to try to preserve some backwards comaptibleness with the previous implementation which stored all 3 as separate buffers. Though there is still incompatibility when it comes to being able to save and load actual buffer values so perhaps we can consider deprecating them instead
|
|
||
| self._num_workers = 1 | ||
|
|
||
| self._mode: str = "idle" # "idle" | "training" | "calibrating" |
There was a problem hiding this comment.
I'll define it in BaseModelCompressor but not add it to pruning or quantizers yet (that can come separately)
| self._fp_to_schedule[param] = schedule | ||
| break | ||
|
|
||
| def _resolve_schedule(self, module_name: str) -> PATSchedule | None: |
There was a problem hiding this comment.
This part of the code is pretty much a mirror of what we have in quantizer: https://github.com/apple/coreai-optimization/blob/main/src/coreai_opt/quantization/quantizer.py#L184
so it may inherently have the same issue as QAT
|
|
||
| def _apply_schedule(self) -> None: | ||
| for fp_module, schedule in self._fp_to_schedule.items(): | ||
| fp_module.enable_fake_palett(schedule._compute_state(self._step_count)) |
There was a problem hiding this comment.
Given that fp_to_schedule is supposed to carry only leaf level modules mapping to schedules, it may be better to keep it as is to ensure we are setting the schedule for exactly the module we intend and no more accidentally
| with palettizer.training_mode(): | ||
| prepared(torch.randn(2, 16)).sum().backward() | ||
|
|
||
| assert prepared.palettized.parametrizations.weight.original.grad is None | ||
| assert prepared.head.weight.grad is not None |
There was a problem hiding this comment.
Sounds good, I'll add some coverage for this too
b44e145 to
734573b
Compare
734573b to
a5d23ad
Compare
No description provided.