unsloth/docker/Dockerfile.studio
Daniel Han 9ca7be82c4 docker: trim redundant comments in the image build files
Comment-only pass over the PR's own files. No executable line changes.

- Dockerfile / Dockerfile.studio: drop the decorative stage banner rules, the
  stale "5)" / "6)" step numbering, and the entrypoint pre-flight list that
  restated (and had drifted from) entrypoint.sh's own accurate header. Cut the
  llama.cpp asset bullet list that repeats fetch_llama_prebuilt.py's docstring
  and the structlog rationale already spelled out at the install site.
- entrypoint.sh / studio_launch.sh: fold the section banners into the
  explanation lines that follow them.
- docker-publish.yml: remove the comment rule lines around the job headers.
- validate_studio_features.py: same for the numbered section headers.
- smoke_test.py: drop the stale "~125M params" note on a 1B model.
- unsloth_branding.py, unsloth_nb_view.py, unsloth_nb_pip_magic.py,
  colabTitle.ts: remove comments that restate the adjacent line.
2026-07-26 15:45:58 +00:00

210 lines
13 KiB
Text

# Full Unsloth image: base training stack + Studio + JupyterLab + sshd.
# Published as unsloth/unsloth:studio (and default :latest); layers Studio on the
# lean core image and runs Studio:8000, JupyterLab:8888, sshd: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
#
# Studio on :8000 (first-boot admin password in the logs, persisted under
# /opt/unsloth-studio/auth/); JupyterLab on :8888 (JUPYTER_PASSWORD env, else a
# random one is printed). Without GPU passthrough add -e UNSLOTH_ALLOW_CPU=1:
# training is unavailable but Studio chat / Data Recipes / GGUF / Jupyter work.
# CI pins BASE_IMAGE to the published base digest so both images ship the same stack.
ARG BASE_IMAGE=unsloth-blackwell:test
# Builds the "Unsloth Dark" (Monokai) theme + Colab-style cell-nav keymap. Node
# lives only in this throwaway stage; the final image copies just the prebuilt
# labextension (runtime stays Node-free). Uses the base's bundled jlpm+jupyterlab.
FROM ${BASE_IMAGE} AS labext-builder
ENV DEBIAN_FRONTEND=noninteractive
# JupyterLab 4.6 needs Node >=20; Ubuntu 24.04 ships 18, so pull Node 20 LTS from
# NodeSource. This stage is thrown away, so the apt sources never reach 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; CI pins it (same UNSLOTH_REF as
# the base) so the published image is reproducible.
ARG UNSLOTH_STUDIO_REF=main
# unsloth-zoo ref overlaid into the Studio venv by install.sh --local. The publish
# workflow passes ONE zoo ref to both builds, so Studio runs the same zoo as base.
ARG UNSLOTH_STUDIO_ZOO_REF=main
# The SAME llama.cpp tag the base baked. setup.sh honours UNSLOTH_LLAMA_TAG;
# without the pin the Studio build could re-resolve "latest" and diverge.
ARG LLAMA_PREBUILT_TAG=latest
ARG TARGETARCH
# Services run as root here (non-root parity is a follow-up). sshd is key-only,
# disabled unless PUBLIC_KEY/SSH_KEY is set (see studio_launch.sh). The
# JUPYTER_PORT / UNSLOTH_ENABLE_SSHD defaults let supervisord's %(ENV_*)s resolve.
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 is editable, so the source MUST persist -- keep it at $STUDIO_HOME/src,
# strip .git (~120MB).
#
# The llama.cpp symlink BEFORE install.sh points Studio's prebuilt dir at the
# base's baked bundle so the installer skips a second ~400MB download; the
# .unsloth-studio-owned marker satisfies setup.sh's ownership assertion.
#
# UNSLOTH_TORCH_INDEX_FAMILY pins the Studio venv's torch index (no nvidia-smi at
# build time would land on cpu/cu126). cu128 on both arches, mirroring the base.
# Blackwell JIT (sm_103/sm_121) comes from the same cu13 NVRTC swap, repeated below.
#
# UNSLOTH_PYTHON=3.12 pins the Studio venv to the base's Python minor so the
# nvidia-*-cu12 wheels are byte-identical and the dedup below can symlink them.
#
# fetch+checkout FETCH_HEAD, not `clone --branch`: CI passes a commit SHA.
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_ZOO_REF="${UNSLOTH_STUDIO_ZOO_REF}" \
UNSLOTH_LLAMA_TAG="${LLAMA_PREBUILT_TAG}" \
UNSLOTH_PYTHON=3.12 \
bash install.sh --local \
# Fail loud unless the Studio venv torch EXACTLY matches the base (version AND
# CUDA family) before the dedup symlinks their CUDA libs. Compare to the base's
# own torch (no hardcoded version); metadata only (QEMU arm64 can't import torch).
&& BASE_TORCH="$(/opt/unsloth-venv/bin/python -c "from importlib.metadata import version; print(version('torch'))")" \
&& "${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 == '${BASE_TORCH}', 'Studio venv torch ' + v + ' does not match base venv torch ${BASE_TORCH} (CUDA dedup would link mismatched libs)'; print('Studio venv python %d.%d torch' % sys.version_info[:2], v, '== base', '${BASE_TORCH}')" \
# setup.sh may relink llama-quantize into build/bin; prove it still resolves its
# libraries. Content check, not rc: --help exits nonzero but prints usage.
&& { "${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 \
# Stage the Studio venv's NVRTC like the base (.cu128.orig default + .cu13
# alias, retargeted per device by select_cuda_jit_tools). Both arches.
&& 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" ] && [ ! -L "${NVRTC_DIR}/libnvrtc.so.12" ]; then \
mv "${NVRTC_DIR}/libnvrtc.so.12" "${NVRTC_DIR}/libnvrtc.so.12.cu128.orig"; \
ln -s libnvrtc.so.12.cu128.orig "${NVRTC_DIR}/libnvrtc.so.12"; \
ln -s /usr/local/cuda-13.0/lib64/libnvrtc.so.13 "${NVRTC_DIR}/libnvrtc.so.12.cu13"; \
fi; \
done \
&& 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 refresh Studio packages (backend + frontend) and restart
# unsloth-llama-update swap the baked llama.cpp prebuilt to the latest release
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, not rate-
# limited; deterministic portable bundle) 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 (theme, non-advancing run button,
# labeled "Restart & Run All", windowing off, cell-nav keymap, news prompt off).
# overrides.json is the settings override; theme + keymap + logo ship as the
# prebuilt labextension from labext-builder 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 (applied to jupyter_server's site-packages): replace favicon +
# logo, brand login.html, disable+lock the stock top-left logo. Only the
# sloth-sticker install is fail-soft (`|| echo`); the copies above stay fatal.
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" \
&& { /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 \
&& /opt/unsloth-venv/bin/jupyter labextension disable @jupyterlab/apputils-extension:splash \
&& /opt/unsloth-venv/bin/jupyter labextension lock @jupyterlab/apputils-extension:splash \
&& /opt/unsloth-venv/bin/jupyter labextension lock unsloth-jupyterlab
# Branding integrity guard: the attribution checker (a jupyter_server extension),
# the AGPLv3 license text, and its enabling config, into the base venv. --verify
# FAILS the build if any attribution / license asset is missing or altered.
COPY jupyter/unsloth_branding.py /tmp/unsloth-branding-guard/unsloth_branding.py
COPY jupyter/jupyter_server_config.d/unsloth_branding_guard.json /tmp/unsloth-branding-guard/unsloth_branding_guard.json
RUN SP="$(/opt/unsloth-venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))')" \
&& cp /tmp/unsloth-branding-guard/unsloth_branding.py "${SP}/unsloth_branding.py" \
&& mkdir -p /opt/unsloth-venv/etc/jupyter/jupyter_server_config.d \
&& cp /tmp/unsloth-branding-guard/unsloth_branding_guard.json \
/opt/unsloth-venv/etc/jupyter/jupyter_server_config.d/unsloth_branding_guard.json \
&& cp "${UNSLOTH_STUDIO_HOME}/src/studio/LICENSE.AGPL-3.0" \
/opt/unsloth-venv/share/jupyter/UNSLOTH_LICENSE.AGPL-3.0 \
&& rm -rf /tmp/unsloth-branding-guard \
&& /opt/unsloth-venv/bin/python -m unsloth_branding --verify
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, JupyterLab, sshd. All bind 0.0.0.0 in the container; publish 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"]