docker: fix notebook pip-shim drops, first-boot overwrite, base :latest tag, arm64 decord
- pip shim: count editable/local/url/vcs targets (-e ., ., git+https, wheel URLs) as install targets, not just canonical package names, so they are no longer silently skipped inside notebooks - notebook sync: never overwrite a pre-existing user notebook on first boot (match the refresh path's ownership rule); skip .unsloth_sync_state.tmp when recording state so it is not tracked as a managed file - docker-publish: set flavor latest=false on the base image metadata so a v* tag push cannot publish :latest from the base image (the Studio image owns it) - notebook deps: pin to tested versions and install decord on its own, hard on amd64 and fail-soft on arm64 (no aarch64 wheel) so the arm64 base build works
This commit is contained in:
parent
52067fb0af
commit
8f693c6207
4 changed files with 45 additions and 8 deletions
7
.github/workflows/docker-publish.yml
vendored
7
.github/workflows/docker-publish.yml
vendored
|
|
@ -232,6 +232,10 @@ jobs:
|
|||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# The base image must NEVER claim :latest. metadata-action defaults to
|
||||
# flavor latest=auto, which would tag :latest on a v* (semver) tag push
|
||||
# and collide with the Studio image that legitimately owns :latest.
|
||||
flavor: latest=false
|
||||
tags: |
|
||||
# The lean training image publishes under the base- prefix; the
|
||||
# full Studio image (build-studio/merge-studio below) owns
|
||||
|
|
@ -437,6 +441,9 @@ jobs:
|
|||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# Keep the base image off :latest here too (this recomputes the same
|
||||
# tag list the merge step pushed, so the smoke test pulls the right ref).
|
||||
flavor: latest=false
|
||||
tags: |
|
||||
type=raw,value=base,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.inputs.unsloth_ref == '' }}
|
||||
type=ref,event=tag,prefix=base-
|
||||
|
|
|
|||
|
|
@ -285,18 +285,35 @@ RUN set -eux \
|
|||
# omegaconf TTS families + both NeMo-Gym RL notebooks' config objects
|
||||
# einx TTS codec tensor-rearrange (Llasa / Oute / Spark TTS)
|
||||
# librosa Whisper audio feature extraction (pairs with soundfile + torchcodec)
|
||||
# decord ERNIE-VL vision notebook video decode
|
||||
# ftfy Oute TTS text normalisation
|
||||
# decord (ERNIE-VL video decode) is installed separately below: it ships no
|
||||
# aarch64 wheel, so a hard install here would break the arm64 build.
|
||||
# librosa pulls numba/soxr/audioread; numba is already pinned >=0.65 (numpy 2.4
|
||||
# compatible) by the vLLM pass, so the resolve must NOT move torch/numpy/numba --
|
||||
# the assertion below fails the build loudly if it did.
|
||||
# Pinned (==) to the resolved, tested versions for reproducible rebuilds -- the
|
||||
# same convention as the cu128 core (torch/torchvision/torchaudio). Bump these
|
||||
# deliberately, not silently on the next build. Transitive deps of these are
|
||||
# captured by the full venv lockfile (docker/freeze.sh -> requirements.lock.txt).
|
||||
RUN ${VENV}/bin/uv pip install \
|
||||
--python ${VENV}/bin/python \
|
||||
jupyterlab notebook ipywidgets matplotlib \
|
||||
soundfile evaluate jiwer tensorboard langid easydict protobuf \
|
||||
omegaconf einx librosa decord ftfy \
|
||||
"jupyterlab==4.6.0" "notebook==7.6.0" "ipywidgets==8.1.8" "matplotlib==3.11.0" \
|
||||
"soundfile==0.14.0" "evaluate==0.4.6" "jiwer==4.0.0" "tensorboard==2.20.0" \
|
||||
"langid==1.1.6" "easydict==1.13" "protobuf==6.33.6" \
|
||||
"omegaconf==2.3.1" "einx==0.4.3" "librosa==0.11.0" "ftfy==6.3.1" \
|
||||
&& ${VENV}/bin/python -c "import torch, numpy, numba; from packaging.version import Version; assert torch.__version__.startswith('2.10.0'), torch.__version__; assert Version(numpy.__version__) >= Version('2.3'), numpy.__version__; assert Version(numba.__version__) >= Version('0.65'), numba.__version__; print('notebook-deps pins OK:', torch.__version__, numpy.__version__, numba.__version__)"
|
||||
|
||||
# decord (ERNIE-VL video decode) publishes wheels only for x86_64 / win_amd64.
|
||||
# Install it on its own: HARD on amd64 (a missing/incompatible wheel is a real
|
||||
# regression there and must fail the build), fail-soft on arm64/other (no wheel
|
||||
# exists, so drop the ERNIE-VL video path rather than break the image build).
|
||||
RUN if [ "${TARGETARCH:-amd64}" = "amd64" ]; then \
|
||||
${VENV}/bin/uv pip install --python ${VENV}/bin/python "decord==0.6.0"; \
|
||||
else \
|
||||
${VENV}/bin/uv pip install --python ${VENV}/bin/python "decord==0.6.0" \
|
||||
|| echo ">> decord skipped (no matching wheel for ${TARGETARCH:-}); ERNIE-VL video decode unavailable"; \
|
||||
fi
|
||||
|
||||
# Audio decode out of the box: the TTS/STT notebooks feed datasets' Audio
|
||||
# features, which decode through torchcodec. Three traps, all defended:
|
||||
# * version pairing: torchcodec 0.10 pairs with torch 2.10 (newer builds
|
||||
|
|
|
|||
|
|
@ -155,9 +155,12 @@ def main():
|
|||
if dropped:
|
||||
print("[unsloth-nb] kept baked versions, skipped: " + " ".join(dropped))
|
||||
|
||||
# Anything left to actually install? (a requirement, not just flags)
|
||||
real_reqs = [t for t in keep_args if _canon(t)]
|
||||
if not real_reqs:
|
||||
# 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:
|
||||
print("[unsloth-nb] nothing to install after keeping the baked stack; ok.")
|
||||
return
|
||||
cmd = [REAL[tool]] + head + keep_args
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ record_state() {
|
|||
( cd "$DEST" && find . -type f -print0 ) | while IFS= read -r -d '' rel; do
|
||||
rel="${rel#./}"
|
||||
case "$rel" in
|
||||
.unsloth_sync_state|.unsloth_sync_commit) continue ;;
|
||||
.unsloth_sync_state|.unsloth_sync_state.tmp|.unsloth_sync_commit) continue ;;
|
||||
esac
|
||||
printf '%s %s\n' "$(hash_of "$DEST/$rel")" "$rel" >> "$STATE.tmp"
|
||||
done
|
||||
|
|
@ -82,6 +82,16 @@ if [ ! -f "$STATE" ]; then
|
|||
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.
|
||||
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
|
||||
done
|
||||
record_state
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue