diff --git a/install.ps1 b/install.ps1 index 1396353283..d438e5ed2d 100644 --- a/install.ps1 +++ b/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 diff --git a/studio/backend/routes/data_recipe/seed.py b/studio/backend/routes/data_recipe/seed.py index 60fb1d7916..74a3abe972 100644 --- a/studio/backend/routes/data_recipe/seed.py +++ b/studio/backend/routes/data_recipe/seed.py @@ -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) diff --git a/studio/backend/run.py b/studio/backend/run.py index d1ee93ed64..5c24c550c7 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -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): diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 40e68e2ba0..8966449423 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -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 } diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index c4008aa4a6..192e138a9c 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -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)