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:
Daniel Han 2026-03-25 05:01:40 +00:00
commit a1eff8ee22
61 changed files with 448 additions and 448 deletions

View file

@ -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..."

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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")

View file

@ -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)

View file

@ -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:

View file

@ -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."""

View file

@ -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")

View file

@ -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)
# 24. ~/.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)
# 56. 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

View file

@ -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

View file

@ -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")

View file

@ -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:

View file

@ -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,
)

View file

@ -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",

View file

@ -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)

View file

@ -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):

View file

@ -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.",
)

View file

@ -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,

View file

@ -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).

View file

@ -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 = ""

View file

@ -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")

View file

@ -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

View file

@ -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:

View file

@ -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
# ---------------------------------------------------------------------------

View file

@ -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()

View file

@ -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,

View file

@ -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(

View file

@ -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."
)

View file

@ -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))

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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,
)

View file

@ -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,

View file

@ -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)) {

View file

@ -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;

View file

@ -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}</>;
}

View file

@ -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\`\`\``,

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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(() => {

View file

@ -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;
}

View file

@ -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);

View file

@ -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) => {

View file

@ -62,7 +62,7 @@ export async function uploadTrainingDataset(
return res.json();
}
// ── AI Assist ────────────────────────────────────────────────────────
// -- AI Assist --------------------------------------------------------
type AiAssistMappingArgs = {
columns: string[];

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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",

View file

@ -341,7 +341,7 @@
}
}
/* Minimal scrollbar thumb only, no track */
/* Minimal scrollbar -- thumb only, no track */
* {
scrollbar-width: thin;
scrollbar-color: transparent transparent;

View file

@ -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",

View file

@ -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) {

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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: