Spaces:
Sleeping
Sleeping
Download src/train.py from Bhuvandesai/phi3-text-to-sql-studio: direct link, hf CLI and curl.
- Browser
- Download file 8.84 kB
-
https://huggingface.co/spaces/Bhuvandesai/phi3-text-to-sql-studio/resolve/main/src/train.py
- Command line
-
hf download hf://spaces/Bhuvandesai/phi3-text-to-sql-studio/src/train.py
-
curl -L -o train.py https://huggingface.co/spaces/Bhuvandesai/phi3-text-to-sql-studio/resolve/main/src/train.py
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() | |