From 0ebbdbb9cccf31e1611968639d9ec41bdc580c75 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Fri, 26 Jun 2026 09:06:09 +0000 Subject: [PATCH] docker: address review follow-ups (pip-shim flags, sync ownership, tags, zoo ref, arch list) - pip shim: do not treat the value of an index-url / find-links / constraint flag as an install target. A cell like 'pip install --extra-index-url torch' now no-ops after keeping the baked stack instead of exec'ing a bare 'pip install --extra-index-url ' that fails. Positional . / url / vcs and -r/--requirement files still count as targets. - notebook sync: on first boot, record only files we actually wrote (or that are byte-identical to the template), never a kept pre-existing user file; and on the GitHub refresh, treat a file present in DEST but absent from the sync state as user-owned and keep it. Previously a bind-mounted notebook was recorded as managed and then overwritten by upstream. - docker-publish: add flavor latest=false to the Studio metadata steps too, so a v* tag push cannot emit an implicit :latest via metadata-action's latest=auto; :latest stays default-branch-only, and the smoke test pulls the published tag. - unsloth-studio-update: resolve the unsloth-zoo ref independently of --ref (new --zoo-ref, else use the ref only when the zoo repo has it, else fall back to main) so 'update --ref ' does not fail on a missing zoo ref. - Dockerfile: drop 10.3 (compute_103) from TORCH_CUDA_ARCH_LIST in both the builder and runtime stages. B300 runs sm_100 SASS, and the bundled CUDA 12.8 nvcc cannot compile compute_103 (added in 12.9), which broke arch-list-honoring source / JIT builds. --- .github/workflows/docker-publish.yml | 10 +++++++++- docker/Dockerfile | 17 ++++++++++------ docker/unsloth_pip_shim.py | 30 +++++++++++++++++++++------- docker/unsloth_studio_update.sh | 21 +++++++++++++++++-- docker/unsloth_sync_notebooks.sh | 23 +++++++++++++++------ 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 9085b261a4..d68f7c1889 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -429,9 +429,14 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # latest=false disables metadata-action's implicit latest=auto, which + # would otherwise emit :latest on a v* tag push and bypass the + # default-branch-only gate below. :latest is published only by the + # explicit type=raw rule (default-branch pushes), matching the base job. + flavor: latest=false tags: | # The full Studio image owns the unprefixed namespace, headed by - # :latest. Same :latest gating rationale as the base job. + # :latest (default branch only). Tag pushes publish the version tag. type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.inputs.unsloth_ref == '' }} type=ref,event=tag type=schedule,pattern=nightly @@ -508,6 +513,9 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # Mirror the studio tag rules (incl. latest=false) so the smoke test + # pulls the tag just published, not an implicit latest=auto :latest. + flavor: latest=false tags: | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.inputs.unsloth_ref == '' }} type=ref,event=tag diff --git a/docker/Dockerfile b/docker/Dockerfile index 5ae04ef038..8b937ab827 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,7 +15,7 @@ # https://developer.nvidia.com/cuda/gpus is covered. # * Unsloth's runtime kernels are Triton, which JIT-compiles per device at first run. # * Anything that DOES need to be source-built (rare on this pin set) compiles -# against TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9;9.0;10.0;10.3;12.0+PTX", +# against TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9;9.0;10.0;12.0+PTX", # covering every current NVIDIA compute capability per # https://developer.nvidia.com/cuda/gpus. # The host GPU is irrelevant for compilation; nvcc emits whatever the arch @@ -65,7 +65,10 @@ ENV DEBIAN_FRONTEND=noninteractive \ # sm_89 Ada L4, L40, L40S, RTX 40-series (amd64 only) # sm_90 Hopper H100, H200, GH200 (Grace-Hopper is arm64) # sm_100 Blackwell DC B100, B200, GB200 (GB200 is arm64) - # sm_103 Blackwell DC B300, GB300 + # sm_103 Blackwell DC B300, GB300 -- covered by sm_100 SASS (see above), + # NOT a separate target here: the bundled CUDA 12.8 nvcc cannot + # compile compute_103 (added in CUDA 12.9), so listing 10.3 would + # break any source / JIT build that honors TORCH_CUDA_ARCH_LIST. # sm_120 Blackwell RTX 50-series, RTX PRO 6000 Blackwell (amd64 only) # sm_121 Blackwell GB10 (DGX Spark) (arm64 only) # +PTX on the highest lets future arch revisions run via JIT-PTX. @@ -73,7 +76,7 @@ ENV DEBIAN_FRONTEND=noninteractive \ # don't exist on the build host, and any extras that aren't relevant for # the target just bloat compile time slightly (not size, since we don't # source-build any extension at install time on the pin set). - TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9;9.0;10.0;10.3;12.0+PTX" \ + TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9;9.0;10.0;12.0+PTX" \ MAX_JOBS=4 \ CUDA_HOME=/usr/local/cuda \ # Build-host-independence guards. The build must NEVER introspect a GPU, @@ -502,9 +505,11 @@ ENV DEBIAN_FRONTEND=noninteractive \ TRITON_CACHE_DIR=/workspace/.cache/triton \ # Keep the arch list visible at runtime in case the user source-builds anything # extra inside the container (e.g. a custom CUDA op). Same list as the builder - # stage so a `pip install some-cuda-ext` inside the container gets a SASS - # blob that covers every supported arch. - TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9;9.0;10.0;10.3;12.0+PTX" + # stage so a `pip install some-cuda-ext` inside the container gets a SASS blob + # that covers every supported arch. 10.3 (B300) is intentionally omitted: it + # runs sm_100 SASS, and the bundled CUDA 12.8 nvcc cannot compile compute_103 + # (added in CUDA 12.9), so listing it would fail any such in-container build. + TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9;9.0;10.0;12.0+PTX" # zstd: the official Ollama notebooks run `curl ollama.com/install.sh | sh` # inside the container, and that installer extracts a zstd tarball -- without diff --git a/docker/unsloth_pip_shim.py b/docker/unsloth_pip_shim.py index 635de00fe7..42c774f401 100644 --- a/docker/unsloth_pip_shim.py +++ b/docker/unsloth_pip_shim.py @@ -71,6 +71,10 @@ _VALUE_FLAGS = { "--abi", "--implementation", } +# Of those value-flags, the ones whose VALUE is itself an install target: a +# requirements file pulls real requirements. An index-url / find-links / +# constraint / target value is an option, not something to install. +_REQ_FILE_FLAGS = {"-r", "--requirement"} def _canon(token): @@ -116,19 +120,30 @@ def main(): head, tail = argv[: i + 1], argv[i + 1 :] keep_args, dropped, recorded = [], [], None + has_target = False skip_next = False + prev_flag = None for tok in tail: if skip_next: keep_args.append(tok) + # The value of -r/--requirement pulls real requirements (a target); + # the value of an index-url / find-links / constraint / etc. flag is + # an option, not something to install. + if prev_flag in _REQ_FILE_FLAGS: + has_target = True skip_next = False + prev_flag = None continue if tok in _VALUE_FLAGS: keep_args.append(tok) skip_next = True + prev_flag = tok continue name = _canon(tok) if name is None: - keep_args.append(tok) # flag / url / path + keep_args.append(tok) # bare flag, or a positional url / path / vcs + if not tok.startswith("-"): + has_target = True # standalone . / ./pkg / git+... / *.whl continue if name == "transformers": v = _version_pin(tok) @@ -140,6 +155,7 @@ def main(): dropped.append(tok) continue keep_args.append(tok) + has_target = True # a kept package spec if recorded: try: @@ -155,12 +171,12 @@ def main(): if dropped: print("[unsloth-nb] kept baked versions, skipped: " + " ".join(dropped)) - # Anything left to actually install? Count any non-flag token as a target, - # not just tokens with a canonical pkg name: editable / local / url / vcs - # installs (`-e .`, `.`, `git+https://...`, a wheel URL) carry no canonical - # name but must still run, and a `-r`/`-c` file pulls in real requirements. - has_install_target = any(not t.startswith("-") for t in keep_args) - if not has_install_target: + # Anything left to actually install? `has_target` was set during the scan for + # a kept package spec, a positional url / path / vcs / editable target, or a + # -r/--requirement file. A line carrying only baked packages plus option flags + # (e.g. `--extra-index-url torch`) leaves no target, so no-op instead of + # exec'ing a bare `pip install --extra-index-url ` that would fail. + if not has_target: print("[unsloth-nb] nothing to install after keeping the baked stack; ok.") return cmd = [REAL[tool]] + head + keep_args diff --git a/docker/unsloth_studio_update.sh b/docker/unsloth_studio_update.sh index fb888ddd59..287d805ab8 100755 --- a/docker/unsloth_studio_update.sh +++ b/docker/unsloth_studio_update.sh @@ -23,6 +23,7 @@ set -euo pipefail STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-/opt/unsloth-studio}" REF="" +ZOO_REF="" NO_DEPS="--no-deps" RESTART=1 PACKAGES="unsloth unsloth_zoo" @@ -32,6 +33,7 @@ usage() { sed -n '2,21p' "$0"; } while [ $# -gt 0 ]; do case "$1" in --ref) REF="$2"; shift 2;; + --zoo-ref) ZOO_REF="$2"; shift 2;; --with-deps) NO_DEPS=""; shift;; --no-restart) RESTART=0; shift;; --packages) PACKAGES="$2"; shift 2;; @@ -63,8 +65,23 @@ echo "[studio-update] before: unsloth $(version_of)" # (or any branch/tag/sha); otherwise take the latest PyPI release. if [ -n "$REF" ]; then SPECS="git+https://github.com/unslothai/unsloth.git@${REF}#egg=unsloth" - SPECS="$SPECS git+https://github.com/unslothai/unsloth-zoo.git@${REF}#egg=unsloth_zoo" - echo "[studio-update] installing from git @${REF}" + # unsloth-zoo does NOT track unsloth's tags/SHAs (its release cadence differs; + # the publish workflow resolves the zoo ref separately for the same reason). + # Use --zoo-ref if given; else use the unsloth ref only when the zoo repo + # actually has it, falling back to main so `--ref ` does not fail + # on a tag/SHA that simply does not exist in unsloth-zoo. + _zoo_ref="$ZOO_REF" + if [ -z "$_zoo_ref" ]; then + if git ls-remote --exit-code https://github.com/unslothai/unsloth-zoo.git \ + "$REF" >/dev/null 2>&1; then + _zoo_ref="$REF" + else + _zoo_ref="main" + echo "[studio-update] unsloth-zoo has no ref '${REF}'; using zoo main" + fi + fi + SPECS="$SPECS git+https://github.com/unslothai/unsloth-zoo.git@${_zoo_ref}#egg=unsloth_zoo" + echo "[studio-update] installing from git: unsloth @${REF}, unsloth-zoo @${_zoo_ref}" else SPECS="$PACKAGES" echo "[studio-update] installing latest release of: $PACKAGES" diff --git a/docker/unsloth_sync_notebooks.sh b/docker/unsloth_sync_notebooks.sh index 73db179e34..2727e4427b 100644 --- a/docker/unsloth_sync_notebooks.sh +++ b/docker/unsloth_sync_notebooks.sh @@ -78,23 +78,27 @@ record_state() { # 1) First-boot populate from the baked template (instant, works offline). if [ ! -f "$STATE" ]; then + : > "$STATE.tmp" 2>/dev/null || true ( cd "$TEMPLATE" && find . -type f -print0 ) | while IFS= read -r -d '' rel; do rel="${rel#./}" case "$rel" in .unsloth_template_commit) continue ;; esac mkdir -p "$DEST/$(dirname "$rel")" 2>/dev/null || true # A pre-existing file at this path (bind-mounted or hand-created before - # the first boot) is user data: never clobber it. Only lay down the baked - # template when the path is empty or already byte-identical to it. The - # refresh path below has the same ownership rule; this keeps first boot - # symmetric so a mounted notebook survives the very first start too. + # the first boot) is user data: never clobber it, and -- crucially -- do + # NOT record it in the sync state. If it were recorded, the GitHub refresh + # below would see its hash match the recorded hash, treat it as pristine + # and overwrite it with upstream. Only files we actually lay down (or that + # are already byte-identical to the template) are recorded as managed. if [ -e "$DEST/$rel" ] \ && [ "$(hash_of "$DEST/$rel")" != "$(hash_of "$TEMPLATE/$rel")" ]; then echo "[unsloth-nb] kept existing user file: $DEST/$rel" continue fi - cp -a "$TEMPLATE/$rel" "$DEST/$rel" 2>/dev/null || true + if cp -a "$TEMPLATE/$rel" "$DEST/$rel" 2>/dev/null; then + printf '%s %s\n' "$(hash_of "$DEST/$rel")" "$rel" >> "$STATE.tmp" + fi done - record_state + mv "$STATE.tmp" "$STATE" 2>/dev/null || rm -f "$STATE.tmp" cp -a "$TEMPLATE/.unsloth_template_commit" "$SYNCED" 2>/dev/null || true echo "[unsloth-nb] notebooks ready at $DEST" fi @@ -158,6 +162,13 @@ while IFS= read -r -d '' f; do dst="$DEST/$rel" if [ -e "$dst" ]; then rec="${LAST[$rel]:-}" + if [ -z "$rec" ]; then + # File exists in DEST but the sync state never recorded it -> it is a + # pre-existing user / bind-mounted file. Treat it as user-owned: keep + # it and do not adopt it into the state (so it stays protected). + kept=$((kept + 1)) + continue + fi if [ -n "$rec" ] && [ "$(hash_of "$dst")" != "$rec" ]; then # User changed this file since we wrote it -> keep theirs, keep marker. printf '%s %s\n' "$rec" "$rel" >> "$TMPSTATE"