|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 |
|
| 16 | +import os |
| 17 | + |
16 | 18 | import pytest |
| 19 | +import safetensors.torch |
17 | 20 | import torch |
18 | 21 | from PIL import Image |
19 | 22 | from transformers import AutoConfig, AutoTokenizer, T5EncoderModel |
|
25 | 28 | WanVACEPipeline, |
26 | 29 | WanVACETransformer3DModel, |
27 | 30 | ) |
| 31 | +from diffusers.utils.import_utils import is_peft_available |
| 32 | + |
| 33 | +from ...testing_utils import assert_tensors_close, require_peft_version_greater, torch_device |
| 34 | +from ..testing_utils import ( |
| 35 | + BasePipelineTesterConfig, |
| 36 | + LoraMemoryTesterMixin, |
| 37 | + LoraTesterMixin, |
| 38 | + MemoryTesterMixin, |
| 39 | + PipelineTesterMixin, |
| 40 | +) |
| 41 | + |
28 | 42 |
|
29 | | -from ...testing_utils import assert_tensors_close, torch_device |
30 | | -from ..testing_utils import BasePipelineTesterConfig, MemoryTesterMixin, PipelineTesterMixin |
| 43 | +if is_peft_available(): |
| 44 | + from peft.utils import get_peft_model_state_dict |
31 | 45 |
|
32 | 46 |
|
33 | 47 | class WanVACEPipelineTesterConfig(BasePipelineTesterConfig): |
@@ -242,3 +256,48 @@ def test_save_load_optional_components(self, tmp_path, expected_max_difference=1 |
242 | 256 |
|
243 | 257 | class TestWanVACEPipelineMemory(WanVACEPipelineTesterConfig, MemoryTesterMixin): |
244 | 258 | pass |
| 259 | + |
| 260 | + |
| 261 | +class TestWanVACEPipelineLoRA(WanVACEPipelineTesterConfig, LoraTesterMixin): |
| 262 | + """LoRA tests for the Wan VACE pipeline.""" |
| 263 | + |
| 264 | + @require_peft_version_greater("0.13.2") |
| 265 | + def test_lora_exclude_modules(self, tmp_path, base_pipe_output): |
| 266 | + exclude_module_name = "vace_blocks.0.proj_out" |
| 267 | + pipe = self.get_pipeline().to(torch_device) |
| 268 | + |
| 269 | + self.add_adapters_to_pipeline( |
| 270 | + pipe, components=["transformer"], target_modules=["proj_out"], exclude_modules=[exclude_module_name] |
| 271 | + ) |
| 272 | + # The state dict should not contain the modules excluded from LoRA. |
| 273 | + state_dict_from_model = get_peft_model_state_dict(pipe.transformer, adapter_name="default") |
| 274 | + assert not any(exclude_module_name in k for k in state_dict_from_model) |
| 275 | + assert any("proj_out" in k for k in state_dict_from_model) |
| 276 | + output_lora_exclude_modules = self.run_pipe(pipe) |
| 277 | + |
| 278 | + denoiser_state_dict = get_peft_model_state_dict(pipe.transformer) |
| 279 | + self.pipeline_class.save_lora_weights(tmp_path, transformer_lora_layers=denoiser_state_dict) |
| 280 | + pipe.unload_lora_weights() |
| 281 | + |
| 282 | + # Check in the saved state dict. |
| 283 | + loaded_state_dict = safetensors.torch.load_file(os.path.join(tmp_path, "pytorch_lora_weights.safetensors")) |
| 284 | + assert not any(exclude_module_name in k for k in loaded_state_dict) |
| 285 | + assert any("proj_out" in k for k in loaded_state_dict) |
| 286 | + |
| 287 | + # Check in the state dict obtained after loading LoRA. |
| 288 | + pipe.load_lora_weights(tmp_path) |
| 289 | + state_dict_from_model = get_peft_model_state_dict(pipe.transformer, adapter_name="default_0") |
| 290 | + assert not any(exclude_module_name in k for k in state_dict_from_model) |
| 291 | + assert any("proj_out" in k for k in state_dict_from_model) |
| 292 | + |
| 293 | + output_lora_pretrained = self.run_pipe(pipe) |
| 294 | + assert not torch.allclose(base_pipe_output, output_lora_exclude_modules, atol=1e-3, rtol=1e-3), ( |
| 295 | + "LoRA should change outputs." |
| 296 | + ) |
| 297 | + assert torch.allclose(output_lora_exclude_modules, output_lora_pretrained, atol=1e-3, rtol=1e-3), ( |
| 298 | + "Lora outputs should match." |
| 299 | + ) |
| 300 | + |
| 301 | + |
| 302 | +class TestWanVACEPipelineLoRAMemory(WanVACEPipelineTesterConfig, LoraMemoryTesterMixin): |
| 303 | + """LoRA offloading tests for the Wan VACE pipeline.""" |
0 commit comments