From b67a3b039f36c3729a417915b377cc949dae74b4 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 19 Jul 2026 15:32:20 +0000 Subject: [PATCH] docker: tighten comments --- .github/workflows/docker-publish.yml | 37 +-- .github/workflows/studio-backend-ci.yml | 12 +- docker/Dockerfile | 248 +++++++----------- docker/Dockerfile.studio | 78 ++---- docker/build.sh | 7 +- docker/entrypoint.sh | 54 ++-- docker/fetch_llama_prebuilt.py | 42 ++- docker/jupyter/install_sloth_stickers.py | 8 +- docker/jupyter/unsloth_branding.py | 25 +- docker/jupyter/unsloth_labext/src/about.ts | 15 +- docker/jupyter/unsloth_labext/src/branding.ts | 18 +- docker/jupyter/unsloth_labext/src/cellNav.ts | 28 +- .../jupyter/unsloth_labext/src/colabTitle.ts | 16 +- docker/jupyter/unsloth_labext/src/index.ts | 12 +- docker/jupyter/unsloth_labext/src/logo.ts | 5 +- .../unsloth_labext/src/outputSelect.ts | 33 +-- docker/jupyter/unsloth_labext/src/splash.ts | 5 +- docker/jupyter/unsloth_labext/src/uiChrome.ts | 10 +- docker/run.sh | 14 +- docker/smoke_test.py | 20 +- docker/studio_launch.sh | 4 +- docker/supervisord.conf | 10 +- docker/unsloth_colab_compat.py | 13 +- docker/unsloth_ipython_startup.py | 21 +- docker/unsloth_llama_update.sh | 13 +- docker/unsloth_nb_compat.py | 5 +- docker/unsloth_nb_content_sig.py | 10 +- docker/unsloth_nb_pip_magic.py | 8 +- docker/unsloth_nb_strip_colab.py | 32 +-- docker/unsloth_nb_view.py | 44 ++-- docker/unsloth_pip_shim.py | 210 ++++++--------- docker/unsloth_run.py | 13 +- docker/unsloth_studio_update.sh | 10 +- docker/unsloth_sync_notebooks.sh | 43 ++- install.ps1 | 5 +- install.sh | 13 +- studio/install_llama_prebuilt.py | 5 +- studio/install_python_stack.py | 10 +- tests/python/test_unsloth_pip_shim.py | 37 +-- tests/sh/test_select_cuda_jit_tools.sh | 8 +- tests/validate_studio_features.py | 3 +- unsloth/_gpu_init.py | 26 +- unsloth/dataprep/synthetic.py | 18 +- unsloth/models/vision.py | 7 +- 44 files changed, 490 insertions(+), 765 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 57675429b3..824425d833 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -61,11 +61,9 @@ permissions: jobs: # --------------------------------------------------------------------------- # Resolve every upstream ref ONCE (llama tag + unsloth/zoo shas + notebooks - # commit) so both arch legs and the Studio build bake identical bits; resolving - # per-leg would let upstream advance mid-run under one tag. A dispatch input - # pins a frozen value; else a branch/tag is frozen to a sha via ls-remote - # (falling back to the bare ref on a miss), and llama "latest" follows the - # /releases/latest redirect (mirrors build.sh). + # commit) so both arch legs and Studio bake identical bits. A dispatch input + # pins a frozen value; else a branch/tag is frozen to a sha via ls-remote, and + # llama "latest" follows the /releases/latest redirect (mirrors build.sh). # --------------------------------------------------------------------------- prepare: runs-on: ubuntu-latest @@ -157,10 +155,9 @@ jobs: echo "notebooks commit: ${SHA}" # --------------------------------------------------------------------------- - # Per-arch build. The matrix fans out two parallel jobs on native runners; - # each pushes a single-arch image by digest (no tag), and the merge job - # stitches the digests into one multi-arch manifest. Canonical build-push-action - # pattern; avoids the "last push wins" race of two jobs pushing the same tag. + # Per-arch build: two parallel jobs on native runners, each pushing a single-arch + # image by digest (no tag); the merge job stitches them into one manifest. Avoids + # the "last push wins" race of two jobs pushing the same tag. # --------------------------------------------------------------------------- build: needs: prepare @@ -185,9 +182,7 @@ jobs: # lacks /usr/share/dotnet), hence `|| true`. - name: Reclaim disk run: | - # Hosted runners keep only ~14-20 GB free -- not enough for the image + - # buildkit state. None of these toolchains are used; paths differ across - # runners, hence `|| true`. + # None of these toolchains are used; paths differ across runners, hence `|| true`. sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ /opt/hostedtoolcache "$AGENT_TOOLSDIRECTORY" \ /usr/local/.ghcup /usr/share/swift \ @@ -359,9 +354,7 @@ jobs: - name: Reclaim disk run: | - # Hosted runners keep only ~14-20 GB free -- not enough for the image + - # buildkit state. None of these toolchains are used; paths differ across - # runners, hence `|| true`. + # None of these toolchains are used; paths differ across runners, hence `|| true`. sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ /opt/hostedtoolcache "$AGENT_TOOLSDIRECTORY" \ /usr/local/.ghcup /usr/share/swift \ @@ -489,16 +482,10 @@ jobs: steps: - uses: actions/checkout@v4 - # Re-compute the tag list deterministically from the same metadata-action - # config the merge job used, so tag/schedule/SHA runs pull the image - # they just published instead of an unrelated tag from a prior run. - # IMPORTANT: keep the `enable=` expressions byte-identical to the - # corresponding merge jobs' gates above. The two used to differ - # (merge: ref + unsloth_ref guard; smoke: is_default_branch only), - # which meant workflow_dispatch with unsloth_ref defaulting to "main" - # would skip :latest on merge but still emit :latest as tags[0] on - # smoke -- so docker pull would fetch a previously-published :latest - # from Docker Hub, not the image just merged. + # Re-compute the tag list from the same metadata-action config the merge job + # used, so a run pulls the image it just published. IMPORTANT: keep the + # `enable=` expressions byte-identical to the merge jobs' gates above, else + # smoke could pull a previously-published :latest instead of the merged image. - name: Resolve published base tag id: meta_base uses: docker/metadata-action@v5 diff --git a/.github/workflows/studio-backend-ci.yml b/.github/workflows/studio-backend-ci.yml index 243e295318..8e4b86f2f1 100644 --- a/.github/workflows/studio-backend-ci.yml +++ b/.github/workflows/studio-backend-ci.yml @@ -30,10 +30,8 @@ on: - 'unsloth/**' - 'unsloth_cli/**' - 'tests/**' - # The "Docker JupyterLab/notebook feature validation" step below runs - # tests/validate_studio_features.py, which checks docker/jupyter (the - # labextension, overrides.json, login branding) and the docker notebook - # helpers. Without docker/** here a docker-only change skips that guard. + # The validate_studio_features.py step below guards docker/jupyter and the + # docker notebook helpers, so a docker-only change must trigger this CI. - 'docker/**' - 'pyproject.toml' - '.github/workflows/studio-backend-ci.yml' @@ -245,8 +243,6 @@ jobs: done - name: Docker JupyterLab/notebook feature validation - # Named validate_studio_features.py (not test_*.py) so pytest's default - # discovery skips it; run it explicitly here so a regression in the - # notebook view, Colab compat, strip, JupyterLab defaults or login - # branding fails CI instead of only when someone runs it by hand. + # Named validate_studio_features.py (not test_*.py) so pytest skips it; + # run explicitly so notebook/Colab/branding regressions fail CI. run: python tests/validate_studio_features.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 9e7bf98892..35083878c1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -51,23 +51,21 @@ ENV DEBIAN_FRONTEND=noninteractive \ # sm_75 Turing (T4, RTX 20xx) | sm_80 A100/A30 | sm_86 A40/RTX 30xx # sm_89 Ada (L4/L40/RTX 40xx) | sm_90 Hopper (H100/H200/GH200) # sm_100 Blackwell DC (B100/B200/GB200) | sm_120 Blackwell (RTX 50xx, RTX PRO 6000) - # sm_103 (B300/GB300) and sm_121 (GB10) are omitted: CUDA 12.8 nvcc cannot - # compile compute_103/121 and sm_100/sm_120 SASS cover them via forward-compat. - # +PTX on the highest lets future revisions JIT. Same list on both arches: - # nvcc emits archs absent from the host, irrelevant extras only cost a little - # compile time (no source builds on this pin set). + # sm_103 (B300/GB300) and sm_121 (GB10) omitted: CUDA 12.8 nvcc can't compile + # them; sm_100/sm_120 SASS covers them via forward-compat. +PTX lets future + # revisions JIT. Same list on both arches. 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 (host - # may be a B200, RTX 6000, or GPU-less CI) so all yield byte-identical images. + # Build-host-independence guards: the build must NEVER introspect a GPU so all + # hosts yield byte-identical images. # 1) no JIT-compiled sm_NNN blob into unsloth_compiled_cache/ at import. UNSLOTH_COMPILE_DISABLE=1 \ UNSLOTH_COMPILE_OVERWRITE=0 \ # 2) don't probe torch.cuda.is_available() at setup (would silently skip wheels). UNSLOTH_DISABLE_GPU_PROBE=1 \ - # 3) empty CUDA_VISIBLE_DEVICES so stray torch.cuda calls see no devices, not - # host-specific paths (re-enabled at runtime via `docker run --gpus all`). + # 3) empty CUDA_VISIBLE_DEVICES so stray torch.cuda calls see no devices + # (re-enabled at runtime via `docker run --gpus all`). CUDA_VISIBLE_DEVICES="" RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -80,34 +78,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 \ && rm -rf /var/lib/apt/lists/* -# Isolated prefix. We do NOT touch the system Python (Ubuntu 24.04 marks it -# externally-managed, PEP 668); the venv bootstraps pip via ensurepip (from the -# python3.12-venv apt package) and gets uv a few lines below. +# Isolated prefix; never touch the system Python (PEP 668 externally-managed). +# The venv bootstraps pip via ensurepip and gets uv a few lines below. ENV VENV=/opt/unsloth-venv RUN python -m venv ${VENV} && ${VENV}/bin/pip install -U pip wheel setuptools # Unified install: torch + triton + bitsandbytes + unsloth + unsloth_zoo in a # SINGLE uv pass. Mandatory -- splitting it lets bnb's transitive `cuda-toolkit` -# silently upgrade torch to 2.12.0+cu130 in a later pass, breaking the pinned -# cu128 xformers wheel (the cu cascade hits after xformers is on disk). +# silently upgrade torch to 2.12.0+cu130, breaking the pinned cu128 xformers wheel. # # Flags: # --index-strategy unsafe-best-match: the PyTorch index serves an old -# requests==2.28.1 that conflicts with datasets>=2.32.2; both indexes -# (pytorch cu128 + pypi) are equally trusted, so override uv's first-wins. -# --extra-index-url .../cu128: torch +cu128 wheels + the xformers/cu128 URLs -# from unsloth's cu128onlytorch2110 extra. +# requests==2.28.1 conflicting with datasets>=2.32.2; both indexes are equally +# trusted, so override uv's first-wins. +# --extra-index-url .../cu128: torch +cu128 wheels + the xformers/cu128 URLs. # # Plain `huggingface` extra + explicit xformers pin (amd64): the cu128 extras on -# main stop at torch2100 (xformers 0.0.34 -> torch 2.10.0), conflicting with the -# torch 2.11.0 held below; a missing extra name would only WARN and drop xformers -# until the check below failed. Pinning xformers==0.0.35 (untied to torch) keeps -# this self-contained; arm64 stays xformers-less (no cu128 aarch64 wheel). +# main stop at torch2100, conflicting with the torch 2.11.0 held below. Pinning +# xformers==0.0.35 (untied to torch) keeps this self-contained; arm64 stays +# xformers-less (no cu128 aarch64 wheel). # # No flash-attn: FA3 is hard-refused on Blackwell (Dao-AILab/flash-attention#1810); -# FA2 has no cu128+torch2.11+cp312 wheel (~30min fragile source build on CI) and -# Unsloth falls back to xformers/SDPA anyway. Users on Ampere/Ada/Hopper can -# `pip install flash-attn` on top at deploy time. +# FA2 has no cu128+torch2.11+cp312 wheel and Unsloth falls back to xformers/SDPA. +# Ampere/Ada/Hopper users can `pip install flash-attn` at deploy time. ARG UNSLOTH_REF=main ARG UNSLOTH_ZOO_REF=main RUN set -eux \ @@ -130,12 +123,10 @@ RUN set -eux \ "unsloth[${UNSLOTH_EXTRA}] @ git+https://github.com/unslothai/unsloth@${UNSLOTH_REF}" \ "timm>=1.0.11" "addict" -# vLLM: required by Unsloth's GRPO path (fast_inference=True). Installed as a -# SECOND uv pass so the unified pass settles on torch 2.11.0 first, then vLLM -# bolts on top: with torch held, uv picks the newest compatible vLLM (0.20+ pins -# torch 2.11.0) and tracks our pin. PyPI ships x86_64 + aarch64 wheels since 0.17, -# so this runs on arm64 too; amd64 failures abort, arm64 is fail-soft (aarch64 -# kernels are validated on Spark hardware, not CI). +# vLLM: required by Unsloth's GRPO path (fast_inference=True). A SECOND uv pass so +# torch 2.11.0 settles first; with torch held, uv picks the newest compatible vLLM +# (0.20+ pins torch 2.11.0). PyPI ships x86_64 + aarch64 wheels since 0.17. amd64 +# failures abort, arm64 is fail-soft (aarch64 kernels validated on Spark, not CI). # https://docs.vllm.ai/en/latest/getting_started/installation/gpu/ # https://wheels.vllm.ai/nightly ARG INSTALL_VLLM=auto @@ -150,15 +141,11 @@ RUN set -eux \ echo ">> installing vLLM (TARGETARCH=${TARGETARCH:-amd64})"; \ # Explicit && chain, not `set -e` -- POSIX shells disable errexit inside a # condition context (verified on dash), masking install failures. - # Step 1: uv resolves vLLM's deps with torch==2.11.0 held, landing on the - # newest matching vLLM (fails loudly if none); unsafe-best-match picks the - # best wheel across the three indexes. - # Step 2: vLLM pulls numpy down to 2.2.6 whose wheel ships a broken - # numpy.testing (tests/ stripped), crashing `from numpy import *` and thus - # `import unsloth`; upgrade numpy back to a self-consistent release. - # Step 3: vLLM pins numba 0.61.2 which hard-refuses numpy>=2.3, but the - # stack needs numpy>=2.3; lift numba to one supporting numpy 2.4 (0.65 - # imports cleanly, vllm still imports). + # 1: uv resolves vLLM's deps with torch==2.11.0 held (fails loudly if none). + # 2: vLLM pulls numpy down to 2.2.6 with a broken numpy.testing that breaks + # `import unsloth`; upgrade numpy back to a self-consistent release. + # 3: vLLM pins numba 0.61.2 (refuses numpy>=2.3); lift numba to one + # supporting numpy 2.4 (0.65 imports cleanly, vllm still imports). { ${VENV}/bin/uv pip install \ --python ${VENV}/bin/python \ --pre \ @@ -176,10 +163,8 @@ RUN set -eux \ && ${VENV}/bin/python -c "import vllm; print('vllm', vllm.__version__)" \ && ${VENV}/bin/python -c "import numpy.testing, numpy; print('numpy', numpy.__version__, 'testing ok')" \ && ${VENV}/bin/python -c "import numba; print('numba', numba.__version__, 'imports ok')" \ - # flashinfer-jit-cache: precompiled cubins so flashinfer ops don't hit the - # JIT path (standalone `vllm serve` dies there for fmha_gen on sm_100a; - # in-process GRPO survives since zoo blocks the FlashInfer JIT). Removes - # the runtime-compile failure class for ~1.5 GB. + # flashinfer-jit-cache: precompiled cubins so flashinfer ops skip the JIT + # path (standalone `vllm serve` dies there for fmha_gen on sm_100a). ~1.5 GB. && { ${VENV}/bin/uv pip install \ --python ${VENV}/bin/python \ --index-url https://flashinfer.ai/whl/cu128 \ @@ -190,8 +175,7 @@ RUN set -eux \ if [ "${TARGETARCH:-amd64}" != "amd64" ]; then \ echo ">> vLLM skipped on ${TARGETARCH}: install or import check failed (fail-soft on non-amd64)"; \ # A partial install must not poison the base stack: drop vllm and - # restore the numpy/numba floor it may have moved (numpy 2.2.6's - # broken numpy.testing breaks `import unsloth`). arm64 staging CI + # restore the numpy/numba floor it may have moved. arm64 staging CI # re-verifies `import unsloth` after this. ${VENV}/bin/uv pip uninstall --python ${VENV}/bin/python vllm || true; \ ${VENV}/bin/uv pip install --python ${VENV}/bin/python \ @@ -208,9 +192,8 @@ RUN set -eux \ # JupyterLab so the image runs unslothai/notebooks out of the box: # docker run --gpus all -p 8888:8888 unsloth/unsloth \ # jupyter lab --ip 0.0.0.0 --port 8888 --allow-root --no-browser -# Separate pass AFTER the torch-pinned resolves: pure-Python, never names torch, -# so uv can't disturb the cu128 pin set. These are declared by notebook install -# cells (neutralised by the in-image runner), so bake them here: +# Separate pass AFTER the torch pin: pure-Python, never names torch, so uv can't +# disturb the cu128 pin set. Declared by notebook install cells, so bake them: # matplotlib plotting; some trust_remote_code files import it (DeepSeek-OCR) # soundfile TTS audio read/write (bundles libsndfile) # evaluate+jiwer Whisper WER metric @@ -223,9 +206,7 @@ RUN set -eux \ # librosa Whisper audio features (pulls numba, already pinned >=0.65) # ftfy Oute TTS text normalisation # decord is separate below (no aarch64 wheel). Pinned (==) for reproducible -# rebuilds; bump deliberately. The resolve must NOT move torch/numpy/numba -- -# the assertion below fails the build if it did. Transitive deps land in -# /opt/unsloth-venv/requirements.lock.txt. +# rebuilds. The resolve must NOT move torch/numpy/numba (asserted below). RUN ${VENV}/bin/uv pip install \ --python ${VENV}/bin/python \ "jupyterlab==4.6.0" "notebook==7.6.0" "ipywidgets==8.1.8" "matplotlib==3.11.0" \ @@ -243,13 +224,10 @@ RUN if [ "${TARGETARCH:-amd64}" = "amd64" ]; then \ || echo ">> decord skipped (no matching wheel for ${TARGETARCH:-}); ERNIE-VL video decode unavailable"; \ fi -# Audio decode out of the box: TTS/STT notebooks decode datasets' Audio features -# through torchcodec. Three traps: (1) torchcodec 0.11 must pair with torch 2.11; -# (2) the wheel must come from the cu128 channel, not the PyPI default (cu13, -# dlopens libnvrtc.so.13); (3) its libs dlopen venv torch/NVIDIA libs that ld.so -# can't see, registered via ld.so.conf.d in the runtime stage (this layer only -# installs the wheels; the import check needs ffmpeg from the runtime stage). -# Fail-soft on arches without a matching wheel. +# Audio decode out of the box (torchcodec). Three traps: (1) torchcodec 0.11 must +# pair with torch 2.11; (2) the wheel must come from cu128, not the PyPI cu13 +# default; (3) its libs dlopen venv torch/NVIDIA libs registered via ld.so.conf.d +# in the runtime stage. Fail-soft on arches without a matching wheel. RUN set -eux \ && { ${VENV}/bin/uv pip install \ --python ${VENV}/bin/python \ @@ -259,14 +237,11 @@ RUN set -eux \ || echo ">> torchcodec bake skipped (no matching wheel for ${TARGETARCH:-amd64})" # transformers SIDECARS for per-notebook version activation (see -# unsloth_nb_compat.py). unslothai/notebooks pin many transformers versions; the -# base venv ships the newest 5.x. Each sidecar is transformers==X + matched -# huggingface_hub/tokenizers/safetensors, installed --no-deps into its own -# --target under ${VENV}/tf-sidecars. Activating one (prepend to sys.path before -# any ML import) swaps transformers WITHOUT touching the cu128 torch/vLLM/unsloth -# base -- verified: base unsloth loads + generates under 4.57.6 and 5.5.0 on B200. -# Versions mirror Studio's tiers (4.57.6 + 5.3.0/5.5.0/5.10.2); companions are -# resolved at build time. ~300MB after the strip below. Fail-soft per arch/wheel. +# unsloth_nb_compat.py). Each sidecar is transformers==X + matched +# huggingface_hub/tokenizers/safetensors, --no-deps into its own --target under +# ${VENV}/tf-sidecars. Prepending one to sys.path swaps transformers without +# touching the cu128 base. Versions mirror Studio's tiers (4.57.6 + +# 5.3.0/5.5.0/5.10.2). ~300MB after the strip below. Fail-soft per arch/wheel. RUN set -eux \ && for TFV in 4.57.6 5.3.0 5.5.0 5.10.2; do \ SCRATCH="$(mktemp -d)"; \ @@ -289,18 +264,15 @@ RUN set -eux \ && { du -sh ${VENV}/tf-sidecars || true; } # 5) Informational pin record (NOT byte-reproducible: pip freeze omits wheel -# hashes and unsloth/unsloth_zoo/vllm --pre float from VCS/nightly). Read via -# `docker run --rm cat /opt/unsloth-venv/requirements.lock.txt`. +# hashes and unsloth/vllm --pre float from VCS/nightly). RUN ${VENV}/bin/pip freeze --exclude-editable > ${VENV}/requirements.lock.txt \ && head -50 ${VENV}/requirements.lock.txt -# 6) Strip pip cache & __pycache__ to shrink the layer copied to runtime. -# The `-name tests` strip excludes numpy's tests dirs: numpy 2.4 ships -# numpy/_core/tests/ back, and removing it re-triggers the -# `from numpy._core.tests._natype import pd_NA` ImportError the earlier upgrade -# fixed. Other verified-safe size cuts: -# * npp: torchcodec dlopens only libnppicc + libnppc; drop the other ~10 libs (~388MB). -# * static .a archives (~143MB): link-time only, nothing links venv archives. +# 6) Strip pip cache & __pycache__ to shrink the runtime layer. The `-name tests` +# strip excludes numpy's tests dirs (numpy 2.4 needs numpy/_core/tests/ or +# `import numpy` breaks). Other verified-safe cuts: +# * npp: torchcodec dlopens only libnppicc + libnppc; drop the rest (~388MB). +# * static .a archives (~143MB): link-time only. # * nvshmem device .bc (~30MB): device-relink only; host .so kept. # Do NOT strip headers (torch/include): causal-conv1d / mamba-ssm build against # them at notebook time with --no-build-isolation. @@ -375,11 +347,9 @@ PY # ============================================================================= # Stage 2: runtime -- slim runtime image, no nvcc, no cuDNN/cuBLAS layers # ============================================================================= -# The "-base-" variant (vs "-cudnn-runtime-") drops ~2.7 GB of system CUDA libs -# we never load: torch wheels bake their own cuDNN/cuBLAS/etc into torch/lib/ and -# libtorch_cuda.so's RPATH resolves through the wheel, not the system (verified -# via `readelf -d torch/lib/libtorch_cuda.so`). The base still provides -# nvidia-smi + libcuda stubs + libnvidia-ml, all the entrypoint + torch.cuda need. +# The "-base-" variant drops ~2.7 GB of system CUDA libs we never load: torch +# wheels bake their own cuDNN/cuBLAS into torch/lib/ and resolve via RPATH. The +# base still provides nvidia-smi + libcuda stubs + libnvidia-ml. FROM nvidia/cuda:${CUDA_VERSION}-base-ubuntu${UBUNTU_VERSION} AS runtime # The base manifest is multi-arch; buildx picks the right one for @@ -395,19 +365,16 @@ ENV DEBIAN_FRONTEND=noninteractive \ HF_HOME=/workspace/.cache/huggingface \ TRITON_CACHE_DIR=/workspace/.cache/triton \ # Keep the arch list at runtime so an in-container source build gets the same - # SASS coverage as the builder; 10.3 omitted (cu12.8 can't emit compute_103; - # sm_103 runs sm_100 SASS via forward-compat). + # SASS coverage as the builder (10.3 omitted; cu12.8 can't emit it). TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9;9.0;10.0;12.0+PTX" # System packages needed by the notebooks: # zstd Ollama installer (`curl ollama.com/install.sh | sh`) extracts a zstd tarball # ffmpeg torchcodec dlopens system FFmpeg libs (not bundled in the wheel) # wget notebooks fetch assets with `!wget URL` -# ninja-build flashinfer cpp_ext JIT shells out to ninja; subprocesses may not -# inherit the venv pip ninja on PATH -# cuda-nvcc + cudart-dev flash-linear-attention's TileLang backend (Qwen3.5 in -# Studio) JIT-compiles CUDA kernels via /usr/local/cuda/bin/nvcc, -# absent from the -base image +# ninja-build flashinfer cpp_ext JIT shells out to ninja +# cuda-nvcc + cudart-dev flash-linear-attention TileLang JIT-compiles CUDA +# kernels via nvcc, absent from the -base image RUN CUDA_PKG="$(echo "${CUDA_VERSION}" | awk -F. '{print $1"-"$2}')" \ && apt-get update && apt-get install -y --no-install-recommends \ software-properties-common ca-certificates curl wget git libgomp1 \ @@ -420,51 +387,39 @@ RUN CUDA_PKG="$(echo "${CUDA_VERSION}" | awk -F. '{print $1"-"$2}')" \ && ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 \ && test -x /usr/local/cuda/bin/nvcc \ && rm -rf /var/lib/apt/lists/* -# gcc + g++ + python3.12-dev in runtime: Triton's nvidia backend lazily compiles -# a C extension (CudaUtils) on first GPU access; without a C compiler + Python -# headers the first forward pass dies with "Failed to find C compiler". ~250MB, -# the cost of correct JIT (pre-compiling would need a GPU). +# gcc + g++ + python3.12-dev in runtime: Triton's nvidia backend compiles a C +# extension (CudaUtils) on first GPU access; without a compiler + headers the +# first forward pass dies with "Failed to find C compiler". ~250MB. COPY --from=builder /opt/unsloth-venv /opt/unsloth-venv -# Blackwell JIT fix for sm_103 (amd64) and sm_121 (arm64) -- the cu12.8 JIT gap -# from the header. Two JIT paths need the cu13 override: -# (1) torch's bundled libnvrtc.so.12 (CUDA 12.8): any NVRTC JIT path (e.g. -# torch.fft.rfft(complex).abs() in mel-spectrogram code) errors on -# sm_103/sm_121. Fix: stage a cu13 NVRTC alias beside the cu12.8 default. -# (2) Triton's bundled ptxas (still 12.8 in triton 3.6.0) tops out at sm_120, -# rejects sm_103, silently downgrades sm_121 to sm_80 (triton-lang/triton#8335). -# Fix: install cu13 ptxas, point Triton at it via TRITON_PTXAS_PATH. -# Both cu13 tools are CPU-side compilers (no install-time driver bump), but their -# output cubin needs a >=580 driver to LOAD, so neither is a global default (would -# break Ampere/Ada/Hopper/Turing on 570-579 drivers). select_cuda_jit_tools in -# entrypoint.sh activates them per device, only for sm_103/sm_121 (>=580 drivers, -# always safe). Both arches carry the ~400 MB. +# Blackwell JIT fix for sm_103 (amd64) and sm_121 (arm64) -- the cu12.8 JIT gap. +# Two JIT paths need the cu13 override: +# (1) torch's bundled libnvrtc.so.12 errors on sm_103/sm_121. Fix: stage a cu13 +# NVRTC alias beside the cu12.8 default. +# (2) Triton's bundled ptxas (12.8) rejects sm_103, downgrades sm_121 to sm_80 +# (triton-lang/triton#8335). Fix: cu13 ptxas via TRITON_PTXAS_PATH. +# Both cu13 tools are CPU-side compilers, but their cubin needs a >=580 driver to +# LOAD, so neither is a global default (would break 570-579 drivers). +# select_cuda_jit_tools in entrypoint.sh activates them per device, only for +# sm_103/sm_121 (>=580 drivers). Both arches carry the ~400 MB. RUN set -eux; \ - # The nvidia/cuda base already configures the CUDA apt repo with its own - # Signed-By keyring; adding cuda-keyring_1.1-1_all.deb would add a second - # sources file with a different Signed-By and make `apt-get update` refuse - # the repo ("Conflicting values set for option Signed-By"). The base repo - # is monolithic and serves 13.x too, so install cu13 packages directly - # without touching the keyring. Verified on x86_64 and arm runners. + # The base already configures the CUDA apt repo with its own Signed-By + # keyring; a second cuda-keyring would make apt-get update refuse the repo. + # The base repo serves 13.x too, so install cu13 packages directly. apt-get update; \ apt-get install -y --no-install-recommends \ cuda-nvrtc-13-0 \ cuda-nvcc-13-0; \ - # cu13's config-common postinst flips the /usr/local/cuda alternative to - # cuda-13.0 (priority 130 beats 12.8's 128). Pin it back: TileLang JIT - # and torch.utils.cpp_extension resolve /usr/local/cuda/bin/nvcc, and - # cu13-emitted cubins need driver >= 580 while this image supports 570+. - # The cu13 tools stay reachable by absolute path (how the entrypoint - # activates them on sm_103/sm_121), and --set switches the alternative - # to manual mode so later apt operations cannot flip it again. + # cu13's postinst flips /usr/local/cuda to cuda-13.0; pin it back (cpp + # builds resolve /usr/local/cuda/bin/nvcc, and cu13 cubins need driver + # >= 580 while this image supports 570+). The cu13 tools stay reachable by + # absolute path; --set also stops later apt ops flipping it again. update-alternatives --set cuda /usr/local/cuda-12.8; \ rm -rf /var/lib/apt/lists/*; \ # (1) NVRTC staging: keep the wheel's cu12.8 lib as .cu128.orig, point - # libnvrtc.so.12 at it (relative symlink), stage .cu13 -> the cu13 lib; + # libnvrtc.so.12 at it, stage .cu13 -> the cu13 lib; # select_cuda_jit_tools retargets the symlink only on sm_103/sm_121. - # The default needs no runtime write, so a non-root --user container - # keeps cu12.8, loadable on every supported 570+ driver. NVRTC_DIR=/opt/unsloth-venv/lib/python${PYTHON_VERSION}/site-packages/nvidia/cuda_nvrtc/lib; \ if [ -f "${NVRTC_DIR}/libnvrtc.so.12" ] && [ ! -L "${NVRTC_DIR}/libnvrtc.so.12" ]; then \ mv "${NVRTC_DIR}/libnvrtc.so.12" "${NVRTC_DIR}/libnvrtc.so.12.cu128.orig"; \ @@ -476,8 +431,7 @@ RUN set -eux; \ # Register the venv's torch + NVIDIA lib dirs with the loader so torchcodec can # dlopen them. ld.so.conf.d, NOT LD_LIBRARY_PATH: the cache is consulted after -# DT_RUNPATH, so llama.cpp keeps resolving its own $ORIGIN libs first. Import -# check runs here (ffmpeg lives in this stage); fail-soft if the wheel is absent. +# DT_RUNPATH, so llama.cpp keeps resolving its own $ORIGIN libs first. RUN set -eux \ && SP=/opt/unsloth-venv/lib/python${PYTHON_VERSION}/site-packages \ && printf "%s\n" "$SP/torch/lib" "$SP/nvidia/cuda_nvrtc/lib" \ @@ -488,25 +442,20 @@ RUN set -eux \ "import torchcodec; print('torchcodec', torchcodec.__version__)" \ || echo ">> torchcodec unavailable on this arch (audio decode falls back)"; } -# Prebuilt llama.cpp so GGUF export works out of the box. unsloth_zoo's -# save_pretrained_gguf() -> check_llama_cpp() looks for llama-quantize + -# convert_hf_to_gguf.py + gguf-py/ in $UNSLOTH_LLAMA_CPP_PATH; without a baked -# install the first export hits install_llama_cpp()'s prompt + slow source build. +# Prebuilt llama.cpp so GGUF export works out of the box; without it the first +# export hits install_llama_cpp()'s prompt + slow source build. # -# NOT studio/install_llama_prebuilt.py: it selects a bundle for the CURRENT host -# (nvidia-smi etc., which leak through a GPU build host). The build must never -# introspect the host, so pin release + asset by build target (see -# fetch_llama_prebuilt.py): +# NOT studio/install_llama_prebuilt.py: it selects a bundle for the CURRENT host, +# but the build must never introspect the host, so pin release + asset by build +# target (see fetch_llama_prebuilt.py): # * amd64 -> app--linux-x64-cuda12-portable.tar.gz (sm_70..sm_120) # arm64 -> app--linux-arm64-cuda13-portable.tar.gz (sm_90..sm_121) # * portable bundles carry their own CUDA libs, so they also run CPU-only # * sha256-verified against the release's llama-prebuilt-sha256.json # * converter + gguf-py from the SAME release's source tarball (mappings match) -# /opt (not /root) so it survives `docker run --user`; UNSLOTH_LLAMA_CPP_PATH lets -# zoo find it regardless of $HOME. Default "latest" resolves the newest -# unslothai/llama.cpp release at build time; build.sh pins it to a concrete tag so -# the cache busts only on new upstream releases. --build-arg LLAMA_PREBUILT_TAG= -# for a frozen build. +# /opt (not /root) so it survives `docker run --user`. Default "latest" resolves +# the newest release; build.sh pins a concrete tag so the cache busts only on new +# releases. --build-arg LLAMA_PREBUILT_TAG= for a frozen build. ARG LLAMA_PREBUILT_TAG=latest COPY fetch_llama_prebuilt.py /tmp/fetch_llama_prebuilt.py RUN /opt/unsloth-venv/bin/python /tmp/fetch_llama_prebuilt.py \ @@ -524,13 +473,11 @@ RUN mkdir -p ${HF_HOME} ${TRITON_CACHE_DIR} \ # --------------------------------------------------------------------------- # Per-notebook transformers version activation -- run unslothai/notebooks # UNCHANGED (see unsloth_nb_compat.py). Pieces: -# * unsloth_nb_compat.py -> site-packages: tier detection + sidecar resolution -# + activation + IPython hook. -# * pip/uv shim on a PATH dir AHEAD of the venv bin: makes `!pip install` / -# `!uv pip install` cells safe + idempotent (keeps the baked stack, records -# the requested transformers to activate its sidecar). -# * unsloth_nb_pip_magic.py -> site-packages: re-points `%pip`/`%uv` magics and -# `!python -m pip` at the same shim so in-process installs can't bypass PATH. +# * unsloth_nb_compat.py: tier detection + sidecar resolution + IPython hook. +# * pip/uv shim on a PATH dir AHEAD of the venv bin: makes `!pip install` cells +# safe + idempotent (keeps the baked stack, records requested transformers). +# * unsloth_nb_pip_magic.py: re-points `%pip`/`%uv` and `!python -m pip` at the +# same shim so in-process installs can't bypass PATH. # * IPython startup hook: activates the right sidecar before the first model cell. # * unsloth-run: headless `unsloth-run `, the robust driven path. # --------------------------------------------------------------------------- @@ -555,23 +502,18 @@ RUN set -eux \ && /opt/unsloth-venv/bin/python /opt/unsloth-nb/unsloth_pip_shim.py --unsloth-selfcheck-value-flags # 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} -# Load the notebook startup hook (sidecar activation + %pip/%uv magic re-point) -# for EVERY kernel, any uid: IPYTHONDIR (via ENV) points IPython at this shared -# profile, so it loads under `--user ` too -- unlike /root/.ipython, which -# only a root kernel reads. Writable state (history.sqlite) still lands per-user. +# Load the notebook startup hook for EVERY kernel, any uid: IPYTHONDIR points +# IPython at this shared profile, so it loads under `--user ` too (unlike +# /root/.ipython). Writable state (history.sqlite) still lands per-user. ENV IPYTHONDIR=/opt/unsloth-nb/ipython # Pre-clone unslothai/notebooks so JupyterLab opens with them present. Baked as a # READ-ONLY template (~206MB, .git stripped); on boot the entrypoint copies it to # /workspace/unsloth-notebooks and best-effort refreshes from GitHub, never -# overwriting a user-touched notebook and skipping rewrites when only the install -# header/announcements/footer moved (see unsloth_sync_notebooks.sh + -# unsloth_nb_content_sig.py). Inherited as-is by the studio image. +# overwriting a user-touched notebook (see unsloth_sync_notebooks.sh). # # UNSLOTH_NOTEBOOKS_REF pins ONE commit/branch/tag so a multi-arch publish bakes -# identical templates into both legs. The publish workflow resolves the live HEAD -# sha once (like LLAMA_PREBUILT_TAG); default "main" tracks the tip. Fetched at -# depth 1 (sha by object, branch/tag by name). +# identical templates into both legs; default "main" tracks the tip. ARG UNSLOTH_NOTEBOOKS_REF=main RUN set -eux \ && git init -q /opt/unsloth-notebooks \ diff --git a/docker/Dockerfile.studio b/docker/Dockerfile.studio index 722ee84c95..3c7f2a0aae 100644 --- a/docker/Dockerfile.studio +++ b/docker/Dockerfile.studio @@ -41,19 +41,16 @@ FROM ${BASE_IMAGE} # the base) so the published image is reproducible. ARG UNSLOTH_STUDIO_REF=main # unsloth-zoo ref overlaid into the Studio venv by install.sh --local. The publish -# workflow passes ONE zoo ref to both builds, so Studio runs the same zoo as the -# base and the operator-requested ref instead of always main. +# workflow passes ONE zoo ref to both builds, so Studio runs the same zoo as base. ARG UNSLOTH_STUDIO_ZOO_REF=main -# The SAME llama.cpp tag the base baked (prepare resolves it once). install.sh -> -# setup.sh honours UNSLOTH_LLAMA_TAG; without the pin the Studio build could -# re-resolve "latest" and replace the base's pinned bundle instead of reusing it. +# The SAME llama.cpp tag the base baked. setup.sh honours UNSLOTH_LLAMA_TAG; +# without the pin the Studio build could re-resolve "latest" and diverge. ARG LLAMA_PREBUILT_TAG=latest ARG TARGETARCH -# Services run as root here (base is root-only; non-root parity is a follow-up). -# sshd is key-only and stays disabled unless PUBLIC_KEY/SSH_KEY is set; no secrets -# are persisted (see studio_launch.sh). The JUPYTER_PORT / UNSLOTH_ENABLE_SSHD -# defaults let supervisord's %(ENV_*)s resolve when run directly (bypassing the launcher). +# Services run as root here (non-root parity is a follow-up). sshd is key-only, +# disabled unless PUBLIC_KEY/SSH_KEY is set (see studio_launch.sh). The +# JUPYTER_PORT / UNSLOTH_ENABLE_SSHD defaults let supervisord's %(ENV_*)s resolve. USER root ENV UNSLOTH_STUDIO_HOME=/opt/unsloth-studio \ JUPYTER_PORT=8888 \ @@ -68,22 +65,19 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* # Clone + install Studio into a dedicated venv under $UNSLOTH_STUDIO_HOME. -# --local uses the cloned tree (editable install), so the source MUST persist for -# the venv's unsloth_cli entrypoint -- move it to $STUDIO_HOME/src, strip .git (~120MB). +# --local is editable, so the source MUST persist -- keep it at $STUDIO_HOME/src, +# strip .git (~120MB). # # The llama.cpp symlink BEFORE install.sh points Studio's prebuilt dir at the -# base image's baked bundle so the installer skips a second ~400MB download; the +# base's baked bundle so the installer skips a second ~400MB download; the # .unsloth-studio-owned marker satisfies setup.sh's ownership assertion. # -# UNSLOTH_TORCH_INDEX_FAMILY pins the Studio venv's torch index: no GPU/nvidia-smi -# at build time would land install.sh on cpu/cu126 wheels. cu128 on both arches, -# mirroring the base (cu130 would lift the arm64 floor to 580+). Blackwell JIT -# (sm_103/sm_121) comes from the same cu13 NVRTC swap the base applies, repeated -# below for the Studio venv on both arches. +# UNSLOTH_TORCH_INDEX_FAMILY pins the Studio venv's torch index (no nvidia-smi at +# build time would land on cpu/cu126). cu128 on both arches, mirroring the base. +# Blackwell JIT (sm_103/sm_121) comes from the same cu13 NVRTC swap, repeated below. # -# UNSLOTH_PYTHON=3.12 pins the Studio venv to the base's Python minor (install.sh -# defaults to 3.13), making the nvidia-*-cu12 wheels byte-identical so the dedup -# below can symlink the Studio venv's ~3.7GB of CUDA .so into the base venv's. +# UNSLOTH_PYTHON=3.12 pins the Studio venv to the base's Python minor so the +# nvidia-*-cu12 wheels are byte-identical and the dedup below can symlink them. # # fetch+checkout FETCH_HEAD, not `clone --branch`: CI passes a commit SHA. RUN set -eux \ @@ -106,25 +100,18 @@ RUN set -eux \ UNSLOTH_PYTHON=3.12 \ bash install.sh --local \ # Fail loud unless the Studio venv torch EXACTLY matches the base (version AND - # CUDA family) before the dedup symlinks their CUDA libs. A family-only check - # would miss a torch that ignored UNSLOTH_TORCH_INDEX_FAMILY (cu126 probe) or - # capped below the base's version, linking incompatible libs. Compare to the - # base's own torch (no hardcoded version); metadata only, since importing torch - # needs native libs QEMU arm64 can't load. + # CUDA family) before the dedup symlinks their CUDA libs. Compare to the base's + # own torch (no hardcoded version); metadata only (QEMU arm64 can't import torch). && BASE_TORCH="$(/opt/unsloth-venv/bin/python -c "from importlib.metadata import version; print(version('torch'))")" \ && "${UNSLOTH_STUDIO_HOME}/unsloth_studio/bin/python" -c "import sys; from importlib.metadata import version; assert sys.version_info[:2] == (3, 12), 'Studio venv python %d.%d is not 3.12 (UNSLOTH_PYTHON pin ignored) -- CUDA dedup below depends on it' % sys.version_info[:2]; v = version('torch'); assert v == '${BASE_TORCH}', 'Studio venv torch ' + v + ' does not match base venv torch ${BASE_TORCH} (CUDA dedup would link mismatched libs)'; print('Studio venv python %d.%d torch' % sys.version_info[:2], v, '== base', '${BASE_TORCH}')" \ - # setup.sh may relink llama-quantize into build/bin; prove it still resolves - # its libraries or GGUF export breaks with "No working quantizer found". - # Content check, not rc: --help exits nonzero but prints usage; a loader - # failure prints "error while loading shared libraries" and no usage. + # setup.sh may relink llama-quantize into build/bin; prove it still resolves its + # libraries. Content check, not rc: --help exits nonzero but prints usage. && { "${UNSLOTH_STUDIO_HOME}/llama.cpp/llama-quantize" --help 2>&1 || true; } | grep -q "usage" \ && rm -rf "${UNSLOTH_STUDIO_HOME}/src/.git" \ "${UNSLOTH_STUDIO_HOME}/src/studio/frontend/node_modules" \ /root/.cache \ - # Stage the Studio venv's NVRTC like the base venv (.cu128.orig default + - # staged .cu13 alias, retargeted per device by select_cuda_jit_tools). Both - # arches: sm_103 needs cu13 NVRTC as much as sm_121, the dedup never touches - # cuda_nvrtc, and the base layer installed cuda-nvrtc-13-0 on both arches. + # Stage the Studio venv's NVRTC like the base (.cu128.orig default + .cu13 + # alias, retargeted per device by select_cuda_jit_tools). Both arches. && for NVRTC_DIR in "${UNSLOTH_STUDIO_HOME}"/unsloth_studio/lib/python*/site-packages/nvidia/cuda_nvrtc/lib; do \ if [ -f "${NVRTC_DIR}/libnvrtc.so.12" ] && [ ! -L "${NVRTC_DIR}/libnvrtc.so.12" ]; then \ mv "${NVRTC_DIR}/libnvrtc.so.12" "${NVRTC_DIR}/libnvrtc.so.12.cu128.orig"; \ @@ -168,20 +155,15 @@ COPY fetch_llama_prebuilt.py /usr/local/lib/unsloth/fetch_llama_prebuilt.py # Optional public Cloudflare tunnel for JupyterLab (UNSLOTH_JUPYTER_CLOUDFLARE=1, # or `unsloth-jupyter-tunnel --force`); supervisord runs it as jupyter-cloudflare. COPY unsloth_jupyter_tunnel.sh /usr/local/bin/unsloth-jupyter-tunnel -# JupyterLab defaults baked for every container: "Unsloth Dark" (Monokai) theme -# with adaptive light/dark, a per-cell run button that doesn't auto-advance, a -# labeled "Restart & Run All", windowing off (collapsing output won't snap to -# top), ArrowDown/Up to the top of the next/prev cell, and the "news" prompt off. -# overrides.json is the system-wide settings override; the theme + keymap + logo -# ship as the prebuilt labextension from labext-builder above. +# JupyterLab defaults baked for every container (theme, non-advancing run button, +# labeled "Restart & Run All", windowing off, cell-nav keymap, news prompt off). +# overrides.json is the settings override; theme + keymap + logo ship as the +# prebuilt labextension from labext-builder above. COPY jupyter/overrides.json /opt/unsloth-venv/share/jupyter/lab/settings/overrides.json COPY --from=labext-builder /opt/labext-src/unsloth-jupyterlab/labextension /opt/unsloth-venv/share/jupyter/labextensions/unsloth-jupyterlab -# Unsloth branding (served by jupyter_server, applied to its site-packages): -# replace the favicon + page logo, brand the login screen (login.html), and -# disable+lock the stock top-left Jupyter logo so only the labextension's Unsloth -# logo renders. The sloth-sticker install is the ONLY fail-soft step (own { } -# group with `|| echo`), so a missing "Sloth emojis" folder doesn't break the -# build; the required steps above (JS resolve, favicon/logo/login copy) stay fatal. +# Unsloth branding (applied to jupyter_server's site-packages): replace favicon + +# logo, brand login.html, disable+lock the stock top-left logo. Only the +# sloth-sticker install is fail-soft (`|| echo`); the copies above stay fatal. COPY jupyter/favicon.ico /tmp/unsloth-branding/favicon.ico COPY jupyter/logo.png /tmp/unsloth-branding/logo.png COPY jupyter/login.html /tmp/unsloth-branding/login.html @@ -203,10 +185,8 @@ RUN JS="$(/opt/unsloth-venv/bin/python -c 'import os, jupyter_server; print(os.p && /opt/unsloth-venv/bin/jupyter labextension lock @jupyterlab/apputils-extension:splash \ && /opt/unsloth-venv/bin/jupyter labextension lock unsloth-jupyterlab # Branding integrity guard: the attribution checker (a jupyter_server extension), -# the AGPLv3 license text, and its enabling config, installed into the base venv -# (on the jupyter import + config path). The stock splash is disabled+locked -# above so the labextension's splash is the sole provider. --verify FAILS the -# build if any attribution / license asset is missing or altered. +# the AGPLv3 license text, and its enabling config, into the base venv. --verify +# FAILS the build if any attribution / license asset is missing or altered. COPY jupyter/unsloth_branding.py /tmp/unsloth-branding-guard/unsloth_branding.py COPY jupyter/jupyter_server_config.d/unsloth_branding_guard.json /tmp/unsloth-branding-guard/unsloth_branding_guard.json RUN SP="$(/opt/unsloth-venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))')" \ diff --git a/docker/build.sh b/docker/build.sh index f84aa9f36e..f5d369e84b 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -18,10 +18,9 @@ PYTHON_VERSION="${PYTHON_VERSION:-3.12}" UNSLOTH_REF="${UNSLOTH_REF:-main}" UNSLOTH_ZOO_REF="${UNSLOTH_ZOO_REF:-main}" -# llama.cpp prebuilt: default to the newest unslothai/llama.cpp release, resolved -# here to a concrete tag so the build-arg changes only when upstream publishes a -# new release (correct Docker layer caching) and the build stays reproducible. -# Pin it explicitly for a frozen build: LLAMA_PREBUILT_TAG=b9596-mix-e6f2453 ./build.sh +# llama.cpp prebuilt: default to the newest release, resolved here to a concrete +# tag so the build-arg changes only on a new release (correct layer caching). +# Pin for a frozen build: LLAMA_PREBUILT_TAG=b9596-mix-e6f2453 ./build.sh resolve_latest_llama_tag() { curl -fsSL -o /dev/null -w '%{url_effective}' \ "https://github.com/unslothai/llama.cpp/releases/latest" 2>/dev/null \ diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0157aef440..28b4608d37 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -8,38 +8,28 @@ set -euo pipefail # --- CUDA JIT toolchain selection (device-gated) ---------------------------- -# The image bakes CUDA 13 ptxas + NVRTC only for the two Blackwell datacenter -# arches cu12.8 can't target -- sm_103 (B300/GB300) and sm_121 (GB10/DGX Spark). -# Both launched after cu12.8, so their hosts run a >=580 driver, exactly what a -# cu13 cubin needs to load. Every other arch (Turing..sm_120) uses the cu12.8 -# tools on the documented 570-579 floor; a cu13 cubin can't load there (CUDA -# driver compat is forward-only), so routing their JIT through cu13 would break -# training. ptxas/NVRTC are host-side compilers, so they RUN under any driver -- -# only their output the old driver rejects. -# Pick per DEVICE at boot (cap unknown at build time): cu12.8 is the immutable -# default (loadable on 570+), only sm_103/sm_121 switch Triton to cu13 ptxas and -# retarget the NVRTC symlink. Runs before every early-exit. Best-effort: the safe -# default needs no write (non-root/read-only fine); only a non-root datacenter -# host can't switch. +# The image bakes CUDA 13 ptxas + NVRTC only for sm_103 (B300/GB300) and sm_121 +# (GB10/DGX Spark), which cu12.8 can't target. Both ship on >=580 drivers, which a +# cu13 cubin needs. Every other arch uses cu12.8 on the 570-579 floor, where a +# cu13 cubin can't load. Pick per DEVICE at boot: cu12.8 is the immutable default, +# only sm_103/sm_121 switch Triton to cu13 ptxas and retarget the NVRTC symlink. +# Best-effort: the default needs no write; only a non-root datacenter host can't switch. select_cuda_jit_tools() { local caps="" cc nvrtc_dir need_cu13=0 if command -v nvidia-smi >/dev/null 2>&1; then caps="$( { nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null || true; } )" fi - # Scan EVERY visible GPU: a sm_103/sm_121 part can sit behind an H100/B200 in - # nvidia-smi ordering. If ANY needs cu13, switch for the whole process -- those - # parts ship on >=580 drivers, so the host tolerates cu13 cubins for all archs. + # Scan EVERY visible GPU (a sm_103/sm_121 part can sit behind an H100). If ANY + # needs cu13, switch the whole process -- those hosts run >=580 drivers. while IFS= read -r cc || [[ -n "${cc}" ]]; do cc="$(printf '%s' "${cc}" | tr -d '[:space:]')" case "${cc}" in 10.3|12.1) need_cu13=1 ;; esac done <<< "${caps}" - # Non-datacenter / undetectable / CPU host: keep cu12.8 (libnvrtc.so.12 -> - # .cu128.orig, Triton on bundled cu12.8 ptxas), loadable on 570+ and needs no - # write. One exception needs a write: an earlier boot on sm_103/sm_121 left - # libnvrtc.so.12 -> .cu13 and this GPU's 570-579 driver can't load it -- - # reverse that selection (best-effort, same non-root caveat). + # Non-datacenter / undetectable / CPU host: keep cu12.8 (needs no write). One + # exception: an earlier sm_103/sm_121 boot left libnvrtc.so.12 -> .cu13 that a + # 570-579 driver can't load -- reverse that (best-effort). if [[ "${need_cu13}" -ne 1 ]]; then for nvrtc_dir in \ /opt/unsloth-venv/lib/python*/site-packages/nvidia/cuda_nvrtc/lib \ @@ -51,9 +41,8 @@ select_cuda_jit_tools() { return 0 fi # Blackwell datacenter present: point Triton at cu13 ptxas and retarget each - # venv's libnvrtc.so.12 -> the staged cu13 alias. -z guard lets an explicit - # TRITON_PTXAS_PATH win. Best-effort: a read-only/--user rootfs keeps cu12.8. - # Covers the base venv and the Studio venv. + # venv's libnvrtc.so.12 -> the cu13 alias. -z guard lets an explicit + # TRITON_PTXAS_PATH win. Covers the base + Studio venvs. if [[ -x /usr/local/cuda-13.0/bin/ptxas && -z "${TRITON_PTXAS_PATH:-}" ]]; then export TRITON_PTXAS_PATH=/usr/local/cuda-13.0/bin/ptxas fi @@ -84,12 +73,10 @@ fi err() { printf "\033[1;31mERROR:\033[0m %s\n" "$*" >&2; } warn() { printf "\033[1;33mWARN:\033[0m %s\n" "$*" >&2; } -# CPU mode for hosts that can't pass a GPU into a container (Docker Desktop on -# macOS/Windows-without-WSL2, CPU Linux, CI). Covers Jupyter, GGUF tooling, -# llama.cpp Studio chat and Data Recipes; NOT training or loading an Unsloth -# model (FastLanguageModel runs CUDA probes and raises without a GPU). With -# UNSLOTH_ALLOW_CPU=1 a missing GPU warns instead of failing pre-flight; a -# visible GPU still runs the checks below. +# CPU mode for hosts that can't pass a GPU (Docker Desktop, CPU Linux, CI). Covers +# Jupyter, GGUF tooling, Studio chat; NOT training or loading a model. With +# UNSLOTH_ALLOW_CPU=1 a missing GPU warns instead of failing; a visible GPU still +# runs the checks below. 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." @@ -207,10 +194,9 @@ for d in range(1, n): PY # --- arm64 note: baked llama.cpp is a CUDA 13 build ------------------------- -# Upstream ships no CUDA 12 arm64 llama.cpp (only arm64-cpu/arm64-cuda13), so the -# arm64 image bakes cu13 while the torch stack (cu128) runs on 570+. A cu13 cubin -# can't load on 570-579, so below 580 GGUF export / Studio chat fail even though -# training works -- say so up front instead of failing mysteriously later. +# Upstream ships no CUDA 12 arm64 llama.cpp, so the arm64 image bakes cu13 while +# torch (cu128) runs on 570+. A cu13 cubin can't load on 570-579, so below 580 +# GGUF export / Studio chat fail even though training works -- warn up front. if [ "$(uname -m)" = "aarch64" ]; then _drv="$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1)" _drv_major="${_drv%%.*}" diff --git a/docker/fetch_llama_prebuilt.py b/docker/fetch_llama_prebuilt.py index 25c934d5b3..03a4af484c 100644 --- a/docker/fetch_llama_prebuilt.py +++ b/docker/fetch_llama_prebuilt.py @@ -45,8 +45,7 @@ RELEASE_REPO = "unslothai/llama.cpp" def resolve_latest_tag(repo: str) -> str: - # Follow the /releases/latest redirect: no API token, no rate limit, works on - # any build host. + # Follow the /releases/latest redirect: no API token or rate limit. url = f"https://github.com/{repo}/releases/latest" request = urllib.request.Request(url, headers = {"User-Agent": "unsloth-docker-build"}) with urllib.request.urlopen(request, timeout = 60) as response: @@ -127,9 +126,8 @@ def main() -> None: if os.path.isfile(target) and not entry.startswith("lib") and ".so" not in entry: os.chmod(target, 0o755) - # Converter + gguf-py from the same-tag source tarball, so the python - # side's tensor mappings match the binaries (mirrors unsloth_zoo's - # _hydrate_converter_sources). + # Converter + gguf-py from the same-tag source tarball so tensor mappings + # match the binaries (mirrors unsloth_zoo's _hydrate_converter_sources). source_path = fetch_verified(base_url, source_name, sums, work) source_dir = os.path.join(work, "source") os.makedirs(source_dir) @@ -148,12 +146,10 @@ def main() -> None: if os.path.isdir(conversion): shutil.copytree(conversion, os.path.join(install_dir, "conversion"), dirs_exist_ok = True) - # Make the baked marker readable by Studio's freshness check so the in-app - # "newer llama.cpp available" banner works. The tarball's marker carries - # upstream_tag/source_repo, but the reader keys off tag/release_tag/ - # published_repo (the schema install_llama_prebuilt.py writes). setdefault() - # leaves a future tarball that already has these keys untouched; no build - # timestamp, so the layer stays byte-identical across build hosts. + # Make the baked marker readable by Studio's freshness check. The tarball keys + # off upstream_tag/source_repo, but the reader wants tag/release_tag/ + # published_repo (the install_llama_prebuilt.py schema). setdefault() leaves an + # already-populated tarball untouched; no timestamp, so layers stay identical. marker_path = os.path.join(install_dir, "UNSLOTH_PREBUILT_INFO.json") try: with open(marker_path) as f: @@ -168,11 +164,10 @@ def main() -> None: f.write("\n") print(f"marker augmented for freshness: tag={tag} published_repo={RELEASE_REPO}") - # Mirror the install into build/bin/ via hardlinks (zero extra bytes). Studio's - # setup.sh treats executable build/bin/llama-server + llama-quantize as a - # complete local build and skips its source-build fallback (which would - # otherwise compile a CPU-only llama.cpp over the baked CUDA bundle). Hardlinks - # (not symlinks) keep $ORIGIN rpath resolution and avoid a cycle when setup.sh + # Mirror the install into build/bin/ via hardlinks (zero extra bytes) so + # Studio's setup.sh treats it as a complete local build and skips its + # source-build fallback (which would compile CPU-only llama.cpp over the baked + # CUDA bundle). Hardlinks keep $ORIGIN rpath and avoid a cycle when setup.sh # relinks the root quantizer to build/bin/llama-quantize. build_bin = os.path.join(install_dir, "build", "bin") os.makedirs(build_bin, exist_ok = True) @@ -184,20 +179,19 @@ def main() -> None: except OSError: shutil.copy2(source, os.path.join(build_bin, entry)) elif os.path.islink(source): - # Mirror same-directory soname symlinks (libllama.so.0 -> ...). - # Without these, a binary relinked into build/bin fails $ORIGIN - # resolution: the loader wants the soname, not the real file. + # Mirror same-dir soname symlinks (libllama.so.0 -> ...); without them + # a binary relinked into build/bin fails $ORIGIN (loader wants soname). target = os.readlink(source) dest = os.path.join(build_bin, entry) if "/" not in target and not os.path.lexists(dest): os.symlink(target, dest) - # Sanity: the server must execute on a GPU-less host (the CUDA backend is a - # dlopen'd plugin). Check the quantizer from BOTH roots: setup.sh relinks the - # root llama-quantize to build/bin, so the build/bin copy must resolve standalone. + # Sanity: the server must run on a GPU-less host (CUDA backend is a dlopen'd + # plugin). Check the quantizer from both roots: setup.sh relinks the root copy + # to build/bin, so build/bin must resolve standalone. checks = ( - # llama-quantize has no --version; a healthy run prints usage with - # rc 0, while a loader failure prints to stderr with rc 127. + # llama-quantize has no --version: healthy run prints usage (rc 0), + # loader failure rc 127. (os.path.join(install_dir, "llama-server"), "version"), (os.path.join(install_dir, "llama-quantize"), "usage"), (os.path.join(build_bin, "llama-quantize"), "usage"), diff --git a/docker/jupyter/install_sloth_stickers.py b/docker/jupyter/install_sloth_stickers.py index 4289eb432b..4ca9c54b4a 100644 --- a/docker/jupyter/install_sloth_stickers.py +++ b/docker/jupyter/install_sloth_stickers.py @@ -24,9 +24,8 @@ import os import shutil import sys -# Curated, in display order -> NN.png. Mirrors Studio's SLOTH_AVATARS -# (frontend/src/features/profile/sloth-avatars.ts): the square, low-whitespace -# stickers that frame cleanly. Kept in sync by hand; missing names are skipped. +# Curated, in display order -> NN.png. Mirrors Studio's SLOTH_AVATARS: the square, +# low-whitespace stickers that frame cleanly. Synced by hand; missing names skipped. CURATED = [ "large sloth yay.png", "large sloth heart.png", @@ -72,8 +71,7 @@ def main() -> int: print(" skip (%s): %s" % (error, name)) print("installed %d/%d sloth stickers into %s" % (installed, len(CURATED), args.dest)) - # Non-fatal: the login page degrades to the logo if none were installed, but - # a totally empty copy usually means a wrong --src, so signal that. + # Non-fatal, but an empty copy usually means a wrong --src, so signal it. return 0 if installed else 1 diff --git a/docker/jupyter/unsloth_branding.py b/docker/jupyter/unsloth_branding.py index 3323d76ece..861d0d5a1c 100644 --- a/docker/jupyter/unsloth_branding.py +++ b/docker/jupyter/unsloth_branding.py @@ -29,9 +29,8 @@ import os import sys # --------------------------------------------------------------------------- -# Canonical attribution strings. Plain text. Keep in sync with the TypeScript -# mirror at unsloth_labext/src/branding.ts (the guard checks the built bundle -# contains these same strings). +# Canonical attribution strings. Plain text; keep in sync with the TS mirror +# unsloth_labext/src/branding.ts (the guard greps the built bundle for these). # --------------------------------------------------------------------------- PRODUCT = "Unsloth Docker Studio" SHORT_LABEL = "Built by the Unsloth team" @@ -45,9 +44,8 @@ SOURCE_URL = "https://github.com/unslothai/unsloth" LICENSE_URL = "https://github.com/unslothai/unsloth#license" AGPL_URL = "https://www.gnu.org/licenses/agpl-3.0.html" APACHE_URL = "https://www.apache.org/licenses/LICENSE-2.0" -# ONE plain literal, byte-identical to PHRASE in unsloth_labext/src/branding.ts. -# The guard greps the built labext bundle for this exact string, so it must match -# the TS literal verbatim (webpack keeps single string literals as-is). +# ONE plain literal, byte-identical to PHRASE in unsloth_labext/src/branding.ts; +# the guard greps the built bundle for it verbatim. PHRASE = ( "Unsloth Docker Studio and JupyterLab image. Built by the Unsloth team. " "Licensed under Apache 2.0 and the GNU AGPLv3. " @@ -80,9 +78,8 @@ def resolve_paths( jupyter_server_dir = os.path.dirname(jupyter_server.__file__) labext_dir = os.path.join(venv_share, "labextensions", LABEXT_NAME) - # Every page_config.json JupyterLab merges for disabledExtensions: the - # app-settings file plus a labconfig/ file per jupyter config dir. Tests pass - # config_dirs=[] for a hermetic tree; live resolution scans the real path. + # Every page_config.json JupyterLab merges for disabledExtensions (app-settings + # + a labconfig/ file per config dir). Tests pass config_dirs=[] for hermeticity. if config_dirs is None: try: from jupyter_core.paths import jupyter_config_path @@ -200,9 +197,8 @@ def verify_branding(paths = None): problems.append("missing or empty logo: " + paths["logo"]) # 7. No page_config.json disables the Unsloth extension or its plugins. - # Disabling via disabledExtensions leaves the bundle on disk (check 5 passes) - # yet strips the logo/About/splash at load, so reject it too. We only flag - # ids belonging to unsloth-jupyterlab (our own stock disables are fine). + # disabledExtensions leaves the bundle on disk (check 5 passes) but strips + # it at load, so reject it. Only flag unsloth-jupyterlab ids. for pc_path in paths.get("page_configs", []): text = _read(pc_path) if not text: @@ -270,9 +266,8 @@ def _load_jupyter_server_extension(serverapp): serverapp.log.critical(msg) except Exception: pass - # Stop the server cleanly, then guarantee exit if that is swallowed during - # extension load. studio_launch.sh (Layer A) normally refuses the whole - # container first; this is defense in depth for a direct `jupyter lab` run. + # Stop the server cleanly, then force exit if that's swallowed. Layer A + # (studio_launch.sh) refuses the container first; this backstops a direct run. try: serverapp.exit(1) except Exception: diff --git a/docker/jupyter/unsloth_labext/src/about.ts b/docker/jupyter/unsloth_labext/src/about.ts index f07639e1c0..70bf028279 100644 --- a/docker/jupyter/unsloth_labext/src/about.ts +++ b/docker/jupyter/unsloth_labext/src/about.ts @@ -2,8 +2,7 @@ // Copyright 2026-Present the Unsloth team. See /studio/LICENSE.AGPL-3.0 // // "About Unsloth Docker Studio" command -> Help menu + command palette. Surfaces -// the AGPLv3 license, the copyright line and the Unsloth source/website links so -// the image's provenance is one click away inside JupyterLab. +// the AGPLv3 license, copyright and source/website links inside JupyterLab. import { JupyterFrontEnd, @@ -30,10 +29,9 @@ import { const COMMAND_ID = 'unsloth:about'; /** - * Build the About dialog body. The content is composed only from the trusted - * constants in branding.ts (no user input), so the static innerHTML carries no - * injection surface. PHRASE is stamped as a data attribute so the canonical - * attribution string is bundled verbatim for the integrity guard to find. + * Build the About dialog body from the trusted branding.ts constants only (no + * user input, so innerHTML has no injection surface). PHRASE is stamped as a data + * attribute so it's bundled verbatim for the integrity guard. */ function aboutBody(): Widget { const body = new Widget(); @@ -42,9 +40,8 @@ function aboutBody(): Widget { el.style.padding = '4px 10px 10px'; el.style.maxWidth = '430px'; el.setAttribute('data-unsloth-attribution', PHRASE); - // The link rows sit in a left-aligned inline-block centered in the dialog, so - // the "Source:/Website:/Licenses" labels line up instead of each row centering - // independently (the previous ragged look). + // Link rows in a left-aligned inline-block centered in the dialog, so the + // labels line up instead of each row centering independently. el.innerHTML = ` Unsloth diff --git a/docker/jupyter/unsloth_labext/src/branding.ts b/docker/jupyter/unsloth_labext/src/branding.ts index 67fe498d0a..a17b1d108c 100644 --- a/docker/jupyter/unsloth_labext/src/branding.ts +++ b/docker/jupyter/unsloth_labext/src/branding.ts @@ -1,17 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-Present the Unsloth team. See /studio/LICENSE.AGPL-3.0 // -// Canonical attribution strings for the Unsloth Docker Studio image, mirrored -// from docker/jupyter/unsloth_branding.py. These are imported by the About and -// splash plugins so they are bundled verbatim into the built labextension; the -// Python integrity guard checks the built bundle still contains them. Plain -// readable text only -- never base64/encoded (that would trip antivirus and is -// pointless for an open-source image). +// Canonical attribution strings, mirrored from unsloth_branding.py. Imported by +// the About and splash plugins so they're bundled verbatim; the Python guard +// checks the built bundle still contains them. Plain text only, never encoded. export const PRODUCT = 'Unsloth Docker Studio'; export const SHORT_LABEL = 'Built by the Unsloth team'; -// Loading-splash caption. Deliberately distinct from SHORT_LABEL (which the -// About dialog + guard use): the splash says what is loading, not attribution. +// Loading-splash caption; distinct from SHORT_LABEL (says what's loading). export const SPLASH_LABEL = 'Loading Unsloth Docker'; export const COPYRIGHT = 'Copyright 2026-Present the Unsloth team'; export const AGPL_NOTICE = 'Licensed under Apache 2.0 and the GNU AGPLv3'; @@ -22,9 +18,7 @@ export const LICENSE_URL = 'https://github.com/unslothai/unsloth#license'; export const AGPL_URL = 'https://www.gnu.org/licenses/agpl-3.0.html'; export const APACHE_URL = 'https://www.apache.org/licenses/LICENSE-2.0'; -// Must equal PHRASE in unsloth_branding.py (the guard greps the built bundle for -// it). Kept as ONE plain literal -- not a concatenation of the constants above -- -// so webpack/terser preserves the full phrase contiguously in the bundle instead -// of folding it into a runtime `+` expression the guard could not grep for. +// Must equal PHRASE in unsloth_branding.py (the guard greps the bundle for it). +// ONE plain literal, not a concatenation, so webpack keeps it contiguous. export const PHRASE = 'Unsloth Docker Studio and JupyterLab image. Built by the Unsloth team. Licensed under Apache 2.0 and the GNU AGPLv3. Source: https://github.com/unslothai/unsloth Website: https://unsloth.ai'; diff --git a/docker/jupyter/unsloth_labext/src/cellNav.ts b/docker/jupyter/unsloth_labext/src/cellNav.ts index 45cf263e2c..4261fd2a37 100644 --- a/docker/jupyter/unsloth_labext/src/cellNav.ts +++ b/docker/jupyter/unsloth_labext/src/cellNav.ts @@ -11,13 +11,9 @@ import { INotebookTracker } from '@jupyterlab/notebook'; * Colab-style cell navigation in BOTH command and edit mode. * * ArrowDown on a cell's last line (edit) or while selected (command) moves to the - * next cell and aligns its TOP to the viewport; ArrowUp mirrors it. JupyterLab's - * built-in scroll CENTERS cells taller than the viewport, dropping the view in - * the middle of a long output (e.g. `trainer.train()`). - * - * Settings can't fix this (JupyterLab 4.1 handles keydown in the bubbling phase, - * command-mode arrows are Lumino's), so we listen in the CAPTURE phase, detect a - * cell boundary, and move + scroll-to-top ourselves. + * next cell and aligns its TOP to the viewport; ArrowUp mirrors it. JupyterLab + * centers tall cells, dropping the view mid-output. Settings can't fix this, so + * we listen in the CAPTURE phase, detect a cell boundary, and scroll-to-top. */ const cellNavPlugin: JupyterFrontEndPlugin = { id: 'unsloth-jupyterlab:cell-nav', @@ -40,9 +36,8 @@ const cellNavPlugin: JupyterFrontEndPlugin = { if (!panel.node.contains(event.target as Node)) { return; } - // Never hijack arrows that belong to an interactive output (an ipywidgets - // slider / dropdown / text box created by a cell) or a plain form control; - // only the cell editor and the notebook's own command-mode cell nav. + // Never hijack arrows belonging to an interactive output (ipywidgets) or a + // form control; only the cell editor and command-mode cell nav. const targetEl = event.target as HTMLElement | null; if (targetEl) { if (targetEl.closest('.jp-OutputArea')) { @@ -61,9 +56,8 @@ const cellNavPlugin: JupyterFrontEndPlugin = { if (!editor) { return; } - // While a completion / autocomplete popup is open, the arrows belong to - // it (moving through the suggestions) -- do not take over even at a cell - // boundary, which is common in one-line setup cells. + // While a completion popup is open the arrows belong to it; don't take + // over even at a cell boundary (common in one-line setup cells). if ( document.querySelector( '.jp-Completer:not(.lm-mod-hidden), .cm-tooltip-autocomplete' @@ -72,8 +66,7 @@ const cellNavPlugin: JupyterFrontEndPlugin = { return; } const line = editor.getCursorPosition().line; - // Only take over at the cell boundary; otherwise let CodeMirror move the - // cursor within the editor as usual (do not preventDefault/stop). + // Only take over at the cell boundary; else let CodeMirror move the cursor. if (direction === 1 && line !== editor.lineCount - 1) { return; } @@ -85,9 +78,8 @@ const cellNavPlugin: JupyterFrontEndPlugin = { if (target < 0 || target >= notebook.widgets.length) { return; } - // We own this key now: stop CodeMirror (edit mode) and the Lumino command - // system (command mode) from also handling it, which would re-trigger the - // centering scroll we are trying to replace. + // We own this key: stop CodeMirror and Lumino from also handling it and + // re-triggering the centering scroll we replace. event.preventDefault(); event.stopPropagation(); notebook.activeCellIndex = target; diff --git a/docker/jupyter/unsloth_labext/src/colabTitle.ts b/docker/jupyter/unsloth_labext/src/colabTitle.ts index 317ec57290..997ae40885 100644 --- a/docker/jupyter/unsloth_labext/src/colabTitle.ts +++ b/docker/jupyter/unsloth_labext/src/colabTitle.ts @@ -9,12 +9,10 @@ import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; import { Cell } from '@jupyterlab/cells'; /** - * Colab "#@title" form cells. In Colab a code cell whose first line is - * `#@title Some Title` renders as a titled, collapsed form (clickable header, - * code hidden by default, output visible). JupyterLab has no equivalent, so this - * reproduces it: inject a clickable title bar and hide the input via a CSS class - * (not the model's source_hidden, so notebook metadata is never mutated). - * Clicking toggles the code. Windowing is disabled image-wide, so the bar persists. + * Colab "#@title" form cells. A code cell whose first line is `#@title Some Title` + * renders in Colab as a titled, collapsed form. JupyterLab has no equivalent, so + * inject a clickable title bar and hide the input via a CSS class (not + * source_hidden, so metadata is never mutated). Clicking toggles the code. */ const TITLE_RE = /^\s*#\s*@title\b[ \t]*(.*)$/; @@ -138,9 +136,9 @@ const colabTitlePlugin: JupyterFrontEndPlugin = { panel.content.widgets.forEach(applyTitle); }; panel.revealed.then(scan).catch(() => undefined); - // Re-scan when cells are added/removed/moved or the user switches cells - // (covers editing a #@title line). applyTitle never re-collapses a cell - // that already has a bar, so manual expansions are preserved. + // Re-scan on cell add/remove/move or active-cell switch (covers editing a + // #@title line). applyTitle never re-collapses an existing bar, so manual + // expansions are preserved. const model = panel.content.model; if (model) { model.cells.changed.connect(() => window.setTimeout(scan, 0)); diff --git a/docker/jupyter/unsloth_labext/src/index.ts b/docker/jupyter/unsloth_labext/src/index.ts index ce413ac1d4..75a493c389 100644 --- a/docker/jupyter/unsloth_labext/src/index.ts +++ b/docker/jupyter/unsloth_labext/src/index.ts @@ -17,10 +17,9 @@ import splashPlugin from './splash'; import uiChromePlugin from './uiChrome'; /** - * The "Unsloth Dark" theme: JupyterLab Dark repainted with the Sublime/Colab - * Monokai palette (see style/variables.css). Registered as a named theme so it - * appears in Settings > Theme and works with the adaptive (system) light/dark - * switch configured in overrides.json. + * The "Unsloth Dark" theme: JupyterLab Dark repainted with the Monokai palette + * (style/variables.css). A named theme so it appears in Settings > Theme and + * works with the adaptive light/dark switch in overrides.json. */ const themePlugin: JupyterFrontEndPlugin = { id: 'unsloth-jupyterlab:theme', @@ -41,9 +40,8 @@ const themePlugin: JupyterFrontEndPlugin = { /** * Replace the top-left Jupyter logo with the Unsloth logo. The stock logo plugin - * is disabled + locked at build time, so this is the only logo widget. Rendered - * as an with inline styles (not a LabIcon/CSS) so branding shows identically - * in any theme (the theme CSS loads only while Unsloth Dark is selected). + * is disabled + locked at build, so this is the only logo widget. An with + * inline styles (not a LabIcon) so branding shows in any theme. */ const logoPlugin: JupyterFrontEndPlugin = { id: 'unsloth-jupyterlab:logo', diff --git a/docker/jupyter/unsloth_labext/src/logo.ts b/docker/jupyter/unsloth_labext/src/logo.ts index a4617a3b5e..3a6e83a3ae 100644 --- a/docker/jupyter/unsloth_labext/src/logo.ts +++ b/docker/jupyter/unsloth_labext/src/logo.ts @@ -1,8 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-Present the Unsloth team. See /studio/LICENSE.AGPL-3.0 -// Auto-generated: Unsloth circle logo (circle-logo-small.png) as a base64 -// PNG data URI, embedded so the top-bar logo plugin has no runtime asset -// dependency and renders identically in light and dark themes. +// Auto-generated: Unsloth circle logo as a base64 PNG data URI, embedded so the +// logo plugin has no runtime asset dependency and renders in any theme. export const UNSLOTH_LOGO_DATA_URI = "data:image/png;base64,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"; diff --git a/docker/jupyter/unsloth_labext/src/outputSelect.ts b/docker/jupyter/unsloth_labext/src/outputSelect.ts index cd1bffa961..54074c2d1a 100644 --- a/docker/jupyter/unsloth_labext/src/outputSelect.ts +++ b/docker/jupyter/unsloth_labext/src/outputSelect.ts @@ -9,21 +9,15 @@ import { /** * Colab-style Ctrl/Cmd+A inside a cell output. * - * Clicking a cell's output leaves the notebook in command mode, so Ctrl/Cmd+A - * fires `notebook:select-all` (selects EVERY cell). Colab instead selects only - * the clicked output's text; this reproduces that and stops the event so the - * notebook-wide select-all never runs. - * - * Listens in the CAPTURE phase and acts only when the chord is exactly Ctrl/Cmd+A - * (no Alt), focus is NOT in an editor/input/contenteditable, and the keystroke - * target or last pointer-down landed in an output area. We use the last - * pointer-down, not the text selection anchor, because a stale anchor survives a - * click away and would hijack select-all elsewhere. + * Clicking an output leaves the notebook in command mode, so Ctrl/Cmd+A fires + * `notebook:select-all` (every cell). Colab selects only the clicked output's + * text; reproduce that and stop the event. Listens in the CAPTURE phase, acts + * only on exactly Ctrl/Cmd+A (no Alt) outside an editor/input, keyed off the + * target or last pointer-down (not the stale selection anchor). */ -// Output containers, widest first. `.jp-OutputArea-output` is a single output; -// `.jp-Cell-outputWrapper` is the whole output column of one cell (covers the -// case where a click lands on padding between outputs). +// Output containers, widest first: a single output, then the whole output column +// (covers a click on padding between outputs). const OUTPUT_SELECTORS = ['.jp-OutputArea-output', '.jp-Cell-outputWrapper']; function closestOutput(node: Node | null): HTMLElement | null { @@ -67,9 +61,8 @@ const outputSelectPlugin: JupyterFrontEndPlugin = { 'Ctrl/Cmd+A inside a cell output selects only that output, not every cell.', autoStart: true, activate: (_app: JupyterFrontEnd): void => { - // Remember where the last pointer-down landed: a click on an image / widget - // output may not leave a text selection inside it, so the selection anchor - // alone is not enough to know which output the user means. + // Remember the last pointer-down: a click on an image/widget output leaves no + // text selection, so the anchor alone can't tell which output is meant. let lastPointerOutput: HTMLElement | null = null; document.addEventListener( 'pointerdown', @@ -89,16 +82,14 @@ const outputSelectPlugin: JupyterFrontEndPlugin = { if (inEditableContext()) { return; } - // Own the chord only when in an output now: the keystroke target, else the - // last click. Not the selection anchor -- it goes stale after clicking away - // (see the header) and would hijack select-all elsewhere. + // Own the chord only when in an output: the target, else the last click + // (not the stale selection anchor; see the header). const output = closestOutput(event.target as Node | null) ?? lastPointerOutput; if (!output) { return; } - // We own this key: prevent `notebook:select-all` (Lumino, command mode) - // from also running and selecting the whole notebook. + // We own this key: prevent Lumino's `notebook:select-all` from also running. event.preventDefault(); event.stopPropagation(); try { diff --git a/docker/jupyter/unsloth_labext/src/splash.ts b/docker/jupyter/unsloth_labext/src/splash.ts index 8f4b3f548a..6215bf0877 100644 --- a/docker/jupyter/unsloth_labext/src/splash.ts +++ b/docker/jupyter/unsloth_labext/src/splash.ts @@ -2,9 +2,8 @@ // Copyright 2026-Present the Unsloth team. See /studio/LICENSE.AGPL-3.0 // // Replace the JupyterLab loading splash with a spinning Unsloth logo. Provides -// the core ISplashScreen token; the stock @jupyterlab/apputils-extension:splash -// is disabled + locked at image build time so this is the only provider. The -// animation honors prefers-reduced-motion and keeps the default loader footprint. +// the core ISplashScreen token; the stock splash is disabled + locked at build, +// so this is the only provider. Animation honors prefers-reduced-motion. import { JupyterFrontEndPlugin } from '@jupyterlab/application'; import { ISplashScreen } from '@jupyterlab/apputils'; diff --git a/docker/jupyter/unsloth_labext/src/uiChrome.ts b/docker/jupyter/unsloth_labext/src/uiChrome.ts index 56b57d6a8a..d7abaa083c 100644 --- a/docker/jupyter/unsloth_labext/src/uiChrome.ts +++ b/docker/jupyter/unsloth_labext/src/uiChrome.ts @@ -10,10 +10,9 @@ import { /** * Colab-like chrome tweaks applied image-wide. * - * Hide the right activity bar (Property Inspector / Debugger tabs) by default. - * JupyterLab has no settings key to hide a side activity bar, so hide the strip - * with always-on CSS and collapse the right panel once on startup. Panels can - * still be reopened from the View menu; nothing is removed, only hidden. + * Hide the right activity bar (Property Inspector / Debugger) by default. + * JupyterLab has no settings key for this, so hide the strip with CSS and + * collapse the right panel once on startup. Reopen from the View menu. */ const STYLE_ID = 'unsloth-ui-chrome-style'; @@ -40,8 +39,7 @@ const uiChromePlugin: JupyterFrontEndPlugin = { requires: [ILabShell], activate: (app: JupyterFrontEnd, shell: ILabShell): void => { injectStyle(); - // Collapse the right area once the layout is restored so a previously - // expanded right panel does not linger on first paint. + // Collapse the right area once restored so an expanded panel doesn't linger. app.restored .then(() => { try { diff --git a/docker/run.sh b/docker/run.sh index d383f698c7..c52cb22320 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -42,10 +42,9 @@ set -euo pipefail IMAGE="${UNSLOTH_IMAGE:-unsloth/unsloth:latest}" GPUS="${UNSLOTH_GPUS:-all}" -# Translate index selectors to Docker's `device=` form: Docker reads a bare -# integer for --gpus as a COUNT not an INDEX, so `UNSLOTH_GPUS=0` would expose -# zero GPUs. `all` and already-quoted `device=...` selectors pass through; -# "none" omits --gpus (CPU mode; pair with UNSLOTH_ALLOW_CPU=1). +# Translate index selectors to Docker's `device=` form: a bare integer is a COUNT +# not an INDEX, so `UNSLOTH_GPUS=0` would expose zero GPUs. `all`/quoted `device=` +# pass through; "none" omits --gpus (CPU mode). GPU_FLAG=(--gpus "$GPUS") case "$GPUS" in none) GPU_FLAG=() ;; @@ -78,10 +77,9 @@ declare -a ENV_FORWARD=(-e HF_HUB_ENABLE_HF_TRANSFER=1) [[ -n "${WANDB_API_KEY:-}" ]] && ENV_FORWARD+=(-e WANDB_API_KEY) [[ -n "${UNSLOTH_LICENSE:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_LICENSE) [[ -n "${UNSLOTH_ALLOW_CPU:-}" ]] && ENV_FORWARD+=(-e UNSLOTH_ALLOW_CPU) -# Studio/Jupyter service config read by studio_launch.sh. Same dash-only -e VAR -# form so even JUPYTER_PASSWORD never lands in argv. Without these, the bundled -# launcher got a random password and never enabled sshd (PUBLIC_KEY/SSH_KEY) or -# the tunnel (UNSLOTH_JUPYTER_CLOUDFLARE). +# Studio/Jupyter service config read by studio_launch.sh. Dash-only -e VAR so +# JUPYTER_PASSWORD never lands in argv. Without these the launcher gets a random +# password and no sshd/tunnel. [[ -n "${JUPYTER_PASSWORD:-}" ]] && ENV_FORWARD+=(-e JUPYTER_PASSWORD) [[ -n "${PUBLIC_KEY:-}" ]] && ENV_FORWARD+=(-e PUBLIC_KEY) [[ -n "${SSH_KEY:-}" ]] && ENV_FORWARD+=(-e SSH_KEY) diff --git a/docker/smoke_test.py b/docker/smoke_test.py index a07285eaf1..41427da603 100644 --- a/docker/smoke_test.py +++ b/docker/smoke_test.py @@ -32,8 +32,7 @@ def check_torch() -> tuple[int, int]: banner("torch + arch list") import torch - # Use the raw C++ accessor so this works even when CUDA isn't available - # (lets us run a partial smoke test on a no-GPU host). + # Raw C++ accessor works even without CUDA (partial smoke test on no-GPU host). arches = torch._C._cuda_getArchFlags().split() print(f"torch {torch.__version__}") print(f"cuda build {torch.version.cuda}") @@ -45,9 +44,8 @@ def check_torch() -> tuple[int, int]: cap = torch.cuda.get_device_capability(0) name = torch.cuda.get_device_name(0) print(f"device 0 {name} sm_{cap[0]}{cap[1]}") - # cu128 wheels ship SASS down to sm_75 (Turing); match the runtime entrypoint's - # floor so the smoke job doesn't false-fail on a Turing-only runner. Turing - # falls back to fp16 (a capability hint, not a hard failure). + # cu128 wheels ship SASS down to sm_75 (Turing); match the entrypoint floor so + # a Turing-only runner doesn't false-fail (Turing falls back to fp16). if cap[0] < 7 or (cap[0] == 7 and cap[1] < 5): sys.exit(f"FAIL: pre-Turing GPU {name} is not supported by this image") if cap[0] < 8: @@ -60,17 +58,16 @@ def check_imports() -> None: import triton print(f"triton {triton.__version__}") - # Import order matters: unsloth BEFORE transformers/trl/peft (so its patches - # land) and BEFORE unsloth_zoo (which needs the UNSLOTH_IS_PRESENT marker, - # else its __init__ guard raises "Please install Unsloth via pip install unsloth"). + # Import order matters: unsloth before transformers/trl/peft (so its patches + # land) and before unsloth_zoo (which needs the UNSLOTH_IS_PRESENT marker). import unsloth print(f"unsloth {unsloth.__version__}") import unsloth_zoo print(f"unsloth_zoo {unsloth_zoo.__version__}") - # xformers has no aarch64 cu128 wheel, so the arm64 image omits it - # ([huggingface] extras). Best-effort import so one script covers both arches. + # xformers has no aarch64 cu128 wheel; arm64 omits it. Best-effort so one + # script covers both arches. try: import xformers print(f"xformers {xformers.__version__}") @@ -92,8 +89,7 @@ def check_imports() -> None: def check_unsloth_import() -> None: banner("unsloth FastLanguageModel reachable") - # unsloth itself was already imported in check_imports() above (it has to be - # imported first for unsloth_zoo to load). This re-import is a no-op. + # Already imported in check_imports(); this re-import is a no-op. import unsloth from unsloth import FastLanguageModel diff --git a/docker/studio_launch.sh b/docker/studio_launch.sh index 1c792fb09b..a5bf5ddea8 100644 --- a/docker/studio_launch.sh +++ b/docker/studio_launch.sh @@ -61,9 +61,7 @@ c.PasswordIdentityProvider.hashed_password = "${HASH}" EOF # Land in the categorized notebook view, but only when it's enabled AND under # root_dir (expressible as /lab/tree). Mirror unsloth_sync_notebooks.sh's - # gating (UNSLOTH_NOTEBOOKS_VIEW_DIR + SKIP_NOTEBOOK_VIEW + SKIP_NOTEBOOK_SYNC) - # so a relocated/disabled/unsynced view never points at a missing dir; - # otherwise JupyterLab opens on its default /lab over /workspace. + # gating so a relocated/disabled/unsynced view never points at a missing dir. _root_dir="/workspace" _view_dir="${UNSLOTH_NOTEBOOKS_VIEW_DIR:-/workspace/Unsloth Notebooks}" if [[ "${UNSLOTH_SKIP_NOTEBOOK_VIEW:-0}" != "1" \ diff --git a/docker/supervisord.conf b/docker/supervisord.conf index 0367f4ea83..d2be57fe33 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -5,9 +5,8 @@ # jupyter JupyterLab for the notebooks port $JUPYTER_PORT (default 8888) # sshd key-only SSH for cloud hosts port 22 # -# All three log to the container's stdout/stderr (the Docker-native pattern) -# so `docker logs` shows everything, including Studio's first-boot password -# and Jupyter's startup line. +# All three log to stdout/stderr so `docker logs` shows everything, including +# Studio's first-boot password and Jupyter's startup line. [unix_http_server] file=/run/supervisor.sock @@ -44,9 +43,8 @@ command=jupyter lab --no-browser --ip=0.0.0.0 --port=%(ENV_JUPYTER_PORT)s --allo directory=/workspace autostart=true autorestart=true -; HOME pins the config lookup to /root/.jupyter, where the launcher wrote -; the password config; without it an unset HOME would silently fall back -; to token auth. +; HOME pins config lookup to /root/.jupyter (where the launcher wrote the +; password config); without it an unset HOME falls back to token auth. environment=HOME="/root",USER="root" stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 diff --git a/docker/unsloth_colab_compat.py b/docker/unsloth_colab_compat.py index a4b3d9b7a5..cb35a5088d 100644 --- a/docker/unsloth_colab_compat.py +++ b/docker/unsloth_colab_compat.py @@ -37,12 +37,11 @@ from __future__ import annotations import sys -# Cell magics whose body runs as code (Python or shell), so a hoisted comment -# stays inert. We ONLY hoist these; content/data magics (%%writefile, %%html, -# ...) are left untouched (see the module docstring). +# Cell magics whose body runs as code, so a hoisted comment stays inert. Only +# these; content/data magics (%%writefile, %%html, ...) untouched (see docstring). _SAFE_CELL_MAGICS = frozenset( { - "capture", # the Colab install pattern: suppress pip/install output + "capture", # Colab install pattern: suppress pip output "time", "timeit", "prun", @@ -71,15 +70,13 @@ def colab_cell_magic_fix(lines): if stripped == "" or stripped.startswith("#"): skipped.append(line) # blank or comment (incl. #@title) continue - # First real line. Only act if it is a cell magic that is not yet on - # top (i.e. something was skipped before it). + # First real line. Act only if it's a cell magic not already on top. if stripped.startswith("%%") and i > 0: name = stripped[2:].split(maxsplit = 1) name = name[0] if name else "" if name in _SAFE_CELL_MAGICS: return [line] + skipped + lines[i + 1 :] - # Content/data magic (%%writefile, %%html, ...): do not move the - # comment into its body. Leave the cell exactly as written. + # Content/data magic: don't move the comment into its body. return lines return lines # already on top, or not a magic return lines # all blank/comment -> nothing to do diff --git a/docker/unsloth_ipython_startup.py b/docker/unsloth_ipython_startup.py index 26edc4400c..9939b2ece3 100644 --- a/docker/unsloth_ipython_startup.py +++ b/docker/unsloth_ipython_startup.py @@ -12,18 +12,15 @@ outside IPython, when no version was requested, or once transformers is imported try: import os - # Tell the pip/uv shim it's running inside a notebook kernel, so a cell's - # `!pip install ...` / `!uv pip install ...` (which inherits this env) gets - # the safe-install behaviour. Unset everywhere else => shim is a passthrough. + # Tell the pip/uv shim it's inside a notebook kernel, so a cell's + # `!pip install ...` gets safe-install behaviour. Unset elsewhere => passthrough. os.environ["UNSLOTH_NB_SHIM"] = "1" # Scope the transformers-request marker to THIS kernel so concurrent notebooks - # don't read each other's pin. The pip/uv shim (a child of this kernel) - # inherits UNSLOTH_NB_TF_MARKER, so writer and reader agree on the path. Falls - # back to the shared default when unset (e.g. `unsloth-run`, one notebook/process). + # don't read each other's pin. The shim (a child) inherits UNSLOTH_NB_TF_MARKER, + # so writer and reader agree. Unset => shared default (one notebook/process). if not os.environ.get("UNSLOTH_NB_TF_MARKER"): - # A kernel id that is stable for the kernel's lifetime and unique per - # kernel: the ipykernel connection file name, else the kernel PID. + # Stable, unique kernel id: the ipykernel connection file name, else the PID. _kid = "" try: from ipykernel import get_connection_file # type: ignore @@ -37,9 +34,8 @@ try: unsloth_nb_compat.register_ipython() - # Re-point the %pip / %uv line magics and `!python -m pip` at the same shim, - # so the in-process / module install paths cannot bypass the PATH shim and - # overwrite the baked torch/vLLM stack. Independent of the sidecar hook. + # Re-point %pip / %uv and `!python -m pip` at the same shim so in-process + # installs can't bypass it and overwrite the baked torch/vLLM stack. import unsloth_nb_pip_magic unsloth_nb_pip_magic.register_ipython() @@ -48,8 +44,7 @@ except Exception as _e: # never break a kernel because of the helper print(f"[unsloth-nb] startup hook skipped: {_e!r}", file = sys.stderr) # Colab cell-magic compatibility (hoist `%%capture` above a leading `#@title` -# form so it fires instead of raising UsageError). Independent try/except so a -# failure here never disables the transformers-sidecar hook above and vice versa. +# form). Separate try/except so it can't disable the hook above, or vice versa. try: import unsloth_colab_compat unsloth_colab_compat.register_ipython() diff --git a/docker/unsloth_llama_update.sh b/docker/unsloth_llama_update.sh index 9c00ded52f..7b6dfc984e 100755 --- a/docker/unsloth_llama_update.sh +++ b/docker/unsloth_llama_update.sh @@ -99,10 +99,9 @@ fi # an atomic rename), then swap. On any failure the existing install is untouched. parent="$(dirname "$INSTALL_DIR")" -# The persistence recipe mounts a named volume AT the install dir. A mount point -# can't be renamed (rename(2) EBUSY), so the whole-dir swap below would fail -# there; detect the mount and swap the CONTENTS inside the tree (also keeps the -# update in the volume). UNSLOTH_LLAMA_UPDATE_IN_PLACE=1/0 overrides autodetection. +# A named volume mounted AT the install dir can't be renamed (EBUSY), so the +# whole-dir swap below would fail; detect the mount and swap the CONTENTS inside +# the tree. UNSLOTH_LLAMA_UPDATE_IN_PLACE=1/0 overrides autodetection. IN_PLACE="${UNSLOTH_LLAMA_UPDATE_IN_PLACE:-}" if [ -z "$IN_PLACE" ]; then IN_PLACE=0 @@ -122,9 +121,9 @@ else backup="${INSTALL_DIR}.old.$$" fi swap_done=0 -# The exit handler must never delete $backup while it is the ONLY copy of the -# install: put the old tree back first, and remove it only after the new tree is -# verifiably active. The signal traps run the EXIT trap on HUP/INT/TERM too. +# The exit handler must never delete $backup while it's the ONLY copy: restore the +# old tree first, remove it only after the new tree is active. Signal traps run +# the EXIT trap on HUP/INT/TERM too. cleanup() { if [ "$swap_done" -ne 1 ]; then if [ "$IN_PLACE" = "1" ]; then diff --git a/docker/unsloth_nb_compat.py b/docker/unsloth_nb_compat.py index 59ff2bc770..216acc6c9d 100644 --- a/docker/unsloth_nb_compat.py +++ b/docker/unsloth_nb_compat.py @@ -45,9 +45,8 @@ def _logging_enabled() -> bool: ) -# Model-name -> minimum transformers tier, ported from Studio's -# transformers_version.py (substring match on the lowered model id). Used as a -# fallback when a notebook does not pin transformers but names a new-family model. +# Model-name -> minimum transformers tier (substring match on the lowered id), +# ported from Studio. Fallback when a notebook names a new model but pins nothing. _TIER_SUBSTRINGS = { "5.10.2": ("gemma-4-12b", "gemma4-12b"), "5.5.0": ("gemma-4", "gemma4", "qwen3.6"), diff --git a/docker/unsloth_nb_content_sig.py b/docker/unsloth_nb_content_sig.py index 95c71484f7..c9918540ee 100644 --- a/docker/unsloth_nb_content_sig.py +++ b/docker/unsloth_nb_content_sig.py @@ -29,8 +29,7 @@ def _text(cell): return src.replace("\r\n", "\n").replace("\r", "\n") -# Package-manager command fragments that mark a cell as the generated install -# cell rather than substantive tutorial code. +# Command fragments that mark a cell as the generated install cell. _INSTALL_MARKERS = ( "pip install", "pip3-autoremove", @@ -48,10 +47,9 @@ def _is_install_code(cell): low = t.lower() if any(m in low for m in _INSTALL_MARKERS): return True - # A %%capture / %%bash cell is boilerplate ONLY when it also carries an install - # command. A bare %%capture or a %%bash doing real setup is substantive: hash - # it so the boot refresh doesn't skip an upstream fix (a false SAME). The - # install markers above already catch the generated install cell. + # A %%capture / %%bash cell is boilerplate only if it also carries an install + # command (caught above); a bare one doing real setup is substantive, so hash + # it to avoid a false SAME on the boot refresh. return False diff --git a/docker/unsloth_nb_pip_magic.py b/docker/unsloth_nb_pip_magic.py index defcdb94c9..116aaf1bda 100644 --- a/docker/unsloth_nb_pip_magic.py +++ b/docker/unsloth_nb_pip_magic.py @@ -24,10 +24,10 @@ subprocess, so the shim applies. Safe no-op outside IPython. import re -# Only the explicit `! -m pip|uv ...` shell form. Input transformers see -# the RAW cell text (IPython expands `{sys.executable}` later), so the braced form -# (`!{sys.executable} -m pip install ...`) and absolute interpreter paths, quoted -# or bare, must be matched here too or module-pip bypasses the PATH shim. +# Only the explicit `! -m pip|uv ...` shell form. Transformers see the RAW +# cell text (IPython expands `{sys.executable}` later), so the braced form and +# quoted/bare interpreter paths must be matched here too, else module-pip bypasses +# the shim. _PY_M_PIP = re.compile( r"""^(\s*)!\s* (?: diff --git a/docker/unsloth_nb_strip_colab.py b/docker/unsloth_nb_strip_colab.py index 3bfdc87053..95b89a72f0 100644 --- a/docker/unsloth_nb_strip_colab.py +++ b/docker/unsloth_nb_strip_colab.py @@ -4,38 +4,32 @@ # Remove the Colab-only "how to run" sentence from Unsloth notebooks for Docker. # -# Every generated notebook's first markdown cell opens with a Colab instruction -# ("To run this, press Runtime > Run all on a free Tesla T4 ...", plus A100/L4/AMD -# variants). Inside Docker there is no such menu or Colab GPU, so it is wrong; -# strip ONLY that leading sentence and keep the rest of the cell (badge row, -# local-install link, "You will learn ..." line). Docker-only, applied at sync -# time; NOT pushed upstream (on Colab the sentence is correct). +# Each generated notebook's first markdown cell opens with a Colab instruction +# ("To run this, press Runtime > Run all ...") that is wrong inside Docker. Strip +# only that leading sentence and keep the rest (badge row, install link, etc). +# Docker-only, applied at sync time; NOT pushed upstream. # # Two modes: # unsloth_nb_strip_colab.py [b.ipynb ...] strip in place (idempotent) # unsloth_nb_strip_colab.py --state --dest -# STATE-aware sync migration: for each .ipynb in the STATE file that still -# hashes to its recorded value (owned + unedited), strip the intro and update -# the hash; user-edited notebooks are left untouched. Runs after every STATE -# write (populate, restore, refresh, in-place upgrade). +# STATE-aware migration: strip + rehash each owned+unedited notebook (one +# whose hash still matches STATE); user-edited ones are left untouched. # -# Safe with refresh decisions: content_sig already classifies the intro cell as -# boilerplate, so the body digest is identical with or without the sentence. -# Exit code is always 0. +# Safe with refresh: content_sig classifies the intro cell as boilerplate, so the +# body digest is unchanged. Exit code is always 0. import argparse import hashlib import json import os import sys -# The stable identifier for the offending line (covers every GPU/Cloud variant). +# Stable identifier for the offending line (all GPU/Cloud variants). _INTRO_PREFIX = "to run this, press" -# The baked notebooks ship example tqdm widget outputs + a metadata.widgets state -# block; JupyterLab can't always rebuild the Colab-saved state, so they render as -# a stuck "Loading widget..." placeholder. Dropping the widget outputs + orphan -# state removes it (running the cell recreates a fresh widget). Outputs aren't in -# the refresh signature (content_sig hashes cell type+source), so this is safe. +# Baked notebooks ship tqdm widget outputs + a metadata.widgets block that +# JupyterLab can't rebuild, so they render as a stuck "Loading widget...". Drop +# them (the cell recreates a fresh widget). Outputs aren't in the refresh +# signature (content_sig hashes type+source), so this is safe. _WIDGET_VIEW_MIME = "application/vnd.jupyter.widget-view+json" diff --git a/docker/unsloth_nb_view.py b/docker/unsloth_nb_view.py index 87dfa76534..718649a282 100644 --- a/docker/unsloth_nb_view.py +++ b/docker/unsloth_nb_view.py @@ -4,35 +4,33 @@ # Build a categorized, Colab-like folder VIEW of the Unsloth notebooks. # -# The canonical notebooks live flat under DEST/nb/.ipynb (mirror of -# unslothai/notebooks, kept by unsloth_sync_notebooks.sh). This builds a sibling -# dir of *relative symlinks* grouped into folders mirroring the README headers: +# The canonical notebooks live flat under DEST/nb/.ipynb (kept by +# unsloth_sync_notebooks.sh). This builds a sibling dir of *relative symlinks* +# grouped into folders mirroring the README headers: # /01 Main Notebooks/Llama3_2_(1B_and_3B)_Conversational.ipynb # /99 Other Notebooks/ -# Symlinks so the real files never move (the sync state machine skips symlinks); -# the VIEW is a disposable sibling of DEST, rebuilt from scratch on every boot. +# Symlinks so real files never move (the sync state machine skips them); the VIEW +# is a disposable sibling of DEST, rebuilt on every boot. # # Categorization rules: -# * Section = nearest preceding `###` header in README.md; a header repeated -# across Fine-tuning/Kaggle/AMD domains merges into one folder (first order). +# * Section = nearest preceding `###` header; a header repeated across domains +# merges into one folder (first order). # * Folder names cleaned (dashes/slashes -> spaces) and numbered `NN ` by first -# appearance so JupyterLab's alpha sort keeps README order; "Other" is last. +# appearance so JupyterLab's sort keeps README order; "Other" is last. # * A notebook linked under several sections lands in its first. # * AMD-*.ipynb hidden unless --amd; unlinked nb/*.ipynb go to "Other Notebooks". # # Usage: # unsloth_nb_view.py [--amd] build the symlink view # unsloth_nb_view.py --print [--amd] print "section\tfile" rows -# Exits 0 on success; on error prints to stderr and exits nonzero so the caller -# can fall back to the raw tree. +# Exits nonzero on error (caller falls back to the raw tree). import argparse import os import re import sys import urllib.parse -# nb/.ipynb in any link form (markdown badge, HTML href, plain link, -# Kaggle ?src= form). Filenames use [\w.()-] plus %-escapes (%28/%29 for parens). +# nb/.ipynb in any link form. Filenames use [\w.()-] plus %-escapes. _NB_RE = re.compile(r"nb/([\w.()%\-]+?\.ipynb)") _OTHER = "Other Notebooks" @@ -40,8 +38,7 @@ _OTHER = "Other Notebooks" def clean_section(title): """README header text -> a filesystem-friendly folder label.""" title = title.strip().strip("#").strip() - # Strip a leading run of emoji/symbols some domain headers lead with so the - # folder label is clean text. + # Strip a leading emoji/symbol run so the folder label is clean text. title = re.sub(r"^[^\w]+", "", title) title = title.replace("-", " ").replace("/", " ") title = re.sub(r"\s+", " ", title).strip() @@ -67,8 +64,7 @@ def parse_readme(readme_path): seen_pairs = set() # (section, filename) already emitted section = None # Reset on ANY markdown heading, not just `###`: `#`/`##` domain headers carry - # their own nb/*.ipynb tables with no intervening `###`, so matching only `###` - # left `section` stale and mis-filed those links under the previous section. + # their own nb/*.ipynb tables, so matching only `###` mis-filed those links. for line in text.splitlines(): m = re.match(r"^#{1,6}\s+(.*)$", line) if m: @@ -107,8 +103,7 @@ def build_view( if not os.path.isdir(nb_dir): raise SystemExit(f"no nb/ dir under {dest}") - # An operator may route the VIEW through a symlink to persistent/mounted - # storage. Build inside its target instead of unlinking the routing. + # The VIEW may be a symlink to mounted storage; build inside its target. if os.path.islink(view): resolved = os.path.realpath(view) if not os.path.isdir(resolved): @@ -143,9 +138,8 @@ def build_view( if _OTHER in by_section and _OTHER not in order: order.append(_OTHER) - # Rebuild VIEW: drop the symlinks/empty folders we made last boot, but never - # the user's own files (VIEW is also JupyterLab's landing dir, so a user may - # have saved real notebooks here). + # Rebuild VIEW: drop our own symlinks/empty folders, never the user's files + # (VIEW is also JupyterLab's landing dir). _clear_view(view, os.path.realpath(dest)) os.makedirs(view, exist_ok = True) @@ -161,8 +155,7 @@ def build_view( if os.path.islink(link) and _points_into(link, os.path.realpath(dest)): os.remove(link) # replace our own stale symlink elif os.path.islink(link) or os.path.exists(link): - # a real user file/dir already occupies this name -- never - # clobber it; leave it and skip linking this notebook. + # a real user file occupies this name: keep it, skip linking. print(f"[unsloth-nb] view: keep user file, skip link {fname}", file = sys.stderr) continue os.symlink(rel, link) @@ -190,9 +183,8 @@ def _points_into(link, dest_real): def _clear_view(path, dest_real): # Tear down a previously built VIEW in place. It is also JupyterLab's landing - # dir, so user files/symlinks MUST survive: unlink only the symlinks we own - # (resolve into DEST, see _points_into) and rmdir only emptied folders. The - # VIEW root is never unlinked (build_view already resolved a symlinked root). + # dir, so user files/symlinks must survive: unlink only symlinks we own (see + # _points_into) and rmdir only emptied folders. The VIEW root is never unlinked. if os.path.islink(path) or not os.path.isdir(path): return for root, dirs, files in os.walk(path, topdown = False): diff --git a/docker/unsloth_pip_shim.py b/docker/unsloth_pip_shim.py index 793ad68cef..5d793303c7 100644 --- a/docker/unsloth_pip_shim.py +++ b/docker/unsloth_pip_shim.py @@ -47,8 +47,7 @@ _KEEP = { "unsloth_zoo", } _KEEP_PREFIX = ("nvidia-", "nvidia_") -# pip/uv flags that consume the following token as a value (so we don't mistake -# that value for a requirement). +# pip/uv flags that consume the next token as a value (not a requirement). _VALUE_FLAGS = { "-r", "--requirement", @@ -80,8 +79,7 @@ _VALUE_FLAGS = { "-e", "--editable", # Every remaining value-taking flag of pip/uv install (from both --help). A - # missing one makes the scanner misread its VALUE: `--torch-backend cu128 torch` - # dropped torch then exec'd uv with no target (hard-error). uv: + # missing one makes the scanner misread its VALUE. uv: "--allow-insecure-host", "--build-constraints", "-b", @@ -139,35 +137,30 @@ _VALUE_FLAGS = { "--requirements-from-script", "--uploaded-prior-to", } -# Value-flags whose VALUE is itself an install target: a requirements file pulls -# real requirements (index-url/find-links/constraint/target values are options). -# uv spells the long forms plural (--requirements/--constraints); include both. +# Value-flags whose VALUE is itself an install target (a requirements file pulls +# real requirements). uv spells the long forms plural; include both. _REQ_FILE_FLAGS = {"-r", "--requirement", "--requirements"} -# Constraint files aren't install targets, but pip applies their pins during -# resolution, so a -c that pins torch/transformers can still downgrade a baked -# package. Filter them like requirement files. (uv's long form is --constraints.) +# Constraint files aren't install targets, but pip applies their pins, so a -c +# pinning torch/transformers can downgrade a baked package. Filter like -r files. _CONSTRAINT_FILE_FLAGS = {"-c", "--constraint", "--constraints"} -# -e/--editable takes the NEXT token as a real install target. A -# protected editable must drop BOTH flag and value; dropping only the value -# leaves pip a dangling -e that swallows the next kept package and fails the cell. +# -e/--editable takes the next token as a real install target. A protected +# editable must drop BOTH flag and value, else a dangling -e swallows the next +# kept package and fails the cell. _EDITABLE_FLAGS = {"-e", "--editable"} -# -P/--upgrade-package and --reinstall-package are uv's selective upgrade flags: -# naming a baked package lets an ordinary target refresh it. Filter the value -# through _KEEP, dropping the flag+value pair for a protected name. Unlike -e, -# none is itself an install target. +# -P/--upgrade-package/--reinstall-package are uv's selective upgrade flags: +# filter the value through _KEEP, dropping the flag+value pair for a protected +# name. Unlike -e, none is itself an install target. _UPGRADE_PKG_FLAGS = {"-P", "--upgrade-package", "--reinstall-package"} -# Short value-flags accepted ATTACHED (flag glued to value): -rreqs.txt, -cX, -# -epath, -Pname. The scanner splits flag from value so it is filtered/classified, -# else an attached -r-only cell no-ops and -c/-e/-P bypasses _KEEP. +# Short value-flags accepted ATTACHED (-rreqs.txt, -cX, -epath, -Pname). Split +# flag from value so it's filtered, else -r no-ops and -c/-e/-P bypass _KEEP. _ATTACHED_SHORT_FLAGS = {"-r", "-c", "-e", "-P"} # Resolver-wide reinstall/ignore-installed switches (pip --force-reinstall, -# --ignore-installed, -I; uv --reinstall) rebuild already-satisfied baked deps; -# drop them (the kept target still installs). uv's --exact is destructive the -# other way (SYNC removes everything outside the target's closure), so drop it too. +# --ignore-installed, -I; uv --reinstall) rebuild baked deps; drop them (the kept +# target still installs). uv's --exact removes everything outside the closure, so +# drop it too. _REINSTALL_FLAGS = {"--force-reinstall", "--ignore-installed", "-I", "--reinstall", "--exact"} -# Value-flags whose flag+value pair is dropped outright. --upgrade-strategy eager -# would upgrade EVERY dep of a kept target; dropping it falls back to pip's -# only-if-needed default so satisfied protected deps stay. +# Value-flags dropped outright with their value. --upgrade-strategy eager would +# upgrade every dep of a kept target; dropping it falls back to only-if-needed. _DROP_VALUE_FLAGS = {"--upgrade-strategy"} @@ -198,9 +191,8 @@ def _canon(token): if the token is not a plain pkg spec (url / path / vcs / option).""" if token.startswith("-"): return None - # PEP 508 direct reference: "name [extras] @ ". The name is at the front, - # so pull it out BEFORE the url/vcs guard below, or a protected package pinned - # through a URL slips past _KEEP. Non-protected refs still return their name. + # PEP 508 direct reference: "name [extras] @ ". Pull the name out BEFORE + # the url/vcs guard below, else a protected package pinned via URL slips _KEEP. _dref = re.match( r"^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*@(?:\s|git\+|hg\+|bzr\+|svn\+|[a-z]+://)", token, @@ -208,37 +200,31 @@ def _canon(token): if _dref: return _dref.group(1).lower().replace("_", "-") or None if re.match(r"^[a-z]+\+", token) or "://" in token or token.startswith((".", "/")): - # A VCS/URL install can name a protected package via the legacy #egg=NAME - # (or &egg=NAME) fragment; pull it out so _KEEP can drop it, else the shim - # execs the URL and reinstalls a baked package. + # A VCS/URL install can name a protected package via the #egg=NAME + # fragment; pull it out so _KEEP can drop it. _egg = re.search(r"[#&]egg=([A-Za-z0-9][A-Za-z0-9._-]*)", token) if _egg: return _egg.group(1).lower().replace("_", "-") or None - # A wheel URL/path names its distribution in the PEP 427 filename, so a - # bare `pip install .../torch-2.11.0+cu128-...whl` would slip torch past - # _KEEP. Dashes can't appear in the distribution component, so the leading - # dash-split of the basename is the name; pull it so _KEEP can drop it. + # A wheel URL/path names its distribution in the PEP 427 filename (leading + # dash-split of the basename), so a bare torch-*.whl would slip _KEEP. _whl = re.search(r"([^/\\#?]+)\.whl(?:[#?]|$)", token) if _whl: dist = _whl.group(1).split("-", 1)[0].strip().lower().replace("_", "-") if dist: return dist - # A source archive URL/path names its distribution the same way - # ({name}-{version}.tar.gz), so match it against _KEEP too instead of - # passing it through as an opaque positional. + # A source archive ({name}-{version}.tar.gz) names its distribution too; + # match it against _KEEP instead of passing it through as opaque. _arch = _sdist_name(token.split("#", 1)[0].split("?", 1)[0].rstrip("/").rsplit("/", 1)[-1]) if _arch: return _arch - # A VCS URL without #egg= still installs a named project: the repo - # basename equals the distribution for the packages we protect - # (huggingface/transformers.git -> transformers). Infer it from the last - # path segment so a bare egg-less git+ URL can't reinstall past _KEEP. + # A VCS URL without #egg= still installs a named project; the repo basename + # equals the distribution for our protected packages. Infer from the last + # path segment so an egg-less git+ URL can't reinstall past _KEEP. if re.match(r"^[a-z]+\+", token): _rest = token.split("#", 1)[0].split("?", 1)[0] - # Drop the @ref from the PATH before taking the basename: a ref may - # contain a slash (@feature/foo) and dodge _KEEP. Split path from - # authority first so an SSH userinfo @ isn't mistaken for the ref; - # like pip, the ref is everything after the LAST @. + # Drop the @ref before the basename (a ref may contain a slash). Split + # path from authority first so an SSH userinfo @ isn't the ref; like + # pip, the ref is everything after the LAST @. if "://" in _rest: _authority, _slash, _path = _rest.partition("://")[2].partition("/") if "@" in _path: @@ -251,10 +237,8 @@ def _canon(token): _seg = _seg.strip().lower().replace("_", "-") if _seg: return _seg - # A local project DIRECTORY installs the project it contains; a same- - # version dev build slips past even the constraints file (which only - # rejects a MISMATCH). Resolve the name from its metadata so _KEEP applies - # like every other artifact form. Metadata-less dirs pass through. + # A local project DIRECTORY installs the project it contains; resolve its + # name from metadata so _KEEP applies. Metadata-less dirs pass through. _local = _local_project_name(token) if _local: return _local @@ -265,15 +249,13 @@ def _canon(token): _local = _local_project_name(token) if _local: return _local - # A bare wheel filename from the CWD (no ./ or scheme) is still a valid pip - # target; without this it falls through and misses _KEEP. Parse its PEP 427 - # distribution like the URL/path wheel case above. + # A bare wheel filename from the CWD is a valid pip target; parse its PEP 427 + # distribution like the URL/path wheel case above, else it misses _KEEP. if token.lower().endswith(".whl"): dist = token.rsplit("/", 1)[-1][:-4].split("-", 1)[0].strip().lower().replace("_", "-") if dist: return dist - # A bare source-archive filename from the CWD (`pip install torch-2.11.0.tar.gz`) - # is a valid pip target too; parse its distribution the same way. + # A bare source-archive filename from the CWD is a valid target too; parse it. _barch = _sdist_name(token.rsplit("/", 1)[-1]) if _barch: return _barch @@ -330,9 +312,8 @@ def _version_pin(token): return m.group(1) if m else None -# pip expands ${UPPERCASE_NAME} in requirements files after we classify the text, -# so `${PKG}==...` with PKG=torch would slip past _KEEP. Expand for CLASSIFICATION -# only; kept lines are forwarded verbatim. +# pip expands ${UPPERCASE_NAME} in requirements files, so `${PKG}==...` with +# PKG=torch would slip _KEEP. Expand for CLASSIFICATION only; kept lines verbatim. _ENV_REF_RE = re.compile(r"\$\{([A-Z0-9_]+)\}") @@ -410,11 +391,9 @@ def _rewrite_include(line, stripped, src_dir, depth): rebuilt += " " + comment return rebuilt + newline_char - # A remote (URL) nested include cannot be fetched/filtered here, so its - # protected pins would reach the real tool untouched. Drop the include line - # instead of letting pip pull an unfiltered requirements file off the network - # (mirrors the top-level remote `-r`/`-c` refusal in main). new_line=None - # tells the caller to remove the line entirely. + # A remote (URL) nested include can't be filtered here, so drop it rather than + # let pip pull unfiltered pins off the network (mirrors main's top-level + # refusal). new_line=None tells the caller to remove the line. if "://" in target: return None, True, None, [flag + " " + raw_target] abs_target = target if os.path.isabs(target) else os.path.join(src_dir, target) @@ -422,14 +401,12 @@ def _rewrite_include(line, stripped, src_dir, depth): if depth < 8: f_path, f_rec, f_drp = _filter_requirements_file(abs_target, _depth = depth + 1) # A nested -c include is a resolver CONSTRAINT, not an install request, so - # a transformers pin inside it must NOT be recorded as a request (mirrors - # the top-level -c path in main(), which ignores _c_rec). Only a nested -r - # requirement include carries real install requests, so keep its pin. + # don't record its transformers pin (mirrors main's -c path). Only -r + # includes carry real requests, so keep their pin. if flag in _CONSTRAINT_FILE_FLAGS: f_rec = None if f_path != abs_target: - # The include was rewritten (protected specs dropped and/or its own - # nested includes absolutised); point at the filtered copy. + # The include was rewritten; point at the filtered copy. return _emit(f_path), True, f_rec, f_drp # Nothing to filter inside; just make sure the path still resolves from /tmp. if not os.path.isabs(target): @@ -462,12 +439,10 @@ def _filter_requirements_file(path, _depth = 0): out.append(line) # comment / blank -> keep continue if stripped.startswith("-"): - # An editable requirement (-e/--editable ) inside the file is - # a real install target, so a protected editable such as - # `-e git+https://.../unsloth.git#egg=unsloth` would reinstall the - # baked stack. Classify it through _KEEP exactly like the - # command-line -e case and drop the whole line (flag + target) when - # the target is protected; a transformers pin is still recorded. + # An -e/--editable in the file is a real install target, so a + # protected editable would reinstall the baked stack. Classify through + # _KEEP like the command-line -e case; drop the whole line when + # protected (a transformers pin is still recorded). e_flag, e_target, _e_comment = _parse_flag_line(stripped, ("-e", "--editable")) if e_target is not None: _action, _ver = _classify_flag_target(_expand_env_refs(e_target)) @@ -480,8 +455,7 @@ def _filter_requirements_file(path, _depth = 0): out.append(line) # kept editable -> forward the line verbatim continue # Option or nested include. Recursively filter a nested `-r`/`-c` - # include (so protected specs deep in the include tree cannot slip - # past _KEEP) and repoint it so it still resolves from /tmp. + # include (protected specs deep in the tree) and repoint it for /tmp. new_line, rewrote, inc_rec, inc_drp = _rewrite_include(line, stripped, src_dir, _depth) if new_line is not None: out.append(new_line) # None -> a remote include was dropped @@ -516,9 +490,8 @@ def _filter_requirements_file(path, _depth = 0): with os.fdopen(fd, "w", encoding = "utf-8") as f: f.writelines(out) except OSError as exc: - # Fail CLOSED: protected requirements were detected in this file, so - # forwarding the original would hand pip exactly the specs we must - # filter. Abort the install with a clear error instead. + # Fail CLOSED: protected requirements were detected, so forwarding the + # original would hand pip the specs we must filter. Abort instead. raise SystemExit( f"[unsloth-nb] could not write a filtered copy of {path} ({exc}); " "refusing to forward a requirements file that pins protected packages." @@ -604,8 +577,7 @@ def main(): if argv[:1] == ["--unsloth-selfcheck-value-flags"]: _selfcheck_value_flags() - # Only intercept inside a notebook kernel (UNSLOTH_NB_SHIM set by the baked - # IPython startup and unsloth-run). Everywhere else (build install.sh, shells) + # Only intercept inside a notebook kernel (UNSLOTH_NB_SHIM); everywhere else # behave exactly like the real tool. if os.environ.get("UNSLOTH_NB_SHIM") != "1": os.execv(REAL[tool], [REAL[tool]] + argv) @@ -626,23 +598,19 @@ def main(): prev_flag = None for tok in tail: if skip_next: - # 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. + # -r/--requirement's value pulls real requirements (a target); an + # index-url / find-links / constraint value is an option, not a target. if prev_flag in _REQ_FILE_FLAGS or prev_flag in _CONSTRAINT_FILE_FLAGS: if "://" in tok: - # Remote requirement/constraint file: it cannot be inspected - # or filtered, so refuse it in shim mode rather than let the - # real tool fetch and install protected pins off the network. - # The flag was appended when we first saw it; pop it so pip/uv - # is not left a dangling -r/-c. + # Remote requirement/constraint file: can't be filtered, so + # refuse it rather than fetch protected pins off the network. + # Pop the flag we appended so pip/uv has no dangling -r/-c. if keep_args and keep_args[-1] == prev_flag: keep_args.pop() dropped.append(prev_flag + " " + tok) elif prev_flag in _REQ_FILE_FLAGS: - # Filter baked/protected packages out of the requirements file - # so a notebook `pip install -r reqs.txt` cannot clobber the - # cu128 stack or push transformers into the base venv. + # Filter protected packages out of the requirements file so + # `pip install -r reqs.txt` can't clobber the cu128 stack. _req_path, _req_rec, _req_drp = _filter_requirements_file(tok) keep_args.append(_req_path) has_target = True @@ -650,24 +618,22 @@ def main(): recorded = _req_rec dropped.extend(_req_drp) else: - # Strip protected pins from the constraint file so it cannot - # downgrade the baked stack, but a constraint is not an install - # target and its transformers pin is not an install request, so - # do not set has_target / recorded here. + # Strip protected pins from the constraint file so it can't + # downgrade the baked stack; a constraint isn't an install + # target, so don't set has_target / recorded here. _c_path, _c_rec, _c_drp = _filter_requirements_file(tok) keep_args.append(_c_path) dropped.extend(_c_drp) elif prev_flag in _DROP_VALUE_FLAGS: - # --upgrade-strategy (eager): pop the appended flag and drop the - # pair so pip falls back to only-if-needed. + # --upgrade-strategy (eager): drop the pair so pip falls back to + # only-if-needed. if keep_args and keep_args[-1] == prev_flag: keep_args.pop() dropped.append(prev_flag + " " + tok) elif prev_flag in _EDITABLE_FLAGS or prev_flag in _UPGRADE_PKG_FLAGS: - # The flag was held back: its value is an install target (-e) or - # upgrade selector (-P), both filtered through _KEEP. Dropping a - # protected value drops the flag too (no dangling -e/-P). A kept - # editable sets has_target; -P does not. + # Flag held back: its value is an install target (-e) or upgrade + # selector (-P), filtered through _KEEP. A protected value drops + # the flag too. A kept editable sets has_target; -P does not. _action, _ver = _classify_flag_target(tok) if _action == "drop": if _ver and not recorded: @@ -683,16 +649,14 @@ def main(): skip_next = False prev_flag = None continue - # --flag=value form (--requirement=reqs.txt / --index-url=URL as one - # token). Without this it is kept as an opaque option, the -r file is never - # filtered, and a file-only cell silently installs nothing. + # --flag=value form (--requirement=reqs.txt / --index-url=URL as one token). + # Without this the -r file is never filtered and a file-only cell no-ops. if tok.startswith("--") and "=" in tok: _flag, _, _val = tok.partition("=") if _flag in _VALUE_FLAGS: if (_flag in _REQ_FILE_FLAGS or _flag in _CONSTRAINT_FILE_FLAGS) and "://" in _val: # Remote requirement/constraint file in `--flag=URL` form: - # refuse it in shim mode (the flag rides in the same token, so - # dropping the token leaves nothing dangling). + # refuse it (dropping the token leaves nothing dangling). dropped.append(tok) elif _flag in _REQ_FILE_FLAGS: _req_path, _req_rec, _req_drp = _filter_requirements_file(_val) @@ -709,8 +673,7 @@ def main(): dropped.extend(_c_drp) elif _flag in _EDITABLE_FLAGS or _flag in _UPGRADE_PKG_FLAGS: # --editable= / --upgrade-package=: filter the - # inline value through _KEEP just like the space-separated - # form, dropping the whole token for a protected package. + # inline value through _KEEP, dropping the token if protected. _action, _ver = _classify_flag_target(_val) if _action == "drop": if _ver and not recorded: @@ -724,15 +687,13 @@ def main(): keep_args.append(tok) # option with inline value, not a target continue # Attached short value-flag form (-rreqs.txt, -cX, -epath, -Pname as ONE - # token). Without this it falls through as an opaque option: an -r-only - # cell no-ops and -c/-e/-P bypasses _KEEP. Split the flag from its value - # and reuse the separated-form handling. + # token). Split flag from value and reuse the separated-form handling, + # else -r no-ops and -c/-e/-P bypass _KEEP. if len(tok) > 2 and tok[0] == "-" and tok[1] != "-" and tok[:2] in _ATTACHED_SHORT_FLAGS: _sflag, _sval = tok[:2], tok[2:] if (_sflag in _REQ_FILE_FLAGS or _sflag in _CONSTRAINT_FILE_FLAGS) and "://" in _sval: # Remote requirement/constraint file in attached `-rURL`/`-cURL` - # form: refuse it in shim mode (nothing was appended yet, so just - # drop the whole token). + # form: refuse it (nothing appended yet, drop the whole token). dropped.append(_sflag + " " + _sval) elif _sflag in _REQ_FILE_FLAGS: _req_path, _req_rec, _req_drp = _filter_requirements_file(_sval) @@ -761,16 +722,14 @@ def main(): continue if tok in _REINSTALL_FLAGS: # Resolver-wide reinstall / ignore-installed switch: drop it so pip/uv - # cannot rebuild already-satisfied baked deps (torch/transformers - # pulled in by a kept target). The kept target still installs. + # can't rebuild satisfied baked deps. The kept target still installs. dropped.append(tok) continue if tok in _VALUE_FLAGS: - # -e/--editable and -P/--upgrade-package carry a value that is a - # potential install target, so hold the flag back and let the - # skip_next handler emit or drop the flag+value pair together. Every - # other value-flag keeps its flag verbatim; only its value (an - # index-url / find-links / target dir / etc.) is an opaque option. + # -e/--editable and -P/--upgrade-package carry a potential install + # target, so hold the flag back and let skip_next emit or drop the + # pair together. Every other value-flag keeps its flag verbatim; only + # its value is an opaque option. if tok not in _EDITABLE_FLAGS and tok not in _UPGRADE_PKG_FLAGS: keep_args.append(tok) skip_next = True @@ -808,15 +767,14 @@ def main(): if dropped: print("[unsloth-nb] kept baked versions, skipped: " + " ".join(dropped)) - # Anything left to install? has_target was set for a kept spec, a positional - # url/path/vcs/editable, or a -r file. A line with only baked packages + option - # flags leaves no target, so no-op instead of exec'ing a bare install that fails. + # Anything left to install? A line with only baked packages + option flags + # leaves no target, so no-op instead of exec'ing a bare install that fails. if not has_target: print("[unsloth-nb] nothing to install after keeping the baked stack; ok.") return cmd = [REAL[tool]] + head + keep_args # Constrain the resolver too: an allowed target could pull an incompatible - # torch/transformers in as a DEPENDENCY and replace the baked wheel. + # torch/transformers in as a dependency and replace the baked wheel. constraints = _protected_constraints_file() if constraints: cmd += ["--constraint", constraints] diff --git a/docker/unsloth_run.py b/docker/unsloth_run.py index b4591c7151..5a68644fd9 100644 --- a/docker/unsloth_run.py +++ b/docker/unsloth_run.py @@ -71,10 +71,9 @@ def main(): want = args.tf or pin or (compat.tier_for_model(model) if compat else None) sidecar = compat.sidecar_for(want) if (compat and want) else None - # Materialise the notebook for nbconvert. With --out, stage the input copy and - # the result as temp files NEXT TO the destination (same dir, so kernel cwd - # matches and publish is one atomic os.replace) and only publish on success -- - # a timeout / failed cell / missing kernel must not destroy the previous output. + # Materialise the notebook for nbconvert. With --out, stage input + result as + # temp files next to the destination (same dir => atomic os.replace publish) + # and publish only on success, so a failed run can't destroy the old output. tmp_dir = None tmp_files = [] publish_from = None @@ -104,8 +103,7 @@ def main(): env = dict(os.environ) env["UNSLOTH_NB_SHIM"] = "1" # enable safe-install for the notebook's cells # Per-run marker unless the caller pinned one: the shared default would leak - # this run's transformers pin into later or concurrent runs in the same - # container (their kernels would activate a stale sidecar). An empty marker + # this run's transformers pin into concurrent/later runs. An empty marker # reads as "no pin", so pre-creating it is safe. marker = env.get("UNSLOTH_NB_TF_MARKER") if not marker: @@ -149,8 +147,7 @@ def main(): if rc == 0 and publish_from is not None: os.replace(publish_from, out_path) finally: - # Clean up the temp dir we materialised a downloaded notebook into and - # any staging files left next to --out (already gone when published). + # Clean up the temp dir and any staging files (already gone when published). if tmp_dir is not None: shutil.rmtree(tmp_dir, ignore_errors = True) for p in tmp_files: diff --git a/docker/unsloth_studio_update.sh b/docker/unsloth_studio_update.sh index e1c534bde4..fbda364fe4 100755 --- a/docker/unsloth_studio_update.sh +++ b/docker/unsloth_studio_update.sh @@ -65,9 +65,9 @@ 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" - # unsloth-zoo does NOT track unsloth's tags/SHAs (different cadence). Use - # --zoo-ref if given; else the unsloth ref only when the zoo repo has it, - # falling back to main so `--ref ` doesn't fail on a missing ref. + # unsloth-zoo does NOT track unsloth's tags (different cadence). Use --zoo-ref + # if given; else the unsloth ref only when the zoo repo has it, falling back to + # main. _zoo_ref="$ZOO_REF" if [ -z "$_zoo_ref" ]; then if git ls-remote --exit-code https://github.com/unslothai/unsloth-zoo.git \ @@ -90,8 +90,8 @@ fi echo "[studio-update] after: unsloth $(version_of)" -# Sanity: the backend must still import after the swap (a missing transitive -# dep from --no-deps shows up here). Non-fatal: just warn with the remedy. +# Sanity: the backend must still import after the swap (a missing --no-deps +# transitive dep shows up here). Non-fatal: just warn with the remedy. if ! "$PY" -c "import studio.backend.main" >/dev/null 2>&1; then echo "[studio-update] WARNING: 'import studio.backend.main' failed after update." >&2 echo "[studio-update] A new dependency may be missing. Re-run with --with-deps:" >&2 diff --git a/docker/unsloth_sync_notebooks.sh b/docker/unsloth_sync_notebooks.sh index a1406cee8b..78de1e1b77 100644 --- a/docker/unsloth_sync_notebooks.sh +++ b/docker/unsloth_sync_notebooks.sh @@ -1,14 +1,12 @@ #!/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 notebooks are -# present instantly and offline. On boot this copies the template into -# /workspace/unsloth-notebooks (first run) then best-effort refreshes from GitHub -# when upstream advances. +# On boot this copies the baked read-only template into /workspace/unsloth-notebooks +# (first run), then best-effort refreshes from GitHub when upstream advances. # -# The user's edits ALWAYS win: we record each written file's hash; on refresh a -# file whose hash differs is treated as user-modified and left untouched. So a -# refresh only updates unchanged files and adds new ones, never clobbering edits. +# The user's edits ALWAYS win: each written file's hash is recorded; on refresh a +# file whose hash differs is left untouched. So a refresh only updates unchanged +# files and adds new ones. # # Opt-out / tuning (all optional): # UNSLOTH_SKIP_NOTEBOOK_SYNC=1 do nothing (no populate, no refresh) @@ -36,8 +34,7 @@ SYNCED="$DEST/.unsloth_sync_commit" # upstream commit we last synced to TIMEOUT="${UNSLOTH_NOTEBOOK_FETCH_TIMEOUT:-60}" # Resolve a helper script ($1 override, $2 PATH command, $3 sibling filename), -# echoing the path or nothing (empty lets the caller degrade). Used for SIG, -# VIEW and STRIP helpers. +# echoing the path or nothing. Used for SIG, VIEW and STRIP helpers. PYBIN="$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)" _self_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" resolve_helper() { @@ -50,10 +47,9 @@ SIG_HELPER="$(resolve_helper "${UNSLOTH_NB_SIG_HELPER:-}" unsloth-nb-content-sig VIEW_HELPER="$(resolve_helper "${UNSLOTH_NB_VIEW_HELPER:-}" unsloth-nb-view unsloth_nb_view.py)" STRIP_HELPER="$(resolve_helper "${UNSLOTH_NB_STRIP_HELPER:-}" unsloth-nb-strip-colab unsloth_nb_strip_colab.py)" -# True only when both are .ipynb, the SIG helper is usable, and it reports the -# non-boilerplate middle (ignoring install header/announcements/footer) identical, -# so a refresh doesn't rewrite an untouched notebook when only boilerplate moved. -# Any failure returns false (caller falls back to a normal refresh). +# True only when both are .ipynb and the SIG helper reports the non-boilerplate +# middle identical, so a refresh doesn't rewrite a notebook when only boilerplate +# moved. Any failure returns false. middle_unchanged() { case "$1" in *.ipynb) : ;; *) return 1 ;; esac [ -n "$PYBIN" ] && [ -n "$SIG_HELPER" ] || return 1 @@ -86,9 +82,8 @@ nb_gpu_is_amd() { return 1 # default: treat as non-AMD (hide AMD-* notebooks) } -# Rebuild the sibling symlink VIEW (categorized folders mirroring the README -# headers) from scratch. Symlinks live OUTSIDE $DEST, so the sync state machine -# (find -type f) never sees them. +# Rebuild the sibling symlink VIEW from scratch. Symlinks live OUTSIDE $DEST, so +# the sync state machine (find -type f) never sees them. build_categorized_view() { [ "${UNSLOTH_SKIP_NOTEBOOK_VIEW:-0}" = "1" ] && return 0 [ -n "$PYBIN" ] && [ -n "$VIEW_HELPER" ] || return 0 @@ -136,9 +131,8 @@ if [ ! -f "$STATE" ]; then case "$rel" in .unsloth_template_commit) continue ;; esac mkdir -p "$DEST/$(dirname "$rel")" 2>/dev/null || true # A pre-existing file (bind-mounted or hand-created) is user data: keep it - # and do NOT record it -- if recorded, the refresh below would see a hash - # match, treat it as pristine and overwrite it. Only files we lay down (or - # that match the template byte-for-byte) are recorded as managed. + # and do NOT record it, else the refresh below would treat it as pristine + # and overwrite it. Only files we lay down 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" @@ -154,10 +148,8 @@ if [ ! -f "$STATE" ]; then fi # 1b) Every-boot OFFLINE restore of deleted notebooks: a file we wrote that the -# user has since DELETED comes back from the baked template (no network). Existing -# files are never touched (can't clobber an edit); the restored hash is reset to -# the template's so the refresh below bumps it to latest. Opt out with -# UNSLOTH_KEEP_DELETED_NOTEBOOKS=1. +# user DELETED comes back from the baked template (no network). Existing files are +# never touched. Opt out with UNSLOTH_KEEP_DELETED_NOTEBOOKS=1. if [ -f "$STATE" ] && [ "${UNSLOTH_KEEP_DELETED_NOTEBOOKS:-0}" != "1" ]; then restored=0 RS_TMP="$(mktemp)" @@ -229,9 +221,8 @@ while IFS= read -r -d '' f; do continue fi elif [ -n "${LAST[$rel]:-}" ] && [ "${UNSLOTH_KEEP_DELETED_NOTEBOOKS:-0}" = "1" ]; then - # We wrote this notebook and the user DELETED it. With the opt-out set, - # honor the deletion instead of restoring it from the fresh clone. Keep - # the record so it stays known as managed-but-deleted. + # We wrote this notebook and the user DELETED it; with the opt-out set, + # honor the deletion. Keep the record as managed-but-deleted. printf '%s %s\n' "${LAST[$rel]}" "$rel" >> "$TMPSTATE" kept=$((kept + 1)) continue diff --git a/install.ps1 b/install.ps1 index 3e674fe15b..fe477714da 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1964,9 +1964,8 @@ exit 0 # Mirrors Get-PytorchCudaTag in setup.ps1. function Get-TorchIndexUrl { $baseUrl = if ($env:UNSLOTH_PYTORCH_MIRROR) { $env:UNSLOTH_PYTORCH_MIRROR.TrimEnd('/') } else { "https://download.pytorch.org/whl" } - # Explicit override (parity with install.sh): - # UNSLOTH_TORCH_INDEX_FAMILY=cu128|cu130|cu126|cpu|... pins the wheel index - # when probing is wrong or impossible (no GPU, containers, CI). + # Explicit override (parity with install.sh): UNSLOTH_TORCH_INDEX_FAMILY= + # cu128|cu130|cu126|cpu|... pins the wheel index when probing can't (no GPU, CI). if ($env:UNSLOTH_TORCH_INDEX_FAMILY) { return "$baseUrl/$($env:UNSLOTH_TORCH_INDEX_FAMILY)" } if (-not $NvidiaSmiExe) { return "$baseUrl/cpu" } try { diff --git a/install.sh b/install.sh index e119ec8049..16edee7b22 100755 --- a/install.sh +++ b/install.sh @@ -2006,10 +2006,8 @@ fi _REPO_ROOT="$(cd "$(dirname "$0" 2>/dev/null || echo ".")" && pwd)" # ── unsloth-zoo overlay ref (for --local installs) ── -# --local overlays unsloth-zoo from git so the Studio venv tracks the same zoo as -# the editable unsloth checkout. Honor UNSLOTH_ZOO_REF (the Docker publish -# workflow forwards one ref to both builds) so the image runs the requested zoo. -# Unset -> main, byte-identical to the previous bare git URL. +# Honor UNSLOTH_ZOO_REF so the Studio venv tracks the requested zoo (the Docker +# publish workflow forwards one ref to both builds). Unset -> main. _ZOO_REF="${UNSLOTH_ZOO_REF:-main}" _ZOO_GIT_SPEC="unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo@${_ZOO_REF}" @@ -2077,10 +2075,9 @@ _has_amd_rocm_gpu() { get_torch_index_url() { _base="${UNSLOTH_PYTORCH_MIRROR:-https://download.pytorch.org/whl}" _base="${_base%/}" - # Explicit pin for hosts where probing is impossible (Docker builds, CI). - # Names the index leaf: UNSLOTH_TORCH_INDEX_FAMILY=cu128|cu130|cu126|rocm7.2|cpu|... - # The Blackwell build uses this: no GPU/nvidia-smi at build time, but the image - # targets CUDA, so probing would land on cpu (CI) or cu126 wheels. + # Explicit pin for hosts where probing is impossible (Docker builds, CI): + # UNSLOTH_TORCH_INDEX_FAMILY=cu128|cu130|cu126|rocm7.2|cpu|... names the index + # leaf. The Blackwell build needs it: no GPU at build time but a CUDA target. if [ -n "${UNSLOTH_TORCH_INDEX_FAMILY:-}" ]; then echo "$_base/${UNSLOTH_TORCH_INDEX_FAMILY}"; return fi diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index d7bdb81f14..15672df6d7 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -4995,9 +4995,8 @@ def activate_staged_dir(staging_dir: Path, dst: Path) -> None: try: os.replace(staging_dir, dst) except OSError as exc: - # Busy/in-use (Windows AV) OR cross-device (overlayfs in a Docker build): - # both are safe to complete by copying the staging tree and removing it. - # Anything else (disk full, missing path) re-raises. + # Busy/in-use (Windows AV) or cross-device (Docker overlayfs): both safe to + # complete by copy + remove. Anything else (disk full, missing path) re-raises. if not (is_busy_lock_error(exc) or is_cross_device_error(exc)): raise log(f"os.replace failed ({exc!r}); falling back to file-by-file copy of staging tree") diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index 93344a309c..dbc811ffcf 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -1027,9 +1027,8 @@ def _detect_cuda_torch_index_url() -> str: Defaults to cu126 when nvidia-smi is missing or the version is unreadable (e.g. NVIDIA detected only via the /proc/driver/nvidia/gpus fallback). """ - # Explicit override (parity with install.sh / install.ps1): - # UNSLOTH_TORCH_INDEX_FAMILY=cu128|cu130|cu126|cpu|... pins the wheel index - # when probing is wrong or impossible (no GPU at build time, CI). + # Explicit override (parity with install.sh / install.ps1): UNSLOTH_TORCH_INDEX_FAMILY= + # cu128|cu130|cu126|cpu|... pins the wheel index when probing can't (no GPU, CI). family = os.environ.get("UNSLOTH_TORCH_INDEX_FAMILY") if family: return f"{_PYTORCH_WHL_BASE}/{family}" @@ -2069,9 +2068,8 @@ def install_python_stack() -> int: package_name = os.environ.get("STUDIO_PACKAGE_NAME", "unsloth") # --local overlays a local repo checkout after updating deps. local_repo = os.environ.get("STUDIO_LOCAL_REPO", "") - # unsloth-zoo git ref for the --local overlay. Honor UNSLOTH_ZOO_REF (the - # publish workflow / unsloth-studio-update forward one ref) so the Studio venv - # tracks the requested zoo, not always main. Unset -> main. + # unsloth-zoo git ref for the --local overlay. Honor UNSLOTH_ZOO_REF so the + # Studio venv tracks the requested zoo, not always main. Unset -> main. zoo_ref = os.environ.get("UNSLOTH_ZOO_REF", "").strip() or "main" zoo_git_spec = "unsloth-zoo @ git+https://github.com/unslothai/unsloth-zoo@" + zoo_ref base_total = 11 if IS_WINDOWS else 12 # +1 for the anyio repair check (step 8b) diff --git a/tests/python/test_unsloth_pip_shim.py b/tests/python/test_unsloth_pip_shim.py index f6b3a74869..7a9d818bed 100644 --- a/tests/python/test_unsloth_pip_shim.py +++ b/tests/python/test_unsloth_pip_shim.py @@ -101,11 +101,9 @@ def _run(shim, tool, args): # -------------------------------------------------------------------------- -# Item 3541142907 -- pair -e/--editable with its target (the attached short -# `-e` form from item 3541404845 is folded in here). A protected -# editable such as `pip install -e git+...unsloth...#egg=unsloth peft` must -# NOT become `pip install -e peft` (which pip rejects): the flag drops WITH -# its value, and an unprotected editable is forwarded verbatim. +# Item 3541142907 -- pair -e/--editable with its target. A protected editable +# drops the flag WITH its value (never `pip install -e peft`); an unprotected +# editable is forwarded verbatim. # -------------------------------------------------------------------------- UNSLOTH_VCS = "git+https://github.com/unslothai/unsloth.git#egg=unsloth" @@ -497,10 +495,9 @@ def test_upgrade_strategy_forms(shim, args, expected): # -------------------------------------------------------------------------- -# Resolver-level protection: every forwarded install carries a constraints -# file pinning the installed protected packages, so a kept target's -# DEPENDENCY on an incompatible torch/transformers/etc. fails loudly instead -# of replacing the baked wheel. +# Resolver-level protection: every forwarded install carries a constraints file +# pinning the installed protected packages, so a kept target's dependency on an +# incompatible torch/transformers fails loudly instead of replacing the wheel. # -------------------------------------------------------------------------- def _raw_execd(shim, tool, args): """Like _run but WITHOUT stripping the injected constraint pair.""" @@ -658,11 +655,9 @@ def test_local_dir_without_metadata_passes_through(shim, tmp_path): # -------------------------------------------------------------------------- # Item 3592835033 -- every uv/pip value-taking flag must be in _VALUE_FLAGS. -# `uv pip install --torch-backend cu128 torch` used to drop the protected -# torch but keep the SEPARATED flag pair, exec'ing uv with no install target -# at all (uv hard-errors) instead of no-oping like the attached `=` form; and -# `--extra torch peft` misread the extra NAME "torch" as a protected target, -# leaving a dangling `--extra` that swallowed peft. +# `--torch-backend cu128 torch` used to drop torch but keep the separated flag +# pair, exec'ing uv with no target; `--extra torch peft` misread the extra NAME +# "torch" as a target, leaving a dangling `--extra` that swallowed peft. @pytest.mark.parametrize( @@ -721,11 +716,9 @@ def _value_flags_from_help(cmd): # The help-derived drift guards are OPT-IN: repo CI runs whatever pip/uv are -# current that week, so a hard assert here turns every upstream flag addition -# into an unrelated red PR. The authoritative check runs at image BUILD time -# against the exact baked tools (`unsloth_pip_shim.py -# --unsloth-selfcheck-value-flags` in the Dockerfile verify step); set -# UNSLOTH_SHIM_FLAG_DRIFT_CHECK=1 to run these locally. +# current, so a hard assert would turn every upstream flag addition into a red +# PR. The authoritative check runs at image BUILD time against the baked tools +# (--unsloth-selfcheck-value-flags); set UNSLOTH_SHIM_FLAG_DRIFT_CHECK=1 locally. _DRIFT_OPT_IN = os.environ.get("UNSLOTH_SHIM_FLAG_DRIFT_CHECK") == "1" @@ -750,10 +743,8 @@ def test_uv_help_value_flags_all_classified(shim): # -------------------------------------------------------------------------- -# Item 3592947879 -- a VCS @ref may itself contain a slash (@feature/foo); -# the ref must be stripped from the PATH before the last-segment split, or -# `git+https://github.com/unslothai/unsloth.git@feature/foo` canonicalizes as -# "foo" and a protected repo installed from a branch dodges _KEEP. +# Item 3592947879 -- a VCS @ref may contain a slash (@feature/foo); strip it +# before the last-segment split, else the ref's basename dodges _KEEP. @pytest.mark.parametrize( diff --git a/tests/sh/test_select_cuda_jit_tools.sh b/tests/sh/test_select_cuda_jit_tools.sh index a626b3ac7b..c72d3d1a41 100755 --- a/tests/sh/test_select_cuda_jit_tools.sh +++ b/tests/sh/test_select_cuda_jit_tools.sh @@ -30,11 +30,9 @@ assert_eq() { } # $1 = compute_cap(s) the mock nvidia-smi reports, ONE PER LINE ("none" -> no -# nvidia-smi). A multi-line value models a mixed-GPU host (checks every cap is -# scanned). $2 (optional) = the target libnvrtc.so.12 starts on; defaults to the -# cu12.8 default, "libnvrtc.so.12.cu13" models a stale link from an earlier boot. -# Builds a fake Studio venv NVRTC dir as the build stages it and runs the function -# via UNSLOTH_STUDIO_HOME. Prints " ". +# nvidia-smi; multi-line models a mixed-GPU host). $2 (optional) = the initial +# libnvrtc.so.12 target (default cu12.8; "libnvrtc.so.12.cu13" models a stale +# link). Builds a fake Studio venv NVRTC dir. Prints " ". run_select() { _cap="$1" _init="${2:-libnvrtc.so.12.cu128.orig}" diff --git a/tests/validate_studio_features.py b/tests/validate_studio_features.py index 4f4c2cdd82..948dc37a00 100644 --- a/tests/validate_studio_features.py +++ b/tests/validate_studio_features.py @@ -56,8 +56,7 @@ def test_colab_compat() -> None: # non-magic cell untouched plain = ["x = 1\n", "y = 2\n"] check("plain cell untouched", m.colab_cell_magic_fix(plain) == plain) - # content/data magic (%%writefile) NOT hoisted -- never inject the #@title - # comment into the written file body + # content magic (%%writefile) NOT hoisted into the written file body wf = ["#@title Config\n", "%%writefile config.json\n", "{}\n"] check("content magic (%%writefile) left untouched", m.colab_cell_magic_fix(wf) == wf) # safe magic with arg still hoisted diff --git a/unsloth/_gpu_init.py b/unsloth/_gpu_init.py index fe8fb619bb..8c9353df20 100644 --- a/unsloth/_gpu_init.py +++ b/unsloth/_gpu_init.py @@ -114,13 +114,11 @@ del maybe_set_windows_rocm_bnb_version # Fixes https://github.com/unslothai/unsloth/issues/1266 os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" -# `docker --gpus '"device=N"'` sets only NVIDIA_VISIBLE_DEVICES to specific ids -# and leaves CUDA_VISIBLE_DEVICES absent, so Inductor's compile-worker pool can't -# enumerate the cgroup-pinned GPU and raises "Could not find an active GPU -# backend". Force a single in-process compile thread so the pool never spawns. -# Gate only on the cgroup-pinned fingerprint (specific ids); "all"/"none"/"void"/"" -# (the `--gpus all` default) must NOT trigger it. Opt out with -# UNSLOTH_FORCE_SINGLE_COMPILE_WORKER=0. +# `docker --gpus '"device=N"'` sets NVIDIA_VISIBLE_DEVICES but not +# CUDA_VISIBLE_DEVICES, so Inductor's compile-worker pool can't enumerate the +# cgroup-pinned GPU ("Could not find an active GPU backend"). Force a single +# in-process compile thread. Trigger only on pinned ids, not "all"/"none"/"void"/"" +# (the `--gpus all` default). Opt out with UNSLOTH_FORCE_SINGLE_COMPILE_WORKER=0. _nvd = os.environ.get("NVIDIA_VISIBLE_DEVICES", "").strip().lower() _cgroup_pinned = _nvd not in ("", "all", "none", "void") if ( @@ -128,8 +126,7 @@ if ( and _cgroup_pinned and "CUDA_VISIBLE_DEVICES" not in os.environ ): - # Set the env var if absent (honour an existing value), but always plant the - # sentinel so the zoo-side patch preserves the forcing. + # Honour an existing thread count; always plant the sentinel for the zoo patch. if os.environ.get("TORCHINDUCTOR_COMPILE_THREADS") in (None, "", "1"): os.environ["TORCHINDUCTOR_COMPILE_THREADS"] = "1" os.environ["UNSLOTH_FORCE_SINGLE_COMPILE_WORKER"] = "1" @@ -179,11 +176,10 @@ except ModuleNotFoundError: except: raise -# Re-assert the single-compile-worker policy after unsloth_zoo's -# patch_torch_compile (which historically popped TORCHINDUCTOR_COMPILE_THREADS). -# Force the Inductor config directly so the bug is fixed even against an older -# unsloth_zoo, and monkey-patch the zoo's determine_compile_threads so the -# per-call options dict always sees 1. No-op when the user opted out. +# Re-assert single-compile-worker after unsloth_zoo's patch_torch_compile (which +# historically popped TORCHINDUCTOR_COMPILE_THREADS). Set the Inductor config +# directly and patch the zoo's determine_compile_threads so every options dict +# sees 1. No-op when the user opted out. if os.environ.get("UNSLOTH_FORCE_SINGLE_COMPILE_WORKER", "0") == "1": try: torch._inductor.config.compile_threads = 1 @@ -304,7 +300,7 @@ del patch_accelerate_recursively_apply # Torch 2.4 has including_emulation if DEVICE_TYPE == "cuda" and not torch.cuda.is_available(): # UNSLOTH_ALLOW_CPU=1 keeps DEVICE_TYPE "cuda" on driverless hosts; probing - # would raise. bf16 stays on (CPU bf16 kernels exist, fp16 largely don't). + # would raise. bf16 on (CPU bf16 kernels exist, fp16 largely don't). SUPPORTS_BFLOAT16 = True torch.cuda.is_bf16_supported = lambda *args, **kwargs: True elif DEVICE_TYPE == "cuda": diff --git a/unsloth/dataprep/synthetic.py b/unsloth/dataprep/synthetic.py index e057052f02..b94adef3b4 100644 --- a/unsloth/dataprep/synthetic.py +++ b/unsloth/dataprep/synthetic.py @@ -267,9 +267,8 @@ class SyntheticDataKit: stderr = subprocess.PIPE, start_new_session = True, ) - # vLLM <= 0.18 logs "Starting vLLM API server on ..."; 0.19 renamed it - # to "Starting vLLM server on ...". Accept both, with the optional - # server index some versions insert before "on". + # Accept both "Starting vLLM API server on" (<= 0.18) and "Starting vLLM + # server on" (0.19), with the optional server index some versions insert. ready_re = re.compile(r"Starting vLLM(?:\s+API)?\s+server(?:\s+\d+)?\s+on\b") self.vllm_process = vllm_process self.stdout_capture = PipeCapture( @@ -285,22 +284,19 @@ class SyntheticDataKit: keep_lines = 2000, echo = False, name = "vLLM STDERR", - # vLLM >= 0.19 emits the startup lines through logging, which writes - # to STDERR; watching stdout alone makes a healthy server look like a - # timeout and get killed. + # vLLM >= 0.19 logs startup lines to STDERR; watching stdout alone + # makes a healthy server look like a timeout and get killed. ready_regex = ready_re, text = False, ) # we don't print stderr to console but self.stderr_capture.tail(200) will print the last 200 lines ready = False - # timeout None/0 keeps the previous Event.wait(None): wait indefinitely - # for readiness (large models / slow downloads). A positive value is a deadline. + # timeout None/0 waits indefinitely (large models / slow downloads); + # a positive value is a deadline. deadline = (time.monotonic() + timeout) if timeout else None while True: - # Cap the final wait to the remaining budget so a fractional - # timeout stays a real deadline instead of overshooting by up - # to a full second. + # Cap the wait to the remaining budget so we don't overshoot the deadline. _wait = 1 if deadline is None else min(1, deadline - time.monotonic()) if _wait <= 0: break diff --git a/unsloth/models/vision.py b/unsloth/models/vision.py index a3d37c935a..f20f32cc3e 100644 --- a/unsloth/models/vision.py +++ b/unsloth/models/vision.py @@ -398,10 +398,9 @@ def unsloth_base_fast_generate(self, *args, **kwargs): ): kwargs.pop("mm_token_type_ids", None) - # VLMs do not allow logits_to_keep. transformers >= 5.0 sets logits_to_keep=1 - # itself in GenerationMixin.generate AFTER _validate_model_kwargs, so pre- - # injecting it makes the strict validator raise on PEFT models. Skip on v5+ - # and strip any leaked kwarg defensively. + # VLMs do not allow logits_to_keep. transformers >= 5.0 sets it itself in + # generate() after _validate_model_kwargs, so pre-injecting makes the strict + # validator raise on PEFT models. Skip on v5+ and strip any leaked kwarg. if Version(transformers_version) < Version("5.0.0.dev0"): global NUM_LOGITS_TO_KEEP if arch not in NUM_LOGITS_TO_KEEP: