fix: replace Unicode dashes with ASCII equivalents across codebase
Box-drawing chars (U+2500), em dashes (U+2014), and en dashes (U+2013) in comments, section dividers, log messages, and docstrings are not representable on legacy code pages like CP1252. Replace them with plain ASCII dashes so the codebase is consistently ASCII-safe. User-facing UI strings (placeholders, separators, display text in the frontend) are left unchanged since they render in the browser which handles Unicode natively.
This commit is contained in:
parent
8c94b461fb
commit
a1eff8ee22
61 changed files with 448 additions and 448 deletions
20
install.ps1
20
install.ps1
|
|
@ -14,7 +14,7 @@ function Install-UnslothStudio {
|
|||
Write-Host "========================================="
|
||||
Write-Host ""
|
||||
|
||||
# ── Helper: refresh PATH from registry (deduplicating entries) ──
|
||||
# -- Helper: refresh PATH from registry (deduplicating entries) --
|
||||
function Refresh-SessionPath {
|
||||
$machine = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
$user = [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
|
|
@ -31,7 +31,7 @@ function Install-UnslothStudio {
|
|||
$env:Path = $unique -join ";"
|
||||
}
|
||||
|
||||
# ── Check winget ──
|
||||
# -- Check winget --
|
||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Error: winget is not available." -ForegroundColor Red
|
||||
Write-Host " Install it from https://aka.ms/getwinget" -ForegroundColor Yellow
|
||||
|
|
@ -39,7 +39,7 @@ function Install-UnslothStudio {
|
|||
return
|
||||
}
|
||||
|
||||
# ── Helper: detect a working Python 3.11-3.13 on the system ──
|
||||
# -- Helper: detect a working Python 3.11-3.13 on the system --
|
||||
# Returns the version string (e.g. "3.13") or "" if none found.
|
||||
# Uses try-catch + stderr redirection so that App Execution Alias stubs
|
||||
# (WindowsApps) and other non-functional executables are probed safely
|
||||
|
|
@ -109,7 +109,7 @@ function Install-UnslothStudio {
|
|||
return $null
|
||||
}
|
||||
|
||||
# ── Install Python if no compatible version (3.11-3.13) found ──
|
||||
# -- Install Python if no compatible version (3.11-3.13) found --
|
||||
# Find-CompatiblePython returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null.
|
||||
$DetectedPython = Find-CompatiblePython
|
||||
if ($DetectedPython) {
|
||||
|
|
@ -158,7 +158,7 @@ function Install-UnslothStudio {
|
|||
}
|
||||
}
|
||||
|
||||
# ── Install uv if not present ──
|
||||
# -- Install uv if not present --
|
||||
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "==> Installing uv package manager..."
|
||||
$prevEAP = $ErrorActionPreference
|
||||
|
|
@ -180,7 +180,7 @@ function Install-UnslothStudio {
|
|||
return
|
||||
}
|
||||
|
||||
# ── Create venv (skip if it already exists and has a valid interpreter) ──
|
||||
# -- Create venv (skip if it already exists and has a valid interpreter) --
|
||||
# Pass the resolved executable path to uv so it does not re-resolve
|
||||
# a version string back to a conda interpreter.
|
||||
$VenvPython = Join-Path $VenvName "Scripts\python.exe"
|
||||
|
|
@ -196,7 +196,7 @@ function Install-UnslothStudio {
|
|||
Write-Host "==> Virtual environment ${VenvName} already exists, skipping creation."
|
||||
}
|
||||
|
||||
# ── Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) ──
|
||||
# -- Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) --
|
||||
$HasNvidiaSmi = $false
|
||||
$NvidiaSmiExe = $null
|
||||
try {
|
||||
|
|
@ -227,7 +227,7 @@ function Install-UnslothStudio {
|
|||
Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── Choose the correct PyTorch index URL based on driver CUDA version ──
|
||||
# -- Choose the correct PyTorch index URL based on driver CUDA version --
|
||||
# Mirrors Get-PytorchCudaTag in setup.ps1.
|
||||
function Get-TorchIndexUrl {
|
||||
$baseUrl = "https://download.pytorch.org/whl"
|
||||
|
|
@ -249,7 +249,7 @@ function Install-UnslothStudio {
|
|||
}
|
||||
$TorchIndexUrl = Get-TorchIndexUrl
|
||||
|
||||
# ── Install PyTorch first, then unsloth separately ──
|
||||
# -- Install PyTorch first, then unsloth separately --
|
||||
#
|
||||
# Why two steps?
|
||||
# `uv pip install unsloth --torch-backend=cpu` on Windows resolves to
|
||||
|
|
@ -281,7 +281,7 @@ function Install-UnslothStudio {
|
|||
return
|
||||
}
|
||||
|
||||
# ── Run studio setup ──
|
||||
# -- Run studio setup --
|
||||
# setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools,
|
||||
# CUDA Toolkit, Node.js, and other dependencies automatically via winget.
|
||||
Write-Host "==> Running unsloth studio setup..."
|
||||
|
|
|
|||
18
install.sh
18
install.sh
|
|
@ -7,7 +7,7 @@ set -e
|
|||
VENV_NAME="unsloth_studio"
|
||||
PYTHON_VERSION="3.13"
|
||||
|
||||
# ── Helper: download a URL to a file (supports curl and wget) ──
|
||||
# -- Helper: download a URL to a file (supports curl and wget) --
|
||||
download() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -LsSf "$1" -o "$2"
|
||||
|
|
@ -19,7 +19,7 @@ download() {
|
|||
fi
|
||||
}
|
||||
|
||||
# ── Helper: check if a single package is available on the system ──
|
||||
# -- Helper: check if a single package is available on the system --
|
||||
_is_pkg_installed() {
|
||||
case "$1" in
|
||||
build-essential) command -v gcc >/dev/null 2>&1 ;;
|
||||
|
|
@ -31,7 +31,7 @@ _is_pkg_installed() {
|
|||
esac
|
||||
}
|
||||
|
||||
# ── Helper: install packages via apt, escalating to sudo only if needed ──
|
||||
# -- Helper: install packages via apt, escalating to sudo only if needed --
|
||||
# Usage: _smart_apt_install pkg1 pkg2 pkg3 ...
|
||||
_smart_apt_install() {
|
||||
_PKGS="$*"
|
||||
|
|
@ -95,7 +95,7 @@ echo " Unsloth Studio Installer"
|
|||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# ── Detect platform ──
|
||||
# -- Detect platform --
|
||||
OS="linux"
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
OS="macos"
|
||||
|
|
@ -104,7 +104,7 @@ elif grep -qi microsoft /proc/version 2>/dev/null; then
|
|||
fi
|
||||
echo "==> Platform: $OS"
|
||||
|
||||
# ── Check system dependencies ──
|
||||
# -- Check system dependencies --
|
||||
# cmake and git are needed by unsloth studio setup to build the GGUF inference
|
||||
# engine (llama.cpp). build-essential and libcurl-dev are also needed on Linux.
|
||||
MISSING=""
|
||||
|
|
@ -170,7 +170,7 @@ else
|
|||
echo "==> All system dependencies found."
|
||||
fi
|
||||
|
||||
# ── Install uv ──
|
||||
# -- Install uv --
|
||||
UV_MIN_VERSION="0.7.14"
|
||||
|
||||
version_ge() {
|
||||
|
|
@ -224,7 +224,7 @@ if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then
|
|||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
|
||||
# ── Create venv (skip if it already exists and has a valid interpreter) ──
|
||||
# -- Create venv (skip if it already exists and has a valid interpreter) --
|
||||
if [ ! -x "$VENV_NAME/bin/python" ]; then
|
||||
[ -e "$VENV_NAME" ] && rm -rf "$VENV_NAME"
|
||||
echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_NAME})..."
|
||||
|
|
@ -233,11 +233,11 @@ else
|
|||
echo "==> Virtual environment ${VENV_NAME} already exists, skipping creation."
|
||||
fi
|
||||
|
||||
# ── Install unsloth directly into the venv (no activation needed) ──
|
||||
# -- Install unsloth directly into the venv (no activation needed) --
|
||||
echo "==> Installing unsloth (this may take a few minutes)..."
|
||||
uv pip install --python "$VENV_NAME/bin/python" "unsloth>=2026.3.11" --torch-backend=auto
|
||||
|
||||
# ── Run studio setup ──
|
||||
# -- Run studio setup --
|
||||
# Ensure the venv's Python is on PATH for setup.sh's Python discovery.
|
||||
# On macOS the system Python may be outside the 3.11-3.13 range that
|
||||
# setup.sh requires, but uv already installed a compatible interpreter
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ def refresh_access_token(refresh_token: str) -> Tuple[Optional[str], Optional[st
|
|||
"""
|
||||
Validate a refresh token and issue a new access token.
|
||||
|
||||
The refresh token itself is NOT consumed — it stays valid until expiry.
|
||||
The refresh token itself is NOT consumed -- it stays valid until expiry.
|
||||
Returns a new access_token or None if the refresh token is invalid/expired.
|
||||
"""
|
||||
username = verify_refresh_token(refresh_token)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from utils.paths import auth_db_path, ensure_dir
|
|||
DB_PATH = auth_db_path()
|
||||
DEFAULT_ADMIN_USERNAME = "unsloth"
|
||||
|
||||
# Plaintext bootstrap password file — lives beside auth.db, deleted on
|
||||
# Plaintext bootstrap password file -- lives beside auth.db, deleted on
|
||||
# first password change so the credential never lingers on disk.
|
||||
_BOOTSTRAP_PW_PATH = DB_PATH.parent / ".bootstrap_password"
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ def generate_bootstrap_password() -> str:
|
|||
if _bootstrap_password:
|
||||
return _bootstrap_password
|
||||
|
||||
# 3. First-ever startup — generate a fresh passphrase.
|
||||
# 3. First-ever startup -- generate a fresh passphrase.
|
||||
import diceware
|
||||
|
||||
_bootstrap_password = diceware.get_passphrase(
|
||||
|
|
@ -314,7 +314,7 @@ def verify_refresh_token(token: str) -> Optional[str]:
|
|||
Verify a refresh token and return the username.
|
||||
|
||||
Returns the username if valid and not expired, None otherwise.
|
||||
The token is NOT consumed — it stays valid until it expires.
|
||||
The token is NOT consumed -- it stays valid until it expires.
|
||||
"""
|
||||
token_hash = _hash_token(token)
|
||||
conn = get_connection()
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ def _apply_wsl_sudo_patch():
|
|||
|
||||
def _wsl_do_we_need_sudo(system_type = "debian"):
|
||||
logger.info(
|
||||
"WSL detected — skipping sudo check "
|
||||
"WSL detected -- skipping sudo check "
|
||||
"(build deps pre-installed by setup.sh)"
|
||||
)
|
||||
return False
|
||||
|
|
@ -532,7 +532,7 @@ class ExportBackend:
|
|||
cwd = os.getcwd()
|
||||
pre_existing_ggufs = set(glob.glob(os.path.join(cwd, "*.gguf")))
|
||||
|
||||
# Pass absolute path — no os.chdir needed.
|
||||
# Pass absolute path -- no os.chdir needed.
|
||||
# unsloth saves intermediate HF model files into model_save_path.
|
||||
# unsloth-zoo's check_llama_cpp() uses ~/.unsloth/llama.cpp by default.
|
||||
model_save_path = os.path.join(abs_save_dir, "model")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""
|
||||
Export orchestrator — subprocess-based.
|
||||
Export orchestrator -- subprocess-based.
|
||||
|
||||
Provides the same API as ExportBackend, but delegates all ML work
|
||||
to a persistent subprocess. The subprocess is spawned on first checkpoint
|
||||
|
|
@ -32,7 +32,7 @@ _CTX = mp.get_context("spawn")
|
|||
|
||||
class ExportOrchestrator:
|
||||
"""
|
||||
Export backend orchestrator — subprocess-based.
|
||||
Export backend orchestrator -- subprocess-based.
|
||||
|
||||
Exposes the same API surface as ExportBackend so routes/export.py
|
||||
needs minimal changes. Internally, all heavy ML operations happen in
|
||||
|
|
@ -154,7 +154,7 @@ class ExportOrchestrator:
|
|||
def _wait_response(self, expected_type: str, timeout: float = 3600.0) -> dict:
|
||||
"""Block until a response of the expected type arrives.
|
||||
|
||||
Export operations can take a very long time — GGUF conversion for
|
||||
Export operations can take a very long time -- GGUF conversion for
|
||||
large models (30B+) easily takes 20-30 minutes. Default timeout
|
||||
is 1 hour.
|
||||
"""
|
||||
|
|
@ -183,7 +183,7 @@ class ExportOrchestrator:
|
|||
logger.info("Export subprocess status: %s", resp.get("message", ""))
|
||||
continue
|
||||
|
||||
# Other response types during wait — skip
|
||||
# Other response types during wait -- skip
|
||||
logger.debug(
|
||||
"Skipping response type '%s' while waiting for '%s'",
|
||||
rtype,
|
||||
|
|
@ -208,7 +208,7 @@ class ExportOrchestrator:
|
|||
return events
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API — same interface as ExportBackend
|
||||
# Public API -- same interface as ExportBackend
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def load_checkpoint(
|
||||
|
|
@ -365,7 +365,7 @@ class ExportOrchestrator:
|
|||
def cleanup_memory(self) -> bool:
|
||||
"""Cleanup export-related models from memory."""
|
||||
if not self._ensure_subprocess_alive():
|
||||
# No subprocess — just clear local state
|
||||
# No subprocess -- just clear local state
|
||||
self.current_checkpoint = None
|
||||
self.is_vision = False
|
||||
self.is_peft = False
|
||||
|
|
@ -378,7 +378,7 @@ class ExportOrchestrator:
|
|||
except RuntimeError:
|
||||
success = False
|
||||
|
||||
# Shut down subprocess after cleanup — no model loaded
|
||||
# Shut down subprocess after cleanup -- no model loaded
|
||||
self._shutdown_subprocess()
|
||||
|
||||
self.current_checkpoint = None
|
||||
|
|
@ -389,7 +389,7 @@ class ExportOrchestrator:
|
|||
def scan_checkpoints(
|
||||
self, outputs_dir: str = str(outputs_root())
|
||||
) -> List[Tuple[str, list]]:
|
||||
"""Scan for checkpoints — no ML imports needed, runs locally."""
|
||||
"""Scan for checkpoints -- no ML imports needed, runs locally."""
|
||||
from utils.models.checkpoints import scan_checkpoints
|
||||
|
||||
return scan_checkpoints(outputs_dir = outputs_dir)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Export subprocess entry point.
|
||||
|
||||
Each export session runs in a persistent subprocess (mp.get_context("spawn")).
|
||||
This gives us a clean Python interpreter with no stale module state —
|
||||
This gives us a clean Python interpreter with no stale module state --
|
||||
solving the transformers version-switching problem completely.
|
||||
|
||||
The subprocess stays alive while a model is loaded, accepting commands
|
||||
|
|
@ -217,7 +217,7 @@ def run_export_process(
|
|||
resp_queue: Any,
|
||||
config: dict,
|
||||
) -> None:
|
||||
"""Subprocess entrypoint. Persistent — runs command loop until shutdown.
|
||||
"""Subprocess entrypoint. Persistent -- runs command loop until shutdown.
|
||||
|
||||
Args:
|
||||
cmd_queue: mp.Queue for receiving commands from parent.
|
||||
|
|
@ -244,7 +244,7 @@ def run_export_process(
|
|||
|
||||
checkpoint_path = config["checkpoint_path"]
|
||||
|
||||
# ── 1. Activate correct transformers version BEFORE any ML imports ──
|
||||
# -- 1. Activate correct transformers version BEFORE any ML imports --
|
||||
try:
|
||||
_activate_transformers_version(checkpoint_path)
|
||||
except Exception as exc:
|
||||
|
|
@ -259,20 +259,20 @@ def run_export_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 1b. On Windows, check Triton availability (must be before import torch) ──
|
||||
# -- 1b. On Windows, check Triton availability (must be before import torch) --
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import triton # noqa: F401
|
||||
|
||||
logger.info("Triton available — torch.compile enabled")
|
||||
logger.info("Triton available -- torch.compile enabled")
|
||||
except ImportError:
|
||||
os.environ["TORCHDYNAMO_DISABLE"] = "1"
|
||||
logger.warning(
|
||||
"Triton not found on Windows — torch.compile disabled. "
|
||||
"Triton not found on Windows -- torch.compile disabled. "
|
||||
'Install for better performance: pip install "triton-windows<3.7"'
|
||||
)
|
||||
|
||||
# ── 2. Import ML libraries (fresh in this clean process) ──
|
||||
# -- 2. Import ML libraries (fresh in this clean process) --
|
||||
try:
|
||||
_send_response(
|
||||
resp_queue,
|
||||
|
|
@ -307,7 +307,7 @@ def run_export_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 3. Create export backend and load initial checkpoint ──
|
||||
# -- 3. Create export backend and load initial checkpoint --
|
||||
try:
|
||||
backend = ExportBackend()
|
||||
|
||||
|
|
@ -325,7 +325,7 @@ def run_export_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 4. Command loop — process commands until shutdown ──
|
||||
# -- 4. Command loop -- process commands until shutdown --
|
||||
logger.info("Export subprocess ready, entering command loop")
|
||||
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class AudioCodecManager:
|
|||
else:
|
||||
raise ValueError(f"Unknown audio_type: {audio_type}")
|
||||
|
||||
# ── Lazy loaders ─────────────────────────────────────────────
|
||||
# -- Lazy loaders ---------------------------------------------
|
||||
|
||||
def _load_snac(self, device: str) -> None:
|
||||
if self._snac_model is not None:
|
||||
|
|
@ -84,7 +84,7 @@ class AudioCodecManager:
|
|||
import subprocess
|
||||
|
||||
# Clone SparkAudio/Spark-TTS GitHub repo for the sparktts Python package
|
||||
# (same approach as training — the HF model repos don't contain the package)
|
||||
# (same approach as training -- the HF model repos don't contain the package)
|
||||
spark_code_dir = os.path.join(
|
||||
os.path.dirname(model_repo_path or "."), "Spark-TTS"
|
||||
)
|
||||
|
|
@ -167,7 +167,7 @@ class AudioCodecManager:
|
|||
self._dac_audio_codec = processor.audio_codec
|
||||
logger.info("Loaded DAC audio codec")
|
||||
|
||||
# ── Decoders ─────────────────────────────────────────────────
|
||||
# -- Decoders -------------------------------------------------
|
||||
|
||||
def decode_snac(
|
||||
self, generated_ids: torch.Tensor, device: str
|
||||
|
|
@ -188,7 +188,7 @@ class AudioCodecManager:
|
|||
else:
|
||||
# Gracefully fall back to using entire output if marker not found
|
||||
logger.warning(
|
||||
"No START_OF_SPEECH token (128257) found — using full generated output"
|
||||
"No START_OF_SPEECH token (128257) found -- using full generated output"
|
||||
)
|
||||
cropped = generated_ids
|
||||
row = cropped[0]
|
||||
|
|
@ -309,7 +309,7 @@ class AudioCodecManager:
|
|||
token_ids: Optional[list] = None,
|
||||
text: Optional[str] = None,
|
||||
) -> Tuple[bytes, int]:
|
||||
"""Unified decode — dispatches to the right codec decoder."""
|
||||
"""Unified decode -- dispatches to the right codec decoder."""
|
||||
if audio_type == "snac":
|
||||
if not token_ids:
|
||||
raise ValueError("SNAC decoding requires token_ids")
|
||||
|
|
@ -324,7 +324,7 @@ class AudioCodecManager:
|
|||
return self.decode_dac(text, device)
|
||||
raise ValueError(f"Cannot decode audio_type: {audio_type}")
|
||||
|
||||
# ── Cleanup ──────────────────────────────────────────────────
|
||||
# -- Cleanup --------------------------------------------------
|
||||
|
||||
def unload(self) -> None:
|
||||
"""Release all codec models from memory."""
|
||||
|
|
|
|||
|
|
@ -74,14 +74,14 @@ class HarmonyTextStreamer:
|
|||
self._is_first_put: bool = True
|
||||
self._stop: bool = False
|
||||
|
||||
# Stateful channel tracking — avoids delta-on-transformed bugs
|
||||
# Stateful channel tracking -- avoids delta-on-transformed bugs
|
||||
self._emitted_think_open: bool = False
|
||||
self._emitted_think_close: bool = False
|
||||
self._analysis_emitted: int = 0 # chars of analysis content emitted
|
||||
self._final_emitted: int = 0 # chars of final content emitted
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# put / end — called from the generation thread
|
||||
# put / end -- called from the generation thread
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def put(self, value):
|
||||
|
|
@ -89,7 +89,7 @@ class HarmonyTextStreamer:
|
|||
import torch
|
||||
|
||||
if isinstance(value, torch.Tensor):
|
||||
# value shape: (batch, seq) — take first batch element
|
||||
# value shape: (batch, seq) -- take first batch element
|
||||
ids = value[0].tolist() if value.dim() > 1 else value.tolist()
|
||||
elif isinstance(value, (list, tuple)):
|
||||
ids = list(value)
|
||||
|
|
@ -127,7 +127,7 @@ class HarmonyTextStreamer:
|
|||
self._queue.put(None) # sentinel
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Iterator interface — consumed by the streaming loop
|
||||
# Iterator interface -- consumed by the streaming loop
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def __iter__(self):
|
||||
|
|
@ -164,16 +164,16 @@ class HarmonyTextStreamer:
|
|||
- final content deltas
|
||||
"""
|
||||
# If raw contains <|channel|> but no complete channel+message pair yet,
|
||||
# buffer silently — don't emit partial channel names as text.
|
||||
# buffer silently -- don't emit partial channel names as text.
|
||||
has_channel_token = "<|channel|>" in raw
|
||||
matches = list(self._HARMONY_RE.finditer(raw))
|
||||
|
||||
if has_channel_token and not matches:
|
||||
# Partial harmony markup still building — wait for more tokens
|
||||
# Partial harmony markup still building -- wait for more tokens
|
||||
return
|
||||
|
||||
if not has_channel_token and not matches:
|
||||
# No harmony protocol at all — should not happen for gpt-oss
|
||||
# No harmony protocol at all -- should not happen for gpt-oss
|
||||
# but handle gracefully by not emitting anything
|
||||
return
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ class InferenceBackend:
|
|||
self.device = get_device().value
|
||||
self._audio_codec_manager = AudioCodecManager()
|
||||
|
||||
# Thread safety — _generation_lock serializes model.generate() calls.
|
||||
# Thread safety -- _generation_lock serializes model.generate() calls.
|
||||
# Must be a regular Lock (NOT RLock) because in async FastAPI, multiple
|
||||
# requests share the same event-loop thread, so RLock reentrancy lets
|
||||
# concurrent compare-mode requests race on the GPU. The lock is
|
||||
|
|
@ -273,7 +273,7 @@ class InferenceBackend:
|
|||
"active_adapter": None,
|
||||
}
|
||||
|
||||
# ── Audio model loading path ──────────────────────────
|
||||
# -- Audio model loading path --------------------------
|
||||
if config.is_audio:
|
||||
audio_type = config.audio_type
|
||||
adapter_info = " (LoRA adapter)" if config.is_lora else ""
|
||||
|
|
@ -309,7 +309,7 @@ class InferenceBackend:
|
|||
if os.path.isdir(base_path):
|
||||
abs_repo_path = os.path.abspath(os.path.dirname(base_path))
|
||||
else:
|
||||
# base_model is an HF ID — download it
|
||||
# base_model is an HF ID -- download it
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
local_dir = base_path.split("/")[-1]
|
||||
|
|
@ -368,7 +368,7 @@ class InferenceBackend:
|
|||
self.models[model_name]["model"] = model
|
||||
self.models[model_name]["tokenizer"] = tokenizer
|
||||
elif audio_type == "whisper":
|
||||
# Whisper ASR — uses FastModel with WhisperForConditionalGeneration
|
||||
# Whisper ASR -- uses FastModel with WhisperForConditionalGeneration
|
||||
from unsloth import FastModel
|
||||
from transformers import WhisperForConditionalGeneration
|
||||
|
||||
|
|
@ -413,7 +413,7 @@ class InferenceBackend:
|
|||
self.models[model_name]["tokenizer"] = tokenizer
|
||||
|
||||
# Load the external codec for TTS audio types
|
||||
# (Whisper is ASR, audio_vlm is audio input — neither needs a codec)
|
||||
# (Whisper is ASR, audio_vlm is audio input -- neither needs a codec)
|
||||
if audio_type not in ("whisper", "audio_vlm"):
|
||||
model_repo_path = self.models[model_name].get("model_repo_path")
|
||||
self._audio_codec_manager.load_codec(
|
||||
|
|
@ -473,7 +473,7 @@ class InferenceBackend:
|
|||
pass
|
||||
logger.warning(
|
||||
f"FastVisionModel returned {type(processor).__name__} (no image_processor) "
|
||||
f"for '{model_name}' — loading proper processor from '{processor_source}'"
|
||||
f"for '{model_name}' -- loading proper processor from '{processor_source}'"
|
||||
)
|
||||
from transformers import AutoProcessor
|
||||
|
||||
|
|
@ -725,7 +725,7 @@ class InferenceBackend:
|
|||
Uses PEFT's disable_adapter_layers() / enable_adapter_layers() which toggle
|
||||
a boolean flag on each LoRA layer. Unsloth's fast_linear_forward checks this
|
||||
flag (proj.disable_adapters) and skips LoRA computation when True.
|
||||
This is non-destructive — no model unloading/reloading needed.
|
||||
This is non-destructive -- no model unloading/reloading needed.
|
||||
|
||||
Args:
|
||||
use_adapter: None = no change, False = disable (base model),
|
||||
|
|
@ -788,7 +788,7 @@ class InferenceBackend:
|
|||
Thread-safe generation with optional adapter toggling.
|
||||
|
||||
The adapter toggle + model.generate() are serialized by _generation_lock
|
||||
inside the background generation thread — NOT in the event-loop thread.
|
||||
inside the background generation thread -- NOT in the event-loop thread.
|
||||
This prevents the RLock-reentrant race that occurs when two async SSE
|
||||
handlers share the same event-loop thread.
|
||||
|
||||
|
|
@ -891,7 +891,7 @@ class InferenceBackend:
|
|||
else:
|
||||
logger.warning(
|
||||
f"Model '{self.active_model_name}' is marked as vision but its processor "
|
||||
f"({type(processor).__name__}) has no image_processor — "
|
||||
f"({type(processor).__name__}) has no image_processor -- "
|
||||
f"falling back to text-only generation (image will be ignored)."
|
||||
)
|
||||
|
||||
|
|
@ -938,7 +938,7 @@ class InferenceBackend:
|
|||
raise ValueError(
|
||||
f"Model '{self.active_model_name}' has no chat_template set in its "
|
||||
f"tokenizer_config.json. This is usually a problem with the model's "
|
||||
f"HuggingFace repository — it is missing a 'chat_template' key. "
|
||||
f"HuggingFace repository -- it is missing a 'chat_template' key. "
|
||||
f"Please use a model that includes a chat template, or manually set "
|
||||
f"one via tokenizer.chat_template before inference."
|
||||
)
|
||||
|
|
@ -1138,7 +1138,7 @@ class InferenceBackend:
|
|||
repetition_penalty,
|
||||
cancel_event = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""Handle audio input (ASR) generation — accepts audio numpy array, streams text output.
|
||||
"""Handle audio input (ASR) generation -- accepts audio numpy array, streams text output.
|
||||
|
||||
Uses processor.apply_chat_template with audio embedded in messages (Gemma 3n pattern).
|
||||
"""
|
||||
|
|
@ -1150,7 +1150,7 @@ class InferenceBackend:
|
|||
processor = model_info.get("processor") or model_info.get("tokenizer")
|
||||
raw_tokenizer = getattr(processor, "tokenizer", processor)
|
||||
|
||||
# Extract last user text — default matches notebook prompt
|
||||
# Extract last user text -- default matches notebook prompt
|
||||
user_text = "Please transcribe this audio."
|
||||
if messages:
|
||||
for msg in reversed(messages):
|
||||
|
|
@ -1162,7 +1162,7 @@ class InferenceBackend:
|
|||
if not system_prompt:
|
||||
system_prompt = "You are an assistant that transcribes speech accurately."
|
||||
|
||||
# Build messages in Gemma 3n format — audio goes INTO apply_chat_template
|
||||
# Build messages in Gemma 3n format -- audio goes INTO apply_chat_template
|
||||
audio_messages = [
|
||||
{"role": "system", "content": [{"type": "text", "text": system_prompt}]},
|
||||
{
|
||||
|
|
@ -1257,7 +1257,7 @@ class InferenceBackend:
|
|||
def generate_whisper_response(
|
||||
self, audio_array, cancel_event = None
|
||||
) -> Generator[str, None, None]:
|
||||
"""Whisper ASR — takes audio numpy array, yields transcribed text.
|
||||
"""Whisper ASR -- takes audio numpy array, yields transcribed text.
|
||||
|
||||
Uses the pre-built transformers pipeline (created during model loading).
|
||||
"""
|
||||
|
|
@ -1433,7 +1433,7 @@ class InferenceBackend:
|
|||
finally:
|
||||
# Only set cancel_event when we exited early (user cancel),
|
||||
# NOT on normal completion. cancel_event is a shared mp.Event
|
||||
# — setting it unconditionally would leave a stale cancel
|
||||
# -- setting it unconditionally would leave a stale cancel
|
||||
# signal that could interfere with the next serialized
|
||||
# generation request (e.g. in compare mode).
|
||||
if cancel_event is not None and not generation_complete:
|
||||
|
|
@ -1451,7 +1451,7 @@ class InferenceBackend:
|
|||
logger.error(f"Error during generation: {e}")
|
||||
yield f"Error: {str(e)}"
|
||||
|
||||
# ── Audio (TTS) Generation ────────────────────────────────────
|
||||
# -- Audio (TTS) Generation ------------------------------------
|
||||
|
||||
def generate_audio_response(
|
||||
self,
|
||||
|
|
@ -1467,7 +1467,7 @@ class InferenceBackend:
|
|||
"""
|
||||
Generate audio from text for TTS models.
|
||||
Returns (wav_bytes, sample_rate).
|
||||
Blocking — generates complete audio before returning.
|
||||
Blocking -- generates complete audio before returning.
|
||||
"""
|
||||
if not self.active_model_name:
|
||||
raise RuntimeError("No active model")
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ class LlamaCppBackend:
|
|||
Manages a llama-server subprocess for GGUF model inference.
|
||||
|
||||
Lifecycle:
|
||||
1. load_model() — starts llama-server with the GGUF file
|
||||
2. generate_chat_completion() — proxies to /v1/chat/completions, streams back
|
||||
3. unload_model() — terminates llama-server subprocess
|
||||
1. load_model() -- starts llama-server with the GGUF file
|
||||
2. generate_chat_completion() -- proxies to /v1/chat/completions, streams back
|
||||
3. unload_model() -- terminates llama-server subprocess
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -61,7 +61,7 @@ class LlamaCppBackend:
|
|||
self._kill_orphaned_servers()
|
||||
atexit.register(self._cleanup)
|
||||
|
||||
# ── Properties ────────────────────────────────────────────────
|
||||
# -- Properties ------------------------------------------------
|
||||
|
||||
@property
|
||||
def is_loaded(self) -> bool:
|
||||
|
|
@ -112,7 +112,7 @@ class LlamaCppBackend:
|
|||
def cache_type_kv(self) -> Optional[str]:
|
||||
return self._cache_type_kv
|
||||
|
||||
# ── Binary discovery ──────────────────────────────────────────
|
||||
# -- Binary discovery ------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _find_llama_server_binary() -> Optional[str]:
|
||||
|
|
@ -135,12 +135,12 @@ class LlamaCppBackend:
|
|||
|
||||
binary_name = "llama-server.exe" if sys.platform == "win32" else "llama-server"
|
||||
|
||||
# 1. Env var — direct path to binary
|
||||
# 1. Env var -- direct path to binary
|
||||
env_path = os.environ.get("LLAMA_SERVER_PATH")
|
||||
if env_path and Path(env_path).is_file():
|
||||
return env_path
|
||||
|
||||
# 1b. UNSLOTH_LLAMA_CPP_PATH — custom llama.cpp install directory
|
||||
# 1b. UNSLOTH_LLAMA_CPP_PATH -- custom llama.cpp install directory
|
||||
custom_llama_cpp = os.environ.get("UNSLOTH_LLAMA_CPP_PATH")
|
||||
if custom_llama_cpp:
|
||||
custom_dir = Path(custom_llama_cpp)
|
||||
|
|
@ -158,7 +158,7 @@ class LlamaCppBackend:
|
|||
if win_bin.is_file():
|
||||
return str(win_bin)
|
||||
|
||||
# 2–4. ~/.unsloth/llama.cpp (primary — setup.sh / setup.ps1 build here)
|
||||
# 2-4. ~/.unsloth/llama.cpp (primary -- setup.sh / setup.ps1 build here)
|
||||
unsloth_home = Path.home() / ".unsloth" / "llama.cpp"
|
||||
# Root dir (make builds copy binaries here)
|
||||
home_root = unsloth_home / binary_name
|
||||
|
|
@ -175,7 +175,7 @@ class LlamaCppBackend:
|
|||
if home_win.is_file():
|
||||
return str(home_win)
|
||||
|
||||
# 5–6. Legacy: in-tree build (older setup.sh / setup.ps1 versions)
|
||||
# 5-6. Legacy: in-tree build (older setup.sh / setup.ps1 versions)
|
||||
project_root = Path(__file__).resolve().parents[4]
|
||||
# Root dir (make builds)
|
||||
root_path = project_root / "llama.cpp" / binary_name
|
||||
|
|
@ -204,7 +204,7 @@ class LlamaCppBackend:
|
|||
|
||||
return None
|
||||
|
||||
# ── GPU allocation ────────────────────────────────────────────
|
||||
# -- GPU allocation --------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _get_gguf_size_bytes(model_path: str) -> int:
|
||||
|
|
@ -318,7 +318,7 @@ class LlamaCppBackend:
|
|||
# Model is too large even for all GPUs, let --fit handle it
|
||||
return None, True
|
||||
|
||||
# ── Variant fallback ────────────────────────────────────────────
|
||||
# -- Variant fallback --------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _find_smallest_fitting_variant(
|
||||
|
|
@ -374,7 +374,7 @@ class LlamaCppBackend:
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
# ── Port allocation ───────────────────────────────────────────
|
||||
# -- Port allocation -------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _find_free_port() -> int:
|
||||
|
|
@ -383,7 +383,7 @@ class LlamaCppBackend:
|
|||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
# ── Stdout drain (prevents pipe deadlock on Windows) ─────────
|
||||
# -- Stdout drain (prevents pipe deadlock on Windows) ---------
|
||||
|
||||
def _drain_stdout(self):
|
||||
"""
|
||||
|
|
@ -400,7 +400,7 @@ class LlamaCppBackend:
|
|||
self._stdout_lines.append(line)
|
||||
logger.debug(f"[llama-server] {line}")
|
||||
except (ValueError, OSError):
|
||||
# Pipe closed — process is terminating
|
||||
# Pipe closed -- process is terminating
|
||||
pass
|
||||
|
||||
# GGUF KV type sizes for fast skipping
|
||||
|
|
@ -530,7 +530,7 @@ class LlamaCppBackend:
|
|||
except Exception as e:
|
||||
logger.warning(f"Failed to read GGUF metadata: {e}")
|
||||
|
||||
# ── HF download (no lock held) ───────────────────────────────
|
||||
# -- HF download (no lock held) -------------------------------
|
||||
|
||||
def _download_gguf(
|
||||
self,
|
||||
|
|
@ -740,7 +740,7 @@ class LlamaCppBackend:
|
|||
logger.warning(f"Could not download mmproj: {e}")
|
||||
return None
|
||||
|
||||
# ── Lifecycle ─────────────────────────────────────────────────
|
||||
# -- Lifecycle -------------------------------------------------
|
||||
|
||||
def load_model(
|
||||
self,
|
||||
|
|
@ -776,7 +776,7 @@ class LlamaCppBackend:
|
|||
"""
|
||||
self._cancel_event.clear()
|
||||
|
||||
# ── Phase 1: kill old process (under lock, fast) ──────────
|
||||
# -- Phase 1: kill old process (under lock, fast) ----------
|
||||
with self._lock:
|
||||
self._kill_process()
|
||||
|
||||
|
|
@ -788,7 +788,7 @@ class LlamaCppBackend:
|
|||
"or set LLAMA_SERVER_PATH environment variable."
|
||||
)
|
||||
|
||||
# ── Phase 2: download (NO lock held, so cancel can proceed) ──
|
||||
# -- Phase 2: download (NO lock held, so cancel can proceed) --
|
||||
if hf_repo:
|
||||
model_path = self._download_gguf(
|
||||
hf_repo = hf_repo,
|
||||
|
|
@ -819,7 +819,7 @@ class LlamaCppBackend:
|
|||
logger.info("Load cancelled after download phase")
|
||||
return False
|
||||
|
||||
# ── Phase 3: start llama-server (under lock) ──────────────
|
||||
# -- Phase 3: start llama-server (under lock) --------------
|
||||
with self._lock:
|
||||
# Re-check cancel inside lock
|
||||
if self._cancel_event.is_set():
|
||||
|
|
@ -1163,7 +1163,7 @@ class LlamaCppBackend:
|
|||
logger.error(f"llama-server health check timed out after {timeout}s")
|
||||
return False
|
||||
|
||||
# ── Message building (OpenAI format) ──────────────────────────
|
||||
# -- Message building (OpenAI format) --------------------------
|
||||
|
||||
@staticmethod
|
||||
def _parse_tool_calls_from_text(content: str) -> list[dict]:
|
||||
|
|
@ -1327,7 +1327,7 @@ class LlamaCppBackend:
|
|||
|
||||
return result
|
||||
|
||||
# ── Generation (proxy to llama-server) ────────────────────────
|
||||
# -- Generation (proxy to llama-server) ------------------------
|
||||
|
||||
@staticmethod
|
||||
def _iter_text_cancellable(
|
||||
|
|
@ -1463,7 +1463,7 @@ class LlamaCppBackend:
|
|||
"""
|
||||
Send a chat completion request to llama-server and stream tokens back.
|
||||
|
||||
Uses /v1/chat/completions — llama-server handles chat template
|
||||
Uses /v1/chat/completions -- llama-server handles chat template
|
||||
application and vision (multimodal image_url parts) natively.
|
||||
|
||||
Yields cumulative text (matching InferenceBackend's convention).
|
||||
|
|
@ -1598,7 +1598,7 @@ class LlamaCppBackend:
|
|||
return
|
||||
raise
|
||||
|
||||
# ── Tool-calling agentic loop ──────────────────────────────
|
||||
# -- Tool-calling agentic loop ------------------------------
|
||||
|
||||
def generate_chat_completion_with_tools(
|
||||
self,
|
||||
|
|
@ -2028,7 +2028,7 @@ class LlamaCppBackend:
|
|||
return
|
||||
raise
|
||||
|
||||
# ── TTS support ────────────────────────────────────────────
|
||||
# -- TTS support --------------------------------------------
|
||||
|
||||
def detect_audio_type(self) -> Optional[str]:
|
||||
"""Detect audio/TTS codec by probing the loaded model's vocabulary."""
|
||||
|
|
@ -2103,7 +2103,7 @@ class LlamaCppBackend:
|
|||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
model_repo_path = None
|
||||
|
||||
# BiCodec needs a repo with BiCodec/ weights — download canonical SparkTTS
|
||||
# BiCodec needs a repo with BiCodec/ weights -- download canonical SparkTTS
|
||||
if audio_type == "bicodec":
|
||||
from huggingface_hub import snapshot_download
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""
|
||||
Inference orchestrator — subprocess-based.
|
||||
Inference orchestrator -- subprocess-based.
|
||||
|
||||
Provides the same API as InferenceBackend, but delegates all ML work
|
||||
to a persistent subprocess. The subprocess is spawned on first model load
|
||||
|
|
@ -42,7 +42,7 @@ _DISPATCH_DRAIN_TIMEOUT = 5.0
|
|||
|
||||
class InferenceOrchestrator:
|
||||
"""
|
||||
Inference backend orchestrator — subprocess-based.
|
||||
Inference backend orchestrator -- subprocess-based.
|
||||
|
||||
Exposes the same API surface as InferenceBackend so routes/inference.py
|
||||
needs minimal changes. Internally, all heavy ML operations happen in
|
||||
|
|
@ -54,13 +54,13 @@ class InferenceOrchestrator:
|
|||
self._proc: Optional[mp.Process] = None
|
||||
self._cmd_queue: Any = None
|
||||
self._resp_queue: Any = None
|
||||
self._cancel_event: Any = None # mp.Event — set to cancel generation instantly
|
||||
self._cancel_event: Any = None # mp.Event -- set to cancel generation instantly
|
||||
self._lock = threading.Lock()
|
||||
self._gen_lock = (
|
||||
threading.Lock()
|
||||
) # Serializes generation — one request at a time
|
||||
) # Serializes generation -- one request at a time
|
||||
|
||||
# Dispatcher state — for compare mode (adapter-controlled requests).
|
||||
# Dispatcher state -- for compare mode (adapter-controlled requests).
|
||||
# Instead of serializing via _gen_lock, adapter-controlled requests
|
||||
# send commands directly to the subprocess and read from per-request
|
||||
# mailboxes. A dispatcher thread routes resp_queue events by request_id.
|
||||
|
|
@ -294,7 +294,7 @@ class InferenceOrchestrator:
|
|||
logger.info("Subprocess status: %s", resp.get("message", ""))
|
||||
continue
|
||||
|
||||
# Other response types during wait — skip
|
||||
# Other response types during wait -- skip
|
||||
logger.debug(
|
||||
"Skipping response type '%s' while waiting for '%s'",
|
||||
rtype,
|
||||
|
|
@ -337,7 +337,7 @@ class InferenceOrchestrator:
|
|||
logger.warning("Timed out waiting for gen_done after cancel")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dispatcher — per-request mailbox routing for compare mode
|
||||
# Dispatcher -- per-request mailbox routing for compare mode
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _start_dispatcher(self) -> None:
|
||||
|
|
@ -385,7 +385,7 @@ class InferenceOrchestrator:
|
|||
rid = resp.get("request_id")
|
||||
rtype = resp.get("type", "")
|
||||
|
||||
# Status messages — log and skip
|
||||
# Status messages -- log and skip
|
||||
if rtype == "status":
|
||||
logger.info("Subprocess status: %s", resp.get("message", ""))
|
||||
continue
|
||||
|
|
@ -398,7 +398,7 @@ class InferenceOrchestrator:
|
|||
mbox.put(resp)
|
||||
continue
|
||||
|
||||
# No matching mailbox — might be for a _gen_lock reader or orphaned
|
||||
# No matching mailbox -- might be for a _gen_lock reader or orphaned
|
||||
# Push it back so _read_resp can pick it up. But we can't un-get
|
||||
# from mp.Queue, so log a warning.
|
||||
if rtype not in ("status",):
|
||||
|
|
@ -422,14 +422,14 @@ class InferenceOrchestrator:
|
|||
cancel_event = None,
|
||||
use_adapter = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""Dispatched generation — sends command without holding _gen_lock.
|
||||
"""Dispatched generation -- sends command without holding _gen_lock.
|
||||
|
||||
Uses a per-request mailbox to receive tokens. This allows two
|
||||
compare-mode requests to be queued in the subprocess simultaneously,
|
||||
eliminating the inter-generation round-trip overhead.
|
||||
|
||||
The subprocess processes commands sequentially from its cmd_queue,
|
||||
so generation is still serialized at the GPU level — we just avoid
|
||||
so generation is still serialized at the GPU level -- we just avoid
|
||||
the orchestrator-level lock contention.
|
||||
"""
|
||||
if not self._ensure_subprocess_alive():
|
||||
|
|
@ -486,7 +486,7 @@ class InferenceOrchestrator:
|
|||
try:
|
||||
resp = mailbox.get(timeout = _DISPATCH_READ_TIMEOUT)
|
||||
except queue.Empty:
|
||||
# Timeout — check subprocess health
|
||||
# Timeout -- check subprocess health
|
||||
if not self._ensure_subprocess_alive():
|
||||
yield "Error: Inference subprocess crashed during generation"
|
||||
return
|
||||
|
|
@ -560,7 +560,7 @@ class InferenceOrchestrator:
|
|||
self._stop_dispatcher()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API — same interface as InferenceBackend
|
||||
# Public API -- same interface as InferenceBackend
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def load_model(
|
||||
|
|
@ -575,7 +575,7 @@ class InferenceOrchestrator:
|
|||
"""Load a model for inference.
|
||||
|
||||
Always spawns a fresh subprocess for each model load. This ensures
|
||||
a clean Python interpreter — no stale unsloth patches, torch.compile
|
||||
a clean Python interpreter -- no stale unsloth patches, torch.compile
|
||||
caches, or inspect.getsource() failures from a previous model.
|
||||
"""
|
||||
from utils.transformers_version import needs_transformers_5
|
||||
|
|
@ -605,7 +605,7 @@ class InferenceOrchestrator:
|
|||
self._shutdown_subprocess()
|
||||
|
||||
elif self._proc is not None:
|
||||
# Dead subprocess — clean up
|
||||
# Dead subprocess -- clean up
|
||||
self._shutdown_subprocess(timeout = 2)
|
||||
|
||||
logger.info(
|
||||
|
|
@ -648,7 +648,7 @@ class InferenceOrchestrator:
|
|||
def unload_model(self, model_name: str) -> bool:
|
||||
"""Unload a model from the subprocess."""
|
||||
if not self._ensure_subprocess_alive():
|
||||
# No subprocess — just clear local state
|
||||
# No subprocess -- just clear local state
|
||||
self.models.pop(model_name, None)
|
||||
if self.active_model_name == model_name:
|
||||
self.active_model_name = None
|
||||
|
|
@ -739,7 +739,7 @@ class InferenceOrchestrator:
|
|||
cancel_event = None,
|
||||
use_adapter = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""Inner generation logic — sends command to subprocess, yields tokens.
|
||||
"""Inner generation logic -- sends command to subprocess, yields tokens.
|
||||
|
||||
Serialized by _gen_lock: only one generation runs at a time.
|
||||
This prevents concurrent readers from consuming each other's
|
||||
|
|
@ -758,7 +758,7 @@ class InferenceOrchestrator:
|
|||
# so we can safely read from resp_queue directly.
|
||||
self._wait_dispatcher_idle()
|
||||
|
||||
# Serialize generation — single GPU, one generation at a time.
|
||||
# Serialize generation -- single GPU, one generation at a time.
|
||||
# Without this lock, two concurrent readers on the same resp_queue
|
||||
# can consume and drop each other's token events.
|
||||
with self._gen_lock:
|
||||
|
|
@ -790,7 +790,7 @@ class InferenceOrchestrator:
|
|||
cancel_event = None,
|
||||
use_adapter = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""Actual generation logic — must be called under _gen_lock."""
|
||||
"""Actual generation logic -- must be called under _gen_lock."""
|
||||
request_id = str(uuid.uuid4())
|
||||
|
||||
# Convert PIL Image to base64 if needed
|
||||
|
|
@ -821,7 +821,7 @@ class InferenceOrchestrator:
|
|||
yield f"Error: {exc}"
|
||||
return
|
||||
|
||||
# Yield tokens from response queue — we are the only reader
|
||||
# Yield tokens from response queue -- we are the only reader
|
||||
# because _gen_lock is held.
|
||||
while True:
|
||||
resp = self._read_resp(timeout = 30.0)
|
||||
|
|
@ -835,7 +835,7 @@ class InferenceOrchestrator:
|
|||
|
||||
rtype = resp.get("type", "")
|
||||
|
||||
# Status messages — skip
|
||||
# Status messages -- skip
|
||||
if rtype == "status":
|
||||
continue
|
||||
|
||||
|
|
@ -874,7 +874,7 @@ class InferenceOrchestrator:
|
|||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Audio generation — TTS, ASR, audio input
|
||||
# Audio generation -- TTS, ASR, audio input
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def generate_audio_response(
|
||||
|
|
@ -890,7 +890,7 @@ class InferenceOrchestrator:
|
|||
) -> Tuple[bytes, int]:
|
||||
"""Generate TTS audio. Returns (wav_bytes, sample_rate).
|
||||
|
||||
Blocking — sends command and waits for the complete audio response.
|
||||
Blocking -- sends command and waits for the complete audio response.
|
||||
"""
|
||||
if not self._ensure_subprocess_alive():
|
||||
raise RuntimeError("Inference subprocess is not running")
|
||||
|
|
@ -953,7 +953,7 @@ class InferenceOrchestrator:
|
|||
audio_array,
|
||||
cancel_event = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""Whisper ASR — sends audio to subprocess, yields text."""
|
||||
"""Whisper ASR -- sends audio to subprocess, yields text."""
|
||||
yield from self._generate_audio_input_inner(
|
||||
audio_array = audio_array,
|
||||
audio_type = "whisper",
|
||||
|
|
@ -975,7 +975,7 @@ class InferenceOrchestrator:
|
|||
repetition_penalty: float = 1.0,
|
||||
cancel_event = None,
|
||||
) -> Generator[str, None, None]:
|
||||
"""Audio input generation (e.g. Gemma 3n) — streams text tokens."""
|
||||
"""Audio input generation (e.g. Gemma 3n) -- streams text tokens."""
|
||||
yield from self._generate_audio_input_inner(
|
||||
audio_array = audio_array,
|
||||
audio_type = None, # worker will use generate_audio_input_response
|
||||
|
|
@ -1045,7 +1045,7 @@ class InferenceOrchestrator:
|
|||
yield f"Error: {exc}"
|
||||
return
|
||||
|
||||
# Yield tokens — same pattern as _generate_locked
|
||||
# Yield tokens -- same pattern as _generate_locked
|
||||
while True:
|
||||
resp = self._read_resp(timeout = 30.0)
|
||||
|
||||
|
|
@ -1084,7 +1084,7 @@ class InferenceOrchestrator:
|
|||
|
||||
def resize_image(self, img, max_size: int = 800):
|
||||
"""Resize image while maintaining aspect ratio.
|
||||
No ML imports needed — runs locally in parent process.
|
||||
No ML imports needed -- runs locally in parent process.
|
||||
"""
|
||||
if img is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Inference subprocess entry point.
|
||||
|
||||
Each inference session runs in a persistent subprocess (mp.get_context("spawn")).
|
||||
This gives us a clean Python interpreter with no stale module state —
|
||||
This gives us a clean Python interpreter with no stale module state --
|
||||
solving the transformers version-switching problem completely.
|
||||
|
||||
The subprocess stays alive while a model is loaded, accepting commands
|
||||
|
|
@ -135,12 +135,12 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
|
|||
training_method = adapter_cfg.get("unsloth_training_method")
|
||||
if training_method == "lora" and load_in_4bit:
|
||||
logger.info(
|
||||
"adapter_config.json says lora — setting load_in_4bit=False"
|
||||
"adapter_config.json says lora -- setting load_in_4bit=False"
|
||||
)
|
||||
load_in_4bit = False
|
||||
elif training_method == "qlora" and not load_in_4bit:
|
||||
logger.info(
|
||||
"adapter_config.json says qlora — setting load_in_4bit=True"
|
||||
"adapter_config.json says qlora -- setting load_in_4bit=True"
|
||||
)
|
||||
load_in_4bit = True
|
||||
elif not training_method:
|
||||
|
|
@ -150,7 +150,7 @@ def _handle_load(backend, config: dict, resp_queue: Any) -> None:
|
|||
and load_in_4bit
|
||||
):
|
||||
logger.info(
|
||||
"No training method, base model has no -bnb-4bit — setting load_in_4bit=False"
|
||||
"No training method, base model has no -bnb-4bit -- setting load_in_4bit=False"
|
||||
)
|
||||
load_in_4bit = False
|
||||
except Exception as e:
|
||||
|
|
@ -259,7 +259,7 @@ def _handle_generate(
|
|||
logger.info("Starting text generation for request_id=%s", request_id)
|
||||
|
||||
for cumulative_text in generator:
|
||||
# cancel_event is an mp.Event — checked instantly, no queue polling
|
||||
# cancel_event is an mp.Event -- checked instantly, no queue polling
|
||||
if cancel_event.is_set():
|
||||
logger.info("Generation cancelled for request %s", request_id)
|
||||
break
|
||||
|
|
@ -303,7 +303,7 @@ def _handle_generate_audio(
|
|||
cmd: dict,
|
||||
resp_queue: Any,
|
||||
) -> None:
|
||||
"""Handle TTS audio generation — returns WAV bytes + sample_rate."""
|
||||
"""Handle TTS audio generation -- returns WAV bytes + sample_rate."""
|
||||
request_id = cmd.get("request_id", "")
|
||||
try:
|
||||
logger.info("Starting audio generation for request_id=%s", request_id)
|
||||
|
|
@ -351,7 +351,7 @@ def _handle_generate_audio_input(
|
|||
resp_queue: Any,
|
||||
cancel_event,
|
||||
) -> None:
|
||||
"""Handle audio input generation (ASR/Whisper) — streams text tokens back."""
|
||||
"""Handle audio input generation (ASR/Whisper) -- streams text tokens back."""
|
||||
request_id = cmd.get("request_id", "")
|
||||
|
||||
try:
|
||||
|
|
@ -461,12 +461,12 @@ def run_inference_process(
|
|||
cancel_event,
|
||||
config: dict,
|
||||
) -> None:
|
||||
"""Subprocess entrypoint. Persistent — runs command loop until shutdown.
|
||||
"""Subprocess entrypoint. Persistent -- runs command loop until shutdown.
|
||||
|
||||
Args:
|
||||
cmd_queue: mp.Queue for receiving commands from parent.
|
||||
resp_queue: mp.Queue for sending responses to parent.
|
||||
cancel_event: mp.Event shared with parent — set by parent to cancel generation.
|
||||
cancel_event: mp.Event shared with parent -- set by parent to cancel generation.
|
||||
config: Initial configuration dict with model info.
|
||||
"""
|
||||
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||
|
|
@ -487,7 +487,7 @@ def run_inference_process(
|
|||
|
||||
model_name = config["model_name"]
|
||||
|
||||
# ── 1. Activate correct transformers version BEFORE any ML imports ──
|
||||
# -- 1. Activate correct transformers version BEFORE any ML imports --
|
||||
try:
|
||||
_activate_transformers_version(model_name)
|
||||
except Exception as exc:
|
||||
|
|
@ -502,20 +502,20 @@ def run_inference_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 1b. On Windows, check Triton availability (must be before import torch) ──
|
||||
# -- 1b. On Windows, check Triton availability (must be before import torch) --
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import triton # noqa: F401
|
||||
|
||||
logger.info("Triton available — torch.compile enabled")
|
||||
logger.info("Triton available -- torch.compile enabled")
|
||||
except ImportError:
|
||||
os.environ["TORCHDYNAMO_DISABLE"] = "1"
|
||||
logger.warning(
|
||||
"Triton not found on Windows — torch.compile disabled. "
|
||||
"Triton not found on Windows -- torch.compile disabled. "
|
||||
'Install for better performance: pip install "triton-windows<3.7"'
|
||||
)
|
||||
|
||||
# ── 2. Import ML libraries (fresh in this clean process) ──
|
||||
# -- 2. Import ML libraries (fresh in this clean process) --
|
||||
try:
|
||||
_send_response(
|
||||
resp_queue,
|
||||
|
|
@ -548,7 +548,7 @@ def run_inference_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 3. Create inference backend and load initial model ──
|
||||
# -- 3. Create inference backend and load initial model --
|
||||
try:
|
||||
backend = InferenceBackend()
|
||||
|
||||
|
|
@ -575,8 +575,8 @@ def run_inference_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 4. Command loop — process commands until shutdown ──
|
||||
# cancel_event is an mp.Event shared with parent — parent can set it
|
||||
# -- 4. Command loop -- process commands until shutdown --
|
||||
# cancel_event is an mp.Event shared with parent -- parent can set it
|
||||
# at any time to cancel generation instantly (no queue polling needed).
|
||||
logger.info("Inference subprocess ready, entering command loop")
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ class UnslothTrainer:
|
|||
is_dataset_audio: bool = False,
|
||||
trust_remote_code: bool = False,
|
||||
) -> None:
|
||||
"""Lightweight detection and tokenizer load — no model weights, no VRAM.
|
||||
"""Lightweight detection and tokenizer load -- no model weights, no VRAM.
|
||||
|
||||
Sets is_vlm, _audio_type, is_audio_vlm, model_name and loads a
|
||||
lightweight tokenizer for dataset formatting. Call this before
|
||||
|
|
@ -159,7 +159,7 @@ class UnslothTrainer:
|
|||
BEFORE loading the training model (avoids VRAM contention with
|
||||
the LLM-assisted detection helper).
|
||||
|
||||
load_model() may be called afterwards — it will re-detect and load
|
||||
load_model() may be called afterwards -- it will re-detect and load
|
||||
the full model + tokenizer, overwriting the lightweight one set here.
|
||||
"""
|
||||
self.model_name = model_name
|
||||
|
|
@ -513,7 +513,7 @@ class UnslothTrainer:
|
|||
# the compiled cache. unsloth_compile_transformers() sets __UNSLOTH_PATCHED__
|
||||
# on each modeling module and replaces methods with exec'd code.
|
||||
# clear_unsloth_compiled_cache() deletes the disk cache, but the flag
|
||||
# prevents re-compilation — leaving missing cache files. Reloading
|
||||
# prevents re-compilation -- leaving missing cache files. Reloading
|
||||
# restores original class definitions so Unsloth can re-compile cleanly.
|
||||
import sys as _sys
|
||||
import importlib
|
||||
|
|
@ -524,7 +524,7 @@ class UnslothTrainer:
|
|||
try:
|
||||
importlib.reload(_mod)
|
||||
except Exception:
|
||||
pass # Non-critical — Unsloth will handle stale modules
|
||||
pass # Non-critical -- Unsloth will handle stale modules
|
||||
|
||||
# Remove stale compiled cache so the new model gets a fresh one
|
||||
from utils.cache_cleanup import clear_unsloth_compiled_cache
|
||||
|
|
@ -733,7 +733,7 @@ class UnslothTrainer:
|
|||
|
||||
elif self.is_audio_vlm:
|
||||
# Audio VLM: multimodal model trained on audio (e.g. Gemma 3N)
|
||||
# Uses FastModel (general loader) — returns (model, processor)
|
||||
# Uses FastModel (general loader) -- returns (model, processor)
|
||||
from unsloth import FastModel
|
||||
|
||||
self.model, self.tokenizer = FastModel.from_pretrained(
|
||||
|
|
@ -814,7 +814,7 @@ class UnslothTrainer:
|
|||
# second attempt because the failed first call's partial
|
||||
# imports clean up the stale state as a side effect.
|
||||
self._source_code_retried = True
|
||||
logger.info(f"\n'could not get source code' — retrying once...\n")
|
||||
logger.info(f"\n'could not get source code' -- retrying once...\n")
|
||||
return self.load_model(
|
||||
model_name = model_name,
|
||||
max_seq_length = max_seq_length,
|
||||
|
|
@ -1324,7 +1324,7 @@ class UnslothTrainer:
|
|||
trust_remote_code = getattr(self, "trust_remote_code", False),
|
||||
)
|
||||
|
||||
# Strip pad_to_multiple_of from tokenizer init_kwargs — fine-tuned models
|
||||
# Strip pad_to_multiple_of from tokenizer init_kwargs -- fine-tuned models
|
||||
# (e.g. keanteng/sesame-csm-elise) save it in tokenizer_config.json, and
|
||||
# _merge_kwargs leaks it into audio_kwargs where EncodecFeatureExtractor rejects it.
|
||||
processor.tokenizer.init_kwargs.pop("pad_to_multiple_of", None)
|
||||
|
|
@ -1383,7 +1383,7 @@ class UnslothTrainer:
|
|||
],
|
||||
}
|
||||
]
|
||||
# NOTE: pad_to_multiple_of intentionally omitted from text_kwargs —
|
||||
# NOTE: pad_to_multiple_of intentionally omitted from text_kwargs --
|
||||
# CsmProcessor._merge_kwargs leaks it to EncodecFeatureExtractor which rejects it.
|
||||
model_inputs = processor.apply_chat_template(
|
||||
conversation,
|
||||
|
|
@ -1654,7 +1654,7 @@ class UnslothTrainer:
|
|||
# Truncate to max_length
|
||||
input_ids = input_ids[:max_length]
|
||||
|
||||
# Labels = input_ids (no masking — Orpheus trains on full sequence)
|
||||
# Labels = input_ids (no masking -- Orpheus trains on full sequence)
|
||||
labels = list(input_ids)
|
||||
attention_mask = [1] * len(input_ids)
|
||||
|
||||
|
|
@ -1753,7 +1753,7 @@ class UnslothTrainer:
|
|||
)
|
||||
|
||||
# Cast audio column so datasets 4.x AudioDecoder objects are decoded to dicts.
|
||||
# Don't resample here — BiCodec's target_sr may differ; the loop handles resampling.
|
||||
# Don't resample here -- BiCodec's target_sr may differ; the loop handles resampling.
|
||||
from datasets import Audio
|
||||
|
||||
dataset = dataset.cast_column(audio_col, Audio())
|
||||
|
|
@ -2305,7 +2305,7 @@ class UnslothTrainer:
|
|||
"""
|
||||
Load and prepare dataset for training.
|
||||
|
||||
Strategy: format first, then split — ensures both train and eval
|
||||
Strategy: format first, then split -- ensures both train and eval
|
||||
portions are properly formatted and templated.
|
||||
|
||||
Returns:
|
||||
|
|
@ -2368,7 +2368,7 @@ class UnslothTrainer:
|
|||
and dataset_slice_end >= 0
|
||||
and dataset_slice_end >= _slice_start
|
||||
):
|
||||
# Manual slice — stream only the rows we need instead of
|
||||
# Manual slice -- stream only the rows we need instead of
|
||||
# downloading the entire dataset.
|
||||
rows_to_stream = dataset_slice_end + 1
|
||||
logger.info(
|
||||
|
|
@ -2419,9 +2419,9 @@ class UnslothTrainer:
|
|||
f"Loaded eval split '{eval_split}' with {len(eval_dataset)} rows\n"
|
||||
)
|
||||
elif eval_split and eval_split == effective_train:
|
||||
# Same split as training — will do 80/20 split after formatting
|
||||
# Same split as training -- will do 80/20 split after formatting
|
||||
logger.info(
|
||||
f"Eval split '{eval_split}' is the same as train split — will split 80/20\n"
|
||||
f"Eval split '{eval_split}' is the same as train split -- will split 80/20\n"
|
||||
)
|
||||
else:
|
||||
# Auto-detect eval split from HF (returns a separate dataset, or None)
|
||||
|
|
@ -2538,7 +2538,7 @@ class UnslothTrainer:
|
|||
|
||||
# ========== THEN SPLIT ==========
|
||||
if has_separate_eval_source and eval_dataset is not None:
|
||||
# Eval came from a separate HF split — format it too
|
||||
# Eval came from a separate HF split -- format it too
|
||||
logger.info(f"Formatting eval dataset ({len(eval_dataset)} rows)...\n")
|
||||
eval_info = format_and_template_dataset(
|
||||
eval_dataset,
|
||||
|
|
@ -2552,7 +2552,7 @@ class UnslothTrainer:
|
|||
eval_dataset = eval_info["dataset"]
|
||||
logger.info(f"Eval dataset formatted successfully\n")
|
||||
elif eval_enabled and not has_separate_eval_source:
|
||||
# No separate eval source — split the already-formatted dataset
|
||||
# No separate eval source -- split the already-formatted dataset
|
||||
formatted_dataset = dataset_info["dataset"]
|
||||
split_result = self._resolve_eval_split_from_dataset(formatted_dataset)
|
||||
if split_result is not None:
|
||||
|
|
@ -2599,7 +2599,7 @@ class UnslothTrainer:
|
|||
except Exception as e:
|
||||
logger.warning(f"Could not check dataset splits: {e}")
|
||||
|
||||
# No separate HF eval split found — caller will handle programmatic splitting
|
||||
# No separate HF eval split found -- caller will handle programmatic splitting
|
||||
return None
|
||||
|
||||
def _resolve_eval_split_from_dataset(self, dataset) -> Optional[tuple]:
|
||||
|
|
@ -2666,7 +2666,7 @@ class UnslothTrainer:
|
|||
# Unsloth's patched_import hook (deepseek_v3_moe.py) is not thread-safe
|
||||
# with Python's importlib cache, causing KeyError: 'size' if these are
|
||||
# first imported inside the worker thread.
|
||||
import transformers # noqa: F401 – ensures submodules are cached
|
||||
import transformers # noqa: F401 - ensures submodules are cached
|
||||
from transformers import ( # noqa: F401
|
||||
Trainer as _HFTrainer,
|
||||
TrainingArguments as _TrainingArguments,
|
||||
|
|
@ -2796,7 +2796,7 @@ class UnslothTrainer:
|
|||
return
|
||||
|
||||
elif self._audio_type == "snac":
|
||||
# Orpheus: language model with SNAC codec tokens — plain HF Trainer
|
||||
# Orpheus: language model with SNAC codec tokens -- plain HF Trainer
|
||||
# DataCollatorForSeq2Seq dynamically pads variable-length sequences per batch
|
||||
# (text + audio codes vary in length) and pads labels with -100.
|
||||
from transformers import (
|
||||
|
|
@ -3083,7 +3083,7 @@ class UnslothTrainer:
|
|||
)
|
||||
logger.info("To enable evaluation, set eval_steps > 0.0\n")
|
||||
else:
|
||||
logger.info("No eval dataset — evaluation disabled\n")
|
||||
logger.info("No eval dataset -- evaluation disabled\n")
|
||||
|
||||
# Add model-specific parameters
|
||||
# Use optim and lr_scheduler_type from training_args if provided, otherwise use defaults
|
||||
|
|
@ -3130,7 +3130,7 @@ class UnslothTrainer:
|
|||
f"Sequence packing: {'enabled' if packing_enabled else 'disabled'}\n"
|
||||
)
|
||||
|
||||
# Audio codec overrides — BiCodec/DAC use the text SFTTrainer path
|
||||
# Audio codec overrides -- BiCodec/DAC use the text SFTTrainer path
|
||||
if self._audio_type == "bicodec":
|
||||
config_args["packing"] = False
|
||||
logger.info("Applied BiCodec overrides: packing=False\n")
|
||||
|
|
@ -3284,7 +3284,7 @@ class UnslothTrainer:
|
|||
)
|
||||
logger.info("Train on responses only configured successfully\n")
|
||||
|
||||
# ── Safety net: check if all samples were filtered out ──
|
||||
# -- Safety net: check if all samples were filtered out --
|
||||
# Unsloth's train_on_responses_only masks non-response
|
||||
# tokens with -100. If max_seq_length is too short and the
|
||||
# response portion gets truncated away, EVERY sample ends
|
||||
|
|
@ -3304,7 +3304,7 @@ class UnslothTrainer:
|
|||
error_msg = (
|
||||
f"{dropped}/{original_len} samples ({drop_pct}%) "
|
||||
f"were dropped after applying 'train on responses "
|
||||
f"only' — only {filtered_len} remain. This usually "
|
||||
f"only' -- only {filtered_len} remain. This usually "
|
||||
f"means max_seq_length ({max_seq}) is too short "
|
||||
f"and the response portion is being truncated "
|
||||
f"away. Try increasing max_seq_length (e.g. 8192) "
|
||||
|
|
@ -3394,7 +3394,7 @@ class UnslothTrainer:
|
|||
"""
|
||||
config_path = os.path.join(output_dir, "adapter_config.json")
|
||||
if not os.path.exists(config_path):
|
||||
logger.info("No adapter_config.json found — skipping training method patch")
|
||||
logger.info("No adapter_config.json found -- skipping training method patch")
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""
|
||||
Training backend — subprocess orchestrator.
|
||||
Training backend -- subprocess orchestrator.
|
||||
|
||||
Each training job runs in a fresh subprocess (mp.get_context("spawn")),
|
||||
solving the transformers version-switching problem. The old in-process
|
||||
|
|
@ -38,7 +38,7 @@ PLOT_HEIGHT = 3.5
|
|||
|
||||
@dataclass
|
||||
class TrainingProgress:
|
||||
"""Mirror of trainer.TrainingProgress — kept here so the parent process
|
||||
"""Mirror of trainer.TrainingProgress -- kept here so the parent process
|
||||
never needs to import the heavy ML modules."""
|
||||
|
||||
epoch: float = 0
|
||||
|
|
@ -59,7 +59,7 @@ class TrainingProgress:
|
|||
|
||||
class TrainingBackend:
|
||||
"""
|
||||
Training orchestration backend — subprocess-based.
|
||||
Training orchestration backend -- subprocess-based.
|
||||
Launches a fresh subprocess per training job, communicates via mp.Queue.
|
||||
"""
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ class TrainingBackend:
|
|||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Compatibility shims — routes/training.py accesses these
|
||||
# Compatibility shims -- routes/training.py accesses these
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class _TrainerShim:
|
||||
|
|
@ -369,11 +369,11 @@ class TrainingBackend:
|
|||
self._handle_event(event)
|
||||
continue
|
||||
|
||||
# No event — check if process is still alive
|
||||
# No event -- check if process is still alive
|
||||
if self._proc.is_alive():
|
||||
continue
|
||||
|
||||
# Process exited — drain remaining events
|
||||
# Process exited -- drain remaining events
|
||||
for e in self._drain_queue(self._event_queue):
|
||||
self._handle_event(e)
|
||||
|
||||
|
|
@ -610,7 +610,7 @@ class TrainingBackend:
|
|||
checkpoint on disk. This is a no-op placeholder.
|
||||
"""
|
||||
logger.info(
|
||||
"_transfer_to_inference_backend: subprocess training — "
|
||||
"_transfer_to_inference_backend: subprocess training -- "
|
||||
"model must be loaded from disk (output_dir=%s)",
|
||||
self._output_dir,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
Training subprocess entry point.
|
||||
|
||||
Each training job runs in a fresh subprocess (mp.get_context("spawn")).
|
||||
This gives us a clean Python interpreter with no stale module state —
|
||||
This gives us a clean Python interpreter with no stale module state --
|
||||
solving the transformers version-switching problem completely.
|
||||
|
||||
Pattern follows core/data_recipe/jobs/worker.py.
|
||||
|
|
@ -65,7 +65,7 @@ def run_training_process(
|
|||
stop_queue: Any,
|
||||
config: dict,
|
||||
) -> None:
|
||||
"""Subprocess entrypoint. Fresh Python — no stale module state.
|
||||
"""Subprocess entrypoint. Fresh Python -- no stale module state.
|
||||
|
||||
Args:
|
||||
event_queue: mp.Queue for sending progress/status/error events to parent.
|
||||
|
|
@ -90,7 +90,7 @@ def run_training_process(
|
|||
|
||||
model_name = config["model_name"]
|
||||
|
||||
# ── 1. Activate correct transformers version BEFORE any ML imports ──
|
||||
# -- 1. Activate correct transformers version BEFORE any ML imports --
|
||||
try:
|
||||
_activate_transformers_version(model_name)
|
||||
except Exception as exc:
|
||||
|
|
@ -104,7 +104,7 @@ def run_training_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 1a. Auto-enable trust_remote_code for unsloth/* transformers 5.x models ──
|
||||
# -- 1a. Auto-enable trust_remote_code for unsloth/* transformers 5.x models --
|
||||
# Some newer architectures (e.g. NemotronH) have config parsing bugs in
|
||||
# transformers that require trust_remote_code=True as a workaround.
|
||||
# Only auto-enable for unsloth/* prefixed models (trusted source).
|
||||
|
|
@ -121,7 +121,7 @@ def run_training_process(
|
|||
model_name,
|
||||
)
|
||||
|
||||
# ── 1b. Auto-install mamba-ssm for SSM/hybrid models (NemotronH, Falcon-H1) ──
|
||||
# -- 1b. Auto-install mamba-ssm for SSM/hybrid models (NemotronH, Falcon-H1) --
|
||||
_SSM_MODEL_SUBSTRINGS = ("nemotron_h", "nemotron-3-nano", "falcon_h1", "falcon-h1")
|
||||
if any(sub in model_name.lower() for sub in _SSM_MODEL_SUBSTRINGS):
|
||||
try:
|
||||
|
|
@ -130,7 +130,7 @@ def run_training_process(
|
|||
logger.info("mamba-ssm already installed")
|
||||
except ImportError:
|
||||
logger.info(
|
||||
"SSM model detected — installing mamba-ssm and causal-conv1d (this may take several minutes)..."
|
||||
"SSM model detected -- installing mamba-ssm and causal-conv1d (this may take several minutes)..."
|
||||
)
|
||||
_send_status(
|
||||
event_queue, "Installing mamba-ssm (first time only, ~7 min)..."
|
||||
|
|
@ -161,7 +161,7 @@ def run_training_process(
|
|||
logger.info("Installed %s successfully", _pkg)
|
||||
logger.info("mamba-ssm installation complete")
|
||||
|
||||
# ── 1c. Set fork start method so dataset.map() can multiprocess ──
|
||||
# -- 1c. Set fork start method so dataset.map() can multiprocess --
|
||||
# The parent launched us via spawn (clean process), but the compiled
|
||||
# SFTTrainer checks get_start_method() and disables num_proc if not "fork".
|
||||
# Linux only: fork is the default start method and is safe here (no CUDA
|
||||
|
|
@ -176,20 +176,20 @@ def run_training_process(
|
|||
except RuntimeError:
|
||||
pass # Already set
|
||||
|
||||
# ── 1c. On Windows, check Triton availability (must be before import torch) ──
|
||||
# -- 1c. On Windows, check Triton availability (must be before import torch) --
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import triton # noqa: F401
|
||||
|
||||
logger.info("Triton available — torch.compile enabled")
|
||||
logger.info("Triton available -- torch.compile enabled")
|
||||
except ImportError:
|
||||
os.environ["TORCHDYNAMO_DISABLE"] = "1"
|
||||
logger.warning(
|
||||
"Triton not found on Windows — torch.compile disabled. "
|
||||
"Triton not found on Windows -- torch.compile disabled. "
|
||||
'Install for better performance: pip install "triton-windows<3.7"'
|
||||
)
|
||||
|
||||
# ── 2. Now import ML libraries (fresh in this clean process) ──
|
||||
# -- 2. Now import ML libraries (fresh in this clean process) --
|
||||
try:
|
||||
_send_status(event_queue, "Importing Unsloth...")
|
||||
|
||||
|
|
@ -219,7 +219,7 @@ def run_training_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 2b. EMBEDDING MODEL FAST-PATH ──
|
||||
# -- 2b. EMBEDDING MODEL FAST-PATH --
|
||||
# Embedding models use a completely different pipeline (FastSentenceTransformer
|
||||
# + SentenceTransformerTrainer + MultipleNegativesRankingLoss) so we branch
|
||||
# early and handle the entire flow in a self-contained function.
|
||||
|
|
@ -237,7 +237,7 @@ def run_training_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 3. Create a fresh trainer instance ──
|
||||
# -- 3. Create a fresh trainer instance --
|
||||
trainer = UnslothTrainer()
|
||||
|
||||
# Wire up progress callback → event_queue
|
||||
|
|
@ -289,7 +289,7 @@ def run_training_process(
|
|||
stop_thread = threading.Thread(target = _poll_stop, daemon = True)
|
||||
stop_thread.start()
|
||||
|
||||
# ── 4. Execute the training pipeline ──
|
||||
# -- 4. Execute the training pipeline --
|
||||
# Order: detect → dataset → model → prepare → train
|
||||
# Dataset processing (including LLM-assisted detection) runs BEFORE model
|
||||
# loading so both never occupy VRAM at the same time.
|
||||
|
|
@ -297,7 +297,7 @@ def run_training_process(
|
|||
hf_token = config.get("hf_token", "")
|
||||
hf_token = hf_token if hf_token and hf_token.strip() else None
|
||||
|
||||
# ── 4a. Lightweight detection + tokenizer (no VRAM) ──
|
||||
# -- 4a. Lightweight detection + tokenizer (no VRAM) --
|
||||
_send_status(event_queue, "Detecting model type...")
|
||||
trainer.pre_detect_and_load_tokenizer(
|
||||
model_name = model_name,
|
||||
|
|
@ -311,7 +311,7 @@ def run_training_process(
|
|||
event_queue.put({"type": "complete", "output_dir": None, "ts": time.time()})
|
||||
return
|
||||
|
||||
# ── 4b. Load and format dataset (LLM helper may use VRAM briefly) ──
|
||||
# -- 4b. Load and format dataset (LLM helper may use VRAM briefly) --
|
||||
_send_status(event_queue, "Loading and formatting dataset...")
|
||||
hf_dataset = config.get("hf_dataset", "")
|
||||
dataset_result = trainer.load_and_format_dataset(
|
||||
|
|
@ -385,7 +385,7 @@ def run_training_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── Start tqdm monitor early so it captures download + tokenization bars ──
|
||||
# -- Start tqdm monitor early so it captures download + tokenization bars --
|
||||
import threading as _th
|
||||
|
||||
_tqdm_stop = _th.Event()
|
||||
|
|
@ -413,7 +413,7 @@ def run_training_process(
|
|||
training_type = config.get("training_type", "LoRA/QLoRA")
|
||||
use_lora = training_type == "LoRA/QLoRA"
|
||||
|
||||
# ── 4c. Load training model (uses VRAM — dataset already formatted) ──
|
||||
# -- 4c. Load training model (uses VRAM -- dataset already formatted) --
|
||||
_send_status(event_queue, "Loading model...")
|
||||
success = trainer.load_model(
|
||||
model_name = model_name,
|
||||
|
|
@ -442,7 +442,7 @@ def run_training_process(
|
|||
)
|
||||
return
|
||||
|
||||
# ── 4d. Prepare model (LoRA or full finetuning) ──
|
||||
# -- 4d. Prepare model (LoRA or full finetuning) --
|
||||
if use_lora:
|
||||
_send_status(event_queue, "Configuring LoRA adapters...")
|
||||
success = trainer.prepare_model_for_training(
|
||||
|
|
@ -510,7 +510,7 @@ def run_training_process(
|
|||
tensorboard_dir = str(resolve_tensorboard_dir(tensorboard_dir))
|
||||
ensure_dir(Path(tensorboard_dir))
|
||||
|
||||
# Start training (directly — no inner thread, we ARE the subprocess)
|
||||
# Start training (directly -- no inner thread, we ARE the subprocess)
|
||||
dataset_display = (
|
||||
config.get("hf_dataset", "") or config.get("uploaded_file", "") or ""
|
||||
)
|
||||
|
|
@ -598,7 +598,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
"""Self-contained embedding model training pipeline.
|
||||
|
||||
Uses FastSentenceTransformer + SentenceTransformerTrainer +
|
||||
MultipleNegativesRankingLoss — completely separate from the
|
||||
MultipleNegativesRankingLoss -- completely separate from the
|
||||
LLM/VLM/audio paths in UnslothTrainer.
|
||||
|
||||
Mirrors the pattern from the reference embedding notebooks:
|
||||
|
|
@ -612,7 +612,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
model_name = config["model_name"]
|
||||
training_start_time = time.time()
|
||||
|
||||
# ── 1. Import embedding-specific libraries ──
|
||||
# -- 1. Import embedding-specific libraries --
|
||||
_send_status(event_queue, "Importing embedding libraries...")
|
||||
try:
|
||||
from unsloth import FastSentenceTransformer, is_bfloat16_supported
|
||||
|
|
@ -637,7 +637,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
)
|
||||
return
|
||||
|
||||
# ── Stop signal handling ──
|
||||
# -- Stop signal handling --
|
||||
_should_stop = False
|
||||
_save_on_stop = True
|
||||
|
||||
|
|
@ -662,7 +662,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
stop_thread = threading.Thread(target = _poll_stop, daemon = True)
|
||||
stop_thread.start()
|
||||
|
||||
# ── 2. Load model ──
|
||||
# -- 2. Load model --
|
||||
_send_status(event_queue, "Loading embedding model...")
|
||||
try:
|
||||
hf_token = config.get("hf_token", "")
|
||||
|
|
@ -692,7 +692,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
event_queue.put({"type": "complete", "output_dir": None, "ts": time.time()})
|
||||
return
|
||||
|
||||
# ── 3. Apply LoRA ──
|
||||
# -- 3. Apply LoRA --
|
||||
if use_lora:
|
||||
_send_status(event_queue, "Configuring LoRA adapters (FEATURE_EXTRACTION)...")
|
||||
try:
|
||||
|
|
@ -732,7 +732,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
event_queue.put({"type": "complete", "output_dir": None, "ts": time.time()})
|
||||
return
|
||||
|
||||
# ── 4. Load dataset ──
|
||||
# -- 4. Load dataset --
|
||||
_send_status(event_queue, "Loading dataset...")
|
||||
try:
|
||||
hf_dataset = config.get("hf_dataset", "")
|
||||
|
|
@ -750,7 +750,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
token = hf_token,
|
||||
)
|
||||
elif local_datasets:
|
||||
# Load from local file(s) — mirrors the non-embedding pipeline's
|
||||
# Load from local file(s) -- mirrors the non-embedding pipeline's
|
||||
# directory handling so recipe outputs (parquet-files/) work.
|
||||
all_files: list[str] = []
|
||||
for dataset_file in local_datasets:
|
||||
|
|
@ -833,10 +833,10 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
event_queue.put({"type": "complete", "output_dir": None, "ts": time.time()})
|
||||
return
|
||||
|
||||
# ── 5. Create loss function ──
|
||||
# -- 5. Create loss function --
|
||||
loss = MultipleNegativesRankingLoss(model)
|
||||
|
||||
# ── 6. Build training arguments ──
|
||||
# -- 6. Build training arguments --
|
||||
_send_status(event_queue, "Configuring training...")
|
||||
try:
|
||||
lr_value = float(config.get("learning_rate", "2e-4"))
|
||||
|
|
@ -902,7 +902,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
|
||||
args = SentenceTransformerTrainingArguments(**training_args_kwargs)
|
||||
|
||||
# ── 7. Calculate total steps for progress tracking ──
|
||||
# -- 7. Calculate total steps for progress tracking --
|
||||
if max_steps_val and max_steps_val > 0:
|
||||
total_steps = max_steps_val
|
||||
else:
|
||||
|
|
@ -911,7 +911,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
steps_per_epoch = max(len_dataloader // gradient_accumulation_steps, 1)
|
||||
total_steps = steps_per_epoch * effective_epochs
|
||||
|
||||
# ── 8. Create progress callback ──
|
||||
# -- 8. Create progress callback --
|
||||
class _EmbeddingProgressCallback(TrainerCallback):
|
||||
"""Sends training progress events to the parent process via event_queue."""
|
||||
|
||||
|
|
@ -952,7 +952,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
control.should_training_stop = True
|
||||
return control
|
||||
|
||||
# ── 9. Create trainer and train ──
|
||||
# -- 9. Create trainer and train --
|
||||
_send_status(event_queue, "Starting embedding training...")
|
||||
try:
|
||||
trainer = SentenceTransformerTrainer(
|
||||
|
|
@ -975,7 +975,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
)
|
||||
return
|
||||
|
||||
# ── 10. Save model ──
|
||||
# -- 10. Save model --
|
||||
if _should_stop and not _save_on_stop:
|
||||
event_queue.put(
|
||||
{
|
||||
|
|
@ -1004,7 +1004,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) ->
|
|||
)
|
||||
return
|
||||
|
||||
# ── 11. Done ──
|
||||
# -- 11. Done --
|
||||
event_queue.put(
|
||||
{
|
||||
"type": "complete",
|
||||
|
|
|
|||
|
|
@ -64,13 +64,13 @@ async def lifespan(app: FastAPI):
|
|||
# Clean up any stale compiled cache from previous runs
|
||||
clear_unsloth_compiled_cache()
|
||||
|
||||
# Remove stale .venv_overlay from previous versions — no longer used.
|
||||
# Remove stale .venv_overlay from previous versions -- no longer used.
|
||||
# Version switching now uses .venv_t5/ (pre-installed by setup.sh).
|
||||
overlay_dir = Path(__file__).resolve().parent.parent.parent / ".venv_overlay"
|
||||
if overlay_dir.is_dir():
|
||||
shutil.rmtree(overlay_dir, ignore_errors = True)
|
||||
|
||||
# Detect hardware first — sets DEVICE global used everywhere
|
||||
# Detect hardware first -- sets DEVICE global used everywhere
|
||||
detect_hardware()
|
||||
|
||||
# Pre-cache the helper GGUF model for LLM-assisted dataset detection.
|
||||
|
|
@ -177,7 +177,7 @@ async def get_system_info():
|
|||
import psutil
|
||||
from utils.hardware import get_device, get_gpu_memory_info, DeviceType
|
||||
|
||||
# GPU Info — query nvidia-smi for physical GPUs, filtered by
|
||||
# GPU Info -- query nvidia-smi for physical GPUs, filtered by
|
||||
# CUDA_VISIBLE_DEVICES when set (the frontend uses this for GGUF
|
||||
# fit estimation and llama-server respects CVD too).
|
||||
import os
|
||||
|
|
@ -289,7 +289,7 @@ def _inject_bootstrap(html_bytes: bytes, app: FastAPI) -> bytes:
|
|||
|
||||
The script tag is only injected while the default admin account still
|
||||
has ``must_change_password=True``. Once the user changes the password
|
||||
the HTML is served clean — no credentials leak.
|
||||
the HTML is served clean -- no credentials leak.
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
|
|
@ -340,14 +340,14 @@ def setup_frontend(app: FastAPI, build_path: Path):
|
|||
|
||||
file_path = (build_path / full_path).resolve()
|
||||
|
||||
# Block path traversal — ensure resolved path stays inside build_path
|
||||
# Block path traversal -- ensure resolved path stays inside build_path
|
||||
if not file_path.is_relative_to(build_path.resolve()):
|
||||
return Response(status_code = 403)
|
||||
|
||||
if file_path.is_file():
|
||||
return FileResponse(file_path)
|
||||
|
||||
# Serve index.html as bytes — avoids Content-Length mismatch
|
||||
# Serve index.html as bytes -- avoids Content-Length mismatch
|
||||
content = (build_path / "index.html").read_bytes()
|
||||
content = _strip_crossorigin(content)
|
||||
content = _inject_bootstrap(content, app)
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class InferenceStatusResponse(BaseModel):
|
|||
# =====================================================================
|
||||
|
||||
|
||||
# ── Multimodal content parts (OpenAI vision format) ──────────────
|
||||
# -- Multimodal content parts (OpenAI vision format) --------------
|
||||
|
||||
|
||||
class TextContentPart(BaseModel):
|
||||
|
|
@ -214,7 +214,7 @@ class TextContentPart(BaseModel):
|
|||
|
||||
|
||||
class ImageUrl(BaseModel):
|
||||
"""Image URL object — supports data URIs and remote URLs."""
|
||||
"""Image URL object -- supports data URIs and remote URLs."""
|
||||
|
||||
url: str = Field(..., description = "data:image/png;base64,... or https://...")
|
||||
detail: Optional[Literal["auto", "low", "high"]] = "auto"
|
||||
|
|
@ -243,7 +243,7 @@ ContentPart = Annotated[
|
|||
"""Union type for multimodal content parts, discriminated by the 'type' field."""
|
||||
|
||||
|
||||
# ── Messages ─────────────────────────────────────────────────────
|
||||
# -- Messages -----------------------------------------------------
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
|
|
@ -282,7 +282,7 @@ class ChatCompletionRequest(BaseModel):
|
|||
)
|
||||
presence_penalty: float = Field(0.0, ge = 0.0, le = 2.0, description = "Presence penalty")
|
||||
|
||||
# ── Unsloth extensions (ignored by standard OpenAI clients) ──
|
||||
# -- Unsloth extensions (ignored by standard OpenAI clients) --
|
||||
top_k: int = Field(20, ge = -1, le = 100, description = "[x-unsloth] Top-k sampling")
|
||||
min_p: float = Field(
|
||||
0.01, ge = 0.0, le = 1.0, description = "[x-unsloth] Min-p sampling threshold"
|
||||
|
|
@ -338,7 +338,7 @@ class ChatCompletionRequest(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
# ── Streaming response chunks ────────────────────────────────────
|
||||
# -- Streaming response chunks ------------------------------------
|
||||
|
||||
|
||||
class ChoiceDelta(BaseModel):
|
||||
|
|
@ -368,7 +368,7 @@ class ChatCompletionChunk(BaseModel):
|
|||
timings: Optional[dict] = None
|
||||
|
||||
|
||||
# ── Non-streaming response ───────────────────────────────────────
|
||||
# -- Non-streaming response ---------------------------------------
|
||||
|
||||
|
||||
class CompletionMessage(BaseModel):
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ class TrainingStatus(BaseModel):
|
|||
metric_history: Optional[dict] = Field(
|
||||
None,
|
||||
description = "Full metric history arrays for chart recovery after SSE reconnection. "
|
||||
"Keys: 'steps', 'loss', 'lr', 'grad_norm', 'grad_norm_steps' — each a list of numeric values.",
|
||||
"Keys: 'steps', 'loss', 'lr', 'grad_norm', 'grad_norm_steps' -- each a list of numeric values.",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ def check_format(
|
|||
total_rows = None
|
||||
|
||||
if dataset_path.exists():
|
||||
# ── Local file ──────────────────────────────────────────
|
||||
# -- Local file ------------------------------------------
|
||||
train_split = request.train_split or "train"
|
||||
preview_slice, total_rows = _load_local_preview_slice(
|
||||
dataset_path = dataset_path,
|
||||
|
|
@ -347,7 +347,7 @@ def check_format(
|
|||
preview_size = PREVIEW_SIZE,
|
||||
)
|
||||
else:
|
||||
# ── HuggingFace dataset ─────────────────────────────────
|
||||
# -- HuggingFace dataset ---------------------------------
|
||||
# Tier 1: list_repo_files → load only the first data file
|
||||
preview_slice = None
|
||||
|
||||
|
|
@ -402,7 +402,7 @@ def check_format(
|
|||
logger.warning(f"Tier 1 (single-file) failed: {e}")
|
||||
|
||||
if preview_slice is None:
|
||||
# Tier 2: full streaming (resolves all files — slow for large repos)
|
||||
# Tier 2: full streaming (resolves all files -- slow for large repos)
|
||||
logger.info("Tier 2: falling back to full streaming load_dataset")
|
||||
load_kwargs = {
|
||||
"path": request.dataset_name,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ async def load_checkpoint(
|
|||
"""
|
||||
try:
|
||||
# Version switching is handled automatically by the subprocess-based
|
||||
# export backend — no need for ensure_transformers_version() here.
|
||||
# export backend -- no need for ensure_transformers_version() here.
|
||||
|
||||
# Free GPU memory: shut down any running inference/training subprocesses
|
||||
# before loading the export checkpoint (they'd compete for VRAM).
|
||||
|
|
|
|||
|
|
@ -94,9 +94,9 @@ async def load_model(
|
|||
"""
|
||||
try:
|
||||
# Version switching is handled automatically by the subprocess-based
|
||||
# inference backend — no need for ensure_transformers_version() here.
|
||||
# inference backend -- no need for ensure_transformers_version() here.
|
||||
|
||||
# ── Already-loaded check: skip reload if the exact model is active ──
|
||||
# -- Already-loaded check: skip reload if the exact model is active --
|
||||
backend = get_inference_backend()
|
||||
llama_backend = get_llama_cpp_backend()
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ async def load_model(
|
|||
detail = f"Invalid model identifier: {request.model_path}",
|
||||
)
|
||||
|
||||
# ── GGUF path: load via llama-server ──────────────────────
|
||||
# -- GGUF path: load via llama-server ----------------------
|
||||
if config.is_gguf:
|
||||
llama_backend = get_llama_cpp_backend()
|
||||
unsloth_backend = get_inference_backend()
|
||||
|
|
@ -264,7 +264,7 @@ async def load_model(
|
|||
chat_template = llama_backend.chat_template,
|
||||
)
|
||||
|
||||
# ── Standard path: load via Unsloth/transformers ──────────
|
||||
# -- Standard path: load via Unsloth/transformers ----------
|
||||
backend = get_inference_backend()
|
||||
|
||||
# Unload any active GGUF model first
|
||||
|
|
@ -305,13 +305,13 @@ async def load_model(
|
|||
training_method = adapter_cfg.get("unsloth_training_method")
|
||||
if training_method == "lora" and load_in_4bit:
|
||||
logger.info(
|
||||
f"adapter_config.json says unsloth_training_method='lora' — "
|
||||
f"adapter_config.json says unsloth_training_method='lora' -- "
|
||||
f"setting load_in_4bit=False to match 16-bit training"
|
||||
)
|
||||
load_in_4bit = False
|
||||
elif training_method == "qlora" and not load_in_4bit:
|
||||
logger.info(
|
||||
f"adapter_config.json says unsloth_training_method='qlora' — "
|
||||
f"adapter_config.json says unsloth_training_method='qlora' -- "
|
||||
f"setting load_in_4bit=True to match QLoRA training"
|
||||
)
|
||||
load_in_4bit = True
|
||||
|
|
@ -320,7 +320,7 @@ async def load_model(
|
|||
f"Training method: {training_method}, load_in_4bit={load_in_4bit}"
|
||||
)
|
||||
else:
|
||||
# No unsloth_training_method — fallback to base model name
|
||||
# No unsloth_training_method -- fallback to base model name
|
||||
if (
|
||||
config.base_model
|
||||
and "-bnb-4bit" not in config.base_model.lower()
|
||||
|
|
@ -328,7 +328,7 @@ async def load_model(
|
|||
):
|
||||
logger.info(
|
||||
f"No unsloth_training_method in adapter_config.json. "
|
||||
f"Base model '{config.base_model}' has no -bnb-4bit suffix — "
|
||||
f"Base model '{config.base_model}' has no -bnb-4bit suffix -- "
|
||||
f"setting load_in_4bit=False"
|
||||
)
|
||||
load_in_4bit = False
|
||||
|
|
@ -657,7 +657,7 @@ async def generate_audio(
|
|||
raise HTTPException(status_code = 400, detail = "No user message found.")
|
||||
text = last_user_msg["content"]
|
||||
|
||||
# Pick backend — both return (wav_bytes, sample_rate)
|
||||
# Pick backend -- both return (wav_bytes, sample_rate)
|
||||
llama_backend = get_llama_cpp_backend()
|
||||
if llama_backend.is_loaded and getattr(llama_backend, "_is_audio", False):
|
||||
model_name = llama_backend.model_identifier
|
||||
|
|
@ -780,7 +780,7 @@ def _extract_content_parts(
|
|||
first_image_b64: Optional[str] = None
|
||||
|
||||
for msg in messages:
|
||||
# ── System messages → extract as system_prompt ────────
|
||||
# -- System messages → extract as system_prompt --------
|
||||
if msg.role == "system":
|
||||
if isinstance(msg.content, str):
|
||||
system_prompt = msg.content
|
||||
|
|
@ -791,9 +791,9 @@ def _extract_content_parts(
|
|||
)
|
||||
continue
|
||||
|
||||
# ── User / assistant messages ─────────────────────────
|
||||
# -- User / assistant messages -------------------------
|
||||
if isinstance(msg.content, str):
|
||||
# Plain string content — pass through
|
||||
# Plain string content -- pass through
|
||||
chat_messages.append({"role": msg.role, "content": msg.content})
|
||||
elif isinstance(msg.content, list):
|
||||
# Multimodal content parts
|
||||
|
|
@ -838,7 +838,7 @@ async def openai_chat_completions(
|
|||
llama_backend = get_llama_cpp_backend()
|
||||
using_gguf = llama_backend.is_loaded
|
||||
|
||||
# ── Determine which backend is active ─────────────────────
|
||||
# -- Determine which backend is active ---------------------
|
||||
if using_gguf:
|
||||
model_name = llama_backend.model_identifier or payload.model
|
||||
if getattr(llama_backend, "_is_audio", False):
|
||||
|
|
@ -852,20 +852,20 @@ async def openai_chat_completions(
|
|||
)
|
||||
model_name = backend.active_model_name or payload.model
|
||||
|
||||
# ── Audio TTS path: auto-route to audio generation ────
|
||||
# (Whisper is ASR not TTS — handled below in audio input path)
|
||||
# -- Audio TTS path: auto-route to audio generation ----
|
||||
# (Whisper is ASR not TTS -- handled below in audio input path)
|
||||
model_info = backend.models.get(backend.active_model_name, {})
|
||||
if model_info.get("is_audio") and model_info.get("audio_type") != "whisper":
|
||||
return await generate_audio(payload, request)
|
||||
|
||||
# ── Whisper without audio: return clear error ──
|
||||
# -- Whisper without audio: return clear error --
|
||||
if model_info.get("audio_type") == "whisper" and not payload.audio_base64:
|
||||
raise HTTPException(
|
||||
status_code = 400,
|
||||
detail = "Whisper models require audio input. Please upload an audio file.",
|
||||
)
|
||||
|
||||
# ── Audio INPUT path: decode WAV and route to audio input generation ──
|
||||
# -- Audio INPUT path: decode WAV and route to audio input generation --
|
||||
if payload.audio_base64 and model_info.get("has_audio_input"):
|
||||
audio_array = _decode_audio_base64(payload.audio_base64)
|
||||
system_prompt, chat_messages, _ = _extract_content_parts(payload.messages)
|
||||
|
|
@ -970,7 +970,7 @@ async def openai_chat_completions(
|
|||
)
|
||||
return JSONResponse(content = response.model_dump())
|
||||
|
||||
# ── Parse messages (handles multimodal content parts) ─────
|
||||
# -- Parse messages (handles multimodal content parts) -----
|
||||
system_prompt, chat_messages, extracted_image_b64 = _extract_content_parts(
|
||||
payload.messages
|
||||
)
|
||||
|
|
@ -981,7 +981,7 @@ async def openai_chat_completions(
|
|||
detail = "At least one non-system message is required.",
|
||||
)
|
||||
|
||||
# ── GGUF path: proxy to llama-server /v1/chat/completions ──
|
||||
# -- GGUF path: proxy to llama-server /v1/chat/completions --
|
||||
if using_gguf:
|
||||
# Reject images if this GGUF model doesn't support vision
|
||||
image_b64 = extracted_image_b64 or payload.image_base64
|
||||
|
|
@ -1021,7 +1021,7 @@ async def openai_chat_completions(
|
|||
completion_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
|
||||
created = int(time.time())
|
||||
|
||||
# ── Tool-calling path (agentic loop) ──────────────────
|
||||
# -- Tool-calling path (agentic loop) ------------------
|
||||
use_tools = (
|
||||
payload.enable_tools and llama_backend.supports_tools and not image_b64
|
||||
)
|
||||
|
|
@ -1192,7 +1192,7 @@ async def openai_chat_completions(
|
|||
},
|
||||
)
|
||||
|
||||
# ── Standard GGUF path (no tools) ─────────────────────
|
||||
# -- Standard GGUF path (no tools) ---------------------
|
||||
|
||||
def gguf_generate():
|
||||
return llama_backend.generate_chat_completion(
|
||||
|
|
@ -1354,7 +1354,7 @@ async def openai_chat_completions(
|
|||
logger.error(f"Error during GGUF completion: {e}", exc_info = True)
|
||||
raise HTTPException(status_code = 500, detail = str(e))
|
||||
|
||||
# ── Standard Unsloth path ─────────────────────────────────
|
||||
# -- Standard Unsloth path ---------------------------------
|
||||
|
||||
# Decode image (from content parts OR legacy field)
|
||||
image_b64 = extracted_image_b64 or payload.image_base64
|
||||
|
|
@ -1416,7 +1416,7 @@ async def openai_chat_completions(
|
|||
completion_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
|
||||
created = int(time.time())
|
||||
|
||||
# ── Streaming response ────────────────────────────────────────
|
||||
# -- Streaming response ----------------------------------------
|
||||
if payload.stream:
|
||||
|
||||
async def stream_chunks():
|
||||
|
|
@ -1446,7 +1446,7 @@ async def openai_chat_completions(
|
|||
gen = generate()
|
||||
while True:
|
||||
# next(gen, _DONE) returns _DONE instead of raising
|
||||
# StopIteration — StopIteration cannot propagate
|
||||
# StopIteration -- StopIteration cannot propagate
|
||||
# through asyncio futures (Python limitation).
|
||||
cumulative = await loop.run_in_executor(None, next, gen, _DONE)
|
||||
if cumulative is _DONE:
|
||||
|
|
@ -1511,7 +1511,7 @@ async def openai_chat_completions(
|
|||
},
|
||||
)
|
||||
|
||||
# ── Non-streaming response ────────────────────────────────────
|
||||
# -- Non-streaming response ------------------------------------
|
||||
else:
|
||||
try:
|
||||
full_text = ""
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ async def scan_loras(
|
|||
)
|
||||
)
|
||||
|
||||
# Scan exported models (merged, LoRA, base — skips GGUF)
|
||||
# Scan exported models (merged, LoRA, base -- skips GGUF)
|
||||
exported = scan_exported_models(exports_dir = resolved_exports_dir)
|
||||
for display_name, model_path, export_type, base_model in exported:
|
||||
lora_list.append(
|
||||
|
|
@ -1003,7 +1003,7 @@ async def delete_cached_model(
|
|||
if target_repo is None:
|
||||
raise HTTPException(status_code = 404, detail = "Model not found in cache")
|
||||
|
||||
# ── Per-variant GGUF deletion ────────────────────────────
|
||||
# -- Per-variant GGUF deletion ----------------------------
|
||||
if variant:
|
||||
deleted_bytes = 0
|
||||
deleted_count = 0
|
||||
|
|
@ -1041,7 +1041,7 @@ async def delete_cached_model(
|
|||
)
|
||||
return {"status": "deleted", "repo_id": repo_id, "variant": variant}
|
||||
|
||||
# ── Full repo deletion ───────────────────────────────────
|
||||
# -- Full repo deletion -----------------------------------
|
||||
revision_hashes = [rev.commit_hash for rev in target_repo.revisions]
|
||||
if not revision_hashes:
|
||||
raise HTTPException(status_code = 404, detail = "No revisions found for model")
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ async def start_training(
|
|||
"trust_remote_code": request.trust_remote_code,
|
||||
}
|
||||
|
||||
# Training page has no trust_remote_code toggle — the value comes from
|
||||
# Training page has no trust_remote_code toggle -- the value comes from
|
||||
# YAML model defaults applied when the user selects a model. As a safety
|
||||
# net, consult the YAML directly so models that need it always get it.
|
||||
if not training_kwargs["trust_remote_code"]:
|
||||
|
|
@ -323,7 +323,7 @@ async def reset_training(
|
|||
|
||||
if is_active:
|
||||
if backend._cancel_requested:
|
||||
# Cancel (save=False) was requested — force-terminate so we can reset immediately
|
||||
# Cancel (save=False) was requested -- force-terminate so we can reset immediately
|
||||
logger.info(
|
||||
"Force-terminating subprocess for immediate reset (cancel path)"
|
||||
)
|
||||
|
|
@ -523,7 +523,7 @@ async def stream_training_progress(
|
|||
backend = get_training_backend()
|
||||
job_id: str = getattr(backend, "current_job_id", "") or ""
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────
|
||||
# -- Helpers ----------------------------------------------
|
||||
def build_progress(
|
||||
step: int,
|
||||
loss: float,
|
||||
|
|
@ -585,11 +585,11 @@ async def stream_training_progress(
|
|||
lines.append("") # double newline terminates the event
|
||||
return "\n".join(lines)
|
||||
|
||||
# ── Retry directive ──────────────────────────────────────
|
||||
# -- Retry directive --------------------------------------
|
||||
# Tell the browser to reconnect after 3 seconds if the connection drops
|
||||
yield "retry: 3000\n\n"
|
||||
|
||||
# ── Replay missed steps on reconnect ─────────────────────
|
||||
# -- Replay missed steps on reconnect ---------------------
|
||||
if resume_from_step is not None and backend.step_history:
|
||||
replayed = 0
|
||||
grad_norm_by_step = {
|
||||
|
|
@ -636,7 +636,7 @@ async def stream_training_progress(
|
|||
if replayed:
|
||||
logger.info(f"SSE reconnect: replayed {replayed} missed steps")
|
||||
|
||||
# ── Initial status (only on fresh connections) ───────────
|
||||
# -- Initial status (only on fresh connections) -----------
|
||||
if resume_from_step is None:
|
||||
is_active = backend.is_training_active()
|
||||
tp = getattr(getattr(backend, "trainer", None), "training_progress", None)
|
||||
|
|
@ -686,7 +686,7 @@ async def stream_training_progress(
|
|||
)
|
||||
return
|
||||
|
||||
# ── Live polling loop ────────────────────────────────────
|
||||
# -- Live polling loop ------------------------------------
|
||||
last_step = resume_from_step if resume_from_step is not None else -1
|
||||
no_update_count = 0
|
||||
max_no_updates = (
|
||||
|
|
@ -805,7 +805,7 @@ async def stream_training_progress(
|
|||
)
|
||||
break
|
||||
|
||||
# ── Final "complete" event ───────────────────────────────
|
||||
# -- Final "complete" event -------------------------------
|
||||
final_step = backend.step_history[-1] if backend.step_history else last_step
|
||||
final_loss = backend.loss_history[-1] if backend.loss_history else 0.0
|
||||
final_lr = backend.lr_history[-1] if backend.lr_history else 0.0
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ def _graceful_shutdown(server = None):
|
|||
before the parent exits. This is critical on Windows where atexit
|
||||
handlers are unreliable after Ctrl+C.
|
||||
"""
|
||||
logger.info("Graceful shutdown initiated — cleaning up subprocesses...")
|
||||
logger.info("Graceful shutdown initiated -- cleaning up subprocesses...")
|
||||
|
||||
# 1. Shut down uvicorn server (releases the listening socket)
|
||||
if server is not None:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from utils.transformers_version import (
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_base_model — config.json fallback
|
||||
# _resolve_base_model -- config.json fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ class TestResolveBaseModel:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _check_tokenizer_config_needs_v5 — local file check
|
||||
# _check_tokenizer_config_needs_v5 -- local file check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ class TestCheckTokenizerConfigNeedsV5:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# needs_transformers_5 — integration-level
|
||||
# needs_transformers_5 -- integration-level
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""
|
||||
Tests for utils/hardware and utils/utils — device detection, GPU memory, error formatting.
|
||||
Tests for utils/hardware and utils/utils -- device detection, GPU memory, error formatting.
|
||||
|
||||
These tests are designed to pass on ANY platform:
|
||||
• NVIDIA GPU (CUDA backend, requires torch)
|
||||
|
|
@ -75,7 +75,7 @@ def _reset_and_detect():
|
|||
|
||||
|
||||
class TestGetDevice:
|
||||
"""Tests for get_device() — should agree with the real hardware."""
|
||||
"""Tests for get_device() -- should agree with the real hardware."""
|
||||
|
||||
def setup_method(self):
|
||||
self._saved_device = _hw_module.DEVICE
|
||||
|
|
@ -167,7 +167,7 @@ class TestClearGpuCache:
|
|||
|
||||
@needs_mlx
|
||||
def test_mlx_does_not_raise(self):
|
||||
"""MLX cache clear is a no-op — should just succeed."""
|
||||
"""MLX cache clear is a no-op -- should just succeed."""
|
||||
with patch("utils.hardware.hardware.get_device", return_value = DeviceType.MLX):
|
||||
clear_gpu_cache()
|
||||
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ def get_tokenizer_chat_template(tokenizer, model_name):
|
|||
if has_chat_template:
|
||||
logger.info(f"📝 Using tokenizer's own chat template (no Unsloth template match)")
|
||||
else:
|
||||
# Base model with no chat template — apply default ChatML
|
||||
logger.info(f"📝 No chat template found — applying default ChatML template (base model)")
|
||||
# Base model with no chat template -- apply default ChatML
|
||||
logger.info(f"📝 No chat template found -- applying default ChatML template (base model)")
|
||||
try:
|
||||
tokenizer = get_chat_template(
|
||||
tokenizer,
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def check_dataset_format(dataset, is_vlm: bool = False) -> dict:
|
|||
}
|
||||
|
||||
if is_audio:
|
||||
# Audio dataset — require manual mapping only when columns can't be auto-detected
|
||||
# Audio dataset -- require manual mapping only when columns can't be auto-detected
|
||||
detected_audio = multimodal_info.get("detected_audio_column")
|
||||
detected_text = multimodal_info.get("detected_text_column")
|
||||
needs_mapping = not detected_audio or not detected_text
|
||||
|
|
@ -155,7 +155,7 @@ def check_dataset_format(dataset, is_vlm: bool = False) -> dict:
|
|||
**audio_fields,
|
||||
}
|
||||
else:
|
||||
# Heuristic failed — user must map manually (or use AI Assist)
|
||||
# Heuristic failed -- user must map manually (or use AI Assist)
|
||||
return {
|
||||
"requires_manual_mapping": True,
|
||||
"detected_format": "unknown",
|
||||
|
|
@ -206,7 +206,7 @@ def _apply_user_mapping(dataset, mapping: dict, batch_size: int = 1000):
|
|||
Apply user-provided column mapping to convert dataset to conversations format.
|
||||
|
||||
Accepts chatml (user/assistant/system), sharegpt (human/gpt/system), and
|
||||
alpaca (instruction/input/output) role names — all normalised to chatml output.
|
||||
alpaca (instruction/input/output) role names -- all normalised to chatml output.
|
||||
|
||||
If the mapping contains ``__``-prefixed metadata keys (from the conversion
|
||||
advisor), routes to template-based conversion instead of simple role mapping.
|
||||
|
|
@ -221,7 +221,7 @@ def _apply_user_mapping(dataset, mapping: dict, batch_size: int = 1000):
|
|||
if meta:
|
||||
return _apply_template_mapping(dataset, column_roles, meta, batch_size)
|
||||
|
||||
# ── Simple mode (original logic) ──
|
||||
# -- Simple mode (original logic) --
|
||||
# Pre-compute: group columns by canonical chatml role
|
||||
role_groups: dict[str, list[str]] = {r: [] for r in _CHATML_ROLE_ORDER}
|
||||
for col_name, role in column_roles.items():
|
||||
|
|
@ -257,7 +257,7 @@ def _apply_user_mapping(dataset, mapping: dict, batch_size: int = 1000):
|
|||
|
||||
def _extract_column_value(val, col: str, label_mapping: dict) -> str:
|
||||
"""Extract a string value from a column, handling complex types and label mapping."""
|
||||
# Handle complex types (dicts, lists) — extract useful text instead of raw repr
|
||||
# Handle complex types (dicts, lists) -- extract useful text instead of raw repr
|
||||
if isinstance(val, dict):
|
||||
# Common pattern: {"text": [...]} in QA datasets
|
||||
if "text" in val:
|
||||
|
|
@ -354,7 +354,7 @@ def _apply_user_mapping_alpaca(dataset, mapping: dict, batch_size: int = 1000):
|
|||
"""
|
||||
Apply user-provided column mapping to convert dataset to Alpaca format.
|
||||
|
||||
Accepts any format's role names — normalises via _TO_CHATML, then maps
|
||||
Accepts any format's role names -- normalises via _TO_CHATML, then maps
|
||||
user → instruction, system → input, assistant → output.
|
||||
|
||||
Returns:
|
||||
|
|
@ -447,7 +447,7 @@ def format_dataset(
|
|||
final_format = "alpaca"
|
||||
chat_column = None
|
||||
else:
|
||||
# auto / chatml / sharegpt / conversational — all produce chatml conversations
|
||||
# auto / chatml / sharegpt / conversational -- all produce chatml conversations
|
||||
# (sharegpt is always standardized to role/content internally)
|
||||
mapped_dataset = _apply_user_mapping(
|
||||
dataset, custom_format_mapping, batch_size
|
||||
|
|
@ -906,11 +906,11 @@ def format_and_template_dataset(
|
|||
"errors": [],
|
||||
}
|
||||
except Exception as e:
|
||||
# User mapping failed — fall back to auto-detection instead
|
||||
# User mapping failed -- fall back to auto-detection instead
|
||||
# of giving up (handles stale cached mappings gracefully)
|
||||
warnings.append(
|
||||
f"User VLM mapping (image='{user_vlm_image_column}', "
|
||||
f"text='{user_vlm_text_column}') failed: {e} — "
|
||||
f"text='{user_vlm_text_column}') failed: {e} -- "
|
||||
f"falling back to auto-detection"
|
||||
)
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ def convert_to_vlm_format(
|
|||
else:
|
||||
image_data = Image.open(image_data).convert("RGB")
|
||||
|
||||
# Get text (if list of strings, pick a random one — e.g. multiple captions)
|
||||
# Get text (if list of strings, pick a random one -- e.g. multiple captions)
|
||||
text_data = sample[text_column]
|
||||
if isinstance(text_data, list) and len(text_data) > 0:
|
||||
import random
|
||||
|
|
@ -391,7 +391,7 @@ def convert_to_vlm_format(
|
|||
("http://", "https://")
|
||||
)
|
||||
|
||||
# ── Bare-filename detection: images stored as filenames (e.g. "img_001.png")
|
||||
# -- Bare-filename detection: images stored as filenames (e.g. "img_001.png")
|
||||
# that don't exist locally. Build a basename→repo_path lookup so we can
|
||||
# resolve them via hf_hub_download during conversion.
|
||||
_image_lookup = None
|
||||
|
|
@ -407,7 +407,7 @@ def convert_to_vlm_format(
|
|||
|
||||
_notify("Resolving image filenames from HF repo...")
|
||||
logger.info(
|
||||
f"🔍 Image column contains bare filenames (e.g. '{first_image}') — building repo lookup..."
|
||||
f"🔍 Image column contains bare filenames (e.g. '{first_image}') -- building repo lookup..."
|
||||
)
|
||||
repo_files = HfApi().list_repo_files(dataset_name, repo_type = "dataset")
|
||||
_image_lookup = {
|
||||
|
|
@ -421,14 +421,14 @@ def convert_to_vlm_format(
|
|||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"⚠️ Built lookup with {len(_image_lookup)} images but '{first_image}' not found — falling back to local open"
|
||||
f"⚠️ Built lookup with {len(_image_lookup)} images but '{first_image}' not found -- falling back to local open"
|
||||
)
|
||||
_image_lookup = None
|
||||
except Exception as e:
|
||||
logger.info(f"⚠️ Failed to build HF repo image lookup: {e}")
|
||||
_image_lookup = None
|
||||
|
||||
# ── URL probe: 200 samples with parallel workers to estimate speed + failure rate ──
|
||||
# -- URL probe: 200 samples with parallel workers to estimate speed + failure rate --
|
||||
PROBE_SIZE = 200
|
||||
MAX_FAIL_RATE = 0.3
|
||||
|
||||
|
|
@ -510,7 +510,7 @@ def convert_to_vlm_format(
|
|||
logger.info(f"⏱️ Estimated time for {total:,} samples: ~{eta_str}")
|
||||
_notify(info_msg)
|
||||
|
||||
# ── Full conversion with progress ──
|
||||
# -- Full conversion with progress --
|
||||
from tqdm import tqdm
|
||||
|
||||
logger.info(f"🔄 Converting {total} samples to VLM format...")
|
||||
|
|
@ -617,7 +617,7 @@ def convert_to_vlm_format(
|
|||
|
||||
if len(converted_list) == 0:
|
||||
issues = [
|
||||
f"All {total} samples failed during VLM conversion — no usable images found",
|
||||
f"All {total} samples failed during VLM conversion -- no usable images found",
|
||||
f"Image column '{image_column}' may contain URLs that are no longer accessible, "
|
||||
"or local file paths that don't exist",
|
||||
]
|
||||
|
|
@ -636,7 +636,7 @@ def convert_to_vlm_format(
|
|||
raise ValueError(
|
||||
friendly
|
||||
or (
|
||||
f"All {total} samples failed during VLM conversion — no usable images found. "
|
||||
f"All {total} samples failed during VLM conversion -- no usable images found. "
|
||||
"This dataset may contain only image URLs that are no longer accessible."
|
||||
)
|
||||
)
|
||||
|
|
@ -687,7 +687,7 @@ def convert_sharegpt_with_images_to_vlm_format(
|
|||
if progress_callback:
|
||||
progress_callback(status_message = msg)
|
||||
|
||||
# ── Resolve image loading strategy (same 3-tier as convert_to_vlm_format) ──
|
||||
# -- Resolve image loading strategy (same 3-tier as convert_to_vlm_format) --
|
||||
total = len(dataset)
|
||||
first_image = next(iter(dataset))[image_column]
|
||||
|
||||
|
|
@ -703,7 +703,7 @@ def convert_sharegpt_with_images_to_vlm_format(
|
|||
|
||||
_notify("Resolving image filenames from HF repo...")
|
||||
logger.info(
|
||||
f"🔍 Image column contains bare filenames (e.g. '{first_image}') — building repo lookup..."
|
||||
f"🔍 Image column contains bare filenames (e.g. '{first_image}') -- building repo lookup..."
|
||||
)
|
||||
repo_files = HfApi().list_repo_files(dataset_name, repo_type = "dataset")
|
||||
_image_lookup = {
|
||||
|
|
@ -721,7 +721,7 @@ def convert_sharegpt_with_images_to_vlm_format(
|
|||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"⚠️ Built lookup with {len(_image_lookup)} images but '{first_image}' not found — falling back to local open"
|
||||
f"⚠️ Built lookup with {len(_image_lookup)} images but '{first_image}' not found -- falling back to local open"
|
||||
)
|
||||
_image_lookup = None
|
||||
except Exception as e:
|
||||
|
|
@ -792,7 +792,7 @@ def convert_sharegpt_with_images_to_vlm_format(
|
|||
|
||||
return {"messages": new_messages}
|
||||
|
||||
# ── Full conversion with progress ──
|
||||
# -- Full conversion with progress --
|
||||
logger.info(f"🔄 Converting {total} samples from ShareGPT+image format...")
|
||||
converted_list = []
|
||||
failed_count = 0
|
||||
|
|
@ -815,7 +815,7 @@ def convert_sharegpt_with_images_to_vlm_format(
|
|||
|
||||
if len(converted_list) == 0:
|
||||
raise ValueError(
|
||||
f"All {total} samples failed during ShareGPT+image conversion — "
|
||||
f"All {total} samples failed during ShareGPT+image conversion -- "
|
||||
"no usable samples found."
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ def detect_multimodal_dataset(dataset):
|
|||
audio_columns = []
|
||||
modality_types = set()
|
||||
|
||||
# ── Image detection ─────────────────────────────────────
|
||||
# -- Image detection -------------------------------------
|
||||
# Pass 1: column-name heuristic (word-boundary match to avoid
|
||||
# false positives like 'pic' in 'topic')
|
||||
for col_name in column_names:
|
||||
|
|
@ -453,7 +453,7 @@ def detect_multimodal_dataset(dataset):
|
|||
multimodal_columns.append(col_name)
|
||||
modality_types.add("image")
|
||||
|
||||
# ── Audio detection ─────────────────────────────────────
|
||||
# -- Audio detection -------------------------------------
|
||||
# Pass 1: column-name heuristic (word-boundary match)
|
||||
for col_name in column_names:
|
||||
for keyword in audio_keywords:
|
||||
|
|
@ -812,17 +812,17 @@ def detect_vlm_dataset_structure(dataset):
|
|||
Returns True if likely valid, False if definitely broken."""
|
||||
import os
|
||||
|
||||
# PIL / dict — already loaded, always valid
|
||||
# PIL / dict -- already loaded, always valid
|
||||
if not isinstance(sample_value, str):
|
||||
return True
|
||||
|
||||
# Local file — check it exists
|
||||
# Local file -- check it exists
|
||||
if not sample_value.startswith(("http://", "https://")):
|
||||
return os.path.exists(
|
||||
sample_value
|
||||
) # bare filenames return False here, that's OK
|
||||
|
||||
# URL — quick HEAD request with short timeout
|
||||
# URL -- quick HEAD request with short timeout
|
||||
try:
|
||||
import urllib.request
|
||||
|
||||
|
|
@ -845,7 +845,7 @@ def detect_vlm_dataset_structure(dataset):
|
|||
if score > 0:
|
||||
candidates.append((col, score))
|
||||
|
||||
# Pass 2: value-based fallback — find columns with image URLs/paths
|
||||
# Pass 2: value-based fallback -- find columns with image URLs/paths
|
||||
# even if the column name doesn't match image keywords
|
||||
already = {c[0] for c in candidates}
|
||||
for col in column_names:
|
||||
|
|
@ -862,17 +862,17 @@ def detect_vlm_dataset_structure(dataset):
|
|||
|
||||
candidates.sort(key = lambda x: x[1], reverse = True)
|
||||
|
||||
# Single candidate or top candidate is PIL/dict — no probing needed
|
||||
# Single candidate or top candidate is PIL/dict -- no probing needed
|
||||
if len(candidates) == 1 or candidates[0][1] >= 75:
|
||||
return candidates[0][0]
|
||||
|
||||
# Multiple string-based candidates — probe to find one that actually works
|
||||
# Multiple string-based candidates -- probe to find one that actually works
|
||||
for col, score in candidates:
|
||||
sample_value = sample[col]
|
||||
if _probe_image_candidate(col, sample_value):
|
||||
return col
|
||||
|
||||
# Nothing probed successfully — return highest-scored anyway and let
|
||||
# Nothing probed successfully -- return highest-scored anyway and let
|
||||
# conversion handle the error (it may still resolve via hf_hub_download)
|
||||
return candidates[0][0]
|
||||
|
||||
|
|
@ -899,7 +899,7 @@ def detect_vlm_dataset_structure(dataset):
|
|||
and len(sample_value) > 0
|
||||
and isinstance(sample_value[0], str)
|
||||
):
|
||||
# List of strings (e.g. captions list) — lower priority than plain strings
|
||||
# List of strings (e.g. captions list) -- lower priority than plain strings
|
||||
priority = min(len(sample_value[0]), 1000) // 2
|
||||
candidates.append((col, priority))
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ vlm_processing.py. Only invoked when heuristics are uncertain.
|
|||
|
||||
Architecture:
|
||||
- Instantiates LlamaCppBackend, loads model, runs completion(s), unloads.
|
||||
- Not kept warm — VRAM is freed immediately after use.
|
||||
- Not kept warm -- VRAM is freed immediately after use.
|
||||
- Gracefully degrades: returns None when unavailable (no binary, OOM, disabled).
|
||||
"""
|
||||
|
||||
|
|
@ -43,12 +43,12 @@ def _strip_think_tags(text: str) -> str:
|
|||
if "<think>" not in text:
|
||||
return text
|
||||
|
||||
# Try stripping think blocks — keep content outside them
|
||||
# Try stripping think blocks -- keep content outside them
|
||||
stripped = re.sub(r"<think>.*?</think>\s*", "", text, flags = re.DOTALL).strip()
|
||||
if stripped:
|
||||
return stripped
|
||||
|
||||
# Everything was inside <think> tags — extract the inner content of the last block
|
||||
# Everything was inside <think> tags -- extract the inner content of the last block
|
||||
matches = re.findall(r"<think>(.*?)</think>", text, flags = re.DOTALL)
|
||||
if matches:
|
||||
return matches[-1].strip()
|
||||
|
|
@ -158,7 +158,7 @@ def _run_with_helper(prompt: str, max_tokens: int = 256) -> Optional[str]:
|
|||
):
|
||||
if isinstance(chunk, dict):
|
||||
continue # skip metadata events
|
||||
cumulative = chunk # cumulative — last value is full text
|
||||
cumulative = chunk # cumulative -- last value is full text
|
||||
|
||||
result = cumulative.strip()
|
||||
result = _strip_think_tags(result)
|
||||
|
|
@ -178,7 +178,7 @@ def _run_with_helper(prompt: str, max_tokens: int = 256) -> Optional[str]:
|
|||
pass
|
||||
|
||||
|
||||
# ─── Public API ───────────────────────────────────────────────────────
|
||||
# --- Public API -------------------------------------------------------
|
||||
|
||||
|
||||
def llm_generate_vlm_instruction(
|
||||
|
|
@ -270,7 +270,7 @@ def llm_classify_columns(
|
|||
"- user: The input/question/prompt from the human\n"
|
||||
"- assistant: The expected output/answer/response from the AI\n"
|
||||
"- system: Context, persona, or task description\n"
|
||||
"- metadata: IDs, scores, labels, timestamps — not part of conversation\n\n"
|
||||
"- metadata: IDs, scores, labels, timestamps -- not part of conversation\n\n"
|
||||
f"Columns: {column_names}\n\n"
|
||||
f"{formatted}"
|
||||
"Respond with ONLY a JSON object mapping column names to roles.\n"
|
||||
|
|
@ -383,7 +383,7 @@ def llm_generate_dataset_warning(
|
|||
return warning
|
||||
|
||||
|
||||
# ─── Dataset Conversion Advisor ──────────────────────────────────────
|
||||
# --- Dataset Conversion Advisor --------------------------------------
|
||||
|
||||
|
||||
def _parse_json_response(text: str) -> Optional[dict]:
|
||||
|
|
@ -534,7 +534,7 @@ def _run_multi_pass_advisor(
|
|||
return None
|
||||
|
||||
logger.info(f"Advisor model loaded in {time.monotonic() - t0:.1f}s")
|
||||
# ── Format samples ──
|
||||
# -- Format samples --
|
||||
samples_text = ""
|
||||
for i, row in enumerate(samples[:5], 1):
|
||||
parts = [f" {col}: {str(row.get(col, ''))[:200]}" for col in columns]
|
||||
|
|
@ -547,7 +547,7 @@ def _run_multi_pass_advisor(
|
|||
)
|
||||
card_excerpt = (dataset_card or "")[:1200] or "N/A"
|
||||
|
||||
# ── Target Model Hints ──
|
||||
# -- Target Model Hints --
|
||||
target_hints = ""
|
||||
is_gemma_3n = False
|
||||
if model_name:
|
||||
|
|
@ -583,7 +583,7 @@ def _run_multi_pass_advisor(
|
|||
"Ensure the dataset format mapped reflects these specialized tasks."
|
||||
)
|
||||
|
||||
# ── Pass 1: Classify ──
|
||||
# -- Pass 1: Classify --
|
||||
logger.info("Pass 1: Classifying dataset...")
|
||||
t1 = time.monotonic()
|
||||
messages1 = [
|
||||
|
|
@ -595,7 +595,7 @@ def _run_multi_pass_advisor(
|
|||
"a conversational format suitable for LLM fine-tuning. A dataset is "
|
||||
'"conversational" if it already has columns like "messages", "conversations", '
|
||||
'or multiturn "user"/"assistant" pairs. Some datasets are NOT conversational '
|
||||
"— they are things like summarization, question answering, translation, "
|
||||
"-- they are things like summarization, question answering, translation, "
|
||||
"classification, etc. Those need conversion. You must respond with ONLY a "
|
||||
"valid JSON object. Do not write any explanation before or after the JSON."
|
||||
f"{target_hints}"
|
||||
|
|
@ -645,11 +645,11 @@ def _run_multi_pass_advisor(
|
|||
"is_conversational": True,
|
||||
"user_notification": (
|
||||
"This dataset is already in conversational format. "
|
||||
"No conversion needed — columns can be mapped directly."
|
||||
"No conversion needed -- columns can be mapped directly."
|
||||
),
|
||||
}
|
||||
|
||||
# ── Pass 2: Map columns to roles ──
|
||||
# -- Pass 2: Map columns to roles --
|
||||
logger.info("Pass 2: Mapping columns to roles...")
|
||||
|
||||
t2 = time.monotonic()
|
||||
|
|
@ -693,23 +693,23 @@ def _run_multi_pass_advisor(
|
|||
|
||||
Here are worked examples to guide you:
|
||||
|
||||
Example 1 — Summarization dataset with columns ["document", "summary"]:
|
||||
Example 1 -- Summarization dataset with columns ["document", "summary"]:
|
||||
"document" is the input text → "user"
|
||||
"summary" is the output the model should generate → "assistant"
|
||||
Result: {{"document": "user", "summary": "assistant"}}
|
||||
|
||||
Example 2 — Question answering dataset with columns ["context", "question", "answer"]:
|
||||
Example 2 -- Question answering dataset with columns ["context", "question", "answer"]:
|
||||
"context" is input → "user"
|
||||
"question" is input → "user"
|
||||
"answer" is what the model should generate → "assistant"
|
||||
Result: {{"context": "user", "question": "user", "answer": "assistant"}}
|
||||
|
||||
Example 3 — Classification dataset with columns ["text", "label"]:
|
||||
Example 3 -- Classification dataset with columns ["text", "label"]:
|
||||
"text" is input → "user"
|
||||
"label" is the output the model should predict → "assistant"
|
||||
Result: {{"text": "user", "label": "assistant"}}
|
||||
|
||||
Example 4 — Translation dataset with columns ["en", "fr"]:
|
||||
Example 4 -- Translation dataset with columns ["en", "fr"]:
|
||||
"en" is the source language (input) → "user"
|
||||
"fr" is the target language (output) → "assistant"
|
||||
Result: {{"en": "user", "fr": "assistant"}}
|
||||
|
|
@ -725,7 +725,7 @@ def _run_multi_pass_advisor(
|
|||
"notes": "<brief explanation of why you assigned roles this way>"
|
||||
}}
|
||||
|
||||
REMEMBER: There must be at least one "user" column AND at least one "assistant" column. If all columns are "user", you made a mistake — the output/target column should be "assistant".
|
||||
REMEMBER: There must be at least one "user" column AND at least one "assistant" column. If all columns are "user", you made a mistake -- the output/target column should be "assistant".
|
||||
|
||||
Respond with ONLY the JSON object."""),
|
||||
},
|
||||
|
|
@ -738,7 +738,7 @@ def _run_multi_pass_advisor(
|
|||
logger.warning(f"Advisor Pass 2 failed to produce JSON: {raw2[:200]}")
|
||||
return None
|
||||
|
||||
# ── Extract and validate column roles from Pass 2 ──
|
||||
# -- Extract and validate column roles from Pass 2 --
|
||||
column_roles = pass2.get("column_roles", {})
|
||||
label_map = pass2.get("label_mapping") or {} # may be null
|
||||
|
||||
|
|
@ -750,7 +750,7 @@ def _run_multi_pass_advisor(
|
|||
)
|
||||
return None # triggers fallback to simple classification
|
||||
|
||||
# ── Pass 3: System prompt (non-conversational datasets only) ──
|
||||
# -- Pass 3: System prompt (non-conversational datasets only) --
|
||||
sys_prompt = ""
|
||||
dtype = pass1.get("dataset_type", "unknown")
|
||||
is_conv = pass1.get("is_conversational", False)
|
||||
|
|
@ -802,7 +802,7 @@ def _run_multi_pass_advisor(
|
|||
)
|
||||
|
||||
if raw3:
|
||||
# Pass 3 returns raw text, not JSON — clean it up
|
||||
# Pass 3 returns raw text, not JSON -- clean it up
|
||||
cleaned = raw3.strip().strip('"').strip("'").strip()
|
||||
if len(cleaned) >= 20 and cleaned.lower() not in ("null", "none", ""):
|
||||
sys_prompt = cleaned
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""
|
||||
Hardware detection — run once at startup, read everywhere.
|
||||
Hardware detection -- run once at startup, read everywhere.
|
||||
|
||||
Usage:
|
||||
# At FastAPI lifespan startup:
|
||||
|
|
@ -93,14 +93,14 @@ def detect_hardware() -> DeviceType:
|
|||
DEVICE = DeviceType.CUDA
|
||||
CHAT_ONLY = False
|
||||
device_name = torch.cuda.get_device_properties(0).name
|
||||
print(f"Hardware detected: CUDA — {device_name}")
|
||||
print(f"Hardware detected: CUDA -- {device_name}")
|
||||
return DEVICE
|
||||
|
||||
# --- MLX: Apple Silicon ---
|
||||
if is_apple_silicon() and _has_mlx():
|
||||
DEVICE = DeviceType.MLX
|
||||
chip = platform.processor() or platform.machine()
|
||||
print(f"Hardware detected: MLX — Apple Silicon ({chip})")
|
||||
print(f"Hardware detected: MLX -- Apple Silicon ({chip})")
|
||||
return DEVICE
|
||||
|
||||
# --- Fallback ---
|
||||
|
|
@ -126,7 +126,7 @@ def get_device() -> DeviceType:
|
|||
def clear_gpu_cache():
|
||||
"""
|
||||
Clear GPU memory cache for the current device.
|
||||
Safe to call on any platform — no-ops gracefully.
|
||||
Safe to call on any platform -- no-ops gracefully.
|
||||
"""
|
||||
import gc
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ def clear_gpu_cache():
|
|||
torch.cuda.ipc_collect()
|
||||
elif device == DeviceType.MLX:
|
||||
# MLX manages memory automatically; no explicit cache clear needed.
|
||||
# mlx.core has no empty_cache equivalent — gc.collect() above is enough.
|
||||
# mlx.core has no empty_cache equivalent -- gc.collect() above is enough.
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ def get_gpu_memory_info() -> Dict[str, Any]:
|
|||
import mlx.core as mx
|
||||
import psutil
|
||||
|
||||
# MLX uses unified memory — report system memory as the pool
|
||||
# MLX uses unified memory -- report system memory as the pool
|
||||
total = psutil.virtual_memory().total
|
||||
# MLX doesn't expose per-process GPU allocation; report 0 as allocated
|
||||
allocated = 0
|
||||
|
|
@ -235,8 +235,8 @@ def get_gpu_summary() -> Dict[str, Any]:
|
|||
Return a compact summary of the primary GPU.
|
||||
|
||||
Returns dict with keys:
|
||||
gpu_name – e.g. "NVIDIA L4" (or None)
|
||||
vram_total_gb – e.g. 22.17 (or None)
|
||||
gpu_name - e.g. "NVIDIA L4" (or None)
|
||||
vram_total_gb - e.g. 22.17 (or None)
|
||||
"""
|
||||
mem = get_gpu_memory_info()
|
||||
if mem.get("available"):
|
||||
|
|
@ -289,19 +289,19 @@ def get_gpu_utilization() -> Dict[str, Any]:
|
|||
|
||||
Designed to be polled by the frontend during training (not streaming).
|
||||
Uses ``nvidia-smi --query-gpu`` which is the most accurate source for
|
||||
utilization %, temperature, and power draw – stats that PyTorch does
|
||||
utilization %, temperature, and power draw - stats that PyTorch does
|
||||
not expose.
|
||||
|
||||
Returns dict with keys:
|
||||
available – bool, whether stats could be retrieved
|
||||
gpu_utilization_pct – GPU core utilization %
|
||||
temperature_c – GPU temperature in °C
|
||||
vram_used_gb – VRAM currently used (GiB)
|
||||
vram_total_gb – VRAM total (GiB)
|
||||
vram_utilization_pct – VRAM used / total * 100
|
||||
power_draw_w – current power draw (W)
|
||||
power_limit_w – power limit (W)
|
||||
power_utilization_pct – power draw / limit * 100
|
||||
available - bool, whether stats could be retrieved
|
||||
gpu_utilization_pct - GPU core utilization %
|
||||
temperature_c - GPU temperature in °C
|
||||
vram_used_gb - VRAM currently used (GiB)
|
||||
vram_total_gb - VRAM total (GiB)
|
||||
vram_utilization_pct - VRAM used / total * 100
|
||||
power_draw_w - current power draw (W)
|
||||
power_limit_w - power limit (W)
|
||||
power_utilization_pct - power draw / limit * 100
|
||||
"""
|
||||
device = get_device()
|
||||
|
||||
|
|
@ -318,7 +318,7 @@ def get_gpu_utilization() -> Dict[str, Any]:
|
|||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
# ── nvidia-smi (most complete source) ───────────────────────
|
||||
# -- nvidia-smi (most complete source) -----------------------
|
||||
smi_data = {}
|
||||
try:
|
||||
import subprocess
|
||||
|
|
@ -354,7 +354,7 @@ def get_gpu_utilization() -> Dict[str, Any]:
|
|||
except Exception as e:
|
||||
logger.warning(f"nvidia-smi query failed: {e}")
|
||||
|
||||
# ── Backfill VRAM from torch.cuda if nvidia-smi returned [N/A] ──
|
||||
# -- Backfill VRAM from torch.cuda if nvidia-smi returned [N/A] --
|
||||
vram_used_mb = smi_data.get("vram_used_mb")
|
||||
vram_total_mb = smi_data.get("vram_total_mb")
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ def get_gpu_utilization() -> Dict[str, Any]:
|
|||
except Exception as e:
|
||||
logger.debug(f"torch.cuda VRAM backfill failed: {e}")
|
||||
|
||||
# ── Build response ──────────────────────────────────────────
|
||||
# -- Build response ------------------------------------------
|
||||
gpu_util = smi_data.get("gpu_util")
|
||||
temp = smi_data.get("temp")
|
||||
power_draw = smi_data.get("power_draw")
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from utils.models.model_config import load_model_defaults
|
|||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# ── Family-based inference defaults (loaded once, cached) ──────────────
|
||||
# -- Family-based inference defaults (loaded once, cached) --------------
|
||||
|
||||
_FAMILY_DEFAULTS: Optional[Dict[str, Any]] = None
|
||||
_FAMILY_PATTERNS: Optional[list] = None
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ def scan_checkpoints(
|
|||
# This is a valid training run
|
||||
checkpoints = []
|
||||
|
||||
# Placeholder for the main adapter — loss filled from last checkpoint below
|
||||
# Placeholder for the main adapter -- loss filled from last checkpoint below
|
||||
checkpoints.append((item.name, str(item), None))
|
||||
|
||||
# Scan for intermediate checkpoints (checkpoint-N subdirs)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ logger = get_logger(__name__)
|
|||
# Format: "canonical_model_name.yaml": [list of all equivalent model names]
|
||||
# Based on the model mapper provided - canonical filename is based on the first model name in the mapper
|
||||
MODEL_NAME_MAPPING = {
|
||||
# ── Embedding models ──
|
||||
# -- Embedding models --
|
||||
"unsloth_all-MiniLM-L6-v2.yaml": [
|
||||
"unsloth/all-MiniLM-L6-v2",
|
||||
"sentence-transformers/all-MiniLM-L6-v2",
|
||||
|
|
@ -58,7 +58,7 @@ MODEL_NAME_MAPPING = {
|
|||
"unsloth/Qwen3-Embedding-4B",
|
||||
"Qwen/Qwen3-Embedding-4B",
|
||||
],
|
||||
# ── Other models ──
|
||||
# -- Other models --
|
||||
"unsloth_answerdotai_ModernBERT-large.yaml": [
|
||||
"answerdotai/ModernBERT-large",
|
||||
],
|
||||
|
|
@ -551,7 +551,7 @@ def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool:
|
|||
Works for fine-tuned models since they inherit the base architecture.
|
||||
|
||||
For models that require transformers 5.x (e.g. GLM-4.7-Flash), the check
|
||||
runs in a subprocess with .venv_t5/ activated — same pattern as the
|
||||
runs in a subprocess with .venv_t5/ activated -- same pattern as the
|
||||
training and inference workers.
|
||||
|
||||
Args:
|
||||
|
|
@ -565,7 +565,7 @@ def is_vision_model(model_name: str, hf_token: Optional[str] = None) -> bool:
|
|||
|
||||
if needs_transformers_5(model_name):
|
||||
logger.info(
|
||||
"Model '%s' needs transformers 5.x — checking vision via subprocess",
|
||||
"Model '%s' needs transformers 5.x -- checking vision via subprocess",
|
||||
model_name,
|
||||
)
|
||||
return _is_vision_model_subprocess(model_name, hf_token = hf_token)
|
||||
|
|
@ -643,7 +643,7 @@ def detect_audio_type(model_name: str, hf_token: Optional[str] = None) -> Option
|
|||
"""
|
||||
Dynamically detect if a model is an audio model and return its type.
|
||||
|
||||
Fully dynamic — works for any model, not just known ones.
|
||||
Fully dynamic -- works for any model, not just known ones.
|
||||
Uses tokenizer_config.json special tokens to detect all 6 audio types.
|
||||
|
||||
Returns: audio_type string ('snac', 'csm', 'bicodec', 'dac', 'whisper', 'audio_vlm') or None.
|
||||
|
|
@ -751,7 +751,7 @@ def detect_mmproj_file(path: str) -> Optional[str]:
|
|||
Find the mmproj (vision projection) GGUF file in a directory.
|
||||
|
||||
Args:
|
||||
path: Directory to search — or a .gguf file (uses its parent dir).
|
||||
path: Directory to search -- or a .gguf file (uses its parent dir).
|
||||
|
||||
Returns:
|
||||
Full path to the mmproj .gguf file, or None if not found.
|
||||
|
|
@ -775,7 +775,7 @@ def detect_gguf_model(path: str) -> Optional[str]:
|
|||
1. path is a direct .gguf file path
|
||||
2. path is a directory containing .gguf files
|
||||
|
||||
Skips mmproj (vision projection) files — those must be passed via
|
||||
Skips mmproj (vision projection) files -- those must be passed via
|
||||
``--mmproj``, not ``-m``. Use :func:`detect_mmproj_file` instead.
|
||||
|
||||
Returns the full path to the .gguf file if found, None otherwise.
|
||||
|
|
@ -1149,7 +1149,7 @@ def scan_exported_models(
|
|||
continue
|
||||
|
||||
# Check for flat GGUF export (e.g. exports/gemma-3-4b-it-finetune-gguf/)
|
||||
# Filter out mmproj (vision projection) files — they aren't loadable as main models
|
||||
# Filter out mmproj (vision projection) files -- they aren't loadable as main models
|
||||
gguf_files = [f for f in run_dir.glob("*.gguf") if not _is_mmproj(f.name)]
|
||||
if gguf_files:
|
||||
base_model = None
|
||||
|
|
@ -1585,7 +1585,7 @@ class ModelConfig:
|
|||
|
||||
if not LlamaCppBackend._find_llama_server_binary():
|
||||
raise RuntimeError(
|
||||
"llama-server binary not found — cannot load GGUF models. "
|
||||
"llama-server binary not found -- cannot load GGUF models. "
|
||||
"Run setup.sh to build it, or set LLAMA_SERVER_PATH."
|
||||
)
|
||||
|
||||
|
|
@ -1599,7 +1599,7 @@ class ModelConfig:
|
|||
if best:
|
||||
variant = _extract_quant_label(best)
|
||||
else:
|
||||
variant = "Q4_K_M" # Fallback — llama-server's own default
|
||||
variant = "Q4_K_M" # Fallback -- llama-server's own default
|
||||
|
||||
display_name = f"{identifier.split('/')[-1]} ({variant})"
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ logger = get_logger(__name__)
|
|||
# Detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Lowercase substrings — if ANY appears anywhere in the lowered model name,
|
||||
# Lowercase substrings -- if ANY appears anywhere in the lowered model name,
|
||||
# we need transformers 5.x.
|
||||
TRANSFORMERS_5_MODEL_SUBSTRINGS: tuple[str, ...] = (
|
||||
"ministral-3-", # Ministral-3-{3,8,14}B-{Instruct,Reasoning,Base}-2512
|
||||
|
|
@ -61,7 +61,7 @@ _tokenizer_class_cache: dict[str, bool] = {}
|
|||
TRANSFORMERS_5_VERSION = "5.3.0"
|
||||
TRANSFORMERS_DEFAULT_VERSION = "4.57.6"
|
||||
|
||||
# Pre-installed directory for transformers 5.x — created by setup.sh / setup.ps1
|
||||
# Pre-installed directory for transformers 5.x -- created by setup.sh / setup.ps1
|
||||
_VENV_T5_DIR = str(Path.home() / ".unsloth" / "studio" / ".venv_t5")
|
||||
|
||||
|
||||
|
|
@ -205,7 +205,7 @@ def needs_transformers_5(model_name: str) -> bool:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Version switching (in-process — used only by export)
|
||||
# Version switching (in-process -- used only by export)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ _PURGE_PREFIXES = (
|
|||
"trl",
|
||||
"accelerate",
|
||||
"auto_gptq",
|
||||
# NOTE: bitsandbytes is intentionally EXCLUDED — it registers torch custom
|
||||
# NOTE: bitsandbytes is intentionally EXCLUDED -- it registers torch custom
|
||||
# operators at import time via torch.library.define(). Those registrations
|
||||
# live in torch's global operator registry which survives module purge.
|
||||
# Re-importing bitsandbytes after purge → duplicate registration → crash.
|
||||
|
|
@ -442,7 +442,7 @@ def ensure_transformers_version(model_name: str) -> None:
|
|||
in_memory_major = int(in_memory.split(".")[0])
|
||||
if in_memory_major == target_major:
|
||||
logger.info(
|
||||
"transformers %s already loaded — correct for '%s'",
|
||||
"transformers %s already loaded -- correct for '%s'",
|
||||
in_memory,
|
||||
model_name,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
"use client";
|
||||
|
||||
// Avatar removed — caused circular crop on image thumbnails
|
||||
// Avatar removed -- caused circular crop on image thumbnails
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import {
|
||||
Dialog,
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ function ModelRow({
|
|||
return content;
|
||||
}
|
||||
|
||||
// ── GGUF Variant Expander ────────────────────────────────────
|
||||
// -- GGUF Variant Expander ------------------------------------
|
||||
|
||||
function GgufVariantExpander({
|
||||
repoId,
|
||||
|
|
@ -358,7 +358,7 @@ function GgufVariantExpander({
|
|||
);
|
||||
}
|
||||
|
||||
// ── Detect GGUF repos by naming convention ────────────────────
|
||||
// -- Detect GGUF repos by naming convention --------------------
|
||||
|
||||
function isGgufRepo(id: string): boolean {
|
||||
return id.toUpperCase().includes("-GGUF");
|
||||
|
|
@ -376,7 +376,7 @@ function extractParamLabel(id: string): string | undefined {
|
|||
let _cachedGgufCache: CachedGgufRepo[] = [];
|
||||
let _cachedModelsCache: CachedModelRepo[] = [];
|
||||
|
||||
// ── Hub Model Picker ──────────────────────────────────────────
|
||||
// -- Hub Model Picker ------------------------------------------
|
||||
|
||||
export function HubModelPicker({
|
||||
models,
|
||||
|
|
@ -583,7 +583,7 @@ export function HubModelPicker({
|
|||
return () => { clearTimeout(timer); obs.disconnect(); };
|
||||
}, [recommendedSentinel, hasMoreRecommended, recommendedPage, scrollRef]);
|
||||
|
||||
/** Handle clicking a model row — GGUF repos expand, others load directly. */
|
||||
/** Handle clicking a model row -- GGUF repos expand, others load directly. */
|
||||
const handleModelClick = useCallback(
|
||||
(id: string) => {
|
||||
if (isGgufRepo(id)) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
HoverCardContent,
|
||||
} from "@/components/ui/hover-card";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
// -- Helpers --------------------------------------------------
|
||||
|
||||
const extractDomain = (url: string): string => {
|
||||
try {
|
||||
|
|
@ -33,7 +33,7 @@ const getDomainInitial = (url: string): string => {
|
|||
return domain.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
// ── Sub-components ───────────────────────────────────────────
|
||||
// -- Sub-components -------------------------------------------
|
||||
|
||||
function SourceIcon({
|
||||
url,
|
||||
|
|
@ -116,7 +116,7 @@ function Source({
|
|||
);
|
||||
}
|
||||
|
||||
// ── Source badge with hover card ─────────────────────────────
|
||||
// -- Source badge with hover card -----------------------------
|
||||
|
||||
interface SourceData {
|
||||
url: string;
|
||||
|
|
@ -158,7 +158,7 @@ const SourceBadge: FC<{ source: SourceData }> = ({ source }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// ── Grouped sources with 2-row collapse ─────────────────────
|
||||
// -- Grouped sources with 2-row collapse ---------------------
|
||||
|
||||
const SourcesGroup: FC = () => {
|
||||
const message = useMessage();
|
||||
|
|
@ -238,7 +238,7 @@ const SourcesGroup: FC = () => {
|
|||
|
||||
return (
|
||||
<div className="relative mt-2">
|
||||
{/* Hidden measurement container — renders all badges to measure row positions */}
|
||||
{/* Hidden measurement container -- renders all badges to measure row positions */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
aria-hidden
|
||||
|
|
@ -288,11 +288,11 @@ const SourcesGroup: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// ── Individual source (renders null — SourcesGroup handles all) ──
|
||||
// -- Individual source (renders null -- SourcesGroup handles all) --
|
||||
|
||||
const SourcesNoop: FC<Record<string, unknown>> = () => null;
|
||||
|
||||
// ── Exports ──────────────────────────────────────────────────
|
||||
// -- Exports --------------------------------------------------
|
||||
|
||||
const Sources = memo(SourcesNoop) as unknown as FC<Record<string, unknown>> & {
|
||||
Root: typeof Source;
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ const ToolGroupImpl: FC<
|
|||
> = ({ children, startIndex, endIndex }) => {
|
||||
const toolCount = endIndex - startIndex + 1;
|
||||
|
||||
// Single tool call — render directly without wrapper
|
||||
// Single tool call -- render directly without wrapper
|
||||
if (toolCount <= 1) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function CopyBtn({ text }: { text: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
/** Render code with syntax highlighting via Streamdown + shiki. No extra borders — inherits parent container. */
|
||||
/** Render code with syntax highlighting via Streamdown + shiki. No extra borders -- inherits parent container. */
|
||||
function HighlightedCode({ code: source, language }: { code: string; language: string }) {
|
||||
const markdown = useMemo(
|
||||
() => `\`\`\`${language}\n${truncate(source)}\n\`\`\``,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export const env = {
|
|||
BASE_URL: import.meta.env.BASE_URL,
|
||||
} as const;
|
||||
|
||||
// ── Platform / device type ──────────────────────────────────
|
||||
// -- Platform / device type ----------------------------------
|
||||
|
||||
export type DeviceType = "mac" | "windows" | "linux" | string;
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null {
|
|||
let canceled = false;
|
||||
|
||||
async function initializeAuthForm(): Promise<void> {
|
||||
// Always check the server first — localStorage flags can be stale
|
||||
// Always check the server first -- localStorage flags can be stale
|
||||
// (e.g. tokens from a previous install attempt). The server's
|
||||
// /api/auth/status is the source of truth for requires_password_change.
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ async function resolveUseAdapter(
|
|||
if (!thread?.pairId) {
|
||||
return undefined;
|
||||
}
|
||||
// model1/model2 threads don't use the adapter toggle — each side
|
||||
// model1/model2 threads don't use the adapter toggle -- each side
|
||||
// loads its own model via /api/inference/load before generation.
|
||||
if (thread.modelType === "model1" || thread.modelType === "model2") {
|
||||
return undefined;
|
||||
|
|
@ -357,7 +357,7 @@ async function autoLoadSmallestModel(): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
// No cached models found — try downloading a small default GGUF
|
||||
// No cached models found -- try downloading a small default GGUF
|
||||
toast("Downloading a small model…", {
|
||||
id: toastId,
|
||||
description: "No downloaded models found. Fetching Qwen3.5-4B (UD-Q4_K_XL).",
|
||||
|
|
@ -463,7 +463,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
}
|
||||
const useAdapter = await resolveUseAdapter(unstable_threadId);
|
||||
|
||||
// ── Audio model path (non-streaming) ─────────────────────
|
||||
// -- Audio model path (non-streaming) ---------------------
|
||||
const activeModel = runtime.models.find(
|
||||
(m) => m.id === params.checkpoint,
|
||||
);
|
||||
|
|
@ -552,7 +552,7 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
let cumulativeText = "";
|
||||
let reasoningStartAt: number | null = null;
|
||||
let reasoningDuration = 0;
|
||||
// Tool call content parts — accumulated and yielded cumulatively.
|
||||
// Tool call content parts -- accumulated and yielded cumulatively.
|
||||
// result is set directly on the tool-call part when tool_end arrives.
|
||||
const toolCallParts: ToolCallMessagePart[] = [];
|
||||
let serverMetadata: { usage?: ServerUsage; timings?: ServerTimings } | null = null;
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ type CompareModelSelection = {
|
|||
|
||||
/**
|
||||
* Detect if this is a LoRA base-vs-fine-tuned compare.
|
||||
* Returns true when the loaded checkpoint is a LoRA — in that case
|
||||
* Returns true when the loaded checkpoint is a LoRA -- in that case
|
||||
* we use the fast simultaneous base/lora adapter-toggle path.
|
||||
*/
|
||||
function useIsLoraCompare(): boolean {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function WizardLayout() {
|
|||
const hasFiredRef = useRef(false);
|
||||
const isFinalStep = currentStep === STEPS.length;
|
||||
|
||||
// Only redirect on initial mount — not on re-renders after markOnboardingDone()
|
||||
// Only redirect on initial mount -- not on re-renders after markOnboardingDone()
|
||||
// which would override explicit /chat navigation from skip buttons.
|
||||
const checkedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ const TAB_SWITCH_FIT_DELAY_MS = 110;
|
|||
/**
|
||||
* Maximum RAF iterations to wait for React Flow's ResizeObserver to populate
|
||||
* `node.measured` dimensions before calling fitView. ~20 frames ≈ 333 ms at
|
||||
* 60 fps — more than enough for the render → layout → ResizeObserver cycle.
|
||||
* 60 fps -- more than enough for the render → layout → ResizeObserver cycle.
|
||||
*/
|
||||
const MAX_FIT_VIEW_RETRIES = 20;
|
||||
/**
|
||||
|
|
@ -477,7 +477,7 @@ export function RecipeStudioPage({
|
|||
return;
|
||||
}
|
||||
if (retries >= MAX_FIT_VIEW_RETRIES) {
|
||||
// Timed out waiting — fit with whatever we have (graceful fallback).
|
||||
// Timed out waiting -- fit with whatever we have (graceful fallback).
|
||||
doFit();
|
||||
return;
|
||||
}
|
||||
|
|
@ -491,7 +491,7 @@ export function RecipeStudioPage({
|
|||
return;
|
||||
}
|
||||
} else {
|
||||
// Measurements were reset (e.g. by updateNodeInternals) — restart
|
||||
// Measurements were reset (e.g. by updateNodeInternals) -- restart
|
||||
// the stability counter.
|
||||
stableCount = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export function DatasetPreviewDialog({
|
|||
const availableRoles = getAvailableRoles(effectiveIsVlm, datasetFormat, effectiveIsAudio);
|
||||
const isHfDataset = datasetSource === "huggingface";
|
||||
|
||||
// ── AI Assist ──────────────────────────────────────────────────────
|
||||
// -- AI Assist ------------------------------------------------------
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
const [aiError, setAiError] = useState<string | null>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export function ModelSection() {
|
|||
return applyPriorityOrdering(ids);
|
||||
}, [hfResults, selectedModel]);
|
||||
|
||||
// Filter out GGUF models — they can't be used for training
|
||||
// Filter out GGUF models -- they can't be used for training
|
||||
const trainableLocalModels = useMemo(
|
||||
() =>
|
||||
localModels.filter((m) => {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export async function uploadTrainingDataset(
|
|||
return res.json();
|
||||
}
|
||||
|
||||
// ── AI Assist ────────────────────────────────────────────────────────
|
||||
// -- AI Assist --------------------------------------------------------
|
||||
|
||||
type AiAssistMappingArgs = {
|
||||
columns: string[];
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function useGpuUtilization(
|
|||
const json = (await res.json()) as GpuUtilization;
|
||||
if (!cancelled) setData(json);
|
||||
} catch {
|
||||
// Silently ignore — next poll will retry
|
||||
// Silently ignore -- next poll will retry
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ async function fetchOnce(): Promise<HardwareInfo> {
|
|||
/**
|
||||
* Fetch hardware info from `GET /api/system/hardware`.
|
||||
*
|
||||
* The result is cached at module level — only one network request is made
|
||||
* The result is cached at module level -- only one network request is made
|
||||
* regardless of how many components call this hook.
|
||||
*/
|
||||
export function useHardwareInfo(): HardwareInfo {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const EXCLUDED_TAGS = new Set([
|
|||
]);
|
||||
|
||||
// Embedding / sentence-transformer models ship with onnx/openvino as additional
|
||||
// export formats — they should not be excluded by the tag check above.
|
||||
// export formats -- they should not be excluded by the tag check above.
|
||||
const EMBEDDING_TAGS = new Set([
|
||||
"sentence-transformers",
|
||||
"feature-extraction",
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Minimal scrollbar — thumb only, no track */
|
||||
/* Minimal scrollbar -- thumb only, no track */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
|
|
|
|||
|
|
@ -22,20 +22,20 @@ from pathlib import Path
|
|||
|
||||
IS_WINDOWS = sys.platform == "win32"
|
||||
|
||||
# ── Verbosity control ──────────────────────────────────────────────────────────
|
||||
# -- Verbosity control ----------------------------------------------------------
|
||||
# By default the installer shows a minimal progress bar (one line, in-place).
|
||||
# Set UNSLOTH_VERBOSE=1 in the environment to restore full per-step output:
|
||||
# Linux/Mac: UNSLOTH_VERBOSE=1 ./studio/setup.sh
|
||||
# Windows: $env:UNSLOTH_VERBOSE="1" ; .\studio\setup.ps1
|
||||
VERBOSE: bool = os.environ.get("UNSLOTH_VERBOSE", "0") == "1"
|
||||
|
||||
# Progress bar state — updated by _progress() as each install step runs.
|
||||
# Progress bar state -- updated by _progress() as each install step runs.
|
||||
# _TOTAL counts: pip-upgrade + 7 shared steps + triton (non-Windows) + local-plugin + finalize
|
||||
# Update _TOTAL here if you add or remove install steps in install_python_stack().
|
||||
_STEP: int = 0
|
||||
_TOTAL: int = 0 # set at runtime in install_python_stack() based on platform
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────
|
||||
# -- Paths --------------------------------------------------------------
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
REQ_ROOT = SCRIPT_DIR / "backend" / "requirements"
|
||||
SINGLE_ENV = REQ_ROOT / "single-env"
|
||||
|
|
@ -44,7 +44,7 @@ LOCAL_DD_UNSTRUCTURED_PLUGIN = (
|
|||
SCRIPT_DIR / "backend" / "plugins" / "data-designer-unstructured-seed"
|
||||
)
|
||||
|
||||
# ── Color support ──────────────────────────────────────────────────────
|
||||
# -- Color support ------------------------------------------------------
|
||||
|
||||
|
||||
def _enable_colors() -> bool:
|
||||
|
|
@ -72,7 +72,7 @@ def _enable_colors() -> bool:
|
|||
return True # Unix terminals support ANSI by default
|
||||
|
||||
|
||||
# Colors disabled — Colab and most CI runners render ANSI fine, but plain output
|
||||
# Colors disabled -- Colab and most CI runners render ANSI fine, but plain output
|
||||
# is cleaner in the notebook cell. Re-enable by setting _HAS_COLOR = _enable_colors()
|
||||
_HAS_COLOR = False
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ def _red(msg: str) -> str:
|
|||
def _progress(label: str) -> None:
|
||||
"""Print an in-place progress bar for the current install step.
|
||||
|
||||
Uses only stdlib (sys.stdout) — no extra packages required.
|
||||
Uses only stdlib (sys.stdout) -- no extra packages required.
|
||||
In VERBOSE mode this is a no-op; per-step labels are printed by run() instead.
|
||||
"""
|
||||
global _STEP
|
||||
|
|
@ -129,7 +129,7 @@ def run(
|
|||
# Packages to skip on Windows (require special build steps)
|
||||
WINDOWS_SKIP_PACKAGES = {"open_spiel", "triton_kernels"}
|
||||
|
||||
# ── uv bootstrap ──────────────────────────────────────────────────────
|
||||
# -- uv bootstrap ------------------------------------------------------
|
||||
|
||||
USE_UV = False # Set by _bootstrap_uv() at the start of install_python_stack()
|
||||
UV_NEEDS_SYSTEM = False # Set by _bootstrap_uv() via probe
|
||||
|
|
@ -285,7 +285,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None:
|
|||
download_file(url, dest)
|
||||
|
||||
|
||||
# ── Main install sequence ─────────────────────────────────────────────
|
||||
# -- Main install sequence ---------------------------------------------
|
||||
|
||||
|
||||
def install_python_stack() -> int:
|
||||
|
|
@ -316,7 +316,7 @@ def install_python_stack() -> int:
|
|||
req = REQ_ROOT / "extras.txt",
|
||||
)
|
||||
|
||||
# 3b. Extra dependencies (no-deps) — audio model support etc.
|
||||
# 3b. Extra dependencies (no-deps) -- audio model support etc.
|
||||
_progress("extra codecs")
|
||||
pip_install(
|
||||
"Installing extras (no-deps)",
|
||||
|
|
@ -325,7 +325,7 @@ def install_python_stack() -> int:
|
|||
req = REQ_ROOT / "extras-no-deps.txt",
|
||||
)
|
||||
|
||||
# 4. Overrides (torchao, transformers) — force-reinstall
|
||||
# 4. Overrides (torchao, transformers) -- force-reinstall
|
||||
_progress("dependency overrides")
|
||||
pip_install(
|
||||
"Installing dependency overrides",
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ $FrontendDir = Join-Path $ScriptDir "frontend"
|
|||
$OxcValidatorDir = Join-Path $ScriptDir "backend\core\data_recipe\oxc-validator"
|
||||
$IsPipInstall = -not (Test-Path $FrontendDir)
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# ---------------------------------------------
|
||||
# Helper functions
|
||||
# ─────────────────────────────────────────────
|
||||
# ---------------------------------------------
|
||||
|
||||
# Reload ALL environment variables from registry.
|
||||
# Picks up changes made by installers (winget, msi, etc.) including
|
||||
|
|
@ -78,7 +78,7 @@ function Find-Nvcc {
|
|||
return $null
|
||||
}
|
||||
|
||||
# Fallback: no version constraint — pick latest or whatever is available
|
||||
# Fallback: no version constraint -- pick latest or whatever is available
|
||||
|
||||
# 1. Check nvcc on PATH
|
||||
$cmd = Get-Command nvcc -ErrorAction SilentlyContinue
|
||||
|
|
@ -133,7 +133,7 @@ function Get-CudaComputeCapability {
|
|||
# Check if an nvcc binary supports a given sm_ architecture.
|
||||
# Uses `nvcc --list-gpu-code` which outputs sm_* tokens (--list-gpu-arch
|
||||
# outputs compute_* tokens instead). Available since CUDA 11.6.
|
||||
# Returns $false if the flag isn't supported (old toolkit) — safer to reject
|
||||
# Returns $false if the flag isn't supported (old toolkit) -- safer to reject
|
||||
# and fall back to scanning/PTX than to assume support and fail later.
|
||||
function Test-NvccArchSupport {
|
||||
param([string]$NvccExe, [string]$Arch)
|
||||
|
|
@ -247,9 +247,9 @@ function Find-VsBuildTools {
|
|||
return $null
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# ---------------------------------------------
|
||||
# Banner
|
||||
# ─────────────────────────────────────────────
|
||||
# ---------------------------------------------
|
||||
Write-Host "+==============================================+" -ForegroundColor Green
|
||||
Write-Host "| Unsloth Studio Setup (Windows) |" -ForegroundColor Green
|
||||
Write-Host "+==============================================+" -ForegroundColor Green
|
||||
|
|
@ -861,7 +861,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
|
|||
Write-Host ""
|
||||
Write-Host "Building frontend..." -ForegroundColor Cyan
|
||||
|
||||
# ── Tailwind v4 .gitignore workaround ──
|
||||
# -- Tailwind v4 .gitignore workaround --
|
||||
# Tailwind v4's oxide scanner respects .gitignore in parent directories.
|
||||
# Python venvs create a .gitignore with "*" (ignore everything), which
|
||||
# prevents Tailwind from scanning .tsx source files for class names.
|
||||
|
|
@ -907,12 +907,12 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
|
|||
Pop-Location
|
||||
$ErrorActionPreference = $prevEAP_npm
|
||||
|
||||
# ── Restore hidden .gitignore files ──
|
||||
# -- Restore hidden .gitignore files --
|
||||
foreach ($gi in $HiddenGitignores) {
|
||||
Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# ── Validate CSS output ──
|
||||
# -- Validate CSS output --
|
||||
$CssFiles = Get-ChildItem (Join-Path $DistDir "assets") -Filter "*.css" -ErrorAction SilentlyContinue
|
||||
$MaxCssSize = ($CssFiles | Measure-Object -Property Length -Maximum).Maximum
|
||||
if ($MaxCssSize -lt 100000) {
|
||||
|
|
@ -1214,7 +1214,7 @@ python "$PSScriptRoot\install_python_stack.py"
|
|||
# Restore ErrorActionPreference after pip/python work
|
||||
$ErrorActionPreference = $prevEAP
|
||||
|
||||
# ── Pre-install transformers 5.x into .venv_t5/ ──
|
||||
# -- Pre-install transformers 5.x into .venv_t5/ --
|
||||
# Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing
|
||||
# at runtime (slow, ~10-15s), we pre-install into a separate directory.
|
||||
# The training subprocess just prepends .venv_t5/ to sys.path -- instant switch.
|
||||
|
|
@ -1291,7 +1291,7 @@ if ($OpenSslRoot) {
|
|||
# ==========================================================================
|
||||
# PHASE 4: Build llama.cpp with CUDA for GGUF inference + export
|
||||
# ==========================================================================
|
||||
# Builds at ~/.unsloth/llama.cpp — a single shared location under the user's
|
||||
# Builds at ~/.unsloth/llama.cpp -- a single shared location under the user's
|
||||
# home directory. This is used by both the inference server and the GGUF
|
||||
# export pipeline (unsloth-zoo).
|
||||
# We build:
|
||||
|
|
@ -1360,7 +1360,7 @@ if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) {
|
|||
$BuildOk = $true
|
||||
$FailedStep = ""
|
||||
|
||||
# Re-sanitize CUDA_PATH_V* vars — Refresh-Environment (called during
|
||||
# Re-sanitize CUDA_PATH_V* vars -- Refresh-Environment (called during
|
||||
# Node/Python installs above) may have repopulated conflicting versioned
|
||||
# vars from the Machine registry.
|
||||
if ($HasNvidiaSmi -and $CudaToolkitRoot) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ set -euo pipefail
|
|||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# ── Helper: run command quietly, show output only on failure ──
|
||||
# -- Helper: run command quietly, show output only on failure --
|
||||
_run_quiet() {
|
||||
local on_fail=$1
|
||||
local label=$2
|
||||
|
|
@ -48,19 +48,19 @@ echo "╔═══════════════════════
|
|||
echo "║ Unsloth Studio Setup Script ║"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
|
||||
# ── Clean up stale Unsloth compiled caches ──
|
||||
# -- Clean up stale Unsloth compiled caches --
|
||||
rm -rf "$REPO_ROOT/unsloth_compiled_cache"
|
||||
rm -rf "$SCRIPT_DIR/backend/unsloth_compiled_cache"
|
||||
rm -rf "$SCRIPT_DIR/tmp/unsloth_compiled_cache"
|
||||
|
||||
# ── Detect Colab (like unsloth does) ──
|
||||
# -- Detect Colab (like unsloth does) --
|
||||
IS_COLAB=false
|
||||
keynames=$'\n'$(printenv | cut -d= -f1)
|
||||
if [[ "$keynames" == *$'\nCOLAB_'* ]]; then
|
||||
IS_COLAB=true
|
||||
fi
|
||||
|
||||
# ── Detect whether frontend needs building ──
|
||||
# -- Detect whether frontend needs building --
|
||||
# Skip if dist/ exists AND no tracked input is newer than dist/.
|
||||
# Checks top-level config/entry files and src/, public/ recursively.
|
||||
# This handles: PyPI installs (dist/ bundled), repeat runs (no changes),
|
||||
|
|
@ -107,7 +107,7 @@ else
|
|||
fi
|
||||
|
||||
if [ "$NEED_NODE" = true ]; then
|
||||
# ── 2. Install nvm ──
|
||||
# -- 2. Install nvm --
|
||||
export NODE_OPTIONS=--dns-result-order=ipv4first # or else fails on colab.
|
||||
echo "Installing nvm..."
|
||||
curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash > /dev/null 2>&1
|
||||
|
|
@ -117,7 +117,7 @@ if [ "$NEED_NODE" = true ]; then
|
|||
set +u
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
|
||||
# ── Fix npmrc conflict with nvm ──
|
||||
# -- Fix npmrc conflict with nvm --
|
||||
# System npm (apt, conda, etc.) may have written `prefix` or `globalconfig`
|
||||
# to ~/.npmrc, which is incompatible with nvm and causes "nvm use" to fail
|
||||
# with: "has a `globalconfig` and/or a `prefix` setting, which are
|
||||
|
|
@ -129,12 +129,12 @@ if [ "$NEED_NODE" = true ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# ── 3. Install Node LTS ──
|
||||
# -- 3. Install Node LTS --
|
||||
echo "Installing Node LTS..."
|
||||
run_quiet "nvm install" nvm install --lts
|
||||
nvm use --lts > /dev/null 2>&1
|
||||
set -u
|
||||
# ── 4. Verify versions ──
|
||||
# -- 4. Verify versions --
|
||||
NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1)
|
||||
NPM_MAJOR=$(npm -v | cut -d. -f1)
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ fi
|
|||
|
||||
echo "✅ Node $(node -v) | npm $(npm -v)"
|
||||
|
||||
# ── 5. Build frontend ──
|
||||
# -- 5. Build frontend --
|
||||
cd "$SCRIPT_DIR/frontend"
|
||||
|
||||
# Tailwind v4's oxide scanner respects .gitignore in parent directories.
|
||||
|
|
@ -194,16 +194,16 @@ echo "✅ Frontend built to frontend/dist"
|
|||
|
||||
fi # end frontend build check
|
||||
|
||||
# ── oxc-validator runtime (needs npm -- skip if not available) ──
|
||||
# -- oxc-validator runtime (needs npm -- skip if not available) --
|
||||
if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm &>/dev/null; then
|
||||
cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator"
|
||||
run_quiet "npm install (oxc validator runtime)" npm install
|
||||
cd "$SCRIPT_DIR"
|
||||
fi
|
||||
|
||||
# ── 6. Python venv + deps ──
|
||||
# -- 6. Python venv + deps --
|
||||
|
||||
# ── 6a. Discover best Python >= 3.11 and < 3.14 (i.e. 3.11.x, 3.12.x, or 3.13.x) ──
|
||||
# -- 6a. Discover best Python >= 3.11 and < 3.14 (i.e. 3.11.x, 3.12.x, or 3.13.x) --
|
||||
MIN_PY_MINOR=11 # minimum minor version (>= 3.11)
|
||||
MAX_PY_MINOR=13 # maximum minor version (< 3.14)
|
||||
BEST_PY=""
|
||||
|
|
@ -274,7 +274,7 @@ if [ -z "$BEST_PY" ]; then
|
|||
fi
|
||||
|
||||
BEST_VER=$("$BEST_PY" --version 2>&1 | awk '{print $2}')
|
||||
echo "✅ Using $BEST_PY ($BEST_VER) — compatible (3.${MIN_PY_MINOR}.x – 3.${MAX_PY_MINOR}.x)"
|
||||
echo "✅ Using $BEST_PY ($BEST_VER) -- compatible (3.${MIN_PY_MINOR}.x - 3.${MAX_PY_MINOR}.x)"
|
||||
|
||||
REQ_ROOT="$SCRIPT_DIR/backend/requirements"
|
||||
SINGLE_ENV_CONSTRAINTS="$REQ_ROOT/single-env/constraints.txt"
|
||||
|
|
@ -311,7 +311,7 @@ else
|
|||
source "$VENV_DIR/bin/activate"
|
||||
fi
|
||||
|
||||
# ── Ensure uv is available (much faster than pip) ──
|
||||
# -- Ensure uv is available (much faster than pip) --
|
||||
USE_UV=false
|
||||
if command -v uv &>/dev/null; then
|
||||
USE_UV=true
|
||||
|
|
@ -331,7 +331,7 @@ fast_install() {
|
|||
cd "$SCRIPT_DIR"
|
||||
install_python_stack
|
||||
|
||||
# ── 6b. Pre-install transformers 5.x into .venv_t5/ ──
|
||||
# -- 6b. Pre-install transformers 5.x into .venv_t5/ --
|
||||
# Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing
|
||||
# at runtime (slow, ~10-15s), we pre-install into a separate directory.
|
||||
# The training subprocess just prepends .venv_t5/ to sys.path -- instant switch.
|
||||
|
|
@ -346,7 +346,7 @@ run_quiet "install hf_xet for t5" fast_install --target "$VENV_T5_DIR" --no-deps
|
|||
run_quiet "install tiktoken for t5" fast_install --target "$VENV_T5_DIR" "tiktoken"
|
||||
echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
|
||||
|
||||
# ── 7. WSL: pre-install GGUF build dependencies ──
|
||||
# -- 7. WSL: pre-install GGUF build dependencies --
|
||||
# On WSL, sudo requires a password and can't be entered during GGUF export
|
||||
# (runs in a non-interactive subprocess). Install build deps here instead.
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
|
|
@ -407,8 +407,8 @@ if grep -qi microsoft /proc/version 2>/dev/null; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# ── 8. Build llama.cpp binaries for GGUF inference + export ──
|
||||
# Builds at ~/.unsloth/llama.cpp — a single shared location under the user's
|
||||
# -- 8. Build llama.cpp binaries for GGUF inference + export --
|
||||
# Builds at ~/.unsloth/llama.cpp -- a single shared location under the user's
|
||||
# home directory. This is used by both the inference server and the GGUF
|
||||
# export pipeline (unsloth-zoo).
|
||||
# - llama-server: for GGUF model inference
|
||||
|
|
@ -427,11 +427,11 @@ rm -rf "$LLAMA_CPP_DIR"
|
|||
# Check prerequisites
|
||||
if ! command -v cmake &>/dev/null; then
|
||||
echo ""
|
||||
echo "⚠️ cmake not found — skipping llama-server build (GGUF inference won't be available)"
|
||||
echo "⚠️ cmake not found -- skipping llama-server build (GGUF inference won't be available)"
|
||||
echo " Install cmake and re-run setup.sh to enable GGUF inference."
|
||||
elif ! command -v git &>/dev/null; then
|
||||
echo ""
|
||||
echo "⚠️ git not found — skipping llama-server build (GGUF inference won't be available)"
|
||||
echo "⚠️ git not found -- skipping llama-server build (GGUF inference won't be available)"
|
||||
else
|
||||
echo ""
|
||||
echo "Building llama-server for GGUF inference..."
|
||||
|
|
@ -495,7 +495,7 @@ rm -rf "$LLAMA_CPP_DIR"
|
|||
# Multi-threaded nvcc compilation (uses all CPU cores per .cu file)
|
||||
CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_FLAGS=--threads=0"
|
||||
elif [ -d /usr/local/cuda ] || nvidia-smi &>/dev/null; then
|
||||
echo " CUDA driver detected but nvcc not found — building CPU-only"
|
||||
echo " CUDA driver detected but nvcc not found -- building CPU-only"
|
||||
echo " To enable GPU: install cuda-toolkit or add nvcc to PATH"
|
||||
else
|
||||
echo " Building CPU-only (no CUDA detected)..."
|
||||
|
|
@ -519,7 +519,7 @@ rm -rf "$LLAMA_CPP_DIR"
|
|||
# Also build llama-quantize (needed by unsloth-zoo's GGUF export pipeline)
|
||||
if [ "$BUILD_OK" = true ]; then
|
||||
run_quiet_no_exit "build llama-quantize" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-quantize -j"$NCPU" || true
|
||||
# Symlink to llama.cpp root — check_llama_cpp() looks for the binary there
|
||||
# Symlink to llama.cpp root -- check_llama_cpp() looks for the binary there
|
||||
QUANTIZE_BIN="$LLAMA_CPP_DIR/build/bin/llama-quantize"
|
||||
if [ -f "$QUANTIZE_BIN" ]; then
|
||||
ln -sf build/bin/llama-quantize "$LLAMA_CPP_DIR/llama-quantize"
|
||||
|
|
@ -530,13 +530,13 @@ rm -rf "$LLAMA_CPP_DIR"
|
|||
if [ -f "$LLAMA_SERVER_BIN" ]; then
|
||||
echo "✅ llama-server built at $LLAMA_SERVER_BIN"
|
||||
else
|
||||
echo "⚠️ llama-server binary not found after build — GGUF inference won't be available"
|
||||
echo "⚠️ llama-server binary not found after build -- GGUF inference won't be available"
|
||||
fi
|
||||
if [ -f "$LLAMA_CPP_DIR/llama-quantize" ]; then
|
||||
echo "✅ llama-quantize available for GGUF export"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ llama-server build failed — GGUF inference won't be available, but everything else works"
|
||||
echo "⚠️ llama-server build failed -- GGUF inference won't be available, but everything else works"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1373,7 +1373,7 @@ def openenv_vllm_reload_weights():
|
|||
except (ImportError, NameError, Exception) as e:
|
||||
logger.info(f"Unsloth: Failed to import trl openenv: {e}")
|
||||
logger.info(
|
||||
"Unsloth: trl.experimental.openenv not available — skipping RL openenv patches."
|
||||
"Unsloth: trl.experimental.openenv not available -- skipping RL openenv patches."
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -1537,7 +1537,7 @@ def create_huggingface_repo(
|
|||
card.data.datasets = datasets
|
||||
card.push_to_hub(save_directory, token = token)
|
||||
except:
|
||||
# Repo already exists — update datasets metadata separately
|
||||
# Repo already exists -- update datasets metadata separately
|
||||
if datasets:
|
||||
try:
|
||||
from huggingface_hub import metadata_update
|
||||
|
|
@ -1593,7 +1593,7 @@ def upload_to_huggingface(
|
|||
card.data.datasets = datasets
|
||||
card.push_to_hub(save_directory, token = token)
|
||||
except:
|
||||
# Repo already exists — update datasets metadata separately
|
||||
# Repo already exists -- update datasets metadata separately
|
||||
if datasets:
|
||||
try:
|
||||
from huggingface_hub import metadata_update
|
||||
|
|
@ -1991,7 +1991,7 @@ def unsloth_save_pretrained_gguf(
|
|||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to save/merge model: {e}")
|
||||
else:
|
||||
# Non-PEFT model — checkpoint files already exist on disk.
|
||||
# Non-PEFT model -- checkpoint files already exist on disk.
|
||||
# Point save_to_gguf at the original checkpoint path instead of
|
||||
# re-saving to a temporary "model" subdirectory.
|
||||
original_path = getattr(self.config, "_name_or_path", None)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def _studio_venv_python() -> Optional[Path]:
|
|||
def _find_run_py() -> Optional[Path]:
|
||||
"""Find studio/backend/run.py.
|
||||
|
||||
No CWD dependency — works from any directory.
|
||||
No CWD dependency -- works from any directory.
|
||||
Since studio/ is now a proper package (has __init__.py), it lives in
|
||||
site-packages after pip install, right next to unsloth_cli/.
|
||||
"""
|
||||
|
|
@ -52,7 +52,7 @@ def _find_run_py() -> Optional[Path]:
|
|||
def _find_setup_script() -> Optional[Path]:
|
||||
"""Find studio/setup.sh or studio/setup.ps1.
|
||||
|
||||
No CWD dependency — works from any directory.
|
||||
No CWD dependency -- works from any directory.
|
||||
"""
|
||||
name = "setup.ps1" if platform.system() == "Windows" else "setup.sh"
|
||||
# 1. Relative to __file__ (site-packages or editable repo root)
|
||||
|
|
@ -69,7 +69,7 @@ def _find_setup_script() -> Optional[Path]:
|
|||
return None
|
||||
|
||||
|
||||
# ── unsloth studio (server) ──────────────────────────────────────────
|
||||
# -- unsloth studio (server) ------------------------------------------
|
||||
|
||||
|
||||
@studio_app.callback(invoke_without_command = True)
|
||||
|
|
@ -116,7 +116,7 @@ def studio_default(
|
|||
try:
|
||||
rc = proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
# Child has its own signal handler — let it finish
|
||||
# Child has its own signal handler -- let it finish
|
||||
rc = proc.wait()
|
||||
if rc != 0:
|
||||
typer.echo(
|
||||
|
|
@ -166,7 +166,7 @@ def studio_default(
|
|||
typer.echo("\nShutting down...")
|
||||
|
||||
|
||||
# ── unsloth studio setup ─────────────────────────────────────────────
|
||||
# -- unsloth studio setup ---------------------------------------------
|
||||
|
||||
|
||||
@studio_app.command()
|
||||
|
|
@ -188,7 +188,7 @@ def setup():
|
|||
raise typer.Exit(result.returncode)
|
||||
|
||||
|
||||
# ── unsloth studio reset-password ────────────────────────────────────
|
||||
# -- unsloth studio reset-password ------------------------------------
|
||||
|
||||
|
||||
@studio_app.command("reset-password")
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ def ui(
|
|||
try:
|
||||
rc = proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
# Child has its own signal handler — let it finish
|
||||
# Child has its own signal handler -- let it finish
|
||||
rc = proc.wait()
|
||||
raise typer.Exit(rc)
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue