Skip to content

Commit fc92295

Browse files
committed
update
1 parent 5bc3948 commit fc92295

7 files changed

Lines changed: 187 additions & 39 deletions

File tree

‎docs/source/en/using-diffusers/cli.md‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,15 @@ Configure how the CLI loads model weights and custom pipeline code.
141141
pipelines. See [Attention backends](../optimization/attention_backends).
142142
- `--vae-tiling` / `--vae-slicing` — lower VAE decode VRAM. See
143143
[VAE tiling](../optimization/memory#vae-tiling) and [VAE slicing](../optimization/memory#vae-slicing).
144-
- `--compile [JSON]` — compile denoiser modules with [torch.compile](../optimization/fp16#torchcompile). The
145-
CLI prefers [regional compilation](../optimization/fp16#regional-compilation) for modules with repeated
146-
blocks. Bare `--compile` uses `fullgraph=true`. A JSON object is forwarded to `torch.compile`. Not supported
147-
with `--context-parallel`.
144+
- `--compile [JSON]` — compile denoiser modules and the VAE decoder with
145+
[torch.compile](../optimization/fp16#torchcompile). The CLI prefers
146+
[regional compilation](../optimization/fp16#regional-compilation) for denoisers with repeated blocks; the VAE
147+
decoder is always compiled whole. Bare `--compile` uses `fullgraph=true`. A JSON object is forwarded to
148+
`torch.compile`. Not supported with `--context-parallel`.
149+
- `--compile-mode {regional,full}` — what `--compile` compiles. `regional` (default) compiles only the repeated
150+
blocks, which keeps the first step fast. `full` compiles the whole denoiser. `torch.compile` modes that use CUDA
151+
graphs (`max-autotune`, `reduce-overhead`) need `--compile-mode full`: repeated blocks share one compiled graph,
152+
so with CUDA graphs each block's output is overwritten by the next block's run.
148153
- `--context-parallel` — Ulysses-style context parallelism on a DiT-based pipeline. Locally requires torchrun;
149154
under `--remote` the CLI wraps `torchrun --nproc-per-node=gpu` for you. See
150155
[Context parallelism](../training/distributed_inference#context-parallelism).
@@ -375,6 +380,14 @@ diffusers-cli generate --sandbox-id <id> --inputs '{"prompt": "a cat on the moon
375380
[`run --remote`](#remote-execution---remote). `--model` must be a Hub repo id. A local `--manifest` file is
376381
uploaded to the sandbox.
377382

383+
With `--backend sglang` or `--backend vllm`, the sandbox starts from the engine's own image
384+
(`lmsysorg/sglang:latest` or `vllm/vllm-omni:latest`) unless `--image` names another, so the engine does not need
385+
installing:
386+
387+
```bash
388+
diffusers-cli serve --model black-forest-labs/FLUX.2-klein-4B --backend sglang --remote --flavor a100-large
389+
```
390+
378391
### Manifests
379392

380393
What a model accepts is described by its manifest. `serve` looks for one in this order:
@@ -425,8 +438,8 @@ diffusers-cli serve --model Qwen/Qwen-Image --backend sglang --backend-args '--n
425438
diffusers-cli serve --model Wan-AI/Wan2.2-T2V-A14B-Diffusers --backend vllm
426439
```
427440

428-
The pipeline flags (`--dtype`, `--cpu-offload`, `--compile`, ...) do not apply to these backends; pass the
429-
engine's own flags with `--backend-args`. Compared with the in-process backend, they serve image and video
441+
The pipeline flags (`--dtype`, `--cpu-offload`, `--compile`, ...) do not apply to these backends: they are
442+
ignored with a warning. Pass the engine's own flags with `--backend-args`. Compared with the in-process backend, they serve image and video
430443
outputs only, accept `image` as the only media input, expose the parameters both engines share (size, steps,
431444
guidance scale, seed, number of frames, fps, number of images), report no step progress, and apply no request
432445
adapters.

‎src/diffusers/commands/run.py‎

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@
6363

6464

6565
ATTENTION_BACKEND_CHOICES = ("default", *sorted(b.value for b in _HUB_KERNELS_REGISTRY))
66+
COMPILE_MODE_CHOICES = ("regional", "full")
67+
# zlib level for PNG outputs. Pillow's default of 6 takes close to four times as long to encode a
68+
# 1024x1024 image for a file about 7% smaller; the output is lossless either way.
69+
PNG_COMPRESS_LEVEL = 1
70+
71+
# `torch.compile` modes that replay CUDA graphs. Repeated blocks share one compiled graph, so each
72+
# block's output is overwritten when the next block replays it.
73+
_CUDA_GRAPH_COMPILE_MODES = ("max-autotune", "reduce-overhead")
6674

6775
# Kwarg keys whose string value gets auto-loaded before being passed to the pipeline call.
6876
# Images resolve via `diffusers.utils.load_image` → PIL.Image.Image; videos resolve via
@@ -211,13 +219,23 @@ def _add_optimization_arguments(parser: ArgumentParser) -> None:
211219
default=None,
212220
metavar="JSON",
213221
help=(
214-
"torch.compile every denoiser submodule on the pipeline. Accepts an optional JSON "
222+
"torch.compile every denoiser submodule on the pipeline, and the VAE decoder. Accepts an optional JSON "
215223
'object of kwargs forwarded to `torch.compile`, e.g. \'{"mode": "max-autotune", '
216224
'"fullgraph": true}\'. Bare `--compile` uses `fullgraph=true`. Adds a one-time '
217225
"compilation cost on the first step but speeds up every subsequent step — worth it "
218226
"for multi-step generation (50+ steps)."
219227
),
220228
)
229+
parser.add_argument(
230+
"--compile-mode",
231+
choices=COMPILE_MODE_CHOICES,
232+
default="regional",
233+
help=(
234+
"What --compile compiles. 'regional' (default) compiles only the repeated blocks of a denoiser "
235+
"that declares them, which keeps the first step fast. 'full' compiles the whole denoiser, which "
236+
"`torch.compile` modes that use CUDA graphs (max-autotune, reduce-overhead) require."
237+
),
238+
)
221239

222240

223241
def _add_output_arguments(parser: ArgumentParser) -> None:
@@ -461,26 +479,44 @@ def _apply_optimizations(pipeline: Any, args: Namespace) -> None:
461479
if args.context_parallel:
462480
logger.warning("--compile is currently not supported with --context-parallel; skipping compile.")
463481
else:
464-
_compile_denoiser(pipeline, args.compile)
482+
_compile_denoiser(pipeline, args.compile, args.compile_mode)
483+
_compile_vae_decoder(pipeline, args.compile)
484+
485+
486+
def _parse_compile_spec(compile_spec: str) -> dict[str, Any]:
487+
try:
488+
compile_kwargs = json.loads(compile_spec)
489+
except json.JSONDecodeError as e:
490+
raise SystemExit(f"--compile must be valid JSON: {e}") from e
491+
if not isinstance(compile_kwargs, dict):
492+
raise SystemExit("--compile must decode to a JSON object.")
493+
return compile_kwargs
465494

466495

467-
def _compile_denoiser(pipeline: Any, compile_spec: str) -> None:
496+
def _compile_vae_decoder(pipeline: Any, compile_spec: str) -> None:
497+
"""Compile the decoder of the pipeline's VAE with the `--compile` kwargs.
498+
499+
The decoder runs once per call, so it has no repeated blocks to compile regionally and is always compiled whole.
500+
"""
501+
decoder = getattr(getattr(pipeline, "vae", None), "decoder", None)
502+
if not isinstance(decoder, torch.nn.Module):
503+
return
504+
pipeline.vae.decoder = torch.compile(decoder, **_parse_compile_spec(compile_spec))
505+
506+
507+
def _compile_denoiser(pipeline: Any, compile_spec: str, compile_mode: str = "regional") -> None:
468508
"""Compile every `transformer*` and `unet*` submodule on the pipeline.
469509
470510
`compile_spec` is the raw JSON string from `--compile` (`"{}"` for bare flag). Decoded into kwargs and forwarded
471511
verbatim to the compile call.
472512
473-
Prefers regional compilation via `module.compile_repeated_blocks(**kwargs)` — only compiles the repeated inner
474-
blocks (the bulk of the compute), much faster first-step latency than compiling the whole module. Falls back to
475-
full `torch.compile` if the model doesn't expose `_repeated_blocks`.
513+
With `compile_mode="regional"`, prefers regional compilation via `module.compile_repeated_blocks(**kwargs)` — only
514+
compiles the repeated inner blocks (the bulk of the compute), much faster first-step latency than compiling the
515+
whole module. Falls back to full `torch.compile` if the model doesn't expose `_repeated_blocks`. With
516+
`compile_mode="full"`, always compiles the whole module.
476517
"""
477518

478-
try:
479-
compile_kwargs = json.loads(compile_spec)
480-
except json.JSONDecodeError as e:
481-
raise SystemExit(f"--compile must be valid JSON: {e}") from e
482-
if not isinstance(compile_kwargs, dict):
483-
raise SystemExit("--compile must decode to a JSON object.")
519+
compile_kwargs = _parse_compile_spec(compile_spec)
484520

485521
for attr in dir(pipeline):
486522
if not any(attr.startswith(key) for key in _DENOISER_COMPONENT_KEYS):
@@ -489,11 +525,17 @@ def _compile_denoiser(pipeline: Any, compile_spec: str) -> None:
489525
if not isinstance(module, torch.nn.Module):
490526
continue
491527

492-
if getattr(module, "_repeated_blocks", None):
528+
if compile_mode == "regional" and getattr(module, "_repeated_blocks", None):
529+
if compile_kwargs.get("mode") in _CUDA_GRAPH_COMPILE_MODES:
530+
raise SystemExit(
531+
f"--compile mode {compile_kwargs['mode']!r} uses CUDA graphs, which fail when the repeated "
532+
f"blocks of {type(module).__name__} are compiled separately. Add `--compile-mode full`, or "
533+
"use mode 'max-autotune-no-cudagraphs'."
534+
)
493535
# Regional compile — only the repeated blocks. Mutates `module` in place.
494536
module.compile_repeated_blocks(**compile_kwargs)
495537
else:
496-
# No regional metadata declared; fall back to compiling the whole module.
538+
# Full compile was asked for, or no regional metadata is declared.
497539
setattr(pipeline, attr, torch.compile(module, **compile_kwargs))
498540

499541

@@ -535,6 +577,9 @@ def _load_lora(pipeline: Any, args: Namespace) -> None:
535577

536578

537579
def _load_pipeline(args: Namespace) -> Any:
580+
if args.compile is None and args.compile_mode != "regional":
581+
raise SystemExit(f"--compile-mode {args.compile_mode} only applies together with --compile.")
582+
538583
# Detect modular repos by trying the standard config; `ModularPipeline` repos ship
539584
# `modular_model_index.json` instead of `model_index.json`, so `load_config` OSErrors.
540585
# A repo can also ship a `model_index.json` whose `_class_name` is a modular pipeline
@@ -800,7 +845,7 @@ def _save_videos(videos: list[Any], args: Namespace) -> list[str]:
800845
arr = (np.clip(arr, 0.0, 1.0) * 255).round().astype(np.uint8)
801846
frame = Image.fromarray(arr)
802847
frame_path = frames_dir / f"{i:04d}.png"
803-
frame.save(frame_path)
848+
frame.save(frame_path, compress_level=PNG_COMPRESS_LEVEL)
804849
saved.append(str(frame_path))
805850
try:
806851
export_to_video(frames, str(path), fps=args.fps)
@@ -837,14 +882,14 @@ def _save_output(value: Any, args: Namespace) -> list[str]:
837882
for arr, path in zip(value, paths):
838883
if arr.dtype != np.uint8:
839884
arr = (np.clip(arr, 0.0, 1.0) * 255).round().astype(np.uint8)
840-
Image.fromarray(arr).save(path)
885+
Image.fromarray(arr).save(path, compress_level=PNG_COMPRESS_LEVEL)
841886
return [str(p) for p in paths]
842887

843888
pil_images = _as_pil_list(value)
844889
if pil_images is not None:
845890
paths = _resolve_output_paths(len(pil_images), args.output, ext="png")
846891
for img, path in zip(pil_images, paths):
847-
img.save(path)
892+
img.save(path, compress_level=PNG_COMPRESS_LEVEL)
848893
return [str(p) for p in paths]
849894

850895
frames = _as_frame_sequence(value)

‎src/diffusers/commands/serve/__init__.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
from .remote import add_remote_arguments, serve_remote
3434

3535

36+
logger = logging.get_logger("diffusers-cli/serve")
37+
3638
DEFAULT_OUTPUT_DIR = str(Path.home() / ".diffusers" / "cli" / "serve" / "outputs")
3739
BACKEND_CHOICES = ("diffusers", "sglang", "vllm")
3840

@@ -47,6 +49,7 @@
4749
"vae_tiling": False,
4850
"vae_slicing": False,
4951
"compile": None,
52+
"compile_mode": "regional",
5053
}
5154

5255

@@ -212,14 +215,14 @@ def run(self) -> None:
212215
if args.backend == "diffusers" and args.backend_args is not None:
213216
raise SystemExit("--backend-args only applies to --backend sglang or vllm.")
214217
if args.backend != "diffusers":
215-
rejected = [
218+
ignored = [
216219
"--" + name.replace("_", "-")
217220
for name, unset in _DIFFUSERS_ONLY_FLAGS.items()
218221
if getattr(args, name) != unset
219222
]
220-
if rejected:
221-
raise SystemExit(
222-
f"{', '.join(rejected)} configure the in-process pipeline and have no effect with "
223+
if ignored:
224+
logger.warning(
225+
f"{', '.join(ignored)} configure the in-process pipeline and are ignored with "
223226
f"--backend {args.backend}. Pass the engine's own flags through --backend-args instead."
224227
)
225228

‎src/diffusers/commands/serve/backends.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,14 @@
4141
from ...modular_pipelines import ModularPipeline
4242
from ...utils import export_to_video, load_image, load_video, logging, numpy_to_pil
4343
from ...utils.constants import DIFFUSERS_REQUEST_TIMEOUT
44-
from ..run import _as_audio_arrays, _as_pil_list, _get_generator, _load_pipeline, _save_audio_arrays
44+
from ..run import (
45+
PNG_COMPRESS_LEVEL,
46+
_as_audio_arrays,
47+
_as_pil_list,
48+
_get_generator,
49+
_load_pipeline,
50+
_save_audio_arrays,
51+
)
4552
from .generations import Generation
4653
from .manifest import GMSError, ServedModel, blocks_signature
4754

@@ -144,7 +151,10 @@ def _save_images(images: Any, directory: Path, name: str, media_type: str, rate:
144151
filename = f"{name}-{index}.{media_type.split('/')[1]}"
145152
if media_type == "image/jpeg":
146153
image = image.convert("RGB")
147-
image.save(directory / filename)
154+
if media_type == "image/png":
155+
image.save(directory / filename, compress_level=PNG_COMPRESS_LEVEL)
156+
else:
157+
image.save(directory / filename)
148158
artifacts.append({"file": filename, "media_type": media_type, "width": image.width, "height": image.height})
149159
return artifacts
150160

‎src/diffusers/commands/serve/remote.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
SANDBOX_PORT = 8000
3838
_SANDBOX_MANIFEST_PATH = "/tmp/diffusers-cli/gms.json"
3939
_SERVE_DEPS = ("fastapi", "uvicorn")
40+
# Each engine's own image carries the engine, its kernels and the CUDA toolchain they are built with.
41+
_ENGINE_IMAGES = {"sglang": "lmsysorg/sglang:latest", "vllm": "vllm/vllm-omni:latest"}
4042
_READY_POLL_SECONDS = 5.0
4143

4244
# Flags that say how the sandbox is set up, or that the sandbox server sets itself.
@@ -69,7 +71,10 @@ def add_remote_arguments(parser: ArgumentParser) -> None:
6971
parser.add_argument(
7072
"--image",
7173
default=None,
72-
help=f"Sandbox image for --remote (defaults to {_DEFAULT_REMOTE_IMAGE!r}). Must provide torch + CUDA.",
74+
help=(
75+
f"Sandbox image for --remote. Defaults to {_DEFAULT_REMOTE_IMAGE!r}, or to the engine's own image "
76+
f"with --backend sglang ({_ENGINE_IMAGES['sglang']!r}) or vllm ({_ENGINE_IMAGES['vllm']!r})."
77+
),
7378
)
7479
parser.add_argument(
7580
"--idle-timeout",
@@ -109,7 +114,7 @@ def serve_remote(args: Namespace, task: str) -> None:
109114
hf_token = args.token or get_token()
110115
logger.info(f"creating sandbox on flavor={args.flavor!r}...")
111116
create_kwargs: dict[str, Any] = {
112-
"image": args.image or _DEFAULT_REMOTE_IMAGE,
117+
"image": args.image or _ENGINE_IMAGES.get(args.backend, _DEFAULT_REMOTE_IMAGE),
113118
"flavor": args.flavor,
114119
"forward_hf_token": True,
115120
"token": hf_token,
@@ -133,6 +138,9 @@ def _stream(chunk: str) -> None:
133138
install_cmd = shlex.join(["uv", "pip", "install", "--system", "--break-system-packages", *dependencies])
134139
logger.info("installing dependencies in the sandbox...")
135140
sbx.run(install_cmd, on_stdout=_stream, on_stderr=_stream)
141+
# An image can put another environment first on PATH, with its own older `diffusers-cli`
142+
# (the SGLang image does), so the server is started with the interpreter the install went into.
143+
python = sbx.run(["uv", "python", "find", "--system"]).stdout.strip()
136144

137145
task_kwargs = _build_task_kwargs(args)
138146
for key in (*_LOCAL_KEYS, *_SANDBOX_SET_KEYS):
@@ -143,7 +151,8 @@ def _stream(chunk: str) -> None:
143151
task_kwargs["port"] = SANDBOX_PORT
144152
# Clients reach the server through the proxy, so output links must be built on the proxied address.
145153
task_kwargs["public_url"] = sbx.proxy_url_for(SANDBOX_PORT)
146-
cli_argv = ["diffusers-cli", "--format", "quiet", *_kwargs_to_argv(task, task_kwargs)]
154+
cli_module = [python, "-m", "diffusers.commands.diffusers_cli"]
155+
cli_argv = [*cli_module, "--format", "quiet", *_kwargs_to_argv(task, task_kwargs)]
147156

148157
threading.Thread(target=_announce_when_ready, args=(sbx, args, stopped), daemon=True).start()
149158
logger.info(f"starting the server in sandbox {sbx.id}; stop it with Ctrl-C.")

‎tests/others/test_cli_commands.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
RunCommand,
3535
_build_task_kwargs,
3636
_collapse_frame_dirs,
37+
_compile_denoiser,
38+
_compile_vae_decoder,
3739
_download_outputs_from_sandbox,
3840
_kwargs_to_argv,
3941
_load_lora,
@@ -241,6 +243,41 @@ def test_compile_arg(self):
241243
assert compiled_blocks
242244
assert all(m._compiled_call_impl is not None for m in compiled_blocks)
243245

246+
def test_compile_mode_full_compiles_the_whole_denoiser(self, monkeypatch):
247+
class Denoiser(torch.nn.Module):
248+
_repeated_blocks = ["Block"]
249+
250+
def compile_repeated_blocks(self, **kwargs):
251+
raise AssertionError("regional compilation must not run with compile_mode='full'")
252+
253+
compiled = {}
254+
monkeypatch.setattr(torch, "compile", lambda module, **kwargs: compiled.update(kwargs) or "compiled")
255+
pipeline = SimpleNamespace(transformer=Denoiser())
256+
_compile_denoiser(pipeline, '{"mode": "max-autotune"}', "full")
257+
assert pipeline.transformer == "compiled"
258+
assert compiled == {"mode": "max-autotune"}
259+
260+
def test_regional_compile_rejects_cuda_graph_modes(self):
261+
class Denoiser(torch.nn.Module):
262+
_repeated_blocks = ["Block"]
263+
264+
pipeline = SimpleNamespace(transformer=Denoiser())
265+
with pytest.raises(SystemExit, match="uses CUDA graphs.*--compile-mode full"):
266+
_compile_denoiser(pipeline, '{"mode": "max-autotune"}', "regional")
267+
268+
def test_compile_vae_decoder_compiles_the_decoder(self, monkeypatch):
269+
compiled = {}
270+
monkeypatch.setattr(torch, "compile", lambda module, **kwargs: compiled.update(kwargs) or "compiled")
271+
pipeline = SimpleNamespace(vae=SimpleNamespace(decoder=torch.nn.Conv2d(4, 3, 3)))
272+
_compile_vae_decoder(pipeline, '{"fullgraph": true}')
273+
assert pipeline.vae.decoder == "compiled"
274+
assert compiled == {"fullgraph": True}
275+
276+
def test_compile_vae_decoder_skips_a_pipeline_without_one(self):
277+
pipeline = SimpleNamespace(vae=None)
278+
_compile_vae_decoder(pipeline, "{}")
279+
assert pipeline.vae is None
280+
244281
@require_torch_gpu
245282
# `--attention-backend` only exposes Hub-hosted kernels, all of which need `kernels>=0.12`.
246283
@require_kernels_version_greater_or_equal("0.12")

0 commit comments

Comments
 (0)