customercore / src /ml /modal_train.py
saibalajiomg's picture
Upload folder using huggingface_hub
f47b400 verified
Raw
History Blame Contribute Delete
6.79 kB
"""
src/ml/modal_train.py
On-demand serverless QLoRA fine-tuning for CustomerCore LLM.
Spins up a high-powered cloud GPU (Nvidia A10G/A100) on Modal,
extracts tickets from Supabase, runs PEFT training, and pushes
the trained adapter back to Hugging Face.
Usage:
doppler run -- python src/ml/modal_train.py
"""
import os
import modal
# ── Define the GPU Container Environment ──────────────────────────────────────
image = (
modal.Image.debian_slim()
.apt_install("git")
.pip_install(
"setuptools",
"numpy<2",
"torch==2.2.0",
"transformers==4.38.1",
"peft==0.8.2",
"bitsandbytes==0.42.0",
"accelerate==0.27.2",
"datasets==2.17.1",
"supabase==2.30.0",
)
)
app = modal.App("customercore-llm-finetuning")
# Pull credentials automatically from your active shell environment variables
secrets = [
modal.Secret.from_local_environ(
[
"HF_TOKEN",
"SUPABASE_URL",
"SUPABASE_SERVICE_ROLE_KEY",
]
)
]
# ── Remote GPU Execution Function ─────────────────────────────────────────────
# Requesting an Nvidia A10G GPU (24GB VRAM) - perfect for QLoRA on 8B/9B models.
# Timeout set to 2 hours (7200s).
@app.function(image=image, gpu="A10G", timeout=7200, secrets=secrets)
def train_llm():
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 Dataset
from supabase import create_client
print("πŸš€ Initializing on-demand GPU environment...")
# 1. Fetch training data from Supabase
supabase_url = os.environ.get("SUPABASE_URL")
supabase_key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
if not supabase_url or not supabase_key:
raise ValueError("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set.")
print("πŸ“‘ Fetching completed support runs from Supabase...")
supabase = create_client(supabase_url, supabase_key)
# Fetch completed triage tickets to use as training ground-truth
response = (
supabase.table("tickets")
.select("raw_text, suggested_resolution")
.eq("status", "complete")
.limit(1000) # Limit for demonstration, can be raised to fetch full dataset
.execute()
)
rows = response.data
if not rows or len(rows) < 10:
print(f"❌ Insufficient training data (found {len(rows)} rows). Need at least 10 completed runs.")
return
print(f"πŸ“Š Loaded {len(rows)} rows of training data from database.")
# 2. Format dataset for instruction training
# Standard prompt template matching Llama 3 style
def format_prompt(row):
system_prompt = "You are a CustomerCore B2B support agent. Respond to the support ticket professionally."
user_input = row["raw_text"]
response = row["suggested_resolution"]
prompt = (
f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n{system_prompt}<|eot_id|>"
f"<|start_header_id|>user<|end_header_id|>\n\n{user_input}<|eot_id|>"
f"<|start_header_id|>assistant<|end_header_id|>\n\n{response}<|eot_id|>"
)
return {"text": prompt}
formatted_data = [format_prompt(r) for r in rows]
dataset = Dataset.from_list(formatted_data)
print("🧹 Data formatted into instruction training prompt template.")
# 3. Configure 4-bit quantization (QLoRA)
base_model_name = "NousResearch/Meta-Llama-3-8B-Instruct"
print(f"πŸ“₯ Loading base model on GPU: {base_model_name}")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
base_model_name,
quantization_config=bnb_config,
device_map="auto",
)
# Prepare model for k-bit quantization training
model = prepare_model_for_kbit_training(model)
# 4. Apply LoRA configuration
peft_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# Tokenize the dataset
def tokenize_function(examples):
return tokenizer(examples["text"], truncation=True, max_length=512)
tokenized_dataset = dataset.map(tokenize_function, batched=True, remove_columns=["text"])
# 5. Define Training Arguments
training_args = TrainingArguments(
output_dir="./results",
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=100,
max_steps=500, # Adjust depending on dataset size
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_strategy="no",
report_to="none",
)
trainer = Trainer(
model=model,
train_dataset=tokenized_dataset,
args=training_args,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
print("πŸ‹οΈ Starting training loop...")
trainer.train()
print("πŸŽ‰ Training completed successfully!")
# 6. Push custom fine-tuned adapter to Hugging Face
hf_token = os.environ.get("HF_TOKEN")
if not hf_token:
print("⚠️ HF_TOKEN not found in environment. Skipping adapter push.")
return
custom_adapter_name = "customercore-llama3-adapter"
print(f"πŸ“€ Pushing fine-tuned LoRA adapter to Hugging Face: {custom_adapter_name}")
# Push weights and tokenizer
model.push_to_hub(custom_adapter_name, token=hf_token)
tokenizer.push_to_hub(custom_adapter_name, token=hf_token)
print("πŸ† Adapter weights successfully published to Hugging Face Hub!")
# ── Local CLI Entrypoint ──────────────────────────────────────────────────────
@app.local_entrypoint()
def main():
print("⏳ Connecting to Modal serverless runners...")
train_llm.remote()
print("🏁 Execution finished.")