Buckets:
| import json | |
| import time | |
| from datetime import datetime | |
| import torch | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| print(f"Using device: {device}") | |
| from datasets import Dataset | |
| from transformers import ( | |
| AutoTokenizer, | |
| AutoModelForCausalLM, | |
| TrainingArguments, | |
| Trainer, | |
| DataCollatorForLanguageModeling, | |
| BitsAndBytesConfig, | |
| TrainerCallback, | |
| ) | |
| from peft import ( | |
| get_peft_model, | |
| LoraConfig, | |
| TaskType, | |
| prepare_model_for_kbit_training, | |
| ) | |
| # ========================= | |
| # Config | |
| # ========================= | |
| OUTPUT_DIR = input("Enter the path where you want to save the finetuned model: ").strip() | |
| epoch_num = int(input("Number of epochs: ").strip()) | |
| MAX_LENGTH = 2048 | |
| # ========================= | |
| # Training time window | |
| # (solar energy schedule) | |
| # ========================= | |
| TRAINING_WINDOW_ENABLED = True | |
| TRAINING_WINDOW_START = 7 | |
| TRAINING_WINDOW_END = 20 | |
| # ========================= | |
| # Time window helpers | |
| # ========================= | |
| def is_within_training_window(): | |
| if not TRAINING_WINDOW_ENABLED: | |
| return True | |
| hour = datetime.now().hour | |
| return TRAINING_WINDOW_START <= hour < TRAINING_WINDOW_END | |
| def wait_for_training_window(): | |
| if is_within_training_window(): | |
| return | |
| now = datetime.now() | |
| print(f"\n⏸ Outside training window ({now.strftime('%H:%M')}). " | |
| f"Pausing until {TRAINING_WINDOW_START:02d}:00 ...") | |
| while not is_within_training_window(): | |
| time.sleep(60) | |
| print(f"▶ Training window open ({datetime.now().strftime('%H:%M')}). Resuming ...") | |
| class TimeWindowCallback(TrainerCallback): | |
| def on_step_end(self, args, state, control, **kwargs): | |
| if TRAINING_WINDOW_ENABLED and not is_within_training_window(): | |
| wait_for_training_window() | |
| # ========================= | |
| # Dataset builders | |
| # ========================= | |
| def build_dataset_from_txt(path): | |
| with open(path, "r", encoding="utf-8") as f: | |
| text = f.read() | |
| samples = [t.strip() for t in text.split("\n\n") if t.strip()] | |
| return Dataset.from_dict({"text": samples}) | |
| def build_dataset_from_chat_json(path): | |
| """Full JSON array — [{messages: [{role, content}]}]""" | |
| with open(path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| samples = [] | |
| for convo in data: | |
| lines = [] | |
| for msg in convo["messages"]: | |
| role = msg["role"].capitalize() | |
| lines.append(f"{role}: {msg['content']}") | |
| samples.append("\n".join(lines)) | |
| return Dataset.from_dict({"text": samples}) | |
| def build_dataset_from_chat_jsonl(path): | |
| """Line-delimited JSONL — OpenAssistant / messages+input style""" | |
| samples = [] | |
| with open(path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| convo = json.loads(line) | |
| lines = [] | |
| for msg in convo["messages"]: | |
| role = msg["role"].capitalize() | |
| if "input" in msg and msg["input"].strip(): | |
| lines.append(f"{role} [Input: {msg['input']}]: {msg['content']}") | |
| else: | |
| lines.append(f"{role}: {msg['content']}") | |
| samples.append("\n".join(lines)) | |
| return Dataset.from_dict({"text": samples}) | |
| def build_dataset_from_sharegpt_jsonl(path): | |
| """ | |
| ShareGPT-style JSONL: | |
| {"conversations": [{"from": "system", "value": "..."}, {"from": "human", "value": "..."}, {"from": "gpt", "value": "..."}]} | |
| """ | |
| role_map = { | |
| "system": "System", | |
| "human": "User", | |
| "gpt": "Assistant", | |
| "assistant": "Assistant", | |
| } | |
| samples = [] | |
| with open(path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| convo = json.loads(line) | |
| turns = convo.get("conversations", []) | |
| lines = [] | |
| for msg in turns: | |
| role = role_map.get(msg.get("from", "").lower(), | |
| msg.get("from", "Unknown").capitalize()) | |
| value = msg.get("value", "").strip() | |
| if value: | |
| lines.append(f"{role}: {value}") | |
| if lines: | |
| samples.append("\n".join(lines)) | |
| return Dataset.from_dict({"text": samples}) | |
| def build_dataset_from_alpaca_json(path): | |
| """Alpaca-style JSON array: [{"instruction": "...", "input": "...", "output": "..."}]""" | |
| with open(path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| samples = [] | |
| for item in data: | |
| instruction = item.get("instruction", "").strip() | |
| inp = item.get("input", "").strip() | |
| output = item.get("output", "").strip() | |
| if inp: | |
| text = f"User: {instruction}\nInput: {inp}\nAssistant: {output}" | |
| else: | |
| text = f"User: {instruction}\nAssistant: {output}" | |
| if text.strip(): | |
| samples.append(text) | |
| return Dataset.from_dict({"text": samples}) | |
| def build_dataset_from_alpaca_jsonl(path): | |
| """Alpaca-style JSONL — same schema as above but line-delimited.""" | |
| samples = [] | |
| with open(path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| item = json.loads(line) | |
| instruction = item.get("instruction", "").strip() | |
| inp = item.get("input", "").strip() | |
| output = item.get("output", "").strip() | |
| if inp: | |
| text = f"User: {instruction}\nInput: {inp}\nAssistant: {output}" | |
| else: | |
| text = f"User: {instruction}\nAssistant: {output}" | |
| if text.strip(): | |
| samples.append(text) | |
| return Dataset.from_dict({"text": samples}) | |
| # ========================= | |
| # Dataset menu helper | |
| # ========================= | |
| def pick_dataset(): | |
| print("\nDataset type:") | |
| print("1) Plain .txt") | |
| print("2) Chat JSON (full array, messages/role/content)") | |
| print("3) Chat JSONL (line-delimited, OpenAssistant-style)") | |
| print("4) ShareGPT JSONL (conversations/from/value)") | |
| print("5) Alpaca JSON (instruction/input/output array)") | |
| print("6) Alpaca JSONL (instruction/input/output line-delimited)") | |
| choice = input("Choice: ").strip() | |
| if choice == "1": | |
| path = input("Path to .txt file: ").strip() | |
| return build_dataset_from_txt(path) | |
| elif choice == "2": | |
| path = input("Path to chat JSON: ").strip() | |
| return build_dataset_from_chat_json(path) | |
| elif choice == "3": | |
| path = input("Path to chat JSONL: ").strip() | |
| return build_dataset_from_chat_jsonl(path) | |
| elif choice == "4": | |
| path = input("Path to ShareGPT JSONL: ").strip() | |
| return build_dataset_from_sharegpt_jsonl(path) | |
| elif choice == "5": | |
| path = input("Path to Alpaca JSON: ").strip() | |
| return build_dataset_from_alpaca_json(path) | |
| elif choice == "6": | |
| path = input("Path to Alpaca JSONL: ").strip() | |
| return build_dataset_from_alpaca_jsonl(path) | |
| else: | |
| print("❌ Invalid choice") | |
| return None | |
| # ========================= | |
| # Preprocess factory | |
| # ========================= | |
| def preprocess_factory(tokenizer): | |
| def preprocess(example): | |
| # Try ShareGPT conversations format first | |
| conversations = example.get("conversations") | |
| if conversations: | |
| text = "" | |
| for turn in conversations: | |
| frm = turn.get("from", "") | |
| val = turn.get("value", "") | |
| if frm == "system": | |
| text += f"<|system|>{val}</s>" | |
| elif frm == "human": | |
| text += f"<|user|>{val}</s>" | |
| elif frm == "gpt": | |
| text += f"<|assistant|>{val}</s>" | |
| else: | |
| # Fallback: plain text field | |
| text = example.get("text", "") | |
| enc = tokenizer( | |
| text, | |
| truncation=True, | |
| padding="max_length", | |
| max_length=MAX_LENGTH, | |
| ) | |
| enc["labels"] = enc["input_ids"].copy() | |
| return enc | |
| return preprocess | |
| # ========================= | |
| # QLoRA finetune | |
| # ========================= | |
| def finetune(): | |
| base_model = input("\nBase model (HuggingFace ID or local path, e.g. mistralai/Mistral-7B-v0.1): ").strip() | |
| dataset = pick_dataset() | |
| if dataset is None: | |
| return | |
| # ── QLoRA quantisation config ────────────────────────────────────────── | |
| bnb_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_use_double_quant=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_compute_dtype=torch.bfloat16, | |
| ) | |
| print(f"\n📦 Loading {base_model} in 4-bit …") | |
| tokenizer = AutoTokenizer.from_pretrained(base_model, use_fast=True) | |
| tokenizer.pad_token = tokenizer.eos_token | |
| model = AutoModelForCausalLM.from_pretrained( | |
| base_model, | |
| quantization_config=bnb_config, | |
| device_map="auto", | |
| ) | |
| # ── Prepare for k-bit training ───────────────────────────────────────── | |
| model = prepare_model_for_kbit_training(model) | |
| # ── LoRA config ──────────────────────────────────────────────────────── | |
| print("\nLoRA settings (press Enter to use defaults):") | |
| r_val = input(" LoRA rank r [default 16]: ").strip() or "16" | |
| alpha_val = input(" LoRA alpha [default 32]: ").strip() or "32" | |
| dropout_val = input(" LoRA dropout [default 0.05]: ").strip() or "0.05" | |
| lora_config = LoraConfig( | |
| r=int(r_val), | |
| lora_alpha=int(alpha_val), | |
| lora_dropout=float(dropout_val), | |
| bias="none", | |
| task_type=TaskType.CAUSAL_LM, | |
| target_modules=["q_proj", "k_proj", "v_proj", "o_proj", | |
| "gate_proj", "up_proj", "down_proj"], | |
| ) | |
| model = get_peft_model(model, lora_config) | |
| model.print_trainable_parameters() | |
| # ── Tokenise dataset ─────────────────────────────────────────────────── | |
| dataset = dataset.map( | |
| preprocess_factory(tokenizer), | |
| remove_columns=dataset.column_names, | |
| ) | |
| collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False) | |
| # ── Training args ────────────────────────────────────────────────────── | |
| args = TrainingArguments( | |
| output_dir=OUTPUT_DIR, | |
| num_train_epochs=epoch_num, | |
| per_device_train_batch_size=2, | |
| gradient_accumulation_steps=4, | |
| gradient_checkpointing=True, | |
| optim="paged_adamw_8bit", | |
| bf16=True, | |
| learning_rate=2e-4, | |
| lr_scheduler_type="cosine", | |
| warmup_ratio=0.03, | |
| logging_steps=50, | |
| save_strategy="epoch", | |
| report_to="none", | |
| ) | |
| wait_for_training_window() | |
| trainer = Trainer( | |
| model=model, | |
| args=args, | |
| train_dataset=dataset, | |
| data_collator=collator, | |
| callbacks=[TimeWindowCallback()], | |
| ) | |
| print("\n🚀 Starting QLoRA finetuning …") | |
| trainer.train() | |
| model.save_pretrained(OUTPUT_DIR) | |
| tokenizer.save_pretrained(OUTPUT_DIR) | |
| print(f"\n✅ Finetuning complete — adapter saved to {OUTPUT_DIR}") | |
| print(" To merge weights later, load the base model + adapter with PEFT and call merge_and_unload().") | |
| # ========================= | |
| # Merge adapter → full model | |
| # ========================= | |
| def merge(): | |
| base_model = input("Base model used during finetuning: ").strip() | |
| adapter_path = input("Path to saved adapter (OUTPUT_DIR): ").strip() | |
| merge_out = input("Where to save the merged model: ").strip() | |
| from peft import PeftModel | |
| print(f"\n📦 Loading {base_model} in float16 for merging …") | |
| tokenizer = AutoTokenizer.from_pretrained(adapter_path, use_fast=True) | |
| base = AutoModelForCausalLM.from_pretrained( | |
| base_model, | |
| torch_dtype=torch.float16, | |
| device_map="auto", | |
| ) | |
| print("🔗 Applying adapter …") | |
| model = PeftModel.from_pretrained(base, adapter_path) | |
| print("🔀 Merging weights …") | |
| merged = model.merge_and_unload() | |
| print(f"💾 Saving merged model to {merge_out} …") | |
| merged.save_pretrained(merge_out) | |
| tokenizer.save_pretrained(merge_out) | |
| print(f"\n✅ Merged model saved to {merge_out}") | |
| print(" You can now load it with AutoModelForCausalLM.from_pretrained() directly — no PEFT needed.") | |
| # ========================= | |
| # Run (inference) | |
| # ========================= | |
| def run(): | |
| from peft import PeftModel | |
| base_model = input("Base model used during finetuning: ").strip() | |
| adapter_path = input("Path to saved adapter (OUTPUT_DIR): ").strip() | |
| print(f"\n📦 Loading {base_model} + adapter …") | |
| bnb_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_use_double_quant=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_compute_dtype=torch.bfloat16, | |
| ) | |
| tokenizer = AutoTokenizer.from_pretrained(adapter_path, use_fast=True) | |
| tokenizer.pad_token = tokenizer.eos_token | |
| base = AutoModelForCausalLM.from_pretrained( | |
| base_model, | |
| quantization_config=bnb_config, | |
| device_map="auto", | |
| ) | |
| model = PeftModel.from_pretrained(base, adapter_path) | |
| model.eval() | |
| print("\nModel ready. Type 'exit' to quit.") | |
| while True: | |
| user_prompt = input("\nUser > ").strip() | |
| if user_prompt.lower() == "exit": | |
| break | |
| system = input("System prompt (optional, leave empty to skip): ").strip() | |
| if system: | |
| prompt = f"<|system|>{system}</s><|user|>{user_prompt}</s><|assistant|>" | |
| else: | |
| prompt = f"<|user|>{user_prompt}</s><|assistant|>" | |
| inputs = tokenizer(prompt, return_tensors="pt").to(model.device) | |
| with torch.no_grad(): | |
| out = model.generate( | |
| **inputs, | |
| max_new_tokens=200, | |
| do_sample=True, | |
| temperature=0.8, | |
| top_p=0.95, | |
| repetition_penalty=1.1, | |
| ) | |
| print("\n" + tokenizer.decode(out[0], skip_special_tokens=True)) | |
| # ========================= | |
| # Menu | |
| # ========================= | |
| def main(): | |
| print(""" | |
| ============================= | |
| QLoRA FINETUNING SCRIPT | |
| ============================= | |
| 1) Finetune (QLoRA) | |
| 2) Run (inference) | |
| 3) Merge adapter into base model | |
| 4) Exit | |
| """) | |
| c = input("Select: ").strip() | |
| if c == "1": | |
| finetune() | |
| elif c == "2": | |
| run() | |
| elif c == "3": | |
| merge() | |
| else: | |
| print("👋 Bye") | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 15.2 kB
- Xet hash:
- 8e952d492e5182cc6245d3379d20732a4cb3174a116f425c7b5d0f55be061496
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.