From b897cf8e5f49d793a388ecda15fe8cfc951c3ee9 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 24 Jun 2026 06:16:54 +0000 Subject: [PATCH] docker: add unsloth-studio-update for in-place Studio updates Updating Studio in the container previously meant pulling a fresh ~25GB image (or at best the ~6GB fused Studio layer) for what is usually a small Python/UI change. Add a baked helper so a running container can update in place: docker exec unsloth-studio-update It updates only the Studio packages -- the backend code and the pre-built frontend, which ships inside the unsloth wheel -- with `pip install -U --no-deps unsloth unsloth_zoo`, then restarts just the studio service via supervisor. The torch/CUDA stack is left untouched, so it is safe in both GPU and CPU-only containers. This deliberately avoids `unsloth studio update`, which re-runs the full installer and re-probes the GPU to pick torch wheels -- in a container started without --gpus that finds no GPU and can downgrade torch to CPU/cu126. Options: --ref installs from git (track main) instead of the latest PyPI release; --with-deps also updates dependencies; --no-restart defers the restart. After the swap the helper smoke-imports studio.backend.main and, if a transitive dep is now missing, points the user at --with-deps. The update lands in the container's writable layer (survives docker restart); mount -v unsloth_studio_home:/opt/unsloth-studio to keep it across a recreate. --- docker/.dockerignore | 1 + docker/Dockerfile.studio | 6 +- docker/unsloth_studio_update.sh | 101 ++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100755 docker/unsloth_studio_update.sh diff --git a/docker/.dockerignore b/docker/.dockerignore index 1b6d68223c..52b632d8b6 100644 --- a/docker/.dockerignore +++ b/docker/.dockerignore @@ -5,6 +5,7 @@ !fetch_llama_prebuilt.py !supervisord.conf !studio_launch.sh +!unsloth_studio_update.sh !unsloth_nb_compat.py !unsloth_pip_shim.py !unsloth_ipython_startup.py diff --git a/docker/Dockerfile.studio b/docker/Dockerfile.studio index 89609ef6fc..89969084b3 100644 --- a/docker/Dockerfile.studio +++ b/docker/Dockerfile.studio @@ -156,7 +156,11 @@ RUN set -eux \ COPY supervisord.conf /etc/supervisor/supervisord.conf COPY studio_launch.sh /usr/local/bin/unsloth-studio-launch -RUN chmod +x /usr/local/bin/unsloth-studio-launch +# In-place Studio updater: `docker exec unsloth-studio-update` +# refreshes the Studio packages (backend + baked frontend) and restarts the +# service, without pulling a new image or touching the torch/CUDA stack. +COPY unsloth_studio_update.sh /usr/local/bin/unsloth-studio-update +RUN chmod +x /usr/local/bin/unsloth-studio-launch /usr/local/bin/unsloth-studio-update # Studio web UI, JupyterLab, sshd. All bind 0.0.0.0 inside the container's # network namespace; the operator publishes them explicitly with -p. diff --git a/docker/unsloth_studio_update.sh b/docker/unsloth_studio_update.sh new file mode 100755 index 0000000000..fb888ddd59 --- /dev/null +++ b/docker/unsloth_studio_update.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Update Unsloth Studio in place, inside a running container, without pulling a +# new image. Updates ONLY the Studio Python packages (the backend code and the +# pre-built frontend, which ships inside the unsloth wheel) and restarts the +# Studio service. The torch/CUDA stack is left untouched. +# +# docker exec unsloth-studio-update # latest PyPI release +# docker exec unsloth-studio-update --ref main # latest git main +# docker exec unsloth-studio-update --with-deps # also update deps +# docker exec unsloth-studio-update --no-restart # update, restart later +# +# Why not `unsloth studio update`: that command re-runs the full installer, +# which re-probes the host GPU to pick torch wheels. In a CPU-only container +# (run without --gpus) it finds no GPU and can downgrade torch to CPU/cu126, +# breaking CUDA. This helper only touches the Studio packages, so it is safe in +# both GPU and CPU containers. +# +# Persistence: the update is written to the container's writable layer, so it +# survives `docker restart`. To keep it across a full `docker rm` + `docker run` +# (and to keep your chats/users/models), run Studio with its home on a named +# volume: -v unsloth_studio_home:/opt/unsloth-studio +set -euo pipefail + +STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-/opt/unsloth-studio}" +REF="" +NO_DEPS="--no-deps" +RESTART=1 +PACKAGES="unsloth unsloth_zoo" + +usage() { sed -n '2,21p' "$0"; } + +while [ $# -gt 0 ]; do + case "$1" in + --ref) REF="$2"; shift 2;; + --with-deps) NO_DEPS=""; shift;; + --no-restart) RESTART=0; shift;; + --packages) PACKAGES="$2"; shift 2;; + -h|--help) usage; exit 0;; + *) echo "unsloth-studio-update: unknown argument: $1" >&2; usage; exit 2;; + esac +done + +# Resolve the Studio venv python. Prefer the venv directly; fall back to +# following the launcher symlink ($STUDIO_HOME/bin/unsloth -> venv/bin/unsloth). +PY="" +for cand in \ + "$STUDIO_HOME/unsloth_studio/bin/python" \ + "$STUDIO_HOME/unsloth_studio/bin/python3"; do + [ -x "$cand" ] && { PY="$cand"; break; } +done +if [ -z "$PY" ] && [ -L "$STUDIO_HOME/bin/unsloth" ]; then + venv_bin="$(dirname "$(readlink -f "$STUDIO_HOME/bin/unsloth")")" + [ -x "$venv_bin/python" ] && PY="$venv_bin/python" +fi +[ -n "$PY" ] || { echo "unsloth-studio-update: could not find the Studio venv under $STUDIO_HOME" >&2; exit 1; } + +version_of() { "$PY" -c "from importlib.metadata import version; print(version('unsloth'))" 2>/dev/null || echo "unknown"; } + +echo "[studio-update] Studio venv: $PY" +echo "[studio-update] before: unsloth $(version_of)" + +# Build the package specs. With --ref, install from git so you can track main +# (or any branch/tag/sha); otherwise take the latest PyPI release. +if [ -n "$REF" ]; then + SPECS="git+https://github.com/unslothai/unsloth.git@${REF}#egg=unsloth" + SPECS="$SPECS git+https://github.com/unslothai/unsloth-zoo.git@${REF}#egg=unsloth_zoo" + echo "[studio-update] installing from git @${REF}" +else + SPECS="$PACKAGES" + echo "[studio-update] installing latest release of: $PACKAGES" +fi + +# shellcheck disable=SC2086 +"$PY" -m pip install -U $NO_DEPS $SPECS + +echo "[studio-update] after: unsloth $(version_of)" + +# Sanity: the backend must still import after the swap (a missing transitive +# dep from --no-deps shows up here). Non-fatal: just warn with the remedy. +if ! "$PY" -c "import studio.backend.main" >/dev/null 2>&1; then + echo "[studio-update] WARNING: 'import studio.backend.main' failed after update." >&2 + echo "[studio-update] A new dependency may be missing. Re-run with --with-deps:" >&2 + echo "[studio-update] unsloth-studio-update --with-deps" >&2 +fi + +if [ "$RESTART" = "1" ]; then + SUPCTL="$(command -v supervisorctl || true)" + [ -n "$SUPCTL" ] || SUPCTL="/opt/unsloth-venv/bin/supervisorctl" + if [ -x "$SUPCTL" ] && "$SUPCTL" status studio >/dev/null 2>&1; then + echo "[studio-update] restarting the studio service" + "$SUPCTL" restart studio + else + echo "[studio-update] supervisor not managing 'studio' here; restart Studio yourself" + echo "[studio-update] (e.g. 'docker restart ')" + fi +else + echo "[studio-update] --no-restart: restart Studio to load the update" + echo "[studio-update] docker exec supervisorctl restart studio" +fi + +echo "[studio-update] done"