- studio_launch.sh: also gate the categorized-view landing URL on
UNSLOTH_SKIP_NOTEBOOK_SYNC (the entrypoint skips building the view entirely in
that mode), not just UNSLOTH_SKIP_NOTEBOOK_VIEW, so a no-sync container does not
land on a missing folder.
- Dockerfile.studio: scope the sticker-install "|| echo" fallback to only the
sticker step via a { ...; } group. It was attached to the whole branding &&
chain, so a failure in a REQUIRED step (JS resolve, favicon/logo/login copy)
was swallowed and the build continued with broken branding.
- unsloth_nb_view.py: when creating the categorized symlinks, only replace our
own stale symlinks; if a real user file already occupies that name, keep it and
skip the link instead of os.remove-ing it.
- overrides.json: drop doNotDisturbMode (it silenced ALL JupyterLab toasts,
including kernel-restart / connection-drop feedback). The news/update prompts
are already off via fetchNews / checkForUpdates.
- Dockerfile: keep decord mandatory on amd64 (fail the build on a missing or
incompatible wheel) and only fail-soft on arm64/other arches that have no wheel.
- cellNav.ts: do not hijack ArrowUp/Down when focus is in an interactive output
widget / form control, or while a completion popup is open, so ipywidgets
controls and autocomplete at cell boundaries keep working.
246 lines
14 KiB
Text
246 lines
14 KiB
Text
# Full Unsloth image: base training stack + Studio + JupyterLab + sshd.
|
|
#
|
|
# This is the image published as docker.io/unsloth/unsloth:studio (and the
|
|
# default :latest). It layers Unsloth Studio on top of the lean core image
|
|
# (Dockerfile, published under the `core` tags) and runs the same service trio
|
|
# as the previous production
|
|
# image: Studio on 8000, JupyterLab on 8888, key-only sshd on 22.
|
|
#
|
|
# Build (local):
|
|
# docker buildx build \
|
|
# --build-arg BASE_IMAGE=unsloth-blackwell:test \
|
|
# -f docker/Dockerfile.studio \
|
|
# -t unsloth-blackwell:studio docker/
|
|
#
|
|
# Run:
|
|
# docker run --rm --gpus all -p 8000:8000 -p 8888:8888 \
|
|
# -v $HOME/.cache/huggingface:/workspace/.cache/huggingface \
|
|
# unsloth-blackwell:studio
|
|
#
|
|
# Open http://localhost:8000 for Studio (first-boot admin password is printed
|
|
# in the container logs and persisted under /opt/unsloth-studio/auth/) and
|
|
# http://localhost:8888 for JupyterLab (password: JUPYTER_PASSWORD env; when
|
|
# unset a random one is generated and printed in the container logs). On
|
|
# hosts without GPU passthrough (Docker Desktop on macOS, Windows without
|
|
# WSL2 GPU) add -e UNSLOTH_ALLOW_CPU=1: training is unavailable but Studio
|
|
# chat / Data Recipes / GGUF tooling / Jupyter work.
|
|
#
|
|
# CI pins BASE_IMAGE to the just-published multi-arch base digest so the two
|
|
# images always ship the same stack.
|
|
|
|
ARG BASE_IMAGE=unsloth-blackwell:test
|
|
|
|
# --- builder stage: prebuild the Unsloth JupyterLab extension -----------------
|
|
# Builds the named "Unsloth Dark" (Monokai) theme + the Colab-style Down/Up
|
|
# cell-navigation keymap. Node lives ONLY in this throwaway stage; the final
|
|
# image copies just the prebuilt static labextension, so the runtime stays
|
|
# Node-free. Uses the base image's bundled jlpm + jupyterlab (version-matched).
|
|
FROM ${BASE_IMAGE} AS labext-builder
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
# JupyterLab 4.6's build tooling declares a Node >=20 engine; Ubuntu 24.04's
|
|
# distro nodejs is 18, so pull Node 20 LTS from NodeSource (it bundles npm).
|
|
# This stage is thrown away, so the extra apt sources never reach the runtime.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg git \
|
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
&& apt-get install -y --no-install-recommends nodejs \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
COPY jupyter/unsloth_labext /opt/labext-src
|
|
RUN cd /opt/labext-src \
|
|
&& /opt/unsloth-venv/bin/jlpm install \
|
|
&& /opt/unsloth-venv/bin/jlpm build:prod
|
|
|
|
FROM ${BASE_IMAGE}
|
|
|
|
# Studio source ref to clone. Defaults to `main`, but a CI publish pipeline
|
|
# that pins BASE_IMAGE to a digest should pin this too (same UNSLOTH_REF as
|
|
# the base) so the published image is reproducible against a known ref.
|
|
ARG UNSLOTH_STUDIO_REF=main
|
|
ARG TARGETARCH
|
|
|
|
# Services run as root in this revision (the base image is root-only by
|
|
# design); the previous production image ran them as a dedicated uid-1001
|
|
# user. Non-root parity is a tracked follow-up. sshd is key-only and stays
|
|
# disabled unless a PUBLIC_KEY/SSH_KEY is provided, and no secrets are
|
|
# persisted to disk (see studio_launch.sh).
|
|
#
|
|
# The JUPYTER_PORT / UNSLOTH_ENABLE_SSHD defaults exist so supervisord's
|
|
# %(ENV_*)s expansions still resolve when someone bypasses the launcher
|
|
# and runs supervisord directly.
|
|
USER root
|
|
ENV UNSLOTH_STUDIO_HOME=/opt/unsloth-studio \
|
|
JUPYTER_PORT=8888 \
|
|
UNSLOTH_ENABLE_SSHD=false \
|
|
DEBIAN_FRONTEND=noninteractive
|
|
|
|
# install.sh needs curl + git; supervisor + openssh-server run the service
|
|
# trio. The base image already has python + uv + pip.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
curl git ca-certificates supervisor openssh-server \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Clone + install Studio into a dedicated venv under $UNSLOTH_STUDIO_HOME.
|
|
# --local makes install.sh use the just-cloned source tree (editable
|
|
# install), so the source dir MUST persist for the venv's `unsloth_cli`
|
|
# entrypoint to keep resolving. Move it under $UNSLOTH_STUDIO_HOME/src
|
|
# (already inside the persistent layer) instead of deleting it. Strip
|
|
# .git to save ~120MB.
|
|
#
|
|
# The llama.cpp symlink BEFORE install.sh points Studio's prebuilt dir at
|
|
# the bundle already baked into the base image (validated, sha256-checked,
|
|
# UNSLOTH_PREBUILT_INFO.json present), so the installer's prebuilt step
|
|
# recognises it and skips a second ~400MB download. The
|
|
# .unsloth-studio-owned marker satisfies setup.sh's ownership assertion for
|
|
# custom STUDIO_HOMEs -- the dir IS provisioned exclusively for Studio.
|
|
#
|
|
# UNSLOTH_TORCH_INDEX_FAMILY pins the torch wheel index for the Studio
|
|
# venv: at build time there is no GPU and no nvidia-smi, so install.sh's
|
|
# probing would land on cpu or cu126 wheels depending on which host built
|
|
# the image. cu128 on BOTH arches, mirroring the base venv: cu130 wheels
|
|
# would silently lift the arm64 driver floor to 580+ while the base venv
|
|
# keeps the documented 570+ floor. DGX Spark / GB10 (sm_121) support comes
|
|
# from the same NVRTC cu13 swap the base image applies to its venv --
|
|
# repeated below for the Studio venv's own bundled libnvrtc (the base's
|
|
# arm64 layer already installed cuda-nvrtc-13-0, so the cu13 .so exists).
|
|
#
|
|
# UNSLOTH_PYTHON=3.12 pins the Studio venv to the SAME Python minor as the base
|
|
# venv (install.sh defaults Linux to 3.13). Matching minors makes the two venvs'
|
|
# nvidia-*-cu12 CUDA wheels byte-identical, which lets the dedup RUN further down
|
|
# replace the Studio venv's ~3.7GB of CUDA .so with symlinks into the base venv's
|
|
# copies (cudnn/cublas/nccl/... are plain C libs, Python-minor independent).
|
|
#
|
|
# fetch+checkout FETCH_HEAD instead of `clone --branch` because the CI
|
|
# pipeline passes a commit SHA as the ref (clone --branch only accepts
|
|
# branch/tag names).
|
|
RUN set -eux \
|
|
&& case "${TARGETARCH:-amd64}" in \
|
|
amd64|arm64) TORCH_FAMILY="cu128" ;; \
|
|
*) echo "ERROR: unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
|
|
esac \
|
|
&& mkdir -p "${UNSLOTH_STUDIO_HOME}" \
|
|
&& ln -s /opt/unsloth/llama.cpp "${UNSLOTH_STUDIO_HOME}/llama.cpp" \
|
|
&& touch /opt/unsloth/llama.cpp/.unsloth-studio-owned \
|
|
&& git init -q "${UNSLOTH_STUDIO_HOME}/src" \
|
|
&& cd "${UNSLOTH_STUDIO_HOME}/src" \
|
|
&& git remote add origin https://github.com/unslothai/unsloth \
|
|
&& git fetch -q --depth 1 origin "${UNSLOTH_STUDIO_REF}" \
|
|
&& git checkout -q FETCH_HEAD \
|
|
&& UNSLOTH_STUDIO_HOME="${UNSLOTH_STUDIO_HOME}" \
|
|
UNSLOTH_TORCH_INDEX_FAMILY="${TORCH_FAMILY}" \
|
|
UNSLOTH_PYTHON=3.12 \
|
|
bash install.sh --local \
|
|
# Fail loud if the Studio venv torch missed the pinned CUDA family (an
|
|
# install.sh that ignores UNSLOTH_TORCH_INDEX_FAMILY falls back to
|
|
# nvidia-smi probing, which cannot work at build time and lands on cu126
|
|
# wheels with no sm_100/sm_120 kernels). metadata check only: importing
|
|
# torch needs native libs, which QEMU arm64 builds cannot load.
|
|
&& "${UNSLOTH_STUDIO_HOME}/unsloth_studio/bin/python" -c "import sys; from importlib.metadata import version; assert sys.version_info[:2] == (3, 12), 'Studio venv python %d.%d is not 3.12 (UNSLOTH_PYTHON pin ignored) -- CUDA dedup below depends on it' % sys.version_info[:2]; v = version('torch'); assert v.endswith('+${TORCH_FAMILY}'), 'Studio venv torch ' + v + ' does not match ${TORCH_FAMILY}'; print('Studio venv python %d.%d' % sys.version_info[:2], 'torch', v)" \
|
|
# setup.sh may relink the root llama-quantize into build/bin; prove the
|
|
# relinked quantizer still resolves its libraries, or GGUF export breaks
|
|
# at runtime with "No working quantizer found". Content check, not rc:
|
|
# llama-quantize exits nonzero on --help, while a loader failure prints
|
|
# "error while loading shared libraries" and no usage text.
|
|
&& { "${UNSLOTH_STUDIO_HOME}/llama.cpp/llama-quantize" --help 2>&1 || true; } | grep -q "usage" \
|
|
&& rm -rf "${UNSLOTH_STUDIO_HOME}/src/.git" \
|
|
"${UNSLOTH_STUDIO_HOME}/src/studio/frontend/node_modules" \
|
|
/root/.cache \
|
|
&& if [ "${TARGETARCH:-amd64}" = "arm64" ]; then \
|
|
for NVRTC_DIR in "${UNSLOTH_STUDIO_HOME}"/unsloth_studio/lib/python*/site-packages/nvidia/cuda_nvrtc/lib; do \
|
|
if [ -f "${NVRTC_DIR}/libnvrtc.so.12" ]; then \
|
|
mv "${NVRTC_DIR}/libnvrtc.so.12" "${NVRTC_DIR}/libnvrtc.so.12.cu128.orig"; \
|
|
ln -s /usr/local/cuda-13.0/lib64/libnvrtc.so.13 "${NVRTC_DIR}/libnvrtc.so.12"; \
|
|
fi; \
|
|
done; \
|
|
fi \
|
|
&& BASE_NV=/opt/unsloth-venv/lib/python3.12/site-packages/nvidia \
|
|
&& STU_NV="${UNSLOTH_STUDIO_HOME}/unsloth_studio/lib/python3.12/site-packages/nvidia" \
|
|
&& if [ ! -d "${STU_NV}" ] || [ ! -d "${BASE_NV}" ]; then \
|
|
echo ">> nvidia dir missing (STU=${STU_NV} BASE=${BASE_NV}); skipping CUDA dedup"; \
|
|
else \
|
|
find "${UNSLOTH_STUDIO_HOME}/unsloth_studio" -name '*.a' -delete; \
|
|
rm -f "${STU_NV}/nvshmem/lib/libnvshmem_device.bc"; \
|
|
for c in cudnn cublas cusparselt nccl cusolver cusparse cufft curand nvjitlink cuda_cupti nvshmem npp; do \
|
|
b="${BASE_NV}/${c}/lib"; s="${STU_NV}/${c}/lib"; \
|
|
{ [ -d "$b" ] && [ -d "$s" ]; } || { echo ">> skip ${c} (dir missing)"; continue; }; \
|
|
if [ "${c}" = "npp" ]; then \
|
|
rm -rf "$s" && ln -s "$b" "$s" && readlink -e "$s" >/dev/null; \
|
|
echo ">> deduped npp -> base (pruned)"; \
|
|
elif [ "$(cd "$s" && ls | sort | tr '\n' ' ')" = "$(cd "$b" && ls | sort | tr '\n' ' ')" ]; then \
|
|
rm -rf "$s" && ln -s "$b" "$s" && readlink -e "$s" >/dev/null; \
|
|
echo ">> deduped ${c} -> base"; \
|
|
else \
|
|
echo ">> skip ${c} (file set differs base vs studio)"; \
|
|
fi; \
|
|
done; \
|
|
echo "studio venv size after dedup:"; du -sh "${UNSLOTH_STUDIO_HOME}/unsloth_studio"; \
|
|
fi
|
|
|
|
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
|
COPY studio_launch.sh /usr/local/bin/unsloth-studio-launch
|
|
# In-place updaters (no image pull):
|
|
# unsloth-studio-update refreshes the Studio packages (backend + baked
|
|
# frontend) and restarts the service.
|
|
# unsloth-llama-update swaps the baked llama.cpp prebuilt to the latest
|
|
# release (the same swap the in-app banner performs).
|
|
COPY unsloth_studio_update.sh /usr/local/bin/unsloth-studio-update
|
|
COPY unsloth_llama_update.sh /usr/local/bin/unsloth-llama-update
|
|
# unsloth-llama-update reuses the build-time fetcher (redirect-based, no GitHub
|
|
# API, so it is not rate-limited; deterministic portable bundle that runs on CPU
|
|
# and every supported GPU) rather than the host-probing installer.
|
|
COPY fetch_llama_prebuilt.py /usr/local/lib/unsloth/fetch_llama_prebuilt.py
|
|
# Optional public Cloudflare tunnel for JupyterLab (UNSLOTH_JUPYTER_CLOUDFLARE=1,
|
|
# or `unsloth-jupyter-tunnel --force`); supervisord runs it as jupyter-cloudflare.
|
|
COPY unsloth_jupyter_tunnel.sh /usr/local/bin/unsloth-jupyter-tunnel
|
|
# JupyterLab defaults baked for every container: the named "Unsloth Dark"
|
|
# (Monokai) theme with adaptive light/dark by system preference, a per-cell run
|
|
# button that does NOT auto-advance, a labeled "Restart & Run All", windowing
|
|
# disabled so collapsing a long output does not snap to the cell top,
|
|
# ArrowDown/Up jumping to the TOP of the next/previous cell, and the official
|
|
# Jupyter "get notified about news" prompt suppressed (fetchNews/checkForUpdates
|
|
# off). overrides.json is the system-wide settings override (read from the base
|
|
# venv's share/jupyter/lab/settings); the theme + keymap + Unsloth top-bar logo
|
|
# ship as the prebuilt labextension built in the labext-builder stage above.
|
|
COPY jupyter/overrides.json /opt/unsloth-venv/share/jupyter/lab/settings/overrides.json
|
|
COPY --from=labext-builder /opt/labext-src/unsloth-jupyterlab/labextension /opt/unsloth-venv/share/jupyter/labextensions/unsloth-jupyterlab
|
|
# Unsloth branding (all served by jupyter_server, so applied to its site-packages
|
|
# the same way): replace the browser-tab favicon and the page logo with the
|
|
# Unsloth logo, and brand the login screen (dark Unsloth-themed login.html).
|
|
# Also disable + lock the stock top-left Jupyter logo plugin so the Unsloth logo
|
|
# widget shipped by the labextension is the only one rendered in the top bar
|
|
# (lock keeps users from re-enabling it in the UI).
|
|
COPY jupyter/favicon.ico /tmp/unsloth-branding/favicon.ico
|
|
COPY jupyter/logo.png /tmp/unsloth-branding/logo.png
|
|
COPY jupyter/login.html /tmp/unsloth-branding/login.html
|
|
COPY jupyter/install_sloth_stickers.py /tmp/unsloth-branding/install_sloth_stickers.py
|
|
RUN JS="$(/opt/unsloth-venv/bin/python -c 'import os, jupyter_server; print(os.path.dirname(jupyter_server.__file__))')" \
|
|
&& for n in favicon.ico favicon-notebook.ico favicon-file.ico favicon-terminal.ico; do \
|
|
cp /tmp/unsloth-branding/favicon.ico "${JS}/static/favicons/${n}"; \
|
|
done \
|
|
&& cp /tmp/unsloth-branding/logo.png "${JS}/static/logo/logo.png" \
|
|
&& cp /tmp/unsloth-branding/login.html "${JS}/templates/login.html" \
|
|
# Copy the curated Studio sloth stickers the login page rotates through into
|
|
# jupyter_server's static dir (sloth/NN.png). Fail-soft -- but scoped to ONLY
|
|
# the sticker step via the { ...; } group so a failure in a REQUIRED branding
|
|
# step above (JS resolve, favicon/logo/login copy) still fails the build
|
|
# instead of being swallowed by this fallback. If the Studio public folder
|
|
# ever moves, login.html's onerror falls back to the logo.
|
|
&& { /opt/unsloth-venv/bin/python /tmp/unsloth-branding/install_sloth_stickers.py \
|
|
--src "${UNSLOTH_STUDIO_HOME}/src/studio/frontend/public/Sloth emojis" \
|
|
--dest "${JS}/static/sloth" \
|
|
|| echo ">> sloth stickers not installed (login falls back to the Unsloth logo)"; } \
|
|
&& rm -rf /tmp/unsloth-branding \
|
|
&& /opt/unsloth-venv/bin/jupyter labextension disable @jupyterlab/application-extension:logo \
|
|
&& /opt/unsloth-venv/bin/jupyter labextension lock @jupyterlab/application-extension:logo
|
|
RUN chmod +x /usr/local/bin/unsloth-studio-launch \
|
|
/usr/local/bin/unsloth-studio-update \
|
|
/usr/local/bin/unsloth-llama-update \
|
|
/usr/local/bin/unsloth-jupyter-tunnel
|
|
|
|
# Studio web UI, JupyterLab, sshd. All bind 0.0.0.0 inside the container's
|
|
# network namespace; the operator publishes them explicitly with -p.
|
|
EXPOSE 8000 8888 22
|
|
|
|
# The base ENTRYPOINT (unsloth-entrypoint) still runs its GPU pre-flight
|
|
# first, then hands off to the service launcher.
|
|
CMD ["/usr/local/bin/unsloth-studio-launch"]
|