docker: pre-load unslothai/notebooks into JupyterLab, edit-safe refresh
JupyterLab now opens with the unslothai/notebooks collection already present, so people can open and run a notebook directly without a git clone or wget. - Bake the repo into the image as a read-only template at /opt/unsloth-notebooks (~206MB, .git stripped, build commit recorded). Inherited by the studio image. - On boot the entrypoint populates /workspace/unsloth-notebooks from the template (instant, works offline) and best-effort refreshes from GitHub, but only when upstream has actually advanced (cheap git ls-remote gate, no download otherwise). - The user's edits always win. We record the content hash of every file we write; on refresh a file whose hash differs from what we last wrote is treated as user-modified and is left untouched, so the refresh only updates files the user has not changed and adds new ones. It never overwrites an edited notebook and never produces merge conflicts. Verified: an edited notebook stays the user's version across repeated upstream changes. - Fully best-effort and gated: UNSLOTH_SKIP_NOTEBOOK_SYNC=1 disables it, UNSLOTH_SKIP_NOTEBOOK_REFRESH=1 keeps the baked copy and never hits the network. Offline boots keep what is there and never error. base 18.45 -> 18.67GB, studio 24.88 -> 25.10GB (+~206MB baked notebooks).
This commit is contained in:
parent
448e2251e6
commit
d0d5f3c27f
4 changed files with 143 additions and 4 deletions
|
|
@ -9,3 +9,4 @@
|
|||
!unsloth_pip_shim.py
|
||||
!unsloth_ipython_startup.py
|
||||
!unsloth_run.py
|
||||
!unsloth_sync_notebooks.sh
|
||||
|
|
|
|||
|
|
@ -642,22 +642,36 @@ RUN mkdir -p ${HF_HOME} ${TRITON_CACHE_DIR}
|
|||
# * unsloth-run: headless `unsloth-run <notebook|url>` that auto-picks the
|
||||
# sidecar and executes every cell -- the robust driven path.
|
||||
# ---------------------------------------------------------------------------
|
||||
COPY unsloth_nb_compat.py unsloth_pip_shim.py unsloth_ipython_startup.py unsloth_run.py /opt/unsloth-nb/
|
||||
COPY unsloth_nb_compat.py unsloth_pip_shim.py unsloth_ipython_startup.py unsloth_run.py unsloth_sync_notebooks.sh /opt/unsloth-nb/
|
||||
RUN set -eux \
|
||||
&& SP=/opt/unsloth-venv/lib/python${PYTHON_VERSION}/site-packages \
|
||||
&& cp /opt/unsloth-nb/unsloth_nb_compat.py "$SP/unsloth_nb_compat.py" \
|
||||
&& chmod +x /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/unsloth_run.py \
|
||||
&& chmod +x /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/unsloth_run.py /opt/unsloth-nb/unsloth_sync_notebooks.sh \
|
||||
&& mkdir -p /opt/unsloth-nb/bin \
|
||||
&& for t in pip pip3 uv; do ln -sf /opt/unsloth-nb/unsloth_pip_shim.py /opt/unsloth-nb/bin/$t; done \
|
||||
&& ln -sf /opt/unsloth-nb/unsloth_run.py /usr/local/bin/unsloth-run \
|
||||
&& ln -sf /opt/unsloth-nb/unsloth_sync_notebooks.sh /usr/local/bin/unsloth-sync-notebooks \
|
||||
&& mkdir -p /root/.ipython/profile_default/startup \
|
||||
&& cp /opt/unsloth-nb/unsloth_ipython_startup.py /root/.ipython/profile_default/startup/00-unsloth-nb.py \
|
||||
&& /opt/unsloth-venv/bin/python -c "import sys, glob; sys.path.insert(0, '$SP'); import unsloth_nb_compat; print('nb-compat OK; baked sidecars:', sorted(glob.glob('/opt/unsloth-venv/tf-sidecars/t_*')))"
|
||||
# Shim dir AHEAD of the venv bin so `!pip`/`!uv` resolve to the shim, not the real tool.
|
||||
ENV PATH=/opt/unsloth-nb/bin:${PATH}
|
||||
|
||||
# JupyterLab lives in the venv (see builder stage). Persistent notebooks
|
||||
# should be bind-mounted onto /workspace.
|
||||
# Pre-clone unslothai/notebooks so JupyterLab opens with the notebooks already
|
||||
# present (no git clone or wget needed). Baked here as a READ-ONLY template
|
||||
# (~206MB, .git stripped); on boot the entrypoint copies it to
|
||||
# /workspace/unsloth-notebooks and best-effort refreshes from GitHub when
|
||||
# upstream has advanced, never overwriting a notebook the user has edited (see
|
||||
# unsloth_sync_notebooks.sh). Inherited as-is by the studio image (FROM base).
|
||||
RUN set -eux \
|
||||
&& git clone --depth 1 https://github.com/unslothai/notebooks /opt/unsloth-notebooks \
|
||||
&& git -C /opt/unsloth-notebooks rev-parse HEAD > /opt/unsloth-notebooks/.unsloth_template_commit \
|
||||
&& rm -rf /opt/unsloth-notebooks/.git \
|
||||
&& du -sh /opt/unsloth-notebooks
|
||||
|
||||
# JupyterLab lives in the venv (see builder stage). The unslothai/notebooks
|
||||
# collection is pre-populated into /workspace/unsloth-notebooks on boot; mount a
|
||||
# volume on /workspace to persist your own notebooks and outputs across runs.
|
||||
EXPOSE 8888
|
||||
|
||||
COPY smoke_test.py /workspace/smoke_test.py
|
||||
|
|
|
|||
|
|
@ -25,7 +25,18 @@ if [[ -x /usr/local/cuda-13.0/bin/ptxas ]] && [[ -z "${TRITON_PTXAS_PATH:-}" ]];
|
|||
export TRITON_PTXAS_PATH=/usr/local/cuda-13.0/bin/ptxas
|
||||
fi
|
||||
|
||||
# Make the unslothai/notebooks collection available under /workspace before the
|
||||
# user command runs (JupyterLab, unsloth-run, or a shell). Best-effort: it is
|
||||
# fully gated by UNSLOTH_SKIP_NOTEBOOK_SYNC and never blocks or fails the
|
||||
# container (see unsloth_sync_notebooks.sh).
|
||||
sync_notebooks() {
|
||||
if [[ -x /usr/local/bin/unsloth-sync-notebooks ]]; then
|
||||
/usr/local/bin/unsloth-sync-notebooks || true
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${UNSLOTH_SKIP_GPU_CHECK:-0}" == "1" ]]; then
|
||||
sync_notebooks
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
|
|
@ -44,6 +55,7 @@ if [[ "${UNSLOTH_ALLOW_CPU:-0}" == "1" ]]; then
|
|||
if ! command -v nvidia-smi >/dev/null 2>&1 || ! nvidia-smi -L 2>/dev/null | grep -q '^GPU'; then
|
||||
warn "UNSLOTH_ALLOW_CPU=1 and no GPU visible -- continuing on CPU."
|
||||
warn "Training requires an NVIDIA GPU. CPU mode covers Jupyter, GGUF tooling and Studio chat."
|
||||
sync_notebooks
|
||||
exec "$@"
|
||||
fi
|
||||
fi
|
||||
|
|
@ -146,4 +158,5 @@ if major < 8:
|
|||
print(" Unsloth will fall back to fp16. Training works but is slightly slower.")
|
||||
PY
|
||||
|
||||
sync_notebooks
|
||||
exec "$@"
|
||||
|
|
|
|||
111
docker/unsloth_sync_notebooks.sh
Normal file
111
docker/unsloth_sync_notebooks.sh
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
# Populate and refresh /workspace/unsloth-notebooks from unslothai/notebooks.
|
||||
#
|
||||
# The image bakes a read-only template at /opt/unsloth-notebooks so the
|
||||
# notebooks are present in JupyterLab instantly and offline. On boot this script
|
||||
# copies the template into /workspace/unsloth-notebooks (first run only) and then
|
||||
# best-effort refreshes from GitHub when upstream has actually advanced.
|
||||
#
|
||||
# The user's edits ALWAYS win. We remember the content hash of every file we
|
||||
# wrote; on refresh a file whose current hash differs from what we last wrote is
|
||||
# treated as user-modified and is left untouched. So a refresh only updates files
|
||||
# the user has not changed and adds new ones -- it never clobbers an edited
|
||||
# notebook and never produces merge conflicts.
|
||||
#
|
||||
# Opt-out / tuning (all optional):
|
||||
# UNSLOTH_SKIP_NOTEBOOK_SYNC=1 do nothing (no populate, no refresh)
|
||||
# UNSLOTH_SKIP_NOTEBOOK_REFRESH=1 populate from the baked template only;
|
||||
# never touch the network
|
||||
# UNSLOTH_NOTEBOOKS_DIR=<path> target dir (default /workspace/unsloth-notebooks)
|
||||
# UNSLOTH_NOTEBOOKS_REPO=<url> source repo (default unslothai/notebooks)
|
||||
# UNSLOTH_NOTEBOOK_FETCH_TIMEOUT=N seconds for each network op (default 60)
|
||||
set -u
|
||||
|
||||
TEMPLATE="${UNSLOTH_NOTEBOOKS_TEMPLATE:-/opt/unsloth-notebooks}"
|
||||
DEST="${UNSLOTH_NOTEBOOKS_DIR:-/workspace/unsloth-notebooks}"
|
||||
REMOTE="${UNSLOTH_NOTEBOOKS_REPO:-https://github.com/unslothai/notebooks}"
|
||||
STATE="$DEST/.unsloth_sync_state" # "sha256 relpath" of what we last wrote
|
||||
SYNCED="$DEST/.unsloth_sync_commit" # upstream commit we last synced to
|
||||
TIMEOUT="${UNSLOTH_NOTEBOOK_FETCH_TIMEOUT:-60}"
|
||||
|
||||
[ "${UNSLOTH_SKIP_NOTEBOOK_SYNC:-0}" = "1" ] && exit 0
|
||||
[ -d "$TEMPLATE" ] || exit 0
|
||||
mkdir -p "$DEST" 2>/dev/null || exit 0
|
||||
|
||||
hash_of() { sha256sum "$1" 2>/dev/null | cut -d' ' -f1; }
|
||||
|
||||
# Record "<hash> <relpath>" for every file currently under DEST (skip metadata).
|
||||
record_state() {
|
||||
: > "$STATE.tmp" 2>/dev/null || return 0
|
||||
( 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 ;;
|
||||
esac
|
||||
printf '%s %s\n' "$(hash_of "$DEST/$rel")" "$rel" >> "$STATE.tmp"
|
||||
done
|
||||
mv "$STATE.tmp" "$STATE" 2>/dev/null || rm -f "$STATE.tmp"
|
||||
}
|
||||
|
||||
# 1) First-boot populate from the baked template (instant, works offline).
|
||||
if [ ! -f "$STATE" ]; then
|
||||
( 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
|
||||
cp -a "$TEMPLATE/$rel" "$DEST/$rel" 2>/dev/null || true
|
||||
done
|
||||
record_state
|
||||
cp -a "$TEMPLATE/.unsloth_template_commit" "$SYNCED" 2>/dev/null || true
|
||||
echo "[unsloth-nb] notebooks ready at $DEST"
|
||||
fi
|
||||
|
||||
# 2) Best-effort GitHub refresh -- only when upstream has advanced. Edits win.
|
||||
[ "${UNSLOTH_SKIP_NOTEBOOK_REFRESH:-0}" = "1" ] && exit 0
|
||||
command -v git >/dev/null 2>&1 || exit 0
|
||||
command -v sha256sum >/dev/null 2>&1 || exit 0
|
||||
|
||||
last="$(cat "$SYNCED" 2>/dev/null || true)"
|
||||
remote="$(timeout "$TIMEOUT" git ls-remote "$REMOTE" HEAD 2>/dev/null | cut -f1)"
|
||||
[ -z "$remote" ] && exit 0 # offline / unreachable -> keep what we have
|
||||
[ "$remote" = "$last" ] && exit 0 # nothing new since last sync -> done
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
if ! timeout "$TIMEOUT" git clone -q --depth 1 "$REMOTE" "$TMP" 2>/dev/null; then
|
||||
rm -rf "$TMP"; exit 0 # network died mid-way -> keep what we have
|
||||
fi
|
||||
|
||||
declare -A LAST
|
||||
if [ -f "$STATE" ]; then
|
||||
while read -r h p; do
|
||||
[ -n "${p:-}" ] && LAST["$p"]="$h"
|
||||
done < "$STATE"
|
||||
fi
|
||||
|
||||
TMPSTATE="$(mktemp)"
|
||||
updated=0; kept=0
|
||||
while IFS= read -r -d '' f; do
|
||||
rel="${f#"$TMP"/}"
|
||||
case "$rel" in .git|.git/*) continue ;; esac
|
||||
dst="$DEST/$rel"
|
||||
if [ -e "$dst" ]; then
|
||||
rec="${LAST[$rel]:-}"
|
||||
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"
|
||||
kept=$((kept + 1))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
mkdir -p "$(dirname "$dst")" 2>/dev/null || true
|
||||
if cp -a "$f" "$dst" 2>/dev/null; then
|
||||
printf '%s %s\n' "$(hash_of "$dst")" "$rel" >> "$TMPSTATE"
|
||||
updated=$((updated + 1))
|
||||
fi
|
||||
done < <(find "$TMP" -type f -print0)
|
||||
|
||||
mv "$TMPSTATE" "$STATE" 2>/dev/null || rm -f "$TMPSTATE"
|
||||
echo "$remote" > "$SYNCED" 2>/dev/null || true
|
||||
rm -rf "$TMP"
|
||||
echo "[unsloth-nb] notebooks refreshed from GitHub: $updated updated, $kept kept (your edits)"
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue