Commit graph

56 commits

Author SHA1 Message Date
Daniel Han
faf1821fcb docker: keep transformers sidecar selection inside what the baked vLLM can import
The image runs unslothai/notebooks unchanged by refusing a notebook's
transformers pin and activating a baked sidecar on sys.path instead. Selection
was a pure ceiling (smallest baked version >= the request) and ignored that vLLM
is version-locked to transformers, so two of the four baked sidecars could not be
imported by the baked vLLM 0.26.0 at all:

  4.57.6  ImportError: Support for Transformers v4 is deprecated and was removed
          in vLLM v0.24.0
  5.3.0   ImportError: cannot import name 'ALLOWED_LAYER_TYPES' from
          transformers.configuration_utils

Those two are exactly the ones the common pins select. 241 notebooks pin
4.48/4.52.3/4.55.4/4.56.1/4.56.2/4.57.x and land on the 4.57.6 sidecar, 13 pin
5.2.0/5.3.0 and land on the 5.3.0 sidecar. All 254 died at
`from unsloth import FastModel`, before the first model cell. Pointing
UNSLOTH_TF_SIDECAR_ROOT at an empty directory and changing nothing else turned
Gemma3 (270M) and Gemma3 (1B) GRPO into clean 22/22 and 25/25 passes.

Put a floor in front of the ceiling. Which versions clear the floor is measured,
not hardcoded: the build imports vllm.transformers_utils.config under every
candidate sidecar, deletes the ones that raise, and records the lowest survivor.
That is the vLLM module which reads the transformers API, it reproduces both
failures, and it imports without a GPU, which matters because the build host has
none. A request below the floor is clamped up to the lowest eligible sidecar,
the closest version to the notebook's pin this image can actually run; a request
above every sidecar still falls through to the baked transformers.

Measured on the rebuilt image: sidecars 5.5.0 and 5.10.2 survive, floor 5.5.0,
tf-sidecars drops from 250M to 123M, and all 13 distinct transformers pins found
across the 433 shipped notebooks now reach `from unsloth import FastModel`.
Gemma3 (270M) runs end to end exactly as shipped, 22 of 22 cells, loss 4.09 down
to 0.85 over 10 steps.
2026-07-26 17:28:08 +00:00
Daniel Han
9ca7be82c4 docker: trim redundant comments in the image build files
Comment-only pass over the PR's own files. No executable line changes.

- Dockerfile / Dockerfile.studio: drop the decorative stage banner rules, the
  stale "5)" / "6)" step numbering, and the entrypoint pre-flight list that
  restated (and had drifted from) entrypoint.sh's own accurate header. Cut the
  llama.cpp asset bullet list that repeats fetch_llama_prebuilt.py's docstring
  and the structlog rationale already spelled out at the install site.
- entrypoint.sh / studio_launch.sh: fold the section banners into the
  explanation lines that follow them.
- docker-publish.yml: remove the comment rule lines around the job headers.
- validate_studio_features.py: same for the numbered section headers.
- smoke_test.py: drop the stale "~125M params" note on a 1B model.
- unsloth_branding.py, unsloth_nb_view.py, unsloth_nb_pip_magic.py,
  colabTitle.ts: remove comments that restate the adjacent line.
2026-07-26 15:45:58 +00:00
Daniel Han
0f88219618 docker: fix the unsloth CLI and the vLLM engine in the image
Two defects found by running the built image rather than reading it.

1. Every unsloth_cli subcommand that touches the studio backend died on
   import. `unsloth list-checkpoints` on the published image:

       ModuleNotFoundError: No module named 'structlog'

   and the same for train / export / chat, since all four import
   studio.backend.core.*. structlog is a studio backend requirement, not
   an unsloth[huggingface] one, so nothing in the base install pulled it
   in. Added it to the base venv, and added a build-time
   `from studio.backend.core.export import ExportBackend` so a future
   missing dependency in that closure fails the build instead of the
   user's first CLI invocation. That guard has to live in the LAST
   builder verification block: the closure also needs starlette, which
   only arrives with vLLM two stages later.

2. flashinfer-jit-cache was pinned to a literal 0.6.6 while vLLM 0.26.0
   resolves flashinfer-python 0.6.14. flashinfer raises at import when
   the two disagree, and that exception is thrown inside the vLLM
   EngineCore, so Unsloth's GRPO fast_inference path fails at engine
   start with no earlier warning. A literal pin drifts again on the next
   vLLM bump, so the version is now read back from the resolved
   flashinfer-python, and the build proves `import flashinfer` works.

Verified on the rebuilt image: flashinfer-python 0.6.14 with
flashinfer-jit-cache 0.6.14+cu128, structlog 26.1.0, the export backend
importable, and `unsloth list-checkpoints` exiting 0.

tests/python/test_docker_llama_cuda_backend.py gains two static cases
pinning both: the jit-cache version must be derived rather than literal
and the build must import flashinfer, and the base venv must ask for
structlog with the CLI reachability guard present.
2026-07-26 15:32:57 +00:00
Daniel Han
9e3f3671d0 docker: give llama.cpp its libcublas so GGUF stops running on the CPU
The portable llama.cpp bundle loads libggml-cuda.so with dlopen, links it
against libcublas, and does not ship libcublas. The CUDA runtime base
image only carries libcudart, and the only libcublas in the image is
torch's wheel copy under site-packages/nvidia/cublas/lib, which was not
on the loader path. So the CUDA backend failed to load, and llama.cpp
said nothing about it: `--list-devices` printed an empty list and every
GGUF request ran on the CPU.

Measured in the built image on a B200 with gemma-4-E2B-it UD-Q4_K_XL:
1.6 tok/s from llama-cli and 4.2 tok/s from llama-server. With the fix,
the same image and model report `CUDA0: NVIDIA B200` and run at 229 tok/s
and 193 tok/s. Studio's GGUF chat and the GGUF export path go through the
same bundle, so both were affected.

The venv loader config already existed for torchcodec, so cublas/lib
joins it there rather than on LD_LIBRARY_PATH: ld.so.conf.d is consulted
after DT_RUNPATH, which keeps llama.cpp resolving its own $ORIGIN libs
first. cu13/lib comes along for the arm64 bundle's layout.

A silent 140x slowdown deserves a build-time gate, so the layer after the
fetch runs ldd over libggml-cuda.so, installs the cublas major the bundle
actually asks for when it is missing, and fails the build on anything
still unresolved. The amd64 bundle wants libcublas.so.12 and torch
already provides it; the arm64 bundle is CUDA 13, and deriving the major
from ldd keeps that leg honest without hardcoding either. libcuda.so.1 is
exempt: nvidia-container-toolkit injects the driver stub at
`docker run --gpus`, so it is never resolvable at build time. ldd needs
no GPU, so the build stays host-independent.

tests/python/test_docker_llama_cuda_backend.py pins the loader entry, the
guard, the driver-stub exemption and the ordering.
2026-07-26 14:59:27 +00:00
Daniel Han
b67a3b039f docker: tighten comments 2026-07-19 15:32:20 +00:00
Daniel Han
9f96419446 docker: per-run transformers marker, exact vLLM deadline, non-root workspace
Three fixes from review:

unsloth-run now gives each invocation its own UNSLOTH_NB_TF_MARKER (a
temp file, cleaned up afterwards) unless the caller pinned one. The
shared default marker leaked one run's transformers pin into later or
concurrent runs in the same container: a notebook pinned to 4.57.6
left the marker behind and the next unpinned run's kernel activated
the stale sidecar. An empty marker reads as no pin, so pre-creating
the file is safe.

The vLLM startup wait in dataprep/synthetic.py capped every poll at a
full second regardless of the remaining budget, so a fractional
timeout could overshoot by up to a second. The final wait is now
clamped to the remaining time; verified empirically (timeout=1.1
elapses 1.10s).

/workspace and the default HF/Triton cache dirs were root-owned, so
docker run --user without a bind mount could not sync notebooks or
populate caches. They are now world-writable (a+rwX), matching the
documented non-root use the /opt prebuilt placement already supports.
2026-07-19 14:14:34 +00:00
Daniel Han
a26ead4957 docker: tighten comments across the Blackwell image and helpers
Condense the verbose explanatory comments added by this branch to their essential
points without dropping any load-bearing rationale. Touches comments and
docstrings only, no code changes. Leaves the stable-tag gate rationale, the
byte-identical enable= sync notes, and the update-alternatives pin comment as is.
2026-07-18 11:49:15 +00:00
Daniel Han
8a02d123b3 docker: pin /usr/local/cuda to 12.8 and quote native multi-device selectors
Installing cuda-nvcc-13-0 for the sm_103/sm_121 JIT tools also flips the
update-alternatives-managed /usr/local/cuda link to cuda-13.0: the package
hard-depends on cuda-toolkit-13-0-config-common, whose postinst registers
priority 130 over 12.8's 128 (reproduced in a clean
nvidia/cuda:12.8.1-base-ubuntu24.04 container; --no-install-recommends does
not help against hard Depends). TileLang JIT and torch.utils.cpp_extension
resolve nvcc through /usr/local/cuda, so on the 570-driver hosts this image
supports they would emit cu13 cubins that need driver 580 and fail at load.
Pin the alternative back to 12.8 right after the cu13 install; the cu13
tools stay reachable by absolute path, which is exactly how the entrypoint
activates them, and manual mode prevents future apt flips.

run.sh accepted the native --gpus device=0,1 form through an unquoted
passthrough, but docker requires the comma-carrying value to be quoted
(daemon rejects it with 'cannot set both Count and DeviceIDs'; reproduced
against a live daemon, and the docker GPU docs call the quoting out
explicitly). A native multi-device selector is now wrapped in the same
embedded quotes the other comma paths already use; single-device and
pre-quoted forms pass through unchanged. All eight selector forms verified
through the case block.
2026-07-18 10:07:48 +00:00
Daniel Han
8fa588db2c docker: pin xformers explicitly, forward the llama tag to the Studio build
The amd64 base install named the cu128-ampere-torch2110 extra, which does not
exist on main yet (the CUDA extras stop at torch2100): pip/uv only warn on an
unknown extra, so plain unsloth installed without xformers and the required-
package check failed the build. Both arches now take the plain huggingface
extra and amd64 pins xformers==0.0.35 explicitly in the same resolve (it
requires torch>=2.10 without an exact pin, pairing with the baked 2.11.0;
verified on PyPI, x86_64 wheels only, matching the arm64 exclusion). This
decouples the base image from the pending extras PR.

The Studio build now receives the SAME llama.cpp tag the base image baked:
Dockerfile.studio grows a LLAMA_PREBUILT_TAG arg exported as UNSLOTH_LLAMA_TAG
to install.sh (setup.sh honours it; the "latest" default is byte-identical to
setup.sh's own default for local builds), and the publish workflow forwards
the prepare job's resolved tag in the studio build-args. Without the pin a
dispatch override or an upstream release landing between the two jobs let the
no-GPU Studio build re-resolve "latest" and replace the pinned CUDA bundle.

The Studio venv-match assertion also needs installer support for torch 2.11
on the CUDA path; that lands in a separate installer PR and is now declared
as a merge-order dependency in the PR description (the publish workflow only
runs on main pushes, so nothing builds before both are merged).
2026-07-16 06:53:55 +00:00
Daniel Han
6d0f184781 docker: strip VCS refs before the basename, bake the value-flag drift check into the build
A VCS @ref can itself contain a slash (@feature/foo), and the shim split the
last path segment BEFORE dropping the ref, so
git+https://github.com/unslothai/unsloth.git@feature/foo canonicalized as
"foo" and a protected repo installed from a branch dodged _KEEP. The ref is
now stripped from the path portion first (after the authority, so an SSH
userinfo @ is never mistaken for the ref separator, matching pip's own
last-@ parsing), with regressions for slash refs, SSH userinfo, plain tags
and the no-ref form.

The help-derived value-flag drift guards were version-sensitive: repo CI runs
whatever pip/uv are current, so the next tool release turned unrelated PRs
red (pip 26 added --all-releases/--only-final/--requirements-from-script/
--uploaded-prior-to, uv added --no-editable-package/--upgrade-group; all six
now classified). The guards are opt-in for local runs
(UNSLOTH_SHIM_FLAG_DRIFT_CHECK=1) and the authoritative check now runs at
image build time via a new --unsloth-selfcheck-value-flags mode wired into
the Dockerfile verify step, where the baked pip/uv are exactly the tools the
shim fronts, so a flag added by a future baked-tool bump fails the build
instead of a user's notebook cell.
2026-07-16 06:16:20 +00:00
Daniel Han
cd982a121d docker: dedupe repeated rationale comments and parametrize the pip-shim tests
Comment-only consolidation: the sm_103/sm_121 + cu13 JIT story and the
xformers-aarch64 note were each told four times across docker/Dockerfile; keep
the header telling canonical and cross-reference it elsewhere (same for the
workflow's six retellings of the resolve-refs-once rationale and
Dockerfile.studio's NVRTC block). Comments that pointed at the removed dev
scripts now name the underlying command or artifact instead. Non-comment lines
of both Dockerfiles and the workflow are byte-identical.

unsloth_sync_notebooks.sh folds the three copies of the override -> PATH ->
sibling helper resolution into one resolve_helper(), behavior verified for all
four modes including graceful absence under set -u.

unsloth_pip_shim.py collapses an if/else whose branches were identical and
merges the structurally duplicate _parse_include/_parse_editable into one
_parse_flag_line. The test suite folds 35 near-duplicate tests into 8
parametrized groups with exact case-count parity (69 collected before and
after, 81 passing including the nb-pip-magic suite).

Cuts another 144 lines with zero behavior change outside the two refactors.
2026-07-16 05:27:22 +00:00
Daniel Han
4c8be5a1be docker: tighten comments 2026-07-14 14:08:32 +00:00
Daniel Han
6a078b1a45 docker: close more pip-shim bypasses and make cu12.8 NVRTC the default
Notebook pip/uv shim (docker/unsloth_pip_shim.py, active only under
UNSLOTH_NB_SHIM=1):
  - Parse protected source archives (sdist/zip) by basename too, e.g.
    `pip install https://.../unsloth-2026.7.1.tar.gz` or `./torch-2.11.0.tar.gz`,
    mirroring the wheel-basename handling. A first-hyphen-before-digit split
    keeps hyphenated names like flashinfer-python intact.
  - Recognise uv's PLURAL long flags --requirements / --constraints, so those
    files go through the same protected-package filter as the singular names.
  - Drop --upgrade-strategy eager in shim mode so a kept target cannot eagerly
    rebuild already-satisfied baked deps (falls back to pip's only-if-needed).

NVRTC default (docker/Dockerfile, docker/Dockerfile.studio, docker/entrypoint.sh):
  - Make cu12.8 the immutable baked default (libnvrtc.so.12 -> .cu128.orig) with
    a staged .cu13 alias, and have select_cuda_jit_tools retarget to cu13 ONLY
    for sm_103/sm_121. Previously cu13 was baked as the default and restored to
    cu12.8 at runtime, so a non-root `docker run --user` container that cannot
    rewrite the symlink stayed on cu13 NVRTC and emitted cubins a 570-579 driver
    cannot load. The safe default now needs no runtime write.

Adds regression tests for each case (tests/python/test_unsloth_pip_shim.py,
tests/sh/test_select_cuda_jit_tools.sh).
2026-07-08 08:06:45 +00:00
Daniel Han
251e3edf93 docker: address review round 3 (requirement-file shim edges + device-gate cu13 JIT tools)
unsloth_pip_shim.py: close three more ways a protected package slipped past
_KEEP. An editable line (-e/--editable <target>) inside a -r requirements file
is a real install target, so a protected editable there is now classified and
dropped like the command-line case (new _parse_editable). pip/uv accept the
attached short forms -rreqs.txt / -cconstraints.txt / -epath / -Pname as one
token; these were falling through as opaque options (so an attached -r-only cell
no-op'd and an attached -c/-e/-P value bypassed _KEEP), so the 2-char flag is now
split from its value and routed through the separated-form handling. And a nested
-c constraint inside a -r file no longer records its transformers pin as an
install request (a constraint is not a request; mirrors the top-level -c path).

entrypoint.sh / Dockerfile: gate the CUDA 13 ptxas + NVRTC to sm_103 / sm_121 at
runtime instead of a global build-time default. A cu13 cubin needs a >= 580
driver to LOAD even when it targets an older arch (CUDA has forward, not
backward, cross-major driver compatibility), but the image supports Turing..
sm_120 on a 570+ driver, so the previous global TRITON_PTXAS_PATH ENV + cu13
NVRTC symlink would break ordinary Triton/NVRTC JIT on 570-579 driver hosts. The
build still bakes cu13 (saving the cu12.8 NVRTC as .cu128.orig); a new
select_cuda_jit_tools() in the entrypoint reads the device compute_cap and only
activates cu13 for sm_103/sm_121 (which ship >= 580 drivers), otherwise leaving
Triton on its bundled cu12.8 ptxas and restoring the cu12.8 NVRTC in both the
base and Studio venvs. The base ENTRYPOINT runs for the Studio image too.

Adds 9 pip-shim regression tests and tests/sh/test_select_cuda_jit_tools.sh
(7 device-gating cases); registers the latter in CI and tests/run_all.sh.
2026-07-08 06:20:31 +00:00
Daniel Han
e105076503 docker: finish the torch 2.11.0 move and extend the cu13 JIT override to amd64
Base image (torch 2.11.0):
  - amd64 unsloth extra: cu128-ampere-torch2100 -> cu128-ampere-torch2110.
    The old extra pulls xformers 0.0.34, which hard-pins torch==2.10.0 and
    conflicts with the torch==2.11.0 held throughout the build; the torch2110
    family pulls xformers 0.0.35 (no torch pin) and resolves cleanly. This
    needs an unsloth carrying the torch2110 CUDA extras on main, so merge the
    torch2110 extras PR first (default UNSLOTH_REF=main).
  - notebook-deps assertion: startswith('2.10.0') -> '2.11.0' so the layer
    actually verifies the torch it now installs.
  - refresh the torch2100/xformers 0.0.34 references in the surrounding
    comments to the torch2110/0.0.35 line.

sm_103 (B300/GB300) JIT override (Codex item):
  The cu13 NVRTC/ptxas override was arm64-only (sm_121), and its comment
  claimed triton 3.6.0 bundles cu13 ptxas and set TRITON_PTXAS_PATH -- neither
  was true: triton 3.6.0's bundled ptxas is CUDA 12.8 (V12.8.93, tops out at
  sm_120) and TRITON_PTXAS_PATH was never set. So sm_103 (amd64) and even
  sm_121 (arm64) Triton JIT were unfixed.

  Run the cu13 install on both arches and actually wire the ptxas override:
    - NVRTC swap (cu13 libnvrtc.so.13 over torch's bundled cu12.8 .so.12) now
      runs on amd64 too.
    - ENV TRITON_PTXAS_PATH=/usr/local/cuda-13.0/bin/ptxas routes every Triton
      JIT through the cu13 ptxas. Global rather than per-arch is safe: cu13.0
      ptxas spans sm_70..sm_121 (verified: Volta/Turing/Ampere/Hopper through
      Blackwell), so no regression for the older GPUs in the arch list.

  Verified on amd64 in the built base image: cuda-nvrtc-13-0/cuda-nvcc-13-0
  install cleanly from the base's CUDA repo, ptxas lands at
  /usr/local/cuda-13.0/bin/ptxas (V13.0.88) and libnvrtc.so.13 at
  /usr/local/cuda-13.0/lib64/. The sm_103/sm_121 runtime path itself is not
  hardware-tested (no such GPU on hand); precompiled SASS still covers both
  via sm_100/sm_120 forward-compat, so only JIT-heavy paths rely on this.
2026-07-08 03:41:42 +00:00
Daniel Han
cba7223ebe
docker: Colab-grade JupyterLab and Studio UX for the Unsloth image (#6681)
* docker: Colab-grade JupyterLab and Studio UX for the Blackwell image

Stacks a Colab-like JupyterLab and Studio experience on top of the
existing Blackwell image. Additive only: the training stack, CUDA/torch
pinning, and the Studio/JupyterLab/sshd service trio are unchanged.

JupyterLab labextension (prebuilt in a throwaway builder stage, so the
runtime image stays Node-free):
  - Unsloth Dark (Monokai) theme, adaptive light/dark by system preference
  - Colab-style ArrowDown/Up cell navigation
  - top-bar Unsloth logo (stock Jupyter logo disabled and locked)
  - #@title lines render as collapsible Heading-2 form bars
  - Ctrl+A in a cell output selects only that output, not the whole
    notebook (the old behaviour ran notebook:select-all and was laggy)
  - right activity bar hidden by default
  - overrides.json: per-cell run button without auto-advance, labeled
    Restart and Run All, windowing off so collapsing an output does not
    snap to the cell top, news/update prompts suppressed

Studio and login branding: Unsloth favicon, page logo, and a dark
Unsloth login page that rotates through the curated Studio sloth
stickers (fail-soft to the logo).

Notebook organization and Colab compatibility (base image):
  - categorized folder view built from relative symlinks mirroring the
    README sections, rebuilt each boot; real .ipynb files never moved,
    and the symlink tree is invisible to the sync state machine
  - AMD-* notebooks shown only on an AMD/HIP host (autodetected)
  - Docker-only strip of the Colab "Run all on Colab" intro sentence
    from unedited notebooks (upstream notebooks unchanged)
  - hoist %%capture above a leading #@title form so the cell runs
  - the per-cell transformers-sidecar log is silent unless
    UNSLOTH_ENABLE_LOGGING=1

Dependency pinning and naming: the curated notebook extras are pinned to
their resolved versions for reproducible rebuilds; decord is split into
its own fail-soft install (no aarch64 wheel). The lean base image is
renamed from :base to :core.

Adds tests/validate_studio_features.py, a static self-test for the
labextension plugins, overrides keys, and branding wiring.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docker: address review feedback on the JupyterLab/Studio UX

- unsloth_nb_view.py: rebuilding the categorized view no longer deletes
  user files. The view is also JupyterLab's landing dir, so a user may
  save real notebooks there; _clear_view now unlinks only the symlinks we
  own and removes only folders that end up empty, leaving regular files
  in place. It also tests islink before isdir, so a view that is itself a
  symlink to a directory is unlinked instead of being walked into (which
  would have wiped the symlink target).

- studio_launch.sh: derive the landing URL and preferred_dir from
  UNSLOTH_NOTEBOOKS_VIEW_DIR / UNSLOTH_SKIP_NOTEBOOK_VIEW, the same env
  the sync script uses, instead of hard-coding /workspace/Unsloth
  Notebooks. A relocated or disabled view no longer opens JupyterLab on a
  missing folder; it falls back to the default /lab over /workspace.

- Dockerfile.studio: the labext-builder stage now installs Node 20 from
  NodeSource. Ubuntu 24.04's distro nodejs is 18, below JupyterLab 4.6's
  declared Node >=20 engine. Node stays confined to the throwaway builder
  stage, so the runtime image is unchanged.

- .dockerignore: explicitly allowlist jupyter/install_sloth_stickers.py
  alongside its sibling jupyter assets, rather than relying on the
  directory re-inclusion.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docker: publish lean image as :core and full image as :studio

Complete the base->core (and "studio as studio") tag rename so the publish
workflow matches the user-facing helpers and the Dockerfile.studio header.

- The lean training image now publishes as :core (core-<tag>, core-nightly,
  core-sha-*); run.sh / docker_confirm.* already told users to pull :core, but
  docker-publish.yml still tagged it :base, so that pull would have 404'd. The
  per-arch digest artifacts are renamed to match.
- The full Studio image keeps :latest and gains a stable :studio alias, matching
  the Dockerfile.studio header.

Both the merge and post-publish smoke-test metadata blocks are updated together.
Internal "base image" wording (the layer Studio builds FROM) is left as-is.

* docker: address second-round review feedback on the JupyterLab/Studio UX

- studio_launch.sh: also gate the categorized-view landing URL on
  UNSLOTH_SKIP_NOTEBOOK_SYNC (the entrypoint skips building the view entirely in
  that mode), not just UNSLOTH_SKIP_NOTEBOOK_VIEW, so a no-sync container does not
  land on a missing folder.

- Dockerfile.studio: scope the sticker-install "|| echo" fallback to only the
  sticker step via a { ...; } group. It was attached to the whole branding &&
  chain, so a failure in a REQUIRED step (JS resolve, favicon/logo/login copy)
  was swallowed and the build continued with broken branding.

- unsloth_nb_view.py: when creating the categorized symlinks, only replace our
  own stale symlinks; if a real user file already occupies that name, keep it and
  skip the link instead of os.remove-ing it.

- overrides.json: drop doNotDisturbMode (it silenced ALL JupyterLab toasts,
  including kernel-restart / connection-drop feedback). The news/update prompts
  are already off via fetchNews / checkForUpdates.

- Dockerfile: keep decord mandatory on amd64 (fail the build on a missing or
  incompatible wheel) and only fail-soft on arm64/other arches that have no wheel.

- cellNav.ts: do not hijack ArrowUp/Down when focus is in an interactive output
  widget / form control, or while a completion popup is open, so ipywidgets
  controls and autocomplete at cell boundaries keep working.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docker: keep Studio branding RUN free of comments inside the line continuation

Move the sloth-sticker fail-soft explanation above the RUN so no comment line
sits between backslash-continued commands. BuildKit strips such comments, but
keeping the RUN body a plain && chain removes the ambiguity for non-BuildKit
builders and static linters. The { ...; } fail-soft scoping is unchanged.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docker: AGPLv3 attribution + integrity guard for the Studio/JupyterLab image

Make it obvious the image is built by Unsloth and hard to white-label out with a
shallow find-and-replace, and surface the AGPLv3 license + copyright in the UI.

Visible attribution (labextension):
- Help > "About Unsloth Docker Studio" dialog (about.ts): Unsloth logo, the
  AGPLv3 notice, "Copyright 2026-Present the Unsloth team", and source/website/
  license links. Added to the Help menu and the command palette.
- The JupyterLab loading splash is replaced with a spinning Unsloth logo
  (splash.ts, provides ISplashScreen; honors prefers-reduced-motion). The stock
  @jupyterlab/apputils-extension:splash is disabled+locked at build time, like
  the stock logo.
- AGPLv3 footer (license + copyright + links) on the branded login page.
- Labextension relicensed AGPL-3.0-only; SPDX headers on every source file.

Anti-tamper (no encoded/obfuscated strings -- plain readable text only; the one
data URI is the logo image):
- A canonical, plain-text attribution set lives in unsloth_branding.py with a
  TypeScript mirror (branding.ts) bundled verbatim into the labextension, so the
  phrase, copyright, links and plugin ids are spread across independent layers.
- unsloth_branding.py verifies all of these across the installed files (AGPLv3
  text, login footer, theme, labextension package + built bundle strings, logo,
  favicon) and fails loudly if any are missing. It runs at three layers:
  build time (fails the image build), the whole-container launcher
  (studio_launch.sh refuses to start), and as a jupyter_server extension
  (refuses to serve JupyterLab).
- tests/studio/test_branding_guard.py: positive + per-marker negative coverage,
  plus a check that no base64/decoder obfuscation crept into the attribution.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docker: address #6681 review round 2 (colab magics, output select, branding guard)

- unsloth_colab_compat.py: only hoist a leading `%%` cell magic above the Colab
  `#@title` form for magics whose body runs as code (capture/time/bash/python/
  ...). Content magics (%%writefile, %%html, %%latex, ...) are left untouched so
  the form comment is never injected into the written file / rendered output.
- outputSelect.ts: stop trusting the text selection anchor to decide ownership
  of Ctrl/Cmd+A. A stale selection inside an output survives a click onto a
  command-mode cell or the file browser, which made select-all keep re-selecting
  the old output. Gate on the keystroke target or the last pointer-down (reset to
  null on any click outside an output) instead.
- unsloth_branding.py: also reject page_config.json that disables the Unsloth
  labextension or any of its plugin ids via disabledExtensions (dict or list
  form); that leaves the bundle on disk so the prior checks passed while the
  logo/About/splash attribution was stripped at load. Lock unsloth-jupyterlab in
  Dockerfile.studio as well (defense in depth), and add guard tests.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* labext: pin JupyterLab extension deps; confirm.ps1 /login probe

Pin the unsloth-jupyterlab npm deps to exact versions matching the baked
jupyterlab==4.6.0 (builder stays 4.5.9, its newest release) instead of floating
^/~ ranges, so the same commit always builds the same labextension bundle.
Also probe JupyterLab /login (not /api, which 403s behind a password hash) in
the Windows confirmation script.

* docker: categorize AMD/domain notebooks and wire the feature validation into CI

unsloth_nb_view.parse_readme only reset the folder section on level-3
(###) headings. The notebooks README carries level-1 domain headers
(# AMD Notebooks, # Kaggle Notebooks) with their own nb/*.ipynb link
tables and no intervening ###, so those notebooks were mis-filed under
the previous stale section (all 148 AMD notebooks landed in Other
Notebooks on an --amd build). Reset on any heading level and strip a
leading emoji/symbol run so the domain notebooks get their own clean
folder.

Also run tests/validate_studio_features.py explicitly in the repo CPU
job. It is named validate_* (not test_*) so pytest never collected it,
which meant a regression in the notebook view, Colab compat, strip,
JupyterLab defaults or login branding failed CI only when run by hand.

* labext: use caret ranges so jlpm dedups JupyterLab/Lumino singletons

The exact pins introduced earlier (@jupyterlab/* 4.6.0, @lumino/widgets
2.8.0, @jupyterlab/builder 4.5.9) break the Dockerfile.studio
labext-builder stage. Exact-pinning the framework packages defeats
jlpm's (yarn classic) hoisting: transitive @jupyterlab deps request
caret ranges that resolve to newer patch releases (e.g. @jupyterlab/
notebook pulls @jupyterlab/cells ^4.6.0 -> a newer patch), so jlpm
installs a second nested copy alongside the exact top-level one. Two
copies of @jupyterlab/cells and @lumino/widgets in the tree produce
TS2345 "not assignable" errors (protected-member/identity mismatch)
and the build fails.

Caret ranges let jlpm collapse every @jupyterlab and @lumino package
to a single hoisted copy, which is required for a JupyterLab prebuilt
(federated) extension: at runtime those packages are shared singletons
provided by the host JupyterLab, so the build-time versions only need
to type-check against one consistent tree, not match an exact runtime
patch. This is the version set the published image was built and
validated with end to end.

Verified by building the labext in isolation against the base image
(Node 20 + bundled jlpm): caret ranges build clean (webpack compiled
successfully); the exact pins fail with the duplicate-package TS
errors.

* ci(studio-backend): trigger on docker/** so the JupyterLab feature validation guards docker-only changes

The 'Docker JupyterLab/notebook feature validation' step runs
tests/validate_studio_features.py, which checks docker/jupyter (the
labextension, overrides.json, login branding) and the docker notebook
helpers. The pull_request paths filter listed studio/unsloth/tests but
not docker/**, so a PR that only touches docker/ would skip that step
and a regression in those files could pass CI. Add docker/** so the
validation runs whenever the files it checks change.

* jupyter: center the login card and place the attribution below it

#site was a flex container using the default row direction with two children
(the login card and the AGPLv3 attribution), so they rendered side by side:
the card sat left of centre and the attribution floated up to the top-right.
Stack them in a column so the card is horizontally centred and the attribution
sits below it as a footer, matching the intended single-column layout.

* jupyter: refresh Studio attribution, About dialog and loading splash

- Attribution now reads 'Built by the Unsloth team' with a single Apache 2.0 /
  AGPLv3 license link (to the repo license section) on the login page and in the
  About dialog, replacing the plain 'Built by Unsloth. Licensed under the GNU
  AGPLv3.' line. The integrity guard, its canonical PHRASE and the branding tests
  are updated to match.
- About dialog: left-align the link rows so the labels line up instead of each
  row centering independently; add an 'Unsloth Reference' link to the docs, and a
  Licenses section listing Unsloth Studio (AGPLv3) and Unsloth Core (Apache 2.0)
  alongside the full license link.
- Loading splash now reads 'Loading Unsloth Docker' instead of the attribution
  label, via a dedicated SPLASH_LABEL constant.

* docker: document the branding attribution as an AGPLv3 Section 7 notice

Add docker/NOTICE and docker/jupyter/BRANDING.md so the Unsloth attribution that
unsloth_branding.py enforces is also a written license condition, not only a
build check. docker/NOTICE designates the attribution (the "Built by the Unsloth
team" label, the copyright line, the license notice, the logo and theme, and the
Help > About links) as required Appropriate Legal Notices under AGPLv3 Section
7(b), referencing /studio/LICENSE.AGPL-3.0 and /LICENSE. BRANDING.md is a
human-readable note next to the guard describing what must stay, where it lives
and how it is enforced.

* ci(studio-backend): restore docker/** trigger path

The docker/** pull_request path added in b558bc7d was dropped by a later
rebase, so the "Docker JupyterLab/notebook feature validation" step (which runs
tests/validate_studio_features.py against docker/jupyter branding and notebook
helpers) no longer ran on PRs that only touch docker/. Re-add docker/** so a
docker-only change is validated on the PR rather than only after merge to main.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-06 08:27:00 -07:00
Daniel Han
19e3bc3301 docker: move the image torch stack to 2.11.0 and document the amd64 sm_103 JIT limit
Bump the base image torch triplet to torch==2.11.0 / torchvision==0.26.0 /
torchaudio==2.11.0 and the paired torchcodec to 0.11.0, and hold torch at
2.11.0 during the vLLM resolve so uv lands on the vLLM 0.20+ line that pins
torch 2.11.0 (the split-install rationale already anticipated the bump). Update
the build-time self-test assertion, its status line, and the test_locally.sh
log grep to match, plus the FA2 wheel note.

Also clarify the advertised architecture support: forward-compatible SASS
covers precompiled kernels on sm_103 (B300/GB300), but runtime Triton/NVRTC JIT
targets the actual device cap and the bundled cu12.8 ptxas/NVRTC cannot emit
compute_103. arm64 sm_121 is handled by the cu13 NVRTC/ptxas override; amd64
sm_103 has no cu13 override yet, so JIT-heavy paths there can fail until it
lands. Precompiled SASS still runs on sm_103 via sm_100 forward-compat.
2026-07-06 14:09:07 +00:00
Daniel Han
034fbc9785 Docker notebook safety hardening and vLLM startup timeout fix
pip shim (docker/unsloth_pip_shim.py):
- Drop protected packages named via a VCS/URL #egg=NAME fragment so
  git+... #egg=torch no longer reinstalls into the baked venv.
- Filter constraint files (-c/--constraint) through the same protected
  package filter as requirement files, so a pinned torch/transformers in
  a constraint cannot downgrade the baked stack during resolution.
- Recursively filter nested -r/-c includes and absolutise their paths so
  the filtered /tmp copy still resolves them and no protected spec deep in
  the include tree slips past the keep list.
- Remove an unused subprocess import.

Notebook environment:
- Scope the transformers-request marker per kernel (UNSLOTH_NB_TF_MARKER
  keyed on the kernel connection-file id) so concurrent notebooks no
  longer read each other's pin.
- Install the IPython startup hook under IPYTHONDIR (set via ENV) so it
  loads for any uid, including docker run --user, not just root.
- unsloth_nb_content_sig.py: only treat a %%capture / %%bash cell as
  install boilerplate when it carries an install command, so substantive
  captured/bash cells are hashed and upstream changes are not skipped.
- unsloth_run.py: clean up the temp dir used to materialise a downloaded
  notebook.
- unsloth_sync_notebooks.sh: honor UNSLOTH_KEEP_DELETED_NOTEBOOKS across
  GitHub refreshes so a deleted notebook is not restored when upstream
  advances.

install_python_stack.py: the --local unsloth-zoo overlay now honors
UNSLOTH_ZOO_REF (default main), matching the install.sh overlay.

synthetic.py: preserve the timeout=None unbounded vLLM startup wait
instead of coercing it to 1200s.
2026-07-05 13:53:39 +00:00
Daniel Han
2c316862f8 docker: address review round 4 (jupyter probe, CPU messaging, llama EXDEV, %pip shim)
- docker-publish smoke + docker_confirm.sh probe Jupyter /login, not /api: the
  launcher always configures a password hash so /api returns 403 and curl -f
  would never flip the health flag (false build failure).
- entrypoint.sh CPU messaging: CPU mode covers Jupyter, GGUF tooling and
  llama.cpp (GGUF) Studio chat; training AND loading an Unsloth model
  (FastLanguageModel) still need a GPU, since from_pretrained runs CUDA probes.
- install_llama_prebuilt.py: rollback/activation moves used bare os.replace,
  which fails with EXDEV across overlayfs in a Docker build and fell back to a
  broken source build (no nvcc). Add is_cross_device_error + move_install_dir_aside
  (os.replace fast path, copy+remove on EXDEV; busy errors still re-raise).
- notebooks: %pip / %uv line magics and the `!python -m pip` form bypassed the
  PATH pip/uv shim and could overwrite the baked cu128 torch/vLLM stack. Add
  unsloth_nb_pip_magic.py to re-point them at the shim, wired via the IPython
  startup hook and installed into the venv site-packages.
2026-06-27 08:46:47 +00:00
Daniel Han
d476c7764b docker: address review round 3 (notebook -r filter, studio zoo ref, pinned notebooks commit)
- unsloth_pip_shim.py: filter protected packages out of a notebook
  `pip install -r requirements.txt`. The -r value was passed to the real pip
  unchanged, so torch / transformers / vLLM / nvidia pins inside the file could
  overwrite the baked cu128 stack or push transformers into the base venv.
  _filter_requirements_file() applies the same _KEEP / transformers-sidecar
  rules per line, writes the survivors to a temp file, keeps comments, option
  lines, nested includes and urls verbatim, and records a pinned transformers
  version for the sidecar.

- install.sh + Dockerfile.studio + docker-publish.yml: forward the resolved
  unsloth-zoo ref into the Studio build. install.sh --local overlaid
  unsloth-zoo from git main regardless of the operator-requested or base-image
  ref, so the full image could run a different zoo than the base. install.sh
  now honors UNSLOTH_ZOO_REF across all four --local overlays, Dockerfile.studio
  passes UNSLOTH_STUDIO_ZOO_REF through to it, and the workflow resolves one zoo
  ref in the prepare job and shares it with both the base and Studio builds.

- Dockerfile + docker-publish.yml: pin unslothai/notebooks to one resolved
  commit. Each arch leg cloned HEAD independently, so the same tag could seed
  different baked templates and .unsloth_template_commit depending on the pulled
  platform. The prepare job freezes notebooks to one sha (like the llama.cpp
  prebuilt tag) and the Dockerfile fetches that single ref at depth 1.
2026-06-26 10:51:51 +00:00
Daniel Han
0ebbdbb9cc docker: address review follow-ups (pip-shim flags, sync ownership, tags, zoo ref, arch list)
- pip shim: do not treat the value of an index-url / find-links / constraint flag
  as an install target. A cell like 'pip install --extra-index-url <url> torch'
  now no-ops after keeping the baked stack instead of exec'ing a bare
  'pip install --extra-index-url <url>' that fails. Positional . / url / vcs and
  -r/--requirement files still count as targets.
- notebook sync: on first boot, record only files we actually wrote (or that are
  byte-identical to the template), never a kept pre-existing user file; and on the
  GitHub refresh, treat a file present in DEST but absent from the sync state as
  user-owned and keep it. Previously a bind-mounted notebook was recorded as
  managed and then overwritten by upstream.
- docker-publish: add flavor latest=false to the Studio metadata steps too, so a
  v* tag push cannot emit an implicit :latest via metadata-action's latest=auto;
  :latest stays default-branch-only, and the smoke test pulls the published tag.
- unsloth-studio-update: resolve the unsloth-zoo ref independently of --ref (new
  --zoo-ref, else use the ref only when the zoo repo has it, else fall back to
  main) so 'update --ref <unsloth-tag/sha>' does not fail on a missing zoo ref.
- Dockerfile: drop 10.3 (compute_103) from TORCH_CUDA_ARCH_LIST in both the
  builder and runtime stages. B300 runs sm_100 SASS, and the bundled CUDA 12.8
  nvcc cannot compile compute_103 (added in 12.9), which broke arch-list-honoring
  source / JIT builds.
2026-06-26 09:06:39 +00:00
Daniel Han
8f693c6207 docker: fix notebook pip-shim drops, first-boot overwrite, base :latest tag, arm64 decord
- pip shim: count editable/local/url/vcs targets (-e ., ., git+https, wheel
  URLs) as install targets, not just canonical package names, so they are no
  longer silently skipped inside notebooks
- notebook sync: never overwrite a pre-existing user notebook on first boot
  (match the refresh path's ownership rule); skip .unsloth_sync_state.tmp when
  recording state so it is not tracked as a managed file
- docker-publish: set flavor latest=false on the base image metadata so a v*
  tag push cannot publish :latest from the base image (the Studio image owns it)
- notebook deps: pin to tested versions and install decord on its own, hard on
  amd64 and fail-soft on arm64 (no aarch64 wheel) so the arm64 base build works
2026-06-26 08:20:36 +00:00
Daniel Han
d5df6c00de docker: track latest llama.cpp release + show its update banner in Studio
Two related changes to the baked llama.cpp prebuilt.

1. Dynamically follow the newest unslothai/llama.cpp release. build.sh resolves
   the latest release tag (following the /releases/latest redirect, no API
   token) to a concrete tag and passes it as LLAMA_PREBUILT_TAG, so the layer
   cache busts only when upstream publishes. The Dockerfile default is now
   "latest" and fetch_llama_prebuilt.py resolves it the same way, so a plain
   `docker build .` also tracks latest. Pin LLAMA_PREBUILT_TAG to a concrete
   tag for a reproducible, frozen build.

2. Make the in-app "newer llama.cpp available" banner work inside the image.
   Studio's freshness check (utils.llama_cpp_freshness.check_prebuilt_freshness)
   keys off tag / release_tag / published_repo in UNSLOTH_PREBUILT_INFO.json --
   the schema install_llama_prebuilt.py writes. The image bakes the bundle
   directly, so the marker was the release tarball's own, which only carries
   upstream_tag / source_repo; the freshness check then bailed with
   installed_tag=None and could never report "behind", hiding the banner.
   fetch_llama_prebuilt.py now augments the baked marker with those keys
   (setdefault, no build timestamp so the layer stays byte-identical). A fresh
   build is on latest -> no banner; once upstream publishes a newer release the
   banner appears, as verified against the real freshness backend.
2026-06-24 01:21:17 +00:00
danielhanchen
338aff5d82 docker: notebook refresh ignores header/footer-only upstream changes
The boot-time refresh now compares only the tutorial body (the
non-boilerplate cells) when deciding whether to update an untouched
notebook. If only the install header, announcements, or footer moved
upstream, the user's file is left as-is so it is not churned. Notebooks
the user has edited or run are still kept untouched, and non-notebook
files keep the whole-file refresh. Adds unsloth_nb_content_sig.py to
segment head/middle/tail and bakes it into the image.
2026-06-16 03:48:35 +00:00
Daniel Han
d0d5f3c27f docker: pre-load unslothai/notebooks into JupyterLab, edit-safe refresh
JupyterLab now opens with the unslothai/notebooks collection already present,
so people can open and run a notebook directly without a git clone or wget.

- Bake the repo into the image as a read-only template at /opt/unsloth-notebooks
  (~206MB, .git stripped, build commit recorded). Inherited by the studio image.
- On boot the entrypoint populates /workspace/unsloth-notebooks from the template
  (instant, works offline) and best-effort refreshes from GitHub, but only when
  upstream has actually advanced (cheap git ls-remote gate, no download otherwise).
- The user's edits always win. We record the content hash of every file we write;
  on refresh a file whose hash differs from what we last wrote is treated as
  user-modified and is left untouched, so the refresh only updates files the user
  has not changed and adds new ones. It never overwrites an edited notebook and
  never produces merge conflicts. Verified: an edited notebook stays the user's
  version across repeated upstream changes.
- Fully best-effort and gated: UNSLOTH_SKIP_NOTEBOOK_SYNC=1 disables it,
  UNSLOTH_SKIP_NOTEBOOK_REFRESH=1 keeps the baked copy and never hits the network.
  Offline boots keep what is there and never error.

base 18.45 -> 18.67GB, studio 24.88 -> 25.10GB (+~206MB baked notebooks).
2026-06-15 06:35:08 +00:00
Daniel Han
aba16af123 docker: notebook deps, image size cuts, per-notebook transformers
Notebook dependency coverage (base Dockerfile):
- Bake omegaconf, einx, librosa, decord, ftfy so the TTS/STT and vision
  notebooks stop dying on a silent No module named X. Installed in the
  notebook-deps layer (after the torch/vLLM resolve) with an assertion that
  the resolve did not move torch 2.10.0 / numpy>=2.3 / numba>=0.65.

Image size (no functional change):
- Base: prune npp to the two libs torchcodec actually dlopens
  (libnppicc + libnppc), drop link-time-only .a archives and the nvshmem
  device bitcode. Headers (torch/include etc) are kept so causal-conv1d /
  mamba-ssm still build at notebook time with --no-build-isolation.
- Studio: pin the Studio venv to Python 3.12 (matches base) so its
  nvidia-*-cu12 wheels are byte-identical to the base venv's, then symlink
  the heavy arch-independent CUDA libs (cudnn/cublas/nccl/...) into the base
  venv copy. cuda_nvrtc and cuda_runtime are excluded (the arm64 nvrtc swap
  mutates nvrtc in place). Also remove the build-only frontend node_modules
  (runtime serves the committed dist). Studio image drops ~4.8GB.

Per-notebook transformers version, run notebooks unchanged:
- Bake coherent transformers sidecars (4.57.6 default + 5.3.0/5.5.0/5.10.2),
  each transformers==X with its matched huggingface_hub/tokenizers/
  safetensors installed --no-deps into its own dir. Companion versions are
  resolved at build time so they satisfy each transformers' requirements.
- unsloth_nb_compat.py: pick the sidecar from the notebook's pin or the
  model name and activate it (prepend to sys.path) before any ML import,
  without touching the base cu128 torch/vLLM/unsloth stack.
- pip/uv shim on PATH: a notebook install cell becomes safe and idempotent
  inside a kernel (keeps the baked stack, records the requested transformers
  for its sidecar); passthrough to the real tool everywhere else.
- IPython startup hook for manual JupyterLab, and unsloth-run for the
  headless driven path.
2026-06-15 03:13:16 +00:00
Daniel Han
ea91c7a20b docker: add jiwer, langid, easydict, protobuf to baked notebook deps
Continuation of the notebook-dep prebaking: the in-image notebook runner
neutralises pip cells, so declared deps must be prebaked. evaluate's WER
metric imports jiwer (Whisper), DeepSeek-R1 GRPO's reward uses langid,
some vision trust_remote_code files need easydict, and sentencepiece
tokenizer conversion needs protobuf. All pure-Python; torch pin intact.
2026-06-13 11:32:18 +00:00
Daniel Han
5bb47cf3cb docker: bake soundfile, evaluate, tensorboard for notebook deps
The TTS notebooks (Sesame CSM, Orpheus) read audio via soundfile, the
Whisper notebook computes WER via evaluate, and TrainingArguments
defaults report_to to tensorboard. These are declared by notebook pip
cells that the in-image notebook runner neutralises (deps are meant to be
prebaked), so without them those notebooks die on import. All are
pure-Python or self-contained wheels and never name torch, so the cu128
pin set is undisturbed.
2026-06-13 11:04:39 +00:00
Daniel Han
2cd58d5f38 docker: ship cuda-nvcc and cudart-dev in the runtime image
flash-linear-attention's TileLang backend JIT-compiles CUDA kernels via
nvcc at runtime for gated-delta-rule models (Qwen3.5 family). The -base
image only ships runtime libraries, so Studio vision training of
unsloth/Qwen3.5-2B died on the first backward pass with
[Errno 2] No such file or directory: /usr/local/cuda/bin/nvcc.
Install cuda-nvcc and cuda-cudart-dev matching the image CUDA version
and assert nvcc is executable at build time. Found by driving a real
Qwen3.5-2B training run through the Studio UI in the image.
2026-06-12 18:34:29 +00:00
Daniel Han
11430aaab5 docker: unbreak standalone vllm serve (ninja-build + flashinfer-jit-cache)
The notebook validation matrix caught the synthetic-data notebook dying
because the vllm server SyntheticDataKit launches never came up. Two
layers to the failure:

1. flashinfer's cpp_ext JIT shells out to ninja. The pip ninja lives in
   the venv bin, which subprocesses like vllm serve do not always
   inherit on PATH, so the JIT failed with exit 127. Install ninja-build
   so the binary is reachable from any PATH.
2. With ninja present the JIT still cannot succeed for device code: the
   runtime image deliberately ships no nvcc. Bake flashinfer-jit-cache
   (cu128) so ops missing from the cubin package (fmha_gen on sm_100a
   was the repro) come precompiled. In-process GRPO never hit this
   because unsloth-zoo blocks the FlashInfer JIT path; standalone
   vllm serve gets no zoo patches.

Fail-soft on the jit-cache for arches without a wheel; the vLLM chain
itself stays fail-loud on amd64.
2026-06-12 11:04:13 +00:00
Daniel Han
9f9cd41a13 docker: add wget to the runtime image
Notebooks fetch sample assets with !wget; without the binary the shell
prints not-found to stderr, the cell still exits zero from Jupyter's
perspective, and the next cell crashes confusingly on the missing file.
The Whisper notebook died exactly this way in the validation matrix.
2026-06-12 09:20:11 +00:00
Daniel Han
3e6d37cad0 docker: install vLLM on the arm64 leg too and probe it in the confirm scripts
PyPI has shipped aarch64 abi3 wheels for every vLLM release since 0.17,
so the arm64 skip rested on a stale premise. With torch held at 2.10.0
the resolver lands on vllm 0.19.1 (the release pinning torch==2.10.0)
on both arches; verified by cross-resolving the exact index set for
aarch64-unknown-linux-gnu.

amd64 keeps fail-loud semantics. arm64 is fail-soft because the aarch64
wheels are newer and their GPU kernels get validated on Spark hardware
via docker_confirm.sh rather than in CI; on failure the fallback
uninstalls vllm and restores the numpy/numba floor so a partial install
cannot break import unsloth (numpy 2.2.6 ships a broken numpy.testing).

The install steps form an explicit && chain instead of a set -e
subshell: POSIX shells disable errexit inside condition contexts
(verified on dash), so a (set -e; ...) condition would mask failures.

Both confirm scripts gain a 5b vLLM phase: ok on import, bad if missing
on x86_64, warn on other arches where fast_inference=True is best-effort.
2026-06-12 09:09:26 +00:00
Daniel Han
e06b1fb5f5 docker: split the torchcodec bake across build stages
The wheel install belongs in the builder (the venv copy carries it),
but the ld.so.conf.d registration and the import check belong in the
runtime stage: the conf file does not survive the stage copy and the
import needs ffmpeg, which only the runtime stage installs.
2026-06-12 07:53:39 +00:00
Daniel Han
c515aa0bbd docker: audio decode out of the box (ffmpeg + matched torchcodec bake)
The TTS/STT notebooks decode datasets Audio features through torchcodec,
which fails three different ways on a fresh image: the PyPI wheel pairs
with the cu13 torch line and dlopens libnvrtc.so.13; builds newer than
0.10 reference torch 2.11+ symbols; and the matching +cu128 build dlopens
torch and NVIDIA runtime libraries that live inside the venv where the
loader cannot see them. Bake ffmpeg, torchcodec==0.10.0 from the cu128
channel, nvidia-npp-cu12, and register the venv lib dirs via ld.so.conf.d
(not LD_LIBRARY_PATH, so the llama.cpp bundle keeps winning through its
own RUNPATH). Verified in-container: AudioDecoder imports and llama-server
still resolves its bundled libraries.
2026-06-12 07:37:12 +00:00
Daniel Han
25d95c02f3 docker: zstd + matplotlib for out-of-the-box notebook coverage
Running the published unslothai/notebooks set inside the image surfaced
two gaps: the Ollama export notebook installs ollama in-container and
that installer needs zstd for extraction, and DeepSeek-OCR's
trust_remote_code modeling file imports matplotlib unconditionally
(plotting is also simply expected in a Jupyter image).
2026-06-12 07:13:34 +00:00
Daniel Han
81b0d1ef10 docker: second review pass fixes
- Dockerfile: lift numba past vllm's 0.61.2 pin after the numpy>=2.4
  re-upgrade; 0.61.2 refuses numpy 2.3+ at import time and the stack
  cannot move numpy down. Verified numba 0.65 + numpy 2.4.6 + vllm
  import cleanly together.
- docker-publish.yml: resolve UNSLOTH_ZOO_REF in a step that mirrors
  the pushed tag only when the tag exists in unsloth-zoo (the zoo
  currently cuts no tags, so blind mirroring broke every tag publish);
  falls back to main.
- Dockerfile.studio: Studio venv stays on cu128 for arm64 too, matching
  the base venv (cu130 wheels would lift the driver floor to 580+), and
  gets the same NVRTC cu13 swap for DGX Spark / GB10 sm_121 support.
- docker_confirm.sh: do not drop to CPU mode when docker info lacks a
  nvidia runtime entry; CDI installs and Docker Desktop WSL2 expose
  GPUs without one. The phase 3 --gpus probe is now the authority.
- docker_confirm.ps1: GPU selector built as an args array; comma device
  lists get version-aware CSV quoting (native arg passing changed in
  PowerShell 7.3).
- studio_launch.sh: no fixed Jupyter default password; generate a
  random one and print it when JUPYTER_PASSWORD is unset. Env snapshot
  for SSH sessions now written via shlex.quote instead of sed so
  values with quotes or command substitution cannot break or inject
  into /etc/profile.d.
- install.ps1: honour UNSLOTH_TORCH_INDEX_FAMILY like install.sh does.
2026-06-12 05:59:52 +00:00
danielhanchen
9e9877e11e docker: pin the llama.cpp bake by target arch, add docker_confirm.sh
The first bake attempt reused studio/install_llama_prebuilt.py, but that
resolver selects a bundle for the CURRENT host: on a GPU build host
/proc/driver/nvidia leaks into docker build and the resolver goes down the
CUDA path with no readable driver runtime (chosen_asset=none, exit 2),
while on a GPU-less CI runner it would resolve a CPU bundle instead. Both
violate the image's build-host-independence rule.

fetch_llama_prebuilt.py pins by build target only: amd64 takes the
linux-x64-cuda12-portable bundle, arm64 the linux-arm64-cuda13-portable
bundle (DGX Spark / Grace), both sha256-verified against the release's
llama-prebuilt-sha256.json. convert_hf_to_gguf.py plus gguf-py/ are
hydrated from the same release's source tarball so the converter's tensor
mappings match the binaries, mirroring unsloth_zoo's
_hydrate_converter_sources layout. LLAMA_PREBUILT_TAG build-arg overrides
the pinned release.

docker_confirm.sh: one-command confirmation script for any machine
(Linux / WSL2 / macOS) following the staging confirm-script conventions:
host + docker + GPU detection with CPU-mode auto-fallback, image pulls,
in-container torch.cuda check, 5-step LoRA training smoke, baked llama.cpp
verification, full-image boot probing Studio /api/health and JupyterLab
/api, PASS/WARN/FAIL summary with RESULT line.
2026-06-12 05:06:51 +00:00
danielhanchen
f1a63db6fa docker: ship Jupyter, Studio and prebuilt llama.cpp out of the box
Base image (docker/Dockerfile):
- Install JupyterLab + notebook + ipywidgets in a separate pure-Python uv
  pass so the cu128 pin set cannot move; EXPOSE 8888.
- Bake the prebuilt llama.cpp bundle into /opt/unsloth/llama.cpp at the
  runtime stage using studio/install_llama_prebuilt.py from the same
  UNSLOTH_REF (sha256-verified, portable CUDA bundle since the build host
  has no GPU; arm64 resolves the linux-arm64-cuda13 bundle). Export
  UNSLOTH_LLAMA_CPP_PATH so unsloth_zoo's save_pretrained_gguf finds it
  and never reaches the interactive install prompt or a source build.
- Optional github_token BuildKit secret for the resolver's API calls on
  shared CI runner IPs.

Entrypoint: UNSLOTH_ALLOW_CPU=1 degrades a missing GPU to a warning so
Docker Desktop on macOS / Windows-without-WSL2-GPU and plain CPU hosts can
run Jupyter, GGUF tooling and Studio chat; with a GPU visible the normal
pre-flight still runs.

Full image (docker/Dockerfile.studio): now mirrors the production service
set under supervisord - Studio on 8000, JupyterLab on 8888, key-only sshd
on 22 (enabled only when PUBLIC_KEY/SSH_KEY is set). Points Studio's
llama.cpp dir at the baked bundle to skip a duplicate download, accepts
any git ref via fetch+checkout (CI passes commit SHAs), and FROMs a
digest-pinned BASE_IMAGE.

Publish workflow: base image moves to the base-* tag namespace; new
build-studio/merge-studio jobs publish the full image as :latest (hub
parity with the previous production image, which shipped Studio + Jupyter
+ SSH). Studio builds FROM the exact base manifest digest published by the
same run. GPU smoke job now also boots the full image and probes Studio
/api/health and Jupyter /api.

run.sh: UNSLOTH_GPUS=none, UNSLOTH_ALLOW_CPU forwarding, UNSLOTH_PORTS
publish flags, CPU-mode and Jupyter usage examples.
2026-06-12 05:06:51 +00:00
Daniel Han
2faf827f42 docker: round-3 review fixes (concurrency, lockfile wording)
- docker-publish.yml: add `concurrency: docker-publish-${{ github.ref }}`
  (cancel-in-progress: false) so two pushes to main never race the
  `:latest` retag. Don't cancel in-progress runs -- the build is
  expensive and a half-built image left around is worse than a stale
  :latest for a few minutes.
- Dockerfile: soften the requirements.lock.txt comment. `pip freeze`
  captures versions but not wheel hashes, and several deps resolve
  from VCS / nightly indexes that float, so the file is not actually
  byte-reproducible. Reword as an "informational pin record".
2026-05-25 14:02:11 +00:00
Daniel Han
cceeeb1e1b Address 3 MAJOR review findings on the docker PR
1. Stop leaking secrets via docker run -e VAR=VALUE argv (run.sh, test_locally.sh)

   `docker run ... -e HF_TOKEN=hf_xxx ...` puts the literal token in
   the docker CLI's argv, which is visible to any user on the host
   via `ps auxe` / `/proc/<pid>/cmdline` for the lifetime of the
   process. Switch to the dash-only form `-e HF_TOKEN`, which tells
   docker to read the value from the parent shell's env and never
   appears in argv. Same fix for WANDB_API_KEY and UNSLOTH_LICENSE in
   run.sh and HF_TOKEN in test_locally.sh.

2. Stop stripping numpy/tests/ in the runtime layer (Dockerfile)

   The Dockerfile explicitly upgrades numpy >= 2.4 because numpy 2.2.6
   shipped a stripped wheel where `from numpy._core.tests._natype
   import pd_NA` fails. Numpy 2.4 restores `numpy/_core/tests/`, then
   the existing `find ${VENV} -name tests -exec rm -rf {} +` deleted
   it again -- re-introducing the same broken-import state on the
   deployed image (the build-time verification at line 220 runs
   BEFORE the strip so it passed). Whitelist numpy's tests directories
   from the strip; keep stripping the rest.

3. Align :latest tag gate between merge and smoke-test jobs
   (.github/workflows/docker-publish.yml)

   merge job:       enable = is-default-branch AND unsloth_ref == ''
   smoke-test job:  enable = is_default_branch only

   On `workflow_dispatch`, `github.event.inputs.unsloth_ref` defaults to
   "main" (not ""), so the merge step skipped `:latest` but the smoke
   step still emitted `:latest` as tags[0]. The smoke step then
   `docker pull`-ed a prior `:latest` from Docker Hub instead of the
   image just merged -- so the smoke test verified the OLD image, not
   the new one. Copy the merge step's exact `enable=` expression into
   the smoke-test step so the two stay byte-identical and a workflow_
   dispatch run validates whatever was actually merged.
2026-05-25 13:36:56 +00:00
danielhanchen
0d574d8161 Address reviewer-2 findings on PR #5748
Round-2 of the 12-persona reviewer.py pass found 17 issues. Address the
P1s + the regression-class P2s in this commit; the remaining nits are
left for a follow-up cleanup pass.

1. unsloth/_gpu_init.py: the `NVIDIA_VISIBLE_DEVICES in os.environ` check
   triggered for every NVIDIA-runtime container including `--gpus all`
   (NVIDIA_VISIBLE_DEVICES=all is the default). Gate strictly on a
   non-special device list. Also drop the precondition that the env var
   was absent: if the user already pinned TORCHINDUCTOR_COMPILE_THREADS=1
   we should still plant the UNSLOTH_FORCE_SINGLE_COMPILE_WORKER sentinel
   so the zoo-side patch knows to preserve the forcing.

2. unsloth/_gpu_init.py: after the post-`import unsloth_zoo` reassertion,
   monkey-patch `unsloth_zoo.temporary_patches.common.determine_compile_threads`
   to return 1, so any later `torch.compile` call that rebuilds the
   options dict still sees the single-worker forcing even if a downstream
   patch_torch_compile pops the env var again.

3. docker/Dockerfile: torchaudio==2.11.0 mismatched the torch==2.10.0
   release pairing; pin to 2.10.0 so the ABI is correct and the audio
   stack matches torch/cu128.

4. docker/Dockerfile: drop `12.1+PTX` from TORCH_CUDA_ARCH_LIST. The
   cu128 toolkit compiler does not know about compute_121; the trailing
   PTX entry forced nvcc to emit a `sm_121` gencode that breaks any
   in-container source builds.

5. docker/smoke_test.py: the device-capability floor said `cap[0] < 8`,
   rejecting Turing (sm_75) while the Dockerfile + entrypoint advertise
   sm_75 as supported. Lower the smoke floor to sm_75 and print a hint
   that bf16 is not available on Turing.

6. docker/run.sh: `-it` is unconditional; CI / non-TTY invocations died
   with "the input device is not a TTY". Probe `[ -t 0 ] && [ -t 1 ]`
   first. Also remove `set -x` which echoed the forwarded HF_TOKEN /
   WANDB_API_KEY / UNSLOTH_LICENSE values to stdout.

7. docker/test_locally.sh: `-e HF_TOKEN="${HF_TOKEN:-}"` either pasted
   the secret verbatim into the process arg list or shadowed any
   in-container value with an empty string. Forward conditionally.

8. .github/workflows/docker-publish.yml: gate `latest` on default branch
   AND on `unsloth_ref` not being overridden via workflow_dispatch.
   Otherwise a maintainer testing a feature SHA from main could overwrite
   `:latest` with non-main source.

9. docker/Dockerfile.studio: add an `UNSLOTH_STUDIO_REF` build-arg so
   the Studio companion image is pinned to a known unsloth ref instead
   of cloning `main` whenever it builds.
2026-05-24 15:24:20 +00:00
danielhanchen
914f91c7a4 docker: add timm + addict to the base image
Two vision-notebook deps that ship by reference rather than via unsloth
extras: transformers' Gemma3N + TimmWrapperModel needs `timm`, and
DeepSeek-OCR's dynamic modeling file requires `addict`. Both are tiny
(~30MB combined). Including them in the base unified resolve avoids
hitting `ImportError: TimmWrapperModel requires the timm library`
or `ImportError: This modeling file requires the following packages
that were not found in your environment: addict` after the user has
already downloaded the model.

Repros: nb/Gemma3N_(4B)-Vision.ipynb (timm), nb/Deepseek_OCR_(3B).ipynb
(addict).
2026-05-24 15:21:04 +00:00
danielhanchen
6d536d824d Dockerfile: re-upgrade numpy after vLLM install (2.2.6 wheel is broken)
vLLM 0.19.1 pulls numpy down to 2.2.6 whose wheel ships numpy/_core/
without the tests/ subdir, but numpy/testing/_private/utils.py imports
`from numpy._core.tests._natype import pd_NA`. Anything that hits
`from numpy import *` (scipy._lib.array_api_compat does) then crashes.
unsloth_zoo's gemma patch does `from transformers.processing_utils import
Unpack` which touches that path, so `import unsloth` blew up on every
GRPO notebook in the vLLM image.

Bump numpy to >=2.4 right after the vllm install; vllm still imports
fine on numpy 2.4.6 (verified locally).
2026-05-24 13:24:45 +00:00
danielhanchen
215ed9b5f6 Dockerfile: arm64 build aborted by set -e in vLLM auto-gate
The case-arm `auto) [ "${TARGETARCH}" = "amd64" ] && WANT_VLLM=1` exits 1
on arm64 (the [ test ] is false and nothing follows ||), which with
`set -e` aborts the entire RUN. Replace with an explicit if/then/fi so
each arch's auto branch returns 0.

Caught by ubuntu-24.04-arm CI on the staging fork.
2026-05-24 13:01:40 +00:00
Daniel Han
34fb65fc37 Dockerfile: install vLLM nightly on amd64 for GRPO fast_inference
Unsloth's GRPO notebooks (Qwen3_4B-GRPO.ipynb, Qwen3_8B_FP8_GRPO.ipynb,
Llama_FP8_GRPO.ipynb, etc.) set `fast_inference=True` which requires
vLLM to be importable in the same venv. Install vllm pre-release wheels
from https://wheels.vllm.ai/nightly alongside the cu128 pytorch index,
holding torch==2.10.0 fixed so uv refuses any vLLM build that would
yank torch out from under unsloth.

amd64 only -- vLLM does not publish aarch64 wheels yet
(vllm-project/vllm#31128 is open). On arm64 the GRPO notebooks that
need fast_inference will fail to import vllm; non-GRPO and
fast_inference=False paths are unaffected.

Gated by ARG INSTALL_VLLM=auto so the install can be disabled for
contributors who want a smaller image or are blocked by vllm/torch
resolve conflicts during iteration.
2026-05-24 12:18:27 +00:00
Daniel Han
fa9609d659 Dockerfile: arm64 install cu13 nvrtc/nvcc directly without cuda-keyring deb
The nvidia/cuda base image already registers the CUDA apt repo with its own
Signed-By keyring. Installing cuda-keyring_1.1-1_all.deb on top adds a
duplicate sources entry with a different Signed-By value, which makes
`apt-get update` refuse the entire repo:

  E: Conflicting values set for option Signed-By regarding source
  https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/sbsa/

The repo URL is monolithic (every CUDA version is served from the same
path), so we can install cuda-nvrtc-13-0 + cuda-nvcc-13-0 directly without
touching the keyring. Empirically reproduced on the ubuntu-24.04-arm
GitHub Actions runner (staging-fork CI run 26360461375); fix verified via
the same staging-fork after force-push.
2026-05-24 12:16:49 +00:00
Daniel Han
e728eeda6f Dockerfile: switch runtime base cudnn-runtime -> base (~2.7 GB lighter)
torch wheels ship their own cuDNN/cuBLAS/cuSPARSE/cuRAND/cuSOLVER/cuFFT/
NCCL/cuSparseLt inside torch/lib/, and libtorch_cuda.so's RPATH
($ORIGIN/../../nvidia/cudnn/lib:$ORIGIN/../../nvidia/cublas/lib:...)
points at those wheel-bundled copies. The dynamic loader resolves through
the wheel, never the system, so the libcudnn/libcublas in the system
cudnn-runtime layer are unreachable code on every pull.

Verified empirically via `readelf -d torch/lib/libtorch_cuda.so` and
confirmed bitsandbytes' NEEDED list resolves against torch's bundled
libcudart/libcublas/libcublasLt/libcusparse/libnvJitLink before bnb
loads. Triton's .so files have zero CUDA NEEDED entries -- they dlopen
through the host driver.

Compressed image saving: ~2.7 GB (cudnn-runtime base 2.86 GB -> base
0.10 GB, on amd64; arm64 similar). Uncompressed: ~5 GB. Zero functional
impact.

Source: Fork 5 image-size audit, May 2026.
2026-05-24 11:35:31 +00:00
Daniel Han
1769204ade Dockerfile: arm64 DGX Spark NVRTC + ptxas fix (cu13 alongside cu128)
Empirically (cu128 wheel SASS list `sm_80;90;90a;100;100a;120;120a` on
aarch64) the cu128 wheel covers DGX Spark sm_121 via sm_120 binary
forward-compat. BUT two CPU-side compilers shipped at cu12.8 do not know
sm_121 and need a cu13 swap:

  (1) torch's bundled libnvrtc.so.12 from CUDA 12.8 rejects sm_121 as a
      --gpu-architecture. Symlinks libnvrtc.so.13 over it.

  (2) Triton's nvidia backend runs ptxas. Wheels older than 3.6.0 bundled
      cu12.8 ptxas which silently downgrades sm_121 to sm_80 (see
      triton-lang/triton#8335). Bump pin triton>=3.6.0 (3.6 bundles cu13
      ptxas) AND install cuda-nvcc-13-0 so the entrypoint can point
      TRITON_PTXAS_PATH at it as defense in depth.

Both fixes are arm64-only (gated on TARGETARCH, ~400 MB on the arm64
image; amd64 is untouched, no sm_121 hardware exists on x86_64). Neither
component talks to libcuda, so this does NOT bump the toolkit driver
floor away from cu128's 570+.

TRITON_PTXAS_PATH is set from the entrypoint (only when the cu13 ptxas
actually exists in the image) rather than via a Dockerfile ENV, because
ENV is unconditional and Triton errors out if TRITON_PTXAS_PATH points
at a nonexistent file.

Sources: martimramos/dgx-spark-ml-guide Challenge 14; triton-lang/triton
issue #8335; ptrblck PyTorch forum thread on sm_121 fwd-compat from
sm_120.
2026-05-24 11:35:07 +00:00
Daniel Han
897e5e723a Dockerfile: tighten arch-flag assertion + correct fat-binary claims
Empirical reality (cuobjdump on the downloaded cu128 wheels):
  amd64:  sm_70 sm_75 sm_80 sm_86 sm_90 sm_100 sm_120
  arm64:  sm_80 sm_90 sm_90a sm_100 sm_100a sm_120 sm_120a

Earlier comments claimed sm_89 native and a "+PTX JIT to sm_121" fallback;
both are wrong. cu128 wheels ship NO PTX. Ada (sm_89) runs on sm_86 SASS,
B300/GB300 (sm_103) on sm_100, DGX Spark (sm_121) on sm_120 -- all
forward-compat WITHIN a major architecture, which is the canonical CUDA
rule and ptrblck (PyTorch maintainer) confirmed it directly:
"the compatibility ... is also used for e.g. sm_89 with sm_86 and sm_80."

Build-time assertion was `any(a in ("sm_120", "sm_121"))` on arm64. Since
sm_121 is never in any cu128 wheel, the OR was misleading and could mask
a real wheel regression. Tightened to just `assert "sm_120" in arches`
on both arches.
2026-05-24 11:33:47 +00:00
Daniel Han
131f1d3065 docker-publish.yml: native arm64 runner + per-arch digest merge
GitHub announced free linux/arm64 hosted runners for public repos (GA Aug
2025) under labels `ubuntu-24.04-arm` / `ubuntu-22.04-arm`. Switching the
arm64 leg from QEMU-on-amd64 to a native arm64 matrix runner is ~3x
faster and avoids QEMU's occasional flakiness on long cu128 installs.

The workflow now:
  * builds amd64 and arm64 in parallel on their native runners,
    pushing each as a single-arch image *by digest* (no tag)
  * stitches both digests into one multi-platform manifest in a
    follow-up `merge` job, using `docker buildx imagetools create`
  * keeps a separate buildx cache scope per platform to avoid
    cross-arch cache collisions

Smoke-test job now needs `merge` (was `build`) so it only runs once the
final manifest is published.

Dockerfile header: replace the speculative aarch64 SASS list with the
verified one from pytorch/pytorch v2.10.0 .ci/manywheel/build_cuda.sh
(8.0;9.0;10.0;12.0 on aarch64), and note that sm_120 is forward-compatible
to sm_121 per PyTorch maintainers -- which is what makes DGX Spark work
without an explicit sm_121 SASS section in the wheel.

setup_qemu.sh / test_locally.sh --platform stay in place: they're for
the local-dev path on x86_64 boxes that don't have arm64 hardware.
2026-05-24 10:42:28 +00:00