Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions .ai/skills/diffusers-cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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/<run_id>/`
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/<run_id>/` 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
Expand All @@ -182,12 +184,13 @@ Flags:

- `--flavor <name>` — sandbox hardware (e.g. `a10g-small`, `a100-large`, `4xa100-large`).
- `--timeout <duration>` — max wallclock for the run command inside the sandbox (e.g. `30m`, `2h`). Defaults to `10m`.
- `--dependencies <pkg>` — extra pip deps (repeatable). Appends to the defaults.
- `--dependencies <pkg>` — extra pip deps (repeatable), installed on top of whatever the image ships.
- `--namespace <name>` — create the sandbox under a different account.
- `--push-to <bucket>` — 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 <ref>` — 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 <ref>` — 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 <bucket-id>[:<mount-path>]` — mount an HF storage bucket into the sandbox as a read-write directory.
Repeatable. Default mount is `/mnt/buckets/<bucket-id>`. Reference mounted files from `--pipeline-kwargs`
like any other local path — no upload happens, the container reads straight from the FUSE mount. Applied
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build_docker_images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
diffusers-pytorch-xformers-cuda
diffusers-pytorch-minimum-cuda
diffusers-doc-builder
diffusers-cli-cuda
)

declare -A IMAGES_TO_BUILD=()
Expand Down Expand Up @@ -102,6 +103,7 @@ jobs:
- diffusers-pytorch-xformers-cuda
- diffusers-pytorch-minimum-cuda
- diffusers-doc-builder
- diffusers-cli-cuda

steps:
- name: Checkout repository
Expand Down
60 changes: 60 additions & 0 deletions docker/diffusers-cli-cuda/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 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 — 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 \
safetensors \
sentencepiece \
ftfy \
peft \
imageio \
imageio-ffmpeg

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

CMD ["/bin/bash"]
15 changes: 10 additions & 5 deletions docs/source/en/using-diffusers/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -256,11 +260,12 @@ Remote flags:

- `--flavor <name>` — sandbox hardware (e.g. `a10g-small`, `h200`, `rtx-pro-6000`).
- `--timeout <duration>` — max wallclock for the run command inside the sandbox (default `10m`).
- `--dependencies <pkg>` — extra pip deps (repeatable). Useful for pinning a diffusers branch tarball or
adding pipeline-specific extras.
- `--dependencies <pkg>` — extra pip deps (repeatable), installed on top of the image. Useful for pinning a
diffusers branch tarball or adding pipeline-specific extras.
- `--namespace <name>` — create the sandbox under a different HF org/account.
- `--image <ref>` — 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 <bucket-id>[:<mount-path>]` — 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/<bucket-id>`. Reference mounted files from `--pipeline-kwargs` like any other local path.
Expand Down
47 changes: 31 additions & 16 deletions src/diffusers/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
Loading