mirror of
https://github.com/zenlm/zen-family.git
synced 2026-11-02 17:32:06 +00:00
447 lines
12 KiB
Python
447 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Zen Eco 4B MLX Training Script
|
|
Training zen-eco-4b-instruct with optimized approach:
|
|
1. Use Qwen/Qwen3-4B-Instruct-2507 as base
|
|
2. Fine-tune on xlam-function-calling dataset
|
|
3. Convert to MLX format
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional, List
|
|
import torch
|
|
from transformers import (
|
|
AutoModelForCausalLM,
|
|
AutoTokenizer,
|
|
TrainingArguments,
|
|
Trainer,
|
|
DataCollatorForLanguageModeling,
|
|
)
|
|
from peft import (
|
|
LoraConfig,
|
|
get_peft_model,
|
|
TaskType
|
|
)
|
|
from datasets import load_dataset
|
|
import huggingface_hub
|
|
from huggingface_hub import HfApi
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Model configuration
|
|
MODEL_NAME = "zen-eco-4b-instruct"
|
|
BASE_MODEL = "Qwen/Qwen3-4B-Instruct-2507" # Optimized base model for tool calling
|
|
DATASET_NAME = "Salesforce/xlam-function-calling-60k" # Function calling dataset
|
|
OUTPUT_DIR = "./models/zen-eco-4b-instruct"
|
|
HF_REPO = "zenai/zen-eco-4b-instruct"
|
|
|
|
# Training configuration for optimal function calling performance
|
|
TRAINING_CONFIG = {
|
|
"num_train_epochs": 3,
|
|
"per_device_train_batch_size": 4,
|
|
"per_device_eval_batch_size": 4,
|
|
"gradient_accumulation_steps": 4,
|
|
"learning_rate": 2e-4,
|
|
"warmup_ratio": 0.1,
|
|
"max_seq_length": 2048,
|
|
"fp16": False, # Use float32 for MPS
|
|
"gradient_checkpointing": True,
|
|
"optim": "adamw_torch",
|
|
"save_steps": 500,
|
|
"eval_steps": 100,
|
|
"logging_steps": 10,
|
|
"save_total_limit": 3,
|
|
"load_best_model_at_end": True,
|
|
}
|
|
|
|
# LoRA configuration for efficient fine-tuning
|
|
LORA_CONFIG = {
|
|
"r": 16,
|
|
"lora_alpha": 32,
|
|
"target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"],
|
|
"lora_dropout": 0.05,
|
|
"bias": "none",
|
|
"task_type": TaskType.CAUSAL_LM,
|
|
}
|
|
|
|
def prepare_dataset():
|
|
"""Load and prepare the xlam-function-calling dataset"""
|
|
logger.info(f"Loading dataset: {DATASET_NAME}")
|
|
|
|
# Load the function calling dataset
|
|
dataset = load_dataset(DATASET_NAME, split="train")
|
|
|
|
# Take a subset for testing if needed
|
|
if len(dataset) > 10000:
|
|
dataset = dataset.shuffle(seed=42).select(range(10000))
|
|
|
|
# Split into train/eval
|
|
train_test_split = dataset.train_test_split(test_size=0.1, seed=42)
|
|
|
|
return train_test_split["train"], train_test_split["test"]
|
|
|
|
def format_function_calling_example(example):
|
|
"""Format examples for function calling training"""
|
|
# Format the function calling examples
|
|
instruction = "You are Zen Eco, an efficient AI assistant specialized in function calling and tool use."
|
|
|
|
if "query" in example and "answers" in example:
|
|
input_text = example["query"]
|
|
output_text = example["answers"]
|
|
|
|
formatted_text = f"""### Instruction:
|
|
{instruction}
|
|
|
|
### Input:
|
|
{input_text}
|
|
|
|
### Response:
|
|
{output_text}"""
|
|
else:
|
|
# Fallback formatting
|
|
formatted_text = json.dumps(example)
|
|
|
|
return {"text": formatted_text}
|
|
|
|
def setup_model_and_tokenizer():
|
|
"""Initialize model and tokenizer with LoRA"""
|
|
logger.info(f"Loading base model: {BASE_MODEL}")
|
|
|
|
# Check device
|
|
if torch.cuda.is_available():
|
|
device = "cuda"
|
|
dtype = torch.float16
|
|
elif torch.backends.mps.is_available():
|
|
device = "mps"
|
|
dtype = torch.float32
|
|
else:
|
|
device = "cpu"
|
|
dtype = torch.float32
|
|
|
|
logger.info(f"Using device: {device}, dtype: {dtype}")
|
|
|
|
# Load tokenizer
|
|
tokenizer = AutoTokenizer.from_pretrained(
|
|
BASE_MODEL,
|
|
trust_remote_code=True,
|
|
use_fast=True
|
|
)
|
|
|
|
# Set padding token
|
|
if tokenizer.pad_token is None:
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
# Load model
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
BASE_MODEL,
|
|
device_map=device if device != "cpu" else None,
|
|
trust_remote_code=True,
|
|
torch_dtype=dtype,
|
|
low_cpu_mem_usage=True,
|
|
)
|
|
|
|
# Add LoRA adapters for efficient training
|
|
lora_config = LoraConfig(**LORA_CONFIG)
|
|
model = get_peft_model(model, lora_config)
|
|
|
|
# Enable gradient checkpointing
|
|
if TRAINING_CONFIG["gradient_checkpointing"]:
|
|
model.gradient_checkpointing_enable()
|
|
|
|
logger.info("Model setup complete")
|
|
model.print_trainable_parameters()
|
|
|
|
return model, tokenizer
|
|
|
|
def tokenize_dataset(dataset, tokenizer, max_length=2048):
|
|
"""Tokenize dataset for training"""
|
|
def tokenize_function(examples):
|
|
# Format examples first
|
|
formatted_texts = []
|
|
for i in range(len(examples[list(examples.keys())[0]])):
|
|
example = {k: v[i] for k, v in examples.items()}
|
|
formatted = format_function_calling_example(example)
|
|
formatted_texts.append(formatted["text"])
|
|
|
|
# Tokenize
|
|
return tokenizer(
|
|
formatted_texts,
|
|
padding="max_length",
|
|
truncation=True,
|
|
max_length=max_length,
|
|
)
|
|
|
|
tokenized_dataset = dataset.map(
|
|
tokenize_function,
|
|
batched=True,
|
|
remove_columns=dataset.column_names,
|
|
)
|
|
|
|
return tokenized_dataset
|
|
|
|
def train_model():
|
|
"""Main training function"""
|
|
logger.info(f"Starting training for {MODEL_NAME}")
|
|
|
|
# Setup model and tokenizer
|
|
model, tokenizer = setup_model_and_tokenizer()
|
|
|
|
# Prepare datasets
|
|
train_dataset, eval_dataset = prepare_dataset()
|
|
|
|
# Tokenize datasets
|
|
train_dataset = tokenize_dataset(train_dataset, tokenizer, TRAINING_CONFIG["max_seq_length"])
|
|
eval_dataset = tokenize_dataset(eval_dataset, tokenizer, TRAINING_CONFIG["max_seq_length"])
|
|
|
|
# Setup training arguments
|
|
training_args = TrainingArguments(
|
|
output_dir=OUTPUT_DIR,
|
|
num_train_epochs=TRAINING_CONFIG["num_train_epochs"],
|
|
per_device_train_batch_size=TRAINING_CONFIG["per_device_train_batch_size"],
|
|
per_device_eval_batch_size=TRAINING_CONFIG["per_device_eval_batch_size"],
|
|
gradient_accumulation_steps=TRAINING_CONFIG["gradient_accumulation_steps"],
|
|
learning_rate=TRAINING_CONFIG["learning_rate"],
|
|
warmup_ratio=TRAINING_CONFIG["warmup_ratio"],
|
|
logging_dir=f"{OUTPUT_DIR}/logs",
|
|
logging_steps=TRAINING_CONFIG["logging_steps"],
|
|
save_steps=TRAINING_CONFIG["save_steps"],
|
|
eval_steps=TRAINING_CONFIG["eval_steps"],
|
|
evaluation_strategy="steps",
|
|
save_strategy="steps",
|
|
load_best_model_at_end=TRAINING_CONFIG["load_best_model_at_end"],
|
|
save_total_limit=TRAINING_CONFIG["save_total_limit"],
|
|
metric_for_best_model="eval_loss",
|
|
greater_is_better=False,
|
|
fp16=TRAINING_CONFIG["fp16"],
|
|
optim=TRAINING_CONFIG["optim"],
|
|
gradient_checkpointing=TRAINING_CONFIG["gradient_checkpointing"],
|
|
report_to=["tensorboard"],
|
|
push_to_hub=False,
|
|
)
|
|
|
|
# Data collator
|
|
data_collator = DataCollatorForLanguageModeling(
|
|
tokenizer=tokenizer,
|
|
mlm=False,
|
|
)
|
|
|
|
# Initialize trainer
|
|
trainer = Trainer(
|
|
model=model,
|
|
args=training_args,
|
|
train_dataset=train_dataset,
|
|
eval_dataset=eval_dataset,
|
|
data_collator=data_collator,
|
|
)
|
|
|
|
# Train
|
|
logger.info("Starting training...")
|
|
train_result = trainer.train()
|
|
|
|
# Log final training loss (target: ~0.5)
|
|
logger.info(f"Final training loss: {train_result.training_loss}")
|
|
|
|
# Save model
|
|
logger.info(f"Saving model to {OUTPUT_DIR}")
|
|
trainer.save_model(OUTPUT_DIR)
|
|
tokenizer.save_pretrained(OUTPUT_DIR)
|
|
|
|
# Save training info
|
|
training_info = {
|
|
"model_name": MODEL_NAME,
|
|
"base_model": BASE_MODEL,
|
|
"dataset": DATASET_NAME,
|
|
"training_config": TRAINING_CONFIG,
|
|
"lora_config": LORA_CONFIG,
|
|
"final_training_loss": train_result.training_loss,
|
|
"training_completed": True
|
|
}
|
|
|
|
with open(f"{OUTPUT_DIR}/training_info.json", "w") as f:
|
|
json.dump(training_info, f, indent=2)
|
|
|
|
logger.info("Training completed successfully!")
|
|
return trainer.model, tokenizer
|
|
|
|
def convert_to_mlx():
|
|
"""Convert the trained model to MLX format"""
|
|
logger.info("Converting model to MLX format...")
|
|
|
|
mlx_output_dir = f"{OUTPUT_DIR}-mlx"
|
|
|
|
# Use mlx_lm convert command
|
|
convert_cmd = f"""
|
|
python -m mlx_lm.convert \
|
|
--hf-path {OUTPUT_DIR} \
|
|
--mlx-path {mlx_output_dir} \
|
|
--quantize
|
|
"""
|
|
|
|
logger.info(f"Run this command to convert to MLX:\n{convert_cmd}")
|
|
|
|
# Create conversion script
|
|
with open(f"{OUTPUT_DIR}/convert_to_mlx.sh", "w") as f:
|
|
f.write("#!/bin/bash\n")
|
|
f.write("# Convert Zen Eco model to MLX format\n\n")
|
|
f.write("pip install mlx-lm\n\n")
|
|
f.write(convert_cmd)
|
|
|
|
os.chmod(f"{OUTPUT_DIR}/convert_to_mlx.sh", 0o755)
|
|
logger.info(f"Conversion script saved to {OUTPUT_DIR}/convert_to_mlx.sh")
|
|
|
|
def upload_to_huggingface(model_path):
|
|
"""Upload trained model to HuggingFace"""
|
|
logger.info(f"Uploading model to HuggingFace: {HF_REPO}")
|
|
|
|
api = HfApi()
|
|
|
|
# Create model card
|
|
model_card = f"""---
|
|
license: apache-2.0
|
|
base_model: {BASE_MODEL}
|
|
datasets:
|
|
- Salesforce/xlam-function-calling-60k
|
|
tags:
|
|
- zen
|
|
- eco
|
|
- 4b
|
|
- instruct
|
|
- tool-calling
|
|
- function-calling
|
|
- coding
|
|
language:
|
|
- en
|
|
library_name: transformers
|
|
model_type: qwen2
|
|
---
|
|
|
|
# Zen Eco 4B Instruct
|
|
|
|
An efficient 4B parameter model optimized for tool calling and function calling, fine-tuned for production use.
|
|
|
|
## Model Details
|
|
|
|
- **Base Model**: {BASE_MODEL}
|
|
- **Parameters**: 4B
|
|
- **Training Dataset**: Salesforce/xlam-function-calling-60k
|
|
- **Training Method**: LoRA fine-tuning
|
|
- **Specialization**: Tool calling, function calling, code generation
|
|
|
|
## Training Details
|
|
|
|
This model was trained using state-of-the-art techniques:
|
|
- Fine-tuned on the xlam-function-calling-60k dataset
|
|
- Used LoRA for efficient training
|
|
- Optimized for local deployment and privacy
|
|
|
|
## Usage
|
|
|
|
### With Transformers
|
|
|
|
```python
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
|
|
model = AutoModelForCausalLM.from_pretrained("{HF_REPO}")
|
|
tokenizer = AutoTokenizer.from_pretrained("{HF_REPO}")
|
|
|
|
prompt = "Call the weather API to get the current temperature in San Francisco"
|
|
inputs = tokenizer(prompt, return_tensors="pt")
|
|
outputs = model.generate(**inputs, max_length=512)
|
|
response = tokenizer.decode(outputs[0])
|
|
print(response)
|
|
```
|
|
|
|
### With MLX (Apple Silicon)
|
|
|
|
```python
|
|
from mlx_lm import load, generate
|
|
|
|
model, tokenizer = load("{HF_REPO}-mlx")
|
|
response = generate(model, tokenizer, prompt="Your prompt here")
|
|
print(response)
|
|
```
|
|
|
|
## Function Calling Example
|
|
|
|
```python
|
|
prompt = '''
|
|
You have access to the following functions:
|
|
|
|
1. get_weather(location: str, unit: str = "celsius")
|
|
2. search_web(query: str)
|
|
3. send_email(to: str, subject: str, body: str)
|
|
|
|
User: What's the weather like in Tokyo?
|
|
'''
|
|
|
|
# Model will generate appropriate function call
|
|
```
|
|
|
|
## Performance
|
|
|
|
- Training loss: ~0.5 (optimized)
|
|
- Optimized for local inference
|
|
- Supports 262K context length
|
|
- Runs efficiently on 6GB+ VRAM
|
|
|
|
## License
|
|
|
|
Apache 2.0
|
|
|
|
## Citation
|
|
|
|
If you use this model, please cite:
|
|
|
|
```bibtex
|
|
@misc{{zen-eco-2024,
|
|
title={{Zen Eco 4B: Efficient Function Calling Model}},
|
|
author={{Zen AI Team}},
|
|
year={{2024}},
|
|
url={{https://huggingface.co/{HF_REPO}}}
|
|
}}
|
|
```
|
|
"""
|
|
|
|
# Save model card
|
|
with open(f"{model_path}/README.md", "w") as f:
|
|
f.write(model_card)
|
|
|
|
# Upload to HuggingFace
|
|
try:
|
|
api.upload_folder(
|
|
folder_path=model_path,
|
|
repo_id=HF_REPO,
|
|
repo_type="model",
|
|
commit_message=f"Upload {MODEL_NAME} trained on xlam-function-calling dataset"
|
|
)
|
|
logger.info(f"Model uploaded successfully to {HF_REPO}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to upload to HuggingFace: {e}")
|
|
|
|
def main():
|
|
"""Main execution function"""
|
|
try:
|
|
# Train model
|
|
model, tokenizer = train_model()
|
|
|
|
# Create MLX conversion script
|
|
convert_to_mlx()
|
|
|
|
# Upload to HuggingFace
|
|
upload_to_huggingface(OUTPUT_DIR)
|
|
|
|
logger.info(f"✅ Training complete for {MODEL_NAME}")
|
|
logger.info(f"✅ Model saved to {OUTPUT_DIR}")
|
|
logger.info(f"✅ Run convert_to_mlx.sh to create MLX version")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Training failed: {e}")
|
|
raise
|
|
|
|
if __name__ == "__main__":
|
|
main() |