mirror of
https://github.com/zenlm/zen-trainer.git
synced 2026-10-30 14:57:38 +00:00
308 lines
9.1 KiB
Python
308 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Zen Coder 4B Training Script
|
|
Fine-tune Qwen3-4B-Instruct on zen-agentic-dataset using MLX
|
|
|
|
Hardware: M1 Max 64GB
|
|
Expected time: ~2 days for 3.35M samples
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add zen_trainer to path
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print("Zen Coder 4B Training - MLX Backend")
|
|
print("=" * 60)
|
|
|
|
# Check if running on Apple Silicon
|
|
import platform
|
|
if platform.processor() != 'arm':
|
|
print("WARNING: Not running on Apple Silicon, MLX may not work")
|
|
|
|
# Configuration
|
|
MODEL_ID = "Qwen/Qwen3-4B-Instruct-2507"
|
|
DATASET_PATH = "/Users/z/work/zen/zen-agentic-dataset"
|
|
OUTPUT_DIR = "/Users/z/work/zen/zen-trainer/output/zen-coder-4b"
|
|
|
|
# Training params from models.py
|
|
LORA_R = 64
|
|
LORA_ALPHA = 128
|
|
BATCH_SIZE = 4
|
|
LEARNING_RATE = 2e-4
|
|
EPOCHS = 2
|
|
MAX_SEQ_LENGTH = 4096
|
|
|
|
print(f"\nModel: {MODEL_ID}")
|
|
print(f"Dataset: {DATASET_PATH}")
|
|
print(f"Output: {OUTPUT_DIR}")
|
|
print(f"LoRA r={LORA_R}, alpha={LORA_ALPHA}")
|
|
print(f"Batch size: {BATCH_SIZE}, LR: {LEARNING_RATE}")
|
|
print(f"Epochs: {EPOCHS}, Max seq length: {MAX_SEQ_LENGTH}")
|
|
|
|
# Create output dir
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
try:
|
|
import mlx.core as mx
|
|
from importlib.metadata import version
|
|
mlx_version = version("mlx")
|
|
print(f"\nMLX version: {mlx_version}")
|
|
print(f"Metal available: {mx.metal.is_available()}")
|
|
except ImportError:
|
|
print("\nMLX not available, falling back to transformers...")
|
|
return train_with_transformers(
|
|
MODEL_ID, DATASET_PATH, OUTPUT_DIR,
|
|
LORA_R, LORA_ALPHA, BATCH_SIZE, LEARNING_RATE, EPOCHS, MAX_SEQ_LENGTH
|
|
)
|
|
|
|
# MLX training
|
|
return train_with_mlx(
|
|
MODEL_ID, DATASET_PATH, OUTPUT_DIR,
|
|
LORA_R, LORA_ALPHA, BATCH_SIZE, LEARNING_RATE, EPOCHS, MAX_SEQ_LENGTH
|
|
)
|
|
|
|
|
|
def train_with_mlx(model_id, dataset_path, output_dir, lora_r, lora_alpha,
|
|
batch_size, lr, epochs, max_seq_length):
|
|
"""Train using MLX (Apple Silicon optimized)."""
|
|
from mlx_lm import load
|
|
from mlx_lm.tuner import train as mlx_train
|
|
from mlx_lm.tuner.trainer import TrainingArgs
|
|
from mlx_lm.tuner.utils import linear_to_lora_layers
|
|
import mlx.core as mx
|
|
import mlx.optimizers as optim
|
|
import json
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Loading model with MLX...")
|
|
print("=" * 60)
|
|
|
|
model, tokenizer = load(model_id)
|
|
|
|
# Convert to LoRA
|
|
print(f"\nApplying LoRA (r={lora_r}, alpha={lora_alpha})...")
|
|
lora_config = {
|
|
"rank": lora_r,
|
|
"alpha": lora_alpha,
|
|
"dropout": 0.05,
|
|
"scale": lora_alpha / lora_r,
|
|
}
|
|
# Apply LoRA to last 32 layers (for 4B model)
|
|
linear_to_lora_layers(model, num_layers=32, config=lora_config)
|
|
|
|
# Count trainable params
|
|
trainable = sum(p.size for n, p in model.trainable_parameters().items())
|
|
total = sum(p.size for n, p in model.parameters().items())
|
|
print(f"Trainable parameters: {trainable:,} / {total:,} ({100*trainable/total:.2f}%)")
|
|
|
|
# Load dataset
|
|
print(f"\nLoading dataset from {dataset_path}...")
|
|
train_data = []
|
|
train_file = Path(dataset_path) / "train.jsonl"
|
|
|
|
# Load samples (limit for initial test)
|
|
max_samples = 10000 # Start with 10K for testing, then scale up
|
|
with open(train_file, 'r') as f:
|
|
for i, line in enumerate(f):
|
|
if i >= max_samples:
|
|
break
|
|
try:
|
|
item = json.loads(line)
|
|
# Format for chat
|
|
if 'messages' in item:
|
|
text = tokenizer.apply_chat_template(
|
|
item['messages'],
|
|
tokenize=False,
|
|
add_generation_prompt=False
|
|
)
|
|
elif 'content' in item:
|
|
text = item['content']
|
|
else:
|
|
text = str(item)
|
|
train_data.append({"text": text})
|
|
except:
|
|
continue
|
|
|
|
print(f"Loaded {len(train_data)} training samples")
|
|
|
|
# Save formatted data
|
|
formatted_path = Path(output_dir) / "train_formatted.jsonl"
|
|
with open(formatted_path, 'w') as f:
|
|
for item in train_data:
|
|
f.write(json.dumps(item) + '\n')
|
|
|
|
# Create optimizer
|
|
num_iters = len(train_data) // batch_size * epochs
|
|
optimizer = optim.AdamW(learning_rate=lr)
|
|
|
|
# Training args
|
|
args = TrainingArgs(
|
|
batch_size=batch_size,
|
|
iters=num_iters,
|
|
steps_per_report=10,
|
|
steps_per_eval=100,
|
|
steps_per_save=500,
|
|
max_seq_length=max_seq_length,
|
|
adapter_file=os.path.join(output_dir, "adapters.safetensors"),
|
|
grad_checkpoint=True,
|
|
)
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Starting MLX training...")
|
|
print(f"Total iterations: {args.iters}")
|
|
print(f"Batch size: {batch_size}, LR: {lr}")
|
|
print("=" * 60)
|
|
|
|
# Load datasets for train function
|
|
from mlx_lm.tuner.datasets import load_dataset as mlx_load_dataset
|
|
train_set = mlx_load_dataset(str(formatted_path), tokenizer)
|
|
|
|
mlx_train(
|
|
model=model,
|
|
optimizer=optimizer,
|
|
train_dataset=train_set,
|
|
val_dataset=train_set, # Use same for validation initially
|
|
args=args,
|
|
)
|
|
|
|
# Save tokenizer
|
|
tokenizer.save_pretrained(output_dir)
|
|
print(f"\nTraining complete! Model saved to {output_dir}")
|
|
return 0
|
|
|
|
|
|
def train_with_transformers(model_id, dataset_path, output_dir, lora_r, lora_alpha,
|
|
batch_size, lr, epochs, max_seq_length):
|
|
"""Fallback training using transformers + PEFT."""
|
|
import torch
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
|
|
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
|
from trl import SFTTrainer
|
|
from datasets import load_dataset
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Loading model with transformers...")
|
|
print("=" * 60)
|
|
|
|
# Determine device
|
|
if torch.backends.mps.is_available():
|
|
device = "mps"
|
|
dtype = torch.float16
|
|
elif torch.cuda.is_available():
|
|
device = "cuda"
|
|
dtype = torch.bfloat16
|
|
else:
|
|
device = "cpu"
|
|
dtype = torch.float32
|
|
|
|
print(f"Device: {device}, dtype: {dtype}")
|
|
|
|
# Load tokenizer
|
|
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
|
if tokenizer.pad_token is None:
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
# Load model
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
model_id,
|
|
torch_dtype=dtype,
|
|
device_map="auto" if device != "mps" else None,
|
|
trust_remote_code=True,
|
|
)
|
|
|
|
if device == "mps":
|
|
model = model.to(device)
|
|
|
|
# Prepare for training
|
|
model = prepare_model_for_kbit_training(model)
|
|
|
|
# LoRA config
|
|
lora_config = LoraConfig(
|
|
r=lora_r,
|
|
lora_alpha=lora_alpha,
|
|
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
|
|
lora_dropout=0.05,
|
|
bias="none",
|
|
task_type="CAUSAL_LM",
|
|
)
|
|
model = get_peft_model(model, lora_config)
|
|
model.print_trainable_parameters()
|
|
|
|
# Load dataset
|
|
print(f"\nLoading dataset from {dataset_path}...")
|
|
dataset = load_dataset(
|
|
"json",
|
|
data_files={"train": f"{dataset_path}/train.jsonl"},
|
|
split="train",
|
|
streaming=True, # Stream large dataset
|
|
).take(10000) # Start with 10K for testing
|
|
|
|
def format_func(examples):
|
|
texts = []
|
|
for item in examples:
|
|
if 'messages' in item:
|
|
text = tokenizer.apply_chat_template(
|
|
item['messages'],
|
|
tokenize=False,
|
|
add_generation_prompt=False
|
|
)
|
|
elif 'content' in item:
|
|
text = item['content']
|
|
else:
|
|
text = str(item)
|
|
texts.append(text)
|
|
return {"text": texts}
|
|
|
|
# Training
|
|
training_args = TrainingArguments(
|
|
output_dir=output_dir,
|
|
per_device_train_batch_size=batch_size,
|
|
gradient_accumulation_steps=4,
|
|
warmup_ratio=0.05,
|
|
num_train_epochs=epochs,
|
|
learning_rate=lr,
|
|
fp16=dtype == torch.float16,
|
|
bf16=dtype == torch.bfloat16,
|
|
logging_steps=10,
|
|
save_steps=500,
|
|
save_total_limit=3,
|
|
optim="adamw_torch",
|
|
weight_decay=0.01,
|
|
lr_scheduler_type="cosine",
|
|
seed=42,
|
|
report_to="tensorboard",
|
|
gradient_checkpointing=True,
|
|
max_grad_norm=1.0,
|
|
)
|
|
|
|
trainer = SFTTrainer(
|
|
model=model,
|
|
tokenizer=tokenizer,
|
|
train_dataset=list(dataset),
|
|
dataset_text_field="text",
|
|
max_seq_length=max_seq_length,
|
|
args=training_args,
|
|
)
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Starting training...")
|
|
print("=" * 60)
|
|
|
|
trainer.train()
|
|
|
|
# Save
|
|
model.save_pretrained(output_dir)
|
|
tokenizer.save_pretrained(output_dir)
|
|
|
|
print(f"\nTraining complete! Model saved to {output_dir}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|