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.
This commit is contained in:
Daniel Han 2026-07-26 15:45:58 +00:00
commit 9ca7be82c4
11 changed files with 25 additions and 76 deletions

View file

@ -59,12 +59,10 @@ permissions:
contents: read
jobs:
# ---------------------------------------------------------------------------
# Resolve every upstream ref ONCE (llama tag + unsloth/zoo shas + notebooks
# commit) so both arch legs and Studio bake identical bits. A dispatch input
# pins a frozen value; else a branch/tag is frozen to a sha via ls-remote, and
# llama "latest" follows the /releases/latest redirect (mirrors build.sh).
# ---------------------------------------------------------------------------
prepare:
runs-on: ubuntu-latest
timeout-minutes: 5
@ -154,11 +152,9 @@ jobs:
echo "commit=${SHA}" >> "$GITHUB_OUTPUT"
echo "notebooks commit: ${SHA}"
# ---------------------------------------------------------------------------
# Per-arch build: two parallel jobs on native runners, each pushing a single-arch
# image by digest (no tag); the merge job stitches them into one manifest. Avoids
# the "last push wins" race of two jobs pushing the same tag.
# ---------------------------------------------------------------------------
build:
needs: prepare
strategy:
@ -248,10 +244,8 @@ jobs:
if-no-files-found: error
retention-days: 1
# ---------------------------------------------------------------------------
# Merge the two per-arch digests into a multi-platform manifest under the real
# user-facing tag(s). Runs only after both build legs succeed.
# ---------------------------------------------------------------------------
merge:
runs-on: ubuntu-latest
needs: build
@ -326,12 +320,10 @@ jobs:
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
echo "base manifest: ${TAG} @ ${DIGEST}"
# ---------------------------------------------------------------------------
# Full image: base + Unsloth Studio + JupyterLab + sshd (Dockerfile.studio).
# This is :latest. Same by-digest build + merge pattern as the base, FROMing the
# base manifest digest from the merge job. The arm64 leg builds Studio's vite
# frontend natively (the long pole), hence the larger timeout.
# ---------------------------------------------------------------------------
build-studio:
# `merge` for the freshly-published base manifest digest; `prepare` for the
# one resolved zoo ref (job outputs only flow through direct `needs`).
@ -470,10 +462,8 @@ jobs:
docker buildx imagetools inspect "$tag"
done
# ---------------------------------------------------------------------------
# Optional: pull the freshly published image onto a self-hosted GPU runner and
# run smoke_test.py. Skipped when no GPU runner is registered.
# ---------------------------------------------------------------------------
smoke-test:
needs: [merge, merge-studio]
if: ${{ vars.HAS_GPU_RUNNER == 'true' }}

View file

