From 9ca7be82c43f29beb57d8886a4951ca6c1fd4280 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 26 Jul 2026 15:45:58 +0000 Subject: [PATCH] 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. --- .github/workflows/docker-publish.yml | 10 ---- docker/Dockerfile | 47 ++++++------------- docker/Dockerfile.studio | 1 - docker/entrypoint.sh | 13 ++--- docker/jupyter/unsloth_branding.py | 2 - .../jupyter/unsloth_labext/src/colabTitle.ts | 2 +- docker/smoke_test.py | 2 +- docker/studio_launch.sh | 7 +-- docker/unsloth_nb_pip_magic.py | 1 - docker/unsloth_nb_view.py | 4 +- tests/validate_studio_features.py | 12 ----- 11 files changed, 25 insertions(+), 76 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 824425d833..d6f9a1d905 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -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' }} diff --git a/docker/Dockerfile b/docker/Dockerfile index 9348c89d1a..3c531615f8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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--linux-x64-cuda12-portable.tar.gz (sm_70..sm_120) -# arm64 -> app--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= 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 `, 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 diff --git a/docker/Dockerfile.studio b/docker/Dockerfile.studio index 3c7f2a0aae..d8e1766c6d 100644 --- a/docker/Dockerfile.studio +++ b/docker/Dockerfile.studio @@ -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. diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 28b4608d37..ea430cb222 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -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=.") 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. diff --git a/docker/jupyter/unsloth_branding.py b/docker/jupyter/unsloth_branding.py index 861d0d5a1c..ffbb6b3410 100644 --- a/docker/jupyter/unsloth_branding.py +++ b/docker/jupyter/unsloth_branding.py @@ -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). diff --git a/docker/jupyter/unsloth_labext/src/colabTitle.ts b/docker/jupyter/unsloth_labext/src/colabTitle.ts index 997ae40885..4c589cc6a2 100644 --- a/docker/jupyter/unsloth_labext/src/colabTitle.ts +++ b/docker/jupyter/unsloth_labext/src/colabTitle.ts @@ -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); diff --git a/docker/smoke_test.py b/docker/smoke_test.py index 41427da603..b763b53527 100644 --- a/docker/smoke_test.py +++ b/docker/smoke_test.py @@ -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( diff --git a/docker/studio_launch.sh b/docker/studio_launch.sh index a5bf5ddea8..a2f0d7dc3b 100644 --- a/docker/studio_launch.sh +++ b/docker/studio_launch.sh @@ -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 diff --git a/docker/unsloth_nb_pip_magic.py b/docker/unsloth_nb_pip_magic.py index 116aaf1bda..49c91852c9 100644 --- a/docker/unsloth_nb_pip_magic.py +++ b/docker/unsloth_nb_pip_magic.py @@ -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") diff --git a/docker/unsloth_nb_view.py b/docker/unsloth_nb_view.py index 718649a282..99f795d83f 100644 --- a/docker/unsloth_nb_view.py +++ b/docker/unsloth_nb_view.py @@ -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: diff --git a/tests/validate_studio_features.py b/tests/validate_studio_features.py index 948dc37a00..5c7f4338c7 100644 --- a/tests/validate_studio_features.py +++ b/tests/validate_studio_features.py @@ -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")