Replaced deepseek with qwq

This commit is contained in:
Niels Geens
2025-03-14 11:22:30 +01:00
parent 3ea3239a9b
commit 2e66020f8e
13 changed files with 693 additions and 530 deletions

View File

@@ -12,4 +12,4 @@ fi
mkdir -p "$OUTPUT_DIR"
train_deepseek --output-dir "$OUTPUT_DIR" --device cpu
python -m train.qwq "$@"

View File

@@ -1,10 +0,0 @@
#!/root/venvs/train/bin/python
"""
Command-line utility for fine-tuning DeepSeek models using Unsloth.
Always trains from a base model to create a new fine-tuned model.
"""
import sys
from train.unsloth_deepseek import main
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,9 +0,0 @@
#!/root/venvs/train/bin/python
"""
Command-line utility for fine-tuning Mistral models using Mistral API.
"""
import sys
from train.mistral_api import main
if __name__ == "__main__":
sys.exit(main())

View File

@@ -5,26 +5,27 @@ setup(
version="0.1.0",
packages=find_packages(),
scripts=[
'bin/train_deepseek',
'bin/train_mistral'
'bin/train'
],
install_requires=[
'pyyaml>=6.0',
'requests>=2.28.0',
'torch>=2.0.0',
'transformers>=4.30.0',
'accelerate>=0.25.0',
'bitsandbytes>=0.41.1',
'einops>=0.7.0',
'sentencepiece>=0.1.99',
'unsloth>=2025.2',
'trl>=0.7.8',
'datasets>=2.14.6',
'peft>=0.8.0',
'pytest>=7.0.0',
'pytest-cov>=4.0.0',
'black>=22.0.0',
'flake8>=4.0.0'
'datasets>=2.14.6',
'einops>=0.7.0',
'flake8>=4.0.0',
'peft>=0.8.0',
'peft>=0.8.0',
'pytest-cov>=4.0.0',
'pytest>=7.0.0',
'pyyaml>=6.0',
'requests>=2.28.0',
'sentencepiece>=0.1.99',
'torch>=2.0.0',
'transformers>=4.30.0',
'trl>=0.7.8',
'unsloth>=2025.2',
],
classifiers=[
'Development Status :: 3 - Alpha',

334
tools/train/train/qwq.py Normal file
View File

@@ -0,0 +1,334 @@
#!/root/venvs/train/bin/python
"""
Fine-tuning script for QwQ models to support SIA's action schema.
Supports both full and LoRA finetuning methods.
"""
import argparse
import os
import sys
import torch
from dataclasses import dataclass
from pathlib import Path
import json
import logging
import gc
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Import from shared library
from .util import prepare_training_data
@dataclass
class Config:
def __init__(self):
parser = argparse.ArgumentParser(description='Train SIA model using QwQ')
parser.add_argument(
'--config',
type=Path,
default=Path('/root/sia/training/config.yaml'),
help='Path to config file'
)
parser.add_argument(
'--base-model',
type=str,
default='Qwen/QwQ-32B',
help='HuggingFace model ID for base model'
)
parser.add_argument(
'--output-dir',
type=Path,
required=True,
help='Directory to save the trained model'
)
parser.add_argument(
'--api-key',
type=str,
default=os.environ.get('SIA_HF_API_KEY'),
help='HuggingFace API key'
)
parser.add_argument(
'--method',
type=str,
choices=['lora', 'qlora', 'full'],
default='qlora',
help='Finetuning method: LoRA, QLoRA (quantized LoRA), or full-model'
)
parser.add_argument(
'--device',
type=str,
default='auto',
help='Override device (cpu, cuda, auto) from config'
)
self.args = parser.parse_args()
@property
def config_path(self) -> Path:
return self.args.config
@property
def base_model(self) -> str:
return self.args.base_model
@property
def output_dir(self) -> Path:
return self.args.output_dir
@property
def api_key(self) -> str:
return self.args.api_key
@property
def device(self) -> str:
return self.args.device
@property
def method(self) -> str:
return self.args.method
def format_data_for_qwq(training_data):
"""
Format training data for QwQ model focusing on action schema formats.
Ensures each example shows the model how to directly use action elements.
"""
formatted_data = []
for sample in training_data:
# Get the system prompt, context, and response
system_content = ""
context_content = ""
response_content = ""
for message in sample.get("messages", []):
if message["role"] == "system":
system_content = message["content"]
elif message["role"] == "user":
context_content = message["content"]
elif message["role"] == "assistant":
response_content = message["content"]
# Create conversations with explicit instruction to use action schema
formatted_data.append({
"conversations": [
{"role": "system", "content": system_content},
{"role": "user", "content": context_content},
{"role": "assistant", "content": response_content}
]
})
logger.info(f"Formatted {len(formatted_data)} examples for QwQ training")
return formatted_data
def train_model_lora(config, training_data, train_params):
"""
Train QwQ model using LoRA or QLoRA for parameter-efficient fine-tuning.
This is the recommended approach for most use cases.
"""
try:
# Import required libraries
from transformers import (
AutoModelForCausalLM, AutoTokenizer,
TrainingArguments, DataCollatorForSeq2Seq
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import Dataset
from trl import SFTTrainer
except ImportError as e:
logger.error(f"Error importing required libraries: {e}")
logger.error("Please ensure transformers, peft, and trl are installed.")
sys.exit(1)
# Format data specifically for QwQ
formatted_data = format_data_for_qwq(training_data)
dataset = Dataset.from_list(formatted_data)
logger.info(f"Starting QwQ fine-tuning using {config.method}")
logger.info(f"Base model: {config.base_model}")
logger.info(f"Device: {config.device}")
# Configure device mapping and precision
if torch.cuda.is_available() and config.device != "cpu":
logger.info("Using GPU for training")
device_map = "auto"
# Configure precision based on method
if config.method == "qlora":
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
load_in_4bit = True
load_in_8bit = False
else:
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
load_in_4bit = False
load_in_8bit = False
else:
logger.info("Using CPU for training")
device_map = "cpu"
dtype = torch.float32
load_in_4bit = False
load_in_8bit = False
# Configure quantization for QLoRA
if config.method == "qlora":
from transformers import BitsAndBytesConfig
logger.info("Setting up 4-bit quantization for QLoRA")
compute_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=True
)
else:
bnb_config = None
# Load tokenizer
logger.info(f"Loading tokenizer from {config.base_model}")
tokenizer = AutoTokenizer.from_pretrained(
config.base_model,
token=config.api_key,
trust_remote_code=True
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# Load model
logger.info(f"Loading model from {config.base_model}")
model = AutoModelForCausalLM.from_pretrained(
config.base_model,
torch_dtype=dtype,
device_map=device_map,
quantization_config=bnb_config,
token=config.api_key,
trust_remote_code=True
)
# Configure LoRA
if config.method in ["lora", "qlora"]:
if config.method == "qlora":
model = prepare_model_for_kbit_training(model)
logger.info("Setting up LoRA configuration")
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=target_modules,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# Create output directory
config.output_dir.mkdir(parents=True, exist_ok=True)
# Configure training arguments
batch_size = train_params.per_device_batch_size
gradient_accumulation = train_params.gradient_accumulation_steps
# Scale down batch size based on model
if "32B" in config.base_model and batch_size > 1:
batch_size = 1
gradient_accumulation *= 2
training_args = TrainingArguments(
output_dir=str(config.output_dir),
per_device_train_batch_size=batch_size,
gradient_accumulation_steps=gradient_accumulation,
learning_rate=train_params.learning_rate,
num_train_epochs=train_params.epochs,
logging_steps=10,
save_strategy="epoch",
save_total_limit=2,
fp16=dtype == torch.float16,
bf16=dtype == torch.bfloat16,
report_to="none",
remove_unused_columns=False,
optim="adamw_torch",
weight_decay=0.01,
max_grad_norm=0.3,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
seed=42
)
# Set up trainer
logger.info("Setting up trainer")
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
max_seq_length=train_params.max_seq_length,
dataset_text_field="conversations",
packing=False
)
# Start training
logger.info("Starting training")
trainer.train()
# Save the final model
logger.info(f"Saving model to {config.output_dir}")
trainer.save_model(config.output_dir)
tokenizer.save_pretrained(config.output_dir)
# Create metadata file
with open(config.output_dir / "training_info.json", "w") as f:
json.dump({
"base_model": config.base_model,
"method": config.method,
"learning_rate": train_params.learning_rate,
"epochs": train_params.epochs,
"dataset_size": len(dataset),
"batch_size": batch_size,
"gradient_accumulation": gradient_accumulation
}, f, indent=2)
logger.info("Training complete!")
return True
def main():
# Initialize configuration
config = Config()
# Prepare training data from config
training_data, train_params = prepare_training_data(config.config_path)
if not training_data:
logger.error("No valid training data found. Exiting.")
return 1
# Force garbage collection
gc.collect()
# Train using appropriate method
if config.method in ["lora", "qlora"]:
success = train_model_lora(config, training_data, train_params)
else:
logger.error(f"Training method '{config.method}' not yet implemented")
return 1
if success:
# Create symlink to current
current_link = Path("/root/models/current")
if os.path.exists(current_link) or os.path.islink(current_link):
os.unlink(current_link)
os.symlink(config.output_dir, current_link, target_is_directory=True)
logger.info(f"Training complete. Model saved to {config.output_dir}")
logger.info(f"Symlink created at {current_link}")
return 0
else:
logger.error("Training failed")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,289 +0,0 @@
#!/root/venvs/train/bin/python
"""
Script for fine-tuning DeepSeek models for SIA using Unsloth.
Training always starts from a base model and creates a new fine-tuned model.
"""
import argparse
import os
import sys
import torch
from dataclasses import dataclass
from pathlib import Path
import json
# Import from shared library
from .util import prepare_training_data
@dataclass
class Config:
def __init__(self):
parser = argparse.ArgumentParser(description='Train SIA model using Unsloth')
parser.add_argument(
'--config',
type=Path,
default=Path('/root/sia/training/config.yaml'),
help='Path to config file'
)
parser.add_argument(
'--base-model',
type=str,
default='deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B',
help='HuggingFace model ID for base model'
)
parser.add_argument(
'--output-dir',
type=Path,
required=True,
help='Directory to save the trained model'
)
parser.add_argument(
'--api-key',
type=str,
default=os.environ.get('SIA_HF_API_KEY'),
help='HuggingFace API key'
)
parser.add_argument(
'--device',
type=str,
default='auto',
help='Override device (cpu, cuda, auto) from config'
)
self.args = parser.parse_args()
@property
def config_path(self) -> Path:
return self.args.config
@property
def base_model(self) -> str:
return self.args.base_model
@property
def output_dir(self) -> Path:
return self.args.output_dir
@property
def api_key(self) -> str:
return self.args.api_key
@property
def device(self) -> str:
return self.args.device
def train_model(config: Config, training_data, train_params):
"""Train the model using Unsloth"""
try:
from unsloth import FastLanguageModel
from transformers import TrainingArguments, DataCollatorForSeq2Seq
from trl import SFTTrainer
from datasets import Dataset
from unsloth.chat_templates import get_chat_template, train_on_responses_only
except ImportError as e:
print(f"Error importing required libraries: {e}")
print("Please ensure Unsloth and its dependencies are installed.")
sys.exit(1)
print(f"Starting training from base model: {config.base_model}")
print(f"Using device: {config.device}")
print(f"Training configuration:")
print(f" Max sequence length: {train_params.max_seq_length}")
print(f" Quantization: {train_params.quantization}")
print(f" Batch size: {train_params.per_device_batch_size}")
print(f" Gradient accumulation: {train_params.gradient_accumulation_steps}")
print(f" Mixed precision: {train_params.mixed_precision}")
# Convert to datasets format
dataset = Dataset.from_list(training_data)
# Configure device and dtype
if train_params.mixed_precision == "bf16" and torch.cuda.is_bf16_supported():
dtype = torch.bfloat16
else:
dtype = torch.float16
# Configure quantization settings
load_in_4bit = train_params.quantization == "4bit"
load_in_8bit = train_params.quantization == "8bit"
# Configure device mapping
device_map = config.device
if config.device == "cpu":
# Force CPU even for quantized model
bnb_config = None
# When on CPU, we should disable quantization
load_in_4bit = False
load_in_8bit = False
dtype = torch.float32
print("CPU-only mode: Disabling quantization and using float32")
else:
# Setup quantization config for GPU
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=load_in_4bit,
load_in_8bit=load_in_8bit,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=dtype,
llm_int8_enable_fp32_cpu_offload=True
) if (load_in_4bit or load_in_8bit) else None
# Load the model with appropriate settings
try:
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=config.base_model,
max_seq_length=train_params.max_seq_length,
dtype=dtype,
quantization_config=bnb_config,
device_map=device_map,
token=config.api_key,
)
except Exception as e:
print(f"Error loading base model: {e}")
sys.exit(1)
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
model = FastLanguageModel.get_peft_model(
model,
r=8 if config.device == "cpu" else 16, # Lower rank for CPU to save memory
target_modules=target_modules,
lora_alpha=16,
lora_dropout=0,
bias="none",
# Only use gradient checkpointing for GPU
use_gradient_checkpointing="unsloth" if config.device != "cpu" else None,
random_state=3407,
)
# Function to format conversations
def formatting_prompts_func(examples):
convos = examples["conversations"]
texts = [tokenizer.apply_chat_template(convo, tokenize=False, add_generation_prompt=False) for convo in convos]
return {"text": texts}
# Standardize dataset and format
from unsloth.chat_templates import standardize_sharegpt
# Add conversations field if not present
if "conversations" not in dataset.column_names:
if "messages" in dataset.column_names:
dataset = dataset.rename_column("messages", "conversations")
else:
dataset = dataset.map(lambda x: {"conversations": [{"role": "system", "content": x.get("system_prompt", "")},
{"role": "user", "content": x.get("prompt", "")},
{"role": "assistant", "content": x.get("response", "")}]})
# Standardize format
dataset = standardize_sharegpt(dataset)
# Apply formatting
dataset = dataset.map(formatting_prompts_func, batched=True)
# Configure the trainer
config.output_dir.mkdir(parents=True, exist_ok=True)
# Determine steps or epochs based on dataset size
max_steps = -1
num_train_epochs = train_params.epochs
if len(dataset) < 100: # Small dataset
# Aim for at least 500 steps for small datasets
max_steps = 500
num_train_epochs = -1
# Configure mixed precision settings
fp16 = train_params.mixed_precision == "fp16"
bf16 = train_params.mixed_precision == "bf16" and torch.cuda.is_bf16_supported()
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=train_params.max_seq_length,
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer),
dataset_num_proc=1 if config.device == "cpu" else 2,
packing=False,
args=TrainingArguments(
per_device_train_batch_size=train_params.per_device_batch_size,
gradient_accumulation_steps=train_params.gradient_accumulation_steps,
warmup_steps=5,
max_steps=max_steps,
num_train_epochs=num_train_epochs,
learning_rate=train_params.learning_rate,
fp16=fp16,
bf16=bf16,
logging_steps=10,
optim="adamw_torch" if config.device == "cpu" else "adamw_8bit",
weight_decay=0.01,
lr_scheduler_type="linear",
seed=3407,
output_dir=str(config.output_dir),
report_to="none",
dataloader_num_workers=0 if config.device == "cpu" else 2,
gradient_checkpointing=config.device != "cpu",
max_grad_norm=0.3,
),
)
# Train only on responses
trainer = train_on_responses_only(
trainer,
instruction_part="<|im_start|>user",
response_part="<|im_start|>assistant",
)
# Train the model
trainer.train()
# Enable inference mode for the model
if config.device != "cpu":
model = FastLanguageModel.for_inference(model)
# Save the model
model.save_pretrained(config.output_dir)
tokenizer.save_pretrained(config.output_dir)
# Create a metadata file with training information
with open(config.output_dir / "training_info.json", "w") as f:
json.dump({
"base_model": config.base_model,
"learning_rate": train_params.learning_rate,
"epochs": train_params.epochs,
"dataset_size": len(dataset),
"device": config.device,
"training_method": "unsloth",
"max_seq_length": train_params.max_seq_length,
"quantization": train_params.quantization,
}, f, indent=2)
def main():
config = Config()
# Prepare training data
training_data, train_params = prepare_training_data(config.config_path)
if not training_data:
print("No valid training data found. Exiting.")
return 1
# Train the model
try:
train_model(config, training_data, train_params)
# Create symlink to current
current_link = Path("/root/models/current")
if os.path.exists(current_link) or os.path.islink(current_link):
os.unlink(current_link)
os.symlink(config.output_dir, current_link, target_is_directory=True)
print(f"Training complete. Model saved to {config.output_dir}")
print(f"Symlink created at {current_link}")
return 0
except Exception as e:
print(f"Error during training: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit(main())