@ -33,9 +33,7 @@ ARG CUDA_VERSION=12.8.1
ARG UBUNTU_VERSION=24.04
ARG PYTHON_VERSION=3.12
# =============================================================================
# Stage 1: builder -- toolkit + dev headers, builds any source extensions
# =============================================================================
# Stage 1: builder -- toolkit + dev headers, builds any source extensions.
FROM nvidia/cuda:${CUDA_VERSION}-cudnn-devel-ubuntu${UBUNTU_VERSION} AS builder
# TARGETARCH (buildx: amd64/arm64) selects the unsloth extras matching the
@ -277,12 +275,12 @@ RUN set -eux \
done \
&& { du -sh ${VENV}/tf-sidecars || true; }
# 5) Informational pin record (NOT byte-reproducible: pip freeze omits wheel
# hashes and unsloth/vllm --pre float from VCS/nightly).
# Informational pin record (NOT byte-reproducible: pip freeze omits wheel hashes
# and unsloth/vllm --pre float from VCS/nightly).
RUN ${VENV}/bin/pip freeze --exclude-editable > ${VENV}/requirements.lock.txt \
&& head -50 ${VENV}/requirements.lock.txt
# 6) Strip pip cache & __pycache__ to shrink the runtime layer. The `-name tests`
# Strip pip cache & __pycache__ to shrink the runtime layer. The `-name tests`
# strip excludes numpy's tests dirs (numpy 2.4 needs numpy/_core/tests/ or
# `import numpy` breaks). Other verified-safe cuts:
# * npp: torchcodec dlopens only libnppicc + libnppc; drop the rest (~388MB).
@ -357,19 +355,14 @@ for pkg in LIGHT_IMPORTS:
importlib.import_module(pkg)
print(f"OK: {' + '.join(LIGHT_IMPORTS)} import cleanly on no-GPU host")
# `unsloth train` / `export` / `chat` / `list-checkpoints` all import
# studio.backend.core.*, whose dependency closure (structlog, and starlette by
# way of the logging handlers) is NOT part of unsloth[huggingface]. Missing any
# of it turns every one of those commands into a ModuleNotFoundError traceback,
# which no functional test here would otherwise catch. Runs last in the builder,
# after vLLM, because that is what pulls starlette in.
# Guard for the studio.backend.core.* closure the unsloth CLI needs (structlog,
# plus starlette via the logging handlers). Runs last in the builder, after vLLM,
# because that is what pulls starlette in.
from studio.backend.core.export import ExportBackend # noqa: F401
print("OK: the unsloth CLI can reach the studio export backend")
PY
# =============================================================================
# Stage 2: runtime -- slim runtime image, no nvcc, no cuDNN/cuBLAS layers
# =============================================================================
# Stage 2: runtime -- slim, no nvcc, no cuDNN/cuBLAS layers.
# The "-base-" variant drops ~2.7 GB of system CUDA libs we never load: torch
# wheels bake their own cuDNN/cuBLAS into torch/lib/ and resolve via RPATH. The
# base still provides nvidia-smi + libcuda stubs + libnvidia-ml.
@ -472,13 +465,9 @@ RUN set -eux \
# export hits install_llama_cpp()'s prompt + slow source build.
#
# NOT studio/install_llama_prebuilt.py: it selects a bundle for the CURRENT host,
# but the build must never introspect the host, so pin release + asset by build
# target (see fetch_llama_prebuilt.py):
# * amd64 -> app-<tag>-linux-x64-cuda12-portable.tar.gz (sm_70..sm_120)
# arm64 -> app-<tag>-linux-arm64-cuda13-portable.tar.gz (sm_90..sm_121)
# * portable bundles carry their own ggml backends, so they also run CPU-only
# * sha256-verified against the release's llama-prebuilt-sha256.json
# * converter + gguf-py from the SAME release's source tarball (mappings match)
# but the build must never introspect the host, so release + asset are pinned by
# build target instead (see fetch_llama_prebuilt.py).
#
# /opt (not /root) so it survives `docker run --user`. Default "latest" resolves
# the newest release; build.sh pins a concrete tag so the cache busts only on new
# releases. --build-arg LLAMA_PREBUILT_TAG=<tag> for a frozen build.
@ -530,7 +519,6 @@ WORKDIR /workspace
RUN mkdir -p ${HF_HOME} ${TRITON_CACHE_DIR} \
&& chmod -R a+rwX /workspace
# ---------------------------------------------------------------------------
# Per-notebook transformers version activation -- run unslothai/notebooks
# UNCHANGED (see unsloth_nb_compat.py). Pieces:
# * unsloth_nb_compat.py: tier detection + sidecar resolution + IPython hook.
@ -540,7 +528,6 @@ RUN mkdir -p ${HF_HOME} ${TRITON_CACHE_DIR} \
# same shim so in-process installs can't bypass PATH.
# * IPython startup hook: activates the right sidecar before the first model cell.
# * unsloth-run: headless `unsloth-run <notebook|url>`, the robust driven path.
# ---------------------------------------------------------------------------
COPY unsloth_nb_compat.py unsloth_pip_shim.py unsloth_nb_pip_magic.py unsloth_ipython_startup.py unsloth_run.py unsloth_sync_notebooks.sh unsloth_nb_content_sig.py unsloth_nb_view.py unsloth_nb_strip_colab.py unsloth_colab_compat.py /opt/unsloth-nb/
RUN set -eux \
&& SP=/opt/unsloth-venv/lib/python${PYTHON_VERSION}/site-packages \
@ -584,23 +571,17 @@ RUN set -eux \
&& rm -rf /opt/unsloth-notebooks/.git \
&& du -sh /opt/unsloth-notebooks
# JupyterLab lives in the venv. notebooks are pre-populated into
# /workspace/unsloth-notebooks on boot; mount a volume on /workspace to persist.
# Mount a volume on /workspace to persist the notebooks and caches.
EXPOSE 8888
COPY smoke_test.py /workspace/smoke_test.py
COPY entrypoint.sh /usr/local/bin/unsloth-entrypoint
RUN chmod +x /usr/local/bin/unsloth-entrypoint
# Entrypoint runs three fast pre-flight checks before user code:
# 1. nvidia-smi sees at least one GPU (catches missing --gpus all)
# 2. torch.cuda.is_available() is True (catches host driver too old)
# 3. compute capability >= sm_80 (catches pre-Ampere GPUs)
# Each check fails with an actionable error pointing to the fix.
# Bypass for offline tooling: docker run -e UNSLOTH_SKIP_GPU_CHECK=1 ...
# Fast GPU pre-flight checks before user code, each with an actionable error (see
# entrypoint.sh). Bypass for offline tooling: docker run -e UNSLOTH_SKIP_GPU_CHECK=1
ENTRYPOINT ["/usr/local/bin/unsloth-entrypoint"]
# Default command: interactive python REPL.
# Override examples:
# docker run --gpus all unsloth/unsloth:latest python /workspace/smoke_test.py
# docker run --gpus all -it unsloth/unsloth:latest bash

