| | """ |
| | export_to_lms.py — Export LoRA adapter back to LM Studio. |
| | |
| | Workflow: |
| | 1. Fuse LoRA adapter with base model via MLX |
| | 2. Export to GGUF format |
| | 3. Copy to LM Studio models directory |
| | 4. Load via lms CLI |
| | """ |
| |
|
| | import json |
| | import logging |
| | import shutil |
| | import subprocess |
| | import time |
| | from pathlib import Path |
| | from typing import Optional |
| |
|
| | log = logging.getLogger("export_to_lms") |
| |
|
| |
|
| | def export_adapter_to_lms(config, version: Optional[int] = None) -> dict: |
| | """Export current LoRA adapter as GGUF to LM Studio. |
| | |
| | Args: |
| | config: NeuralConfig instance |
| | version: adapter version tag (auto if None) |
| | |
| | Returns: |
| | dict with export details |
| | """ |
| | try: |
| | import mlx_lm |
| | except ImportError: |
| | raise RuntimeError("mlx-lm required for export") |
| |
|
| | config.resolve_paths() |
| |
|
| | if version is None: |
| | version = int(time.time()) % 100000 |
| |
|
| | model_dir = str(Path(config.model_path).parent) |
| | adapter_dir = config.adapter_dir |
| | export_name = f"{config.model_key}-tuned-v{version}" |
| | export_dir = Path(config.base_dir) / "exports" / export_name |
| | export_dir.mkdir(parents=True, exist_ok=True) |
| |
|
| | log.info(f"Exporting adapter: {adapter_dir} + {model_dir} → {export_dir}") |
| |
|
| | |
| | |
| | try: |
| | mlx_lm.fuse( |
| | model=model_dir, |
| | adapter_path=adapter_dir, |
| | save_path=str(export_dir / "merged"), |
| | ) |
| | log.info("LoRA adapter fused with base model") |
| | except Exception as e: |
| | log.error(f"Fuse failed: {e}") |
| | raise |
| |
|
| | |
| | gguf_path = export_dir / f"{export_name}.gguf" |
| | try: |
| | |
| | result = subprocess.run( |
| | ["python3", "-m", "mlx_lm.convert", |
| | "--model", str(export_dir / "merged"), |
| | "--quantize", "--q-bits", "4", |
| | "-o", str(gguf_path)], |
| | capture_output=True, text=True, timeout=600) |
| |
|
| | if result.returncode != 0: |
| | log.warning(f"GGUF convert failed: {result.stderr}") |
| | |
| | gguf_path = export_dir / "merged" |
| | except Exception as e: |
| | log.warning(f"GGUF conversion error: {e}") |
| | gguf_path = export_dir / "merged" |
| |
|
| | |
| | lms_dest = Path.home() / ".lmstudio" / "models" / "jarvis-tuned" / export_name |
| | try: |
| | lms_dest.mkdir(parents=True, exist_ok=True) |
| | if gguf_path.is_file(): |
| | shutil.copy2(str(gguf_path), str(lms_dest)) |
| | else: |
| | |
| | shutil.copytree(str(gguf_path), str(lms_dest), dirs_exist_ok=True) |
| | log.info(f"Copied to LM Studio: {lms_dest}") |
| | except Exception as e: |
| | log.warning(f"Copy to LM Studio failed: {e}") |
| |
|
| | |
| | lms = config.lms_cli_path |
| | if lms: |
| | try: |
| | subprocess.run( |
| | [lms, "load", str(lms_dest)], |
| | capture_output=True, timeout=120) |
| | log.info(f"Loaded {export_name} in LM Studio") |
| | except Exception as e: |
| | log.warning(f"LM Studio load failed: {e}") |
| |
|
| | |
| | meta = { |
| | "export_name": export_name, |
| | "version": version, |
| | "source_model": config.model_key, |
| | "adapter_dir": adapter_dir, |
| | "gguf_path": str(gguf_path), |
| | "lms_path": str(lms_dest), |
| | "timestamp": time.time(), |
| | } |
| | with open(export_dir / "export_meta.json", "w") as f: |
| | json.dump(meta, f, indent=2) |
| |
|
| | return meta |
| |
|