mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-10 00:08:58 +00:00
Fix Studio silently exiting on Windows without error output (#4527)
* Fix Studio silently exiting on Windows without error output On Windows, `unsloth studio` launches a child process via subprocess.Popen to run the server in the studio venv. If the child crashes (e.g. due to a missing package), the parent just calls typer.Exit(rc) with no message -- the user sees "Launching Unsloth Studio... Please wait..." and then the prompt returns with zero feedback. Root cause: `data_designer_unstructured_seed` is imported at the top level in seed.py. If this package is not installed in the studio venv, the entire import chain (seed.py -> routes/__init__.py -> main.py -> run_server()) crashes with ModuleNotFoundError. Since run.py has no try/except around run_server() and studio.py does not report nonzero exit codes, the failure is completely silent. Changes: - run.py: wrap run_server() in try/except, print clear error with traceback to stderr. Also reconfigure stderr encoding on Windows so tracebacks with non-ASCII paths do not cause secondary failures. - studio.py: print an error message when the child process exits with a nonzero code on Windows, so the user knows something went wrong. - seed.py: make data_designer_unstructured_seed import optional with a try/except fallback. The server starts normally and only returns HTTP 500 if the unstructured seed endpoints are actually called. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Skip Anaconda/Miniconda Python when creating Studio venv on Windows Conda-bundled CPython ships modified DLL search paths that prevent torch from loading c10.dll on Windows. The Studio server fails silently at startup because the venv was created with conda's Python. Standalone CPython (python.org, winget, uv) does not have this issue. Both install.ps1 and setup.ps1 now skip any Python binary whose path contains conda, miniconda, anaconda, miniforge, or mambaforge when selecting the interpreter for the studio venv. If only conda Python is available, the scripts print an error with instructions to install standalone CPython. * Fix multi-file preview crash and improve setup.ps1 Python discovery Addresses review findings [10/10] and [8/10]: 1. seed.py: _read_preview_rows_from_multi_files() had a hard import of build_multi_file_preview_rows inside the function body, bypassing the optional-plugin guard. Moved it into the top-level try/except block and added a None guard matching the other functions. 2. setup.ps1: Python discovery now probes py.exe (Python Launcher) first, uses Get-Command -All to look past conda entries that shadow standalone CPython further down PATH, skips WindowsApps stubs, and resolves the actual executable path so venv creation does not re-resolve back to a conda interpreter. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Check sys.base_prefix to catch venvs created from conda Python A venv created from conda Python (e.g. C:\Users\danie\.venv) has a path that does not contain "conda", but sys.base_prefix still points to the conda install (e.g. C:\Users\danie\miniconda3). The previous path-only check missed this case entirely. Both install.ps1 and setup.ps1 now use a Test-IsConda helper that checks both the executable path AND sys.base_prefix against the conda/miniconda/anaconda/miniforge/mambaforge pattern. This catches: - Direct conda Python executables - Venvs created from conda Python (base_prefix reveals the origin) * Fix install.ps1 passing version string to uv venv instead of resolved path Find-CompatiblePython returned a bare version string (e.g. "3.13") which was passed to `uv venv --python 3.13`. uv performs its own interpreter discovery and can resolve that version string back to a conda Python, defeating the entire conda-skip logic. Now Find-CompatiblePython returns a hashtable with both .Version (for display) and .Path (the resolved absolute executable path). The venv is created with `uv venv --python <absolute-path>`, ensuring uv uses the exact interpreter we validated. * Quote resolved Python path in uv venv call for paths with spaces --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
866cb33ce0
commit
797ddd201e
5 changed files with 183 additions and 32 deletions
66
install.ps1
66
install.ps1
|
|
@ -44,14 +44,45 @@ function Install-UnslothStudio {
|
|||
# Uses try-catch + stderr redirection so that App Execution Alias stubs
|
||||
# (WindowsApps) and other non-functional executables are probed safely
|
||||
# without triggering $ErrorActionPreference = "Stop".
|
||||
#
|
||||
# Skips Anaconda/Miniconda Python: conda-bundled CPython ships modified
|
||||
# DLL search paths that break torch's c10.dll loading on Windows.
|
||||
# Standalone CPython (python.org, winget, uv) does not have this issue.
|
||||
#
|
||||
# NOTE: A venv created from conda Python inherits conda's base_prefix
|
||||
# even if the venv path does not contain "conda". We check both the
|
||||
# executable path AND sys.base_prefix to catch this.
|
||||
$script:CondaSkipPattern = '(?i)(conda|miniconda|anaconda|miniforge|mambaforge)'
|
||||
|
||||
function Test-IsCondaPython {
|
||||
param([string]$Exe)
|
||||
if ($Exe -match $script:CondaSkipPattern) { return $true }
|
||||
try {
|
||||
$basePrefix = (& $Exe -c "import sys; print(sys.base_prefix)" 2>$null | Out-String).Trim()
|
||||
if ($basePrefix -match $script:CondaSkipPattern) { return $true }
|
||||
} catch { }
|
||||
return $false
|
||||
}
|
||||
|
||||
# Returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null.
|
||||
# The resolved Path is passed to `uv venv --python` to prevent uv from
|
||||
# re-resolving the version string back to a conda interpreter.
|
||||
function Find-CompatiblePython {
|
||||
# Try the Python Launcher first (most reliable on Windows)
|
||||
# py.exe resolves to the standard CPython install, not conda.
|
||||
$pyLauncher = Get-Command py -CommandType Application -ErrorAction SilentlyContinue
|
||||
if ($pyLauncher) {
|
||||
if ($pyLauncher -and $pyLauncher.Source -notmatch $script:CondaSkipPattern) {
|
||||
foreach ($minor in @("3.13", "3.12", "3.11")) {
|
||||
try {
|
||||
$out = & $pyLauncher.Source "-$minor" --version 2>&1 | Out-String
|
||||
if ($out -match "Python (3\.1[1-3])\.\d+") { return $Matches[1] }
|
||||
if ($out -match "Python (3\.1[1-3])\.\d+") {
|
||||
$ver = $Matches[1]
|
||||
# Resolve the actual executable path and verify it is not conda-based
|
||||
$resolvedExe = (& $pyLauncher.Source "-$minor" -c "import sys; print(sys.executable)" 2>$null | Out-String).Trim()
|
||||
if ($resolvedExe -and (Test-Path $resolvedExe) -and -not (Test-IsCondaPython $resolvedExe)) {
|
||||
return @{ Version = $ver; Path = $resolvedExe }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,25 +92,30 @@ function Install-UnslothStudio {
|
|||
# and can open the Microsoft Store as a side effect. Legitimate Store
|
||||
# Python is already detected via the py launcher above (Store packages
|
||||
# include py since Python 3.11).
|
||||
# Skip Anaconda/Miniconda: check both path and sys.base_prefix.
|
||||
foreach ($name in @("python3", "python")) {
|
||||
foreach ($cmd in @(Get-Command $name -All -ErrorAction SilentlyContinue)) {
|
||||
if (-not $cmd.Source) { continue }
|
||||
if ($cmd.Source -like "*\WindowsApps\*") { continue }
|
||||
if (Test-IsCondaPython $cmd.Source) { continue }
|
||||
try {
|
||||
$out = & $cmd.Source --version 2>&1 | Out-String
|
||||
if ($out -match "Python (3\.1[1-3])\.\d+") { return $Matches[1] }
|
||||
if ($out -match "Python (3\.1[1-3])\.\d+") {
|
||||
return @{ Version = $Matches[1]; Path = $cmd.Source }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return $null
|
||||
}
|
||||
|
||||
# ── Install Python if no compatible version (3.11-3.13) found ──
|
||||
$DetectedPythonVersion = Find-CompatiblePython
|
||||
if ($DetectedPythonVersion) {
|
||||
Write-Host "==> Python already installed: Python $DetectedPythonVersion"
|
||||
# Find-CompatiblePython returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null.
|
||||
$DetectedPython = Find-CompatiblePython
|
||||
if ($DetectedPython) {
|
||||
Write-Host "==> Python already installed: Python $($DetectedPython.Version)"
|
||||
}
|
||||
if (-not $DetectedPythonVersion) {
|
||||
if (-not $DetectedPython) {
|
||||
Write-Host "==> Installing Python ${PythonVersion}..."
|
||||
$pythonPackageId = "Python.Python.$PythonVersion"
|
||||
# Temporarily lower ErrorActionPreference so that winget stderr
|
||||
|
|
@ -95,9 +131,9 @@ function Install-UnslothStudio {
|
|||
Refresh-SessionPath
|
||||
|
||||
# Re-detect after install (PATH may have changed)
|
||||
$DetectedPythonVersion = Find-CompatiblePython
|
||||
$DetectedPython = Find-CompatiblePython
|
||||
|
||||
if (-not $DetectedPythonVersion) {
|
||||
if (-not $DetectedPython) {
|
||||
# Python still not functional after winget -- force reinstall.
|
||||
# This handles both real failures AND "already installed" codes where
|
||||
# winget thinks Python is present but it's not actually on PATH
|
||||
|
|
@ -110,10 +146,10 @@ function Install-UnslothStudio {
|
|||
} catch { $wingetExit = 1 }
|
||||
$ErrorActionPreference = $prevEAP
|
||||
Refresh-SessionPath
|
||||
$DetectedPythonVersion = Find-CompatiblePython
|
||||
$DetectedPython = Find-CompatiblePython
|
||||
}
|
||||
|
||||
if (-not $DetectedPythonVersion) {
|
||||
if (-not $DetectedPython) {
|
||||
Write-Host "[ERROR] Python installation failed (exit code $wingetExit)" -ForegroundColor Red
|
||||
Write-Host " Please install Python $PythonVersion manually from https://www.python.org/downloads/" -ForegroundColor Yellow
|
||||
Write-Host " Make sure to check 'Add Python to PATH' during installation." -ForegroundColor Yellow
|
||||
|
|
@ -145,11 +181,13 @@ function Install-UnslothStudio {
|
|||
}
|
||||
|
||||
# ── 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"
|
||||
if (-not (Test-Path $VenvPython)) {
|
||||
if (Test-Path $VenvName) { Remove-Item -Recurse -Force $VenvName }
|
||||
Write-Host "==> Creating Python ${DetectedPythonVersion} virtual environment (${VenvName})..."
|
||||
uv venv $VenvName --python $DetectedPythonVersion
|
||||
Write-Host "==> Creating Python $($DetectedPython.Version) virtual environment (${VenvName})..."
|
||||
uv venv $VenvName --python "$($DetectedPython.Path)"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "[ERROR] Failed to create virtual environment (exit code $LASTEXITCODE)" -ForegroundColor Red
|
||||
return
|
||||
|
|
|
|||
|
|
@ -15,11 +15,19 @@ from typing import Any
|
|||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File as FastAPIFile, Form
|
||||
from data_designer_unstructured_seed.chunking import (
|
||||
build_unstructured_preview_rows,
|
||||
normalize_unstructured_text,
|
||||
resolve_chunking,
|
||||
)
|
||||
|
||||
try:
|
||||
from data_designer_unstructured_seed.chunking import (
|
||||
build_multi_file_preview_rows,
|
||||
build_unstructured_preview_rows,
|
||||
normalize_unstructured_text,
|
||||
resolve_chunking,
|
||||
)
|
||||
except ImportError:
|
||||
build_multi_file_preview_rows = None
|
||||
build_unstructured_preview_rows = None
|
||||
normalize_unstructured_text = None
|
||||
resolve_chunking = None
|
||||
from core.data_recipe.jsonable import to_preview_jsonable
|
||||
from utils.paths import ensure_dir, seed_uploads_root, unstructured_uploads_root
|
||||
|
||||
|
|
@ -232,6 +240,11 @@ def _read_preview_rows_from_unstructured_file(
|
|||
chunk_size: int | None,
|
||||
chunk_overlap: int | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if resolve_chunking is None or build_unstructured_preview_rows is None:
|
||||
raise HTTPException(
|
||||
500,
|
||||
"Unstructured seed support not available (missing data_designer_unstructured_seed)",
|
||||
)
|
||||
size, overlap = resolve_chunking(chunk_size, chunk_overlap)
|
||||
try:
|
||||
rows = build_unstructured_preview_rows(
|
||||
|
|
@ -256,7 +269,11 @@ def _read_preview_rows_from_multi_files(
|
|||
chunk_size: int | None,
|
||||
chunk_overlap: int | None,
|
||||
) -> list[dict[str, str]]:
|
||||
from data_designer_unstructured_seed.chunking import build_multi_file_preview_rows
|
||||
if build_multi_file_preview_rows is None:
|
||||
raise HTTPException(
|
||||
500,
|
||||
"Unstructured seed support not available (missing data_designer_unstructured_seed)",
|
||||
)
|
||||
|
||||
_validate_safe_id(block_id, "block_id")
|
||||
block_dir = UNSTRUCTURED_UPLOAD_ROOT / block_id
|
||||
|
|
@ -382,6 +399,8 @@ def _extract_text_from_file(file_path: Path, ext: str) -> str:
|
|||
else:
|
||||
raise ValueError(f"Unsupported file type: {ext}")
|
||||
|
||||
if normalize_unstructured_text is None:
|
||||
return raw
|
||||
return normalize_unstructured_text(raw)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -256,6 +256,14 @@ def run_server(
|
|||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import signal
|
||||
import traceback
|
||||
|
||||
# Ensure stderr can handle Unicode on Windows (tracebacks with non-ASCII paths)
|
||||
if sys.platform == "win32" and hasattr(sys.stderr, "reconfigure"):
|
||||
try:
|
||||
sys.stderr.reconfigure(encoding = "utf-8", errors = "replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
parser = argparse.ArgumentParser(description = "Run Unsloth UI Backend server")
|
||||
parser.add_argument("--host", default = "0.0.0.0", help = "Host to bind to")
|
||||
|
|
@ -273,7 +281,21 @@ if __name__ == "__main__":
|
|||
kwargs = dict(host = args.host, port = args.port, silent = args.silent)
|
||||
if args.frontend is not None:
|
||||
kwargs["frontend_path"] = Path(args.frontend)
|
||||
run_server(**kwargs)
|
||||
|
||||
try:
|
||||
run_server(**kwargs)
|
||||
except Exception:
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.write("=" * 60 + "\n")
|
||||
sys.stderr.write("ERROR: Unsloth Studio failed to start.\n")
|
||||
sys.stderr.write("=" * 60 + "\n")
|
||||
traceback.print_exc(file = sys.stderr)
|
||||
sys.stderr.write("\n")
|
||||
sys.stderr.write(
|
||||
"If a package is missing, try re-running: unsloth studio setup\n"
|
||||
)
|
||||
sys.stderr.flush()
|
||||
sys.exit(1)
|
||||
|
||||
# ── Signal handler — ensures subprocess cleanup on Ctrl+C ────
|
||||
def _signal_handler(signum, frame):
|
||||
|
|
|
|||
|
|
@ -946,23 +946,85 @@ if (Test-Path $OxcValidatorDir) {
|
|||
Write-Host ""
|
||||
Write-Host "Setting up Python environment..." -ForegroundColor Cyan
|
||||
|
||||
# Find Python
|
||||
# Find Python -- skip Anaconda/Miniconda distributions.
|
||||
# Conda-bundled CPython ships modified DLL search paths that break
|
||||
# torch's c10.dll loading on Windows. Standalone CPython (python.org,
|
||||
# winget, uv) does not have this issue.
|
||||
# Uses Get-Command -All to look past conda entries that shadow a valid
|
||||
# standalone Python further down PATH, and probes py.exe (the Python
|
||||
# Launcher) which reliably finds python.org installs.
|
||||
#
|
||||
# NOTE: A venv created from conda Python inherits conda's base_prefix
|
||||
# even though the venv path itself does not contain "conda". We check
|
||||
# both the executable path AND sys.base_prefix to catch this case.
|
||||
$CondaSkipPattern = '(?i)(conda|miniconda|anaconda|miniforge|mambaforge)'
|
||||
$PythonCmd = $null
|
||||
foreach ($candidate in @("python3.13", "python3.12", "python3.11", "python3", "python")) {
|
||||
|
||||
# Helper: check if a Python executable is conda-based by inspecting
|
||||
# both the path and sys.base_prefix (catches venvs created from conda).
|
||||
function Test-IsConda {
|
||||
param([string]$Exe)
|
||||
if ($Exe -match $CondaSkipPattern) { return $true }
|
||||
try {
|
||||
$ver = & $candidate --version 2>&1
|
||||
if ($ver -match 'Python 3\.(\d+)') {
|
||||
$minor = [int]$Matches[1]
|
||||
if ($minor -ge 11 -and $minor -le 13) {
|
||||
$PythonCmd = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
$basePrefix = (& $Exe -c "import sys; print(sys.base_prefix)" 2>$null | Out-String).Trim()
|
||||
if ($basePrefix -match $CondaSkipPattern) { return $true }
|
||||
} catch { }
|
||||
return $false
|
||||
}
|
||||
|
||||
# 1. Try the Python Launcher (py.exe) first -- most reliable on Windows.
|
||||
# py.exe is installed by python.org and resolves to standalone CPython.
|
||||
$pyLauncher = Get-Command py -CommandType Application -ErrorAction SilentlyContinue
|
||||
if ($pyLauncher -and $pyLauncher.Source -notmatch $CondaSkipPattern) {
|
||||
foreach ($minor in @("3.13", "3.12", "3.11")) {
|
||||
try {
|
||||
$out = & $pyLauncher.Source "-$minor" --version 2>&1 | Out-String
|
||||
if ($out -match 'Python 3\.(\d+)') {
|
||||
$pyMinor = [int]$Matches[1]
|
||||
if ($pyMinor -ge 11 -and $pyMinor -le 13) {
|
||||
# Resolve the actual executable path so venv creation
|
||||
# does not re-resolve back to a conda interpreter.
|
||||
$resolvedExe = (& $pyLauncher.Source "-$minor" -c "import sys; print(sys.executable)" 2>$null | Out-String).Trim()
|
||||
if ($resolvedExe -and (Test-Path $resolvedExe) -and -not (Test-IsConda $resolvedExe)) {
|
||||
$PythonCmd = $resolvedExe
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Fall back to scanning python3.x / python3 / python on PATH.
|
||||
# Use Get-Command -All to look past conda entries.
|
||||
if (-not $PythonCmd) {
|
||||
foreach ($candidate in @("python3.13", "python3.12", "python3.11", "python3", "python")) {
|
||||
foreach ($cmdInfo in @(Get-Command $candidate -All -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
if (-not $cmdInfo.Source) { continue }
|
||||
if ($cmdInfo.Source -like "*\WindowsApps\*") { continue }
|
||||
if (Test-IsConda $cmdInfo.Source) {
|
||||
Write-Host " [SKIP] $($cmdInfo.Source) (conda Python breaks torch DLL loading)" -ForegroundColor Yellow
|
||||
continue
|
||||
}
|
||||
$ver = & $cmdInfo.Source --version 2>&1
|
||||
if ($ver -match 'Python 3\.(\d+)') {
|
||||
$minor = [int]$Matches[1]
|
||||
if ($minor -ge 11 -and $minor -le 13) {
|
||||
$PythonCmd = $cmdInfo.Source
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
if ($PythonCmd) { break }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $PythonCmd) {
|
||||
Write-Host "[ERROR] No Python 3.11-3.13 found." -ForegroundColor Red
|
||||
Write-Host "[ERROR] No standalone Python 3.11-3.13 found (conda Python is not supported)." -ForegroundColor Red
|
||||
Write-Host " Install Python from https://python.org/downloads/ or via:" -ForegroundColor Yellow
|
||||
Write-Host " winget install -e --id Python.Python.3.12" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,16 @@ def studio_default(
|
|||
except KeyboardInterrupt:
|
||||
# Child has its own signal handler — let it finish
|
||||
rc = proc.wait()
|
||||
if rc != 0:
|
||||
typer.echo(
|
||||
f"\nError: Studio server exited unexpectedly (code {rc}).",
|
||||
err = True,
|
||||
)
|
||||
typer.echo(
|
||||
"Check the error above. If a package is missing, "
|
||||
"re-run: unsloth studio setup",
|
||||
err = True,
|
||||
)
|
||||
raise typer.Exit(rc)
|
||||
else:
|
||||
os.execvp(str(studio_python), args)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue