unsloth/studio/setup.sh
2026-03-19 16:43:02 +00:00

425 lines
18 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
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 ──
run_quiet() {
local label="$1"
shift
local tmplog
tmplog=$(mktemp)
if "$@" > "$tmplog" 2>&1; then
rm -f "$tmplog"
else
local exit_code=$?
echo "$label failed (exit code $exit_code):"
cat "$tmplog"
rm -f "$tmplog"
exit $exit_code
fi
}
echo "╔══════════════════════════════════════╗"
echo "║ Unsloth Studio Setup Script ║"
echo "╚══════════════════════════════════════╝"
# ── 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) ──
IS_COLAB=false
keynames=$'\n'$(printenv | cut -d= -f1)
if [[ "$keynames" == *$'\nCOLAB_'* ]]; then
IS_COLAB=true
fi
# ── Detect whether frontend needs building ──
# Only skip when BOTH conditions are true:
# 1. We're inside site-packages (PyPI / pip install, not editable)
# 2. dist/ already exists (pre-built in the wheel)
# Otherwise always (re)build — handles upgrades, editable installs, and
# pip-from-source where dist/ was never built.
if [[ "$SCRIPT_DIR" == */site-packages/* ]] && [ -d "$SCRIPT_DIR/frontend/dist" ]; then
echo "✅ Frontend pre-built (PyPI) — skipping Node/npm check."
else
NEED_NODE=true
if command -v node &>/dev/null && command -v npm &>/dev/null; then
NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1)
NPM_MAJOR=$(npm -v | cut -d. -f1)
if [ "$NODE_MAJOR" -ge 20 ] && [ "$NPM_MAJOR" -ge 11 ]; then
echo "✅ Node $(node -v) and npm $(npm -v) already meet requirements. Skipping nvm install."
NEED_NODE=false
else
if [ "$IS_COLAB" = true ]; then
echo "✅ Node $(node -v) and npm $(npm -v) detected in Colab."
# In Colab, just upgrade npm directly - nvm doesn't work well
if [ "$NPM_MAJOR" -lt 11 ]; then
echo " Upgrading npm to latest..."
npm install -g npm@latest > /dev/null 2>&1
fi
NEED_NODE=false
else
echo "⚠️ Node $(node -v) / npm $(npm -v) too old. Installing via nvm..."
fi
fi
else
echo "⚠️ Node/npm not found. Installing via nvm..."
fi
if [ "$NEED_NODE" = true ]; then
# ── 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
# Load nvm (source ~/.bashrc won't work inside a script)
export NVM_DIR="$HOME/.nvm"
set +u
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# ── 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
# incompatible with nvm."
if [ -f "$HOME/.npmrc" ]; then
if grep -qE '^\s*(prefix|globalconfig)\s*=' "$HOME/.npmrc"; then
echo " Removing incompatible prefix/globalconfig from ~/.npmrc for nvm..."
sed -i.bak '/^\s*\(prefix\|globalconfig\)\s*=/d' "$HOME/.npmrc"
fi
fi
# ── 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 ──
NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1)
NPM_MAJOR=$(npm -v | cut -d. -f1)
if [ "$NODE_MAJOR" -lt 20 ]; then
echo "❌ ERROR: Node version must be >= 20 (got $(node -v))"
exit 1
fi
if [ "$NPM_MAJOR" -lt 11 ]; then
echo "⚠️ npm version is $(npm -v), expected >= 11. Updating..."
run_quiet "npm update" npm install -g npm@latest
fi
fi
echo "✅ Node $(node -v) | npm $(npm -v)"
# ── 5. Build frontend ──
cd "$SCRIPT_DIR/frontend"
# 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.
# Temporarily hide any such .gitignore during the build, then restore it.
_HIDDEN_GITIGNORES=()
_dir="$(pwd)"
while [ "$_dir" != "/" ]; do
_dir="$(dirname "$_dir")"
if [ -f "$_dir/.gitignore" ] && grep -qx '\*' "$_dir/.gitignore" 2>/dev/null; then
mv "$_dir/.gitignore" "$_dir/.gitignore._twbuild"
_HIDDEN_GITIGNORES+=("$_dir/.gitignore")
fi
done
_restore_gitignores() {
for _gi in "${_HIDDEN_GITIGNORES[@]+"${_HIDDEN_GITIGNORES[@]}"}"; do
mv "${_gi}._twbuild" "$_gi" 2>/dev/null || true
done
}
trap _restore_gitignores EXIT
run_quiet "npm install" npm install
run_quiet "npm run build" npm run build
_restore_gitignores
trap - EXIT
cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator"
run_quiet "npm install (oxc validator runtime)" npm install
cd "$SCRIPT_DIR"
echo "✅ Frontend built to frontend/dist"
fi # end frontend dist check
# ── 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) ──
MIN_PY_MINOR=11 # minimum minor version (>= 3.11)
MAX_PY_MINOR=13 # maximum minor version (< 3.14)
BEST_PY=""
BEST_MAJOR=0
BEST_MINOR=0
# Collect candidate python3 binaries (python3, python3.9, python3.10, …)
for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)?$' | sort -u); do
if ! command -v "$candidate" &>/dev/null; then
continue
fi
# Get version string, e.g. "Python 3.12.5"
ver_str=$("$candidate" --version 2>&1) || continue
ver_str=$(echo "$ver_str" | awk '{print $2}')
py_major=$(echo "$ver_str" | cut -d. -f1)
py_minor=$(echo "$ver_str" | cut -d. -f2)
# Skip anything that isn't Python 3
if [ "$py_major" -ne 3 ] 2>/dev/null; then
continue
fi
# Skip versions below 3.12 (require > 3.11)
if [ "$py_minor" -lt "$MIN_PY_MINOR" ] 2>/dev/null; then
continue
fi
# Skip versions above 3.13 (require < 3.14)
if [ "$py_minor" -gt "$MAX_PY_MINOR" ] 2>/dev/null; then
continue
fi
# Keep the highest qualifying version
if [ "$py_minor" -gt "$BEST_MINOR" ]; then
BEST_PY="$candidate"
BEST_MAJOR="$py_major"
BEST_MINOR="$py_minor"
fi
done
echo "finished finding best python"
if [ -z "$BEST_PY" ]; then
echo "❌ ERROR: No Python version between 3.${MIN_PY_MINOR} and 3.${MAX_PY_MINOR} found on this system."
echo " Detected Python 3 installations:"
for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)?$' | sort -u); do
if command -v "$candidate" &>/dev/null; then
echo " - $candidate ($($candidate --version 2>&1))"
fi
done
echo ""
echo " Please install Python 3.${MIN_PY_MINOR} or 3.${MAX_PY_MINOR}."
echo " For example: sudo apt install python3.12 python3.12-venv"
exit 1
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)"
REQ_ROOT="$SCRIPT_DIR/backend/requirements"
SINGLE_ENV_CONSTRAINTS="$REQ_ROOT/single-env/constraints.txt"
SINGLE_ENV_DATA_DESIGNER="$REQ_ROOT/single-env/data-designer.txt"
SINGLE_ENV_DATA_DESIGNER_DEPS="$REQ_ROOT/single-env/data-designer-deps.txt"
SINGLE_ENV_PATCH="$REQ_ROOT/single-env/patch_metadata.py"
install_python_stack() {
python "$SCRIPT_DIR/install_python_stack.py"
}
if [ "$IS_COLAB" = true ]; then
# Colab: install packages directly without venv
install_python_stack
else
# Local: create venv under studio home (shared location, not in repo)
# Configurable via UNSLOTH_STUDIO_HOME; defaults to ~/.unsloth/studio
STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-$HOME/.unsloth/studio}"
echo " Studio home: $STUDIO_HOME"
# Persist for future `unsloth studio` runs (survives shell restarts)
mkdir -p "$HOME/.unsloth"
echo "$STUDIO_HOME" > "$HOME/.unsloth/studio_home"
VENV_DIR="$STUDIO_HOME/.venv"
VENV_T5_DIR="$STUDIO_HOME/.venv_t5"
mkdir -p "$STUDIO_HOME"
# Clean up legacy in-repo venvs if they exist
[ -d "$REPO_ROOT/.venv" ] && rm -rf "$REPO_ROOT/.venv"
[ -d "$REPO_ROOT/.venv_overlay" ] && rm -rf "$REPO_ROOT/.venv_overlay"
[ -d "$REPO_ROOT/.venv_t5" ] && rm -rf "$REPO_ROOT/.venv_t5"
rm -rf "$VENV_DIR"
rm -rf "$VENV_T5_DIR"
"$BEST_PY" -m venv "$VENV_DIR"
source "$VENV_DIR/bin/activate"
cd "$SCRIPT_DIR"
install_python_stack
# ── 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.
echo ""
echo " Pre-installing transformers 5.x for newer model support..."
mkdir -p "$VENV_T5_DIR"
run_quiet "pip install transformers 5.x" pip install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0"
run_quiet "pip install huggingface_hub for t5" pip install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.3.0"
echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
# ── 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
echo ""
echo "⚠️ WSL detected — installing build dependencies for GGUF export..."
echo " You may be prompted for your password."
sudo apt-get update -y
sudo apt-get install -y build-essential cmake curl git libcurl4-openssl-dev
echo "✅ GGUF build dependencies installed"
fi
fi
# ── 8. Build llama.cpp binaries for GGUF inference + export ──
# Disabled: llama.cpp build is commented out for now.
# UNCOMMENT the block below to re-enable.
#
# # 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
# # - llama-quantize: for GGUF export quantization (symlinked to root for check_llama_cpp())
# UNSLOTH_HOME="$HOME/.unsloth"
# mkdir -p "$UNSLOTH_HOME"
# LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
# LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
# 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 " 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)"
# else
# echo ""
# echo "Building llama-server for GGUF inference..."
#
# BUILD_OK=true
# run_quiet "clone llama.cpp" git clone --depth 1 https://github.com/ggml-org/llama.cpp.git "$LLAMA_CPP_DIR" || BUILD_OK=false
#
# if [ "$BUILD_OK" = true ]; then
# # Skip tests/examples we don't need (faster build)
# CMAKE_ARGS="-DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF -DLLAMA_BUILD_SERVER=ON -DGGML_NATIVE=ON"
#
# # Use ccache if available (dramatically faster rebuilds)
# if command -v ccache &>/dev/null; then
# CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache"
# echo " Using ccache for faster compilation"
# fi
#
# # Detect CUDA: check nvcc on PATH, then common install locations
# NVCC_PATH=""
# if command -v nvcc &>/dev/null; then
# NVCC_PATH="$(command -v nvcc)"
# elif [ -x /usr/local/cuda/bin/nvcc ]; then
# NVCC_PATH="/usr/local/cuda/bin/nvcc"
# export PATH="/usr/local/cuda/bin:$PATH"
# elif ls /usr/local/cuda-*/bin/nvcc &>/dev/null 2>&1; then
# # Pick the newest cuda-XX.X directory
# NVCC_PATH="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)"
# export PATH="$(dirname "$NVCC_PATH"):$PATH"
# fi
#
# if [ -n "$NVCC_PATH" ]; then
# echo " Building with CUDA support (nvcc: $NVCC_PATH)..."
# CMAKE_ARGS="$CMAKE_ARGS -DGGML_CUDA=ON"
#
# # Detect GPU compute capability and limit CUDA architectures
# # Without this, cmake builds for ALL default archs (very slow)
# CUDA_ARCHS=""
# if command -v nvidia-smi &>/dev/null; then
# # Read all GPUs, deduplicate (handles mixed-GPU hosts)
# _raw_caps=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true)
# while IFS= read -r _cap; do
# _cap=$(echo "$_cap" | tr -d '[:space:]')
# if [[ "$_cap" =~ ^([0-9]+)\.([0-9]+)$ ]]; then
# _arch="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
# # Append if not already present
# case ";$CUDA_ARCHS;" in
# *";$_arch;"*) ;;
# *) CUDA_ARCHS="${CUDA_ARCHS:+$CUDA_ARCHS;}$_arch" ;;
# esac
# fi
# done <<< "$_raw_caps"
# fi
#
# if [ -n "$CUDA_ARCHS" ]; then
# echo " GPU compute capabilities: ${CUDA_ARCHS//;/, } -- limiting build to detected archs"
# CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}"
# else
# echo " Could not detect GPU arch -- building for all default CUDA architectures (slower)"
# fi
#
# # 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 " To enable GPU: install cuda-toolkit or add nvcc to PATH"
# else
# echo " Building CPU-only (no CUDA detected)..."
# fi
#
# NCPU=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
#
# # Use Ninja if available (faster parallel builds than Make)
# CMAKE_GENERATOR_ARGS=""
# if command -v ninja &>/dev/null; then
# CMAKE_GENERATOR_ARGS="-G Ninja"
# fi
#
# run_quiet "cmake llama.cpp" cmake $CMAKE_GENERATOR_ARGS -S "$LLAMA_CPP_DIR" -B "$LLAMA_CPP_DIR/build" $CMAKE_ARGS || BUILD_OK=false
# fi
#
# if [ "$BUILD_OK" = true ]; then
# run_quiet "build llama-server" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false
# fi
#
# # Also build llama-quantize (needed by unsloth-zoo's GGUF export pipeline)
# if [ "$BUILD_OK" = true ]; then
# run_quiet "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
# 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"
# fi
# fi
#
# if [ "$BUILD_OK" = true ]; then
# 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"
# 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"
# fi
# fi
# }
echo ""
if [ "$IS_COLAB" = true ]; then
echo "╔══════════════════════════════════════╗"
echo "║ Setup Complete! ║"
echo "╠══════════════════════════════════════╣"
echo "║ Unsloth Studio is ready to start ║"
echo "║ in your Colab notebook! ║"
echo "╚══════════════════════════════════════╝"
else
echo "╔══════════════════════════════════════╗"
echo "║ Setup Complete! ║"
echo "╠══════════════════════════════════════╣"
echo "║ Launch with: ║"
echo "║ ║"
echo "║ unsloth studio -H 0.0.0.0 -p 8000 ║"
echo "╚══════════════════════════════════════╝"
fi