-
Notifications
You must be signed in to change notification settings - Fork 100
feat: enable save/load after recovery without additional steps #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3bfd375
ad13bbd
89a1cbd
c49d50a
d4058ed
e5d7d6a
43f4839
aec268e
b8173c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from abc import ABCMeta | ||
| from typing import Any, Dict | ||
|
|
||
| import torch | ||
|
|
@@ -23,19 +24,19 @@ | |
| from pruna.algorithms.global_utils.recovery.finetuners import PrunaFinetuner | ||
| from pruna.algorithms.global_utils.recovery.finetuners.diffusers.utils import get_denoiser_attr | ||
| from pruna.algorithms.global_utils.recovery.utils import get_trainable_parameters | ||
| from pruna.config.smash_config import SmashConfigPrefixWrapper | ||
| from pruna.config.smash_config import SmashConfig, SmashConfigPrefixWrapper | ||
| from pruna.engine.model_checks import ( | ||
| is_causal_lm, | ||
| is_flux_pipeline, | ||
| is_sana_pipeline, | ||
| is_sd_pipeline, | ||
| is_sdxl_pipeline, | ||
| ) | ||
| from pruna.engine.save import SAVE_FUNCTIONS | ||
| from pruna.engine.save import refresh_saved_model | ||
| from pruna.logging.logger import pruna_logger | ||
|
|
||
|
|
||
| class PERPRecoverer(PrunaAlgorithmBase): | ||
| class PERPRecoverer(PrunaAlgorithmBase, metaclass=ABCMeta): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't use this anywhere else in pruna, to keep it consistent we can remove this |
||
| """ | ||
| General purpose PERP recoverer using norm, head and bias finetuning and optionally HuggingFace's LoRA. | ||
|
|
||
|
|
@@ -52,7 +53,7 @@ class PERPRecoverer(PrunaAlgorithmBase): | |
| """ | ||
|
|
||
| group_tags: list[AlgorithmTag] = [AlgorithmTag.RECOVERER] # type: ignore[attr-defined] | ||
| save_fn = SAVE_FUNCTIONS.pickled | ||
| save_fn = None | ||
| required_install: str = "``pip install pruna[transformers-legacy]``" | ||
| references: dict[str, str] = { | ||
| "GitHub": "https://github.com/huggingface/peft", | ||
|
|
@@ -64,7 +65,6 @@ class PERPRecoverer(PrunaAlgorithmBase): | |
|
|
||
| def __init__(self, task_name: str, use_lora: bool, use_in_place: bool, is_distillation: bool) -> None: | ||
| self.task_name = task_name | ||
| self.tokenizer_required = task_name == "text_to_text" # type: ignore[misc] | ||
|
|
||
| if not use_lora and not use_in_place: | ||
| raise ValueError("Arguments use_lora and use_in_place cannot both be False, please use one of the two.") | ||
|
|
@@ -90,6 +90,11 @@ def __init__(self, task_name: str, use_lora: bool, use_in_place: bool, is_distil | |
|
|
||
| super().__init__() # self.adapters need to be set before calling get_hyperparameters | ||
|
|
||
| @property | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please also revert this, to keep it consistent with the rest of the code |
||
| def tokenizer_required(self) -> bool: | ||
| """Overwritten ``tokenizer_required`` property.""" | ||
| return self.task_name == "text_to_text" | ||
|
|
||
| def get_hyperparameters(self) -> list: | ||
| """ | ||
| Configure all algorithm-specific hyperparameters with ConfigSpace. | ||
|
|
@@ -182,6 +187,20 @@ def _pre_smash_hook(self, model: Any, smash_config: SmashConfigPrefixWrapper) -> | |
| adapter_smash_config = SmashConfigPrefixWrapper(smash_config, adapter.adapter_prefix + "_") | ||
| adapter.pre_smash_hook(model_recovery, adapter_smash_config, seed=adapter_seed) | ||
|
|
||
| def post_apply_hook(self, model: Any, smash_config: SmashConfig): | ||
| """ | ||
| Override to run side effects after the algorithm has been applied. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you please change the docstring to what the override does specifically here? |
||
|
|
||
| Parameters | ||
| ---------- | ||
| model : Any | ||
| The model. | ||
| smash_config : SmashConfig | ||
| The SmashConfig configuration to apply. | ||
| """ | ||
| if smash_config.prepare_saving: | ||
| refresh_saved_model(model, self.get_save_before_smash_dir(smash_config), smash_config) | ||
|
|
||
| def _apply(self, model: Any, smash_config: SmashConfigPrefixWrapper) -> Any: | ||
| """ | ||
| Recover performances from a given model with a given config. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -355,9 +355,7 @@ def save_model_hqq(model: Any, model_path: str | Path, smash_config: SmashConfig | |
| # save pipeline info so we can call transformers.pipeline at load time | ||
| save_pipeline_info(model, str(model_path)) | ||
| # pipeline loading requires a safetensor file so we save a fake, lightweight one | ||
| save_model( | ||
| torch.nn.Linear(1, 1), str(model_path / "model.safetensors"), metadata={"format": "pt"} | ||
| ) | ||
| save_model(torch.nn.Linear(1, 1), str(model_path / "model.safetensors"), metadata={"format": "pt"}) | ||
|
|
||
| save_model_hqq(model.model, model_path, smash_config) | ||
| elif is_janus_llamagen_ar(model): | ||
|
|
@@ -470,6 +468,37 @@ def save_component(attr_name: str | None, module: torch.nn.Module, subpaths: lis | |
| smash_config.load_fns.append(LOAD_FUNCTIONS.hqq_diffusers.name) | ||
|
|
||
|
|
||
| def refresh_saved_model(model: Any, model_path: Path, smash_config: SmashConfig) -> None: | ||
| """ | ||
| Refresh the saved save-before-apply model, and the cache will reflect the current model state. | ||
|
|
||
| Recovery modifies weights in-place without changing the model's serialization | ||
| format. If a prior algorithm used ``save_before_apply`` (caching the model before | ||
| its transformation), the cached snapshot is now stale because recovery changed | ||
| the weights. This override refreshes that cache so the already saved model includes | ||
| the recovered weights. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| model : Any | ||
| The model to apply the algorithm to. | ||
| model_path : Path | ||
| The model path to be saved. | ||
| smash_config : SmashConfig | ||
| The SmashConfig object containing the save and load functions. | ||
| """ | ||
| if not model_path.exists(): | ||
| return None | ||
|
|
||
| ori_save_fns = smash_config.save_fns[:] | ||
| smash_config.save_fns = [fn for fn in smash_config.save_fns if fn != SAVE_FUNCTIONS.save_before_apply.name] | ||
| # Re-save with recovered weights | ||
| shutil.rmtree(model_path, ignore_errors=True) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not super comfortable with ignoring errors. Is there a particular reason why you went for this - otherwise i would prefer to at least log something if something went wrong |
||
| save_pruna_model(model, model_path, smash_config) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you think it's possible to first save the new pruna model before removing the old one? if something goes wrong in the save here at least we didn't lose the original model save new model |
||
| # Restore save_fns | ||
| smash_config.save_fns = ori_save_fns | ||
|
|
||
|
|
||
| def save_model_llama_cpp(model: Any, model_path: str | Path, smash_config: SmashConfig) -> None: | ||
| """ | ||
| Save the model with llama.cpp functionality. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,17 @@ | ||
| import os | ||
| import pytest | ||
| import torch | ||
| from pathlib import Path | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
| import torch | ||
| from diffusers import DiffusionPipeline | ||
| from transformers import AutoModelForCausalLM | ||
| from pruna.config.smash_config import SmashConfig | ||
|
|
||
| from pruna import smash | ||
| from pruna.engine.save import save_pruna_model | ||
| from pruna.engine.save import save_pruna_model_to_hub | ||
| from pruna.engine.save import SAVE_FUNCTIONS | ||
| from pruna.engine.load import load_pruna_model | ||
| from pruna.config.smash_config import SmashConfig | ||
| from diffusers import DiffusionPipeline | ||
| from pruna.engine.load import load_pruna_model | ||
| from pruna.engine.pruna_model import PrunaModel | ||
| from pruna.engine.save import SAVE_FUNCTIONS, save_pruna_model, save_pruna_model_to_hub | ||
|
|
||
|
|
||
| @pytest.mark.slow | ||
|
|
@@ -29,6 +28,7 @@ def test_save_llm_to_hub() -> None: | |
| ) | ||
| pruna_model.push_to_hub(upload_repo_id, private=False) | ||
|
|
||
|
|
||
| @pytest.mark.slow | ||
| @pytest.mark.cpu | ||
| def test_save_diffusers_to_hub() -> None: | ||
|
|
@@ -160,3 +160,39 @@ def test_push_to_hub_path_types(tmp_path) -> None: | |
| private=True | ||
| ) | ||
| assert mock_upload.called | ||
|
|
||
|
|
||
| @pytest.mark.cpu | ||
| def test_perp_post_apply_hook_round_trip(tmp_path) -> None: | ||
| """Test whether PERPRecoverer saves the correct model and load from it.""" | ||
| from pruna.algorithms.base.pruna_base import PrunaAlgorithmBase | ||
| from pruna.algorithms.global_utils.recovery.perp_recoverer import PERPRecoverer | ||
|
|
||
| class FakeRecoverer(PERPRecoverer): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i like the idea. but i would feel safer, if we actually use the smash-workflow instead of re-implementing the workflow in the test. you could use monkeypatching to make these overrides and then add an algorithm which uses saving before apply to test things and finally call smash with this algorithm + monkeypatched PERPRecoverer |
||
| algorithm_name = "test_fake_recoverer" | ||
|
|
||
| def __init__(self): # noqa: D107 | ||
| pass | ||
|
|
||
| def _apply(self, model, smash_config): # noqa: D401 | ||
| model.weight.data.fill_(0.99) | ||
| return model | ||
|
|
||
| model = torch.nn.Linear(3, 2) | ||
| model.weight.data.fill_(0.1) | ||
| config = SmashConfig(device="cpu") | ||
|
|
||
| save_dir = PrunaAlgorithmBase.get_save_before_smash_dir(config) | ||
| save_pruna_model(model, save_dir, config) | ||
| config.save_fns.append(SAVE_FUNCTIONS.save_before_apply.name) | ||
|
|
||
| model = FakeRecoverer().apply(model, config) | ||
|
|
||
| save_path = tmp_path / "final_model" | ||
| save_pruna_model(model, save_path, config) | ||
|
|
||
| loaded_model, _ = load_pruna_model(save_path) | ||
| loaded_model = loaded_model.cpu() | ||
| assert torch.allclose( | ||
| loaded_model.weight, torch.full_like(loaded_model.weight, 0.99) | ||
| ), "Recovered weights should survive save/load through save_before_apply" | ||
Uh oh!
There was an error while loading. Please reload this page.