import inspect import math import os import torch from datasets import load_dataset from peft import LoraConfig from transformers import ( AutoModelForCausalLM, AutoTokenizer, set_seed, ) from trl import SFTConfig, SFTTrainer from config import ( DATASET_ID, MAX_TRAIN_SAMPLES, MAX_VALIDATION_SAMPLES, MODEL_ID, OUTPUT_DIR, SEED, SYSTEM_PROMPT, ) # ============================================================ # TRAINING CONFIGURATION # ============================================================ NUM_EPOCHS = 2 TRAIN_BATCH_SIZE = 4 EVAL_BATCH_SIZE = 4 GRADIENT_ACCUMULATION_STEPS = 4 LEARNING_RATE = 2e-4 MAX_SEQUENCE_LENGTH = 2048 WARMUP_PERCENT = 0.03 # ============================================================ # DATA FORMATTING # ============================================================ def format_for_sft(example): """ Convert one Gretel Text-to-SQL record into conversational prompt-completion format. Input: Database schema/context + Natural-language request Target: SQL query """ user_message = ( "Database context:\n" f"{example['sql_context']}\n\n" "Request:\n" f"{example['sql_prompt']}" ) return { "prompt": [ { "role": "system", "content": SYSTEM_PROMPT, }, { "role": "user", "content": user_message, }, ], "completion": [ { "role": "assistant", "content": example["sql"].strip(), }, ], } # ============================================================ # CUDA / RTX CHECK # ============================================================ def configure_gpu(): """ Require an NVIDIA CUDA GPU. This intentionally DOES NOT fall back to CPU. Training a 0.5B model on CPU would be extremely slow. """ print("=" * 65) print("GPU CHECK") print("=" * 65) print("PyTorch version:", torch.__version__) print("PyTorch CUDA runtime:", torch.version.cuda) print("CUDA available:", torch.cuda.is_available()) if not torch.cuda.is_available(): raise RuntimeError( "\nCUDA is NOT available.\n\n" "Your train.py is configured for an NVIDIA GeForce RTX GPU, " "but your current PyTorch installation cannot access CUDA.\n\n" "If your PyTorch version contains '+cpu', you installed the " "CPU-only version of PyTorch.\n\n" "Run `nvidia-smi` first to verify Windows sees your GPU, " "then install the CUDA-enabled version of PyTorch." ) # NVIDIA GPU device = torch.device("cuda:0") torch.cuda.set_device(device) gpu_name = torch.cuda.get_device_name(device) properties = torch.cuda.get_device_properties(device) total_vram_gb = ( properties.total_memory / (1024 ** 3) ) print("GPU:", gpu_name) print( "VRAM:", f"{total_vram_gb:.2f} GB" ) print( "CUDA device:", torch.cuda.current_device() ) print("=" * 65) return device # ============================================================ # SELECT TRAINING PRECISION # ============================================================ def select_precision(): """ Automatically select BF16 or FP16. BF16: Preferred when the GPU supports it. FP16: Used otherwise on CUDA GPUs. """ bf16_supported = ( torch.cuda.is_bf16_supported() ) if bf16_supported: model_dtype = torch.bfloat16 use_bf16 = True use_fp16 = False precision_name = "BF16" else: model_dtype = torch.float16 use_bf16 = False use_fp16 = True precision_name = "FP16" print( "Training precision:", precision_name ) print( "Model dtype:", model_dtype ) return ( model_dtype, use_bf16, use_fp16, ) # ============================================================ # BUILD SFT CONFIG SAFELY # ============================================================ def build_sft_config( train_dataset, use_bf16, use_fp16, ): """ Build SFTConfig while checking which arguments are supported by the installed TRL version. This prevents errors such as: unexpected keyword argument 'warmup_ratio' caused by different TRL versions exposing slightly different SFTConfig parameters. """ # -------------------------------------------------------- # Estimate optimizer steps # -------------------------------------------------------- effective_batch_size = ( TRAIN_BATCH_SIZE * GRADIENT_ACCUMULATION_STEPS ) steps_per_epoch = math.ceil( len(train_dataset) / effective_batch_size ) total_training_steps = ( steps_per_epoch * NUM_EPOCHS ) warmup_steps = max( 1, int( total_training_steps * WARMUP_PERCENT ), ) print() print("=" * 65) print("TRAINING PLAN") print("=" * 65) print( "Training samples:", len(train_dataset) ) print( "Batch size:", TRAIN_BATCH_SIZE ) print( "Gradient accumulation:", GRADIENT_ACCUMULATION_STEPS ) print( "Effective batch size:", effective_batch_size ) print( "Epochs:", NUM_EPOCHS ) print( "Estimated optimizer steps:", total_training_steps ) print( "Warmup steps:", warmup_steps ) print("=" * 65) # -------------------------------------------------------- # Configuration # -------------------------------------------------------- config_kwargs = { "output_dir": OUTPUT_DIR, "num_train_epochs": NUM_EPOCHS, "per_device_train_batch_size": TRAIN_BATCH_SIZE, "per_device_eval_batch_size": EVAL_BATCH_SIZE, "gradient_accumulation_steps": GRADIENT_ACCUMULATION_STEPS, "learning_rate": LEARNING_RATE, "warmup_steps": warmup_steps, "weight_decay": 0.01, "lr_scheduler_type": "cosine", "logging_steps": 10, "eval_strategy": "steps", "eval_steps": 100, "save_strategy": "steps", "save_steps": 100, "save_total_limit": 2, "load_best_model_at_end": True, "metric_for_best_model": "eval_loss", "greater_is_better": False, "bf16": use_bf16, "fp16": use_fp16, "max_length": MAX_SEQUENCE_LENGTH, # Prompt-completion datasets can calculate # loss only on the SQL completion. "completion_only_loss": True, "gradient_checkpointing": True, "report_to": "none", "seed": SEED, } # -------------------------------------------------------- # TRL compatibility check # -------------------------------------------------------- signature = inspect.signature( SFTConfig.__init__ ) supported_parameters = ( signature.parameters ) filtered_kwargs = {} skipped_kwargs = [] for key, value in config_kwargs.items(): if key in supported_parameters: filtered_kwargs[key] = value else: skipped_kwargs.append(key) if skipped_kwargs: print() print( "TRL compatibility notice:" ) print( "Unsupported SFTConfig arguments:", skipped_kwargs, ) print( "They were skipped automatically." ) return SFTConfig( **filtered_kwargs ) # ============================================================ # MAIN TRAINING FUNCTION # ============================================================ def main(): # -------------------------------------------------------- # Reproducibility # -------------------------------------------------------- set_seed(SEED) # -------------------------------------------------------- # Require NVIDIA RTX GPU # -------------------------------------------------------- device = configure_gpu() # -------------------------------------------------------- # Select BF16 / FP16 # -------------------------------------------------------- ( model_dtype, use_bf16, use_fp16, ) = select_precision() # -------------------------------------------------------- # Load Hugging Face dataset # -------------------------------------------------------- print() print("=" * 65) print("LOADING DATASET") print("=" * 65) print( "Dataset:", DATASET_ID ) dataset = load_dataset( DATASET_ID ) # Shuffle the original train/test splits. train_dataset = ( dataset["train"] .shuffle(seed=SEED) ) validation_dataset = ( dataset["test"] .shuffle(seed=SEED) ) # -------------------------------------------------------- # Limit dataset during development # -------------------------------------------------------- if MAX_TRAIN_SAMPLES is not None: train_dataset = ( train_dataset.select( range( min( MAX_TRAIN_SAMPLES, len(train_dataset), ) ) ) ) if MAX_VALIDATION_SAMPLES is not None: validation_dataset = ( validation_dataset.select( range( min( MAX_VALIDATION_SAMPLES, len(validation_dataset), ) ) ) ) print( "Training examples:", len(train_dataset) ) print( "Validation examples:", len(validation_dataset) ) # -------------------------------------------------------- # Format dataset for instruction tuning # -------------------------------------------------------- print() print("Formatting training data...") train_dataset = train_dataset.map( format_for_sft, remove_columns=( train_dataset.column_names ), desc="Formatting training data", ) print() print("Formatting validation data...") validation_dataset = ( validation_dataset.map( format_for_sft, remove_columns=( validation_dataset.column_names ), desc="Formatting validation data", ) ) # -------------------------------------------------------- # Tokenizer # -------------------------------------------------------- print() print("=" * 65) print("LOADING TOKENIZER") print("=" * 65) tokenizer = ( AutoTokenizer.from_pretrained( MODEL_ID ) ) if tokenizer.pad_token is None: tokenizer.pad_token = ( tokenizer.eos_token ) print( "EOS token:", tokenizer.eos_token ) print( "PAD token:", tokenizer.pad_token ) # -------------------------------------------------------- # Base model # -------------------------------------------------------- print() print("=" * 65) print("LOADING QWEN BASE MODEL") print("=" * 65) print( "Model:", MODEL_ID ) print( "Target GPU:", torch.cuda.get_device_name(0) ) model = ( AutoModelForCausalLM .from_pretrained( MODEL_ID, # Modern Transformers syntax. dtype=model_dtype, ) ) # Trainer / Accelerate handles moving the model # onto the CUDA device. # single-GPU training job. device_map is primarily # useful for model dispatch/offloading. model.config.use_cache = False # -------------------------------------------------------- # Enable gradient checkpointing # -------------------------------------------------------- model.gradient_checkpointing_enable() # -------------------------------------------------------- # LoRA configuration # -------------------------------------------------------- print() print("=" * 65) print("CONFIGURING LoRA") print("=" * 65) peft_config = LoraConfig( # Rank of low-rank matrices A and B. r=16, # LoRA scaling parameter. lora_alpha=32, # Regularization on adapter branch. lora_dropout=0.05, # Do not train bias parameters. bias="none", # Qwen is a causal LM. task_type="CAUSAL_LM", # Apply LoRA to all four major # self-attention projections. target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", ], ) print("LoRA rank: 16") print("LoRA alpha: 32") print("LoRA dropout: 0.05") print( "LoRA targets:", "q_proj, k_proj, v_proj, o_proj", ) # -------------------------------------------------------- # Training configuration # -------------------------------------------------------- training_args = build_sft_config( train_dataset, use_bf16, use_fp16, ) # -------------------------------------------------------- # Trainer # -------------------------------------------------------- print() print("=" * 65) print("CREATING SFT TRAINER") print("=" * 65) trainer = SFTTrainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=validation_dataset, processing_class=tokenizer, peft_config=peft_config, ) # -------------------------------------------------------- # Print parameter counts # -------------------------------------------------------- trainable_parameters = 0 total_parameters = 0 for parameter in trainer.model.parameters(): parameter_count = ( parameter.numel() ) total_parameters += ( parameter_count ) if parameter.requires_grad: trainable_parameters += ( parameter_count ) trainable_percent = ( 100 * trainable_parameters / total_parameters ) print() print("=" * 65) print("PARAMETER SUMMARY") print("=" * 65) print( f"Total parameters: " f"{total_parameters:,}" ) print( f"Trainable parameters: " f"{trainable_parameters:,}" ) print( f"Trainable percentage: " f"{trainable_percent:.4f}%" ) print() print( "Active GPU:", torch.cuda.get_device_name(0) ) print( "Allocated VRAM:", f"{torch.cuda.memory_allocated(0) / 1024**3:.2f} GB" ) # -------------------------------------------------------- # Train # -------------------------------------------------------- print() print("=" * 65) print("STARTING LoRA FINE-TUNING") print("=" * 65) trainer.train() # -------------------------------------------------------- # Save trained LoRA adapter # -------------------------------------------------------- print() print("=" * 65) print("SAVING MODEL") print("=" * 65) trainer.save_model( OUTPUT_DIR ) tokenizer.save_pretrained( OUTPUT_DIR ) print() print( "LoRA adapter saved to:", OUTPUT_DIR ) print( "Final GPU:", torch.cuda.get_device_name(0) ) print( "Maximum VRAM allocated:", f"{torch.cuda.max_memory_allocated(0) / 1024**3:.2f} GB" ) # ============================================================ # ENTRY POINT # ============================================================ if __name__ == "__main__": main()