-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathLLMRipper.py
More file actions
447 lines (389 loc) · 19.5 KB
/
Copy pathLLMRipper.py
File metadata and controls
447 lines (389 loc) · 19.5 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LLMRipper v2 – Interactive LLM Fine‑Tuner
----------------------------------------
New in this release (2025‑06‑07)
✓ Asks whether the user wants **LoRA‑based** (parameter‑efficient) or **Full** fine‑tuning *before* any other training hyper‑parameters are chosen. The rest of the workflow adapts automatically.
✓ Warns that datasets **must** follow the *System/User/Assistant* schema and verifies these columns exist.
✓ Prompts for dataset file‑format – **csv, json, jsonl, parquet** – and loads the file with the correct HuggingFace dataset loader.
✓ Improved validation & helpful error messages.
✓ Conditional questions (e.g. LoRA weight merge) only appear when relevant.
"""
import os
import sys
import subprocess
import getpass
import torch
from datasets import load_dataset, DatasetDict
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
Trainer,
TrainingArguments,
DefaultDataCollator,
EarlyStoppingCallback,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
)
try:
from peft import prepare_model_for_int8_training
except ImportError:
prepare_model_for_int8_training = None
print("Warning: prepare_model_for_int8_training not found. Update peft if you need 8‑bit training support.")
from huggingface_hub import login
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
def print_banner():
"""Pretty ASCII banner (installs *pyfiglet* the first time)."""
try:
import pyfiglet
except ImportError:
print("pyfiglet not found. Installing pyfiglet…")
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyfiglet"])
import pyfiglet
ascii_banner = pyfiglet.figlet_format("LLMRipper")
print(ascii_banner)
print("Created by Alican Kiraz – v2.0")
def get_input(prompt: str, valid_options=None):
"""Small helper that forces the user to pick a valid option (case‑insensitive)."""
while True:
response = input(prompt).strip()
if valid_options:
if response.lower() in valid_options:
return response.lower()
else:
print(f"Invalid input. Please choose from: {', '.join(valid_options)}")
else:
if response:
return response
else:
print("Input cannot be empty. Please try again.")
def get_secure_input(prompt: str):
"""Secure input for tokens and passwords (hidden from terminal)."""
while True:
response = getpass.getpass(prompt).strip()
if response:
return response
else:
print("Input cannot be empty. Please try again.")
# ---------------------------------------------------------------------------
# Model architecture helpers
# ---------------------------------------------------------------------------
def get_target_modules_for_model(model):
"""Automatically detect target modules for LoRA based on model architecture."""
# Common target modules for different architectures
TARGET_MODULES_MAP = {
# Llama, Alpaca, Vicuna, etc.
"LlamaForCausalLM": ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
# Gemma models
"GemmaForCausalLM": ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
# GPT models
"GPTNeoXForCausalLM": ["query_key_value", "dense", "dense_h_to_4h", "dense_4h_to_h"],
# Mistral models
"MistralForCausalLM": ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
# Qwen models
"QWenLMHeadModel": ["c_attn", "c_proj", "w1", "w2"],
# Phi models
"PhiForCausalLM": ["q_proj", "k_proj", "v_proj", "dense", "fc1", "fc2"],
# Default fallback for transformer models
"default": ["q_proj", "v_proj"]
}
model_type = model.__class__.__name__
print(f"Detected model type: {model_type}")
if model_type in TARGET_MODULES_MAP:
target_modules = TARGET_MODULES_MAP[model_type]
print(f"Using target modules: {target_modules}")
return target_modules
else:
# Try to find common attention projection layers
available_modules = []
for name, module in model.named_modules():
if any(target in name for target in ["q_proj", "k_proj", "v_proj", "query", "key", "value"]):
module_name = name.split('.')[-1]
if module_name not in available_modules:
available_modules.append(module_name)
if available_modules:
print(f"Auto-detected modules: {available_modules}")
return available_modules
else:
# Fallback to default
print(f"Using default target modules for unknown architecture: {TARGET_MODULES_MAP['default']}")
return TARGET_MODULES_MAP["default"]
# ---------------------------------------------------------------------------
# Dataset helpers
# ---------------------------------------------------------------------------
SUPPORTED_FORMATS = ["csv", "json", "jsonl", "parquet"]
REQ_COLUMNS = {"System", "User", "Assistant"}
def load_local_dataset(path: str, file_fmt: str):
"""Load a local dataset file with the correct HF loader."""
if file_fmt not in SUPPORTED_FORMATS:
raise ValueError(f"Unsupported format: {file_fmt} – choose one of {', '.join(SUPPORTED_FORMATS)}")
# Check if file exists
if not os.path.exists(path):
raise FileNotFoundError(f"Dataset file not found: {path}")
try:
if file_fmt == "jsonl":
return load_dataset("json", data_files={"train": path}, split=None)
elif file_fmt == "json":
return load_dataset("json", data_files={"train": path})
else:
return load_dataset(file_fmt, data_files={"train": path})
except Exception as e:
raise ValueError(f"Failed to load dataset from {path}: {str(e)}")
def ensure_columns(dataset):
"""Verify that required columns exist in *all* splits."""
missing = REQ_COLUMNS - set(dataset["train"].column_names)
if missing:
raise ValueError(f"Dataset must contain columns {', '.join(REQ_COLUMNS)}. Missing: {', '.join(missing)}")
# ---------------------------------------------------------------------------
# Main interactive script
# ---------------------------------------------------------------------------
def main():
print_banner()
# Privacy & authentication ------------------------------------------------
model_privacy = get_input("Is your *base model* private or public? [private/public]: ", ["private", "public"])
hf_token = None
if model_privacy == "private":
hf_token = get_secure_input("Enter your Hugging Face token: ")
login(token=hf_token)
base_model_name = get_input("Enter the Hugging Face repo for the base model (e.g. AlicanKiraz0/LLM‑Base): ")
# NEW ▶ Finetuning strategy ------------------------------------------------
tuning_type = get_input("Select tuning strategy – LoRA (parameter‑efficient) or Full? [lora/full]: ", ["lora", "full"])
is_lora = (tuning_type == "lora")
# Dataset location & format ----------------------------------------------
print("\n*** Dataset Requirements ***")
print("Your file MUST contain the columns: System | User | Assistant\n")
dataset_source = get_input("Is the dataset local or on HuggingFace Hub? [local/huggingface]: ", ["local", "huggingface"])
dataset_format = get_input("Which file format are you using? [csv/json/jsonl/parquet]: ", SUPPORTED_FORMATS)
if dataset_source == "local":
dataset_path = get_input("Enter the path to your local dataset file: ")
try:
raw_datasets = load_local_dataset(dataset_path, dataset_format)
except (FileNotFoundError, ValueError) as e:
print(f"Error loading dataset: {e}")
sys.exit(1)
else:
hub_privacy = get_input("Is the HF dataset repo public or private? [public/private]: ", ["public", "private"])
if hub_privacy == "private":
if not hf_token:
hf_token = get_secure_input("Enter your Hugging Face token for the *dataset*: ")
login(token=hf_token)
dataset_repo = get_input("Enter the HF dataset repo (e.g. AlicanKiraz0/my‑chat‑dataset): ")
load_args = {"token": hf_token} if (hub_privacy == "private" and hf_token) else {}
try:
raw_datasets = load_dataset(dataset_repo, **load_args)
except Exception as e:
print(f"Error loading dataset from HuggingFace Hub: {e}")
sys.exit(1)
# Ensure train/validation splits exist
if len(raw_datasets) == 1 and "train" in raw_datasets:
split_ds = raw_datasets["train"].train_test_split(test_size=0.1, shuffle=True, seed=42)
raw_datasets = DatasetDict({"train": split_ds["train"], "validation": split_ds["test"]})
try:
ensure_columns(raw_datasets)
except ValueError as e:
print(f"Dataset validation error: {e}")
print("Please ensure your dataset contains the required columns: System, User, Assistant")
sys.exit(1)
# Hyper‑parameters --------------------------------------------------------
try:
max_length = int(get_input("Enter maximum sequence length (e.g. 1024 or 2048): "))
if max_length < 128 or max_length > 8192:
print("Warning: Sequence length should typically be between 128 and 8192")
grad_acc_steps = int(get_input("Gradient accumulation steps (1/2/4/8): "))
if grad_acc_steps not in [1, 2, 4, 8]:
print("Warning: Gradient accumulation steps should typically be 1, 2, 4, or 8")
per_device_bs = int(get_input("Per‑device train batch size (1/2/4/8): "))
if per_device_bs not in [1, 2, 4, 8]:
print("Warning: Batch size should typically be 1, 2, 4, or 8")
num_epochs = int(get_input("Number of epochs: "))
if num_epochs < 1 or num_epochs > 100:
print("Warning: Number of epochs should typically be between 1 and 100")
except ValueError as e:
print(f"Invalid input for hyperparameters: {e}")
sys.exit(1)
# Quantisation choice -----------------------------------------------------
quantize_choice = get_input("Use model quantisation? [yes/no]: ", ["yes", "no"])
# NOTE: full fine‑tuning + 4‑/8‑bit weights is *experimental*; fall back to fp16/bf16/fp32.
if tuning_type == "full" and quantize_choice == "yes":
print("Full fine‑tuning with 4‑/8‑bit adapters is not officially supported – switching to non‑quantised training.")
quantize_choice = "no"
# Precision (only asked when not quantising) -----------------------------
precision_choice = None
torch_dtype = None
if quantize_choice == "no":
precision_choice = get_input("Choose precision [fp16/bf16/fp32]: ", ["fp16", "bf16", "fp32"])
torch_dtype = {
"fp16": torch.float16,
"bf16": torch.bfloat16,
"fp32": torch.float32,
}[precision_choice]
# -----------------------------------------------------------------------
# MODEL LOADING SECTION
# -----------------------------------------------------------------------
print("Loading model – this can take a while…")
common_kwargs = dict(
token=hf_token if model_privacy == "private" else None,
trust_remote_code=True,
device_map="auto",
)
if quantize_choice == "yes": # 4‑bit / 8‑bit LoRA fine‑tuning
bit_choice = get_input("Quantisation bits – 4 or 8? [4/8]: ", ["4", "8"])
load_kwargs = {
"load_in_4bit": bit_choice == "4",
"load_in_8bit": bit_choice == "8",
}
model = AutoModelForCausalLM.from_pretrained(base_model_name, **common_kwargs, **load_kwargs)
if bit_choice == "4":
model = prepare_model_for_kbit_training(model)
else: # 8‑bit
if prepare_model_for_int8_training is None:
raise ImportError("prepare_model_for_int8_training missing – update *peft* package.")
model = prepare_model_for_int8_training(model)
else: # No quantisation
model = AutoModelForCausalLM.from_pretrained(base_model_name, **common_kwargs, torch_dtype=torch_dtype)
model.config.use_cache = False
# Wrap with LoRA if requested -------------------------------------------
if is_lora:
try:
target_modules = get_target_modules_for_model(model)
lora_cfg = LoraConfig(
r=4,
lora_alpha=16,
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM",
target_modules=target_modules
)
model = get_peft_model(model, lora_cfg)
model.enable_input_require_grads()
print("✅ LoRA adapter successfully applied")
except Exception as e:
print(f"❌ Error applying LoRA adapter: {e}")
print("Falling back to alternative target modules...")
# Try with minimal target modules
try:
fallback_modules = ["q_proj", "v_proj"]
lora_cfg = LoraConfig(
r=4,
lora_alpha=16,
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM",
target_modules=fallback_modules
)
model = get_peft_model(model, lora_cfg)
model.enable_input_require_grads()
print(f"✅ LoRA adapter applied with fallback modules: {fallback_modules}")
except Exception as e2:
print(f"❌ LoRA fallback also failed: {e2}")
print("Switching to full fine-tuning mode...")
is_lora = False
# -----------------------------------------------------------------------
# TOKENISER & PRE‑PROCESSING
# -----------------------------------------------------------------------
tokenizer = AutoTokenizer.from_pretrained(base_model_name, **common_kwargs, use_fast=False)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
def create_prompt(sys_text: str, usr_text: str):
return f"[SYSTEM]\n{sys_text}\n[USER]\n{usr_text}\n[ASSISTANT]\n"
def chunk(seq, size):
return [seq[i:i + size] for i in range(0, len(seq), size)]
def preprocess_function(examples):
final_input_ids, final_attention_masks, final_labels = [], [], []
for sys_t, usr_t, as_t in zip(examples["System"], examples["User"], examples["Assistant"]):
prompt = create_prompt(sys_t or "", usr_t or "")
full_text = prompt + (as_t or "")
tokenised = tokenizer(full_text, truncation=False, padding=False)
ids = tokenised["input_ids"]
prompt_len = len(tokenizer(prompt, truncation=False, padding=False)["input_ids"])
labels = [-100] * len(ids)
for i in range(prompt_len, len(ids)):
labels[i] = ids[i]
# Split or pad to max_length
if len(ids) > max_length:
id_chunks = chunk(ids, max_length)
label_chunks = chunk(labels, max_length)
for ic, lc in zip(id_chunks, label_chunks):
pad_len = max_length - len(ic)
ic += [tokenizer.pad_token_id] * pad_len
lc += [-100] * pad_len
attn = [int(tok != tokenizer.pad_token_id) for tok in ic]
final_input_ids.append(ic)
final_attention_masks.append(attn)
final_labels.append(lc)
else:
pad_len = max_length - len(ids)
ids += [tokenizer.pad_token_id] * pad_len
labels += [-100] * pad_len
attn = [int(tok != tokenizer.pad_token_id) for tok in ids]
final_input_ids.append(ids)
final_attention_masks.append(attn)
final_labels.append(labels)
return {"input_ids": final_input_ids, "attention_mask": final_attention_masks, "labels": final_labels}
print("Tokenising dataset…")
tokenised_train = raw_datasets["train"].map(preprocess_function, batched=True, remove_columns=raw_datasets["train"].column_names)
tokenised_val = raw_datasets["validation"].map(preprocess_function, batched=True, remove_columns=raw_datasets["validation"].column_names)
data_collator = DefaultDataCollator()
# -----------------------------------------------------------------------
# TRAINING
# -----------------------------------------------------------------------
print("\n*** Training ***")
training_args = TrainingArguments(
output_dir="./finetuned_model",
overwrite_output_dir=True,
num_train_epochs=num_epochs,
per_device_train_batch_size=per_device_bs,
gradient_accumulation_steps=grad_acc_steps,
eval_steps=200,
save_steps=200,
logging_steps=100,
learning_rate=2e-5,
warmup_steps=100,
weight_decay=0.01,
fp16=(precision_choice == "fp16" if precision_choice else False),
bf16=(precision_choice == "bf16" if precision_choice else False),
gradient_checkpointing=True,
max_grad_norm=0.5,
eval_strategy="steps",
save_strategy="steps",
save_total_limit=2,
load_best_model_at_end=True,
push_to_hub=False,
optim="adamw_torch_fused",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenised_train,
eval_dataset=tokenised_val,
data_collator=data_collator,
callbacks=[EarlyStoppingCallback(early_stopping_patience=3)],
)
trainer.train()
# Save outputs -----------------------------------------------------------
trainer.save_model("./finetuned_model")
tokenizer.save_pretrained("./finetuned_model")
print("Training finished → ./finetuned_model created.")
# Merge LoRA weights (only if LoRA strategy) -----------------------------
if is_lora:
merge_choice = get_input("Merge LoRA weights with the base model? [yes/no]: ", ["yes", "no"])
if merge_choice == "yes":
merged_dir = "./merged_final_model"
print("Merging…")
merged_model = model.merge_and_unload()
merged_model.save_pretrained(merged_dir)
tokenizer.save_pretrained(merged_dir)
print(f"Merged model saved → {merged_dir}")
if get_input("Push merged model to HuggingFace Hub? [yes/no]: ", ["yes", "no"]) == "yes":
repo_id = get_input("HF repo ID to push to (e.g. AlicanKiraz0/LLMRipper‑finetuned): ")
print("Uploading…")
merged_model.push_to_hub(repo_id, token=hf_token)
tokenizer.push_to_hub(repo_id, token=hf_token)
print("Upload complete.")
print("All done – happy prompting! ✨")
if __name__ == "__main__":
main()