Skip to content

Commit aa57512

Browse files
committed
[bugfix]: preserve runtime-unsupported LoRA deltas
1 parent 78948fb commit aa57512

4 files changed

Lines changed: 88 additions & 59 deletions

File tree

docs/training/finetune.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ python scripts/lora_extraction/extract_lora.py \
121121
--base Wan-AI/Wan2.1-T2V-1.3B-Diffusers \
122122
--ft path/to/your/finetuned_model \
123123
--out adapter_r32.safetensors \
124-
--rank 32
124+
--rank 32 \
125+
--exact-tensor-pattern '^condition_embedder\.' \
126+
--exact-tensor-pattern '^proj_out\.weight$'
125127
```
126128

127129
| Argument | Description |
@@ -136,8 +138,9 @@ python scripts/lora_extraction/extract_lora.py \
136138
| `--svd-method` | Exact or randomized SVD |
137139
| `--factor-dtype` | Storage dtype for the low-rank factors |
138140
| `--dense-dtype` | Storage dtype for exact `.diff`/`.diff_b`/`.diff_param` payloads |
141+
| `--exact-tensor-pattern` | Repeatable regex for matrices the target runtime cannot load as LoRA factors |
139142

140-
For large checkpoints, indexed loading streams one transformer tensor pair at a time and downloads only `transformer/*`. The extractor also preserves changed norms, biases, and standalone parameters as exact deltas, and fine-tuned-only parameters as `.set_weight` or `.set_param`. See [LoRA Extraction and Merging](../utilities/lora.md) for the GPU/randomized-SVD command, resume options, and accuracy controls.
143+
For large checkpoints, indexed loading streams one transformer tensor pair at a time and downloads only `transformer/*`. The extractor also preserves changed norms, biases, and standalone parameters as exact deltas, and fine-tuned-only parameters as `.set_weight` or `.set_param`. Matrix selection is runtime-agnostic, so full-finetune extraction must keep runtime-unsupported matrices exact, as the Wan example does. See [LoRA Extraction and Merging](../utilities/lora.md) for the GPU/randomized-SVD command, resume options, and accuracy controls.
141144

142145
### Merge LoRA Adapter
143146

docs/utilities/lora.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ python scripts/lora_extraction/extract_lora.py \
99
--base Wan-AI/Wan2.2-TI2V-5B-Diffusers \
1010
--ft FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers \
1111
--out adapter_r32.safetensors \
12-
--rank 32
12+
--rank 32 \
13+
--exact-tensor-pattern '^condition_embedder\.' \
14+
--exact-tensor-pattern '^proj_out\.weight$'
1315
```
1416

17+
The extractor is runtime-agnostic by default and cannot determine from checkpoint tensors whether the target runtime
18+
wraps a given matrix as a LoRA layer. Use `--exact-tensor-pattern` for changed matrices that the runtime does not wrap;
19+
the extractor preserves them as exact `.diff` tensors. The Wan patterns above cover its excluded condition embedders
20+
and its unwrapped output projection.
21+
1522
Exact CPU SVD remains the default. For a large transformer, stream its indexed safetensors and factorize on a GPU:
1623

1724
```bash
Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,92 @@
1-
"""Test LoRA extraction, merging, and verification pipeline."""
2-
import sys
1+
"""Test extraction through the real FastVideo LoRA loading path."""
32
from pathlib import Path
3+
import sys
4+
import tempfile
45

56
import pytest
67
import torch
78

9+
from fastvideo import VideoGenerator
10+
from fastvideo.api import ComponentConfig, EngineConfig, GeneratorConfig, OffloadConfig, ParallelismConfig, PipelineSelection
11+
812
# Add scripts/lora_extraction to path for imports
913
repo_root = Path(__file__).parents[3]
1014
lora_scripts = repo_root / "scripts" / "lora_extraction"
1115
sys.path.insert(0, str(lora_scripts))
1216

13-
# Import the core functions
14-
from extract_lora import extract_lora_adapter
15-
from merge_lora import merge_lora
16-
from verify_lora import main as verify_lora_main
17+
from extract_lora import extract_lora_adapter # noqa: E402
1718

1819

19-
@pytest.mark.parametrize(
20-
"extraction_device",
21-
[
22-
pytest.param("cpu", id="cpu"),
23-
pytest.param(
24-
"cuda:0",
25-
id="gpu",
26-
marks=pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is unavailable"),
27-
),
28-
],
29-
)
30-
def test_lora_extraction_pipeline(extraction_device: str):
31-
"""Test the existing Wan2.2 extraction workflow on CPU and GPU."""
32-
import tempfile
20+
def _collect_lora_application(worker) -> dict[str, object]:
21+
"""Inspect the worker after the constructor applied its adapter."""
22+
pipeline = worker.pipeline
23+
adapter = pipeline.lora_adapters[pipeline.cur_adapter_name]
24+
available: set[str] = set()
25+
adapted = 0
26+
for transformer_layers in pipeline.lora_layers.values():
27+
for _, layers in transformer_layers.lora_layers_by_block():
28+
for name, layer in layers.items():
29+
available.update((name + ".lora_A", name + ".lora_B", name + ".lora_alpha"))
30+
if layer.lora_A is not None and layer.lora_B is not None and not layer.disable_lora:
31+
adapted += 1
32+
unmatched = sorted(set(adapter) - available)
33+
return {
34+
"adapted": adapted,
35+
"pipeline": type(pipeline).__name__,
36+
"unmatched": unmatched,
37+
}
3338

34-
# Use temp directory for outputs to avoid polluting repo
35-
with tempfile.TemporaryDirectory() as tmpdir:
36-
tmpdir_path = Path(tmpdir)
37-
device_name = extraction_device.replace(":", "-")
38-
adapter_path = tmpdir_path / f"adapter_r16_{device_name}.safetensors"
39-
merged_dir = tmpdir_path / f"merged_r16_{device_name}"
4039

41-
# 1. Extract rank-16 adapter
42-
print(f"\nExtracting rank-16 adapter on {extraction_device}")
40+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="Wan2.2 integration requires a CUDA GPU")
41+
def test_lora_extraction_pipeline() -> None:
42+
"""Extract Wan2.2 on a GPU and require every factor to reach the DMD pipeline."""
43+
base = "Wan-AI/Wan2.2-TI2V-5B-Diffusers"
44+
with tempfile.TemporaryDirectory() as tmpdir:
45+
adapter_path = Path(tmpdir) / "adapter_r16.safetensors"
4346
extract_lora_adapter(
44-
base="Wan-AI/Wan2.2-TI2V-5B-Diffusers",
47+
base=base,
4548
ft="FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers",
4649
out=str(adapter_path),
4750
rank=16,
4851
load_mode="indexed",
49-
device=extraction_device,
52+
device="cuda:0",
5053
svd_method="exact",
54+
exact_tensor_patterns=(r"^condition_embedder\.", r"^proj_out\.weight$"),
5155
)
52-
assert adapter_path.exists(), "Adapter file was not created"
53-
54-
# 2. Merge adapter
55-
print("\nMerging adapter")
56-
merge_lora(
57-
base="Wan-AI/Wan2.2-TI2V-5B-Diffusers",
58-
adapter=str(adapter_path),
59-
ft="FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers",
60-
output=str(merged_dir),
61-
)
62-
assert merged_dir.exists(), "Merged model directory was not created"
6356

64-
# 3. Verify numerical accuracy
65-
print("\nVerifying merged model")
66-
# verify_lora uses sys.argv, so we need to mock it
67-
old_argv = sys.argv
57+
generator = VideoGenerator.from_config(
58+
GeneratorConfig(
59+
model_path=base,
60+
pipeline=PipelineSelection(
61+
components=ComponentConfig(
62+
lora_path=str(adapter_path),
63+
override_pipeline_cls_name="WanDMDPipeline",
64+
),
65+
experimental={
66+
"dmd_denoising_steps": [1000, 757, 522],
67+
"flow_shift": 5.0,
68+
},
69+
),
70+
engine=EngineConfig(
71+
num_gpus=1,
72+
use_fsdp_inference=False,
73+
parallelism=ParallelismConfig(tp_size=1, sp_size=1),
74+
offload=OffloadConfig(
75+
dit=False,
76+
dit_layerwise=False,
77+
text_encoder=True,
78+
vae=True,
79+
pin_cpu_memory=False,
80+
),
81+
),
82+
))
6883
try:
69-
sys.argv = [
70-
"verify_lora.py",
71-
"--merged",
72-
str(merged_dir),
73-
"--ft",
74-
"FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers",
75-
]
76-
verify_lora_main()
84+
summaries = generator.executor.collective_rpc(_collect_lora_application)
7785
finally:
78-
sys.argv = old_argv
86+
generator.shutdown()
7987

80-
print("\nLoRA extraction pipeline test PASSED")
88+
assert summaries == [{
89+
"adapted": 300,
90+
"pipeline": "WanDMDPipeline",
91+
"unmatched": [],
92+
}]

scripts/lora_extraction/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@ python extract_lora.py \
1111
--base Wan-AI/Wan2.2-TI2V-5B-Diffusers \
1212
--ft FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers \
1313
--out adapter_r32.safetensors \
14-
--rank 32
14+
--rank 32 \
15+
--exact-tensor-pattern '^condition_embedder\.' \
16+
--exact-tensor-pattern '^proj_out\.weight$'
1517
```
1618

19+
The extractor is runtime-agnostic by default: it cannot infer from checkpoint tensors whether a runtime wraps a
20+
particular matrix as a LoRA layer. When extracting a full fine-tune, select matrices unsupported by the target runtime
21+
with `--exact-tensor-pattern`; their changes remain exact `.diff` tensors rather than being discarded. The Wan patterns
22+
above cover its excluded condition embedders and its unwrapped output projection.
23+
1724
For large transformers, stream their indexed safetensors and factorize on a GPU:
1825

1926
```bash

0 commit comments

Comments
 (0)