docker: address review round 2 (CI ref freeze, Studio NVRTC amd64, pip-shim edges)

docker-publish.yml: freeze the requested unsloth ref to one sha in the prepare
job before the matrix fans out. UNSLOTH_REF / UNSLOTH_STUDIO_REF were raw
expressions re-evaluated per base arch leg and in the Studio build, so a mutable
branch (the workflow_dispatch default unsloth_ref=main) advancing during the run
could bake different unsloth commits under one manifest. Resolve once (same
precedence: dispatch input, else pushed tag, else triggering sha, else main;
ls-remote a branch/tag to a sha, mirroring the zoo/notebooks steps) and read
needs.prepare.outputs.unsloth_ref everywhere.

Dockerfile.studio: run the Studio venv NVRTC cu13 swap on both arches, not arm64
only. amd64 sm_103 (B300/GB300) needs cu13 NVRTC just as arm64 sm_121 does, and
the CUDA dedup never touches cuda_nvrtc, so an amd64 Studio venv would otherwise
keep its bundled cu12.8 libnvrtc and fail NVRTC/jiterator JIT on compute_103. The
base cu13 layer installs cuda-nvrtc-13-0 on both arches, so the target .so.13
exists here regardless of TARGETARCH.

unsloth_pip_shim.py: close three ways a protected package slipped past _KEEP.
Treat -e/--editable as a value-taking flag paired with its target and drop both
when the target is protected (was leaving a dangling -e that failed the cell);
filter -P/--upgrade-package values through _KEEP (a named baked package could be
refreshed while installing another target); and parse the PEP 427 distribution
name out of a wheel URL/path so a bare `pip install https://.../torch-...whl`
drops instead of reinstalling the baked torch. Non-protected editables, upgrade
selectors, and wheels are unchanged. Adds tests/python/test_unsloth_pip_shim.py
(18 regression tests, exec captured via a patched os.execv).
This commit is contained in:
Daniel Han 2026-07-08 05:02:10 +00:00
commit d4dc8b6391
4 changed files with 369 additions and 25 deletions

View file

