GitHub Actions
Automated deployment from GitHub Actions: 7d4014a5c183e96e6393846fe0e1bb702f5bdeb9
9f358ed
Raw
History Blame Contribute Delete
21.5 kB
"""Training pipelines for SFT and DPO.
This module contains the setup and execution logic for fine-tuning language models
using Supervised Fine-Tuning and Direct Preference Optimization via the TRL library.
"""
import ast
import logging
import os
import re
from typing import Any, Tuple
import torch
from datasets import load_dataset
from omegaconf import DictConfig
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from transformers.trainer_utils import get_last_checkpoint
from trl import (
DPOConfig,
DPOTrainer,
GRPOConfig,
GRPOTrainer,
ORPOConfig,
ORPOTrainer,
SFTConfig,
SFTTrainer,
)
logger = logging.getLogger(__name__)
def load_model_and_tokenizer(
model_cfg: DictConfig, is_train: bool = True
) -> Tuple[Any, Any]:
"""Load tokenizer and model with given configuration.
Args:
model_cfg (DictConfig): The Hydra configuration for the model.
is_train (bool): Whether to prepare the model for training
(e.g. enable gradients).
Returns:
Tuple[Any, Any]: A tuple containing the loaded (model, tokenizer).
"""
logger.info(f"Loading model {model_cfg.name}...")
tokenizer = AutoTokenizer.from_pretrained(
model_cfg.name, trust_remote_code=model_cfg.trust_remote_code
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
kwargs = {
"torch_dtype": getattr(torch, model_cfg.torch_dtype),
"trust_remote_code": model_cfg.trust_remote_code,
}
load_in_8bit = model_cfg.get("load_in_8bit", False)
load_in_4bit = model_cfg.get("load_in_4bit", False)
if torch.cuda.is_available():
if model_cfg.device_map == "auto" and (load_in_8bit or load_in_4bit):
from accelerate import Accelerator
kwargs["device_map"] = {"": Accelerator().local_process_index}
else:
kwargs["device_map"] = model_cfg.device_map
if load_in_8bit or load_in_4bit:
compute_dtype = getattr(
torch, model_cfg.get("bnb_4bit_compute_dtype", "bfloat16")
)
quantization_config = BitsAndBytesConfig(
load_in_8bit=load_in_8bit,
load_in_4bit=load_in_4bit,
# QLoRA settings
bnb_4bit_quant_type=model_cfg.get("bnb_4bit_quant_type", "nf4"),
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=model_cfg.get(
"bnb_4bit_use_double_quant", True
),
)
kwargs["quantization_config"] = quantization_config
else:
# Prevent meta device offloading on Mac/CPU which crashes PEFT backward pass
if torch.backends.mps.is_available():
kwargs["device_map"] = {"": "mps"}
else:
kwargs["device_map"] = {"": "cpu"}
if load_in_8bit or load_in_4bit:
logger.warning(
"CUDA is not available. Disabling 8-bit/4-bit quantization as "
"bitsandbytes requires CUDA."
)
model = AutoModelForCausalLM.from_pretrained(model_cfg.name, **kwargs)
if is_train:
model.config.use_cache = False
model.gradient_checkpointing_enable()
# This forces the model to track gradients for the initial inputs so
# the gradients successfully flow backward to your trainable LoRA adapters
if hasattr(model, "enable_input_require_grads"):
model.enable_input_require_grads()
# Apply LoRA
if "lora" in model_cfg:
from peft import PeftModel
if isinstance(model, PeftModel):
logger.info(
"PEFT model detected. Continuing training on existing adapter."
)
# Ensure the existing adapter requires gradients
for name, param in model.named_parameters():
if "lora_" in name:
param.requires_grad = True
else:
lora_cfg = model_cfg.lora
config = LoraConfig(
r=lora_cfg.r,
lora_alpha=lora_cfg.lora_alpha,
lora_dropout=lora_cfg.lora_dropout,
bias=lora_cfg.bias,
task_type=lora_cfg.task_type,
target_modules=list(lora_cfg.target_modules),
)
model = get_peft_model(model, config)
logger.info("Applied LoRA configuration.")
return model, tokenizer
def run_sft_training(cfg: DictConfig):
"""Run Supervised Fine-Tuning."""
logger.info("Initializing SFT Training...")
model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
logger.info(
f"Loading SFT training dataset from {cfg.data.output_sft_train_file}..."
)
dataset_train = load_dataset("json", data_files=cfg.data.output_sft_train_file)[
"train"
]
logger.info(
f"Loading SFT validation dataset from {cfg.data.output_sft_val_file}..."
)
dataset_val = load_dataset("json", data_files=cfg.data.output_sft_val_file)["train"]
# Format text for training
def format_chat(example):
messages = [
{"role": "system", "content": example["instruction"]},
{"role": "user", "content": example["input"]},
{
"role": "assistant",
"content": (
f"Action: {example['output']['action']}\\n"
f"Reasoning: {example['output']['reasoning']}\\n"
f"Facets: {example['output']['facets']}\\n"
f"Response: {example['output']['response']}"
),
},
]
formatted = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
return {"text": formatted}
formatted_train = dataset_train.map(
format_chat, remove_columns=dataset_train.column_names
)
formatted_val = dataset_val.map(
format_chat, remove_columns=dataset_val.column_names
)
training_args = SFTConfig(
output_dir=cfg.training.output_dir,
per_device_train_batch_size=cfg.training.per_device_train_batch_size,
gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
num_train_epochs=cfg.training.num_train_epochs,
learning_rate=cfg.training.learning_rate,
warmup_ratio=cfg.training.warmup_ratio,
bf16=cfg.training.bf16,
eval_strategy="steps",
eval_steps=cfg.training.logging_steps,
logging_steps=cfg.training.logging_steps,
save_steps=cfg.training.save_steps,
save_total_limit=cfg.training.save_total_limit,
optim=cfg.training.optim,
report_to=cfg.training.report_to,
run_name="sft_training",
dataset_text_field="text",
max_seq_length=cfg.training.max_seq_length,
packing=cfg.training.packing,
remove_unused_columns=cfg.training.get("remove_unused_columns", True),
)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=formatted_train,
eval_dataset=formatted_val,
args=training_args,
)
logger.info("Starting SFT Training...")
last_checkpoint = None
if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
cfg.training.output_dir
):
last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
if last_checkpoint is not None:
logger.info(f"Resuming SFT training from {last_checkpoint}")
trainer.train(resume_from_checkpoint=last_checkpoint)
trainer.save_model(f"{cfg.training.output_dir}/final")
tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
logger.info("SFT Training complete and model saved.")
def run_dpo_training(cfg: DictConfig):
"""Run Direct Preference Optimization."""
logger.info("Initializing DPO Training...")
# Load model and reference model
model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
ref_model, _ = load_model_and_tokenizer(
cfg.model, is_train=False
) # Ref model without LoRA adapters trainable
logger.info(
f"Loading DPO training dataset from {cfg.data.output_dpo_train_file}..."
)
dataset_train = load_dataset("json", data_files=cfg.data.output_dpo_train_file)[
"train"
]
logger.info(
f"Loading DPO validation dataset from {cfg.data.output_dpo_val_file}..."
)
dataset_val = load_dataset("json", data_files=cfg.data.output_dpo_val_file)["train"]
# Wrap DPO dataset with ChatML to match SFT
def format_dpo(example):
system_prompt = (
"You are a helpful assistant. "
"Given a question, you must decide whether it is ambiguous or not. "
"Output MUST follow this format:\n"
"Action: Clarify|Answer\n"
"Reasoning: <your reasoning>\n"
"Facets: <list of facets if ambiguous, else empty>\n"
"Response: <clarifying question or direct answer>"
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": example["prompt"]},
]
prompt_str = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
return {
"prompt": prompt_str,
"chosen": example["chosen"] + tokenizer.eos_token,
"rejected": example["rejected"] + tokenizer.eos_token,
}
dataset_train = dataset_train.map(format_dpo)
dataset_val = dataset_val.map(format_dpo)
training_args = DPOConfig(
output_dir=cfg.training.output_dir,
per_device_train_batch_size=cfg.training.per_device_train_batch_size,
gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
num_train_epochs=cfg.training.num_train_epochs,
learning_rate=cfg.training.learning_rate,
warmup_ratio=cfg.training.warmup_ratio,
bf16=cfg.training.bf16,
eval_strategy="steps",
eval_steps=cfg.training.logging_steps,
logging_steps=cfg.training.logging_steps,
save_steps=cfg.training.save_steps,
save_total_limit=cfg.training.save_total_limit,
optim=cfg.training.optim,
report_to=cfg.training.report_to,
run_name="dpo_training",
beta=cfg.training.beta,
max_prompt_length=cfg.training.max_prompt_length,
max_length=cfg.training.max_length,
remove_unused_columns=cfg.training.get("remove_unused_columns", False),
)
trainer = DPOTrainer(
model=model,
ref_model=ref_model,
args=training_args,
train_dataset=dataset_train,
eval_dataset=dataset_val,
tokenizer=tokenizer,
)
logger.info("Starting DPO Training...")
last_checkpoint = None
if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
cfg.training.output_dir
):
last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
if last_checkpoint is not None:
logger.info(f"Resuming DPO training from {last_checkpoint}")
trainer.train(resume_from_checkpoint=last_checkpoint)
trainer.save_model(f"{cfg.training.output_dir}/final")
tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
logger.info("DPO Training complete and model saved.")
def run_orpo_training(cfg: DictConfig):
"""Run Odds Ratio Preference Optimization."""
logger.info("Initializing ORPO Training...")
# Load model (NO reference model needed for ORPO)
model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
logger.info(
f"Loading ORPO training dataset from {cfg.data.output_dpo_train_file}..."
)
dataset_train = load_dataset("json", data_files=cfg.data.output_dpo_train_file)[
"train"
]
logger.info(
f"Loading ORPO validation dataset from {cfg.data.output_dpo_val_file}..."
)
dataset_val = load_dataset("json", data_files=cfg.data.output_dpo_val_file)["train"]
# Wrap DPO dataset with ChatML to match SFT
def format_dpo(example):
system_prompt = (
"You are a helpful assistant. "
"Given a question, you must decide whether it is ambiguous or not. "
"Output MUST follow this format:\n"
"Action: Clarify|Answer\n"
"Reasoning: <your reasoning>\n"
"Facets: <list of facets if ambiguous, else empty>\n"
"Response: <clarifying question or direct answer>"
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": example["prompt"]},
]
prompt_str = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
return {
"prompt": prompt_str,
"chosen": example["chosen"] + tokenizer.eos_token,
"rejected": example["rejected"] + tokenizer.eos_token,
}
dataset_train = dataset_train.map(format_dpo)
dataset_val = dataset_val.map(format_dpo)
training_args = ORPOConfig(
output_dir=cfg.training.output_dir,
per_device_train_batch_size=cfg.training.per_device_train_batch_size,
gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
num_train_epochs=cfg.training.num_train_epochs,
learning_rate=cfg.training.learning_rate,
warmup_ratio=cfg.training.warmup_ratio,
bf16=cfg.training.bf16,
eval_strategy="steps",
eval_steps=cfg.training.logging_steps,
logging_steps=cfg.training.logging_steps,
save_steps=cfg.training.save_steps,
save_total_limit=cfg.training.save_total_limit,
optim=cfg.training.optim,
report_to=cfg.training.report_to,
run_name="orpo_training",
beta=cfg.training.beta,
max_prompt_length=cfg.training.max_prompt_length,
max_length=cfg.training.max_length,
remove_unused_columns=cfg.training.get("remove_unused_columns", False),
)
trainer = ORPOTrainer(
model=model,
args=training_args,
train_dataset=dataset_train,
eval_dataset=dataset_val,
tokenizer=tokenizer,
)
logger.info("Starting ORPO Training...")
last_checkpoint = None
if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
cfg.training.output_dir
):
last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
if last_checkpoint is not None:
logger.info(f"Resuming ORPO training from {last_checkpoint}")
trainer.train(resume_from_checkpoint=last_checkpoint)
trainer.save_model(f"{cfg.training.output_dir}/final")
tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
logger.info("ORPO Training complete and model saved.")
def format_reward_func(prompts, completions, **kwargs):
"""Reward function that checks for the exact format constraints."""
rewards = []
for completion in completions:
# A simple check: do we have all the sections in order?
text = completion[0]["content"] if isinstance(completion, list) else completion
has_action = "Action:" in text
has_reasoning = "Reasoning:" in text
has_facets = "Facets:" in text
has_response = "Response:" in text
if has_action and has_reasoning and has_facets and has_response:
rewards.append(1.0)
else:
rewards.append(-1.0)
return rewards
def action_reward_func(prompts, completions, **kwargs):
"""Reward function that checks if the predicted action matches the target."""
rewards = []
target_actions = kwargs.get("target_action", [])
for i, completion in enumerate(completions):
text = completion[0]["content"] if isinstance(completion, list) else completion
target = target_actions[i]
action_match = re.search(r"Action:\s*(Clarify|Answer)", text)
if action_match:
pred_action = action_match.group(1)
if pred_action == target:
rewards.append(1.0)
else:
rewards.append(-1.0)
else:
rewards.append(-1.0)
return rewards
def facet_logic_reward_func(prompts, completions, **kwargs):
"""Reward function that checks facet presence/absence based on action."""
rewards = []
for completion in completions:
text = completion[0]["content"] if isinstance(completion, list) else completion
action_match = re.search(r"Action:\s*(Clarify|Answer)", text)
facets_match = re.search(r"Facets:\s*(\[.*?\])", text, re.DOTALL)
if not action_match or not facets_match:
rewards.append(-0.5)
continue
pred_action = action_match.group(1)
facets_str = facets_match.group(1)
try:
facets = ast.literal_eval(facets_str)
if not isinstance(facets, list):
facets = []
except Exception:
facets = []
if pred_action == "Clarify":
# Clarify MUST have non-empty facets
if len(facets) > 0:
rewards.append(0.5)
else:
rewards.append(-0.5)
else:
# Answer MUST have empty facets
if len(facets) == 0:
rewards.append(0.5)
else:
rewards.append(-0.5)
return rewards
def run_grpo_training(cfg: DictConfig):
"""Run Group Relative Policy Optimization."""
logger.info("Initializing GRPO Training...")
model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
logger.info(
f"Loading GRPO training dataset from {cfg.data.output_dpo_train_file}..."
)
dataset_train = load_dataset("json", data_files=cfg.data.output_dpo_train_file)[
"train"
]
logger.info(
f"Loading GRPO validation dataset from {cfg.data.output_dpo_val_file}..."
)
dataset_val = load_dataset("json", data_files=cfg.data.output_dpo_val_file)["train"]
def format_grpo(example):
system_prompt = (
"You are a helpful assistant. "
"Given a question, you must decide whether it is ambiguous or not. "
"Output MUST follow this format:\n"
"Action: Clarify|Answer\n"
"Reasoning: <your reasoning>\n"
"Facets: <list of facets if ambiguous, else empty>\n"
"Response: <clarifying question or direct answer>"
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": example["prompt"]},
]
prompt_str = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
# Extract the target action from the 'chosen' string in the DPO dataset
target_action = "Answer"
if "Action: Clarify" in example["chosen"]:
target_action = "Clarify"
return {
"prompt": prompt_str,
"target_action": target_action,
}
dataset_train = dataset_train.map(
format_grpo, remove_columns=dataset_train.column_names
)
dataset_val = dataset_val.map(format_grpo, remove_columns=dataset_val.column_names)
training_args = GRPOConfig(
output_dir=cfg.training.output_dir,
per_device_train_batch_size=cfg.training.per_device_train_batch_size,
gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
num_train_epochs=cfg.training.num_train_epochs,
learning_rate=cfg.training.learning_rate,
warmup_ratio=cfg.training.warmup_ratio,
bf16=cfg.training.bf16,
logging_steps=cfg.training.logging_steps,
save_steps=cfg.training.save_steps,
save_total_limit=cfg.training.save_total_limit,
optim=cfg.training.optim,
report_to=cfg.training.report_to,
run_name="grpo_training",
beta=cfg.training.beta,
max_prompt_length=cfg.training.max_prompt_length,
max_completion_length=cfg.training.max_completion_length,
num_generations=cfg.training.num_generations,
loss_type=cfg.training.get("loss_type", "grpo"),
)
trainer = GRPOTrainer(
model=model,
reward_funcs=[format_reward_func, action_reward_func, facet_logic_reward_func],
args=training_args,
train_dataset=dataset_train,
eval_dataset=dataset_val,
processing_class=tokenizer,
)
logger.info("Starting GRPO Training...")
last_checkpoint = None
if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
cfg.training.output_dir
):
last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
if last_checkpoint is not None:
logger.info(f"Resuming GRPO training from {last_checkpoint}")
trainer.train(resume_from_checkpoint=last_checkpoint)
trainer.save_model(f"{cfg.training.output_dir}/final")
tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
logger.info("GRPO Training complete and model saved.")