vectorplasticity commited on
Commit
76a4048
·
verified ·
1 Parent(s): 278a1e7

Add PEFT utilities for LoRA/adapter support

Browse files
Files changed (1) hide show
  1. app/utils/peft_utils.py +469 -0
app/utils/peft_utils.py ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PEFT Utilities for Parameter-Efficient Fine-Tuning
3
+
4
+ Supports LoRA, AdaLoRA, IA3, Prefix Tuning, and Prompt Tuning
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import logging
10
+ from typing import Dict, List, Optional, Union, Any
11
+ from dataclasses import dataclass, field
12
+
13
+ import torch
14
+ from transformers import PreTrainedModel, PreTrainedTokenizer
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # PEFT configuration classes
19
+ @dataclass
20
+ class LoRAConfig:
21
+ """LoRA configuration"""
22
+ r: int = 8
23
+ lora_alpha: int = 16
24
+ lora_dropout: float = 0.05
25
+ target_modules: List[str] = field(default_factory=lambda: ["q_proj", "v_proj"])
26
+ bias: str = "none"
27
+ modules_to_save: List[str] = field(default_factory=list)
28
+
29
+
30
+ @dataclass
31
+ class AdaLoRAConfig:
32
+ """AdaLoRA configuration"""
33
+ target_r: int = 8
34
+ init_r: int = 12
35
+ tinit: int = 200
36
+ tfinal: int = 1000
37
+ deltaT: int = 10
38
+ beta1: float = 0.85
39
+ beta2: float = 0.85
40
+ lora_alpha: int = 16
41
+ lora_dropout: float = 0.05
42
+ target_modules: List[str] = field(default_factory=lambda: ["q_proj", "v_proj"])
43
+ modules_to_save: List[str] = field(default_factory=list)
44
+
45
+
46
+ @dataclass
47
+ class IA3Config:
48
+ """IA3 configuration"""
49
+ target_modules: List[str] = field(default_factory=lambda: ["q_proj", "v_proj", "k_proj"])
50
+ feedforward_modules: List[str] = field(default_factory=lambda: ["up_proj", "down_proj"])
51
+ modules_to_save: List[str] = field(default_factory=list)
52
+
53
+
54
+ @dataclass
55
+ class PrefixTuningConfig:
56
+ """Prefix Tuning configuration"""
57
+ num_virtual_tokens: int = 20
58
+ encoder_hidden_size: Optional[int] = None
59
+ prefix_projection: bool = False
60
+ projection_dim: int = 128
61
+ dropout: float = 0.0
62
+
63
+
64
+ @dataclass
65
+ class PromptTuningConfig:
66
+ """Prompt Tuning configuration"""
67
+ num_virtual_tokens: int = 20
68
+ tokenizer_name_or_path: Optional[str] = None
69
+ num_layers: Optional[int] = None
70
+ token_dim: Optional[int] = None
71
+
72
+
73
+ PEFT_CONFIG_MAP = {
74
+ "lora": LoRAConfig,
75
+ "adalora": AdaLoRAConfig,
76
+ "ia3": IA3Config,
77
+ "prefix_tuning": PrefixTuningConfig,
78
+ "prompt_tuning": PromptTuningConfig,
79
+ }
80
+
81
+
82
+ def get_peft_config(peft_type: str, **kwargs) -> Any:
83
+ """
84
+ Get PEFT configuration for the specified type.
85
+
86
+ Args:
87
+ peft_type: Type of PEFT method ('lora', 'adalora', 'ia3', 'prefix_tuning', 'prompt_tuning')
88
+ **kwargs: Configuration parameters
89
+
90
+ Returns:
91
+ PEFT configuration object
92
+ """
93
+ peft_type = peft_type.lower()
94
+
95
+ if peft_type not in PEFT_CONFIG_MAP:
96
+ raise ValueError(f"Unknown PEFT type: {peft_type}. Available: {list(PEFT_CONFIG_MAP.keys())}")
97
+
98
+ config_class = PEFT_CONFIG_MAP[peft_type]
99
+ return config_class(**kwargs)
100
+
101
+
102
+ def apply_peft_to_model(
103
+ model: PreTrainedModel,
104
+ peft_type: str,
105
+ config: Optional[Union[Dict, Any]] = None,
106
+ **kwargs
107
+ ) -> PreTrainedModel:
108
+ """
109
+ Apply PEFT to a model.
110
+
111
+ Args:
112
+ model: The base model to apply PEFT to
113
+ peft_type: Type of PEFT method
114
+ config: PEFT configuration (dict or dataclass)
115
+ **kwargs: Additional configuration parameters
116
+
117
+ Returns:
118
+ Model with PEFT applied
119
+ """
120
+ try:
121
+ from peft import (
122
+ LoraConfig, AdaLoraConfig, IA3Config,
123
+ PrefixTuningConfig, PromptTuningConfig,
124
+ get_peft_model, TaskType, prepare_model_for_kbit_training
125
+ )
126
+ except ImportError:
127
+ logger.warning("PEFT library not installed. Returning original model.")
128
+ return model
129
+
130
+ peft_type = peft_type.lower()
131
+
132
+ # Build PEFT config
133
+ if config is None:
134
+ config = {}
135
+
136
+ if isinstance(config, dict):
137
+ config_data = {**config, **kwargs}
138
+ else:
139
+ config_data = {k: v for k, v in vars(config).items() if not k.startswith('_')}
140
+ config_data.update(kwargs)
141
+
142
+ # Map to PEFT library config classes
143
+ peft_config_map = {
144
+ "lora": LoraConfig,
145
+ "adalora": AdaLoraConfig,
146
+ "ia3": IA3Config,
147
+ "prefix_tuning": PrefixTuningConfig,
148
+ "prompt_tuning": PromptTuningConfig,
149
+ }
150
+
151
+ if peft_type not in peft_config_map:
152
+ raise ValueError(f"Unknown PEFT type: {peft_type}")
153
+
154
+ peft_config_class = peft_config_map[peft_type]
155
+
156
+ # Determine task type
157
+ task_type = config_data.pop('task_type', None)
158
+ if task_type:
159
+ task_type_map = {
160
+ 'causal-lm': TaskType.CAUSAL_LM,
161
+ 'seq2seq': TaskType.SEQ_2_SEQ_LM,
162
+ 'token-classification': TaskType.TOKEN_CLS,
163
+ 'text-classification': TaskType.SEQ_CLS,
164
+ 'question-answering': TaskType.QUESTION_ANS,
165
+ }
166
+ task_type = task_type_map.get(task_type)
167
+ if task_type:
168
+ config_data['task_type'] = task_type
169
+
170
+ # Create PEFT config
171
+ peft_config = peft_config_class(**config_data)
172
+
173
+ # Prepare model for k-bit training if needed
174
+ if hasattr(model, 'is_loaded_in_8bit') and model.is_loaded_in_8bit:
175
+ model = prepare_model_for_kbit_training(model)
176
+ elif hasattr(model, 'is_loaded_in_4bit') and model.is_loaded_in_4bit:
177
+ model = prepare_model_for_kbit_training(model)
178
+
179
+ # Apply PEFT
180
+ model = get_peft_model(model, peft_config)
181
+
182
+ # Log trainable parameters
183
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
184
+ all_params = sum(p.numel() for p in model.parameters())
185
+ logger.info(f"Trainable params: {trainable_params:,} / {all_params:,} ({100 * trainable_params / all_params:.2f}%)")
186
+
187
+ return model
188
+
189
+
190
+ def get_target_modules_for_architecture(model_name: str) -> List[str]:
191
+ """
192
+ Get recommended target modules based on model architecture.
193
+
194
+ Args:
195
+ model_name: Name of the model
196
+
197
+ Returns:
198
+ List of target module names
199
+ """
200
+ model_name_lower = model_name.lower()
201
+
202
+ # LLaMA, Alpaca, Vicuna
203
+ if any(name in model_name_lower for name in ['llama', 'alpaca', 'vicuna']):
204
+ return ['q_proj', 'k_proj', 'v_proj', 'o_proj', 'gate_proj', 'up_proj', 'down_proj']
205
+
206
+ # Mistral
207
+ if 'mistral' in model_name_lower:
208
+ return ['q_proj', 'k_proj', 'v_proj', 'o_proj', 'gate_proj', 'up_proj', 'down_proj']
209
+
210
+ # BERT, RoBERTa, DeBERTa
211
+ if any(name in model_name_lower for name in ['bert', 'roberta', 'deberta']):
212
+ return ['query', 'key', 'value', 'dense']
213
+
214
+ # T5, Flan-T5
215
+ if 't5' in model_name_lower:
216
+ return ['q', 'k', 'v', 'o', 'wi', 'wo']
217
+
218
+ # GPT-2, GPT-Neo, GPT-J
219
+ if any(name in model_name_lower for name in ['gpt2', 'gpt-neo', 'gptj', 'gpt-j']):
220
+ return ['c_attn', 'c_proj', 'mlp.c_fc', 'mlp.c_proj']
221
+
222
+ # Bloom
223
+ if 'bloom' in model_name_lower:
224
+ return ['query_key_value', 'dense', 'dense_h_to_4h', 'dense_4h_to_h']
225
+
226
+ # OPT
227
+ if 'opt' in model_name_lower:
228
+ return ['q_proj', 'k_proj', 'v_proj', 'out_proj', 'fc1', 'fc2']
229
+
230
+ # Falcon
231
+ if 'falcon' in model_name_lower:
232
+ return ['query_key_value', 'dense', 'dense_h_to_4h', 'dense_4h_to_h']
233
+
234
+ # Default for transformer models
235
+ return ['q_proj', 'v_proj']
236
+
237
+
238
+ def estimate_lora_parameters(
239
+ base_model_params: int,
240
+ r: int,
241
+ target_modules: List[str],
242
+ lora_alpha: int = 16
243
+ ) -> Dict[str, int]:
244
+ """
245
+ Estimate the number of trainable parameters for LoRA.
246
+
247
+ Args:
248
+ base_model_params: Number of parameters in the base model
249
+ r: LoRA rank
250
+ target_modules: List of target module names
251
+ lora_alpha: LoRA alpha parameter
252
+
253
+ Returns:
254
+ Dictionary with parameter estimates
255
+ """
256
+ # Rough estimate: each target module gets 2 LoRA matrices (A and B)
257
+ # Size depends on layer dimensions and rank
258
+
259
+ # For a typical transformer layer:
260
+ # - attention projections: hidden_size x hidden_size
261
+ # - LoRA adds: hidden_size x r + r x hidden_size = 2 * hidden_size * r per module
262
+
263
+ # Estimate hidden size from total params (rough approximation)
264
+ hidden_size = int((base_model_params ** 0.5) * 0.5)
265
+
266
+ # Estimate params per target module
267
+ params_per_module = 2 * hidden_size * r
268
+
269
+ # Total trainable params (rough estimate)
270
+ total_lora_params = params_per_module * len(target_modules)
271
+
272
+ return {
273
+ 'estimated_trainable_params': total_lora_params,
274
+ 'params_per_module': params_per_module,
275
+ 'compression_ratio': base_model_params / total_lora_params if total_lora_params > 0 else 0,
276
+ 'memory_reduction_percent': 100 * (1 - total_lora_params / base_model_params) if base_model_params > 0 else 0
277
+ }
278
+
279
+
280
+ def save_peft_model(
281
+ model,
282
+ output_dir: str,
283
+ tokenizer: Optional[PreTrainedTokenizer] = None,
284
+ save_merged: bool = False
285
+ ) -> Dict[str, str]:
286
+ """
287
+ Save PEFT model and associated files.
288
+
289
+ Args:
290
+ model: PEFT model to save
291
+ output_dir: Directory to save to
292
+ tokenizer: Optional tokenizer to save
293
+ save_merged: Whether to save merged model
294
+
295
+ Returns:
296
+ Dictionary with saved file paths
297
+ """
298
+ os.makedirs(output_dir, exist_ok=True)
299
+ saved_files = []
300
+
301
+ try:
302
+ # Save PEFT adapters
303
+ model.save_pretrained(output_dir)
304
+ saved_files.append(f"{output_dir}/adapter_config.json")
305
+ saved_files.append(f"{output_dir}/adapter_model.safetensors")
306
+
307
+ # Save tokenizer if provided
308
+ if tokenizer:
309
+ tokenizer.save_pretrained(output_dir)
310
+ saved_files.append(f"{output_dir}/tokenizer.json")
311
+
312
+ # Optionally save merged model
313
+ if save_merged:
314
+ try:
315
+ merged_model = model.merge_and_unload()
316
+ merged_dir = os.path.join(output_dir, "merged")
317
+ merged_model.save_pretrained(merged_dir)
318
+ if tokenizer:
319
+ tokenizer.save_pretrained(merged_dir)
320
+ saved_files.append(f"{merged_dir}/pytorch_model.bin")
321
+ except Exception as e:
322
+ logger.warning(f"Could not merge model: {e}")
323
+
324
+ # Save training config
325
+ config = {
326
+ 'peft_type': model.active_peft_config.peft_type.value if hasattr(model, 'active_peft_config') else 'unknown',
327
+ 'trainable_params': sum(p.numel() for p in model.parameters() if p.requires_grad),
328
+ 'total_params': sum(p.numel() for p in model.parameters()),
329
+ }
330
+
331
+ config_path = os.path.join(output_dir, "training_config.json")
332
+ with open(config_path, 'w') as f:
333
+ json.dump(config, f, indent=2)
334
+ saved_files.append(config_path)
335
+
336
+ logger.info(f"Saved PEFT model to {output_dir}")
337
+
338
+ except Exception as e:
339
+ logger.error(f"Error saving PEFT model: {e}")
340
+ raise
341
+
342
+ return {'saved_files': saved_files, 'output_dir': output_dir}
343
+
344
+
345
+ def load_peft_model(
346
+ base_model_name: str,
347
+ peft_model_path: str,
348
+ device: str = 'auto'
349
+ ):
350
+ """
351
+ Load a PEFT model.
352
+
353
+ Args:
354
+ base_model_name: Name or path of the base model
355
+ peft_model_path: Path to the saved PEFT adapters
356
+ device: Device to load to
357
+
358
+ Returns:
359
+ Loaded PEFT model
360
+ """
361
+ try:
362
+ from peft import PeftModel
363
+ from transformers import AutoModelForCausalLM, AutoTokenizer
364
+
365
+ # Load base model
366
+ base_model = AutoModelForCausalLM.from_pretrained(
367
+ base_model_name,
368
+ torch_dtype=torch.float16 if device != 'cpu' else torch.float32,
369
+ device_map=device
370
+ )
371
+
372
+ # Load PEFT model
373
+ model = PeftModel.from_pretrained(base_model, peft_model_path)
374
+
375
+ return model
376
+
377
+ except Exception as e:
378
+ logger.error(f"Error loading PEFT model: {e}")
379
+ raise
380
+
381
+
382
+ def get_peft_memory_requirements(
383
+ model_params: int,
384
+ peft_type: str = 'lora',
385
+ r: int = 8,
386
+ batch_size: int = 1,
387
+ seq_length: int = 512,
388
+ gradient_checkpointing: bool = True
389
+ ) -> Dict[str, float]:
390
+ """
391
+ Estimate memory requirements for PEFT training.
392
+
393
+ Args:
394
+ model_params: Number of model parameters
395
+ peft_type: Type of PEFT method
396
+ r: LoRA rank (if applicable)
397
+ batch_size: Training batch size
398
+ seq_length: Sequence length
399
+ gradient_checkpointing: Whether gradient checkpointing is enabled
400
+
401
+ Returns:
402
+ Dictionary with memory estimates in GB
403
+ """
404
+ # Base model memory (FP16)
405
+ base_memory = model_params * 2 / 1e9
406
+
407
+ # Optimizer states (AdamW: 2 states per param)
408
+ # Only for trainable params with PEFT
409
+ trainable_ratio = r / 512 # Approximate ratio for LoRA
410
+ trainable_params = model_params * trainable_ratio
411
+ optimizer_memory = trainable_params * 2 * 4 / 1e9 # 2 states, FP32
412
+
413
+ # Gradients (only for trainable params)
414
+ gradient_memory = trainable_params * 2 / 1e9
415
+
416
+ # Activations (depends on batch size, seq length, and gradient checkpointing)
417
+ # Rough estimate: ~batch_size * seq_length * hidden_size * num_layers
418
+ activation_memory = batch_size * seq_length * (model_params ** 0.5) * 0.1 / 1e9
419
+ if gradient_checkpointing:
420
+ activation_memory *= 0.2 # Significant reduction
421
+
422
+ # Total
423
+ total_memory = base_memory + optimizer_memory + gradient_memory + activation_memory
424
+
425
+ return {
426
+ 'base_model_gb': round(base_memory, 2),
427
+ 'optimizer_states_gb': round(optimizer_memory, 2),
428
+ 'gradients_gb': round(gradient_memory, 2),
429
+ 'activations_gb': round(activation_memory, 2),
430
+ 'total_gb': round(total_memory, 2),
431
+ 'peak_gb': round(total_memory * 1.1, 2), # 10% buffer
432
+ 'recommended_gpu_vram': round(total_memory * 1.2, 2) # 20% buffer
433
+ }
434
+
435
+
436
+ # Convenience function for quick LoRA setup
437
+ def quick_lora_setup(
438
+ model: PreTrainedModel,
439
+ r: int = 8,
440
+ lora_alpha: int = 16,
441
+ lora_dropout: float = 0.05,
442
+ target_modules: Optional[List[str]] = None
443
+ ) -> PreTrainedModel:
444
+ """
445
+ Quick setup for LoRA fine-tuning.
446
+
447
+ Args:
448
+ model: Base model
449
+ r: LoRA rank
450
+ lora_alpha: LoRA alpha
451
+ lora_dropout: Dropout rate
452
+ target_modules: Target modules (auto-detected if None)
453
+
454
+ Returns:
455
+ Model with LoRA applied
456
+ """
457
+ if target_modules is None:
458
+ # Try to auto-detect from model config
459
+ model_name = getattr(model.config, '_name_or_path', '')
460
+ target_modules = get_target_modules_for_architecture(model_name)
461
+
462
+ return apply_peft_to_model(
463
+ model,
464
+ 'lora',
465
+ r=r,
466
+ lora_alpha=lora_alpha,
467
+ lora_dropout=lora_dropout,
468
+ target_modules=target_modules
469
+ )