unsloth/docker/unsloth_llama_update.sh
Daniel Han 7606081ef6 docker: add unsloth-llama-update for in-place llama.cpp prebuilt updates
Parity with unsloth-studio-update: update the baked llama.cpp prebuilt in a
running container without pulling a new image.

    docker exec <container> unsloth-llama-update           # latest release
    docker exec <container> unsloth-llama-update --check   # report only

It reuses the build-time fetcher (fetch_llama_prebuilt.py, now baked at
/usr/local/lib/unsloth) rather than the host-probing installer behind the
in-app banner. The fetcher resolves the latest release via the GitHub
/releases/latest redirect (no API token, not rate-limited) and installs the
portable CUDA bundle that runs on CPU and every supported GPU, so it works the
same in a CPU-only or a --gpus container. The installer path, by contrast,
scans the GitHub API (rate-limited to 403 in practice) and probes the host GPU,
which falls back to a slow source build in a container started without --gpus.

The fetch lands in a sibling temp dir on the same filesystem and is swapped in
with an atomic rename; on any failure the existing install is left untouched.
The Studio ownership marker is preserved across the swap. Verified end to end
in a CPU-only container: b9596-mix-e6f2453 -> b9773-mix-1f1aaa4.
2026-06-24 06:41:33 +00:00

121 lines
4.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# Update the baked llama.cpp prebuilt in place, inside a running container,
# without pulling a new image. Downloads the newest portable llama.cpp bundle
# (the same target-pinned, sha256-verified bundle the image is built with) and
# atomically swaps it into $UNSLOTH_LLAMA_CPP_PATH, so the next GGUF export /
# model load uses it.
#
# docker exec <container> unsloth-llama-update # latest release
# docker exec <container> unsloth-llama-update --tag b9773-mix-1f1aaa4
# docker exec <container> unsloth-llama-update --check # report only, no download
#
# This reuses the build-time fetcher, which resolves the latest release via the
# GitHub /releases/latest redirect (no API token, not rate-limited) and installs
# the portable CUDA bundle that runs on CPU and every supported GPU. That makes
# it work the same in a CPU-only or a --gpus container, unlike the host-probing
# installer behind the in-app banner.
#
# Persistence: the swap lands in the container's writable layer (survives
# docker restart). To keep it across a full recreate, mount the prebuilt dir on
# a named volume: -v unsloth_llama:/opt/unsloth/llama.cpp
set -euo pipefail
INSTALL_DIR="${UNSLOTH_LLAMA_CPP_PATH:-/opt/unsloth/llama.cpp}"
STUDIO_HOME="${UNSLOTH_STUDIO_HOME:-/opt/unsloth-studio}"
FETCHER="${UNSLOTH_LLAMA_FETCHER:-/usr/local/lib/unsloth/fetch_llama_prebuilt.py}"
REPO="unslothai/llama.cpp"
TAG="latest"
CHECK_ONLY=0
usage() { sed -n '2,24p' "$0"; }
while [ $# -gt 0 ]; do
case "$1" in
--tag) TAG="$2"; shift 2;;
--install-dir) INSTALL_DIR="$2"; shift 2;;
--check) CHECK_ONLY=1; shift;;
-h|--help) usage; exit 0;;
*) echo "unsloth-llama-update: unknown argument: $1" >&2; usage; exit 2;;
esac
done
[ -f "$FETCHER" ] || { echo "unsloth-llama-update: fetcher not found at $FETCHER" >&2; exit 1; }
# Any python works (the fetcher is stdlib-only); prefer the Studio venv, then base.
PY=""
for cand in \
"$STUDIO_HOME/unsloth_studio/bin/python" \
/opt/unsloth-venv/bin/python \
python3 python; do
command -v "$cand" >/dev/null 2>&1 && { PY="$cand"; break; }
[ -x "$cand" ] && { PY="$cand"; break; }
done
[ -n "$PY" ] || { echo "unsloth-llama-update: no python found" >&2; exit 1; }
# amd64 -> linux-x64-cuda12 portable; arm64 -> linux-arm64-cuda13 portable.
case "$(uname -m)" in
x86_64|amd64) ARCH="amd64";;
aarch64|arm64) ARCH="arm64";;
*) echo "unsloth-llama-update: unsupported arch $(uname -m)" >&2; exit 1;;
esac
installed_tag() {
"$PY" - "$INSTALL_DIR" <<'PY' 2>/dev/null || echo "unknown"
import json, os, sys
p = os.path.join(sys.argv[1], "UNSLOTH_PREBUILT_INFO.json")
try:
d = json.load(open(p)); print(d.get("tag") or d.get("release_tag") or d.get("upstream_tag") or "unknown")
except Exception:
print("unknown")
PY
}
resolve_latest() {
"$PY" - "$FETCHER" "$REPO" <<'PY' 2>/dev/null || echo ""
import importlib.util, sys
spec = importlib.util.spec_from_file_location("flp", sys.argv[1])
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(m.resolve_latest_tag(sys.argv[2]))
PY
}
CUR="$(installed_tag)"
echo "[llama-update] install dir: $INSTALL_DIR"
echo "[llama-update] installed: $CUR"
if [ "$CHECK_ONLY" = "1" ]; then
LATEST="$(resolve_latest)"
echo "[llama-update] latest: ${LATEST:-unknown}"
if [ -n "$LATEST" ] && [ "$LATEST" != "$CUR" ]; then
echo "[llama-update] an update is available (run without --check to apply)"
else
echo "[llama-update] up to date"
fi
exit 0
fi
# Fetch into a sibling temp dir (same filesystem as INSTALL_DIR, so the swap is
# an atomic rename), then swap. On any failure the existing install is untouched.
parent="$(dirname "$INSTALL_DIR")"
work="$(mktemp -d "$parent/.llamaupd.XXXXXX")"
trap 'rm -rf "$work" "${INSTALL_DIR}.old.$$" 2>/dev/null || true' EXIT
new="$work/llama.cpp"
echo "[llama-update] fetching llama.cpp '$TAG' ($ARCH portable) ..."
"$PY" "$FETCHER" "$TAG" "$ARCH" "$new"
# Preserve the Studio ownership marker so setup.sh keeps recognising the dir.
[ -e "$INSTALL_DIR/.unsloth-studio-owned" ] && touch "$new/.unsloth-studio-owned"
echo "[llama-update] swapping into place ..."
mv "$INSTALL_DIR" "${INSTALL_DIR}.old.$$"
if mv "$new" "$INSTALL_DIR"; then
rm -rf "${INSTALL_DIR}.old.$$"
else
echo "[llama-update] swap failed; restoring previous install" >&2
mv "${INSTALL_DIR}.old.$$" "$INSTALL_DIR"
exit 1
fi
echo "[llama-update] installed now: $(installed_tag)"
echo "[llama-update] done (reload your model / re-run export to use it)"