e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import typer
|
|
|
|
from unsloth_cli._inference import ensure_studio_backend_path
|
|
from unsloth_cli.config import Config, load_config
|
|
from unsloth_cli.options import add_options_from_config
|
|
|
|
|
|
def _should_use_mlx_backend_for_cli() -> bool:
|
|
ensure_studio_backend_path()
|
|
from studio.backend.core.training.training import should_use_mlx_training_backend
|
|
return should_use_mlx_training_backend()
|
|
|
|
|
|
def _activate_mlx_transformers(model_name: str, hf_token: Optional[str]) -> None:
|
|
# Activate before any transformers import: adapter model-type detection imports utils.models.
|
|
ensure_studio_backend_path()
|
|
from utils.transformers_version import activate_transformers_for_subprocess
|
|
try:
|
|
activate_transformers_for_subprocess(model_name, hf_token)
|
|
except Exception as exc:
|
|
typer.echo(f"Warning: failed to activate Transformers sidecar: {exc}", err = True)
|
|
|
|
|
|
def _create_cli_trainer(model_name: str, hf_token: Optional[str]):
|
|
if _should_use_mlx_backend_for_cli():
|
|
_activate_mlx_transformers(model_name, hf_token)
|
|
# MLX is torch-free: use the lightweight adapter, not trainer.py (imports torch/unsloth/trl at load).
|
|
ensure_studio_backend_path()
|
|
from studio.backend.core.training.training import create_mlx_trainer_adapter
|
|
|
|
return create_mlx_trainer_adapter()
|
|
|
|
ensure_studio_backend_path()
|
|
from studio.backend.core.training.trainer import UnslothTrainer
|
|
|
|
return UnslothTrainer()
|
|
|
|
|
|
@add_options_from_config(Config)
|
|
def train(
|
|
config: Optional[Path] = typer.Option(
|
|
None,
|
|
"--config",
|
|
"-c",
|
|
help = "Path to YAML/JSON config file. CLI flags override config values.",
|
|
),
|
|
hf_token: Optional[str] = typer.Option(
|
|
None, "--hf-token", envvar = "HF_TOKEN", help = "Hugging Face token if needed."
|
|
),
|
|
wandb_token: Optional[str] = typer.Option(
|
|
None, "--wandb-token", envvar = "WANDB_API_KEY", help = "Weights & Biases API key."
|
|
),
|
|
dry_run: bool = typer.Option(
|
|
False,
|
|
"--dry-run",
|
|
help = "Show resolved config and exit without training.",
|
|
),
|
|
config_overrides: dict = None,
|
|
):
|
|
"""Launch training using the existing Unsloth training backend."""
|
|
try:
|
|
cfg = load_config(config)
|
|
except FileNotFoundError as e:
|
|
typer.echo(f"Error: {e}", err = True)
|
|
raise typer.Exit(code = 2)
|
|
|
|
config_overrides = config_overrides or {}
|
|
cfg.apply_overrides(**config_overrides)
|
|
|
|
# CLI/env tokens take precedence; guard against unresolved typer.Option
|
|
# (decorator interaction)
|
|
from typer.models import OptionInfo
|
|
|
|
if isinstance(hf_token, OptionInfo):
|
|
hf_token = None
|
|
if isinstance(wandb_token, OptionInfo):
|
|
wandb_token = None
|
|
hf_token = hf_token or cfg.logging.hf_token
|
|
wandb_token = wandb_token or cfg.logging.wandb_token
|
|
|
|
if dry_run:
|
|
import yaml
|
|
|
|
data = cfg.model_dump()
|
|
data["training"]["output_dir"] = str(data["training"]["output_dir"])
|
|
typer.echo(yaml.dump(data, default_flow_style = False, sort_keys = False))
|
|
raise typer.Exit(code = 0)
|
|
|
|
if not cfg.model:
|
|
typer.echo("Error: provide --model or set model in --config", err = True)
|
|
raise typer.Exit(code = 2)
|
|
|
|
if not cfg.data.dataset and not cfg.data.local_dataset:
|
|
typer.echo("Error: provide --dataset or --local-dataset (or via --config)", err = True)
|
|
raise typer.Exit(code = 2)
|
|
|
|
# A LoRA adapter dir has adapter_config.json
|
|
model_path = Path(cfg.model) if cfg.model else None
|
|
model_is_lora = (
|
|
model_path and model_path.is_dir() and (model_path / "adapter_config.json").exists()
|
|
)
|
|
use_lora = cfg.training.training_type.lower() == "lora"
|
|
|
|
if model_is_lora and not use_lora:
|
|
typer.echo(
|
|
"Error: Cannot do full finetuning on a LoRA adapter. "
|
|
"Use --training-type lora or provide a base model.",
|
|
err = True,
|
|
)
|
|
raise typer.Exit(code = 2)
|
|
|
|
trainer = _create_cli_trainer(cfg.model, hf_token)
|
|
|
|
# Load model (trainer.is_vlm is set after this)
|
|
if not trainer.load_model(
|
|
model_name = cfg.model,
|
|
max_seq_length = cfg.training.max_seq_length,
|
|
load_in_4bit = cfg.training.load_in_4bit if use_lora else False,
|
|
hf_token = hf_token,
|
|
):
|
|
typer.echo("Model load failed", err = True)
|
|
raise typer.Exit(code = 1)
|
|
|
|
is_vision = trainer.is_vlm
|
|
|
|
if not trainer.prepare_model_for_training(**cfg.model_kwargs(use_lora, is_vision)):
|
|
typer.echo("Model preparation failed", err = True)
|
|
raise typer.Exit(code = 1)
|
|
|
|
result = trainer.load_and_format_dataset(
|
|
dataset_source = cfg.data.dataset or "",
|
|
format_type = cfg.data.format_type,
|
|
local_datasets = cfg.data.local_dataset,
|
|
)
|
|
if result is None:
|
|
typer.echo("Dataset load failed", err = True)
|
|
raise typer.Exit(code = 1)
|
|
|
|
ds, eval_ds = result
|
|
|
|
training_kwargs = cfg.training_kwargs()
|
|
training_kwargs["wandb_token"] = wandb_token # CLI/env takes precedence
|
|
started = trainer.start_training(dataset = ds, eval_dataset = eval_ds, **training_kwargs)
|
|
|
|
if not started:
|
|
typer.echo("Training failed to start", err = True)
|
|
raise typer.Exit(code = 1)
|
|
|
|
try:
|
|
while trainer.training_thread and trainer.training_thread.is_alive():
|
|
progress = trainer.get_training_progress()
|
|
if getattr(progress, "error", None):
|
|
break
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
typer.echo("Stopping training (Ctrl+C detected)...")
|
|
trainer.stop_training()
|
|
finally:
|
|
if trainer.training_thread:
|
|
progress = trainer.get_training_progress()
|
|
if getattr(progress, "error", None):
|
|
trainer.training_thread.join(timeout = 5)
|
|
else:
|
|
trainer.training_thread.join()
|
|
|
|
final = trainer.get_training_progress()
|
|
if getattr(final, "error", None):
|
|
typer.echo(f"Training error: {final.error}", err = True)
|
|
raise typer.Exit(code = 1)
|