From aa3951a4bbe35cb13b4c017c7a1a1a2cbd1b0635 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 21 Aug 2026 08:44:36 +0530 Subject: [PATCH 1/3] add a dockerfile for diffusers cli hf jobs --- .github/workflows/build_docker_images.yml | 2 + docker/diffusers-cli-cuda/Dockerfile | 58 +++++++++++++++++++++++ docs/source/en/using-diffusers/cli.md | 15 ++++-- src/diffusers/commands/run.py | 47 +++++++++++------- 4 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 docker/diffusers-cli-cuda/Dockerfile diff --git a/.github/workflows/build_docker_images.yml b/.github/workflows/build_docker_images.yml index 6de59f569a55..0167e203db09 100644 --- a/.github/workflows/build_docker_images.yml +++ b/.github/workflows/build_docker_images.yml @@ -54,6 +54,7 @@ jobs: diffusers-pytorch-xformers-cuda diffusers-pytorch-minimum-cuda diffusers-doc-builder + diffusers-cli-cuda ) declare -A IMAGES_TO_BUILD=() @@ -102,6 +103,7 @@ jobs: - diffusers-pytorch-xformers-cuda - diffusers-pytorch-minimum-cuda - diffusers-doc-builder + - diffusers-cli-cuda steps: - name: Checkout repository diff --git a/docker/diffusers-cli-cuda/Dockerfile b/docker/diffusers-cli-cuda/Dockerfile new file mode 100644 index 000000000000..fc04e740fe25 --- /dev/null +++ b/docker/diffusers-cli-cuda/Dockerfile @@ -0,0 +1,58 @@ +# Prebuilt image for `diffusers-cli run --remote`, so a sandbox starts straight into the run +# instead of installing the dependency set on every cold boot. +# +# The base is the same image the CLI used to boot into, so the torch + CUDA layer is unchanged: +# cuda12.8 is the highest cuda12.x tag below the HF Jobs host driver's CUDA 12.9 max. +# +# The Python deps below mirror `_DEFAULT_REMOTE_DEPS` in `src/diffusers/commands/run.py` — +# keep the two lists in sync. +FROM pytorch/pytorch:2.10.0-cuda12.8-cudnn9-runtime +LABEL maintainer="Hugging Face" +LABEL repository="diffusers" + +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONDONTWRITEBYTECODE=1 +# Weight loading dominates what is left of the startup cost. Xet-accelerated downloads come from +# the `huggingface_hub[hf_xet]` install below (on by default once `hf_xet` is present); parallel +# shard loading is opt-in, and the sandbox env at creation time can still override it. +ENV HF_ENABLE_PARALLEL_LOADING=1 + +# libgl1/libglib2.0-0 back PIL + the cv2 video fallback; libsndfile1 backs torchaudio decoding. +RUN apt-get -y update && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + curl \ + git \ + git-lfs \ + libgl1 \ + libglib2.0-0 \ + libsndfile1 \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:$PATH" + +# torch, torchvision, and torchaudio come from the base image as a matching set — never reinstall +# them here, an ABI mismatch breaks the torchvision/torchaudio C++ extensions. Fail the build +# instead if that assumption ever stops holding. +RUN python -c "import torch, torchvision, torchaudio" + +# `uv pip install --system` targets the base image's Python (/opt/conda), which is the same +# interpreter the CLI installs into at runtime when `--dependencies` adds extras on top. +RUN uv pip install --system --no-cache-dir \ + "huggingface_hub[hf_xet]>=1.23" \ + accelerate \ + transformers \ + safetensors \ + sentencepiece \ + ftfy \ + peft \ + imageio \ + imageio-ffmpeg + +RUN uv pip install --system --no-cache-dir "git+https://github.com/huggingface/diffusers.git@main#egg=diffusers" + +# Smoke-test the console script the sandbox invokes, so a broken install fails the build. +RUN diffusers-cli env + +CMD ["/bin/bash"] diff --git a/docs/source/en/using-diffusers/cli.md b/docs/source/en/using-diffusers/cli.md index bdf3aa2c83b7..abd9b8dad069 100644 --- a/docs/source/en/using-diffusers/cli.md +++ b/docs/source/en/using-diffusers/cli.md @@ -242,8 +242,12 @@ the media itself. Written images are always PNG, videos MP4, audio WAV. ### Remote execution (`--remote`) Run the same call inside a [Hugging Face Sandbox](https://huggingface.co/docs/huggingface_hub/en/guides/sandbox) -— an isolated cloud VM the CLI drives over HTTP: it uploads inputs, installs deps, runs the pipeline, downloads -outputs, then terminates the sandbox. Requires `huggingface_hub>=1.23`. +— an isolated cloud VM the CLI drives over HTTP: it uploads inputs, runs the pipeline, downloads outputs, then +terminates the sandbox. Requires `huggingface_hub>=1.23`. + +The sandbox boots from [`diffusers/diffusers-cli-cuda`](https://hub.docker.com/r/diffusers/diffusers-cli-cuda), a +prebuilt image that already ships torch, CUDA, and the CLI's dependencies, so a cold run starts generating instead +of installing first. ```bash diffusers-cli run \ @@ -256,11 +260,12 @@ Remote flags: - `--flavor ` — sandbox hardware (e.g. `a10g-small`, `h200`, `rtx-pro-6000`). - `--timeout ` — max wallclock for the run command inside the sandbox (default `10m`). -- `--dependencies ` — extra pip deps (repeatable). Useful for pinning a diffusers branch tarball or - adding pipeline-specific extras. +- `--dependencies ` — extra pip deps (repeatable), installed on top of the image. Useful for pinning a + diffusers branch tarball or adding pipeline-specific extras. - `--namespace ` — create the sandbox under a different HF org/account. - `--image ` — override the sandbox image. Must ship torch + CUDA compatible with your `--flavor`'s - driver. + driver. The CLI then installs its own dependencies on top on every cold sandbox, which the default image + avoids. - `--volume [:]` — mount an [HF storage bucket](https://huggingface.co/docs/hub/en/storage-buckets) into the sandbox as a read-write directory. Repeatable. Default mount path is `/mnt/buckets/`. Reference mounted files from `--pipeline-kwargs` like any other local path. diff --git a/src/diffusers/commands/run.py b/src/diffusers/commands/run.py index 969e2f6816b8..e579a97d13c1 100644 --- a/src/diffusers/commands/run.py +++ b/src/diffusers/commands/run.py @@ -90,6 +90,9 @@ # (`transformer`, `unet`) and their numbered variants (`transformer_2`, etc.). _DENOISER_COMPONENT_KEYS = ("transformer", "unet") +# What a sandbox needs on top of a bare torch + CUDA image. `_DEFAULT_REMOTE_IMAGE` already +# ships all of it, so this list is only installed when `--image` points somewhere else. +# `docker/diffusers-cli-cuda/Dockerfile` mirrors it — keep the two in sync. _DEFAULT_REMOTE_DEPS = ( "diffusers", "accelerate", @@ -102,12 +105,14 @@ "imageio-ffmpeg", # bundles a static ffmpeg; the cv2 fallback needs system libs the slim image lacks ) -# Base sandbox image — provides torch + CUDA so `uv pip install --system` -# only has to add the small Python deps. cuda12.8 is the highest cuda12.x tag -# below the HF Jobs host driver's CUDA 12.9 max. -_DEFAULT_REMOTE_IMAGE = "pytorch/pytorch:2.10.0-cuda12.8-cudnn9-runtime" +# Base sandbox image — torch + CUDA with `_DEFAULT_REMOTE_DEPS` and `diffusers-cli` baked in, so a +# cold sandbox boots straight into the run instead of resolving and installing them first. Built +# nightly from `docker/diffusers-cli-cuda/`. Its cuda12.8 base is the highest cuda12.x tag below +# the HF Jobs host driver's CUDA 12.9 max. +_DEFAULT_REMOTE_IMAGE = "diffusers/diffusers-cli-cuda:latest" -# Installed console-script name invoked inside the sandbox after the deps land. +# Console-script name invoked inside the sandbox — baked into the default image, installed +# with the `diffusers` dep on any other one. _CONTAINER_CLI_BINARY = "diffusers-cli" # Working directories inside the sandbox: local media from `--pipeline-kwargs` is uploaded @@ -276,8 +281,9 @@ def _add_remote_arguments(parser: ArgumentParser) -> None: default=None, help=( "Sandbox image for --remote (defaults to " - f"{_DEFAULT_REMOTE_IMAGE!r}). Must provide torch + CUDA; the CLI installs the " - "small Python deps on top via `uv pip install --system`." + f"{_DEFAULT_REMOTE_IMAGE!r}, which ships the deps prebuilt). Any other image must " + "provide torch + CUDA; the CLI then installs the small Python deps on top via " + "`uv pip install --system` on every cold sandbox." ), ) parser.add_argument( @@ -1046,6 +1052,12 @@ def _maybe_submit_remote(args: Namespace, task: str) -> bool: download_locally = (not user_bucket) or (args.output is not None) local_dir = Path(args.output) if args.output else Path(DEFAULT_OUTPUT_DIR) / run_id + # On a reconnect `--image` is meaningless (the image was fixed at creation time), and the + # deps landed during the run that created the sandbox — so the default's "already prebuilt" + # treatment is the right one either way. + image = args.image or _DEFAULT_REMOTE_IMAGE + prebuilt_image = image == _DEFAULT_REMOTE_IMAGE + use_existing_sandbox = bool(args.sandbox_id) keep_alive = args.keep_alive or use_existing_sandbox if use_existing_sandbox and args.volume: @@ -1058,7 +1070,7 @@ def _maybe_submit_remote(args: Namespace, task: str) -> bool: else: logger.info(f"creating sandbox on flavor={args.flavor!r}...") create_kwargs: dict[str, Any] = { - "image": args.image or _DEFAULT_REMOTE_IMAGE, + "image": image, "flavor": args.flavor, "forward_hf_token": True, "token": hf_token, @@ -1097,14 +1109,17 @@ def _stream(chunk: str) -> None: try: _upload_inputs_to_sandbox(args, sbx, run_id) - dependencies = list(_DEFAULT_REMOTE_DEPS) - if args.dependencies: - dependencies.extend(args.dependencies) - # --break-system-packages bypasses PEP 668; harmless in a throwaway sandbox. uv is a - # near no-op when the deps are already satisfied, so this stays cheap on a reused sandbox. - install_cmd = shlex.join(["uv", "pip", "install", "--system", "--break-system-packages", *dependencies]) - logger.info("installing dependencies in the sandbox...") - sbx.run(install_cmd, on_stdout=_stream, on_stderr=_stream) + # The prebuilt image already carries `_DEFAULT_REMOTE_DEPS`, so only `--dependencies` + # extras get installed there; any other image needs the full set on top. + dependencies = list(args.dependencies or ()) + if not prebuilt_image: + dependencies = [*_DEFAULT_REMOTE_DEPS, *dependencies] + if dependencies: + # --break-system-packages bypasses PEP 668; harmless in a throwaway sandbox. uv is a + # near no-op when the deps are already satisfied, so this stays cheap on a reused sandbox. + install_cmd = shlex.join(["uv", "pip", "install", "--system", "--break-system-packages", *dependencies]) + logger.info("installing dependencies in the sandbox...") + sbx.run(install_cmd, on_stdout=_stream, on_stderr=_stream) # Per-run outputs subdirectory so a reused sandbox doesn't leak files from prior runs # into this run's download set. From c635ca0d457dde64d408ad9bb28375a3ad96bdf5 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 21 Aug 2026 08:52:16 +0530 Subject: [PATCH 2/3] up --- docker/diffusers-cli-cuda/Dockerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docker/diffusers-cli-cuda/Dockerfile b/docker/diffusers-cli-cuda/Dockerfile index fc04e740fe25..1e1716438bd6 100644 --- a/docker/diffusers-cli-cuda/Dockerfile +++ b/docker/diffusers-cli-cuda/Dockerfile @@ -37,9 +37,11 @@ ENV PATH="/root/.local/bin:$PATH" # instead if that assumption ever stops holding. RUN python -c "import torch, torchvision, torchaudio" -# `uv pip install --system` targets the base image's Python (/opt/conda), which is the same -# interpreter the CLI installs into at runtime when `--dependencies` adds extras on top. -RUN uv pip install --system --no-cache-dir \ +# `uv pip install --system` targets the base image's Python — Ubuntu's system interpreter, which +# is where torch already lives and where the CLI installs `--dependencies` extras at runtime. +# --break-system-packages bypasses the PEP 668 marker that interpreter carries; harmless in a +# single-purpose image, and it matches what the CLI does inside the sandbox. +RUN uv pip install --system --break-system-packages --no-cache-dir \ "huggingface_hub[hf_xet]>=1.23" \ accelerate \ transformers \ @@ -50,7 +52,7 @@ RUN uv pip install --system --no-cache-dir \ imageio \ imageio-ffmpeg -RUN uv pip install --system --no-cache-dir "git+https://github.com/huggingface/diffusers.git@main#egg=diffusers" +RUN uv pip install --system --break-system-packages --no-cache-dir "git+https://github.com/huggingface/diffusers.git@main#egg=diffusers" # Smoke-test the console script the sandbox invokes, so a broken install fails the build. RUN diffusers-cli env From 0d2813b0187773c9154c65ff51e56ff8630fcb8a Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 21 Aug 2026 09:20:41 +0530 Subject: [PATCH 3/3] up --- .ai/skills/diffusers-cli/run.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.ai/skills/diffusers-cli/run.md b/.ai/skills/diffusers-cli/run.md index 5eb56d826535..13099d5626b5 100644 --- a/.ai/skills/diffusers-cli/run.md +++ b/.ai/skills/diffusers-cli/run.md @@ -151,8 +151,8 @@ user also explicitly asked for a local target via `--output`. ## Remote execution (`--remote`) Add `--remote` to run the same call inside a [Hugging Face Sandbox](https://huggingface.co/docs/huggingface_hub/en/guides/sandbox) -— an isolated cloud VM (built on HF Jobs) the CLI drives over HTTP: it uploads inputs, installs deps, runs -the pipeline, downloads outputs, then terminates the sandbox. +— an isolated cloud VM (built on HF Jobs) the CLI drives over HTTP: it uploads inputs, runs the pipeline, +downloads outputs, then terminates the sandbox. ```bash diffusers-cli run \ @@ -167,12 +167,14 @@ What happens: 1. Your HF token is picked up (from `--token` or your login) and forwarded into the sandbox as `HF_TOKEN`. 2. `--pipeline-kwargs` are parsed locally so JSON errors fail fast (no wasted sandbox time). -3. A dedicated sandbox is created on `--flavor` from a pytorch image - (`pytorch/pytorch:2.10.0-cuda12.8-cudnn9-runtime` by default) that already has torch + CUDA. Any local - file paths in `--pipeline-kwargs` are uploaded into the sandbox under `/tmp/diffusers-cli/inputs//` - via native file transfer (no bucket), and the JSON paths are rewritten to point at them. -4. The small Python deps (`diffusers`, `accelerate`, `transformers`, `safetensors`, `sentencepiece`, `ftfy`) - are installed with `uv pip install --system`. Output (install + run) streams live to your terminal. +3. A dedicated sandbox is created on `--flavor` from `diffusers/diffusers-cli-cuda:latest` by default — a + prebuilt image (`docker/diffusers-cli-cuda/` in this repo, rebuilt nightly) that already ships torch, + CUDA, `diffusers`, and the rest of the CLI's deps. Any local file paths in `--pipeline-kwargs` are + uploaded into the sandbox under `/tmp/diffusers-cli/inputs//` via native file transfer (no + bucket), and the JSON paths are rewritten to point at them. +4. No dependency install runs on the default image — that step is skipped unless you passed `--dependencies` + (only the extras are installed then) or pointed `--image` somewhere else (the full set is installed). + Output streams live to your terminal. 5. The sandbox CLI writes outputs to `/tmp/diffusers-cli/outputs/`; the CLI downloads every artifact back into the local target (see [`--push-to`](#-push-to) for when the download is skipped). 6. The sandbox is terminated (unless `--keep-alive`/`--sandbox-id`), and the wallclock `run_seconds` for the @@ -182,12 +184,13 @@ Flags: - `--flavor ` — sandbox hardware (e.g. `a10g-small`, `a100-large`, `4xa100-large`). - `--timeout ` — max wallclock for the run command inside the sandbox (e.g. `30m`, `2h`). Defaults to `10m`. -- `--dependencies ` — extra pip deps (repeatable). Appends to the defaults. +- `--dependencies ` — extra pip deps (repeatable), installed on top of whatever the image ships. - `--namespace ` — create the sandbox under a different account. - `--push-to ` — see [`--push-to`](#-push-to) above. The upload runs inside the sandbox; an explicit value with no `--output` makes the bucket the sole destination and skips the local download. -- `--image ` — override the sandbox image. Must ship torch + CUDA; the CLI installs the small Python - deps on top via `uv pip install --system`. Useful for pinning a specific torch or bundling extra system libs. +- `--image ` — override the sandbox image. Must ship torch + CUDA; the CLI then installs the small + Python deps on top via `uv pip install --system` on every cold sandbox, which the default image avoids. + Useful for pinning a specific torch or bundling extra system libs. - `--volume [:]` — mount an HF storage bucket into the sandbox as a read-write directory. Repeatable. Default mount is `/mnt/buckets/`. Reference mounted files from `--pipeline-kwargs` like any other local path — no upload happens, the container reads straight from the FUSE mount. Applied