View file

@ -17,7 +17,6 @@
ARG BASE_IMAGE=unsloth-blackwell:test
# --- builder stage: prebuild the Unsloth JupyterLab extension -----------------
# 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.

View file

@ -7,7 +7,6 @@
# Bypass for offline tooling/docs/CI: docker run -e UNSLOTH_SKIP_GPU_CHECK=1 ...
set -euo pipefail
# --- CUDA JIT toolchain selection (device-gated) ----------------------------
# The image bakes CUDA 13 ptxas + NVRTC only for sm_103 (B300/GB300) and sm_121
# (GB10/DGX Spark), which cu12.8 can't target. Both ship on >=580 drivers, which a
# cu13 cubin needs. Every other arch uses cu12.8 on the 570-579 floor, where a
@ -87,9 +86,8 @@ if [[ "${UNSLOTH_ALLOW_CPU:-0}" == "1" ]]; then
fi
fi
# --- Check 1: nvidia-smi present and enumerates at least one GPU ------------
# nvidia-smi is injected by nvidia-container-toolkit on a GPU request, not baked
# in; a missing binary means "no GPU attached", same class as an empty -L.
# Check 1: nvidia-smi is injected by nvidia-container-toolkit on a GPU request,
# not baked in; a missing binary means "no GPU attached", same as an empty -L.
if ! command -v nvidia-smi >/dev/null 2>&1 || ! nvidia-smi -L 2>/dev/null | grep -q '^GPU'; then
err "No GPU visible inside the container."
cat >&2 <<'MSG'
@ -126,8 +124,8 @@ MSG
exit 1
fi
# --- Check 2: torch can actually use the GPU --------------------------------
# Catches host-driver-too-old (nvidia-smi enumerates but CUDA contexts fail).
# Check 2: torch can use the GPU. Catches host-driver-too-old (nvidia-smi
# enumerates but CUDA contexts fail).
python - >&2 <<'PY' || exit 1
import sys
import torch
@ -148,7 +146,7 @@ print("Then upgrade the driver to match.")
sys.exit(1)
PY
# --- Check 3: compute capability is supported -------------------------------
# Check 3: compute capability is supported.
python - >&2 <<'PY' || exit 1
import sys
import torch
@ -193,7 +191,6 @@ for d in range(1, n):
print(" exclude it with CUDA_VISIBLE_DEVICES or --gpus device=<supported>.")
PY
# --- arm64 note: baked llama.cpp is a CUDA 13 build -------------------------
# Upstream ships no CUDA 12 arm64 llama.cpp, so the arm64 image bakes cu13 while
# torch (cu128) runs on 570+. A cu13 cubin can't load on 570-579, so below 580
# GGUF export / Studio chat fail even though training works -- warn up front.

View file

@ -28,10 +28,8 @@ import json
import os
import sys
# ---------------------------------------------------------------------------
# Canonical attribution strings. Plain text; keep in sync with the TS mirror
# unsloth_labext/src/branding.ts (the guard greps the built bundle for these).
# ---------------------------------------------------------------------------
PRODUCT = "Unsloth Docker Studio"
SHORT_LABEL = "Built by the Unsloth team"
# Loading-splash caption; distinct from SHORT_LABEL (see branding.ts).

View file

@ -103,7 +103,7 @@ function applyTitle(cell: Cell): void {
barEl.className = 'unsloth-title-bar unsloth-collapsed';
const caret = document.createElement('span');
caret.className = 'unsloth-title-caret';
caret.textContent = '▾'; // down-pointing triangle
caret.textContent = '▾';
const text = document.createElement('span');
text.className = 'unsloth-title-text';
barEl.appendChild(caret);

View file

@ -106,7 +106,7 @@ def check_tiny_train(cap: tuple[int, int]) -> None:
from unsloth import FastLanguageModel
import torch
# Small, public, no-gate. ~125M params.
# Small, public, no-gate.
model_name = "unsloth/Llama-3.2-1B-Instruct-bnb-4bit"
print(f"loading {model_name}")
model, tokenizer = FastLanguageModel.from_pretrained(

View file

@ -33,8 +33,7 @@ for key, value in sorted(os.environ.items()):
print(f"export {key}={shlex.quote(value)}")
PY
# --- Jupyter -----------------------------------------------------------------
# Hash the password with jupyter's helper; never store plaintext. No fixed
# Hash the Jupyter password with jupyter's helper; never store plaintext. No fixed
# default: when JUPYTER_PASSWORD is unset, generate a random one and print it once.
JUPYTER_CONFIG_DIR=/root/.jupyter
JUPYTER_NOTE="password from JUPYTER_PASSWORD env"
@ -79,8 +78,7 @@ EOF
fi
fi
# --- sshd (opt-in) -----------------------------------------------------------
# Enabled only when a public key is provided; root password login is never
# sshd is enabled only when a public key is provided; root password login is never
# allowed. Cloud GPU platforms (e.g. runpod-style hosts) inject PUBLIC_KEY.
PUBLIC_SSH_KEY="${SSH_KEY:-${PUBLIC_KEY:-}}"
export UNSLOTH_ENABLE_SSHD=false
@ -95,7 +93,6 @@ fi
mkdir -p /workspace
# --- Branding / AGPLv3 attribution integrity gate (whole container) -----------
# This image ships under the GNU AGPLv3. Refuse to start if the Unsloth
# attribution (Help/About, splash, login, theme, AGPLv3 license + source links)
# is stripped or altered. The same checker runs as a jupyter_server extension and

View file

@ -74,7 +74,6 @@ def register_ipython():
return _magic
# Override the built-in %pip / %uv so they route through the shim too.
ip.register_magic_function(_make("pip"), "line", "pip")
ip.register_magic_function(_make("pip"), "line", "pip3")
ip.register_magic_function(_make("uv"), "line", "uv")

View file

@ -61,7 +61,7 @@ def parse_readme(readme_path):
text = f.read()
rows = []
seen_pairs = set() # (section, filename) already emitted
seen_pairs = set()
section = None
# Reset on ANY markdown heading, not just `###`: `#`/`##` domain headers carry
# their own nb/*.ipynb tables, so matching only `###` mis-filed those links.
@ -190,7 +190,7 @@ def _clear_view(path, dest_real):
for root, dirs, files in os.walk(path, topdown = False):
for name in files:
p = os.path.join(root, name)
if os.path.islink(p) and _points_into(p, dest_real): # our notebook symlinks only
if os.path.islink(p) and _points_into(p, dest_real):
try:
os.remove(p)
except OSError:

View file

@ -42,9 +42,7 @@ def check(
_failures.append(name)
# --------------------------------------------------------------------------
# 1. Colab cell-magic compatibility (#@title then %%capture)
# --------------------------------------------------------------------------
def test_colab_compat() -> None:
print("colab cell-magic compat (unsloth_colab_compat):")
m = importlib.import_module("unsloth_colab_compat")
@ -64,9 +62,7 @@ def test_colab_compat() -> None:
check("safe magic (%%bash) hoisted", m.colab_cell_magic_fix(bash)[0] == "%%bash\n")
# --------------------------------------------------------------------------
# 2. Notebook categorisation (clean_section) + README parsing
# --------------------------------------------------------------------------
def test_nb_view() -> None:
print("notebook view (unsloth_nb_view):")
v = importlib.import_module("unsloth_nb_view")
@ -82,9 +78,7 @@ def test_nb_view() -> None:
)
# --------------------------------------------------------------------------
# 3. Colab-intro + stale-widget stripping
# --------------------------------------------------------------------------
def test_strip() -> None:
print("notebook strip (unsloth_nb_strip_colab):")
s = importlib.import_module("unsloth_nb_strip_colab")
@ -142,9 +136,7 @@ def test_strip() -> None:
check("strip idempotent", not s._strip_intro(nb) and not s._clean_widgets(nb))
# --------------------------------------------------------------------------
# 4. Sidecar-log gating
# --------------------------------------------------------------------------
def test_sidecar_log_gate() -> None:
print("sidecar log gate (unsloth_nb_compat):")
c = importlib.import_module("unsloth_nb_compat")
@ -161,9 +153,7 @@ def test_sidecar_log_gate() -> None:
os.environ["UNSLOTH_ENABLE_LOGGING"] = old
# --------------------------------------------------------------------------
# 5. JupyterLab defaults (overrides.json)
# --------------------------------------------------------------------------
def test_overrides() -> None:
print("jupyterlab defaults (jupyter/overrides.json):")
path = os.path.join(JUPYTER, "overrides.json")
@ -200,9 +190,7 @@ def test_overrides() -> None:
)
# --------------------------------------------------------------------------
# 6. Labextension source (plugins) + login branding assets
# --------------------------------------------------------------------------
def test_labext_and_branding() -> None:
print("labextension + branding assets:")
pkg = os.path.join(LABEXT, "package.json")