From a1eff8ee22e41632f642927324c696789e25cccb Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 25 Mar 2026 05:01:40 +0000 Subject: [PATCH] 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. --- install.ps1 | 20 +++--- install.sh | 18 ++--- studio/backend/auth/authentication.py | 2 +- studio/backend/auth/storage.py | 6 +- studio/backend/core/export/export.py | 4 +- studio/backend/core/export/orchestrator.py | 16 ++--- studio/backend/core/export/worker.py | 18 ++--- studio/backend/core/inference/audio_codecs.py | 12 ++-- studio/backend/core/inference/inference.py | 48 ++++++------- studio/backend/core/inference/llama_cpp.py | 50 +++++++------- studio/backend/core/inference/orchestrator.py | 54 +++++++-------- studio/backend/core/inference/worker.py | 34 +++++----- studio/backend/core/training/trainer.py | 48 ++++++------- studio/backend/core/training/training.py | 14 ++-- studio/backend/core/training/worker.py | 68 +++++++++---------- studio/backend/main.py | 12 ++-- studio/backend/models/inference.py | 12 ++-- studio/backend/models/training.py | 2 +- studio/backend/routes/datasets.py | 6 +- studio/backend/routes/export.py | 2 +- studio/backend/routes/inference.py | 50 +++++++------- studio/backend/routes/models.py | 6 +- studio/backend/routes/training.py | 16 ++--- studio/backend/run.py | 2 +- .../tests/test_transformers_version.py | 6 +- studio/backend/tests/test_utils.py | 6 +- .../backend/utils/datasets/chat_templates.py | 4 +- .../backend/utils/datasets/dataset_utils.py | 18 ++--- .../utils/datasets/format_conversion.py | 26 +++---- .../utils/datasets/format_detection.py | 20 +++--- studio/backend/utils/datasets/llm_assist.py | 42 ++++++------ studio/backend/utils/hardware/hardware.py | 42 ++++++------ .../utils/inference/inference_config.py | 2 +- studio/backend/utils/models/checkpoints.py | 2 +- studio/backend/utils/models/model_config.py | 20 +++--- studio/backend/utils/transformers_version.py | 10 +-- .../components/assistant-ui/attachment.tsx | 2 +- .../assistant-ui/model-selector/pickers.tsx | 8 +-- .../src/components/assistant-ui/sources.tsx | 14 ++-- .../components/assistant-ui/tool-group.tsx | 2 +- .../assistant-ui/tool-ui-python.tsx | 2 +- studio/frontend/src/config/env.ts | 2 +- .../features/auth/components/auth-form.tsx | 2 +- .../src/features/chat/api/chat-adapter.ts | 8 +-- .../frontend/src/features/chat/chat-page.tsx | 2 +- .../onboarding/components/wizard-layout.tsx | 2 +- .../recipe-studio/recipe-studio-page.tsx | 6 +- .../sections/dataset-preview-dialog.tsx | 2 +- .../studio/sections/model-section.tsx | 2 +- .../src/features/training/api/datasets-api.ts | 2 +- .../frontend/src/hooks/use-gpu-utilization.ts | 2 +- .../frontend/src/hooks/use-hardware-info.ts | 2 +- .../frontend/src/hooks/use-hf-model-search.ts | 2 +- studio/frontend/src/index.css | 2 +- studio/install_python_stack.py | 20 +++--- studio/setup.ps1 | 24 +++---- studio/setup.sh | 48 ++++++------- unsloth/models/rl_replacements.py | 2 +- unsloth/save.py | 6 +- unsloth_cli/commands/studio.py | 12 ++-- unsloth_cli/commands/ui.py | 2 +- 61 files changed, 448 insertions(+), 448 deletions(-) diff --git a/install.ps1 b/install.ps1 index 52386dd2fd..e639a3f841 100644 --- a/install.ps1 +++ b/install.ps1 @@ -14,7 +14,7 @@ function Install-UnslothStudio { Write-Host "=========================================" Write-Host "" - # ── Helper: refresh PATH from registry (deduplicating entries) ── + # -- Helper: refresh PATH from registry (deduplicating entries) -- function Refresh-SessionPath { $machine = [System.Environment]::GetEnvironmentVariable("Path", "Machine") $user = [System.Environment]::GetEnvironmentVariable("Path", "User") @@ -31,7 +31,7 @@ function Install-UnslothStudio { $env:Path = $unique -join ";" } - # ── Check winget ── + # -- Check winget -- if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Host "Error: winget is not available." -ForegroundColor Red Write-Host " Install it from https://aka.ms/getwinget" -ForegroundColor Yellow @@ -39,7 +39,7 @@ function Install-UnslothStudio { return } - # ── Helper: detect a working Python 3.11-3.13 on the system ── + # -- Helper: detect a working Python 3.11-3.13 on the system -- # Returns the version string (e.g. "3.13") or "" if none found. # Uses try-catch + stderr redirection so that App Execution Alias stubs # (WindowsApps) and other non-functional executables are probed safely @@ -109,7 +109,7 @@ function Install-UnslothStudio { return $null } - # ── Install Python if no compatible version (3.11-3.13) found ── + # -- Install Python if no compatible version (3.11-3.13) found -- # Find-CompatiblePython returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null. $DetectedPython = Find-CompatiblePython if ($DetectedPython) { @@ -158,7 +158,7 @@ function Install-UnslothStudio { } } - # ── Install uv if not present ── + # -- Install uv if not present -- if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { Write-Host "==> Installing uv package manager..." $prevEAP = $ErrorActionPreference @@ -180,7 +180,7 @@ function Install-UnslothStudio { return } - # ── Create venv (skip if it already exists and has a valid interpreter) ── + # -- Create venv (skip if it already exists and has a valid interpreter) -- # Pass the resolved executable path to uv so it does not re-resolve # a version string back to a conda interpreter. $VenvPython = Join-Path $VenvName "Scripts\python.exe" @@ -196,7 +196,7 @@ function Install-UnslothStudio { Write-Host "==> Virtual environment ${VenvName} already exists, skipping creation." } - # ── Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) ── + # -- Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) -- $HasNvidiaSmi = $false $NvidiaSmiExe = $null try { @@ -227,7 +227,7 @@ function Install-UnslothStudio { Write-Host " https://www.nvidia.com/Download/index.aspx" -ForegroundColor Yellow } - # ── Choose the correct PyTorch index URL based on driver CUDA version ── + # -- Choose the correct PyTorch index URL based on driver CUDA version -- # Mirrors Get-PytorchCudaTag in setup.ps1. function Get-TorchIndexUrl { $baseUrl = "https://download.pytorch.org/whl" @@ -249,7 +249,7 @@ function Install-UnslothStudio { } $TorchIndexUrl = Get-TorchIndexUrl - # ── Install PyTorch first, then unsloth separately ── + # -- Install PyTorch first, then unsloth separately -- # # Why two steps? # `uv pip install unsloth --torch-backend=cpu` on Windows resolves to @@ -281,7 +281,7 @@ function Install-UnslothStudio { return } - # ── Run studio setup ── + # -- Run studio setup -- # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, # CUDA Toolkit, Node.js, and other dependencies automatically via winget. Write-Host "==> Running unsloth studio setup..." diff --git a/install.sh b/install.sh index 3d8e08612c..fedda6a1cb 100755 --- a/install.sh +++ b/install.sh @@ -7,7 +7,7 @@ set -e VENV_NAME="unsloth_studio" PYTHON_VERSION="3.13" -# ── Helper: download a URL to a file (supports curl and wget) ── +# -- Helper: download a URL to a file (supports curl and wget) -- download() { if command -v curl >/dev/null 2>&1; then curl -LsSf "$1" -o "$2" @@ -19,7 +19,7 @@ download() { fi } -# ── Helper: check if a single package is available on the system ── +# -- Helper: check if a single package is available on the system -- _is_pkg_installed() { case "$1" in build-essential) command -v gcc >/dev/null 2>&1 ;; @@ -31,7 +31,7 @@ _is_pkg_installed() { esac } -# ── Helper: install packages via apt, escalating to sudo only if needed ── +# -- Helper: install packages via apt, escalating to sudo only if needed -- # Usage: _smart_apt_install pkg1 pkg2 pkg3 ... _smart_apt_install() { _PKGS="$*" @@ -95,7 +95,7 @@ echo " Unsloth Studio Installer" echo "=========================================" echo "" -# ── Detect platform ── +# -- Detect platform -- OS="linux" if [ "$(uname)" = "Darwin" ]; then OS="macos" @@ -104,7 +104,7 @@ elif grep -qi microsoft /proc/version 2>/dev/null; then fi echo "==> Platform: $OS" -# ── Check system dependencies ── +# -- Check system dependencies -- # cmake and git are needed by unsloth studio setup to build the GGUF inference # engine (llama.cpp). build-essential and libcurl-dev are also needed on Linux. MISSING="" @@ -170,7 +170,7 @@ else echo "==> All system dependencies found." fi -# ── Install uv ── +# -- Install uv -- UV_MIN_VERSION="0.7.14" version_ge() { @@ -224,7 +224,7 @@ if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then export PATH="$HOME/.local/bin:$PATH" fi -# ── Create venv (skip if it already exists and has a valid interpreter) ── +# -- Create venv (skip if it already exists and has a valid interpreter) -- if [ ! -x "$VENV_NAME/bin/python" ]; then [ -e "$VENV_NAME" ] && rm -rf "$VENV_NAME" echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_NAME})..." @@ -233,11 +233,11 @@ else echo "==> Virtual environment ${VENV_NAME} already exists, skipping creation." fi -# ── Install unsloth directly into the venv (no activation needed) ── +# -- Install unsloth directly into the venv (no activation needed) -- echo "==> Installing unsloth (this may take a few minutes)..." uv pip install --python "$VENV_NAME/bin/python" "unsloth>=2026.3.11" --torch-backend=auto -# ── Run studio setup ── +# -- Run studio setup -- # Ensure the venv's Python is on PATH for setup.sh's Python discovery. # On macOS the system Python may be outside the 3.11-3.13 range that # setup.sh requires, but uv already installed a compatible interpreter diff --git a/studio/backend/auth/authentication.py b/studio/backend/auth/authentication.py index b39f915764..44747bf9b2 100644 --- a/studio/backend/auth/authentication.py +++ b/studio/backend/auth/authentication.py @@ -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) diff --git a/studio/backend/auth/storage.py b/studio/backend/auth/storage.py index 1395574cce..01d252914e 100644 --- a/studio/backend/auth/storage.py +++ b/studio/backend/auth/storage.py @@ -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() diff --git a/studio/backend/core/export/export.py b/studio/backend/core/export/export.py index 966e045b13..50a636d5ab 100644 --- a/studio/backend/core/export/export.py +++ b/studio/backend/core/export/export.py @@ -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") diff --git a/studio/backend/core/export/orchestrator.py b/studio/backend/core/export/orchestrator.py index a9fbe659b3..0fc23b73eb 100644 --- a/studio/backend/core/export/orchestrator.py +++ b/studio/backend/core/export/orchestrator.py @@ -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) diff --git a/studio/backend/core/export/worker.py b/studio/backend/core/export/worker.py index 6af6ff1193..d2afc2cf9a 100644 --- a/studio/backend/core/export/worker.py +++ b/studio/backend/core/export/worker.py @@ -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: diff --git a/studio/backend/core/inference/audio_codecs.py b/studio/backend/core/inference/audio_codecs.py index bcf3ec2937..6d3ea4cec7 100644 --- a/studio/backend/core/inference/audio_codecs.py +++ b/studio/backend/core/inference/audio_codecs.py @@ -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.""" diff --git a/studio/backend/core/inference/inference.py b/studio/backend/core/inference/inference.py index 1a265690ff..0933122ad4 100644 --- a/studio/backend/core/inference/inference.py +++ b/studio/backend/core/inference/inference.py @@ -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") diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 7b1db8fd04..7202702c24 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -33,9 +33,9 @@ class LlamaCppBackend: Manages a llama-server subprocess for GGUF model inference. Lifecycle: - 1. load_model() — starts llama-server with the GGUF file - 2. generate_chat_completion() — proxies to /v1/chat/completions, streams back - 3. unload_model() — terminates llama-server subprocess + 1. load_model() -- starts llama-server with the GGUF file + 2. generate_chat_completion() -- proxies to /v1/chat/completions, streams back + 3. unload_model() -- terminates llama-server subprocess """ def __init__(self): @@ -61,7 +61,7 @@ class LlamaCppBackend: self._kill_orphaned_servers() atexit.register(self._cleanup) - # ── Properties ──────────────────────────────────────────────── + # -- Properties ------------------------------------------------ @property def is_loaded(self) -> bool: @@ -112,7 +112,7 @@ class LlamaCppBackend: def cache_type_kv(self) -> Optional[str]: return self._cache_type_kv - # ── Binary discovery ────────────────────────────────────────── + # -- Binary discovery ------------------------------------------ @staticmethod def _find_llama_server_binary() -> Optional[str]: @@ -135,12 +135,12 @@ class LlamaCppBackend: binary_name = "llama-server.exe" if sys.platform == "win32" else "llama-server" - # 1. Env var — direct path to binary + # 1. Env var -- direct path to binary env_path = os.environ.get("LLAMA_SERVER_PATH") if env_path and Path(env_path).is_file(): return env_path - # 1b. UNSLOTH_LLAMA_CPP_PATH — custom llama.cpp install directory + # 1b. UNSLOTH_LLAMA_CPP_PATH -- custom llama.cpp install directory custom_llama_cpp = os.environ.get("UNSLOTH_LLAMA_CPP_PATH") if custom_llama_cpp: custom_dir = Path(custom_llama_cpp) @@ -158,7 +158,7 @@ class LlamaCppBackend: if win_bin.is_file(): return str(win_bin) - # 2–4. ~/.unsloth/llama.cpp (primary — setup.sh / setup.ps1 build here) + # 2-4. ~/.unsloth/llama.cpp (primary -- setup.sh / setup.ps1 build here) unsloth_home = Path.home() / ".unsloth" / "llama.cpp" # Root dir (make builds copy binaries here) home_root = unsloth_home / binary_name @@ -175,7 +175,7 @@ class LlamaCppBackend: if home_win.is_file(): return str(home_win) - # 5–6. Legacy: in-tree build (older setup.sh / setup.ps1 versions) + # 5-6. Legacy: in-tree build (older setup.sh / setup.ps1 versions) project_root = Path(__file__).resolve().parents[4] # Root dir (make builds) root_path = project_root / "llama.cpp" / binary_name @@ -204,7 +204,7 @@ class LlamaCppBackend: return None - # ── GPU allocation ──────────────────────────────────────────── + # -- GPU allocation -------------------------------------------- @staticmethod def _get_gguf_size_bytes(model_path: str) -> int: @@ -318,7 +318,7 @@ class LlamaCppBackend: # Model is too large even for all GPUs, let --fit handle it return None, True - # ── Variant fallback ──────────────────────────────────────────── + # -- Variant fallback -------------------------------------------- @staticmethod def _find_smallest_fitting_variant( @@ -374,7 +374,7 @@ class LlamaCppBackend: except Exception: return None - # ── Port allocation ─────────────────────────────────────────── + # -- Port allocation ------------------------------------------- @staticmethod def _find_free_port() -> int: @@ -383,7 +383,7 @@ class LlamaCppBackend: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] - # ── Stdout drain (prevents pipe deadlock on Windows) ───────── + # -- Stdout drain (prevents pipe deadlock on Windows) --------- def _drain_stdout(self): """ @@ -400,7 +400,7 @@ class LlamaCppBackend: self._stdout_lines.append(line) logger.debug(f"[llama-server] {line}") except (ValueError, OSError): - # Pipe closed — process is terminating + # Pipe closed -- process is terminating pass # GGUF KV type sizes for fast skipping @@ -530,7 +530,7 @@ class LlamaCppBackend: except Exception as e: logger.warning(f"Failed to read GGUF metadata: {e}") - # ── HF download (no lock held) ─────────────────────────────── + # -- HF download (no lock held) ------------------------------- def _download_gguf( self, @@ -740,7 +740,7 @@ class LlamaCppBackend: logger.warning(f"Could not download mmproj: {e}") return None - # ── Lifecycle ───────────────────────────────────────────────── + # -- Lifecycle ------------------------------------------------- def load_model( self, @@ -776,7 +776,7 @@ class LlamaCppBackend: """ self._cancel_event.clear() - # ── Phase 1: kill old process (under lock, fast) ────────── + # -- Phase 1: kill old process (under lock, fast) ---------- with self._lock: self._kill_process() @@ -788,7 +788,7 @@ class LlamaCppBackend: "or set LLAMA_SERVER_PATH environment variable." ) - # ── Phase 2: download (NO lock held, so cancel can proceed) ── + # -- Phase 2: download (NO lock held, so cancel can proceed) -- if hf_repo: model_path = self._download_gguf( hf_repo = hf_repo, @@ -819,7 +819,7 @@ class LlamaCppBackend: logger.info("Load cancelled after download phase") return False - # ── Phase 3: start llama-server (under lock) ────────────── + # -- Phase 3: start llama-server (under lock) -------------- with self._lock: # Re-check cancel inside lock if self._cancel_event.is_set(): @@ -1163,7 +1163,7 @@ class LlamaCppBackend: logger.error(f"llama-server health check timed out after {timeout}s") return False - # ── Message building (OpenAI format) ────────────────────────── + # -- Message building (OpenAI format) -------------------------- @staticmethod def _parse_tool_calls_from_text(content: str) -> list[dict]: @@ -1327,7 +1327,7 @@ class LlamaCppBackend: return result - # ── Generation (proxy to llama-server) ──────────────────────── + # -- Generation (proxy to llama-server) ------------------------ @staticmethod def _iter_text_cancellable( @@ -1463,7 +1463,7 @@ class LlamaCppBackend: """ Send a chat completion request to llama-server and stream tokens back. - Uses /v1/chat/completions — llama-server handles chat template + Uses /v1/chat/completions -- llama-server handles chat template application and vision (multimodal image_url parts) natively. Yields cumulative text (matching InferenceBackend's convention). @@ -1598,7 +1598,7 @@ class LlamaCppBackend: return raise - # ── Tool-calling agentic loop ────────────────────────────── + # -- Tool-calling agentic loop ------------------------------ def generate_chat_completion_with_tools( self, @@ -2028,7 +2028,7 @@ class LlamaCppBackend: return raise - # ── TTS support ──────────────────────────────────────────── + # -- TTS support -------------------------------------------- def detect_audio_type(self) -> Optional[str]: """Detect audio/TTS codec by probing the loaded model's vocabulary.""" @@ -2103,7 +2103,7 @@ class LlamaCppBackend: device = "cuda" if torch.cuda.is_available() else "cpu" model_repo_path = None - # BiCodec needs a repo with BiCodec/ weights — download canonical SparkTTS + # BiCodec needs a repo with BiCodec/ weights -- download canonical SparkTTS if audio_type == "bicodec": from huggingface_hub import snapshot_download import os diff --git a/studio/backend/core/inference/orchestrator.py b/studio/backend/core/inference/orchestrator.py index 6ff7fd2cbf..cd44cf05f1 100644 --- a/studio/backend/core/inference/orchestrator.py +++ b/studio/backend/core/inference/orchestrator.py @@ -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 diff --git a/studio/backend/core/inference/worker.py b/studio/backend/core/inference/worker.py index 2eb46f3217..b3a9a3fcb8 100644 --- a/studio/backend/core/inference/worker.py +++ b/studio/backend/core/inference/worker.py @@ -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") diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 5f504bbdf4..52c0f2214b 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -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: diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 9626f9df2e..126545dc84 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -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, ) diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index ccd805b7ac..80d898d0c4 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -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", diff --git a/studio/backend/main.py b/studio/backend/main.py index 7134c5a783..e1fa9a2b48 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -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) diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index b0498319ca..94d27faf7c 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -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): diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py index 342e44cc09..8440e85a2e 100644 --- a/studio/backend/models/training.py +++ b/studio/backend/models/training.py @@ -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.", ) diff --git a/studio/backend/routes/datasets.py b/studio/backend/routes/datasets.py index 8333009626..c6aae301dc 100644 --- a/studio/backend/routes/datasets.py +++ b/studio/backend/routes/datasets.py @@ -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, diff --git a/studio/backend/routes/export.py b/studio/backend/routes/export.py index 3e60eaaf20..7319b9b8b4 100644 --- a/studio/backend/routes/export.py +++ b/studio/backend/routes/export.py @@ -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). diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index aa8c34a3c5..ae99345c8e 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -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 = "" diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index e705762447..25781c775a 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -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") diff --git a/studio/backend/routes/training.py b/studio/backend/routes/training.py index 4f8054f80e..e91cd86232 100644 --- a/studio/backend/routes/training.py +++ b/studio/backend/routes/training.py @@ -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 diff --git a/studio/backend/run.py b/studio/backend/run.py index e32b912c37..a42c81439b 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -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: diff --git a/studio/backend/tests/test_transformers_version.py b/studio/backend/tests/test_transformers_version.py index f3dae537c7..dde4e16088 100644 --- a/studio/backend/tests/test_transformers_version.py +++ b/studio/backend/tests/test_transformers_version.py @@ -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 # --------------------------------------------------------------------------- diff --git a/studio/backend/tests/test_utils.py b/studio/backend/tests/test_utils.py index 3c33b33cb3..7a1bd0a79a 100644 --- a/studio/backend/tests/test_utils.py +++ b/studio/backend/tests/test_utils.py @@ -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() diff --git a/studio/backend/utils/datasets/chat_templates.py b/studio/backend/utils/datasets/chat_templates.py index 0c498281a4..a0d8c3bc8e 100644 --- a/studio/backend/utils/datasets/chat_templates.py +++ b/studio/backend/utils/datasets/chat_templates.py @@ -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, diff --git a/studio/backend/utils/datasets/dataset_utils.py b/studio/backend/utils/datasets/dataset_utils.py index fac8c3d295..056bb698ac 100644 --- a/studio/backend/utils/datasets/dataset_utils.py +++ b/studio/backend/utils/datasets/dataset_utils.py @@ -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( diff --git a/studio/backend/utils/datasets/format_conversion.py b/studio/backend/utils/datasets/format_conversion.py index f29ea0e559..5c29394977 100644 --- a/studio/backend/utils/datasets/format_conversion.py +++ b/studio/backend/utils/datasets/format_conversion.py @@ -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." ) diff --git a/studio/backend/utils/datasets/format_detection.py b/studio/backend/utils/datasets/format_detection.py index 7b70ff3a76..1f7ce167d2 100644 --- a/studio/backend/utils/datasets/format_detection.py +++ b/studio/backend/utils/datasets/format_detection.py @@ -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)) diff --git a/studio/backend/utils/datasets/llm_assist.py b/studio/backend/utils/datasets/llm_assist.py index fdc4f374ab..4199971507 100644 --- a/studio/backend/utils/datasets/llm_assist.py +++ b/studio/backend/utils/datasets/llm_assist.py @@ -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 "" 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".*?\s*", "", text, flags = re.DOTALL).strip() if stripped: return stripped - # Everything was inside tags — extract the inner content of the last block + # Everything was inside tags -- extract the inner content of the last block matches = re.findall(r"(.*?)", 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": "" }} - 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 diff --git a/studio/backend/utils/hardware/hardware.py b/studio/backend/utils/hardware/hardware.py index 61ee8a0967..1266f62b8e 100644 --- a/studio/backend/utils/hardware/hardware.py +++ b/studio/backend/utils/hardware/hardware.py @@ -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") diff --git a/studio/backend/utils/inference/inference_config.py b/studio/backend/utils/inference/inference_config.py index a0377f9869..d4e50e6f6c 100644 --- a/studio/backend/utils/inference/inference_config.py +++ b/studio/backend/utils/inference/inference_config.py @@ -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 diff --git a/studio/backend/utils/models/checkpoints.py b/studio/backend/utils/models/checkpoints.py index b6b2e11c2e..fee3b3faab 100644 --- a/studio/backend/utils/models/checkpoints.py +++ b/studio/backend/utils/models/checkpoints.py @@ -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) diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index 13f1b5febf..83a2c55686 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -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( diff --git a/studio/backend/utils/transformers_version.py b/studio/backend/utils/transformers_version.py index d8724de723..d4b414dc17 100644 --- a/studio/backend/utils/transformers_version.py +++ b/studio/backend/utils/transformers_version.py @@ -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, ) diff --git a/studio/frontend/src/components/assistant-ui/attachment.tsx b/studio/frontend/src/components/assistant-ui/attachment.tsx index 074dba5320..2ee760c3c5 100644 --- a/studio/frontend/src/components/assistant-ui/attachment.tsx +++ b/studio/frontend/src/components/assistant-ui/attachment.tsx @@ -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, diff --git a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx index 328cba3acd..a9ea7db1fe 100644 --- a/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx +++ b/studio/frontend/src/components/assistant-ui/model-selector/pickers.tsx @@ -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)) { diff --git a/studio/frontend/src/components/assistant-ui/sources.tsx b/studio/frontend/src/components/assistant-ui/sources.tsx index 18ee03cad0..6457d2c5fa 100644 --- a/studio/frontend/src/components/assistant-ui/sources.tsx +++ b/studio/frontend/src/components/assistant-ui/sources.tsx @@ -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 (
- {/* Hidden measurement container — renders all badges to measure row positions */} + {/* Hidden measurement container -- renders all badges to measure row positions */}
{ ); }; -// ── Individual source (renders null — SourcesGroup handles all) ── +// -- Individual source (renders null -- SourcesGroup handles all) -- const SourcesNoop: FC> = () => null; -// ── Exports ────────────────────────────────────────────────── +// -- Exports -------------------------------------------------- const Sources = memo(SourcesNoop) as unknown as FC> & { Root: typeof Source; diff --git a/studio/frontend/src/components/assistant-ui/tool-group.tsx b/studio/frontend/src/components/assistant-ui/tool-group.tsx index f29adb510f..256bc6b4ac 100644 --- a/studio/frontend/src/components/assistant-ui/tool-group.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-group.tsx @@ -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}; } diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx index 28468a10ad..87741ce89e 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx @@ -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\`\`\``, diff --git a/studio/frontend/src/config/env.ts b/studio/frontend/src/config/env.ts index 91e17f6bb9..530464ea37 100644 --- a/studio/frontend/src/config/env.ts +++ b/studio/frontend/src/config/env.ts @@ -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; diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx index d9190429bd..a862f546b0 100644 --- a/studio/frontend/src/features/auth/components/auth-form.tsx +++ b/studio/frontend/src/features/auth/components/auth-form.tsx @@ -92,7 +92,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { let canceled = false; async function initializeAuthForm(): Promise { - // 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 { diff --git a/studio/frontend/src/features/chat/api/chat-adapter.ts b/studio/frontend/src/features/chat/api/chat-adapter.ts index 15ac416b1f..00df4747ed 100644 --- a/studio/frontend/src/features/chat/api/chat-adapter.ts +++ b/studio/frontend/src/features/chat/api/chat-adapter.ts @@ -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 { } } - // 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; diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index c04cfbc89c..0b4e8bc412 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -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 { diff --git a/studio/frontend/src/features/onboarding/components/wizard-layout.tsx b/studio/frontend/src/features/onboarding/components/wizard-layout.tsx index 940ac333be..cd3a1f4fcd 100644 --- a/studio/frontend/src/features/onboarding/components/wizard-layout.tsx +++ b/studio/frontend/src/features/onboarding/components/wizard-layout.tsx @@ -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(() => { diff --git a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx index c922293c02..510135f59f 100644 --- a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx +++ b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx @@ -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; } diff --git a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx index 5ad2d582c4..7d13cc10cf 100644 --- a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx @@ -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(null); diff --git a/studio/frontend/src/features/studio/sections/model-section.tsx b/studio/frontend/src/features/studio/sections/model-section.tsx index e9bcde17b5..b03de1fd92 100644 --- a/studio/frontend/src/features/studio/sections/model-section.tsx +++ b/studio/frontend/src/features/studio/sections/model-section.tsx @@ -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) => { diff --git a/studio/frontend/src/features/training/api/datasets-api.ts b/studio/frontend/src/features/training/api/datasets-api.ts index c56aec03be..331109619e 100644 --- a/studio/frontend/src/features/training/api/datasets-api.ts +++ b/studio/frontend/src/features/training/api/datasets-api.ts @@ -62,7 +62,7 @@ export async function uploadTrainingDataset( return res.json(); } -// ── AI Assist ──────────────────────────────────────────────────────── +// -- AI Assist -------------------------------------------------------- type AiAssistMappingArgs = { columns: string[]; diff --git a/studio/frontend/src/hooks/use-gpu-utilization.ts b/studio/frontend/src/hooks/use-gpu-utilization.ts index 1262dcbc2f..a4e62b79c5 100644 --- a/studio/frontend/src/hooks/use-gpu-utilization.ts +++ b/studio/frontend/src/hooks/use-gpu-utilization.ts @@ -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 } } diff --git a/studio/frontend/src/hooks/use-hardware-info.ts b/studio/frontend/src/hooks/use-hardware-info.ts index 1e9210be5a..aac613bbe4 100644 --- a/studio/frontend/src/hooks/use-hardware-info.ts +++ b/studio/frontend/src/hooks/use-hardware-info.ts @@ -61,7 +61,7 @@ async function fetchOnce(): Promise { /** * 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 { diff --git a/studio/frontend/src/hooks/use-hf-model-search.ts b/studio/frontend/src/hooks/use-hf-model-search.ts index 69ea4d3b83..f17a746659 100644 --- a/studio/frontend/src/hooks/use-hf-model-search.ts +++ b/studio/frontend/src/hooks/use-hf-model-search.ts @@ -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", diff --git a/studio/frontend/src/index.css b/studio/frontend/src/index.css index 307bf36819..11619f20f0 100644 --- a/studio/frontend/src/index.css +++ b/studio/frontend/src/index.css @@ -341,7 +341,7 @@ } } -/* Minimal scrollbar — thumb only, no track */ +/* Minimal scrollbar -- thumb only, no track */ * { scrollbar-width: thin; scrollbar-color: transparent transparent; diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index a141c64425..f691299407 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -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", diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 8966449423..ef5a717d45 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -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) { diff --git a/studio/setup.sh b/studio/setup.sh index 851fcadc81..174190ff1d 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -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 } diff --git a/unsloth/models/rl_replacements.py b/unsloth/models/rl_replacements.py index 9f555416d4..903308f36e 100755 --- a/unsloth/models/rl_replacements.py +++ b/unsloth/models/rl_replacements.py @@ -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 diff --git a/unsloth/save.py b/unsloth/save.py index 1759d86fb1..93202c3c01 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -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) diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index 192e138a9c..cf56429118 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -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") diff --git a/unsloth_cli/commands/ui.py b/unsloth_cli/commands/ui.py index 8f76636990..165771eca3 100644 --- a/unsloth_cli/commands/ui.py +++ b/unsloth_cli/commands/ui.py @@ -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: