| import os |
| import torch |
| from transformers import ( |
| AutoModelForCausalLM, |
| AutoTokenizer, |
| BitsAndBytesConfig, |
| TrainingArguments, |
| Trainer, |
| DataCollatorForLanguageModeling |
| ) |
| from peft import ( |
| LoraConfig, |
| get_peft_model, |
| prepare_model_for_kbit_training |
| ) |
| from datasets import load_dataset |
|
|
| |
| os.environ["HF_HUB_OFFLINE"] = "1" |
| os.environ["WANDB_DISABLED"] = "true" |
| os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" |
|
|
| def finetune( |
| model_path: str, |
| dataset_path: str, |
| output_dir: str = "./output", |
| batch_size: int = 1, |
| gradient_accumulation_steps: int = 4, |
| learning_rate: float = 2e-4, |
| epochs: int = 1, |
| lora_r: int = 8, |
| lora_alpha: int = 16, |
| lora_dropout: float = 0.05, |
| hf_token: str = None |
| ): |
| if hf_token: |
| from huggingface_hub import login |
| login(token=hf_token) |
|
|
| print(f"Loading tokenizer from {model_path}...") |
| tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) |
| if tokenizer.pad_token is None: |
| tokenizer.pad_token = tokenizer.eos_token |
|
|
| print("Loading model in 4-bit (QLoRA) for 8GB VRAM optimization...") |
| bnb_config = BitsAndBytesConfig( |
| load_in_4bit=True, |
| bnb_4bit_use_double_quant=True, |
| bnb_4bit_quant_type="nf4", |
| bnb_4bit_compute_dtype=torch.bfloat16 |
| ) |
|
|
| model = AutoModelForCausalLM.from_pretrained( |
| model_path, |
| quantization_config=bnb_config, |
| device_map="auto", |
| trust_remote_code=True |
| ) |
|
|
| |
| model.gradient_checkpointing_enable() |
| model = prepare_model_for_kbit_training(model) |
|
|
| |
| config = LoraConfig( |
| r=lora_r, |
| lora_alpha=lora_alpha, |
| target_modules=["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], |
| lora_dropout=lora_dropout, |
| bias="none", |
| task_type="CAUSAL_LM" |
| ) |
|
|
| model = get_peft_model(model, config) |
| model.print_trainable_parameters() |
|
|
| print(f"Loading dataset from {dataset_path}...") |
| |
| if os.path.exists(dataset_path): |
| dataset = load_dataset("json", data_files=dataset_path, split="train") |
| else: |
| print(f"Dataset path '{dataset_path}' not found locally. Attempting to load from Hugging Face...") |
| dataset = load_dataset(dataset_path, split="train", token=hf_token) |
|
|
| def tokenize_function(examples): |
| |
| if "messages" in examples: |
| |
| texts = [] |
| for msg_list in examples["messages"]: |
| formatted_text = "" |
| for msg in msg_list: |
| role = msg['role'].capitalize() |
| content = msg['content'] |
| formatted_text += f"{role}: {content}\n" |
| texts.append(formatted_text) |
| return tokenizer(texts, truncation=True, padding="max_length", max_length=512) |
|
|
| |
| if "system_prompt" in examples and "question" in examples: |
| texts = [ |
| f"### System:\n{s}\n\n### Question:\n{q}\n\n### Response:\n{r}" |
| for s, q, r in zip(examples["system_prompt"], examples["question"], examples["response"]) |
| ] |
| return tokenizer(texts, truncation=True, padding="max_length", max_length=512) |
|
|
| |
| if "instruction" in examples and "output" in examples: |
| texts = [] |
| for i, inp, o in zip(examples["instruction"], examples.get("input", [""] * len(examples["instruction"])), examples["output"]): |
| text = f"### Instruction:\n{i}" |
| if inp: |
| text += f"\n\n### Input:\n{inp}" |
| text += f"\n\n### Response:\n{o}" |
| texts.append(text) |
| return tokenizer(texts, truncation=True, padding="max_length", max_length=512) |
|
|
| |
| text_field = "text" |
| if "text" not in examples: |
| for field in ["instruction", "content", "response", "output"]: |
| if field in examples: |
| text_field = field |
| break |
| |
| return tokenizer(examples[text_field], truncation=True, padding="max_length", max_length=512) |
|
|
| tokenized_dataset = dataset.map(tokenize_function, batched=True, remove_columns=dataset.column_names) |
|
|
| training_args = TrainingArguments( |
| output_dir=output_dir, |
| per_device_train_batch_size=batch_size, |
| gradient_accumulation_steps=gradient_accumulation_steps, |
| learning_rate=learning_rate, |
| num_train_epochs=epochs, |
| logging_steps=10, |
| save_strategy="steps", |
| save_steps=100, |
| evaluation_strategy="no", |
| fp16=False, |
| bf16=True, |
| optim="paged_adamw_8bit", |
| report_to="none" |
| ) |
|
|
| trainer = Trainer( |
| model=model, |
| args=training_args, |
| train_dataset=tokenized_dataset, |
| data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False), |
| ) |
|
|
| print("Starting training...") |
| trainer.train() |
| |
| print(f"Saving final adapter to {output_dir}...") |
| model.save_pretrained(output_dir) |
| print("Finetuning complete.") |
|
|
| if __name__ == "__main__": |
| import argparse |
| parser = argparse.ArgumentParser(description="8GB VRAM Optimized QLoRA Finetuner") |
| parser.add_argument("--model", type=str, required=True, help="Local path to the model") |
| parser.add_argument("--dataset", type=str, required=True, help="Local path or HF ID to .json/.jsonl dataset") |
| parser.add_argument("--output", type=str, default="./finetuned_model", help="Directory to save output") |
| parser.add_argument("--hf-token", type=str, help="Hugging Face API token") |
| args = parser.parse_args() |
|
|
| finetune(args.model, args.dataset, args.output, hf_token=args.hf_token) |
|
|