Simo76 commited on
Commit
48afacd
·
1 Parent(s): 16831f7

Implement scale test for rank impact on models

Browse files

Added a scale test for evaluating the impact of rank on larger models using Qwen2.5-3B in 4-bit precision. The test includes data loading, model training, and evaluation metrics.

Files changed (1) hide show
  1. scale_test.py +344 -0
scale_test.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SCALE TEST: Does rank matter on larger models?
3
+ ===============================================
4
+ Qwen2.5-3B in 4-bit, MRPC, 3 seeds
5
+ r=8 vs r=16 vs r=32 vs Adaptive
6
+
7
+ Colab Pro: select A100 in Runtime → Change runtime type
8
+ Estimated time: ~30-45 min on A100
9
+ """
10
+
11
+ !pip install -q transformers datasets evaluate accelerate scikit-learn bitsandbytes
12
+
13
+ import copy, torch, time, gc
14
+ import torch.nn as nn
15
+ import numpy as np
16
+ from datasets import load_dataset
17
+ from transformers import (
18
+ AutoTokenizer,
19
+ AutoModelForSequenceClassification,
20
+ BitsAndBytesConfig,
21
+ DataCollatorWithPadding,
22
+ )
23
+ from torch.utils.data import DataLoader
24
+ import evaluate
25
+
26
+ DEVICE = "cuda"
27
+ MODEL_NAME = "Qwen/Qwen2.5-3B"
28
+
29
+ BATCH_SIZE = 4
30
+ EPOCHS = 2
31
+ LR = 1e-4
32
+ MAX_RANK = 32
33
+ MIN_RANK = 4
34
+ ALPHA = 16
35
+ GRAD_CLIP = 0.5
36
+ MAX_LENGTH = 128
37
+
38
+ SEEDS = [0, 1, 2]
39
+
40
+ # ================================================================
41
+ # SEED
42
+ # ================================================================
43
+ def set_seed(seed):
44
+ torch.manual_seed(seed)
45
+ torch.cuda.manual_seed_all(seed)
46
+ np.random.seed(seed)
47
+ torch.backends.cudnn.deterministic = True
48
+ torch.backends.cudnn.benchmark = False
49
+
50
+ # ================================================================
51
+ # DATA
52
+ # ================================================================
53
+ def load_data():
54
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=False)
55
+ if tokenizer.pad_token is None:
56
+ tokenizer.pad_token = tokenizer.eos_token
57
+
58
+ ds = load_dataset("glue", "mrpc")
59
+
60
+ def preprocess(batch):
61
+ return tokenizer(
62
+ batch["sentence1"], batch["sentence2"],
63
+ truncation=True, padding="max_length", max_length=MAX_LENGTH,
64
+ )
65
+
66
+ ds = ds.map(preprocess, batched=True)
67
+ ds = ds.rename_column("label", "labels")
68
+ ds.set_format(type="torch", columns=["input_ids", "attention_mask", "labels"])
69
+
70
+ collator = DataCollatorWithPadding(tokenizer=tokenizer)
71
+ train_loader = DataLoader(
72
+ ds["train"], batch_size=BATCH_SIZE, shuffle=True,
73
+ collate_fn=collator, generator=torch.Generator().manual_seed(0),
74
+ )
75
+ val_loader = DataLoader(
76
+ ds["validation"], batch_size=8, collate_fn=collator,
77
+ )
78
+ metric = evaluate.load("glue", "mrpc")
79
+
80
+ return train_loader, val_loader, metric, tokenizer
81
+
82
+ # ================================================================
83
+ # LoRA MODULE
84
+ # ================================================================
85
+ class LoRALinear(nn.Module):
86
+ def __init__(self, base, max_r=32, layer_name=""):
87
+ super().__init__()
88
+ self.base = base
89
+ for p in self.base.parameters():
90
+ p.requires_grad = False
91
+
92
+ self.max_r = max_r
93
+ self.layer_name = layer_name
94
+ self.A = nn.Parameter(torch.randn(max_r, base.in_features, dtype=torch.float32) * 0.01)
95
+ self.B = nn.Parameter(torch.zeros(base.out_features, max_r, dtype=torch.float32))
96
+ self.active_r = MIN_RANK
97
+
98
+ self.grad_ema = None
99
+ self.prev_grad_ema = None
100
+
101
+ def set_rank(self, r):
102
+ self.active_r = max(MIN_RANK, min(r, self.max_r))
103
+
104
+ def update_rank(self):
105
+ if self.A.grad is None:
106
+ return
107
+
108
+ grad_norm = self.A.grad[:self.active_r].norm().item()
109
+
110
+ if self.grad_ema is None:
111
+ self.grad_ema = grad_norm
112
+ self.prev_grad_ema = grad_norm
113
+ return
114
+
115
+ self.prev_grad_ema = self.grad_ema
116
+ self.grad_ema = 0.9 * self.grad_ema + 0.1 * grad_norm
117
+
118
+ delta = self.grad_ema - self.prev_grad_ema
119
+ threshold = 0.01 * self.grad_ema if self.grad_ema > 0 else 0.01
120
+
121
+ if delta > threshold:
122
+ self.active_r = min(self.max_r, self.active_r + 2)
123
+ elif delta < -threshold:
124
+ self.active_r = max(MIN_RANK, self.active_r - 2)
125
+
126
+ def forward(self, x):
127
+ base_out = self.base(x)
128
+ A = self.A[:self.active_r].to(device=x.device, dtype=x.dtype)
129
+ B = self.B[:, :self.active_r].to(device=x.device, dtype=x.dtype)
130
+ lora_out = x @ A.t() @ B.t()
131
+ lora_out = torch.clamp(lora_out, -5, 5)
132
+ scale = ALPHA / self.active_r
133
+ return base_out + scale * lora_out
134
+
135
+ # ================================================================
136
+ # INJECT
137
+ # ================================================================
138
+ def inject_lora(model, fixed_rank=None):
139
+ replace_names = []
140
+ for name, module in model.named_modules():
141
+ if isinstance(module, nn.Linear) and (
142
+ name.endswith("q_proj") or name.endswith("v_proj")
143
+ ):
144
+ replace_names.append(name)
145
+
146
+ for name in replace_names:
147
+ parts = name.split(".")
148
+ parent = model
149
+ for p in parts[:-1]:
150
+ parent = getattr(parent, p)
151
+ original = getattr(parent, parts[-1])
152
+ lora = LoRALinear(original, MAX_RANK, layer_name=name)
153
+ if fixed_rank is not None:
154
+ lora.set_rank(fixed_rank)
155
+ setattr(parent, parts[-1], lora)
156
+
157
+ print(f" Injected LoRA into {len(replace_names)} layers")
158
+ return model
159
+
160
+ def get_lora_modules(model):
161
+ return [m for m in model.modules() if isinstance(m, LoRALinear)]
162
+
163
+ # ================================================================
164
+ # TRAIN
165
+ # ================================================================
166
+ def train(mode="r16", seed=0):
167
+ set_seed(seed)
168
+ train_loader, val_loader, metric, tokenizer = load_data()
169
+
170
+ bnb_config = BitsAndBytesConfig(
171
+ load_in_4bit=True,
172
+ bnb_4bit_compute_dtype=torch.float16,
173
+ bnb_4bit_use_double_quant=True,
174
+ bnb_4bit_quant_type="nf4",
175
+ )
176
+
177
+ model = AutoModelForSequenceClassification.from_pretrained(
178
+ MODEL_NAME,
179
+ num_labels=2,
180
+ quantization_config=bnb_config,
181
+ device_map="auto",
182
+ )
183
+ model.config.pad_token_id = tokenizer.pad_token_id
184
+ if getattr(model, "generation_config", None) is not None:
185
+ model.generation_config.pad_token_id = tokenizer.pad_token_id
186
+
187
+ if mode == "adaptive":
188
+ model = inject_lora(model, fixed_rank=None)
189
+ else:
190
+ rank = int(mode.replace("r", ""))
191
+ model = inject_lora(model, fixed_rank=rank)
192
+
193
+ for p in model.parameters():
194
+ p.requires_grad = False
195
+
196
+ for m in get_lora_modules(model):
197
+ m.A.requires_grad = True
198
+ m.B.requires_grad = True
199
+
200
+ for n, p in model.named_parameters():
201
+ if "score" in n or "classifier" in n:
202
+ p.requires_grad = True
203
+
204
+ trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
205
+ print(f" Trainable params: {trainable:,}")
206
+
207
+ opt = torch.optim.AdamW(
208
+ filter(lambda p: p.requires_grad, model.parameters()), lr=LR
209
+ )
210
+
211
+ rank_history = []
212
+ t0 = time.time()
213
+
214
+ for epoch in range(EPOCHS):
215
+ model.train()
216
+ for step, batch in enumerate(train_loader):
217
+ batch = {k: v.to(DEVICE) for k, v in batch.items()}
218
+
219
+ out = model(**batch)
220
+ loss = out.loss
221
+
222
+ if torch.isnan(loss) or torch.isinf(loss):
223
+ opt.zero_grad()
224
+ continue
225
+
226
+ loss.backward()
227
+ torch.nn.utils.clip_grad_norm_(
228
+ [p for p in model.parameters() if p.requires_grad], GRAD_CLIP
229
+ )
230
+
231
+ if mode == "adaptive":
232
+ for m in get_lora_modules(model):
233
+ m.update_rank()
234
+ rank_history.append(m.active_r)
235
+
236
+ opt.step()
237
+ opt.zero_grad()
238
+
239
+ if step % 50 == 0:
240
+ r_str = f" rank={np.mean([m.active_r for m in get_lora_modules(model)]):.1f}" if mode == "adaptive" else ""
241
+ print(f" e={epoch} s={step} loss={loss.item():.4f}{r_str}")
242
+
243
+ elapsed = time.time() - t0
244
+
245
+ model.eval()
246
+ preds, labels = [], []
247
+ with torch.no_grad():
248
+ for batch in val_loader:
249
+ batch = {k: v.to(DEVICE) for k, v in batch.items()}
250
+ logits = model(**batch).logits
251
+ p = torch.argmax(logits, dim=1)
252
+ preds += p.cpu().tolist()
253
+ labels += batch["labels"].cpu().tolist()
254
+
255
+ res = metric.compute(predictions=preds, references=labels)
256
+ avg_rank = np.mean(rank_history) if rank_history else int(mode.replace("r", "")) if mode != "adaptive" else MIN_RANK
257
+
258
+ del model, opt
259
+ gc.collect()
260
+ torch.cuda.empty_cache()
261
+
262
+ return {**res, "avg_rank": float(avg_rank), "time": elapsed, "mode": mode, "seed": seed}
263
+
264
+ # ================================================================
265
+ # RUN
266
+ # ================================================================
267
+ print("=" * 60)
268
+ print(f" SCALE TEST: {MODEL_NAME}")
269
+ print(f" Does rank matter at this scale?")
270
+ print("=" * 60)
271
+
272
+ print(f"\nGPU: {torch.cuda.get_device_name(0)}")
273
+ print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
274
+
275
+ MODES = ["r8", "r16", "r32", "adaptive"]
276
+ results = {m: [] for m in MODES}
277
+
278
+ for seed in SEEDS:
279
+ for mode in MODES:
280
+ label = f"{mode}/seed={seed}"
281
+ print(f"\n {label}...")
282
+
283
+ try:
284
+ res = train(mode=mode, seed=seed)
285
+ results[mode].append(res)
286
+ print(f" → acc={res['accuracy']:.4f} f1={res['f1']:.4f} rank={res['avg_rank']:.1f} ({res['time']:.0f}s)")
287
+ except Exception as e:
288
+ print(f" → FAILED: {e}")
289
+ import traceback
290
+ traceback.print_exc()
291
+
292
+ # ================================================================
293
+ # RESULTS
294
+ # ================================================================
295
+ print("\n" + "=" * 60)
296
+ print(" RESULTS (mean ± std)")
297
+ print("=" * 60)
298
+
299
+ print(f"\n{'Mode':<12} {'Acc':>12} {'F1':>12} {'Acc Std':>10} {'F1 Std':>10} {'Rank':>8}")
300
+ print("-" * 56)
301
+
302
+ for mode in MODES:
303
+ if not results[mode]:
304
+ print(f"{mode:<12} {'FAILED':>12}")
305
+ continue
306
+
307
+ accs = [r["accuracy"] for r in results[mode]]
308
+ f1s = [r["f1"] for r in results[mode]]
309
+ ranks = [r["avg_rank"] for r in results[mode]]
310
+
311
+ print(f"{mode:<12} {np.mean(accs):>12.4f} {np.mean(f1s):>12.4f} {np.std(accs):>10.4f} {np.std(f1s):>10.4f} {np.mean(ranks):>8.1f}")
312
+
313
+ # ================================================================
314
+ # KEY QUESTION
315
+ # ================================================================
316
+ print("\n" + "=" * 60)
317
+ print(" KEY QUESTION: Does rank matter at this scale?")
318
+ print("=" * 60)
319
+
320
+ if results["r8"] and results["r32"]:
321
+ r8_f1 = np.mean([r["f1"] for r in results["r8"]])
322
+ r32_f1 = np.mean([r["f1"] for r in results["r32"]])
323
+ gap = abs(r32_f1 - r8_f1)
324
+
325
+ print(f"\n r=8 F1: {r8_f1:.4f}")
326
+ print(f" r=32 F1: {r32_f1:.4f}")
327
+ print(f" Gap: {gap:.4f}")
328
+
329
+ if gap > 0.02:
330
+ print(f"\n → YES. Rank matters ({gap:.1%} gap). The adaptive controller has a real problem to solve.")
331
+ elif gap > 0.01:
332
+ print(f"\n → MAYBE. Small gap ({gap:.1%}). Marginal benefit possible.")
333
+ else:
334
+ print(f"\n → NO. Rank doesn't matter at this scale either ({gap:.1%} gap).")
335
+
336
+ if results["adaptive"] and results["r16"]:
337
+ a_f1 = np.mean([r["f1"] for r in results["adaptive"]])
338
+ a_std = np.std([r["f1"] for r in results["adaptive"]])
339
+ r16_f1 = np.mean([r["f1"] for r in results["r16"]])
340
+ a_rank = np.mean([r["avg_rank"] for r in results["adaptive"]])
341
+
342
+ print(f"\n Adaptive F1: {a_f1:.4f} ± {a_std:.4f} (rank={a_rank:.1f})")
343
+ print(f" r=16 F1: {r16_f1:.4f}")
344
+ print(f" Delta: {a_f1 - r16_f1:+.4f}")