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.
210 lines
13 KiB
Text
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"]
|