-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathextract_svd.py
More file actions
114 lines (94 loc) · 4.45 KB
/
Copy pathextract_svd.py
File metadata and controls
114 lines (94 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import os
import argparse
import torch
try:
from safetensors.torch import load_file
except ImportError:
load_file = None
def load_weights(adapter_dir):
"""
Attempts to load LoRA weights from adapter_model.bin or adapter_model.safetensors.
"""
bin_path = os.path.join(adapter_dir, "adapter_model.bin")
st_path = os.path.join(adapter_dir, "adapter_model.safetensors")
if os.path.exists(bin_path):
return torch.load(bin_path, map_location="cpu")
elif os.path.exists(st_path) and load_file is not None:
return load_file(st_path)
else:
return None
def compute_svd_for_dataset(base_dir, r=8, output_file="lora_svd_dataset.pt"):
dataset = []
for entry in os.scandir(base_dir):
if not entry.is_dir():
continue
adapter_dir = entry.path
state_dict = load_weights(adapter_dir)
if state_dict is None:
print(f"Warning: No adapter weights found in {adapter_dir}")
continue
modules = {}
for k, v in state_dict.items():
if "lora_A" in k or "lora_B" in k:
# Group by base module name
base_name = k.replace(".lora_A.default.weight", "").replace(".lora_B.default.weight", "")
base_name = base_name.replace(".lora_A.weight", "").replace(".lora_B.weight", "")
if base_name not in modules:
modules[base_name] = {}
if "lora_A" in k:
modules[base_name]["A"] = v
elif "lora_B" in k:
modules[base_name]["B"] = v
U_dict = {}
log_Sigma_dict = {}
V_dict = {}
valid_modules = 0
for name, parts in modules.items():
if "A" in parts and "B" in parts:
A = parts["A"]
B = parts["B"]
# PEFT standard usage: forward pass uses output = input @ W + input @ A.T @ B.T
# Meaning A is shaped (r, d_in) and B is shaped (d_out, r).
# To match spec (Delta_W = A x B), let's ensure inner dimensions match.
if A.shape[0] == B.shape[1]:
delta_W = B @ A # (d_out, r) @ (r, d_in) = (d_out, d_in)
else:
delta_W = A @ B
# Perform SVD natively
U, S, Vh = torch.linalg.svd(delta_W, full_matrices=False)
# Truncate to rank r
U_r = U[:, :r]
S_r = S[:r]
Vh_r = Vh[:r, :]
# Convert Sigma to log(Sigma) to enforce strict positivity on recreation
log_S_r = torch.log(S_r + 1e-8)
U_dict[name] = U_r
log_Sigma_dict[name] = log_S_r
V_dict[name] = Vh_r
valid_modules += 1
if valid_modules > 0:
# Fallback for task description if the text file isn't found
desc_path = os.path.join(adapter_dir, "task_description.txt")
if os.path.exists(desc_path):
with open(desc_path, "r", encoding="utf-8") as f:
task_desc = f.read().strip()
else:
task_desc = f"Instruction tuning task for adapter {os.path.basename(adapter_dir)}"
dataset.append({
"task_description_string": task_desc,
"tensors": {
"U_dict": U_dict,
"log_Sigma_dict": log_Sigma_dict,
"V_dict": V_dict
}
})
print(f"Processed 1 adapter: '{task_desc[:30]}...' with {valid_modules} matrices.")
torch.save(dataset, output_file)
print(f"Extraction complete. Saved {len(dataset)} adapters to {output_file}.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Extract SVD components from pre-trained LoRA models")
parser.add_argument("--base_dir", type=str, required=True, help="Directory containing the 50 LoRA subdirectories")
parser.add_argument("--r", type=int, default=8, help="Truncation rank")
parser.add_argument("--output_file", type=str, default="lora_svd_dataset.pt", help="Path to output .pt file")
args = parser.parse_args()
compute_svd_for_dataset(args.base_dir, args.r, args.output_file)