Bhuvandesai's picture
fix: set per_device_eval_batch_size=1 to prevent OOM during evaluation loop
26efb80
Raw History Blame Contribute Delete
8.84 kB
import os
import sys
import sqlite3
import pandas as pd
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
TrainerCallback
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig
def adapted_projection_types(peft_model) -> set:
"""Base-module suffixes that actually received a LoRA adapter."""
return {name.rsplit(".lora_A", 1)[0].split(".")[-1]
for name, _ in peft_model.named_modules() if name.endswith("lora_A")}
EXPECTED_TARGETS = {"qkv_proj", "o_proj", "gate_up_proj", "down_proj"}
MAX_LEN = 2560 # training sequence cap; longer examples are dropped so their SQL labels aren't truncated
class VRAMLoggerCallback(TrainerCallback):
"""Callback to print VRAM usage during training to monitor resource consumption."""
def on_step_end(self, args, state, control, **kwargs):
if torch.cuda.is_available():
allocated = torch.cuda.memory_allocated(0) / (1024 ** 3)
reserved = torch.cuda.memory_reserved(0) / (1024 ** 3)
print(f" - [Step {state.global_step}] VRAM Allocated: {allocated:.2f} GB | Reserved: {reserved:.2f} GB")
def train():
print("=" * 60)
print("Starting Phi-3 Text-to-SQL QLoRA Fine-Tuning")
print("=" * 60)
# 1. Device Configuration and Hardware Checks
if not torch.cuda.is_available():
print("WARNING: CUDA is NOT available. Running on CPU is extremely slow and NOT recommended.")
device = "cpu"
use_bf16 = False
use_fp16 = False
else:
device = "cuda"
gpu_name = torch.cuda.get_device_name(0)
print(f"CUDA Device Detected: {gpu_name}")
# Native bf16 only on Ampere+ (compute capability >= 8). Turing (T4, sm_75) reports
# is_bf16_supported()=True but EMULATES bf16 slowly, so gate on the real capability.
major, _ = torch.cuda.get_device_capability()
use_bf16 = major >= 8
use_fp16 = not use_bf16
print(f"Precision: bf16={use_bf16} | fp16={use_fp16} (sm_{major}x)")
# Quantization backend: 4-bit QLoRA only if bitsandbytes actually imports. It is broken on
# some Kaggle/CUDA images (missing CUDA binary; 'No module named triton.ops'). Otherwise fall
# back to plain LoRA in bf16/fp16 -- a 3.8B model fits a 16GB T4 without 4-bit.
use_4bit = False
if device == "cuda":
try:
import bitsandbytes # noqa: F401
use_4bit = True
print("bitsandbytes available -> using 4-bit QLoRA")
except Exception as e:
print(f"[info] bitsandbytes unavailable ({type(e).__name__}: {e}); "
f"falling back to {'bf16' if use_bf16 else 'fp16'} LoRA (no 4-bit).")
# 2. Paths
dataset_dir = "data/spider"
train_file = os.path.join(dataset_dir, "train.jsonl")
val_file = os.path.join(dataset_dir, "dev.jsonl")
output_dir = "models/phi3-text-to-sql"
adapter_dir = "models/phi3-text-to-sql-adapter"
if not os.path.exists(train_file) or not os.path.exists(val_file):
print("ERROR: Dataset files not found. Run dataset.py first.")
sys.exit(1)
# 3. Model load config: 4-bit quant when available, else full-precision (bf16/fp16) LoRA.
compute_dtype = torch.bfloat16 if use_bf16 else torch.float16
model_id = "microsoft/Phi-3-mini-4k-instruct"
load_kwargs = dict(trust_remote_code=False, attn_implementation="eager")
if use_4bit:
print("Configuring 4-bit Quantization (QLoRA)...")
load_kwargs["quantization_config"] = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=compute_dtype,
)
load_kwargs["device_map"] = "auto"
from accelerate import Accelerator
load_kwargs["device_map"] = {"": Accelerator().local_process_index}
else:
load_kwargs["torch_dtype"] = compute_dtype
load_kwargs["low_cpu_mem_usage"] = True # no device_map: the Trainer places it on the GPU
# 4. Load Model and Tokenizer
print(f"Loading base model: {model_id} (4bit={use_4bit})...")
model = AutoModelForCausalLM.from_pretrained(model_id, **load_kwargs)
print("Loading tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=False)
# Configure padding tokens for Phi-3 (which uses EOS for padding natively in standard chat SFT)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 5. Prepare Model for Peft Training (gradient checkpointing + input grads)
if use_4bit:
print("Preparing model for k-bit training...")
model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)
else:
print("Enabling gradient checkpointing for full-precision LoRA...")
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
# 6. Configure LoRA
# Phi-3 fuses q/k/v -> qkv_proj and gate/up -> gate_up_proj; targeting these plus o_proj
# and down_proj adapts all 7 logical projections (the v1 config silently hit only 2).
print("Configuring LoRA parameters...")
peft_config = LoraConfig(
r=16, lora_alpha=32,
target_modules=["qkv_proj", "o_proj", "gate_up_proj", "down_proj"],
lora_dropout=0.05, bias="none", task_type="CAUSAL_LM",
)
model = get_peft_model(model, peft_config)
got = adapted_projection_types(model)
assert got == EXPECTED_TARGETS, (
f"LoRA adapted {got}, expected {EXPECTED_TARGETS}. Phi-3 fuses q/k/v->qkv_proj and "
"gate/up->gate_up_proj; the v1 config silently matched only o_proj+down_proj. "
"Refusing to train a partial adapter.")
model.print_trainable_parameters() # expect ~0.5-0.9% now (was 0.12%)
# 7. Load Datasets
print("Loading formatted training datasets...")
dataset = load_dataset("json", data_files={
"train": train_file,
"validation": val_file
})
# Drop examples longer than MAX_LEN so their SQL labels are never truncated
# (~0.95% of Spider train exceeds 2560 tokens; corrupt labels are worse than fewer rows).
def _fits(ex):
return len(tokenizer.apply_chat_template(ex["messages"], tokenize=True)) <= MAX_LEN
before = len(dataset["train"])
dataset["train"] = dataset["train"].filter(_fits)
dataset["validation"] = dataset["validation"].filter(_fits)
print(f"Length filter (<= {MAX_LEN} tok): kept {len(dataset['train'])}/{before} train "
f"(dropped {before - len(dataset['train'])}); dev now {len(dataset['validation'])}")
# 8. Training Arguments
# Memory-optimized: batch_size=1, gradient_accumulation=8 (eff. batch size 8), paged_adamw optimizer
print("Setting up memory-optimized SFT training arguments...")
training_args = SFTConfig(
output_dir=output_dir,
num_train_epochs=2,
per_device_train_batch_size=2,
per_device_eval_batch_size=1,
gradient_accumulation_steps=4,
group_by_length=True,
learning_rate=2e-4,
weight_decay=0.01,
lr_scheduler_type="cosine",
warmup_ratio=0.05,
logging_steps=1,
eval_strategy="steps",
eval_steps=200,
save_strategy="steps",
save_steps=200,
save_total_limit=2,
optim="paged_adamw_8bit" if use_4bit else "adamw_torch",
bf16=use_bf16,
fp16=use_fp16,
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
max_grad_norm=0.3,
report_to="none",
ddp_find_unused_parameters=False,
remove_unused_columns=True, # Allow Trainer to drop the raw 'messages' column after tokenization
max_seq_length=2560
)
# 9. Initialize SFTTrainer
# SFTTrainer natively formats 'messages' using the model's chat template
print("Initializing SFTTrainer...")
trainer = SFTTrainer(
model=model,
train_dataset=dataset["train"],
eval_dataset=dataset["validation"],
peft_config=None,
processing_class=tokenizer,
args=training_args,
callbacks=[VRAMLoggerCallback()] if device == "cuda" else []
)
# 10. Execute Fine-Tuning
print("Launching Fine-Tuning loop...")
trainer.train()
# 11. Save the Fine-Tuned Adapter
print(f"Fine-tuning complete! Saving adapter to {adapter_dir}...")
trainer.model.save_pretrained(adapter_dir)
tokenizer.save_pretrained(adapter_dir)
print("Adapter saved successfully. Ready for evaluation and deployment!")
if __name__ == "__main__":
train()