@ -89,10 +89,12 @@ jobs:
contents: read
outputs:
llama_tag: ${{ steps.llama.outputs.tag }}
# One zoo ref + one notebooks commit, resolved here so BOTH arch legs of
# the base build (and the Studio build) bake the identical bits. Resolving
# them per-leg would let upstream advance between the amd64 and arm64
# builds, putting different content under one published tag.
# One unsloth ref + one zoo ref + one notebooks commit, resolved here so
# BOTH arch legs of the base build AND the Studio build bake the identical
# bits. Resolving them per-leg would let upstream advance between the amd64
# and arm64 builds (or between the base and Studio builds), putting
# different content under one published tag.
unsloth_ref: ${{ steps.unsloth_ref.outputs.ref }}
zoo_ref: ${{ steps.zoo_ref.outputs.ref }}
notebooks_commit: ${{ steps.notebooks.outputs.commit }}
steps:
@ -110,6 +112,36 @@ jobs:
echo "tag=${TAG:-latest}" >> "$GITHUB_OUTPUT"
echo "llama.cpp prebuilt tag: ${TAG:-latest}"
# Freeze the requested unsloth ref to ONE concrete sha before the matrix
# fans out, so both base arch legs AND the Studio build bake the identical
# unsloth commit even when the requested ref is a mutable branch (the
# workflow_dispatch default is unsloth_ref=main) that advances during the
# ~4h base + Studio run. Same requested-ref precedence the inline build-arg
# used: the dispatch input wins (default main), else the pushed tag, else
# the triggering commit sha, else main. A 40-char sha (branch/schedule
# push) is already frozen; a branch/tag is resolved via ls-remote, exactly
# like the zoo and notebooks steps, falling back to the bare ref on a
# lookup miss so the Dockerfile can still fetch it by name.
- name: Resolve unsloth ref
id: unsloth_ref
env:
INPUT_REF: ${{ github.event.inputs.unsloth_ref }}
TAG_REF: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }}
PUSH_SHA: ${{ github.sha }}
run: |
REF="$INPUT_REF"
[ -n "$REF" ] || REF="$TAG_REF"
[ -n "$REF" ] || REF="$PUSH_SHA"
REF="${REF:-main}"
if printf '%s' "$REF" | grep -Eq '^[0-9a-f]{40}$'; then
SHA="$REF"
else
SHA="$(git ls-remote https://github.com/unslothai/unsloth "$REF" | awk 'NR==1{print $1}')"
[ -n "$SHA" ] || SHA="$REF"
fi
echo "ref=${SHA}" >> "$GITHUB_OUTPUT"
echo "unsloth ref: ${SHA}"
# Mirror the unsloth tag into the zoo ONLY when that tag actually exists
# there. unsloth's v* tags are Studio releases the zoo never cuts (the zoo
# repo currently has no tags at all), so blindly mirroring github.ref_name
@ -242,10 +274,12 @@ jobs:
# NOTE: keep prose OUT of build-args -- docker/build-push-action
# forwards every non-empty line verbatim, so a leading-# line would be
# passed as a bogus --build-arg. Explanations live here instead:
# UNSLOTH_REF: workflow-dispatch honours the explicit input; tag
# pushes bake the tag's source ref (e.g. v1.2.3) so the published
# image actually contains that release; branch + scheduled runs bake
# the triggering commit SHA; any other event falls back to main.
# UNSLOTH_REF (from the prepare job): resolved to ONE sha before the
# matrix fans out, so both arch legs and the Studio build bake the
# identical unsloth commit even if a mutable branch (dispatch's
# unsloth_ref=main default) advances mid-run. Same requested-ref
# precedence as before: dispatch input, else the pushed tag, else
# the triggering commit sha, else main.
# UNSLOTH_ZOO_REF (from the prepare job): explicit dispatch input,
# else the pushed tag IF the zoo repo has it, else main -- a branch
# SHA does not exist in the zoo repo. Resolved once in `prepare` and
@ -257,7 +291,7 @@ jobs:
CUDA_VERSION=12.8.1
UBUNTU_VERSION=24.04
PYTHON_VERSION=3.12
UNSLOTH_REF=${{ github.event.inputs.unsloth_ref || (startsWith(github.ref, 'refs/tags/') && github.ref_name) || github.sha || 'main' }}
UNSLOTH_REF=${{ needs.prepare.outputs.unsloth_ref }}
UNSLOTH_ZOO_REF=${{ needs.prepare.outputs.zoo_ref }}
LLAMA_PREBUILT_TAG=${{ needs.prepare.outputs.llama_tag }}
UNSLOTH_NOTEBOOKS_REF=${{ needs.prepare.outputs.notebooks_commit }}
@ -430,15 +464,16 @@ jobs:
cache-from: type=gha,scope=studio-${{ matrix.platform }}
cache-to: type=gha,scope=studio-${{ matrix.platform }},mode=min
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
# UNSLOTH_STUDIO_REF mirrors the base job's UNSLOTH_REF resolution so the
# Studio tree matches the unsloth baked into the base venv.
# UNSLOTH_STUDIO_REF is the SAME resolved unsloth sha the base build
# baked (needs.prepare.outputs.unsloth_ref), so the Studio tree matches
# the unsloth in the base venv even if the branch moved mid-run.
# UNSLOTH_STUDIO_ZOO_REF is the SAME resolved zoo ref the base build
# baked, so install.sh --local overlays the Studio venv with that zoo
# instead of always tracking main. (Prose stays out of build-args --
# forwarded lines must be KEY=VALUE only.)
build-args: |
BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.merge.outputs.digest }}
UNSLOTH_STUDIO_REF=${{ github.event.inputs.unsloth_ref || (startsWith(github.ref, 'refs/tags/') && github.ref_name) || github.sha || 'main' }}
UNSLOTH_STUDIO_REF=${{ needs.prepare.outputs.unsloth_ref }}
UNSLOTH_STUDIO_ZOO_REF=${{ needs.prepare.outputs.zoo_ref }}
- name: Export digest