#!/usr/bin/env python3 import os # below line clears the console os.system('clear') # below line calls gpustat if the gpu id is 0 if os.environ.get('RANK') == '0': os.system('gpustat') import re import torch import logging from typing import List, Dict, Any, Optional from datasets import load_dataset, Dataset import transformers from transformers import AutoTokenizer, AutoModelForCausalLM from trl import GRPOConfig, GRPOTrainer from accelerate import Accelerator import torch.distributed as dist from premsql.executors import SQLiteExecutor from premsql.datasets import BirdDataset from premsql.evaluator import Text2SQLEvaluator import pandas as pd import json from pathlib import Path # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Constants SYSTEM_PROMPT = """You are an expert SQL analyst. For the given question and database schema, generate a SQL query. First analyze the schema and requirements, then write the query. The database schema and tables are: {schema} Respond in the following format: [Step by step analysis of the requirements and schema] [Your SQL query] """ class PremSQLv2Trainer: def __init__( self, model_name: str, output_dir: str, dataset_folder: str = "./data", hf_token: Optional[str] = None ): self.model_name = model_name self.output_dir = output_dir self.dataset_folder = Path(dataset_folder) self.hf_token = hf_token self.device_count = torch.cuda.device_count() self.executor = SQLiteExecutor() self.evaluator = Text2SQLEvaluator( executor=self.executor, experiment_path=output_dir ) # Create dataset directory if it doesn't exist self.dataset_folder.mkdir(parents=True, exist_ok=True) self.setup_environment() def setup_environment(self): """Setup training environment and distributed training""" os.environ["CUDA_LAUNCH_BLOCKING"] = "1" if self.device_count > 1: if os.environ.get('RANK') is not None and os.environ.get('WORLD_SIZE') is not None: if not dist.is_initialized(): dist.init_process_group(backend='nccl') else: logger.info("Multiple GPUs detected but distributed training environment variables not set. Running on single GPU.") @staticmethod def extract_reasoning(text: str) -> str: """Extract reasoning from model output""" try: reasoning = text.split("")[-1] reasoning = reasoning.split("")[0] return reasoning.strip() except IndexError: return "" @staticmethod def extract_sql_query(text: str) -> str: """Extract SQL query from model output""" try: sql = text.split("")[-1] sql = sql.split("")[0] return sql.strip() except IndexError: return "" def reasoning_quality_reward(self, text: str) -> float: """Evaluate the quality of reasoning""" reasoning = self.extract_reasoning(text) if not reasoning: return 0.0 reward = 0.0 # Check for key reasoning components if "schema" in reasoning.lower(): reward += 0.2 if "table" in reasoning.lower(): reward += 0.2 if "join" in reasoning.lower(): reward += 0.2 if "where" in reasoning.lower(): reward += 0.2 if "condition" in reasoning.lower(): reward += 0.2 # Check for step-by-step analysis steps = [s for s in reasoning.split('\n') if s.strip()] if len(steps) >= 3: reward += 0.3 # Check for schema understanding if any(word in reasoning.lower() for word in ["primary key", "foreign key", "relationship"]): reward += 0.2 return min(reward, 1.5) # Cap at 1.5 def show_gpu_usage(self): """Show GPU usage if on main process""" if os.environ.get('RANK') == '0': os.system('gpustat') def get_training_dataset(self) -> Dataset: """Load and prepare BirdBench dataset""" if os.environ.get('RANK') == '0': logger.info("Loading BirdBench dataset...") # Initialize BirdDataset without forcing download bird_dataset = BirdDataset( split="train", dataset_folder=self.dataset_folder, hf_token=self.hf_token, force_download=False ) # Format the examples with schema information formatted_examples = [] for example in bird_dataset.dataset: # Get database schema db_path = f"{self.dataset_folder}/train_databases/{example['db_id']}/{example['db_id']}.sqlite" schema = self.get_database_schema(db_path) # Format prompt with schema prompt = [ {'role': 'system', 'content': SYSTEM_PROMPT.format(schema=schema)}, {'role': 'user', 'content': f"Database: {example['db_id']}\nQuestion: {example['question']}"} ] formatted_examples.append({ 'prompt': prompt, 'ground_truth': example['SQL'], 'db_id': example['db_id'] }) # Convert to Dataset dataset = Dataset.from_list(formatted_examples) # Add tokenized inputs tokenizer = AutoTokenizer.from_pretrained( self.model_name, padding_side="right", token=self.hf_token ) def tokenize_function(examples): # Apply chat template texts = [tokenizer.apply_chat_template(p, tokenize=False) for p in examples['prompt']] tokenized = tokenizer(texts, padding=True, truncation=True) # Keep original fields return { 'input_ids': tokenized['input_ids'], 'attention_mask': tokenized['attention_mask'], 'prompt': examples['prompt'], 'ground_truth': examples['ground_truth'], 'db_id': examples['db_id'] } # Tokenize while keeping original fields dataset = dataset.map( tokenize_function, batched=True, remove_columns=None ) if os.environ.get('RANK') == '0': logger.info(f"Dataset loaded with {len(dataset)} examples") self.show_gpu_usage() return dataset def get_database_schema(self, db_path: str) -> str: """Extract database schema from SQLite database""" try: import sqlite3 conn = sqlite3.connect(db_path) cursor = conn.cursor() # Get all tables cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") tables = cursor.fetchall() schema_info = [] for table in tables: table_name = table[0] # Get columns for each table cursor.execute(f"PRAGMA table_info({table_name});") columns = cursor.fetchall() # Format table schema column_info = [f"{col[1]} ({col[2]})" for col in columns] schema_info.append(f"Table: {table_name}\nColumns: {', '.join(column_info)}\n") conn.close() return "\n".join(schema_info) except Exception as e: if os.environ.get('RANK') == '0': logger.warning(f"Failed to get schema for {db_path}: {str(e)}") return "Schema information unavailable" def execution_accuracy_reward( self, prompts: List, completions: List, **kwargs ) -> List[float]: """Reward function based on SQL execution accuracy""" responses = [completion[0]['content'] for completion in completions] rewards = [] # Get ground truth from dataset dataset = kwargs.get('train_dataset') batch_size = len(prompts) current_idx = kwargs.get('current_step', 0) * batch_size for i, response in enumerate(responses): query = self.extract_sql_query(response) reward = 0.0 try: if query: # Get corresponding ground truth example = dataset[current_idx + i] db_path = f"{self.dataset_folder}/train_databases/{example['db_id']}/{example['db_id']}.sqlite" # Debug information if os.environ.get('RANK') == '0': debug_info = { 'query': query, 'ground_truth': example['ground_truth'], 'db_path': db_path } print("\n=== Debug Info ===") print(f"Full Response:\n{response}") print(f"\nExtracted Query:\n{query}") print(f"\nDB Path: {db_path}") print(f"DB Path exists: {os.path.exists(db_path)}") print(f"\nExample Data:") print(json.dumps(example, indent=2)) print("\nEvaluator Input:") print(json.dumps(debug_info, indent=2)) print("================\n") result = self.evaluator.execute( metric_name="accuracy", model_responses=[{ 'query': query, 'ground_truth': example['ground_truth'], 'db_path': db_path }], filter_by="db_id", meta_time_out=10 ) # Debug evaluator result if os.environ.get('RANK') == '0': print("Evaluator Result:") print(json.dumps(result, indent=2)) # Reward based on accuracy reward = result['accuracy'] * 2.0 # Add small rewards for SQL components reward += 0.1 * query.upper().count("SELECT") reward += 0.1 * query.upper().count("FROM") reward += 0.2 * query.upper().count("JOIN") except Exception as e: if os.environ.get('RANK') == '0': logger.warning(f"Query execution error: {str(e)}") logger.warning(f"Error occurred with query: {query}") logger.warning(f"Full response was: {response}") if all(k in query.upper() for k in ["SELECT", "FROM"]): reward = 0.1 rewards.append(reward) return rewards def setup_training_args(self) -> GRPOConfig: """Configure training arguments""" return GRPOConfig( output_dir=self.output_dir, run_name=f"{self.model_name.split('/')[-1]}-GRPO-v2", learning_rate=5e-6, adam_beta1=0.9, adam_beta2=0.99, weight_decay=0.1, warmup_ratio=0.1, lr_scheduler_type='cosine', logging_steps=1, bf16=True, per_device_train_batch_size=2, gradient_accumulation_steps=2, num_generations=16, max_prompt_length=512, max_completion_length=256, num_train_epochs=1, save_steps=50, max_grad_norm=0.1, log_on_each_node=True, use_vllm=False, report_to="wandb", # Using wandb for tracking ddp_find_unused_parameters=False, push_to_hub=False, hub_model_id=None, hub_token=None, ) def train(self): """Main training function""" try: # Load dataset dataset = self.get_training_dataset() if os.environ.get('RANK') == '0': logger.info(f"Loaded dataset with {len(dataset)} examples") # Initialize model and tokenizer model = AutoModelForCausalLM.from_pretrained( self.model_name, torch_dtype=torch.bfloat16, ).cuda() tokenizer = AutoTokenizer.from_pretrained(self.model_name) tokenizer.pad_token = tokenizer.eos_token # Setup training arguments training_args = self.setup_training_args() class GPUMonitorCallback(transformers.TrainerCallback): def __init__(self, trainer): self.trainer = trainer def on_save(self, args, state, control, **kwargs): if os.environ.get('RANK') == '0' and state.global_step % 50 == 0: self.trainer.show_gpu_usage() # Initialize trainer trainer = GRPOTrainer( model=model, processing_class=tokenizer, reward_funcs=[self.execution_accuracy_reward], args=training_args, train_dataset=dataset, callbacks=[GPUMonitorCallback(self)] ) # Start training trainer.train() # Save model if dist.get_rank() == 0: logger.info(f"Saving model to {self.output_dir}") trainer.save_model() self.show_gpu_usage() # Show final GPU usage # Cleanup distributed training if dist.is_initialized(): dist.destroy_process_group() except Exception as e: if os.environ.get('RANK') == '0': logger.error(f"Training failed with error: {str(e)}") if dist.is_initialized(): dist.destroy_process_group() raise def main(): """Main entry point""" model_name = "premai-io/prem-1B-SQL" output_dir = "outputs/Prem-1B-SQL-GRPO-v2" dataset_folder = "./data/birdbench" hf_token = "hf_" trainer = PremSQLv2Trainer( model_name=model_name, output_dir=output_dir, dataset_folder=dataset_folder, hf_token=hf_token ) trainer.train() if __name__ == "__main__": main()