mirror of
https://github.com/zenlm/zen-family.git
synced 2026-10-30 14:58:06 +00:00
332 lines
9.7 KiB
Python
Executable File
332 lines
9.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Zen Models MLX Conversion Pipeline
|
|
Optimized for Apple Silicon (M1/M2/M3/M4)
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Optional, Dict, Any
|
|
import logging
|
|
|
|
from mlx_lm import convert, generate
|
|
from huggingface_hub import snapshot_download
|
|
import mlx.core as mx
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Model configurations
|
|
MODELS = {
|
|
"zen-nano-instruct": {
|
|
"hf_path": "shanin/zen-nano-instruct",
|
|
"size": "4B",
|
|
"recommended_quant": [4, 8],
|
|
"supports_lora": True
|
|
},
|
|
"zen-nano-thinking": {
|
|
"hf_path": "shanin/zen-nano-thinking",
|
|
"size": "4B",
|
|
"recommended_quant": [4, 8],
|
|
"supports_lora": True
|
|
},
|
|
"zen-omni": {
|
|
"hf_path": "shanin/zen-omni",
|
|
"size": "30B",
|
|
"recommended_quant": [4],
|
|
"supports_lora": True
|
|
},
|
|
"zen-omni-thinking": {
|
|
"hf_path": "shanin/zen-omni-thinking",
|
|
"size": "30B",
|
|
"recommended_quant": [4],
|
|
"supports_lora": True
|
|
},
|
|
"zen-omni-captioner": {
|
|
"hf_path": "shanin/zen-omni-captioner",
|
|
"size": "30B",
|
|
"recommended_quant": [4],
|
|
"supports_lora": False
|
|
},
|
|
"zen-coder": {
|
|
"hf_path": "shanin/zen-coder",
|
|
"size": "7B",
|
|
"recommended_quant": [4, 8],
|
|
"supports_lora": True
|
|
},
|
|
"zen-next": {
|
|
"hf_path": "shanin/zen-next",
|
|
"size": "13B",
|
|
"recommended_quant": [4, 8],
|
|
"supports_lora": True
|
|
}
|
|
}
|
|
|
|
class ZenMLXConverter:
|
|
"""Convert Zen models to MLX format optimized for Apple Silicon"""
|
|
|
|
def __init__(self, cache_dir: Optional[Path] = None):
|
|
self.cache_dir = cache_dir or Path.home() / ".cache" / "zen-mlx"
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
self.models_dir = Path("models")
|
|
self.models_dir.mkdir(exist_ok=True)
|
|
|
|
def download_model(self, model_name: str) -> Path:
|
|
"""Download model from HuggingFace Hub"""
|
|
if model_name not in MODELS:
|
|
raise ValueError(f"Unknown model: {model_name}")
|
|
|
|
config = MODELS[model_name]
|
|
logger.info(f"Downloading {model_name} from {config['hf_path']}")
|
|
|
|
model_path = snapshot_download(
|
|
repo_id=config['hf_path'],
|
|
cache_dir=self.cache_dir,
|
|
resume_download=True
|
|
)
|
|
|
|
return Path(model_path)
|
|
|
|
def convert_model(
|
|
self,
|
|
model_name: str,
|
|
quantize: bool = True,
|
|
q_bits: int = 4,
|
|
q_group_size: int = 64,
|
|
force: bool = False
|
|
) -> Path:
|
|
"""Convert HuggingFace model to MLX format"""
|
|
|
|
if model_name not in MODELS:
|
|
raise ValueError(f"Unknown model: {model_name}")
|
|
|
|
config = MODELS[model_name]
|
|
|
|
# Setup paths
|
|
quant_suffix = f"-{q_bits}bit" if quantize else ""
|
|
output_dir = self.models_dir / f"{model_name}{quant_suffix}-mlx"
|
|
|
|
if output_dir.exists() and not force:
|
|
logger.info(f"Model already exists at {output_dir}. Use --force to re-convert")
|
|
return output_dir
|
|
|
|
# Download model
|
|
hf_path = self.download_model(model_name)
|
|
|
|
# Convert to MLX
|
|
logger.info(f"Converting {model_name} to MLX format")
|
|
logger.info(f"Quantization: {q_bits}-bit" if quantize else "No quantization")
|
|
|
|
convert_args = {
|
|
"hf_path": str(hf_path),
|
|
"mlx_path": str(output_dir),
|
|
"quantize": quantize
|
|
}
|
|
|
|
if quantize:
|
|
convert_args.update({
|
|
"q_bits": q_bits,
|
|
"q_group_size": q_group_size
|
|
})
|
|
|
|
# Perform conversion
|
|
convert.convert(**convert_args)
|
|
|
|
# Save metadata
|
|
metadata = {
|
|
"model_name": model_name,
|
|
"original_size": config["size"],
|
|
"quantized": quantize,
|
|
"quantization_bits": q_bits if quantize else None,
|
|
"quantization_group_size": q_group_size if quantize else None,
|
|
"supports_lora": config["supports_lora"],
|
|
"hf_source": config["hf_path"]
|
|
}
|
|
|
|
with open(output_dir / "metadata.json", "w") as f:
|
|
json.dump(metadata, f, indent=2)
|
|
|
|
logger.info(f"Model converted successfully to {output_dir}")
|
|
|
|
# Estimate memory usage
|
|
self._estimate_memory(model_name, quantize, q_bits)
|
|
|
|
return output_dir
|
|
|
|
def _estimate_memory(self, model_name: str, quantized: bool, q_bits: int):
|
|
"""Estimate memory usage for the model"""
|
|
config = MODELS[model_name]
|
|
size_gb = float(config["size"].rstrip("B"))
|
|
|
|
if quantized:
|
|
# Rough estimation
|
|
memory_gb = size_gb * (q_bits / 16)
|
|
else:
|
|
memory_gb = size_gb * 2 # FP16
|
|
|
|
logger.info(f"Estimated memory usage: ~{memory_gb:.1f} GB")
|
|
|
|
# Check Apple Silicon unified memory
|
|
try:
|
|
import subprocess
|
|
result = subprocess.run(
|
|
["sysctl", "hw.memsize"],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
if result.returncode == 0:
|
|
mem_bytes = int(result.stdout.split(":")[1].strip())
|
|
mem_gb = mem_bytes / (1024**3)
|
|
logger.info(f"Available unified memory: {mem_gb:.1f} GB")
|
|
|
|
if memory_gb > mem_gb * 0.8:
|
|
logger.warning("Model may use more than 80% of available memory")
|
|
except Exception:
|
|
pass
|
|
|
|
def create_lora_adapter(
|
|
self,
|
|
model_name: str,
|
|
lora_rank: int = 16,
|
|
lora_alpha: float = 32.0,
|
|
target_modules: Optional[list] = None
|
|
) -> Dict[str, Any]:
|
|
"""Create LoRA adapter configuration for fine-tuning"""
|
|
|
|
if model_name not in MODELS:
|
|
raise ValueError(f"Unknown model: {model_name}")
|
|
|
|
if not MODELS[model_name]["supports_lora"]:
|
|
raise ValueError(f"Model {model_name} does not support LoRA")
|
|
|
|
config = {
|
|
"rank": lora_rank,
|
|
"alpha": lora_alpha,
|
|
"dropout": 0.05,
|
|
"target_modules": target_modules or ["q_proj", "v_proj", "k_proj", "o_proj"]
|
|
}
|
|
|
|
adapter_dir = self.models_dir / f"{model_name}-lora"
|
|
adapter_dir.mkdir(exist_ok=True)
|
|
|
|
with open(adapter_dir / "adapter_config.json", "w") as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
logger.info(f"LoRA adapter configuration saved to {adapter_dir}")
|
|
return config
|
|
|
|
def batch_convert(self, models: Optional[list] = None, q_bits: int = 4):
|
|
"""Convert multiple models in batch"""
|
|
target_models = models or list(MODELS.keys())
|
|
|
|
results = {}
|
|
for model_name in target_models:
|
|
try:
|
|
logger.info(f"\nConverting {model_name}...")
|
|
output_path = self.convert_model(
|
|
model_name=model_name,
|
|
quantize=True,
|
|
q_bits=q_bits
|
|
)
|
|
results[model_name] = {
|
|
"status": "success",
|
|
"path": str(output_path)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to convert {model_name}: {e}")
|
|
results[model_name] = {
|
|
"status": "failed",
|
|
"error": str(e)
|
|
}
|
|
|
|
# Save conversion report
|
|
with open("conversion_report.json", "w") as f:
|
|
json.dump(results, f, indent=2)
|
|
|
|
return results
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert Zen models to MLX format for Apple Silicon"
|
|
)
|
|
parser.add_argument(
|
|
"model",
|
|
choices=list(MODELS.keys()) + ["all"],
|
|
help="Model to convert or 'all' for batch conversion"
|
|
)
|
|
parser.add_argument(
|
|
"--quantize",
|
|
action="store_true",
|
|
default=True,
|
|
help="Apply quantization (default: True)"
|
|
)
|
|
parser.add_argument(
|
|
"--q-bits",
|
|
type=int,
|
|
choices=[2, 4, 8],
|
|
default=4,
|
|
help="Quantization bits (default: 4)"
|
|
)
|
|
parser.add_argument(
|
|
"--q-group-size",
|
|
type=int,
|
|
default=64,
|
|
help="Quantization group size (default: 64)"
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Force re-conversion even if model exists"
|
|
)
|
|
parser.add_argument(
|
|
"--cache-dir",
|
|
type=Path,
|
|
help="Cache directory for downloaded models"
|
|
)
|
|
parser.add_argument(
|
|
"--create-lora",
|
|
action="store_true",
|
|
help="Create LoRA adapter configuration"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
converter = ZenMLXConverter(cache_dir=args.cache_dir)
|
|
|
|
if args.model == "all":
|
|
# Batch conversion
|
|
logger.info("Starting batch conversion of all Zen models")
|
|
results = converter.batch_convert(q_bits=args.q_bits)
|
|
|
|
# Print summary
|
|
print("\nConversion Summary:")
|
|
print("-" * 50)
|
|
for model, result in results.items():
|
|
status = "✓" if result["status"] == "success" else "✗"
|
|
print(f"{status} {model}: {result['status']}")
|
|
else:
|
|
# Single model conversion
|
|
output_path = converter.convert_model(
|
|
model_name=args.model,
|
|
quantize=args.quantize,
|
|
q_bits=args.q_bits,
|
|
q_group_size=args.q_group_size,
|
|
force=args.force
|
|
)
|
|
|
|
if args.create_lora and MODELS[args.model]["supports_lora"]:
|
|
converter.create_lora_adapter(args.model)
|
|
|
|
print(f"\nModel converted successfully!")
|
|
print(f"Location: {output_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |