Spaces:
Running on Zero
Running on Zero
FLUX.3 layout-control canvas (custom HTML) — initial
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -35
- README.md +27 -7
- app.py +624 -0
- diffusers/__init__.py +1812 -0
- diffusers/callbacks.py +244 -0
- diffusers/commands/__init__.py +27 -0
- diffusers/commands/custom_blocks.py +145 -0
- diffusers/commands/diffusers_cli.py +69 -0
- diffusers/commands/env.py +185 -0
- diffusers/commands/fp16_safetensors.py +144 -0
- diffusers/commands/run.py +1335 -0
- diffusers/commands/schema.py +287 -0
- diffusers/commands/skills.py +459 -0
- diffusers/configuration_utils.py +726 -0
- diffusers/dependency_versions_check.py +34 -0
- diffusers/dependency_versions_table.py +56 -0
- diffusers/experimental/__init__.py +1 -0
- diffusers/experimental/rl/__init__.py +1 -0
- diffusers/experimental/rl/value_guided_sampling.py +153 -0
- diffusers/guiders/__init__.py +32 -0
- diffusers/guiders/adaptive_projected_guidance.py +253 -0
- diffusers/guiders/adaptive_projected_guidance_mix.py +297 -0
- diffusers/guiders/auto_guidance.py +198 -0
- diffusers/guiders/classifier_free_guidance.py +156 -0
- diffusers/guiders/classifier_free_zero_star_guidance.py +164 -0
- diffusers/guiders/frequency_decoupled_guidance.py +335 -0
- diffusers/guiders/guider_utils.py +396 -0
- diffusers/guiders/ltx2_guidance.py +171 -0
- diffusers/guiders/magnitude_aware_guidance.py +159 -0
- diffusers/guiders/perturbed_attention_guidance.py +289 -0
- diffusers/guiders/skip_layer_guidance.py +280 -0
- diffusers/guiders/smoothed_energy_guidance.py +269 -0
- diffusers/guiders/tangential_classifier_free_guidance.py +151 -0
- diffusers/hooks/__init__.py +31 -0
- diffusers/hooks/_common.py +61 -0
- diffusers/hooks/_helpers.py +401 -0
- diffusers/hooks/context_parallel.py +382 -0
- diffusers/hooks/faster_cache.py +654 -0
- diffusers/hooks/first_block_cache.py +258 -0
- diffusers/hooks/group_offloading.py +1056 -0
- diffusers/hooks/hooks.py +326 -0
- diffusers/hooks/layer_skip.py +263 -0
- diffusers/hooks/layerwise_casting.py +240 -0
- diffusers/hooks/mag_cache.py +468 -0
- diffusers/hooks/pyramid_attention_broadcast.py +314 -0
- diffusers/hooks/smoothed_energy_guidance_utils.py +166 -0
- diffusers/hooks/taylorseer_cache.py +345 -0
- diffusers/hooks/tensor_parallel.py +273 -0
- diffusers/hooks/tensor_parallel_neuron.py +178 -0
- diffusers/hooks/text_kv_cache.py +173 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,13 +1,33 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FLUX.3 Layout Canvas
|
| 3 |
+
emoji: 🎛️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.49.1
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
python_version: 3.12.12
|
| 11 |
+
short_description: FLUX.3 image-small bounding-box layout canvas (ZeroGPU)
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# FLUX.3 image-small — layout-control canvas (private)
|
| 15 |
+
|
| 16 |
+
A custom-HTML canvas on top of the **diffusers-native FLUX.3 modular pipeline**
|
| 17 |
+
([`diffusers-new-model-addition-f3` PR #23](https://github.com/huggingface/diffusers-new-model-addition-f3/pull/23)),
|
| 18 |
+
on ZeroGPU.
|
| 19 |
+
|
| 20 |
+
Drag a bounding box for every element that matters, give each box an id and a
|
| 21 |
+
description, write a one-line scene prompt, and FLUX.3 renders the scene with
|
| 22 |
+
every element placed inside its box. Toggle the **Bounding boxes** overlay on the
|
| 23 |
+
result, or **Replay** to regenerate.
|
| 24 |
+
|
| 25 |
+
> **Private.** Weights are read at runtime from the private repo
|
| 26 |
+
> `diffusers-internal-dev/f3-small-img` (never copied here). Keep this Space private.
|
| 27 |
+
|
| 28 |
+
Layout control uses FLUX.3's region format: the final prompt is the scene caption
|
| 29 |
+
followed by a JSON list of regions, each `{"id", "bbox": [y0, x0, y1, x1] (0-1000), "desc"}`.
|
| 30 |
+
|
| 31 |
+
## Setup
|
| 32 |
+
|
| 33 |
+
Needs an `HF_TOKEN` secret with read access to `diffusers-internal-dev/f3-small-img`.
|
app.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FLUX.3 image-small — layout-control canvas (private ZeroGPU demo, diffusers modular).
|
| 2 |
+
|
| 3 |
+
A custom HTML/JS canvas on top of the diffusers-native FLUX.3 modular pipeline
|
| 4 |
+
(PR #23, vendored `diffusers/`). You drag bounding boxes on an aspect-ratio
|
| 5 |
+
artboard, give each box an id + a description, write a one-line scene prompt, and
|
| 6 |
+
FLUX.3 renders the scene with every element placed inside its box.
|
| 7 |
+
|
| 8 |
+
Layout control uses FLUX.3's region format (see `integration-notes/flux3_examples.py`):
|
| 9 |
+
the final prompt is the scene caption followed by a JSON list of regions, each
|
| 10 |
+
`{"id", "bbox": [y0, x0, y1, x1] (normalized 0-1000), "desc"}`.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import importlib.util
|
| 14 |
+
import os
|
| 15 |
+
|
| 16 |
+
os.environ.setdefault("TORCH_ALLOW_TF32_CUBLAS_OVERRIDE", "1")
|
| 17 |
+
|
| 18 |
+
spaces = None
|
| 19 |
+
if importlib.util.find_spec("spaces"):
|
| 20 |
+
import spaces # before torch so ZeroGPU can patch torch
|
| 21 |
+
|
| 22 |
+
import base64
|
| 23 |
+
import io
|
| 24 |
+
import json
|
| 25 |
+
import random
|
| 26 |
+
|
| 27 |
+
import gradio as gr
|
| 28 |
+
import torch
|
| 29 |
+
|
| 30 |
+
from diffusers import ModularPipeline
|
| 31 |
+
|
| 32 |
+
REPO = os.environ.get("F3_REPO", "diffusers-internal-dev/f3-small-img")
|
| 33 |
+
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 34 |
+
|
| 35 |
+
MAX_SEED = 2**63 - 1
|
| 36 |
+
DEFAULT_STEPS = int(os.environ.get("F3_STEPS", "50"))
|
| 37 |
+
DEFAULT_GUIDANCE = 3.5
|
| 38 |
+
|
| 39 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 40 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 41 |
+
|
| 42 |
+
# Aspect ratio -> pixel dims (multiples of 16, ~1 MP, the model's default area).
|
| 43 |
+
RATIOS = {
|
| 44 |
+
"1:1": (1024, 1024),
|
| 45 |
+
"3:2": (1216, 816),
|
| 46 |
+
"2:3": (816, 1216),
|
| 47 |
+
"4:3": (1152, 864),
|
| 48 |
+
"3:4": (864, 1152),
|
| 49 |
+
"16:9": (1344, 768),
|
| 50 |
+
"9:16": (768, 1344),
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# ---- load the pipeline on CPU at import; place on cuda under ZeroGPU ----
|
| 54 |
+
print(f"[app] loading modular pipeline from {REPO} ...", flush=True)
|
| 55 |
+
PIPE = ModularPipeline.from_pretrained(REPO, token=HF_TOKEN)
|
| 56 |
+
PIPE.load_components(dtype={"vae": torch.float32, "default": torch.bfloat16}, token=HF_TOKEN)
|
| 57 |
+
print("[app] components loaded on CPU.", flush=True)
|
| 58 |
+
if spaces is not None:
|
| 59 |
+
PIPE.to("cuda")
|
| 60 |
+
print("[app] pipeline on cuda; ready.", flush=True)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _build_prompt(scene: str, elements: list) -> str:
|
| 64 |
+
"""Scene caption + JSON region list, FLUX.3's layout-control format."""
|
| 65 |
+
scene = (scene or "").strip()
|
| 66 |
+
regions = []
|
| 67 |
+
for e in elements:
|
| 68 |
+
desc = (e.get("desc") or "").strip()
|
| 69 |
+
if not desc:
|
| 70 |
+
continue
|
| 71 |
+
bbox = e.get("bbox")
|
| 72 |
+
if not (isinstance(bbox, list) and len(bbox) == 4):
|
| 73 |
+
continue
|
| 74 |
+
regions.append({
|
| 75 |
+
"id": e.get("id") or "element",
|
| 76 |
+
"bbox": [int(round(v)) for v in bbox],
|
| 77 |
+
"desc": desc,
|
| 78 |
+
})
|
| 79 |
+
if not regions:
|
| 80 |
+
return scene
|
| 81 |
+
return (scene + " " if scene else "") + json.dumps(regions)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _duration(state_json, *_):
|
| 85 |
+
try:
|
| 86 |
+
st = json.loads(state_json) if state_json else {}
|
| 87 |
+
w, h = RATIOS.get(st.get("aspect", "1:1"), (1024, 1024))
|
| 88 |
+
except Exception:
|
| 89 |
+
w, h = 1024, 1024
|
| 90 |
+
px = w * h
|
| 91 |
+
return int(min(300, 45 + DEFAULT_STEPS * 2.6 * max(1.0, px / (1024 * 1024))))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def _generate(state_json):
|
| 95 |
+
try:
|
| 96 |
+
st = json.loads(state_json) if state_json else {}
|
| 97 |
+
except Exception as e:
|
| 98 |
+
raise gr.Error(f"Bad canvas state: {e}")
|
| 99 |
+
|
| 100 |
+
elements = st.get("elements") or []
|
| 101 |
+
scene = st.get("scene") or ""
|
| 102 |
+
aspect = st.get("aspect") or "1:1"
|
| 103 |
+
w, h = RATIOS.get(aspect, (1024, 1024))
|
| 104 |
+
|
| 105 |
+
full_prompt = _build_prompt(scene, elements)
|
| 106 |
+
if not full_prompt.strip():
|
| 107 |
+
raise gr.Error("Add at least a scene prompt or one described element.")
|
| 108 |
+
|
| 109 |
+
seed = st.get("seed")
|
| 110 |
+
seed = int(seed) if isinstance(seed, (int, float)) and seed >= 0 else random.randint(0, MAX_SEED)
|
| 111 |
+
|
| 112 |
+
PIPE.guider = PIPE.guider.new(guidance_scale=DEFAULT_GUIDANCE)
|
| 113 |
+
image = PIPE(
|
| 114 |
+
prompt=full_prompt,
|
| 115 |
+
height=h,
|
| 116 |
+
width=w,
|
| 117 |
+
num_inference_steps=DEFAULT_STEPS,
|
| 118 |
+
generator=torch.Generator().manual_seed(seed),
|
| 119 |
+
output="images",
|
| 120 |
+
)[0]
|
| 121 |
+
if torch.cuda.is_available():
|
| 122 |
+
torch.cuda.synchronize()
|
| 123 |
+
|
| 124 |
+
buf = io.BytesIO()
|
| 125 |
+
image.save(buf, format="WEBP", quality=92)
|
| 126 |
+
data_uri = "data:image/webp;base64," + base64.b64encode(buf.getvalue()).decode()
|
| 127 |
+
print(f"[app] generate {w}x{h} steps={DEFAULT_STEPS} seed={seed} "
|
| 128 |
+
f"elements={len(elements)} prompt_len={len(full_prompt)}", flush=True)
|
| 129 |
+
return json.dumps({
|
| 130 |
+
"image": data_uri,
|
| 131 |
+
"aspect": aspect,
|
| 132 |
+
"w": w, "h": h,
|
| 133 |
+
"seed": seed,
|
| 134 |
+
"elements": [{"id": e.get("id"), "bbox": e.get("bbox"), "color": e.get("color")}
|
| 135 |
+
for e in elements if e.get("bbox")],
|
| 136 |
+
"prompt": full_prompt,
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
generate = spaces.GPU(_generate, duration=_duration) if spaces is not None else _generate
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
# Preset scenes (the top tabs), reconstructed from BFL's FLUX.3 layout examples.
|
| 145 |
+
# bbox is [y0, x0, y1, x1] normalized 0-1000.
|
| 146 |
+
# ---------------------------------------------------------------------------
|
| 147 |
+
PRESETS = {
|
| 148 |
+
"Le Festival du Soleil": {
|
| 149 |
+
"aspect": "3:4",
|
| 150 |
+
"scene": "A glowing concrete dome rises from a twilight bay, a crowd on the beach before it.",
|
| 151 |
+
"elements": [
|
| 152 |
+
{"id": "Fr_Text_1", "bbox": [90, 230, 230, 780],
|
| 153 |
+
"desc": "\"LE FESTIVAL DU SOLEIL\" written in a thin, elegant, serif typeface in a light cream color."},
|
| 154 |
+
{"id": "dome_1", "bbox": [250, 150, 650, 850], "desc": "a massive, smooth parabolic dome of pale concrete"},
|
| 155 |
+
{"id": "town_1", "bbox": [250, 640, 430, 900], "desc": "faint lights and small buildings at the foot of the hills"},
|
| 156 |
+
{"id": "swimmers_1", "bbox": [580, 200, 720, 800],
|
| 157 |
+
"desc": "dozens of small, silhouetted figures scattered in the dark water, wading"},
|
| 158 |
+
{"id": "crowd_1", "bbox": [740, 0, 1000, 1000],
|
| 159 |
+
"desc": "a large crowd of people seated on the beach in casual, light-colored summer attire"},
|
| 160 |
+
],
|
| 161 |
+
},
|
| 162 |
+
"Five frames of winter": {
|
| 163 |
+
"aspect": "16:9",
|
| 164 |
+
"scene": "Five vertical frames of a quiet winter day, left to right from dawn to dusk.",
|
| 165 |
+
"elements": [
|
| 166 |
+
{"id": "frame_1", "bbox": [0, 0, 1000, 190], "desc": "a bare birch forest at pale blue dawn, frost on every branch"},
|
| 167 |
+
{"id": "frame_2", "bbox": [0, 200, 1000, 390], "desc": "a frozen lake under flat grey light, a lone figure skating"},
|
| 168 |
+
{"id": "frame_3", "bbox": [0, 400, 1000, 590], "desc": "heavy snowfall over a red wooden cabin with a lit window"},
|
| 169 |
+
{"id": "frame_4", "bbox": [0, 600, 1000, 790], "desc": "a snowy pine ridge at golden hour, long blue shadows"},
|
| 170 |
+
{"id": "frame_5", "bbox": [0, 800, 1000, 1000], "desc": "the same valley at dusk, deep indigo sky and the first stars"},
|
| 171 |
+
],
|
| 172 |
+
},
|
| 173 |
+
"Knitted iceberg village": {
|
| 174 |
+
"aspect": "4:3",
|
| 175 |
+
"scene": "A knitted diorama: three cable-knit icebergs with tiny houses, a banner and a magenta whale tail.",
|
| 176 |
+
"elements": [
|
| 177 |
+
{"id": "En_Text_1", "bbox": [130, 220, 220, 780],
|
| 178 |
+
"desc": "The exact words \"KNITTED ICEBERG VILLAGE\" embroidered in thick, raised navy-blue yarn across a banner."},
|
| 179 |
+
{"id": "iceberg_3", "bbox": [200, 300, 650, 700],
|
| 180 |
+
"desc": "The largest central iceberg, a bulky mound of white and pale grey ribbed knitting, dominating the midground with thick, vertical columns of stitches."},
|
| 181 |
+
{"id": "iceberg_1", "bbox": [230, 60, 650, 320],
|
| 182 |
+
"desc": "A conical soft sculpture made of stark white, chunky roving yarn in a thick cable-knit pattern. The left side is brightly lit, while the right side falls into deep, fuzzy shadow."},
|
| 183 |
+
{"id": "iceberg_2", "bbox": [230, 680, 640, 960],
|
| 184 |
+
"desc": "A tall, asymmetrical peak of white cable-knit yarn, featuring a few visibly dropped stitches near the base and slightly uneven tension in the cables."},
|
| 185 |
+
{"id": "whale_tail_1", "bbox": [500, 380, 850, 620],
|
| 186 |
+
"desc": "A giant whale tail knitted from bright magenta and coral-pink yarn, bursting vertically from the teal sea."},
|
| 187 |
+
],
|
| 188 |
+
},
|
| 189 |
+
"Up from the subway": {
|
| 190 |
+
"aspect": "3:2",
|
| 191 |
+
"scene": "Looking up a subway staircase at dusk, a blurred figure climbing toward the skyline.",
|
| 192 |
+
"elements": [
|
| 193 |
+
{"id": "building_1", "bbox": [50, 120, 630, 470],
|
| 194 |
+
"desc": "a modern, red-brick high-rise skyscraper stretching into the sky"},
|
| 195 |
+
{"id": "building_2", "bbox": [0, 450, 650, 850],
|
| 196 |
+
"desc": "a tall, classical white stone skyscraper with a tiered, set-back top"},
|
| 197 |
+
{"id": "person_1", "bbox": [300, 380, 820, 640],
|
| 198 |
+
"desc": "a heavily blurred, dark shape of a person seen from behind"},
|
| 199 |
+
{"id": "stairs_1", "bbox": [600, 200, 1000, 800],
|
| 200 |
+
"desc": "a flight of concrete subway stairs filling the bottom center of the frame"},
|
| 201 |
+
{"id": "En_Text_1", "bbox": [300, 760, 640, 1000],
|
| 202 |
+
"desc": "a partially visible, glowing green and white sign on the building"},
|
| 203 |
+
],
|
| 204 |
+
},
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
RATIO_LIST = list(RATIOS.keys())
|
| 208 |
+
|
| 209 |
+
CONFIG_JS = (
|
| 210 |
+
"<script>window.F3_PRESETS = " + json.dumps(PRESETS) + ";"
|
| 211 |
+
"window.F3_RATIOS = " + json.dumps(RATIO_LIST) + ";"
|
| 212 |
+
"window.F3_PRESET_ORDER = " + json.dumps(list(PRESETS.keys())) + ";</script>"
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# Styles + behavior live in <head>; the component mounts into #f3-root (gr.HTML).
|
| 216 |
+
STYLE = """
|
| 217 |
+
<style>
|
| 218 |
+
#f3-root, #f3-root * { box-sizing: border-box; }
|
| 219 |
+
#f3-root {
|
| 220 |
+
--bg:#09090b; --panel:#101013; --panel2:#0c0c0f; --line:#26262b; --line2:#1b1b1f;
|
| 221 |
+
--txt:#e7e7ea; --dim:#8a8a93; --dim2:#5f5f68; --violet:#8b7cf6; --violetbg:rgba(139,124,246,.14);
|
| 222 |
+
--green:#3fd19b; --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
| 223 |
+
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;
|
| 224 |
+
color:var(--txt); width:100%;
|
| 225 |
+
}
|
| 226 |
+
#f3-root .tabs{display:flex;gap:4px;background:var(--panel);border:1px solid var(--line2);
|
| 227 |
+
border-radius:12px;padding:5px;width:max-content;max-width:100%;overflow:auto;margin-bottom:14px;}
|
| 228 |
+
#f3-root .tab{padding:9px 16px;border-radius:8px;color:var(--dim);font-size:14px;cursor:pointer;
|
| 229 |
+
white-space:nowrap;transition:.15s;}
|
| 230 |
+
#f3-root .tab:hover{color:var(--txt);}
|
| 231 |
+
#f3-root .tab.active{background:#222228;color:var(--txt);}
|
| 232 |
+
#f3-root .main{display:grid;grid-template-columns:1fr 390px;gap:16px;align-items:start;}
|
| 233 |
+
@media (max-width:980px){#f3-root .main{grid-template-columns:1fr;}}
|
| 234 |
+
|
| 235 |
+
#f3-root .canvas-wrap{background:var(--panel2);border:1px solid var(--line2);border-radius:14px;
|
| 236 |
+
padding:22px;position:relative;min-height:560px;
|
| 237 |
+
background-image:radial-gradient(var(--line) 1px,transparent 1px);background-size:17px 17px;}
|
| 238 |
+
#f3-root .board{position:relative;margin:0 auto;background:#0b0b0e;border:1px solid #34343c;
|
| 239 |
+
box-shadow:0 0 0 1px #000 inset; user-select:none; touch-action:none;}
|
| 240 |
+
#f3-root .board.empty{background:#0b0b0e;}
|
| 241 |
+
#f3-root .board img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:1px;}
|
| 242 |
+
#f3-root .ratio-tag{position:absolute;left:4px;bottom:-24px;color:var(--dim);font:12px/1 var(--mono);}
|
| 243 |
+
#f3-root .board-tools{position:absolute;right:22px;bottom:22px;display:flex;gap:8px;align-items:center;z-index:30;}
|
| 244 |
+
#f3-root .btn{display:inline-flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--line);
|
| 245 |
+
color:var(--dim);border-radius:9px;padding:8px 13px;font-size:13px;cursor:pointer;transition:.15s;}
|
| 246 |
+
#f3-root .btn:hover{color:var(--txt);border-color:#3a3a42;}
|
| 247 |
+
#f3-root .btn.on{color:var(--txt);}
|
| 248 |
+
#f3-root .toggle{width:34px;height:19px;border-radius:12px;background:#2a2a30;position:relative;transition:.15s;flex:none;}
|
| 249 |
+
#f3-root .toggle::after{content:'';position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;
|
| 250 |
+
background:#777;transition:.15s;}
|
| 251 |
+
#f3-root .btn.on .toggle{background:var(--violet);}
|
| 252 |
+
#f3-root .btn.on .toggle::after{left:17px;background:#fff;}
|
| 253 |
+
|
| 254 |
+
#f3-root .box{position:absolute;border:1.5px solid var(--violet);background:var(--violetbg);cursor:move;}
|
| 255 |
+
#f3-root .box.sel{box-shadow:0 0 0 1px var(--violet);}
|
| 256 |
+
#f3-root .box .tag{position:absolute;left:-1px;top:-22px;display:flex;gap:7px;align-items:center;
|
| 257 |
+
background:var(--violet);color:#0a0a0c;font:600 11px/1.4 var(--mono);padding:3px 7px;border-radius:4px;
|
| 258 |
+
white-space:nowrap;max-width:420px;}
|
| 259 |
+
#f3-root .box .tag .id{outline:none;}
|
| 260 |
+
#f3-root .box .tag .coords{color:rgba(10,10,12,.62);font-weight:500;}
|
| 261 |
+
#f3-root .box .desc{position:absolute;inset:7px;overflow:auto;color:var(--txt);font-size:12.5px;line-height:1.35;
|
| 262 |
+
outline:none;background:transparent;}
|
| 263 |
+
#f3-root .box .desc:empty::before{content:'describe this element…';color:var(--dim2);}
|
| 264 |
+
#f3-root .box .rm{position:absolute;right:-9px;top:-9px;width:18px;height:18px;border-radius:50%;background:#222;
|
| 265 |
+
border:1px solid var(--line);color:var(--dim);font-size:12px;line-height:16px;text-align:center;cursor:pointer;
|
| 266 |
+
display:none;}
|
| 267 |
+
#f3-root .box:hover .rm{display:block;}
|
| 268 |
+
#f3-root .box .h{position:absolute;width:10px;height:10px;background:#0b0b0e;border:1.5px solid var(--violet);border-radius:2px;}
|
| 269 |
+
#f3-root .box .h.se{right:-6px;bottom:-6px;cursor:se-resize;}
|
| 270 |
+
#f3-root .box .h.ne{right:-6px;top:-6px;cursor:ne-resize;}
|
| 271 |
+
#f3-root .box .h.sw{left:-6px;bottom:-6px;cursor:sw-resize;}
|
| 272 |
+
#f3-root .box .h.nw{left:-6px;top:-6px;cursor:nw-resize;}
|
| 273 |
+
#f3-root .board.rendered .box{background:transparent;}
|
| 274 |
+
#f3-root .board.rendered .box .desc{display:none;}
|
| 275 |
+
#f3-root .board.hideboxes .box{display:none;}
|
| 276 |
+
#f3-root .board .hint{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
| 277 |
+
color:var(--dim2);font-size:13px;text-align:center;padding:30px;pointer-events:none;}
|
| 278 |
+
#f3-root .board .spin{position:absolute;inset:0;display:none;align-items:center;justify-content:center;
|
| 279 |
+
background:rgba(8,8,10,.55);color:var(--txt);font-size:13px;z-index:40;}
|
| 280 |
+
#f3-root .board.busy .spin{display:flex;}
|
| 281 |
+
|
| 282 |
+
#f3-root .side{background:var(--panel);border:1px solid var(--line2);border-radius:14px;overflow:hidden;}
|
| 283 |
+
#f3-root .step{padding:15px 18px;border-bottom:1px solid var(--line2);cursor:pointer;}
|
| 284 |
+
#f3-root .step:last-child{border-bottom:none;}
|
| 285 |
+
#f3-root .step .row{display:flex;gap:14px;align-items:center;}
|
| 286 |
+
#f3-root .step .n{font:12px/1 var(--mono);color:var(--dim2);}
|
| 287 |
+
#f3-root .step .t{font-size:15px;color:var(--dim);}
|
| 288 |
+
#f3-root .step.active{background:#17171b;}
|
| 289 |
+
#f3-root .step.active .t{color:var(--txt);}
|
| 290 |
+
#f3-root .step .hint{margin:9px 0 0 26px;color:var(--dim);font-size:13px;line-height:1.45;display:none;}
|
| 291 |
+
#f3-root .step.active .hint{display:block;}
|
| 292 |
+
#f3-root .summary{border-top:1px solid var(--line2);padding:16px 18px;max-height:230px;overflow:auto;}
|
| 293 |
+
#f3-root .summary .e{margin-bottom:13px;}
|
| 294 |
+
#f3-root .summary .e .h{font:600 12.5px/1.5 var(--mono);color:var(--violet);}
|
| 295 |
+
#f3-root .summary .e .h .c{color:var(--dim2);font-weight:500;}
|
| 296 |
+
#f3-root .summary .e .d{color:var(--dim);font-size:13px;line-height:1.4;}
|
| 297 |
+
#f3-root .summary .empty{color:var(--dim2);font-size:13px;}
|
| 298 |
+
#f3-root .scene{border-top:1px solid var(--line2);padding:16px 18px;}
|
| 299 |
+
#f3-root .scene label{display:block;font:11px/1 var(--mono);letter-spacing:.08em;color:var(--dim2);margin-bottom:9px;}
|
| 300 |
+
#f3-root .scene textarea{width:100%;background:transparent;border:none;color:var(--txt);font-size:15.5px;
|
| 301 |
+
line-height:1.4;resize:none;outline:none;min-height:46px;font-family:inherit;}
|
| 302 |
+
#f3-root .scene textarea::placeholder{color:var(--dim2);}
|
| 303 |
+
#f3-root .ratios{display:flex;flex-wrap:wrap;gap:7px;margin:11px 0 2px 26px;}
|
| 304 |
+
#f3-root .chip{padding:6px 11px;border:1px solid var(--line);border-radius:7px;color:var(--dim);
|
| 305 |
+
font:12px/1 var(--mono);cursor:pointer;}
|
| 306 |
+
#f3-root .chip.sel{border-color:var(--violet);color:var(--txt);background:var(--violetbg);}
|
| 307 |
+
#f3-root .gen{margin:12px 18px 18px;}
|
| 308 |
+
#f3-root .genbtn{width:100%;background:var(--violet);color:#0a0a0c;border:none;border-radius:10px;
|
| 309 |
+
padding:13px;font-size:14.5px;font-weight:600;cursor:pointer;transition:.15s;}
|
| 310 |
+
#f3-root .genbtn:hover{filter:brightness(1.08);}
|
| 311 |
+
#f3-root .genbtn[disabled]{opacity:.55;cursor:default;}
|
| 312 |
+
#f3-root .table-bar{margin-top:16px;background:var(--panel);border:1px solid var(--line2);border-radius:12px;
|
| 313 |
+
padding:16px 18px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;}
|
| 314 |
+
#f3-root .table-bar .lab{font:12px/1 var(--mono);letter-spacing:.06em;color:var(--dim);}
|
| 315 |
+
#f3-root .table-bar .cnt{font:12px/1 var(--mono);color:var(--dim2);}
|
| 316 |
+
#f3-root .etable{margin-top:8px;background:var(--panel);border:1px solid var(--line2);border-radius:12px;
|
| 317 |
+
overflow:auto;display:none;}
|
| 318 |
+
#f3-root .etable.open{display:block;}
|
| 319 |
+
#f3-root table{width:100%;border-collapse:collapse;font-size:13px;}
|
| 320 |
+
#f3-root th,#f3-root td{text-align:left;padding:10px 14px;border-bottom:1px solid var(--line2);vertical-align:top;}
|
| 321 |
+
#f3-root th{font:11px/1 var(--mono);letter-spacing:.06em;color:var(--dim2);text-transform:uppercase;}
|
| 322 |
+
#f3-root td.id{font:600 12.5px/1.4 var(--mono);color:var(--violet);white-space:nowrap;}
|
| 323 |
+
#f3-root td.bb{font:12px/1.4 var(--mono);color:var(--dim);white-space:nowrap;}
|
| 324 |
+
#f3-root td.d{color:var(--txt);}
|
| 325 |
+
</style>
|
| 326 |
+
"""
|
| 327 |
+
|
| 328 |
+
# Selection accent (green) + small extras, appended to the head styles.
|
| 329 |
+
STYLE2 = """
|
| 330 |
+
<style>
|
| 331 |
+
#f3-root .box.sel{border-color:var(--green);}
|
| 332 |
+
#f3-root .box.sel .tag{background:var(--green);}
|
| 333 |
+
#f3-root .box.sel .tag .coords{color:rgba(10,12,11,.6);}
|
| 334 |
+
#f3-root .box.sel .h{border-color:var(--green);}
|
| 335 |
+
</style>
|
| 336 |
+
"""
|
| 337 |
+
|
| 338 |
+
SCRIPT = r"""
|
| 339 |
+
<script>
|
| 340 |
+
(function(){
|
| 341 |
+
const NS = window.F3 = window.F3 || {};
|
| 342 |
+
const RATIOS = {"1:1":[1024,1024],"3:2":[1216,816],"2:3":[816,1216],"4:3":[1152,864],
|
| 343 |
+
"3:4":[864,1152],"16:9":[1344,768],"9:16":[768,1344]};
|
| 344 |
+
const STEP_HINTS = [
|
| 345 |
+
["Aspect ratio","Pick the frame your scene lives in."],
|
| 346 |
+
["Elements","Drag out a box for every element that matters, then describe what goes in it."],
|
| 347 |
+
["Scene prompt","Give the scene one line that ties the elements together."],
|
| 348 |
+
["Generate","FLUX.3 renders the scene with every element inside its box."],
|
| 349 |
+
];
|
| 350 |
+
let S = {aspect:"1:1", elements:[], scene:"", seed:-1, rendered:false, step:1, showBoxes:true};
|
| 351 |
+
let seq = 0, sel = null;
|
| 352 |
+
|
| 353 |
+
function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
|
| 354 |
+
function uid(base){ base=(base||"element").replace(/[^A-Za-z0-9_]+/g,"_").replace(/^_+|_+$/g,"")||"element";
|
| 355 |
+
let id=base, n=1; const ids=new Set(S.elements.map(e=>e.id));
|
| 356 |
+
while(ids.has(id)){ id=base+"_"+(++n);} return id; }
|
| 357 |
+
|
| 358 |
+
function setHidden(id,val){
|
| 359 |
+
const w=document.getElementById(id); if(!w) return;
|
| 360 |
+
const el=w.querySelector('textarea,input'); if(!el) return;
|
| 361 |
+
const proto = el.tagName==='TEXTAREA'?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype;
|
| 362 |
+
const setter=Object.getOwnPropertyDescriptor(proto,'value').set;
|
| 363 |
+
setter.call(el,val); el.dispatchEvent(new Event('input',{bubbles:true}));
|
| 364 |
+
}
|
| 365 |
+
function syncState(){
|
| 366 |
+
setHidden('f3-state', JSON.stringify({
|
| 367 |
+
aspect:S.aspect, scene:S.scene, seed:S.seed,
|
| 368 |
+
elements:S.elements.map(e=>({id:e.id,bbox:e.bbox,desc:e.desc,color:e.color})),
|
| 369 |
+
}));
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// ---------- board geometry ----------
|
| 373 |
+
function boardSize(){
|
| 374 |
+
const wrap=NS.el.wrap; const availW=wrap.clientWidth-44; const maxH=600;
|
| 375 |
+
const [W,H]=RATIOS[S.aspect]; let w=availW, h=w*H/W;
|
| 376 |
+
if(h>maxH){ h=maxH; w=h*W/H; } if(w>availW){ w=availW; h=w*H/W; }
|
| 377 |
+
return [Math.round(w),Math.round(h)];
|
| 378 |
+
}
|
| 379 |
+
function layoutBoard(){
|
| 380 |
+
const [w,h]=boardSize(); const b=NS.el.board; b.style.width=w+'px'; b.style.height=h+'px';
|
| 381 |
+
NS.el.ratioTag.textContent=S.aspect;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
// ---------- rendering ----------
|
| 385 |
+
function boxEl(e){
|
| 386 |
+
const d=document.createElement('div'); d.className='box'; d.dataset.id=e.id;
|
| 387 |
+
d.innerHTML='<div class="tag"><span class="id" contenteditable="true" spellcheck="false"></span>'
|
| 388 |
+
+'<span class="coords"></span></div>'
|
| 389 |
+
+'<div class="desc" contenteditable="true" spellcheck="false"></div>'
|
| 390 |
+
+'<div class="rm">×</div>'
|
| 391 |
+
+'<div class="h nw"></div><div class="h ne"></div><div class="h sw"></div><div class="h se"></div>';
|
| 392 |
+
d.querySelector('.id').textContent=e.id;
|
| 393 |
+
d.querySelector('.desc').textContent=e.desc||'';
|
| 394 |
+
wireBox(d,e); return d;
|
| 395 |
+
}
|
| 396 |
+
function posBox(d,e){
|
| 397 |
+
const [y0,x0,y1,x1]=e.bbox;
|
| 398 |
+
d.style.left=(x0/10)+'%'; d.style.top=(y0/10)+'%';
|
| 399 |
+
d.style.width=((x1-x0)/10)+'%'; d.style.height=((y1-y0)/10)+'%';
|
| 400 |
+
d.querySelector('.coords').textContent='['+[y0,x0,y1,x1].map(Math.round).join(', ')+']';
|
| 401 |
+
}
|
| 402 |
+
function renderBoxes(){
|
| 403 |
+
const b=NS.el.board;
|
| 404 |
+
b.querySelectorAll('.box').forEach(n=>n.remove());
|
| 405 |
+
S.elements.forEach(e=>{ const d=boxEl(e); posBox(d,e); if(e===sel)d.classList.add('sel'); b.appendChild(d); });
|
| 406 |
+
}
|
| 407 |
+
function renderSummary(){
|
| 408 |
+
const s=NS.el.summary;
|
| 409 |
+
if(!S.elements.length){ s.innerHTML='<div class="empty">No elements yet — drag a box on the canvas.</div>'; return; }
|
| 410 |
+
s.innerHTML=S.elements.map(e=>'<div class="e"><div class="h">'+esc(e.id)
|
| 411 |
+
+' <span class="c">['+e.bbox.map(Math.round).join(', ')+']</span></div>'
|
| 412 |
+
+'<div class="d">'+esc(e.desc||'')+'</div></div>').join('');
|
| 413 |
+
}
|
| 414 |
+
function renderTable(){
|
| 415 |
+
NS.el.cnt.textContent=S.elements.length+' ROW'+(S.elements.length===1?'':'S');
|
| 416 |
+
const body=S.elements.map(e=>'<tr><td class="id">'+esc(e.id)+'</td><td class="bb">['
|
| 417 |
+
+e.bbox.map(Math.round).join(', ')+']</td><td class="d">'+esc(e.desc||'')+'</td></tr>').join('');
|
| 418 |
+
NS.el.etable.innerHTML='<table><thead><tr><th>Element</th><th>bbox [y0,x0,y1,x1]</th><th>Description</th></tr></thead><tbody>'
|
| 419 |
+
+(body||'<tr><td colspan="3" class="d" style="color:var(--dim2)">empty</td></tr>')+'</tbody></table>';
|
| 420 |
+
}
|
| 421 |
+
function renderSteps(){
|
| 422 |
+
NS.el.steps.forEach((st,i)=>st.classList.toggle('active', (i+1)===S.step));
|
| 423 |
+
NS.el.ratios.style.display = S.step===1?'flex':'none';
|
| 424 |
+
}
|
| 425 |
+
function renderAll(){ layoutBoard(); renderBoxes(); renderSummary(); renderTable(); renderSteps();
|
| 426 |
+
NS.el.ratioChips.forEach(c=>c.classList.toggle('sel', c.dataset.r===S.aspect)); syncState(); }
|
| 427 |
+
function esc(s){return (s||'').replace(/[&<>]/g,m=>({'&':'&','<':'<','>':'>'}[m]));}
|
| 428 |
+
|
| 429 |
+
function select(e){ sel=e; NS.el.board.querySelectorAll('.box').forEach(d=>
|
| 430 |
+
d.classList.toggle('sel', d.dataset.id===(e&&e.id))); }
|
| 431 |
+
|
| 432 |
+
// ---------- box interactions ----------
|
| 433 |
+
function px2norm(ev){
|
| 434 |
+
const r=NS.el.board.getBoundingClientRect();
|
| 435 |
+
return [clamp((ev.clientY-r.top)/r.height*1000,0,1000), clamp((ev.clientX-r.left)/r.width*1000,0,1000)];
|
| 436 |
+
}
|
| 437 |
+
function wireBox(d,e){
|
| 438 |
+
const idEl=d.querySelector('.id'), descEl=d.querySelector('.desc');
|
| 439 |
+
d.addEventListener('mousedown',ev=>{
|
| 440 |
+
if(ev.target.classList.contains('h')||ev.target.classList.contains('rm')||
|
| 441 |
+
ev.target.isContentEditable) { select(e); return; }
|
| 442 |
+
ev.preventDefault(); select(e); startMove(ev,e,d);
|
| 443 |
+
});
|
| 444 |
+
d.querySelectorAll('.h').forEach(h=>h.addEventListener('mousedown',ev=>{
|
| 445 |
+
ev.preventDefault(); ev.stopPropagation(); select(e);
|
| 446 |
+
startResize(ev,e,d,[...h.classList].find(c=>['nw','ne','sw','se'].includes(c)));
|
| 447 |
+
}));
|
| 448 |
+
d.querySelector('.rm').addEventListener('click',ev=>{ ev.stopPropagation();
|
| 449 |
+
S.elements=S.elements.filter(x=>x!==e); if(sel===e)sel=null; renderAll(); });
|
| 450 |
+
idEl.addEventListener('blur',()=>{ let v=idEl.textContent.trim().replace(/[^A-Za-z0-9_]+/g,'_');
|
| 451 |
+
if(!v){v=e.id;} if(v!==e.id){ const ids=new Set(S.elements.filter(x=>x!==e).map(x=>x.id));
|
| 452 |
+
let f=v,n=1; while(ids.has(f)){f=v+'_'+(++n);} e.id=f;} idEl.textContent=e.id; renderAll(); });
|
| 453 |
+
idEl.addEventListener('keydown',ev=>{ if(ev.key==='Enter'){ev.preventDefault();idEl.blur();} });
|
| 454 |
+
descEl.addEventListener('input',()=>{ e.desc=descEl.textContent; renderSummaryTable(); syncState(); });
|
| 455 |
+
descEl.addEventListener('focus',()=>select(e));
|
| 456 |
+
}
|
| 457 |
+
function renderSummaryTable(){ renderSummary(); renderTable(); }
|
| 458 |
+
|
| 459 |
+
function startMove(ev,e,d){
|
| 460 |
+
const [y,x]=px2norm(ev); const b=e.bbox.slice(); const oy=y-b[0], ox=x-b[1];
|
| 461 |
+
const w=b[3]-b[1], h=b[2]-b[0];
|
| 462 |
+
function mv(ev2){ const [ny,nx]=px2norm(ev2); let x0=clamp(nx-ox,0,1000-w), y0=clamp(ny-oy,0,1000-h);
|
| 463 |
+
e.bbox=[y0,x0,y0+h,x0+w]; posBox(d,e); liveMeta(e); }
|
| 464 |
+
function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); renderAll(); }
|
| 465 |
+
document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
|
| 466 |
+
}
|
| 467 |
+
function startResize(ev,e,d,corner){
|
| 468 |
+
function mv(ev2){ const [ny,nx]=px2norm(ev2); let [y0,x0,y1,x1]=e.bbox;
|
| 469 |
+
if(corner.includes('n'))y0=ny; if(corner.includes('s'))y1=ny;
|
| 470 |
+
if(corner.includes('w'))x0=nx; if(corner.includes('e'))x1=nx;
|
| 471 |
+
e.bbox=[Math.min(y0,y1),Math.min(x0,x1),Math.max(y0,y1),Math.max(x0,x1)]; posBox(d,e); liveMeta(e); }
|
| 472 |
+
function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); renderAll(); }
|
| 473 |
+
document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
|
| 474 |
+
}
|
| 475 |
+
function liveMeta(e){ renderSummary(); renderTable(); }
|
| 476 |
+
|
| 477 |
+
function startDraw(ev){
|
| 478 |
+
if(ev.target!==NS.el.board) return; // only empty canvas
|
| 479 |
+
if(S.rendered) return; // don't draw over a render
|
| 480 |
+
ev.preventDefault();
|
| 481 |
+
const [sy,sx]=px2norm(ev);
|
| 482 |
+
const e={id:uid('element'), color:'violet', bbox:[sy,sx,sy,sx], desc:''};
|
| 483 |
+
const d=boxEl(e); NS.el.board.appendChild(d);
|
| 484 |
+
function mv(ev2){ const [ny,nx]=px2norm(ev2);
|
| 485 |
+
e.bbox=[Math.min(sy,ny),Math.min(sx,nx),Math.max(sy,ny),Math.max(sx,nx)]; posBox(d,e); }
|
| 486 |
+
function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up);
|
| 487 |
+
if((e.bbox[2]-e.bbox[0])<30||(e.bbox[3]-e.bbox[1])<30){ d.remove(); return; }
|
| 488 |
+
S.elements.push(e); select(e); S.step=2; renderAll();
|
| 489 |
+
const de=NS.el.board.querySelector('.box[data-id="'+e.id+'"] .desc'); if(de)de.focus();
|
| 490 |
+
}
|
| 491 |
+
document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
// ---------- presets / tabs ----------
|
| 495 |
+
function loadPreset(name){
|
| 496 |
+
const p=window.F3_PRESETS[name]; if(!p) return;
|
| 497 |
+
S.aspect=p.aspect; S.scene=p.scene; S.seed=-1; S.rendered=false; S.step=2; sel=null;
|
| 498 |
+
S.elements=p.elements.map(e=>({id:e.id,color:'violet',bbox:e.bbox.slice(),desc:e.desc}));
|
| 499 |
+
NS.el.board.classList.remove('rendered','hideboxes','busy');
|
| 500 |
+
const img=NS.el.board.querySelector('img'); if(img)img.remove();
|
| 501 |
+
NS.el.scene.value=S.scene; NS.el.active=name;
|
| 502 |
+
NS.el.tabs.forEach(t=>t.classList.toggle('active', t.dataset.name===name));
|
| 503 |
+
NS.el.toggleBtn.style.display='none';
|
| 504 |
+
renderAll();
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
// ---------- generate / render ----------
|
| 508 |
+
function doGenerate(){
|
| 509 |
+
if(!S.elements.some(e=>(e.desc||'').trim()) && !(S.scene||'').trim()){
|
| 510 |
+
NS.el.scene.focus(); return;
|
| 511 |
+
}
|
| 512 |
+
S.seed=-1; S.step=4; renderSteps(); syncState();
|
| 513 |
+
NS.el.board.classList.add('busy');
|
| 514 |
+
NS.el.genbtn.setAttribute('disabled','');
|
| 515 |
+
setTimeout(()=>{ const b=document.querySelector('#f3-gen button'); if(b)b.click(); },70);
|
| 516 |
+
clearTimeout(NS._to); NS._to=setTimeout(()=>{ NS.el.board.classList.remove('busy');
|
| 517 |
+
NS.el.genbtn.removeAttribute('disabled'); }, 300000);
|
| 518 |
+
}
|
| 519 |
+
window.f3_render=function(resStr){
|
| 520 |
+
clearTimeout(NS._to);
|
| 521 |
+
NS.el.board.classList.remove('busy'); NS.el.genbtn.removeAttribute('disabled');
|
| 522 |
+
let r; try{ r=JSON.parse(resStr);}catch(e){ return; }
|
| 523 |
+
if(!r||!r.image) return;
|
| 524 |
+
layoutBoard();
|
| 525 |
+
let img=NS.el.board.querySelector('img'); if(!img){ img=document.createElement('img');
|
| 526 |
+
NS.el.board.insertBefore(img,NS.el.board.firstChild); } img.src=r.image;
|
| 527 |
+
S.rendered=true; S.seed=r.seed;
|
| 528 |
+
NS.el.board.classList.add('rendered'); NS.el.board.classList.remove('hideboxes');
|
| 529 |
+
S.showBoxes=true; updateToggle();
|
| 530 |
+
NS.el.toggleBtn.style.display='inline-flex';
|
| 531 |
+
renderBoxes();
|
| 532 |
+
};
|
| 533 |
+
function updateToggle(){
|
| 534 |
+
NS.el.toggleBtn.classList.toggle('on',S.showBoxes);
|
| 535 |
+
NS.el.board.classList.toggle('hideboxes',!S.showBoxes);
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
// ---------- build DOM ----------
|
| 539 |
+
function build(root){
|
| 540 |
+
const order=window.F3_PRESET_ORDER||Object.keys(window.F3_PRESETS||{});
|
| 541 |
+
root.innerHTML=''
|
| 542 |
+
+'<div class="tabs"></div>'
|
| 543 |
+
+'<div class="main">'
|
| 544 |
+
+ '<div class="canvas-wrap"><div class="board empty"><div class="hint">Drag to draw your first element box, or pick a scene above.</div><div class="spin">Rendering with FLUX.3…</div></div>'
|
| 545 |
+
+ '<div class="ratio-tag"></div>'
|
| 546 |
+
+ '<div class="board-tools">'
|
| 547 |
+
+ '<div class="btn toggleBtn on" style="display:none"><span class="toggle"></span>Bounding boxes</div>'
|
| 548 |
+
+ '<div class="btn replayBtn">↻ Replay</div>'
|
| 549 |
+
+ '</div></div>'
|
| 550 |
+
+ '<div class="side">'
|
| 551 |
+
+ '<div class="steps"></div>'
|
| 552 |
+
+ '<div class="summary"></div>'
|
| 553 |
+
+ '<div class="scene"><label>SCENE PROMPT</label><textarea rows="2" placeholder="Describe the whole scene in one line."></textarea></div>'
|
| 554 |
+
+ '<div class="gen"><button class="genbtn">Generate</button></div>'
|
| 555 |
+
+ '</div>'
|
| 556 |
+
+'</div>'
|
| 557 |
+
+'<div class="table-bar"><span class="lab">+ VIEW THE ELEMENT TABLE</span><span class="cnt">0 ROWS</span></div>'
|
| 558 |
+
+'<div class="etable"></div>';
|
| 559 |
+
|
| 560 |
+
// tabs
|
| 561 |
+
const tabsEl=root.querySelector('.tabs');
|
| 562 |
+
const tabs=order.map(name=>{ const t=document.createElement('div'); t.className='tab'; t.dataset.name=name;
|
| 563 |
+
t.textContent=name; t.addEventListener('click',()=>loadPreset(name)); tabsEl.appendChild(t); return t; });
|
| 564 |
+
// steps
|
| 565 |
+
const stepsWrap=root.querySelector('.steps'); const steps=STEP_HINTS.map((h,i)=>{
|
| 566 |
+
const st=document.createElement('div'); st.className='step';
|
| 567 |
+
st.innerHTML='<div class="row"><span class="n">0'+(i+1)+'</span><span class="t">'+h[0]+'</span></div>'
|
| 568 |
+
+'<div class="hint">'+h[1]+'</div>'+(i===0?'<div class="ratios"></div>':'');
|
| 569 |
+
st.addEventListener('click',()=>{ S.step=i+1; renderSteps(); }); stepsWrap.appendChild(st); return st; });
|
| 570 |
+
// ratio chips in step 1
|
| 571 |
+
const ratiosEl=steps[0].querySelector('.ratios');
|
| 572 |
+
const ratioChips=(window.F3_RATIOS||Object.keys(RATIOS)).map(r=>{ const c=document.createElement('div');
|
| 573 |
+
c.className='chip'; c.dataset.r=r; c.textContent=r;
|
| 574 |
+
c.addEventListener('click',ev=>{ ev.stopPropagation(); S.aspect=r; S.rendered=false;
|
| 575 |
+
const img=NS.el.board.querySelector('img'); if(img)img.remove();
|
| 576 |
+
NS.el.board.classList.remove('rendered','hideboxes'); NS.el.toggleBtn.style.display='none';
|
| 577 |
+
renderAll(); }); ratiosEl.appendChild(c); return c; });
|
| 578 |
+
|
| 579 |
+
NS.el={ root, wrap:root.querySelector('.canvas-wrap'), board:root.querySelector('.board'),
|
| 580 |
+
ratioTag:root.querySelector('.ratio-tag'), tabs, steps, ratios:ratiosEl, ratioChips,
|
| 581 |
+
summary:root.querySelector('.summary'), scene:root.querySelector('.scene textarea'),
|
| 582 |
+
genbtn:root.querySelector('.genbtn'), toggleBtn:root.querySelector('.toggleBtn'),
|
| 583 |
+
replayBtn:root.querySelector('.replayBtn'), cnt:root.querySelector('.table-bar .cnt'),
|
| 584 |
+
etable:root.querySelector('.etable') };
|
| 585 |
+
|
| 586 |
+
NS.el.board.addEventListener('mousedown',startDraw);
|
| 587 |
+
NS.el.scene.addEventListener('input',()=>{ S.scene=NS.el.scene.value; S.step=3; syncState(); });
|
| 588 |
+
NS.el.genbtn.addEventListener('click',doGenerate);
|
| 589 |
+
NS.el.replayBtn.addEventListener('click',doGenerate);
|
| 590 |
+
NS.el.toggleBtn.addEventListener('click',()=>{ S.showBoxes=!S.showBoxes; updateToggle(); });
|
| 591 |
+
root.querySelector('.table-bar').addEventListener('click',()=>NS.el.etable.classList.toggle('open'));
|
| 592 |
+
window.addEventListener('resize',()=>{ layoutBoard(); });
|
| 593 |
+
|
| 594 |
+
loadPreset(order[order.length-1]); // start on "Up from the subway" like the screenshots
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
function init(){
|
| 598 |
+
const root=document.getElementById('f3-root');
|
| 599 |
+
if(!root || !document.getElementById('f3-state') || !document.querySelector('#f3-gen button')){
|
| 600 |
+
return false;
|
| 601 |
+
}
|
| 602 |
+
if(root.dataset.ready) return true;
|
| 603 |
+
root.dataset.ready='1'; build(root); return true;
|
| 604 |
+
}
|
| 605 |
+
const iv=setInterval(()=>{ if(init()) clearInterval(iv); }, 120);
|
| 606 |
+
})();
|
| 607 |
+
</script>
|
| 608 |
+
"""
|
| 609 |
+
|
| 610 |
+
HEAD = STYLE + STYLE2 + CONFIG_JS + SCRIPT
|
| 611 |
+
|
| 612 |
+
with gr.Blocks(head=HEAD, title="FLUX.3 layout canvas",
|
| 613 |
+
css="footer{display:none!important} .gradio-container{max-width:1240px!important}") as demo:
|
| 614 |
+
state_box = gr.Textbox(visible=False, elem_id="f3-state")
|
| 615 |
+
result_box = gr.Textbox(visible=False, elem_id="f3-result")
|
| 616 |
+
gen_btn = gr.Button("gen", visible=False, elem_id="f3-gen")
|
| 617 |
+
gr.HTML('<div id="f3-root"></div>')
|
| 618 |
+
|
| 619 |
+
ev = gen_btn.click(generate, [state_box], [result_box], api_name="generate",
|
| 620 |
+
show_progress="hidden")
|
| 621 |
+
ev.then(None, [result_box], None, js="(r)=>{ if(window.f3_render) window.f3_render(r); }")
|
| 622 |
+
|
| 623 |
+
if __name__ == "__main__":
|
| 624 |
+
demo.queue(max_size=12).launch(show_error=True)
|
diffusers/__init__.py
ADDED
|
@@ -0,0 +1,1812 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__version__ = "0.41.0.dev0"
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
from .utils import (
|
| 6 |
+
DIFFUSERS_SLOW_IMPORT,
|
| 7 |
+
OptionalDependencyNotAvailable,
|
| 8 |
+
_LazyModule,
|
| 9 |
+
is_accelerate_available,
|
| 10 |
+
is_auto_round_available,
|
| 11 |
+
is_bitsandbytes_available,
|
| 12 |
+
is_gguf_available,
|
| 13 |
+
is_librosa_available,
|
| 14 |
+
is_note_seq_available,
|
| 15 |
+
is_nvidia_modelopt_available,
|
| 16 |
+
is_onnx_available,
|
| 17 |
+
is_opencv_available,
|
| 18 |
+
is_optimum_quanto_available,
|
| 19 |
+
is_scipy_available,
|
| 20 |
+
is_sdnq_available,
|
| 21 |
+
is_sentencepiece_available,
|
| 22 |
+
is_torch_available,
|
| 23 |
+
is_torchao_available,
|
| 24 |
+
is_torchsde_available,
|
| 25 |
+
is_transformers_available,
|
| 26 |
+
is_transformers_version,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Lazy Import based on
|
| 31 |
+
# https://github.com/huggingface/transformers/blob/main/src/transformers/__init__.py
|
| 32 |
+
|
| 33 |
+
# When adding a new object to this init, please add it to `_import_structure`. The `_import_structure` is a dictionary submodule to list of object names,
|
| 34 |
+
# and is used to defer the actual importing for when the objects are requested.
|
| 35 |
+
# This way `import diffusers` provides the names in the namespace without actually importing anything (and especially none of the backends).
|
| 36 |
+
|
| 37 |
+
_import_structure = {
|
| 38 |
+
"configuration_utils": ["ConfigMixin"],
|
| 39 |
+
"guiders": [],
|
| 40 |
+
"hooks": [],
|
| 41 |
+
"loaders": ["FromOriginalModelMixin"],
|
| 42 |
+
"models": [],
|
| 43 |
+
"modular_pipelines": [],
|
| 44 |
+
"pipelines": [],
|
| 45 |
+
"quantizers.pipe_quant_config": ["PipelineQuantizationConfig"],
|
| 46 |
+
"quantizers.quantization_config": [],
|
| 47 |
+
"schedulers": [],
|
| 48 |
+
"utils": [
|
| 49 |
+
"OptionalDependencyNotAvailable",
|
| 50 |
+
"is_inflect_available",
|
| 51 |
+
"is_invisible_watermark_available",
|
| 52 |
+
"is_librosa_available",
|
| 53 |
+
"is_note_seq_available",
|
| 54 |
+
"is_onnx_available",
|
| 55 |
+
"is_scipy_available",
|
| 56 |
+
"is_torch_available",
|
| 57 |
+
"is_torchsde_available",
|
| 58 |
+
"is_transformers_available",
|
| 59 |
+
"is_transformers_version",
|
| 60 |
+
"is_unidecode_available",
|
| 61 |
+
"logging",
|
| 62 |
+
],
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
if not is_torch_available() and not is_accelerate_available() and not is_bitsandbytes_available():
|
| 67 |
+
raise OptionalDependencyNotAvailable()
|
| 68 |
+
except OptionalDependencyNotAvailable:
|
| 69 |
+
from .utils import dummy_bitsandbytes_objects
|
| 70 |
+
|
| 71 |
+
_import_structure["utils.dummy_bitsandbytes_objects"] = [
|
| 72 |
+
name for name in dir(dummy_bitsandbytes_objects) if not name.startswith("_")
|
| 73 |
+
]
|
| 74 |
+
else:
|
| 75 |
+
_import_structure["quantizers.quantization_config"].append("BitsAndBytesConfig")
|
| 76 |
+
|
| 77 |
+
try:
|
| 78 |
+
if not is_torch_available() and not is_accelerate_available() and not is_gguf_available():
|
| 79 |
+
raise OptionalDependencyNotAvailable()
|
| 80 |
+
except OptionalDependencyNotAvailable:
|
| 81 |
+
from .utils import dummy_gguf_objects
|
| 82 |
+
|
| 83 |
+
_import_structure["utils.dummy_gguf_objects"] = [
|
| 84 |
+
name for name in dir(dummy_gguf_objects) if not name.startswith("_")
|
| 85 |
+
]
|
| 86 |
+
else:
|
| 87 |
+
_import_structure["quantizers.quantization_config"].append("GGUFQuantizationConfig")
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
if not is_torch_available() and not is_accelerate_available() and not is_torchao_available():
|
| 91 |
+
raise OptionalDependencyNotAvailable()
|
| 92 |
+
except OptionalDependencyNotAvailable:
|
| 93 |
+
from .utils import dummy_torchao_objects
|
| 94 |
+
|
| 95 |
+
_import_structure["utils.dummy_torchao_objects"] = [
|
| 96 |
+
name for name in dir(dummy_torchao_objects) if not name.startswith("_")
|
| 97 |
+
]
|
| 98 |
+
else:
|
| 99 |
+
_import_structure["quantizers.quantization_config"].append("TorchAoConfig")
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
if not is_torch_available() and not is_accelerate_available() and not is_optimum_quanto_available():
|
| 103 |
+
raise OptionalDependencyNotAvailable()
|
| 104 |
+
except OptionalDependencyNotAvailable:
|
| 105 |
+
from .utils import dummy_optimum_quanto_objects
|
| 106 |
+
|
| 107 |
+
_import_structure["utils.dummy_optimum_quanto_objects"] = [
|
| 108 |
+
name for name in dir(dummy_optimum_quanto_objects) if not name.startswith("_")
|
| 109 |
+
]
|
| 110 |
+
else:
|
| 111 |
+
_import_structure["quantizers.quantization_config"].append("QuantoConfig")
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
if not is_torch_available() and not is_accelerate_available() and not is_nvidia_modelopt_available():
|
| 115 |
+
raise OptionalDependencyNotAvailable()
|
| 116 |
+
except OptionalDependencyNotAvailable:
|
| 117 |
+
from .utils import dummy_nvidia_modelopt_objects
|
| 118 |
+
|
| 119 |
+
_import_structure["utils.dummy_nvidia_modelopt_objects"] = [
|
| 120 |
+
name for name in dir(dummy_nvidia_modelopt_objects) if not name.startswith("_")
|
| 121 |
+
]
|
| 122 |
+
else:
|
| 123 |
+
_import_structure["quantizers.quantization_config"].append("NVIDIAModelOptConfig")
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
if not is_torch_available():
|
| 127 |
+
raise OptionalDependencyNotAvailable()
|
| 128 |
+
except OptionalDependencyNotAvailable:
|
| 129 |
+
from .utils import dummy_nunchaku_lite_objects
|
| 130 |
+
|
| 131 |
+
_import_structure["utils.dummy_nunchaku_lite_objects"] = [
|
| 132 |
+
name for name in dir(dummy_nunchaku_lite_objects) if not name.startswith("_")
|
| 133 |
+
]
|
| 134 |
+
else:
|
| 135 |
+
_import_structure["quantizers.quantization_config"].append("NunchakuLiteQuantizationConfig")
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
if not is_auto_round_available():
|
| 139 |
+
raise OptionalDependencyNotAvailable()
|
| 140 |
+
except OptionalDependencyNotAvailable:
|
| 141 |
+
from .utils import dummy_auto_round_objects
|
| 142 |
+
|
| 143 |
+
_import_structure["utils.dummy_auto_round_objects"] = [
|
| 144 |
+
name for name in dir(dummy_auto_round_objects) if not name.startswith("_")
|
| 145 |
+
]
|
| 146 |
+
else:
|
| 147 |
+
_import_structure["quantizers.quantization_config"].append("AutoRoundConfig")
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
if not is_torch_available() and not is_accelerate_available() and not is_sdnq_available():
|
| 151 |
+
raise OptionalDependencyNotAvailable()
|
| 152 |
+
except OptionalDependencyNotAvailable:
|
| 153 |
+
from .utils import dummy_sdnq_objects
|
| 154 |
+
|
| 155 |
+
_import_structure["utils.dummy_sdnq_objects"] = [
|
| 156 |
+
name for name in dir(dummy_sdnq_objects) if not name.startswith("_")
|
| 157 |
+
]
|
| 158 |
+
else:
|
| 159 |
+
_import_structure["quantizers.quantization_config"].append("SDNQConfig")
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
if not is_onnx_available():
|
| 163 |
+
raise OptionalDependencyNotAvailable()
|
| 164 |
+
except OptionalDependencyNotAvailable:
|
| 165 |
+
from .utils import dummy_onnx_objects # noqa F403
|
| 166 |
+
|
| 167 |
+
_import_structure["utils.dummy_onnx_objects"] = [
|
| 168 |
+
name for name in dir(dummy_onnx_objects) if not name.startswith("_")
|
| 169 |
+
]
|
| 170 |
+
|
| 171 |
+
else:
|
| 172 |
+
_import_structure["pipelines"].extend(["OnnxRuntimeModel"])
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
if not is_torch_available():
|
| 176 |
+
raise OptionalDependencyNotAvailable()
|
| 177 |
+
except OptionalDependencyNotAvailable:
|
| 178 |
+
from .utils import dummy_pt_objects # noqa F403
|
| 179 |
+
|
| 180 |
+
_import_structure["utils.dummy_pt_objects"] = [name for name in dir(dummy_pt_objects) if not name.startswith("_")]
|
| 181 |
+
|
| 182 |
+
else:
|
| 183 |
+
_import_structure["guiders"].extend(
|
| 184 |
+
[
|
| 185 |
+
"AdaptiveProjectedGuidance",
|
| 186 |
+
"AdaptiveProjectedMixGuidance",
|
| 187 |
+
"AutoGuidance",
|
| 188 |
+
"BaseGuidance",
|
| 189 |
+
"ClassifierFreeGuidance",
|
| 190 |
+
"ClassifierFreeZeroStarGuidance",
|
| 191 |
+
"FrequencyDecoupledGuidance",
|
| 192 |
+
"LTX2Guidance",
|
| 193 |
+
"PerturbedAttentionGuidance",
|
| 194 |
+
"SkipLayerGuidance",
|
| 195 |
+
"SmoothedEnergyGuidance",
|
| 196 |
+
"TangentialClassifierFreeGuidance",
|
| 197 |
+
]
|
| 198 |
+
)
|
| 199 |
+
_import_structure["hooks"].extend(
|
| 200 |
+
[
|
| 201 |
+
"FasterCacheConfig",
|
| 202 |
+
"FirstBlockCacheConfig",
|
| 203 |
+
"HookRegistry",
|
| 204 |
+
"LayerSkipConfig",
|
| 205 |
+
"MagCacheConfig",
|
| 206 |
+
"PyramidAttentionBroadcastConfig",
|
| 207 |
+
"SmoothedEnergyGuidanceConfig",
|
| 208 |
+
"TaylorSeerCacheConfig",
|
| 209 |
+
"TextKVCacheConfig",
|
| 210 |
+
"apply_faster_cache",
|
| 211 |
+
"apply_first_block_cache",
|
| 212 |
+
"apply_layer_skip",
|
| 213 |
+
"apply_mag_cache",
|
| 214 |
+
"apply_pyramid_attention_broadcast",
|
| 215 |
+
"apply_taylorseer_cache",
|
| 216 |
+
"apply_text_kv_cache",
|
| 217 |
+
]
|
| 218 |
+
)
|
| 219 |
+
_import_structure["image_processor"] = [
|
| 220 |
+
"InpaintProcessor",
|
| 221 |
+
"IPAdapterMaskProcessor",
|
| 222 |
+
"PixArtImageProcessor",
|
| 223 |
+
"VaeImageProcessor",
|
| 224 |
+
"VaeImageProcessorLDM3D",
|
| 225 |
+
]
|
| 226 |
+
_import_structure["models"].extend(
|
| 227 |
+
[
|
| 228 |
+
"AceStepTransformer1DModel",
|
| 229 |
+
"AllegroTransformer3DModel",
|
| 230 |
+
"AnimaTextConditioner",
|
| 231 |
+
"AnyFlowFARTransformer3DModel",
|
| 232 |
+
"AnyFlowTransformer3DModel",
|
| 233 |
+
"AsymmetricAutoencoderKL",
|
| 234 |
+
"AttentionBackendName",
|
| 235 |
+
"AuraFlowTransformer2DModel",
|
| 236 |
+
"AutoencoderDC",
|
| 237 |
+
"AutoencoderKL",
|
| 238 |
+
"AutoencoderKLAllegro",
|
| 239 |
+
"AutoencoderKLCogVideoX",
|
| 240 |
+
"AutoencoderKLCosmos",
|
| 241 |
+
"AutoencoderKLFlux2",
|
| 242 |
+
"AutoencoderKLHunyuanImage",
|
| 243 |
+
"AutoencoderKLHunyuanImageRefiner",
|
| 244 |
+
"AutoencoderKLHunyuanVideo",
|
| 245 |
+
"AutoencoderKLHunyuanVideo15",
|
| 246 |
+
"AutoencoderKLKVAE",
|
| 247 |
+
"AutoencoderKLKVAEVideo",
|
| 248 |
+
"AutoencoderKLLTX2Audio",
|
| 249 |
+
"AutoencoderKLLTX2Video",
|
| 250 |
+
"AutoencoderKLLTXVideo",
|
| 251 |
+
"AutoencoderKLMagvit",
|
| 252 |
+
"AutoencoderKLMiniMaxH3",
|
| 253 |
+
"AutoencoderKLMiniMaxH3Audio",
|
| 254 |
+
"AutoencoderKLMochi",
|
| 255 |
+
"AutoencoderKLQwenImage",
|
| 256 |
+
"AutoencoderKLTemporalDecoder",
|
| 257 |
+
"AutoencoderKLWan",
|
| 258 |
+
"AutoencoderOobleck",
|
| 259 |
+
"AutoencoderRAE",
|
| 260 |
+
"AutoencoderSAME",
|
| 261 |
+
"AutoencoderTiny",
|
| 262 |
+
"AutoencoderVidTok",
|
| 263 |
+
"AutoModel",
|
| 264 |
+
"BriaFiboTransformer2DModel",
|
| 265 |
+
"BriaTransformer2DModel",
|
| 266 |
+
"CacheMixin",
|
| 267 |
+
"ChromaTransformer2DModel",
|
| 268 |
+
"ChronoEditTransformer3DModel",
|
| 269 |
+
"CogVideoXTransformer3DModel",
|
| 270 |
+
"CogView3PlusTransformer2DModel",
|
| 271 |
+
"CogView4Transformer2DModel",
|
| 272 |
+
"ConsisIDTransformer3DModel",
|
| 273 |
+
"ConsistencyDecoderVAE",
|
| 274 |
+
"ContextParallelConfig",
|
| 275 |
+
"ControlNetModel",
|
| 276 |
+
"ControlNetUnionModel",
|
| 277 |
+
"ControlNetXSAdapter",
|
| 278 |
+
"Cosmos3AVAEAudioTokenizer",
|
| 279 |
+
"Cosmos3OmniTransformer",
|
| 280 |
+
"CosmosControlNetModel",
|
| 281 |
+
"CosmosTransformer3DModel",
|
| 282 |
+
"DiTTransformer2DModel",
|
| 283 |
+
"DreamLiteTransformer2DModel",
|
| 284 |
+
"DreamLiteUNetModel",
|
| 285 |
+
"EasyAnimateTransformer3DModel",
|
| 286 |
+
"ErnieImageTransformer2DModel",
|
| 287 |
+
"Flux2Transformer2DModel",
|
| 288 |
+
"Flux3Transformer2DModel",
|
| 289 |
+
"FluxControlNetModel",
|
| 290 |
+
"FluxMultiControlNetModel",
|
| 291 |
+
"FluxTransformer2DModel",
|
| 292 |
+
"GlmImageTransformer2DModel",
|
| 293 |
+
"HeliosTransformer3DModel",
|
| 294 |
+
"HiDreamImageTransformer2DModel",
|
| 295 |
+
"HunyuanDiT2DControlNetModel",
|
| 296 |
+
"HunyuanDiT2DModel",
|
| 297 |
+
"HunyuanDiT2DMultiControlNetModel",
|
| 298 |
+
"HunyuanImageTransformer2DModel",
|
| 299 |
+
"HunyuanVideo15Transformer3DModel",
|
| 300 |
+
"HunyuanVideoFramepackTransformer3DModel",
|
| 301 |
+
"HunyuanVideoTransformer3DModel",
|
| 302 |
+
"I2VGenXLUNet",
|
| 303 |
+
"Ideogram4Transformer2DModel",
|
| 304 |
+
"JoyImageEditPlusTransformer3DModel",
|
| 305 |
+
"JoyImageEditTransformer3DModel",
|
| 306 |
+
"Kandinsky3UNet",
|
| 307 |
+
"Kandinsky5Transformer3DModel",
|
| 308 |
+
"Krea2Transformer2DModel",
|
| 309 |
+
"LatteTransformer3DModel",
|
| 310 |
+
"LongCatAudioDiTTransformer",
|
| 311 |
+
"LongCatAudioDiTVae",
|
| 312 |
+
"LongCatImageTransformer2DModel",
|
| 313 |
+
"LTX2VideoDiffusionDecoderModel",
|
| 314 |
+
"LTX2VideoTransformer3DModel",
|
| 315 |
+
"LTXVideoTransformer3DModel",
|
| 316 |
+
"Lumina2Transformer2DModel",
|
| 317 |
+
"LuminaNextDiT2DModel",
|
| 318 |
+
"MiniMaxH3Transformer3DModel",
|
| 319 |
+
"MiniMaxMusic3ConditionEncoder",
|
| 320 |
+
"MiniMaxMusic3RVQDepthDecoder",
|
| 321 |
+
"MiniMaxMusic3Transformer1DModel",
|
| 322 |
+
"MiniMaxMusic3Vocoder",
|
| 323 |
+
"MochiTransformer3DModel",
|
| 324 |
+
"ModelMixin",
|
| 325 |
+
"MotifVideoTransformer3DModel",
|
| 326 |
+
"MotionAdapter",
|
| 327 |
+
"MultiAdapter",
|
| 328 |
+
"MultiControlNetModel",
|
| 329 |
+
"NucleusMoEImageTransformer2DModel",
|
| 330 |
+
"OmniGenTransformer2DModel",
|
| 331 |
+
"OvisImageTransformer2DModel",
|
| 332 |
+
"ParallelConfig",
|
| 333 |
+
"PixArtTransformer2DModel",
|
| 334 |
+
"PriorTransformer",
|
| 335 |
+
"PRXTransformer2DModel",
|
| 336 |
+
"QwenImageControlNetModel",
|
| 337 |
+
"QwenImageMultiControlNetModel",
|
| 338 |
+
"QwenImageTransformer2DModel",
|
| 339 |
+
"SanaControlNetModel",
|
| 340 |
+
"SanaTransformer2DModel",
|
| 341 |
+
"SanaVideoTransformer3DModel",
|
| 342 |
+
"SD3ControlNetModel",
|
| 343 |
+
"SD3MultiControlNetModel",
|
| 344 |
+
"SD3Transformer2DModel",
|
| 345 |
+
"SkyReelsV2Transformer3DModel",
|
| 346 |
+
"SparseControlNetModel",
|
| 347 |
+
"StableAudio3DiTModel",
|
| 348 |
+
"StableAudioDiTModel",
|
| 349 |
+
"StableCascadeUNet",
|
| 350 |
+
"T2IAdapter",
|
| 351 |
+
"T5FilmDecoder",
|
| 352 |
+
"TensorParallelConfig",
|
| 353 |
+
"Transformer2DModel",
|
| 354 |
+
"TransformerTemporalModel",
|
| 355 |
+
"UNet1DModel",
|
| 356 |
+
"UNet2DConditionModel",
|
| 357 |
+
"UNet2DModel",
|
| 358 |
+
"UNet3DConditionModel",
|
| 359 |
+
"UNetControlNetXSModel",
|
| 360 |
+
"UNetMotionModel",
|
| 361 |
+
"UNetSpatioTemporalConditionModel",
|
| 362 |
+
"UVit2DModel",
|
| 363 |
+
"VQModel",
|
| 364 |
+
"WanAnimate2Transformer3DModel",
|
| 365 |
+
"WanAnimateTransformer3DModel",
|
| 366 |
+
"WanTransformer3DModel",
|
| 367 |
+
"WanVACETransformer3DModel",
|
| 368 |
+
"ZImageControlNetModel",
|
| 369 |
+
"ZImageTransformer2DModel",
|
| 370 |
+
"attention_backend",
|
| 371 |
+
]
|
| 372 |
+
)
|
| 373 |
+
_import_structure["modular_pipelines"].extend(
|
| 374 |
+
[
|
| 375 |
+
"AutoPipelineBlocks",
|
| 376 |
+
"ComponentsManager",
|
| 377 |
+
"ComponentSpec",
|
| 378 |
+
"ConditionalPipelineBlocks",
|
| 379 |
+
"ConfigSpec",
|
| 380 |
+
"InputParam",
|
| 381 |
+
"LoopSequentialPipelineBlocks",
|
| 382 |
+
"ModularPipeline",
|
| 383 |
+
"ModularPipelineBlocks",
|
| 384 |
+
"OutputParam",
|
| 385 |
+
"SequentialPipelineBlocks",
|
| 386 |
+
]
|
| 387 |
+
)
|
| 388 |
+
_import_structure["optimization"] = [
|
| 389 |
+
"get_constant_schedule",
|
| 390 |
+
"get_constant_schedule_with_warmup",
|
| 391 |
+
"get_cosine_schedule_with_warmup",
|
| 392 |
+
"get_cosine_with_hard_restarts_schedule_with_warmup",
|
| 393 |
+
"get_linear_schedule_with_warmup",
|
| 394 |
+
"get_polynomial_decay_schedule_with_warmup",
|
| 395 |
+
"get_scheduler",
|
| 396 |
+
]
|
| 397 |
+
_import_structure["pipelines"].extend(
|
| 398 |
+
[
|
| 399 |
+
"AudioPipelineOutput",
|
| 400 |
+
"AutoPipelineForImage2Image",
|
| 401 |
+
"AutoPipelineForInpainting",
|
| 402 |
+
"AutoPipelineForText2Audio",
|
| 403 |
+
"AutoPipelineForText2Image",
|
| 404 |
+
"ConsistencyModelPipeline",
|
| 405 |
+
"DanceDiffusionPipeline",
|
| 406 |
+
"DDIMPipeline",
|
| 407 |
+
"DDPMPipeline",
|
| 408 |
+
"DiffusionPipeline",
|
| 409 |
+
"DiTPipeline",
|
| 410 |
+
"ImagePipelineOutput",
|
| 411 |
+
"KarrasVePipeline",
|
| 412 |
+
"LDMPipeline",
|
| 413 |
+
"LDMSuperResolutionPipeline",
|
| 414 |
+
"PNDMPipeline",
|
| 415 |
+
"RePaintPipeline",
|
| 416 |
+
"ScoreSdeVePipeline",
|
| 417 |
+
"StableDiffusionMixin",
|
| 418 |
+
]
|
| 419 |
+
)
|
| 420 |
+
_import_structure["quantizers"] = ["DiffusersQuantizer"]
|
| 421 |
+
_import_structure["schedulers"].extend(
|
| 422 |
+
[
|
| 423 |
+
"AmusedScheduler",
|
| 424 |
+
"BlockRefinementScheduler",
|
| 425 |
+
"BlockRefinementSchedulerOutput",
|
| 426 |
+
"CMStochasticIterativeScheduler",
|
| 427 |
+
"CogVideoXDDIMScheduler",
|
| 428 |
+
"CogVideoXDPMScheduler",
|
| 429 |
+
"DDIMInverseScheduler",
|
| 430 |
+
"DDIMParallelScheduler",
|
| 431 |
+
"DDIMScheduler",
|
| 432 |
+
"DDPMParallelScheduler",
|
| 433 |
+
"DDPMScheduler",
|
| 434 |
+
"DDPMWuerstchenScheduler",
|
| 435 |
+
"DEISMultistepScheduler",
|
| 436 |
+
"DiscreteDDIMScheduler",
|
| 437 |
+
"DiscreteDDIMSchedulerOutput",
|
| 438 |
+
"DPMSolverMultistepInverseScheduler",
|
| 439 |
+
"DPMSolverMultistepScheduler",
|
| 440 |
+
"DPMSolverSinglestepScheduler",
|
| 441 |
+
"EDMDPMSolverMultistepScheduler",
|
| 442 |
+
"EDMEulerScheduler",
|
| 443 |
+
"EntropyBoundScheduler",
|
| 444 |
+
"EntropyBoundSchedulerOutput",
|
| 445 |
+
"EulerAncestralDiscreteScheduler",
|
| 446 |
+
"EulerDiscreteScheduler",
|
| 447 |
+
"FlowMapEulerDiscreteScheduler",
|
| 448 |
+
"FlowMatchEulerDiscreteScheduler",
|
| 449 |
+
"FlowMatchHeunDiscreteScheduler",
|
| 450 |
+
"FlowMatchLCMScheduler",
|
| 451 |
+
"HeliosDMDScheduler",
|
| 452 |
+
"HeliosScheduler",
|
| 453 |
+
"HeunDiscreteScheduler",
|
| 454 |
+
"IPNDMScheduler",
|
| 455 |
+
"KarrasVeScheduler",
|
| 456 |
+
"KDPM2AncestralDiscreteScheduler",
|
| 457 |
+
"KDPM2DiscreteScheduler",
|
| 458 |
+
"LCMScheduler",
|
| 459 |
+
"LTXEulerAncestralRFScheduler",
|
| 460 |
+
"MiniMaxH3Scheduler",
|
| 461 |
+
"PNDMScheduler",
|
| 462 |
+
"RePaintScheduler",
|
| 463 |
+
"SASolverScheduler",
|
| 464 |
+
"SchedulerMixin",
|
| 465 |
+
"SCMScheduler",
|
| 466 |
+
"ScoreSdeVeScheduler",
|
| 467 |
+
"TCDScheduler",
|
| 468 |
+
"UnCLIPScheduler",
|
| 469 |
+
"UniPCMultistepScheduler",
|
| 470 |
+
"VQDiffusionScheduler",
|
| 471 |
+
]
|
| 472 |
+
)
|
| 473 |
+
_import_structure["training_utils"] = ["EMAModel"]
|
| 474 |
+
_import_structure["video_processor"] = ["VideoProcessor"]
|
| 475 |
+
|
| 476 |
+
try:
|
| 477 |
+
if not (is_torch_available() and is_scipy_available()):
|
| 478 |
+
raise OptionalDependencyNotAvailable()
|
| 479 |
+
except OptionalDependencyNotAvailable:
|
| 480 |
+
from .utils import dummy_torch_and_scipy_objects # noqa F403
|
| 481 |
+
|
| 482 |
+
_import_structure["utils.dummy_torch_and_scipy_objects"] = [
|
| 483 |
+
name for name in dir(dummy_torch_and_scipy_objects) if not name.startswith("_")
|
| 484 |
+
]
|
| 485 |
+
|
| 486 |
+
else:
|
| 487 |
+
_import_structure["schedulers"].extend(["LMSDiscreteScheduler"])
|
| 488 |
+
|
| 489 |
+
try:
|
| 490 |
+
if not (is_torch_available() and is_torchsde_available()):
|
| 491 |
+
raise OptionalDependencyNotAvailable()
|
| 492 |
+
except OptionalDependencyNotAvailable:
|
| 493 |
+
from .utils import dummy_torch_and_torchsde_objects # noqa F403
|
| 494 |
+
|
| 495 |
+
_import_structure["utils.dummy_torch_and_torchsde_objects"] = [
|
| 496 |
+
name for name in dir(dummy_torch_and_torchsde_objects) if not name.startswith("_")
|
| 497 |
+
]
|
| 498 |
+
|
| 499 |
+
else:
|
| 500 |
+
_import_structure["schedulers"].extend(["CosineDPMSolverMultistepScheduler", "DPMSolverSDEScheduler"])
|
| 501 |
+
|
| 502 |
+
try:
|
| 503 |
+
if not (is_torch_available() and is_transformers_available()):
|
| 504 |
+
raise OptionalDependencyNotAvailable()
|
| 505 |
+
except OptionalDependencyNotAvailable:
|
| 506 |
+
from .utils import dummy_torch_and_transformers_objects # noqa F403
|
| 507 |
+
|
| 508 |
+
_import_structure["utils.dummy_torch_and_transformers_objects"] = [
|
| 509 |
+
name for name in dir(dummy_torch_and_transformers_objects) if not name.startswith("_")
|
| 510 |
+
]
|
| 511 |
+
|
| 512 |
+
else:
|
| 513 |
+
_import_structure["modular_pipelines"].extend(
|
| 514 |
+
[
|
| 515 |
+
"AnimaAutoBlocks",
|
| 516 |
+
"AnimaModularPipeline",
|
| 517 |
+
"Cosmos3DistilledBlocks",
|
| 518 |
+
"Cosmos3DistilledModularPipeline",
|
| 519 |
+
"Cosmos3OmniBlocks",
|
| 520 |
+
"Cosmos3OmniModularPipeline",
|
| 521 |
+
"ErnieImageAutoBlocks",
|
| 522 |
+
"ErnieImageModularPipeline",
|
| 523 |
+
"Flux2AutoBlocks",
|
| 524 |
+
"Flux2KleinAutoBlocks",
|
| 525 |
+
"Flux2KleinBaseAutoBlocks",
|
| 526 |
+
"Flux2KleinBaseModularPipeline",
|
| 527 |
+
"Flux2KleinModularPipeline",
|
| 528 |
+
"Flux2ModularPipeline",
|
| 529 |
+
"Flux3AutoBlocks",
|
| 530 |
+
"Flux3ModularPipeline",
|
| 531 |
+
"FluxAutoBlocks",
|
| 532 |
+
"FluxKontextAutoBlocks",
|
| 533 |
+
"FluxKontextModularPipeline",
|
| 534 |
+
"FluxModularPipeline",
|
| 535 |
+
"HeliosAutoBlocks",
|
| 536 |
+
"HeliosModularPipeline",
|
| 537 |
+
"HeliosPyramidAutoBlocks",
|
| 538 |
+
"HeliosPyramidDistilledAutoBlocks",
|
| 539 |
+
"HeliosPyramidDistilledModularPipeline",
|
| 540 |
+
"HeliosPyramidModularPipeline",
|
| 541 |
+
"HunyuanVideo15AutoBlocks",
|
| 542 |
+
"HunyuanVideo15ModularPipeline",
|
| 543 |
+
"Ideogram4AutoBlocks",
|
| 544 |
+
"Ideogram4ModularPipeline",
|
| 545 |
+
"Krea2AutoBlocks",
|
| 546 |
+
"Krea2ModularPipeline",
|
| 547 |
+
"Krea2TurboAutoBlocks",
|
| 548 |
+
"Krea2TurboModularPipeline",
|
| 549 |
+
"LTX25AutoBlocks",
|
| 550 |
+
"LTX25ModularPipeline",
|
| 551 |
+
"LTX2AutoBlocks",
|
| 552 |
+
"LTX2ModularPipeline",
|
| 553 |
+
"LTXAutoBlocks",
|
| 554 |
+
"LTXModularPipeline",
|
| 555 |
+
"MiniMaxH3Blocks",
|
| 556 |
+
"MiniMaxH3ModularPipeline",
|
| 557 |
+
"MiniMaxMusic3Blocks",
|
| 558 |
+
"MiniMaxMusic3ModularPipeline",
|
| 559 |
+
"QwenImageAutoBlocks",
|
| 560 |
+
"QwenImageEditAutoBlocks",
|
| 561 |
+
"QwenImageEditModularPipeline",
|
| 562 |
+
"QwenImageEditPlusAutoBlocks",
|
| 563 |
+
"QwenImageEditPlusModularPipeline",
|
| 564 |
+
"QwenImageLayeredAutoBlocks",
|
| 565 |
+
"QwenImageLayeredModularPipeline",
|
| 566 |
+
"QwenImageModularPipeline",
|
| 567 |
+
"StableDiffusion3AutoBlocks",
|
| 568 |
+
"StableDiffusion3ModularPipeline",
|
| 569 |
+
"StableDiffusionXLAutoBlocks",
|
| 570 |
+
"StableDiffusionXLModularPipeline",
|
| 571 |
+
"Wan22Blocks",
|
| 572 |
+
"Wan22Image2VideoBlocks",
|
| 573 |
+
"Wan22Image2VideoModularPipeline",
|
| 574 |
+
"Wan22ModularPipeline",
|
| 575 |
+
"Wan22VaceBlocks",
|
| 576 |
+
"Wan22VaceModularPipeline",
|
| 577 |
+
"WanAnimate2Blocks",
|
| 578 |
+
"WanAnimate2DistilledBlocks",
|
| 579 |
+
"WanAnimate2DistilledModularPipeline",
|
| 580 |
+
"WanAnimate2ModularPipeline",
|
| 581 |
+
"WanBlocks",
|
| 582 |
+
"WanImage2VideoAutoBlocks",
|
| 583 |
+
"WanImage2VideoModularPipeline",
|
| 584 |
+
"WanModularPipeline",
|
| 585 |
+
"ZImageAutoBlocks",
|
| 586 |
+
"ZImageModularPipeline",
|
| 587 |
+
]
|
| 588 |
+
)
|
| 589 |
+
_import_structure["pipelines"].extend(
|
| 590 |
+
[
|
| 591 |
+
"AceStepAudioTokenDetokenizer",
|
| 592 |
+
"AceStepAudioTokenizer",
|
| 593 |
+
"AceStepConditionEncoder",
|
| 594 |
+
"AceStepPipeline",
|
| 595 |
+
"AllegroPipeline",
|
| 596 |
+
"AltDiffusionImg2ImgPipeline",
|
| 597 |
+
"AltDiffusionPipeline",
|
| 598 |
+
"AmusedImg2ImgPipeline",
|
| 599 |
+
"AmusedInpaintPipeline",
|
| 600 |
+
"AmusedPipeline",
|
| 601 |
+
"AnimateDiffControlNetPipeline",
|
| 602 |
+
"AnimateDiffPAGPipeline",
|
| 603 |
+
"AnimateDiffPipeline",
|
| 604 |
+
"AnimateDiffSDXLPipeline",
|
| 605 |
+
"AnimateDiffSparseControlNetPipeline",
|
| 606 |
+
"AnimateDiffVideoToVideoControlNetPipeline",
|
| 607 |
+
"AnimateDiffVideoToVideoPipeline",
|
| 608 |
+
"AnyFlowFARPipeline",
|
| 609 |
+
"AnyFlowPipeline",
|
| 610 |
+
"AudioLDM2Pipeline",
|
| 611 |
+
"AudioLDM2ProjectionModel",
|
| 612 |
+
"AudioLDM2UNet2DConditionModel",
|
| 613 |
+
"AudioLDMPipeline",
|
| 614 |
+
"AuraFlowPipeline",
|
| 615 |
+
"BlipDiffusionControlNetPipeline",
|
| 616 |
+
"BlipDiffusionPipeline",
|
| 617 |
+
"BriaFiboEditPipeline",
|
| 618 |
+
"BriaFiboPipeline",
|
| 619 |
+
"BriaPipeline",
|
| 620 |
+
"ChromaImg2ImgPipeline",
|
| 621 |
+
"ChromaInpaintPipeline",
|
| 622 |
+
"ChromaPipeline",
|
| 623 |
+
"ChronoEditPipeline",
|
| 624 |
+
"CLIPImageProjection",
|
| 625 |
+
"CogVideoXFunControlPipeline",
|
| 626 |
+
"CogVideoXImageToVideoPipeline",
|
| 627 |
+
"CogVideoXPipeline",
|
| 628 |
+
"CogVideoXVideoToVideoPipeline",
|
| 629 |
+
"CogView3PlusPipeline",
|
| 630 |
+
"CogView4ControlPipeline",
|
| 631 |
+
"CogView4Pipeline",
|
| 632 |
+
"ConsisIDPipeline",
|
| 633 |
+
"Cosmos2_5_PredictBasePipeline",
|
| 634 |
+
"Cosmos2_5_TransferPipeline",
|
| 635 |
+
"Cosmos2TextToImagePipeline",
|
| 636 |
+
"Cosmos2VideoToWorldPipeline",
|
| 637 |
+
"Cosmos3OmniPipeline",
|
| 638 |
+
"CosmosActionCondition",
|
| 639 |
+
"CosmosTextToWorldPipeline",
|
| 640 |
+
"CosmosVideoToWorldPipeline",
|
| 641 |
+
"CycleDiffusionPipeline",
|
| 642 |
+
"DiffusionGemmaPipeline",
|
| 643 |
+
"DiffusionGemmaPipelineOutput",
|
| 644 |
+
"DreamLiteMobilePipeline",
|
| 645 |
+
"DreamLitePipeline",
|
| 646 |
+
"DreamLitePipelineOutput",
|
| 647 |
+
"EasyAnimateControlPipeline",
|
| 648 |
+
"EasyAnimateInpaintPipeline",
|
| 649 |
+
"EasyAnimatePipeline",
|
| 650 |
+
"ErnieImagePipeline",
|
| 651 |
+
"Flux2KleinInpaintPipeline",
|
| 652 |
+
"Flux2KleinKVPipeline",
|
| 653 |
+
"Flux2KleinPipeline",
|
| 654 |
+
"Flux2Pipeline",
|
| 655 |
+
"FluxControlImg2ImgPipeline",
|
| 656 |
+
"FluxControlInpaintPipeline",
|
| 657 |
+
"FluxControlNetImg2ImgPipeline",
|
| 658 |
+
"FluxControlNetInpaintPipeline",
|
| 659 |
+
"FluxControlNetPipeline",
|
| 660 |
+
"FluxControlPipeline",
|
| 661 |
+
"FluxFillPipeline",
|
| 662 |
+
"FluxImg2ImgPipeline",
|
| 663 |
+
"FluxInpaintPipeline",
|
| 664 |
+
"FluxKontextInpaintPipeline",
|
| 665 |
+
"FluxKontextPipeline",
|
| 666 |
+
"FluxPipeline",
|
| 667 |
+
"FluxPriorReduxPipeline",
|
| 668 |
+
"GlmImagePipeline",
|
| 669 |
+
"HeliosPipeline",
|
| 670 |
+
"HeliosPyramidPipeline",
|
| 671 |
+
"HiDreamImagePipeline",
|
| 672 |
+
"HunyuanDiTControlNetPipeline",
|
| 673 |
+
"HunyuanDiTPAGPipeline",
|
| 674 |
+
"HunyuanDiTPipeline",
|
| 675 |
+
"HunyuanImagePipeline",
|
| 676 |
+
"HunyuanImageRefinerPipeline",
|
| 677 |
+
"HunyuanSkyreelsImageToVideoPipeline",
|
| 678 |
+
"HunyuanVideo15ImageToVideoPipeline",
|
| 679 |
+
"HunyuanVideo15Pipeline",
|
| 680 |
+
"HunyuanVideoFramepackPipeline",
|
| 681 |
+
"HunyuanVideoImageToVideoPipeline",
|
| 682 |
+
"HunyuanVideoPipeline",
|
| 683 |
+
"I2VGenXLPipeline",
|
| 684 |
+
"Ideogram4Pipeline",
|
| 685 |
+
"Ideogram4PromptEnhancerHead",
|
| 686 |
+
"IFImg2ImgPipeline",
|
| 687 |
+
"IFImg2ImgSuperResolutionPipeline",
|
| 688 |
+
"IFInpaintingPipeline",
|
| 689 |
+
"IFInpaintingSuperResolutionPipeline",
|
| 690 |
+
"IFPipeline",
|
| 691 |
+
"IFSuperResolutionPipeline",
|
| 692 |
+
"ImageTextPipelineOutput",
|
| 693 |
+
"JoyImageEditPipeline",
|
| 694 |
+
"JoyImageEditPipelineOutput",
|
| 695 |
+
"JoyImageEditPlusPipeline",
|
| 696 |
+
"JoyImageEditPlusPipelineOutput",
|
| 697 |
+
"Kandinsky3Img2ImgPipeline",
|
| 698 |
+
"Kandinsky3Pipeline",
|
| 699 |
+
"Kandinsky5I2IPipeline",
|
| 700 |
+
"Kandinsky5I2VPipeline",
|
| 701 |
+
"Kandinsky5T2IPipeline",
|
| 702 |
+
"Kandinsky5T2VPipeline",
|
| 703 |
+
"KandinskyCombinedPipeline",
|
| 704 |
+
"KandinskyImg2ImgCombinedPipeline",
|
| 705 |
+
"KandinskyImg2ImgPipeline",
|
| 706 |
+
"KandinskyInpaintCombinedPipeline",
|
| 707 |
+
"KandinskyInpaintPipeline",
|
| 708 |
+
"KandinskyPipeline",
|
| 709 |
+
"KandinskyPriorPipeline",
|
| 710 |
+
"KandinskyV22CombinedPipeline",
|
| 711 |
+
"KandinskyV22ControlnetImg2ImgPipeline",
|
| 712 |
+
"KandinskyV22ControlnetPipeline",
|
| 713 |
+
"KandinskyV22Img2ImgCombinedPipeline",
|
| 714 |
+
"KandinskyV22Img2ImgPipeline",
|
| 715 |
+
"KandinskyV22InpaintCombinedPipeline",
|
| 716 |
+
"KandinskyV22InpaintPipeline",
|
| 717 |
+
"KandinskyV22Pipeline",
|
| 718 |
+
"KandinskyV22PriorEmb2EmbPipeline",
|
| 719 |
+
"KandinskyV22PriorPipeline",
|
| 720 |
+
"Krea2Pipeline",
|
| 721 |
+
"LatentConsistencyModelImg2ImgPipeline",
|
| 722 |
+
"LatentConsistencyModelPipeline",
|
| 723 |
+
"LattePipeline",
|
| 724 |
+
"LDMTextToImagePipeline",
|
| 725 |
+
"LEditsPPPipelineStableDiffusion",
|
| 726 |
+
"LEditsPPPipelineStableDiffusionXL",
|
| 727 |
+
"LLaDA2Pipeline",
|
| 728 |
+
"LLaDA2PipelineOutput",
|
| 729 |
+
"LongCatAudioDiTPipeline",
|
| 730 |
+
"LongCatImageEditPipeline",
|
| 731 |
+
"LongCatImagePipeline",
|
| 732 |
+
"LTX2ConditionPipeline",
|
| 733 |
+
"LTX2DFRPipeline",
|
| 734 |
+
"LTX2DFRPipelineOutput",
|
| 735 |
+
"LTX2DFRTemporalRefinePipeline",
|
| 736 |
+
"LTX2HDRPipeline",
|
| 737 |
+
"LTX2ImageToVideoPipeline",
|
| 738 |
+
"LTX2InContextPipeline",
|
| 739 |
+
"LTX2LatentUpsamplePipeline",
|
| 740 |
+
"LTX2Pipeline",
|
| 741 |
+
"LTX2VideoDiffusionDecodePipeline",
|
| 742 |
+
"LTXConditionPipeline",
|
| 743 |
+
"LTXI2VLongMultiPromptPipeline",
|
| 744 |
+
"LTXImageToVideoPipeline",
|
| 745 |
+
"LTXLatentUpsamplePipeline",
|
| 746 |
+
"LTXPipeline",
|
| 747 |
+
"LucyEditPipeline",
|
| 748 |
+
"Lumina2Pipeline",
|
| 749 |
+
"Lumina2Text2ImgPipeline",
|
| 750 |
+
"LuminaPipeline",
|
| 751 |
+
"LuminaText2ImgPipeline",
|
| 752 |
+
"MarigoldDepthPipeline",
|
| 753 |
+
"MarigoldIntrinsicsPipeline",
|
| 754 |
+
"MarigoldNormalsPipeline",
|
| 755 |
+
"MochiPipeline",
|
| 756 |
+
"MotifVideoImage2VideoPipeline",
|
| 757 |
+
"MotifVideoPipeline",
|
| 758 |
+
"MotifVideoPipelineOutput",
|
| 759 |
+
"MusicLDMPipeline",
|
| 760 |
+
"NucleusMoEImagePipeline",
|
| 761 |
+
"OmniGenPipeline",
|
| 762 |
+
"OvisImagePipeline",
|
| 763 |
+
"PaintByExamplePipeline",
|
| 764 |
+
"PIAPipeline",
|
| 765 |
+
"PixArtAlphaPipeline",
|
| 766 |
+
"PixArtSigmaPAGPipeline",
|
| 767 |
+
"PixArtSigmaPipeline",
|
| 768 |
+
"PRXPipeline",
|
| 769 |
+
"PRXPixelPipeline",
|
| 770 |
+
"QwenImageControlNetInpaintPipeline",
|
| 771 |
+
"QwenImageControlNetPipeline",
|
| 772 |
+
"QwenImageEditInpaintPipeline",
|
| 773 |
+
"QwenImageEditPipeline",
|
| 774 |
+
"QwenImageEditPlusPipeline",
|
| 775 |
+
"QwenImageImg2ImgPipeline",
|
| 776 |
+
"QwenImageInpaintPipeline",
|
| 777 |
+
"QwenImageLayeredPipeline",
|
| 778 |
+
"QwenImagePipeline",
|
| 779 |
+
"ReduxImageEncoder",
|
| 780 |
+
"SanaControlNetPipeline",
|
| 781 |
+
"SanaImageToVideoPipeline",
|
| 782 |
+
"SanaPAGPipeline",
|
| 783 |
+
"SanaPipeline",
|
| 784 |
+
"SanaSprintImg2ImgPipeline",
|
| 785 |
+
"SanaSprintPipeline",
|
| 786 |
+
"SanaVideoPipeline",
|
| 787 |
+
"SanaVideoPipeline",
|
| 788 |
+
"SemanticStableDiffusionPipeline",
|
| 789 |
+
"ShapEImg2ImgPipeline",
|
| 790 |
+
"ShapEPipeline",
|
| 791 |
+
"SkyReelsV2DiffusionForcingImageToVideoPipeline",
|
| 792 |
+
"SkyReelsV2DiffusionForcingPipeline",
|
| 793 |
+
"SkyReelsV2DiffusionForcingVideoToVideoPipeline",
|
| 794 |
+
"SkyReelsV2ImageToVideoPipeline",
|
| 795 |
+
"SkyReelsV2Pipeline",
|
| 796 |
+
"StableAudio3AudioToAudioPipeline",
|
| 797 |
+
"StableAudio3DurationEmbedder",
|
| 798 |
+
"StableAudio3InpaintPipeline",
|
| 799 |
+
"StableAudio3Pipeline",
|
| 800 |
+
"StableAudioPipeline",
|
| 801 |
+
"StableAudioProjectionModel",
|
| 802 |
+
"StableCascadeCombinedPipeline",
|
| 803 |
+
"StableCascadeDecoderPipeline",
|
| 804 |
+
"StableCascadePriorPipeline",
|
| 805 |
+
"StableDiffusion3ControlNetInpaintingPipeline",
|
| 806 |
+
"StableDiffusion3ControlNetPipeline",
|
| 807 |
+
"StableDiffusion3Img2ImgPipeline",
|
| 808 |
+
"StableDiffusion3InpaintPipeline",
|
| 809 |
+
"StableDiffusion3PAGImg2ImgPipeline",
|
| 810 |
+
"StableDiffusion3PAGImg2ImgPipeline",
|
| 811 |
+
"StableDiffusion3PAGPipeline",
|
| 812 |
+
"StableDiffusion3Pipeline",
|
| 813 |
+
"StableDiffusionAdapterPipeline",
|
| 814 |
+
"StableDiffusionAttendAndExcitePipeline",
|
| 815 |
+
"StableDiffusionControlNetImg2ImgPipeline",
|
| 816 |
+
"StableDiffusionControlNetInpaintPipeline",
|
| 817 |
+
"StableDiffusionControlNetPAGInpaintPipeline",
|
| 818 |
+
"StableDiffusionControlNetPAGPipeline",
|
| 819 |
+
"StableDiffusionControlNetPipeline",
|
| 820 |
+
"StableDiffusionControlNetXSPipeline",
|
| 821 |
+
"StableDiffusionDepth2ImgPipeline",
|
| 822 |
+
"StableDiffusionDiffEditPipeline",
|
| 823 |
+
"StableDiffusionGLIGENPipeline",
|
| 824 |
+
"StableDiffusionGLIGENTextImagePipeline",
|
| 825 |
+
"StableDiffusionImageVariationPipeline",
|
| 826 |
+
"StableDiffusionImg2ImgPipeline",
|
| 827 |
+
"StableDiffusionInpaintPipeline",
|
| 828 |
+
"StableDiffusionInpaintPipelineLegacy",
|
| 829 |
+
"StableDiffusionInstructPix2PixPipeline",
|
| 830 |
+
"StableDiffusionLatentUpscalePipeline",
|
| 831 |
+
"StableDiffusionLDM3DPipeline",
|
| 832 |
+
"StableDiffusionModelEditingPipeline",
|
| 833 |
+
"StableDiffusionPAGImg2ImgPipeline",
|
| 834 |
+
"StableDiffusionPAGInpaintPipeline",
|
| 835 |
+
"StableDiffusionPAGPipeline",
|
| 836 |
+
"StableDiffusionPanoramaPipeline",
|
| 837 |
+
"StableDiffusionParadigmsPipeline",
|
| 838 |
+
"StableDiffusionPipeline",
|
| 839 |
+
"StableDiffusionPipelineSafe",
|
| 840 |
+
"StableDiffusionPix2PixZeroPipeline",
|
| 841 |
+
"StableDiffusionSAGPipeline",
|
| 842 |
+
"StableDiffusionUpscalePipeline",
|
| 843 |
+
"StableDiffusionXLAdapterPipeline",
|
| 844 |
+
"StableDiffusionXLControlNetImg2ImgPipeline",
|
| 845 |
+
"StableDiffusionXLControlNetInpaintPipeline",
|
| 846 |
+
"StableDiffusionXLControlNetPAGImg2ImgPipeline",
|
| 847 |
+
"StableDiffusionXLControlNetPAGPipeline",
|
| 848 |
+
"StableDiffusionXLControlNetPipeline",
|
| 849 |
+
"StableDiffusionXLControlNetUnionImg2ImgPipeline",
|
| 850 |
+
"StableDiffusionXLControlNetUnionInpaintPipeline",
|
| 851 |
+
"StableDiffusionXLControlNetUnionPipeline",
|
| 852 |
+
"StableDiffusionXLControlNetXSPipeline",
|
| 853 |
+
"StableDiffusionXLImg2ImgPipeline",
|
| 854 |
+
"StableDiffusionXLInpaintPipeline",
|
| 855 |
+
"StableDiffusionXLInstructPix2PixPipeline",
|
| 856 |
+
"StableDiffusionXLPAGImg2ImgPipeline",
|
| 857 |
+
"StableDiffusionXLPAGInpaintPipeline",
|
| 858 |
+
"StableDiffusionXLPAGPipeline",
|
| 859 |
+
"StableDiffusionXLPipeline",
|
| 860 |
+
"StableUnCLIPImg2ImgPipeline",
|
| 861 |
+
"StableUnCLIPPipeline",
|
| 862 |
+
"StableVideoDiffusionPipeline",
|
| 863 |
+
"TextToVideoSDPipeline",
|
| 864 |
+
"TextToVideoZeroPipeline",
|
| 865 |
+
"TextToVideoZeroSDXLPipeline",
|
| 866 |
+
"UnCLIPImageVariationPipeline",
|
| 867 |
+
"UnCLIPPipeline",
|
| 868 |
+
"UniDiffuserModel",
|
| 869 |
+
"UniDiffuserPipeline",
|
| 870 |
+
"UniDiffuserTextDecoder",
|
| 871 |
+
"VersatileDiffusionDualGuidedPipeline",
|
| 872 |
+
"VersatileDiffusionImageVariationPipeline",
|
| 873 |
+
"VersatileDiffusionPipeline",
|
| 874 |
+
"VersatileDiffusionTextToImagePipeline",
|
| 875 |
+
"VideoToVideoSDPipeline",
|
| 876 |
+
"VisualClozeGenerationPipeline",
|
| 877 |
+
"VisualClozePipeline",
|
| 878 |
+
"VQDiffusionPipeline",
|
| 879 |
+
"WanAnimatePipeline",
|
| 880 |
+
"WanImageToVideoPipeline",
|
| 881 |
+
"WanPipeline",
|
| 882 |
+
"WanVACEPipeline",
|
| 883 |
+
"WanVideoToVideoPipeline",
|
| 884 |
+
"WuerstchenCombinedPipeline",
|
| 885 |
+
"WuerstchenDecoderPipeline",
|
| 886 |
+
"WuerstchenPriorPipeline",
|
| 887 |
+
"ZImageControlNetInpaintPipeline",
|
| 888 |
+
"ZImageControlNetPipeline",
|
| 889 |
+
"ZImageImg2ImgPipeline",
|
| 890 |
+
"ZImageInpaintPipeline",
|
| 891 |
+
"ZImageOmniPipeline",
|
| 892 |
+
"ZImagePipeline",
|
| 893 |
+
]
|
| 894 |
+
)
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
try:
|
| 898 |
+
if not (is_torch_available() and is_transformers_available() and is_opencv_available()):
|
| 899 |
+
raise OptionalDependencyNotAvailable()
|
| 900 |
+
except OptionalDependencyNotAvailable:
|
| 901 |
+
from .utils import dummy_torch_and_transformers_and_opencv_objects # noqa F403
|
| 902 |
+
|
| 903 |
+
_import_structure["utils.dummy_torch_and_transformers_and_opencv_objects"] = [
|
| 904 |
+
name for name in dir(dummy_torch_and_transformers_and_opencv_objects) if not name.startswith("_")
|
| 905 |
+
]
|
| 906 |
+
|
| 907 |
+
else:
|
| 908 |
+
_import_structure["pipelines"].extend(["ConsisIDPipeline"])
|
| 909 |
+
|
| 910 |
+
try:
|
| 911 |
+
if not (is_torch_available() and is_transformers_available() and is_sentencepiece_available()):
|
| 912 |
+
raise OptionalDependencyNotAvailable()
|
| 913 |
+
except OptionalDependencyNotAvailable:
|
| 914 |
+
from .utils import dummy_torch_and_transformers_and_sentencepiece_objects # noqa F403
|
| 915 |
+
|
| 916 |
+
_import_structure["utils.dummy_torch_and_transformers_and_sentencepiece_objects"] = [
|
| 917 |
+
name for name in dir(dummy_torch_and_transformers_and_sentencepiece_objects) if not name.startswith("_")
|
| 918 |
+
]
|
| 919 |
+
|
| 920 |
+
else:
|
| 921 |
+
_import_structure["pipelines"].extend(["KolorsImg2ImgPipeline", "KolorsPAGPipeline", "KolorsPipeline"])
|
| 922 |
+
|
| 923 |
+
try:
|
| 924 |
+
if not (is_torch_available() and is_transformers_available() and is_onnx_available()):
|
| 925 |
+
raise OptionalDependencyNotAvailable()
|
| 926 |
+
except OptionalDependencyNotAvailable:
|
| 927 |
+
from .utils import dummy_torch_and_transformers_and_onnx_objects # noqa F403
|
| 928 |
+
|
| 929 |
+
_import_structure["utils.dummy_torch_and_transformers_and_onnx_objects"] = [
|
| 930 |
+
name for name in dir(dummy_torch_and_transformers_and_onnx_objects) if not name.startswith("_")
|
| 931 |
+
]
|
| 932 |
+
|
| 933 |
+
else:
|
| 934 |
+
_import_structure["pipelines"].extend(
|
| 935 |
+
[
|
| 936 |
+
"OnnxStableDiffusionImg2ImgPipeline",
|
| 937 |
+
"OnnxStableDiffusionInpaintPipeline",
|
| 938 |
+
"OnnxStableDiffusionInpaintPipelineLegacy",
|
| 939 |
+
"OnnxStableDiffusionPipeline",
|
| 940 |
+
"OnnxStableDiffusionUpscalePipeline",
|
| 941 |
+
"StableDiffusionOnnxPipeline",
|
| 942 |
+
]
|
| 943 |
+
)
|
| 944 |
+
|
| 945 |
+
try:
|
| 946 |
+
if not (is_torch_available() and is_librosa_available()):
|
| 947 |
+
raise OptionalDependencyNotAvailable()
|
| 948 |
+
except OptionalDependencyNotAvailable:
|
| 949 |
+
from .utils import dummy_torch_and_librosa_objects # noqa F403
|
| 950 |
+
|
| 951 |
+
_import_structure["utils.dummy_torch_and_librosa_objects"] = [
|
| 952 |
+
name for name in dir(dummy_torch_and_librosa_objects) if not name.startswith("_")
|
| 953 |
+
]
|
| 954 |
+
|
| 955 |
+
else:
|
| 956 |
+
_import_structure["pipelines"].extend(["AudioDiffusionPipeline", "Mel"])
|
| 957 |
+
|
| 958 |
+
try:
|
| 959 |
+
if not (is_transformers_available() and is_torch_available() and is_note_seq_available()):
|
| 960 |
+
raise OptionalDependencyNotAvailable()
|
| 961 |
+
except OptionalDependencyNotAvailable:
|
| 962 |
+
from .utils import dummy_transformers_and_torch_and_note_seq_objects # noqa F403
|
| 963 |
+
|
| 964 |
+
_import_structure["utils.dummy_transformers_and_torch_and_note_seq_objects"] = [
|
| 965 |
+
name for name in dir(dummy_transformers_and_torch_and_note_seq_objects) if not name.startswith("_")
|
| 966 |
+
]
|
| 967 |
+
|
| 968 |
+
|
| 969 |
+
else:
|
| 970 |
+
_import_structure["pipelines"].extend(["SpectrogramDiffusionPipeline"])
|
| 971 |
+
|
| 972 |
+
try:
|
| 973 |
+
if not (is_note_seq_available()):
|
| 974 |
+
raise OptionalDependencyNotAvailable()
|
| 975 |
+
except OptionalDependencyNotAvailable:
|
| 976 |
+
from .utils import dummy_note_seq_objects # noqa F403
|
| 977 |
+
|
| 978 |
+
_import_structure["utils.dummy_note_seq_objects"] = [
|
| 979 |
+
name for name in dir(dummy_note_seq_objects) if not name.startswith("_")
|
| 980 |
+
]
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
else:
|
| 984 |
+
_import_structure["pipelines"].extend(["MidiProcessor"])
|
| 985 |
+
|
| 986 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 987 |
+
from .configuration_utils import ConfigMixin
|
| 988 |
+
from .quantizers import PipelineQuantizationConfig
|
| 989 |
+
|
| 990 |
+
try:
|
| 991 |
+
if not is_bitsandbytes_available():
|
| 992 |
+
raise OptionalDependencyNotAvailable()
|
| 993 |
+
except OptionalDependencyNotAvailable:
|
| 994 |
+
from .utils.dummy_bitsandbytes_objects import *
|
| 995 |
+
else:
|
| 996 |
+
from .quantizers.quantization_config import BitsAndBytesConfig
|
| 997 |
+
|
| 998 |
+
try:
|
| 999 |
+
if not is_gguf_available():
|
| 1000 |
+
raise OptionalDependencyNotAvailable()
|
| 1001 |
+
except OptionalDependencyNotAvailable:
|
| 1002 |
+
from .utils.dummy_gguf_objects import *
|
| 1003 |
+
else:
|
| 1004 |
+
from .quantizers.quantization_config import GGUFQuantizationConfig
|
| 1005 |
+
|
| 1006 |
+
try:
|
| 1007 |
+
if not is_torchao_available():
|
| 1008 |
+
raise OptionalDependencyNotAvailable()
|
| 1009 |
+
except OptionalDependencyNotAvailable:
|
| 1010 |
+
from .utils.dummy_torchao_objects import *
|
| 1011 |
+
else:
|
| 1012 |
+
from .quantizers.quantization_config import TorchAoConfig
|
| 1013 |
+
|
| 1014 |
+
try:
|
| 1015 |
+
if not is_optimum_quanto_available():
|
| 1016 |
+
raise OptionalDependencyNotAvailable()
|
| 1017 |
+
except OptionalDependencyNotAvailable:
|
| 1018 |
+
from .utils.dummy_optimum_quanto_objects import *
|
| 1019 |
+
else:
|
| 1020 |
+
from .quantizers.quantization_config import QuantoConfig
|
| 1021 |
+
|
| 1022 |
+
try:
|
| 1023 |
+
if not is_nvidia_modelopt_available():
|
| 1024 |
+
raise OptionalDependencyNotAvailable()
|
| 1025 |
+
except OptionalDependencyNotAvailable:
|
| 1026 |
+
from .utils.dummy_nvidia_modelopt_objects import *
|
| 1027 |
+
else:
|
| 1028 |
+
from .quantizers.quantization_config import NVIDIAModelOptConfig
|
| 1029 |
+
|
| 1030 |
+
try:
|
| 1031 |
+
if not is_torch_available():
|
| 1032 |
+
raise OptionalDependencyNotAvailable()
|
| 1033 |
+
except OptionalDependencyNotAvailable:
|
| 1034 |
+
from .utils.dummy_nunchaku_lite_objects import *
|
| 1035 |
+
else:
|
| 1036 |
+
from .quantizers.quantization_config import NunchakuLiteQuantizationConfig
|
| 1037 |
+
|
| 1038 |
+
try:
|
| 1039 |
+
if not is_auto_round_available():
|
| 1040 |
+
raise OptionalDependencyNotAvailable()
|
| 1041 |
+
except OptionalDependencyNotAvailable:
|
| 1042 |
+
from .utils.dummy_auto_round_objects import *
|
| 1043 |
+
else:
|
| 1044 |
+
from .quantizers.quantization_config import AutoRoundConfig
|
| 1045 |
+
|
| 1046 |
+
try:
|
| 1047 |
+
if not is_sdnq_available():
|
| 1048 |
+
raise OptionalDependencyNotAvailable()
|
| 1049 |
+
except OptionalDependencyNotAvailable:
|
| 1050 |
+
from .utils.dummy_sdnq_objects import *
|
| 1051 |
+
else:
|
| 1052 |
+
from .quantizers.quantization_config import SDNQConfig
|
| 1053 |
+
|
| 1054 |
+
try:
|
| 1055 |
+
if not is_onnx_available():
|
| 1056 |
+
raise OptionalDependencyNotAvailable()
|
| 1057 |
+
except OptionalDependencyNotAvailable:
|
| 1058 |
+
from .utils.dummy_onnx_objects import * # noqa F403
|
| 1059 |
+
else:
|
| 1060 |
+
from .pipelines import OnnxRuntimeModel
|
| 1061 |
+
|
| 1062 |
+
try:
|
| 1063 |
+
if not is_torch_available():
|
| 1064 |
+
raise OptionalDependencyNotAvailable()
|
| 1065 |
+
except OptionalDependencyNotAvailable:
|
| 1066 |
+
from .utils.dummy_pt_objects import * # noqa F403
|
| 1067 |
+
else:
|
| 1068 |
+
from .guiders import (
|
| 1069 |
+
AdaptiveProjectedGuidance,
|
| 1070 |
+
AdaptiveProjectedMixGuidance,
|
| 1071 |
+
AutoGuidance,
|
| 1072 |
+
BaseGuidance,
|
| 1073 |
+
ClassifierFreeGuidance,
|
| 1074 |
+
ClassifierFreeZeroStarGuidance,
|
| 1075 |
+
FrequencyDecoupledGuidance,
|
| 1076 |
+
LTX2Guidance,
|
| 1077 |
+
PerturbedAttentionGuidance,
|
| 1078 |
+
SkipLayerGuidance,
|
| 1079 |
+
SmoothedEnergyGuidance,
|
| 1080 |
+
TangentialClassifierFreeGuidance,
|
| 1081 |
+
)
|
| 1082 |
+
from .hooks import (
|
| 1083 |
+
FasterCacheConfig,
|
| 1084 |
+
FirstBlockCacheConfig,
|
| 1085 |
+
HookRegistry,
|
| 1086 |
+
LayerSkipConfig,
|
| 1087 |
+
MagCacheConfig,
|
| 1088 |
+
PyramidAttentionBroadcastConfig,
|
| 1089 |
+
SmoothedEnergyGuidanceConfig,
|
| 1090 |
+
TaylorSeerCacheConfig,
|
| 1091 |
+
TextKVCacheConfig,
|
| 1092 |
+
apply_faster_cache,
|
| 1093 |
+
apply_first_block_cache,
|
| 1094 |
+
apply_layer_skip,
|
| 1095 |
+
apply_mag_cache,
|
| 1096 |
+
apply_pyramid_attention_broadcast,
|
| 1097 |
+
apply_taylorseer_cache,
|
| 1098 |
+
apply_text_kv_cache,
|
| 1099 |
+
)
|
| 1100 |
+
from .image_processor import (
|
| 1101 |
+
InpaintProcessor,
|
| 1102 |
+
IPAdapterMaskProcessor,
|
| 1103 |
+
PixArtImageProcessor,
|
| 1104 |
+
VaeImageProcessor,
|
| 1105 |
+
VaeImageProcessorLDM3D,
|
| 1106 |
+
)
|
| 1107 |
+
from .models import (
|
| 1108 |
+
AceStepTransformer1DModel,
|
| 1109 |
+
AllegroTransformer3DModel,
|
| 1110 |
+
AnimaTextConditioner,
|
| 1111 |
+
AnyFlowFARTransformer3DModel,
|
| 1112 |
+
AnyFlowTransformer3DModel,
|
| 1113 |
+
AsymmetricAutoencoderKL,
|
| 1114 |
+
AttentionBackendName,
|
| 1115 |
+
AuraFlowTransformer2DModel,
|
| 1116 |
+
AutoencoderDC,
|
| 1117 |
+
AutoencoderKL,
|
| 1118 |
+
AutoencoderKLAllegro,
|
| 1119 |
+
AutoencoderKLCogVideoX,
|
| 1120 |
+
AutoencoderKLCosmos,
|
| 1121 |
+
AutoencoderKLFlux2,
|
| 1122 |
+
AutoencoderKLHunyuanImage,
|
| 1123 |
+
AutoencoderKLHunyuanImageRefiner,
|
| 1124 |
+
AutoencoderKLHunyuanVideo,
|
| 1125 |
+
AutoencoderKLHunyuanVideo15,
|
| 1126 |
+
AutoencoderKLKVAE,
|
| 1127 |
+
AutoencoderKLKVAEVideo,
|
| 1128 |
+
AutoencoderKLLTX2Audio,
|
| 1129 |
+
AutoencoderKLLTX2Video,
|
| 1130 |
+
AutoencoderKLLTXVideo,
|
| 1131 |
+
AutoencoderKLMagvit,
|
| 1132 |
+
AutoencoderKLMiniMaxH3,
|
| 1133 |
+
AutoencoderKLMiniMaxH3Audio,
|
| 1134 |
+
AutoencoderKLMochi,
|
| 1135 |
+
AutoencoderKLQwenImage,
|
| 1136 |
+
AutoencoderKLTemporalDecoder,
|
| 1137 |
+
AutoencoderKLWan,
|
| 1138 |
+
AutoencoderOobleck,
|
| 1139 |
+
AutoencoderRAE,
|
| 1140 |
+
AutoencoderSAME,
|
| 1141 |
+
AutoencoderTiny,
|
| 1142 |
+
AutoencoderVidTok,
|
| 1143 |
+
AutoModel,
|
| 1144 |
+
BriaFiboTransformer2DModel,
|
| 1145 |
+
BriaTransformer2DModel,
|
| 1146 |
+
CacheMixin,
|
| 1147 |
+
ChromaTransformer2DModel,
|
| 1148 |
+
ChronoEditTransformer3DModel,
|
| 1149 |
+
CogVideoXTransformer3DModel,
|
| 1150 |
+
CogView3PlusTransformer2DModel,
|
| 1151 |
+
CogView4Transformer2DModel,
|
| 1152 |
+
ConsisIDTransformer3DModel,
|
| 1153 |
+
ConsistencyDecoderVAE,
|
| 1154 |
+
ContextParallelConfig,
|
| 1155 |
+
ControlNetModel,
|
| 1156 |
+
ControlNetUnionModel,
|
| 1157 |
+
ControlNetXSAdapter,
|
| 1158 |
+
Cosmos3AVAEAudioTokenizer,
|
| 1159 |
+
Cosmos3OmniTransformer,
|
| 1160 |
+
CosmosControlNetModel,
|
| 1161 |
+
CosmosTransformer3DModel,
|
| 1162 |
+
DiTTransformer2DModel,
|
| 1163 |
+
DreamLiteTransformer2DModel,
|
| 1164 |
+
DreamLiteUNetModel,
|
| 1165 |
+
EasyAnimateTransformer3DModel,
|
| 1166 |
+
ErnieImageTransformer2DModel,
|
| 1167 |
+
Flux2Transformer2DModel,
|
| 1168 |
+
Flux3Transformer2DModel,
|
| 1169 |
+
FluxControlNetModel,
|
| 1170 |
+
FluxMultiControlNetModel,
|
| 1171 |
+
FluxTransformer2DModel,
|
| 1172 |
+
GlmImageTransformer2DModel,
|
| 1173 |
+
HeliosTransformer3DModel,
|
| 1174 |
+
HiDreamImageTransformer2DModel,
|
| 1175 |
+
HunyuanDiT2DControlNetModel,
|
| 1176 |
+
HunyuanDiT2DModel,
|
| 1177 |
+
HunyuanDiT2DMultiControlNetModel,
|
| 1178 |
+
HunyuanImageTransformer2DModel,
|
| 1179 |
+
HunyuanVideo15Transformer3DModel,
|
| 1180 |
+
HunyuanVideoFramepackTransformer3DModel,
|
| 1181 |
+
HunyuanVideoTransformer3DModel,
|
| 1182 |
+
I2VGenXLUNet,
|
| 1183 |
+
Ideogram4Transformer2DModel,
|
| 1184 |
+
JoyImageEditPlusTransformer3DModel,
|
| 1185 |
+
JoyImageEditTransformer3DModel,
|
| 1186 |
+
Kandinsky3UNet,
|
| 1187 |
+
Kandinsky5Transformer3DModel,
|
| 1188 |
+
Krea2Transformer2DModel,
|
| 1189 |
+
LatteTransformer3DModel,
|
| 1190 |
+
LongCatAudioDiTTransformer,
|
| 1191 |
+
LongCatAudioDiTVae,
|
| 1192 |
+
LongCatImageTransformer2DModel,
|
| 1193 |
+
LTX2VideoDiffusionDecoderModel,
|
| 1194 |
+
LTX2VideoTransformer3DModel,
|
| 1195 |
+
LTXVideoTransformer3DModel,
|
| 1196 |
+
Lumina2Transformer2DModel,
|
| 1197 |
+
LuminaNextDiT2DModel,
|
| 1198 |
+
MiniMaxH3Transformer3DModel,
|
| 1199 |
+
MiniMaxMusic3ConditionEncoder,
|
| 1200 |
+
MiniMaxMusic3RVQDepthDecoder,
|
| 1201 |
+
MiniMaxMusic3Transformer1DModel,
|
| 1202 |
+
MiniMaxMusic3Vocoder,
|
| 1203 |
+
MochiTransformer3DModel,
|
| 1204 |
+
ModelMixin,
|
| 1205 |
+
MotifVideoTransformer3DModel,
|
| 1206 |
+
MotionAdapter,
|
| 1207 |
+
MultiAdapter,
|
| 1208 |
+
MultiControlNetModel,
|
| 1209 |
+
NucleusMoEImageTransformer2DModel,
|
| 1210 |
+
OmniGenTransformer2DModel,
|
| 1211 |
+
OvisImageTransformer2DModel,
|
| 1212 |
+
ParallelConfig,
|
| 1213 |
+
PixArtTransformer2DModel,
|
| 1214 |
+
PriorTransformer,
|
| 1215 |
+
PRXTransformer2DModel,
|
| 1216 |
+
QwenImageControlNetModel,
|
| 1217 |
+
QwenImageMultiControlNetModel,
|
| 1218 |
+
QwenImageTransformer2DModel,
|
| 1219 |
+
SanaControlNetModel,
|
| 1220 |
+
SanaTransformer2DModel,
|
| 1221 |
+
SanaVideoTransformer3DModel,
|
| 1222 |
+
SD3ControlNetModel,
|
| 1223 |
+
SD3MultiControlNetModel,
|
| 1224 |
+
SD3Transformer2DModel,
|
| 1225 |
+
SkyReelsV2Transformer3DModel,
|
| 1226 |
+
SparseControlNetModel,
|
| 1227 |
+
StableAudio3DiTModel,
|
| 1228 |
+
StableAudioDiTModel,
|
| 1229 |
+
T2IAdapter,
|
| 1230 |
+
T5FilmDecoder,
|
| 1231 |
+
TensorParallelConfig,
|
| 1232 |
+
Transformer2DModel,
|
| 1233 |
+
TransformerTemporalModel,
|
| 1234 |
+
UNet1DModel,
|
| 1235 |
+
UNet2DConditionModel,
|
| 1236 |
+
UNet2DModel,
|
| 1237 |
+
UNet3DConditionModel,
|
| 1238 |
+
UNetControlNetXSModel,
|
| 1239 |
+
UNetMotionModel,
|
| 1240 |
+
UNetSpatioTemporalConditionModel,
|
| 1241 |
+
UVit2DModel,
|
| 1242 |
+
VQModel,
|
| 1243 |
+
WanAnimate2Transformer3DModel,
|
| 1244 |
+
WanAnimateTransformer3DModel,
|
| 1245 |
+
WanTransformer3DModel,
|
| 1246 |
+
WanVACETransformer3DModel,
|
| 1247 |
+
ZImageControlNetModel,
|
| 1248 |
+
ZImageTransformer2DModel,
|
| 1249 |
+
attention_backend,
|
| 1250 |
+
)
|
| 1251 |
+
from .modular_pipelines import (
|
| 1252 |
+
AutoPipelineBlocks,
|
| 1253 |
+
ComponentsManager,
|
| 1254 |
+
ComponentSpec,
|
| 1255 |
+
ConditionalPipelineBlocks,
|
| 1256 |
+
ConfigSpec,
|
| 1257 |
+
InputParam,
|
| 1258 |
+
LoopSequentialPipelineBlocks,
|
| 1259 |
+
ModularPipeline,
|
| 1260 |
+
ModularPipelineBlocks,
|
| 1261 |
+
OutputParam,
|
| 1262 |
+
SequentialPipelineBlocks,
|
| 1263 |
+
)
|
| 1264 |
+
from .optimization import (
|
| 1265 |
+
get_constant_schedule,
|
| 1266 |
+
get_constant_schedule_with_warmup,
|
| 1267 |
+
get_cosine_schedule_with_warmup,
|
| 1268 |
+
get_cosine_with_hard_restarts_schedule_with_warmup,
|
| 1269 |
+
get_linear_schedule_with_warmup,
|
| 1270 |
+
get_polynomial_decay_schedule_with_warmup,
|
| 1271 |
+
get_scheduler,
|
| 1272 |
+
)
|
| 1273 |
+
from .pipelines import (
|
| 1274 |
+
AudioPipelineOutput,
|
| 1275 |
+
AutoPipelineForImage2Image,
|
| 1276 |
+
AutoPipelineForInpainting,
|
| 1277 |
+
AutoPipelineForText2Audio,
|
| 1278 |
+
AutoPipelineForText2Image,
|
| 1279 |
+
BlipDiffusionControlNetPipeline,
|
| 1280 |
+
BlipDiffusionPipeline,
|
| 1281 |
+
CLIPImageProjection,
|
| 1282 |
+
ConsistencyModelPipeline,
|
| 1283 |
+
DanceDiffusionPipeline,
|
| 1284 |
+
DDIMPipeline,
|
| 1285 |
+
DDPMPipeline,
|
| 1286 |
+
DiffusionPipeline,
|
| 1287 |
+
DiTPipeline,
|
| 1288 |
+
ImagePipelineOutput,
|
| 1289 |
+
KarrasVePipeline,
|
| 1290 |
+
LDMPipeline,
|
| 1291 |
+
LDMSuperResolutionPipeline,
|
| 1292 |
+
PNDMPipeline,
|
| 1293 |
+
RePaintPipeline,
|
| 1294 |
+
ScoreSdeVePipeline,
|
| 1295 |
+
StableDiffusionMixin,
|
| 1296 |
+
)
|
| 1297 |
+
from .quantizers import DiffusersQuantizer
|
| 1298 |
+
from .schedulers import (
|
| 1299 |
+
AmusedScheduler,
|
| 1300 |
+
BlockRefinementScheduler,
|
| 1301 |
+
BlockRefinementSchedulerOutput,
|
| 1302 |
+
CMStochasticIterativeScheduler,
|
| 1303 |
+
CogVideoXDDIMScheduler,
|
| 1304 |
+
CogVideoXDPMScheduler,
|
| 1305 |
+
DDIMInverseScheduler,
|
| 1306 |
+
DDIMParallelScheduler,
|
| 1307 |
+
DDIMScheduler,
|
| 1308 |
+
DDPMParallelScheduler,
|
| 1309 |
+
DDPMScheduler,
|
| 1310 |
+
DDPMWuerstchenScheduler,
|
| 1311 |
+
DEISMultistepScheduler,
|
| 1312 |
+
DiscreteDDIMScheduler,
|
| 1313 |
+
DiscreteDDIMSchedulerOutput,
|
| 1314 |
+
DPMSolverMultistepInverseScheduler,
|
| 1315 |
+
DPMSolverMultistepScheduler,
|
| 1316 |
+
DPMSolverSinglestepScheduler,
|
| 1317 |
+
EDMDPMSolverMultistepScheduler,
|
| 1318 |
+
EDMEulerScheduler,
|
| 1319 |
+
EntropyBoundScheduler,
|
| 1320 |
+
EntropyBoundSchedulerOutput,
|
| 1321 |
+
EulerAncestralDiscreteScheduler,
|
| 1322 |
+
EulerDiscreteScheduler,
|
| 1323 |
+
FlowMapEulerDiscreteScheduler,
|
| 1324 |
+
FlowMatchEulerDiscreteScheduler,
|
| 1325 |
+
FlowMatchHeunDiscreteScheduler,
|
| 1326 |
+
FlowMatchLCMScheduler,
|
| 1327 |
+
HeliosDMDScheduler,
|
| 1328 |
+
HeliosScheduler,
|
| 1329 |
+
HeunDiscreteScheduler,
|
| 1330 |
+
IPNDMScheduler,
|
| 1331 |
+
KarrasVeScheduler,
|
| 1332 |
+
KDPM2AncestralDiscreteScheduler,
|
| 1333 |
+
KDPM2DiscreteScheduler,
|
| 1334 |
+
LCMScheduler,
|
| 1335 |
+
LTXEulerAncestralRFScheduler,
|
| 1336 |
+
MiniMaxH3Scheduler,
|
| 1337 |
+
PNDMScheduler,
|
| 1338 |
+
RePaintScheduler,
|
| 1339 |
+
SASolverScheduler,
|
| 1340 |
+
SchedulerMixin,
|
| 1341 |
+
SCMScheduler,
|
| 1342 |
+
ScoreSdeVeScheduler,
|
| 1343 |
+
TCDScheduler,
|
| 1344 |
+
UnCLIPScheduler,
|
| 1345 |
+
UniPCMultistepScheduler,
|
| 1346 |
+
VQDiffusionScheduler,
|
| 1347 |
+
)
|
| 1348 |
+
from .training_utils import EMAModel
|
| 1349 |
+
from .video_processor import VideoProcessor
|
| 1350 |
+
|
| 1351 |
+
try:
|
| 1352 |
+
if not (is_torch_available() and is_scipy_available()):
|
| 1353 |
+
raise OptionalDependencyNotAvailable()
|
| 1354 |
+
except OptionalDependencyNotAvailable:
|
| 1355 |
+
from .utils.dummy_torch_and_scipy_objects import * # noqa F403
|
| 1356 |
+
else:
|
| 1357 |
+
from .schedulers import LMSDiscreteScheduler
|
| 1358 |
+
|
| 1359 |
+
try:
|
| 1360 |
+
if not (is_torch_available() and is_torchsde_available()):
|
| 1361 |
+
raise OptionalDependencyNotAvailable()
|
| 1362 |
+
except OptionalDependencyNotAvailable:
|
| 1363 |
+
from .utils.dummy_torch_and_torchsde_objects import * # noqa F403
|
| 1364 |
+
else:
|
| 1365 |
+
from .schedulers import CosineDPMSolverMultistepScheduler, DPMSolverSDEScheduler
|
| 1366 |
+
|
| 1367 |
+
try:
|
| 1368 |
+
if not (is_torch_available() and is_transformers_available()):
|
| 1369 |
+
raise OptionalDependencyNotAvailable()
|
| 1370 |
+
except OptionalDependencyNotAvailable:
|
| 1371 |
+
from .utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 1372 |
+
else:
|
| 1373 |
+
from .modular_pipelines import (
|
| 1374 |
+
AnimaAutoBlocks,
|
| 1375 |
+
AnimaModularPipeline,
|
| 1376 |
+
Cosmos3DistilledBlocks,
|
| 1377 |
+
Cosmos3DistilledModularPipeline,
|
| 1378 |
+
Cosmos3OmniBlocks,
|
| 1379 |
+
Cosmos3OmniModularPipeline,
|
| 1380 |
+
ErnieImageAutoBlocks,
|
| 1381 |
+
ErnieImageModularPipeline,
|
| 1382 |
+
Flux2AutoBlocks,
|
| 1383 |
+
Flux2KleinAutoBlocks,
|
| 1384 |
+
Flux2KleinBaseAutoBlocks,
|
| 1385 |
+
Flux2KleinBaseModularPipeline,
|
| 1386 |
+
Flux2KleinModularPipeline,
|
| 1387 |
+
Flux2ModularPipeline,
|
| 1388 |
+
Flux3AutoBlocks,
|
| 1389 |
+
Flux3ModularPipeline,
|
| 1390 |
+
FluxAutoBlocks,
|
| 1391 |
+
FluxKontextAutoBlocks,
|
| 1392 |
+
FluxKontextModularPipeline,
|
| 1393 |
+
FluxModularPipeline,
|
| 1394 |
+
HeliosAutoBlocks,
|
| 1395 |
+
HeliosModularPipeline,
|
| 1396 |
+
HeliosPyramidAutoBlocks,
|
| 1397 |
+
HeliosPyramidDistilledAutoBlocks,
|
| 1398 |
+
HeliosPyramidDistilledModularPipeline,
|
| 1399 |
+
HeliosPyramidModularPipeline,
|
| 1400 |
+
HunyuanVideo15AutoBlocks,
|
| 1401 |
+
HunyuanVideo15ModularPipeline,
|
| 1402 |
+
Ideogram4AutoBlocks,
|
| 1403 |
+
Ideogram4ModularPipeline,
|
| 1404 |
+
Krea2AutoBlocks,
|
| 1405 |
+
Krea2ModularPipeline,
|
| 1406 |
+
Krea2TurboAutoBlocks,
|
| 1407 |
+
Krea2TurboModularPipeline,
|
| 1408 |
+
LTX2AutoBlocks,
|
| 1409 |
+
LTX2ModularPipeline,
|
| 1410 |
+
LTX25AutoBlocks,
|
| 1411 |
+
LTX25ModularPipeline,
|
| 1412 |
+
LTXAutoBlocks,
|
| 1413 |
+
LTXModularPipeline,
|
| 1414 |
+
MiniMaxH3Blocks,
|
| 1415 |
+
MiniMaxH3ModularPipeline,
|
| 1416 |
+
MiniMaxMusic3Blocks,
|
| 1417 |
+
MiniMaxMusic3ModularPipeline,
|
| 1418 |
+
QwenImageAutoBlocks,
|
| 1419 |
+
QwenImageEditAutoBlocks,
|
| 1420 |
+
QwenImageEditModularPipeline,
|
| 1421 |
+
QwenImageEditPlusAutoBlocks,
|
| 1422 |
+
QwenImageEditPlusModularPipeline,
|
| 1423 |
+
QwenImageLayeredAutoBlocks,
|
| 1424 |
+
QwenImageLayeredModularPipeline,
|
| 1425 |
+
QwenImageModularPipeline,
|
| 1426 |
+
StableDiffusion3AutoBlocks,
|
| 1427 |
+
StableDiffusion3ModularPipeline,
|
| 1428 |
+
StableDiffusionXLAutoBlocks,
|
| 1429 |
+
StableDiffusionXLModularPipeline,
|
| 1430 |
+
Wan22Blocks,
|
| 1431 |
+
Wan22Image2VideoBlocks,
|
| 1432 |
+
Wan22Image2VideoModularPipeline,
|
| 1433 |
+
Wan22ModularPipeline,
|
| 1434 |
+
Wan22VaceBlocks,
|
| 1435 |
+
Wan22VaceModularPipeline,
|
| 1436 |
+
WanAnimate2Blocks,
|
| 1437 |
+
WanAnimate2DistilledBlocks,
|
| 1438 |
+
WanAnimate2DistilledModularPipeline,
|
| 1439 |
+
WanAnimate2ModularPipeline,
|
| 1440 |
+
WanBlocks,
|
| 1441 |
+
WanImage2VideoAutoBlocks,
|
| 1442 |
+
WanImage2VideoModularPipeline,
|
| 1443 |
+
WanModularPipeline,
|
| 1444 |
+
ZImageAutoBlocks,
|
| 1445 |
+
ZImageModularPipeline,
|
| 1446 |
+
)
|
| 1447 |
+
from .pipelines import (
|
| 1448 |
+
AceStepAudioTokenDetokenizer,
|
| 1449 |
+
AceStepAudioTokenizer,
|
| 1450 |
+
AceStepConditionEncoder,
|
| 1451 |
+
AceStepPipeline,
|
| 1452 |
+
AllegroPipeline,
|
| 1453 |
+
AltDiffusionImg2ImgPipeline,
|
| 1454 |
+
AltDiffusionPipeline,
|
| 1455 |
+
AmusedImg2ImgPipeline,
|
| 1456 |
+
AmusedInpaintPipeline,
|
| 1457 |
+
AmusedPipeline,
|
| 1458 |
+
AnimateDiffControlNetPipeline,
|
| 1459 |
+
AnimateDiffPAGPipeline,
|
| 1460 |
+
AnimateDiffPipeline,
|
| 1461 |
+
AnimateDiffSDXLPipeline,
|
| 1462 |
+
AnimateDiffSparseControlNetPipeline,
|
| 1463 |
+
AnimateDiffVideoToVideoControlNetPipeline,
|
| 1464 |
+
AnimateDiffVideoToVideoPipeline,
|
| 1465 |
+
AnyFlowFARPipeline,
|
| 1466 |
+
AnyFlowPipeline,
|
| 1467 |
+
AudioLDM2Pipeline,
|
| 1468 |
+
AudioLDM2ProjectionModel,
|
| 1469 |
+
AudioLDM2UNet2DConditionModel,
|
| 1470 |
+
AudioLDMPipeline,
|
| 1471 |
+
AuraFlowPipeline,
|
| 1472 |
+
BriaFiboEditPipeline,
|
| 1473 |
+
BriaFiboPipeline,
|
| 1474 |
+
BriaPipeline,
|
| 1475 |
+
ChromaImg2ImgPipeline,
|
| 1476 |
+
ChromaInpaintPipeline,
|
| 1477 |
+
ChromaPipeline,
|
| 1478 |
+
ChronoEditPipeline,
|
| 1479 |
+
CLIPImageProjection,
|
| 1480 |
+
CogVideoXFunControlPipeline,
|
| 1481 |
+
CogVideoXImageToVideoPipeline,
|
| 1482 |
+
CogVideoXPipeline,
|
| 1483 |
+
CogVideoXVideoToVideoPipeline,
|
| 1484 |
+
CogView3PlusPipeline,
|
| 1485 |
+
CogView4ControlPipeline,
|
| 1486 |
+
CogView4Pipeline,
|
| 1487 |
+
ConsisIDPipeline,
|
| 1488 |
+
Cosmos2_5_PredictBasePipeline,
|
| 1489 |
+
Cosmos2_5_TransferPipeline,
|
| 1490 |
+
Cosmos2TextToImagePipeline,
|
| 1491 |
+
Cosmos2VideoToWorldPipeline,
|
| 1492 |
+
Cosmos3OmniPipeline,
|
| 1493 |
+
CosmosActionCondition,
|
| 1494 |
+
CosmosTextToWorldPipeline,
|
| 1495 |
+
CosmosVideoToWorldPipeline,
|
| 1496 |
+
CycleDiffusionPipeline,
|
| 1497 |
+
DiffusionGemmaPipeline,
|
| 1498 |
+
DiffusionGemmaPipelineOutput,
|
| 1499 |
+
DreamLiteMobilePipeline,
|
| 1500 |
+
DreamLitePipeline,
|
| 1501 |
+
DreamLitePipelineOutput,
|
| 1502 |
+
EasyAnimateControlPipeline,
|
| 1503 |
+
EasyAnimateInpaintPipeline,
|
| 1504 |
+
EasyAnimatePipeline,
|
| 1505 |
+
ErnieImagePipeline,
|
| 1506 |
+
Flux2KleinInpaintPipeline,
|
| 1507 |
+
Flux2KleinKVPipeline,
|
| 1508 |
+
Flux2KleinPipeline,
|
| 1509 |
+
Flux2Pipeline,
|
| 1510 |
+
FluxControlImg2ImgPipeline,
|
| 1511 |
+
FluxControlInpaintPipeline,
|
| 1512 |
+
FluxControlNetImg2ImgPipeline,
|
| 1513 |
+
FluxControlNetInpaintPipeline,
|
| 1514 |
+
FluxControlNetPipeline,
|
| 1515 |
+
FluxControlPipeline,
|
| 1516 |
+
FluxFillPipeline,
|
| 1517 |
+
FluxImg2ImgPipeline,
|
| 1518 |
+
FluxInpaintPipeline,
|
| 1519 |
+
FluxKontextInpaintPipeline,
|
| 1520 |
+
FluxKontextPipeline,
|
| 1521 |
+
FluxPipeline,
|
| 1522 |
+
FluxPriorReduxPipeline,
|
| 1523 |
+
GlmImagePipeline,
|
| 1524 |
+
HeliosPipeline,
|
| 1525 |
+
HeliosPyramidPipeline,
|
| 1526 |
+
HiDreamImagePipeline,
|
| 1527 |
+
HunyuanDiTControlNetPipeline,
|
| 1528 |
+
HunyuanDiTPAGPipeline,
|
| 1529 |
+
HunyuanDiTPipeline,
|
| 1530 |
+
HunyuanImagePipeline,
|
| 1531 |
+
HunyuanImageRefinerPipeline,
|
| 1532 |
+
HunyuanSkyreelsImageToVideoPipeline,
|
| 1533 |
+
HunyuanVideo15ImageToVideoPipeline,
|
| 1534 |
+
HunyuanVideo15Pipeline,
|
| 1535 |
+
HunyuanVideoFramepackPipeline,
|
| 1536 |
+
HunyuanVideoImageToVideoPipeline,
|
| 1537 |
+
HunyuanVideoPipeline,
|
| 1538 |
+
I2VGenXLPipeline,
|
| 1539 |
+
Ideogram4Pipeline,
|
| 1540 |
+
Ideogram4PromptEnhancerHead,
|
| 1541 |
+
IFImg2ImgPipeline,
|
| 1542 |
+
IFImg2ImgSuperResolutionPipeline,
|
| 1543 |
+
IFInpaintingPipeline,
|
| 1544 |
+
IFInpaintingSuperResolutionPipeline,
|
| 1545 |
+
IFPipeline,
|
| 1546 |
+
IFSuperResolutionPipeline,
|
| 1547 |
+
ImageTextPipelineOutput,
|
| 1548 |
+
JoyImageEditPipeline,
|
| 1549 |
+
JoyImageEditPipelineOutput,
|
| 1550 |
+
JoyImageEditPlusPipeline,
|
| 1551 |
+
JoyImageEditPlusPipelineOutput,
|
| 1552 |
+
Kandinsky3Img2ImgPipeline,
|
| 1553 |
+
Kandinsky3Pipeline,
|
| 1554 |
+
Kandinsky5I2IPipeline,
|
| 1555 |
+
Kandinsky5I2VPipeline,
|
| 1556 |
+
Kandinsky5T2IPipeline,
|
| 1557 |
+
Kandinsky5T2VPipeline,
|
| 1558 |
+
KandinskyCombinedPipeline,
|
| 1559 |
+
KandinskyImg2ImgCombinedPipeline,
|
| 1560 |
+
KandinskyImg2ImgPipeline,
|
| 1561 |
+
KandinskyInpaintCombinedPipeline,
|
| 1562 |
+
KandinskyInpaintPipeline,
|
| 1563 |
+
KandinskyPipeline,
|
| 1564 |
+
KandinskyPriorPipeline,
|
| 1565 |
+
KandinskyV22CombinedPipeline,
|
| 1566 |
+
KandinskyV22ControlnetImg2ImgPipeline,
|
| 1567 |
+
KandinskyV22ControlnetPipeline,
|
| 1568 |
+
KandinskyV22Img2ImgCombinedPipeline,
|
| 1569 |
+
KandinskyV22Img2ImgPipeline,
|
| 1570 |
+
KandinskyV22InpaintCombinedPipeline,
|
| 1571 |
+
KandinskyV22InpaintPipeline,
|
| 1572 |
+
KandinskyV22Pipeline,
|
| 1573 |
+
KandinskyV22PriorEmb2EmbPipeline,
|
| 1574 |
+
KandinskyV22PriorPipeline,
|
| 1575 |
+
Krea2Pipeline,
|
| 1576 |
+
LatentConsistencyModelImg2ImgPipeline,
|
| 1577 |
+
LatentConsistencyModelPipeline,
|
| 1578 |
+
LattePipeline,
|
| 1579 |
+
LDMTextToImagePipeline,
|
| 1580 |
+
LEditsPPPipelineStableDiffusion,
|
| 1581 |
+
LEditsPPPipelineStableDiffusionXL,
|
| 1582 |
+
LLaDA2Pipeline,
|
| 1583 |
+
LLaDA2PipelineOutput,
|
| 1584 |
+
LongCatAudioDiTPipeline,
|
| 1585 |
+
LongCatImageEditPipeline,
|
| 1586 |
+
LongCatImagePipeline,
|
| 1587 |
+
LTX2ConditionPipeline,
|
| 1588 |
+
LTX2DFRPipeline,
|
| 1589 |
+
LTX2DFRPipelineOutput,
|
| 1590 |
+
LTX2DFRTemporalRefinePipeline,
|
| 1591 |
+
LTX2HDRPipeline,
|
| 1592 |
+
LTX2ImageToVideoPipeline,
|
| 1593 |
+
LTX2InContextPipeline,
|
| 1594 |
+
LTX2LatentUpsamplePipeline,
|
| 1595 |
+
LTX2Pipeline,
|
| 1596 |
+
LTX2VideoDiffusionDecodePipeline,
|
| 1597 |
+
LTXConditionPipeline,
|
| 1598 |
+
LTXI2VLongMultiPromptPipeline,
|
| 1599 |
+
LTXImageToVideoPipeline,
|
| 1600 |
+
LTXLatentUpsamplePipeline,
|
| 1601 |
+
LTXPipeline,
|
| 1602 |
+
LucyEditPipeline,
|
| 1603 |
+
Lumina2Pipeline,
|
| 1604 |
+
Lumina2Text2ImgPipeline,
|
| 1605 |
+
LuminaPipeline,
|
| 1606 |
+
LuminaText2ImgPipeline,
|
| 1607 |
+
MarigoldDepthPipeline,
|
| 1608 |
+
MarigoldIntrinsicsPipeline,
|
| 1609 |
+
MarigoldNormalsPipeline,
|
| 1610 |
+
MochiPipeline,
|
| 1611 |
+
MotifVideoImage2VideoPipeline,
|
| 1612 |
+
MotifVideoPipeline,
|
| 1613 |
+
MotifVideoPipelineOutput,
|
| 1614 |
+
MusicLDMPipeline,
|
| 1615 |
+
NucleusMoEImagePipeline,
|
| 1616 |
+
OmniGenPipeline,
|
| 1617 |
+
OvisImagePipeline,
|
| 1618 |
+
PaintByExamplePipeline,
|
| 1619 |
+
PIAPipeline,
|
| 1620 |
+
PixArtAlphaPipeline,
|
| 1621 |
+
PixArtSigmaPAGPipeline,
|
| 1622 |
+
PixArtSigmaPipeline,
|
| 1623 |
+
PRXPipeline,
|
| 1624 |
+
PRXPixelPipeline,
|
| 1625 |
+
QwenImageControlNetInpaintPipeline,
|
| 1626 |
+
QwenImageControlNetPipeline,
|
| 1627 |
+
QwenImageEditInpaintPipeline,
|
| 1628 |
+
QwenImageEditPipeline,
|
| 1629 |
+
QwenImageEditPlusPipeline,
|
| 1630 |
+
QwenImageImg2ImgPipeline,
|
| 1631 |
+
QwenImageInpaintPipeline,
|
| 1632 |
+
QwenImageLayeredPipeline,
|
| 1633 |
+
QwenImagePipeline,
|
| 1634 |
+
ReduxImageEncoder,
|
| 1635 |
+
SanaControlNetPipeline,
|
| 1636 |
+
SanaImageToVideoPipeline,
|
| 1637 |
+
SanaPAGPipeline,
|
| 1638 |
+
SanaPipeline,
|
| 1639 |
+
SanaSprintImg2ImgPipeline,
|
| 1640 |
+
SanaSprintPipeline,
|
| 1641 |
+
SanaVideoPipeline,
|
| 1642 |
+
SemanticStableDiffusionPipeline,
|
| 1643 |
+
ShapEImg2ImgPipeline,
|
| 1644 |
+
ShapEPipeline,
|
| 1645 |
+
SkyReelsV2DiffusionForcingImageToVideoPipeline,
|
| 1646 |
+
SkyReelsV2DiffusionForcingPipeline,
|
| 1647 |
+
SkyReelsV2DiffusionForcingVideoToVideoPipeline,
|
| 1648 |
+
SkyReelsV2ImageToVideoPipeline,
|
| 1649 |
+
SkyReelsV2Pipeline,
|
| 1650 |
+
StableAudio3AudioToAudioPipeline,
|
| 1651 |
+
StableAudio3DurationEmbedder,
|
| 1652 |
+
StableAudio3InpaintPipeline,
|
| 1653 |
+
StableAudio3Pipeline,
|
| 1654 |
+
StableAudioPipeline,
|
| 1655 |
+
StableAudioProjectionModel,
|
| 1656 |
+
StableCascadeCombinedPipeline,
|
| 1657 |
+
StableCascadeDecoderPipeline,
|
| 1658 |
+
StableCascadePriorPipeline,
|
| 1659 |
+
StableDiffusion3ControlNetInpaintingPipeline,
|
| 1660 |
+
StableDiffusion3ControlNetPipeline,
|
| 1661 |
+
StableDiffusion3Img2ImgPipeline,
|
| 1662 |
+
StableDiffusion3InpaintPipeline,
|
| 1663 |
+
StableDiffusion3PAGImg2ImgPipeline,
|
| 1664 |
+
StableDiffusion3PAGPipeline,
|
| 1665 |
+
StableDiffusion3Pipeline,
|
| 1666 |
+
StableDiffusionAdapterPipeline,
|
| 1667 |
+
StableDiffusionAttendAndExcitePipeline,
|
| 1668 |
+
StableDiffusionControlNetImg2ImgPipeline,
|
| 1669 |
+
StableDiffusionControlNetInpaintPipeline,
|
| 1670 |
+
StableDiffusionControlNetPAGInpaintPipeline,
|
| 1671 |
+
StableDiffusionControlNetPAGPipeline,
|
| 1672 |
+
StableDiffusionControlNetPipeline,
|
| 1673 |
+
StableDiffusionControlNetXSPipeline,
|
| 1674 |
+
StableDiffusionDepth2ImgPipeline,
|
| 1675 |
+
StableDiffusionDiffEditPipeline,
|
| 1676 |
+
StableDiffusionGLIGENPipeline,
|
| 1677 |
+
StableDiffusionGLIGENTextImagePipeline,
|
| 1678 |
+
StableDiffusionImageVariationPipeline,
|
| 1679 |
+
StableDiffusionImg2ImgPipeline,
|
| 1680 |
+
StableDiffusionInpaintPipeline,
|
| 1681 |
+
StableDiffusionInpaintPipelineLegacy,
|
| 1682 |
+
StableDiffusionInstructPix2PixPipeline,
|
| 1683 |
+
StableDiffusionLatentUpscalePipeline,
|
| 1684 |
+
StableDiffusionLDM3DPipeline,
|
| 1685 |
+
StableDiffusionModelEditingPipeline,
|
| 1686 |
+
StableDiffusionPAGImg2ImgPipeline,
|
| 1687 |
+
StableDiffusionPAGInpaintPipeline,
|
| 1688 |
+
StableDiffusionPAGPipeline,
|
| 1689 |
+
StableDiffusionPanoramaPipeline,
|
| 1690 |
+
StableDiffusionParadigmsPipeline,
|
| 1691 |
+
StableDiffusionPipeline,
|
| 1692 |
+
StableDiffusionPipelineSafe,
|
| 1693 |
+
StableDiffusionPix2PixZeroPipeline,
|
| 1694 |
+
StableDiffusionSAGPipeline,
|
| 1695 |
+
StableDiffusionUpscalePipeline,
|
| 1696 |
+
StableDiffusionXLAdapterPipeline,
|
| 1697 |
+
StableDiffusionXLControlNetImg2ImgPipeline,
|
| 1698 |
+
StableDiffusionXLControlNetInpaintPipeline,
|
| 1699 |
+
StableDiffusionXLControlNetPAGImg2ImgPipeline,
|
| 1700 |
+
StableDiffusionXLControlNetPAGPipeline,
|
| 1701 |
+
StableDiffusionXLControlNetPipeline,
|
| 1702 |
+
StableDiffusionXLControlNetUnionImg2ImgPipeline,
|
| 1703 |
+
StableDiffusionXLControlNetUnionInpaintPipeline,
|
| 1704 |
+
StableDiffusionXLControlNetUnionPipeline,
|
| 1705 |
+
StableDiffusionXLControlNetXSPipeline,
|
| 1706 |
+
StableDiffusionXLImg2ImgPipeline,
|
| 1707 |
+
StableDiffusionXLInpaintPipeline,
|
| 1708 |
+
StableDiffusionXLInstructPix2PixPipeline,
|
| 1709 |
+
StableDiffusionXLPAGImg2ImgPipeline,
|
| 1710 |
+
StableDiffusionXLPAGInpaintPipeline,
|
| 1711 |
+
StableDiffusionXLPAGPipeline,
|
| 1712 |
+
StableDiffusionXLPipeline,
|
| 1713 |
+
StableUnCLIPImg2ImgPipeline,
|
| 1714 |
+
StableUnCLIPPipeline,
|
| 1715 |
+
StableVideoDiffusionPipeline,
|
| 1716 |
+
TextToVideoSDPipeline,
|
| 1717 |
+
TextToVideoZeroPipeline,
|
| 1718 |
+
TextToVideoZeroSDXLPipeline,
|
| 1719 |
+
UnCLIPImageVariationPipeline,
|
| 1720 |
+
UnCLIPPipeline,
|
| 1721 |
+
UniDiffuserModel,
|
| 1722 |
+
UniDiffuserPipeline,
|
| 1723 |
+
UniDiffuserTextDecoder,
|
| 1724 |
+
VersatileDiffusionDualGuidedPipeline,
|
| 1725 |
+
VersatileDiffusionImageVariationPipeline,
|
| 1726 |
+
VersatileDiffusionPipeline,
|
| 1727 |
+
VersatileDiffusionTextToImagePipeline,
|
| 1728 |
+
VideoToVideoSDPipeline,
|
| 1729 |
+
VisualClozeGenerationPipeline,
|
| 1730 |
+
VisualClozePipeline,
|
| 1731 |
+
VQDiffusionPipeline,
|
| 1732 |
+
WanAnimatePipeline,
|
| 1733 |
+
WanImageToVideoPipeline,
|
| 1734 |
+
WanPipeline,
|
| 1735 |
+
WanVACEPipeline,
|
| 1736 |
+
WanVideoToVideoPipeline,
|
| 1737 |
+
WuerstchenCombinedPipeline,
|
| 1738 |
+
WuerstchenDecoderPipeline,
|
| 1739 |
+
WuerstchenPriorPipeline,
|
| 1740 |
+
ZImageControlNetInpaintPipeline,
|
| 1741 |
+
ZImageControlNetPipeline,
|
| 1742 |
+
ZImageImg2ImgPipeline,
|
| 1743 |
+
ZImageInpaintPipeline,
|
| 1744 |
+
ZImageOmniPipeline,
|
| 1745 |
+
ZImagePipeline,
|
| 1746 |
+
)
|
| 1747 |
+
|
| 1748 |
+
try:
|
| 1749 |
+
if not (is_torch_available() and is_transformers_available() and is_sentencepiece_available()):
|
| 1750 |
+
raise OptionalDependencyNotAvailable()
|
| 1751 |
+
except OptionalDependencyNotAvailable:
|
| 1752 |
+
from .utils.dummy_torch_and_transformers_and_sentencepiece_objects import * # noqa F403
|
| 1753 |
+
else:
|
| 1754 |
+
from .pipelines import KolorsImg2ImgPipeline, KolorsPAGPipeline, KolorsPipeline
|
| 1755 |
+
|
| 1756 |
+
try:
|
| 1757 |
+
if not (is_torch_available() and is_transformers_available() and is_opencv_available()):
|
| 1758 |
+
raise OptionalDependencyNotAvailable()
|
| 1759 |
+
except OptionalDependencyNotAvailable:
|
| 1760 |
+
from .utils.dummy_torch_and_transformers_and_opencv_objects import * # noqa F403
|
| 1761 |
+
else:
|
| 1762 |
+
from .pipelines import ConsisIDPipeline
|
| 1763 |
+
|
| 1764 |
+
try:
|
| 1765 |
+
if not (is_torch_available() and is_transformers_available() and is_onnx_available()):
|
| 1766 |
+
raise OptionalDependencyNotAvailable()
|
| 1767 |
+
except OptionalDependencyNotAvailable:
|
| 1768 |
+
from .utils.dummy_torch_and_transformers_and_onnx_objects import * # noqa F403
|
| 1769 |
+
else:
|
| 1770 |
+
from .pipelines import (
|
| 1771 |
+
OnnxStableDiffusionImg2ImgPipeline,
|
| 1772 |
+
OnnxStableDiffusionInpaintPipeline,
|
| 1773 |
+
OnnxStableDiffusionInpaintPipelineLegacy,
|
| 1774 |
+
OnnxStableDiffusionPipeline,
|
| 1775 |
+
OnnxStableDiffusionUpscalePipeline,
|
| 1776 |
+
StableDiffusionOnnxPipeline,
|
| 1777 |
+
)
|
| 1778 |
+
|
| 1779 |
+
try:
|
| 1780 |
+
if not (is_torch_available() and is_librosa_available()):
|
| 1781 |
+
raise OptionalDependencyNotAvailable()
|
| 1782 |
+
except OptionalDependencyNotAvailable:
|
| 1783 |
+
from .utils.dummy_torch_and_librosa_objects import * # noqa F403
|
| 1784 |
+
else:
|
| 1785 |
+
from .pipelines import AudioDiffusionPipeline, Mel
|
| 1786 |
+
|
| 1787 |
+
try:
|
| 1788 |
+
if not (is_transformers_available() and is_torch_available() and is_note_seq_available()):
|
| 1789 |
+
raise OptionalDependencyNotAvailable()
|
| 1790 |
+
except OptionalDependencyNotAvailable:
|
| 1791 |
+
from .utils.dummy_transformers_and_torch_and_note_seq_objects import * # noqa F403
|
| 1792 |
+
else:
|
| 1793 |
+
from .pipelines import SpectrogramDiffusionPipeline
|
| 1794 |
+
|
| 1795 |
+
try:
|
| 1796 |
+
if not (is_note_seq_available()):
|
| 1797 |
+
raise OptionalDependencyNotAvailable()
|
| 1798 |
+
except OptionalDependencyNotAvailable:
|
| 1799 |
+
from .utils.dummy_note_seq_objects import * # noqa F403
|
| 1800 |
+
else:
|
| 1801 |
+
from .pipelines import MidiProcessor
|
| 1802 |
+
|
| 1803 |
+
else:
|
| 1804 |
+
import sys
|
| 1805 |
+
|
| 1806 |
+
sys.modules[__name__] = _LazyModule(
|
| 1807 |
+
__name__,
|
| 1808 |
+
globals()["__file__"],
|
| 1809 |
+
_import_structure,
|
| 1810 |
+
module_spec=__spec__,
|
| 1811 |
+
extra_objects={"__version__": __version__},
|
| 1812 |
+
)
|
diffusers/callbacks.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any
|
| 2 |
+
|
| 3 |
+
from .configuration_utils import ConfigMixin, register_to_config
|
| 4 |
+
from .utils import CONFIG_NAME
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class PipelineCallback(ConfigMixin):
|
| 8 |
+
"""
|
| 9 |
+
Base class for all the official callbacks used in a pipeline. This class provides a structure for implementing
|
| 10 |
+
custom callbacks and ensures that all callbacks have a consistent interface.
|
| 11 |
+
|
| 12 |
+
Please implement the following:
|
| 13 |
+
`tensor_inputs`: This should return a list of tensor inputs specific to your callback. You will only be able to
|
| 14 |
+
include
|
| 15 |
+
variables listed in the `._callback_tensor_inputs` attribute of your pipeline class.
|
| 16 |
+
`callback_fn`: This method defines the core functionality of your callback.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
config_name = CONFIG_NAME
|
| 20 |
+
|
| 21 |
+
@register_to_config
|
| 22 |
+
def __init__(self, cutoff_step_ratio=1.0, cutoff_step_index=None):
|
| 23 |
+
super().__init__()
|
| 24 |
+
|
| 25 |
+
if (cutoff_step_ratio is None and cutoff_step_index is None) or (
|
| 26 |
+
cutoff_step_ratio is not None and cutoff_step_index is not None
|
| 27 |
+
):
|
| 28 |
+
raise ValueError("Either cutoff_step_ratio or cutoff_step_index should be provided, not both or none.")
|
| 29 |
+
|
| 30 |
+
if cutoff_step_ratio is not None and (
|
| 31 |
+
not isinstance(cutoff_step_ratio, float) or not (0.0 <= cutoff_step_ratio <= 1.0)
|
| 32 |
+
):
|
| 33 |
+
raise ValueError("cutoff_step_ratio must be a float between 0.0 and 1.0.")
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def tensor_inputs(self) -> list[str]:
|
| 37 |
+
raise NotImplementedError(f"You need to set the attribute `tensor_inputs` for {self.__class__}")
|
| 38 |
+
|
| 39 |
+
def callback_fn(self, pipeline, step_index, timesteps, callback_kwargs) -> dict[str, Any]:
|
| 40 |
+
raise NotImplementedError(f"You need to implement the method `callback_fn` for {self.__class__}")
|
| 41 |
+
|
| 42 |
+
def __call__(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
|
| 43 |
+
return self.callback_fn(pipeline, step_index, timestep, callback_kwargs)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class MultiPipelineCallbacks:
|
| 47 |
+
"""
|
| 48 |
+
This class is designed to handle multiple pipeline callbacks. It accepts a list of PipelineCallback objects and
|
| 49 |
+
provides a unified interface for calling all of them.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self, callbacks: list[PipelineCallback]):
|
| 53 |
+
self.callbacks = callbacks
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def tensor_inputs(self) -> list[str]:
|
| 57 |
+
return [input for callback in self.callbacks for input in callback.tensor_inputs]
|
| 58 |
+
|
| 59 |
+
def __call__(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
|
| 60 |
+
"""
|
| 61 |
+
Calls all the callbacks in order with the given arguments and returns the final callback_kwargs.
|
| 62 |
+
"""
|
| 63 |
+
for callback in self.callbacks:
|
| 64 |
+
callback_kwargs = callback(pipeline, step_index, timestep, callback_kwargs)
|
| 65 |
+
|
| 66 |
+
return callback_kwargs
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class SDCFGCutoffCallback(PipelineCallback):
|
| 70 |
+
"""
|
| 71 |
+
Callback function for Stable Diffusion Pipelines. After certain number of steps (set by `cutoff_step_ratio` or
|
| 72 |
+
`cutoff_step_index`), this callback will disable the CFG.
|
| 73 |
+
|
| 74 |
+
Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
tensor_inputs = ["prompt_embeds"]
|
| 78 |
+
|
| 79 |
+
def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
|
| 80 |
+
cutoff_step_ratio = self.config.cutoff_step_ratio
|
| 81 |
+
cutoff_step_index = self.config.cutoff_step_index
|
| 82 |
+
|
| 83 |
+
# Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
|
| 84 |
+
cutoff_step = (
|
| 85 |
+
cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
if step_index == cutoff_step:
|
| 89 |
+
prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
|
| 90 |
+
prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
|
| 91 |
+
|
| 92 |
+
pipeline._guidance_scale = 0.0
|
| 93 |
+
|
| 94 |
+
callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
|
| 95 |
+
return callback_kwargs
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class SDXLCFGCutoffCallback(PipelineCallback):
|
| 99 |
+
"""
|
| 100 |
+
Callback function for the base Stable Diffusion XL Pipelines. After certain number of steps (set by
|
| 101 |
+
`cutoff_step_ratio` or `cutoff_step_index`), this callback will disable the CFG.
|
| 102 |
+
|
| 103 |
+
Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
tensor_inputs = [
|
| 107 |
+
"prompt_embeds",
|
| 108 |
+
"add_text_embeds",
|
| 109 |
+
"add_time_ids",
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
|
| 113 |
+
cutoff_step_ratio = self.config.cutoff_step_ratio
|
| 114 |
+
cutoff_step_index = self.config.cutoff_step_index
|
| 115 |
+
|
| 116 |
+
# Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
|
| 117 |
+
cutoff_step = (
|
| 118 |
+
cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
if step_index == cutoff_step:
|
| 122 |
+
prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
|
| 123 |
+
prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
|
| 124 |
+
|
| 125 |
+
add_text_embeds = callback_kwargs[self.tensor_inputs[1]]
|
| 126 |
+
add_text_embeds = add_text_embeds[-1:] # "-1" denotes the embeddings for conditional pooled text tokens
|
| 127 |
+
|
| 128 |
+
add_time_ids = callback_kwargs[self.tensor_inputs[2]]
|
| 129 |
+
add_time_ids = add_time_ids[-1:] # "-1" denotes the embeddings for conditional added time vector
|
| 130 |
+
|
| 131 |
+
pipeline._guidance_scale = 0.0
|
| 132 |
+
|
| 133 |
+
callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
|
| 134 |
+
callback_kwargs[self.tensor_inputs[1]] = add_text_embeds
|
| 135 |
+
callback_kwargs[self.tensor_inputs[2]] = add_time_ids
|
| 136 |
+
|
| 137 |
+
return callback_kwargs
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class SDXLControlnetCFGCutoffCallback(PipelineCallback):
|
| 141 |
+
"""
|
| 142 |
+
Callback function for the Controlnet Stable Diffusion XL Pipelines. After certain number of steps (set by
|
| 143 |
+
`cutoff_step_ratio` or `cutoff_step_index`), this callback will disable the CFG.
|
| 144 |
+
|
| 145 |
+
Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
tensor_inputs = [
|
| 149 |
+
"prompt_embeds",
|
| 150 |
+
"add_text_embeds",
|
| 151 |
+
"add_time_ids",
|
| 152 |
+
"image",
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
|
| 156 |
+
cutoff_step_ratio = self.config.cutoff_step_ratio
|
| 157 |
+
cutoff_step_index = self.config.cutoff_step_index
|
| 158 |
+
|
| 159 |
+
# Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
|
| 160 |
+
cutoff_step = (
|
| 161 |
+
cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
if step_index == cutoff_step:
|
| 165 |
+
prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
|
| 166 |
+
prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
|
| 167 |
+
|
| 168 |
+
add_text_embeds = callback_kwargs[self.tensor_inputs[1]]
|
| 169 |
+
add_text_embeds = add_text_embeds[-1:] # "-1" denotes the embeddings for conditional pooled text tokens
|
| 170 |
+
|
| 171 |
+
add_time_ids = callback_kwargs[self.tensor_inputs[2]]
|
| 172 |
+
add_time_ids = add_time_ids[-1:] # "-1" denotes the embeddings for conditional added time vector
|
| 173 |
+
|
| 174 |
+
# For Controlnet
|
| 175 |
+
image = callback_kwargs[self.tensor_inputs[3]]
|
| 176 |
+
image = image[-1:]
|
| 177 |
+
|
| 178 |
+
pipeline._guidance_scale = 0.0
|
| 179 |
+
|
| 180 |
+
callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
|
| 181 |
+
callback_kwargs[self.tensor_inputs[1]] = add_text_embeds
|
| 182 |
+
callback_kwargs[self.tensor_inputs[2]] = add_time_ids
|
| 183 |
+
callback_kwargs[self.tensor_inputs[3]] = image
|
| 184 |
+
|
| 185 |
+
return callback_kwargs
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class IPAdapterScaleCutoffCallback(PipelineCallback):
|
| 189 |
+
"""
|
| 190 |
+
Callback function for any pipeline that inherits `IPAdapterMixin`. After certain number of steps (set by
|
| 191 |
+
`cutoff_step_ratio` or `cutoff_step_index`), this callback will set the IP Adapter scale to `0.0`.
|
| 192 |
+
|
| 193 |
+
Note: This callback mutates the IP Adapter attention processors by setting the scale to 0.0 after the cutoff step.
|
| 194 |
+
"""
|
| 195 |
+
|
| 196 |
+
tensor_inputs = []
|
| 197 |
+
|
| 198 |
+
def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
|
| 199 |
+
cutoff_step_ratio = self.config.cutoff_step_ratio
|
| 200 |
+
cutoff_step_index = self.config.cutoff_step_index
|
| 201 |
+
|
| 202 |
+
# Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
|
| 203 |
+
cutoff_step = (
|
| 204 |
+
cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
if step_index == cutoff_step:
|
| 208 |
+
pipeline.set_ip_adapter_scale(0.0)
|
| 209 |
+
return callback_kwargs
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class SD3CFGCutoffCallback(PipelineCallback):
|
| 213 |
+
"""
|
| 214 |
+
Callback function for Stable Diffusion 3 Pipelines. After certain number of steps (set by `cutoff_step_ratio` or
|
| 215 |
+
`cutoff_step_index`), this callback will disable the CFG.
|
| 216 |
+
|
| 217 |
+
Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
tensor_inputs = ["prompt_embeds", "pooled_prompt_embeds"]
|
| 221 |
+
|
| 222 |
+
def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
|
| 223 |
+
cutoff_step_ratio = self.config.cutoff_step_ratio
|
| 224 |
+
cutoff_step_index = self.config.cutoff_step_index
|
| 225 |
+
|
| 226 |
+
# Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
|
| 227 |
+
cutoff_step = (
|
| 228 |
+
cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
if step_index == cutoff_step:
|
| 232 |
+
prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
|
| 233 |
+
prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
|
| 234 |
+
|
| 235 |
+
pooled_prompt_embeds = callback_kwargs[self.tensor_inputs[1]]
|
| 236 |
+
pooled_prompt_embeds = pooled_prompt_embeds[
|
| 237 |
+
-1:
|
| 238 |
+
] # "-1" denotes the embeddings for conditional pooled text tokens.
|
| 239 |
+
|
| 240 |
+
pipeline._guidance_scale = 0.0
|
| 241 |
+
|
| 242 |
+
callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
|
| 243 |
+
callback_kwargs[self.tensor_inputs[1]] = pooled_prompt_embeds
|
| 244 |
+
return callback_kwargs
|
diffusers/commands/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from abc import ABC, abstractmethod
|
| 16 |
+
from argparse import ArgumentParser
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class BaseDiffusersCLICommand(ABC):
|
| 20 |
+
@staticmethod
|
| 21 |
+
@abstractmethod
|
| 22 |
+
def register_subcommand(parser: ArgumentParser):
|
| 23 |
+
raise NotImplementedError()
|
| 24 |
+
|
| 25 |
+
@abstractmethod
|
| 26 |
+
def run(self):
|
| 27 |
+
raise NotImplementedError()
|
diffusers/commands/custom_blocks.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""`diffusers-cli custom_blocks` — package a local `ModularPipelineBlocks` subclass for the Hub.
|
| 16 |
+
|
| 17 |
+
Parses `block.py` (or `--block_module_name`), instantiates the chosen block, and calls `save_pretrained` in the current
|
| 18 |
+
working directory.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import ast
|
| 22 |
+
import importlib.util
|
| 23 |
+
import os
|
| 24 |
+
from argparse import ArgumentParser, Namespace
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
from ..utils import logging
|
| 28 |
+
from . import BaseDiffusersCLICommand
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
EXPECTED_PARENT_CLASSES = ["ModularPipelineBlocks"]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def conversion_command_factory(args: Namespace):
|
| 35 |
+
return CustomBlocksCommand(args.block_module_name, args.block_class_name)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class CustomBlocksCommand(BaseDiffusersCLICommand):
|
| 39 |
+
@staticmethod
|
| 40 |
+
def register_subcommand(parser: ArgumentParser):
|
| 41 |
+
from argparse import RawDescriptionHelpFormatter
|
| 42 |
+
|
| 43 |
+
epilog = (
|
| 44 |
+
"Examples\n"
|
| 45 |
+
" $ diffusers-cli custom_blocks\n"
|
| 46 |
+
" $ diffusers-cli custom_blocks --block_module_name my_block.py\n"
|
| 47 |
+
" $ diffusers-cli custom_blocks --block_module_name my_block.py --block_class_name MyDenoiseBlock\n"
|
| 48 |
+
"\n"
|
| 49 |
+
"Learn more\n"
|
| 50 |
+
" Use `diffusers-cli <command> --help` for more information about a command.\n"
|
| 51 |
+
" Read the documentation at https://huggingface.co/docs/diffusers\n"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
conversion_parser = parser.add_parser(
|
| 55 |
+
"custom_blocks",
|
| 56 |
+
help="Package a local ModularPipelineBlocks subclass for the Hub.",
|
| 57 |
+
usage="\n diffusers-cli custom_blocks [options]",
|
| 58 |
+
epilog=epilog,
|
| 59 |
+
formatter_class=RawDescriptionHelpFormatter,
|
| 60 |
+
)
|
| 61 |
+
conversion_parser._optionals.title = "Options"
|
| 62 |
+
conversion_parser.add_argument(
|
| 63 |
+
"--block_module_name",
|
| 64 |
+
type=str,
|
| 65 |
+
default="block.py",
|
| 66 |
+
help="Module filename in which the custom block will be implemented.",
|
| 67 |
+
)
|
| 68 |
+
conversion_parser.add_argument(
|
| 69 |
+
"--block_class_name",
|
| 70 |
+
type=str,
|
| 71 |
+
default=None,
|
| 72 |
+
help="Name of the custom block. If provided None, we will try to infer it.",
|
| 73 |
+
)
|
| 74 |
+
conversion_parser.set_defaults(func=conversion_command_factory)
|
| 75 |
+
|
| 76 |
+
def __init__(self, block_module_name: str = "block.py", block_class_name: str = None):
|
| 77 |
+
self.logger = logging.get_logger("diffusers-cli/custom_blocks")
|
| 78 |
+
self.block_module_name = Path(block_module_name)
|
| 79 |
+
self.block_class_name = block_class_name
|
| 80 |
+
|
| 81 |
+
def run(self):
|
| 82 |
+
# determine the block to be saved.
|
| 83 |
+
out = self._get_class_names(self.block_module_name)
|
| 84 |
+
classes_found = list({cls for cls, _ in out})
|
| 85 |
+
|
| 86 |
+
if self.block_class_name is not None:
|
| 87 |
+
child_class, parent_class = self._choose_block(out, self.block_class_name)
|
| 88 |
+
if child_class is None and parent_class is None:
|
| 89 |
+
raise ValueError(
|
| 90 |
+
"`block_class_name` could not be retrieved. Available classes from "
|
| 91 |
+
f"{self.block_module_name}:\n{classes_found}"
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
self.logger.info(
|
| 95 |
+
f"Found classes: {classes_found} will be using {classes_found[0]}. "
|
| 96 |
+
"If this needs to be changed, re-run the command specifying `block_class_name`."
|
| 97 |
+
)
|
| 98 |
+
child_class, parent_class = out[0][0], out[0][1]
|
| 99 |
+
|
| 100 |
+
# dynamically get the custom block and initialize it to call `save_pretrained` in the current directory.
|
| 101 |
+
# the user is responsible for running it, so I guess that is safe?
|
| 102 |
+
module_name = f"__dynamic__{self.block_module_name.stem}"
|
| 103 |
+
spec = importlib.util.spec_from_file_location(module_name, str(self.block_module_name))
|
| 104 |
+
module = importlib.util.module_from_spec(spec)
|
| 105 |
+
spec.loader.exec_module(module)
|
| 106 |
+
block = getattr(module, child_class)()
|
| 107 |
+
block.save_pretrained(os.getcwd())
|
| 108 |
+
# `ModularPipeline.from_pretrained` (and therefore `diffusers-cli run`) loads a repo
|
| 109 |
+
# through `modular_model_index.json`, which only the pipeline-level save writes — without
|
| 110 |
+
# it the packaged repo is importable as blocks but not runnable as a pipeline.
|
| 111 |
+
block.init_pipeline().save_pretrained(os.getcwd())
|
| 112 |
+
|
| 113 |
+
def _choose_block(self, candidates, chosen=None):
|
| 114 |
+
for cls, base in candidates:
|
| 115 |
+
if cls == chosen:
|
| 116 |
+
return cls, base
|
| 117 |
+
return None, None
|
| 118 |
+
|
| 119 |
+
def _get_class_names(self, file_path):
|
| 120 |
+
source = file_path.read_text(encoding="utf-8")
|
| 121 |
+
try:
|
| 122 |
+
tree = ast.parse(source, filename=file_path)
|
| 123 |
+
except SyntaxError as e:
|
| 124 |
+
raise ValueError(f"Could not parse {file_path!r}: {e}") from e
|
| 125 |
+
|
| 126 |
+
results: list[tuple[str, str]] = []
|
| 127 |
+
for node in tree.body:
|
| 128 |
+
if not isinstance(node, ast.ClassDef):
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
base_names = [bname for b in node.bases if (bname := self._get_base_name(b)) is not None]
|
| 132 |
+
|
| 133 |
+
for allowed in EXPECTED_PARENT_CLASSES:
|
| 134 |
+
if allowed in base_names:
|
| 135 |
+
results.append((node.name, allowed))
|
| 136 |
+
|
| 137 |
+
return results
|
| 138 |
+
|
| 139 |
+
def _get_base_name(self, node: ast.expr):
|
| 140 |
+
if isinstance(node, ast.Name):
|
| 141 |
+
return node.id
|
| 142 |
+
elif isinstance(node, ast.Attribute):
|
| 143 |
+
val = self._get_base_name(node.value)
|
| 144 |
+
return f"{val}.{node.attr}" if val else node.attr
|
| 145 |
+
return None
|
diffusers/commands/diffusers_cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
from argparse import ArgumentParser
|
| 17 |
+
|
| 18 |
+
from huggingface_hub.cli._output import OutputFormat, out
|
| 19 |
+
|
| 20 |
+
from .custom_blocks import CustomBlocksCommand
|
| 21 |
+
from .env import EnvironmentCommand
|
| 22 |
+
from .fp16_safetensors import FP16SafetensorsCommand
|
| 23 |
+
from .run import RunCommand
|
| 24 |
+
from .schema import SchemaCommand
|
| 25 |
+
from .skills import SkillsCommand
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def main():
|
| 29 |
+
parser = ArgumentParser(
|
| 30 |
+
prog="diffusers-cli",
|
| 31 |
+
usage="\n diffusers-cli [--format <fmt>] <command> [options]",
|
| 32 |
+
)
|
| 33 |
+
parser._optionals.title = "Options"
|
| 34 |
+
parser.add_argument(
|
| 35 |
+
"--format",
|
| 36 |
+
choices=[m.value for m in OutputFormat],
|
| 37 |
+
default=OutputFormat.auto.value,
|
| 38 |
+
help=(
|
| 39 |
+
"Output format. 'auto' (default) picks 'agent' when an AI coding agent is detected "
|
| 40 |
+
"(via CLAUDECODE/CURSOR_AI/AIDER_AI_CONTEXT/... env vars) and 'human' otherwise. "
|
| 41 |
+
"Must appear before the subcommand."
|
| 42 |
+
),
|
| 43 |
+
)
|
| 44 |
+
commands_parser = parser.add_subparsers(title="Commands", metavar="<command>")
|
| 45 |
+
|
| 46 |
+
# Register commands
|
| 47 |
+
EnvironmentCommand.register_subcommand(commands_parser)
|
| 48 |
+
FP16SafetensorsCommand.register_subcommand(commands_parser)
|
| 49 |
+
CustomBlocksCommand.register_subcommand(commands_parser)
|
| 50 |
+
RunCommand.register_subcommand(commands_parser)
|
| 51 |
+
SchemaCommand.register_subcommand(commands_parser)
|
| 52 |
+
SkillsCommand.register_subcommand(commands_parser)
|
| 53 |
+
|
| 54 |
+
# Let's go
|
| 55 |
+
args = parser.parse_args()
|
| 56 |
+
|
| 57 |
+
out.set_mode(OutputFormat(args.format))
|
| 58 |
+
|
| 59 |
+
if not hasattr(args, "func"):
|
| 60 |
+
parser.print_help()
|
| 61 |
+
exit(1)
|
| 62 |
+
|
| 63 |
+
# Run
|
| 64 |
+
service = args.func(args)
|
| 65 |
+
service.run()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
diffusers/commands/env.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import importlib.metadata
|
| 16 |
+
import platform
|
| 17 |
+
import subprocess
|
| 18 |
+
from argparse import ArgumentParser
|
| 19 |
+
|
| 20 |
+
import huggingface_hub
|
| 21 |
+
|
| 22 |
+
from .. import __version__ as version
|
| 23 |
+
from ..utils import (
|
| 24 |
+
is_accelerate_available,
|
| 25 |
+
is_bitsandbytes_available,
|
| 26 |
+
is_gguf_available,
|
| 27 |
+
is_google_colab,
|
| 28 |
+
is_nvidia_modelopt_available,
|
| 29 |
+
is_optimum_quanto_available,
|
| 30 |
+
is_peft_available,
|
| 31 |
+
is_safetensors_available,
|
| 32 |
+
is_torch_available,
|
| 33 |
+
is_torchao_available,
|
| 34 |
+
is_transformers_available,
|
| 35 |
+
is_xformers_available,
|
| 36 |
+
)
|
| 37 |
+
from . import BaseDiffusersCLICommand
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# (display name, availability_fn, pypi distribution name for importlib.metadata.version)
|
| 41 |
+
_QUANTIZATION_BACKENDS = (
|
| 42 |
+
("bitsandbytes", is_bitsandbytes_available, "bitsandbytes"),
|
| 43 |
+
("gguf", is_gguf_available, "gguf"),
|
| 44 |
+
("optimum-quanto", is_optimum_quanto_available, "optimum-quanto"),
|
| 45 |
+
("torchao", is_torchao_available, "torchao"),
|
| 46 |
+
("nvidia-modelopt", is_nvidia_modelopt_available, "nvidia-modelopt"),
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def info_command_factory(_):
|
| 51 |
+
return EnvironmentCommand()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class EnvironmentCommand(BaseDiffusersCLICommand):
|
| 55 |
+
@staticmethod
|
| 56 |
+
def register_subcommand(parser: ArgumentParser) -> None:
|
| 57 |
+
download_parser = parser.add_parser(
|
| 58 |
+
"env",
|
| 59 |
+
help="Print versions of diffusers and its dependencies (for bug reports).",
|
| 60 |
+
usage="\n diffusers-cli env",
|
| 61 |
+
)
|
| 62 |
+
download_parser._optionals.title = "Options"
|
| 63 |
+
download_parser.set_defaults(func=info_command_factory)
|
| 64 |
+
|
| 65 |
+
def run(self) -> dict:
|
| 66 |
+
hub_version = huggingface_hub.__version__
|
| 67 |
+
|
| 68 |
+
safetensors_version = "not installed"
|
| 69 |
+
if is_safetensors_available():
|
| 70 |
+
import safetensors
|
| 71 |
+
|
| 72 |
+
safetensors_version = safetensors.__version__
|
| 73 |
+
|
| 74 |
+
pt_version = "not installed"
|
| 75 |
+
pt_cuda_available = "NA"
|
| 76 |
+
if is_torch_available():
|
| 77 |
+
import torch
|
| 78 |
+
|
| 79 |
+
pt_version = torch.__version__
|
| 80 |
+
pt_cuda_available = torch.cuda.is_available()
|
| 81 |
+
|
| 82 |
+
transformers_version = "not installed"
|
| 83 |
+
if is_transformers_available():
|
| 84 |
+
import transformers
|
| 85 |
+
|
| 86 |
+
transformers_version = transformers.__version__
|
| 87 |
+
|
| 88 |
+
accelerate_version = "not installed"
|
| 89 |
+
if is_accelerate_available():
|
| 90 |
+
import accelerate
|
| 91 |
+
|
| 92 |
+
accelerate_version = accelerate.__version__
|
| 93 |
+
|
| 94 |
+
peft_version = "not installed"
|
| 95 |
+
if is_peft_available():
|
| 96 |
+
import peft
|
| 97 |
+
|
| 98 |
+
peft_version = peft.__version__
|
| 99 |
+
|
| 100 |
+
quantization_versions = {}
|
| 101 |
+
for backend_name, is_available_fn, dist_name in _QUANTIZATION_BACKENDS:
|
| 102 |
+
if not is_available_fn():
|
| 103 |
+
continue
|
| 104 |
+
try:
|
| 105 |
+
quantization_versions[backend_name] = importlib.metadata.version(dist_name)
|
| 106 |
+
except importlib.metadata.PackageNotFoundError:
|
| 107 |
+
quantization_versions[backend_name] = "N/A"
|
| 108 |
+
|
| 109 |
+
xformers_version = "not installed"
|
| 110 |
+
if is_xformers_available():
|
| 111 |
+
import xformers
|
| 112 |
+
|
| 113 |
+
xformers_version = xformers.__version__
|
| 114 |
+
|
| 115 |
+
platform_info = platform.platform()
|
| 116 |
+
|
| 117 |
+
is_google_colab_str = "Yes" if is_google_colab() else "No"
|
| 118 |
+
|
| 119 |
+
accelerator = "NA"
|
| 120 |
+
if platform.system() in {"Linux", "Windows"}:
|
| 121 |
+
try:
|
| 122 |
+
sp = subprocess.Popen(
|
| 123 |
+
["nvidia-smi", "--query-gpu=gpu_name,memory.total", "--format=csv,noheader"],
|
| 124 |
+
stdout=subprocess.PIPE,
|
| 125 |
+
stderr=subprocess.PIPE,
|
| 126 |
+
)
|
| 127 |
+
out_str, _ = sp.communicate()
|
| 128 |
+
out_str = out_str.decode("utf-8")
|
| 129 |
+
|
| 130 |
+
if len(out_str) > 0:
|
| 131 |
+
accelerator = out_str.strip()
|
| 132 |
+
except FileNotFoundError:
|
| 133 |
+
pass
|
| 134 |
+
elif platform.system() == "Darwin": # Mac OS
|
| 135 |
+
try:
|
| 136 |
+
sp = subprocess.Popen(
|
| 137 |
+
["system_profiler", "SPDisplaysDataType"],
|
| 138 |
+
stdout=subprocess.PIPE,
|
| 139 |
+
stderr=subprocess.PIPE,
|
| 140 |
+
)
|
| 141 |
+
out_str, _ = sp.communicate()
|
| 142 |
+
out_str = out_str.decode("utf-8")
|
| 143 |
+
|
| 144 |
+
start = out_str.find("Chipset Model:")
|
| 145 |
+
if start != -1:
|
| 146 |
+
start += len("Chipset Model:")
|
| 147 |
+
end = out_str.find("\n", start)
|
| 148 |
+
accelerator = out_str[start:end].strip()
|
| 149 |
+
|
| 150 |
+
start = out_str.find("VRAM (Total):")
|
| 151 |
+
if start != -1:
|
| 152 |
+
start += len("VRAM (Total):")
|
| 153 |
+
end = out_str.find("\n", start)
|
| 154 |
+
accelerator += " VRAM: " + out_str[start:end].strip()
|
| 155 |
+
except FileNotFoundError:
|
| 156 |
+
pass
|
| 157 |
+
else:
|
| 158 |
+
print("It seems you are running an unusual OS. Could you fill in the accelerator manually?")
|
| 159 |
+
|
| 160 |
+
info = {
|
| 161 |
+
"🤗 Diffusers version": version,
|
| 162 |
+
"Platform": platform_info,
|
| 163 |
+
"Running on Google Colab?": is_google_colab_str,
|
| 164 |
+
"Python version": platform.python_version(),
|
| 165 |
+
"PyTorch version (GPU?)": f"{pt_version} ({pt_cuda_available})",
|
| 166 |
+
"Huggingface_hub version": hub_version,
|
| 167 |
+
"Transformers version": transformers_version,
|
| 168 |
+
"Accelerate version": accelerate_version,
|
| 169 |
+
"PEFT version": peft_version,
|
| 170 |
+
**{f"{name} version": ver for name, ver in quantization_versions.items()},
|
| 171 |
+
"Safetensors version": safetensors_version,
|
| 172 |
+
"xFormers version": xformers_version,
|
| 173 |
+
"Accelerator": accelerator,
|
| 174 |
+
"Using GPU in script?": "<fill in>",
|
| 175 |
+
"Using distributed or parallel set-up in script?": "<fill in>",
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
print("\nCopy-and-paste the text below in your GitHub issue and FILL OUT the two last points.\n")
|
| 179 |
+
print(self.format_dict(info))
|
| 180 |
+
|
| 181 |
+
return info
|
| 182 |
+
|
| 183 |
+
@staticmethod
|
| 184 |
+
def format_dict(d: dict) -> str:
|
| 185 |
+
return "\n".join([f"- {prop}: {val}" for prop, val in d.items()]) + "\n"
|
diffusers/commands/fp16_safetensors.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Usage example:
|
| 17 |
+
diffusers-cli fp16_safetensors --ckpt_id=openai/shap-e --fp16 --use_safetensors
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import glob
|
| 21 |
+
import json
|
| 22 |
+
import warnings
|
| 23 |
+
from argparse import ArgumentParser, Namespace
|
| 24 |
+
from importlib import import_module
|
| 25 |
+
|
| 26 |
+
import huggingface_hub
|
| 27 |
+
import torch
|
| 28 |
+
from huggingface_hub import hf_hub_download
|
| 29 |
+
from packaging import version
|
| 30 |
+
|
| 31 |
+
from ..utils import logging
|
| 32 |
+
from . import BaseDiffusersCLICommand
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def conversion_command_factory(args: Namespace):
|
| 36 |
+
warnings.warn(
|
| 37 |
+
"`diffusers-cli fp16_safetensors` is deprecated and will be removed in a future version. "
|
| 38 |
+
"Convert weights to fp16 safetensors directly with `safetensors.torch.save_file` or via "
|
| 39 |
+
"`pipeline.save_pretrained(..., safe_serialization=True, variant='fp16')`.",
|
| 40 |
+
FutureWarning,
|
| 41 |
+
stacklevel=2,
|
| 42 |
+
)
|
| 43 |
+
if args.use_auth_token:
|
| 44 |
+
warnings.warn(
|
| 45 |
+
"The `--use_auth_token` flag is deprecated and will be removed in a future version."
|
| 46 |
+
"Authentication is now handled automatically if the user is logged in."
|
| 47 |
+
)
|
| 48 |
+
return FP16SafetensorsCommand(args.ckpt_id, args.fp16, args.use_safetensors)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class FP16SafetensorsCommand(BaseDiffusersCLICommand):
|
| 52 |
+
@staticmethod
|
| 53 |
+
def register_subcommand(parser: ArgumentParser):
|
| 54 |
+
conversion_parser = parser.add_parser(
|
| 55 |
+
"fp16_safetensors",
|
| 56 |
+
help="[DEPRECATED] Convert a Hub checkpoint's weights to fp16 safetensors and push back as a PR.",
|
| 57 |
+
usage="\n diffusers-cli fp16_safetensors [options]",
|
| 58 |
+
)
|
| 59 |
+
conversion_parser._optionals.title = "Options"
|
| 60 |
+
conversion_parser.add_argument(
|
| 61 |
+
"--ckpt_id",
|
| 62 |
+
type=str,
|
| 63 |
+
help="Repo id of the checkpoints on which to run the conversion. Example: 'openai/shap-e'.",
|
| 64 |
+
)
|
| 65 |
+
conversion_parser.add_argument(
|
| 66 |
+
"--fp16", action="store_true", help="If serializing the variables in FP16 precision."
|
| 67 |
+
)
|
| 68 |
+
conversion_parser.add_argument(
|
| 69 |
+
"--use_safetensors", action="store_true", help="If serializing in the safetensors format."
|
| 70 |
+
)
|
| 71 |
+
conversion_parser.add_argument(
|
| 72 |
+
"--use_auth_token",
|
| 73 |
+
action="store_true",
|
| 74 |
+
help="When working with checkpoints having private visibility. When used `hf auth login` needs to be run beforehand.",
|
| 75 |
+
)
|
| 76 |
+
conversion_parser.set_defaults(func=conversion_command_factory)
|
| 77 |
+
|
| 78 |
+
def __init__(self, ckpt_id: str, fp16: bool, use_safetensors: bool):
|
| 79 |
+
self.logger = logging.get_logger("diffusers-cli/fp16_safetensors")
|
| 80 |
+
self.ckpt_id = ckpt_id
|
| 81 |
+
self.local_ckpt_dir = f"/tmp/{ckpt_id}"
|
| 82 |
+
self.fp16 = fp16
|
| 83 |
+
|
| 84 |
+
self.use_safetensors = use_safetensors
|
| 85 |
+
|
| 86 |
+
if not self.use_safetensors and not self.fp16:
|
| 87 |
+
raise NotImplementedError(
|
| 88 |
+
"When `use_safetensors` and `fp16` both are False, then this command is of no use."
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
def run(self):
|
| 92 |
+
if version.parse(huggingface_hub.__version__) < version.parse("0.9.0"):
|
| 93 |
+
raise ImportError(
|
| 94 |
+
"The huggingface_hub version must be >= 0.9.0 to use this command. Please update your huggingface_hub"
|
| 95 |
+
" installation."
|
| 96 |
+
)
|
| 97 |
+
else:
|
| 98 |
+
from huggingface_hub import create_commit
|
| 99 |
+
from huggingface_hub._commit_api import CommitOperationAdd
|
| 100 |
+
|
| 101 |
+
model_index = hf_hub_download(repo_id=self.ckpt_id, filename="model_index.json")
|
| 102 |
+
with open(model_index, "r") as f:
|
| 103 |
+
pipeline_class_name = json.load(f)["_class_name"]
|
| 104 |
+
pipeline_class = getattr(import_module("diffusers"), pipeline_class_name)
|
| 105 |
+
self.logger.info(f"Pipeline class imported: {pipeline_class_name}.")
|
| 106 |
+
|
| 107 |
+
# Load the appropriate pipeline. We could have used `DiffusionPipeline`
|
| 108 |
+
# here, but just to avoid potential edge cases.
|
| 109 |
+
pipeline = pipeline_class.from_pretrained(
|
| 110 |
+
self.ckpt_id, torch_dtype=torch.float16 if self.fp16 else torch.float32
|
| 111 |
+
)
|
| 112 |
+
pipeline.save_pretrained(
|
| 113 |
+
self.local_ckpt_dir,
|
| 114 |
+
safe_serialization=True if self.use_safetensors else False,
|
| 115 |
+
variant="fp16" if self.fp16 else None,
|
| 116 |
+
)
|
| 117 |
+
self.logger.info(f"Pipeline locally saved to {self.local_ckpt_dir}.")
|
| 118 |
+
|
| 119 |
+
# Fetch all the paths.
|
| 120 |
+
if self.fp16:
|
| 121 |
+
modified_paths = glob.glob(f"{self.local_ckpt_dir}/*/*.fp16.*")
|
| 122 |
+
elif self.use_safetensors:
|
| 123 |
+
modified_paths = glob.glob(f"{self.local_ckpt_dir}/*/*.safetensors")
|
| 124 |
+
|
| 125 |
+
# Prepare for the PR.
|
| 126 |
+
commit_message = f"Serialize variables with FP16: {self.fp16} and safetensors: {self.use_safetensors}."
|
| 127 |
+
operations = []
|
| 128 |
+
for path in modified_paths:
|
| 129 |
+
operations.append(CommitOperationAdd(path_in_repo="/".join(path.split("/")[4:]), path_or_fileobj=path))
|
| 130 |
+
|
| 131 |
+
# Open the PR.
|
| 132 |
+
commit_description = (
|
| 133 |
+
"Variables converted by the [`diffusers`' `fp16_safetensors`"
|
| 134 |
+
" CLI](https://github.com/huggingface/diffusers/blob/main/src/diffusers/commands/fp16_safetensors.py)."
|
| 135 |
+
)
|
| 136 |
+
hub_pr_url = create_commit(
|
| 137 |
+
repo_id=self.ckpt_id,
|
| 138 |
+
operations=operations,
|
| 139 |
+
commit_message=commit_message,
|
| 140 |
+
commit_description=commit_description,
|
| 141 |
+
repo_type="model",
|
| 142 |
+
create_pr=True,
|
| 143 |
+
).pr_url
|
| 144 |
+
self.logger.info(f"PR created here: {hub_pr_url}.")
|
diffusers/commands/run.py
ADDED
|
@@ -0,0 +1,1335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""`diffusers-cli run` — single agentic entry point.
|
| 16 |
+
|
| 17 |
+
Runs any diffusers pipeline (standard or modular) by forwarding `--pipeline-kwargs` verbatim, saves the output by
|
| 18 |
+
detecting its runtime type, and can submit the same call to an HF Sandbox via `--remote`.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import io
|
| 24 |
+
import json
|
| 25 |
+
import os
|
| 26 |
+
import shlex
|
| 27 |
+
import sys
|
| 28 |
+
import time
|
| 29 |
+
import uuid
|
| 30 |
+
import wave
|
| 31 |
+
from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter, _SubParsersAction
|
| 32 |
+
from datetime import datetime
|
| 33 |
+
from pathlib import Path
|
| 34 |
+
from typing import Any
|
| 35 |
+
|
| 36 |
+
import numpy as np
|
| 37 |
+
import torch
|
| 38 |
+
from huggingface_hub import HfApi, Sandbox, Volume, get_token, parse_hf_uri
|
| 39 |
+
from huggingface_hub.cli._output import out
|
| 40 |
+
from huggingface_hub.utils import httpx, send_telemetry
|
| 41 |
+
from PIL import Image
|
| 42 |
+
|
| 43 |
+
import diffusers
|
| 44 |
+
from diffusers import ContextParallelConfig
|
| 45 |
+
from diffusers.models.attention_dispatch import _HUB_KERNELS_REGISTRY
|
| 46 |
+
from diffusers.utils import export_to_video, load_image, load_video, logging
|
| 47 |
+
from diffusers.utils.constants import DIFFUSERS_REQUEST_TIMEOUT
|
| 48 |
+
from diffusers.utils.torch_utils import torch_device
|
| 49 |
+
|
| 50 |
+
from . import BaseDiffusersCLICommand
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
logger = logging.get_logger("diffusers-cli/run")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ---------------------------------------------------------------------------
|
| 57 |
+
# Constants
|
| 58 |
+
# ---------------------------------------------------------------------------
|
| 59 |
+
|
| 60 |
+
DEFAULT_OUTPUT_DIR = str(Path.home() / ".diffusers" / "cli" / "run" / "outputs")
|
| 61 |
+
DTYPE_CHOICES = ("auto", "float16", "fp16", "bfloat16", "bf16", "float32", "fp32")
|
| 62 |
+
CPU_OFFLOAD_CHOICES = ("model", "group", "auto")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
ATTENTION_BACKEND_CHOICES = ("default", *sorted(b.value for b in _HUB_KERNELS_REGISTRY))
|
| 66 |
+
|
| 67 |
+
# Kwarg keys whose string value gets auto-loaded before being passed to the pipeline call.
|
| 68 |
+
# Images resolve via `diffusers.utils.load_image` → PIL.Image.Image; videos resolve via
|
| 69 |
+
# `diffusers.utils.load_video` → list[PIL.Image.Image].
|
| 70 |
+
_IMAGE_INPUT_KEYS = (
|
| 71 |
+
"image",
|
| 72 |
+
"last_image",
|
| 73 |
+
"mask_image",
|
| 74 |
+
"control_image",
|
| 75 |
+
"ip_adapter_image",
|
| 76 |
+
"image_2",
|
| 77 |
+
)
|
| 78 |
+
_VIDEO_INPUT_KEYS = (
|
| 79 |
+
"video",
|
| 80 |
+
"control_video",
|
| 81 |
+
)
|
| 82 |
+
_AUDIO_INPUT_KEYS = (
|
| 83 |
+
"initial_audio_waveforms",
|
| 84 |
+
"reference_audio",
|
| 85 |
+
"src_audio",
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Pipeline attribute prefixes that identify a denoiser submodule. Matches base names
|
| 89 |
+
# (`transformer`, `unet`) and their numbered variants (`transformer_2`, etc.).
|
| 90 |
+
_DENOISER_COMPONENT_KEYS = ("transformer", "unet")
|
| 91 |
+
|
| 92 |
+
_DEFAULT_REMOTE_DEPS = (
|
| 93 |
+
"diffusers",
|
| 94 |
+
"accelerate",
|
| 95 |
+
"transformers",
|
| 96 |
+
"safetensors",
|
| 97 |
+
"sentencepiece", # required by several text-encoder tokenizers (T5, LLaMA, …)
|
| 98 |
+
"ftfy", # required by older CLIP text-encoder paths
|
| 99 |
+
"peft", # required by `load_lora_weights` when `--lora` is passed
|
| 100 |
+
"imageio", # preferred `export_to_video` backend
|
| 101 |
+
"imageio-ffmpeg", # bundles a static ffmpeg; the cv2 fallback needs system libs the slim image lacks
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Base sandbox image — provides torch + CUDA so `uv pip install --system`
|
| 105 |
+
# only has to add the small Python deps. cuda12.8 is the highest cuda12.x tag
|
| 106 |
+
# below the HF Jobs host driver's CUDA 12.9 max.
|
| 107 |
+
_DEFAULT_REMOTE_IMAGE = "pytorch/pytorch:2.10.0-cuda12.8-cudnn9-runtime"
|
| 108 |
+
|
| 109 |
+
# Installed console-script name invoked inside the sandbox after the deps land.
|
| 110 |
+
_CONTAINER_CLI_BINARY = "diffusers-cli"
|
| 111 |
+
|
| 112 |
+
# Working directories inside the sandbox: local media from `--pipeline-kwargs` is uploaded
|
| 113 |
+
# under _SANDBOX_INPUTS_DIR, and the sandbox CLI is told to write its outputs under
|
| 114 |
+
# _SANDBOX_OUTPUTS_DIR so we can download them back afterwards.
|
| 115 |
+
_SANDBOX_INPUTS_DIR = "/tmp/diffusers-cli/inputs"
|
| 116 |
+
_SANDBOX_OUTPUTS_DIR = "/tmp/diffusers-cli/outputs"
|
| 117 |
+
|
| 118 |
+
RUN_ID_ENV = "DIFFUSERS_CLI_RUN_ID"
|
| 119 |
+
|
| 120 |
+
# Namespace keys that control *how* a remote run is dispatched, not what the sandbox CLI
|
| 121 |
+
# runs. They are stripped when forwarding argv to the sandbox.
|
| 122 |
+
REMOTE_KEYS = frozenset(
|
| 123 |
+
{
|
| 124 |
+
"remote",
|
| 125 |
+
"flavor",
|
| 126 |
+
"timeout",
|
| 127 |
+
"dependencies",
|
| 128 |
+
"namespace",
|
| 129 |
+
"image",
|
| 130 |
+
"keep_alive",
|
| 131 |
+
"sandbox_id",
|
| 132 |
+
"idle_timeout",
|
| 133 |
+
"volume",
|
| 134 |
+
"func",
|
| 135 |
+
"format", # top-level --format is a local rendering flag; never forward to the sandbox
|
| 136 |
+
}
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# ---------------------------------------------------------------------------
|
| 141 |
+
# Argparse helpers
|
| 142 |
+
# ---------------------------------------------------------------------------
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _add_loading_arguments(parser: ArgumentParser) -> None:
|
| 146 |
+
parser.add_argument("--model", "-m", required=True, help="Model id on the Hugging Face Hub or local path.")
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--device-map",
|
| 149 |
+
default=None,
|
| 150 |
+
help=(
|
| 151 |
+
"Component placement. Accepts a torch device string (`cuda`, `cuda:0`, `cpu`, `mps`), "
|
| 152 |
+
"`balanced` for pipeline-level auto-split across visible GPUs, or a JSON dict of "
|
| 153 |
+
'`{"<component>": <device>}` for explicit per-component placement. Auto-detected if omitted.'
|
| 154 |
+
),
|
| 155 |
+
)
|
| 156 |
+
parser.add_argument("--dtype", default="auto", choices=DTYPE_CHOICES, help="Torch dtype for pipeline weights.")
|
| 157 |
+
parser.add_argument("--variant", default=None, help='Optional weight variant (e.g. "fp16").')
|
| 158 |
+
parser.add_argument("--revision", default=None, help="Model revision (branch, tag, or commit SHA).")
|
| 159 |
+
parser.add_argument("--token", default=None, help="Hugging Face token for gated/private models.")
|
| 160 |
+
parser.add_argument("--trust-remote-code", action="store_true", help="Allow custom code from the Hub.")
|
| 161 |
+
parser.add_argument(
|
| 162 |
+
"--lora",
|
| 163 |
+
action="append",
|
| 164 |
+
default=None,
|
| 165 |
+
metavar="JSON",
|
| 166 |
+
help=(
|
| 167 |
+
"JSON dict describing a LoRA adapter to attach after the pipeline loads. Repeat to stack "
|
| 168 |
+
'multiple adapters. Format: \'{"lora_id": "<id>", "lora_scale": <float>}\'. `lora_scale` '
|
| 169 |
+
"defaults to 1.0; `adapter_name` is optional (auto-generated as `lora_<i>` when stacking); "
|
| 170 |
+
"`weight_name` picks the weight file when the repo ships more than one."
|
| 171 |
+
),
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _add_optimization_arguments(parser: ArgumentParser) -> None:
|
| 176 |
+
parser.add_argument(
|
| 177 |
+
"--cpu-offload",
|
| 178 |
+
choices=CPU_OFFLOAD_CHOICES,
|
| 179 |
+
default=None,
|
| 180 |
+
help=(
|
| 181 |
+
"Offload pipeline components to CPU during inference. "
|
| 182 |
+
"'model' uses enable_model_cpu_offload, "
|
| 183 |
+
"'group' uses pipeline.enable_group_offload(leaf_level, use_stream=True). "
|
| 184 |
+
"Modular pipelines only support 'auto', which offloads through a ComponentsManager "
|
| 185 |
+
"via enable_auto_cpu_offload."
|
| 186 |
+
),
|
| 187 |
+
)
|
| 188 |
+
parser.add_argument(
|
| 189 |
+
"--attention-backend",
|
| 190 |
+
choices=ATTENTION_BACKEND_CHOICES,
|
| 191 |
+
default="default",
|
| 192 |
+
help=(
|
| 193 |
+
"Override the attention backend on the transformer/UNet. "
|
| 194 |
+
"Only Hub-hosted kernels are exposed — they auto-download on first use."
|
| 195 |
+
),
|
| 196 |
+
)
|
| 197 |
+
parser.add_argument("--vae-tiling", action="store_true", help="Enable VAE tiling (lower peak VRAM).")
|
| 198 |
+
parser.add_argument("--vae-slicing", action="store_true", help="Enable VAE slicing (lower peak VRAM).")
|
| 199 |
+
parser.add_argument(
|
| 200 |
+
"--context-parallel",
|
| 201 |
+
action="store_true",
|
| 202 |
+
help=(
|
| 203 |
+
"Enable Ulysses-style context parallelism (ulysses_anything mode). "
|
| 204 |
+
"Requires a DiT-based pipeline and launching the CLI under torchrun with ≥2 GPUs."
|
| 205 |
+
),
|
| 206 |
+
)
|
| 207 |
+
parser.add_argument(
|
| 208 |
+
"--compile",
|
| 209 |
+
nargs="?",
|
| 210 |
+
const='{"fullgraph": true}',
|
| 211 |
+
default=None,
|
| 212 |
+
metavar="JSON",
|
| 213 |
+
help=(
|
| 214 |
+
"torch.compile every denoiser submodule on the pipeline. Accepts an optional JSON "
|
| 215 |
+
'object of kwargs forwarded to `torch.compile`, e.g. \'{"mode": "max-autotune", '
|
| 216 |
+
'"fullgraph": true}\'. Bare `--compile` uses `fullgraph=true`. Adds a one-time '
|
| 217 |
+
"compilation cost on the first step but speeds up every subsequent step — worth it "
|
| 218 |
+
"for multi-step generation (50+ steps)."
|
| 219 |
+
),
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _add_output_arguments(parser: ArgumentParser) -> None:
|
| 224 |
+
parser.add_argument(
|
| 225 |
+
"--output",
|
| 226 |
+
"-o",
|
| 227 |
+
default=None,
|
| 228 |
+
help=(
|
| 229 |
+
"Output file or directory. Defaults to "
|
| 230 |
+
"~/.diffusers/cli/run/outputs/diffusers-run-<YYYYMMDDTHHMMSS>-<short-uuid>/<NNNN>.<ext>."
|
| 231 |
+
),
|
| 232 |
+
)
|
| 233 |
+
parser.add_argument(
|
| 234 |
+
"--push-to",
|
| 235 |
+
default=None,
|
| 236 |
+
help=(
|
| 237 |
+
"Upload the generated files to this HF bucket after saving (created if missing). Accepts "
|
| 238 |
+
"an HF bucket id (`<namespace>/<name>`), an `hf://buckets/<namespace>/<name>[/<subpath>]` "
|
| 239 |
+
"URI, or a browser URL for the same — a subpath is used as a folder prefix. Under --remote "
|
| 240 |
+
"the upload runs inside the sandbox; without an explicit --output the bucket becomes the "
|
| 241 |
+
"sole destination and nothing is downloaded back."
|
| 242 |
+
),
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def _add_remote_arguments(parser: ArgumentParser) -> None:
|
| 247 |
+
parser.add_argument(
|
| 248 |
+
"--remote",
|
| 249 |
+
action="store_true",
|
| 250 |
+
help="Run this command in a Hugging Face Sandbox instead of on the local machine.",
|
| 251 |
+
)
|
| 252 |
+
parser.add_argument(
|
| 253 |
+
"--flavor",
|
| 254 |
+
default="a10g-small",
|
| 255 |
+
help="HF Sandbox hardware flavor for --remote (e.g. a10g-small, a100-large, cpu-basic).",
|
| 256 |
+
)
|
| 257 |
+
parser.add_argument(
|
| 258 |
+
"--timeout",
|
| 259 |
+
default="10m",
|
| 260 |
+
help="Max wallclock for the run command inside the sandbox (e.g. 30m, 2h). Defaults to 10m.",
|
| 261 |
+
)
|
| 262 |
+
parser.add_argument(
|
| 263 |
+
"--dependencies",
|
| 264 |
+
action="append",
|
| 265 |
+
default=None,
|
| 266 |
+
help="Extra pip dependencies to install in the sandbox. Repeat to add multiple.",
|
| 267 |
+
)
|
| 268 |
+
parser.add_argument(
|
| 269 |
+
"--namespace",
|
| 270 |
+
default=None,
|
| 271 |
+
help="HF namespace to create the sandbox under (defaults to the current user).",
|
| 272 |
+
)
|
| 273 |
+
parser.add_argument(
|
| 274 |
+
"--image",
|
| 275 |
+
default=None,
|
| 276 |
+
help=(
|
| 277 |
+
"Sandbox image for --remote (defaults to "
|
| 278 |
+
f"{_DEFAULT_REMOTE_IMAGE!r}). Must provide torch + CUDA; the CLI installs the "
|
| 279 |
+
"small Python deps on top via `uv pip install --system`."
|
| 280 |
+
),
|
| 281 |
+
)
|
| 282 |
+
parser.add_argument(
|
| 283 |
+
"--keep-alive",
|
| 284 |
+
action="store_true",
|
| 285 |
+
help=(
|
| 286 |
+
"Don't terminate the sandbox after the run. Its id is printed so a later --remote run "
|
| 287 |
+
"can reconnect with --sandbox-id and reuse the warm deps/weights/compile cache."
|
| 288 |
+
),
|
| 289 |
+
)
|
| 290 |
+
parser.add_argument(
|
| 291 |
+
"--sandbox-id",
|
| 292 |
+
default=None,
|
| 293 |
+
help=(
|
| 294 |
+
"Reconnect to an existing sandbox (from a prior --keep-alive run) instead of creating a new "
|
| 295 |
+
"one, reusing its warm deps/weights/compile cache. Implies --keep-alive; stop it with "
|
| 296 |
+
"`hf sandbox kill <id>`."
|
| 297 |
+
),
|
| 298 |
+
)
|
| 299 |
+
parser.add_argument(
|
| 300 |
+
"--idle-timeout",
|
| 301 |
+
default="10m",
|
| 302 |
+
help=(
|
| 303 |
+
"Auto-shutdown the sandbox after this much inactivity (e.g. 30m, 1h). Defaults to 10m. "
|
| 304 |
+
"Only applied on new sandbox creation — ignored when reconnecting via --sandbox-id."
|
| 305 |
+
),
|
| 306 |
+
)
|
| 307 |
+
parser.add_argument(
|
| 308 |
+
"--volume",
|
| 309 |
+
action="append",
|
| 310 |
+
default=None,
|
| 311 |
+
metavar="BUCKET_ID[:MOUNT_PATH]",
|
| 312 |
+
help=(
|
| 313 |
+
"Mount an HF bucket into the sandbox as a read-write directory. Repeatable. Format: "
|
| 314 |
+
"`<namespace>/<name>` (mounts at `/mnt/buckets/<namespace>/<name>`) or "
|
| 315 |
+
"`<namespace>/<name>:/some/path` for a custom path. Reference mounted files from "
|
| 316 |
+
"--pipeline-kwargs like any other local path. Applied only on new sandbox creation — "
|
| 317 |
+
"ignored when reconnecting via --sandbox-id."
|
| 318 |
+
),
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
# ---------------------------------------------------------------------------
|
| 323 |
+
# Pipeline loading + optimization
|
| 324 |
+
# ---------------------------------------------------------------------------
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def _resolve_dtype(name: str | None):
|
| 328 |
+
if name in (None, "auto"):
|
| 329 |
+
return "auto"
|
| 330 |
+
|
| 331 |
+
mapping = {
|
| 332 |
+
"fp32": torch.float32,
|
| 333 |
+
"float32": torch.float32,
|
| 334 |
+
"fp16": torch.float16,
|
| 335 |
+
"float16": torch.float16,
|
| 336 |
+
"bf16": torch.bfloat16,
|
| 337 |
+
"bfloat16": torch.bfloat16,
|
| 338 |
+
}
|
| 339 |
+
if name not in mapping:
|
| 340 |
+
raise ValueError(f"Unknown dtype: {name}")
|
| 341 |
+
return mapping[name]
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def _resolve_device_map(raw: str | None) -> str | dict:
|
| 345 |
+
"""Parse `--device-map` into a value acceptable by `from_pretrained(device_map=...)`.
|
| 346 |
+
|
| 347 |
+
Returns a JSON dict if the value looks like one, `"balanced"` verbatim, or a single-device string (e.g. `"cuda"`,
|
| 348 |
+
`"cuda:1"`, `"cpu"`, `"mps"`). Auto-detects when `raw is None`, pinning to `cuda:$LOCAL_RANK` under torchrun.
|
| 349 |
+
"""
|
| 350 |
+
if raw is None:
|
| 351 |
+
if torch_device == "cuda":
|
| 352 |
+
local_rank = os.environ.get("LOCAL_RANK")
|
| 353 |
+
if local_rank is not None:
|
| 354 |
+
torch.cuda.set_device(int(local_rank))
|
| 355 |
+
return f"cuda:{local_rank}"
|
| 356 |
+
return torch_device
|
| 357 |
+
|
| 358 |
+
if raw.strip().startswith("{"):
|
| 359 |
+
try:
|
| 360 |
+
parsed = json.loads(raw)
|
| 361 |
+
except json.JSONDecodeError as e:
|
| 362 |
+
raise SystemExit(f"--device-map must be a device string or a JSON dict: {e}") from e
|
| 363 |
+
if not isinstance(parsed, dict):
|
| 364 |
+
raise SystemExit("--device-map JSON must decode to an object.")
|
| 365 |
+
return parsed
|
| 366 |
+
|
| 367 |
+
return raw
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _apply_cpu_offload(pipeline: Any, mode: str, device_map: str | dict, offload_margin: str | None = None) -> None:
|
| 371 |
+
"""Apply CPU offload. Requires a single-device target (not balanced or dict).
|
| 372 |
+
|
| 373 |
+
Standard pipelines support 'model' and 'group'; modular pipelines offload through the ComponentsManager they were
|
| 374 |
+
loaded with ('auto').
|
| 375 |
+
"""
|
| 376 |
+
if not isinstance(device_map, str) or device_map == "balanced":
|
| 377 |
+
raise SystemExit(
|
| 378 |
+
"--cpu-offload requires --device-map to be a single device string (e.g. 'cuda'); "
|
| 379 |
+
f"got {device_map!r}. balanced/dict placement is incompatible with CPU offload."
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
if isinstance(pipeline, diffusers.ModularPipeline):
|
| 383 |
+
offload_kwargs = {"memory_reserve_margin": offload_margin} if offload_margin is not None else {}
|
| 384 |
+
pipeline._components_manager.enable_auto_cpu_offload(device=device_map, **offload_kwargs)
|
| 385 |
+
return
|
| 386 |
+
|
| 387 |
+
if mode == "auto":
|
| 388 |
+
raise SystemExit(
|
| 389 |
+
"--cpu-offload auto only applies to modular pipelines (it offloads through a "
|
| 390 |
+
"ComponentsManager). Use 'model' or 'group' for standard pipelines."
|
| 391 |
+
)
|
| 392 |
+
if offload_margin is not None:
|
| 393 |
+
logger.warning(
|
| 394 |
+
f"--offload-margin {offload_margin!r} only applies to `--cpu-offload auto`; "
|
| 395 |
+
f"ignoring it for `--cpu-offload {mode}`."
|
| 396 |
+
)
|
| 397 |
+
if mode == "model":
|
| 398 |
+
pipeline.enable_model_cpu_offload(device=device_map)
|
| 399 |
+
elif mode == "group":
|
| 400 |
+
pipeline.enable_group_offload(
|
| 401 |
+
onload_device=torch.device(device_map),
|
| 402 |
+
offload_type="leaf_level",
|
| 403 |
+
use_stream=True,
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def _set_attention_backend(pipeline: Any, backend: str) -> None:
|
| 408 |
+
transformer = getattr(pipeline, "transformer", None)
|
| 409 |
+
if transformer is None or not hasattr(transformer, "set_attention_backend"):
|
| 410 |
+
logger.warning(
|
| 411 |
+
f"--attention-backend is only supported on transformer-based pipelines; "
|
| 412 |
+
f"{type(pipeline).__name__} uses the legacy UNet attention path."
|
| 413 |
+
)
|
| 414 |
+
return
|
| 415 |
+
try:
|
| 416 |
+
transformer.set_attention_backend(backend)
|
| 417 |
+
except (ValueError, ImportError, RuntimeError) as e:
|
| 418 |
+
logger.warning(
|
| 419 |
+
f"Attention backend {backend!r} could not be set on {type(transformer).__name__}: "
|
| 420 |
+
f"{type(e).__name__}: {e}. Falling back to the model's default backend."
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def _enable_context_parallel(pipeline: Any) -> None:
|
| 425 |
+
if not torch.distributed.is_available():
|
| 426 |
+
raise SystemExit("--context-parallel requires a torch build with distributed support.")
|
| 427 |
+
|
| 428 |
+
if not torch.distributed.is_initialized():
|
| 429 |
+
# Hybrid backend: ulysses_anything's per-rank size coordination wants Gloo on CPU
|
| 430 |
+
# (avoids H2D/D2H for a tiny int tensor); the main attention all-to-all stays on NCCL.
|
| 431 |
+
torch.distributed.init_process_group(backend="cpu:gloo,cuda:nccl")
|
| 432 |
+
|
| 433 |
+
transformer = getattr(pipeline, "transformer", None)
|
| 434 |
+
if transformer is None or not hasattr(transformer, "enable_parallelism"):
|
| 435 |
+
raise SystemExit(
|
| 436 |
+
"--context-parallel requires a DiT-based pipeline. "
|
| 437 |
+
f"{type(pipeline).__name__} does not expose a `transformer` with `enable_parallelism`."
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
transformer.enable_parallelism(
|
| 441 |
+
config=ContextParallelConfig(
|
| 442 |
+
ulysses_degree=torch.distributed.get_world_size(),
|
| 443 |
+
ring_degree=1,
|
| 444 |
+
ulysses_anything=True,
|
| 445 |
+
)
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def _apply_optimizations(pipeline: Any, args: Namespace) -> None:
|
| 450 |
+
"""Apply VAE tiling/slicing, attention backend, context-parallel, and torch.compile toggles."""
|
| 451 |
+
vae = getattr(pipeline, "vae", None)
|
| 452 |
+
if args.vae_tiling and vae is not None and hasattr(vae, "enable_tiling"):
|
| 453 |
+
vae.enable_tiling()
|
| 454 |
+
if args.vae_slicing and vae is not None and hasattr(vae, "enable_slicing"):
|
| 455 |
+
vae.enable_slicing()
|
| 456 |
+
if args.attention_backend != "default":
|
| 457 |
+
_set_attention_backend(pipeline, args.attention_backend)
|
| 458 |
+
if args.context_parallel:
|
| 459 |
+
_enable_context_parallel(pipeline)
|
| 460 |
+
if args.compile is not None:
|
| 461 |
+
if args.context_parallel:
|
| 462 |
+
logger.warning("--compile is currently not supported with --context-parallel; skipping compile.")
|
| 463 |
+
else:
|
| 464 |
+
_compile_denoiser(pipeline, args.compile)
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def _compile_denoiser(pipeline: Any, compile_spec: str) -> None:
|
| 468 |
+
"""Compile every `transformer*` and `unet*` submodule on the pipeline.
|
| 469 |
+
|
| 470 |
+
`compile_spec` is the raw JSON string from `--compile` (`"{}"` for bare flag). Decoded into kwargs and forwarded
|
| 471 |
+
verbatim to the compile call.
|
| 472 |
+
|
| 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`.
|
| 476 |
+
"""
|
| 477 |
+
|
| 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.")
|
| 484 |
+
|
| 485 |
+
for attr in dir(pipeline):
|
| 486 |
+
if not any(attr.startswith(key) for key in _DENOISER_COMPONENT_KEYS):
|
| 487 |
+
continue
|
| 488 |
+
module = getattr(pipeline, attr, None)
|
| 489 |
+
if not isinstance(module, torch.nn.Module):
|
| 490 |
+
continue
|
| 491 |
+
|
| 492 |
+
if getattr(module, "_repeated_blocks", None):
|
| 493 |
+
# Regional compile — only the repeated blocks. Mutates `module` in place.
|
| 494 |
+
module.compile_repeated_blocks(**compile_kwargs)
|
| 495 |
+
else:
|
| 496 |
+
# No regional metadata declared; fall back to compiling the whole module.
|
| 497 |
+
setattr(pipeline, attr, torch.compile(module, **compile_kwargs))
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
def _load_lora(pipeline: Any, args: Namespace) -> None:
|
| 501 |
+
"""Attach one or more LoRA adapters. Each `--lora` value is a JSON dict.
|
| 502 |
+
|
| 503 |
+
Per-entry fields: `lora_id` (required), `lora_scale` (optional float, default 1.0), `adapter_name` (optional;
|
| 504 |
+
auto-generated as `lora_<i>` when stacking), `weight_name` (optional; required when the repo ships more than one
|
| 505 |
+
weight file, e.g. a ComfyUI variant alongside the diffusers one). Multiple `--lora` flags stack via a single
|
| 506 |
+
`set_adapters(...)` call at the end.
|
| 507 |
+
"""
|
| 508 |
+
if not args.lora:
|
| 509 |
+
return
|
| 510 |
+
specs = []
|
| 511 |
+
for raw in args.lora:
|
| 512 |
+
try:
|
| 513 |
+
parsed = json.loads(raw)
|
| 514 |
+
except json.JSONDecodeError as e:
|
| 515 |
+
raise SystemExit(f"--lora must be valid JSON: {e}") from e
|
| 516 |
+
if not isinstance(parsed, dict):
|
| 517 |
+
raise SystemExit(f"--lora must decode to a JSON object; got {type(parsed).__name__}.")
|
| 518 |
+
specs.append(parsed)
|
| 519 |
+
if not hasattr(pipeline, "load_lora_weights"):
|
| 520 |
+
raise SystemExit(f"{type(pipeline).__name__} does not support LoRA loading.")
|
| 521 |
+
|
| 522 |
+
names: list[str] = []
|
| 523 |
+
scales: list[float] = []
|
| 524 |
+
for i, spec in enumerate(specs):
|
| 525 |
+
lora_id = spec.get("lora_id")
|
| 526 |
+
if not lora_id:
|
| 527 |
+
raise SystemExit(f"--lora entry {i} is missing 'lora_id'.")
|
| 528 |
+
adapter_name = spec.get("adapter_name") or (f"lora_{i}" if len(specs) > 1 else "default")
|
| 529 |
+
pipeline.load_lora_weights(lora_id, adapter_name=adapter_name, weight_name=spec.get("weight_name", None))
|
| 530 |
+
names.append(adapter_name)
|
| 531 |
+
scales.append(float(spec.get("lora_scale", 1.0)))
|
| 532 |
+
|
| 533 |
+
if hasattr(pipeline, "set_adapters"):
|
| 534 |
+
pipeline.set_adapters(names, adapter_weights=scales)
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
def _load_pipeline(args: Namespace) -> Any:
|
| 538 |
+
# Detect modular repos by trying the standard config; `ModularPipeline` repos ship
|
| 539 |
+
# `modular_model_index.json` instead of `model_index.json`, so `load_config` OSErrors.
|
| 540 |
+
# A repo can also ship a `model_index.json` whose `_class_name` is a modular pipeline
|
| 541 |
+
# (e.g. MiniMax-H3's repository-level entry), so route on the class, not just the file.
|
| 542 |
+
try:
|
| 543 |
+
config = diffusers.DiffusionPipeline.load_config(args.model, token=args.token, revision=args.revision)
|
| 544 |
+
cls = getattr(diffusers, str(config.get("_class_name")), None)
|
| 545 |
+
modular = isinstance(cls, type) and issubclass(cls, diffusers.ModularPipeline)
|
| 546 |
+
except OSError:
|
| 547 |
+
modular = True
|
| 548 |
+
|
| 549 |
+
dtype = _resolve_dtype(args.dtype)
|
| 550 |
+
device_map = _resolve_device_map(args.device_map)
|
| 551 |
+
common_kwargs: dict[str, Any] = {
|
| 552 |
+
"trust_remote_code": args.trust_remote_code,
|
| 553 |
+
}
|
| 554 |
+
if dtype != "auto":
|
| 555 |
+
common_kwargs["torch_dtype"] = dtype
|
| 556 |
+
if args.variant:
|
| 557 |
+
common_kwargs["variant"] = args.variant
|
| 558 |
+
if args.token:
|
| 559 |
+
common_kwargs["token"] = args.token
|
| 560 |
+
# CPU offload sets up its own placement hooks, so leave weights on CPU at load time.
|
| 561 |
+
if not args.cpu_offload:
|
| 562 |
+
common_kwargs["device_map"] = device_map
|
| 563 |
+
|
| 564 |
+
if modular:
|
| 565 |
+
if args.cpu_offload and args.cpu_offload != "auto":
|
| 566 |
+
raise SystemExit(
|
| 567 |
+
f"--cpu-offload {args.cpu_offload!r} is not supported for modular pipelines — they "
|
| 568 |
+
"offload through a ComponentsManager. Use `--cpu-offload auto`."
|
| 569 |
+
)
|
| 570 |
+
components_manager = diffusers.ComponentsManager() if args.cpu_offload else None
|
| 571 |
+
# ModularPipeline.from_pretrained fetches only the pipeline config; component
|
| 572 |
+
# weights come in via load_components(). `revision` scopes the config fetch,
|
| 573 |
+
# so it stays on from_pretrained — each ComponentSpec pins its own revision,
|
| 574 |
+
# and forwarding a global `revision` to load_components() would override those.
|
| 575 |
+
pipeline = diffusers.ModularPipeline.from_pretrained(
|
| 576 |
+
args.model,
|
| 577 |
+
trust_remote_code=args.trust_remote_code,
|
| 578 |
+
token=args.token,
|
| 579 |
+
revision=args.revision,
|
| 580 |
+
components_manager=components_manager,
|
| 581 |
+
workflow=args.workflow,
|
| 582 |
+
)
|
| 583 |
+
pipeline.load_components(**common_kwargs)
|
| 584 |
+
else:
|
| 585 |
+
if args.workflow:
|
| 586 |
+
logger.warning(
|
| 587 |
+
f"--workflow {args.workflow!r} only applies to modular pipelines; "
|
| 588 |
+
f"ignoring it — '{args.model}' loads as a standard DiffusionPipeline."
|
| 589 |
+
)
|
| 590 |
+
pipeline = diffusers.DiffusionPipeline.from_pretrained(args.model, revision=args.revision, **common_kwargs)
|
| 591 |
+
|
| 592 |
+
_load_lora(pipeline, args)
|
| 593 |
+
if args.cpu_offload:
|
| 594 |
+
_apply_cpu_offload(pipeline, args.cpu_offload, device_map, args.offload_margin)
|
| 595 |
+
_apply_optimizations(pipeline, args)
|
| 596 |
+
|
| 597 |
+
return pipeline
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
# ---------------------------------------------------------------------------
|
| 601 |
+
# Pipeline call helpers
|
| 602 |
+
# ---------------------------------------------------------------------------
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def _parse_pipeline_kwargs(raw: str | None) -> dict[str, Any]:
|
| 606 |
+
if not raw:
|
| 607 |
+
return {}
|
| 608 |
+
try:
|
| 609 |
+
parsed = json.loads(raw)
|
| 610 |
+
except json.JSONDecodeError as e:
|
| 611 |
+
raise SystemExit(f"--pipeline-kwargs must be valid JSON: {e}") from e
|
| 612 |
+
if not isinstance(parsed, dict):
|
| 613 |
+
raise SystemExit("--pipeline-kwargs must decode to a JSON object.")
|
| 614 |
+
return parsed
|
| 615 |
+
|
| 616 |
+
|
| 617 |
+
def _load_audio(url_or_path: str) -> tuple[Any, int]:
|
| 618 |
+
"""Load audio from a URL or local path via torchaudio. Returns `(waveform, sampling_rate)`."""
|
| 619 |
+
import torchaudio
|
| 620 |
+
|
| 621 |
+
if url_or_path.startswith(("http://", "https://")):
|
| 622 |
+
resp = httpx.get(url_or_path, follow_redirects=True, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 623 |
+
resp.raise_for_status()
|
| 624 |
+
return torchaudio.load(io.BytesIO(resp.content))
|
| 625 |
+
return torchaudio.load(url_or_path)
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
def _resolve_media_inputs(call_kwargs: dict[str, Any]) -> None:
|
| 629 |
+
"""Replace string paths/URLs at known media-input keys with loaded tensors.
|
| 630 |
+
|
| 631 |
+
Images resolve to `PIL.Image.Image` via `load_image`; videos to `list[PIL.Image.Image]` via `load_video`; audio to
|
| 632 |
+
a `torch.Tensor` via `_load_audio` (also auto-sets the paired sampling-rate kwarg for `initial_audio_waveforms` if
|
| 633 |
+
the user didn't supply it). A `list[str]` at any key is treated as a batch: each entry is loaded and the value
|
| 634 |
+
becomes a list of loaded objects. Non-string, non-list values pass through untouched.
|
| 635 |
+
"""
|
| 636 |
+
|
| 637 |
+
def _is_string_list(v: Any) -> bool:
|
| 638 |
+
return isinstance(v, list) and bool(v) and all(isinstance(x, str) for x in v)
|
| 639 |
+
|
| 640 |
+
for key in _IMAGE_INPUT_KEYS:
|
| 641 |
+
value = call_kwargs.get(key)
|
| 642 |
+
if isinstance(value, str):
|
| 643 |
+
call_kwargs[key] = load_image(value)
|
| 644 |
+
elif _is_string_list(value):
|
| 645 |
+
call_kwargs[key] = [load_image(v) for v in value]
|
| 646 |
+
for key in _VIDEO_INPUT_KEYS:
|
| 647 |
+
value = call_kwargs.get(key)
|
| 648 |
+
if isinstance(value, str):
|
| 649 |
+
call_kwargs[key] = load_video(value)
|
| 650 |
+
elif _is_string_list(value):
|
| 651 |
+
call_kwargs[key] = [load_video(v) for v in value]
|
| 652 |
+
for key in _AUDIO_INPUT_KEYS:
|
| 653 |
+
value = call_kwargs.get(key)
|
| 654 |
+
if isinstance(value, str):
|
| 655 |
+
waveform, sr = _load_audio(value)
|
| 656 |
+
call_kwargs[key] = waveform
|
| 657 |
+
if key == "initial_audio_waveforms" and "initial_audio_sampling_rate" not in call_kwargs:
|
| 658 |
+
call_kwargs["initial_audio_sampling_rate"] = sr
|
| 659 |
+
elif _is_string_list(value):
|
| 660 |
+
pairs = [_load_audio(v) for v in value]
|
| 661 |
+
call_kwargs[key] = [w for w, _ in pairs]
|
| 662 |
+
if key == "initial_audio_waveforms" and "initial_audio_sampling_rate" not in call_kwargs:
|
| 663 |
+
# All batched waveforms must share a sampling rate; use the first entry's.
|
| 664 |
+
call_kwargs["initial_audio_sampling_rate"] = pairs[0][1]
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
def _get_generator(seed: int | None, device: str):
|
| 668 |
+
if seed is None:
|
| 669 |
+
return None
|
| 670 |
+
|
| 671 |
+
generator_device = "cpu" if device == "mps" else device
|
| 672 |
+
return torch.Generator(device=generator_device).manual_seed(seed)
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def _unwrap_pipeline_output(result: Any) -> list[Any]:
|
| 676 |
+
"""Resolve a pipeline-output object into the media payloads the saver dispatches on.
|
| 677 |
+
|
| 678 |
+
An output can carry more than one media field (e.g. LTX2 returns video in `frames` and a waveform in `audio`), so
|
| 679 |
+
every known field that is present is saved, not just the first match. Payloads keep their batch dimension —
|
| 680 |
+
`_save_output` dispatches on the full batched shape.
|
| 681 |
+
"""
|
| 682 |
+
payloads = [
|
| 683 |
+
getattr(result, name)
|
| 684 |
+
for name in ("images", "frames", "audios", "audio")
|
| 685 |
+
if getattr(result, name, None) is not None
|
| 686 |
+
]
|
| 687 |
+
return payloads or [result]
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
# ---------------------------------------------------------------------------
|
| 691 |
+
# Output saving (dispatch by type)
|
| 692 |
+
# ---------------------------------------------------------------------------
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
def _get_or_create_run_id() -> str:
|
| 696 |
+
"""Return the current run's id, creating one if not yet set.
|
| 697 |
+
|
| 698 |
+
Format: `diffusers-run-<YYYYMMDDTHHMMSS>-<6-char-uuid>`. Same id is reused as the local output subdirectory, the
|
| 699 |
+
remote bucket prefix, and the container-side `RUN_ID_ENV` so a run's artifacts are traceable end-to-end.
|
| 700 |
+
"""
|
| 701 |
+
|
| 702 |
+
existing = os.environ.get(RUN_ID_ENV)
|
| 703 |
+
if existing:
|
| 704 |
+
return existing
|
| 705 |
+
run_id = f"diffusers-run-{datetime.now().strftime('%Y%m%dT%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
| 706 |
+
os.environ[RUN_ID_ENV] = run_id
|
| 707 |
+
return run_id
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def _resolve_output_paths(num: int, explicit: str | None, ext: str) -> list[Path]:
|
| 711 |
+
if explicit is None:
|
| 712 |
+
base = Path(DEFAULT_OUTPUT_DIR) / _get_or_create_run_id()
|
| 713 |
+
base.mkdir(parents=True, exist_ok=True)
|
| 714 |
+
return [base / f"{i:04d}.{ext}" for i in range(num)]
|
| 715 |
+
|
| 716 |
+
p = Path(explicit)
|
| 717 |
+
if explicit.endswith(os.sep) or p.is_dir():
|
| 718 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 719 |
+
return [p / f"{i:04d}.{ext}" for i in range(num)]
|
| 720 |
+
|
| 721 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 722 |
+
if num == 1:
|
| 723 |
+
return [p]
|
| 724 |
+
stem, suffix = p.stem, p.suffix or f".{ext}"
|
| 725 |
+
return [p.with_name(f"{stem}-{i:04d}{suffix}") for i in range(num)]
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
def _as_pil_list(value: Any):
|
| 729 |
+
if isinstance(value, Image.Image):
|
| 730 |
+
return [value]
|
| 731 |
+
if isinstance(value, (list, tuple)) and value and all(isinstance(v, Image.Image) for v in value):
|
| 732 |
+
return list(value)
|
| 733 |
+
return None
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
def _as_frame_sequence(value: Any):
|
| 737 |
+
if isinstance(value, (list, tuple)) and len(value) >= 2 and isinstance(value[0], (Image.Image, np.ndarray)):
|
| 738 |
+
return list(value)
|
| 739 |
+
return None
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
def _as_audio_arrays(value: Any):
|
| 743 |
+
if isinstance(value, np.ndarray) and value.ndim <= 2:
|
| 744 |
+
return [value]
|
| 745 |
+
if isinstance(value, np.ndarray) and value.ndim == 3:
|
| 746 |
+
return list(value)
|
| 747 |
+
if isinstance(value, (list, tuple)) and value and all(isinstance(v, np.ndarray) for v in value):
|
| 748 |
+
return list(value)
|
| 749 |
+
return None
|
| 750 |
+
|
| 751 |
+
|
| 752 |
+
def _save_audio_arrays(audios, sampling_rate: int, args: Namespace) -> list[str]:
|
| 753 |
+
"""Write each numpy audio array to a 16-bit PCM WAV at `sampling_rate` Hz.
|
| 754 |
+
|
| 755 |
+
Uses the stdlib `wave` module so no scipy dependency is required.
|
| 756 |
+
"""
|
| 757 |
+
|
| 758 |
+
paths = _resolve_output_paths(len(audios), args.output, ext="wav")
|
| 759 |
+
saved: list[str] = []
|
| 760 |
+
for audio, path in zip(audios, paths):
|
| 761 |
+
data = np.asarray(audio)
|
| 762 |
+
if data.dtype.kind == "f":
|
| 763 |
+
data = (np.clip(data, -1.0, 1.0) * 32767).astype(np.int16)
|
| 764 |
+
else:
|
| 765 |
+
data = data.astype(np.int16)
|
| 766 |
+
if data.ndim == 1:
|
| 767 |
+
n_channels = 1
|
| 768 |
+
else:
|
| 769 |
+
# Heuristic: shorter axis is channels (interleaved layout for `wave` is
|
| 770 |
+
# samples × channels, so transpose if needed).
|
| 771 |
+
if data.shape[0] < data.shape[-1]:
|
| 772 |
+
data = data.T
|
| 773 |
+
n_channels = data.shape[1]
|
| 774 |
+
with wave.open(str(path), "wb") as w:
|
| 775 |
+
w.setnchannels(n_channels)
|
| 776 |
+
w.setsampwidth(2) # 16-bit PCM
|
| 777 |
+
w.setframerate(sampling_rate)
|
| 778 |
+
w.writeframes(data.tobytes())
|
| 779 |
+
saved.append(str(path))
|
| 780 |
+
return saved
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
def _save_videos(videos: list[Any], args: Namespace) -> list[str]:
|
| 784 |
+
"""Write each frame sequence to mp4, plus every frame as `<video-stem>-frames/<NNNN>.png` beside it.
|
| 785 |
+
|
| 786 |
+
The per-video subfolder ties each frame to its video and keeps paths unique across a batch. Frames are written
|
| 787 |
+
first and need no video backend, so they double as the safety net: if `export_to_video` fails (e.g. `imageio`
|
| 788 |
+
missing), the frames are already on disk and only the mp4 is skipped.
|
| 789 |
+
"""
|
| 790 |
+
mp4_paths = _resolve_output_paths(len(videos), args.output, ext="mp4")
|
| 791 |
+
saved: list[str] = []
|
| 792 |
+
for frames, path in zip(videos, mp4_paths):
|
| 793 |
+
frames = list(frames)
|
| 794 |
+
frames_dir = path.with_name(f"{path.stem}-frames")
|
| 795 |
+
frames_dir.mkdir(parents=True, exist_ok=True)
|
| 796 |
+
for i, frame in enumerate(frames):
|
| 797 |
+
if not isinstance(frame, Image.Image):
|
| 798 |
+
arr = np.asarray(frame)
|
| 799 |
+
if arr.dtype != np.uint8:
|
| 800 |
+
arr = (np.clip(arr, 0.0, 1.0) * 255).round().astype(np.uint8)
|
| 801 |
+
frame = Image.fromarray(arr)
|
| 802 |
+
frame_path = frames_dir / f"{i:04d}.png"
|
| 803 |
+
frame.save(frame_path)
|
| 804 |
+
saved.append(str(frame_path))
|
| 805 |
+
try:
|
| 806 |
+
export_to_video(frames, str(path), fps=args.fps)
|
| 807 |
+
saved.append(str(path))
|
| 808 |
+
except Exception as e:
|
| 809 |
+
logger.warning(
|
| 810 |
+
f"Video export failed ({e}); the individual frames of {path.stem} are saved next to it as PNGs. "
|
| 811 |
+
"Install a video backend with: pip install imageio imageio-ffmpeg"
|
| 812 |
+
)
|
| 813 |
+
return saved
|
| 814 |
+
|
| 815 |
+
|
| 816 |
+
def _save_output(value: Any, args: Namespace) -> list[str]:
|
| 817 |
+
"""Save `value` by dispatching on its runtime type and, for arrays, its shape."""
|
| 818 |
+
# Tensors arrive only when the user explicitly asked for `output_type="pt"` (or the pipeline
|
| 819 |
+
# natively defaults to it, e.g. StableAudio). Postprocessed pt outputs are channels-first per
|
| 820 |
+
# frame — (B, C, H, W) images, (B, F, C, H, W) video from `postprocess_video` — while the array
|
| 821 |
+
# branches below expect channels-last, so convert here.
|
| 822 |
+
if isinstance(value, torch.Tensor):
|
| 823 |
+
arr = value.detach().to(torch.float32).cpu().numpy()
|
| 824 |
+
if arr.ndim == 5:
|
| 825 |
+
arr = arr.transpose(0, 1, 3, 4, 2)
|
| 826 |
+
elif arr.ndim == 4:
|
| 827 |
+
arr = arr.transpose(0, 2, 3, 1)
|
| 828 |
+
value = arr
|
| 829 |
+
|
| 830 |
+
# Array shapes are unambiguous where PIL lists are not: (B, F, H, W, C) is batched video,
|
| 831 |
+
# (B, H, W, C) is batched images.
|
| 832 |
+
if isinstance(value, np.ndarray):
|
| 833 |
+
if value.ndim == 5:
|
| 834 |
+
return _save_videos(list(value), args)
|
| 835 |
+
if value.ndim == 4:
|
| 836 |
+
paths = _resolve_output_paths(len(value), args.output, ext="png")
|
| 837 |
+
for arr, path in zip(value, paths):
|
| 838 |
+
if arr.dtype != np.uint8:
|
| 839 |
+
arr = (np.clip(arr, 0.0, 1.0) * 255).round().astype(np.uint8)
|
| 840 |
+
Image.fromarray(arr).save(path)
|
| 841 |
+
return [str(p) for p in paths]
|
| 842 |
+
|
| 843 |
+
pil_images = _as_pil_list(value)
|
| 844 |
+
if pil_images is not None:
|
| 845 |
+
paths = _resolve_output_paths(len(pil_images), args.output, ext="png")
|
| 846 |
+
for img, path in zip(pil_images, paths):
|
| 847 |
+
img.save(path)
|
| 848 |
+
return [str(p) for p in paths]
|
| 849 |
+
|
| 850 |
+
frames = _as_frame_sequence(value)
|
| 851 |
+
if frames is not None:
|
| 852 |
+
return _save_videos([frames], args)
|
| 853 |
+
|
| 854 |
+
# A batch of PIL frame sequences — what video pipelines return for an explicit
|
| 855 |
+
# `output_type="pil"`. Previously this matched no branch and fell through to the JSON dump.
|
| 856 |
+
if isinstance(value, (list, tuple)) and value and all(_as_frame_sequence(v) is not None for v in value):
|
| 857 |
+
return _save_videos([list(v) for v in value], args)
|
| 858 |
+
|
| 859 |
+
audios = _as_audio_arrays(value)
|
| 860 |
+
if audios is not None:
|
| 861 |
+
return _save_audio_arrays(audios, args.sampling_rate or 16000, args)
|
| 862 |
+
|
| 863 |
+
raise ValueError(
|
| 864 |
+
f"Cannot save pipeline output of type {type(value).__name__!r}: not a recognized image, video, or audio "
|
| 865 |
+
"payload. For modular pipelines, name the media outputs to save with `--output-key` (repeat it for "
|
| 866 |
+
"several, e.g. `--output-key videos --output-key audio`); non-media values like a sample rate are not "
|
| 867 |
+
"saved and are set with their own flag (`--sampling-rate`)."
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
|
| 871 |
+
# ---------------------------------------------------------------------------
|
| 872 |
+
# Hub bucket upload (--push-to)
|
| 873 |
+
# ---------------------------------------------------------------------------
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
def _parse_push_to(spec: str) -> tuple[str, str]:
|
| 877 |
+
"""Split `--push-to` into a bucket id and an optional subpath prefix.
|
| 878 |
+
|
| 879 |
+
Accepts an HF bucket id (`<namespace>/<name>[/<subpath>]`), a canonical
|
| 880 |
+
`hf://buckets/<namespace>/<name>[/<subpath>]` URI, or a Hub web URL for the same. Non-bucket URIs (models,
|
| 881 |
+
datasets, spaces) are rejected — `--push-to` targets storage buckets only.
|
| 882 |
+
"""
|
| 883 |
+
|
| 884 |
+
# Bare shorthand → canonical URI so a single parser handles every accepted form.
|
| 885 |
+
if not spec.startswith(("hf://", "http://", "https://")):
|
| 886 |
+
spec = f"hf://buckets/{spec.strip('/')}"
|
| 887 |
+
uri = parse_hf_uri(spec)
|
| 888 |
+
if not uri.is_bucket:
|
| 889 |
+
raise SystemExit(f"--push-to must point at a bucket; got {uri.type!r} URI {spec!r}.")
|
| 890 |
+
return uri.id, uri.path_in_repo
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
def _collapse_frame_dirs(paths: list[str]) -> list[str]:
|
| 894 |
+
"""Replace each `<stem>-frames/` group with its directory path for reporting.
|
| 895 |
+
|
| 896 |
+
A video's frames are hundreds of files; listing them all buries the mp4 and floods the terminal scrollback. Entries
|
| 897 |
+
stay real paths, so the reported list is still usable programmatically. The files themselves are untouched — only
|
| 898 |
+
what is reported changes.
|
| 899 |
+
"""
|
| 900 |
+
collapsed: list[str] = []
|
| 901 |
+
for path in paths:
|
| 902 |
+
parent = path.rsplit("/", 1)[0] if "/" in path else ""
|
| 903 |
+
if not parent.endswith("-frames"):
|
| 904 |
+
collapsed.append(path)
|
| 905 |
+
elif parent not in collapsed:
|
| 906 |
+
collapsed.append(parent)
|
| 907 |
+
return collapsed
|
| 908 |
+
|
| 909 |
+
|
| 910 |
+
def _push_outputs(args: Namespace, saved_paths: list[str]) -> dict[str, Any] | None:
|
| 911 |
+
"""Upload `saved_paths` to the `--push-to` bucket. Returns a summary or None."""
|
| 912 |
+
if not args.push_to:
|
| 913 |
+
return None
|
| 914 |
+
|
| 915 |
+
bucket_id, subpath = _parse_push_to(args.push_to)
|
| 916 |
+
api = HfApi(token=args.token)
|
| 917 |
+
api.create_bucket(bucket_id, exist_ok=True)
|
| 918 |
+
|
| 919 |
+
run_id = _get_or_create_run_id()
|
| 920 |
+
prefix = f"{subpath}/{run_id}" if subpath else run_id
|
| 921 |
+
# Destination paths keep their structure relative to the common output dir, so files in
|
| 922 |
+
# subfolders (e.g. `0000-frames/0003.png`) don't collide on basename in the bucket.
|
| 923 |
+
base = Path(os.path.commonpath([str(Path(p).parent) for p in saved_paths]))
|
| 924 |
+
add = [(local, f"{prefix}/{Path(local).relative_to(base).as_posix()}") for local in saved_paths]
|
| 925 |
+
api.batch_bucket_files(bucket_id, add=add)
|
| 926 |
+
|
| 927 |
+
uploaded = _collapse_frame_dirs([f"hf://buckets/{bucket_id}/{dest}" for _, dest in add])
|
| 928 |
+
return {"bucket_id": bucket_id, "uploaded": uploaded}
|
| 929 |
+
|
| 930 |
+
|
| 931 |
+
# ---------------------------------------------------------------------------
|
| 932 |
+
# Remote execution (HF Sandbox)
|
| 933 |
+
# ---------------------------------------------------------------------------
|
| 934 |
+
|
| 935 |
+
|
| 936 |
+
def _build_task_kwargs(args: Namespace) -> dict[str, Any]:
|
| 937 |
+
"""Pick out the kwargs the sandbox CLI should invoke the task with."""
|
| 938 |
+
out: dict[str, Any] = {}
|
| 939 |
+
for key, value in vars(args).items():
|
| 940 |
+
if key in REMOTE_KEYS or value is None or value is False:
|
| 941 |
+
continue
|
| 942 |
+
out[key] = value
|
| 943 |
+
return out
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
def _kwargs_to_argv(task: str, task_kwargs: dict[str, Any]) -> list[str]:
|
| 947 |
+
"""Render `task_kwargs` as the argv list the sandbox CLI's argparse will see."""
|
| 948 |
+
argv: list[str] = [task]
|
| 949 |
+
for key, value in task_kwargs.items():
|
| 950 |
+
flag = "--" + key.replace("_", "-")
|
| 951 |
+
if value is True:
|
| 952 |
+
argv.append(flag)
|
| 953 |
+
elif isinstance(value, list):
|
| 954 |
+
for item in value:
|
| 955 |
+
argv.extend([flag, str(item)])
|
| 956 |
+
else:
|
| 957 |
+
argv.extend([flag, str(value)])
|
| 958 |
+
return argv
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
def _duration_to_seconds(value: str) -> float:
|
| 962 |
+
"""Parse a duration like `30s`, `10m`, `2h` (or a bare number of seconds) into seconds."""
|
| 963 |
+
value = value.strip()
|
| 964 |
+
units = {"s": 1, "m": 60, "h": 3600}
|
| 965 |
+
if value and value[-1] in units:
|
| 966 |
+
return float(value[:-1]) * units[value[-1]]
|
| 967 |
+
return float(value)
|
| 968 |
+
|
| 969 |
+
|
| 970 |
+
def _upload_inputs_to_sandbox(args: Namespace, sbx: Any, run_id: str) -> None:
|
| 971 |
+
"""Upload local media paths in `--pipeline-kwargs` into the sandbox and rewrite the JSON in place.
|
| 972 |
+
|
| 973 |
+
Walks known image/video/audio-input keys; any string value that resolves to a local file is uploaded to
|
| 974 |
+
`<_SANDBOX_INPUTS_DIR>/<run_id>/<key>_<basename>` and the JSON path is rewritten to that in-sandbox path. URLs,
|
| 975 |
+
`hf://` URIs, and non-existent paths pass through untouched.
|
| 976 |
+
"""
|
| 977 |
+
if not args.pipeline_kwargs:
|
| 978 |
+
return
|
| 979 |
+
try:
|
| 980 |
+
parsed = json.loads(args.pipeline_kwargs)
|
| 981 |
+
except json.JSONDecodeError:
|
| 982 |
+
return # the sandbox CLI will fail loudly with a parse error later
|
| 983 |
+
if not isinstance(parsed, dict):
|
| 984 |
+
return
|
| 985 |
+
|
| 986 |
+
def _upload_one(key: str, index: int | None, local_str: str) -> str:
|
| 987 |
+
# `index` is None for scalar entries, an int for list entries (used to disambiguate names).
|
| 988 |
+
local = Path(local_str)
|
| 989 |
+
suffix = f"_{index}" if index is not None else ""
|
| 990 |
+
remote_path = f"{_SANDBOX_INPUTS_DIR}/{run_id}/{key}{suffix}_{local.name}"
|
| 991 |
+
sbx.files.upload(str(local), remote_path)
|
| 992 |
+
return remote_path
|
| 993 |
+
|
| 994 |
+
uploaded = 0
|
| 995 |
+
for key in (*_IMAGE_INPUT_KEYS, *_VIDEO_INPUT_KEYS, *_AUDIO_INPUT_KEYS):
|
| 996 |
+
value = parsed.get(key)
|
| 997 |
+
if isinstance(value, str) and Path(value).is_file():
|
| 998 |
+
parsed[key] = _upload_one(key, None, value)
|
| 999 |
+
uploaded += 1
|
| 1000 |
+
elif isinstance(value, list):
|
| 1001 |
+
# Batched inputs: upload each local path, leave URLs/hf:// URIs alone.
|
| 1002 |
+
new_list = list(value)
|
| 1003 |
+
for i, entry in enumerate(value):
|
| 1004 |
+
if isinstance(entry, str) and Path(entry).is_file():
|
| 1005 |
+
new_list[i] = _upload_one(key, i, entry)
|
| 1006 |
+
uploaded += 1
|
| 1007 |
+
parsed[key] = new_list
|
| 1008 |
+
|
| 1009 |
+
if uploaded:
|
| 1010 |
+
logger.info(f"uploaded {uploaded} local input file(s) to the sandbox")
|
| 1011 |
+
args.pipeline_kwargs = json.dumps(parsed)
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
+
def _download_outputs_from_sandbox(sbx: Any, sandbox_dir: str, local_dir: Path) -> list[str]:
|
| 1015 |
+
"""Download everything the sandbox CLI wrote under `sandbox_dir` into `local_dir`, recursing into subfolders."""
|
| 1016 |
+
local_dir.mkdir(parents=True, exist_ok=True)
|
| 1017 |
+
saved: list[str] = []
|
| 1018 |
+
for entry in sbx.files.list(sandbox_dir):
|
| 1019 |
+
if entry.type == "dir":
|
| 1020 |
+
saved.extend(_download_outputs_from_sandbox(sbx, entry.path, local_dir / Path(entry.path).name))
|
| 1021 |
+
elif entry.type == "file":
|
| 1022 |
+
target = local_dir / Path(entry.path).name
|
| 1023 |
+
sbx.files.download(entry.path, str(target))
|
| 1024 |
+
saved.append(str(target))
|
| 1025 |
+
return saved
|
| 1026 |
+
|
| 1027 |
+
|
| 1028 |
+
def _maybe_submit_remote(args: Namespace, task: str) -> bool:
|
| 1029 |
+
"""If `--remote` was set, run this invocation inside an HF Sandbox and return True."""
|
| 1030 |
+
if not args.remote:
|
| 1031 |
+
return False
|
| 1032 |
+
|
| 1033 |
+
if Path(args.model).exists():
|
| 1034 |
+
raise SystemExit(
|
| 1035 |
+
f"--model {args.model!r} is a local path; the sandbox can't see it. "
|
| 1036 |
+
"Pass a Hub repo id so the sandbox can download it."
|
| 1037 |
+
)
|
| 1038 |
+
|
| 1039 |
+
hf_token = args.token or get_token()
|
| 1040 |
+
run_id = _get_or_create_run_id()
|
| 1041 |
+
|
| 1042 |
+
# An explicit --push-to means the bucket is the user's destination, so skip the local
|
| 1043 |
+
# download unless they also asked for a local path via --output.
|
| 1044 |
+
user_bucket = bool(args.push_to)
|
| 1045 |
+
download_locally = (not user_bucket) or (args.output is not None)
|
| 1046 |
+
local_dir = Path(args.output) if args.output else Path(DEFAULT_OUTPUT_DIR) / run_id
|
| 1047 |
+
|
| 1048 |
+
use_existing_sandbox = bool(args.sandbox_id)
|
| 1049 |
+
keep_alive = args.keep_alive or use_existing_sandbox
|
| 1050 |
+
if use_existing_sandbox and args.volume:
|
| 1051 |
+
logger.warning(
|
| 1052 |
+
"--volume is ignored when reconnecting to an existing sandbox (mounts are set at creation time)."
|
| 1053 |
+
)
|
| 1054 |
+
if use_existing_sandbox:
|
| 1055 |
+
logger.info(f"reconnecting to sandbox {args.sandbox_id!r}...")
|
| 1056 |
+
sbx = Sandbox.connect(args.sandbox_id, token=hf_token)
|
| 1057 |
+
else:
|
| 1058 |
+
logger.info(f"creating sandbox on flavor={args.flavor!r}...")
|
| 1059 |
+
create_kwargs: dict[str, Any] = {
|
| 1060 |
+
"image": args.image or _DEFAULT_REMOTE_IMAGE,
|
| 1061 |
+
"flavor": args.flavor,
|
| 1062 |
+
"forward_hf_token": True,
|
| 1063 |
+
"token": hf_token,
|
| 1064 |
+
"env": {
|
| 1065 |
+
"HF_ENABLE_PARALLEL_LOADING": "1",
|
| 1066 |
+
"DIFFUSERS_VERBOSITY": os.environ.get("DIFFUSERS_VERBOSITY", "info"),
|
| 1067 |
+
},
|
| 1068 |
+
"idle_timeout": args.idle_timeout,
|
| 1069 |
+
# The 120s default expires while a cold node pulls the multi-GB pytorch base image or
|
| 1070 |
+
# waits for GPU capacity, which fails the run before it starts.
|
| 1071 |
+
"start_timeout": 600.0,
|
| 1072 |
+
}
|
| 1073 |
+
if args.volume:
|
| 1074 |
+
volumes = []
|
| 1075 |
+
for spec in args.volume:
|
| 1076 |
+
bucket_id, sep, mount_path = spec.partition(":")
|
| 1077 |
+
if not sep:
|
| 1078 |
+
mount_path = f"/mnt/buckets/{bucket_id}"
|
| 1079 |
+
if bucket_id.count("/") != 1:
|
| 1080 |
+
raise SystemExit(f"--volume: bucket id must be <namespace>/<name>, got {bucket_id!r}")
|
| 1081 |
+
if not mount_path.startswith("/"):
|
| 1082 |
+
raise SystemExit(f"--volume: mount path must be absolute, got {mount_path!r}")
|
| 1083 |
+
volumes.append(Volume(type="bucket", source=bucket_id, mount_path=mount_path))
|
| 1084 |
+
create_kwargs["volumes"] = volumes
|
| 1085 |
+
if args.namespace is not None:
|
| 1086 |
+
create_kwargs["namespace"] = args.namespace
|
| 1087 |
+
sbx = Sandbox.create(**create_kwargs)
|
| 1088 |
+
|
| 1089 |
+
def _stream(chunk: str) -> None:
|
| 1090 |
+
sys.stderr.write(chunk)
|
| 1091 |
+
sys.stderr.flush()
|
| 1092 |
+
|
| 1093 |
+
exit_code = 0
|
| 1094 |
+
saved: list[str] = []
|
| 1095 |
+
run_seconds = 0.0
|
| 1096 |
+
try:
|
| 1097 |
+
_upload_inputs_to_sandbox(args, sbx, run_id)
|
| 1098 |
+
|
| 1099 |
+
dependencies = list(_DEFAULT_REMOTE_DEPS)
|
| 1100 |
+
if args.dependencies:
|
| 1101 |
+
dependencies.extend(args.dependencies)
|
| 1102 |
+
# --break-system-packages bypasses PEP 668; harmless in a throwaway sandbox. uv is a
|
| 1103 |
+
# near no-op when the deps are already satisfied, so this stays cheap on a reused sandbox.
|
| 1104 |
+
install_cmd = shlex.join(["uv", "pip", "install", "--system", "--break-system-packages", *dependencies])
|
| 1105 |
+
logger.info("installing dependencies in the sandbox...")
|
| 1106 |
+
sbx.run(install_cmd, on_stdout=_stream, on_stderr=_stream)
|
| 1107 |
+
|
| 1108 |
+
# Per-run outputs subdirectory so a reused sandbox doesn't leak files from prior runs
|
| 1109 |
+
# into this run's download set.
|
| 1110 |
+
sandbox_output_dir = f"{_SANDBOX_OUTPUTS_DIR}/{run_id}"
|
| 1111 |
+
task_kwargs = _build_task_kwargs(args)
|
| 1112 |
+
task_kwargs["output"] = sandbox_output_dir + "/"
|
| 1113 |
+
cli_argv = _kwargs_to_argv(task, task_kwargs)
|
| 1114 |
+
# Suppress the container CLI's own `out.result(...)` payload — the outer wrapper owns the
|
| 1115 |
+
# final structured output for --remote runs.
|
| 1116 |
+
format_argv = ["--format", "quiet"]
|
| 1117 |
+
# torchrun wraps the CLI for --context-parallel so torch.distributed initializes across
|
| 1118 |
+
# every visible GPU before the run command starts.
|
| 1119 |
+
if args.context_parallel:
|
| 1120 |
+
cli_argv = [
|
| 1121 |
+
"torchrun",
|
| 1122 |
+
"--nproc-per-node=gpu",
|
| 1123 |
+
"-m",
|
| 1124 |
+
"diffusers.commands.diffusers_cli",
|
| 1125 |
+
*format_argv,
|
| 1126 |
+
*cli_argv,
|
| 1127 |
+
]
|
| 1128 |
+
else:
|
| 1129 |
+
cli_argv = [_CONTAINER_CLI_BINARY, *format_argv, *cli_argv]
|
| 1130 |
+
|
| 1131 |
+
started = time.perf_counter()
|
| 1132 |
+
# Per-invocation env: RUN_ID_ENV must be fresh each run. Sandbox.create-time env is
|
| 1133 |
+
# baked in and would go stale on reused sandboxes, silently reusing the initial run's
|
| 1134 |
+
# bucket prefix in `_push_outputs`.
|
| 1135 |
+
result = sbx.run(
|
| 1136 |
+
cli_argv,
|
| 1137 |
+
env={RUN_ID_ENV: run_id},
|
| 1138 |
+
on_stdout=_stream,
|
| 1139 |
+
on_stderr=_stream,
|
| 1140 |
+
timeout=_duration_to_seconds(args.timeout),
|
| 1141 |
+
check=False,
|
| 1142 |
+
)
|
| 1143 |
+
run_seconds = time.perf_counter() - started
|
| 1144 |
+
exit_code = result.exit_code
|
| 1145 |
+
|
| 1146 |
+
if exit_code == 0 and download_locally:
|
| 1147 |
+
saved = _download_outputs_from_sandbox(sbx, sandbox_output_dir, local_dir)
|
| 1148 |
+
finally:
|
| 1149 |
+
if keep_alive:
|
| 1150 |
+
logger.info(
|
| 1151 |
+
f"sandbox {sbx.id} kept alive — reconnect with "
|
| 1152 |
+
f"`--remote --sandbox-id {sbx.id}`, stop with `hf sandbox kill {sbx.id}`."
|
| 1153 |
+
)
|
| 1154 |
+
else:
|
| 1155 |
+
sbx.kill()
|
| 1156 |
+
|
| 1157 |
+
send_telemetry(
|
| 1158 |
+
topic="diffusers/cli/run/remote",
|
| 1159 |
+
library_name="diffusers",
|
| 1160 |
+
library_version=diffusers.__version__,
|
| 1161 |
+
)
|
| 1162 |
+
|
| 1163 |
+
payload: dict[str, Any] = {
|
| 1164 |
+
"exit_code": exit_code,
|
| 1165 |
+
"run_seconds": round(run_seconds, 1),
|
| 1166 |
+
}
|
| 1167 |
+
if keep_alive:
|
| 1168 |
+
payload["sandbox_id"] = sbx.id
|
| 1169 |
+
if download_locally:
|
| 1170 |
+
payload["outputs"] = _collapse_frame_dirs(saved)
|
| 1171 |
+
if args.push_to:
|
| 1172 |
+
bucket_id, subpath = _parse_push_to(args.push_to)
|
| 1173 |
+
prefix = f"{subpath}/{run_id}" if subpath else run_id
|
| 1174 |
+
payload["pushed-to"] = f"hf://buckets/{bucket_id}/{prefix}/"
|
| 1175 |
+
out.result("remote-run", **payload)
|
| 1176 |
+
|
| 1177 |
+
if exit_code != 0:
|
| 1178 |
+
raise SystemExit(f"remote run failed with exit code {exit_code}")
|
| 1179 |
+
return True
|
| 1180 |
+
|
| 1181 |
+
|
| 1182 |
+
# ---------------------------------------------------------------------------
|
| 1183 |
+
# Subcommand
|
| 1184 |
+
# ---------------------------------------------------------------------------
|
| 1185 |
+
|
| 1186 |
+
|
| 1187 |
+
class RunCommand(BaseDiffusersCLICommand):
|
| 1188 |
+
task = "run"
|
| 1189 |
+
|
| 1190 |
+
@staticmethod
|
| 1191 |
+
def register_subcommand(subparsers: _SubParsersAction) -> None:
|
| 1192 |
+
epilog = (
|
| 1193 |
+
"Examples\n"
|
| 1194 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1195 |
+
' --pipeline-kwargs \'{"prompt": "a cat on the moon"}\'\n'
|
| 1196 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1197 |
+
' --pipeline-kwargs \'{"prompt": "make the fur grey", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png"}\'\n'
|
| 1198 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1199 |
+
' --pipeline-kwargs \'{"prompt": "a tiny cat"}\' \\\n'
|
| 1200 |
+
' --lora \'{"lora_id": "alvdansen/littletinies", "lora_scale": 0.8}\'\n'
|
| 1201 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1202 |
+
' --pipeline-kwargs \'{"prompt": "a cat"}\' --remote --flavor a100-large\n'
|
| 1203 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 --context-parallel \\\n"
|
| 1204 |
+
' --pipeline-kwargs \'{"prompt": "a cat"}\' --remote --flavor 4xa100-large\n'
|
| 1205 |
+
"\n"
|
| 1206 |
+
"Learn more\n"
|
| 1207 |
+
" Use `diffusers-cli <command> --help` for more information about a command.\n"
|
| 1208 |
+
" Read the documentation at https://huggingface.co/docs/diffusers\n"
|
| 1209 |
+
)
|
| 1210 |
+
|
| 1211 |
+
parser: ArgumentParser = subparsers.add_parser(
|
| 1212 |
+
"run",
|
| 1213 |
+
help="Run any diffusers pipeline locally or remotely in an HF Sandbox.",
|
| 1214 |
+
usage="\n diffusers-cli run [options]",
|
| 1215 |
+
epilog=epilog,
|
| 1216 |
+
formatter_class=RawDescriptionHelpFormatter,
|
| 1217 |
+
)
|
| 1218 |
+
parser._optionals.title = "Options"
|
| 1219 |
+
_add_loading_arguments(parser)
|
| 1220 |
+
_add_optimization_arguments(parser)
|
| 1221 |
+
parser.add_argument(
|
| 1222 |
+
"--pipeline-kwargs",
|
| 1223 |
+
default=None,
|
| 1224 |
+
help=(
|
| 1225 |
+
"JSON object of kwargs passed to the pipeline call. String values at known "
|
| 1226 |
+
f"image-input keys ({', '.join(_IMAGE_INPUT_KEYS)}) are auto-loaded as PIL images; "
|
| 1227 |
+
f"video-input keys ({', '.join(_VIDEO_INPUT_KEYS)}) are auto-loaded as frame lists; "
|
| 1228 |
+
f"audio-input keys ({', '.join(_AUDIO_INPUT_KEYS)}) are auto-loaded via torchaudio."
|
| 1229 |
+
),
|
| 1230 |
+
)
|
| 1231 |
+
parser.add_argument(
|
| 1232 |
+
"--output-key",
|
| 1233 |
+
action="append",
|
| 1234 |
+
default=None,
|
| 1235 |
+
metavar="NAME",
|
| 1236 |
+
help=(
|
| 1237 |
+
"For modular pipelines: name of the intermediate to extract (passed as `output=` to the call). "
|
| 1238 |
+
"Repeat to request several, e.g. `--output-key videos --output-key audio`; each is saved by its "
|
| 1239 |
+
"own media type."
|
| 1240 |
+
),
|
| 1241 |
+
)
|
| 1242 |
+
parser.add_argument(
|
| 1243 |
+
"--workflow",
|
| 1244 |
+
default=None,
|
| 1245 |
+
help=(
|
| 1246 |
+
"For modular pipelines: workflow to load (passed to `ModularPipeline.from_pretrained`). "
|
| 1247 |
+
"Prunes the blocks to that workflow so `load_components` only fetches its components."
|
| 1248 |
+
),
|
| 1249 |
+
)
|
| 1250 |
+
parser.add_argument(
|
| 1251 |
+
"--offload-margin",
|
| 1252 |
+
default=None,
|
| 1253 |
+
help=(
|
| 1254 |
+
"For `--cpu-offload auto`: device memory kept free for activations "
|
| 1255 |
+
"(passed to `ComponentsManager.enable_auto_cpu_offload` as `memory_reserve_margin`, "
|
| 1256 |
+
"default 3GB). Raise it when a large canvas OOMs mid-forward."
|
| 1257 |
+
),
|
| 1258 |
+
)
|
| 1259 |
+
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility.")
|
| 1260 |
+
parser.add_argument(
|
| 1261 |
+
"--fps",
|
| 1262 |
+
type=int,
|
| 1263 |
+
default=8,
|
| 1264 |
+
help="FPS used when the output happens to be a frame sequence.",
|
| 1265 |
+
)
|
| 1266 |
+
parser.add_argument(
|
| 1267 |
+
"--sampling-rate",
|
| 1268 |
+
type=int,
|
| 1269 |
+
default=None,
|
| 1270 |
+
help="Sample rate used when the output happens to be an audio array.",
|
| 1271 |
+
)
|
| 1272 |
+
_add_remote_arguments(parser)
|
| 1273 |
+
_add_output_arguments(parser)
|
| 1274 |
+
parser.set_defaults(func=RunCommand)
|
| 1275 |
+
|
| 1276 |
+
def __init__(self, args: Namespace):
|
| 1277 |
+
self.args = args
|
| 1278 |
+
|
| 1279 |
+
def run(self) -> None:
|
| 1280 |
+
_get_or_create_run_id() # populate RUN_ID_ENV so local output dir + remote bucket prefix agree
|
| 1281 |
+
|
| 1282 |
+
call_kwargs = _parse_pipeline_kwargs(self.args.pipeline_kwargs)
|
| 1283 |
+
|
| 1284 |
+
if _maybe_submit_remote(self.args, self.task):
|
| 1285 |
+
return
|
| 1286 |
+
|
| 1287 |
+
# Resolve media before loading pipeline weights so dead URLs / missing files fail
|
| 1288 |
+
# fast — cheap to fetch, expensive to load a 20GB model just to hit a 404.
|
| 1289 |
+
_resolve_media_inputs(call_kwargs)
|
| 1290 |
+
pipeline = _load_pipeline(self.args)
|
| 1291 |
+
is_modular = isinstance(pipeline, diffusers.ModularPipeline)
|
| 1292 |
+
|
| 1293 |
+
if self.args.output_key is not None:
|
| 1294 |
+
# One key returns that value directly; several return a dict keyed by name.
|
| 1295 |
+
keys = self.args.output_key
|
| 1296 |
+
call_kwargs["output"] = keys[0] if len(keys) == 1 else keys
|
| 1297 |
+
|
| 1298 |
+
device = pipeline.device.type if hasattr(pipeline, "device") else "cpu"
|
| 1299 |
+
generator = _get_generator(self.args.seed, device)
|
| 1300 |
+
if generator is not None:
|
| 1301 |
+
call_kwargs["generator"] = generator
|
| 1302 |
+
|
| 1303 |
+
try:
|
| 1304 |
+
result = pipeline(**call_kwargs)
|
| 1305 |
+
|
| 1306 |
+
# Under torchrun, ranks > 0 produce identical output to rank 0 (CP shards the
|
| 1307 |
+
# transformer compute but ranks reduce to the same final tensors). Save/push/print
|
| 1308 |
+
# from rank 0 only to avoid clobbering bucket files 4x and printing 4x.
|
| 1309 |
+
if os.environ.get("RANK", "0") == "0":
|
| 1310 |
+
if not is_modular:
|
| 1311 |
+
savables = _unwrap_pipeline_output(result)
|
| 1312 |
+
elif isinstance(result, dict):
|
| 1313 |
+
# Several `--output-key`s: one payload per requested name.
|
| 1314 |
+
savables = list(result.values())
|
| 1315 |
+
else:
|
| 1316 |
+
savables = [result]
|
| 1317 |
+
saved = []
|
| 1318 |
+
for savable in savables:
|
| 1319 |
+
saved.extend(_save_output(savable, self.args))
|
| 1320 |
+
pushed = _push_outputs(self.args, saved)
|
| 1321 |
+
|
| 1322 |
+
out.result(
|
| 1323 |
+
self.task,
|
| 1324 |
+
model=self.args.model,
|
| 1325 |
+
device=device,
|
| 1326 |
+
pipeline_class=type(pipeline).__name__,
|
| 1327 |
+
modular=is_modular,
|
| 1328 |
+
outputs=_collapse_frame_dirs(saved),
|
| 1329 |
+
pushed=pushed,
|
| 1330 |
+
seed=self.args.seed,
|
| 1331 |
+
output_key=self.args.output_key,
|
| 1332 |
+
)
|
| 1333 |
+
finally:
|
| 1334 |
+
if torch.distributed.is_available() and torch.distributed.is_initialized():
|
| 1335 |
+
torch.distributed.destroy_process_group()
|
diffusers/commands/schema.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""`diffusers-cli schema` — print the input schema for any pipeline repo.
|
| 16 |
+
|
| 17 |
+
Tries `DiffusionPipeline.config_name` first (so standard repos get their `__call__` signature introspected); falls back
|
| 18 |
+
to `ModularPipelineBlocks.from_pretrained` for modular repos. No weights are downloaded — only the small index file
|
| 19 |
+
(and any custom block code if `--trust-remote-code` is set).
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import inspect
|
| 25 |
+
import re
|
| 26 |
+
from argparse import ArgumentParser, Namespace, _SubParsersAction
|
| 27 |
+
from typing import Any
|
| 28 |
+
|
| 29 |
+
from huggingface_hub.cli._output import OutputFormat, out
|
| 30 |
+
|
| 31 |
+
from ..utils import logging
|
| 32 |
+
from . import BaseDiffusersCLICommand
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
logger = logging.get_logger("diffusers-cli/schema")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _schema(args: Namespace) -> None:
|
| 39 |
+
"""Print the pipeline's input schema.
|
| 40 |
+
|
| 41 |
+
Tries `DiffusionPipeline.config_name` (= `model_index.json`) first; if present, introspects the declared pipeline
|
| 42 |
+
class's `__call__` signature. Otherwise falls back to `ModularPipelineBlocks.from_pretrained` and reads the
|
| 43 |
+
block-declared `inputs`. No weights downloaded either way.
|
| 44 |
+
"""
|
| 45 |
+
import diffusers
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
index = diffusers.DiffusionPipeline.load_config(args.model, token=args.token, revision=args.revision)
|
| 49 |
+
except OSError:
|
| 50 |
+
index = None
|
| 51 |
+
|
| 52 |
+
if index is not None:
|
| 53 |
+
class_name = index.get("_class_name")
|
| 54 |
+
if class_name is None:
|
| 55 |
+
raise SystemExit(
|
| 56 |
+
f"{diffusers.DiffusionPipeline.config_name} for {args.model!r} has no `_class_name` field."
|
| 57 |
+
)
|
| 58 |
+
pipeline_cls = getattr(diffusers, class_name, None)
|
| 59 |
+
if pipeline_cls is None:
|
| 60 |
+
raise SystemExit(
|
| 61 |
+
f"Pipeline class {class_name!r} declared in {diffusers.DiffusionPipeline.config_name} "
|
| 62 |
+
"is not exported by the installed diffusers."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
sig = inspect.signature(pipeline_cls.__call__)
|
| 66 |
+
descriptions = _parse_docstring_args(pipeline_cls.__call__.__doc__) if args.verbose else {}
|
| 67 |
+
schema: list[dict[str, Any]] = []
|
| 68 |
+
for name, param in sig.parameters.items():
|
| 69 |
+
if name == "self":
|
| 70 |
+
continue
|
| 71 |
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
| 72 |
+
continue
|
| 73 |
+
has_default = param.default is not inspect.Parameter.empty
|
| 74 |
+
schema.append(
|
| 75 |
+
{
|
| 76 |
+
"name": name,
|
| 77 |
+
"type_hint": str(param.annotation) if param.annotation is not inspect.Parameter.empty else None,
|
| 78 |
+
"default": param.default if has_default else None,
|
| 79 |
+
"required": not has_default,
|
| 80 |
+
"description": descriptions.get(name, ""),
|
| 81 |
+
}
|
| 82 |
+
)
|
| 83 |
+
else:
|
| 84 |
+
kwargs: dict[str, Any] = {"trust_remote_code": args.trust_remote_code}
|
| 85 |
+
if args.revision:
|
| 86 |
+
kwargs["revision"] = args.revision
|
| 87 |
+
if args.token:
|
| 88 |
+
kwargs["token"] = args.token
|
| 89 |
+
|
| 90 |
+
# If the repo declares custom code + external dependencies, surface them upfront so
|
| 91 |
+
# the user knows what to install before we hit an ImportError inside from_pretrained.
|
| 92 |
+
_warn_custom_block_requirements(args)
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
blocks = diffusers.ModularPipelineBlocks.from_pretrained(args.model, **kwargs)
|
| 96 |
+
except Exception as e:
|
| 97 |
+
hint = "\nPass --trust-remote-code if it ships custom block code." if not args.trust_remote_code else ""
|
| 98 |
+
raise SystemExit(
|
| 99 |
+
f"Could not read schema for {args.model!r}: no {diffusers.DiffusionPipeline.config_name} and "
|
| 100 |
+
f"loading as a modular pipeline failed with:\n {type(e).__name__}: {e}{hint}"
|
| 101 |
+
) from e
|
| 102 |
+
|
| 103 |
+
class_name = type(blocks).__name__
|
| 104 |
+
schema = [
|
| 105 |
+
{
|
| 106 |
+
"name": p.name,
|
| 107 |
+
"type_hint": str(p.type_hint) if p.type_hint is not None else None,
|
| 108 |
+
"default": p.default,
|
| 109 |
+
"required": p.required,
|
| 110 |
+
"description": p.description,
|
| 111 |
+
}
|
| 112 |
+
for p in blocks.inputs
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
if out.mode == OutputFormat.json:
|
| 116 |
+
out.dict({"task": "schema", "model": args.model, "pipeline_class": class_name, "inputs": schema})
|
| 117 |
+
elif out.mode == OutputFormat.agent:
|
| 118 |
+
out.table(schema, headers=["name", "required", "type_hint", "default", "description"])
|
| 119 |
+
else:
|
| 120 |
+
out.text(f"{class_name} ({args.model}) inputs:")
|
| 121 |
+
for entry in schema:
|
| 122 |
+
tag = "required" if entry["required"] else f"optional, default={entry['default']!r}"
|
| 123 |
+
out.text(f" {entry['name']} ({tag})")
|
| 124 |
+
if entry["type_hint"]:
|
| 125 |
+
out.text(f" type: {entry['type_hint']}")
|
| 126 |
+
if entry["description"]:
|
| 127 |
+
out.text(f" desc: {entry['description']}")
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _warn_custom_block_requirements(args: Namespace) -> None:
|
| 131 |
+
"""Warn upfront when a modular block ships custom code with declared external dependencies.
|
| 132 |
+
|
| 133 |
+
Reads `modular_config.json` if present; if it has an `auto_map` (custom code) and a non-empty `requirements`
|
| 134 |
+
list/dict, prints a heads-up. `from_pretrained` will otherwise fail with an `ImportError` deep in the loader stack
|
| 135 |
+
when a listed dep is missing.
|
| 136 |
+
"""
|
| 137 |
+
import diffusers
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
config = diffusers.ModularPipelineBlocks.load_config(args.model, token=args.token, revision=args.revision)
|
| 141 |
+
except Exception:
|
| 142 |
+
return # no modular_config.json or unreachable — nothing to warn about
|
| 143 |
+
if not isinstance(config, dict):
|
| 144 |
+
return
|
| 145 |
+
if not config.get("auto_map"):
|
| 146 |
+
return
|
| 147 |
+
requirements = config.get("requirements")
|
| 148 |
+
if not requirements:
|
| 149 |
+
return
|
| 150 |
+
|
| 151 |
+
# `requirements` may be a dict {name: version} or (older repos) a list of [name, version] pairs.
|
| 152 |
+
if isinstance(requirements, dict):
|
| 153 |
+
pairs = list(requirements.items())
|
| 154 |
+
elif isinstance(requirements, list):
|
| 155 |
+
pairs = [(item[0], item[1]) for item in requirements if isinstance(item, (list, tuple)) and len(item) >= 2]
|
| 156 |
+
else:
|
| 157 |
+
pairs = []
|
| 158 |
+
if not pairs:
|
| 159 |
+
return
|
| 160 |
+
|
| 161 |
+
formatted = ", ".join(f"{name}=={version}" for name, version in pairs)
|
| 162 |
+
logger.warning(
|
| 163 |
+
f"{args.model!r} ships custom block code with external dependencies: {formatted}. "
|
| 164 |
+
"You will need to install these in order to determine the pipeline schema."
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _parse_docstring_args(docstring: str | None) -> dict[str, str]:
|
| 169 |
+
"""Extract per-argument descriptions from a Google-style `Args:` block.
|
| 170 |
+
|
| 171 |
+
Returns a `{name: description}` mapping. Best-effort — unrecognised formats just yield an empty dict rather than
|
| 172 |
+
raising.
|
| 173 |
+
"""
|
| 174 |
+
if not docstring:
|
| 175 |
+
return {}
|
| 176 |
+
|
| 177 |
+
lines = docstring.expandtabs().splitlines()
|
| 178 |
+
start = None
|
| 179 |
+
section_indent = 0
|
| 180 |
+
for i, line in enumerate(lines):
|
| 181 |
+
if line.strip() in ("Args:", "Arguments:", "Parameters:"):
|
| 182 |
+
start = i + 1
|
| 183 |
+
section_indent = len(line) - len(line.lstrip())
|
| 184 |
+
break
|
| 185 |
+
if start is None:
|
| 186 |
+
return {}
|
| 187 |
+
|
| 188 |
+
descriptions: dict[str, str] = {}
|
| 189 |
+
current_name: str | None = None
|
| 190 |
+
current_lines: list[str] = []
|
| 191 |
+
arg_indent: int | None = None
|
| 192 |
+
name_pattern = re.compile(r"^(\w+)\s*(?:\([^)]*\))?\s*:?\s*(.*)$")
|
| 193 |
+
|
| 194 |
+
def _flush() -> None:
|
| 195 |
+
if current_name and current_lines:
|
| 196 |
+
descriptions[current_name] = " ".join(s.strip() for s in current_lines).strip()
|
| 197 |
+
|
| 198 |
+
for line in lines[start:]:
|
| 199 |
+
if not line.strip():
|
| 200 |
+
continue
|
| 201 |
+
indent = len(line) - len(line.lstrip())
|
| 202 |
+
# A new top-level section ends the Args block.
|
| 203 |
+
if indent <= section_indent and line.strip().endswith(":"):
|
| 204 |
+
break
|
| 205 |
+
if arg_indent is None:
|
| 206 |
+
arg_indent = indent
|
| 207 |
+
if indent == arg_indent:
|
| 208 |
+
_flush()
|
| 209 |
+
current_lines = []
|
| 210 |
+
match = name_pattern.match(line.strip())
|
| 211 |
+
if match:
|
| 212 |
+
current_name = match.group(1)
|
| 213 |
+
tail = match.group(2).strip()
|
| 214 |
+
if tail:
|
| 215 |
+
current_lines.append(tail)
|
| 216 |
+
else:
|
| 217 |
+
current_name = None
|
| 218 |
+
elif current_name is not None and indent > arg_indent:
|
| 219 |
+
current_lines.append(line.strip())
|
| 220 |
+
_flush()
|
| 221 |
+
return descriptions
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
class SchemaCommand(BaseDiffusersCLICommand):
|
| 225 |
+
task = "schema"
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
def register_subcommand(subparsers: _SubParsersAction) -> None:
|
| 229 |
+
from argparse import RawDescriptionHelpFormatter
|
| 230 |
+
|
| 231 |
+
epilog = (
|
| 232 |
+
"Examples\n"
|
| 233 |
+
" $ diffusers-cli schema -m stabilityai/stable-diffusion-xl-base-1.0\n"
|
| 234 |
+
" $ diffusers-cli schema -m black-forest-labs/FLUX.1-dev --verbose\n"
|
| 235 |
+
" $ diffusers-cli --format json schema -m stabilityai/stable-diffusion-xl-base-1.0\n"
|
| 236 |
+
"\n"
|
| 237 |
+
"Learn more\n"
|
| 238 |
+
" Use `diffusers-cli <command> --help` for more information about a command.\n"
|
| 239 |
+
" Read the documentation at https://huggingface.co/docs/diffusers\n"
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
parser: ArgumentParser = subparsers.add_parser(
|
| 243 |
+
"schema",
|
| 244 |
+
help="Print the input schema for a diffusers pipeline repo. No weights downloaded.",
|
| 245 |
+
usage="\n diffusers-cli schema [options]",
|
| 246 |
+
epilog=epilog,
|
| 247 |
+
formatter_class=RawDescriptionHelpFormatter,
|
| 248 |
+
)
|
| 249 |
+
parser._optionals.title = "Options"
|
| 250 |
+
parser.add_argument(
|
| 251 |
+
"--model",
|
| 252 |
+
"-m",
|
| 253 |
+
required=True,
|
| 254 |
+
help="Model id on the Hugging Face Hub or local path.",
|
| 255 |
+
)
|
| 256 |
+
parser.add_argument(
|
| 257 |
+
"--revision",
|
| 258 |
+
default=None,
|
| 259 |
+
help="Model revision (branch, tag, or commit SHA).",
|
| 260 |
+
)
|
| 261 |
+
parser.add_argument(
|
| 262 |
+
"--token",
|
| 263 |
+
default=None,
|
| 264 |
+
help="Hugging Face token for gated/private models.",
|
| 265 |
+
)
|
| 266 |
+
parser.add_argument(
|
| 267 |
+
"--trust-remote-code",
|
| 268 |
+
action="store_true",
|
| 269 |
+
help="Allow custom code from the Hub (required for modular pipelines that ship block code).",
|
| 270 |
+
)
|
| 271 |
+
parser.add_argument(
|
| 272 |
+
"--verbose",
|
| 273 |
+
"-v",
|
| 274 |
+
action="store_true",
|
| 275 |
+
help=(
|
| 276 |
+
"Also include per-argument descriptions from the pipeline's __call__ docstring. "
|
| 277 |
+
"Modular pipelines always include block-declared descriptions; --verbose populates "
|
| 278 |
+
"the equivalent field for standard pipelines by parsing the Google-style Args: block."
|
| 279 |
+
),
|
| 280 |
+
)
|
| 281 |
+
parser.set_defaults(func=SchemaCommand)
|
| 282 |
+
|
| 283 |
+
def __init__(self, args: Namespace):
|
| 284 |
+
self.args = args
|
| 285 |
+
|
| 286 |
+
def run(self) -> None:
|
| 287 |
+
_schema(self.args)
|
diffusers/commands/skills.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""`diffusers-cli skills` — install Agent Skills bundles.
|
| 15 |
+
|
| 16 |
+
Skill bundles live under `.ai/skills/<name>/` in the diffusers repo and follow the Agent Skills standard: a directory
|
| 17 |
+
containing `SKILL.md` (plus optional resources), including the `references/` copies of the guides it cites. Claude Code
|
| 18 |
+
gets a plugin bundle under `.claude/skills/`; Codex and Cursor get `.agents/skills/<name>/`.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import re
|
| 26 |
+
import shutil
|
| 27 |
+
import textwrap
|
| 28 |
+
from argparse import ArgumentParser, Namespace, _SubParsersAction
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
|
| 31 |
+
from huggingface_hub.cli._output import out
|
| 32 |
+
from huggingface_hub.utils import httpx
|
| 33 |
+
|
| 34 |
+
from ..utils import logging
|
| 35 |
+
from ..utils.constants import DIFFUSERS_REQUEST_TIMEOUT
|
| 36 |
+
from . import BaseDiffusersCLICommand
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
logger = logging.get_logger("diffusers-cli/skills")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
_REGISTRY_BASE = "https://api.github.com/repos/huggingface/diffusers/contents/.ai/skills"
|
| 43 |
+
_RAW_BASE = "https://raw.githubusercontent.com/huggingface/diffusers"
|
| 44 |
+
_REGISTRY_REF = "main"
|
| 45 |
+
|
| 46 |
+
# How a skill cites a guide, e.g. `](references/models.md)` or `` `references/testing.md` ``. The install copies exactly
|
| 47 |
+
# the guides named this way, so a skill only has to cite one to get it.
|
| 48 |
+
_CITATION = re.compile(r"(?<![./\w])references/([\w-]+\.md)")
|
| 49 |
+
|
| 50 |
+
# Native skill-discovery paths per agent. Claude Code reads only `.claude/skills/`; Codex and
|
| 51 |
+
# Cursor read `.agents/skills/` (Cursor also honors `.claude/skills/` via compat, but installing
|
| 52 |
+
# to `.agents/skills/` is the portable choice for both).
|
| 53 |
+
_CLAUDE_SKILLS_DIR = Path(".claude") / "skills"
|
| 54 |
+
_AGENTS_SKILLS_DIR = Path(".agents") / "skills"
|
| 55 |
+
|
| 56 |
+
# Env vars set by each agent when it launches the CLI. Values are the install path to use.
|
| 57 |
+
_AGENT_ENV_TO_DIR: dict[str, Path] = {
|
| 58 |
+
"CLAUDECODE": _CLAUDE_SKILLS_DIR,
|
| 59 |
+
"CLAUDE_CODE": _CLAUDE_SKILLS_DIR,
|
| 60 |
+
"CODEX_SANDBOX": _AGENTS_SKILLS_DIR,
|
| 61 |
+
"CURSOR_AI": _AGENTS_SKILLS_DIR,
|
| 62 |
+
}
|
| 63 |
+
# When no agent env var is set, install to every native path so whichever agent the user
|
| 64 |
+
# later switches to picks the skill up.
|
| 65 |
+
_ALL_INSTALL_DIRS: tuple[Path, ...] = (_CLAUDE_SKILLS_DIR, _AGENTS_SKILLS_DIR)
|
| 66 |
+
|
| 67 |
+
# Empty marker dropped inside each installed skill dir so `update` can distinguish our
|
| 68 |
+
# installs from user-placed skills at the same paths.
|
| 69 |
+
_MANAGED_MARKER_FILE = ".diffusers-skill-managed"
|
| 70 |
+
|
| 71 |
+
# Claude Code loads any directory under a skills root that carries a `.claude-plugin/plugin.json`
|
| 72 |
+
# as a plugin, which namespaces the skills it holds as `/diffusers:<name>` and matches what the
|
| 73 |
+
# published plugin gives contributors. Codex and Cursor have no equivalent, so they keep the plain
|
| 74 |
+
# one-skill-per-directory layout their discovery expects.
|
| 75 |
+
_PLUGIN_NAME = "diffusers"
|
| 76 |
+
_PLUGIN_MANIFEST = {
|
| 77 |
+
"name": _PLUGIN_NAME,
|
| 78 |
+
"description": "Conventions and task skills for contributing to diffusers",
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# ---------------------------------------------------------------------------
|
| 83 |
+
# Registry fetch
|
| 84 |
+
# ---------------------------------------------------------------------------
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _registry_url(name: str = "") -> str:
|
| 88 |
+
"""API URL for the registry root, or for a single skill bundle when `name` is given."""
|
| 89 |
+
path = f"/{name}" if name else ""
|
| 90 |
+
return f"{_REGISTRY_BASE}{path}?ref={_REGISTRY_REF}"
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _skill_md_url(name: str) -> str:
|
| 94 |
+
"""Raw URL of a skill's SKILL.md, so `list` can read descriptions without walking each bundle."""
|
| 95 |
+
return f"{_RAW_BASE}/{_REGISTRY_REF}/.ai/skills/{name}/SKILL.md"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _fetch_json(url: str) -> list[dict]:
|
| 99 |
+
try:
|
| 100 |
+
resp = httpx.get(url, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 101 |
+
resp.raise_for_status()
|
| 102 |
+
return resp.json()
|
| 103 |
+
except httpx.HTTPStatusError as e:
|
| 104 |
+
if e.response.status_code == 404:
|
| 105 |
+
raise SystemExit(f"Not found in registry: {url}") from e
|
| 106 |
+
raise SystemExit(f"Registry fetch failed: HTTP {e.response.status_code} {e.response.reason_phrase}") from e
|
| 107 |
+
except httpx.HTTPError as e:
|
| 108 |
+
raise SystemExit(f"Could not reach registry: {e}") from e
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _walk_skill_files(name: str) -> list[tuple[str, str]]:
|
| 112 |
+
files: list[tuple[str, str]] = []
|
| 113 |
+
|
| 114 |
+
def _walk(api_url: str, prefix: str) -> None:
|
| 115 |
+
for entry in _fetch_json(api_url):
|
| 116 |
+
if entry["type"] == "file":
|
| 117 |
+
files.append((f"{prefix}{entry['name']}", entry["download_url"]))
|
| 118 |
+
elif entry["type"] == "dir":
|
| 119 |
+
_walk(entry["url"], f"{prefix}{entry['name']}/")
|
| 120 |
+
|
| 121 |
+
_walk(_registry_url(name), "")
|
| 122 |
+
return files
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _skill_description(skill_md: str) -> str:
|
| 126 |
+
"""The `description` from a SKILL.md YAML frontmatter, collapsed onto one line.
|
| 127 |
+
|
| 128 |
+
Handles both `description: text` and the folded `description: >` form the skills in this repo use. Returns an empty
|
| 129 |
+
string when the file has no frontmatter or no description.
|
| 130 |
+
"""
|
| 131 |
+
lines = skill_md.splitlines()
|
| 132 |
+
if not lines or lines[0].strip() != "---":
|
| 133 |
+
return ""
|
| 134 |
+
for index, line in enumerate(lines[1:], start=1):
|
| 135 |
+
if line.strip() == "---":
|
| 136 |
+
return ""
|
| 137 |
+
if not line.startswith("description:"):
|
| 138 |
+
continue
|
| 139 |
+
value = line[len("description:") :].strip()
|
| 140 |
+
if value and value not in (">", "|", ">-", "|-"):
|
| 141 |
+
return value
|
| 142 |
+
folded = []
|
| 143 |
+
for continuation in lines[index + 1 :]:
|
| 144 |
+
if continuation.strip() == "---" or not continuation.startswith((" ", "\t")):
|
| 145 |
+
break
|
| 146 |
+
folded.append(continuation.strip())
|
| 147 |
+
return " ".join(folded)
|
| 148 |
+
return ""
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _download_skill_bundle(name: str) -> dict[str, bytes]:
|
| 152 |
+
files = _walk_skill_files(name)
|
| 153 |
+
if not files:
|
| 154 |
+
raise SystemExit(f"Skill '{name}' has no files in the registry.")
|
| 155 |
+
bundle: dict[str, bytes] = {}
|
| 156 |
+
for rel_path, url in files:
|
| 157 |
+
resp = httpx.get(url, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 158 |
+
resp.raise_for_status()
|
| 159 |
+
bundle[rel_path] = resp.content
|
| 160 |
+
return bundle
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ---------------------------------------------------------------------------
|
| 164 |
+
# Install / discovery
|
| 165 |
+
# ---------------------------------------------------------------------------
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _detect_install_dirs() -> tuple[Path, ...]:
|
| 169 |
+
"""Pick where to install based on the launching agent.
|
| 170 |
+
|
| 171 |
+
If we detect a specific agent from its env var, install only there. If nothing is detected, install to every native
|
| 172 |
+
path so any agent picks the skill up later.
|
| 173 |
+
"""
|
| 174 |
+
for env_var, skills_dir in _AGENT_ENV_TO_DIR.items():
|
| 175 |
+
if os.environ.get(env_var):
|
| 176 |
+
return (skills_dir,)
|
| 177 |
+
return _ALL_INSTALL_DIRS
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _skill_dir(root: Path, skills_dir: Path, name: str) -> Path:
|
| 181 |
+
"""Where a single skill lands, which differs by target: a plugin bundle for Claude Code, a bare
|
| 182 |
+
directory for Codex and Cursor."""
|
| 183 |
+
if skills_dir == _CLAUDE_SKILLS_DIR:
|
| 184 |
+
return root / skills_dir / _PLUGIN_NAME / "skills" / name
|
| 185 |
+
return root / skills_dir / name
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _install_references(skill_dir: Path, skill_md: bytes) -> None:
|
| 189 |
+
"""Copy the guides a skill cites into its own `references/` directory.
|
| 190 |
+
|
| 191 |
+
A skill cites a guide as `references/<guide>.md`, so the guides it needs are exactly the ones its SKILL.md names.
|
| 192 |
+
The repo keeps one copy of each under `.ai/references/`; an install gets its own so the skill is self-contained.
|
| 193 |
+
"""
|
| 194 |
+
references_dir = skill_dir / "references"
|
| 195 |
+
for guide in sorted(set(_CITATION.findall(skill_md.decode()))):
|
| 196 |
+
resp = httpx.get(f"{_RAW_BASE}/{_REGISTRY_REF}/.ai/references/{guide}", timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 197 |
+
resp.raise_for_status()
|
| 198 |
+
references_dir.mkdir(parents=True, exist_ok=True)
|
| 199 |
+
(references_dir / guide).write_bytes(resp.content)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def _install_skill(name: str, bundle: dict[str, bytes], root: Path, skills_dir: Path, force: bool) -> Path:
|
| 203 |
+
skill_dir = _skill_dir(root, skills_dir, name)
|
| 204 |
+
if skill_dir.exists():
|
| 205 |
+
if not force:
|
| 206 |
+
raise SystemExit(f"Skill already installed at {skill_dir}. Use --force to reinstall.")
|
| 207 |
+
shutil.rmtree(skill_dir)
|
| 208 |
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
| 209 |
+
for rel_path, data in bundle.items():
|
| 210 |
+
target = skill_dir / rel_path
|
| 211 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 212 |
+
target.write_bytes(data)
|
| 213 |
+
(skill_dir / _MANAGED_MARKER_FILE).touch()
|
| 214 |
+
_install_references(skill_dir, bundle["SKILL.md"])
|
| 215 |
+
|
| 216 |
+
if skills_dir == _CLAUDE_SKILLS_DIR:
|
| 217 |
+
manifest = root / skills_dir / _PLUGIN_NAME / ".claude-plugin" / "plugin.json"
|
| 218 |
+
manifest.parent.mkdir(parents=True, exist_ok=True)
|
| 219 |
+
manifest.write_text(json.dumps(_PLUGIN_MANIFEST, indent=2) + "\n")
|
| 220 |
+
|
| 221 |
+
return skill_dir
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _has_local_changes(skill_dir: Path, bundle: dict[str, bytes]) -> bool:
|
| 225 |
+
"""True if the installed skill has any file that differs from `bundle` or has extra files.
|
| 226 |
+
|
| 227 |
+
The marker file and the copied `references/` guides are ignored — neither comes from the skill bundle. Compares raw
|
| 228 |
+
bytes so a whitespace-only edit still counts as dirty.
|
| 229 |
+
"""
|
| 230 |
+
on_disk: dict[str, bytes] = {}
|
| 231 |
+
for path in skill_dir.rglob("*"):
|
| 232 |
+
if not path.is_file():
|
| 233 |
+
continue
|
| 234 |
+
rel = str(path.relative_to(skill_dir))
|
| 235 |
+
if rel.startswith("references/"):
|
| 236 |
+
continue
|
| 237 |
+
if rel == _MANAGED_MARKER_FILE:
|
| 238 |
+
continue
|
| 239 |
+
on_disk[rel] = path.read_bytes()
|
| 240 |
+
return on_disk != bundle
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def _discover_installed(root: Path) -> list[tuple[Path, str]]:
|
| 244 |
+
"""Return `(skills_dir, name)` pairs for every managed install under `root`."""
|
| 245 |
+
found: list[tuple[Path, str]] = []
|
| 246 |
+
for skills_dir in _ALL_INSTALL_DIRS:
|
| 247 |
+
skills_root = root / skills_dir
|
| 248 |
+
if skills_dir == _CLAUDE_SKILLS_DIR:
|
| 249 |
+
skills_root = skills_root / _PLUGIN_NAME / "skills"
|
| 250 |
+
if not skills_root.exists():
|
| 251 |
+
continue
|
| 252 |
+
for d in sorted(skills_root.iterdir()):
|
| 253 |
+
if d.is_dir() and (d / _MANAGED_MARKER_FILE).exists():
|
| 254 |
+
found.append((skills_dir, d.name))
|
| 255 |
+
return found
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class SkillsCommand(BaseDiffusersCLICommand):
|
| 259 |
+
@staticmethod
|
| 260 |
+
def register_subcommand(subparsers: _SubParsersAction) -> None:
|
| 261 |
+
parser: ArgumentParser = subparsers.add_parser(
|
| 262 |
+
"skills",
|
| 263 |
+
help="Manage Agent Skills for AI assistants.",
|
| 264 |
+
usage="\n diffusers-cli skills <add|list|update|preview> [options]",
|
| 265 |
+
)
|
| 266 |
+
parser._optionals.title = "Options"
|
| 267 |
+
actions = parser.add_subparsers(dest="skills_action", required=True, metavar="<action>")
|
| 268 |
+
|
| 269 |
+
add = actions.add_parser("add", help="Download and install a skill.")
|
| 270 |
+
add.add_argument(
|
| 271 |
+
"name",
|
| 272 |
+
nargs="?",
|
| 273 |
+
default=None,
|
| 274 |
+
help="Skill name (e.g. diffusers-cli, custom-blocks). Omit and pass --all to install every skill.",
|
| 275 |
+
)
|
| 276 |
+
add.add_argument(
|
| 277 |
+
"--all",
|
| 278 |
+
dest="install_all",
|
| 279 |
+
action="store_true",
|
| 280 |
+
help="Install every skill in the registry. Mutually exclusive with a positional name.",
|
| 281 |
+
)
|
| 282 |
+
target = add.add_mutually_exclusive_group()
|
| 283 |
+
target.add_argument(
|
| 284 |
+
"--claude",
|
| 285 |
+
dest="install_dirs",
|
| 286 |
+
action="store_const",
|
| 287 |
+
const=(_CLAUDE_SKILLS_DIR,),
|
| 288 |
+
help="Install for Claude Code only, instead of detecting the agent from the environment.",
|
| 289 |
+
)
|
| 290 |
+
target.add_argument(
|
| 291 |
+
"--codex",
|
| 292 |
+
dest="install_dirs",
|
| 293 |
+
action="store_const",
|
| 294 |
+
const=(_AGENTS_SKILLS_DIR,),
|
| 295 |
+
help="Install for Codex only.",
|
| 296 |
+
)
|
| 297 |
+
target.add_argument(
|
| 298 |
+
"--cursor",
|
| 299 |
+
dest="install_dirs",
|
| 300 |
+
action="store_const",
|
| 301 |
+
const=(_AGENTS_SKILLS_DIR,),
|
| 302 |
+
help="Install for Cursor only.",
|
| 303 |
+
)
|
| 304 |
+
add.add_argument(
|
| 305 |
+
"--global",
|
| 306 |
+
"-g",
|
| 307 |
+
dest="install_global",
|
| 308 |
+
action="store_true",
|
| 309 |
+
help="Install globally (user-level) instead of in the current project directory.",
|
| 310 |
+
)
|
| 311 |
+
add.add_argument("--force", action="store_true", help="Overwrite existing skills in the destination.")
|
| 312 |
+
add.set_defaults(func=SkillsCommand)
|
| 313 |
+
|
| 314 |
+
list_action = actions.add_parser("list", help="List available skills in the registry.")
|
| 315 |
+
list_action.set_defaults(func=SkillsCommand)
|
| 316 |
+
|
| 317 |
+
update = actions.add_parser("update", help="Re-download and reinstall managed skills.")
|
| 318 |
+
update.add_argument(
|
| 319 |
+
"name",
|
| 320 |
+
nargs="?",
|
| 321 |
+
default=None,
|
| 322 |
+
help="Optional installed skill name to update. Omit to update every managed skill.",
|
| 323 |
+
)
|
| 324 |
+
update.add_argument(
|
| 325 |
+
"--global",
|
| 326 |
+
"-g",
|
| 327 |
+
dest="install_global",
|
| 328 |
+
action="store_true",
|
| 329 |
+
help="Update skills installed globally (user-level) instead of the current project.",
|
| 330 |
+
)
|
| 331 |
+
update.add_argument(
|
| 332 |
+
"--force",
|
| 333 |
+
action="store_true",
|
| 334 |
+
help="Overwrite skills even if they have local modifications since install.",
|
| 335 |
+
)
|
| 336 |
+
update.set_defaults(func=SkillsCommand)
|
| 337 |
+
|
| 338 |
+
preview = actions.add_parser("preview", help="Print a skill's SKILL.md from the registry.")
|
| 339 |
+
preview.add_argument("name", help="Skill name to preview.")
|
| 340 |
+
preview.set_defaults(func=SkillsCommand)
|
| 341 |
+
|
| 342 |
+
def __init__(self, args: Namespace):
|
| 343 |
+
self.args = args
|
| 344 |
+
|
| 345 |
+
def run(self) -> None:
|
| 346 |
+
if self.args.skills_action == "add":
|
| 347 |
+
self._add()
|
| 348 |
+
elif self.args.skills_action == "list":
|
| 349 |
+
self._list()
|
| 350 |
+
elif self.args.skills_action == "update":
|
| 351 |
+
self._update()
|
| 352 |
+
elif self.args.skills_action == "preview":
|
| 353 |
+
self._preview()
|
| 354 |
+
|
| 355 |
+
def _add(self) -> None:
|
| 356 |
+
if self.args.install_all and self.args.name:
|
| 357 |
+
raise SystemExit("--all and a positional skill name are mutually exclusive.")
|
| 358 |
+
if not self.args.install_all and not self.args.name:
|
| 359 |
+
raise SystemExit("Pass a skill name (e.g. diffusers-cli) or --all to install every skill.")
|
| 360 |
+
|
| 361 |
+
root = Path.home() if self.args.install_global else Path.cwd()
|
| 362 |
+
install_dirs = self.args.install_dirs or _detect_install_dirs()
|
| 363 |
+
names = self._resolve_names()
|
| 364 |
+
|
| 365 |
+
installed: list[str] = []
|
| 366 |
+
failed: list[str] = []
|
| 367 |
+
for name in names:
|
| 368 |
+
try:
|
| 369 |
+
bundle = _download_skill_bundle(name)
|
| 370 |
+
for skills_dir in install_dirs:
|
| 371 |
+
_install_skill(name, bundle, root, skills_dir, self.args.force)
|
| 372 |
+
installed.append(name)
|
| 373 |
+
except (SystemExit, httpx.HTTPError) as e:
|
| 374 |
+
# Downgrade to a warning so one broken skill doesn't abort the batch.
|
| 375 |
+
logger.warning(f"Skipping skill {name!r}: {e}")
|
| 376 |
+
failed.append(name)
|
| 377 |
+
|
| 378 |
+
if not installed:
|
| 379 |
+
raise SystemExit(f"No skills installed. Failed: {failed}")
|
| 380 |
+
|
| 381 |
+
out.result(
|
| 382 |
+
f"Installed {len(installed)} skill(s)",
|
| 383 |
+
installed=", ".join(installed),
|
| 384 |
+
failed=", ".join(failed) if failed else None,
|
| 385 |
+
paths=", ".join(str(root / d) for d in install_dirs),
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
def _update(self) -> None:
|
| 389 |
+
root = Path.home() if self.args.install_global else Path.cwd()
|
| 390 |
+
installed = _discover_installed(root)
|
| 391 |
+
if self.args.name is not None:
|
| 392 |
+
installed = [entry for entry in installed if entry[1] == self.args.name]
|
| 393 |
+
if not installed:
|
| 394 |
+
raise SystemExit(f"No installed skill named {self.args.name!r} found under {root}.")
|
| 395 |
+
if not installed:
|
| 396 |
+
raise SystemExit(f"No managed skills found under {root}.")
|
| 397 |
+
|
| 398 |
+
# Group by skill name so we redownload each bundle once even if it's installed to
|
| 399 |
+
# multiple locations (e.g. both .claude/skills/ and .agents/skills/).
|
| 400 |
+
by_name: dict[str, list[Path]] = {}
|
| 401 |
+
for skills_dir, name in installed:
|
| 402 |
+
by_name.setdefault(name, []).append(skills_dir)
|
| 403 |
+
|
| 404 |
+
updated: list[str] = []
|
| 405 |
+
failed: list[str] = []
|
| 406 |
+
skipped: list[str] = []
|
| 407 |
+
for name, dirs in sorted(by_name.items()):
|
| 408 |
+
try:
|
| 409 |
+
bundle = _download_skill_bundle(name)
|
| 410 |
+
for skills_dir in dirs:
|
| 411 |
+
skill_dir = _skill_dir(root, skills_dir, name)
|
| 412 |
+
if not self.args.force and _has_local_changes(skill_dir, bundle):
|
| 413 |
+
logger.warning(
|
| 414 |
+
f"Skill {name!r} at {skill_dir} has local modifications; "
|
| 415 |
+
"skipping. Pass --force to overwrite them."
|
| 416 |
+
)
|
| 417 |
+
skipped.append(name)
|
| 418 |
+
continue
|
| 419 |
+
_install_skill(name, bundle, root, skills_dir, force=True)
|
| 420 |
+
updated.append(name)
|
| 421 |
+
except (SystemExit, httpx.HTTPError) as e:
|
| 422 |
+
logger.warning(f"Skipping skill {name!r}: {e}")
|
| 423 |
+
failed.append(name)
|
| 424 |
+
|
| 425 |
+
out.result(
|
| 426 |
+
f"Updated {len(updated)} skill(s)",
|
| 427 |
+
updated=", ".join(updated),
|
| 428 |
+
skipped=", ".join(skipped) if skipped else None,
|
| 429 |
+
failed=", ".join(failed) if failed else None,
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
def _preview(self) -> None:
|
| 433 |
+
bundle = _download_skill_bundle(self.args.name)
|
| 434 |
+
skill_md = bundle.get("SKILL.md")
|
| 435 |
+
if skill_md is None:
|
| 436 |
+
raise SystemExit(f"Skill {self.args.name!r} has no SKILL.md in the registry.")
|
| 437 |
+
print(skill_md.decode())
|
| 438 |
+
|
| 439 |
+
def _list(self) -> None:
|
| 440 |
+
entries = _fetch_json(_registry_url())
|
| 441 |
+
names = sorted(e["name"] for e in entries if e["type"] == "dir" and not e["name"].startswith("."))
|
| 442 |
+
if not names:
|
| 443 |
+
raise SystemExit("No skills found in registry.")
|
| 444 |
+
|
| 445 |
+
# Descriptions run several sentences, so they get a wrapped block under each name rather than a table cell.
|
| 446 |
+
width = min(shutil.get_terminal_size().columns, 100)
|
| 447 |
+
installed = {name for _, name in _discover_installed(Path.cwd())}
|
| 448 |
+
for name in names:
|
| 449 |
+
resp = httpx.get(_skill_md_url(name), timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 450 |
+
resp.raise_for_status()
|
| 451 |
+
description = _skill_description(resp.text)
|
| 452 |
+
print(f"\n{name}{' (installed)' if name in installed else ''}")
|
| 453 |
+
print(textwrap.fill(description, width=width, initial_indent=" ", subsequent_indent=" "))
|
| 454 |
+
|
| 455 |
+
def _resolve_names(self) -> list[str]:
|
| 456 |
+
if self.args.install_all:
|
| 457 |
+
entries = _fetch_json(_registry_url())
|
| 458 |
+
return sorted(e["name"] for e in entries if e["type"] == "dir" and not e["name"].startswith("."))
|
| 459 |
+
return [self.args.name]
|
diffusers/configuration_utils.py
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
# Copyright 2025 The HuggingFace Inc. team.
|
| 3 |
+
# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
|
| 4 |
+
#
|
| 5 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
+
# you may not use this file except in compliance with the License.
|
| 7 |
+
# You may obtain a copy of the License at
|
| 8 |
+
#
|
| 9 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
#
|
| 11 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
# See the License for the specific language governing permissions and
|
| 15 |
+
# limitations under the License.
|
| 16 |
+
"""ConfigMixin base class and utilities."""
|
| 17 |
+
|
| 18 |
+
import functools
|
| 19 |
+
import importlib
|
| 20 |
+
import inspect
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import re
|
| 24 |
+
from collections import OrderedDict
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Any
|
| 27 |
+
|
| 28 |
+
import numpy as np
|
| 29 |
+
from huggingface_hub import create_repo, hf_hub_download
|
| 30 |
+
from huggingface_hub.utils import (
|
| 31 |
+
EntryNotFoundError,
|
| 32 |
+
HfHubHTTPError,
|
| 33 |
+
RepositoryNotFoundError,
|
| 34 |
+
RevisionNotFoundError,
|
| 35 |
+
validate_hf_hub_args,
|
| 36 |
+
)
|
| 37 |
+
from typing_extensions import Self
|
| 38 |
+
|
| 39 |
+
from . import __version__
|
| 40 |
+
from .utils import (
|
| 41 |
+
HUGGINGFACE_CO_RESOLVE_ENDPOINT,
|
| 42 |
+
DummyObject,
|
| 43 |
+
deprecate,
|
| 44 |
+
extract_commit_hash,
|
| 45 |
+
http_user_agent,
|
| 46 |
+
logging,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
logger = logging.get_logger(__name__)
|
| 51 |
+
|
| 52 |
+
_re_configuration_file = re.compile(r"config\.(.*)\.json")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class FrozenDict(OrderedDict):
|
| 56 |
+
def __init__(self, *args, **kwargs):
|
| 57 |
+
super().__init__(*args, **kwargs)
|
| 58 |
+
|
| 59 |
+
for key, value in self.items():
|
| 60 |
+
setattr(self, key, value)
|
| 61 |
+
|
| 62 |
+
self.__frozen = True
|
| 63 |
+
|
| 64 |
+
def __delitem__(self, *args, **kwargs):
|
| 65 |
+
raise Exception(f"You cannot use ``__delitem__`` on a {self.__class__.__name__} instance.")
|
| 66 |
+
|
| 67 |
+
def setdefault(self, *args, **kwargs):
|
| 68 |
+
raise Exception(f"You cannot use ``setdefault`` on a {self.__class__.__name__} instance.")
|
| 69 |
+
|
| 70 |
+
def pop(self, *args, **kwargs):
|
| 71 |
+
raise Exception(f"You cannot use ``pop`` on a {self.__class__.__name__} instance.")
|
| 72 |
+
|
| 73 |
+
def update(self, *args, **kwargs):
|
| 74 |
+
raise Exception(f"You cannot use ``update`` on a {self.__class__.__name__} instance.")
|
| 75 |
+
|
| 76 |
+
def __setattr__(self, name, value):
|
| 77 |
+
if hasattr(self, "__frozen") and self.__frozen:
|
| 78 |
+
raise Exception(f"You cannot use ``__setattr__`` on a {self.__class__.__name__} instance.")
|
| 79 |
+
super().__setattr__(name, value)
|
| 80 |
+
|
| 81 |
+
def __setitem__(self, name, value):
|
| 82 |
+
if hasattr(self, "__frozen") and self.__frozen:
|
| 83 |
+
raise Exception(f"You cannot use ``__setattr__`` on a {self.__class__.__name__} instance.")
|
| 84 |
+
super().__setitem__(name, value)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class ConfigMixin:
|
| 88 |
+
r"""
|
| 89 |
+
Base class for all configuration classes. All configuration parameters are stored under `self.config`. Also
|
| 90 |
+
provides the [`~ConfigMixin.from_config`] and [`~ConfigMixin.save_config`] methods for loading, downloading, and
|
| 91 |
+
saving classes that inherit from [`ConfigMixin`].
|
| 92 |
+
|
| 93 |
+
Class attributes:
|
| 94 |
+
- **config_name** (`str`) -- A filename under which the config should stored when calling
|
| 95 |
+
[`~ConfigMixin.save_config`] (should be overridden by parent class).
|
| 96 |
+
- **ignore_for_config** (`list[str]`) -- A list of attributes that should not be saved in the config (should be
|
| 97 |
+
overridden by subclass).
|
| 98 |
+
- **has_compatibles** (`bool`) -- Whether the class has compatible classes (should be overridden by subclass).
|
| 99 |
+
- **_deprecated_kwargs** (`list[str]`) -- Keyword arguments that are deprecated. Note that the `init` function
|
| 100 |
+
should only have a `kwargs` argument if at least one argument is deprecated (should be overridden by
|
| 101 |
+
subclass).
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
config_name = None
|
| 105 |
+
ignore_for_config = []
|
| 106 |
+
has_compatibles = False
|
| 107 |
+
|
| 108 |
+
_deprecated_kwargs = []
|
| 109 |
+
_auto_class = None
|
| 110 |
+
|
| 111 |
+
@classmethod
|
| 112 |
+
def register_for_auto_class(cls, auto_class="AutoModel"):
|
| 113 |
+
"""
|
| 114 |
+
Register this class with the given auto class so that it can be loaded with `AutoModel.from_pretrained(...,
|
| 115 |
+
trust_remote_code=True)`.
|
| 116 |
+
|
| 117 |
+
When the config is saved, the resulting `config.json` will include an `auto_map` entry mapping the auto class
|
| 118 |
+
to this class's module and class name.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
auto_class (`str` or type, *optional*, defaults to `"AutoModel"`):
|
| 122 |
+
The auto class to register this class with. Can be a string (e.g. `"AutoModel"`) or the class itself.
|
| 123 |
+
Currently only `"AutoModel"` is supported.
|
| 124 |
+
|
| 125 |
+
Example:
|
| 126 |
+
|
| 127 |
+
```python
|
| 128 |
+
from diffusers import ModelMixin, ConfigMixin
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class MyCustomModel(ModelMixin, ConfigMixin): ...
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
MyCustomModel.register_for_auto_class("AutoModel")
|
| 135 |
+
```
|
| 136 |
+
"""
|
| 137 |
+
if auto_class != "AutoModel":
|
| 138 |
+
raise ValueError(f"Only 'AutoModel' is supported, got '{auto_class}'.")
|
| 139 |
+
|
| 140 |
+
cls._auto_class = auto_class
|
| 141 |
+
|
| 142 |
+
def register_to_config(self, **kwargs):
|
| 143 |
+
if self.config_name is None:
|
| 144 |
+
raise NotImplementedError(f"Make sure that {self.__class__} has defined a class name `config_name`")
|
| 145 |
+
# Special case for `kwargs` used in deprecation warning added to schedulers
|
| 146 |
+
# TODO: remove this when we remove the deprecation warning, and the `kwargs` argument,
|
| 147 |
+
# or solve in a more general way.
|
| 148 |
+
kwargs.pop("kwargs", None)
|
| 149 |
+
|
| 150 |
+
if not hasattr(self, "_internal_dict"):
|
| 151 |
+
internal_dict = kwargs
|
| 152 |
+
else:
|
| 153 |
+
previous_dict = dict(self._internal_dict)
|
| 154 |
+
internal_dict = {**self._internal_dict, **kwargs}
|
| 155 |
+
logger.debug(f"Updating config from {previous_dict} to {internal_dict}")
|
| 156 |
+
|
| 157 |
+
self._internal_dict = FrozenDict(internal_dict)
|
| 158 |
+
|
| 159 |
+
def __getattr__(self, name: str) -> Any:
|
| 160 |
+
"""The only reason we overwrite `getattr` here is to gracefully deprecate accessing
|
| 161 |
+
config attributes directly. See https://github.com/huggingface/diffusers/pull/3129
|
| 162 |
+
|
| 163 |
+
This function is mostly copied from PyTorch's __getattr__ overwrite:
|
| 164 |
+
https://pytorch.org/docs/stable/_modules/torch/nn/modules/module.html#Module
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
is_in_config = "_internal_dict" in self.__dict__ and hasattr(self.__dict__["_internal_dict"], name)
|
| 168 |
+
is_attribute = name in self.__dict__
|
| 169 |
+
|
| 170 |
+
if is_in_config and not is_attribute:
|
| 171 |
+
deprecation_message = f"Accessing config attribute `{name}` directly via '{type(self).__name__}' object attribute is deprecated. Please access '{name}' over '{type(self).__name__}'s config object instead, e.g. 'scheduler.config.{name}'."
|
| 172 |
+
deprecate("direct config name access", "1.0.0", deprecation_message, standard_warn=False)
|
| 173 |
+
return self._internal_dict[name]
|
| 174 |
+
|
| 175 |
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
| 176 |
+
|
| 177 |
+
def save_config(self, save_directory: str | os.PathLike, push_to_hub: bool = False, **kwargs):
|
| 178 |
+
"""
|
| 179 |
+
Save a configuration object to the directory specified in `save_directory` so that it can be reloaded using the
|
| 180 |
+
[`~ConfigMixin.from_config`] class method.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
save_directory (`str` or `os.PathLike`):
|
| 184 |
+
Directory where the configuration JSON file is saved (will be created if it does not exist).
|
| 185 |
+
push_to_hub (`bool`, *optional*, defaults to `False`):
|
| 186 |
+
Whether or not to push your model to the Hugging Face Hub after saving it. You can specify the
|
| 187 |
+
repository you want to push to with `repo_id` (will default to the name of `save_directory` in your
|
| 188 |
+
namespace).
|
| 189 |
+
kwargs (`dict[str, Any]`, *optional*):
|
| 190 |
+
Additional keyword arguments passed along to the [`~utils.PushToHubMixin.push_to_hub`] method.
|
| 191 |
+
"""
|
| 192 |
+
if os.path.isfile(save_directory):
|
| 193 |
+
raise AssertionError(f"Provided path ({save_directory}) should be a directory, not a file")
|
| 194 |
+
|
| 195 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 196 |
+
|
| 197 |
+
# If we save using the predefined names, we can load using `from_config`
|
| 198 |
+
output_config_file = os.path.join(save_directory, self.config_name)
|
| 199 |
+
|
| 200 |
+
self.to_json_file(output_config_file)
|
| 201 |
+
logger.info(f"Configuration saved in {output_config_file}")
|
| 202 |
+
|
| 203 |
+
if push_to_hub:
|
| 204 |
+
commit_message = kwargs.pop("commit_message", None)
|
| 205 |
+
private = kwargs.pop("private", None)
|
| 206 |
+
create_pr = kwargs.pop("create_pr", False)
|
| 207 |
+
token = kwargs.pop("token", None)
|
| 208 |
+
repo_id = kwargs.pop("repo_id", save_directory.split(os.path.sep)[-1])
|
| 209 |
+
repo_id = create_repo(repo_id, exist_ok=True, private=private, token=token).repo_id
|
| 210 |
+
subfolder = kwargs.pop("subfolder", None)
|
| 211 |
+
|
| 212 |
+
self._upload_folder(
|
| 213 |
+
save_directory,
|
| 214 |
+
repo_id,
|
| 215 |
+
token=token,
|
| 216 |
+
commit_message=commit_message,
|
| 217 |
+
create_pr=create_pr,
|
| 218 |
+
subfolder=subfolder,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
@classmethod
|
| 222 |
+
def from_config(
|
| 223 |
+
cls, config: FrozenDict | dict[str, Any] = None, return_unused_kwargs=False, **kwargs
|
| 224 |
+
) -> Self | tuple[Self, dict[str, Any]]:
|
| 225 |
+
r"""
|
| 226 |
+
Instantiate a Python class from a config dictionary.
|
| 227 |
+
|
| 228 |
+
Parameters:
|
| 229 |
+
config (`dict[str, Any]`):
|
| 230 |
+
A config dictionary from which the Python class is instantiated. Make sure to only load configuration
|
| 231 |
+
files of compatible classes.
|
| 232 |
+
return_unused_kwargs (`bool`, *optional*, defaults to `False`):
|
| 233 |
+
Whether kwargs that are not consumed by the Python class should be returned or not.
|
| 234 |
+
kwargs (remaining dictionary of keyword arguments, *optional*):
|
| 235 |
+
Can be used to update the configuration object (after it is loaded) and initiate the Python class.
|
| 236 |
+
`**kwargs` are passed directly to the underlying scheduler/model's `__init__` method and eventually
|
| 237 |
+
overwrite the same named arguments in `config`.
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
[`ModelMixin`] or [`SchedulerMixin`]:
|
| 241 |
+
A model or scheduler object instantiated from a config dictionary.
|
| 242 |
+
|
| 243 |
+
Examples:
|
| 244 |
+
|
| 245 |
+
```python
|
| 246 |
+
>>> from diffusers import DDPMScheduler, DDIMScheduler, PNDMScheduler
|
| 247 |
+
|
| 248 |
+
>>> # Download scheduler from huggingface.co and cache.
|
| 249 |
+
>>> scheduler = DDPMScheduler.from_pretrained("google/ddpm-cifar10-32")
|
| 250 |
+
|
| 251 |
+
>>> # Instantiate DDIM scheduler class with same config as DDPM
|
| 252 |
+
>>> scheduler = DDIMScheduler.from_config(scheduler.config)
|
| 253 |
+
|
| 254 |
+
>>> # Instantiate PNDM scheduler class with same config as DDPM
|
| 255 |
+
>>> scheduler = PNDMScheduler.from_config(scheduler.config)
|
| 256 |
+
```
|
| 257 |
+
"""
|
| 258 |
+
# <===== TO BE REMOVED WITH DEPRECATION
|
| 259 |
+
# TODO(Patrick) - make sure to remove the following lines when config=="model_path" is deprecated
|
| 260 |
+
if "pretrained_model_name_or_path" in kwargs:
|
| 261 |
+
config = kwargs.pop("pretrained_model_name_or_path")
|
| 262 |
+
|
| 263 |
+
if config is None:
|
| 264 |
+
raise ValueError("Please make sure to provide a config as the first positional argument.")
|
| 265 |
+
# ======>
|
| 266 |
+
|
| 267 |
+
if not isinstance(config, dict):
|
| 268 |
+
deprecation_message = "It is deprecated to pass a pretrained model name or path to `from_config`."
|
| 269 |
+
if "Scheduler" in cls.__name__:
|
| 270 |
+
deprecation_message += (
|
| 271 |
+
f"If you were trying to load a scheduler, please use {cls}.from_pretrained(...) instead."
|
| 272 |
+
" Otherwise, please make sure to pass a configuration dictionary instead. This functionality will"
|
| 273 |
+
" be removed in v1.0.0."
|
| 274 |
+
)
|
| 275 |
+
elif "Model" in cls.__name__:
|
| 276 |
+
deprecation_message += (
|
| 277 |
+
f"If you were trying to load a model, please use {cls}.load_config(...) followed by"
|
| 278 |
+
f" {cls}.from_config(...) instead. Otherwise, please make sure to pass a configuration dictionary"
|
| 279 |
+
" instead. This functionality will be removed in v1.0.0."
|
| 280 |
+
)
|
| 281 |
+
deprecate("config-passed-as-path", "1.0.0", deprecation_message, standard_warn=False)
|
| 282 |
+
config, kwargs = cls.load_config(pretrained_model_name_or_path=config, return_unused_kwargs=True, **kwargs)
|
| 283 |
+
|
| 284 |
+
init_dict, unused_kwargs, hidden_dict = cls.extract_init_dict(config, **kwargs)
|
| 285 |
+
|
| 286 |
+
# Allow dtype to be specified on initialization
|
| 287 |
+
if "dtype" in unused_kwargs:
|
| 288 |
+
init_dict["dtype"] = unused_kwargs.pop("dtype")
|
| 289 |
+
|
| 290 |
+
# add possible deprecated kwargs
|
| 291 |
+
for deprecated_kwarg in cls._deprecated_kwargs:
|
| 292 |
+
if deprecated_kwarg in unused_kwargs:
|
| 293 |
+
init_dict[deprecated_kwarg] = unused_kwargs.pop(deprecated_kwarg)
|
| 294 |
+
|
| 295 |
+
# Return model and optionally state and/or unused_kwargs
|
| 296 |
+
model = cls(**init_dict)
|
| 297 |
+
|
| 298 |
+
# make sure to also save config parameters that might be used for compatible classes
|
| 299 |
+
# update _class_name
|
| 300 |
+
if "_class_name" in hidden_dict:
|
| 301 |
+
hidden_dict["_class_name"] = cls.__name__
|
| 302 |
+
|
| 303 |
+
model.register_to_config(**hidden_dict)
|
| 304 |
+
|
| 305 |
+
# add hidden kwargs of compatible classes to unused_kwargs
|
| 306 |
+
unused_kwargs = {**unused_kwargs, **hidden_dict}
|
| 307 |
+
|
| 308 |
+
if return_unused_kwargs:
|
| 309 |
+
return (model, unused_kwargs)
|
| 310 |
+
else:
|
| 311 |
+
return model
|
| 312 |
+
|
| 313 |
+
@classmethod
|
| 314 |
+
def get_config_dict(cls, *args, **kwargs):
|
| 315 |
+
deprecation_message = (
|
| 316 |
+
f" The function get_config_dict is deprecated. Please use {cls}.load_config instead. This function will be"
|
| 317 |
+
" removed in version v1.0.0"
|
| 318 |
+
)
|
| 319 |
+
deprecate("get_config_dict", "1.0.0", deprecation_message, standard_warn=False)
|
| 320 |
+
return cls.load_config(*args, **kwargs)
|
| 321 |
+
|
| 322 |
+
@classmethod
|
| 323 |
+
@validate_hf_hub_args
|
| 324 |
+
def load_config(
|
| 325 |
+
cls,
|
| 326 |
+
pretrained_model_name_or_path: str | os.PathLike,
|
| 327 |
+
return_unused_kwargs=False,
|
| 328 |
+
return_commit_hash=False,
|
| 329 |
+
**kwargs,
|
| 330 |
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
| 331 |
+
r"""
|
| 332 |
+
Load a model or scheduler configuration.
|
| 333 |
+
|
| 334 |
+
Parameters:
|
| 335 |
+
pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*):
|
| 336 |
+
Can be either:
|
| 337 |
+
|
| 338 |
+
- A string, the *model id* (for example `google/ddpm-celebahq-256`) of a pretrained model hosted on
|
| 339 |
+
the Hub.
|
| 340 |
+
- A path to a *directory* (for example `./my_model_directory`) containing model weights saved with
|
| 341 |
+
[`~ConfigMixin.save_config`].
|
| 342 |
+
|
| 343 |
+
cache_dir (`str | os.PathLike`, *optional*):
|
| 344 |
+
Path to a directory where a downloaded pretrained model configuration is cached if the standard cache
|
| 345 |
+
is not used.
|
| 346 |
+
force_download (`bool`, *optional*, defaults to `False`):
|
| 347 |
+
Whether or not to force the (re-)download of the model weights and configuration files, overriding the
|
| 348 |
+
cached versions if they exist.
|
| 349 |
+
proxies (`dict[str, str]`, *optional*):
|
| 350 |
+
A dictionary of proxy servers to use by protocol or endpoint, for example, `{'http': 'foo.bar:3128',
|
| 351 |
+
'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request.
|
| 352 |
+
output_loading_info(`bool`, *optional*, defaults to `False`):
|
| 353 |
+
Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages.
|
| 354 |
+
local_files_only (`bool`, *optional*, defaults to `False`):
|
| 355 |
+
Whether to only load local model weights and configuration files or not. If set to `True`, the model
|
| 356 |
+
won't be downloaded from the Hub.
|
| 357 |
+
token (`str` or *bool*, *optional*):
|
| 358 |
+
The token to use as HTTP bearer authorization for remote files. If `True`, the token generated from
|
| 359 |
+
`diffusers-cli login` (stored in `~/.huggingface`) is used.
|
| 360 |
+
revision (`str`, *optional*, defaults to `"main"`):
|
| 361 |
+
The specific model version to use. It can be a branch name, a tag name, a commit id, or any identifier
|
| 362 |
+
allowed by Git.
|
| 363 |
+
subfolder (`str`, *optional*, defaults to `""`):
|
| 364 |
+
The subfolder location of a model file within a larger model repository on the Hub or locally.
|
| 365 |
+
return_unused_kwargs (`bool`, *optional*, defaults to `False):
|
| 366 |
+
Whether unused keyword arguments of the config are returned.
|
| 367 |
+
return_commit_hash (`bool`, *optional*, defaults to `False):
|
| 368 |
+
Whether the `commit_hash` of the loaded configuration are returned.
|
| 369 |
+
|
| 370 |
+
Returns:
|
| 371 |
+
`dict`:
|
| 372 |
+
A dictionary of all the parameters stored in a JSON configuration file.
|
| 373 |
+
|
| 374 |
+
"""
|
| 375 |
+
cache_dir = kwargs.pop("cache_dir", None)
|
| 376 |
+
local_dir = kwargs.pop("local_dir", None)
|
| 377 |
+
local_dir_use_symlinks = kwargs.pop("local_dir_use_symlinks", "auto")
|
| 378 |
+
force_download = kwargs.pop("force_download", False)
|
| 379 |
+
proxies = kwargs.pop("proxies", None)
|
| 380 |
+
token = kwargs.pop("token", None)
|
| 381 |
+
local_files_only = kwargs.pop("local_files_only", False)
|
| 382 |
+
revision = kwargs.pop("revision", None)
|
| 383 |
+
_ = kwargs.pop("mirror", None)
|
| 384 |
+
subfolder = kwargs.pop("subfolder", None)
|
| 385 |
+
user_agent = kwargs.pop("user_agent", {})
|
| 386 |
+
|
| 387 |
+
user_agent = {**user_agent, "file_type": "config"}
|
| 388 |
+
user_agent = http_user_agent(user_agent)
|
| 389 |
+
|
| 390 |
+
pretrained_model_name_or_path = str(pretrained_model_name_or_path)
|
| 391 |
+
|
| 392 |
+
if cls.config_name is None:
|
| 393 |
+
raise ValueError(
|
| 394 |
+
"`self.config_name` is not defined. Note that one should not load a config from "
|
| 395 |
+
"`ConfigMixin`. Please make sure to define `config_name` in a class inheriting from `ConfigMixin`"
|
| 396 |
+
)
|
| 397 |
+
if os.path.isfile(pretrained_model_name_or_path):
|
| 398 |
+
config_file = pretrained_model_name_or_path
|
| 399 |
+
elif os.path.isdir(pretrained_model_name_or_path):
|
| 400 |
+
if subfolder is not None and os.path.isfile(
|
| 401 |
+
os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name)
|
| 402 |
+
):
|
| 403 |
+
config_file = os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name)
|
| 404 |
+
elif os.path.isfile(os.path.join(pretrained_model_name_or_path, cls.config_name)):
|
| 405 |
+
# Load from a PyTorch checkpoint
|
| 406 |
+
config_file = os.path.join(pretrained_model_name_or_path, cls.config_name)
|
| 407 |
+
else:
|
| 408 |
+
raise EnvironmentError(
|
| 409 |
+
f"Error no file named {cls.config_name} found in directory {pretrained_model_name_or_path}."
|
| 410 |
+
)
|
| 411 |
+
else:
|
| 412 |
+
try:
|
| 413 |
+
# Load from URL or cache if already cached
|
| 414 |
+
config_file = hf_hub_download(
|
| 415 |
+
pretrained_model_name_or_path,
|
| 416 |
+
filename=cls.config_name,
|
| 417 |
+
cache_dir=cache_dir,
|
| 418 |
+
force_download=force_download,
|
| 419 |
+
proxies=proxies,
|
| 420 |
+
local_files_only=local_files_only,
|
| 421 |
+
token=token,
|
| 422 |
+
user_agent=user_agent,
|
| 423 |
+
subfolder=subfolder,
|
| 424 |
+
revision=revision,
|
| 425 |
+
local_dir=local_dir,
|
| 426 |
+
local_dir_use_symlinks=local_dir_use_symlinks,
|
| 427 |
+
)
|
| 428 |
+
except RepositoryNotFoundError:
|
| 429 |
+
raise EnvironmentError(
|
| 430 |
+
f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier"
|
| 431 |
+
" listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a"
|
| 432 |
+
" token having permission to this repo with `token` or log in with `hf auth login`."
|
| 433 |
+
)
|
| 434 |
+
except RevisionNotFoundError:
|
| 435 |
+
raise EnvironmentError(
|
| 436 |
+
f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for"
|
| 437 |
+
" this model name. Check the model page at"
|
| 438 |
+
f" 'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions."
|
| 439 |
+
)
|
| 440 |
+
except EntryNotFoundError:
|
| 441 |
+
raise EnvironmentError(
|
| 442 |
+
f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}."
|
| 443 |
+
)
|
| 444 |
+
except HfHubHTTPError as err:
|
| 445 |
+
raise EnvironmentError(
|
| 446 |
+
"There was a specific connection error when trying to load"
|
| 447 |
+
f" {pretrained_model_name_or_path}:\n{err}"
|
| 448 |
+
)
|
| 449 |
+
except ValueError:
|
| 450 |
+
raise EnvironmentError(
|
| 451 |
+
f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it"
|
| 452 |
+
f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a"
|
| 453 |
+
f" directory containing a {cls.config_name} file.\nCheckout your internet connection or see how to"
|
| 454 |
+
" run the library in offline mode at"
|
| 455 |
+
" 'https://huggingface.co/docs/diffusers/installation#offline-mode'."
|
| 456 |
+
)
|
| 457 |
+
except EnvironmentError:
|
| 458 |
+
raise EnvironmentError(
|
| 459 |
+
f"Can't load config for '{pretrained_model_name_or_path}'. If you were trying to load it from "
|
| 460 |
+
"'https://huggingface.co/models', make sure you don't have a local directory with the same name. "
|
| 461 |
+
f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory "
|
| 462 |
+
f"containing a {cls.config_name} file"
|
| 463 |
+
)
|
| 464 |
+
try:
|
| 465 |
+
config_dict = cls._dict_from_json_file(config_file)
|
| 466 |
+
|
| 467 |
+
commit_hash = extract_commit_hash(config_file)
|
| 468 |
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
| 469 |
+
raise EnvironmentError(f"It looks like the config file at '{config_file}' is not a valid JSON file.")
|
| 470 |
+
|
| 471 |
+
if not (return_unused_kwargs or return_commit_hash):
|
| 472 |
+
return config_dict
|
| 473 |
+
|
| 474 |
+
outputs = (config_dict,)
|
| 475 |
+
|
| 476 |
+
if return_unused_kwargs:
|
| 477 |
+
outputs += (kwargs,)
|
| 478 |
+
|
| 479 |
+
if return_commit_hash:
|
| 480 |
+
outputs += (commit_hash,)
|
| 481 |
+
|
| 482 |
+
return outputs
|
| 483 |
+
|
| 484 |
+
@staticmethod
|
| 485 |
+
def _get_init_keys(input_class):
|
| 486 |
+
return set(dict(inspect.signature(input_class.__init__).parameters).keys())
|
| 487 |
+
|
| 488 |
+
@classmethod
|
| 489 |
+
def extract_init_dict(cls, config_dict, **kwargs):
|
| 490 |
+
# Skip keys that were not present in the original config, so default __init__ values were used
|
| 491 |
+
used_defaults = config_dict.get("_use_default_values", [])
|
| 492 |
+
config_dict = {k: v for k, v in config_dict.items() if k not in used_defaults and k != "_use_default_values"}
|
| 493 |
+
|
| 494 |
+
# 0. Copy origin config dict
|
| 495 |
+
original_dict = dict(config_dict.items())
|
| 496 |
+
|
| 497 |
+
# 1. Retrieve expected config attributes from __init__ signature
|
| 498 |
+
expected_keys = cls._get_init_keys(cls)
|
| 499 |
+
expected_keys.remove("self")
|
| 500 |
+
# remove general kwargs if present in dict
|
| 501 |
+
if "kwargs" in expected_keys:
|
| 502 |
+
expected_keys.remove("kwargs")
|
| 503 |
+
|
| 504 |
+
# 2. Remove attributes that cannot be expected from expected config attributes
|
| 505 |
+
# remove keys to be ignored
|
| 506 |
+
if len(cls.ignore_for_config) > 0:
|
| 507 |
+
expected_keys = expected_keys - set(cls.ignore_for_config)
|
| 508 |
+
|
| 509 |
+
# load diffusers library to import compatible and original scheduler
|
| 510 |
+
diffusers_library = importlib.import_module(__name__.split(".")[0])
|
| 511 |
+
|
| 512 |
+
if cls.has_compatibles:
|
| 513 |
+
compatible_classes = [c for c in cls._get_compatibles() if not isinstance(c, DummyObject)]
|
| 514 |
+
else:
|
| 515 |
+
compatible_classes = []
|
| 516 |
+
|
| 517 |
+
expected_keys_comp_cls = set()
|
| 518 |
+
for c in compatible_classes:
|
| 519 |
+
expected_keys_c = cls._get_init_keys(c)
|
| 520 |
+
expected_keys_comp_cls = expected_keys_comp_cls.union(expected_keys_c)
|
| 521 |
+
expected_keys_comp_cls = expected_keys_comp_cls - cls._get_init_keys(cls)
|
| 522 |
+
config_dict = {k: v for k, v in config_dict.items() if k not in expected_keys_comp_cls}
|
| 523 |
+
|
| 524 |
+
# remove attributes from orig class that cannot be expected
|
| 525 |
+
orig_cls_name = config_dict.pop("_class_name", cls.__name__)
|
| 526 |
+
if (
|
| 527 |
+
isinstance(orig_cls_name, str)
|
| 528 |
+
and orig_cls_name != cls.__name__
|
| 529 |
+
and hasattr(diffusers_library, orig_cls_name)
|
| 530 |
+
):
|
| 531 |
+
orig_cls = getattr(diffusers_library, orig_cls_name)
|
| 532 |
+
unexpected_keys_from_orig = cls._get_init_keys(orig_cls) - expected_keys
|
| 533 |
+
config_dict = {k: v for k, v in config_dict.items() if k not in unexpected_keys_from_orig}
|
| 534 |
+
elif not isinstance(orig_cls_name, str) and not isinstance(orig_cls_name, (list, tuple)):
|
| 535 |
+
raise ValueError(
|
| 536 |
+
"Make sure that the `_class_name` is of type string or list of string (for custom pipelines)."
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
# remove private attributes
|
| 540 |
+
config_dict = {k: v for k, v in config_dict.items() if not k.startswith("_")}
|
| 541 |
+
|
| 542 |
+
# remove quantization_config
|
| 543 |
+
config_dict = {k: v for k, v in config_dict.items() if k != "quantization_config"}
|
| 544 |
+
|
| 545 |
+
# 3. Create keyword arguments that will be passed to __init__ from expected keyword arguments
|
| 546 |
+
init_dict = {}
|
| 547 |
+
for key in expected_keys:
|
| 548 |
+
# if config param is passed to kwarg and is present in config dict
|
| 549 |
+
# it should overwrite existing config dict key
|
| 550 |
+
if key in kwargs and key in config_dict:
|
| 551 |
+
config_dict[key] = kwargs.pop(key)
|
| 552 |
+
|
| 553 |
+
if key in kwargs:
|
| 554 |
+
# overwrite key
|
| 555 |
+
init_dict[key] = kwargs.pop(key)
|
| 556 |
+
elif key in config_dict:
|
| 557 |
+
# use value from config dict
|
| 558 |
+
init_dict[key] = config_dict.pop(key)
|
| 559 |
+
|
| 560 |
+
# 4. Give nice warning if unexpected values have been passed
|
| 561 |
+
if len(config_dict) > 0:
|
| 562 |
+
logger.warning(
|
| 563 |
+
f"The config attributes {config_dict} were passed to {cls.__name__}, "
|
| 564 |
+
"but are not expected and will be ignored. Please verify your "
|
| 565 |
+
f"{cls.config_name} configuration file."
|
| 566 |
+
)
|
| 567 |
+
|
| 568 |
+
# 5. Give nice info if config attributes are initialized to default because they have not been passed
|
| 569 |
+
passed_keys = set(init_dict.keys())
|
| 570 |
+
if len(expected_keys - passed_keys) > 0:
|
| 571 |
+
logger.info(
|
| 572 |
+
f"{expected_keys - passed_keys} was not found in config. Values will be initialized to default values."
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
# 6. Define unused keyword arguments
|
| 576 |
+
unused_kwargs = {**config_dict, **kwargs}
|
| 577 |
+
|
| 578 |
+
# 7. Define "hidden" config parameters that were saved for compatible classes
|
| 579 |
+
hidden_config_dict = {k: v for k, v in original_dict.items() if k not in init_dict}
|
| 580 |
+
|
| 581 |
+
return init_dict, unused_kwargs, hidden_config_dict
|
| 582 |
+
|
| 583 |
+
@classmethod
|
| 584 |
+
def _dict_from_json_file(cls, json_file: str | os.PathLike):
|
| 585 |
+
with open(json_file, "r", encoding="utf-8") as reader:
|
| 586 |
+
text = reader.read()
|
| 587 |
+
return json.loads(text)
|
| 588 |
+
|
| 589 |
+
def __repr__(self):
|
| 590 |
+
return f"{self.__class__.__name__} {self.to_json_string()}"
|
| 591 |
+
|
| 592 |
+
@property
|
| 593 |
+
def config(self) -> dict[str, Any]:
|
| 594 |
+
"""
|
| 595 |
+
Returns the config of the class as a frozen dictionary
|
| 596 |
+
|
| 597 |
+
Returns:
|
| 598 |
+
`dict[str, Any]`: Config of the class.
|
| 599 |
+
"""
|
| 600 |
+
return self._internal_dict
|
| 601 |
+
|
| 602 |
+
def to_json_string(self) -> str:
|
| 603 |
+
"""
|
| 604 |
+
Serializes the configuration instance to a JSON string.
|
| 605 |
+
|
| 606 |
+
Returns:
|
| 607 |
+
`str`:
|
| 608 |
+
String containing all the attributes that make up the configuration instance in JSON format.
|
| 609 |
+
"""
|
| 610 |
+
config_dict = self._internal_dict if hasattr(self, "_internal_dict") else {}
|
| 611 |
+
config_dict["_class_name"] = self.__class__.__name__
|
| 612 |
+
config_dict["_diffusers_version"] = __version__
|
| 613 |
+
|
| 614 |
+
def to_json_saveable(value):
|
| 615 |
+
if isinstance(value, np.ndarray):
|
| 616 |
+
value = value.tolist()
|
| 617 |
+
elif isinstance(value, Path):
|
| 618 |
+
value = value.as_posix()
|
| 619 |
+
elif hasattr(value, "to_dict") and callable(value.to_dict):
|
| 620 |
+
value = value.to_dict()
|
| 621 |
+
elif isinstance(value, list):
|
| 622 |
+
value = [to_json_saveable(v) for v in value]
|
| 623 |
+
return value
|
| 624 |
+
|
| 625 |
+
if "quantization_config" in config_dict:
|
| 626 |
+
config_dict["quantization_config"] = (
|
| 627 |
+
config_dict.quantization_config.to_dict()
|
| 628 |
+
if not isinstance(config_dict.quantization_config, dict)
|
| 629 |
+
else config_dict.quantization_config
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
config_dict = {k: to_json_saveable(v) for k, v in config_dict.items()}
|
| 633 |
+
# Don't save "_ignore_files" or "_use_default_values"
|
| 634 |
+
config_dict.pop("_ignore_files", None)
|
| 635 |
+
config_dict.pop("_use_default_values", None)
|
| 636 |
+
# pop the `_pre_quantization_dtype` as torch.dtypes are not serializable.
|
| 637 |
+
_ = config_dict.pop("_pre_quantization_dtype", None)
|
| 638 |
+
|
| 639 |
+
if getattr(self, "_auto_class", None) is not None:
|
| 640 |
+
module = self.__class__.__module__.split(".")[-1]
|
| 641 |
+
auto_map = config_dict.get("auto_map", {})
|
| 642 |
+
auto_map[self._auto_class] = f"{module}.{self.__class__.__name__}"
|
| 643 |
+
config_dict["auto_map"] = auto_map
|
| 644 |
+
|
| 645 |
+
return json.dumps(config_dict, indent=2, sort_keys=True) + "\n"
|
| 646 |
+
|
| 647 |
+
def to_json_file(self, json_file_path: str | os.PathLike):
|
| 648 |
+
"""
|
| 649 |
+
Save the configuration instance's parameters to a JSON file.
|
| 650 |
+
|
| 651 |
+
Args:
|
| 652 |
+
json_file_path (`str` or `os.PathLike`):
|
| 653 |
+
Path to the JSON file to save a configuration instance's parameters.
|
| 654 |
+
"""
|
| 655 |
+
with open(json_file_path, "w", encoding="utf-8") as writer:
|
| 656 |
+
writer.write(self.to_json_string())
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def register_to_config(init):
|
| 660 |
+
r"""
|
| 661 |
+
Decorator to apply on the init of classes inheriting from [`ConfigMixin`] so that all the arguments are
|
| 662 |
+
automatically sent to `self.register_for_config`. To ignore a specific argument accepted by the init but that
|
| 663 |
+
shouldn't be registered in the config, use the `ignore_for_config` class variable
|
| 664 |
+
|
| 665 |
+
Warning: Once decorated, all private arguments (beginning with an underscore) are trashed and not sent to the init!
|
| 666 |
+
"""
|
| 667 |
+
|
| 668 |
+
@functools.wraps(init)
|
| 669 |
+
def inner_init(self, *args, **kwargs):
|
| 670 |
+
# Ignore private kwargs in the init.
|
| 671 |
+
init_kwargs = {k: v for k, v in kwargs.items() if not k.startswith("_")}
|
| 672 |
+
config_init_kwargs = {k: v for k, v in kwargs.items() if k.startswith("_")}
|
| 673 |
+
if not isinstance(self, ConfigMixin):
|
| 674 |
+
raise RuntimeError(
|
| 675 |
+
f"`@register_for_config` was applied to {self.__class__.__name__} init method, but this class does "
|
| 676 |
+
"not inherit from `ConfigMixin`."
|
| 677 |
+
)
|
| 678 |
+
|
| 679 |
+
ignore = getattr(self, "ignore_for_config", [])
|
| 680 |
+
# Get positional arguments aligned with kwargs
|
| 681 |
+
new_kwargs = {}
|
| 682 |
+
signature = inspect.signature(init)
|
| 683 |
+
parameters = {
|
| 684 |
+
name: p.default for i, (name, p) in enumerate(signature.parameters.items()) if i > 0 and name not in ignore
|
| 685 |
+
}
|
| 686 |
+
for arg, name in zip(args, parameters.keys()):
|
| 687 |
+
new_kwargs[name] = arg
|
| 688 |
+
|
| 689 |
+
# Then add all kwargs
|
| 690 |
+
new_kwargs.update(
|
| 691 |
+
{
|
| 692 |
+
k: init_kwargs.get(k, default)
|
| 693 |
+
for k, default in parameters.items()
|
| 694 |
+
if k not in ignore and k not in new_kwargs
|
| 695 |
+
}
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
# Take note of the parameters that were not present in the loaded config
|
| 699 |
+
if len(set(new_kwargs.keys()) - set(init_kwargs)) > 0:
|
| 700 |
+
new_kwargs["_use_default_values"] = list(set(new_kwargs.keys()) - set(init_kwargs))
|
| 701 |
+
|
| 702 |
+
new_kwargs = {**config_init_kwargs, **new_kwargs}
|
| 703 |
+
getattr(self, "register_to_config")(**new_kwargs)
|
| 704 |
+
init(self, *args, **init_kwargs)
|
| 705 |
+
|
| 706 |
+
return inner_init
|
| 707 |
+
|
| 708 |
+
|
| 709 |
+
class LegacyConfigMixin(ConfigMixin):
|
| 710 |
+
r"""
|
| 711 |
+
A subclass of `ConfigMixin` to resolve class mapping from legacy classes (like `Transformer2DModel`) to more
|
| 712 |
+
pipeline-specific classes (like `DiTTransformer2DModel`).
|
| 713 |
+
"""
|
| 714 |
+
|
| 715 |
+
@classmethod
|
| 716 |
+
def from_config(cls, config: FrozenDict | dict[str, Any] = None, return_unused_kwargs=False, **kwargs):
|
| 717 |
+
# To prevent dependency import problem.
|
| 718 |
+
from .models.model_loading_utils import _fetch_remapped_cls_from_config
|
| 719 |
+
|
| 720 |
+
# resolve remapping
|
| 721 |
+
remapped_class = _fetch_remapped_cls_from_config(config, cls)
|
| 722 |
+
|
| 723 |
+
if remapped_class is cls:
|
| 724 |
+
return super(LegacyConfigMixin, remapped_class).from_config(config, return_unused_kwargs, **kwargs)
|
| 725 |
+
else:
|
| 726 |
+
return remapped_class.from_config(config, return_unused_kwargs, **kwargs)
|
diffusers/dependency_versions_check.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from .dependency_versions_table import deps
|
| 16 |
+
from .utils.versions import require_version, require_version_core
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# define which module versions we always want to check at run time
|
| 20 |
+
# (usually the ones defined in `install_requires` in setup.py)
|
| 21 |
+
#
|
| 22 |
+
# order specific notes:
|
| 23 |
+
# - tqdm must be checked before tokenizers
|
| 24 |
+
|
| 25 |
+
pkgs_to_check_at_runtime = "python requests filelock numpy".split()
|
| 26 |
+
for pkg in pkgs_to_check_at_runtime:
|
| 27 |
+
if pkg in deps:
|
| 28 |
+
require_version_core(deps[pkg])
|
| 29 |
+
else:
|
| 30 |
+
raise ValueError(f"can't find {pkg} in {deps.keys()}, check dependency_versions_table.py")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def dep_version_check(pkg, hint=None):
|
| 34 |
+
require_version(deps[pkg], hint)
|
diffusers/dependency_versions_table.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# THIS FILE HAS BEEN AUTOGENERATED. To update:
|
| 2 |
+
# 1. modify the `_deps` dict in setup.py
|
| 3 |
+
# 2. run `make deps_table_update`
|
| 4 |
+
deps = {
|
| 5 |
+
"Pillow": "Pillow",
|
| 6 |
+
"accelerate": "accelerate>=0.31.0",
|
| 7 |
+
"datasets": "datasets",
|
| 8 |
+
"filelock": "filelock",
|
| 9 |
+
"ftfy": "ftfy",
|
| 10 |
+
"hf-doc-builder": "hf-doc-builder>=0.3.0",
|
| 11 |
+
"huggingface-hub": "huggingface-hub>=1.31.0,<2.0",
|
| 12 |
+
"requests-mock": "requests-mock==1.10.0",
|
| 13 |
+
"importlib_metadata": "importlib_metadata",
|
| 14 |
+
"invisible-watermark": "invisible-watermark>=0.2.0",
|
| 15 |
+
"isort": "isort>=5.5.4",
|
| 16 |
+
"Jinja2": "Jinja2",
|
| 17 |
+
"torchsde": "torchsde",
|
| 18 |
+
"note_seq": "note_seq",
|
| 19 |
+
"librosa": "librosa",
|
| 20 |
+
"llvmlite": "llvmlite>=0.40.0",
|
| 21 |
+
"numba": "numba>=0.57.0",
|
| 22 |
+
"numpy": "numpy",
|
| 23 |
+
"parameterized": "parameterized",
|
| 24 |
+
"peft": "peft>=0.17.0",
|
| 25 |
+
"protobuf": "protobuf>=3.20.3,<4",
|
| 26 |
+
"pytest": "pytest",
|
| 27 |
+
"pytest-timeout": "pytest-timeout",
|
| 28 |
+
"pytest-xdist": "pytest-xdist",
|
| 29 |
+
"python": "python>=3.10.0",
|
| 30 |
+
"ruff": "ruff==0.9.10",
|
| 31 |
+
"safetensors": "safetensors>=0.8.0",
|
| 32 |
+
"sentencepiece": "sentencepiece>=0.1.91,!=0.1.92",
|
| 33 |
+
"GitPython": "GitPython<3.1.19",
|
| 34 |
+
"scipy": "scipy",
|
| 35 |
+
"onnx": "onnx",
|
| 36 |
+
"optimum_quanto": "optimum_quanto>=0.2.6",
|
| 37 |
+
"gguf": "gguf>=0.10.0",
|
| 38 |
+
"auto-round": "auto-round>=0.13.0",
|
| 39 |
+
"torchao": "torchao>=0.7.0",
|
| 40 |
+
"bitsandbytes": "bitsandbytes>=0.43.3",
|
| 41 |
+
"nvidia_modelopt[hf]": "nvidia_modelopt[hf]>=0.33.1",
|
| 42 |
+
"sdnq": "sdnq>=0.2.2",
|
| 43 |
+
"regex": "regex!=2019.12.17",
|
| 44 |
+
"requests": "requests",
|
| 45 |
+
"tensorboard": "tensorboard",
|
| 46 |
+
"tiktoken": "tiktoken>=0.7.0",
|
| 47 |
+
"torch": "torch>=2.6",
|
| 48 |
+
"torchvision": "torchvision",
|
| 49 |
+
"transformers": "transformers>=4.41.2",
|
| 50 |
+
"urllib3": "urllib3<=2.0.0",
|
| 51 |
+
"black": "black",
|
| 52 |
+
"phonemizer": "phonemizer",
|
| 53 |
+
"opencv-python": "opencv-python",
|
| 54 |
+
"timm": "timm",
|
| 55 |
+
"flashpack": "flashpack",
|
| 56 |
+
}
|
diffusers/experimental/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .rl import ValueGuidedRLPipeline
|
diffusers/experimental/rl/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .value_guided_sampling import ValueGuidedRLPipeline
|
diffusers/experimental/rl/value_guided_sampling.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import torch
|
| 17 |
+
import tqdm
|
| 18 |
+
|
| 19 |
+
from ...models.unets.unet_1d import UNet1DModel
|
| 20 |
+
from ...pipelines import DiffusionPipeline
|
| 21 |
+
from ...utils.dummy_pt_objects import DDPMScheduler
|
| 22 |
+
from ...utils.torch_utils import randn_tensor
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ValueGuidedRLPipeline(DiffusionPipeline):
|
| 26 |
+
r"""
|
| 27 |
+
Pipeline for value-guided sampling from a diffusion model trained to predict sequences of states.
|
| 28 |
+
|
| 29 |
+
This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods
|
| 30 |
+
implemented for all pipelines (downloading, saving, running on a particular device, etc.).
|
| 31 |
+
|
| 32 |
+
Parameters:
|
| 33 |
+
value_function ([`UNet1DModel`]):
|
| 34 |
+
A specialized UNet for fine-tuning trajectories base on reward.
|
| 35 |
+
unet ([`UNet1DModel`]):
|
| 36 |
+
UNet architecture to denoise the encoded trajectories.
|
| 37 |
+
scheduler ([`SchedulerMixin`]):
|
| 38 |
+
A scheduler to be used in combination with `unet` to denoise the encoded trajectories. Default for this
|
| 39 |
+
application is [`DDPMScheduler`].
|
| 40 |
+
env ():
|
| 41 |
+
An environment following the OpenAI gym API to act in. For now only Hopper has pretrained models.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
value_function: UNet1DModel,
|
| 47 |
+
unet: UNet1DModel,
|
| 48 |
+
scheduler: DDPMScheduler,
|
| 49 |
+
env,
|
| 50 |
+
):
|
| 51 |
+
super().__init__()
|
| 52 |
+
|
| 53 |
+
self.register_modules(value_function=value_function, unet=unet, scheduler=scheduler, env=env)
|
| 54 |
+
|
| 55 |
+
self.data = env.get_dataset()
|
| 56 |
+
self.means = {}
|
| 57 |
+
for key in self.data.keys():
|
| 58 |
+
try:
|
| 59 |
+
self.means[key] = self.data[key].mean()
|
| 60 |
+
except: # noqa: E722
|
| 61 |
+
pass
|
| 62 |
+
self.stds = {}
|
| 63 |
+
for key in self.data.keys():
|
| 64 |
+
try:
|
| 65 |
+
self.stds[key] = self.data[key].std()
|
| 66 |
+
except: # noqa: E722
|
| 67 |
+
pass
|
| 68 |
+
self.state_dim = env.observation_space.shape[0]
|
| 69 |
+
self.action_dim = env.action_space.shape[0]
|
| 70 |
+
|
| 71 |
+
def normalize(self, x_in, key):
|
| 72 |
+
return (x_in - self.means[key]) / self.stds[key]
|
| 73 |
+
|
| 74 |
+
def de_normalize(self, x_in, key):
|
| 75 |
+
return x_in * self.stds[key] + self.means[key]
|
| 76 |
+
|
| 77 |
+
def to_torch(self, x_in):
|
| 78 |
+
if isinstance(x_in, dict):
|
| 79 |
+
return {k: self.to_torch(v) for k, v in x_in.items()}
|
| 80 |
+
elif torch.is_tensor(x_in):
|
| 81 |
+
return x_in.to(self.unet.device)
|
| 82 |
+
return torch.tensor(x_in, device=self.unet.device)
|
| 83 |
+
|
| 84 |
+
def reset_x0(self, x_in, cond, act_dim):
|
| 85 |
+
for key, val in cond.items():
|
| 86 |
+
x_in[:, key, act_dim:] = val.clone()
|
| 87 |
+
return x_in
|
| 88 |
+
|
| 89 |
+
def run_diffusion(self, x, conditions, n_guide_steps, scale):
|
| 90 |
+
batch_size = x.shape[0]
|
| 91 |
+
y = None
|
| 92 |
+
for i in tqdm.tqdm(self.scheduler.timesteps):
|
| 93 |
+
# create batch of timesteps to pass into model
|
| 94 |
+
timesteps = torch.full((batch_size,), i, device=self.unet.device, dtype=torch.long)
|
| 95 |
+
for _ in range(n_guide_steps):
|
| 96 |
+
with torch.enable_grad():
|
| 97 |
+
x.requires_grad_()
|
| 98 |
+
|
| 99 |
+
# permute to match dimension for pre-trained models
|
| 100 |
+
y = self.value_function(x.permute(0, 2, 1), timesteps).sample
|
| 101 |
+
grad = torch.autograd.grad([y.sum()], [x])[0]
|
| 102 |
+
|
| 103 |
+
posterior_variance = self.scheduler._get_variance(i)
|
| 104 |
+
model_std = torch.exp(0.5 * posterior_variance)
|
| 105 |
+
grad = model_std * grad
|
| 106 |
+
|
| 107 |
+
grad[timesteps < 2] = 0
|
| 108 |
+
x = x.detach()
|
| 109 |
+
x = x + scale * grad
|
| 110 |
+
x = self.reset_x0(x, conditions, self.action_dim)
|
| 111 |
+
|
| 112 |
+
prev_x = self.unet(x.permute(0, 2, 1), timesteps).sample.permute(0, 2, 1)
|
| 113 |
+
|
| 114 |
+
# TODO: verify deprecation of this kwarg
|
| 115 |
+
x = self.scheduler.step(prev_x, i, x)["prev_sample"]
|
| 116 |
+
|
| 117 |
+
# apply conditions to the trajectory (set the initial state)
|
| 118 |
+
x = self.reset_x0(x, conditions, self.action_dim)
|
| 119 |
+
x = self.to_torch(x)
|
| 120 |
+
return x, y
|
| 121 |
+
|
| 122 |
+
def __call__(self, obs, batch_size=64, planning_horizon=32, n_guide_steps=2, scale=0.1):
|
| 123 |
+
# normalize the observations and create batch dimension
|
| 124 |
+
obs = self.normalize(obs, "observations")
|
| 125 |
+
obs = obs[None].repeat(batch_size, axis=0)
|
| 126 |
+
|
| 127 |
+
conditions = {0: self.to_torch(obs)}
|
| 128 |
+
shape = (batch_size, planning_horizon, self.state_dim + self.action_dim)
|
| 129 |
+
|
| 130 |
+
# generate initial noise and apply our conditions (to make the trajectories start at current state)
|
| 131 |
+
x1 = randn_tensor(shape, device=self.unet.device)
|
| 132 |
+
x = self.reset_x0(x1, conditions, self.action_dim)
|
| 133 |
+
x = self.to_torch(x)
|
| 134 |
+
|
| 135 |
+
# run the diffusion process
|
| 136 |
+
x, y = self.run_diffusion(x, conditions, n_guide_steps, scale)
|
| 137 |
+
|
| 138 |
+
# sort output trajectories by value
|
| 139 |
+
sorted_idx = y.argsort(0, descending=True).squeeze()
|
| 140 |
+
sorted_values = x[sorted_idx]
|
| 141 |
+
actions = sorted_values[:, :, : self.action_dim]
|
| 142 |
+
actions = actions.detach().cpu().numpy()
|
| 143 |
+
denorm_actions = self.de_normalize(actions, key="actions")
|
| 144 |
+
|
| 145 |
+
# select the action with the highest value
|
| 146 |
+
if y is not None:
|
| 147 |
+
selected_index = 0
|
| 148 |
+
else:
|
| 149 |
+
# if we didn't run value guiding, select a random action
|
| 150 |
+
selected_index = np.random.randint(0, batch_size)
|
| 151 |
+
|
| 152 |
+
denorm_actions = denorm_actions[selected_index, 0]
|
| 153 |
+
return denorm_actions
|
diffusers/guiders/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ..utils import is_torch_available, logging
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
if is_torch_available():
|
| 20 |
+
from .adaptive_projected_guidance import AdaptiveProjectedGuidance
|
| 21 |
+
from .adaptive_projected_guidance_mix import AdaptiveProjectedMixGuidance
|
| 22 |
+
from .auto_guidance import AutoGuidance
|
| 23 |
+
from .classifier_free_guidance import ClassifierFreeGuidance
|
| 24 |
+
from .classifier_free_zero_star_guidance import ClassifierFreeZeroStarGuidance
|
| 25 |
+
from .frequency_decoupled_guidance import FrequencyDecoupledGuidance
|
| 26 |
+
from .guider_utils import BaseGuidance
|
| 27 |
+
from .ltx2_guidance import LTX2Guidance
|
| 28 |
+
from .magnitude_aware_guidance import MagnitudeAwareGuidance
|
| 29 |
+
from .perturbed_attention_guidance import PerturbedAttentionGuidance
|
| 30 |
+
from .skip_layer_guidance import SkipLayerGuidance
|
| 31 |
+
from .smoothed_energy_guidance import SmoothedEnergyGuidance
|
| 32 |
+
from .tangential_classifier_free_guidance import TangentialClassifierFreeGuidance
|
diffusers/guiders/adaptive_projected_guidance.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
if TYPE_CHECKING:
|
| 27 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class AdaptiveProjectedGuidance(BaseGuidance):
|
| 31 |
+
"""
|
| 32 |
+
Adaptive Projected Guidance (APG): https://huggingface.co/papers/2410.02416
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 36 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 37 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 38 |
+
deterioration of image quality.
|
| 39 |
+
adaptive_projected_guidance_momentum (`float`, defaults to `None`):
|
| 40 |
+
The momentum parameter for the adaptive projected guidance. Disabled if set to `None`.
|
| 41 |
+
adaptive_projected_guidance_rescale (`float`, defaults to `15.0`):
|
| 42 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 43 |
+
adaptive_projected_guidance_norm_dim (`int` or `tuple[int]`, *optional*):
|
| 44 |
+
Dimension(s) over which to compute the APG norm and projection. If omitted, all non-batch dimensions are
|
| 45 |
+
used, preserving the original behavior.
|
| 46 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 47 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 48 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 49 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 50 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 51 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 52 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 53 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 54 |
+
start (`float`, defaults to `0.0`):
|
| 55 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 56 |
+
stop (`float`, defaults to `1.0`):
|
| 57 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 61 |
+
|
| 62 |
+
@register_to_config
|
| 63 |
+
def __init__(
|
| 64 |
+
self,
|
| 65 |
+
guidance_scale: float = 7.5,
|
| 66 |
+
adaptive_projected_guidance_momentum: float | None = None,
|
| 67 |
+
adaptive_projected_guidance_rescale: float = 15.0,
|
| 68 |
+
adaptive_projected_guidance_norm_dim: int | tuple[int, ...] | None = None,
|
| 69 |
+
eta: float = 1.0,
|
| 70 |
+
guidance_rescale: float = 0.0,
|
| 71 |
+
use_original_formulation: bool = False,
|
| 72 |
+
start: float = 0.0,
|
| 73 |
+
stop: float = 1.0,
|
| 74 |
+
enabled: bool = True,
|
| 75 |
+
):
|
| 76 |
+
super().__init__(start, stop, enabled)
|
| 77 |
+
|
| 78 |
+
self.guidance_scale = guidance_scale
|
| 79 |
+
self.adaptive_projected_guidance_momentum = adaptive_projected_guidance_momentum
|
| 80 |
+
self.adaptive_projected_guidance_rescale = adaptive_projected_guidance_rescale
|
| 81 |
+
self.adaptive_projected_guidance_norm_dim = adaptive_projected_guidance_norm_dim
|
| 82 |
+
self.eta = eta
|
| 83 |
+
self.guidance_rescale = guidance_rescale
|
| 84 |
+
self.use_original_formulation = use_original_formulation
|
| 85 |
+
self.momentum_buffer = None
|
| 86 |
+
|
| 87 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 88 |
+
if self._step == 0:
|
| 89 |
+
if self.adaptive_projected_guidance_momentum is not None:
|
| 90 |
+
self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
|
| 91 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 92 |
+
data_batches = []
|
| 93 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 94 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 95 |
+
data_batches.append(data_batch)
|
| 96 |
+
return data_batches
|
| 97 |
+
|
| 98 |
+
def prepare_inputs_from_block_state(
|
| 99 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 100 |
+
) -> list["BlockState"]:
|
| 101 |
+
if self._step == 0:
|
| 102 |
+
if self.adaptive_projected_guidance_momentum is not None:
|
| 103 |
+
self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
|
| 104 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 105 |
+
data_batches = []
|
| 106 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 107 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 108 |
+
data_batches.append(data_batch)
|
| 109 |
+
return data_batches
|
| 110 |
+
|
| 111 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 112 |
+
pred = None
|
| 113 |
+
|
| 114 |
+
if not self._is_apg_enabled():
|
| 115 |
+
pred = pred_cond
|
| 116 |
+
else:
|
| 117 |
+
pred = normalized_guidance(
|
| 118 |
+
pred_cond,
|
| 119 |
+
pred_uncond,
|
| 120 |
+
self.guidance_scale,
|
| 121 |
+
self.momentum_buffer,
|
| 122 |
+
self.eta,
|
| 123 |
+
self.adaptive_projected_guidance_rescale,
|
| 124 |
+
self.use_original_formulation,
|
| 125 |
+
self.adaptive_projected_guidance_norm_dim,
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if self.guidance_rescale > 0.0:
|
| 129 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 130 |
+
|
| 131 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def is_conditional(self) -> bool:
|
| 135 |
+
return self._count_prepared == 1
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def num_conditions(self) -> int:
|
| 139 |
+
num_conditions = 1
|
| 140 |
+
if self._is_apg_enabled():
|
| 141 |
+
num_conditions += 1
|
| 142 |
+
return num_conditions
|
| 143 |
+
|
| 144 |
+
def _is_apg_enabled(self) -> bool:
|
| 145 |
+
if not self._enabled:
|
| 146 |
+
return False
|
| 147 |
+
|
| 148 |
+
is_within_range = True
|
| 149 |
+
if self._num_inference_steps is not None:
|
| 150 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 151 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 152 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 153 |
+
|
| 154 |
+
is_close = False
|
| 155 |
+
if self.use_original_formulation:
|
| 156 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 157 |
+
else:
|
| 158 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 159 |
+
|
| 160 |
+
return is_within_range and not is_close
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class MomentumBuffer:
|
| 164 |
+
def __init__(self, momentum: float):
|
| 165 |
+
self.momentum = momentum
|
| 166 |
+
self.running_average = 0
|
| 167 |
+
|
| 168 |
+
def update(self, update_value: torch.Tensor):
|
| 169 |
+
new_average = self.momentum * self.running_average
|
| 170 |
+
self.running_average = update_value + new_average
|
| 171 |
+
|
| 172 |
+
def __repr__(self) -> str:
|
| 173 |
+
"""
|
| 174 |
+
Returns a string representation showing momentum, shape, statistics, and a slice of the running_average.
|
| 175 |
+
"""
|
| 176 |
+
if isinstance(self.running_average, torch.Tensor):
|
| 177 |
+
shape = tuple(self.running_average.shape)
|
| 178 |
+
|
| 179 |
+
# Calculate statistics
|
| 180 |
+
with torch.no_grad():
|
| 181 |
+
stats = {
|
| 182 |
+
"mean": self.running_average.mean().item(),
|
| 183 |
+
"std": self.running_average.std().item(),
|
| 184 |
+
"min": self.running_average.min().item(),
|
| 185 |
+
"max": self.running_average.max().item(),
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
# Get a slice (max 3 elements per dimension)
|
| 189 |
+
slice_indices = tuple(slice(None, min(3, dim)) for dim in shape)
|
| 190 |
+
sliced_data = self.running_average[slice_indices]
|
| 191 |
+
|
| 192 |
+
# Format the slice for display (convert to float32 for numpy compatibility with bfloat16)
|
| 193 |
+
slice_str = str(sliced_data.detach().float().cpu().numpy())
|
| 194 |
+
if len(slice_str) > 200: # Truncate if too long
|
| 195 |
+
slice_str = slice_str[:200] + "..."
|
| 196 |
+
|
| 197 |
+
stats_str = ", ".join([f"{k}={v:.4f}" for k, v in stats.items()])
|
| 198 |
+
|
| 199 |
+
return (
|
| 200 |
+
f"MomentumBuffer(\n"
|
| 201 |
+
f" momentum={self.momentum},\n"
|
| 202 |
+
f" shape={shape},\n"
|
| 203 |
+
f" stats=[{stats_str}],\n"
|
| 204 |
+
f" slice={slice_str}\n"
|
| 205 |
+
f")"
|
| 206 |
+
)
|
| 207 |
+
else:
|
| 208 |
+
return f"MomentumBuffer(momentum={self.momentum}, running_average={self.running_average})"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def normalized_guidance(
|
| 212 |
+
pred_cond: torch.Tensor,
|
| 213 |
+
pred_uncond: torch.Tensor,
|
| 214 |
+
guidance_scale: float,
|
| 215 |
+
momentum_buffer: MomentumBuffer | None = None,
|
| 216 |
+
eta: float = 1.0,
|
| 217 |
+
norm_threshold: float = 0.0,
|
| 218 |
+
use_original_formulation: bool = False,
|
| 219 |
+
norm_dim: int | tuple[int, ...] | None = None,
|
| 220 |
+
):
|
| 221 |
+
diff = pred_cond - pred_uncond
|
| 222 |
+
if norm_dim is None:
|
| 223 |
+
dim = [-i for i in range(1, len(diff.shape))]
|
| 224 |
+
elif isinstance(norm_dim, int):
|
| 225 |
+
dim = [norm_dim]
|
| 226 |
+
else:
|
| 227 |
+
dim = list(norm_dim)
|
| 228 |
+
|
| 229 |
+
if momentum_buffer is not None:
|
| 230 |
+
momentum_buffer.update(diff)
|
| 231 |
+
diff = momentum_buffer.running_average
|
| 232 |
+
|
| 233 |
+
if norm_threshold > 0:
|
| 234 |
+
ones = torch.ones_like(diff)
|
| 235 |
+
diff_norm = diff.norm(p=2, dim=dim, keepdim=True)
|
| 236 |
+
scale_factor = torch.minimum(ones, norm_threshold / diff_norm)
|
| 237 |
+
diff = diff * scale_factor
|
| 238 |
+
|
| 239 |
+
if diff.device.type in {"mps", "npu"}:
|
| 240 |
+
v0, v1 = diff.cpu().double(), pred_cond.cpu().double()
|
| 241 |
+
else:
|
| 242 |
+
v0, v1 = diff.double(), pred_cond.double()
|
| 243 |
+
v1 = torch.nn.functional.normalize(v1, dim=dim)
|
| 244 |
+
v0_parallel = (v0 * v1).sum(dim=dim, keepdim=True) * v1
|
| 245 |
+
v0_orthogonal = v0 - v0_parallel
|
| 246 |
+
diff_parallel = v0_parallel.to(device=diff.device, dtype=diff.dtype)
|
| 247 |
+
diff_orthogonal = v0_orthogonal.to(device=diff.device, dtype=diff.dtype)
|
| 248 |
+
normalized_update = diff_orthogonal + eta * diff_parallel
|
| 249 |
+
|
| 250 |
+
pred = pred_cond if use_original_formulation else pred_uncond
|
| 251 |
+
pred = pred + guidance_scale * normalized_update
|
| 252 |
+
|
| 253 |
+
return pred
|
diffusers/guiders/adaptive_projected_guidance_mix.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from typing import TYPE_CHECKING
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ..configuration_utils import register_to_config
|
| 21 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
if TYPE_CHECKING:
|
| 25 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class AdaptiveProjectedMixGuidance(BaseGuidance):
|
| 29 |
+
"""
|
| 30 |
+
Adaptive Projected Guidance (APG) https://huggingface.co/papers/2410.02416 combined with Classifier-Free Guidance
|
| 31 |
+
(CFG). This guider is used in HunyuanImage2.1 https://github.com/Tencent-Hunyuan/HunyuanImage-2.1
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 35 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 36 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 37 |
+
deterioration of image quality.
|
| 38 |
+
adaptive_projected_guidance_momentum (`float`, defaults to `None`):
|
| 39 |
+
The momentum parameter for the adaptive projected guidance. Disabled if set to `None`.
|
| 40 |
+
adaptive_projected_guidance_rescale (`float`, defaults to `15.0`):
|
| 41 |
+
The rescale factor applied to the noise predictions for adaptive projected guidance. This is used to
|
| 42 |
+
improve image quality and fix
|
| 43 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 44 |
+
The rescale factor applied to the noise predictions for classifier-free guidance. This is used to improve
|
| 45 |
+
image quality and fix overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample
|
| 46 |
+
Steps are Flawed](https://huggingface.co/papers/2305.08891).
|
| 47 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 48 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 49 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 50 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 51 |
+
start (`float`, defaults to `0.0`):
|
| 52 |
+
The fraction of the total number of denoising steps after which the classifier-free guidance starts.
|
| 53 |
+
stop (`float`, defaults to `1.0`):
|
| 54 |
+
The fraction of the total number of denoising steps after which the classifier-free guidance stops.
|
| 55 |
+
adaptive_projected_guidance_start_step (`int`, defaults to `5`):
|
| 56 |
+
The step at which the adaptive projected guidance starts (before this step, classifier-free guidance is
|
| 57 |
+
used, and momentum buffer is updated).
|
| 58 |
+
enabled (`bool`, defaults to `True`):
|
| 59 |
+
Whether this guidance is enabled.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 63 |
+
|
| 64 |
+
@register_to_config
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
guidance_scale: float = 3.5,
|
| 68 |
+
guidance_rescale: float = 0.0,
|
| 69 |
+
adaptive_projected_guidance_scale: float = 10.0,
|
| 70 |
+
adaptive_projected_guidance_momentum: float = -0.5,
|
| 71 |
+
adaptive_projected_guidance_rescale: float = 10.0,
|
| 72 |
+
eta: float = 0.0,
|
| 73 |
+
use_original_formulation: bool = False,
|
| 74 |
+
start: float = 0.0,
|
| 75 |
+
stop: float = 1.0,
|
| 76 |
+
adaptive_projected_guidance_start_step: int = 5,
|
| 77 |
+
enabled: bool = True,
|
| 78 |
+
):
|
| 79 |
+
super().__init__(start, stop, enabled)
|
| 80 |
+
|
| 81 |
+
self.guidance_scale = guidance_scale
|
| 82 |
+
self.guidance_rescale = guidance_rescale
|
| 83 |
+
self.adaptive_projected_guidance_scale = adaptive_projected_guidance_scale
|
| 84 |
+
self.adaptive_projected_guidance_momentum = adaptive_projected_guidance_momentum
|
| 85 |
+
self.adaptive_projected_guidance_rescale = adaptive_projected_guidance_rescale
|
| 86 |
+
self.eta = eta
|
| 87 |
+
self.adaptive_projected_guidance_start_step = adaptive_projected_guidance_start_step
|
| 88 |
+
self.use_original_formulation = use_original_formulation
|
| 89 |
+
self.momentum_buffer = None
|
| 90 |
+
|
| 91 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 92 |
+
if self._step == 0:
|
| 93 |
+
if self.adaptive_projected_guidance_momentum is not None:
|
| 94 |
+
self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
|
| 95 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 96 |
+
data_batches = []
|
| 97 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 98 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 99 |
+
data_batches.append(data_batch)
|
| 100 |
+
return data_batches
|
| 101 |
+
|
| 102 |
+
def prepare_inputs_from_block_state(
|
| 103 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 104 |
+
) -> list["BlockState"]:
|
| 105 |
+
if self._step == 0:
|
| 106 |
+
if self.adaptive_projected_guidance_momentum is not None:
|
| 107 |
+
self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
|
| 108 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 109 |
+
data_batches = []
|
| 110 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 111 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 112 |
+
data_batches.append(data_batch)
|
| 113 |
+
return data_batches
|
| 114 |
+
|
| 115 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 116 |
+
pred = None
|
| 117 |
+
|
| 118 |
+
# no guidance
|
| 119 |
+
if not self._is_cfg_enabled():
|
| 120 |
+
pred = pred_cond
|
| 121 |
+
|
| 122 |
+
# CFG + update momentum buffer
|
| 123 |
+
elif not self._is_apg_enabled():
|
| 124 |
+
if self.momentum_buffer is not None:
|
| 125 |
+
update_momentum_buffer(pred_cond, pred_uncond, self.momentum_buffer)
|
| 126 |
+
# CFG + update momentum buffer
|
| 127 |
+
shift = pred_cond - pred_uncond
|
| 128 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 129 |
+
pred = pred + self.guidance_scale * shift
|
| 130 |
+
|
| 131 |
+
# APG
|
| 132 |
+
elif self._is_apg_enabled():
|
| 133 |
+
pred = normalized_guidance(
|
| 134 |
+
pred_cond,
|
| 135 |
+
pred_uncond,
|
| 136 |
+
self.adaptive_projected_guidance_scale,
|
| 137 |
+
self.momentum_buffer,
|
| 138 |
+
self.eta,
|
| 139 |
+
self.adaptive_projected_guidance_rescale,
|
| 140 |
+
self.use_original_formulation,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
if self.guidance_rescale > 0.0:
|
| 144 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 145 |
+
|
| 146 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def is_conditional(self) -> bool:
|
| 150 |
+
return self._count_prepared == 1
|
| 151 |
+
|
| 152 |
+
@property
|
| 153 |
+
def num_conditions(self) -> int:
|
| 154 |
+
num_conditions = 1
|
| 155 |
+
if self._is_apg_enabled() or self._is_cfg_enabled():
|
| 156 |
+
num_conditions += 1
|
| 157 |
+
return num_conditions
|
| 158 |
+
|
| 159 |
+
# Copied from diffusers.guiders.classifier_free_guidance.ClassifierFreeGuidance._is_cfg_enabled
|
| 160 |
+
def _is_cfg_enabled(self) -> bool:
|
| 161 |
+
if not self._enabled:
|
| 162 |
+
return False
|
| 163 |
+
|
| 164 |
+
is_within_range = True
|
| 165 |
+
if self._num_inference_steps is not None:
|
| 166 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 167 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 168 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 169 |
+
|
| 170 |
+
is_close = False
|
| 171 |
+
if self.use_original_formulation:
|
| 172 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 173 |
+
else:
|
| 174 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 175 |
+
|
| 176 |
+
return is_within_range and not is_close
|
| 177 |
+
|
| 178 |
+
def _is_apg_enabled(self) -> bool:
|
| 179 |
+
if not self._enabled:
|
| 180 |
+
return False
|
| 181 |
+
|
| 182 |
+
if not self._is_cfg_enabled():
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
is_within_range = False
|
| 186 |
+
if self._step is not None:
|
| 187 |
+
is_within_range = self._step > self.adaptive_projected_guidance_start_step
|
| 188 |
+
|
| 189 |
+
is_close = False
|
| 190 |
+
if self.use_original_formulation:
|
| 191 |
+
is_close = math.isclose(self.adaptive_projected_guidance_scale, 0.0)
|
| 192 |
+
else:
|
| 193 |
+
is_close = math.isclose(self.adaptive_projected_guidance_scale, 1.0)
|
| 194 |
+
|
| 195 |
+
return is_within_range and not is_close
|
| 196 |
+
|
| 197 |
+
def get_state(self):
|
| 198 |
+
state = super().get_state()
|
| 199 |
+
state["momentum_buffer"] = self.momentum_buffer
|
| 200 |
+
state["is_apg_enabled"] = self._is_apg_enabled()
|
| 201 |
+
state["is_cfg_enabled"] = self._is_cfg_enabled()
|
| 202 |
+
return state
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# Copied from diffusers.guiders.adaptive_projected_guidance.MomentumBuffer
|
| 206 |
+
class MomentumBuffer:
|
| 207 |
+
def __init__(self, momentum: float):
|
| 208 |
+
self.momentum = momentum
|
| 209 |
+
self.running_average = 0
|
| 210 |
+
|
| 211 |
+
def update(self, update_value: torch.Tensor):
|
| 212 |
+
new_average = self.momentum * self.running_average
|
| 213 |
+
self.running_average = update_value + new_average
|
| 214 |
+
|
| 215 |
+
def __repr__(self) -> str:
|
| 216 |
+
"""
|
| 217 |
+
Returns a string representation showing momentum, shape, statistics, and a slice of the running_average.
|
| 218 |
+
"""
|
| 219 |
+
if isinstance(self.running_average, torch.Tensor):
|
| 220 |
+
shape = tuple(self.running_average.shape)
|
| 221 |
+
|
| 222 |
+
# Calculate statistics
|
| 223 |
+
with torch.no_grad():
|
| 224 |
+
stats = {
|
| 225 |
+
"mean": self.running_average.mean().item(),
|
| 226 |
+
"std": self.running_average.std().item(),
|
| 227 |
+
"min": self.running_average.min().item(),
|
| 228 |
+
"max": self.running_average.max().item(),
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
# Get a slice (max 3 elements per dimension)
|
| 232 |
+
slice_indices = tuple(slice(None, min(3, dim)) for dim in shape)
|
| 233 |
+
sliced_data = self.running_average[slice_indices]
|
| 234 |
+
|
| 235 |
+
# Format the slice for display (convert to float32 for numpy compatibility with bfloat16)
|
| 236 |
+
slice_str = str(sliced_data.detach().float().cpu().numpy())
|
| 237 |
+
if len(slice_str) > 200: # Truncate if too long
|
| 238 |
+
slice_str = slice_str[:200] + "..."
|
| 239 |
+
|
| 240 |
+
stats_str = ", ".join([f"{k}={v:.4f}" for k, v in stats.items()])
|
| 241 |
+
|
| 242 |
+
return (
|
| 243 |
+
f"MomentumBuffer(\n"
|
| 244 |
+
f" momentum={self.momentum},\n"
|
| 245 |
+
f" shape={shape},\n"
|
| 246 |
+
f" stats=[{stats_str}],\n"
|
| 247 |
+
f" slice={slice_str}\n"
|
| 248 |
+
f")"
|
| 249 |
+
)
|
| 250 |
+
else:
|
| 251 |
+
return f"MomentumBuffer(momentum={self.momentum}, running_average={self.running_average})"
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def update_momentum_buffer(
|
| 255 |
+
pred_cond: torch.Tensor,
|
| 256 |
+
pred_uncond: torch.Tensor,
|
| 257 |
+
momentum_buffer: MomentumBuffer | None = None,
|
| 258 |
+
):
|
| 259 |
+
diff = pred_cond - pred_uncond
|
| 260 |
+
if momentum_buffer is not None:
|
| 261 |
+
momentum_buffer.update(diff)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def normalized_guidance(
|
| 265 |
+
pred_cond: torch.Tensor,
|
| 266 |
+
pred_uncond: torch.Tensor,
|
| 267 |
+
guidance_scale: float,
|
| 268 |
+
momentum_buffer: MomentumBuffer | None = None,
|
| 269 |
+
eta: float = 1.0,
|
| 270 |
+
norm_threshold: float = 0.0,
|
| 271 |
+
use_original_formulation: bool = False,
|
| 272 |
+
):
|
| 273 |
+
if momentum_buffer is not None:
|
| 274 |
+
update_momentum_buffer(pred_cond, pred_uncond, momentum_buffer)
|
| 275 |
+
diff = momentum_buffer.running_average
|
| 276 |
+
else:
|
| 277 |
+
diff = pred_cond - pred_uncond
|
| 278 |
+
|
| 279 |
+
dim = [-i for i in range(1, len(diff.shape))]
|
| 280 |
+
|
| 281 |
+
if norm_threshold > 0:
|
| 282 |
+
ones = torch.ones_like(diff)
|
| 283 |
+
diff_norm = diff.norm(p=2, dim=dim, keepdim=True)
|
| 284 |
+
scale_factor = torch.minimum(ones, norm_threshold / diff_norm)
|
| 285 |
+
diff = diff * scale_factor
|
| 286 |
+
|
| 287 |
+
v0, v1 = diff.double(), pred_cond.double()
|
| 288 |
+
v1 = torch.nn.functional.normalize(v1, dim=dim)
|
| 289 |
+
v0_parallel = (v0 * v1).sum(dim=dim, keepdim=True) * v1
|
| 290 |
+
v0_orthogonal = v0 - v0_parallel
|
| 291 |
+
diff_parallel, diff_orthogonal = v0_parallel.type_as(diff), v0_orthogonal.type_as(diff)
|
| 292 |
+
normalized_update = diff_orthogonal + eta * diff_parallel
|
| 293 |
+
|
| 294 |
+
pred = pred_cond if use_original_formulation else pred_uncond
|
| 295 |
+
pred = pred + guidance_scale * normalized_update
|
| 296 |
+
|
| 297 |
+
return pred
|
diffusers/guiders/auto_guidance.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING, Any
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from ..hooks import HookRegistry, LayerSkipConfig
|
| 24 |
+
from ..hooks.layer_skip import _apply_layer_skip_hook
|
| 25 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
if TYPE_CHECKING:
|
| 29 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class AutoGuidance(BaseGuidance):
|
| 33 |
+
"""
|
| 34 |
+
AutoGuidance: https://huggingface.co/papers/2406.02507
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 38 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 39 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 40 |
+
deterioration of image quality.
|
| 41 |
+
auto_guidance_layers (`int` or `list[int]`, *optional*):
|
| 42 |
+
The layer indices to apply skip layer guidance to. Can be a single integer or a list of integers. If not
|
| 43 |
+
provided, `skip_layer_config` must be provided.
|
| 44 |
+
auto_guidance_config (`LayerSkipConfig` or `list[LayerSkipConfig]`, *optional*):
|
| 45 |
+
The configuration for the skip layer guidance. Can be a single `LayerSkipConfig` or a list of
|
| 46 |
+
`LayerSkipConfig`. If not provided, `skip_layer_guidance_layers` must be provided.
|
| 47 |
+
dropout (`float`, *optional*):
|
| 48 |
+
The dropout probability for autoguidance on the enabled skip layers (either with `auto_guidance_layers` or
|
| 49 |
+
`auto_guidance_config`). If not provided, the dropout probability will be set to 1.0.
|
| 50 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 51 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 52 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 53 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 54 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 55 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 56 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 57 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 58 |
+
start (`float`, defaults to `0.0`):
|
| 59 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 60 |
+
stop (`float`, defaults to `1.0`):
|
| 61 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 65 |
+
|
| 66 |
+
@register_to_config
|
| 67 |
+
def __init__(
|
| 68 |
+
self,
|
| 69 |
+
guidance_scale: float = 7.5,
|
| 70 |
+
auto_guidance_layers: int | list[int] | None = None,
|
| 71 |
+
auto_guidance_config: LayerSkipConfig | list[LayerSkipConfig] | dict[str, Any] = None,
|
| 72 |
+
dropout: float | None = None,
|
| 73 |
+
guidance_rescale: float = 0.0,
|
| 74 |
+
use_original_formulation: bool = False,
|
| 75 |
+
start: float = 0.0,
|
| 76 |
+
stop: float = 1.0,
|
| 77 |
+
enabled: bool = True,
|
| 78 |
+
):
|
| 79 |
+
super().__init__(start, stop, enabled)
|
| 80 |
+
|
| 81 |
+
self.guidance_scale = guidance_scale
|
| 82 |
+
self.auto_guidance_layers = auto_guidance_layers
|
| 83 |
+
self.auto_guidance_config = auto_guidance_config
|
| 84 |
+
self.dropout = dropout
|
| 85 |
+
self.guidance_rescale = guidance_rescale
|
| 86 |
+
self.use_original_formulation = use_original_formulation
|
| 87 |
+
|
| 88 |
+
is_layer_or_config_provided = auto_guidance_layers is not None or auto_guidance_config is not None
|
| 89 |
+
is_layer_and_config_provided = auto_guidance_layers is not None and auto_guidance_config is not None
|
| 90 |
+
if not is_layer_or_config_provided:
|
| 91 |
+
raise ValueError(
|
| 92 |
+
"Either `auto_guidance_layers` or `auto_guidance_config` must be provided to enable AutoGuidance."
|
| 93 |
+
)
|
| 94 |
+
if is_layer_and_config_provided:
|
| 95 |
+
raise ValueError("Only one of `auto_guidance_layers` or `auto_guidance_config` can be provided.")
|
| 96 |
+
if auto_guidance_config is None and dropout is None:
|
| 97 |
+
raise ValueError("`dropout` must be provided if `auto_guidance_layers` is provided.")
|
| 98 |
+
|
| 99 |
+
if auto_guidance_layers is not None:
|
| 100 |
+
if isinstance(auto_guidance_layers, int):
|
| 101 |
+
auto_guidance_layers = [auto_guidance_layers]
|
| 102 |
+
if not isinstance(auto_guidance_layers, list):
|
| 103 |
+
raise ValueError(
|
| 104 |
+
f"Expected `auto_guidance_layers` to be an int or a list of ints, but got {type(auto_guidance_layers)}."
|
| 105 |
+
)
|
| 106 |
+
auto_guidance_config = [
|
| 107 |
+
LayerSkipConfig(layer, fqn="auto", dropout=dropout) for layer in auto_guidance_layers
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
if isinstance(auto_guidance_config, dict):
|
| 111 |
+
auto_guidance_config = LayerSkipConfig.from_dict(auto_guidance_config)
|
| 112 |
+
|
| 113 |
+
if isinstance(auto_guidance_config, LayerSkipConfig):
|
| 114 |
+
auto_guidance_config = [auto_guidance_config]
|
| 115 |
+
|
| 116 |
+
if not isinstance(auto_guidance_config, list):
|
| 117 |
+
raise ValueError(
|
| 118 |
+
f"Expected `auto_guidance_config` to be a LayerSkipConfig or a list of LayerSkipConfig, but got {type(auto_guidance_config)}."
|
| 119 |
+
)
|
| 120 |
+
elif isinstance(next(iter(auto_guidance_config), None), dict):
|
| 121 |
+
auto_guidance_config = [LayerSkipConfig.from_dict(config) for config in auto_guidance_config]
|
| 122 |
+
|
| 123 |
+
self.auto_guidance_config = auto_guidance_config
|
| 124 |
+
self._auto_guidance_hook_names = [f"AutoGuidance_{i}" for i in range(len(self.auto_guidance_config))]
|
| 125 |
+
|
| 126 |
+
def prepare_models(self, denoiser: torch.nn.Module) -> None:
|
| 127 |
+
self._count_prepared += 1
|
| 128 |
+
if self._is_ag_enabled() and self.is_unconditional:
|
| 129 |
+
for name, config in zip(self._auto_guidance_hook_names, self.auto_guidance_config):
|
| 130 |
+
_apply_layer_skip_hook(denoiser, config, name=name)
|
| 131 |
+
|
| 132 |
+
def cleanup_models(self, denoiser: torch.nn.Module) -> None:
|
| 133 |
+
if self._is_ag_enabled() and self.is_unconditional:
|
| 134 |
+
for name in self._auto_guidance_hook_names:
|
| 135 |
+
registry = HookRegistry.check_if_exists_or_initialize(denoiser)
|
| 136 |
+
registry.remove_hook(name, recurse=True)
|
| 137 |
+
|
| 138 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 139 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 140 |
+
data_batches = []
|
| 141 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 142 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 143 |
+
data_batches.append(data_batch)
|
| 144 |
+
return data_batches
|
| 145 |
+
|
| 146 |
+
def prepare_inputs_from_block_state(
|
| 147 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 148 |
+
) -> list["BlockState"]:
|
| 149 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 150 |
+
data_batches = []
|
| 151 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 152 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 153 |
+
data_batches.append(data_batch)
|
| 154 |
+
return data_batches
|
| 155 |
+
|
| 156 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 157 |
+
pred = None
|
| 158 |
+
|
| 159 |
+
if not self._is_ag_enabled():
|
| 160 |
+
pred = pred_cond
|
| 161 |
+
else:
|
| 162 |
+
shift = pred_cond - pred_uncond
|
| 163 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 164 |
+
pred = pred + self.guidance_scale * shift
|
| 165 |
+
|
| 166 |
+
if self.guidance_rescale > 0.0:
|
| 167 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 168 |
+
|
| 169 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def is_conditional(self) -> bool:
|
| 173 |
+
return self._count_prepared == 1
|
| 174 |
+
|
| 175 |
+
@property
|
| 176 |
+
def num_conditions(self) -> int:
|
| 177 |
+
num_conditions = 1
|
| 178 |
+
if self._is_ag_enabled():
|
| 179 |
+
num_conditions += 1
|
| 180 |
+
return num_conditions
|
| 181 |
+
|
| 182 |
+
def _is_ag_enabled(self) -> bool:
|
| 183 |
+
if not self._enabled:
|
| 184 |
+
return False
|
| 185 |
+
|
| 186 |
+
is_within_range = True
|
| 187 |
+
if self._num_inference_steps is not None:
|
| 188 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 189 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 190 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 191 |
+
|
| 192 |
+
is_close = False
|
| 193 |
+
if self.use_original_formulation:
|
| 194 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 195 |
+
else:
|
| 196 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 197 |
+
|
| 198 |
+
return is_within_range and not is_close
|
diffusers/guiders/classifier_free_guidance.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
if TYPE_CHECKING:
|
| 27 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ClassifierFreeGuidance(BaseGuidance):
|
| 31 |
+
"""
|
| 32 |
+
Implements Classifier-Free Guidance (CFG) for diffusion models.
|
| 33 |
+
|
| 34 |
+
Reference: https://huggingface.co/papers/2207.12598
|
| 35 |
+
|
| 36 |
+
CFG improves generation quality and prompt adherence by jointly training models on both conditional and
|
| 37 |
+
unconditional data, then combining predictions during inference. This allows trading off between quality (high
|
| 38 |
+
guidance) and diversity (low guidance).
|
| 39 |
+
|
| 40 |
+
**Two CFG Formulations:**
|
| 41 |
+
|
| 42 |
+
1. **Original formulation** (from paper):
|
| 43 |
+
```
|
| 44 |
+
x_pred = x_cond + guidance_scale * (x_cond - x_uncond)
|
| 45 |
+
```
|
| 46 |
+
Moves conditional predictions further from unconditional ones.
|
| 47 |
+
|
| 48 |
+
2. **Diffusers-native formulation** (default, from Imagen paper):
|
| 49 |
+
```
|
| 50 |
+
x_pred = x_uncond + guidance_scale * (x_cond - x_uncond)
|
| 51 |
+
```
|
| 52 |
+
Moves unconditional predictions toward conditional ones, effectively suppressing negative features (e.g., "bad
|
| 53 |
+
quality", "watermarks"). Equivalent in theory but more intuitive.
|
| 54 |
+
|
| 55 |
+
Use `use_original_formulation=True` to switch to the original formulation.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 59 |
+
CFG scale applied by this guider during post-processing. Higher values = stronger prompt conditioning but
|
| 60 |
+
may reduce quality. Typical range: 1.0-20.0.
|
| 61 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 62 |
+
Rescaling factor to prevent overexposure from high guidance scales. Based on [Common Diffusion Noise
|
| 63 |
+
Schedules and Sample Steps are Flawed](https://huggingface.co/papers/2305.08891). Range: 0.0 (no rescaling)
|
| 64 |
+
to 1.0 (full rescaling).
|
| 65 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 66 |
+
If `True`, uses the original CFG formulation from the paper. If `False` (default), uses the
|
| 67 |
+
diffusers-native formulation from the Imagen paper.
|
| 68 |
+
start (`float`, defaults to `0.0`):
|
| 69 |
+
Fraction of denoising steps (0.0-1.0) after which CFG starts. Use > 0.0 to disable CFG in early denoising
|
| 70 |
+
steps.
|
| 71 |
+
stop (`float`, defaults to `1.0`):
|
| 72 |
+
Fraction of denoising steps (0.0-1.0) after which CFG stops. Use < 1.0 to disable CFG in late denoising
|
| 73 |
+
steps.
|
| 74 |
+
enabled (`bool`, defaults to `True`):
|
| 75 |
+
Whether CFG is enabled. Set to `False` to disable CFG entirely (uses only conditional predictions).
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 79 |
+
|
| 80 |
+
@register_to_config
|
| 81 |
+
def __init__(
|
| 82 |
+
self,
|
| 83 |
+
guidance_scale: float = 7.5,
|
| 84 |
+
guidance_rescale: float = 0.0,
|
| 85 |
+
use_original_formulation: bool = False,
|
| 86 |
+
start: float = 0.0,
|
| 87 |
+
stop: float = 1.0,
|
| 88 |
+
enabled: bool = True,
|
| 89 |
+
):
|
| 90 |
+
super().__init__(start, stop, enabled)
|
| 91 |
+
|
| 92 |
+
self.guidance_scale = guidance_scale
|
| 93 |
+
self.guidance_rescale = guidance_rescale
|
| 94 |
+
self.use_original_formulation = use_original_formulation
|
| 95 |
+
|
| 96 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 97 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 98 |
+
data_batches = []
|
| 99 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 100 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 101 |
+
data_batches.append(data_batch)
|
| 102 |
+
return data_batches
|
| 103 |
+
|
| 104 |
+
def prepare_inputs_from_block_state(
|
| 105 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 106 |
+
) -> list["BlockState"]:
|
| 107 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 108 |
+
data_batches = []
|
| 109 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 110 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 111 |
+
data_batches.append(data_batch)
|
| 112 |
+
return data_batches
|
| 113 |
+
|
| 114 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 115 |
+
pred = None
|
| 116 |
+
|
| 117 |
+
if not self._is_cfg_enabled():
|
| 118 |
+
pred = pred_cond
|
| 119 |
+
else:
|
| 120 |
+
shift = pred_cond - pred_uncond
|
| 121 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 122 |
+
pred = pred + self.guidance_scale * shift
|
| 123 |
+
|
| 124 |
+
if self.guidance_rescale > 0.0:
|
| 125 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 126 |
+
|
| 127 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 128 |
+
|
| 129 |
+
@property
|
| 130 |
+
def is_conditional(self) -> bool:
|
| 131 |
+
return self._count_prepared == 1
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def num_conditions(self) -> int:
|
| 135 |
+
num_conditions = 1
|
| 136 |
+
if self._is_cfg_enabled():
|
| 137 |
+
num_conditions += 1
|
| 138 |
+
return num_conditions
|
| 139 |
+
|
| 140 |
+
def _is_cfg_enabled(self) -> bool:
|
| 141 |
+
if not self._enabled:
|
| 142 |
+
return False
|
| 143 |
+
|
| 144 |
+
is_within_range = True
|
| 145 |
+
if self._num_inference_steps is not None:
|
| 146 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 147 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 148 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 149 |
+
|
| 150 |
+
is_close = False
|
| 151 |
+
if self.use_original_formulation:
|
| 152 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 153 |
+
else:
|
| 154 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 155 |
+
|
| 156 |
+
return is_within_range and not is_close
|
diffusers/guiders/classifier_free_zero_star_guidance.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
if TYPE_CHECKING:
|
| 27 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ClassifierFreeZeroStarGuidance(BaseGuidance):
|
| 31 |
+
"""
|
| 32 |
+
Classifier-free Zero* (CFG-Zero*): https://huggingface.co/papers/2503.18886
|
| 33 |
+
|
| 34 |
+
This is an implementation of the Classifier-Free Zero* guidance technique, which is a variant of classifier-free
|
| 35 |
+
guidance. It proposes zero initialization of the noise predictions for the first few steps of the diffusion
|
| 36 |
+
process, and also introduces an optimal rescaling factor for the noise predictions, which can help in improving the
|
| 37 |
+
quality of generated images.
|
| 38 |
+
|
| 39 |
+
The authors of the paper suggest setting zero initialization in the first 4% of the inference steps.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 43 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 44 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 45 |
+
deterioration of image quality.
|
| 46 |
+
zero_init_steps (`int`, defaults to `1`):
|
| 47 |
+
The number of inference steps for which the noise predictions are zeroed out (see Section 4.2).
|
| 48 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 49 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 50 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 51 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 52 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 53 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 54 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 55 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 56 |
+
start (`float`, defaults to `0.01`):
|
| 57 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 58 |
+
stop (`float`, defaults to `0.2`):
|
| 59 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 63 |
+
|
| 64 |
+
@register_to_config
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
guidance_scale: float = 7.5,
|
| 68 |
+
zero_init_steps: int = 1,
|
| 69 |
+
guidance_rescale: float = 0.0,
|
| 70 |
+
use_original_formulation: bool = False,
|
| 71 |
+
start: float = 0.0,
|
| 72 |
+
stop: float = 1.0,
|
| 73 |
+
enabled: bool = True,
|
| 74 |
+
):
|
| 75 |
+
super().__init__(start, stop, enabled)
|
| 76 |
+
|
| 77 |
+
self.guidance_scale = guidance_scale
|
| 78 |
+
self.zero_init_steps = zero_init_steps
|
| 79 |
+
self.guidance_rescale = guidance_rescale
|
| 80 |
+
self.use_original_formulation = use_original_formulation
|
| 81 |
+
|
| 82 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 83 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 84 |
+
data_batches = []
|
| 85 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 86 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 87 |
+
data_batches.append(data_batch)
|
| 88 |
+
return data_batches
|
| 89 |
+
|
| 90 |
+
def prepare_inputs_from_block_state(
|
| 91 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 92 |
+
) -> list["BlockState"]:
|
| 93 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 94 |
+
data_batches = []
|
| 95 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 96 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 97 |
+
data_batches.append(data_batch)
|
| 98 |
+
return data_batches
|
| 99 |
+
|
| 100 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 101 |
+
pred = None
|
| 102 |
+
|
| 103 |
+
# YiYi Notes: add default behavior for self._enabled == False
|
| 104 |
+
if not self._enabled:
|
| 105 |
+
pred = pred_cond
|
| 106 |
+
|
| 107 |
+
elif self._step < self.zero_init_steps:
|
| 108 |
+
pred = torch.zeros_like(pred_cond)
|
| 109 |
+
elif not self._is_cfg_enabled():
|
| 110 |
+
pred = pred_cond
|
| 111 |
+
else:
|
| 112 |
+
pred_cond_flat = pred_cond.flatten(1)
|
| 113 |
+
pred_uncond_flat = pred_uncond.flatten(1)
|
| 114 |
+
alpha = cfg_zero_star_scale(pred_cond_flat, pred_uncond_flat)
|
| 115 |
+
alpha = alpha.view(-1, *(1,) * (len(pred_cond.shape) - 1))
|
| 116 |
+
pred_uncond = pred_uncond * alpha
|
| 117 |
+
shift = pred_cond - pred_uncond
|
| 118 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 119 |
+
pred = pred + self.guidance_scale * shift
|
| 120 |
+
|
| 121 |
+
if self.guidance_rescale > 0.0:
|
| 122 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 123 |
+
|
| 124 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 125 |
+
|
| 126 |
+
@property
|
| 127 |
+
def is_conditional(self) -> bool:
|
| 128 |
+
return self._count_prepared == 1
|
| 129 |
+
|
| 130 |
+
@property
|
| 131 |
+
def num_conditions(self) -> int:
|
| 132 |
+
num_conditions = 1
|
| 133 |
+
if self._is_cfg_enabled():
|
| 134 |
+
num_conditions += 1
|
| 135 |
+
return num_conditions
|
| 136 |
+
|
| 137 |
+
def _is_cfg_enabled(self) -> bool:
|
| 138 |
+
if not self._enabled:
|
| 139 |
+
return False
|
| 140 |
+
|
| 141 |
+
is_within_range = True
|
| 142 |
+
if self._num_inference_steps is not None:
|
| 143 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 144 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 145 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 146 |
+
|
| 147 |
+
is_close = False
|
| 148 |
+
if self.use_original_formulation:
|
| 149 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 150 |
+
else:
|
| 151 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 152 |
+
|
| 153 |
+
return is_within_range and not is_close
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def cfg_zero_star_scale(cond: torch.Tensor, uncond: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
|
| 157 |
+
cond_dtype = cond.dtype
|
| 158 |
+
cond = cond.float()
|
| 159 |
+
uncond = uncond.float()
|
| 160 |
+
dot_product = torch.sum(cond * uncond, dim=1, keepdim=True)
|
| 161 |
+
squared_norm = torch.sum(uncond**2, dim=1, keepdim=True) + eps
|
| 162 |
+
# st_star = v_cond^T * v_uncond / ||v_uncond||^2
|
| 163 |
+
scale = dot_product / squared_norm
|
| 164 |
+
return scale.to(dtype=cond_dtype)
|
diffusers/guiders/frequency_decoupled_guidance.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from ..utils import is_kornia_available
|
| 24 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
if TYPE_CHECKING:
|
| 28 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
_CAN_USE_KORNIA = is_kornia_available()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if _CAN_USE_KORNIA:
|
| 35 |
+
from kornia.geometry import pyrup as upsample_and_blur_func
|
| 36 |
+
from kornia.geometry.transform import build_laplacian_pyramid as build_laplacian_pyramid_func
|
| 37 |
+
else:
|
| 38 |
+
upsample_and_blur_func = None
|
| 39 |
+
build_laplacian_pyramid_func = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def project(v0: torch.Tensor, v1: torch.Tensor, upcast_to_double: bool = True) -> tuple[torch.Tensor, torch.Tensor]:
|
| 43 |
+
"""
|
| 44 |
+
Project vector v0 onto vector v1, returning the parallel and orthogonal components of v0. Implementation from paper
|
| 45 |
+
(Algorithm 2).
|
| 46 |
+
"""
|
| 47 |
+
# v0 shape: [B, ...]
|
| 48 |
+
# v1 shape: [B, ...]
|
| 49 |
+
# Assume first dim is a batch dim and all other dims are channel or "spatial" dims
|
| 50 |
+
all_dims_but_first = list(range(1, len(v0.shape)))
|
| 51 |
+
if upcast_to_double:
|
| 52 |
+
dtype = v0.dtype
|
| 53 |
+
v0, v1 = v0.double(), v1.double()
|
| 54 |
+
v1 = torch.nn.functional.normalize(v1, dim=all_dims_but_first)
|
| 55 |
+
v0_parallel = (v0 * v1).sum(dim=all_dims_but_first, keepdim=True) * v1
|
| 56 |
+
v0_orthogonal = v0 - v0_parallel
|
| 57 |
+
if upcast_to_double:
|
| 58 |
+
v0_parallel = v0_parallel.to(dtype)
|
| 59 |
+
v0_orthogonal = v0_orthogonal.to(dtype)
|
| 60 |
+
return v0_parallel, v0_orthogonal
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def build_image_from_pyramid(pyramid: list[torch.Tensor]) -> torch.Tensor:
|
| 64 |
+
"""
|
| 65 |
+
Recovers the data space latents from the Laplacian pyramid frequency space. Implementation from the paper
|
| 66 |
+
(Algorithm 2).
|
| 67 |
+
"""
|
| 68 |
+
# pyramid shapes: [[B, C, H, W], [B, C, H/2, W/2], ...]
|
| 69 |
+
img = pyramid[-1]
|
| 70 |
+
for i in range(len(pyramid) - 2, -1, -1):
|
| 71 |
+
img = upsample_and_blur_func(img) + pyramid[i]
|
| 72 |
+
return img
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class FrequencyDecoupledGuidance(BaseGuidance):
|
| 76 |
+
"""
|
| 77 |
+
Frequency-Decoupled Guidance (FDG): https://huggingface.co/papers/2506.19713
|
| 78 |
+
|
| 79 |
+
FDG is a technique similar to (and based on) classifier-free guidance (CFG) which is used to improve generation
|
| 80 |
+
quality and condition-following in diffusion models. Like CFG, during training we jointly train the model on both
|
| 81 |
+
conditional and unconditional data, and use a combination of the two during inference. (If you want more details on
|
| 82 |
+
how CFG works, you can check out the CFG guider.)
|
| 83 |
+
|
| 84 |
+
FDG differs from CFG in that the normal CFG prediction is instead decoupled into low- and high-frequency components
|
| 85 |
+
using a frequency transform (such as a Laplacian pyramid). The CFG update is then performed in frequency space
|
| 86 |
+
separately for the low- and high-frequency components with different guidance scales. Finally, the inverse
|
| 87 |
+
frequency transform is used to map the CFG frequency predictions back to data space (e.g. pixel space for images)
|
| 88 |
+
to form the final FDG prediction.
|
| 89 |
+
|
| 90 |
+
For images, the FDG authors found that using low guidance scales for the low-frequency components retains sample
|
| 91 |
+
diversity and realistic color composition, while using high guidance scales for high-frequency components enhances
|
| 92 |
+
sample quality (such as better visual details). Therefore, they recommend using low guidance scales (low w_low) for
|
| 93 |
+
the low-frequency components and high guidance scales (high w_high) for the high-frequency components. As an
|
| 94 |
+
example, they suggest w_low = 5.0 and w_high = 10.0 for Stable Diffusion XL (see Table 8 in the paper).
|
| 95 |
+
|
| 96 |
+
As with CFG, Diffusers implements the scaling and shifting on the unconditional prediction based on the [Imagen
|
| 97 |
+
paper](https://huggingface.co/papers/2205.11487), which is equivalent to what the original CFG paper proposed in
|
| 98 |
+
theory. [x_pred = x_uncond + scale * (x_cond - x_uncond)]
|
| 99 |
+
|
| 100 |
+
The `use_original_formulation` argument can be set to `True` to use the original CFG formulation mentioned in the
|
| 101 |
+
paper. By default, we use the diffusers-native implementation that has been in the codebase for a long time.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
guidance_scales (`list[float]`, defaults to `[10.0, 5.0]`):
|
| 105 |
+
The scale parameter for frequency-decoupled guidance for each frequency component, listed from highest
|
| 106 |
+
frequency level to lowest. Higher values result in stronger conditioning on the text prompt, while lower
|
| 107 |
+
values allow for more freedom in generation. Higher values may lead to saturation and deterioration of
|
| 108 |
+
image quality. The FDG authors recommend using higher guidance scales for higher frequency components and
|
| 109 |
+
lower guidance scales for lower frequency components (so `guidance_scales` should typically be sorted in
|
| 110 |
+
descending order).
|
| 111 |
+
guidance_rescale (`float` or `list[float]`, defaults to `0.0`):
|
| 112 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 113 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 114 |
+
Flawed](https://huggingface.co/papers/2305.08891). If a list is supplied, it should be the same length as
|
| 115 |
+
`guidance_scales`.
|
| 116 |
+
parallel_weights (`float` or `list[float]`, *optional*):
|
| 117 |
+
Optional weights for the parallel component of each frequency component of the projected CFG shift. If not
|
| 118 |
+
set, the weights will default to `1.0` for all components, which corresponds to using the normal CFG shift
|
| 119 |
+
(that is, equal weights for the parallel and orthogonal components). If set, a value in `[0, 1]` is
|
| 120 |
+
recommended. If a list is supplied, it should be the same length as `guidance_scales`.
|
| 121 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 122 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 123 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 124 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 125 |
+
start (`float` or `list[float]`, defaults to `0.0`):
|
| 126 |
+
The fraction of the total number of denoising steps after which guidance starts. If a list is supplied, it
|
| 127 |
+
should be the same length as `guidance_scales`.
|
| 128 |
+
stop (`float` or `list[float]`, defaults to `1.0`):
|
| 129 |
+
The fraction of the total number of denoising steps after which guidance stops. If a list is supplied, it
|
| 130 |
+
should be the same length as `guidance_scales`.
|
| 131 |
+
guidance_rescale_space (`str`, defaults to `"data"`):
|
| 132 |
+
Whether to performance guidance rescaling in `"data"` space (after the full FDG update in data space) or in
|
| 133 |
+
`"freq"` space (right after the CFG update, for each freq level). Note that frequency space rescaling is
|
| 134 |
+
speculative and may not produce expected results. If `"data"` is set, the first `guidance_rescale` value
|
| 135 |
+
will be used; otherwise, per-frequency-level guidance rescale values will be used if available.
|
| 136 |
+
upcast_to_double (`bool`, defaults to `True`):
|
| 137 |
+
Whether to upcast certain operations, such as the projection operation when using `parallel_weights`, to
|
| 138 |
+
float64 when performing guidance. This may result in better performance at the cost of increased runtime.
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 142 |
+
|
| 143 |
+
@register_to_config
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
guidance_scales: list[float] | tuple[float] = [10.0, 5.0],
|
| 147 |
+
guidance_rescale: float | list[float] | tuple[float] = 0.0,
|
| 148 |
+
parallel_weights: float | list[float] | tuple[float] | None = None,
|
| 149 |
+
use_original_formulation: bool = False,
|
| 150 |
+
start: float | list[float] | tuple[float] = 0.0,
|
| 151 |
+
stop: float | list[float] | tuple[float] = 1.0,
|
| 152 |
+
guidance_rescale_space: str = "data",
|
| 153 |
+
upcast_to_double: bool = True,
|
| 154 |
+
enabled: bool = True,
|
| 155 |
+
):
|
| 156 |
+
if not _CAN_USE_KORNIA:
|
| 157 |
+
raise ImportError(
|
| 158 |
+
"The `FrequencyDecoupledGuidance` guider cannot be instantiated because the `kornia` library on which "
|
| 159 |
+
"it depends is not available in the current environment. You can install `kornia` with `pip install "
|
| 160 |
+
"kornia`."
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# Set start to earliest start for any freq component and stop to latest stop for any freq component
|
| 164 |
+
min_start = start if isinstance(start, float) else min(start)
|
| 165 |
+
max_stop = stop if isinstance(stop, float) else max(stop)
|
| 166 |
+
super().__init__(min_start, max_stop, enabled)
|
| 167 |
+
|
| 168 |
+
self.guidance_scales = guidance_scales
|
| 169 |
+
self.levels = len(guidance_scales)
|
| 170 |
+
|
| 171 |
+
if isinstance(guidance_rescale, float):
|
| 172 |
+
self.guidance_rescale = [guidance_rescale] * self.levels
|
| 173 |
+
elif len(guidance_rescale) == self.levels:
|
| 174 |
+
self.guidance_rescale = guidance_rescale
|
| 175 |
+
else:
|
| 176 |
+
raise ValueError(
|
| 177 |
+
f"`guidance_rescale` has length {len(guidance_rescale)} but should have the same length as "
|
| 178 |
+
f"`guidance_scales` ({len(self.guidance_scales)})"
|
| 179 |
+
)
|
| 180 |
+
# Whether to perform guidance rescaling in frequency space (right after the CFG update) or data space (after
|
| 181 |
+
# transforming from frequency space back to data space)
|
| 182 |
+
if guidance_rescale_space not in ["data", "freq"]:
|
| 183 |
+
raise ValueError(
|
| 184 |
+
f"Guidance rescale space is {guidance_rescale_space} but must be one of `data` or `freq`."
|
| 185 |
+
)
|
| 186 |
+
self.guidance_rescale_space = guidance_rescale_space
|
| 187 |
+
|
| 188 |
+
if parallel_weights is None:
|
| 189 |
+
# Use normal CFG shift (equal weights for parallel and orthogonal components)
|
| 190 |
+
self.parallel_weights = [1.0] * self.levels
|
| 191 |
+
elif isinstance(parallel_weights, float):
|
| 192 |
+
self.parallel_weights = [parallel_weights] * self.levels
|
| 193 |
+
elif len(parallel_weights) == self.levels:
|
| 194 |
+
self.parallel_weights = parallel_weights
|
| 195 |
+
else:
|
| 196 |
+
raise ValueError(
|
| 197 |
+
f"`parallel_weights` has length {len(parallel_weights)} but should have the same length as "
|
| 198 |
+
f"`guidance_scales` ({len(self.guidance_scales)})"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
self.use_original_formulation = use_original_formulation
|
| 202 |
+
self.upcast_to_double = upcast_to_double
|
| 203 |
+
|
| 204 |
+
if isinstance(start, float):
|
| 205 |
+
self.guidance_start = [start] * self.levels
|
| 206 |
+
elif len(start) == self.levels:
|
| 207 |
+
self.guidance_start = start
|
| 208 |
+
else:
|
| 209 |
+
raise ValueError(
|
| 210 |
+
f"`start` has length {len(start)} but should have the same length as `guidance_scales` "
|
| 211 |
+
f"({len(self.guidance_scales)})"
|
| 212 |
+
)
|
| 213 |
+
if isinstance(stop, float):
|
| 214 |
+
self.guidance_stop = [stop] * self.levels
|
| 215 |
+
elif len(stop) == self.levels:
|
| 216 |
+
self.guidance_stop = stop
|
| 217 |
+
else:
|
| 218 |
+
raise ValueError(
|
| 219 |
+
f"`stop` has length {len(stop)} but should have the same length as `guidance_scales` "
|
| 220 |
+
f"({len(self.guidance_scales)})"
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 224 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 225 |
+
data_batches = []
|
| 226 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 227 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 228 |
+
data_batches.append(data_batch)
|
| 229 |
+
return data_batches
|
| 230 |
+
|
| 231 |
+
def prepare_inputs_from_block_state(
|
| 232 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 233 |
+
) -> list["BlockState"]:
|
| 234 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 235 |
+
data_batches = []
|
| 236 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 237 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 238 |
+
data_batches.append(data_batch)
|
| 239 |
+
return data_batches
|
| 240 |
+
|
| 241 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 242 |
+
pred = None
|
| 243 |
+
|
| 244 |
+
if not self._is_fdg_enabled():
|
| 245 |
+
pred = pred_cond
|
| 246 |
+
else:
|
| 247 |
+
# Apply the frequency transform (e.g. Laplacian pyramid) to the conditional and unconditional predictions.
|
| 248 |
+
pred_cond_pyramid = build_laplacian_pyramid_func(pred_cond, self.levels)
|
| 249 |
+
pred_uncond_pyramid = build_laplacian_pyramid_func(pred_uncond, self.levels)
|
| 250 |
+
|
| 251 |
+
# From high frequencies to low frequencies, following the paper implementation
|
| 252 |
+
pred_guided_pyramid = []
|
| 253 |
+
parameters = zip(self.guidance_scales, self.parallel_weights, self.guidance_rescale)
|
| 254 |
+
for level, (guidance_scale, parallel_weight, guidance_rescale) in enumerate(parameters):
|
| 255 |
+
if self._is_fdg_enabled_for_level(level):
|
| 256 |
+
# Get the cond/uncond preds (in freq space) at the current frequency level
|
| 257 |
+
pred_cond_freq = pred_cond_pyramid[level]
|
| 258 |
+
pred_uncond_freq = pred_uncond_pyramid[level]
|
| 259 |
+
|
| 260 |
+
shift = pred_cond_freq - pred_uncond_freq
|
| 261 |
+
|
| 262 |
+
# Apply parallel weights, if used (1.0 corresponds to using the normal CFG shift)
|
| 263 |
+
if not math.isclose(parallel_weight, 1.0):
|
| 264 |
+
shift_parallel, shift_orthogonal = project(shift, pred_cond_freq, self.upcast_to_double)
|
| 265 |
+
shift = parallel_weight * shift_parallel + shift_orthogonal
|
| 266 |
+
|
| 267 |
+
# Apply CFG update for the current frequency level
|
| 268 |
+
pred = pred_cond_freq if self.use_original_formulation else pred_uncond_freq
|
| 269 |
+
pred = pred + guidance_scale * shift
|
| 270 |
+
|
| 271 |
+
if self.guidance_rescale_space == "freq" and guidance_rescale > 0.0:
|
| 272 |
+
pred = rescale_noise_cfg(pred, pred_cond_freq, guidance_rescale)
|
| 273 |
+
|
| 274 |
+
# Add the current FDG guided level to the FDG prediction pyramid
|
| 275 |
+
pred_guided_pyramid.append(pred)
|
| 276 |
+
else:
|
| 277 |
+
# Add the current pred_cond_pyramid level as the "non-FDG" prediction
|
| 278 |
+
pred_guided_pyramid.append(pred_cond_freq)
|
| 279 |
+
|
| 280 |
+
# Convert from frequency space back to data (e.g. pixel) space by applying inverse freq transform
|
| 281 |
+
pred = build_image_from_pyramid(pred_guided_pyramid)
|
| 282 |
+
|
| 283 |
+
# If rescaling in data space, use the first elem of self.guidance_rescale as the "global" rescale value
|
| 284 |
+
# across all freq levels
|
| 285 |
+
if self.guidance_rescale_space == "data" and self.guidance_rescale[0] > 0.0:
|
| 286 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale[0])
|
| 287 |
+
|
| 288 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 289 |
+
|
| 290 |
+
@property
|
| 291 |
+
def is_conditional(self) -> bool:
|
| 292 |
+
return self._count_prepared == 1
|
| 293 |
+
|
| 294 |
+
@property
|
| 295 |
+
def num_conditions(self) -> int:
|
| 296 |
+
num_conditions = 1
|
| 297 |
+
if self._is_fdg_enabled():
|
| 298 |
+
num_conditions += 1
|
| 299 |
+
return num_conditions
|
| 300 |
+
|
| 301 |
+
def _is_fdg_enabled(self) -> bool:
|
| 302 |
+
if not self._enabled:
|
| 303 |
+
return False
|
| 304 |
+
|
| 305 |
+
is_within_range = True
|
| 306 |
+
if self._num_inference_steps is not None:
|
| 307 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 308 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 309 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 310 |
+
|
| 311 |
+
is_close = False
|
| 312 |
+
if self.use_original_formulation:
|
| 313 |
+
is_close = all(math.isclose(guidance_scale, 0.0) for guidance_scale in self.guidance_scales)
|
| 314 |
+
else:
|
| 315 |
+
is_close = all(math.isclose(guidance_scale, 1.0) for guidance_scale in self.guidance_scales)
|
| 316 |
+
|
| 317 |
+
return is_within_range and not is_close
|
| 318 |
+
|
| 319 |
+
def _is_fdg_enabled_for_level(self, level: int) -> bool:
|
| 320 |
+
if not self._enabled:
|
| 321 |
+
return False
|
| 322 |
+
|
| 323 |
+
is_within_range = True
|
| 324 |
+
if self._num_inference_steps is not None:
|
| 325 |
+
skip_start_step = int(self.guidance_start[level] * self._num_inference_steps)
|
| 326 |
+
skip_stop_step = int(self.guidance_stop[level] * self._num_inference_steps)
|
| 327 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 328 |
+
|
| 329 |
+
is_close = False
|
| 330 |
+
if self.use_original_formulation:
|
| 331 |
+
is_close = math.isclose(self.guidance_scales[level], 0.0)
|
| 332 |
+
else:
|
| 333 |
+
is_close = math.isclose(self.guidance_scales[level], 1.0)
|
| 334 |
+
|
| 335 |
+
return is_within_range and not is_close
|
diffusers/guiders/guider_utils.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
from typing import TYPE_CHECKING, Any
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
from huggingface_hub.utils import validate_hf_hub_args
|
| 22 |
+
from typing_extensions import Self
|
| 23 |
+
|
| 24 |
+
from ..configuration_utils import ConfigMixin
|
| 25 |
+
from ..utils import BaseOutput, PushToHubMixin, get_logger
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
if TYPE_CHECKING:
|
| 29 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
GUIDER_CONFIG_NAME = "guider_config.json"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class BaseGuidance(ConfigMixin, PushToHubMixin):
|
| 39 |
+
r"""Base class providing the skeleton for implementing guidance techniques."""
|
| 40 |
+
|
| 41 |
+
config_name = GUIDER_CONFIG_NAME
|
| 42 |
+
_input_predictions = None
|
| 43 |
+
_identifier_key = "__guidance_identifier__"
|
| 44 |
+
|
| 45 |
+
def __init__(self, start: float = 0.0, stop: float = 1.0, enabled: bool = True):
|
| 46 |
+
logger.warning(
|
| 47 |
+
"Guiders are currently an experimental feature under active development. The API is subject to breaking changes in future releases."
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
self._start = start
|
| 51 |
+
self._stop = stop
|
| 52 |
+
self._step: int = None
|
| 53 |
+
self._num_inference_steps: int = None
|
| 54 |
+
self._timestep: torch.LongTensor = None
|
| 55 |
+
self._count_prepared = 0
|
| 56 |
+
self._input_fields: dict[str, str | tuple[str, str]] = None
|
| 57 |
+
self._enabled = enabled
|
| 58 |
+
|
| 59 |
+
if not (0.0 <= start < 1.0):
|
| 60 |
+
raise ValueError(f"Expected `start` to be between 0.0 and 1.0, but got {start}.")
|
| 61 |
+
if not (start <= stop <= 1.0):
|
| 62 |
+
raise ValueError(f"Expected `stop` to be between {start} and 1.0, but got {stop}.")
|
| 63 |
+
|
| 64 |
+
if self._input_predictions is None or not isinstance(self._input_predictions, list):
|
| 65 |
+
raise ValueError(
|
| 66 |
+
"`_input_predictions` must be a list of required prediction names for the guidance technique."
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
def new(self, **kwargs):
|
| 70 |
+
"""
|
| 71 |
+
Creates a copy of this guider instance, optionally with modified configuration parameters.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
**kwargs: Configuration parameters to override in the new instance. If no kwargs are provided,
|
| 75 |
+
returns an exact copy with the same configuration.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
A new guider instance with the same (or updated) configuration.
|
| 79 |
+
|
| 80 |
+
Example:
|
| 81 |
+
```python
|
| 82 |
+
# Create a CFG guider
|
| 83 |
+
guider = ClassifierFreeGuidance(guidance_scale=3.5)
|
| 84 |
+
|
| 85 |
+
# Create an exact copy
|
| 86 |
+
same_guider = guider.new()
|
| 87 |
+
|
| 88 |
+
# Create a copy with different start step, keeping other config the same
|
| 89 |
+
new_guider = guider.new(guidance_scale=5)
|
| 90 |
+
```
|
| 91 |
+
"""
|
| 92 |
+
return self.__class__.from_config(self.config, **kwargs)
|
| 93 |
+
|
| 94 |
+
def disable(self):
|
| 95 |
+
self._enabled = False
|
| 96 |
+
|
| 97 |
+
def enable(self):
|
| 98 |
+
self._enabled = True
|
| 99 |
+
|
| 100 |
+
def set_state(self, step: int, num_inference_steps: int, timestep: torch.LongTensor) -> None:
|
| 101 |
+
self._step = step
|
| 102 |
+
self._num_inference_steps = num_inference_steps
|
| 103 |
+
self._timestep = timestep
|
| 104 |
+
self._count_prepared = 0
|
| 105 |
+
|
| 106 |
+
def get_state(self) -> dict[str, Any]:
|
| 107 |
+
"""
|
| 108 |
+
Returns the current state of the guidance technique as a dictionary. The state variables will be included in
|
| 109 |
+
the __repr__ method. Returns:
|
| 110 |
+
`dict[str, Any]`: A dictionary containing the current state variables including:
|
| 111 |
+
- step: Current inference step
|
| 112 |
+
- num_inference_steps: Total number of inference steps
|
| 113 |
+
- timestep: Current timestep tensor
|
| 114 |
+
- count_prepared: Number of times prepare_models has been called
|
| 115 |
+
- enabled: Whether the guidance is enabled
|
| 116 |
+
- num_conditions: Number of conditions
|
| 117 |
+
"""
|
| 118 |
+
state = {
|
| 119 |
+
"step": self._step,
|
| 120 |
+
"num_inference_steps": self._num_inference_steps,
|
| 121 |
+
"timestep": self._timestep,
|
| 122 |
+
"count_prepared": self._count_prepared,
|
| 123 |
+
"enabled": self._enabled,
|
| 124 |
+
"num_conditions": self.num_conditions,
|
| 125 |
+
}
|
| 126 |
+
return state
|
| 127 |
+
|
| 128 |
+
def __repr__(self) -> str:
|
| 129 |
+
"""
|
| 130 |
+
Returns a string representation of the guidance object including both config and current state.
|
| 131 |
+
"""
|
| 132 |
+
# Get ConfigMixin's __repr__
|
| 133 |
+
str_repr = super().__repr__()
|
| 134 |
+
|
| 135 |
+
# Get current state
|
| 136 |
+
state = self.get_state()
|
| 137 |
+
|
| 138 |
+
# Format each state variable on its own line with indentation
|
| 139 |
+
state_lines = []
|
| 140 |
+
for k, v in state.items():
|
| 141 |
+
# Convert value to string and handle multi-line values
|
| 142 |
+
v_str = str(v)
|
| 143 |
+
if "\n" in v_str:
|
| 144 |
+
# For multi-line values (like MomentumBuffer), indent subsequent lines
|
| 145 |
+
v_lines = v_str.split("\n")
|
| 146 |
+
v_str = v_lines[0] + "\n" + "\n".join([" " + line for line in v_lines[1:]])
|
| 147 |
+
state_lines.append(f" {k}: {v_str}")
|
| 148 |
+
|
| 149 |
+
state_str = "\n".join(state_lines)
|
| 150 |
+
|
| 151 |
+
return f"{str_repr}\nState:\n{state_str}"
|
| 152 |
+
|
| 153 |
+
def prepare_models(self, denoiser: torch.nn.Module) -> None:
|
| 154 |
+
"""
|
| 155 |
+
Prepares the models for the guidance technique on a given batch of data. This method should be overridden in
|
| 156 |
+
subclasses to implement specific model preparation logic.
|
| 157 |
+
"""
|
| 158 |
+
self._count_prepared += 1
|
| 159 |
+
|
| 160 |
+
def cleanup_models(self, denoiser: torch.nn.Module) -> None:
|
| 161 |
+
"""
|
| 162 |
+
Cleans up the models for the guidance technique after a given batch of data. This method should be overridden
|
| 163 |
+
in subclasses to implement specific model cleanup logic. It is useful for removing any hooks or other stateful
|
| 164 |
+
modifications made during `prepare_models`.
|
| 165 |
+
"""
|
| 166 |
+
pass
|
| 167 |
+
|
| 168 |
+
def prepare_inputs(self, data: "BlockState") -> list["BlockState"]:
|
| 169 |
+
raise NotImplementedError("BaseGuidance::prepare_inputs must be implemented in subclasses.")
|
| 170 |
+
|
| 171 |
+
def prepare_inputs_from_block_state(
|
| 172 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 173 |
+
) -> list["BlockState"]:
|
| 174 |
+
raise NotImplementedError("BaseGuidance::prepare_inputs_from_block_state must be implemented in subclasses.")
|
| 175 |
+
|
| 176 |
+
def __call__(self, data: list["BlockState"]) -> Any:
|
| 177 |
+
if not all(hasattr(d, "noise_pred") for d in data):
|
| 178 |
+
raise ValueError("Expected all data to have `noise_pred` attribute.")
|
| 179 |
+
if len(data) != self.num_conditions:
|
| 180 |
+
raise ValueError(
|
| 181 |
+
f"Expected {self.num_conditions} data items, but got {len(data)}. Please check the input data."
|
| 182 |
+
)
|
| 183 |
+
forward_inputs = {getattr(d, self._identifier_key): d.noise_pred for d in data}
|
| 184 |
+
return self.forward(**forward_inputs)
|
| 185 |
+
|
| 186 |
+
def forward(self, *args, **kwargs) -> Any:
|
| 187 |
+
raise NotImplementedError("BaseGuidance::forward must be implemented in subclasses.")
|
| 188 |
+
|
| 189 |
+
@property
|
| 190 |
+
def is_conditional(self) -> bool:
|
| 191 |
+
raise NotImplementedError("BaseGuidance::is_conditional must be implemented in subclasses.")
|
| 192 |
+
|
| 193 |
+
@property
|
| 194 |
+
def is_unconditional(self) -> bool:
|
| 195 |
+
return not self.is_conditional
|
| 196 |
+
|
| 197 |
+
@property
|
| 198 |
+
def num_conditions(self) -> int:
|
| 199 |
+
raise NotImplementedError("BaseGuidance::num_conditions must be implemented in subclasses.")
|
| 200 |
+
|
| 201 |
+
@classmethod
|
| 202 |
+
def _prepare_batch(
|
| 203 |
+
cls,
|
| 204 |
+
data: dict[str, tuple[torch.Tensor, torch.Tensor]],
|
| 205 |
+
tuple_index: int,
|
| 206 |
+
identifier: str,
|
| 207 |
+
) -> "BlockState":
|
| 208 |
+
"""
|
| 209 |
+
Prepares a batch of data for the guidance technique. This method is used in the `prepare_inputs` method of the
|
| 210 |
+
`BaseGuidance` class. It prepares the batch based on the provided tuple index.
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
input_fields (`dict[str, str | tuple[str, str]]`):
|
| 214 |
+
A dictionary where the keys are the names of the fields that will be used to store the data once it is
|
| 215 |
+
prepared with `prepare_inputs`. The values can be either a string or a tuple of length 2, which is used
|
| 216 |
+
to look up the required data provided for preparation. If a string is provided, it will be used as the
|
| 217 |
+
conditional data (or unconditional if used with a guidance method that requires it). If a tuple of
|
| 218 |
+
length 2 is provided, the first element must be the conditional data identifier and the second element
|
| 219 |
+
must be the unconditional data identifier or None.
|
| 220 |
+
data (`BlockState`):
|
| 221 |
+
The input data to be prepared.
|
| 222 |
+
tuple_index (`int`):
|
| 223 |
+
The index to use when accessing input fields that are tuples.
|
| 224 |
+
|
| 225 |
+
Returns:
|
| 226 |
+
`BlockState`: The prepared batch of data.
|
| 227 |
+
"""
|
| 228 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 229 |
+
|
| 230 |
+
data_batch = {}
|
| 231 |
+
for key, value in data.items():
|
| 232 |
+
try:
|
| 233 |
+
if isinstance(value, torch.Tensor):
|
| 234 |
+
data_batch[key] = value
|
| 235 |
+
elif isinstance(value, tuple):
|
| 236 |
+
data_batch[key] = value[tuple_index]
|
| 237 |
+
else:
|
| 238 |
+
raise ValueError(f"Invalid value type: {type(value)}")
|
| 239 |
+
except ValueError:
|
| 240 |
+
logger.debug(f"`data` does not have attribute(s) {value}, skipping.")
|
| 241 |
+
data_batch[cls._identifier_key] = identifier
|
| 242 |
+
return BlockState(**data_batch)
|
| 243 |
+
|
| 244 |
+
@classmethod
|
| 245 |
+
def _prepare_batch_from_block_state(
|
| 246 |
+
cls,
|
| 247 |
+
input_fields: dict[str, str | tuple[str, str]],
|
| 248 |
+
data: "BlockState",
|
| 249 |
+
tuple_index: int,
|
| 250 |
+
identifier: str,
|
| 251 |
+
) -> "BlockState":
|
| 252 |
+
"""
|
| 253 |
+
Prepares a batch of data for the guidance technique. This method is used in the `prepare_inputs` method of the
|
| 254 |
+
`BaseGuidance` class. It prepares the batch based on the provided tuple index.
|
| 255 |
+
|
| 256 |
+
Args:
|
| 257 |
+
input_fields (`dict[str, str | tuple[str, str]]`):
|
| 258 |
+
A dictionary where the keys are the names of the fields that will be used to store the data once it is
|
| 259 |
+
prepared with `prepare_inputs`. The values can be either a string or a tuple of length 2, which is used
|
| 260 |
+
to look up the required data provided for preparation. If a string is provided, it will be used as the
|
| 261 |
+
conditional data (or unconditional if used with a guidance method that requires it). If a tuple of
|
| 262 |
+
length 2 is provided, the first element must be the conditional data identifier and the second element
|
| 263 |
+
must be the unconditional data identifier or None.
|
| 264 |
+
data (`BlockState`):
|
| 265 |
+
The input data to be prepared.
|
| 266 |
+
tuple_index (`int`):
|
| 267 |
+
The index to use when accessing input fields that are tuples.
|
| 268 |
+
|
| 269 |
+
Returns:
|
| 270 |
+
`BlockState`: The prepared batch of data.
|
| 271 |
+
"""
|
| 272 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 273 |
+
|
| 274 |
+
data_batch = {}
|
| 275 |
+
for key, value in input_fields.items():
|
| 276 |
+
try:
|
| 277 |
+
if isinstance(value, str):
|
| 278 |
+
data_batch[key] = getattr(data, value)
|
| 279 |
+
elif isinstance(value, tuple):
|
| 280 |
+
data_batch[key] = getattr(data, value[tuple_index])
|
| 281 |
+
else:
|
| 282 |
+
# We've already checked that value is a string or a tuple of strings with length 2
|
| 283 |
+
pass
|
| 284 |
+
except AttributeError:
|
| 285 |
+
logger.debug(f"`data` does not have attribute(s) {value}, skipping.")
|
| 286 |
+
data_batch[cls._identifier_key] = identifier
|
| 287 |
+
return BlockState(**data_batch)
|
| 288 |
+
|
| 289 |
+
@classmethod
|
| 290 |
+
@validate_hf_hub_args
|
| 291 |
+
def from_pretrained(
|
| 292 |
+
cls,
|
| 293 |
+
pretrained_model_name_or_path: str | os.PathLike | None = None,
|
| 294 |
+
subfolder: str | None = None,
|
| 295 |
+
return_unused_kwargs=False,
|
| 296 |
+
**kwargs,
|
| 297 |
+
) -> Self:
|
| 298 |
+
r"""
|
| 299 |
+
Instantiate a guider from a pre-defined JSON configuration file in a local directory or Hub repository.
|
| 300 |
+
|
| 301 |
+
Parameters:
|
| 302 |
+
pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*):
|
| 303 |
+
Can be either:
|
| 304 |
+
|
| 305 |
+
- A string, the *model id* (for example `google/ddpm-celebahq-256`) of a pretrained model hosted on
|
| 306 |
+
the Hub.
|
| 307 |
+
- A path to a *directory* (for example `./my_model_directory`) containing the guider configuration
|
| 308 |
+
saved with [`~BaseGuidance.save_pretrained`].
|
| 309 |
+
subfolder (`str`, *optional*):
|
| 310 |
+
The subfolder location of a model file within a larger model repository on the Hub or locally.
|
| 311 |
+
return_unused_kwargs (`bool`, *optional*, defaults to `False`):
|
| 312 |
+
Whether kwargs that are not consumed by the Python class should be returned or not.
|
| 313 |
+
cache_dir (`str | os.PathLike`, *optional*):
|
| 314 |
+
Path to a directory where a downloaded pretrained model configuration is cached if the standard cache
|
| 315 |
+
is not used.
|
| 316 |
+
force_download (`bool`, *optional*, defaults to `False`):
|
| 317 |
+
Whether or not to force the (re-)download of the model weights and configuration files, overriding the
|
| 318 |
+
cached versions if they exist.
|
| 319 |
+
|
| 320 |
+
proxies (`dict[str, str]`, *optional*):
|
| 321 |
+
A dictionary of proxy servers to use by protocol or endpoint, for example, `{'http': 'foo.bar:3128',
|
| 322 |
+
'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request.
|
| 323 |
+
output_loading_info(`bool`, *optional*, defaults to `False`):
|
| 324 |
+
Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages.
|
| 325 |
+
local_files_only(`bool`, *optional*, defaults to `False`):
|
| 326 |
+
Whether to only load local model weights and configuration files or not. If set to `True`, the model
|
| 327 |
+
won't be downloaded from the Hub.
|
| 328 |
+
token (`str` or *bool*, *optional*):
|
| 329 |
+
The token to use as HTTP bearer authorization for remote files. If `True`, the token generated from
|
| 330 |
+
`diffusers-cli login` (stored in `~/.huggingface`) is used.
|
| 331 |
+
revision (`str`, *optional*, defaults to `"main"`):
|
| 332 |
+
The specific model version to use. It can be a branch name, a tag name, a commit id, or any identifier
|
| 333 |
+
allowed by Git.
|
| 334 |
+
|
| 335 |
+
> [!TIP] > To use private or [gated models](https://huggingface.co/docs/hub/models-gated#gated-models), log-in
|
| 336 |
+
with `hf > auth login`. You can also activate the special >
|
| 337 |
+
["offline-mode"](https://huggingface.co/diffusers/installation.html#offline-mode) to use this method in a >
|
| 338 |
+
firewalled environment.
|
| 339 |
+
|
| 340 |
+
"""
|
| 341 |
+
config, kwargs, commit_hash = cls.load_config(
|
| 342 |
+
pretrained_model_name_or_path=pretrained_model_name_or_path,
|
| 343 |
+
subfolder=subfolder,
|
| 344 |
+
return_unused_kwargs=True,
|
| 345 |
+
return_commit_hash=True,
|
| 346 |
+
**kwargs,
|
| 347 |
+
)
|
| 348 |
+
return cls.from_config(config, return_unused_kwargs=return_unused_kwargs, **kwargs)
|
| 349 |
+
|
| 350 |
+
def save_pretrained(self, save_directory: str | os.PathLike, push_to_hub: bool = False, **kwargs):
|
| 351 |
+
"""
|
| 352 |
+
Save a guider configuration object to a directory so that it can be reloaded using the
|
| 353 |
+
[`~BaseGuidance.from_pretrained`] class method.
|
| 354 |
+
|
| 355 |
+
Args:
|
| 356 |
+
save_directory (`str` or `os.PathLike`):
|
| 357 |
+
Directory where the configuration JSON file will be saved (will be created if it does not exist).
|
| 358 |
+
push_to_hub (`bool`, *optional*, defaults to `False`):
|
| 359 |
+
Whether or not to push your model to the Hugging Face Hub after saving it. You can specify the
|
| 360 |
+
repository you want to push to with `repo_id` (will default to the name of `save_directory` in your
|
| 361 |
+
namespace).
|
| 362 |
+
kwargs (`dict[str, Any]`, *optional*):
|
| 363 |
+
Additional keyword arguments passed along to the [`~utils.PushToHubMixin.push_to_hub`] method.
|
| 364 |
+
"""
|
| 365 |
+
self.save_config(save_directory=save_directory, push_to_hub=push_to_hub, **kwargs)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class GuiderOutput(BaseOutput):
|
| 369 |
+
pred: torch.Tensor
|
| 370 |
+
pred_cond: torch.Tensor | None
|
| 371 |
+
pred_uncond: torch.Tensor | None
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def rescale_noise_cfg(noise_cfg, noise_pred_text, guidance_rescale=0.0):
|
| 375 |
+
r"""
|
| 376 |
+
Rescales `noise_cfg` tensor based on `guidance_rescale` to improve image quality and fix overexposure. Based on
|
| 377 |
+
Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 378 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 379 |
+
|
| 380 |
+
Args:
|
| 381 |
+
noise_cfg (`torch.Tensor`):
|
| 382 |
+
The predicted noise tensor for the guided diffusion process.
|
| 383 |
+
noise_pred_text (`torch.Tensor`):
|
| 384 |
+
The predicted noise tensor for the text-guided diffusion process.
|
| 385 |
+
guidance_rescale (`float`, *optional*, defaults to 0.0):
|
| 386 |
+
A rescale factor applied to the noise predictions.
|
| 387 |
+
Returns:
|
| 388 |
+
noise_cfg (`torch.Tensor`): The rescaled noise prediction tensor.
|
| 389 |
+
"""
|
| 390 |
+
std_text = noise_pred_text.std(dim=list(range(1, noise_pred_text.ndim)), keepdim=True)
|
| 391 |
+
std_cfg = noise_cfg.std(dim=list(range(1, noise_cfg.ndim)), keepdim=True)
|
| 392 |
+
# rescale the results from guidance (fixes overexposure)
|
| 393 |
+
noise_pred_rescaled = noise_cfg * (std_text / std_cfg)
|
| 394 |
+
# mix with the original results from guidance by factor guidance_rescale to avoid "plain looking" images
|
| 395 |
+
noise_cfg = guidance_rescale * noise_pred_rescaled + (1 - guidance_rescale) * noise_cfg
|
| 396 |
+
return noise_cfg
|
diffusers/guiders/ltx2_guidance.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from typing import TYPE_CHECKING
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ..configuration_utils import register_to_config
|
| 21 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
if TYPE_CHECKING:
|
| 25 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class LTX2Guidance(BaseGuidance):
|
| 29 |
+
"""
|
| 30 |
+
Single-modality guider for LTX-2.X. Combines up to three terms via the delta formulation:
|
| 31 |
+
- classifier-free guidance (CFG),
|
| 32 |
+
- spatio-temporal guidance (STG), an extra pass with a set of transformer blocks perturbed,
|
| 33 |
+
- modality-isolation guidance, an extra pass with A2V/V2A cross-attention disabled.
|
| 34 |
+
|
| 35 |
+
One instance drives one modality, so a multimodal checkpoint carries several: LTX-2.X pairs a video `guider`
|
| 36 |
+
with an `audio_guider`, each holding its own scales. The final guidance estimate is computed in whatever space
|
| 37 |
+
the caller feeds it — the LTX-2 denoiser converts velocity->x0 before calling this and back afterwards, so
|
| 38 |
+
`forward` operates on x0 predictions.
|
| 39 |
+
|
| 40 |
+
Unlike `SkipLayerGuidance`, which applies its perturbations with hooks, `LTX2Guidance` expects its non-CFG
|
| 41 |
+
perturbations to be configurable through model forward arguments (`spatio_temporal_guidance_blocks` and
|
| 42 |
+
`isolate_modalities` in `LTX2VideoTransformer3DModel`). The calling denoiser block is responsible for setting
|
| 43 |
+
those per pass identifier; see `LTX2LoopDenoiser` for a usage example.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
guidance_scale (`float`, defaults to `1.0`):
|
| 47 |
+
CFG scale for this modality. The CFG pass is skipped entirely at `1.0`.
|
| 48 |
+
stg_scale (`float`, defaults to `0.0`):
|
| 49 |
+
Spatio-temporal guidance scale for this modality. The STG pass is skipped entirely at `0.0`.
|
| 50 |
+
modality_scale (`float`, defaults to `1.0`):
|
| 51 |
+
Modality-isolation guidance scale for this modality. That pass is skipped entirely at `1.0`.
|
| 52 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 53 |
+
Rescaling factor to prevent overexposure from high guidance scales. Based on [Common Diffusion Noise
|
| 54 |
+
Schedules and Sample Steps are Flawed](https://huggingface.co/papers/2305.08891). Range: 0.0 (no rescaling)
|
| 55 |
+
to 1.0 (full rescaling).
|
| 56 |
+
spatio_temporal_guidance_blocks (`list[int]`, *optional*):
|
| 57 |
+
Transformer blocks to perturb on the STG pass. How the value is consumed is up to the denoiser block:
|
| 58 |
+
LTX-2's perturbs whole blocks in a single forward that feeds every modality, so it reads this off the video
|
| 59 |
+
guider alone — setting it on the audio guider has no effect, and that guider joins the same STG forward
|
| 60 |
+
through its own `stg_scale`.
|
| 61 |
+
start (`float`, defaults to `0.0`):
|
| 62 |
+
Fraction of denoising steps (0.0-1.0) after which guidance starts.
|
| 63 |
+
stop (`float`, defaults to `1.0`):
|
| 64 |
+
Fraction of denoising steps (0.0-1.0) after which guidance stops.
|
| 65 |
+
enabled (`bool`, defaults to `True`):
|
| 66 |
+
Whether this guider applies guidance at all. When `False`, only the conditional pass runs.
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
# `pred_cond` is the base; the others are the guidance passes.
|
| 70 |
+
_input_predictions = ["pred_cond", "pred_uncond", "pred_cond_stg", "pred_cond_modality"]
|
| 71 |
+
|
| 72 |
+
@register_to_config
|
| 73 |
+
def __init__(
|
| 74 |
+
self,
|
| 75 |
+
guidance_scale: float = 1.0,
|
| 76 |
+
stg_scale: float = 0.0,
|
| 77 |
+
modality_scale: float = 1.0,
|
| 78 |
+
guidance_rescale: float = 0.0,
|
| 79 |
+
spatio_temporal_guidance_blocks: list[int] | None = None,
|
| 80 |
+
start: float = 0.0,
|
| 81 |
+
stop: float = 1.0,
|
| 82 |
+
enabled: bool = True,
|
| 83 |
+
):
|
| 84 |
+
super().__init__(start, stop, enabled)
|
| 85 |
+
self.guidance_scale = guidance_scale
|
| 86 |
+
self.stg_scale = stg_scale
|
| 87 |
+
self.modality_scale = modality_scale
|
| 88 |
+
self.guidance_rescale = guidance_rescale
|
| 89 |
+
self.spatio_temporal_guidance_blocks = spatio_temporal_guidance_blocks
|
| 90 |
+
|
| 91 |
+
# --- per-term enabled checks (public: the denoiser ORs these across the two guiders) --------------------
|
| 92 |
+
def _within_window(self) -> bool:
|
| 93 |
+
if self._num_inference_steps is None:
|
| 94 |
+
return True
|
| 95 |
+
start_step = int(self._start * self._num_inference_steps)
|
| 96 |
+
stop_step = int(self._stop * self._num_inference_steps)
|
| 97 |
+
return start_step <= self._step < stop_step
|
| 98 |
+
|
| 99 |
+
def is_cfg_enabled(self) -> bool:
|
| 100 |
+
return self._enabled and self._within_window() and not math.isclose(self.guidance_scale, 1.0)
|
| 101 |
+
|
| 102 |
+
def is_stg_enabled(self) -> bool:
|
| 103 |
+
return self._enabled and self._within_window() and not math.isclose(self.stg_scale, 0.0)
|
| 104 |
+
|
| 105 |
+
def is_modality_enabled(self) -> bool:
|
| 106 |
+
return self._enabled and self._within_window() and not math.isclose(self.modality_scale, 1.0)
|
| 107 |
+
|
| 108 |
+
def active_predictions(self) -> list[str]:
|
| 109 |
+
preds = ["pred_cond"]
|
| 110 |
+
if self.is_cfg_enabled():
|
| 111 |
+
preds.append("pred_uncond")
|
| 112 |
+
if self.is_stg_enabled():
|
| 113 |
+
preds.append("pred_cond_stg")
|
| 114 |
+
if self.is_modality_enabled():
|
| 115 |
+
preds.append("pred_cond_modality")
|
| 116 |
+
return preds
|
| 117 |
+
|
| 118 |
+
@property
|
| 119 |
+
def num_conditions(self) -> int:
|
| 120 |
+
return len(self.active_predictions())
|
| 121 |
+
|
| 122 |
+
@property
|
| 123 |
+
def is_conditional(self) -> bool:
|
| 124 |
+
# Not load-bearing here: the denoiser owns pass execution and carries the STG/modality flags on the
|
| 125 |
+
# batches, so we never drive `prepare_models`/hooks off `_count_prepared` the way SkipLayerGuidance does.
|
| 126 |
+
return True
|
| 127 |
+
|
| 128 |
+
# Each pass reads its own slot ([cond, uncond, stg, modality]) of every `guider_input_fields` tuple, selected
|
| 129 |
+
# by this per-identifier index. Distinct slots (rather than cond/uncond only) let STG/modality pull the same
|
| 130 |
+
# conditioning as `pred_cond` while the denoiser overrides their per-pass model flags after preparation.
|
| 131 |
+
_PREDICTION_INDEX = {"pred_cond": 0, "pred_uncond": 1, "pred_cond_stg": 2, "pred_cond_modality": 3}
|
| 132 |
+
|
| 133 |
+
def prepare_inputs(self, guider_inputs: dict) -> list["BlockState"]:
|
| 134 |
+
# One identifier-tagged batch per active pass. Every value in `guider_inputs` is a 4-tuple indexed by
|
| 135 |
+
# `_PREDICTION_INDEX` ([cond, uncond, stg, modality]); each pass reads its own slot, so the per-pass model
|
| 136 |
+
# flags (`spatio_temporal_guidance_blocks`, `isolate_modalities`) are carried exactly like the encoder
|
| 137 |
+
# inputs. The denoiser fills each returned batch's `noise_pred` before calling the guider to combine.
|
| 138 |
+
return [
|
| 139 |
+
self._prepare_batch(guider_inputs, self._PREDICTION_INDEX[pred], pred)
|
| 140 |
+
for pred in self.active_predictions()
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
def prepare_inputs_from_block_state(self, data: "BlockState", input_fields: dict) -> list["BlockState"]:
|
| 144 |
+
# One identifier-tagged batch per active pass. Each value in `input_fields` maps a transformer argument to a
|
| 145 |
+
# 4-tuple of block-state attribute names indexed by `_PREDICTION_INDEX` ([cond, uncond, stg, modality]); the
|
| 146 |
+
# base helper reads the pass's slot off `data`. The denoiser then sets the per-pass model flags and fills
|
| 147 |
+
# each batch's `noise_pred` before calling the guider to combine.
|
| 148 |
+
return [
|
| 149 |
+
self._prepare_batch_from_block_state(input_fields, data, self._PREDICTION_INDEX[pred], pred)
|
| 150 |
+
for pred in self.active_predictions()
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
def forward(
|
| 154 |
+
self,
|
| 155 |
+
pred_cond: torch.Tensor,
|
| 156 |
+
pred_uncond: torch.Tensor | None = None,
|
| 157 |
+
pred_cond_stg: torch.Tensor | None = None,
|
| 158 |
+
pred_cond_modality: torch.Tensor | None = None,
|
| 159 |
+
) -> GuiderOutput:
|
| 160 |
+
# Delta formulation (matches the reference LTX-2 combine, applied in the x0 space the denoiser feeds):
|
| 161 |
+
# x0_cond + (gs-1)*(cond-uncond) + stg*(cond-stg) + (mod-1)*(cond-modality), then optional rescale.
|
| 162 |
+
pred = pred_cond
|
| 163 |
+
if self.is_cfg_enabled():
|
| 164 |
+
pred = pred + (self.guidance_scale - 1.0) * (pred_cond - pred_uncond)
|
| 165 |
+
if self.is_stg_enabled():
|
| 166 |
+
pred = pred + self.stg_scale * (pred_cond - pred_cond_stg)
|
| 167 |
+
if self.is_modality_enabled():
|
| 168 |
+
pred = pred + (self.modality_scale - 1.0) * (pred_cond - pred_cond_modality)
|
| 169 |
+
if self.guidance_rescale > 0.0:
|
| 170 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 171 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
diffusers/guiders/magnitude_aware_guidance.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from typing import TYPE_CHECKING
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ..configuration_utils import register_to_config
|
| 21 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
if TYPE_CHECKING:
|
| 25 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class MagnitudeAwareGuidance(BaseGuidance):
|
| 29 |
+
"""
|
| 30 |
+
Magnitude-Aware Mitigation for Boosted Guidance (MAMBO-G): https://huggingface.co/papers/2508.03442
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
guidance_scale (`float`, defaults to `10.0`):
|
| 34 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 35 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 36 |
+
deterioration of image quality.
|
| 37 |
+
alpha (`float`, defaults to `8.0`):
|
| 38 |
+
The alpha parameter for the magnitude-aware guidance. Higher values cause more aggressive suppression of
|
| 39 |
+
guidance scale when the magnitude of the guidance update is large.
|
| 40 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 41 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 42 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 43 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 44 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 45 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 46 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 47 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 48 |
+
start (`float`, defaults to `0.0`):
|
| 49 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 50 |
+
stop (`float`, defaults to `1.0`):
|
| 51 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 55 |
+
|
| 56 |
+
@register_to_config
|
| 57 |
+
def __init__(
|
| 58 |
+
self,
|
| 59 |
+
guidance_scale: float = 10.0,
|
| 60 |
+
alpha: float = 8.0,
|
| 61 |
+
guidance_rescale: float = 0.0,
|
| 62 |
+
use_original_formulation: bool = False,
|
| 63 |
+
start: float = 0.0,
|
| 64 |
+
stop: float = 1.0,
|
| 65 |
+
enabled: bool = True,
|
| 66 |
+
):
|
| 67 |
+
super().__init__(start, stop, enabled)
|
| 68 |
+
|
| 69 |
+
self.guidance_scale = guidance_scale
|
| 70 |
+
self.alpha = alpha
|
| 71 |
+
self.guidance_rescale = guidance_rescale
|
| 72 |
+
self.use_original_formulation = use_original_formulation
|
| 73 |
+
|
| 74 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 75 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 76 |
+
data_batches = []
|
| 77 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 78 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 79 |
+
data_batches.append(data_batch)
|
| 80 |
+
return data_batches
|
| 81 |
+
|
| 82 |
+
def prepare_inputs_from_block_state(
|
| 83 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 84 |
+
) -> list["BlockState"]:
|
| 85 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 86 |
+
data_batches = []
|
| 87 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 88 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 89 |
+
data_batches.append(data_batch)
|
| 90 |
+
return data_batches
|
| 91 |
+
|
| 92 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 93 |
+
pred = None
|
| 94 |
+
|
| 95 |
+
if not self._is_mambo_g_enabled():
|
| 96 |
+
pred = pred_cond
|
| 97 |
+
else:
|
| 98 |
+
pred = mambo_guidance(
|
| 99 |
+
pred_cond,
|
| 100 |
+
pred_uncond,
|
| 101 |
+
self.guidance_scale,
|
| 102 |
+
self.alpha,
|
| 103 |
+
self.use_original_formulation,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
if self.guidance_rescale > 0.0:
|
| 107 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 108 |
+
|
| 109 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 110 |
+
|
| 111 |
+
@property
|
| 112 |
+
def is_conditional(self) -> bool:
|
| 113 |
+
return self._count_prepared == 1
|
| 114 |
+
|
| 115 |
+
@property
|
| 116 |
+
def num_conditions(self) -> int:
|
| 117 |
+
num_conditions = 1
|
| 118 |
+
if self._is_mambo_g_enabled():
|
| 119 |
+
num_conditions += 1
|
| 120 |
+
return num_conditions
|
| 121 |
+
|
| 122 |
+
def _is_mambo_g_enabled(self) -> bool:
|
| 123 |
+
if not self._enabled:
|
| 124 |
+
return False
|
| 125 |
+
|
| 126 |
+
is_within_range = True
|
| 127 |
+
if self._num_inference_steps is not None:
|
| 128 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 129 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 130 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 131 |
+
|
| 132 |
+
is_close = False
|
| 133 |
+
if self.use_original_formulation:
|
| 134 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 135 |
+
else:
|
| 136 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 137 |
+
|
| 138 |
+
return is_within_range and not is_close
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def mambo_guidance(
|
| 142 |
+
pred_cond: torch.Tensor,
|
| 143 |
+
pred_uncond: torch.Tensor,
|
| 144 |
+
guidance_scale: float,
|
| 145 |
+
alpha: float = 8.0,
|
| 146 |
+
use_original_formulation: bool = False,
|
| 147 |
+
):
|
| 148 |
+
dim = list(range(1, len(pred_cond.shape)))
|
| 149 |
+
diff = pred_cond - pred_uncond
|
| 150 |
+
ratio = torch.norm(diff, dim=dim, keepdim=True) / torch.norm(pred_uncond, dim=dim, keepdim=True)
|
| 151 |
+
guidance_scale_final = (
|
| 152 |
+
guidance_scale * torch.exp(-alpha * ratio)
|
| 153 |
+
if use_original_formulation
|
| 154 |
+
else 1.0 + (guidance_scale - 1.0) * torch.exp(-alpha * ratio)
|
| 155 |
+
)
|
| 156 |
+
pred = pred_cond if use_original_formulation else pred_uncond
|
| 157 |
+
pred = pred + guidance_scale_final * diff
|
| 158 |
+
|
| 159 |
+
return pred
|
diffusers/guiders/perturbed_attention_guidance.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING, Any
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from ..hooks import HookRegistry, LayerSkipConfig
|
| 24 |
+
from ..hooks.layer_skip import _apply_layer_skip_hook
|
| 25 |
+
from ..utils import get_logger
|
| 26 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
if TYPE_CHECKING:
|
| 30 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class PerturbedAttentionGuidance(BaseGuidance):
|
| 37 |
+
"""
|
| 38 |
+
Perturbed Attention Guidance (PAG): https://huggingface.co/papers/2403.17377
|
| 39 |
+
|
| 40 |
+
The intution behind PAG can be thought of as moving the CFG predicted distribution estimates further away from
|
| 41 |
+
worse versions of the conditional distribution estimates. PAG was one of the first techniques to introduce the idea
|
| 42 |
+
of using a worse version of the trained model for better guiding itself in the denoising process. It perturbs the
|
| 43 |
+
attention scores of the latent stream by replacing the score matrix with an identity matrix for selectively chosen
|
| 44 |
+
layers.
|
| 45 |
+
|
| 46 |
+
Additional reading:
|
| 47 |
+
- [Guiding a Diffusion Model with a Bad Version of Itself](https://huggingface.co/papers/2406.02507)
|
| 48 |
+
|
| 49 |
+
PAG is implemented with similar implementation to SkipLayerGuidance due to overlap in the configuration parameters
|
| 50 |
+
and implementation details.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 54 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 55 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 56 |
+
deterioration of image quality.
|
| 57 |
+
perturbed_guidance_scale (`float`, defaults to `2.8`):
|
| 58 |
+
The scale parameter for perturbed attention guidance.
|
| 59 |
+
perturbed_guidance_start (`float`, defaults to `0.01`):
|
| 60 |
+
The fraction of the total number of denoising steps after which perturbed attention guidance starts.
|
| 61 |
+
perturbed_guidance_stop (`float`, defaults to `0.2`):
|
| 62 |
+
The fraction of the total number of denoising steps after which perturbed attention guidance stops.
|
| 63 |
+
perturbed_guidance_layers (`int` or `list[int]`, *optional*):
|
| 64 |
+
The layer indices to apply perturbed attention guidance to. Can be a single integer or a list of integers.
|
| 65 |
+
If not provided, `perturbed_guidance_config` must be provided.
|
| 66 |
+
perturbed_guidance_config (`LayerSkipConfig` or `list[LayerSkipConfig]`, *optional*):
|
| 67 |
+
The configuration for the perturbed attention guidance. Can be a single `LayerSkipConfig` or a list of
|
| 68 |
+
`LayerSkipConfig`. If not provided, `perturbed_guidance_layers` must be provided.
|
| 69 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 70 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 71 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 72 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 73 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 74 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 75 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 76 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 77 |
+
start (`float`, defaults to `0.01`):
|
| 78 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 79 |
+
stop (`float`, defaults to `0.2`):
|
| 80 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
# NOTE: The current implementation does not account for joint latent conditioning (text + image/video tokens in
|
| 84 |
+
# the same latent stream). It assumes the entire latent is a single stream of visual tokens. It would be very
|
| 85 |
+
# complex to support joint latent conditioning in a model-agnostic manner without specializing the implementation
|
| 86 |
+
# for each model architecture.
|
| 87 |
+
|
| 88 |
+
_input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
|
| 89 |
+
|
| 90 |
+
@register_to_config
|
| 91 |
+
def __init__(
|
| 92 |
+
self,
|
| 93 |
+
guidance_scale: float = 7.5,
|
| 94 |
+
perturbed_guidance_scale: float = 2.8,
|
| 95 |
+
perturbed_guidance_start: float = 0.01,
|
| 96 |
+
perturbed_guidance_stop: float = 0.2,
|
| 97 |
+
perturbed_guidance_layers: int | list[int] | None = None,
|
| 98 |
+
perturbed_guidance_config: LayerSkipConfig | list[LayerSkipConfig] | dict[str, Any] = None,
|
| 99 |
+
guidance_rescale: float = 0.0,
|
| 100 |
+
use_original_formulation: bool = False,
|
| 101 |
+
start: float = 0.0,
|
| 102 |
+
stop: float = 1.0,
|
| 103 |
+
enabled: bool = True,
|
| 104 |
+
):
|
| 105 |
+
super().__init__(start, stop, enabled)
|
| 106 |
+
|
| 107 |
+
self.guidance_scale = guidance_scale
|
| 108 |
+
self.skip_layer_guidance_scale = perturbed_guidance_scale
|
| 109 |
+
self.skip_layer_guidance_start = perturbed_guidance_start
|
| 110 |
+
self.skip_layer_guidance_stop = perturbed_guidance_stop
|
| 111 |
+
self.guidance_rescale = guidance_rescale
|
| 112 |
+
self.use_original_formulation = use_original_formulation
|
| 113 |
+
|
| 114 |
+
if perturbed_guidance_config is None:
|
| 115 |
+
if perturbed_guidance_layers is None:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
"`perturbed_guidance_layers` must be provided if `perturbed_guidance_config` is not specified."
|
| 118 |
+
)
|
| 119 |
+
perturbed_guidance_config = LayerSkipConfig(
|
| 120 |
+
indices=perturbed_guidance_layers,
|
| 121 |
+
fqn="auto",
|
| 122 |
+
skip_attention=False,
|
| 123 |
+
skip_attention_scores=True,
|
| 124 |
+
skip_ff=False,
|
| 125 |
+
)
|
| 126 |
+
else:
|
| 127 |
+
if perturbed_guidance_layers is not None:
|
| 128 |
+
raise ValueError(
|
| 129 |
+
"`perturbed_guidance_layers` should not be provided if `perturbed_guidance_config` is specified."
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
if isinstance(perturbed_guidance_config, dict):
|
| 133 |
+
perturbed_guidance_config = LayerSkipConfig.from_dict(perturbed_guidance_config)
|
| 134 |
+
|
| 135 |
+
if isinstance(perturbed_guidance_config, LayerSkipConfig):
|
| 136 |
+
perturbed_guidance_config = [perturbed_guidance_config]
|
| 137 |
+
|
| 138 |
+
if not isinstance(perturbed_guidance_config, list):
|
| 139 |
+
raise ValueError(
|
| 140 |
+
"`perturbed_guidance_config` must be a `LayerSkipConfig`, a list of `LayerSkipConfig`, or a dict that can be converted to a `LayerSkipConfig`."
|
| 141 |
+
)
|
| 142 |
+
elif isinstance(next(iter(perturbed_guidance_config), None), dict):
|
| 143 |
+
perturbed_guidance_config = [LayerSkipConfig.from_dict(config) for config in perturbed_guidance_config]
|
| 144 |
+
|
| 145 |
+
for config in perturbed_guidance_config:
|
| 146 |
+
if config.skip_attention or not config.skip_attention_scores or config.skip_ff:
|
| 147 |
+
logger.warning(
|
| 148 |
+
"Perturbed Attention Guidance is designed to perturb attention scores, so `skip_attention` should be False, `skip_attention_scores` should be True, and `skip_ff` should be False. "
|
| 149 |
+
"Please check your configuration. Modifying the config to match the expected values."
|
| 150 |
+
)
|
| 151 |
+
config.skip_attention = False
|
| 152 |
+
config.skip_attention_scores = True
|
| 153 |
+
config.skip_ff = False
|
| 154 |
+
|
| 155 |
+
self.skip_layer_config = perturbed_guidance_config
|
| 156 |
+
self._skip_layer_hook_names = [f"SkipLayerGuidance_{i}" for i in range(len(self.skip_layer_config))]
|
| 157 |
+
|
| 158 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.prepare_models
|
| 159 |
+
def prepare_models(self, denoiser: torch.nn.Module) -> None:
|
| 160 |
+
self._count_prepared += 1
|
| 161 |
+
if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
|
| 162 |
+
for name, config in zip(self._skip_layer_hook_names, self.skip_layer_config):
|
| 163 |
+
_apply_layer_skip_hook(denoiser, config, name=name)
|
| 164 |
+
|
| 165 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.cleanup_models
|
| 166 |
+
def cleanup_models(self, denoiser: torch.nn.Module) -> None:
|
| 167 |
+
if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
|
| 168 |
+
registry = HookRegistry.check_if_exists_or_initialize(denoiser)
|
| 169 |
+
# Remove the hooks after inference
|
| 170 |
+
for hook_name in self._skip_layer_hook_names:
|
| 171 |
+
registry.remove_hook(hook_name, recurse=True)
|
| 172 |
+
|
| 173 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.prepare_inputs
|
| 174 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 175 |
+
if self.num_conditions == 1:
|
| 176 |
+
tuple_indices = [0]
|
| 177 |
+
input_predictions = ["pred_cond"]
|
| 178 |
+
elif self.num_conditions == 2:
|
| 179 |
+
tuple_indices = [0, 1]
|
| 180 |
+
input_predictions = (
|
| 181 |
+
["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
|
| 182 |
+
)
|
| 183 |
+
else:
|
| 184 |
+
tuple_indices = [0, 1, 0]
|
| 185 |
+
input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
|
| 186 |
+
data_batches = []
|
| 187 |
+
for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
|
| 188 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 189 |
+
data_batches.append(data_batch)
|
| 190 |
+
return data_batches
|
| 191 |
+
|
| 192 |
+
def prepare_inputs_from_block_state(
|
| 193 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 194 |
+
) -> list["BlockState"]:
|
| 195 |
+
if self.num_conditions == 1:
|
| 196 |
+
tuple_indices = [0]
|
| 197 |
+
input_predictions = ["pred_cond"]
|
| 198 |
+
elif self.num_conditions == 2:
|
| 199 |
+
tuple_indices = [0, 1]
|
| 200 |
+
input_predictions = (
|
| 201 |
+
["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
|
| 202 |
+
)
|
| 203 |
+
else:
|
| 204 |
+
tuple_indices = [0, 1, 0]
|
| 205 |
+
input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
|
| 206 |
+
data_batches = []
|
| 207 |
+
for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
|
| 208 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 209 |
+
data_batches.append(data_batch)
|
| 210 |
+
return data_batches
|
| 211 |
+
|
| 212 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.forward
|
| 213 |
+
def forward(
|
| 214 |
+
self,
|
| 215 |
+
pred_cond: torch.Tensor,
|
| 216 |
+
pred_uncond: torch.Tensor | None = None,
|
| 217 |
+
pred_cond_skip: torch.Tensor | None = None,
|
| 218 |
+
) -> GuiderOutput:
|
| 219 |
+
pred = None
|
| 220 |
+
|
| 221 |
+
if not self._is_cfg_enabled() and not self._is_slg_enabled():
|
| 222 |
+
pred = pred_cond
|
| 223 |
+
elif not self._is_cfg_enabled():
|
| 224 |
+
shift = pred_cond - pred_cond_skip
|
| 225 |
+
pred = pred_cond if self.use_original_formulation else pred_cond_skip
|
| 226 |
+
pred = pred + self.skip_layer_guidance_scale * shift
|
| 227 |
+
elif not self._is_slg_enabled():
|
| 228 |
+
shift = pred_cond - pred_uncond
|
| 229 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 230 |
+
pred = pred + self.guidance_scale * shift
|
| 231 |
+
else:
|
| 232 |
+
shift = pred_cond - pred_uncond
|
| 233 |
+
shift_skip = pred_cond - pred_cond_skip
|
| 234 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 235 |
+
pred = pred + self.guidance_scale * shift + self.skip_layer_guidance_scale * shift_skip
|
| 236 |
+
|
| 237 |
+
if self.guidance_rescale > 0.0:
|
| 238 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 239 |
+
|
| 240 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 241 |
+
|
| 242 |
+
@property
|
| 243 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.is_conditional
|
| 244 |
+
def is_conditional(self) -> bool:
|
| 245 |
+
return self._count_prepared == 1 or self._count_prepared == 3
|
| 246 |
+
|
| 247 |
+
@property
|
| 248 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.num_conditions
|
| 249 |
+
def num_conditions(self) -> int:
|
| 250 |
+
num_conditions = 1
|
| 251 |
+
if self._is_cfg_enabled():
|
| 252 |
+
num_conditions += 1
|
| 253 |
+
if self._is_slg_enabled():
|
| 254 |
+
num_conditions += 1
|
| 255 |
+
return num_conditions
|
| 256 |
+
|
| 257 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance._is_cfg_enabled
|
| 258 |
+
def _is_cfg_enabled(self) -> bool:
|
| 259 |
+
if not self._enabled:
|
| 260 |
+
return False
|
| 261 |
+
|
| 262 |
+
is_within_range = True
|
| 263 |
+
if self._num_inference_steps is not None:
|
| 264 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 265 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 266 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 267 |
+
|
| 268 |
+
is_close = False
|
| 269 |
+
if self.use_original_formulation:
|
| 270 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 271 |
+
else:
|
| 272 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 273 |
+
|
| 274 |
+
return is_within_range and not is_close
|
| 275 |
+
|
| 276 |
+
# Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance._is_slg_enabled
|
| 277 |
+
def _is_slg_enabled(self) -> bool:
|
| 278 |
+
if not self._enabled:
|
| 279 |
+
return False
|
| 280 |
+
|
| 281 |
+
is_within_range = True
|
| 282 |
+
if self._num_inference_steps is not None:
|
| 283 |
+
skip_start_step = int(self.skip_layer_guidance_start * self._num_inference_steps)
|
| 284 |
+
skip_stop_step = int(self.skip_layer_guidance_stop * self._num_inference_steps)
|
| 285 |
+
is_within_range = skip_start_step < self._step < skip_stop_step
|
| 286 |
+
|
| 287 |
+
is_zero = math.isclose(self.skip_layer_guidance_scale, 0.0)
|
| 288 |
+
|
| 289 |
+
return is_within_range and not is_zero
|
diffusers/guiders/skip_layer_guidance.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING, Any
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from ..hooks import HookRegistry, LayerSkipConfig
|
| 24 |
+
from ..hooks.layer_skip import _apply_layer_skip_hook
|
| 25 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
if TYPE_CHECKING:
|
| 29 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class SkipLayerGuidance(BaseGuidance):
|
| 33 |
+
"""
|
| 34 |
+
Skip Layer Guidance (SLG): https://github.com/Stability-AI/sd3.5
|
| 35 |
+
|
| 36 |
+
Spatio-Temporal Guidance (STG): https://huggingface.co/papers/2411.18664
|
| 37 |
+
|
| 38 |
+
SLG was introduced by StabilityAI for improving structure and anotomy coherence in generated images. It works by
|
| 39 |
+
skipping the forward pass of specified transformer blocks during the denoising process on an additional conditional
|
| 40 |
+
batch of data, apart from the conditional and unconditional batches already used in CFG
|
| 41 |
+
([~guiders.classifier_free_guidance.ClassifierFreeGuidance]), and then scaling and shifting the CFG predictions
|
| 42 |
+
based on the difference between conditional without skipping and conditional with skipping predictions.
|
| 43 |
+
|
| 44 |
+
The intution behind SLG can be thought of as moving the CFG predicted distribution estimates further away from
|
| 45 |
+
worse versions of the conditional distribution estimates (because skipping layers is equivalent to using a worse
|
| 46 |
+
version of the model for the conditional prediction).
|
| 47 |
+
|
| 48 |
+
STG is an improvement and follow-up work combining ideas from SLG, PAG and similar techniques for improving
|
| 49 |
+
generation quality in video diffusion models.
|
| 50 |
+
|
| 51 |
+
Additional reading:
|
| 52 |
+
- [Guiding a Diffusion Model with a Bad Version of Itself](https://huggingface.co/papers/2406.02507)
|
| 53 |
+
|
| 54 |
+
The values for `skip_layer_guidance_scale`, `skip_layer_guidance_start`, and `skip_layer_guidance_stop` are
|
| 55 |
+
defaulted to the recommendations by StabilityAI for Stable Diffusion 3.5 Medium.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 59 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 60 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 61 |
+
deterioration of image quality.
|
| 62 |
+
skip_layer_guidance_scale (`float`, defaults to `2.8`):
|
| 63 |
+
The scale parameter for skip layer guidance. Anatomy and structure coherence may improve with higher
|
| 64 |
+
values, but it may also lead to overexposure and saturation.
|
| 65 |
+
skip_layer_guidance_start (`float`, defaults to `0.01`):
|
| 66 |
+
The fraction of the total number of denoising steps after which skip layer guidance starts.
|
| 67 |
+
skip_layer_guidance_stop (`float`, defaults to `0.2`):
|
| 68 |
+
The fraction of the total number of denoising steps after which skip layer guidance stops.
|
| 69 |
+
skip_layer_guidance_layers (`int` or `list[int]`, *optional*):
|
| 70 |
+
The layer indices to apply skip layer guidance to. Can be a single integer or a list of integers. If not
|
| 71 |
+
provided, `skip_layer_config` must be provided. The recommended values are `[7, 8, 9]` for Stable Diffusion
|
| 72 |
+
3.5 Medium.
|
| 73 |
+
skip_layer_config (`LayerSkipConfig` or `list[LayerSkipConfig]`, *optional*):
|
| 74 |
+
The configuration for the skip layer guidance. Can be a single `LayerSkipConfig` or a list of
|
| 75 |
+
`LayerSkipConfig`. If not provided, `skip_layer_guidance_layers` must be provided.
|
| 76 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 77 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 78 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 79 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 80 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 81 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 82 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 83 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 84 |
+
start (`float`, defaults to `0.01`):
|
| 85 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 86 |
+
stop (`float`, defaults to `0.2`):
|
| 87 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
_input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
|
| 91 |
+
|
| 92 |
+
@register_to_config
|
| 93 |
+
def __init__(
|
| 94 |
+
self,
|
| 95 |
+
guidance_scale: float = 7.5,
|
| 96 |
+
skip_layer_guidance_scale: float = 2.8,
|
| 97 |
+
skip_layer_guidance_start: float = 0.01,
|
| 98 |
+
skip_layer_guidance_stop: float = 0.2,
|
| 99 |
+
skip_layer_guidance_layers: int | list[int] | None = None,
|
| 100 |
+
skip_layer_config: LayerSkipConfig | list[LayerSkipConfig] | dict[str, Any] = None,
|
| 101 |
+
guidance_rescale: float = 0.0,
|
| 102 |
+
use_original_formulation: bool = False,
|
| 103 |
+
start: float = 0.0,
|
| 104 |
+
stop: float = 1.0,
|
| 105 |
+
enabled: bool = True,
|
| 106 |
+
):
|
| 107 |
+
super().__init__(start, stop, enabled)
|
| 108 |
+
|
| 109 |
+
self.guidance_scale = guidance_scale
|
| 110 |
+
self.skip_layer_guidance_scale = skip_layer_guidance_scale
|
| 111 |
+
self.skip_layer_guidance_start = skip_layer_guidance_start
|
| 112 |
+
self.skip_layer_guidance_stop = skip_layer_guidance_stop
|
| 113 |
+
self.guidance_rescale = guidance_rescale
|
| 114 |
+
self.use_original_formulation = use_original_formulation
|
| 115 |
+
|
| 116 |
+
if not (0.0 <= skip_layer_guidance_start < 1.0):
|
| 117 |
+
raise ValueError(
|
| 118 |
+
f"Expected `skip_layer_guidance_start` to be between 0.0 and 1.0, but got {skip_layer_guidance_start}."
|
| 119 |
+
)
|
| 120 |
+
if not (skip_layer_guidance_start <= skip_layer_guidance_stop <= 1.0):
|
| 121 |
+
raise ValueError(
|
| 122 |
+
f"Expected `skip_layer_guidance_stop` to be between 0.0 and 1.0, but got {skip_layer_guidance_stop}."
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
if skip_layer_guidance_layers is None and skip_layer_config is None:
|
| 126 |
+
raise ValueError(
|
| 127 |
+
"Either `skip_layer_guidance_layers` or `skip_layer_config` must be provided to enable Skip Layer Guidance."
|
| 128 |
+
)
|
| 129 |
+
if skip_layer_guidance_layers is not None and skip_layer_config is not None:
|
| 130 |
+
raise ValueError("Only one of `skip_layer_guidance_layers` or `skip_layer_config` can be provided.")
|
| 131 |
+
|
| 132 |
+
if skip_layer_guidance_layers is not None:
|
| 133 |
+
if isinstance(skip_layer_guidance_layers, int):
|
| 134 |
+
skip_layer_guidance_layers = [skip_layer_guidance_layers]
|
| 135 |
+
if not isinstance(skip_layer_guidance_layers, list):
|
| 136 |
+
raise ValueError(
|
| 137 |
+
f"Expected `skip_layer_guidance_layers` to be an int or a list of ints, but got {type(skip_layer_guidance_layers)}."
|
| 138 |
+
)
|
| 139 |
+
skip_layer_config = [LayerSkipConfig(layer, fqn="auto") for layer in skip_layer_guidance_layers]
|
| 140 |
+
|
| 141 |
+
if isinstance(skip_layer_config, dict):
|
| 142 |
+
skip_layer_config = LayerSkipConfig.from_dict(skip_layer_config)
|
| 143 |
+
|
| 144 |
+
if isinstance(skip_layer_config, LayerSkipConfig):
|
| 145 |
+
skip_layer_config = [skip_layer_config]
|
| 146 |
+
|
| 147 |
+
if not isinstance(skip_layer_config, list):
|
| 148 |
+
raise ValueError(
|
| 149 |
+
f"Expected `skip_layer_config` to be a LayerSkipConfig or a list of LayerSkipConfig, but got {type(skip_layer_config)}."
|
| 150 |
+
)
|
| 151 |
+
elif isinstance(next(iter(skip_layer_config), None), dict):
|
| 152 |
+
skip_layer_config = [LayerSkipConfig.from_dict(config) for config in skip_layer_config]
|
| 153 |
+
|
| 154 |
+
self.skip_layer_config = skip_layer_config
|
| 155 |
+
self._skip_layer_hook_names = [f"SkipLayerGuidance_{i}" for i in range(len(self.skip_layer_config))]
|
| 156 |
+
|
| 157 |
+
def prepare_models(self, denoiser: torch.nn.Module) -> None:
|
| 158 |
+
self._count_prepared += 1
|
| 159 |
+
if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
|
| 160 |
+
for name, config in zip(self._skip_layer_hook_names, self.skip_layer_config):
|
| 161 |
+
_apply_layer_skip_hook(denoiser, config, name=name)
|
| 162 |
+
|
| 163 |
+
def cleanup_models(self, denoiser: torch.nn.Module) -> None:
|
| 164 |
+
if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
|
| 165 |
+
registry = HookRegistry.check_if_exists_or_initialize(denoiser)
|
| 166 |
+
# Remove the hooks after inference
|
| 167 |
+
for hook_name in self._skip_layer_hook_names:
|
| 168 |
+
registry.remove_hook(hook_name, recurse=True)
|
| 169 |
+
|
| 170 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 171 |
+
if self.num_conditions == 1:
|
| 172 |
+
tuple_indices = [0]
|
| 173 |
+
input_predictions = ["pred_cond"]
|
| 174 |
+
elif self.num_conditions == 2:
|
| 175 |
+
tuple_indices = [0, 1]
|
| 176 |
+
input_predictions = (
|
| 177 |
+
["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
|
| 178 |
+
)
|
| 179 |
+
else:
|
| 180 |
+
tuple_indices = [0, 1, 0]
|
| 181 |
+
input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
|
| 182 |
+
data_batches = []
|
| 183 |
+
for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
|
| 184 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 185 |
+
data_batches.append(data_batch)
|
| 186 |
+
return data_batches
|
| 187 |
+
|
| 188 |
+
def prepare_inputs_from_block_state(
|
| 189 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 190 |
+
) -> list["BlockState"]:
|
| 191 |
+
if self.num_conditions == 1:
|
| 192 |
+
tuple_indices = [0]
|
| 193 |
+
input_predictions = ["pred_cond"]
|
| 194 |
+
elif self.num_conditions == 2:
|
| 195 |
+
tuple_indices = [0, 1]
|
| 196 |
+
input_predictions = (
|
| 197 |
+
["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
|
| 198 |
+
)
|
| 199 |
+
else:
|
| 200 |
+
tuple_indices = [0, 1, 0]
|
| 201 |
+
input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
|
| 202 |
+
data_batches = []
|
| 203 |
+
for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
|
| 204 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 205 |
+
data_batches.append(data_batch)
|
| 206 |
+
return data_batches
|
| 207 |
+
|
| 208 |
+
def forward(
|
| 209 |
+
self,
|
| 210 |
+
pred_cond: torch.Tensor,
|
| 211 |
+
pred_uncond: torch.Tensor | None = None,
|
| 212 |
+
pred_cond_skip: torch.Tensor | None = None,
|
| 213 |
+
) -> GuiderOutput:
|
| 214 |
+
pred = None
|
| 215 |
+
|
| 216 |
+
if not self._is_cfg_enabled() and not self._is_slg_enabled():
|
| 217 |
+
pred = pred_cond
|
| 218 |
+
elif not self._is_cfg_enabled():
|
| 219 |
+
shift = pred_cond - pred_cond_skip
|
| 220 |
+
pred = pred_cond if self.use_original_formulation else pred_cond_skip
|
| 221 |
+
pred = pred + self.skip_layer_guidance_scale * shift
|
| 222 |
+
elif not self._is_slg_enabled():
|
| 223 |
+
shift = pred_cond - pred_uncond
|
| 224 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 225 |
+
pred = pred + self.guidance_scale * shift
|
| 226 |
+
else:
|
| 227 |
+
shift = pred_cond - pred_uncond
|
| 228 |
+
shift_skip = pred_cond - pred_cond_skip
|
| 229 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 230 |
+
pred = pred + self.guidance_scale * shift + self.skip_layer_guidance_scale * shift_skip
|
| 231 |
+
|
| 232 |
+
if self.guidance_rescale > 0.0:
|
| 233 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 234 |
+
|
| 235 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 236 |
+
|
| 237 |
+
@property
|
| 238 |
+
def is_conditional(self) -> bool:
|
| 239 |
+
return self._count_prepared == 1 or self._count_prepared == 3
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def num_conditions(self) -> int:
|
| 243 |
+
num_conditions = 1
|
| 244 |
+
if self._is_cfg_enabled():
|
| 245 |
+
num_conditions += 1
|
| 246 |
+
if self._is_slg_enabled():
|
| 247 |
+
num_conditions += 1
|
| 248 |
+
return num_conditions
|
| 249 |
+
|
| 250 |
+
def _is_cfg_enabled(self) -> bool:
|
| 251 |
+
if not self._enabled:
|
| 252 |
+
return False
|
| 253 |
+
|
| 254 |
+
is_within_range = True
|
| 255 |
+
if self._num_inference_steps is not None:
|
| 256 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 257 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 258 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 259 |
+
|
| 260 |
+
is_close = False
|
| 261 |
+
if self.use_original_formulation:
|
| 262 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 263 |
+
else:
|
| 264 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 265 |
+
|
| 266 |
+
return is_within_range and not is_close
|
| 267 |
+
|
| 268 |
+
def _is_slg_enabled(self) -> bool:
|
| 269 |
+
if not self._enabled:
|
| 270 |
+
return False
|
| 271 |
+
|
| 272 |
+
is_within_range = True
|
| 273 |
+
if self._num_inference_steps is not None:
|
| 274 |
+
skip_start_step = int(self.skip_layer_guidance_start * self._num_inference_steps)
|
| 275 |
+
skip_stop_step = int(self.skip_layer_guidance_stop * self._num_inference_steps)
|
| 276 |
+
is_within_range = skip_start_step < self._step < skip_stop_step
|
| 277 |
+
|
| 278 |
+
is_zero = math.isclose(self.skip_layer_guidance_scale, 0.0)
|
| 279 |
+
|
| 280 |
+
return is_within_range and not is_zero
|
diffusers/guiders/smoothed_energy_guidance.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from ..hooks import HookRegistry
|
| 24 |
+
from ..hooks.smoothed_energy_guidance_utils import SmoothedEnergyGuidanceConfig, _apply_smoothed_energy_guidance_hook
|
| 25 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
if TYPE_CHECKING:
|
| 29 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class SmoothedEnergyGuidance(BaseGuidance):
|
| 33 |
+
"""
|
| 34 |
+
Smoothed Energy Guidance (SEG): https://huggingface.co/papers/2408.00760
|
| 35 |
+
|
| 36 |
+
SEG is only supported as an experimental prototype feature for now, so the implementation may be modified in the
|
| 37 |
+
future without warning or guarantee of reproducibility. This implementation assumes:
|
| 38 |
+
- Generated images are square (height == width)
|
| 39 |
+
- The model does not combine different modalities together (e.g., text and image latent streams are not combined
|
| 40 |
+
together such as Flux)
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 44 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 45 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 46 |
+
deterioration of image quality.
|
| 47 |
+
seg_guidance_scale (`float`, defaults to `3.0`):
|
| 48 |
+
The scale parameter for smoothed energy guidance. Anatomy and structure coherence may improve with higher
|
| 49 |
+
values, but it may also lead to overexposure and saturation.
|
| 50 |
+
seg_blur_sigma (`float`, defaults to `9999999.0`):
|
| 51 |
+
The amount by which we blur the attention weights. Setting this value greater than 9999.0 results in
|
| 52 |
+
infinite blur, which means uniform queries. Controlling it exponentially is empirically effective.
|
| 53 |
+
seg_blur_threshold_inf (`float`, defaults to `9999.0`):
|
| 54 |
+
The threshold above which the blur is considered infinite.
|
| 55 |
+
seg_guidance_start (`float`, defaults to `0.0`):
|
| 56 |
+
The fraction of the total number of denoising steps after which smoothed energy guidance starts.
|
| 57 |
+
seg_guidance_stop (`float`, defaults to `1.0`):
|
| 58 |
+
The fraction of the total number of denoising steps after which smoothed energy guidance stops.
|
| 59 |
+
seg_guidance_layers (`int` or `list[int]`, *optional*):
|
| 60 |
+
The layer indices to apply smoothed energy guidance to. Can be a single integer or a list of integers. If
|
| 61 |
+
not provided, `seg_guidance_config` must be provided. The recommended values are `[7, 8, 9]` for Stable
|
| 62 |
+
Diffusion 3.5 Medium.
|
| 63 |
+
seg_guidance_config (`SmoothedEnergyGuidanceConfig` or `list[SmoothedEnergyGuidanceConfig]`, *optional*):
|
| 64 |
+
The configuration for the smoothed energy layer guidance. Can be a single `SmoothedEnergyGuidanceConfig` or
|
| 65 |
+
a list of `SmoothedEnergyGuidanceConfig`. If not provided, `seg_guidance_layers` must be provided.
|
| 66 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 67 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 68 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 69 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 70 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 71 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 72 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 73 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 74 |
+
start (`float`, defaults to `0.01`):
|
| 75 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 76 |
+
stop (`float`, defaults to `0.2`):
|
| 77 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
_input_predictions = ["pred_cond", "pred_uncond", "pred_cond_seg"]
|
| 81 |
+
|
| 82 |
+
@register_to_config
|
| 83 |
+
def __init__(
|
| 84 |
+
self,
|
| 85 |
+
guidance_scale: float = 7.5,
|
| 86 |
+
seg_guidance_scale: float = 2.8,
|
| 87 |
+
seg_blur_sigma: float = 9999999.0,
|
| 88 |
+
seg_blur_threshold_inf: float = 9999.0,
|
| 89 |
+
seg_guidance_start: float = 0.0,
|
| 90 |
+
seg_guidance_stop: float = 1.0,
|
| 91 |
+
seg_guidance_layers: int | list[int] | None = None,
|
| 92 |
+
seg_guidance_config: SmoothedEnergyGuidanceConfig | list[SmoothedEnergyGuidanceConfig] = None,
|
| 93 |
+
guidance_rescale: float = 0.0,
|
| 94 |
+
use_original_formulation: bool = False,
|
| 95 |
+
start: float = 0.0,
|
| 96 |
+
stop: float = 1.0,
|
| 97 |
+
enabled: bool = True,
|
| 98 |
+
):
|
| 99 |
+
super().__init__(start, stop, enabled)
|
| 100 |
+
|
| 101 |
+
self.guidance_scale = guidance_scale
|
| 102 |
+
self.seg_guidance_scale = seg_guidance_scale
|
| 103 |
+
self.seg_blur_sigma = seg_blur_sigma
|
| 104 |
+
self.seg_blur_threshold_inf = seg_blur_threshold_inf
|
| 105 |
+
self.seg_guidance_start = seg_guidance_start
|
| 106 |
+
self.seg_guidance_stop = seg_guidance_stop
|
| 107 |
+
self.guidance_rescale = guidance_rescale
|
| 108 |
+
self.use_original_formulation = use_original_formulation
|
| 109 |
+
|
| 110 |
+
if not (0.0 <= seg_guidance_start < 1.0):
|
| 111 |
+
raise ValueError(f"Expected `seg_guidance_start` to be between 0.0 and 1.0, but got {seg_guidance_start}.")
|
| 112 |
+
if not (seg_guidance_start <= seg_guidance_stop <= 1.0):
|
| 113 |
+
raise ValueError(f"Expected `seg_guidance_stop` to be between 0.0 and 1.0, but got {seg_guidance_stop}.")
|
| 114 |
+
|
| 115 |
+
if seg_guidance_layers is None and seg_guidance_config is None:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
"Either `seg_guidance_layers` or `seg_guidance_config` must be provided to enable Smoothed Energy Guidance."
|
| 118 |
+
)
|
| 119 |
+
if seg_guidance_layers is not None and seg_guidance_config is not None:
|
| 120 |
+
raise ValueError("Only one of `seg_guidance_layers` or `seg_guidance_config` can be provided.")
|
| 121 |
+
|
| 122 |
+
if seg_guidance_layers is not None:
|
| 123 |
+
if isinstance(seg_guidance_layers, int):
|
| 124 |
+
seg_guidance_layers = [seg_guidance_layers]
|
| 125 |
+
if not isinstance(seg_guidance_layers, list):
|
| 126 |
+
raise ValueError(
|
| 127 |
+
f"Expected `seg_guidance_layers` to be an int or a list of ints, but got {type(seg_guidance_layers)}."
|
| 128 |
+
)
|
| 129 |
+
seg_guidance_config = [SmoothedEnergyGuidanceConfig(layer, fqn="auto") for layer in seg_guidance_layers]
|
| 130 |
+
|
| 131 |
+
if isinstance(seg_guidance_config, dict):
|
| 132 |
+
seg_guidance_config = SmoothedEnergyGuidanceConfig.from_dict(seg_guidance_config)
|
| 133 |
+
|
| 134 |
+
if isinstance(seg_guidance_config, SmoothedEnergyGuidanceConfig):
|
| 135 |
+
seg_guidance_config = [seg_guidance_config]
|
| 136 |
+
|
| 137 |
+
if not isinstance(seg_guidance_config, list):
|
| 138 |
+
raise ValueError(
|
| 139 |
+
f"Expected `seg_guidance_config` to be a SmoothedEnergyGuidanceConfig or a list of SmoothedEnergyGuidanceConfig, but got {type(seg_guidance_config)}."
|
| 140 |
+
)
|
| 141 |
+
elif isinstance(next(iter(seg_guidance_config), None), dict):
|
| 142 |
+
seg_guidance_config = [SmoothedEnergyGuidanceConfig.from_dict(config) for config in seg_guidance_config]
|
| 143 |
+
|
| 144 |
+
self.seg_guidance_config = seg_guidance_config
|
| 145 |
+
self._seg_layer_hook_names = [f"SmoothedEnergyGuidance_{i}" for i in range(len(self.seg_guidance_config))]
|
| 146 |
+
|
| 147 |
+
def prepare_models(self, denoiser: torch.nn.Module) -> None:
|
| 148 |
+
if self._is_seg_enabled() and self.is_conditional and self._count_prepared > 1:
|
| 149 |
+
for name, config in zip(self._seg_layer_hook_names, self.seg_guidance_config):
|
| 150 |
+
_apply_smoothed_energy_guidance_hook(denoiser, config, self.seg_blur_sigma, name=name)
|
| 151 |
+
|
| 152 |
+
def cleanup_models(self, denoiser: torch.nn.Module):
|
| 153 |
+
if self._is_seg_enabled() and self.is_conditional and self._count_prepared > 1:
|
| 154 |
+
registry = HookRegistry.check_if_exists_or_initialize(denoiser)
|
| 155 |
+
# Remove the hooks after inference
|
| 156 |
+
for hook_name in self._seg_layer_hook_names:
|
| 157 |
+
registry.remove_hook(hook_name, recurse=True)
|
| 158 |
+
|
| 159 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 160 |
+
if self.num_conditions == 1:
|
| 161 |
+
tuple_indices = [0]
|
| 162 |
+
input_predictions = ["pred_cond"]
|
| 163 |
+
elif self.num_conditions == 2:
|
| 164 |
+
tuple_indices = [0, 1]
|
| 165 |
+
input_predictions = (
|
| 166 |
+
["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_seg"]
|
| 167 |
+
)
|
| 168 |
+
else:
|
| 169 |
+
tuple_indices = [0, 1, 0]
|
| 170 |
+
input_predictions = ["pred_cond", "pred_uncond", "pred_cond_seg"]
|
| 171 |
+
data_batches = []
|
| 172 |
+
for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
|
| 173 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 174 |
+
data_batches.append(data_batch)
|
| 175 |
+
return data_batches
|
| 176 |
+
|
| 177 |
+
def prepare_inputs_from_block_state(
|
| 178 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 179 |
+
) -> list["BlockState"]:
|
| 180 |
+
if self.num_conditions == 1:
|
| 181 |
+
tuple_indices = [0]
|
| 182 |
+
input_predictions = ["pred_cond"]
|
| 183 |
+
elif self.num_conditions == 2:
|
| 184 |
+
tuple_indices = [0, 1]
|
| 185 |
+
input_predictions = (
|
| 186 |
+
["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_seg"]
|
| 187 |
+
)
|
| 188 |
+
else:
|
| 189 |
+
tuple_indices = [0, 1, 0]
|
| 190 |
+
input_predictions = ["pred_cond", "pred_uncond", "pred_cond_seg"]
|
| 191 |
+
data_batches = []
|
| 192 |
+
for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
|
| 193 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 194 |
+
data_batches.append(data_batch)
|
| 195 |
+
return data_batches
|
| 196 |
+
|
| 197 |
+
def forward(
|
| 198 |
+
self,
|
| 199 |
+
pred_cond: torch.Tensor,
|
| 200 |
+
pred_uncond: torch.Tensor | None = None,
|
| 201 |
+
pred_cond_seg: torch.Tensor | None = None,
|
| 202 |
+
) -> GuiderOutput:
|
| 203 |
+
pred = None
|
| 204 |
+
|
| 205 |
+
if not self._is_cfg_enabled() and not self._is_seg_enabled():
|
| 206 |
+
pred = pred_cond
|
| 207 |
+
elif not self._is_cfg_enabled():
|
| 208 |
+
shift = pred_cond - pred_cond_seg
|
| 209 |
+
pred = pred_cond if self.use_original_formulation else pred_cond_seg
|
| 210 |
+
pred = pred + self.seg_guidance_scale * shift
|
| 211 |
+
elif not self._is_seg_enabled():
|
| 212 |
+
shift = pred_cond - pred_uncond
|
| 213 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 214 |
+
pred = pred + self.guidance_scale * shift
|
| 215 |
+
else:
|
| 216 |
+
shift = pred_cond - pred_uncond
|
| 217 |
+
shift_seg = pred_cond - pred_cond_seg
|
| 218 |
+
pred = pred_cond if self.use_original_formulation else pred_uncond
|
| 219 |
+
pred = pred + self.guidance_scale * shift + self.seg_guidance_scale * shift_seg
|
| 220 |
+
|
| 221 |
+
if self.guidance_rescale > 0.0:
|
| 222 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 223 |
+
|
| 224 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 225 |
+
|
| 226 |
+
@property
|
| 227 |
+
def is_conditional(self) -> bool:
|
| 228 |
+
return self._count_prepared == 1 or self._count_prepared == 3
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def num_conditions(self) -> int:
|
| 232 |
+
num_conditions = 1
|
| 233 |
+
if self._is_cfg_enabled():
|
| 234 |
+
num_conditions += 1
|
| 235 |
+
if self._is_seg_enabled():
|
| 236 |
+
num_conditions += 1
|
| 237 |
+
return num_conditions
|
| 238 |
+
|
| 239 |
+
def _is_cfg_enabled(self) -> bool:
|
| 240 |
+
if not self._enabled:
|
| 241 |
+
return False
|
| 242 |
+
|
| 243 |
+
is_within_range = True
|
| 244 |
+
if self._num_inference_steps is not None:
|
| 245 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 246 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 247 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 248 |
+
|
| 249 |
+
is_close = False
|
| 250 |
+
if self.use_original_formulation:
|
| 251 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 252 |
+
else:
|
| 253 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 254 |
+
|
| 255 |
+
return is_within_range and not is_close
|
| 256 |
+
|
| 257 |
+
def _is_seg_enabled(self) -> bool:
|
| 258 |
+
if not self._enabled:
|
| 259 |
+
return False
|
| 260 |
+
|
| 261 |
+
is_within_range = True
|
| 262 |
+
if self._num_inference_steps is not None:
|
| 263 |
+
skip_start_step = int(self.seg_guidance_start * self._num_inference_steps)
|
| 264 |
+
skip_stop_step = int(self.seg_guidance_stop * self._num_inference_steps)
|
| 265 |
+
is_within_range = skip_start_step < self._step < skip_stop_step
|
| 266 |
+
|
| 267 |
+
is_zero = math.isclose(self.seg_guidance_scale, 0.0)
|
| 268 |
+
|
| 269 |
+
return is_within_range and not is_zero
|
diffusers/guiders/tangential_classifier_free_guidance.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from typing import TYPE_CHECKING
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from ..configuration_utils import register_to_config
|
| 23 |
+
from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
if TYPE_CHECKING:
|
| 27 |
+
from ..modular_pipelines.modular_pipeline import BlockState
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class TangentialClassifierFreeGuidance(BaseGuidance):
|
| 31 |
+
"""
|
| 32 |
+
Tangential Classifier Free Guidance (TCFG): https://huggingface.co/papers/2503.18137
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
guidance_scale (`float`, defaults to `7.5`):
|
| 36 |
+
The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
|
| 37 |
+
prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
|
| 38 |
+
deterioration of image quality.
|
| 39 |
+
guidance_rescale (`float`, defaults to `0.0`):
|
| 40 |
+
The rescale factor applied to the noise predictions. This is used to improve image quality and fix
|
| 41 |
+
overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
|
| 42 |
+
Flawed](https://huggingface.co/papers/2305.08891).
|
| 43 |
+
use_original_formulation (`bool`, defaults to `False`):
|
| 44 |
+
Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
|
| 45 |
+
we use the diffusers-native implementation that has been in the codebase for a long time. See
|
| 46 |
+
[~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
|
| 47 |
+
start (`float`, defaults to `0.0`):
|
| 48 |
+
The fraction of the total number of denoising steps after which guidance starts.
|
| 49 |
+
stop (`float`, defaults to `1.0`):
|
| 50 |
+
The fraction of the total number of denoising steps after which guidance stops.
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
_input_predictions = ["pred_cond", "pred_uncond"]
|
| 54 |
+
|
| 55 |
+
@register_to_config
|
| 56 |
+
def __init__(
|
| 57 |
+
self,
|
| 58 |
+
guidance_scale: float = 7.5,
|
| 59 |
+
guidance_rescale: float = 0.0,
|
| 60 |
+
use_original_formulation: bool = False,
|
| 61 |
+
start: float = 0.0,
|
| 62 |
+
stop: float = 1.0,
|
| 63 |
+
enabled: bool = True,
|
| 64 |
+
):
|
| 65 |
+
super().__init__(start, stop, enabled)
|
| 66 |
+
|
| 67 |
+
self.guidance_scale = guidance_scale
|
| 68 |
+
self.guidance_rescale = guidance_rescale
|
| 69 |
+
self.use_original_formulation = use_original_formulation
|
| 70 |
+
|
| 71 |
+
def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
|
| 72 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 73 |
+
data_batches = []
|
| 74 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 75 |
+
data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
|
| 76 |
+
data_batches.append(data_batch)
|
| 77 |
+
return data_batches
|
| 78 |
+
|
| 79 |
+
def prepare_inputs_from_block_state(
|
| 80 |
+
self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
|
| 81 |
+
) -> list["BlockState"]:
|
| 82 |
+
tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
|
| 83 |
+
data_batches = []
|
| 84 |
+
for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
|
| 85 |
+
data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
|
| 86 |
+
data_batches.append(data_batch)
|
| 87 |
+
return data_batches
|
| 88 |
+
|
| 89 |
+
def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
|
| 90 |
+
pred = None
|
| 91 |
+
|
| 92 |
+
if not self._is_tcfg_enabled():
|
| 93 |
+
pred = pred_cond
|
| 94 |
+
else:
|
| 95 |
+
pred = normalized_guidance(pred_cond, pred_uncond, self.guidance_scale, self.use_original_formulation)
|
| 96 |
+
|
| 97 |
+
if self.guidance_rescale > 0.0:
|
| 98 |
+
pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
|
| 99 |
+
|
| 100 |
+
return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
|
| 101 |
+
|
| 102 |
+
@property
|
| 103 |
+
def is_conditional(self) -> bool:
|
| 104 |
+
return self._num_outputs_prepared == 1
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def num_conditions(self) -> int:
|
| 108 |
+
num_conditions = 1
|
| 109 |
+
if self._is_tcfg_enabled():
|
| 110 |
+
num_conditions += 1
|
| 111 |
+
return num_conditions
|
| 112 |
+
|
| 113 |
+
def _is_tcfg_enabled(self) -> bool:
|
| 114 |
+
if not self._enabled:
|
| 115 |
+
return False
|
| 116 |
+
|
| 117 |
+
is_within_range = True
|
| 118 |
+
if self._num_inference_steps is not None:
|
| 119 |
+
skip_start_step = int(self._start * self._num_inference_steps)
|
| 120 |
+
skip_stop_step = int(self._stop * self._num_inference_steps)
|
| 121 |
+
is_within_range = skip_start_step <= self._step < skip_stop_step
|
| 122 |
+
|
| 123 |
+
is_close = False
|
| 124 |
+
if self.use_original_formulation:
|
| 125 |
+
is_close = math.isclose(self.guidance_scale, 0.0)
|
| 126 |
+
else:
|
| 127 |
+
is_close = math.isclose(self.guidance_scale, 1.0)
|
| 128 |
+
|
| 129 |
+
return is_within_range and not is_close
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def normalized_guidance(
|
| 133 |
+
pred_cond: torch.Tensor, pred_uncond: torch.Tensor, guidance_scale: float, use_original_formulation: bool = False
|
| 134 |
+
) -> torch.Tensor:
|
| 135 |
+
cond_dtype = pred_cond.dtype
|
| 136 |
+
preds = torch.stack([pred_cond, pred_uncond], dim=1).float()
|
| 137 |
+
preds = preds.flatten(2)
|
| 138 |
+
U, S, Vh = torch.linalg.svd(preds, full_matrices=False)
|
| 139 |
+
Vh_modified = Vh.clone()
|
| 140 |
+
Vh_modified[:, 1] = 0
|
| 141 |
+
|
| 142 |
+
uncond_flat = pred_uncond.reshape(pred_uncond.size(0), 1, -1).float()
|
| 143 |
+
x_Vh = torch.matmul(uncond_flat, Vh.transpose(-2, -1))
|
| 144 |
+
x_Vh_V = torch.matmul(x_Vh, Vh_modified)
|
| 145 |
+
pred_uncond = x_Vh_V.reshape(pred_uncond.shape).to(cond_dtype)
|
| 146 |
+
|
| 147 |
+
pred = pred_cond if use_original_formulation else pred_uncond
|
| 148 |
+
shift = pred_cond - pred_uncond
|
| 149 |
+
pred = pred + guidance_scale * shift
|
| 150 |
+
|
| 151 |
+
return pred
|
diffusers/hooks/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from ..utils import is_torch_available
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
if is_torch_available():
|
| 19 |
+
from .context_parallel import apply_context_parallel
|
| 20 |
+
from .faster_cache import FasterCacheConfig, apply_faster_cache
|
| 21 |
+
from .first_block_cache import FirstBlockCacheConfig, apply_first_block_cache
|
| 22 |
+
from .group_offloading import apply_group_offloading
|
| 23 |
+
from .hooks import HookRegistry, ModelHook
|
| 24 |
+
from .layer_skip import LayerSkipConfig, apply_layer_skip
|
| 25 |
+
from .layerwise_casting import apply_layerwise_casting, apply_layerwise_casting_hook
|
| 26 |
+
from .mag_cache import MagCacheConfig, apply_mag_cache
|
| 27 |
+
from .pyramid_attention_broadcast import PyramidAttentionBroadcastConfig, apply_pyramid_attention_broadcast
|
| 28 |
+
from .smoothed_energy_guidance_utils import SmoothedEnergyGuidanceConfig
|
| 29 |
+
from .taylorseer_cache import TaylorSeerCacheConfig, apply_taylorseer_cache
|
| 30 |
+
from .tensor_parallel import apply_tensor_parallel
|
| 31 |
+
from .text_kv_cache import TextKVCacheConfig, apply_text_kv_cache
|
diffusers/hooks/_common.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ..models.attention import AttentionModuleMixin, FeedForward, LuminaFeedForward
|
| 18 |
+
from ..models.attention_processor import Attention, MochiAttention
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
_ATTENTION_CLASSES = (Attention, MochiAttention, AttentionModuleMixin)
|
| 22 |
+
_FEEDFORWARD_CLASSES = (FeedForward, LuminaFeedForward)
|
| 23 |
+
|
| 24 |
+
_SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS = (
|
| 25 |
+
"blocks",
|
| 26 |
+
"transformer_blocks",
|
| 27 |
+
"single_transformer_blocks",
|
| 28 |
+
"layers",
|
| 29 |
+
"visual_transformer_blocks",
|
| 30 |
+
)
|
| 31 |
+
_TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS = ("temporal_transformer_blocks",)
|
| 32 |
+
_CROSS_TRANSFORMER_BLOCK_IDENTIFIERS = ("blocks", "transformer_blocks", "layers")
|
| 33 |
+
|
| 34 |
+
_ALL_TRANSFORMER_BLOCK_IDENTIFIERS = tuple(
|
| 35 |
+
{
|
| 36 |
+
*_SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS,
|
| 37 |
+
*_TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS,
|
| 38 |
+
*_CROSS_TRANSFORMER_BLOCK_IDENTIFIERS,
|
| 39 |
+
}
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Layers supported for group offloading and layerwise casting
|
| 43 |
+
_GO_LC_SUPPORTED_PYTORCH_LAYERS = (
|
| 44 |
+
torch.nn.Conv1d,
|
| 45 |
+
torch.nn.Conv2d,
|
| 46 |
+
torch.nn.Conv3d,
|
| 47 |
+
torch.nn.ConvTranspose1d,
|
| 48 |
+
torch.nn.ConvTranspose2d,
|
| 49 |
+
torch.nn.ConvTranspose3d,
|
| 50 |
+
torch.nn.Linear,
|
| 51 |
+
torch.nn.Embedding,
|
| 52 |
+
# TODO(aryan): look into torch.nn.LayerNorm, torch.nn.GroupNorm later, seems to be causing some issues with CogVideoX
|
| 53 |
+
# because of double invocation of the same norm layer in CogVideoXLayerNorm
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _get_submodule_from_fqn(module: torch.nn.Module, fqn: str) -> torch.nn.Module | None:
|
| 58 |
+
for submodule_name, submodule in module.named_modules():
|
| 59 |
+
if submodule_name == fqn:
|
| 60 |
+
return submodule
|
| 61 |
+
return None
|
diffusers/hooks/_helpers.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import inspect
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from typing import Any, Callable, Type
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class AttentionProcessorMetadata:
|
| 22 |
+
skip_processor_output_fn: Callable[[Any], Any]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class TransformerBlockMetadata:
|
| 27 |
+
return_hidden_states_index: int = None
|
| 28 |
+
return_encoder_hidden_states_index: int = None
|
| 29 |
+
hidden_states_argument_name: str = "hidden_states"
|
| 30 |
+
|
| 31 |
+
_cls: Type = None
|
| 32 |
+
_cached_parameter_indices: dict[str, int] = None
|
| 33 |
+
|
| 34 |
+
def _get_parameter_from_args_kwargs(self, identifier: str, args=(), kwargs=None):
|
| 35 |
+
kwargs = kwargs or {}
|
| 36 |
+
if identifier in kwargs:
|
| 37 |
+
return kwargs[identifier]
|
| 38 |
+
if self._cached_parameter_indices is not None:
|
| 39 |
+
return args[self._cached_parameter_indices[identifier]]
|
| 40 |
+
if self._cls is None:
|
| 41 |
+
raise ValueError("Model class is not set for metadata.")
|
| 42 |
+
parameters = list(inspect.signature(self._cls.forward).parameters.keys())
|
| 43 |
+
parameters = parameters[1:] # skip `self`
|
| 44 |
+
self._cached_parameter_indices = {param: i for i, param in enumerate(parameters)}
|
| 45 |
+
if identifier not in self._cached_parameter_indices:
|
| 46 |
+
raise ValueError(f"Parameter '{identifier}' not found in function signature but was requested.")
|
| 47 |
+
index = self._cached_parameter_indices[identifier]
|
| 48 |
+
if index >= len(args):
|
| 49 |
+
raise ValueError(f"Expected {index} arguments but got {len(args)}.")
|
| 50 |
+
return args[index]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class AttentionProcessorRegistry:
|
| 54 |
+
_registry = {}
|
| 55 |
+
# TODO(aryan): this is only required for the time being because we need to do the registrations
|
| 56 |
+
# for classes. If we do it eagerly, i.e. call the functions in global scope, we will get circular
|
| 57 |
+
# import errors because of the models imported in this file.
|
| 58 |
+
_is_registered = False
|
| 59 |
+
|
| 60 |
+
@classmethod
|
| 61 |
+
def register(cls, model_class: Type, metadata: AttentionProcessorMetadata):
|
| 62 |
+
cls._register()
|
| 63 |
+
cls._registry[model_class] = metadata
|
| 64 |
+
|
| 65 |
+
@classmethod
|
| 66 |
+
def get(cls, model_class: Type) -> AttentionProcessorMetadata:
|
| 67 |
+
cls._register()
|
| 68 |
+
if model_class not in cls._registry:
|
| 69 |
+
raise ValueError(f"Model class {model_class} not registered.")
|
| 70 |
+
return cls._registry[model_class]
|
| 71 |
+
|
| 72 |
+
@classmethod
|
| 73 |
+
def _register(cls):
|
| 74 |
+
if cls._is_registered:
|
| 75 |
+
return
|
| 76 |
+
cls._is_registered = True
|
| 77 |
+
_register_attention_processors_metadata()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class TransformerBlockRegistry:
|
| 81 |
+
_registry = {}
|
| 82 |
+
# TODO(aryan): this is only required for the time being because we need to do the registrations
|
| 83 |
+
# for classes. If we do it eagerly, i.e. call the functions in global scope, we will get circular
|
| 84 |
+
# import errors because of the models imported in this file.
|
| 85 |
+
_is_registered = False
|
| 86 |
+
|
| 87 |
+
@classmethod
|
| 88 |
+
def register(cls, model_class: Type, metadata: TransformerBlockMetadata):
|
| 89 |
+
cls._register()
|
| 90 |
+
metadata._cls = model_class
|
| 91 |
+
cls._registry[model_class] = metadata
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def get(cls, model_class: Type) -> TransformerBlockMetadata:
|
| 95 |
+
cls._register()
|
| 96 |
+
if model_class not in cls._registry:
|
| 97 |
+
raise ValueError(f"Model class {model_class} not registered.")
|
| 98 |
+
return cls._registry[model_class]
|
| 99 |
+
|
| 100 |
+
@classmethod
|
| 101 |
+
def _register(cls):
|
| 102 |
+
if cls._is_registered:
|
| 103 |
+
return
|
| 104 |
+
cls._is_registered = True
|
| 105 |
+
_register_transformer_blocks_metadata()
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _register_attention_processors_metadata():
|
| 109 |
+
from ..models.attention_processor import AttnProcessor2_0
|
| 110 |
+
from ..models.transformers.transformer_cogview4 import CogView4AttnProcessor
|
| 111 |
+
from ..models.transformers.transformer_flux import FluxAttnProcessor
|
| 112 |
+
from ..models.transformers.transformer_hunyuanimage import HunyuanImageAttnProcessor
|
| 113 |
+
from ..models.transformers.transformer_qwenimage import QwenDoubleStreamAttnProcessor2_0
|
| 114 |
+
from ..models.transformers.transformer_wan import WanAttnProcessor2_0
|
| 115 |
+
from ..models.transformers.transformer_z_image import ZSingleStreamAttnProcessor
|
| 116 |
+
|
| 117 |
+
# AttnProcessor2_0
|
| 118 |
+
AttentionProcessorRegistry.register(
|
| 119 |
+
model_class=AttnProcessor2_0,
|
| 120 |
+
metadata=AttentionProcessorMetadata(
|
| 121 |
+
skip_processor_output_fn=_skip_proc_output_fn_Attention_AttnProcessor2_0,
|
| 122 |
+
),
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# CogView4AttnProcessor
|
| 126 |
+
AttentionProcessorRegistry.register(
|
| 127 |
+
model_class=CogView4AttnProcessor,
|
| 128 |
+
metadata=AttentionProcessorMetadata(
|
| 129 |
+
skip_processor_output_fn=_skip_proc_output_fn_Attention_CogView4AttnProcessor,
|
| 130 |
+
),
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# WanAttnProcessor2_0
|
| 134 |
+
AttentionProcessorRegistry.register(
|
| 135 |
+
model_class=WanAttnProcessor2_0,
|
| 136 |
+
metadata=AttentionProcessorMetadata(
|
| 137 |
+
skip_processor_output_fn=_skip_proc_output_fn_Attention_WanAttnProcessor2_0,
|
| 138 |
+
),
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# FluxAttnProcessor
|
| 142 |
+
AttentionProcessorRegistry.register(
|
| 143 |
+
model_class=FluxAttnProcessor,
|
| 144 |
+
metadata=AttentionProcessorMetadata(skip_processor_output_fn=_skip_proc_output_fn_Attention_FluxAttnProcessor),
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# QwenDoubleStreamAttnProcessor2
|
| 148 |
+
AttentionProcessorRegistry.register(
|
| 149 |
+
model_class=QwenDoubleStreamAttnProcessor2_0,
|
| 150 |
+
metadata=AttentionProcessorMetadata(
|
| 151 |
+
skip_processor_output_fn=_skip_proc_output_fn_Attention_QwenDoubleStreamAttnProcessor2_0
|
| 152 |
+
),
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# HunyuanImageAttnProcessor
|
| 156 |
+
AttentionProcessorRegistry.register(
|
| 157 |
+
model_class=HunyuanImageAttnProcessor,
|
| 158 |
+
metadata=AttentionProcessorMetadata(
|
| 159 |
+
skip_processor_output_fn=_skip_proc_output_fn_Attention_HunyuanImageAttnProcessor,
|
| 160 |
+
),
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# ZSingleStreamAttnProcessor
|
| 164 |
+
AttentionProcessorRegistry.register(
|
| 165 |
+
model_class=ZSingleStreamAttnProcessor,
|
| 166 |
+
metadata=AttentionProcessorMetadata(
|
| 167 |
+
skip_processor_output_fn=_skip_proc_output_fn_Attention_ZSingleStreamAttnProcessor,
|
| 168 |
+
),
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _register_transformer_blocks_metadata():
|
| 173 |
+
from ..models.attention import BasicTransformerBlock, JointTransformerBlock
|
| 174 |
+
from ..models.transformers.cogvideox_transformer_3d import CogVideoXBlock
|
| 175 |
+
from ..models.transformers.transformer_bria import BriaTransformerBlock
|
| 176 |
+
from ..models.transformers.transformer_cogview4 import CogView4TransformerBlock
|
| 177 |
+
from ..models.transformers.transformer_flux import FluxSingleTransformerBlock, FluxTransformerBlock
|
| 178 |
+
from ..models.transformers.transformer_hunyuan_video import (
|
| 179 |
+
HunyuanVideoSingleTransformerBlock,
|
| 180 |
+
HunyuanVideoTokenReplaceSingleTransformerBlock,
|
| 181 |
+
HunyuanVideoTokenReplaceTransformerBlock,
|
| 182 |
+
HunyuanVideoTransformerBlock,
|
| 183 |
+
)
|
| 184 |
+
from ..models.transformers.transformer_hunyuanimage import (
|
| 185 |
+
HunyuanImageSingleTransformerBlock,
|
| 186 |
+
HunyuanImageTransformerBlock,
|
| 187 |
+
)
|
| 188 |
+
from ..models.transformers.transformer_kandinsky import Kandinsky5TransformerDecoderBlock
|
| 189 |
+
from ..models.transformers.transformer_ltx import LTXVideoTransformerBlock
|
| 190 |
+
from ..models.transformers.transformer_mochi import MochiTransformerBlock
|
| 191 |
+
from ..models.transformers.transformer_motif_video import (
|
| 192 |
+
MotifVideoSingleTransformerBlock,
|
| 193 |
+
MotifVideoTransformerBlock,
|
| 194 |
+
)
|
| 195 |
+
from ..models.transformers.transformer_qwenimage import QwenImageTransformerBlock
|
| 196 |
+
from ..models.transformers.transformer_wan import WanTransformerBlock
|
| 197 |
+
from ..models.transformers.transformer_z_image import ZImageTransformerBlock
|
| 198 |
+
|
| 199 |
+
# BasicTransformerBlock
|
| 200 |
+
TransformerBlockRegistry.register(
|
| 201 |
+
model_class=BasicTransformerBlock,
|
| 202 |
+
metadata=TransformerBlockMetadata(
|
| 203 |
+
return_hidden_states_index=0,
|
| 204 |
+
return_encoder_hidden_states_index=None,
|
| 205 |
+
),
|
| 206 |
+
)
|
| 207 |
+
TransformerBlockRegistry.register(
|
| 208 |
+
model_class=BriaTransformerBlock,
|
| 209 |
+
metadata=TransformerBlockMetadata(
|
| 210 |
+
return_hidden_states_index=0,
|
| 211 |
+
return_encoder_hidden_states_index=None,
|
| 212 |
+
),
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# CogVideoX
|
| 216 |
+
TransformerBlockRegistry.register(
|
| 217 |
+
model_class=CogVideoXBlock,
|
| 218 |
+
metadata=TransformerBlockMetadata(
|
| 219 |
+
return_hidden_states_index=0,
|
| 220 |
+
return_encoder_hidden_states_index=1,
|
| 221 |
+
),
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# CogView4
|
| 225 |
+
TransformerBlockRegistry.register(
|
| 226 |
+
model_class=CogView4TransformerBlock,
|
| 227 |
+
metadata=TransformerBlockMetadata(
|
| 228 |
+
return_hidden_states_index=0,
|
| 229 |
+
return_encoder_hidden_states_index=1,
|
| 230 |
+
),
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Flux
|
| 234 |
+
TransformerBlockRegistry.register(
|
| 235 |
+
model_class=FluxTransformerBlock,
|
| 236 |
+
metadata=TransformerBlockMetadata(
|
| 237 |
+
return_hidden_states_index=1,
|
| 238 |
+
return_encoder_hidden_states_index=0,
|
| 239 |
+
),
|
| 240 |
+
)
|
| 241 |
+
TransformerBlockRegistry.register(
|
| 242 |
+
model_class=FluxSingleTransformerBlock,
|
| 243 |
+
metadata=TransformerBlockMetadata(
|
| 244 |
+
return_hidden_states_index=1,
|
| 245 |
+
return_encoder_hidden_states_index=0,
|
| 246 |
+
),
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
# HunyuanVideo
|
| 250 |
+
TransformerBlockRegistry.register(
|
| 251 |
+
model_class=HunyuanVideoTransformerBlock,
|
| 252 |
+
metadata=TransformerBlockMetadata(
|
| 253 |
+
return_hidden_states_index=0,
|
| 254 |
+
return_encoder_hidden_states_index=1,
|
| 255 |
+
),
|
| 256 |
+
)
|
| 257 |
+
TransformerBlockRegistry.register(
|
| 258 |
+
model_class=HunyuanVideoSingleTransformerBlock,
|
| 259 |
+
metadata=TransformerBlockMetadata(
|
| 260 |
+
return_hidden_states_index=0,
|
| 261 |
+
return_encoder_hidden_states_index=1,
|
| 262 |
+
),
|
| 263 |
+
)
|
| 264 |
+
TransformerBlockRegistry.register(
|
| 265 |
+
model_class=HunyuanVideoTokenReplaceTransformerBlock,
|
| 266 |
+
metadata=TransformerBlockMetadata(
|
| 267 |
+
return_hidden_states_index=0,
|
| 268 |
+
return_encoder_hidden_states_index=1,
|
| 269 |
+
),
|
| 270 |
+
)
|
| 271 |
+
TransformerBlockRegistry.register(
|
| 272 |
+
model_class=HunyuanVideoTokenReplaceSingleTransformerBlock,
|
| 273 |
+
metadata=TransformerBlockMetadata(
|
| 274 |
+
return_hidden_states_index=0,
|
| 275 |
+
return_encoder_hidden_states_index=1,
|
| 276 |
+
),
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# LTXVideo
|
| 280 |
+
TransformerBlockRegistry.register(
|
| 281 |
+
model_class=LTXVideoTransformerBlock,
|
| 282 |
+
metadata=TransformerBlockMetadata(
|
| 283 |
+
return_hidden_states_index=0,
|
| 284 |
+
return_encoder_hidden_states_index=None,
|
| 285 |
+
),
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Mochi
|
| 289 |
+
TransformerBlockRegistry.register(
|
| 290 |
+
model_class=MochiTransformerBlock,
|
| 291 |
+
metadata=TransformerBlockMetadata(
|
| 292 |
+
return_hidden_states_index=0,
|
| 293 |
+
return_encoder_hidden_states_index=1,
|
| 294 |
+
),
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
# MotifVideo
|
| 298 |
+
TransformerBlockRegistry.register(
|
| 299 |
+
model_class=MotifVideoTransformerBlock,
|
| 300 |
+
metadata=TransformerBlockMetadata(
|
| 301 |
+
return_hidden_states_index=0,
|
| 302 |
+
return_encoder_hidden_states_index=1,
|
| 303 |
+
),
|
| 304 |
+
)
|
| 305 |
+
TransformerBlockRegistry.register(
|
| 306 |
+
model_class=MotifVideoSingleTransformerBlock,
|
| 307 |
+
metadata=TransformerBlockMetadata(
|
| 308 |
+
return_hidden_states_index=0,
|
| 309 |
+
return_encoder_hidden_states_index=1,
|
| 310 |
+
),
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
# Wan
|
| 314 |
+
TransformerBlockRegistry.register(
|
| 315 |
+
model_class=WanTransformerBlock,
|
| 316 |
+
metadata=TransformerBlockMetadata(
|
| 317 |
+
return_hidden_states_index=0,
|
| 318 |
+
return_encoder_hidden_states_index=None,
|
| 319 |
+
),
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
# QwenImage
|
| 323 |
+
TransformerBlockRegistry.register(
|
| 324 |
+
model_class=QwenImageTransformerBlock,
|
| 325 |
+
metadata=TransformerBlockMetadata(
|
| 326 |
+
return_hidden_states_index=1,
|
| 327 |
+
return_encoder_hidden_states_index=0,
|
| 328 |
+
),
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
# HunyuanImage2.1
|
| 332 |
+
TransformerBlockRegistry.register(
|
| 333 |
+
model_class=HunyuanImageTransformerBlock,
|
| 334 |
+
metadata=TransformerBlockMetadata(
|
| 335 |
+
return_hidden_states_index=0,
|
| 336 |
+
return_encoder_hidden_states_index=1,
|
| 337 |
+
),
|
| 338 |
+
)
|
| 339 |
+
TransformerBlockRegistry.register(
|
| 340 |
+
model_class=HunyuanImageSingleTransformerBlock,
|
| 341 |
+
metadata=TransformerBlockMetadata(
|
| 342 |
+
return_hidden_states_index=0,
|
| 343 |
+
return_encoder_hidden_states_index=1,
|
| 344 |
+
),
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
# ZImage
|
| 348 |
+
TransformerBlockRegistry.register(
|
| 349 |
+
model_class=ZImageTransformerBlock,
|
| 350 |
+
metadata=TransformerBlockMetadata(
|
| 351 |
+
return_hidden_states_index=0,
|
| 352 |
+
return_encoder_hidden_states_index=None,
|
| 353 |
+
),
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
TransformerBlockRegistry.register(
|
| 357 |
+
model_class=JointTransformerBlock,
|
| 358 |
+
metadata=TransformerBlockMetadata(
|
| 359 |
+
return_hidden_states_index=1,
|
| 360 |
+
return_encoder_hidden_states_index=0,
|
| 361 |
+
),
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
# Kandinsky 5.0 (Kandinsky5TransformerDecoderBlock)
|
| 365 |
+
TransformerBlockRegistry.register(
|
| 366 |
+
model_class=Kandinsky5TransformerDecoderBlock,
|
| 367 |
+
metadata=TransformerBlockMetadata(
|
| 368 |
+
return_hidden_states_index=0,
|
| 369 |
+
return_encoder_hidden_states_index=None,
|
| 370 |
+
hidden_states_argument_name="visual_embed",
|
| 371 |
+
),
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
# fmt: off
|
| 376 |
+
def _skip_attention___ret___hidden_states(self, *args, **kwargs):
|
| 377 |
+
hidden_states = kwargs.get("hidden_states", None)
|
| 378 |
+
if hidden_states is None and len(args) > 0:
|
| 379 |
+
hidden_states = args[0]
|
| 380 |
+
return hidden_states
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def _skip_attention___ret___hidden_states___encoder_hidden_states(self, *args, **kwargs):
|
| 384 |
+
hidden_states = kwargs.get("hidden_states", None)
|
| 385 |
+
encoder_hidden_states = kwargs.get("encoder_hidden_states", None)
|
| 386 |
+
if hidden_states is None and len(args) > 0:
|
| 387 |
+
hidden_states = args[0]
|
| 388 |
+
if encoder_hidden_states is None and len(args) > 1:
|
| 389 |
+
encoder_hidden_states = args[1]
|
| 390 |
+
return hidden_states, encoder_hidden_states
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
_skip_proc_output_fn_Attention_AttnProcessor2_0 = _skip_attention___ret___hidden_states
|
| 394 |
+
_skip_proc_output_fn_Attention_CogView4AttnProcessor = _skip_attention___ret___hidden_states___encoder_hidden_states
|
| 395 |
+
_skip_proc_output_fn_Attention_WanAttnProcessor2_0 = _skip_attention___ret___hidden_states
|
| 396 |
+
# not sure what this is yet.
|
| 397 |
+
_skip_proc_output_fn_Attention_FluxAttnProcessor = _skip_attention___ret___hidden_states
|
| 398 |
+
_skip_proc_output_fn_Attention_QwenDoubleStreamAttnProcessor2_0 = _skip_attention___ret___hidden_states
|
| 399 |
+
_skip_proc_output_fn_Attention_HunyuanImageAttnProcessor = _skip_attention___ret___hidden_states
|
| 400 |
+
_skip_proc_output_fn_Attention_ZSingleStreamAttnProcessor = _skip_attention___ret___hidden_states
|
| 401 |
+
# fmt: on
|
diffusers/hooks/context_parallel.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
import copy
|
| 15 |
+
import inspect
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from typing import Type
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.distributed as dist
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
if torch.distributed.is_available():
|
| 24 |
+
import torch.distributed._functional_collectives as funcol
|
| 25 |
+
|
| 26 |
+
from ..models._modeling_parallel import (
|
| 27 |
+
ContextParallelConfig,
|
| 28 |
+
ContextParallelInput,
|
| 29 |
+
ContextParallelModelPlan,
|
| 30 |
+
ContextParallelOutput,
|
| 31 |
+
gather_size_by_comm,
|
| 32 |
+
)
|
| 33 |
+
from ..utils import get_logger
|
| 34 |
+
from ..utils.torch_utils import lru_cache_unless_export, maybe_allow_in_graph, unwrap_module
|
| 35 |
+
from .hooks import HookRegistry, ModelHook
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 39 |
+
|
| 40 |
+
_CONTEXT_PARALLEL_INPUT_HOOK_TEMPLATE = "cp_input---{}"
|
| 41 |
+
_CONTEXT_PARALLEL_OUTPUT_HOOK_TEMPLATE = "cp_output---{}"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# TODO(aryan): consolidate with ._helpers.TransformerBlockMetadata
|
| 45 |
+
@dataclass
|
| 46 |
+
class ModuleForwardMetadata:
|
| 47 |
+
cached_parameter_indices: dict[str, int] = None
|
| 48 |
+
_cls: Type = None
|
| 49 |
+
|
| 50 |
+
def _get_parameter_from_args_kwargs(self, identifier: str, args=(), kwargs=None):
|
| 51 |
+
kwargs = kwargs or {}
|
| 52 |
+
|
| 53 |
+
if identifier in kwargs:
|
| 54 |
+
return kwargs[identifier], True, None
|
| 55 |
+
|
| 56 |
+
if self.cached_parameter_indices is not None:
|
| 57 |
+
index = self.cached_parameter_indices.get(identifier, None)
|
| 58 |
+
if index is None:
|
| 59 |
+
raise ValueError(f"Parameter '{identifier}' not found in cached indices.")
|
| 60 |
+
return args[index], False, index
|
| 61 |
+
|
| 62 |
+
if self._cls is None:
|
| 63 |
+
raise ValueError("Model class is not set for metadata.")
|
| 64 |
+
|
| 65 |
+
parameters = list(inspect.signature(self._cls.forward).parameters.keys())
|
| 66 |
+
parameters = parameters[1:] # skip `self`
|
| 67 |
+
self.cached_parameter_indices = {param: i for i, param in enumerate(parameters)}
|
| 68 |
+
|
| 69 |
+
if identifier not in self.cached_parameter_indices:
|
| 70 |
+
raise ValueError(f"Parameter '{identifier}' not found in function signature but was requested.")
|
| 71 |
+
|
| 72 |
+
index = self.cached_parameter_indices[identifier]
|
| 73 |
+
|
| 74 |
+
if index >= len(args):
|
| 75 |
+
raise ValueError(f"Expected {index} arguments but got {len(args)}.")
|
| 76 |
+
|
| 77 |
+
return args[index], False, index
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def apply_context_parallel(
|
| 81 |
+
module: torch.nn.Module,
|
| 82 |
+
parallel_config: ContextParallelConfig,
|
| 83 |
+
plan: dict[str, ContextParallelModelPlan],
|
| 84 |
+
) -> None:
|
| 85 |
+
"""Apply context parallel on a model."""
|
| 86 |
+
logger.debug(f"Applying context parallel with CP mesh: {parallel_config._mesh} and plan: {plan}")
|
| 87 |
+
|
| 88 |
+
for module_id, cp_model_plan in plan.items():
|
| 89 |
+
submodule = _get_submodule_by_name(module, module_id)
|
| 90 |
+
if not isinstance(submodule, list):
|
| 91 |
+
submodule = [submodule]
|
| 92 |
+
|
| 93 |
+
logger.debug(f"Applying ContextParallelHook to {module_id=} identifying a total of {len(submodule)} modules")
|
| 94 |
+
|
| 95 |
+
for m in submodule:
|
| 96 |
+
if isinstance(cp_model_plan, dict):
|
| 97 |
+
hook = ContextParallelSplitHook(cp_model_plan, parallel_config)
|
| 98 |
+
hook_name = _CONTEXT_PARALLEL_INPUT_HOOK_TEMPLATE.format(module_id)
|
| 99 |
+
elif isinstance(cp_model_plan, (ContextParallelOutput, list, tuple)):
|
| 100 |
+
if isinstance(cp_model_plan, ContextParallelOutput):
|
| 101 |
+
cp_model_plan = [cp_model_plan]
|
| 102 |
+
if not all(isinstance(x, ContextParallelOutput) for x in cp_model_plan):
|
| 103 |
+
raise ValueError(f"Expected all elements of cp_model_plan to be CPOutput, but got {cp_model_plan}")
|
| 104 |
+
hook = ContextParallelGatherHook(cp_model_plan, parallel_config)
|
| 105 |
+
hook_name = _CONTEXT_PARALLEL_OUTPUT_HOOK_TEMPLATE.format(module_id)
|
| 106 |
+
else:
|
| 107 |
+
raise ValueError(f"Unsupported context parallel model plan type: {type(cp_model_plan)}")
|
| 108 |
+
registry = HookRegistry.check_if_exists_or_initialize(m)
|
| 109 |
+
registry.register_hook(hook, hook_name)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def remove_context_parallel(module: torch.nn.Module, plan: dict[str, ContextParallelModelPlan]) -> None:
|
| 113 |
+
for module_id, cp_model_plan in plan.items():
|
| 114 |
+
submodule = _get_submodule_by_name(module, module_id)
|
| 115 |
+
if not isinstance(submodule, list):
|
| 116 |
+
submodule = [submodule]
|
| 117 |
+
|
| 118 |
+
for m in submodule:
|
| 119 |
+
registry = HookRegistry.check_if_exists_or_initialize(m)
|
| 120 |
+
if isinstance(cp_model_plan, dict):
|
| 121 |
+
hook_name = _CONTEXT_PARALLEL_INPUT_HOOK_TEMPLATE.format(module_id)
|
| 122 |
+
elif isinstance(cp_model_plan, (ContextParallelOutput, list, tuple)):
|
| 123 |
+
hook_name = _CONTEXT_PARALLEL_OUTPUT_HOOK_TEMPLATE.format(module_id)
|
| 124 |
+
else:
|
| 125 |
+
raise ValueError(f"Unsupported context parallel model plan type: {type(cp_model_plan)}")
|
| 126 |
+
registry.remove_hook(hook_name)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class ContextParallelSplitHook(ModelHook):
|
| 130 |
+
def __init__(self, metadata: ContextParallelModelPlan, parallel_config: ContextParallelConfig) -> None:
|
| 131 |
+
super().__init__()
|
| 132 |
+
self.metadata = metadata
|
| 133 |
+
self.parallel_config = parallel_config
|
| 134 |
+
self.module_forward_metadata = None
|
| 135 |
+
|
| 136 |
+
def initialize_hook(self, module):
|
| 137 |
+
cls = unwrap_module(module).__class__
|
| 138 |
+
self.module_forward_metadata = ModuleForwardMetadata(_cls=cls)
|
| 139 |
+
return module
|
| 140 |
+
|
| 141 |
+
def pre_forward(self, module, *args, **kwargs):
|
| 142 |
+
args_list = list(args)
|
| 143 |
+
|
| 144 |
+
for name, cpm in self.metadata.items():
|
| 145 |
+
if isinstance(cpm, ContextParallelInput) and cpm.split_output:
|
| 146 |
+
continue
|
| 147 |
+
|
| 148 |
+
# Maybe the parameter was passed as a keyword argument
|
| 149 |
+
input_val, is_kwarg, index = self.module_forward_metadata._get_parameter_from_args_kwargs(
|
| 150 |
+
name, args_list, kwargs
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
if input_val is None:
|
| 154 |
+
continue
|
| 155 |
+
|
| 156 |
+
# The input_val may be a tensor or list/tuple of tensors. In certain cases, user may specify to shard
|
| 157 |
+
# the output instead of input for a particular layer by setting split_output=True
|
| 158 |
+
if isinstance(input_val, torch.Tensor):
|
| 159 |
+
input_val = self._prepare_cp_input(input_val, cpm)
|
| 160 |
+
elif isinstance(input_val, (list, tuple)):
|
| 161 |
+
if len(input_val) != len(cpm):
|
| 162 |
+
raise ValueError(
|
| 163 |
+
f"Expected input model plan to have {len(input_val)} elements, but got {len(cpm)}."
|
| 164 |
+
)
|
| 165 |
+
sharded_input_val = []
|
| 166 |
+
for i, x in enumerate(input_val):
|
| 167 |
+
if torch.is_tensor(x) and not cpm[i].split_output:
|
| 168 |
+
x = self._prepare_cp_input(x, cpm[i])
|
| 169 |
+
sharded_input_val.append(x)
|
| 170 |
+
input_val = sharded_input_val
|
| 171 |
+
else:
|
| 172 |
+
raise ValueError(f"Unsupported input type: {type(input_val)}")
|
| 173 |
+
|
| 174 |
+
if is_kwarg:
|
| 175 |
+
kwargs[name] = input_val
|
| 176 |
+
elif index is not None and index < len(args_list):
|
| 177 |
+
args_list[index] = input_val
|
| 178 |
+
else:
|
| 179 |
+
raise ValueError(
|
| 180 |
+
f"An unexpected error occurred while processing the input '{name}'. Please open an "
|
| 181 |
+
f"issue at https://github.com/huggingface/diffusers/issues and provide a minimal reproducible "
|
| 182 |
+
f"example along with the full stack trace."
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
return tuple(args_list), kwargs
|
| 186 |
+
|
| 187 |
+
def post_forward(self, module, output):
|
| 188 |
+
is_tensor = isinstance(output, torch.Tensor)
|
| 189 |
+
is_tensor_list = isinstance(output, (list, tuple)) and all(isinstance(x, torch.Tensor) for x in output)
|
| 190 |
+
|
| 191 |
+
if not is_tensor and not is_tensor_list:
|
| 192 |
+
raise ValueError(f"Expected output to be a tensor or a list/tuple of tensors, but got {type(output)}.")
|
| 193 |
+
|
| 194 |
+
output = [output] if is_tensor else list(output)
|
| 195 |
+
for index, cpm in self.metadata.items():
|
| 196 |
+
if not isinstance(cpm, ContextParallelInput) or not cpm.split_output:
|
| 197 |
+
continue
|
| 198 |
+
if index >= len(output):
|
| 199 |
+
raise ValueError(f"Index {index} out of bounds for output of length {len(output)}.")
|
| 200 |
+
current_output = output[index]
|
| 201 |
+
current_output = self._prepare_cp_input(current_output, cpm)
|
| 202 |
+
output[index] = current_output
|
| 203 |
+
|
| 204 |
+
return output[0] if is_tensor else tuple(output)
|
| 205 |
+
|
| 206 |
+
def _prepare_cp_input(self, x: torch.Tensor, cp_input: ContextParallelInput) -> torch.Tensor:
|
| 207 |
+
if cp_input.expected_dims is not None and x.dim() != cp_input.expected_dims:
|
| 208 |
+
logger.warning_once(
|
| 209 |
+
f"Expected input tensor to have {cp_input.expected_dims} dimensions, but got {x.dim()} dimensions, split will not be applied."
|
| 210 |
+
)
|
| 211 |
+
return x
|
| 212 |
+
else:
|
| 213 |
+
if self.parallel_config.ulysses_anything or self.parallel_config.ring_anything:
|
| 214 |
+
return PartitionAnythingSharder.shard_anything(
|
| 215 |
+
x, cp_input.split_dim, self.parallel_config._flattened_mesh
|
| 216 |
+
)
|
| 217 |
+
return EquipartitionSharder.shard(x, cp_input.split_dim, self.parallel_config._flattened_mesh)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class ContextParallelGatherHook(ModelHook):
|
| 221 |
+
def __init__(self, metadata: ContextParallelModelPlan, parallel_config: ContextParallelConfig) -> None:
|
| 222 |
+
super().__init__()
|
| 223 |
+
self.metadata = metadata
|
| 224 |
+
self.parallel_config = parallel_config
|
| 225 |
+
|
| 226 |
+
def post_forward(self, module, output):
|
| 227 |
+
is_tensor = isinstance(output, torch.Tensor)
|
| 228 |
+
|
| 229 |
+
if is_tensor:
|
| 230 |
+
output = [output]
|
| 231 |
+
elif not (isinstance(output, (list, tuple)) and all(isinstance(x, torch.Tensor) for x in output)):
|
| 232 |
+
raise ValueError(f"Expected output to be a tensor or a list/tuple of tensors, but got {type(output)}.")
|
| 233 |
+
|
| 234 |
+
output = list(output)
|
| 235 |
+
|
| 236 |
+
if len(output) != len(self.metadata):
|
| 237 |
+
raise ValueError(f"Expected output to have {len(self.metadata)} elements, but got {len(output)}.")
|
| 238 |
+
|
| 239 |
+
for i, cpm in enumerate(self.metadata):
|
| 240 |
+
if cpm is None:
|
| 241 |
+
continue
|
| 242 |
+
if self.parallel_config.ulysses_anything or self.parallel_config.ring_anything:
|
| 243 |
+
output[i] = PartitionAnythingSharder.unshard_anything(
|
| 244 |
+
output[i], cpm.gather_dim, self.parallel_config._flattened_mesh
|
| 245 |
+
)
|
| 246 |
+
else:
|
| 247 |
+
output[i] = EquipartitionSharder.unshard(
|
| 248 |
+
output[i], cpm.gather_dim, self.parallel_config._flattened_mesh
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
return output[0] if is_tensor else tuple(output)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
class AllGatherFunction(torch.autograd.Function):
|
| 255 |
+
@staticmethod
|
| 256 |
+
def forward(ctx, tensor, dim, group):
|
| 257 |
+
ctx.dim = dim
|
| 258 |
+
ctx.group = group
|
| 259 |
+
ctx.world_size = torch.distributed.get_world_size(group)
|
| 260 |
+
ctx.rank = torch.distributed.get_rank(group)
|
| 261 |
+
return funcol.all_gather_tensor(tensor, dim, group=group)
|
| 262 |
+
|
| 263 |
+
@staticmethod
|
| 264 |
+
def backward(ctx, grad_output):
|
| 265 |
+
grad_chunks = torch.chunk(grad_output, ctx.world_size, dim=ctx.dim)
|
| 266 |
+
return grad_chunks[ctx.rank], None, None
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class EquipartitionSharder:
|
| 270 |
+
@classmethod
|
| 271 |
+
def shard(cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh) -> torch.Tensor:
|
| 272 |
+
# NOTE: the following assertion does not have to be true in general. We simply enforce it for now
|
| 273 |
+
# because the alternate case has not yet been tested/required for any model.
|
| 274 |
+
assert tensor.size()[dim] % mesh.size() == 0, (
|
| 275 |
+
"Tensor size along dimension to be sharded must be divisible by mesh size"
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
# The following is not fullgraph compatible with Dynamo (fails in DeviceMesh.get_rank)
|
| 279 |
+
# return tensor.chunk(mesh.size(), dim=dim)[mesh.get_rank()]
|
| 280 |
+
|
| 281 |
+
return tensor.chunk(mesh.size(), dim=dim)[torch.distributed.get_rank(mesh.get_group())]
|
| 282 |
+
|
| 283 |
+
@classmethod
|
| 284 |
+
def unshard(cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh) -> torch.Tensor:
|
| 285 |
+
tensor = tensor.contiguous()
|
| 286 |
+
tensor = AllGatherFunction.apply(tensor, dim, mesh.get_group())
|
| 287 |
+
return tensor
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
class AllGatherAnythingFunction(torch.autograd.Function):
|
| 291 |
+
@staticmethod
|
| 292 |
+
def forward(ctx, tensor: torch.Tensor, dim: int, group: dist.device_mesh.DeviceMesh):
|
| 293 |
+
ctx.dim = dim
|
| 294 |
+
ctx.group = group
|
| 295 |
+
ctx.world_size = dist.get_world_size(group)
|
| 296 |
+
ctx.rank = dist.get_rank(group)
|
| 297 |
+
gathered_tensor = _all_gather_anything(tensor, dim, group)
|
| 298 |
+
return gathered_tensor
|
| 299 |
+
|
| 300 |
+
@staticmethod
|
| 301 |
+
def backward(ctx, grad_output):
|
| 302 |
+
# NOTE: We use `tensor_split` instead of chunk, because the `chunk`
|
| 303 |
+
# function may return fewer than the specified number of chunks!
|
| 304 |
+
grad_splits = torch.tensor_split(grad_output, ctx.world_size, dim=ctx.dim)
|
| 305 |
+
return grad_splits[ctx.rank], None, None
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class PartitionAnythingSharder:
|
| 309 |
+
@classmethod
|
| 310 |
+
def shard_anything(
|
| 311 |
+
cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh
|
| 312 |
+
) -> torch.Tensor:
|
| 313 |
+
assert tensor.size()[dim] >= mesh.size(), (
|
| 314 |
+
f"Cannot shard tensor of size {tensor.size()} along dim {dim} across mesh of size {mesh.size()}."
|
| 315 |
+
)
|
| 316 |
+
# NOTE: We use `tensor_split` instead of chunk, because the `chunk`
|
| 317 |
+
# function may return fewer than the specified number of chunks!
|
| 318 |
+
return tensor.tensor_split(mesh.size(), dim=dim)[dist.get_rank(mesh.get_group())]
|
| 319 |
+
|
| 320 |
+
@classmethod
|
| 321 |
+
def unshard_anything(
|
| 322 |
+
cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh
|
| 323 |
+
) -> torch.Tensor:
|
| 324 |
+
tensor = tensor.contiguous()
|
| 325 |
+
tensor = AllGatherAnythingFunction.apply(tensor, dim, mesh.get_group())
|
| 326 |
+
return tensor
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
@lru_cache_unless_export(maxsize=64)
|
| 330 |
+
def _fill_gather_shapes(shape: tuple[int], gather_dims: tuple[int], dim: int, world_size: int) -> list[list[int]]:
|
| 331 |
+
gather_shapes = []
|
| 332 |
+
for i in range(world_size):
|
| 333 |
+
rank_shape = list(copy.deepcopy(shape))
|
| 334 |
+
rank_shape[dim] = gather_dims[i]
|
| 335 |
+
gather_shapes.append(rank_shape)
|
| 336 |
+
return gather_shapes
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
@maybe_allow_in_graph
|
| 340 |
+
def _all_gather_anything(tensor: torch.Tensor, dim: int, group: dist.device_mesh.DeviceMesh) -> torch.Tensor:
|
| 341 |
+
world_size = dist.get_world_size(group=group)
|
| 342 |
+
|
| 343 |
+
tensor = tensor.contiguous()
|
| 344 |
+
shape = tensor.shape
|
| 345 |
+
rank_dim = shape[dim]
|
| 346 |
+
gather_dims = gather_size_by_comm(rank_dim, group)
|
| 347 |
+
|
| 348 |
+
gather_shapes = _fill_gather_shapes(tuple(shape), tuple(gather_dims), dim, world_size)
|
| 349 |
+
|
| 350 |
+
gathered_tensors = [torch.empty(shape, device=tensor.device, dtype=tensor.dtype) for shape in gather_shapes]
|
| 351 |
+
|
| 352 |
+
dist.all_gather(gathered_tensors, tensor, group=group)
|
| 353 |
+
gathered_tensor = torch.cat(gathered_tensors, dim=dim)
|
| 354 |
+
return gathered_tensor
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def _get_submodule_by_name(model: torch.nn.Module, name: str) -> torch.nn.Module | list[torch.nn.Module]:
|
| 358 |
+
if name.count("*") > 1:
|
| 359 |
+
raise ValueError("Wildcard '*' can only be used once in the name")
|
| 360 |
+
return _find_submodule_by_name(model, name)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def _find_submodule_by_name(model: torch.nn.Module, name: str) -> torch.nn.Module | list[torch.nn.Module]:
|
| 364 |
+
if name == "":
|
| 365 |
+
return model
|
| 366 |
+
first_atom, remaining_name = name.split(".", 1) if "." in name else (name, "")
|
| 367 |
+
if first_atom == "*":
|
| 368 |
+
if not isinstance(model, torch.nn.ModuleList):
|
| 369 |
+
raise ValueError("Wildcard '*' can only be used with ModuleList")
|
| 370 |
+
submodules = []
|
| 371 |
+
for submodule in model:
|
| 372 |
+
subsubmodules = _find_submodule_by_name(submodule, remaining_name)
|
| 373 |
+
if not isinstance(subsubmodules, list):
|
| 374 |
+
subsubmodules = [subsubmodules]
|
| 375 |
+
submodules.extend(subsubmodules)
|
| 376 |
+
return submodules
|
| 377 |
+
else:
|
| 378 |
+
if hasattr(model, first_atom):
|
| 379 |
+
submodule = getattr(model, first_atom)
|
| 380 |
+
return _find_submodule_by_name(submodule, remaining_name)
|
| 381 |
+
else:
|
| 382 |
+
raise ValueError(f"'{first_atom}' is not a submodule of '{model.__class__.__name__}'")
|
diffusers/hooks/faster_cache.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import re
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from typing import Any, Callable
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
from ..models.attention import AttentionModuleMixin
|
| 22 |
+
from ..models.modeling_outputs import Transformer2DModelOutput
|
| 23 |
+
from ..utils import logging
|
| 24 |
+
from ._common import _ATTENTION_CLASSES
|
| 25 |
+
from .hooks import HookRegistry, ModelHook
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
_FASTER_CACHE_DENOISER_HOOK = "faster_cache_denoiser"
|
| 32 |
+
_FASTER_CACHE_BLOCK_HOOK = "faster_cache_block"
|
| 33 |
+
_SPATIAL_ATTENTION_BLOCK_IDENTIFIERS = (
|
| 34 |
+
"^blocks.*attn",
|
| 35 |
+
"^transformer_blocks.*attn",
|
| 36 |
+
"^single_transformer_blocks.*attn",
|
| 37 |
+
)
|
| 38 |
+
_TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS = ("^temporal_transformer_blocks.*attn",)
|
| 39 |
+
_TRANSFORMER_BLOCK_IDENTIFIERS = _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS + _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS
|
| 40 |
+
_UNCOND_COND_INPUT_KWARGS_IDENTIFIERS = (
|
| 41 |
+
"hidden_states",
|
| 42 |
+
"encoder_hidden_states",
|
| 43 |
+
"timestep",
|
| 44 |
+
"attention_mask",
|
| 45 |
+
"encoder_attention_mask",
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class FasterCacheConfig:
|
| 51 |
+
r"""
|
| 52 |
+
Configuration for [FasterCache](https://huggingface.co/papers/2410.19355).
|
| 53 |
+
|
| 54 |
+
Attributes:
|
| 55 |
+
spatial_attention_block_skip_range (`int`, defaults to `2`):
|
| 56 |
+
Calculate the attention states every `N` iterations. If this is set to `N`, the attention computation will
|
| 57 |
+
be skipped `N - 1` times (i.e., cached attention states will be reused) before computing the new attention
|
| 58 |
+
states again.
|
| 59 |
+
temporal_attention_block_skip_range (`int`, *optional*, defaults to `None`):
|
| 60 |
+
Calculate the attention states every `N` iterations. If this is set to `N`, the attention computation will
|
| 61 |
+
be skipped `N - 1` times (i.e., cached attention states will be reused) before computing the new attention
|
| 62 |
+
states again.
|
| 63 |
+
spatial_attention_timestep_skip_range (`tuple[float, float]`, defaults to `(-1, 681)`):
|
| 64 |
+
The timestep range within which the spatial attention computation can be skipped without a significant loss
|
| 65 |
+
in quality. This is to be determined by the user based on the underlying model. The first value in the
|
| 66 |
+
tuple is the lower bound and the second value is the upper bound. Typically, diffusion timesteps for
|
| 67 |
+
denoising are in the reversed range of 0 to 1000 (i.e. denoising starts at timestep 1000 and ends at
|
| 68 |
+
timestep 0). For the default values, this would mean that the spatial attention computation skipping will
|
| 69 |
+
be applicable only after denoising timestep 681 is reached, and continue until the end of the denoising
|
| 70 |
+
process.
|
| 71 |
+
temporal_attention_timestep_skip_range (`tuple[float, float]`, *optional*, defaults to `None`):
|
| 72 |
+
The timestep range within which the temporal attention computation can be skipped without a significant
|
| 73 |
+
loss in quality. This is to be determined by the user based on the underlying model. The first value in the
|
| 74 |
+
tuple is the lower bound and the second value is the upper bound. Typically, diffusion timesteps for
|
| 75 |
+
denoising are in the reversed range of 0 to 1000 (i.e. denoising starts at timestep 1000 and ends at
|
| 76 |
+
timestep 0).
|
| 77 |
+
low_frequency_weight_update_timestep_range (`tuple[int, int]`, defaults to `(99, 901)`):
|
| 78 |
+
The timestep range within which the low frequency weight scaling update is applied. The first value in the
|
| 79 |
+
tuple is the lower bound and the second value is the upper bound of the timestep range. The callback
|
| 80 |
+
function for the update is called only within this range.
|
| 81 |
+
high_frequency_weight_update_timestep_range (`tuple[int, int]`, defaults to `(-1, 301)`):
|
| 82 |
+
The timestep range within which the high frequency weight scaling update is applied. The first value in the
|
| 83 |
+
tuple is the lower bound and the second value is the upper bound of the timestep range. The callback
|
| 84 |
+
function for the update is called only within this range.
|
| 85 |
+
alpha_low_frequency (`float`, defaults to `1.1`):
|
| 86 |
+
The weight to scale the low frequency updates by. This is used to approximate the unconditional branch from
|
| 87 |
+
the conditional branch outputs.
|
| 88 |
+
alpha_high_frequency (`float`, defaults to `1.1`):
|
| 89 |
+
The weight to scale the high frequency updates by. This is used to approximate the unconditional branch
|
| 90 |
+
from the conditional branch outputs.
|
| 91 |
+
unconditional_batch_skip_range (`int`, defaults to `5`):
|
| 92 |
+
Process the unconditional branch every `N` iterations. If this is set to `N`, the unconditional branch
|
| 93 |
+
computation will be skipped `N - 1` times (i.e., cached unconditional branch states will be reused) before
|
| 94 |
+
computing the new unconditional branch states again.
|
| 95 |
+
unconditional_batch_timestep_skip_range (`tuple[float, float]`, defaults to `(-1, 641)`):
|
| 96 |
+
The timestep range within which the unconditional branch computation can be skipped without a significant
|
| 97 |
+
loss in quality. This is to be determined by the user based on the underlying model. The first value in the
|
| 98 |
+
tuple is the lower bound and the second value is the upper bound.
|
| 99 |
+
spatial_attention_block_identifiers (`tuple[str, ...]`, defaults to `("blocks.*attn1", "transformer_blocks.*attn1", "single_transformer_blocks.*attn1")`):
|
| 100 |
+
The identifiers to match the spatial attention blocks in the model. If the name of the block contains any
|
| 101 |
+
of these identifiers, FasterCache will be applied to that block. This can either be the full layer names,
|
| 102 |
+
partial layer names, or regex patterns. Matching will always be done using a regex match.
|
| 103 |
+
temporal_attention_block_identifiers (`tuple[str, ...]`, defaults to `("temporal_transformer_blocks.*attn1",)`):
|
| 104 |
+
The identifiers to match the temporal attention blocks in the model. If the name of the block contains any
|
| 105 |
+
of these identifiers, FasterCache will be applied to that block. This can either be the full layer names,
|
| 106 |
+
partial layer names, or regex patterns. Matching will always be done using a regex match.
|
| 107 |
+
attention_weight_callback (`Callable[[torch.nn.Module], float]`, defaults to `None`):
|
| 108 |
+
The callback function to determine the weight to scale the attention outputs by. This function should take
|
| 109 |
+
the attention module as input and return a float value. This is used to approximate the unconditional
|
| 110 |
+
branch from the conditional branch outputs. If not provided, the default weight is 0.5 for all timesteps.
|
| 111 |
+
Typically, as described in the paper, this weight should gradually increase from 0 to 1 as the inference
|
| 112 |
+
progresses. Users are encouraged to experiment and provide custom weight schedules that take into account
|
| 113 |
+
the number of inference steps and underlying model behaviour as denoising progresses.
|
| 114 |
+
low_frequency_weight_callback (`Callable[[torch.nn.Module], float]`, defaults to `None`):
|
| 115 |
+
The callback function to determine the weight to scale the low frequency updates by. If not provided, the
|
| 116 |
+
default weight is 1.1 for timesteps within the range specified (as described in the paper).
|
| 117 |
+
high_frequency_weight_callback (`Callable[[torch.nn.Module], float]`, defaults to `None`):
|
| 118 |
+
The callback function to determine the weight to scale the high frequency updates by. If not provided, the
|
| 119 |
+
default weight is 1.1 for timesteps within the range specified (as described in the paper).
|
| 120 |
+
tensor_format (`str`, defaults to `"BCFHW"`):
|
| 121 |
+
The format of the input tensors. This should be one of `"BCFHW"`, `"BFCHW"`, or `"BCHW"`. The format is
|
| 122 |
+
used to split individual latent frames in order for low and high frequency components to be computed.
|
| 123 |
+
is_guidance_distilled (`bool`, defaults to `False`):
|
| 124 |
+
Whether the model is guidance distilled or not. If the model is guidance distilled, FasterCache will not be
|
| 125 |
+
applied at the denoiser-level to skip the unconditional branch computation (as there is none).
|
| 126 |
+
_unconditional_conditional_input_kwargs_identifiers (`list[str]`, defaults to `("hidden_states", "encoder_hidden_states", "timestep", "attention_mask", "encoder_attention_mask")`):
|
| 127 |
+
The identifiers to match the input kwargs that contain the batchwise-concatenated unconditional and
|
| 128 |
+
conditional inputs. If the name of the input kwargs contains any of these identifiers, FasterCache will
|
| 129 |
+
split the inputs into unconditional and conditional branches. This must be a list of exact input kwargs
|
| 130 |
+
names that contain the batchwise-concatenated unconditional and conditional inputs.
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
# In the paper and codebase, they hardcode these values to 2. However, it can be made configurable
|
| 134 |
+
# after some testing. We default to 2 if these parameters are not provided.
|
| 135 |
+
spatial_attention_block_skip_range: int = 2
|
| 136 |
+
temporal_attention_block_skip_range: int | None = None
|
| 137 |
+
|
| 138 |
+
spatial_attention_timestep_skip_range: tuple[int, int] = (-1, 681)
|
| 139 |
+
temporal_attention_timestep_skip_range: tuple[int, int] = (-1, 681)
|
| 140 |
+
|
| 141 |
+
# Indicator functions for low/high frequency as mentioned in Equation 11 of the paper
|
| 142 |
+
low_frequency_weight_update_timestep_range: tuple[int, int] = (99, 901)
|
| 143 |
+
high_frequency_weight_update_timestep_range: tuple[int, int] = (-1, 301)
|
| 144 |
+
|
| 145 |
+
# ⍺1 and ⍺2 as mentioned in Equation 11 of the paper
|
| 146 |
+
alpha_low_frequency: float = 1.1
|
| 147 |
+
alpha_high_frequency: float = 1.1
|
| 148 |
+
|
| 149 |
+
# n as described in CFG-Cache explanation in the paper - dependent on the model
|
| 150 |
+
unconditional_batch_skip_range: int = 5
|
| 151 |
+
unconditional_batch_timestep_skip_range: tuple[int, int] = (-1, 641)
|
| 152 |
+
|
| 153 |
+
spatial_attention_block_identifiers: tuple[str, ...] = _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS
|
| 154 |
+
temporal_attention_block_identifiers: tuple[str, ...] = _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS
|
| 155 |
+
|
| 156 |
+
attention_weight_callback: Callable[[torch.nn.Module], float] = None
|
| 157 |
+
low_frequency_weight_callback: Callable[[torch.nn.Module], float] = None
|
| 158 |
+
high_frequency_weight_callback: Callable[[torch.nn.Module], float] = None
|
| 159 |
+
|
| 160 |
+
tensor_format: str = "BCFHW"
|
| 161 |
+
is_guidance_distilled: bool = False
|
| 162 |
+
|
| 163 |
+
current_timestep_callback: Callable[[], int] = None
|
| 164 |
+
|
| 165 |
+
_unconditional_conditional_input_kwargs_identifiers: list[str] = _UNCOND_COND_INPUT_KWARGS_IDENTIFIERS
|
| 166 |
+
|
| 167 |
+
def __repr__(self) -> str:
|
| 168 |
+
return (
|
| 169 |
+
f"FasterCacheConfig(\n"
|
| 170 |
+
f" spatial_attention_block_skip_range={self.spatial_attention_block_skip_range},\n"
|
| 171 |
+
f" temporal_attention_block_skip_range={self.temporal_attention_block_skip_range},\n"
|
| 172 |
+
f" spatial_attention_timestep_skip_range={self.spatial_attention_timestep_skip_range},\n"
|
| 173 |
+
f" temporal_attention_timestep_skip_range={self.temporal_attention_timestep_skip_range},\n"
|
| 174 |
+
f" low_frequency_weight_update_timestep_range={self.low_frequency_weight_update_timestep_range},\n"
|
| 175 |
+
f" high_frequency_weight_update_timestep_range={self.high_frequency_weight_update_timestep_range},\n"
|
| 176 |
+
f" alpha_low_frequency={self.alpha_low_frequency},\n"
|
| 177 |
+
f" alpha_high_frequency={self.alpha_high_frequency},\n"
|
| 178 |
+
f" unconditional_batch_skip_range={self.unconditional_batch_skip_range},\n"
|
| 179 |
+
f" unconditional_batch_timestep_skip_range={self.unconditional_batch_timestep_skip_range},\n"
|
| 180 |
+
f" spatial_attention_block_identifiers={self.spatial_attention_block_identifiers},\n"
|
| 181 |
+
f" temporal_attention_block_identifiers={self.temporal_attention_block_identifiers},\n"
|
| 182 |
+
f" tensor_format={self.tensor_format},\n"
|
| 183 |
+
f")"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class FasterCacheDenoiserState:
|
| 188 |
+
r"""
|
| 189 |
+
State for [FasterCache](https://huggingface.co/papers/2410.19355) top-level denoiser module.
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
def __init__(self) -> None:
|
| 193 |
+
self.iteration: int = 0
|
| 194 |
+
self.low_frequency_delta: torch.Tensor = None
|
| 195 |
+
self.high_frequency_delta: torch.Tensor = None
|
| 196 |
+
|
| 197 |
+
def reset(self):
|
| 198 |
+
self.iteration = 0
|
| 199 |
+
self.low_frequency_delta = None
|
| 200 |
+
self.high_frequency_delta = None
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class FasterCacheBlockState:
|
| 204 |
+
r"""
|
| 205 |
+
State for [FasterCache](https://huggingface.co/papers/2410.19355). Every underlying block that FasterCache is
|
| 206 |
+
applied to will have an instance of this state.
|
| 207 |
+
"""
|
| 208 |
+
|
| 209 |
+
def __init__(self) -> None:
|
| 210 |
+
self.iteration: int = 0
|
| 211 |
+
self.batch_size: int = None
|
| 212 |
+
self.cache: tuple[torch.Tensor, torch.Tensor] = None
|
| 213 |
+
|
| 214 |
+
def reset(self):
|
| 215 |
+
self.iteration = 0
|
| 216 |
+
self.batch_size = None
|
| 217 |
+
self.cache = None
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class FasterCacheDenoiserHook(ModelHook):
|
| 221 |
+
_is_stateful = True
|
| 222 |
+
|
| 223 |
+
def __init__(
|
| 224 |
+
self,
|
| 225 |
+
unconditional_batch_skip_range: int,
|
| 226 |
+
unconditional_batch_timestep_skip_range: tuple[int, int],
|
| 227 |
+
tensor_format: str,
|
| 228 |
+
is_guidance_distilled: bool,
|
| 229 |
+
uncond_cond_input_kwargs_identifiers: list[str],
|
| 230 |
+
current_timestep_callback: Callable[[], int],
|
| 231 |
+
low_frequency_weight_callback: Callable[[torch.nn.Module], torch.Tensor],
|
| 232 |
+
high_frequency_weight_callback: Callable[[torch.nn.Module], torch.Tensor],
|
| 233 |
+
) -> None:
|
| 234 |
+
super().__init__()
|
| 235 |
+
|
| 236 |
+
self.unconditional_batch_skip_range = unconditional_batch_skip_range
|
| 237 |
+
self.unconditional_batch_timestep_skip_range = unconditional_batch_timestep_skip_range
|
| 238 |
+
# We can't easily detect what args are to be split in unconditional and conditional branches. We
|
| 239 |
+
# can only do it for kwargs, hence they are the only ones we split. The args are passed as-is.
|
| 240 |
+
# If a model is to be made compatible with FasterCache, the user must ensure that the inputs that
|
| 241 |
+
# contain batchwise-concatenated unconditional and conditional inputs are passed as kwargs.
|
| 242 |
+
self.uncond_cond_input_kwargs_identifiers = uncond_cond_input_kwargs_identifiers
|
| 243 |
+
self.tensor_format = tensor_format
|
| 244 |
+
self.is_guidance_distilled = is_guidance_distilled
|
| 245 |
+
|
| 246 |
+
self.current_timestep_callback = current_timestep_callback
|
| 247 |
+
self.low_frequency_weight_callback = low_frequency_weight_callback
|
| 248 |
+
self.high_frequency_weight_callback = high_frequency_weight_callback
|
| 249 |
+
|
| 250 |
+
def initialize_hook(self, module):
|
| 251 |
+
self.state = FasterCacheDenoiserState()
|
| 252 |
+
return module
|
| 253 |
+
|
| 254 |
+
@staticmethod
|
| 255 |
+
def _get_cond_input(input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 256 |
+
# Note: this method assumes that the input tensor is batchwise-concatenated with unconditional inputs
|
| 257 |
+
# followed by conditional inputs.
|
| 258 |
+
_, cond = input.chunk(2, dim=0)
|
| 259 |
+
return cond
|
| 260 |
+
|
| 261 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs) -> Any:
|
| 262 |
+
# Split the unconditional and conditional inputs. We only want to infer the conditional branch if the
|
| 263 |
+
# requirements for skipping the unconditional branch are met as described in the paper.
|
| 264 |
+
# We skip the unconditional branch only if the following conditions are met:
|
| 265 |
+
# 1. We have completed at least one iteration of the denoiser
|
| 266 |
+
# 2. The current timestep is within the range specified by the user. This is the optimal timestep range
|
| 267 |
+
# where approximating the unconditional branch from the computation of the conditional branch is possible
|
| 268 |
+
# without a significant loss in quality.
|
| 269 |
+
# 3. The current iteration is not a multiple of the unconditional batch skip range. This is done so that
|
| 270 |
+
# we compute the unconditional branch at least once every few iterations to ensure minimal quality loss.
|
| 271 |
+
is_within_timestep_range = (
|
| 272 |
+
self.unconditional_batch_timestep_skip_range[0]
|
| 273 |
+
< self.current_timestep_callback()
|
| 274 |
+
< self.unconditional_batch_timestep_skip_range[1]
|
| 275 |
+
)
|
| 276 |
+
should_skip_uncond = (
|
| 277 |
+
self.state.iteration > 0
|
| 278 |
+
and is_within_timestep_range
|
| 279 |
+
and self.state.iteration % self.unconditional_batch_skip_range != 0
|
| 280 |
+
and not self.is_guidance_distilled
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
if should_skip_uncond:
|
| 284 |
+
is_any_kwarg_uncond = any(k in self.uncond_cond_input_kwargs_identifiers for k in kwargs.keys())
|
| 285 |
+
if is_any_kwarg_uncond:
|
| 286 |
+
logger.debug("FasterCache - Skipping unconditional branch computation")
|
| 287 |
+
args = tuple([self._get_cond_input(arg) if torch.is_tensor(arg) else arg for arg in args])
|
| 288 |
+
kwargs = {
|
| 289 |
+
k: v if k not in self.uncond_cond_input_kwargs_identifiers else self._get_cond_input(v)
|
| 290 |
+
for k, v in kwargs.items()
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 294 |
+
|
| 295 |
+
if self.is_guidance_distilled:
|
| 296 |
+
self.state.iteration += 1
|
| 297 |
+
return output
|
| 298 |
+
|
| 299 |
+
if torch.is_tensor(output):
|
| 300 |
+
hidden_states = output
|
| 301 |
+
elif isinstance(output, (tuple, Transformer2DModelOutput)):
|
| 302 |
+
hidden_states = output[0]
|
| 303 |
+
|
| 304 |
+
batch_size = hidden_states.size(0)
|
| 305 |
+
|
| 306 |
+
if should_skip_uncond:
|
| 307 |
+
self.state.low_frequency_delta = self.state.low_frequency_delta * self.low_frequency_weight_callback(
|
| 308 |
+
module
|
| 309 |
+
)
|
| 310 |
+
self.state.high_frequency_delta = self.state.high_frequency_delta * self.high_frequency_weight_callback(
|
| 311 |
+
module
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
if self.tensor_format == "BCFHW":
|
| 315 |
+
hidden_states = hidden_states.permute(0, 2, 1, 3, 4)
|
| 316 |
+
if self.tensor_format == "BCFHW" or self.tensor_format == "BFCHW":
|
| 317 |
+
hidden_states = hidden_states.flatten(0, 1)
|
| 318 |
+
|
| 319 |
+
low_freq_cond, high_freq_cond = _split_low_high_freq(hidden_states.float())
|
| 320 |
+
|
| 321 |
+
# Approximate/compute the unconditional branch outputs as described in Equation 9 and 10 of the paper
|
| 322 |
+
low_freq_uncond = self.state.low_frequency_delta + low_freq_cond
|
| 323 |
+
high_freq_uncond = self.state.high_frequency_delta + high_freq_cond
|
| 324 |
+
uncond_freq = low_freq_uncond + high_freq_uncond
|
| 325 |
+
|
| 326 |
+
uncond_states = torch.fft.ifftshift(uncond_freq)
|
| 327 |
+
uncond_states = torch.fft.ifft2(uncond_states).real
|
| 328 |
+
|
| 329 |
+
if self.tensor_format == "BCFHW" or self.tensor_format == "BFCHW":
|
| 330 |
+
uncond_states = uncond_states.unflatten(0, (batch_size, -1))
|
| 331 |
+
hidden_states = hidden_states.unflatten(0, (batch_size, -1))
|
| 332 |
+
if self.tensor_format == "BCFHW":
|
| 333 |
+
uncond_states = uncond_states.permute(0, 2, 1, 3, 4)
|
| 334 |
+
hidden_states = hidden_states.permute(0, 2, 1, 3, 4)
|
| 335 |
+
|
| 336 |
+
# Concatenate the approximated unconditional and predicted conditional branches
|
| 337 |
+
uncond_states = uncond_states.to(hidden_states.dtype)
|
| 338 |
+
hidden_states = torch.cat([uncond_states, hidden_states], dim=0)
|
| 339 |
+
else:
|
| 340 |
+
uncond_states, cond_states = hidden_states.chunk(2, dim=0)
|
| 341 |
+
if self.tensor_format == "BCFHW":
|
| 342 |
+
uncond_states = uncond_states.permute(0, 2, 1, 3, 4)
|
| 343 |
+
cond_states = cond_states.permute(0, 2, 1, 3, 4)
|
| 344 |
+
if self.tensor_format == "BCFHW" or self.tensor_format == "BFCHW":
|
| 345 |
+
uncond_states = uncond_states.flatten(0, 1)
|
| 346 |
+
cond_states = cond_states.flatten(0, 1)
|
| 347 |
+
|
| 348 |
+
low_freq_uncond, high_freq_uncond = _split_low_high_freq(uncond_states.float())
|
| 349 |
+
low_freq_cond, high_freq_cond = _split_low_high_freq(cond_states.float())
|
| 350 |
+
self.state.low_frequency_delta = low_freq_uncond - low_freq_cond
|
| 351 |
+
self.state.high_frequency_delta = high_freq_uncond - high_freq_cond
|
| 352 |
+
|
| 353 |
+
self.state.iteration += 1
|
| 354 |
+
if torch.is_tensor(output):
|
| 355 |
+
output = hidden_states
|
| 356 |
+
elif isinstance(output, tuple):
|
| 357 |
+
output = (hidden_states, *output[1:])
|
| 358 |
+
else:
|
| 359 |
+
output.sample = hidden_states
|
| 360 |
+
|
| 361 |
+
return output
|
| 362 |
+
|
| 363 |
+
def reset_state(self, module: torch.nn.Module) -> torch.nn.Module:
|
| 364 |
+
self.state.reset()
|
| 365 |
+
return module
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class FasterCacheBlockHook(ModelHook):
|
| 369 |
+
_is_stateful = True
|
| 370 |
+
|
| 371 |
+
def __init__(
|
| 372 |
+
self,
|
| 373 |
+
block_skip_range: int,
|
| 374 |
+
timestep_skip_range: tuple[int, int],
|
| 375 |
+
is_guidance_distilled: bool,
|
| 376 |
+
weight_callback: Callable[[torch.nn.Module], float],
|
| 377 |
+
current_timestep_callback: Callable[[], int],
|
| 378 |
+
) -> None:
|
| 379 |
+
super().__init__()
|
| 380 |
+
|
| 381 |
+
self.block_skip_range = block_skip_range
|
| 382 |
+
self.timestep_skip_range = timestep_skip_range
|
| 383 |
+
self.is_guidance_distilled = is_guidance_distilled
|
| 384 |
+
|
| 385 |
+
self.weight_callback = weight_callback
|
| 386 |
+
self.current_timestep_callback = current_timestep_callback
|
| 387 |
+
|
| 388 |
+
def initialize_hook(self, module):
|
| 389 |
+
self.state = FasterCacheBlockState()
|
| 390 |
+
return module
|
| 391 |
+
|
| 392 |
+
def _compute_approximated_attention_output(
|
| 393 |
+
self, t_2_output: torch.Tensor, t_output: torch.Tensor, weight: float, batch_size: int
|
| 394 |
+
) -> torch.Tensor:
|
| 395 |
+
if t_2_output.size(0) != batch_size:
|
| 396 |
+
# The cache t_2_output contains both batchwise-concatenated unconditional-conditional branch outputs. Just
|
| 397 |
+
# take the conditional branch outputs.
|
| 398 |
+
assert t_2_output.size(0) == 2 * batch_size
|
| 399 |
+
t_2_output = t_2_output[batch_size:]
|
| 400 |
+
if t_output.size(0) != batch_size:
|
| 401 |
+
# The cache t_output contains both batchwise-concatenated unconditional-conditional branch outputs. Just
|
| 402 |
+
# take the conditional branch outputs.
|
| 403 |
+
assert t_output.size(0) == 2 * batch_size
|
| 404 |
+
t_output = t_output[batch_size:]
|
| 405 |
+
return t_output + (t_output - t_2_output) * weight
|
| 406 |
+
|
| 407 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs) -> Any:
|
| 408 |
+
batch_size = [
|
| 409 |
+
*[arg.size(0) for arg in args if torch.is_tensor(arg)],
|
| 410 |
+
*[v.size(0) for v in kwargs.values() if torch.is_tensor(v)],
|
| 411 |
+
][0]
|
| 412 |
+
if self.state.batch_size is None:
|
| 413 |
+
# Will be updated on first forward pass through the denoiser
|
| 414 |
+
self.state.batch_size = batch_size
|
| 415 |
+
|
| 416 |
+
# If we have to skip due to the skip conditions, then let's skip as expected.
|
| 417 |
+
# But, we can't skip if the denoiser wants to infer both unconditional and conditional branches. This
|
| 418 |
+
# is because the expected output shapes of attention layer will not match if we only return values from
|
| 419 |
+
# the cache (which only caches conditional branch outputs). So, if state.batch_size (which is the true
|
| 420 |
+
# unconditional-conditional batch size) is same as the current batch size, we don't perform the layer
|
| 421 |
+
# skip. Otherwise, we conditionally skip the layer based on what state.skip_callback returns.
|
| 422 |
+
is_within_timestep_range = (
|
| 423 |
+
self.timestep_skip_range[0] < self.current_timestep_callback() < self.timestep_skip_range[1]
|
| 424 |
+
)
|
| 425 |
+
if not is_within_timestep_range:
|
| 426 |
+
should_skip_attention = False
|
| 427 |
+
else:
|
| 428 |
+
should_compute_attention = self.state.iteration > 0 and self.state.iteration % self.block_skip_range == 0
|
| 429 |
+
should_skip_attention = not should_compute_attention
|
| 430 |
+
if should_skip_attention:
|
| 431 |
+
should_skip_attention = self.is_guidance_distilled or self.state.batch_size != batch_size
|
| 432 |
+
|
| 433 |
+
if should_skip_attention:
|
| 434 |
+
logger.debug("FasterCache - Skipping attention and using approximation")
|
| 435 |
+
if torch.is_tensor(self.state.cache[-1]):
|
| 436 |
+
t_2_output, t_output = self.state.cache
|
| 437 |
+
weight = self.weight_callback(module)
|
| 438 |
+
output = self._compute_approximated_attention_output(t_2_output, t_output, weight, batch_size)
|
| 439 |
+
else:
|
| 440 |
+
# The cache contains multiple tensors from past N iterations (N=2 for FasterCache). We need to handle all of them.
|
| 441 |
+
# Diffusers blocks can return multiple tensors - let's call them [A, B, C, ...] for simplicity.
|
| 442 |
+
# In our cache, we would have [[A_1, B_1, C_1, ...], [A_2, B_2, C_2, ...], ...] where each list is the output from
|
| 443 |
+
# a forward pass of the block. We need to compute the approximated output for each of these tensors.
|
| 444 |
+
# The zip(*state.cache) operation will give us [(A_1, A_2, ...), (B_1, B_2, ...), (C_1, C_2, ...), ...] which
|
| 445 |
+
# allows us to compute the approximated attention output for each tensor in the cache.
|
| 446 |
+
output = ()
|
| 447 |
+
for t_2_output, t_output in zip(*self.state.cache):
|
| 448 |
+
result = self._compute_approximated_attention_output(
|
| 449 |
+
t_2_output, t_output, self.weight_callback(module), batch_size
|
| 450 |
+
)
|
| 451 |
+
output += (result,)
|
| 452 |
+
else:
|
| 453 |
+
logger.debug("FasterCache - Computing attention")
|
| 454 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 455 |
+
|
| 456 |
+
# Note that the following condition for getting hidden_states should suffice since Diffusers blocks either return
|
| 457 |
+
# a single hidden_states tensor, or a tuple of (hidden_states, encoder_hidden_states) tensors. We need to handle
|
| 458 |
+
# both cases.
|
| 459 |
+
if torch.is_tensor(output):
|
| 460 |
+
cache_output = output
|
| 461 |
+
if not self.is_guidance_distilled and cache_output.size(0) == self.state.batch_size:
|
| 462 |
+
# The output here can be both unconditional-conditional branch outputs or just conditional branch outputs.
|
| 463 |
+
# This is determined at the higher-level denoiser module. We only want to cache the conditional branch outputs.
|
| 464 |
+
cache_output = cache_output.chunk(2, dim=0)[1]
|
| 465 |
+
else:
|
| 466 |
+
# Cache all return values and perform the same operation as above
|
| 467 |
+
cache_output = ()
|
| 468 |
+
for out in output:
|
| 469 |
+
if not self.is_guidance_distilled and out.size(0) == self.state.batch_size:
|
| 470 |
+
out = out.chunk(2, dim=0)[1]
|
| 471 |
+
cache_output += (out,)
|
| 472 |
+
|
| 473 |
+
if self.state.cache is None:
|
| 474 |
+
self.state.cache = [cache_output, cache_output]
|
| 475 |
+
else:
|
| 476 |
+
self.state.cache = [self.state.cache[-1], cache_output]
|
| 477 |
+
|
| 478 |
+
self.state.iteration += 1
|
| 479 |
+
return output
|
| 480 |
+
|
| 481 |
+
def reset_state(self, module: torch.nn.Module) -> torch.nn.Module:
|
| 482 |
+
self.state.reset()
|
| 483 |
+
return module
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def apply_faster_cache(module: torch.nn.Module, config: FasterCacheConfig) -> None:
|
| 487 |
+
r"""
|
| 488 |
+
Applies [FasterCache](https://huggingface.co/papers/2410.19355) to a given pipeline.
|
| 489 |
+
|
| 490 |
+
Args:
|
| 491 |
+
module (`torch.nn.Module`):
|
| 492 |
+
The pytorch module to apply FasterCache to. Typically, this should be a transformer architecture supported
|
| 493 |
+
in Diffusers, such as `CogVideoXTransformer3DModel`, but external implementations may also work.
|
| 494 |
+
config (`FasterCacheConfig`):
|
| 495 |
+
The configuration to use for FasterCache.
|
| 496 |
+
|
| 497 |
+
Example:
|
| 498 |
+
```python
|
| 499 |
+
>>> import torch
|
| 500 |
+
>>> from diffusers import CogVideoXPipeline, FasterCacheConfig, apply_faster_cache
|
| 501 |
+
|
| 502 |
+
>>> pipe = CogVideoXPipeline.from_pretrained("THUDM/CogVideoX-5b", torch_dtype=torch.bfloat16)
|
| 503 |
+
>>> pipe.to("cuda")
|
| 504 |
+
|
| 505 |
+
>>> config = FasterCacheConfig(
|
| 506 |
+
... spatial_attention_block_skip_range=2,
|
| 507 |
+
... spatial_attention_timestep_skip_range=(-1, 681),
|
| 508 |
+
... low_frequency_weight_update_timestep_range=(99, 641),
|
| 509 |
+
... high_frequency_weight_update_timestep_range=(-1, 301),
|
| 510 |
+
... spatial_attention_block_identifiers=["transformer_blocks"],
|
| 511 |
+
... attention_weight_callback=lambda _: 0.3,
|
| 512 |
+
... tensor_format="BFCHW",
|
| 513 |
+
... )
|
| 514 |
+
>>> apply_faster_cache(pipe.transformer, config)
|
| 515 |
+
```
|
| 516 |
+
"""
|
| 517 |
+
|
| 518 |
+
logger.warning(
|
| 519 |
+
"FasterCache is a purely experimental feature and may not work as expected. Not all models support FasterCache. "
|
| 520 |
+
"The API is subject to change in future releases, with no guarantee of backward compatibility. Please report any issues at "
|
| 521 |
+
"https://github.com/huggingface/diffusers/issues."
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
if config.attention_weight_callback is None:
|
| 525 |
+
# If the user has not provided a weight callback, we default to 0.5 for all timesteps.
|
| 526 |
+
# In the paper, they recommend using a gradually increasing weight from 0 to 1 as the inference progresses, but
|
| 527 |
+
# this depends from model-to-model. It is required by the user to provide a weight callback if they want to
|
| 528 |
+
# use a different weight function. Defaulting to 0.5 works well in practice for most cases.
|
| 529 |
+
logger.warning(
|
| 530 |
+
"No `attention_weight_callback` provided when enabling FasterCache. Defaulting to using a weight of 0.5 for all timesteps."
|
| 531 |
+
)
|
| 532 |
+
config.attention_weight_callback = lambda _: 0.5
|
| 533 |
+
|
| 534 |
+
if config.low_frequency_weight_callback is None:
|
| 535 |
+
logger.debug(
|
| 536 |
+
"Low frequency weight callback not provided when enabling FasterCache. Defaulting to behaviour described in the paper."
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
def low_frequency_weight_callback(module: torch.nn.Module) -> float:
|
| 540 |
+
is_within_range = (
|
| 541 |
+
config.low_frequency_weight_update_timestep_range[0]
|
| 542 |
+
< config.current_timestep_callback()
|
| 543 |
+
< config.low_frequency_weight_update_timestep_range[1]
|
| 544 |
+
)
|
| 545 |
+
return config.alpha_low_frequency if is_within_range else 1.0
|
| 546 |
+
|
| 547 |
+
config.low_frequency_weight_callback = low_frequency_weight_callback
|
| 548 |
+
|
| 549 |
+
if config.high_frequency_weight_callback is None:
|
| 550 |
+
logger.debug(
|
| 551 |
+
"High frequency weight callback not provided when enabling FasterCache. Defaulting to behaviour described in the paper."
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
def high_frequency_weight_callback(module: torch.nn.Module) -> float:
|
| 555 |
+
is_within_range = (
|
| 556 |
+
config.high_frequency_weight_update_timestep_range[0]
|
| 557 |
+
< config.current_timestep_callback()
|
| 558 |
+
< config.high_frequency_weight_update_timestep_range[1]
|
| 559 |
+
)
|
| 560 |
+
return config.alpha_high_frequency if is_within_range else 1.0
|
| 561 |
+
|
| 562 |
+
config.high_frequency_weight_callback = high_frequency_weight_callback
|
| 563 |
+
|
| 564 |
+
supported_tensor_formats = ["BCFHW", "BFCHW", "BCHW"] # TODO(aryan): Support BSC for LTX Video
|
| 565 |
+
if config.tensor_format not in supported_tensor_formats:
|
| 566 |
+
raise ValueError(f"`tensor_format` must be one of {supported_tensor_formats}, but got {config.tensor_format}.")
|
| 567 |
+
|
| 568 |
+
_apply_faster_cache_on_denoiser(module, config)
|
| 569 |
+
|
| 570 |
+
for name, submodule in module.named_modules():
|
| 571 |
+
if not isinstance(submodule, _ATTENTION_CLASSES):
|
| 572 |
+
continue
|
| 573 |
+
if any(re.search(identifier, name) is not None for identifier in _TRANSFORMER_BLOCK_IDENTIFIERS):
|
| 574 |
+
_apply_faster_cache_on_attention_class(name, submodule, config)
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def _apply_faster_cache_on_denoiser(module: torch.nn.Module, config: FasterCacheConfig) -> None:
|
| 578 |
+
hook = FasterCacheDenoiserHook(
|
| 579 |
+
config.unconditional_batch_skip_range,
|
| 580 |
+
config.unconditional_batch_timestep_skip_range,
|
| 581 |
+
config.tensor_format,
|
| 582 |
+
config.is_guidance_distilled,
|
| 583 |
+
config._unconditional_conditional_input_kwargs_identifiers,
|
| 584 |
+
config.current_timestep_callback,
|
| 585 |
+
config.low_frequency_weight_callback,
|
| 586 |
+
config.high_frequency_weight_callback,
|
| 587 |
+
)
|
| 588 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 589 |
+
registry.register_hook(hook, _FASTER_CACHE_DENOISER_HOOK)
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
def _apply_faster_cache_on_attention_class(name: str, module: AttentionModuleMixin, config: FasterCacheConfig) -> None:
|
| 593 |
+
is_spatial_self_attention = (
|
| 594 |
+
any(re.search(identifier, name) is not None for identifier in config.spatial_attention_block_identifiers)
|
| 595 |
+
and config.spatial_attention_block_skip_range is not None
|
| 596 |
+
and not getattr(module, "is_cross_attention", False)
|
| 597 |
+
)
|
| 598 |
+
is_temporal_self_attention = (
|
| 599 |
+
any(re.search(identifier, name) is not None for identifier in config.temporal_attention_block_identifiers)
|
| 600 |
+
and config.temporal_attention_block_skip_range is not None
|
| 601 |
+
and not module.is_cross_attention
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
block_skip_range, timestep_skip_range, block_type = None, None, None
|
| 605 |
+
if is_spatial_self_attention:
|
| 606 |
+
block_skip_range = config.spatial_attention_block_skip_range
|
| 607 |
+
timestep_skip_range = config.spatial_attention_timestep_skip_range
|
| 608 |
+
block_type = "spatial"
|
| 609 |
+
elif is_temporal_self_attention:
|
| 610 |
+
block_skip_range = config.temporal_attention_block_skip_range
|
| 611 |
+
timestep_skip_range = config.temporal_attention_timestep_skip_range
|
| 612 |
+
block_type = "temporal"
|
| 613 |
+
|
| 614 |
+
if block_skip_range is None or timestep_skip_range is None:
|
| 615 |
+
logger.debug(
|
| 616 |
+
f'Unable to apply FasterCache to the selected layer: "{name}" because it does '
|
| 617 |
+
f"not match any of the required criteria for spatial or temporal attention layers. Note, "
|
| 618 |
+
f"however, that this layer may still be valid for applying PAB. Please specify the correct "
|
| 619 |
+
f"block identifiers in the configuration or use the specialized `apply_faster_cache_on_module` "
|
| 620 |
+
f"function to apply FasterCache to this layer."
|
| 621 |
+
)
|
| 622 |
+
return
|
| 623 |
+
|
| 624 |
+
logger.debug(f"Enabling FasterCache ({block_type}) for layer: {name}")
|
| 625 |
+
hook = FasterCacheBlockHook(
|
| 626 |
+
block_skip_range,
|
| 627 |
+
timestep_skip_range,
|
| 628 |
+
config.is_guidance_distilled,
|
| 629 |
+
config.attention_weight_callback,
|
| 630 |
+
config.current_timestep_callback,
|
| 631 |
+
)
|
| 632 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 633 |
+
registry.register_hook(hook, _FASTER_CACHE_BLOCK_HOOK)
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
# Reference: https://github.com/Vchitect/FasterCache/blob/fab32c15014636dc854948319c0a9a8d92c7acb4/scripts/latte/faster_cache_sample_latte.py#L127C1-L143C39
|
| 637 |
+
@torch.no_grad()
|
| 638 |
+
def _split_low_high_freq(x):
|
| 639 |
+
fft = torch.fft.fft2(x)
|
| 640 |
+
fft_shifted = torch.fft.fftshift(fft)
|
| 641 |
+
height, width = x.shape[-2:]
|
| 642 |
+
radius = min(height, width) // 5
|
| 643 |
+
|
| 644 |
+
y_grid, x_grid = torch.meshgrid(torch.arange(height), torch.arange(width))
|
| 645 |
+
center_x, center_y = width // 2, height // 2
|
| 646 |
+
mask = (x_grid - center_x) ** 2 + (y_grid - center_y) ** 2 <= radius**2
|
| 647 |
+
|
| 648 |
+
low_freq_mask = mask.unsqueeze(0).unsqueeze(0).to(x.device)
|
| 649 |
+
high_freq_mask = ~low_freq_mask
|
| 650 |
+
|
| 651 |
+
low_freq_fft = fft_shifted * low_freq_mask
|
| 652 |
+
high_freq_fft = fft_shifted * high_freq_mask
|
| 653 |
+
|
| 654 |
+
return low_freq_fft, high_freq_fft
|
diffusers/hooks/first_block_cache.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from ..utils import get_logger
|
| 20 |
+
from ..utils.torch_utils import unwrap_module
|
| 21 |
+
from ._common import _ALL_TRANSFORMER_BLOCK_IDENTIFIERS
|
| 22 |
+
from ._helpers import TransformerBlockRegistry
|
| 23 |
+
from .hooks import BaseState, HookRegistry, ModelHook, StateManager
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 27 |
+
|
| 28 |
+
_FBC_LEADER_BLOCK_HOOK = "fbc_leader_block_hook"
|
| 29 |
+
_FBC_BLOCK_HOOK = "fbc_block_hook"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class FirstBlockCacheConfig:
|
| 34 |
+
r"""
|
| 35 |
+
Configuration for [First Block
|
| 36 |
+
Cache](https://github.com/chengzeyi/ParaAttention/blob/7a266123671b55e7e5a2fe9af3121f07a36afc78/README.md#first-block-cache-our-dynamic-caching).
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
threshold (`float`, defaults to `0.05`):
|
| 40 |
+
The threshold to determine whether or not a forward pass through all layers of the model is required. A
|
| 41 |
+
higher threshold usually results in a forward pass through a lower number of layers and faster inference,
|
| 42 |
+
but might lead to poorer generation quality. A lower threshold may not result in significant generation
|
| 43 |
+
speedup. The threshold is compared against the absmean difference of the residuals between the current and
|
| 44 |
+
cached outputs from the first transformer block. If the difference is below the threshold, the forward pass
|
| 45 |
+
is skipped.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
threshold: float = 0.05
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class FBCSharedBlockState(BaseState):
|
| 52 |
+
def __init__(self) -> None:
|
| 53 |
+
super().__init__()
|
| 54 |
+
|
| 55 |
+
self.head_block_output: torch.Tensor | tuple[torch.Tensor, ...] = None
|
| 56 |
+
self.head_block_residual: torch.Tensor = None
|
| 57 |
+
self.tail_block_residuals: torch.Tensor | tuple[torch.Tensor, ...] = None
|
| 58 |
+
self.should_compute: bool = True
|
| 59 |
+
|
| 60 |
+
def reset(self):
|
| 61 |
+
self.tail_block_residuals = None
|
| 62 |
+
self.should_compute = True
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class FBCHeadBlockHook(ModelHook):
|
| 66 |
+
_is_stateful = True
|
| 67 |
+
|
| 68 |
+
def __init__(self, state_manager: StateManager, threshold: float):
|
| 69 |
+
self.state_manager = state_manager
|
| 70 |
+
self.threshold = threshold
|
| 71 |
+
self._metadata = None
|
| 72 |
+
|
| 73 |
+
def initialize_hook(self, module):
|
| 74 |
+
unwrapped_module = unwrap_module(module)
|
| 75 |
+
self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
|
| 76 |
+
return module
|
| 77 |
+
|
| 78 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 79 |
+
original_hidden_states = self._metadata._get_parameter_from_args_kwargs("hidden_states", args, kwargs)
|
| 80 |
+
|
| 81 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 82 |
+
is_output_tuple = isinstance(output, tuple)
|
| 83 |
+
|
| 84 |
+
if is_output_tuple:
|
| 85 |
+
hidden_states_residual = output[self._metadata.return_hidden_states_index] - original_hidden_states
|
| 86 |
+
else:
|
| 87 |
+
hidden_states_residual = output - original_hidden_states
|
| 88 |
+
|
| 89 |
+
shared_state: FBCSharedBlockState = self.state_manager.get_state()
|
| 90 |
+
hidden_states = encoder_hidden_states = None
|
| 91 |
+
should_compute = self._should_compute_remaining_blocks(hidden_states_residual)
|
| 92 |
+
shared_state.should_compute = should_compute
|
| 93 |
+
|
| 94 |
+
if not should_compute:
|
| 95 |
+
# Apply caching
|
| 96 |
+
if is_output_tuple:
|
| 97 |
+
hidden_states = (
|
| 98 |
+
shared_state.tail_block_residuals[0] + output[self._metadata.return_hidden_states_index]
|
| 99 |
+
)
|
| 100 |
+
else:
|
| 101 |
+
hidden_states = shared_state.tail_block_residuals[0] + output
|
| 102 |
+
|
| 103 |
+
if self._metadata.return_encoder_hidden_states_index is not None:
|
| 104 |
+
assert is_output_tuple
|
| 105 |
+
encoder_hidden_states = (
|
| 106 |
+
shared_state.tail_block_residuals[1] + output[self._metadata.return_encoder_hidden_states_index]
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
if is_output_tuple:
|
| 110 |
+
return_output = [None] * len(output)
|
| 111 |
+
return_output[self._metadata.return_hidden_states_index] = hidden_states
|
| 112 |
+
return_output[self._metadata.return_encoder_hidden_states_index] = encoder_hidden_states
|
| 113 |
+
return_output = tuple(return_output)
|
| 114 |
+
else:
|
| 115 |
+
return_output = hidden_states
|
| 116 |
+
output = return_output
|
| 117 |
+
else:
|
| 118 |
+
if is_output_tuple:
|
| 119 |
+
head_block_output = [None] * len(output)
|
| 120 |
+
head_block_output[0] = output[self._metadata.return_hidden_states_index]
|
| 121 |
+
head_block_output[1] = output[self._metadata.return_encoder_hidden_states_index]
|
| 122 |
+
else:
|
| 123 |
+
head_block_output = output
|
| 124 |
+
shared_state.head_block_output = head_block_output
|
| 125 |
+
shared_state.head_block_residual = hidden_states_residual
|
| 126 |
+
|
| 127 |
+
return output
|
| 128 |
+
|
| 129 |
+
def reset_state(self, module):
|
| 130 |
+
self.state_manager.reset()
|
| 131 |
+
return module
|
| 132 |
+
|
| 133 |
+
@torch.compiler.disable
|
| 134 |
+
def _should_compute_remaining_blocks(self, hidden_states_residual: torch.Tensor) -> bool:
|
| 135 |
+
shared_state = self.state_manager.get_state()
|
| 136 |
+
if shared_state.head_block_residual is None:
|
| 137 |
+
return True
|
| 138 |
+
prev_hidden_states_residual = shared_state.head_block_residual
|
| 139 |
+
absmean = (hidden_states_residual - prev_hidden_states_residual).abs().mean()
|
| 140 |
+
prev_hidden_states_absmean = prev_hidden_states_residual.abs().mean()
|
| 141 |
+
diff = (absmean / prev_hidden_states_absmean).item()
|
| 142 |
+
return diff > self.threshold
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class FBCBlockHook(ModelHook):
|
| 146 |
+
def __init__(self, state_manager: StateManager, is_tail: bool = False):
|
| 147 |
+
super().__init__()
|
| 148 |
+
self.state_manager = state_manager
|
| 149 |
+
self.is_tail = is_tail
|
| 150 |
+
self._metadata = None
|
| 151 |
+
|
| 152 |
+
def initialize_hook(self, module):
|
| 153 |
+
unwrapped_module = unwrap_module(module)
|
| 154 |
+
self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
|
| 155 |
+
return module
|
| 156 |
+
|
| 157 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 158 |
+
original_hidden_states = self._metadata._get_parameter_from_args_kwargs("hidden_states", args, kwargs)
|
| 159 |
+
original_encoder_hidden_states = None
|
| 160 |
+
if self._metadata.return_encoder_hidden_states_index is not None:
|
| 161 |
+
original_encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
|
| 162 |
+
"encoder_hidden_states", args, kwargs
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
shared_state = self.state_manager.get_state()
|
| 166 |
+
|
| 167 |
+
if shared_state.should_compute:
|
| 168 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 169 |
+
if self.is_tail:
|
| 170 |
+
hidden_states_residual = encoder_hidden_states_residual = None
|
| 171 |
+
if isinstance(output, tuple):
|
| 172 |
+
hidden_states_residual = (
|
| 173 |
+
output[self._metadata.return_hidden_states_index] - shared_state.head_block_output[0]
|
| 174 |
+
)
|
| 175 |
+
encoder_hidden_states_residual = (
|
| 176 |
+
output[self._metadata.return_encoder_hidden_states_index] - shared_state.head_block_output[1]
|
| 177 |
+
)
|
| 178 |
+
else:
|
| 179 |
+
hidden_states_residual = output - shared_state.head_block_output
|
| 180 |
+
shared_state.tail_block_residuals = (hidden_states_residual, encoder_hidden_states_residual)
|
| 181 |
+
return output
|
| 182 |
+
|
| 183 |
+
if original_encoder_hidden_states is None:
|
| 184 |
+
return_output = original_hidden_states
|
| 185 |
+
else:
|
| 186 |
+
return_output = [None, None]
|
| 187 |
+
return_output[self._metadata.return_hidden_states_index] = original_hidden_states
|
| 188 |
+
return_output[self._metadata.return_encoder_hidden_states_index] = original_encoder_hidden_states
|
| 189 |
+
return_output = tuple(return_output)
|
| 190 |
+
return return_output
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def apply_first_block_cache(module: torch.nn.Module, config: FirstBlockCacheConfig) -> None:
|
| 194 |
+
"""
|
| 195 |
+
Applies [First Block
|
| 196 |
+
Cache](https://github.com/chengzeyi/ParaAttention/blob/4de137c5b96416489f06e43e19f2c14a772e28fd/README.md#first-block-cache-our-dynamic-caching)
|
| 197 |
+
to a given module.
|
| 198 |
+
|
| 199 |
+
First Block Cache builds on the ideas of [TeaCache](https://huggingface.co/papers/2411.19108). It is much simpler
|
| 200 |
+
to implement generically for a wide range of models and has been integrated first for experimental purposes.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
module (`torch.nn.Module`):
|
| 204 |
+
The pytorch module to apply FBCache to. Typically, this should be a transformer architecture supported in
|
| 205 |
+
Diffusers, such as `CogVideoXTransformer3DModel`, but external implementations may also work.
|
| 206 |
+
config (`FirstBlockCacheConfig`):
|
| 207 |
+
The configuration to use for applying the FBCache method.
|
| 208 |
+
|
| 209 |
+
Example:
|
| 210 |
+
```python
|
| 211 |
+
>>> import torch
|
| 212 |
+
>>> from diffusers import CogView4Pipeline
|
| 213 |
+
>>> from diffusers.hooks import apply_first_block_cache, FirstBlockCacheConfig
|
| 214 |
+
|
| 215 |
+
>>> pipe = CogView4Pipeline.from_pretrained("THUDM/CogView4-6B", torch_dtype=torch.bfloat16)
|
| 216 |
+
>>> pipe.to("cuda")
|
| 217 |
+
|
| 218 |
+
>>> apply_first_block_cache(pipe.transformer, FirstBlockCacheConfig(threshold=0.2))
|
| 219 |
+
|
| 220 |
+
>>> prompt = "A photo of an astronaut riding a horse on mars"
|
| 221 |
+
>>> image = pipe(prompt, generator=torch.Generator().manual_seed(42)).images[0]
|
| 222 |
+
>>> image.save("output.png")
|
| 223 |
+
```
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
state_manager = StateManager(FBCSharedBlockState, (), {})
|
| 227 |
+
remaining_blocks = []
|
| 228 |
+
|
| 229 |
+
for name, submodule in module.named_children():
|
| 230 |
+
if name not in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS or not isinstance(submodule, torch.nn.ModuleList):
|
| 231 |
+
continue
|
| 232 |
+
for index, block in enumerate(submodule):
|
| 233 |
+
remaining_blocks.append((f"{name}.{index}", block))
|
| 234 |
+
|
| 235 |
+
head_block_name, head_block = remaining_blocks.pop(0)
|
| 236 |
+
tail_block_name, tail_block = remaining_blocks.pop(-1)
|
| 237 |
+
|
| 238 |
+
logger.debug(f"Applying FBCHeadBlockHook to '{head_block_name}'")
|
| 239 |
+
_apply_fbc_head_block_hook(head_block, state_manager, config.threshold)
|
| 240 |
+
|
| 241 |
+
for name, block in remaining_blocks:
|
| 242 |
+
logger.debug(f"Applying FBCBlockHook to '{name}'")
|
| 243 |
+
_apply_fbc_block_hook(block, state_manager)
|
| 244 |
+
|
| 245 |
+
logger.debug(f"Applying FBCBlockHook to tail block '{tail_block_name}'")
|
| 246 |
+
_apply_fbc_block_hook(tail_block, state_manager, is_tail=True)
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def _apply_fbc_head_block_hook(block: torch.nn.Module, state_manager: StateManager, threshold: float) -> None:
|
| 250 |
+
registry = HookRegistry.check_if_exists_or_initialize(block)
|
| 251 |
+
hook = FBCHeadBlockHook(state_manager, threshold)
|
| 252 |
+
registry.register_hook(hook, _FBC_LEADER_BLOCK_HOOK)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def _apply_fbc_block_hook(block: torch.nn.Module, state_manager: StateManager, is_tail: bool = False) -> None:
|
| 256 |
+
registry = HookRegistry.check_if_exists_or_initialize(block)
|
| 257 |
+
hook = FBCBlockHook(state_manager, is_tail)
|
| 258 |
+
registry.register_hook(hook, _FBC_BLOCK_HOOK)
|
diffusers/hooks/group_offloading.py
ADDED
|
@@ -0,0 +1,1056 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import hashlib
|
| 16 |
+
import os
|
| 17 |
+
from contextlib import contextmanager, nullcontext
|
| 18 |
+
from dataclasses import dataclass, replace
|
| 19 |
+
from enum import Enum
|
| 20 |
+
from typing import Set
|
| 21 |
+
|
| 22 |
+
import safetensors.torch
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
from ..utils import get_logger, is_accelerate_available, is_torchao_available
|
| 26 |
+
from ._common import _GO_LC_SUPPORTED_PYTORCH_LAYERS
|
| 27 |
+
from .hooks import HookRegistry, ModelHook
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
if is_accelerate_available():
|
| 31 |
+
from accelerate.hooks import AlignDevicesHook, CpuOffload
|
| 32 |
+
from accelerate.utils import send_to_device
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _is_torchao_tensor(tensor: torch.Tensor) -> bool:
|
| 39 |
+
if not is_torchao_available():
|
| 40 |
+
return False
|
| 41 |
+
from torchao.utils import TorchAOBaseTensor
|
| 42 |
+
|
| 43 |
+
return isinstance(tensor, TorchAOBaseTensor)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _get_torchao_inner_tensor_names(tensor: torch.Tensor) -> list[str]:
|
| 47 |
+
"""Get names of all internal tensor data attributes from a TorchAO tensor."""
|
| 48 |
+
cls = type(tensor)
|
| 49 |
+
names = list(getattr(cls, "tensor_data_names", []))
|
| 50 |
+
for attr_name in getattr(cls, "optional_tensor_data_names", []):
|
| 51 |
+
if getattr(tensor, attr_name, None) is not None:
|
| 52 |
+
names.append(attr_name)
|
| 53 |
+
return names
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _swap_torchao_tensor(param: torch.Tensor, source: torch.Tensor) -> None:
|
| 57 |
+
"""Move a TorchAO parameter to the device of `source` via `swap_tensors`.
|
| 58 |
+
|
| 59 |
+
`param.data = source` does not work for `_make_wrapper_subclass` tensors because the `.data` setter only replaces
|
| 60 |
+
the outer wrapper storage while leaving the subclass's internal attributes (e.g. `.qdata`, `.scale`) on the
|
| 61 |
+
original device. `swap_tensors` swaps the full tensor contents in-place, preserving the parameter's identity so
|
| 62 |
+
that any dict keyed by `id(param)` remains valid.
|
| 63 |
+
|
| 64 |
+
Refer to https://github.com/huggingface/diffusers/pull/13276#discussion_r2944471548 for the full discussion.
|
| 65 |
+
"""
|
| 66 |
+
torch.utils.swap_tensors(param, source)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _restore_torchao_tensor(param: torch.Tensor, source: torch.Tensor) -> None:
|
| 70 |
+
"""Restore internal tensor data of a TorchAO parameter from `source` without mutating `source`.
|
| 71 |
+
|
| 72 |
+
Unlike `_swap_torchao_tensor` this copies attribute references one-by-one via `setattr` so that `source` is **not**
|
| 73 |
+
modified. Use this when `source` is a cached tensor that must remain unchanged (e.g. a pinned CPU copy in
|
| 74 |
+
`cpu_param_dict`).
|
| 75 |
+
"""
|
| 76 |
+
for attr_name in _get_torchao_inner_tensor_names(source):
|
| 77 |
+
setattr(param, attr_name, getattr(source, attr_name))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _record_stream_torchao_tensor(param: torch.Tensor, stream) -> None:
|
| 81 |
+
"""Record stream for all internal tensors of a TorchAO parameter."""
|
| 82 |
+
for attr_name in _get_torchao_inner_tensor_names(param):
|
| 83 |
+
getattr(param, attr_name).record_stream(stream)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# fmt: off
|
| 87 |
+
_GROUP_OFFLOADING = "group_offloading"
|
| 88 |
+
_LAYER_EXECUTION_TRACKER = "layer_execution_tracker"
|
| 89 |
+
_LAZY_PREFETCH_GROUP_OFFLOADING = "lazy_prefetch_group_offloading"
|
| 90 |
+
_GROUP_ID_LAZY_LEAF = "lazy_leafs"
|
| 91 |
+
# fmt: on
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class GroupOffloadingType(str, Enum):
|
| 95 |
+
BLOCK_LEVEL = "block_level"
|
| 96 |
+
LEAF_LEVEL = "leaf_level"
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@dataclass
|
| 100 |
+
class GroupOffloadingConfig:
|
| 101 |
+
onload_device: torch.device
|
| 102 |
+
offload_device: torch.device
|
| 103 |
+
offload_type: GroupOffloadingType
|
| 104 |
+
non_blocking: bool
|
| 105 |
+
record_stream: bool
|
| 106 |
+
low_cpu_mem_usage: bool
|
| 107 |
+
num_blocks_per_group: int | None = None
|
| 108 |
+
offload_to_disk_path: str | None = None
|
| 109 |
+
stream: torch.cuda.Stream | torch.Stream | None = None
|
| 110 |
+
block_modules: list[str] | None = None
|
| 111 |
+
exclude_kwargs: list[str] | None = None
|
| 112 |
+
module_prefix: str = ""
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class ModuleGroup:
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
modules: list[torch.nn.Module],
|
| 119 |
+
offload_device: torch.device,
|
| 120 |
+
onload_device: torch.device,
|
| 121 |
+
offload_leader: torch.nn.Module,
|
| 122 |
+
onload_leader: torch.nn.Module | None = None,
|
| 123 |
+
parameters: list[torch.nn.Parameter] | None = None,
|
| 124 |
+
buffers: list[torch.Tensor] | None = None,
|
| 125 |
+
non_blocking: bool = False,
|
| 126 |
+
stream: torch.cuda.Stream | torch.Stream | None = None,
|
| 127 |
+
record_stream: bool | None = False,
|
| 128 |
+
low_cpu_mem_usage: bool = False,
|
| 129 |
+
onload_self: bool = True,
|
| 130 |
+
offload_to_disk_path: str | None = None,
|
| 131 |
+
group_id: int | str | None = None,
|
| 132 |
+
) -> None:
|
| 133 |
+
self.modules = modules
|
| 134 |
+
self.offload_device = offload_device
|
| 135 |
+
self.onload_device = onload_device
|
| 136 |
+
self.offload_leader = offload_leader
|
| 137 |
+
self.onload_leader = onload_leader
|
| 138 |
+
self.parameters = parameters or []
|
| 139 |
+
self.buffers = buffers or []
|
| 140 |
+
self.non_blocking = non_blocking or stream is not None
|
| 141 |
+
self.stream = stream
|
| 142 |
+
self.record_stream = record_stream
|
| 143 |
+
self.onload_self = onload_self
|
| 144 |
+
self.low_cpu_mem_usage = low_cpu_mem_usage
|
| 145 |
+
|
| 146 |
+
self.offload_to_disk_path = offload_to_disk_path
|
| 147 |
+
self._is_offloaded_to_disk = False
|
| 148 |
+
|
| 149 |
+
if self.offload_to_disk_path is not None:
|
| 150 |
+
# Instead of `group_id or str(id(self))` we do this because `group_id` can be "" as well.
|
| 151 |
+
self.group_id = group_id if group_id is not None else str(id(self))
|
| 152 |
+
short_hash = _compute_group_hash(self.group_id)
|
| 153 |
+
self.safetensors_file_path = os.path.join(self.offload_to_disk_path, f"group_{short_hash}.safetensors")
|
| 154 |
+
|
| 155 |
+
all_tensors = []
|
| 156 |
+
for module in self.modules:
|
| 157 |
+
all_tensors.extend(list(module.parameters()))
|
| 158 |
+
all_tensors.extend(list(module.buffers()))
|
| 159 |
+
all_tensors.extend(self.parameters)
|
| 160 |
+
all_tensors.extend(self.buffers)
|
| 161 |
+
all_tensors = list(dict.fromkeys(all_tensors)) # Remove duplicates
|
| 162 |
+
|
| 163 |
+
self.tensor_to_key = {tensor: f"tensor_{i}" for i, tensor in enumerate(all_tensors)}
|
| 164 |
+
self.key_to_tensor = {v: k for k, v in self.tensor_to_key.items()}
|
| 165 |
+
self.cpu_param_dict = {}
|
| 166 |
+
else:
|
| 167 |
+
self.cpu_param_dict = self._init_cpu_param_dict()
|
| 168 |
+
|
| 169 |
+
self._torch_accelerator_module = (
|
| 170 |
+
getattr(torch, torch.accelerator.current_accelerator().type)
|
| 171 |
+
if hasattr(torch, "accelerator")
|
| 172 |
+
else torch.cuda
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
@staticmethod
|
| 176 |
+
def _to_cpu(tensor, low_cpu_mem_usage):
|
| 177 |
+
# For TorchAO tensors, `.data` returns an incomplete wrapper without internal attributes
|
| 178 |
+
# (e.g. `.qdata`, `.scale`), so we must call `.cpu()` on the tensor directly.
|
| 179 |
+
t = tensor.cpu() if _is_torchao_tensor(tensor) else tensor.data.cpu()
|
| 180 |
+
return t if low_cpu_mem_usage else t.pin_memory()
|
| 181 |
+
|
| 182 |
+
def _init_cpu_param_dict(self):
|
| 183 |
+
cpu_param_dict = {}
|
| 184 |
+
if self.stream is None:
|
| 185 |
+
return cpu_param_dict
|
| 186 |
+
|
| 187 |
+
for module in self.modules:
|
| 188 |
+
for param in module.parameters():
|
| 189 |
+
cpu_param_dict[param] = self._to_cpu(param, self.low_cpu_mem_usage)
|
| 190 |
+
for buffer in module.buffers():
|
| 191 |
+
cpu_param_dict[buffer] = self._to_cpu(buffer, self.low_cpu_mem_usage)
|
| 192 |
+
|
| 193 |
+
for param in self.parameters:
|
| 194 |
+
cpu_param_dict[param] = self._to_cpu(param, self.low_cpu_mem_usage)
|
| 195 |
+
|
| 196 |
+
for buffer in self.buffers:
|
| 197 |
+
cpu_param_dict[buffer] = self._to_cpu(buffer, self.low_cpu_mem_usage)
|
| 198 |
+
|
| 199 |
+
return cpu_param_dict
|
| 200 |
+
|
| 201 |
+
@contextmanager
|
| 202 |
+
def _pinned_memory_tensors(self):
|
| 203 |
+
try:
|
| 204 |
+
pinned_dict = {
|
| 205 |
+
param: tensor.pin_memory() if not tensor.is_pinned() else tensor
|
| 206 |
+
for param, tensor in self.cpu_param_dict.items()
|
| 207 |
+
}
|
| 208 |
+
yield pinned_dict
|
| 209 |
+
finally:
|
| 210 |
+
pinned_dict = None
|
| 211 |
+
|
| 212 |
+
def _transfer_tensor_to_device(self, tensor, source_tensor, default_stream):
|
| 213 |
+
moved = source_tensor.to(self.onload_device, non_blocking=self.non_blocking)
|
| 214 |
+
if _is_torchao_tensor(tensor):
|
| 215 |
+
_swap_torchao_tensor(tensor, moved)
|
| 216 |
+
else:
|
| 217 |
+
tensor.data = moved
|
| 218 |
+
if self.record_stream:
|
| 219 |
+
if _is_torchao_tensor(tensor):
|
| 220 |
+
_record_stream_torchao_tensor(tensor, default_stream)
|
| 221 |
+
else:
|
| 222 |
+
tensor.data.record_stream(default_stream)
|
| 223 |
+
|
| 224 |
+
def _process_tensors_from_modules(self, pinned_memory=None, default_stream=None):
|
| 225 |
+
for group_module in self.modules:
|
| 226 |
+
for param in group_module.parameters():
|
| 227 |
+
source = pinned_memory[param] if pinned_memory else param.data
|
| 228 |
+
self._transfer_tensor_to_device(param, source, default_stream)
|
| 229 |
+
for buffer in group_module.buffers():
|
| 230 |
+
source = pinned_memory[buffer] if pinned_memory else buffer.data
|
| 231 |
+
self._transfer_tensor_to_device(buffer, source, default_stream)
|
| 232 |
+
|
| 233 |
+
for param in self.parameters:
|
| 234 |
+
source = pinned_memory[param] if pinned_memory else param.data
|
| 235 |
+
self._transfer_tensor_to_device(param, source, default_stream)
|
| 236 |
+
|
| 237 |
+
for buffer in self.buffers:
|
| 238 |
+
source = pinned_memory[buffer] if pinned_memory else buffer.data
|
| 239 |
+
self._transfer_tensor_to_device(buffer, source, default_stream)
|
| 240 |
+
|
| 241 |
+
def _check_disk_offload_torchao(self):
|
| 242 |
+
all_tensors = list(self.tensor_to_key.keys())
|
| 243 |
+
has_torchao = any(_is_torchao_tensor(t) for t in all_tensors)
|
| 244 |
+
if has_torchao:
|
| 245 |
+
raise ValueError(
|
| 246 |
+
"Disk offloading is not supported for TorchAO quantized tensors because safetensors "
|
| 247 |
+
"cannot serialize TorchAO subclass tensors. Use memory offloading instead by not "
|
| 248 |
+
"setting `offload_to_disk_path`."
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
def _onload_from_disk(self):
|
| 252 |
+
self._check_disk_offload_torchao()
|
| 253 |
+
|
| 254 |
+
if self.stream is not None:
|
| 255 |
+
# Wait for previous Host->Device transfer to complete
|
| 256 |
+
self.stream.synchronize()
|
| 257 |
+
|
| 258 |
+
context = nullcontext() if self.stream is None else self._torch_accelerator_module.stream(self.stream)
|
| 259 |
+
current_stream = self._torch_accelerator_module.current_stream() if self.record_stream else None
|
| 260 |
+
|
| 261 |
+
with context:
|
| 262 |
+
if self.stream is not None:
|
| 263 |
+
# Load to CPU first, pin memory, then async copy to the target device
|
| 264 |
+
loaded_tensors = safetensors.torch.load_file(self.safetensors_file_path, device="cpu")
|
| 265 |
+
for key, tensor_obj in self.key_to_tensor.items():
|
| 266 |
+
pinned_tensor = loaded_tensors[key].pin_memory()
|
| 267 |
+
tensor_obj.data = pinned_tensor.to(self.onload_device, non_blocking=self.non_blocking)
|
| 268 |
+
if self.record_stream:
|
| 269 |
+
tensor_obj.data.record_stream(current_stream)
|
| 270 |
+
else:
|
| 271 |
+
# Load directly to the target device
|
| 272 |
+
onload_device = (
|
| 273 |
+
self.onload_device.type if isinstance(self.onload_device, torch.device) else self.onload_device
|
| 274 |
+
)
|
| 275 |
+
loaded_tensors = safetensors.torch.load_file(self.safetensors_file_path, device=onload_device)
|
| 276 |
+
for key, tensor_obj in self.key_to_tensor.items():
|
| 277 |
+
tensor_obj.data = loaded_tensors[key]
|
| 278 |
+
|
| 279 |
+
def _onload_from_memory(self):
|
| 280 |
+
if self.stream is not None:
|
| 281 |
+
# Wait for previous Host->Device transfer to complete
|
| 282 |
+
self.stream.synchronize()
|
| 283 |
+
|
| 284 |
+
context = nullcontext() if self.stream is None else self._torch_accelerator_module.stream(self.stream)
|
| 285 |
+
default_stream = self._torch_accelerator_module.current_stream() if self.stream is not None else None
|
| 286 |
+
|
| 287 |
+
with context:
|
| 288 |
+
if self.stream is not None:
|
| 289 |
+
with self._pinned_memory_tensors() as pinned_memory:
|
| 290 |
+
self._process_tensors_from_modules(pinned_memory, default_stream=default_stream)
|
| 291 |
+
else:
|
| 292 |
+
self._process_tensors_from_modules(None)
|
| 293 |
+
|
| 294 |
+
def _offload_to_disk(self):
|
| 295 |
+
self._check_disk_offload_torchao()
|
| 296 |
+
|
| 297 |
+
# TODO: we can potentially optimize this code path by checking if the _all_ the desired
|
| 298 |
+
# safetensor files exist on the disk and if so, skip this step entirely, reducing IO
|
| 299 |
+
# overhead. Currently, we just check if the given `safetensors_file_path` exists and if not
|
| 300 |
+
# we perform a write.
|
| 301 |
+
# Check if the file has been saved in this session or if it already exists on disk.
|
| 302 |
+
if not self._is_offloaded_to_disk and not os.path.exists(self.safetensors_file_path):
|
| 303 |
+
os.makedirs(os.path.dirname(self.safetensors_file_path), exist_ok=True)
|
| 304 |
+
tensors_to_save = {key: tensor.data.to(self.offload_device) for tensor, key in self.tensor_to_key.items()}
|
| 305 |
+
safetensors.torch.save_file(tensors_to_save, self.safetensors_file_path)
|
| 306 |
+
|
| 307 |
+
# The group is now considered offloaded to disk for the rest of the session.
|
| 308 |
+
self._is_offloaded_to_disk = True
|
| 309 |
+
|
| 310 |
+
# We do this to free up the RAM which is still holding the up tensor data.
|
| 311 |
+
for tensor_obj in self.tensor_to_key.keys():
|
| 312 |
+
tensor_obj.data = torch.empty_like(tensor_obj.data, device=self.offload_device)
|
| 313 |
+
|
| 314 |
+
def _offload_to_memory(self):
|
| 315 |
+
if self.stream is not None:
|
| 316 |
+
if not self.record_stream:
|
| 317 |
+
self._torch_accelerator_module.current_stream().synchronize()
|
| 318 |
+
|
| 319 |
+
for group_module in self.modules:
|
| 320 |
+
for param in group_module.parameters():
|
| 321 |
+
if _is_torchao_tensor(param):
|
| 322 |
+
_restore_torchao_tensor(param, self.cpu_param_dict[param])
|
| 323 |
+
else:
|
| 324 |
+
param.data = self.cpu_param_dict[param]
|
| 325 |
+
for param in self.parameters:
|
| 326 |
+
if _is_torchao_tensor(param):
|
| 327 |
+
_restore_torchao_tensor(param, self.cpu_param_dict[param])
|
| 328 |
+
else:
|
| 329 |
+
param.data = self.cpu_param_dict[param]
|
| 330 |
+
for buffer in self.buffers:
|
| 331 |
+
if _is_torchao_tensor(buffer):
|
| 332 |
+
_restore_torchao_tensor(buffer, self.cpu_param_dict[buffer])
|
| 333 |
+
else:
|
| 334 |
+
buffer.data = self.cpu_param_dict[buffer]
|
| 335 |
+
else:
|
| 336 |
+
for group_module in self.modules:
|
| 337 |
+
group_module.to(self.offload_device, non_blocking=False)
|
| 338 |
+
for param in self.parameters:
|
| 339 |
+
if _is_torchao_tensor(param):
|
| 340 |
+
moved = param.to(self.offload_device, non_blocking=False)
|
| 341 |
+
_swap_torchao_tensor(param, moved)
|
| 342 |
+
else:
|
| 343 |
+
param.data = param.data.to(self.offload_device, non_blocking=False)
|
| 344 |
+
for buffer in self.buffers:
|
| 345 |
+
if _is_torchao_tensor(buffer):
|
| 346 |
+
moved = buffer.to(self.offload_device, non_blocking=False)
|
| 347 |
+
_swap_torchao_tensor(buffer, moved)
|
| 348 |
+
else:
|
| 349 |
+
buffer.data = buffer.data.to(self.offload_device, non_blocking=False)
|
| 350 |
+
|
| 351 |
+
@torch.compiler.disable()
|
| 352 |
+
def onload_(self):
|
| 353 |
+
r"""Onloads the group of parameters to the onload_device."""
|
| 354 |
+
if self.offload_to_disk_path is not None:
|
| 355 |
+
self._onload_from_disk()
|
| 356 |
+
else:
|
| 357 |
+
self._onload_from_memory()
|
| 358 |
+
|
| 359 |
+
@torch.compiler.disable()
|
| 360 |
+
def offload_(self):
|
| 361 |
+
r"""Offloads the group of parameters to the offload_device."""
|
| 362 |
+
if self.offload_to_disk_path:
|
| 363 |
+
self._offload_to_disk()
|
| 364 |
+
else:
|
| 365 |
+
self._offload_to_memory()
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class GroupOffloadingHook(ModelHook):
|
| 369 |
+
r"""
|
| 370 |
+
A hook that offloads groups of torch.nn.Module to the CPU for storage and onloads to accelerator device for
|
| 371 |
+
computation. Each group has one "onload leader" module that is responsible for onloading, and an "offload leader"
|
| 372 |
+
module that is responsible for offloading. If prefetching is enabled, the onload leader of the previous module
|
| 373 |
+
group is responsible for onloading the current module group.
|
| 374 |
+
"""
|
| 375 |
+
|
| 376 |
+
_is_stateful = False
|
| 377 |
+
|
| 378 |
+
def __init__(self, group: ModuleGroup, *, config: GroupOffloadingConfig) -> None:
|
| 379 |
+
self.group = group
|
| 380 |
+
self.next_group: ModuleGroup | None = None
|
| 381 |
+
self.config = config
|
| 382 |
+
|
| 383 |
+
def initialize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
|
| 384 |
+
if self.group.offload_leader == module:
|
| 385 |
+
self.group.offload_()
|
| 386 |
+
return module
|
| 387 |
+
|
| 388 |
+
def pre_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 389 |
+
# If there wasn't an onload_leader assigned, we assume that the submodule that first called its forward
|
| 390 |
+
# method is the onload_leader of the group.
|
| 391 |
+
if self.group.onload_leader is None:
|
| 392 |
+
self.group.onload_leader = module
|
| 393 |
+
|
| 394 |
+
# If the current module is the onload_leader of the group, we onload the group if it is supposed
|
| 395 |
+
# to onload itself. In the case of using prefetching with streams, we onload the next group if
|
| 396 |
+
# it is not supposed to onload itself.
|
| 397 |
+
if self.group.onload_leader == module:
|
| 398 |
+
if self.group.onload_self:
|
| 399 |
+
self.group.onload_()
|
| 400 |
+
else:
|
| 401 |
+
# onload_self=False means this group relies on prefetching from a previous group.
|
| 402 |
+
# However, for conditionally-executed modules (e.g. patch_short/patch_mid/patch_long in Helios),
|
| 403 |
+
# the prefetch chain may not cover them if they were absent during the first forward pass
|
| 404 |
+
# when the execution order was traced. In that case, their weights remain on offload_device,
|
| 405 |
+
# so we fall back to a synchronous onload here.
|
| 406 |
+
params = [p for m in self.group.modules for p in m.parameters()] + list(self.group.parameters)
|
| 407 |
+
if params and params[0].device == self.group.offload_device:
|
| 408 |
+
self.group.onload_()
|
| 409 |
+
if self.group.stream is not None:
|
| 410 |
+
self.group.stream.synchronize()
|
| 411 |
+
|
| 412 |
+
should_onload_next_group = self.next_group is not None and not self.next_group.onload_self
|
| 413 |
+
if should_onload_next_group:
|
| 414 |
+
self.next_group.onload_()
|
| 415 |
+
|
| 416 |
+
should_synchronize = (
|
| 417 |
+
not self.group.onload_self and self.group.stream is not None and not should_onload_next_group
|
| 418 |
+
)
|
| 419 |
+
if should_synchronize:
|
| 420 |
+
# If this group didn't onload itself, it means it was asynchronously onloaded by the
|
| 421 |
+
# previous group. We need to synchronize the side stream to ensure parameters
|
| 422 |
+
# are completely loaded to proceed with forward pass. Without this, uninitialized
|
| 423 |
+
# weights will be used in the computation, leading to incorrect results
|
| 424 |
+
# Also, we should only do this synchronization if we don't already do it from the sync call in
|
| 425 |
+
# self.next_group.onload_, hence the `not should_onload_next_group` check.
|
| 426 |
+
self.group.stream.synchronize()
|
| 427 |
+
|
| 428 |
+
args = send_to_device(args, self.group.onload_device, non_blocking=self.group.non_blocking)
|
| 429 |
+
|
| 430 |
+
# Some Autoencoder models use a feature cache that is passed through submodules
|
| 431 |
+
# and modified in place. The `send_to_device` call returns a copy of this feature cache object
|
| 432 |
+
# which breaks the inplace updates. Use `exclude_kwargs` to mark these cache features
|
| 433 |
+
exclude_kwargs = self.config.exclude_kwargs or []
|
| 434 |
+
if exclude_kwargs:
|
| 435 |
+
moved_kwargs = send_to_device(
|
| 436 |
+
{k: v for k, v in kwargs.items() if k not in exclude_kwargs},
|
| 437 |
+
self.group.onload_device,
|
| 438 |
+
non_blocking=self.group.non_blocking,
|
| 439 |
+
)
|
| 440 |
+
kwargs.update(moved_kwargs)
|
| 441 |
+
else:
|
| 442 |
+
kwargs = send_to_device(kwargs, self.group.onload_device, non_blocking=self.group.non_blocking)
|
| 443 |
+
|
| 444 |
+
return args, kwargs
|
| 445 |
+
|
| 446 |
+
def post_forward(self, module: torch.nn.Module, output):
|
| 447 |
+
if self.group.offload_leader == module:
|
| 448 |
+
self.group.offload_()
|
| 449 |
+
return output
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
class LazyPrefetchGroupOffloadingHook(ModelHook):
|
| 453 |
+
r"""
|
| 454 |
+
A hook, used in conjunction with GroupOffloadingHook, that applies lazy prefetching to groups of torch.nn.Module.
|
| 455 |
+
This hook is used to determine the order in which the layers are executed during the forward pass. Once the layer
|
| 456 |
+
invocation order is known, assignments of the next_group attribute for prefetching can be made, which allows
|
| 457 |
+
prefetching groups in the correct order.
|
| 458 |
+
"""
|
| 459 |
+
|
| 460 |
+
_is_stateful = False
|
| 461 |
+
|
| 462 |
+
def __init__(self):
|
| 463 |
+
self.execution_order: list[tuple[str, torch.nn.Module]] = []
|
| 464 |
+
self._layer_execution_tracker_module_names = set()
|
| 465 |
+
|
| 466 |
+
def initialize_hook(self, module):
|
| 467 |
+
def make_execution_order_update_callback(current_name, current_submodule):
|
| 468 |
+
def callback():
|
| 469 |
+
if not torch.compiler.is_compiling():
|
| 470 |
+
logger.debug(f"Adding {current_name} to the execution order")
|
| 471 |
+
self.execution_order.append((current_name, current_submodule))
|
| 472 |
+
|
| 473 |
+
return callback
|
| 474 |
+
|
| 475 |
+
# To every submodule that contains a group offloading hook (at this point, no prefetching is enabled for any
|
| 476 |
+
# of the groups), we add a layer execution tracker hook that will be used to determine the order in which the
|
| 477 |
+
# layers are executed during the forward pass.
|
| 478 |
+
for name, submodule in module.named_modules():
|
| 479 |
+
if name == "" or not hasattr(submodule, "_diffusers_hook"):
|
| 480 |
+
continue
|
| 481 |
+
|
| 482 |
+
registry = HookRegistry.check_if_exists_or_initialize(submodule)
|
| 483 |
+
group_offloading_hook = registry.get_hook(_GROUP_OFFLOADING)
|
| 484 |
+
|
| 485 |
+
if group_offloading_hook is not None:
|
| 486 |
+
# For the first forward pass, we have to load in a blocking manner
|
| 487 |
+
group_offloading_hook.group.non_blocking = False
|
| 488 |
+
layer_tracker_hook = LayerExecutionTrackerHook(make_execution_order_update_callback(name, submodule))
|
| 489 |
+
registry.register_hook(layer_tracker_hook, _LAYER_EXECUTION_TRACKER)
|
| 490 |
+
self._layer_execution_tracker_module_names.add(name)
|
| 491 |
+
|
| 492 |
+
return module
|
| 493 |
+
|
| 494 |
+
def post_forward(self, module, output):
|
| 495 |
+
# At this point, for the current modules' submodules, we know the execution order of the layers. We can now
|
| 496 |
+
# remove the layer execution tracker hooks and apply prefetching by setting the next_group attribute for each
|
| 497 |
+
# group offloading hook.
|
| 498 |
+
num_executed = len(self.execution_order)
|
| 499 |
+
execution_order_module_names = {name for name, _ in self.execution_order}
|
| 500 |
+
|
| 501 |
+
# It may be possible that some layers were not executed during the forward pass. This can happen if the layer
|
| 502 |
+
# is not used in the forward pass, or if the layer is not executed due to some other reason. In such cases, we
|
| 503 |
+
# may not be able to apply prefetching in the correct order, which can lead to device-mismatch related errors
|
| 504 |
+
# if the missing layers end up being executed in the future.
|
| 505 |
+
if execution_order_module_names != self._layer_execution_tracker_module_names:
|
| 506 |
+
unexecuted_layers = list(self._layer_execution_tracker_module_names - execution_order_module_names)
|
| 507 |
+
if not torch.compiler.is_compiling():
|
| 508 |
+
logger.warning(
|
| 509 |
+
"It seems like some layers were not executed during the forward pass. This may lead to problems when "
|
| 510 |
+
"applying lazy prefetching with automatic tracing and lead to device-mismatch related errors. Please "
|
| 511 |
+
"make sure that all layers are executed during the forward pass. The following layers were not executed:\n"
|
| 512 |
+
f"{unexecuted_layers=}"
|
| 513 |
+
)
|
| 514 |
+
|
| 515 |
+
# Remove the layer execution tracker hooks from the submodules
|
| 516 |
+
base_module_registry = module._diffusers_hook
|
| 517 |
+
registries = [submodule._diffusers_hook for _, submodule in self.execution_order]
|
| 518 |
+
group_offloading_hooks = [registry.get_hook(_GROUP_OFFLOADING) for registry in registries]
|
| 519 |
+
|
| 520 |
+
for i in range(num_executed):
|
| 521 |
+
registries[i].remove_hook(_LAYER_EXECUTION_TRACKER, recurse=False)
|
| 522 |
+
|
| 523 |
+
# Remove the current lazy prefetch group offloading hook so that it doesn't interfere with the next forward pass
|
| 524 |
+
base_module_registry.remove_hook(_LAZY_PREFETCH_GROUP_OFFLOADING, recurse=False)
|
| 525 |
+
|
| 526 |
+
# LazyPrefetchGroupOffloadingHook is only used with streams, so we know that non_blocking should be True.
|
| 527 |
+
# We disable non_blocking for the first forward pass, but need to enable it for the subsequent passes to
|
| 528 |
+
# see the benefits of prefetching.
|
| 529 |
+
for hook in group_offloading_hooks:
|
| 530 |
+
hook.group.non_blocking = True
|
| 531 |
+
|
| 532 |
+
# Set required attributes for prefetching
|
| 533 |
+
if num_executed > 0:
|
| 534 |
+
base_module_group_offloading_hook = base_module_registry.get_hook(_GROUP_OFFLOADING)
|
| 535 |
+
base_module_group_offloading_hook.next_group = group_offloading_hooks[0].group
|
| 536 |
+
base_module_group_offloading_hook.next_group.onload_self = False
|
| 537 |
+
|
| 538 |
+
for i in range(num_executed - 1):
|
| 539 |
+
name1, _ = self.execution_order[i]
|
| 540 |
+
name2, _ = self.execution_order[i + 1]
|
| 541 |
+
if not torch.compiler.is_compiling():
|
| 542 |
+
logger.debug(f"Applying lazy prefetch group offloading from {name1} to {name2}")
|
| 543 |
+
group_offloading_hooks[i].next_group = group_offloading_hooks[i + 1].group
|
| 544 |
+
group_offloading_hooks[i].next_group.onload_self = False
|
| 545 |
+
|
| 546 |
+
return output
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
class LayerExecutionTrackerHook(ModelHook):
|
| 550 |
+
r"""
|
| 551 |
+
A hook that tracks the order in which the layers are executed during the forward pass by calling back to the
|
| 552 |
+
LazyPrefetchGroupOffloadingHook to update the execution order.
|
| 553 |
+
"""
|
| 554 |
+
|
| 555 |
+
_is_stateful = False
|
| 556 |
+
|
| 557 |
+
def __init__(self, execution_order_update_callback):
|
| 558 |
+
self.execution_order_update_callback = execution_order_update_callback
|
| 559 |
+
|
| 560 |
+
def pre_forward(self, module, *args, **kwargs):
|
| 561 |
+
self.execution_order_update_callback()
|
| 562 |
+
return args, kwargs
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
def apply_group_offloading(
|
| 566 |
+
module: torch.nn.Module,
|
| 567 |
+
onload_device: str | torch.device,
|
| 568 |
+
offload_device: str | torch.device = torch.device("cpu"),
|
| 569 |
+
offload_type: str | GroupOffloadingType = "block_level",
|
| 570 |
+
num_blocks_per_group: int | None = None,
|
| 571 |
+
non_blocking: bool = False,
|
| 572 |
+
use_stream: bool = False,
|
| 573 |
+
record_stream: bool = False,
|
| 574 |
+
low_cpu_mem_usage: bool = False,
|
| 575 |
+
offload_to_disk_path: str | None = None,
|
| 576 |
+
block_modules: list[str] | None = None,
|
| 577 |
+
exclude_kwargs: list[str] | None = None,
|
| 578 |
+
) -> None:
|
| 579 |
+
r"""
|
| 580 |
+
Applies group offloading to the internal layers of a torch.nn.Module. To understand what group offloading is, and
|
| 581 |
+
where it is beneficial, we need to first provide some context on how other supported offloading methods work.
|
| 582 |
+
|
| 583 |
+
Typically, offloading is done at two levels:
|
| 584 |
+
- Module-level: In Diffusers, this can be enabled using the `ModelMixin::enable_model_cpu_offload()` method. It
|
| 585 |
+
works by offloading each component of a pipeline to the CPU for storage, and onloading to the accelerator device
|
| 586 |
+
when needed for computation. This method is more memory-efficient than keeping all components on the accelerator,
|
| 587 |
+
but the memory requirements are still quite high. For this method to work, one needs memory equivalent to size of
|
| 588 |
+
the model in runtime dtype + size of largest intermediate activation tensors to be able to complete the forward
|
| 589 |
+
pass.
|
| 590 |
+
- Leaf-level: In Diffusers, this can be enabled using the `ModelMixin::enable_sequential_cpu_offload()` method. It
|
| 591 |
+
works by offloading the lowest leaf-level parameters of the computation graph to the CPU for storage, and
|
| 592 |
+
onloading only the leafs to the accelerator device for computation. This uses the lowest amount of accelerator
|
| 593 |
+
memory, but can be slower due to the excessive number of device synchronizations.
|
| 594 |
+
|
| 595 |
+
Group offloading is a middle ground between the two methods. It works by offloading groups of internal layers,
|
| 596 |
+
(either `torch.nn.ModuleList` or `torch.nn.Sequential`). This method uses lower memory than module-level
|
| 597 |
+
offloading. It is also faster than leaf-level/sequential offloading, as the number of device synchronizations is
|
| 598 |
+
reduced.
|
| 599 |
+
|
| 600 |
+
Another supported feature (for CUDA devices with support for asynchronous data transfer streams) is the ability to
|
| 601 |
+
overlap data transfer and computation to reduce the overall execution time compared to sequential offloading. This
|
| 602 |
+
is enabled using layer prefetching with streams, i.e., the layer that is to be executed next starts onloading to
|
| 603 |
+
the accelerator device while the current layer is being executed - this increases the memory requirements slightly.
|
| 604 |
+
Note that this implementation also supports leaf-level offloading but can be made much faster when using streams.
|
| 605 |
+
|
| 606 |
+
Args:
|
| 607 |
+
module (`torch.nn.Module`):
|
| 608 |
+
The module to which group offloading is applied.
|
| 609 |
+
onload_device (`torch.device`):
|
| 610 |
+
The device to which the group of modules are onloaded.
|
| 611 |
+
offload_device (`torch.device`, defaults to `torch.device("cpu")`):
|
| 612 |
+
The device to which the group of modules are offloaded. This should typically be the CPU. Default is CPU.
|
| 613 |
+
offload_type (`str` or `GroupOffloadingType`, defaults to "block_level"):
|
| 614 |
+
The type of offloading to be applied. Can be one of "block_level" or "leaf_level". Default is
|
| 615 |
+
"block_level".
|
| 616 |
+
offload_to_disk_path (`str`, *optional*, defaults to `None`):
|
| 617 |
+
The path to the directory where parameters will be offloaded. Setting this option can be useful in limited
|
| 618 |
+
RAM environment settings where a reasonable speed-memory trade-off is desired.
|
| 619 |
+
num_blocks_per_group (`int`, *optional*):
|
| 620 |
+
The number of blocks per group when using offload_type="block_level". This is required when using
|
| 621 |
+
offload_type="block_level".
|
| 622 |
+
non_blocking (`bool`, defaults to `False`):
|
| 623 |
+
If True, offloading and onloading is done with non-blocking data transfer.
|
| 624 |
+
use_stream (`bool`, defaults to `False`):
|
| 625 |
+
If True, offloading and onloading is done asynchronously using a CUDA stream. This can be useful for
|
| 626 |
+
overlapping computation and data transfer.
|
| 627 |
+
record_stream (`bool`, defaults to `False`): When enabled with `use_stream`, it marks the current tensor
|
| 628 |
+
as having been used by this stream. It is faster at the expense of slightly more memory usage. Refer to the
|
| 629 |
+
[PyTorch official docs](https://pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html) more
|
| 630 |
+
details.
|
| 631 |
+
low_cpu_mem_usage (`bool`, defaults to `False`):
|
| 632 |
+
If True, the CPU memory usage is minimized by pinning tensors on-the-fly instead of pre-pinning them. This
|
| 633 |
+
option only matters when using streamed CPU offloading (i.e. `use_stream=True`). This can be useful when
|
| 634 |
+
the CPU memory is a bottleneck but may counteract the benefits of using streams.
|
| 635 |
+
block_modules (`list[str]`, *optional*):
|
| 636 |
+
List of module names that should be treated as blocks for offloading. If provided, only these modules will
|
| 637 |
+
be considered for block-level offloading. If not provided, the default block detection logic will be used.
|
| 638 |
+
exclude_kwargs (`list[str]`, *optional*):
|
| 639 |
+
List of kwarg keys that should not be processed by send_to_device. This is useful for mutable state like
|
| 640 |
+
caching lists that need to maintain their object identity across forward passes. If not provided, will be
|
| 641 |
+
inferred from the module's `_skip_keys` attribute if it exists.
|
| 642 |
+
|
| 643 |
+
Example:
|
| 644 |
+
```python
|
| 645 |
+
>>> from diffusers import CogVideoXTransformer3DModel
|
| 646 |
+
>>> from diffusers.hooks import apply_group_offloading
|
| 647 |
+
|
| 648 |
+
>>> transformer = CogVideoXTransformer3DModel.from_pretrained(
|
| 649 |
+
... "THUDM/CogVideoX-5b", subfolder="transformer", torch_dtype=torch.bfloat16
|
| 650 |
+
... )
|
| 651 |
+
|
| 652 |
+
>>> apply_group_offloading(
|
| 653 |
+
... transformer,
|
| 654 |
+
... onload_device=torch.device("cuda"),
|
| 655 |
+
... offload_device=torch.device("cpu"),
|
| 656 |
+
... offload_type="block_level",
|
| 657 |
+
... num_blocks_per_group=2,
|
| 658 |
+
... use_stream=True,
|
| 659 |
+
... )
|
| 660 |
+
```
|
| 661 |
+
"""
|
| 662 |
+
|
| 663 |
+
onload_device = torch.device(onload_device) if isinstance(onload_device, str) else onload_device
|
| 664 |
+
offload_device = torch.device(offload_device) if isinstance(offload_device, str) else offload_device
|
| 665 |
+
offload_type = GroupOffloadingType(offload_type)
|
| 666 |
+
|
| 667 |
+
stream = None
|
| 668 |
+
if use_stream:
|
| 669 |
+
if torch.cuda.is_available():
|
| 670 |
+
stream = torch.cuda.Stream()
|
| 671 |
+
elif hasattr(torch, "xpu") and torch.xpu.is_available():
|
| 672 |
+
stream = torch.Stream()
|
| 673 |
+
else:
|
| 674 |
+
raise ValueError("Using streams for data transfer requires a CUDA device, or an Intel XPU device.")
|
| 675 |
+
|
| 676 |
+
if not use_stream and record_stream:
|
| 677 |
+
raise ValueError("`record_stream` cannot be True when `use_stream=False`.")
|
| 678 |
+
if offload_type == GroupOffloadingType.BLOCK_LEVEL and num_blocks_per_group is None:
|
| 679 |
+
raise ValueError("`num_blocks_per_group` must be provided when using `offload_type='block_level'.")
|
| 680 |
+
|
| 681 |
+
_raise_error_if_accelerate_model_or_sequential_hook_present(module)
|
| 682 |
+
|
| 683 |
+
if block_modules is None:
|
| 684 |
+
block_modules = getattr(module, "_group_offload_block_modules", None)
|
| 685 |
+
|
| 686 |
+
if exclude_kwargs is None:
|
| 687 |
+
exclude_kwargs = getattr(module, "_skip_keys", None)
|
| 688 |
+
|
| 689 |
+
config = GroupOffloadingConfig(
|
| 690 |
+
onload_device=onload_device,
|
| 691 |
+
offload_device=offload_device,
|
| 692 |
+
offload_type=offload_type,
|
| 693 |
+
num_blocks_per_group=num_blocks_per_group,
|
| 694 |
+
non_blocking=non_blocking,
|
| 695 |
+
stream=stream,
|
| 696 |
+
record_stream=record_stream,
|
| 697 |
+
low_cpu_mem_usage=low_cpu_mem_usage,
|
| 698 |
+
offload_to_disk_path=offload_to_disk_path,
|
| 699 |
+
block_modules=block_modules,
|
| 700 |
+
exclude_kwargs=exclude_kwargs,
|
| 701 |
+
)
|
| 702 |
+
_apply_group_offloading(module, config)
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
def _apply_group_offloading(module: torch.nn.Module, config: GroupOffloadingConfig) -> None:
|
| 706 |
+
if config.offload_type == GroupOffloadingType.BLOCK_LEVEL:
|
| 707 |
+
_apply_group_offloading_block_level(module, config)
|
| 708 |
+
elif config.offload_type == GroupOffloadingType.LEAF_LEVEL:
|
| 709 |
+
_apply_group_offloading_leaf_level(module, config)
|
| 710 |
+
else:
|
| 711 |
+
assert False
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
def _apply_group_offloading_block_level(module: torch.nn.Module, config: GroupOffloadingConfig) -> None:
|
| 715 |
+
r"""
|
| 716 |
+
This function applies offloading to groups of torch.nn.ModuleList or torch.nn.Sequential blocks, and explicitly
|
| 717 |
+
defined block modules. In comparison to the "leaf_level" offloading, which is more fine-grained, this offloading is
|
| 718 |
+
done at the top-level blocks and modules specified in block_modules.
|
| 719 |
+
|
| 720 |
+
When block_modules is provided, only those modules will be treated as blocks for offloading. For each specified
|
| 721 |
+
module, recursively apply block offloading to it.
|
| 722 |
+
"""
|
| 723 |
+
if config.stream is not None and config.num_blocks_per_group != 1:
|
| 724 |
+
logger.warning(
|
| 725 |
+
f"Using streams is only supported for num_blocks_per_group=1. Got {config.num_blocks_per_group=}. Setting it to 1."
|
| 726 |
+
)
|
| 727 |
+
config.num_blocks_per_group = 1
|
| 728 |
+
|
| 729 |
+
block_modules = set(config.block_modules) if config.block_modules is not None else set()
|
| 730 |
+
|
| 731 |
+
# Create module groups for ModuleList and Sequential blocks, and explicitly defined block modules
|
| 732 |
+
modules_with_group_offloading = set()
|
| 733 |
+
unmatched_modules = []
|
| 734 |
+
matched_module_groups = []
|
| 735 |
+
|
| 736 |
+
for name, submodule in module.named_children():
|
| 737 |
+
# Check if this is an explicitly defined block module
|
| 738 |
+
if name in block_modules:
|
| 739 |
+
# Track submodule using a prefix to avoid filename collisions during disk offload.
|
| 740 |
+
# Without this, submodules sharing the same model class would be assigned identical
|
| 741 |
+
# filenames (derived from the class name).
|
| 742 |
+
prefix = f"{config.module_prefix}{name}." if config.module_prefix else f"{name}."
|
| 743 |
+
submodule_config = replace(config, module_prefix=prefix)
|
| 744 |
+
|
| 745 |
+
_apply_group_offloading_block_level(submodule, submodule_config)
|
| 746 |
+
modules_with_group_offloading.add(name)
|
| 747 |
+
|
| 748 |
+
elif isinstance(submodule, (torch.nn.ModuleList, torch.nn.Sequential)):
|
| 749 |
+
# Handle ModuleList and Sequential blocks as before
|
| 750 |
+
for i in range(0, len(submodule), config.num_blocks_per_group):
|
| 751 |
+
current_modules = list(submodule[i : i + config.num_blocks_per_group])
|
| 752 |
+
if len(current_modules) == 0:
|
| 753 |
+
continue
|
| 754 |
+
|
| 755 |
+
group_id = f"{config.module_prefix}{name}_{i}_{i + len(current_modules) - 1}"
|
| 756 |
+
group = ModuleGroup(
|
| 757 |
+
modules=current_modules,
|
| 758 |
+
offload_device=config.offload_device,
|
| 759 |
+
onload_device=config.onload_device,
|
| 760 |
+
offload_to_disk_path=config.offload_to_disk_path,
|
| 761 |
+
offload_leader=current_modules[-1],
|
| 762 |
+
onload_leader=current_modules[0],
|
| 763 |
+
non_blocking=config.non_blocking,
|
| 764 |
+
stream=config.stream,
|
| 765 |
+
record_stream=config.record_stream,
|
| 766 |
+
low_cpu_mem_usage=config.low_cpu_mem_usage,
|
| 767 |
+
onload_self=True,
|
| 768 |
+
group_id=group_id,
|
| 769 |
+
)
|
| 770 |
+
matched_module_groups.append(group)
|
| 771 |
+
for j in range(i, i + len(current_modules)):
|
| 772 |
+
modules_with_group_offloading.add(f"{name}.{j}")
|
| 773 |
+
else:
|
| 774 |
+
# This is an unmatched module
|
| 775 |
+
unmatched_modules.append((name, submodule))
|
| 776 |
+
|
| 777 |
+
# Apply group offloading hooks to the module groups
|
| 778 |
+
for i, group in enumerate(matched_module_groups):
|
| 779 |
+
for group_module in group.modules:
|
| 780 |
+
_apply_group_offloading_hook(group_module, group, config=config)
|
| 781 |
+
|
| 782 |
+
# Parameters and Buffers of the top-level module need to be offloaded/onloaded separately
|
| 783 |
+
# when the forward pass of this module is called. This is because the top-level module is not
|
| 784 |
+
# part of any group (as doing so would lead to no VRAM savings).
|
| 785 |
+
parameters = _gather_parameters_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
| 786 |
+
buffers = _gather_buffers_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
| 787 |
+
parameters = [param for _, param in parameters]
|
| 788 |
+
buffers = [buffer for _, buffer in buffers]
|
| 789 |
+
|
| 790 |
+
# Create a group for the remaining unmatched submodules of the top-level
|
| 791 |
+
# module so that they are on the correct device when the forward pass is called.
|
| 792 |
+
unmatched_modules = [unmatched_module for _, unmatched_module in unmatched_modules]
|
| 793 |
+
if len(unmatched_modules) > 0 or len(parameters) > 0 or len(buffers) > 0:
|
| 794 |
+
unmatched_group = ModuleGroup(
|
| 795 |
+
modules=unmatched_modules,
|
| 796 |
+
offload_device=config.offload_device,
|
| 797 |
+
onload_device=config.onload_device,
|
| 798 |
+
offload_to_disk_path=config.offload_to_disk_path,
|
| 799 |
+
offload_leader=module,
|
| 800 |
+
onload_leader=module,
|
| 801 |
+
parameters=parameters,
|
| 802 |
+
buffers=buffers,
|
| 803 |
+
non_blocking=False,
|
| 804 |
+
stream=None,
|
| 805 |
+
record_stream=False,
|
| 806 |
+
onload_self=True,
|
| 807 |
+
group_id=f"{config.module_prefix}{module.__class__.__name__}_unmatched_group",
|
| 808 |
+
)
|
| 809 |
+
if config.stream is None:
|
| 810 |
+
_apply_group_offloading_hook(module, unmatched_group, config=config)
|
| 811 |
+
else:
|
| 812 |
+
_apply_lazy_group_offloading_hook(module, unmatched_group, config=config)
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
def _apply_group_offloading_leaf_level(module: torch.nn.Module, config: GroupOffloadingConfig) -> None:
|
| 816 |
+
r"""
|
| 817 |
+
This function applies offloading to groups of leaf modules in a torch.nn.Module. This method has minimal memory
|
| 818 |
+
requirements. However, it can be slower compared to other offloading methods due to the excessive number of device
|
| 819 |
+
synchronizations. When using devices that support streams to overlap data transfer and computation, this method can
|
| 820 |
+
reduce memory usage without any performance degradation.
|
| 821 |
+
"""
|
| 822 |
+
# Create module groups for leaf modules and apply group offloading hooks
|
| 823 |
+
modules_with_group_offloading = set()
|
| 824 |
+
for name, submodule in module.named_modules():
|
| 825 |
+
if not isinstance(submodule, _GO_LC_SUPPORTED_PYTORCH_LAYERS):
|
| 826 |
+
continue
|
| 827 |
+
group = ModuleGroup(
|
| 828 |
+
modules=[submodule],
|
| 829 |
+
offload_device=config.offload_device,
|
| 830 |
+
onload_device=config.onload_device,
|
| 831 |
+
offload_to_disk_path=config.offload_to_disk_path,
|
| 832 |
+
offload_leader=submodule,
|
| 833 |
+
onload_leader=submodule,
|
| 834 |
+
non_blocking=config.non_blocking,
|
| 835 |
+
stream=config.stream,
|
| 836 |
+
record_stream=config.record_stream,
|
| 837 |
+
low_cpu_mem_usage=config.low_cpu_mem_usage,
|
| 838 |
+
onload_self=True,
|
| 839 |
+
group_id=name,
|
| 840 |
+
)
|
| 841 |
+
_apply_group_offloading_hook(submodule, group, config=config)
|
| 842 |
+
modules_with_group_offloading.add(name)
|
| 843 |
+
|
| 844 |
+
# Parameters and Buffers at all non-leaf levels need to be offloaded/onloaded separately when the forward pass
|
| 845 |
+
# of the module is called
|
| 846 |
+
module_dict = dict(module.named_modules())
|
| 847 |
+
parameters = _gather_parameters_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
| 848 |
+
buffers = _gather_buffers_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
| 849 |
+
|
| 850 |
+
# Find closest module parent for each parameter and buffer, and attach group hooks
|
| 851 |
+
parent_to_parameters = {}
|
| 852 |
+
for name, param in parameters:
|
| 853 |
+
parent_name = _find_parent_module_in_module_dict(name, module_dict)
|
| 854 |
+
if parent_name in parent_to_parameters:
|
| 855 |
+
parent_to_parameters[parent_name].append(param)
|
| 856 |
+
else:
|
| 857 |
+
parent_to_parameters[parent_name] = [param]
|
| 858 |
+
|
| 859 |
+
parent_to_buffers = {}
|
| 860 |
+
for name, buffer in buffers:
|
| 861 |
+
parent_name = _find_parent_module_in_module_dict(name, module_dict)
|
| 862 |
+
if parent_name in parent_to_buffers:
|
| 863 |
+
parent_to_buffers[parent_name].append(buffer)
|
| 864 |
+
else:
|
| 865 |
+
parent_to_buffers[parent_name] = [buffer]
|
| 866 |
+
|
| 867 |
+
parent_names = set(parent_to_parameters.keys()) | set(parent_to_buffers.keys())
|
| 868 |
+
for name in parent_names:
|
| 869 |
+
parameters = parent_to_parameters.get(name, [])
|
| 870 |
+
buffers = parent_to_buffers.get(name, [])
|
| 871 |
+
parent_module = module_dict[name]
|
| 872 |
+
group = ModuleGroup(
|
| 873 |
+
modules=[],
|
| 874 |
+
offload_device=config.offload_device,
|
| 875 |
+
onload_device=config.onload_device,
|
| 876 |
+
offload_leader=parent_module,
|
| 877 |
+
onload_leader=parent_module,
|
| 878 |
+
offload_to_disk_path=config.offload_to_disk_path,
|
| 879 |
+
parameters=parameters,
|
| 880 |
+
buffers=buffers,
|
| 881 |
+
non_blocking=config.non_blocking,
|
| 882 |
+
stream=config.stream,
|
| 883 |
+
record_stream=config.record_stream,
|
| 884 |
+
low_cpu_mem_usage=config.low_cpu_mem_usage,
|
| 885 |
+
onload_self=True,
|
| 886 |
+
group_id=name,
|
| 887 |
+
)
|
| 888 |
+
_apply_group_offloading_hook(parent_module, group, config=config)
|
| 889 |
+
|
| 890 |
+
if config.stream is not None:
|
| 891 |
+
# When using streams, we need to know the layer execution order for applying prefetching (to overlap data transfer
|
| 892 |
+
# and computation). Since we don't know the order beforehand, we apply a lazy prefetching hook that will find the
|
| 893 |
+
# execution order and apply prefetching in the correct order.
|
| 894 |
+
unmatched_group = ModuleGroup(
|
| 895 |
+
modules=[],
|
| 896 |
+
offload_device=config.offload_device,
|
| 897 |
+
onload_device=config.onload_device,
|
| 898 |
+
offload_to_disk_path=config.offload_to_disk_path,
|
| 899 |
+
offload_leader=module,
|
| 900 |
+
onload_leader=module,
|
| 901 |
+
parameters=None,
|
| 902 |
+
buffers=None,
|
| 903 |
+
non_blocking=False,
|
| 904 |
+
stream=None,
|
| 905 |
+
record_stream=False,
|
| 906 |
+
low_cpu_mem_usage=config.low_cpu_mem_usage,
|
| 907 |
+
onload_self=True,
|
| 908 |
+
group_id=_GROUP_ID_LAZY_LEAF,
|
| 909 |
+
)
|
| 910 |
+
_apply_lazy_group_offloading_hook(module, unmatched_group, config=config)
|
| 911 |
+
|
| 912 |
+
|
| 913 |
+
def _apply_group_offloading_hook(
|
| 914 |
+
module: torch.nn.Module,
|
| 915 |
+
group: ModuleGroup,
|
| 916 |
+
*,
|
| 917 |
+
config: GroupOffloadingConfig,
|
| 918 |
+
) -> None:
|
| 919 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 920 |
+
|
| 921 |
+
# We may have already registered a group offloading hook if the module had a torch.nn.Parameter whose parent
|
| 922 |
+
# is the current module. In such cases, we don't want to overwrite the existing group offloading hook.
|
| 923 |
+
if registry.get_hook(_GROUP_OFFLOADING) is None:
|
| 924 |
+
hook = GroupOffloadingHook(group, config=config)
|
| 925 |
+
registry.register_hook(hook, _GROUP_OFFLOADING)
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
def _apply_lazy_group_offloading_hook(
|
| 929 |
+
module: torch.nn.Module,
|
| 930 |
+
group: ModuleGroup,
|
| 931 |
+
*,
|
| 932 |
+
config: GroupOffloadingConfig,
|
| 933 |
+
) -> None:
|
| 934 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 935 |
+
|
| 936 |
+
# We may have already registered a group offloading hook if the module had a torch.nn.Parameter whose parent
|
| 937 |
+
# is the current module. In such cases, we don't want to overwrite the existing group offloading hook.
|
| 938 |
+
if registry.get_hook(_GROUP_OFFLOADING) is None:
|
| 939 |
+
hook = GroupOffloadingHook(group, config=config)
|
| 940 |
+
registry.register_hook(hook, _GROUP_OFFLOADING)
|
| 941 |
+
|
| 942 |
+
lazy_prefetch_hook = LazyPrefetchGroupOffloadingHook()
|
| 943 |
+
registry.register_hook(lazy_prefetch_hook, _LAZY_PREFETCH_GROUP_OFFLOADING)
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
def _gather_parameters_with_no_group_offloading_parent(
|
| 947 |
+
module: torch.nn.Module, modules_with_group_offloading: Set[str]
|
| 948 |
+
) -> list[torch.nn.Parameter]:
|
| 949 |
+
parameters = []
|
| 950 |
+
for name, parameter in module.named_parameters():
|
| 951 |
+
has_parent_with_group_offloading = False
|
| 952 |
+
atoms = name.split(".")
|
| 953 |
+
while len(atoms) > 0:
|
| 954 |
+
parent_name = ".".join(atoms)
|
| 955 |
+
if parent_name in modules_with_group_offloading:
|
| 956 |
+
has_parent_with_group_offloading = True
|
| 957 |
+
break
|
| 958 |
+
atoms.pop()
|
| 959 |
+
if not has_parent_with_group_offloading:
|
| 960 |
+
parameters.append((name, parameter))
|
| 961 |
+
return parameters
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
def _gather_buffers_with_no_group_offloading_parent(
|
| 965 |
+
module: torch.nn.Module, modules_with_group_offloading: Set[str]
|
| 966 |
+
) -> list[torch.Tensor]:
|
| 967 |
+
buffers = []
|
| 968 |
+
for name, buffer in module.named_buffers():
|
| 969 |
+
has_parent_with_group_offloading = False
|
| 970 |
+
atoms = name.split(".")
|
| 971 |
+
while len(atoms) > 0:
|
| 972 |
+
parent_name = ".".join(atoms)
|
| 973 |
+
if parent_name in modules_with_group_offloading:
|
| 974 |
+
has_parent_with_group_offloading = True
|
| 975 |
+
break
|
| 976 |
+
atoms.pop()
|
| 977 |
+
if not has_parent_with_group_offloading:
|
| 978 |
+
buffers.append((name, buffer))
|
| 979 |
+
return buffers
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
def _find_parent_module_in_module_dict(name: str, module_dict: dict[str, torch.nn.Module]) -> str:
|
| 983 |
+
atoms = name.split(".")
|
| 984 |
+
while len(atoms) > 0:
|
| 985 |
+
parent_name = ".".join(atoms)
|
| 986 |
+
if parent_name in module_dict:
|
| 987 |
+
return parent_name
|
| 988 |
+
atoms.pop()
|
| 989 |
+
return ""
|
| 990 |
+
|
| 991 |
+
|
| 992 |
+
def _raise_error_if_accelerate_model_or_sequential_hook_present(module: torch.nn.Module) -> None:
|
| 993 |
+
if not is_accelerate_available():
|
| 994 |
+
return
|
| 995 |
+
for name, submodule in module.named_modules():
|
| 996 |
+
if not hasattr(submodule, "_hf_hook"):
|
| 997 |
+
continue
|
| 998 |
+
if isinstance(submodule._hf_hook, (AlignDevicesHook, CpuOffload)):
|
| 999 |
+
raise ValueError(
|
| 1000 |
+
f"Cannot apply group offloading to a module that is already applying an alternative "
|
| 1001 |
+
f"offloading strategy from Accelerate. If you want to apply group offloading, please "
|
| 1002 |
+
f"disable the existing offloading strategy first. Offending module: {name} ({type(submodule)})"
|
| 1003 |
+
)
|
| 1004 |
+
|
| 1005 |
+
|
| 1006 |
+
def _get_top_level_group_offload_hook(module: torch.nn.Module) -> GroupOffloadingHook | None:
|
| 1007 |
+
for submodule in module.modules():
|
| 1008 |
+
if hasattr(submodule, "_diffusers_hook"):
|
| 1009 |
+
group_offloading_hook = submodule._diffusers_hook.get_hook(_GROUP_OFFLOADING)
|
| 1010 |
+
if group_offloading_hook is not None:
|
| 1011 |
+
return group_offloading_hook
|
| 1012 |
+
return None
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
+
def _is_group_offload_enabled(module: torch.nn.Module) -> bool:
|
| 1016 |
+
top_level_group_offload_hook = _get_top_level_group_offload_hook(module)
|
| 1017 |
+
return top_level_group_offload_hook is not None
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
+
def _get_group_onload_device(module: torch.nn.Module) -> torch.device:
|
| 1021 |
+
top_level_group_offload_hook = _get_top_level_group_offload_hook(module)
|
| 1022 |
+
if top_level_group_offload_hook is not None:
|
| 1023 |
+
return top_level_group_offload_hook.config.onload_device
|
| 1024 |
+
raise ValueError("Group offloading is not enabled for the provided module.")
|
| 1025 |
+
|
| 1026 |
+
|
| 1027 |
+
def _compute_group_hash(group_id):
|
| 1028 |
+
hashed_id = hashlib.sha256(group_id.encode("utf-8")).hexdigest()
|
| 1029 |
+
# first 16 characters for a reasonably short but unique name
|
| 1030 |
+
return hashed_id[:16]
|
| 1031 |
+
|
| 1032 |
+
|
| 1033 |
+
def _maybe_remove_and_reapply_group_offloading(module: torch.nn.Module) -> None:
|
| 1034 |
+
r"""
|
| 1035 |
+
Removes the group offloading hook from the module and re-applies it. This is useful when the module has been
|
| 1036 |
+
modified in-place and the group offloading hook references-to-tensors needs to be updated. The in-place
|
| 1037 |
+
modification can happen in a number of ways, for example, fusing QKV or unloading/loading LoRAs on-the-fly.
|
| 1038 |
+
|
| 1039 |
+
In this implementation, we make an assumption that group offloading has only been applied at the top-level module,
|
| 1040 |
+
and therefore all submodules have the same onload and offload devices. If this assumption is not true, say in the
|
| 1041 |
+
case where user has applied group offloading at multiple levels, this function will not work as expected.
|
| 1042 |
+
|
| 1043 |
+
There is some performance penalty associated with doing this when non-default streams are used, because we need to
|
| 1044 |
+
retrace the execution order of the layers with `LazyPrefetchGroupOffloadingHook`.
|
| 1045 |
+
"""
|
| 1046 |
+
top_level_group_offload_hook = _get_top_level_group_offload_hook(module)
|
| 1047 |
+
|
| 1048 |
+
if top_level_group_offload_hook is None:
|
| 1049 |
+
return
|
| 1050 |
+
|
| 1051 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 1052 |
+
registry.remove_hook(_GROUP_OFFLOADING, recurse=True)
|
| 1053 |
+
registry.remove_hook(_LAYER_EXECUTION_TRACKER, recurse=True)
|
| 1054 |
+
registry.remove_hook(_LAZY_PREFETCH_GROUP_OFFLOADING, recurse=True)
|
| 1055 |
+
|
| 1056 |
+
_apply_group_offloading(module, top_level_group_offload_hook.config)
|
diffusers/hooks/hooks.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import functools
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ..utils.logging import get_logger
|
| 21 |
+
from ..utils.torch_utils import unwrap_module
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class BaseState:
|
| 28 |
+
def reset(self, *args, **kwargs) -> None:
|
| 29 |
+
raise NotImplementedError(
|
| 30 |
+
"BaseState::reset is not implemented. Please implement this method in the derived class."
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class StateManager:
|
| 35 |
+
def __init__(self, state_cls: BaseState, init_args=None, init_kwargs=None):
|
| 36 |
+
self._state_cls = state_cls
|
| 37 |
+
self._init_args = init_args if init_args is not None else ()
|
| 38 |
+
self._init_kwargs = init_kwargs if init_kwargs is not None else {}
|
| 39 |
+
self._state_cache = {}
|
| 40 |
+
self._current_context = None
|
| 41 |
+
|
| 42 |
+
def get_state(self):
|
| 43 |
+
if self._current_context is None:
|
| 44 |
+
raise ValueError("No context is set. Please set a context before retrieving the state.")
|
| 45 |
+
if self._current_context not in self._state_cache.keys():
|
| 46 |
+
self._state_cache[self._current_context] = self._state_cls(*self._init_args, **self._init_kwargs)
|
| 47 |
+
return self._state_cache[self._current_context]
|
| 48 |
+
|
| 49 |
+
def set_context(self, name: str) -> None:
|
| 50 |
+
self._current_context = name
|
| 51 |
+
|
| 52 |
+
def reset(self, *args, **kwargs) -> None:
|
| 53 |
+
for name, state in list(self._state_cache.items()):
|
| 54 |
+
state.reset(*args, **kwargs)
|
| 55 |
+
self._state_cache.pop(name)
|
| 56 |
+
self._current_context = None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class ModelHook:
|
| 60 |
+
r"""
|
| 61 |
+
A hook that contains callbacks to be executed just before and after the forward method of a model.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
_is_stateful = False
|
| 65 |
+
|
| 66 |
+
def __init__(self):
|
| 67 |
+
self.fn_ref: "HookFunctionReference" = None
|
| 68 |
+
|
| 69 |
+
def initialize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
|
| 70 |
+
r"""
|
| 71 |
+
Hook that is executed when a model is initialized.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
module (`torch.nn.Module`):
|
| 75 |
+
The module attached to this hook.
|
| 76 |
+
"""
|
| 77 |
+
return module
|
| 78 |
+
|
| 79 |
+
def deinitalize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
|
| 80 |
+
r"""
|
| 81 |
+
Hook that is executed when a model is deinitialized.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
module (`torch.nn.Module`):
|
| 85 |
+
The module attached to this hook.
|
| 86 |
+
"""
|
| 87 |
+
return module
|
| 88 |
+
|
| 89 |
+
def pre_forward(self, module: torch.nn.Module, *args, **kwargs) -> tuple[tuple[Any], dict[str, Any]]:
|
| 90 |
+
r"""
|
| 91 |
+
Hook that is executed just before the forward method of the model.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
module (`torch.nn.Module`):
|
| 95 |
+
The module whose forward pass will be executed just after this event.
|
| 96 |
+
args (`tuple[Any]`):
|
| 97 |
+
The positional arguments passed to the module.
|
| 98 |
+
kwargs (`dict[Str, Any]`):
|
| 99 |
+
The keyword arguments passed to the module.
|
| 100 |
+
Returns:
|
| 101 |
+
`tuple[tuple[Any], dict[Str, Any]]`:
|
| 102 |
+
A tuple with the treated `args` and `kwargs`.
|
| 103 |
+
"""
|
| 104 |
+
return args, kwargs
|
| 105 |
+
|
| 106 |
+
def post_forward(self, module: torch.nn.Module, output: Any) -> Any:
|
| 107 |
+
r"""
|
| 108 |
+
Hook that is executed just after the forward method of the model.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
module (`torch.nn.Module`):
|
| 112 |
+
The module whose forward pass been executed just before this event.
|
| 113 |
+
output (`Any`):
|
| 114 |
+
The output of the module.
|
| 115 |
+
Returns:
|
| 116 |
+
`Any`: The processed `output`.
|
| 117 |
+
"""
|
| 118 |
+
return output
|
| 119 |
+
|
| 120 |
+
def detach_hook(self, module: torch.nn.Module) -> torch.nn.Module:
|
| 121 |
+
r"""
|
| 122 |
+
Hook that is executed when the hook is detached from a module.
|
| 123 |
+
|
| 124 |
+
Args:
|
| 125 |
+
module (`torch.nn.Module`):
|
| 126 |
+
The module detached from this hook.
|
| 127 |
+
"""
|
| 128 |
+
return module
|
| 129 |
+
|
| 130 |
+
def reset_state(self, module: torch.nn.Module):
|
| 131 |
+
if self._is_stateful:
|
| 132 |
+
raise NotImplementedError("This hook is stateful and needs to implement the `reset_state` method.")
|
| 133 |
+
return module
|
| 134 |
+
|
| 135 |
+
def _set_context(self, module: torch.nn.Module, name: str) -> None:
|
| 136 |
+
# Iterate over all attributes of the hook to see if any of them have the type `StateManager`. If so, call `set_context` on them.
|
| 137 |
+
for attr_name in dir(self):
|
| 138 |
+
attr = getattr(self, attr_name)
|
| 139 |
+
if isinstance(attr, StateManager):
|
| 140 |
+
attr.set_context(name)
|
| 141 |
+
return module
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class HookFunctionReference:
|
| 145 |
+
def __init__(self) -> None:
|
| 146 |
+
"""A container class that maintains mutable references to forward pass functions in a hook chain.
|
| 147 |
+
|
| 148 |
+
Its mutable nature allows the hook system to modify the execution chain dynamically without rebuilding the
|
| 149 |
+
entire forward pass structure.
|
| 150 |
+
|
| 151 |
+
Attributes:
|
| 152 |
+
pre_forward: A callable that processes inputs before the main forward pass.
|
| 153 |
+
post_forward: A callable that processes outputs after the main forward pass.
|
| 154 |
+
forward: The current forward function in the hook chain.
|
| 155 |
+
original_forward: The original forward function, stored when a hook provides a custom new_forward.
|
| 156 |
+
|
| 157 |
+
The class enables hook removal by allowing updates to the forward chain through reference modification rather
|
| 158 |
+
than requiring reconstruction of the entire chain. When a hook is removed, only the relevant references need to
|
| 159 |
+
be updated, preserving the execution order of the remaining hooks.
|
| 160 |
+
"""
|
| 161 |
+
self.pre_forward = None
|
| 162 |
+
self.post_forward = None
|
| 163 |
+
self.forward = None
|
| 164 |
+
self.original_forward = None
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class HookRegistry:
|
| 168 |
+
def __init__(self, module_ref: torch.nn.Module) -> None:
|
| 169 |
+
super().__init__()
|
| 170 |
+
|
| 171 |
+
self.hooks: dict[str, ModelHook] = {}
|
| 172 |
+
|
| 173 |
+
self._module_ref = module_ref
|
| 174 |
+
self._hook_order = []
|
| 175 |
+
self._fn_refs = []
|
| 176 |
+
|
| 177 |
+
def register_hook(self, hook: ModelHook, name: str) -> None:
|
| 178 |
+
if name in self.hooks.keys():
|
| 179 |
+
raise ValueError(
|
| 180 |
+
f"Hook with name {name} already exists in the registry. Please use a different name or "
|
| 181 |
+
f"first remove the existing hook and then add a new one."
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
self._module_ref = hook.initialize_hook(self._module_ref)
|
| 185 |
+
|
| 186 |
+
def create_new_forward(function_reference: HookFunctionReference):
|
| 187 |
+
def new_forward(module, *args, **kwargs):
|
| 188 |
+
args, kwargs = function_reference.pre_forward(module, *args, **kwargs)
|
| 189 |
+
output = function_reference.forward(*args, **kwargs)
|
| 190 |
+
return function_reference.post_forward(module, output)
|
| 191 |
+
|
| 192 |
+
return new_forward
|
| 193 |
+
|
| 194 |
+
forward = self._module_ref.forward
|
| 195 |
+
|
| 196 |
+
fn_ref = HookFunctionReference()
|
| 197 |
+
fn_ref.pre_forward = hook.pre_forward
|
| 198 |
+
fn_ref.post_forward = hook.post_forward
|
| 199 |
+
fn_ref.forward = forward
|
| 200 |
+
|
| 201 |
+
if hasattr(hook, "new_forward"):
|
| 202 |
+
fn_ref.original_forward = forward
|
| 203 |
+
fn_ref.forward = functools.update_wrapper(
|
| 204 |
+
functools.partial(hook.new_forward, self._module_ref), hook.new_forward
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
rewritten_forward = create_new_forward(fn_ref)
|
| 208 |
+
# Wrap from the original `forward` so `inspect.signature` follows `__wrapped__` to the real
|
| 209 |
+
# signature instead of the generic `(module, *args, **kwargs)`, which breaks `torch.export`.
|
| 210 |
+
self._module_ref.forward = functools.update_wrapper(
|
| 211 |
+
functools.partial(rewritten_forward, self._module_ref), forward
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
hook.fn_ref = fn_ref
|
| 215 |
+
self.hooks[name] = hook
|
| 216 |
+
self._hook_order.append(name)
|
| 217 |
+
self._fn_refs.append(fn_ref)
|
| 218 |
+
|
| 219 |
+
def get_hook(self, name: str) -> ModelHook | None:
|
| 220 |
+
return self.hooks.get(name, None)
|
| 221 |
+
|
| 222 |
+
def remove_hook(self, name: str, recurse: bool = True) -> None:
|
| 223 |
+
if name in self.hooks.keys():
|
| 224 |
+
num_hooks = len(self._hook_order)
|
| 225 |
+
hook = self.hooks[name]
|
| 226 |
+
index = self._hook_order.index(name)
|
| 227 |
+
fn_ref = self._fn_refs[index]
|
| 228 |
+
|
| 229 |
+
old_forward = fn_ref.forward
|
| 230 |
+
if fn_ref.original_forward is not None:
|
| 231 |
+
old_forward = fn_ref.original_forward
|
| 232 |
+
|
| 233 |
+
if index == num_hooks - 1:
|
| 234 |
+
self._module_ref.forward = old_forward
|
| 235 |
+
else:
|
| 236 |
+
self._fn_refs[index + 1].forward = old_forward
|
| 237 |
+
|
| 238 |
+
self._module_ref = hook.deinitalize_hook(self._module_ref)
|
| 239 |
+
del self.hooks[name]
|
| 240 |
+
self._hook_order.pop(index)
|
| 241 |
+
self._fn_refs.pop(index)
|
| 242 |
+
|
| 243 |
+
if recurse:
|
| 244 |
+
for module_name, module in self._module_ref.named_modules():
|
| 245 |
+
if module_name == "":
|
| 246 |
+
continue
|
| 247 |
+
if hasattr(module, "_diffusers_hook"):
|
| 248 |
+
module._diffusers_hook.remove_hook(name, recurse=False)
|
| 249 |
+
|
| 250 |
+
def reset_stateful_hooks(self, recurse: bool = True) -> None:
|
| 251 |
+
for hook_name in reversed(self._hook_order):
|
| 252 |
+
hook = self.hooks[hook_name]
|
| 253 |
+
if hook._is_stateful:
|
| 254 |
+
hook.reset_state(self._module_ref)
|
| 255 |
+
|
| 256 |
+
if recurse:
|
| 257 |
+
for module_name, module in unwrap_module(self._module_ref).named_modules():
|
| 258 |
+
if module_name == "":
|
| 259 |
+
continue
|
| 260 |
+
module = unwrap_module(module)
|
| 261 |
+
if hasattr(module, "_diffusers_hook"):
|
| 262 |
+
module._diffusers_hook.reset_stateful_hooks(recurse=False)
|
| 263 |
+
|
| 264 |
+
@classmethod
|
| 265 |
+
def check_if_exists_or_initialize(cls, module: torch.nn.Module) -> "HookRegistry":
|
| 266 |
+
if not hasattr(module, "_diffusers_hook"):
|
| 267 |
+
module._diffusers_hook = cls(module)
|
| 268 |
+
return module._diffusers_hook
|
| 269 |
+
|
| 270 |
+
def _set_context(self, name: str | None = None) -> None:
|
| 271 |
+
for hook_name in reversed(self._hook_order):
|
| 272 |
+
hook = self.hooks[hook_name]
|
| 273 |
+
if hook._is_stateful:
|
| 274 |
+
hook._set_context(self._module_ref, name)
|
| 275 |
+
|
| 276 |
+
for registry in self._get_child_registries():
|
| 277 |
+
registry._set_context(name)
|
| 278 |
+
|
| 279 |
+
def invalidate_child_registries_cache(self) -> None:
|
| 280 |
+
"""Invalidate the cached child-registry list across this module's tree.
|
| 281 |
+
|
| 282 |
+
`_get_child_registries` caches the registries it finds by walking `named_modules()`, keyed on the registry that
|
| 283 |
+
built it. Registering or removing hooks on descendant modules (e.g. block hooks added by `enable_cache`)
|
| 284 |
+
changes which modules carry a `_diffusers_hook`, which stales that cache. Call this after any operation that
|
| 285 |
+
adds or removes hooks in the subtree so the list is rebuilt on next use. Clears the cache on every registry in
|
| 286 |
+
the tree, since the same registries appear in ancestor caches.
|
| 287 |
+
"""
|
| 288 |
+
for _, module in unwrap_module(self._module_ref).named_modules():
|
| 289 |
+
module = unwrap_module(module)
|
| 290 |
+
if hasattr(module, "_diffusers_hook"):
|
| 291 |
+
module._diffusers_hook._child_registries_cache = None
|
| 292 |
+
|
| 293 |
+
def _get_child_registries(self) -> list["HookRegistry"]:
|
| 294 |
+
"""Return registries of child modules, using a cached list when available.
|
| 295 |
+
|
| 296 |
+
The cache is built on first call and reused for subsequent calls. This avoids the cost of walking the full
|
| 297 |
+
module tree via named_modules() on every _set_context call, which is significant for large models (e.g. ~2.7ms
|
| 298 |
+
per call on Flux2).
|
| 299 |
+
"""
|
| 300 |
+
if not hasattr(self, "_child_registries_cache"):
|
| 301 |
+
self._child_registries_cache = None
|
| 302 |
+
|
| 303 |
+
if self._child_registries_cache is not None:
|
| 304 |
+
return self._child_registries_cache
|
| 305 |
+
|
| 306 |
+
registries = []
|
| 307 |
+
for module_name, module in unwrap_module(self._module_ref).named_modules():
|
| 308 |
+
if module_name == "":
|
| 309 |
+
continue
|
| 310 |
+
module = unwrap_module(module)
|
| 311 |
+
if hasattr(module, "_diffusers_hook"):
|
| 312 |
+
registries.append(module._diffusers_hook)
|
| 313 |
+
self._child_registries_cache = registries
|
| 314 |
+
return registries
|
| 315 |
+
|
| 316 |
+
def __repr__(self) -> str:
|
| 317 |
+
registry_repr = ""
|
| 318 |
+
for i, hook_name in enumerate(self._hook_order):
|
| 319 |
+
if self.hooks[hook_name].__class__.__repr__ is not object.__repr__:
|
| 320 |
+
hook_repr = self.hooks[hook_name].__repr__()
|
| 321 |
+
else:
|
| 322 |
+
hook_repr = self.hooks[hook_name].__class__.__name__
|
| 323 |
+
registry_repr += f" ({i}) {hook_name} - {hook_repr}"
|
| 324 |
+
if i < len(self._hook_order) - 1:
|
| 325 |
+
registry_repr += "\n"
|
| 326 |
+
return f"HookRegistry(\n{registry_repr}\n)"
|
diffusers/hooks/layer_skip.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from dataclasses import asdict, dataclass
|
| 17 |
+
from typing import Callable
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
from ..utils import get_logger
|
| 22 |
+
from ..utils.torch_utils import unwrap_module
|
| 23 |
+
from ._common import (
|
| 24 |
+
_ALL_TRANSFORMER_BLOCK_IDENTIFIERS,
|
| 25 |
+
_ATTENTION_CLASSES,
|
| 26 |
+
_FEEDFORWARD_CLASSES,
|
| 27 |
+
_get_submodule_from_fqn,
|
| 28 |
+
)
|
| 29 |
+
from ._helpers import AttentionProcessorRegistry, TransformerBlockRegistry
|
| 30 |
+
from .hooks import HookRegistry, ModelHook
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 34 |
+
|
| 35 |
+
_LAYER_SKIP_HOOK = "layer_skip_hook"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# Aryan/YiYi TODO: we need to make guider class a config mixin so I think this is not needed
|
| 39 |
+
# either remove or make it serializable
|
| 40 |
+
@dataclass
|
| 41 |
+
class LayerSkipConfig:
|
| 42 |
+
r"""
|
| 43 |
+
Configuration for skipping internal transformer blocks when executing a transformer model.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
indices (`list[int]`):
|
| 47 |
+
The indices of the layer to skip. This is typically the first layer in the transformer block.
|
| 48 |
+
fqn (`str`, defaults to `"auto"`):
|
| 49 |
+
The fully qualified name identifying the stack of transformer blocks. Typically, this is
|
| 50 |
+
`transformer_blocks`, `single_transformer_blocks`, `blocks`, `layers`, or `temporal_transformer_blocks`.
|
| 51 |
+
For automatic detection, set this to `"auto"`. "auto" only works on DiT models. For UNet models, you must
|
| 52 |
+
provide the correct fqn.
|
| 53 |
+
skip_attention (`bool`, defaults to `True`):
|
| 54 |
+
Whether to skip attention blocks.
|
| 55 |
+
skip_ff (`bool`, defaults to `True`):
|
| 56 |
+
Whether to skip feed-forward blocks.
|
| 57 |
+
skip_attention_scores (`bool`, defaults to `False`):
|
| 58 |
+
Whether to skip attention score computation in the attention blocks. This is equivalent to using `value`
|
| 59 |
+
projections as the output of scaled dot product attention.
|
| 60 |
+
dropout (`float`, defaults to `1.0`):
|
| 61 |
+
The dropout probability for dropping the outputs of the skipped layers. By default, this is set to `1.0`,
|
| 62 |
+
meaning that the outputs of the skipped layers are completely ignored. If set to `0.0`, the outputs of the
|
| 63 |
+
skipped layers are fully retained, which is equivalent to not skipping any layers.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
indices: list[int]
|
| 67 |
+
fqn: str = "auto"
|
| 68 |
+
skip_attention: bool = True
|
| 69 |
+
skip_attention_scores: bool = False
|
| 70 |
+
skip_ff: bool = True
|
| 71 |
+
dropout: float = 1.0
|
| 72 |
+
|
| 73 |
+
def __post_init__(self):
|
| 74 |
+
if not (0 <= self.dropout <= 1):
|
| 75 |
+
raise ValueError(f"Expected `dropout` to be between 0.0 and 1.0, but got {self.dropout}.")
|
| 76 |
+
if not math.isclose(self.dropout, 1.0) and self.skip_attention_scores:
|
| 77 |
+
raise ValueError(
|
| 78 |
+
"Cannot set `skip_attention_scores` to True when `dropout` is not 1.0. Please set `dropout` to 1.0."
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
def to_dict(self):
|
| 82 |
+
return asdict(self)
|
| 83 |
+
|
| 84 |
+
@staticmethod
|
| 85 |
+
def from_dict(data: dict) -> "LayerSkipConfig":
|
| 86 |
+
return LayerSkipConfig(**data)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class AttentionScoreSkipFunctionMode(torch.overrides.TorchFunctionMode):
|
| 90 |
+
def __torch_function__(self, func, types, args=(), kwargs=None):
|
| 91 |
+
if kwargs is None:
|
| 92 |
+
kwargs = {}
|
| 93 |
+
if func is torch.nn.functional.scaled_dot_product_attention:
|
| 94 |
+
query = kwargs.get("query", None)
|
| 95 |
+
key = kwargs.get("key", None)
|
| 96 |
+
value = kwargs.get("value", None)
|
| 97 |
+
query = query if query is not None else args[0]
|
| 98 |
+
key = key if key is not None else args[1]
|
| 99 |
+
value = value if value is not None else args[2]
|
| 100 |
+
# If the Q sequence length does not match KV sequence length, methods like
|
| 101 |
+
# Perturbed Attention Guidance cannot be used (because the caller expects
|
| 102 |
+
# the same sequence length as Q, but if we return V here, it will not match).
|
| 103 |
+
# When Q.shape[2] != V.shape[2], PAG will essentially not be applied and
|
| 104 |
+
# the overall effect would that be of normal CFG with a scale of (guidance_scale + perturbed_guidance_scale).
|
| 105 |
+
if query.shape[2] == value.shape[2]:
|
| 106 |
+
return value
|
| 107 |
+
return func(*args, **kwargs)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class AttentionProcessorSkipHook(ModelHook):
|
| 111 |
+
def __init__(self, skip_processor_output_fn: Callable, skip_attention_scores: bool = False, dropout: float = 1.0):
|
| 112 |
+
self.skip_processor_output_fn = skip_processor_output_fn
|
| 113 |
+
self.skip_attention_scores = skip_attention_scores
|
| 114 |
+
self.dropout = dropout
|
| 115 |
+
|
| 116 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 117 |
+
if self.skip_attention_scores:
|
| 118 |
+
if not math.isclose(self.dropout, 1.0):
|
| 119 |
+
raise ValueError(
|
| 120 |
+
"Cannot set `skip_attention_scores` to True when `dropout` is not 1.0. Please set `dropout` to 1.0."
|
| 121 |
+
)
|
| 122 |
+
with AttentionScoreSkipFunctionMode():
|
| 123 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 124 |
+
else:
|
| 125 |
+
if math.isclose(self.dropout, 1.0):
|
| 126 |
+
output = self.skip_processor_output_fn(module, *args, **kwargs)
|
| 127 |
+
else:
|
| 128 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 129 |
+
output = torch.nn.functional.dropout(output, p=self.dropout)
|
| 130 |
+
return output
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class FeedForwardSkipHook(ModelHook):
|
| 134 |
+
def __init__(self, dropout: float):
|
| 135 |
+
super().__init__()
|
| 136 |
+
self.dropout = dropout
|
| 137 |
+
|
| 138 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 139 |
+
if math.isclose(self.dropout, 1.0):
|
| 140 |
+
output = kwargs.get("hidden_states", None)
|
| 141 |
+
if output is None:
|
| 142 |
+
output = kwargs.get("x", None)
|
| 143 |
+
if output is None and len(args) > 0:
|
| 144 |
+
output = args[0]
|
| 145 |
+
else:
|
| 146 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 147 |
+
output = torch.nn.functional.dropout(output, p=self.dropout)
|
| 148 |
+
return output
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class TransformerBlockSkipHook(ModelHook):
|
| 152 |
+
def __init__(self, dropout: float):
|
| 153 |
+
super().__init__()
|
| 154 |
+
self.dropout = dropout
|
| 155 |
+
|
| 156 |
+
def initialize_hook(self, module):
|
| 157 |
+
self._metadata = TransformerBlockRegistry.get(unwrap_module(module).__class__)
|
| 158 |
+
return module
|
| 159 |
+
|
| 160 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 161 |
+
if math.isclose(self.dropout, 1.0):
|
| 162 |
+
original_hidden_states = self._metadata._get_parameter_from_args_kwargs("hidden_states", args, kwargs)
|
| 163 |
+
if self._metadata.return_encoder_hidden_states_index is None:
|
| 164 |
+
output = original_hidden_states
|
| 165 |
+
else:
|
| 166 |
+
original_encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
|
| 167 |
+
"encoder_hidden_states", args, kwargs
|
| 168 |
+
)
|
| 169 |
+
output = (original_hidden_states, original_encoder_hidden_states)
|
| 170 |
+
else:
|
| 171 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 172 |
+
output = torch.nn.functional.dropout(output, p=self.dropout)
|
| 173 |
+
return output
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def apply_layer_skip(module: torch.nn.Module, config: LayerSkipConfig) -> None:
|
| 177 |
+
r"""
|
| 178 |
+
Apply layer skipping to internal layers of a transformer.
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
module (`torch.nn.Module`):
|
| 182 |
+
The transformer model to which the layer skip hook should be applied.
|
| 183 |
+
config (`LayerSkipConfig`):
|
| 184 |
+
The configuration for the layer skip hook.
|
| 185 |
+
|
| 186 |
+
Example:
|
| 187 |
+
|
| 188 |
+
```python
|
| 189 |
+
>>> from diffusers import apply_layer_skip_hook, CogVideoXTransformer3DModel, LayerSkipConfig
|
| 190 |
+
|
| 191 |
+
>>> transformer = CogVideoXTransformer3DModel.from_pretrained("THUDM/CogVideoX-5b", torch_dtype=torch.bfloat16)
|
| 192 |
+
>>> config = LayerSkipConfig(layer_index=[10, 20], fqn="transformer_blocks")
|
| 193 |
+
>>> apply_layer_skip_hook(transformer, config)
|
| 194 |
+
```
|
| 195 |
+
"""
|
| 196 |
+
_apply_layer_skip_hook(module, config)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _apply_layer_skip_hook(module: torch.nn.Module, config: LayerSkipConfig, name: str | None = None) -> None:
|
| 200 |
+
name = name or _LAYER_SKIP_HOOK
|
| 201 |
+
|
| 202 |
+
if config.skip_attention and config.skip_attention_scores:
|
| 203 |
+
raise ValueError("Cannot set both `skip_attention` and `skip_attention_scores` to True. Please choose one.")
|
| 204 |
+
if not math.isclose(config.dropout, 1.0) and config.skip_attention_scores:
|
| 205 |
+
raise ValueError(
|
| 206 |
+
"Cannot set `skip_attention_scores` to True when `dropout` is not 1.0. Please set `dropout` to 1.0."
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if config.fqn == "auto":
|
| 210 |
+
for identifier in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS:
|
| 211 |
+
if hasattr(module, identifier):
|
| 212 |
+
config.fqn = identifier
|
| 213 |
+
break
|
| 214 |
+
else:
|
| 215 |
+
raise ValueError(
|
| 216 |
+
"Could not find a suitable identifier for the transformer blocks automatically. Please provide a valid "
|
| 217 |
+
"`fqn` (fully qualified name) that identifies a stack of transformer blocks."
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
transformer_blocks = _get_submodule_from_fqn(module, config.fqn)
|
| 221 |
+
if transformer_blocks is None or not isinstance(transformer_blocks, torch.nn.ModuleList):
|
| 222 |
+
raise ValueError(
|
| 223 |
+
f"Could not find {config.fqn} in the provided module, or configured `fqn` (fully qualified name) does not identify "
|
| 224 |
+
f"a `torch.nn.ModuleList`. Please provide a valid `fqn` that identifies a stack of transformer blocks."
|
| 225 |
+
)
|
| 226 |
+
if len(config.indices) == 0:
|
| 227 |
+
raise ValueError("Layer index list is empty. Please provide a non-empty list of layer indices to skip.")
|
| 228 |
+
|
| 229 |
+
blocks_found = False
|
| 230 |
+
for i, block in enumerate(transformer_blocks):
|
| 231 |
+
if i not in config.indices:
|
| 232 |
+
continue
|
| 233 |
+
|
| 234 |
+
blocks_found = True
|
| 235 |
+
|
| 236 |
+
if config.skip_attention and config.skip_ff:
|
| 237 |
+
logger.debug(f"Applying TransformerBlockSkipHook to '{config.fqn}.{i}'")
|
| 238 |
+
registry = HookRegistry.check_if_exists_or_initialize(block)
|
| 239 |
+
hook = TransformerBlockSkipHook(config.dropout)
|
| 240 |
+
registry.register_hook(hook, name)
|
| 241 |
+
|
| 242 |
+
elif config.skip_attention or config.skip_attention_scores:
|
| 243 |
+
for submodule_name, submodule in block.named_modules():
|
| 244 |
+
if isinstance(submodule, _ATTENTION_CLASSES) and not submodule.is_cross_attention:
|
| 245 |
+
logger.debug(f"Applying AttentionProcessorSkipHook to '{config.fqn}.{i}.{submodule_name}'")
|
| 246 |
+
output_fn = AttentionProcessorRegistry.get(submodule.processor.__class__).skip_processor_output_fn
|
| 247 |
+
registry = HookRegistry.check_if_exists_or_initialize(submodule)
|
| 248 |
+
hook = AttentionProcessorSkipHook(output_fn, config.skip_attention_scores, config.dropout)
|
| 249 |
+
registry.register_hook(hook, name)
|
| 250 |
+
|
| 251 |
+
if config.skip_ff:
|
| 252 |
+
for submodule_name, submodule in block.named_modules():
|
| 253 |
+
if isinstance(submodule, _FEEDFORWARD_CLASSES):
|
| 254 |
+
logger.debug(f"Applying FeedForwardSkipHook to '{config.fqn}.{i}.{submodule_name}'")
|
| 255 |
+
registry = HookRegistry.check_if_exists_or_initialize(submodule)
|
| 256 |
+
hook = FeedForwardSkipHook(config.dropout)
|
| 257 |
+
registry.register_hook(hook, name)
|
| 258 |
+
|
| 259 |
+
if not blocks_found:
|
| 260 |
+
raise ValueError(
|
| 261 |
+
f"Could not find any transformer blocks matching the provided indices {config.indices} and "
|
| 262 |
+
f"fully qualified name '{config.fqn}'. Please check the indices and fqn for correctness."
|
| 263 |
+
)
|
diffusers/hooks/layerwise_casting.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import re
|
| 16 |
+
from typing import Type
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ..utils import get_logger, is_peft_available
|
| 21 |
+
from ._common import _GO_LC_SUPPORTED_PYTORCH_LAYERS
|
| 22 |
+
from .hooks import HookRegistry, ModelHook
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# fmt: off
|
| 29 |
+
_LAYERWISE_CASTING_HOOK = "layerwise_casting"
|
| 30 |
+
_PEFT_AUTOCAST_DISABLE_HOOK = "peft_autocast_disable"
|
| 31 |
+
DEFAULT_SKIP_MODULES_PATTERN = ("pos_embed", "patch_embed", "norm", "^proj_in$", "^proj_out$")
|
| 32 |
+
# fmt: on
|
| 33 |
+
|
| 34 |
+
_SHOULD_DISABLE_PEFT_INPUT_AUTOCAST = is_peft_available()
|
| 35 |
+
if _SHOULD_DISABLE_PEFT_INPUT_AUTOCAST:
|
| 36 |
+
from peft.helpers import disable_input_dtype_casting
|
| 37 |
+
from peft.tuners.tuners_utils import BaseTunerLayer
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class LayerwiseCastingHook(ModelHook):
|
| 41 |
+
r"""
|
| 42 |
+
A hook that casts the weights of a module to a high precision dtype for computation, and to a low precision dtype
|
| 43 |
+
for storage. This process may lead to quality loss in the output, but can significantly reduce the memory
|
| 44 |
+
footprint.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
_is_stateful = False
|
| 48 |
+
|
| 49 |
+
def __init__(self, storage_dtype: torch.dtype, compute_dtype: torch.dtype, non_blocking: bool) -> None:
|
| 50 |
+
self.storage_dtype = storage_dtype
|
| 51 |
+
self.compute_dtype = compute_dtype
|
| 52 |
+
self.non_blocking = non_blocking
|
| 53 |
+
|
| 54 |
+
def initialize_hook(self, module: torch.nn.Module):
|
| 55 |
+
module.to(dtype=self.storage_dtype, non_blocking=self.non_blocking)
|
| 56 |
+
return module
|
| 57 |
+
|
| 58 |
+
def deinitalize_hook(self, module: torch.nn.Module):
|
| 59 |
+
raise NotImplementedError(
|
| 60 |
+
"LayerwiseCastingHook does not support deinitialization. A model once enabled with layerwise casting will "
|
| 61 |
+
"have casted its weights to a lower precision dtype for storage. Casting this back to the original dtype "
|
| 62 |
+
"will lead to precision loss, which might have an impact on the model's generation quality. The model should "
|
| 63 |
+
"be re-initialized and loaded in the original dtype."
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
def pre_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 67 |
+
module.to(dtype=self.compute_dtype, non_blocking=self.non_blocking)
|
| 68 |
+
return args, kwargs
|
| 69 |
+
|
| 70 |
+
def post_forward(self, module: torch.nn.Module, output):
|
| 71 |
+
module.to(dtype=self.storage_dtype, non_blocking=self.non_blocking)
|
| 72 |
+
return output
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class PeftInputAutocastDisableHook(ModelHook):
|
| 76 |
+
r"""
|
| 77 |
+
A hook that disables the casting of inputs to the module weight dtype during the forward pass. By default, PEFT
|
| 78 |
+
casts the inputs to the weight dtype of the module, which can lead to precision loss.
|
| 79 |
+
|
| 80 |
+
The reasons for needing this are:
|
| 81 |
+
- If we don't add PEFT layers' weight names to `skip_modules_pattern` when applying layerwise casting, the
|
| 82 |
+
inputs will be casted to the, possibly lower precision, storage dtype. Reference:
|
| 83 |
+
https://github.com/huggingface/peft/blob/0facdebf6208139cbd8f3586875acb378813dd97/src/peft/tuners/lora/layer.py#L706
|
| 84 |
+
- We can, on our end, use something like accelerate's `send_to_device` but for dtypes. This way, we can ensure
|
| 85 |
+
that the inputs are casted to the computation dtype correctly always. However, there are two goals we are
|
| 86 |
+
hoping to achieve:
|
| 87 |
+
1. Making forward implementations independent of device/dtype casting operations as much as possible.
|
| 88 |
+
2. Performing inference without losing information from casting to different precisions. With the current
|
| 89 |
+
PEFT implementation (as linked in the reference above), and assuming running layerwise casting inference
|
| 90 |
+
with storage_dtype=torch.float8_e4m3fn and compute_dtype=torch.bfloat16, inputs are cast to
|
| 91 |
+
torch.float8_e4m3fn in the lora layer. We will then upcast back to torch.bfloat16 when we continue the
|
| 92 |
+
forward pass in PEFT linear forward or Diffusers layer forward, with a `send_to_dtype` operation from
|
| 93 |
+
LayerwiseCastingHook. This will be a lossy operation and result in poorer generation quality.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 97 |
+
with disable_input_dtype_casting(module):
|
| 98 |
+
return self.fn_ref.original_forward(*args, **kwargs)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def apply_layerwise_casting(
|
| 102 |
+
module: torch.nn.Module,
|
| 103 |
+
storage_dtype: torch.dtype,
|
| 104 |
+
compute_dtype: torch.dtype,
|
| 105 |
+
skip_modules_pattern: str | tuple[str, ...] = "auto",
|
| 106 |
+
skip_modules_classes: tuple[Type[torch.nn.Module], ...] | None = None,
|
| 107 |
+
non_blocking: bool = False,
|
| 108 |
+
) -> None:
|
| 109 |
+
r"""
|
| 110 |
+
Applies layerwise casting to a given module. The module expected here is a Diffusers ModelMixin but it can be any
|
| 111 |
+
nn.Module using diffusers layers or pytorch primitives.
|
| 112 |
+
|
| 113 |
+
Example:
|
| 114 |
+
|
| 115 |
+
```python
|
| 116 |
+
>>> import torch
|
| 117 |
+
>>> from diffusers import CogVideoXTransformer3DModel
|
| 118 |
+
|
| 119 |
+
>>> transformer = CogVideoXTransformer3DModel.from_pretrained(
|
| 120 |
+
... model_id, subfolder="transformer", torch_dtype=torch.bfloat16
|
| 121 |
+
... )
|
| 122 |
+
|
| 123 |
+
>>> apply_layerwise_casting(
|
| 124 |
+
... transformer,
|
| 125 |
+
... storage_dtype=torch.float8_e4m3fn,
|
| 126 |
+
... compute_dtype=torch.bfloat16,
|
| 127 |
+
... skip_modules_pattern=["patch_embed", "norm", "proj_out"],
|
| 128 |
+
... non_blocking=True,
|
| 129 |
+
... )
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
module (`torch.nn.Module`):
|
| 134 |
+
The module whose leaf modules will be cast to a high precision dtype for computation, and to a low
|
| 135 |
+
precision dtype for storage.
|
| 136 |
+
storage_dtype (`torch.dtype`):
|
| 137 |
+
The dtype to cast the module to before/after the forward pass for storage.
|
| 138 |
+
compute_dtype (`torch.dtype`):
|
| 139 |
+
The dtype to cast the module to during the forward pass for computation.
|
| 140 |
+
skip_modules_pattern (`tuple[str, ...]`, defaults to `"auto"`):
|
| 141 |
+
A list of patterns to match the names of the modules to skip during the layerwise casting process. If set
|
| 142 |
+
to `"auto"`, the default patterns are used. If set to `None`, no modules are skipped. If set to `None`
|
| 143 |
+
alongside `skip_modules_classes` being `None`, the layerwise casting is applied directly to the module
|
| 144 |
+
instead of its internal submodules.
|
| 145 |
+
skip_modules_classes (`tuple[Type[torch.nn.Module], ...]`, defaults to `None`):
|
| 146 |
+
A list of module classes to skip during the layerwise casting process.
|
| 147 |
+
non_blocking (`bool`, defaults to `False`):
|
| 148 |
+
If `True`, the weight casting operations are non-blocking.
|
| 149 |
+
"""
|
| 150 |
+
if skip_modules_pattern == "auto":
|
| 151 |
+
skip_modules_pattern = DEFAULT_SKIP_MODULES_PATTERN
|
| 152 |
+
|
| 153 |
+
if skip_modules_classes is None and skip_modules_pattern is None:
|
| 154 |
+
apply_layerwise_casting_hook(module, storage_dtype, compute_dtype, non_blocking)
|
| 155 |
+
return
|
| 156 |
+
|
| 157 |
+
_apply_layerwise_casting(
|
| 158 |
+
module,
|
| 159 |
+
storage_dtype,
|
| 160 |
+
compute_dtype,
|
| 161 |
+
skip_modules_pattern,
|
| 162 |
+
skip_modules_classes,
|
| 163 |
+
non_blocking,
|
| 164 |
+
)
|
| 165 |
+
_disable_peft_input_autocast(module)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _apply_layerwise_casting(
|
| 169 |
+
module: torch.nn.Module,
|
| 170 |
+
storage_dtype: torch.dtype,
|
| 171 |
+
compute_dtype: torch.dtype,
|
| 172 |
+
skip_modules_pattern: tuple[str, ...] | None = None,
|
| 173 |
+
skip_modules_classes: tuple[Type[torch.nn.Module], ...] | None = None,
|
| 174 |
+
non_blocking: bool = False,
|
| 175 |
+
_prefix: str = "",
|
| 176 |
+
) -> None:
|
| 177 |
+
should_skip = (skip_modules_classes is not None and isinstance(module, skip_modules_classes)) or (
|
| 178 |
+
skip_modules_pattern is not None and any(re.search(pattern, _prefix) for pattern in skip_modules_pattern)
|
| 179 |
+
)
|
| 180 |
+
if should_skip:
|
| 181 |
+
logger.debug(f'Skipping layerwise casting for layer "{_prefix}"')
|
| 182 |
+
return
|
| 183 |
+
|
| 184 |
+
if isinstance(module, _GO_LC_SUPPORTED_PYTORCH_LAYERS):
|
| 185 |
+
logger.debug(f'Applying layerwise casting to layer "{_prefix}"')
|
| 186 |
+
apply_layerwise_casting_hook(module, storage_dtype, compute_dtype, non_blocking)
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
for name, submodule in module.named_children():
|
| 190 |
+
layer_name = f"{_prefix}.{name}" if _prefix else name
|
| 191 |
+
_apply_layerwise_casting(
|
| 192 |
+
submodule,
|
| 193 |
+
storage_dtype,
|
| 194 |
+
compute_dtype,
|
| 195 |
+
skip_modules_pattern,
|
| 196 |
+
skip_modules_classes,
|
| 197 |
+
non_blocking,
|
| 198 |
+
_prefix=layer_name,
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def apply_layerwise_casting_hook(
|
| 203 |
+
module: torch.nn.Module, storage_dtype: torch.dtype, compute_dtype: torch.dtype, non_blocking: bool
|
| 204 |
+
) -> None:
|
| 205 |
+
r"""
|
| 206 |
+
Applies a `LayerwiseCastingHook` to a given module.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
module (`torch.nn.Module`):
|
| 210 |
+
The module to attach the hook to.
|
| 211 |
+
storage_dtype (`torch.dtype`):
|
| 212 |
+
The dtype to cast the module to before the forward pass.
|
| 213 |
+
compute_dtype (`torch.dtype`):
|
| 214 |
+
The dtype to cast the module to during the forward pass.
|
| 215 |
+
non_blocking (`bool`):
|
| 216 |
+
If `True`, the weight casting operations are non-blocking.
|
| 217 |
+
"""
|
| 218 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 219 |
+
hook = LayerwiseCastingHook(storage_dtype, compute_dtype, non_blocking)
|
| 220 |
+
registry.register_hook(hook, _LAYERWISE_CASTING_HOOK)
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _is_layerwise_casting_active(module: torch.nn.Module) -> bool:
|
| 224 |
+
for submodule in module.modules():
|
| 225 |
+
if (
|
| 226 |
+
hasattr(submodule, "_diffusers_hook")
|
| 227 |
+
and submodule._diffusers_hook.get_hook(_LAYERWISE_CASTING_HOOK) is not None
|
| 228 |
+
):
|
| 229 |
+
return True
|
| 230 |
+
return False
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _disable_peft_input_autocast(module: torch.nn.Module) -> None:
|
| 234 |
+
if not _SHOULD_DISABLE_PEFT_INPUT_AUTOCAST:
|
| 235 |
+
return
|
| 236 |
+
for submodule in module.modules():
|
| 237 |
+
if isinstance(submodule, BaseTunerLayer) and _is_layerwise_casting_active(submodule):
|
| 238 |
+
registry = HookRegistry.check_if_exists_or_initialize(submodule)
|
| 239 |
+
hook = PeftInputAutocastDisableHook()
|
| 240 |
+
registry.register_hook(hook, _PEFT_AUTOCAST_DISABLE_HOOK)
|
diffusers/hooks/mag_cache.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import List, Optional, Tuple, Union
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ..utils import get_logger
|
| 21 |
+
from ..utils.torch_utils import unwrap_module
|
| 22 |
+
from ._common import _ALL_TRANSFORMER_BLOCK_IDENTIFIERS
|
| 23 |
+
from ._helpers import TransformerBlockRegistry
|
| 24 |
+
from .hooks import BaseState, HookRegistry, ModelHook, StateManager
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 28 |
+
|
| 29 |
+
_MAG_CACHE_LEADER_BLOCK_HOOK = "mag_cache_leader_block_hook"
|
| 30 |
+
_MAG_CACHE_BLOCK_HOOK = "mag_cache_block_hook"
|
| 31 |
+
|
| 32 |
+
# Default Mag Ratios for Flux models (Dev/Schnell) are provided for convenience.
|
| 33 |
+
# Users must explicitly pass these to the config if using Flux.
|
| 34 |
+
# Reference: https://github.com/Zehong-Ma/MagCache
|
| 35 |
+
FLUX_MAG_RATIOS = torch.tensor(
|
| 36 |
+
[1.0]
|
| 37 |
+
+ [
|
| 38 |
+
1.21094,
|
| 39 |
+
1.11719,
|
| 40 |
+
1.07812,
|
| 41 |
+
1.0625,
|
| 42 |
+
1.03906,
|
| 43 |
+
1.03125,
|
| 44 |
+
1.03906,
|
| 45 |
+
1.02344,
|
| 46 |
+
1.03125,
|
| 47 |
+
1.02344,
|
| 48 |
+
0.98047,
|
| 49 |
+
1.01562,
|
| 50 |
+
1.00781,
|
| 51 |
+
1.0,
|
| 52 |
+
1.00781,
|
| 53 |
+
1.0,
|
| 54 |
+
1.00781,
|
| 55 |
+
1.0,
|
| 56 |
+
1.0,
|
| 57 |
+
0.99609,
|
| 58 |
+
0.99609,
|
| 59 |
+
0.98047,
|
| 60 |
+
0.98828,
|
| 61 |
+
0.96484,
|
| 62 |
+
0.95703,
|
| 63 |
+
0.93359,
|
| 64 |
+
0.89062,
|
| 65 |
+
]
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def nearest_interp(src_array: torch.Tensor, target_length: int) -> torch.Tensor:
|
| 70 |
+
"""
|
| 71 |
+
Interpolate the source array to the target length using nearest neighbor interpolation.
|
| 72 |
+
"""
|
| 73 |
+
src_length = len(src_array)
|
| 74 |
+
if target_length == 1:
|
| 75 |
+
return src_array[-1:]
|
| 76 |
+
|
| 77 |
+
scale = (src_length - 1) / (target_length - 1)
|
| 78 |
+
grid = torch.arange(target_length, device=src_array.device, dtype=torch.float32)
|
| 79 |
+
mapped_indices = torch.round(grid * scale).long()
|
| 80 |
+
return src_array[mapped_indices]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@dataclass
|
| 84 |
+
class MagCacheConfig:
|
| 85 |
+
r"""
|
| 86 |
+
Configuration for [MagCache](https://github.com/Zehong-Ma/MagCache).
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
threshold (`float`, defaults to `0.06`):
|
| 90 |
+
The threshold for the accumulated error. If the accumulated error is below this threshold, the block
|
| 91 |
+
computation is skipped. A higher threshold allows for more aggressive skipping (faster) but may degrade
|
| 92 |
+
quality.
|
| 93 |
+
max_skip_steps (`int`, defaults to `3`):
|
| 94 |
+
The maximum number of consecutive steps that can be skipped (K in the paper).
|
| 95 |
+
retention_ratio (`float`, defaults to `0.2`):
|
| 96 |
+
The fraction of initial steps during which skipping is disabled to ensure stability. For example, if
|
| 97 |
+
`num_inference_steps` is 28 and `retention_ratio` is 0.2, the first 6 steps will never be skipped.
|
| 98 |
+
num_inference_steps (`int`, defaults to `28`):
|
| 99 |
+
The number of inference steps used in the pipeline. This is required to interpolate `mag_ratios` correctly.
|
| 100 |
+
mag_ratios (`torch.Tensor`, *optional*):
|
| 101 |
+
The pre-computed magnitude ratios for the model. These are checkpoint-dependent. If not provided, you must
|
| 102 |
+
set `calibrate=True` to calculate them for your specific model. For Flux models, you can use
|
| 103 |
+
`diffusers.hooks.mag_cache.FLUX_MAG_RATIOS`.
|
| 104 |
+
calibrate (`bool`, defaults to `False`):
|
| 105 |
+
If True, enables calibration mode. In this mode, no blocks are skipped. Instead, the hook calculates the
|
| 106 |
+
magnitude ratios for the current run and logs them at the end. Use this to obtain `mag_ratios` for new
|
| 107 |
+
models or schedulers.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
threshold: float = 0.06
|
| 111 |
+
max_skip_steps: int = 3
|
| 112 |
+
retention_ratio: float = 0.2
|
| 113 |
+
num_inference_steps: int = 28
|
| 114 |
+
mag_ratios: Optional[Union[torch.Tensor, List[float]]] = None
|
| 115 |
+
calibrate: bool = False
|
| 116 |
+
|
| 117 |
+
def __post_init__(self):
|
| 118 |
+
# User MUST provide ratios OR enable calibration.
|
| 119 |
+
if self.mag_ratios is None and not self.calibrate:
|
| 120 |
+
raise ValueError(
|
| 121 |
+
" `mag_ratios` must be provided for MagCache inference because these ratios are model-dependent.\n"
|
| 122 |
+
"To get them for your model:\n"
|
| 123 |
+
"1. Initialize `MagCacheConfig(calibrate=True, ...)`\n"
|
| 124 |
+
"2. Run inference on your model once.\n"
|
| 125 |
+
"3. Copy the printed ratios array and pass it to `mag_ratios` in the config.\n"
|
| 126 |
+
"For Flux models, you can import `FLUX_MAG_RATIOS` from `diffusers.hooks.mag_cache`."
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
if not self.calibrate and self.mag_ratios is not None:
|
| 130 |
+
if not torch.is_tensor(self.mag_ratios):
|
| 131 |
+
self.mag_ratios = torch.tensor(self.mag_ratios)
|
| 132 |
+
|
| 133 |
+
if len(self.mag_ratios) != self.num_inference_steps:
|
| 134 |
+
logger.debug(
|
| 135 |
+
f"Interpolating mag_ratios from length {len(self.mag_ratios)} to {self.num_inference_steps}"
|
| 136 |
+
)
|
| 137 |
+
self.mag_ratios = nearest_interp(self.mag_ratios, self.num_inference_steps)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class MagCacheState(BaseState):
|
| 141 |
+
def __init__(self) -> None:
|
| 142 |
+
super().__init__()
|
| 143 |
+
# Cache for the residual (output - input) from the *previous* timestep
|
| 144 |
+
self.previous_residual: torch.Tensor = None
|
| 145 |
+
|
| 146 |
+
# State inputs/outputs for the current forward pass
|
| 147 |
+
self.head_block_input: Union[torch.Tensor, Tuple[torch.Tensor, ...]] = None
|
| 148 |
+
self.should_compute: bool = True
|
| 149 |
+
|
| 150 |
+
# MagCache accumulators
|
| 151 |
+
self.accumulated_ratio: float = 1.0
|
| 152 |
+
self.accumulated_err: float = 0.0
|
| 153 |
+
self.accumulated_steps: int = 0
|
| 154 |
+
|
| 155 |
+
# Current step counter (timestep index)
|
| 156 |
+
self.step_index: int = 0
|
| 157 |
+
|
| 158 |
+
# Calibration storage
|
| 159 |
+
self.calibration_ratios: List[float] = []
|
| 160 |
+
|
| 161 |
+
def reset(self):
|
| 162 |
+
self.previous_residual = None
|
| 163 |
+
self.should_compute = True
|
| 164 |
+
self.accumulated_ratio = 1.0
|
| 165 |
+
self.accumulated_err = 0.0
|
| 166 |
+
self.accumulated_steps = 0
|
| 167 |
+
self.step_index = 0
|
| 168 |
+
self.calibration_ratios = []
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class MagCacheHeadHook(ModelHook):
|
| 172 |
+
_is_stateful = True
|
| 173 |
+
|
| 174 |
+
def __init__(self, state_manager: StateManager, config: MagCacheConfig):
|
| 175 |
+
self.state_manager = state_manager
|
| 176 |
+
self.config = config
|
| 177 |
+
self._metadata = None
|
| 178 |
+
|
| 179 |
+
def initialize_hook(self, module):
|
| 180 |
+
unwrapped_module = unwrap_module(module)
|
| 181 |
+
self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
|
| 182 |
+
return module
|
| 183 |
+
|
| 184 |
+
@torch.compiler.disable
|
| 185 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 186 |
+
if self.state_manager._current_context is None:
|
| 187 |
+
self.state_manager.set_context("inference")
|
| 188 |
+
|
| 189 |
+
arg_name = self._metadata.hidden_states_argument_name
|
| 190 |
+
hidden_states = self._metadata._get_parameter_from_args_kwargs(arg_name, args, kwargs)
|
| 191 |
+
|
| 192 |
+
state: MagCacheState = self.state_manager.get_state()
|
| 193 |
+
state.head_block_input = hidden_states
|
| 194 |
+
|
| 195 |
+
should_compute = True
|
| 196 |
+
|
| 197 |
+
if self.config.calibrate:
|
| 198 |
+
# Never skip during calibration
|
| 199 |
+
should_compute = True
|
| 200 |
+
else:
|
| 201 |
+
# MagCache Logic
|
| 202 |
+
current_step = state.step_index
|
| 203 |
+
if current_step >= len(self.config.mag_ratios):
|
| 204 |
+
current_scale = 1.0
|
| 205 |
+
else:
|
| 206 |
+
current_scale = self.config.mag_ratios[current_step]
|
| 207 |
+
|
| 208 |
+
retention_step = int(self.config.retention_ratio * self.config.num_inference_steps + 0.5)
|
| 209 |
+
|
| 210 |
+
if current_step >= retention_step:
|
| 211 |
+
state.accumulated_ratio *= current_scale
|
| 212 |
+
state.accumulated_steps += 1
|
| 213 |
+
state.accumulated_err += abs(1.0 - state.accumulated_ratio)
|
| 214 |
+
|
| 215 |
+
if (
|
| 216 |
+
state.previous_residual is not None
|
| 217 |
+
and state.accumulated_err <= self.config.threshold
|
| 218 |
+
and state.accumulated_steps <= self.config.max_skip_steps
|
| 219 |
+
):
|
| 220 |
+
should_compute = False
|
| 221 |
+
else:
|
| 222 |
+
state.accumulated_ratio = 1.0
|
| 223 |
+
state.accumulated_steps = 0
|
| 224 |
+
state.accumulated_err = 0.0
|
| 225 |
+
|
| 226 |
+
state.should_compute = should_compute
|
| 227 |
+
|
| 228 |
+
if not should_compute:
|
| 229 |
+
logger.debug(f"MagCache: Skipping step {state.step_index}")
|
| 230 |
+
# Apply MagCache: Output = Input + Previous Residual
|
| 231 |
+
|
| 232 |
+
output = hidden_states
|
| 233 |
+
res = state.previous_residual
|
| 234 |
+
|
| 235 |
+
if res.device != output.device:
|
| 236 |
+
res = res.to(output.device)
|
| 237 |
+
|
| 238 |
+
# Attempt to apply residual handling shape mismatches (e.g., text+image vs image only)
|
| 239 |
+
if res.shape == output.shape:
|
| 240 |
+
output = output + res
|
| 241 |
+
elif (
|
| 242 |
+
output.ndim == 3
|
| 243 |
+
and res.ndim == 3
|
| 244 |
+
and output.shape[0] == res.shape[0]
|
| 245 |
+
and output.shape[2] == res.shape[2]
|
| 246 |
+
):
|
| 247 |
+
# Assuming concatenation where image part is at the end (standard in Flux/SD3)
|
| 248 |
+
diff = output.shape[1] - res.shape[1]
|
| 249 |
+
if diff > 0:
|
| 250 |
+
output = output.clone()
|
| 251 |
+
output[:, diff:, :] = output[:, diff:, :] + res
|
| 252 |
+
else:
|
| 253 |
+
logger.warning(
|
| 254 |
+
f"MagCache: Dimension mismatch. Input {output.shape}, Residual {res.shape}. "
|
| 255 |
+
"Cannot apply residual safely. Returning input without residual."
|
| 256 |
+
)
|
| 257 |
+
else:
|
| 258 |
+
logger.warning(
|
| 259 |
+
f"MagCache: Dimension mismatch. Input {output.shape}, Residual {res.shape}. "
|
| 260 |
+
"Cannot apply residual safely. Returning input without residual."
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
if self._metadata.return_encoder_hidden_states_index is not None:
|
| 264 |
+
original_encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
|
| 265 |
+
"encoder_hidden_states", args, kwargs
|
| 266 |
+
)
|
| 267 |
+
max_idx = max(
|
| 268 |
+
self._metadata.return_hidden_states_index, self._metadata.return_encoder_hidden_states_index
|
| 269 |
+
)
|
| 270 |
+
ret_list = [None] * (max_idx + 1)
|
| 271 |
+
ret_list[self._metadata.return_hidden_states_index] = output
|
| 272 |
+
ret_list[self._metadata.return_encoder_hidden_states_index] = original_encoder_hidden_states
|
| 273 |
+
return tuple(ret_list)
|
| 274 |
+
else:
|
| 275 |
+
return output
|
| 276 |
+
|
| 277 |
+
else:
|
| 278 |
+
# Compute original forward
|
| 279 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 280 |
+
return output
|
| 281 |
+
|
| 282 |
+
def reset_state(self, module):
|
| 283 |
+
self.state_manager.reset()
|
| 284 |
+
return module
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
class MagCacheBlockHook(ModelHook):
|
| 288 |
+
def __init__(self, state_manager: StateManager, is_tail: bool = False, config: MagCacheConfig = None):
|
| 289 |
+
super().__init__()
|
| 290 |
+
self.state_manager = state_manager
|
| 291 |
+
self.is_tail = is_tail
|
| 292 |
+
self.config = config
|
| 293 |
+
self._metadata = None
|
| 294 |
+
|
| 295 |
+
def initialize_hook(self, module):
|
| 296 |
+
unwrapped_module = unwrap_module(module)
|
| 297 |
+
self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
|
| 298 |
+
return module
|
| 299 |
+
|
| 300 |
+
@torch.compiler.disable
|
| 301 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 302 |
+
if self.state_manager._current_context is None:
|
| 303 |
+
self.state_manager.set_context("inference")
|
| 304 |
+
state: MagCacheState = self.state_manager.get_state()
|
| 305 |
+
|
| 306 |
+
if not state.should_compute:
|
| 307 |
+
arg_name = self._metadata.hidden_states_argument_name
|
| 308 |
+
hidden_states = self._metadata._get_parameter_from_args_kwargs(arg_name, args, kwargs)
|
| 309 |
+
|
| 310 |
+
if self.is_tail:
|
| 311 |
+
# Still need to advance step index even if we skip
|
| 312 |
+
self._advance_step(state)
|
| 313 |
+
|
| 314 |
+
if self._metadata.return_encoder_hidden_states_index is not None:
|
| 315 |
+
encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
|
| 316 |
+
"encoder_hidden_states", args, kwargs
|
| 317 |
+
)
|
| 318 |
+
max_idx = max(
|
| 319 |
+
self._metadata.return_hidden_states_index, self._metadata.return_encoder_hidden_states_index
|
| 320 |
+
)
|
| 321 |
+
ret_list = [None] * (max_idx + 1)
|
| 322 |
+
ret_list[self._metadata.return_hidden_states_index] = hidden_states
|
| 323 |
+
ret_list[self._metadata.return_encoder_hidden_states_index] = encoder_hidden_states
|
| 324 |
+
return tuple(ret_list)
|
| 325 |
+
|
| 326 |
+
return hidden_states
|
| 327 |
+
|
| 328 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 329 |
+
|
| 330 |
+
if self.is_tail:
|
| 331 |
+
# Calculate residual for next steps
|
| 332 |
+
if isinstance(output, tuple):
|
| 333 |
+
out_hidden = output[self._metadata.return_hidden_states_index]
|
| 334 |
+
else:
|
| 335 |
+
out_hidden = output
|
| 336 |
+
|
| 337 |
+
in_hidden = state.head_block_input
|
| 338 |
+
|
| 339 |
+
if in_hidden is None:
|
| 340 |
+
return output
|
| 341 |
+
|
| 342 |
+
# Determine residual
|
| 343 |
+
if out_hidden.shape == in_hidden.shape:
|
| 344 |
+
residual = out_hidden - in_hidden
|
| 345 |
+
elif out_hidden.ndim == 3 and in_hidden.ndim == 3 and out_hidden.shape[2] == in_hidden.shape[2]:
|
| 346 |
+
diff = in_hidden.shape[1] - out_hidden.shape[1]
|
| 347 |
+
if diff == 0:
|
| 348 |
+
residual = out_hidden - in_hidden
|
| 349 |
+
else:
|
| 350 |
+
residual = out_hidden - in_hidden # Fallback to matching tail
|
| 351 |
+
else:
|
| 352 |
+
# Fallback for completely mismatched shapes
|
| 353 |
+
residual = out_hidden
|
| 354 |
+
|
| 355 |
+
if self.config.calibrate:
|
| 356 |
+
self._perform_calibration_step(state, residual)
|
| 357 |
+
|
| 358 |
+
state.previous_residual = residual
|
| 359 |
+
self._advance_step(state)
|
| 360 |
+
|
| 361 |
+
return output
|
| 362 |
+
|
| 363 |
+
def _perform_calibration_step(self, state: MagCacheState, current_residual: torch.Tensor):
|
| 364 |
+
if state.previous_residual is None:
|
| 365 |
+
# First step has no previous residual to compare against.
|
| 366 |
+
# log 1.0 as a neutral starting point.
|
| 367 |
+
ratio = 1.0
|
| 368 |
+
else:
|
| 369 |
+
# MagCache Calibration Formula: mean(norm(curr) / norm(prev))
|
| 370 |
+
# norm(dim=-1) gives magnitude of each token vector
|
| 371 |
+
curr_norm = torch.linalg.norm(current_residual.float(), dim=-1)
|
| 372 |
+
prev_norm = torch.linalg.norm(state.previous_residual.float(), dim=-1)
|
| 373 |
+
|
| 374 |
+
# Avoid division by zero
|
| 375 |
+
ratio = (curr_norm / (prev_norm + 1e-8)).mean().item()
|
| 376 |
+
|
| 377 |
+
state.calibration_ratios.append(ratio)
|
| 378 |
+
|
| 379 |
+
def _advance_step(self, state: MagCacheState):
|
| 380 |
+
state.step_index += 1
|
| 381 |
+
if state.step_index >= self.config.num_inference_steps:
|
| 382 |
+
# End of inference loop
|
| 383 |
+
if self.config.calibrate:
|
| 384 |
+
print("\n[MagCache] Calibration Complete. Copy these values to MagCacheConfig(mag_ratios=...):")
|
| 385 |
+
print(f"{state.calibration_ratios}\n")
|
| 386 |
+
logger.info(f"MagCache Calibration Results: {state.calibration_ratios}")
|
| 387 |
+
|
| 388 |
+
# Reset state
|
| 389 |
+
state.step_index = 0
|
| 390 |
+
state.accumulated_ratio = 1.0
|
| 391 |
+
state.accumulated_steps = 0
|
| 392 |
+
state.accumulated_err = 0.0
|
| 393 |
+
state.previous_residual = None
|
| 394 |
+
state.calibration_ratios = []
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def apply_mag_cache(module: torch.nn.Module, config: MagCacheConfig) -> None:
|
| 398 |
+
"""
|
| 399 |
+
Applies MagCache to a given module (typically a Transformer).
|
| 400 |
+
|
| 401 |
+
Args:
|
| 402 |
+
module (`torch.nn.Module`):
|
| 403 |
+
The module to apply MagCache to.
|
| 404 |
+
config (`MagCacheConfig`):
|
| 405 |
+
The configuration for MagCache.
|
| 406 |
+
"""
|
| 407 |
+
# Initialize registry on the root module so the Pipeline can set context.
|
| 408 |
+
HookRegistry.check_if_exists_or_initialize(module)
|
| 409 |
+
|
| 410 |
+
state_manager = StateManager(MagCacheState, (), {})
|
| 411 |
+
remaining_blocks = []
|
| 412 |
+
|
| 413 |
+
for name, submodule in module.named_children():
|
| 414 |
+
if name not in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS or not isinstance(submodule, torch.nn.ModuleList):
|
| 415 |
+
continue
|
| 416 |
+
for index, block in enumerate(submodule):
|
| 417 |
+
remaining_blocks.append((f"{name}.{index}", block))
|
| 418 |
+
|
| 419 |
+
if not remaining_blocks:
|
| 420 |
+
logger.warning("MagCache: No transformer blocks found to apply hooks.")
|
| 421 |
+
return
|
| 422 |
+
|
| 423 |
+
# Handle single-block models
|
| 424 |
+
if len(remaining_blocks) == 1:
|
| 425 |
+
name, block = remaining_blocks[0]
|
| 426 |
+
logger.info(f"MagCache: Applying Head+Tail Hooks to single block '{name}'")
|
| 427 |
+
_apply_mag_cache_block_hook(block, state_manager, config, is_tail=True)
|
| 428 |
+
_apply_mag_cache_head_hook(block, state_manager, config)
|
| 429 |
+
return
|
| 430 |
+
|
| 431 |
+
head_block_name, head_block = remaining_blocks.pop(0)
|
| 432 |
+
tail_block_name, tail_block = remaining_blocks.pop(-1)
|
| 433 |
+
|
| 434 |
+
logger.info(f"MagCache: Applying Head Hook to {head_block_name}")
|
| 435 |
+
_apply_mag_cache_head_hook(head_block, state_manager, config)
|
| 436 |
+
|
| 437 |
+
for name, block in remaining_blocks:
|
| 438 |
+
_apply_mag_cache_block_hook(block, state_manager, config)
|
| 439 |
+
|
| 440 |
+
logger.info(f"MagCache: Applying Tail Hook to {tail_block_name}")
|
| 441 |
+
_apply_mag_cache_block_hook(tail_block, state_manager, config, is_tail=True)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def _apply_mag_cache_head_hook(block: torch.nn.Module, state_manager: StateManager, config: MagCacheConfig) -> None:
|
| 445 |
+
registry = HookRegistry.check_if_exists_or_initialize(block)
|
| 446 |
+
|
| 447 |
+
# Automatically remove existing hook to allow re-application (e.g. switching modes)
|
| 448 |
+
if registry.get_hook(_MAG_CACHE_LEADER_BLOCK_HOOK) is not None:
|
| 449 |
+
registry.remove_hook(_MAG_CACHE_LEADER_BLOCK_HOOK)
|
| 450 |
+
|
| 451 |
+
hook = MagCacheHeadHook(state_manager, config)
|
| 452 |
+
registry.register_hook(hook, _MAG_CACHE_LEADER_BLOCK_HOOK)
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def _apply_mag_cache_block_hook(
|
| 456 |
+
block: torch.nn.Module,
|
| 457 |
+
state_manager: StateManager,
|
| 458 |
+
config: MagCacheConfig,
|
| 459 |
+
is_tail: bool = False,
|
| 460 |
+
) -> None:
|
| 461 |
+
registry = HookRegistry.check_if_exists_or_initialize(block)
|
| 462 |
+
|
| 463 |
+
# Automatically remove existing hook to allow re-application
|
| 464 |
+
if registry.get_hook(_MAG_CACHE_BLOCK_HOOK) is not None:
|
| 465 |
+
registry.remove_hook(_MAG_CACHE_BLOCK_HOOK)
|
| 466 |
+
|
| 467 |
+
hook = MagCacheBlockHook(state_manager, is_tail, config)
|
| 468 |
+
registry.register_hook(hook, _MAG_CACHE_BLOCK_HOOK)
|
diffusers/hooks/pyramid_attention_broadcast.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import re
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from typing import Any, Callable
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
from ..models.attention import AttentionModuleMixin
|
| 22 |
+
from ..models.attention_processor import Attention, MochiAttention
|
| 23 |
+
from ..utils import logging
|
| 24 |
+
from ._common import (
|
| 25 |
+
_ATTENTION_CLASSES,
|
| 26 |
+
_CROSS_TRANSFORMER_BLOCK_IDENTIFIERS,
|
| 27 |
+
_SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS,
|
| 28 |
+
_TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS,
|
| 29 |
+
)
|
| 30 |
+
from .hooks import HookRegistry, ModelHook
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
_PYRAMID_ATTENTION_BROADCAST_HOOK = "pyramid_attention_broadcast"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class PyramidAttentionBroadcastConfig:
|
| 41 |
+
r"""
|
| 42 |
+
Configuration for Pyramid Attention Broadcast.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
spatial_attention_block_skip_range (`int`, *optional*, defaults to `None`):
|
| 46 |
+
The number of times a specific spatial attention broadcast is skipped before computing the attention states
|
| 47 |
+
to re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times (i.e.,
|
| 48 |
+
old attention states will be reused) before computing the new attention states again.
|
| 49 |
+
temporal_attention_block_skip_range (`int`, *optional*, defaults to `None`):
|
| 50 |
+
The number of times a specific temporal attention broadcast is skipped before computing the attention
|
| 51 |
+
states to re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times
|
| 52 |
+
(i.e., old attention states will be reused) before computing the new attention states again.
|
| 53 |
+
cross_attention_block_skip_range (`int`, *optional*, defaults to `None`):
|
| 54 |
+
The number of times a specific cross-attention broadcast is skipped before computing the attention states
|
| 55 |
+
to re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times (i.e.,
|
| 56 |
+
old attention states will be reused) before computing the new attention states again.
|
| 57 |
+
spatial_attention_timestep_skip_range (`tuple[int, int]`, defaults to `(100, 800)`):
|
| 58 |
+
The range of timesteps to skip in the spatial attention layer. The attention computations will be
|
| 59 |
+
conditionally skipped if the current timestep is within the specified range.
|
| 60 |
+
temporal_attention_timestep_skip_range (`tuple[int, int]`, defaults to `(100, 800)`):
|
| 61 |
+
The range of timesteps to skip in the temporal attention layer. The attention computations will be
|
| 62 |
+
conditionally skipped if the current timestep is within the specified range.
|
| 63 |
+
cross_attention_timestep_skip_range (`tuple[int, int]`, defaults to `(100, 800)`):
|
| 64 |
+
The range of timesteps to skip in the cross-attention layer. The attention computations will be
|
| 65 |
+
conditionally skipped if the current timestep is within the specified range.
|
| 66 |
+
spatial_attention_block_identifiers (`tuple[str, ...]`):
|
| 67 |
+
The identifiers to match against the layer names to determine if the layer is a spatial attention layer.
|
| 68 |
+
temporal_attention_block_identifiers (`tuple[str, ...]`):
|
| 69 |
+
The identifiers to match against the layer names to determine if the layer is a temporal attention layer.
|
| 70 |
+
cross_attention_block_identifiers (`tuple[str, ...]`):
|
| 71 |
+
The identifiers to match against the layer names to determine if the layer is a cross-attention layer.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
spatial_attention_block_skip_range: int | None = None
|
| 75 |
+
temporal_attention_block_skip_range: int | None = None
|
| 76 |
+
cross_attention_block_skip_range: int | None = None
|
| 77 |
+
|
| 78 |
+
spatial_attention_timestep_skip_range: tuple[int, int] = (100, 800)
|
| 79 |
+
temporal_attention_timestep_skip_range: tuple[int, int] = (100, 800)
|
| 80 |
+
cross_attention_timestep_skip_range: tuple[int, int] = (100, 800)
|
| 81 |
+
|
| 82 |
+
spatial_attention_block_identifiers: tuple[str, ...] = _SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS
|
| 83 |
+
temporal_attention_block_identifiers: tuple[str, ...] = _TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS
|
| 84 |
+
cross_attention_block_identifiers: tuple[str, ...] = _CROSS_TRANSFORMER_BLOCK_IDENTIFIERS
|
| 85 |
+
|
| 86 |
+
current_timestep_callback: Callable[[], int] = None
|
| 87 |
+
|
| 88 |
+
# TODO(aryan): add PAB for MLP layers (very limited speedup from testing with original codebase
|
| 89 |
+
# so not added for now)
|
| 90 |
+
|
| 91 |
+
def __repr__(self) -> str:
|
| 92 |
+
return (
|
| 93 |
+
f"PyramidAttentionBroadcastConfig(\n"
|
| 94 |
+
f" spatial_attention_block_skip_range={self.spatial_attention_block_skip_range},\n"
|
| 95 |
+
f" temporal_attention_block_skip_range={self.temporal_attention_block_skip_range},\n"
|
| 96 |
+
f" cross_attention_block_skip_range={self.cross_attention_block_skip_range},\n"
|
| 97 |
+
f" spatial_attention_timestep_skip_range={self.spatial_attention_timestep_skip_range},\n"
|
| 98 |
+
f" temporal_attention_timestep_skip_range={self.temporal_attention_timestep_skip_range},\n"
|
| 99 |
+
f" cross_attention_timestep_skip_range={self.cross_attention_timestep_skip_range},\n"
|
| 100 |
+
f" spatial_attention_block_identifiers={self.spatial_attention_block_identifiers},\n"
|
| 101 |
+
f" temporal_attention_block_identifiers={self.temporal_attention_block_identifiers},\n"
|
| 102 |
+
f" cross_attention_block_identifiers={self.cross_attention_block_identifiers},\n"
|
| 103 |
+
f" current_timestep_callback={self.current_timestep_callback}\n"
|
| 104 |
+
")"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class PyramidAttentionBroadcastState:
|
| 109 |
+
r"""
|
| 110 |
+
State for Pyramid Attention Broadcast.
|
| 111 |
+
|
| 112 |
+
Attributes:
|
| 113 |
+
iteration (`int`):
|
| 114 |
+
The current iteration of the Pyramid Attention Broadcast. It is necessary to ensure that `reset_state` is
|
| 115 |
+
called before starting a new inference forward pass for PAB to work correctly.
|
| 116 |
+
cache (`Any`):
|
| 117 |
+
The cached output from the previous forward pass. This is used to re-use the attention states when the
|
| 118 |
+
attention computation is skipped. It is either a tensor or a tuple of tensors, depending on the module.
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
def __init__(self) -> None:
|
| 122 |
+
self.iteration = 0
|
| 123 |
+
self.cache = None
|
| 124 |
+
|
| 125 |
+
def reset(self):
|
| 126 |
+
self.iteration = 0
|
| 127 |
+
self.cache = None
|
| 128 |
+
|
| 129 |
+
def __repr__(self):
|
| 130 |
+
cache_repr = ""
|
| 131 |
+
if self.cache is None:
|
| 132 |
+
cache_repr = "None"
|
| 133 |
+
else:
|
| 134 |
+
cache_repr = f"Tensor(shape={self.cache.shape}, dtype={self.cache.dtype})"
|
| 135 |
+
return f"PyramidAttentionBroadcastState(iteration={self.iteration}, cache={cache_repr})"
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
class PyramidAttentionBroadcastHook(ModelHook):
|
| 139 |
+
r"""A hook that applies Pyramid Attention Broadcast to a given module."""
|
| 140 |
+
|
| 141 |
+
_is_stateful = True
|
| 142 |
+
|
| 143 |
+
def __init__(
|
| 144 |
+
self, timestep_skip_range: tuple[int, int], block_skip_range: int, current_timestep_callback: Callable[[], int]
|
| 145 |
+
) -> None:
|
| 146 |
+
super().__init__()
|
| 147 |
+
|
| 148 |
+
self.timestep_skip_range = timestep_skip_range
|
| 149 |
+
self.block_skip_range = block_skip_range
|
| 150 |
+
self.current_timestep_callback = current_timestep_callback
|
| 151 |
+
|
| 152 |
+
def initialize_hook(self, module):
|
| 153 |
+
self.state = PyramidAttentionBroadcastState()
|
| 154 |
+
return module
|
| 155 |
+
|
| 156 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs) -> Any:
|
| 157 |
+
is_within_timestep_range = (
|
| 158 |
+
self.timestep_skip_range[0] < self.current_timestep_callback() < self.timestep_skip_range[1]
|
| 159 |
+
)
|
| 160 |
+
should_compute_attention = (
|
| 161 |
+
self.state.cache is None
|
| 162 |
+
or self.state.iteration == 0
|
| 163 |
+
or not is_within_timestep_range
|
| 164 |
+
or self.state.iteration % self.block_skip_range == 0
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
if should_compute_attention:
|
| 168 |
+
output = self.fn_ref.original_forward(*args, **kwargs)
|
| 169 |
+
else:
|
| 170 |
+
output = self.state.cache
|
| 171 |
+
|
| 172 |
+
self.state.cache = output
|
| 173 |
+
self.state.iteration += 1
|
| 174 |
+
return output
|
| 175 |
+
|
| 176 |
+
def reset_state(self, module: torch.nn.Module) -> None:
|
| 177 |
+
self.state.reset()
|
| 178 |
+
return module
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def apply_pyramid_attention_broadcast(module: torch.nn.Module, config: PyramidAttentionBroadcastConfig):
|
| 182 |
+
r"""
|
| 183 |
+
Apply [Pyramid Attention Broadcast](https://huggingface.co/papers/2408.12588) to a given pipeline.
|
| 184 |
+
|
| 185 |
+
PAB is an attention approximation method that leverages the similarity in attention states between timesteps to
|
| 186 |
+
reduce the computational cost of attention computation. The key takeaway from the paper is that the attention
|
| 187 |
+
similarity in the cross-attention layers between timesteps is high, followed by less similarity in the temporal and
|
| 188 |
+
spatial layers. This allows for the skipping of attention computation in the cross-attention layers more frequently
|
| 189 |
+
than in the temporal and spatial layers. Applying PAB will, therefore, speedup the inference process.
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
module (`torch.nn.Module`):
|
| 193 |
+
The module to apply Pyramid Attention Broadcast to.
|
| 194 |
+
config (`PyramidAttentionBroadcastConfig | None`, `optional`, defaults to `None`):
|
| 195 |
+
The configuration to use for Pyramid Attention Broadcast.
|
| 196 |
+
|
| 197 |
+
Example:
|
| 198 |
+
|
| 199 |
+
```python
|
| 200 |
+
>>> import torch
|
| 201 |
+
>>> from diffusers import CogVideoXPipeline, PyramidAttentionBroadcastConfig, apply_pyramid_attention_broadcast
|
| 202 |
+
>>> from diffusers.utils import export_to_video
|
| 203 |
+
|
| 204 |
+
>>> pipe = CogVideoXPipeline.from_pretrained("THUDM/CogVideoX-5b", torch_dtype=torch.bfloat16)
|
| 205 |
+
>>> pipe.to("cuda")
|
| 206 |
+
|
| 207 |
+
>>> config = PyramidAttentionBroadcastConfig(
|
| 208 |
+
... spatial_attention_block_skip_range=2,
|
| 209 |
+
... spatial_attention_timestep_skip_range=(100, 800),
|
| 210 |
+
... current_timestep_callback=lambda: pipe.current_timestep,
|
| 211 |
+
... )
|
| 212 |
+
>>> apply_pyramid_attention_broadcast(pipe.transformer, config)
|
| 213 |
+
```
|
| 214 |
+
"""
|
| 215 |
+
if config.current_timestep_callback is None:
|
| 216 |
+
raise ValueError(
|
| 217 |
+
"The `current_timestep_callback` function must be provided in the configuration to apply Pyramid Attention Broadcast."
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
if (
|
| 221 |
+
config.spatial_attention_block_skip_range is None
|
| 222 |
+
and config.temporal_attention_block_skip_range is None
|
| 223 |
+
and config.cross_attention_block_skip_range is None
|
| 224 |
+
):
|
| 225 |
+
logger.warning(
|
| 226 |
+
"Pyramid Attention Broadcast requires one or more of `spatial_attention_block_skip_range`, `temporal_attention_block_skip_range` "
|
| 227 |
+
"or `cross_attention_block_skip_range` parameters to be set to an integer, not `None`. Defaulting to using `spatial_attention_block_skip_range=2`. "
|
| 228 |
+
"To avoid this warning, please set one of the above parameters."
|
| 229 |
+
)
|
| 230 |
+
config.spatial_attention_block_skip_range = 2
|
| 231 |
+
|
| 232 |
+
for name, submodule in module.named_modules():
|
| 233 |
+
if not isinstance(submodule, (*_ATTENTION_CLASSES, AttentionModuleMixin)):
|
| 234 |
+
# PAB has been implemented specific to Diffusers' Attention classes. However, this does not mean that PAB
|
| 235 |
+
# cannot be applied to this layer. For custom layers, users can extend this functionality and implement
|
| 236 |
+
# their own PAB logic similar to `_apply_pyramid_attention_broadcast_on_attention_class`.
|
| 237 |
+
continue
|
| 238 |
+
_apply_pyramid_attention_broadcast_on_attention_class(name, submodule, config)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _apply_pyramid_attention_broadcast_on_attention_class(
|
| 242 |
+
name: str, module: Attention, config: PyramidAttentionBroadcastConfig
|
| 243 |
+
) -> bool:
|
| 244 |
+
is_spatial_self_attention = (
|
| 245 |
+
any(re.search(identifier, name) is not None for identifier in config.spatial_attention_block_identifiers)
|
| 246 |
+
and config.spatial_attention_block_skip_range is not None
|
| 247 |
+
and not getattr(module, "is_cross_attention", False)
|
| 248 |
+
)
|
| 249 |
+
is_temporal_self_attention = (
|
| 250 |
+
any(re.search(identifier, name) is not None for identifier in config.temporal_attention_block_identifiers)
|
| 251 |
+
and config.temporal_attention_block_skip_range is not None
|
| 252 |
+
and not getattr(module, "is_cross_attention", False)
|
| 253 |
+
)
|
| 254 |
+
is_cross_attention = (
|
| 255 |
+
any(re.search(identifier, name) is not None for identifier in config.cross_attention_block_identifiers)
|
| 256 |
+
and config.cross_attention_block_skip_range is not None
|
| 257 |
+
and getattr(module, "is_cross_attention", False)
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
block_skip_range, timestep_skip_range, block_type = None, None, None
|
| 261 |
+
if is_spatial_self_attention:
|
| 262 |
+
block_skip_range = config.spatial_attention_block_skip_range
|
| 263 |
+
timestep_skip_range = config.spatial_attention_timestep_skip_range
|
| 264 |
+
block_type = "spatial"
|
| 265 |
+
elif is_temporal_self_attention:
|
| 266 |
+
block_skip_range = config.temporal_attention_block_skip_range
|
| 267 |
+
timestep_skip_range = config.temporal_attention_timestep_skip_range
|
| 268 |
+
block_type = "temporal"
|
| 269 |
+
elif is_cross_attention:
|
| 270 |
+
block_skip_range = config.cross_attention_block_skip_range
|
| 271 |
+
timestep_skip_range = config.cross_attention_timestep_skip_range
|
| 272 |
+
block_type = "cross"
|
| 273 |
+
|
| 274 |
+
if block_skip_range is None or timestep_skip_range is None:
|
| 275 |
+
logger.info(
|
| 276 |
+
f'Unable to apply Pyramid Attention Broadcast to the selected layer: "{name}" because it does '
|
| 277 |
+
f"not match any of the required criteria for spatial, temporal or cross attention layers. Note, "
|
| 278 |
+
f"however, that this layer may still be valid for applying PAB. Please specify the correct "
|
| 279 |
+
f"block identifiers in the configuration."
|
| 280 |
+
)
|
| 281 |
+
return False
|
| 282 |
+
|
| 283 |
+
logger.debug(f"Enabling Pyramid Attention Broadcast ({block_type}) in layer: {name}")
|
| 284 |
+
_apply_pyramid_attention_broadcast_hook(
|
| 285 |
+
module, timestep_skip_range, block_skip_range, config.current_timestep_callback
|
| 286 |
+
)
|
| 287 |
+
return True
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def _apply_pyramid_attention_broadcast_hook(
|
| 291 |
+
module: Attention | MochiAttention,
|
| 292 |
+
timestep_skip_range: tuple[int, int],
|
| 293 |
+
block_skip_range: int,
|
| 294 |
+
current_timestep_callback: Callable[[], int],
|
| 295 |
+
):
|
| 296 |
+
r"""
|
| 297 |
+
Apply [Pyramid Attention Broadcast](https://huggingface.co/papers/2408.12588) to a given torch.nn.Module.
|
| 298 |
+
|
| 299 |
+
Args:
|
| 300 |
+
module (`torch.nn.Module`):
|
| 301 |
+
The module to apply Pyramid Attention Broadcast to.
|
| 302 |
+
timestep_skip_range (`tuple[int, int]`):
|
| 303 |
+
The range of timesteps to skip in the attention layer. The attention computations will be conditionally
|
| 304 |
+
skipped if the current timestep is within the specified range.
|
| 305 |
+
block_skip_range (`int`):
|
| 306 |
+
The number of times a specific attention broadcast is skipped before computing the attention states to
|
| 307 |
+
re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times (i.e., old
|
| 308 |
+
attention states will be reused) before computing the new attention states again.
|
| 309 |
+
current_timestep_callback (`Callable[[], int]`):
|
| 310 |
+
A callback function that returns the current inference timestep.
|
| 311 |
+
"""
|
| 312 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 313 |
+
hook = PyramidAttentionBroadcastHook(timestep_skip_range, block_skip_range, current_timestep_callback)
|
| 314 |
+
registry.register_hook(hook, _PYRAMID_ATTENTION_BROADCAST_HOOK)
|
diffusers/hooks/smoothed_energy_guidance_utils.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import math
|
| 16 |
+
from dataclasses import asdict, dataclass
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
|
| 21 |
+
from ..utils import get_logger
|
| 22 |
+
from ._common import _ALL_TRANSFORMER_BLOCK_IDENTIFIERS, _ATTENTION_CLASSES, _get_submodule_from_fqn
|
| 23 |
+
from .hooks import HookRegistry, ModelHook
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 27 |
+
|
| 28 |
+
_SMOOTHED_ENERGY_GUIDANCE_HOOK = "smoothed_energy_guidance_hook"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class SmoothedEnergyGuidanceConfig:
|
| 33 |
+
r"""
|
| 34 |
+
Configuration for skipping internal transformer blocks when executing a transformer model.
|
| 35 |
+
|
| 36 |
+
Args:
|
| 37 |
+
indices (`list[int]`):
|
| 38 |
+
The indices of the layer to skip. This is typically the first layer in the transformer block.
|
| 39 |
+
fqn (`str`, defaults to `"auto"`):
|
| 40 |
+
The fully qualified name identifying the stack of transformer blocks. Typically, this is
|
| 41 |
+
`transformer_blocks`, `single_transformer_blocks`, `blocks`, `layers`, or `temporal_transformer_blocks`.
|
| 42 |
+
For automatic detection, set this to `"auto"`. "auto" only works on DiT models. For UNet models, you must
|
| 43 |
+
provide the correct fqn.
|
| 44 |
+
_query_proj_identifiers (`list[str]`, defaults to `None`):
|
| 45 |
+
The identifiers for the query projection layers. Typically, these are `to_q`, `query`, or `q_proj`. If
|
| 46 |
+
`None`, `to_q` is used by default.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
indices: list[int]
|
| 50 |
+
fqn: str = "auto"
|
| 51 |
+
_query_proj_identifiers: list[str] = None
|
| 52 |
+
|
| 53 |
+
def to_dict(self):
|
| 54 |
+
return asdict(self)
|
| 55 |
+
|
| 56 |
+
@staticmethod
|
| 57 |
+
def from_dict(data: dict) -> "SmoothedEnergyGuidanceConfig":
|
| 58 |
+
return SmoothedEnergyGuidanceConfig(**data)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class SmoothedEnergyGuidanceHook(ModelHook):
|
| 62 |
+
def __init__(self, blur_sigma: float = 1.0, blur_threshold_inf: float = 9999.9) -> None:
|
| 63 |
+
super().__init__()
|
| 64 |
+
self.blur_sigma = blur_sigma
|
| 65 |
+
self.blur_threshold_inf = blur_threshold_inf
|
| 66 |
+
|
| 67 |
+
def post_forward(self, module: torch.nn.Module, output: torch.Tensor) -> torch.Tensor:
|
| 68 |
+
# Copied from https://github.com/SusungHong/SEG-SDXL/blob/cf8256d640d5373541cfea3b3b6caf93272cf986/pipeline_seg.py#L172C31-L172C102
|
| 69 |
+
kernel_size = math.ceil(6 * self.blur_sigma) + 1 - math.ceil(6 * self.blur_sigma) % 2
|
| 70 |
+
smoothed_output = _gaussian_blur_2d(output, kernel_size, self.blur_sigma, self.blur_threshold_inf)
|
| 71 |
+
return smoothed_output
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _apply_smoothed_energy_guidance_hook(
|
| 75 |
+
module: torch.nn.Module, config: SmoothedEnergyGuidanceConfig, blur_sigma: float, name: str | None = None
|
| 76 |
+
) -> None:
|
| 77 |
+
name = name or _SMOOTHED_ENERGY_GUIDANCE_HOOK
|
| 78 |
+
|
| 79 |
+
if config.fqn == "auto":
|
| 80 |
+
for identifier in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS:
|
| 81 |
+
if hasattr(module, identifier):
|
| 82 |
+
config.fqn = identifier
|
| 83 |
+
break
|
| 84 |
+
else:
|
| 85 |
+
raise ValueError(
|
| 86 |
+
"Could not find a suitable identifier for the transformer blocks automatically. Please provide a valid "
|
| 87 |
+
"`fqn` (fully qualified name) that identifies a stack of transformer blocks."
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
if config._query_proj_identifiers is None:
|
| 91 |
+
config._query_proj_identifiers = ["to_q"]
|
| 92 |
+
|
| 93 |
+
transformer_blocks = _get_submodule_from_fqn(module, config.fqn)
|
| 94 |
+
blocks_found = False
|
| 95 |
+
for i, block in enumerate(transformer_blocks):
|
| 96 |
+
if i not in config.indices:
|
| 97 |
+
continue
|
| 98 |
+
|
| 99 |
+
blocks_found = True
|
| 100 |
+
|
| 101 |
+
for submodule_name, submodule in block.named_modules():
|
| 102 |
+
if not isinstance(submodule, _ATTENTION_CLASSES) or submodule.is_cross_attention:
|
| 103 |
+
continue
|
| 104 |
+
for identifier in config._query_proj_identifiers:
|
| 105 |
+
query_proj = getattr(submodule, identifier, None)
|
| 106 |
+
if query_proj is None or not isinstance(query_proj, torch.nn.Linear):
|
| 107 |
+
continue
|
| 108 |
+
logger.debug(
|
| 109 |
+
f"Registering smoothed energy guidance hook on {config.fqn}.{i}.{submodule_name}.{identifier}"
|
| 110 |
+
)
|
| 111 |
+
registry = HookRegistry.check_if_exists_or_initialize(query_proj)
|
| 112 |
+
hook = SmoothedEnergyGuidanceHook(blur_sigma)
|
| 113 |
+
registry.register_hook(hook, name)
|
| 114 |
+
|
| 115 |
+
if not blocks_found:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
f"Could not find any transformer blocks matching the provided indices {config.indices} and "
|
| 118 |
+
f"fully qualified name '{config.fqn}'. Please check the indices and fqn for correctness."
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# Modified from https://github.com/SusungHong/SEG-SDXL/blob/cf8256d640d5373541cfea3b3b6caf93272cf986/pipeline_seg.py#L71
|
| 123 |
+
def _gaussian_blur_2d(query: torch.Tensor, kernel_size: int, sigma: float, sigma_threshold_inf: float) -> torch.Tensor:
|
| 124 |
+
"""
|
| 125 |
+
This implementation assumes that the input query is for visual (image/videos) tokens to apply the 2D gaussian blur.
|
| 126 |
+
However, some models use joint text-visual token attention for which this may not be suitable. Additionally, this
|
| 127 |
+
implementation also assumes that the visual tokens come from a square image/video. In practice, despite these
|
| 128 |
+
assumptions, applying the 2D square gaussian blur on the query projections generates reasonable results for
|
| 129 |
+
Smoothed Energy Guidance.
|
| 130 |
+
|
| 131 |
+
SEG is only supported as an experimental prototype feature for now, so the implementation may be modified in the
|
| 132 |
+
future without warning or guarantee of reproducibility.
|
| 133 |
+
"""
|
| 134 |
+
assert query.ndim == 3
|
| 135 |
+
|
| 136 |
+
is_inf = sigma > sigma_threshold_inf
|
| 137 |
+
batch_size, seq_len, embed_dim = query.shape
|
| 138 |
+
|
| 139 |
+
seq_len_sqrt = int(math.sqrt(seq_len))
|
| 140 |
+
num_square_tokens = seq_len_sqrt * seq_len_sqrt
|
| 141 |
+
query_slice = query[:, :num_square_tokens, :]
|
| 142 |
+
query_slice = query_slice.permute(0, 2, 1)
|
| 143 |
+
query_slice = query_slice.reshape(batch_size, embed_dim, seq_len_sqrt, seq_len_sqrt)
|
| 144 |
+
|
| 145 |
+
if is_inf:
|
| 146 |
+
kernel_size = min(kernel_size, seq_len_sqrt - (seq_len_sqrt % 2 - 1))
|
| 147 |
+
kernel_size_half = (kernel_size - 1) / 2
|
| 148 |
+
|
| 149 |
+
x = torch.linspace(-kernel_size_half, kernel_size_half, steps=kernel_size)
|
| 150 |
+
pdf = torch.exp(-0.5 * (x / sigma).pow(2))
|
| 151 |
+
kernel1d = pdf / pdf.sum()
|
| 152 |
+
kernel1d = kernel1d.to(query)
|
| 153 |
+
kernel2d = torch.matmul(kernel1d[:, None], kernel1d[None, :])
|
| 154 |
+
kernel2d = kernel2d.expand(embed_dim, 1, kernel2d.shape[0], kernel2d.shape[1])
|
| 155 |
+
|
| 156 |
+
padding = [kernel_size // 2, kernel_size // 2, kernel_size // 2, kernel_size // 2]
|
| 157 |
+
query_slice = F.pad(query_slice, padding, mode="reflect")
|
| 158 |
+
query_slice = F.conv2d(query_slice, kernel2d, groups=embed_dim)
|
| 159 |
+
else:
|
| 160 |
+
query_slice[:] = query_slice.mean(dim=(-2, -1), keepdim=True)
|
| 161 |
+
|
| 162 |
+
query_slice = query_slice.reshape(batch_size, embed_dim, num_square_tokens)
|
| 163 |
+
query_slice = query_slice.permute(0, 2, 1)
|
| 164 |
+
query[:, :num_square_tokens, :] = query_slice.clone()
|
| 165 |
+
|
| 166 |
+
return query
|
diffusers/hooks/taylorseer_cache.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import re
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
|
| 8 |
+
from ..utils import logging
|
| 9 |
+
from .hooks import HookRegistry, ModelHook, StateManager
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
logger = logging.get_logger(__name__)
|
| 13 |
+
_TAYLORSEER_CACHE_HOOK = "taylorseer_cache"
|
| 14 |
+
_SPATIAL_ATTENTION_BLOCK_IDENTIFIERS = (
|
| 15 |
+
"^blocks.*attn",
|
| 16 |
+
"^transformer_blocks.*attn",
|
| 17 |
+
"^single_transformer_blocks.*attn",
|
| 18 |
+
)
|
| 19 |
+
_TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS = ("^temporal_transformer_blocks.*attn",)
|
| 20 |
+
_TRANSFORMER_BLOCK_IDENTIFIERS = _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS + _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS
|
| 21 |
+
_BLOCK_IDENTIFIERS = ("^[^.]*block[^.]*\\.[^.]+$",)
|
| 22 |
+
_PROJ_OUT_IDENTIFIERS = ("^proj_out$",)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class TaylorSeerCacheConfig:
|
| 27 |
+
"""
|
| 28 |
+
Configuration for TaylorSeer cache. See: https://huggingface.co/papers/2503.06923
|
| 29 |
+
|
| 30 |
+
Attributes:
|
| 31 |
+
cache_interval (`int`, defaults to `5`):
|
| 32 |
+
The interval between full computation steps. After a full computation, the cached (predicted) outputs are
|
| 33 |
+
reused for this many subsequent denoising steps before refreshing with a new full forward pass.
|
| 34 |
+
|
| 35 |
+
disable_cache_before_step (`int`, defaults to `3`):
|
| 36 |
+
The denoising step index before which caching is disabled, meaning full computation is performed for the
|
| 37 |
+
initial steps (0 to disable_cache_before_step - 1) to gather data for Taylor series approximations. During
|
| 38 |
+
these steps, Taylor factors are updated, but caching/predictions are not applied. Caching begins at this
|
| 39 |
+
step.
|
| 40 |
+
|
| 41 |
+
disable_cache_after_step (`int`, *optional*, defaults to `None`):
|
| 42 |
+
The denoising step index after which caching is disabled. If set, for steps >= this value, all modules run
|
| 43 |
+
full computations without predictions or state updates, ensuring accuracy in later stages if needed.
|
| 44 |
+
|
| 45 |
+
max_order (`int`, defaults to `1`):
|
| 46 |
+
The highest order in the Taylor series expansion for approximating module outputs. Higher orders provide
|
| 47 |
+
better approximations but increase computation and memory usage.
|
| 48 |
+
|
| 49 |
+
taylor_factors_dtype (`torch.dtype`, defaults to `torch.bfloat16`):
|
| 50 |
+
Data type used for storing and computing Taylor series factors. Lower precision reduces memory but may
|
| 51 |
+
affect stability; higher precision improves accuracy at the cost of more memory.
|
| 52 |
+
|
| 53 |
+
skip_predict_identifiers (`list[str]`, *optional*, defaults to `None`):
|
| 54 |
+
Regex patterns (using `re.fullmatch`) for module names to place as "skip" in "cache" mode. In this mode,
|
| 55 |
+
the module computes fully during initial or refresh steps but returns a zero tensor (matching recorded
|
| 56 |
+
shape) during prediction steps to skip computation cheaply.
|
| 57 |
+
|
| 58 |
+
cache_identifiers (`list[str]`, *optional*, defaults to `None`):
|
| 59 |
+
Regex patterns (using `re.fullmatch`) for module names to place in Taylor-series caching mode, where
|
| 60 |
+
outputs are approximated and cached for reuse.
|
| 61 |
+
|
| 62 |
+
use_lite_mode (`bool`, *optional*, defaults to `False`):
|
| 63 |
+
Enables a lightweight TaylorSeer variant that minimizes memory usage by applying predefined patterns for
|
| 64 |
+
skipping and caching (e.g., skipping blocks and caching projections). This overrides any custom
|
| 65 |
+
`inactive_identifiers` or `active_identifiers`.
|
| 66 |
+
|
| 67 |
+
Notes:
|
| 68 |
+
- Patterns are matched using `re.fullmatch` on the module name.
|
| 69 |
+
- If `skip_predict_identifiers` or `cache_identifiers` are provided, only matching modules are hooked.
|
| 70 |
+
- If neither is provided, all attention-like modules are hooked by default.
|
| 71 |
+
|
| 72 |
+
Example of inactive and active usage:
|
| 73 |
+
|
| 74 |
+
```py
|
| 75 |
+
def forward(x):
|
| 76 |
+
x = self.module1(x) # inactive module: returns zeros tensor based on shape recorded during full compute
|
| 77 |
+
x = self.module2(x) # active module: caches output here, avoiding recomputation of prior steps
|
| 78 |
+
return x
|
| 79 |
+
```
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
cache_interval: int = 5
|
| 83 |
+
disable_cache_before_step: int = 3
|
| 84 |
+
disable_cache_after_step: int | None = None
|
| 85 |
+
max_order: int = 1
|
| 86 |
+
taylor_factors_dtype: torch.dtype | None = torch.bfloat16
|
| 87 |
+
skip_predict_identifiers: list[str] | None = None
|
| 88 |
+
cache_identifiers: list[str] | None = None
|
| 89 |
+
use_lite_mode: bool = False
|
| 90 |
+
|
| 91 |
+
def __repr__(self) -> str:
|
| 92 |
+
return (
|
| 93 |
+
"TaylorSeerCacheConfig("
|
| 94 |
+
f"cache_interval={self.cache_interval}, "
|
| 95 |
+
f"disable_cache_before_step={self.disable_cache_before_step}, "
|
| 96 |
+
f"disable_cache_after_step={self.disable_cache_after_step}, "
|
| 97 |
+
f"max_order={self.max_order}, "
|
| 98 |
+
f"taylor_factors_dtype={self.taylor_factors_dtype}, "
|
| 99 |
+
f"skip_predict_identifiers={self.skip_predict_identifiers}, "
|
| 100 |
+
f"cache_identifiers={self.cache_identifiers}, "
|
| 101 |
+
f"use_lite_mode={self.use_lite_mode})"
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class TaylorSeerState:
|
| 106 |
+
def __init__(
|
| 107 |
+
self,
|
| 108 |
+
taylor_factors_dtype: torch.dtype | None = torch.bfloat16,
|
| 109 |
+
max_order: int = 1,
|
| 110 |
+
is_inactive: bool = False,
|
| 111 |
+
):
|
| 112 |
+
self.taylor_factors_dtype = taylor_factors_dtype
|
| 113 |
+
self.max_order = max_order
|
| 114 |
+
self.is_inactive = is_inactive
|
| 115 |
+
|
| 116 |
+
self.module_dtypes: tuple[torch.dtype, ...] = ()
|
| 117 |
+
self.last_update_step: int | None = None
|
| 118 |
+
self.taylor_factors: dict[int, dict[int, torch.Tensor]] = {}
|
| 119 |
+
self.inactive_shapes: tuple[tuple[int, ...], ...] | None = None
|
| 120 |
+
self.device: torch.device | None = None
|
| 121 |
+
self.current_step: int = -1
|
| 122 |
+
|
| 123 |
+
def reset(self) -> None:
|
| 124 |
+
self.current_step = -1
|
| 125 |
+
self.last_update_step = None
|
| 126 |
+
self.taylor_factors = {}
|
| 127 |
+
self.inactive_shapes = None
|
| 128 |
+
self.device = None
|
| 129 |
+
|
| 130 |
+
def update(
|
| 131 |
+
self,
|
| 132 |
+
outputs: tuple[torch.Tensor, ...],
|
| 133 |
+
) -> None:
|
| 134 |
+
self.module_dtypes = tuple(output.dtype for output in outputs)
|
| 135 |
+
self.device = outputs[0].device
|
| 136 |
+
|
| 137 |
+
if self.is_inactive:
|
| 138 |
+
self.inactive_shapes = tuple(output.shape for output in outputs)
|
| 139 |
+
else:
|
| 140 |
+
for i, features in enumerate(outputs):
|
| 141 |
+
new_factors: dict[int, torch.Tensor] = {0: features}
|
| 142 |
+
is_first_update = self.last_update_step is None
|
| 143 |
+
if not is_first_update:
|
| 144 |
+
delta_step = self.current_step - self.last_update_step
|
| 145 |
+
if delta_step == 0:
|
| 146 |
+
raise ValueError("Delta step cannot be zero for TaylorSeer update.")
|
| 147 |
+
|
| 148 |
+
# Recursive divided differences up to max_order
|
| 149 |
+
prev_factors = self.taylor_factors.get(i, {})
|
| 150 |
+
for j in range(self.max_order):
|
| 151 |
+
prev = prev_factors.get(j)
|
| 152 |
+
if prev is None:
|
| 153 |
+
break
|
| 154 |
+
new_factors[j + 1] = (new_factors[j] - prev.to(features.dtype)) / delta_step
|
| 155 |
+
self.taylor_factors[i] = {
|
| 156 |
+
order: factor.to(self.taylor_factors_dtype) for order, factor in new_factors.items()
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
self.last_update_step = self.current_step
|
| 160 |
+
|
| 161 |
+
@torch.compiler.disable
|
| 162 |
+
def predict(self) -> list[torch.Tensor]:
|
| 163 |
+
if self.last_update_step is None:
|
| 164 |
+
raise ValueError("Cannot predict without prior initialization/update.")
|
| 165 |
+
|
| 166 |
+
step_offset = self.current_step - self.last_update_step
|
| 167 |
+
|
| 168 |
+
outputs = []
|
| 169 |
+
if self.is_inactive:
|
| 170 |
+
if self.inactive_shapes is None:
|
| 171 |
+
raise ValueError("Inactive shapes not set during prediction.")
|
| 172 |
+
for i in range(len(self.module_dtypes)):
|
| 173 |
+
outputs.append(
|
| 174 |
+
torch.zeros(
|
| 175 |
+
self.inactive_shapes[i],
|
| 176 |
+
dtype=self.module_dtypes[i],
|
| 177 |
+
device=self.device,
|
| 178 |
+
)
|
| 179 |
+
)
|
| 180 |
+
else:
|
| 181 |
+
if not self.taylor_factors:
|
| 182 |
+
raise ValueError("Taylor factors empty during prediction.")
|
| 183 |
+
num_outputs = len(self.taylor_factors)
|
| 184 |
+
num_orders = len(self.taylor_factors[0])
|
| 185 |
+
for i in range(num_outputs):
|
| 186 |
+
output_dtype = self.module_dtypes[i]
|
| 187 |
+
taylor_factors = self.taylor_factors[i]
|
| 188 |
+
output = torch.zeros_like(taylor_factors[0], dtype=output_dtype)
|
| 189 |
+
for order in range(num_orders):
|
| 190 |
+
coeff = (step_offset**order) / math.factorial(order)
|
| 191 |
+
factor = taylor_factors[order]
|
| 192 |
+
output = output + factor.to(output_dtype) * coeff
|
| 193 |
+
outputs.append(output)
|
| 194 |
+
return outputs
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class TaylorSeerCacheHook(ModelHook):
|
| 198 |
+
_is_stateful = True
|
| 199 |
+
|
| 200 |
+
def __init__(
|
| 201 |
+
self,
|
| 202 |
+
cache_interval: int,
|
| 203 |
+
disable_cache_before_step: int,
|
| 204 |
+
taylor_factors_dtype: torch.dtype,
|
| 205 |
+
state_manager: StateManager,
|
| 206 |
+
disable_cache_after_step: int | None = None,
|
| 207 |
+
):
|
| 208 |
+
super().__init__()
|
| 209 |
+
self.cache_interval = cache_interval
|
| 210 |
+
self.disable_cache_before_step = disable_cache_before_step
|
| 211 |
+
self.disable_cache_after_step = disable_cache_after_step
|
| 212 |
+
self.taylor_factors_dtype = taylor_factors_dtype
|
| 213 |
+
self.state_manager = state_manager
|
| 214 |
+
|
| 215 |
+
def initialize_hook(self, module: torch.nn.Module):
|
| 216 |
+
return module
|
| 217 |
+
|
| 218 |
+
def reset_state(self, module: torch.nn.Module) -> None:
|
| 219 |
+
"""
|
| 220 |
+
Reset state between sampling runs.
|
| 221 |
+
"""
|
| 222 |
+
self.state_manager.reset()
|
| 223 |
+
|
| 224 |
+
@torch.compiler.disable
|
| 225 |
+
def _measure_should_compute(self) -> bool:
|
| 226 |
+
state: TaylorSeerState = self.state_manager.get_state()
|
| 227 |
+
state.current_step += 1
|
| 228 |
+
current_step = state.current_step
|
| 229 |
+
is_warmup_phase = current_step < self.disable_cache_before_step
|
| 230 |
+
is_compute_interval = (current_step - self.disable_cache_before_step - 1) % self.cache_interval == 0
|
| 231 |
+
is_cooldown_phase = self.disable_cache_after_step is not None and current_step >= self.disable_cache_after_step
|
| 232 |
+
should_compute = is_warmup_phase or is_compute_interval or is_cooldown_phase
|
| 233 |
+
return should_compute, state
|
| 234 |
+
|
| 235 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 236 |
+
should_compute, state = self._measure_should_compute()
|
| 237 |
+
if should_compute:
|
| 238 |
+
outputs = self.fn_ref.original_forward(*args, **kwargs)
|
| 239 |
+
wrapped_outputs = (outputs,) if isinstance(outputs, torch.Tensor) else outputs
|
| 240 |
+
state.update(wrapped_outputs)
|
| 241 |
+
return outputs
|
| 242 |
+
|
| 243 |
+
outputs_list = state.predict()
|
| 244 |
+
return outputs_list[0] if len(outputs_list) == 1 else tuple(outputs_list)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _resolve_patterns(config: TaylorSeerCacheConfig) -> tuple[list[str], list[str]]:
|
| 248 |
+
"""
|
| 249 |
+
Resolve effective inactive and active pattern lists from config + templates.
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
inactive_patterns = config.skip_predict_identifiers if config.skip_predict_identifiers is not None else None
|
| 253 |
+
active_patterns = config.cache_identifiers if config.cache_identifiers is not None else None
|
| 254 |
+
|
| 255 |
+
return inactive_patterns or [], active_patterns or []
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def apply_taylorseer_cache(module: torch.nn.Module, config: TaylorSeerCacheConfig):
|
| 259 |
+
"""
|
| 260 |
+
Applies the TaylorSeer cache to a given pipeline (typically the transformer / UNet).
|
| 261 |
+
|
| 262 |
+
This function hooks selected modules in the model to enable caching or skipping based on the provided
|
| 263 |
+
configuration, reducing redundant computations in diffusion denoising loops.
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
module (torch.nn.Module): The model subtree to apply the hooks to.
|
| 267 |
+
config (TaylorSeerCacheConfig): Configuration for the cache.
|
| 268 |
+
|
| 269 |
+
Example:
|
| 270 |
+
```python
|
| 271 |
+
>>> import torch
|
| 272 |
+
>>> from diffusers import FluxPipeline, TaylorSeerCacheConfig
|
| 273 |
+
|
| 274 |
+
>>> pipe = FluxPipeline.from_pretrained(
|
| 275 |
+
... "black-forest-labs/FLUX.1-dev",
|
| 276 |
+
... torch_dtype=torch.bfloat16,
|
| 277 |
+
... )
|
| 278 |
+
>>> pipe.to("cuda")
|
| 279 |
+
|
| 280 |
+
>>> config = TaylorSeerCacheConfig(
|
| 281 |
+
... cache_interval=5,
|
| 282 |
+
... max_order=1,
|
| 283 |
+
... disable_cache_before_step=3,
|
| 284 |
+
... taylor_factors_dtype=torch.float32,
|
| 285 |
+
... )
|
| 286 |
+
>>> pipe.transformer.enable_cache(config)
|
| 287 |
+
```
|
| 288 |
+
"""
|
| 289 |
+
inactive_patterns, active_patterns = _resolve_patterns(config)
|
| 290 |
+
|
| 291 |
+
active_patterns = active_patterns or _TRANSFORMER_BLOCK_IDENTIFIERS
|
| 292 |
+
|
| 293 |
+
if config.use_lite_mode:
|
| 294 |
+
logger.info("Using TaylorSeer Lite variant for cache.")
|
| 295 |
+
active_patterns = _PROJ_OUT_IDENTIFIERS
|
| 296 |
+
inactive_patterns = _BLOCK_IDENTIFIERS
|
| 297 |
+
if config.skip_predict_identifiers or config.cache_identifiers:
|
| 298 |
+
logger.warning("Lite mode overrides user patterns.")
|
| 299 |
+
|
| 300 |
+
for name, submodule in module.named_modules():
|
| 301 |
+
matches_inactive = any(re.fullmatch(pattern, name) for pattern in inactive_patterns)
|
| 302 |
+
matches_active = any(re.fullmatch(pattern, name) for pattern in active_patterns)
|
| 303 |
+
if not (matches_inactive or matches_active):
|
| 304 |
+
continue
|
| 305 |
+
_apply_taylorseer_cache_hook(
|
| 306 |
+
module=submodule,
|
| 307 |
+
config=config,
|
| 308 |
+
is_inactive=matches_inactive,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _apply_taylorseer_cache_hook(
|
| 313 |
+
module: nn.Module,
|
| 314 |
+
config: TaylorSeerCacheConfig,
|
| 315 |
+
is_inactive: bool,
|
| 316 |
+
):
|
| 317 |
+
"""
|
| 318 |
+
Registers the TaylorSeer hook on the specified nn.Module.
|
| 319 |
+
|
| 320 |
+
Args:
|
| 321 |
+
name: Name of the module.
|
| 322 |
+
module: The nn.Module to be hooked.
|
| 323 |
+
config: Cache configuration.
|
| 324 |
+
is_inactive: Whether this module should operate in "inactive" mode.
|
| 325 |
+
"""
|
| 326 |
+
state_manager = StateManager(
|
| 327 |
+
TaylorSeerState,
|
| 328 |
+
init_kwargs={
|
| 329 |
+
"taylor_factors_dtype": config.taylor_factors_dtype,
|
| 330 |
+
"max_order": config.max_order,
|
| 331 |
+
"is_inactive": is_inactive,
|
| 332 |
+
},
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 336 |
+
|
| 337 |
+
hook = TaylorSeerCacheHook(
|
| 338 |
+
cache_interval=config.cache_interval,
|
| 339 |
+
disable_cache_before_step=config.disable_cache_before_step,
|
| 340 |
+
taylor_factors_dtype=config.taylor_factors_dtype,
|
| 341 |
+
disable_cache_after_step=config.disable_cache_after_step,
|
| 342 |
+
state_manager=state_manager,
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
registry.register_hook(hook, _TAYLORSEER_CACHE_HOOK)
|
diffusers/hooks/tensor_parallel.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ..models._modeling_parallel import TensorParallelConfig
|
| 18 |
+
from ..utils import get_logger
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
logger = get_logger(__name__) # pylint: disable=invalid-name
|
| 22 |
+
|
| 23 |
+
_SUPPORTED_TP_DEVICES = ("cuda", "neuron")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class PackedColwiseParallel:
|
| 27 |
+
"""Column-wise sharding for fused projections with heterogeneous block structure.
|
| 28 |
+
|
| 29 |
+
`blocks` is a list of proportional integers whose sum divides the weight's row count. For example, `[1, 1]` for a
|
| 30 |
+
SwiGLU gate+linear projection (two equal halves) or `[1, 1, 1, 3, 3]` for a Q+K+V+gate+linear projection with
|
| 31 |
+
`mlp_ratio=3`. If `blocks` is `None`, the Linear module must carry a `_tp_packed_col_blocks` attribute set during
|
| 32 |
+
model `__init__`.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, blocks: "list[int] | None" = None):
|
| 36 |
+
self.blocks = blocks
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class PackedRowwiseParallel:
|
| 40 |
+
"""Row-wise sharding for fused projections with heterogeneous block structure.
|
| 41 |
+
|
| 42 |
+
`blocks` describes the input-column partition of the fused Linear (e.g. `[1, 3]` when the input concatenates an
|
| 43 |
+
attention projection and an MLP projection with `mlp_ratio=3`). If `blocks` is `None`, the module must carry a
|
| 44 |
+
`_tp_packed_row_blocks` attribute.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, blocks: "list[int] | None" = None):
|
| 48 |
+
self.blocks = blocks
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _blocks_to_block_sizes(total_size: int, blocks: "list[int]") -> "list[int]":
|
| 52 |
+
"""Convert proportional block counts to absolute sizes.
|
| 53 |
+
|
| 54 |
+
`blocks` is a list of positive integers interpreted as proportional weights. Their sum must divide `total_size`
|
| 55 |
+
evenly. Returns a list of absolute sizes that sum to `total_size`. For example, `_blocks_to_block_sizes(1152, [1,
|
| 56 |
+
1, 1, 3, 3])` returns `[128, 128, 128, 384, 384]`.
|
| 57 |
+
"""
|
| 58 |
+
total = sum(blocks)
|
| 59 |
+
if total_size % total != 0:
|
| 60 |
+
raise ValueError(
|
| 61 |
+
f"Cannot split {total_size} into proportional blocks {blocks}: "
|
| 62 |
+
f"sum({blocks})={total} does not divide {total_size}."
|
| 63 |
+
)
|
| 64 |
+
unit = total_size // total
|
| 65 |
+
return [b * unit for b in blocks]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _resolve_tp_plan(model: torch.nn.Module, tp_plan: dict) -> list:
|
| 69 |
+
"""Group a flat `_tp_plan` into per-block `(submodule, {relative_path: style})` plans.
|
| 70 |
+
|
| 71 |
+
Each glob is split at its single `*`; the prefix must resolve to a `ModuleList` and the suffix is the per-element
|
| 72 |
+
key. Grouping by block lets the caller issue one `parallelize_module` call per block, which `RowwiseParallel` needs
|
| 73 |
+
to attach its input redistribution at the block boundary.
|
| 74 |
+
|
| 75 |
+
Example: when `transformer_blocks` is a `ModuleList` of length 2, the input `{"transformer_blocks.*.ff.linear_out":
|
| 76 |
+
"rowwise"}` returns `[(transformer_blocks[0], {"ff.linear_out": "rowwise"}), (transformer_blocks[1],
|
| 77 |
+
{"ff.linear_out": "rowwise"})]`.
|
| 78 |
+
"""
|
| 79 |
+
grouped: dict[int, tuple] = {}
|
| 80 |
+
order: list[int] = []
|
| 81 |
+
|
| 82 |
+
for pattern, style in tp_plan.items():
|
| 83 |
+
if pattern.count("*") > 1:
|
| 84 |
+
raise ValueError(f"Wildcard '*' can only be used once in a `_tp_plan` key, got '{pattern}'.")
|
| 85 |
+
|
| 86 |
+
if "*" in pattern:
|
| 87 |
+
prefix, _, suffix = pattern.partition("*")
|
| 88 |
+
container = model
|
| 89 |
+
for atom in prefix.strip(".").split("."):
|
| 90 |
+
container = getattr(container, atom)
|
| 91 |
+
if not isinstance(container, torch.nn.ModuleList):
|
| 92 |
+
raise ValueError(
|
| 93 |
+
f"`_tp_plan` wildcard '{pattern}' must expand over a `ModuleList`, but "
|
| 94 |
+
f"'{prefix.strip('.')}' resolved to '{container.__class__.__name__}'."
|
| 95 |
+
)
|
| 96 |
+
relative, blocks = suffix.strip("."), list(container)
|
| 97 |
+
else:
|
| 98 |
+
relative, blocks = pattern, [model]
|
| 99 |
+
|
| 100 |
+
for block in blocks:
|
| 101 |
+
key = id(block)
|
| 102 |
+
if key not in grouped:
|
| 103 |
+
grouped[key] = (block, {})
|
| 104 |
+
order.append(key)
|
| 105 |
+
grouped[key][1][relative] = style
|
| 106 |
+
|
| 107 |
+
return [grouped[key] for key in order]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _styles(relative_plan: dict) -> dict:
|
| 111 |
+
"""Map a `{relative_path: style}` plan to `parallelize_module` style instances.
|
| 112 |
+
|
| 113 |
+
Values may be plain strings (`"colwise"` / `"rowwise"`) or `PackedColwiseParallel` / `PackedRowwiseParallel` marker
|
| 114 |
+
instances. Returns `{relative_path: ColwiseParallel() | RowwiseParallel() | <packed impl>}`, each subclassed to
|
| 115 |
+
reject a sharded dim that is not divisible by the TP degree.
|
| 116 |
+
"""
|
| 117 |
+
import torch.nn as nn
|
| 118 |
+
from torch.distributed.tensor import DTensor, Replicate, Shard, distribute_tensor
|
| 119 |
+
from torch.distributed.tensor.parallel import ColwiseParallel, RowwiseParallel
|
| 120 |
+
|
| 121 |
+
def _make_packed_col(marker: PackedColwiseParallel) -> ColwiseParallel:
|
| 122 |
+
_blocks = marker.blocks
|
| 123 |
+
|
| 124 |
+
class _PackedColwiseImpl(ColwiseParallel):
|
| 125 |
+
def _partition_linear_fn(self, name, module, device_mesh):
|
| 126 |
+
blocks = _blocks if _blocks is not None else getattr(module, "_tp_packed_col_blocks")
|
| 127 |
+
rank = device_mesh.get_local_rank()
|
| 128 |
+
tp_size = device_mesh.size()
|
| 129 |
+
# Both weight (`[out, in]`) and bias (`[out]`) are sharded row-wise (dim 0) with the same per-block
|
| 130 |
+
# slicing so each rank's bias rows line up with its weight rows for the packed layout.
|
| 131 |
+
for param_name, param in module.named_parameters():
|
| 132 |
+
full = distribute_tensor(
|
| 133 |
+
param, device_mesh, [Replicate()], src_data_rank=self.src_data_rank
|
| 134 |
+
).to_local()
|
| 135 |
+
block_sizes = _blocks_to_block_sizes(full.shape[0], blocks)
|
| 136 |
+
parts, offset = [], 0
|
| 137 |
+
for bs in block_sizes:
|
| 138 |
+
if bs % tp_size != 0:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
|
| 141 |
+
f"{bs} is not divisible by {tp_size}."
|
| 142 |
+
)
|
| 143 |
+
chunk = bs // tp_size
|
| 144 |
+
parts.append(full[offset + rank * chunk : offset + (rank + 1) * chunk].contiguous())
|
| 145 |
+
offset += bs
|
| 146 |
+
local = torch.cat(parts, dim=0)
|
| 147 |
+
dist_param = nn.Parameter(
|
| 148 |
+
DTensor.from_local(local, device_mesh, [Shard(0)], run_check=False),
|
| 149 |
+
requires_grad=param.requires_grad,
|
| 150 |
+
)
|
| 151 |
+
module.register_parameter(param_name, dist_param)
|
| 152 |
+
|
| 153 |
+
return _PackedColwiseImpl()
|
| 154 |
+
|
| 155 |
+
def _make_packed_row(marker: PackedRowwiseParallel) -> RowwiseParallel:
|
| 156 |
+
_blocks = marker.blocks
|
| 157 |
+
|
| 158 |
+
class _PackedRowwiseImpl(RowwiseParallel):
|
| 159 |
+
def _partition_linear_fn(self, name, module, device_mesh):
|
| 160 |
+
blocks = _blocks if _blocks is not None else getattr(module, "_tp_packed_row_blocks")
|
| 161 |
+
rank = device_mesh.get_local_rank()
|
| 162 |
+
tp_size = device_mesh.size()
|
| 163 |
+
for param_name, param in module.named_parameters():
|
| 164 |
+
if param_name == "weight":
|
| 165 |
+
full = distribute_tensor(
|
| 166 |
+
param, device_mesh, [Replicate()], src_data_rank=self.src_data_rank
|
| 167 |
+
).to_local()
|
| 168 |
+
block_sizes = _blocks_to_block_sizes(full.shape[1], blocks)
|
| 169 |
+
parts, offset = [], 0
|
| 170 |
+
for bs in block_sizes:
|
| 171 |
+
if bs % tp_size != 0:
|
| 172 |
+
raise ValueError(
|
| 173 |
+
f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
|
| 174 |
+
f"{bs} is not divisible by {tp_size}."
|
| 175 |
+
)
|
| 176 |
+
chunk = bs // tp_size
|
| 177 |
+
parts.append(full[:, offset + rank * chunk : offset + (rank + 1) * chunk].contiguous())
|
| 178 |
+
offset += bs
|
| 179 |
+
local = torch.cat(parts, dim=1)
|
| 180 |
+
dist_param = nn.Parameter(
|
| 181 |
+
DTensor.from_local(local, device_mesh, [Shard(1)], run_check=False),
|
| 182 |
+
requires_grad=param.requires_grad,
|
| 183 |
+
)
|
| 184 |
+
else:
|
| 185 |
+
dist_param = nn.Parameter(
|
| 186 |
+
distribute_tensor(param, device_mesh, [Replicate()], src_data_rank=self.src_data_rank),
|
| 187 |
+
requires_grad=param.requires_grad,
|
| 188 |
+
)
|
| 189 |
+
module.register_parameter(param_name, dist_param)
|
| 190 |
+
|
| 191 |
+
return _PackedRowwiseImpl()
|
| 192 |
+
|
| 193 |
+
# `distribute_tensor` accepts an indivisible shard dim and just gives the trailing ranks a smaller (or empty)
|
| 194 |
+
# slice, so an uneven split does not raise here — it surfaces much later as a shape or numerics error, because
|
| 195 |
+
# the attention head split and the paired colwise/rowwise Linear both assume equal shards. Reject it up front,
|
| 196 |
+
# matching what the packed styles above and the Neuron pre-shard path already do.
|
| 197 |
+
def _make_checked_col(path: str) -> ColwiseParallel:
|
| 198 |
+
class _CheckedColwiseImpl(ColwiseParallel):
|
| 199 |
+
def _partition_linear_fn(self, name, module, device_mesh):
|
| 200 |
+
tp_size = device_mesh.size()
|
| 201 |
+
out_features = module.weight.shape[0]
|
| 202 |
+
if out_features % tp_size != 0:
|
| 203 |
+
raise ValueError(
|
| 204 |
+
f"Cannot colwise-shard '{path}' weight rows ({out_features}) across {tp_size} "
|
| 205 |
+
f"tensor-parallel ranks: not divisible by {tp_size}."
|
| 206 |
+
)
|
| 207 |
+
super()._partition_linear_fn(name, module, device_mesh)
|
| 208 |
+
|
| 209 |
+
return _CheckedColwiseImpl()
|
| 210 |
+
|
| 211 |
+
def _make_checked_row(path: str) -> RowwiseParallel:
|
| 212 |
+
class _CheckedRowwiseImpl(RowwiseParallel):
|
| 213 |
+
def _partition_linear_fn(self, name, module, device_mesh):
|
| 214 |
+
tp_size = device_mesh.size()
|
| 215 |
+
in_features = module.weight.shape[1]
|
| 216 |
+
if in_features % tp_size != 0:
|
| 217 |
+
raise ValueError(
|
| 218 |
+
f"Cannot rowwise-shard '{path}' weight columns ({in_features}) across {tp_size} "
|
| 219 |
+
f"tensor-parallel ranks: not divisible by {tp_size}."
|
| 220 |
+
)
|
| 221 |
+
super()._partition_linear_fn(name, module, device_mesh)
|
| 222 |
+
|
| 223 |
+
return _CheckedRowwiseImpl()
|
| 224 |
+
|
| 225 |
+
resolved = {}
|
| 226 |
+
for path, style in relative_plan.items():
|
| 227 |
+
if style == "colwise":
|
| 228 |
+
resolved[path] = _make_checked_col(path)
|
| 229 |
+
elif style == "rowwise":
|
| 230 |
+
resolved[path] = _make_checked_row(path)
|
| 231 |
+
elif isinstance(style, PackedColwiseParallel):
|
| 232 |
+
resolved[path] = _make_packed_col(style)
|
| 233 |
+
elif isinstance(style, PackedRowwiseParallel):
|
| 234 |
+
resolved[path] = _make_packed_row(style)
|
| 235 |
+
else:
|
| 236 |
+
raise ValueError(
|
| 237 |
+
f"Unsupported tensor-parallel style '{style}' for '{path}'. "
|
| 238 |
+
f"Expected 'colwise', 'rowwise', PackedColwiseParallel, or PackedRowwiseParallel."
|
| 239 |
+
)
|
| 240 |
+
return resolved
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def apply_tensor_parallel(
|
| 244 |
+
model: torch.nn.Module,
|
| 245 |
+
config: TensorParallelConfig,
|
| 246 |
+
tp_plan: dict,
|
| 247 |
+
) -> None:
|
| 248 |
+
"""Apply tensor parallel on a model from its flat `_tp_plan`."""
|
| 249 |
+
tp_mesh = config._mesh
|
| 250 |
+
if tp_mesh is None:
|
| 251 |
+
raise ValueError("`config._mesh` is None. Call `config.setup(rank, world_size, device)` before applying TP.")
|
| 252 |
+
|
| 253 |
+
if tp_mesh.device_type not in _SUPPORTED_TP_DEVICES:
|
| 254 |
+
raise ValueError(
|
| 255 |
+
f"Tensor parallelism is not supported on device type '{tp_mesh.device_type}'. Supported device types are "
|
| 256 |
+
f"{list(_SUPPORTED_TP_DEVICES)}. The device type comes from the `mesh` passed to `TensorParallelConfig`, "
|
| 257 |
+
f"or from the active accelerator when the mesh is built from `tp_degree`."
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
backend = "neuron" if tp_mesh.device_type == "neuron" else "default"
|
| 261 |
+
groups = _resolve_tp_plan(model, tp_plan)
|
| 262 |
+
logger.debug(f"Applying tensor parallel (backend={backend}) over {len(groups)} module group(s) on mesh {tp_mesh}.")
|
| 263 |
+
|
| 264 |
+
if backend == "neuron":
|
| 265 |
+
from .tensor_parallel_neuron import _apply_tp_neuron
|
| 266 |
+
|
| 267 |
+
_apply_tp_neuron(model, tp_mesh, groups)
|
| 268 |
+
return
|
| 269 |
+
|
| 270 |
+
from torch.distributed.tensor.parallel import parallelize_module
|
| 271 |
+
|
| 272 |
+
for submodule, relative_plan in groups:
|
| 273 |
+
parallelize_module(submodule, tp_mesh, _styles(relative_plan))
|
diffusers/hooks/tensor_parallel_neuron.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Neuron backend for tensor parallelism, dispatched from `apply_tensor_parallel` when the TP mesh is on Neuron.
|
| 16 |
+
|
| 17 |
+
The difference from the generic path is a workaround for a Neuron NRT bug: consecutive `reduce_scatter` collectives for
|
| 18 |
+
large weight tensors (≥ 5120×5120) can fail when all layers are distributed in a single `parallelize_module` call. The
|
| 19 |
+
fix is to pre-shard each weight locally on CPU via `DTensor.from_local` *before* calling `parallelize_module`; the
|
| 20 |
+
latter then sees already-placed DTensors, skips the collective for weights, but still registers the required
|
| 21 |
+
input/output hooks for the forward pass.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import torch
|
| 25 |
+
import torch.distributed as dist
|
| 26 |
+
import torch.nn as nn
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _neuron_styles(relative_plan: dict) -> dict:
|
| 30 |
+
"""Map a `{relative_path: style}` plan to no-op-partition styles for Neuron.
|
| 31 |
+
|
| 32 |
+
Weights (and biases) are pre-sharded in `_pre_shard_and_tp`, so `parallelize_module` runs only to register the
|
| 33 |
+
forward hooks; `_partition_linear_fn` must not re-partition. Packed and plain styles share hook behavior, so both
|
| 34 |
+
collapse onto the two no-op styles.
|
| 35 |
+
"""
|
| 36 |
+
from torch.distributed.tensor.parallel import ColwiseParallel, RowwiseParallel
|
| 37 |
+
|
| 38 |
+
from .tensor_parallel import PackedColwiseParallel, PackedRowwiseParallel
|
| 39 |
+
|
| 40 |
+
class _NeuronColwise(ColwiseParallel):
|
| 41 |
+
def _partition_linear_fn(self, name, module, device_mesh):
|
| 42 |
+
pass # weight already Shard(0) via DTensor.from_local; parallelize_module runs only for the hooks
|
| 43 |
+
|
| 44 |
+
class _NeuronRowwise(RowwiseParallel):
|
| 45 |
+
def _partition_linear_fn(self, name, module, device_mesh):
|
| 46 |
+
pass # weight already Shard(1) via DTensor.from_local; parallelize_module runs only for the hooks
|
| 47 |
+
|
| 48 |
+
resolved = {}
|
| 49 |
+
for path, style in relative_plan.items():
|
| 50 |
+
if style == "colwise" or isinstance(style, PackedColwiseParallel):
|
| 51 |
+
resolved[path] = _NeuronColwise()
|
| 52 |
+
elif style == "rowwise" or isinstance(style, PackedRowwiseParallel):
|
| 53 |
+
resolved[path] = _NeuronRowwise()
|
| 54 |
+
else:
|
| 55 |
+
raise ValueError(
|
| 56 |
+
f"Unsupported tensor-parallel style '{style}' for '{path}'. "
|
| 57 |
+
f"Expected 'colwise', 'rowwise', PackedColwiseParallel, or PackedRowwiseParallel."
|
| 58 |
+
)
|
| 59 |
+
return resolved
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _pre_shard_and_tp(
|
| 63 |
+
module: nn.Module,
|
| 64 |
+
tp_mesh: "torch.distributed.device_mesh.DeviceMesh",
|
| 65 |
+
original_plan: dict,
|
| 66 |
+
rank: int,
|
| 67 |
+
tp_size: int,
|
| 68 |
+
) -> None:
|
| 69 |
+
"""Pre-shard Linear weights via `DTensor.from_local`, then call `parallelize_module`.
|
| 70 |
+
|
| 71 |
+
Workaround for a Neuron NRT bug where consecutive `reduce_scatter` calls for large weight tensors (≥ 5120×5120)
|
| 72 |
+
fail when all layers are distributed in a single `parallelize_module` call. Pre-sharding each weight on CPU means
|
| 73 |
+
it is already an on-device DTensor when `parallelize_module` runs (via `_neuron_styles`), so the collective is
|
| 74 |
+
skipped while the forward hooks are still registered.
|
| 75 |
+
"""
|
| 76 |
+
from torch.distributed.tensor import DTensor, Replicate, Shard
|
| 77 |
+
from torch.distributed.tensor.parallel import parallelize_module
|
| 78 |
+
|
| 79 |
+
from .tensor_parallel import PackedColwiseParallel, PackedRowwiseParallel, _blocks_to_block_sizes
|
| 80 |
+
|
| 81 |
+
device = torch.neuron.current_device()
|
| 82 |
+
|
| 83 |
+
for path, orig_style in original_plan.items():
|
| 84 |
+
# Resolve nested attribute path (e.g. "attn.to_q" or "attn.to_out.0")
|
| 85 |
+
submod = module
|
| 86 |
+
for part in path.split("."):
|
| 87 |
+
submod = getattr(submod, part)
|
| 88 |
+
|
| 89 |
+
if not hasattr(submod, "weight"):
|
| 90 |
+
raise ValueError(f"`_tp_plan` entry '{path}' does not resolve to a module with a `weight` parameter.")
|
| 91 |
+
|
| 92 |
+
w = submod.weight.data # CPU at this point
|
| 93 |
+
b = submod.bias.data if submod.bias is not None else None
|
| 94 |
+
if isinstance(orig_style, PackedColwiseParallel):
|
| 95 |
+
blocks = orig_style.blocks if orig_style.blocks is not None else getattr(submod, "_tp_packed_col_blocks")
|
| 96 |
+
block_sizes = _blocks_to_block_sizes(w.shape[0], blocks)
|
| 97 |
+
parts, bias_parts, offset = [], [], 0
|
| 98 |
+
for bs in block_sizes:
|
| 99 |
+
if bs % tp_size != 0:
|
| 100 |
+
raise ValueError(
|
| 101 |
+
f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
|
| 102 |
+
f"{bs} is not divisible by {tp_size}."
|
| 103 |
+
)
|
| 104 |
+
chunk = bs // tp_size
|
| 105 |
+
sl = slice(offset + rank * chunk, offset + (rank + 1) * chunk)
|
| 106 |
+
parts.append(w[sl, :].contiguous())
|
| 107 |
+
if b is not None:
|
| 108 |
+
bias_parts.append(b[sl].contiguous())
|
| 109 |
+
offset += bs
|
| 110 |
+
shard = torch.cat(parts, dim=0).to(device)
|
| 111 |
+
submod.weight = nn.Parameter(DTensor.from_local(shard, tp_mesh, [Shard(0)]))
|
| 112 |
+
if b is not None:
|
| 113 |
+
bias_shard = torch.cat(bias_parts, dim=0).to(device)
|
| 114 |
+
submod.bias = nn.Parameter(DTensor.from_local(bias_shard, tp_mesh, [Shard(0)]))
|
| 115 |
+
elif isinstance(orig_style, PackedRowwiseParallel):
|
| 116 |
+
blocks = orig_style.blocks if orig_style.blocks is not None else getattr(submod, "_tp_packed_row_blocks")
|
| 117 |
+
block_sizes = _blocks_to_block_sizes(w.shape[1], blocks)
|
| 118 |
+
parts, offset = [], 0
|
| 119 |
+
for bs in block_sizes:
|
| 120 |
+
if bs % tp_size != 0:
|
| 121 |
+
raise ValueError(
|
| 122 |
+
f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
|
| 123 |
+
f"{bs} is not divisible by {tp_size}."
|
| 124 |
+
)
|
| 125 |
+
chunk = bs // tp_size
|
| 126 |
+
parts.append(w[:, offset + rank * chunk : offset + (rank + 1) * chunk].contiguous())
|
| 127 |
+
offset += bs
|
| 128 |
+
shard = torch.cat(parts, dim=1).to(device)
|
| 129 |
+
submod.weight = nn.Parameter(DTensor.from_local(shard, tp_mesh, [Shard(1)]))
|
| 130 |
+
if b is not None: # rowwise bias is added post-reduction → keep it replicated
|
| 131 |
+
submod.bias = nn.Parameter(DTensor.from_local(b.to(device), tp_mesh, [Replicate()]))
|
| 132 |
+
elif orig_style == "colwise":
|
| 133 |
+
if w.shape[0] % tp_size != 0:
|
| 134 |
+
raise ValueError(
|
| 135 |
+
f"Cannot colwise-shard '{path}' weight rows ({w.shape[0]}) across {tp_size} "
|
| 136 |
+
f"tensor-parallel ranks: not divisible by {tp_size}."
|
| 137 |
+
)
|
| 138 |
+
rows = w.shape[0] // tp_size
|
| 139 |
+
sl = slice(rank * rows, (rank + 1) * rows)
|
| 140 |
+
submod.weight = nn.Parameter(DTensor.from_local(w[sl, :].contiguous().to(device), tp_mesh, [Shard(0)]))
|
| 141 |
+
if b is not None:
|
| 142 |
+
submod.bias = nn.Parameter(DTensor.from_local(b[sl].contiguous().to(device), tp_mesh, [Shard(0)]))
|
| 143 |
+
elif orig_style == "rowwise":
|
| 144 |
+
if w.shape[1] % tp_size != 0:
|
| 145 |
+
raise ValueError(
|
| 146 |
+
f"Cannot rowwise-shard '{path}' weight columns ({w.shape[1]}) across {tp_size} "
|
| 147 |
+
f"tensor-parallel ranks: not divisible by {tp_size}."
|
| 148 |
+
)
|
| 149 |
+
cols = w.shape[1] // tp_size
|
| 150 |
+
shard = w[:, rank * cols : (rank + 1) * cols].contiguous().to(device)
|
| 151 |
+
submod.weight = nn.Parameter(DTensor.from_local(shard, tp_mesh, [Shard(1)]))
|
| 152 |
+
if b is not None: # rowwise bias is added post-reduction → keep it replicated
|
| 153 |
+
submod.bias = nn.Parameter(DTensor.from_local(b.to(device), tp_mesh, [Replicate()]))
|
| 154 |
+
|
| 155 |
+
# parallelize_module is now a no-op for weight distribution (already DTensors)
|
| 156 |
+
# but still registers the input/output hooks required for the forward pass.
|
| 157 |
+
parallelize_module(module, tp_mesh, _neuron_styles(original_plan))
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def _apply_tp_neuron(
|
| 161 |
+
model: nn.Module,
|
| 162 |
+
tp_mesh: "torch.distributed.device_mesh.DeviceMesh",
|
| 163 |
+
groups: list,
|
| 164 |
+
) -> None:
|
| 165 |
+
"""Apply tensor parallelism on Neuron from resolved `_tp_plan` groups.
|
| 166 |
+
|
| 167 |
+
`groups` is produced by `diffusers.hooks.tensor_parallel._resolve_tp_plan` — the same source of truth used by the
|
| 168 |
+
generic path, so the two backends shard identical layers. For each `(block, relative_plan)` group this pre-shards
|
| 169 |
+
the weights via `DTensor.from_local` (Neuron NRT consecutive-reduce-scatter workaround), then calls
|
| 170 |
+
`parallelize_module` to register the forward hooks.
|
| 171 |
+
|
| 172 |
+
Model weights must be on CPU when this is called.
|
| 173 |
+
"""
|
| 174 |
+
rank = dist.get_rank()
|
| 175 |
+
tp_size = tp_mesh.size()
|
| 176 |
+
|
| 177 |
+
for block, relative_plan in groups:
|
| 178 |
+
_pre_shard_and_tp(block, tp_mesh, relative_plan, rank, tp_size)
|
diffusers/hooks/text_kv_cache.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from .hooks import BaseState, HookRegistry, ModelHook, StateManager
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
_TEXT_KV_CACHE_TRANSFORMER_HOOK = "text_kv_cache_transformer"
|
| 23 |
+
_TEXT_KV_CACHE_BLOCK_HOOK = "text_kv_cache_block"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class TextKVCacheConfig:
|
| 28 |
+
"""Enable exact (lossless) text K/V caching for transformer models.
|
| 29 |
+
|
| 30 |
+
Pre-computes per-block text key and value projections once before the denoising loop and reuses them across all
|
| 31 |
+
steps. Positive and negative prompts are distinguished via a stable cache key captured by a transformer-level hook
|
| 32 |
+
before any intermediate tensor allocations.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class TextKVCacheState(BaseState):
|
| 39 |
+
"""Shared state between the transformer-level and block-level hooks.
|
| 40 |
+
|
| 41 |
+
The transformer hook writes the stable ``encoder_hidden_states`` ``data_ptr()`` (captured *before* ``txt_norm``) so
|
| 42 |
+
that block hooks can use it as a reliable cache key across denoising steps.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self):
|
| 46 |
+
self.key: int | None = None
|
| 47 |
+
|
| 48 |
+
def reset(self):
|
| 49 |
+
self.key = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class TextKVCacheBlockState(BaseState):
|
| 53 |
+
"""Per-block state holding cached text key/value projections."""
|
| 54 |
+
|
| 55 |
+
def __init__(self):
|
| 56 |
+
self.kv_cache: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
|
| 57 |
+
|
| 58 |
+
def reset(self):
|
| 59 |
+
self.kv_cache.clear()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class TextKVCacheTransformerHook(ModelHook):
|
| 63 |
+
"""Captures ``encoder_hidden_states.data_ptr()`` before ``txt_norm``
|
| 64 |
+
and writes it to shared state for the block hooks to read."""
|
| 65 |
+
|
| 66 |
+
_is_stateful = True
|
| 67 |
+
|
| 68 |
+
def __init__(self, state_manager: StateManager):
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.state_manager = state_manager
|
| 71 |
+
|
| 72 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 73 |
+
if self.state_manager._current_context is None:
|
| 74 |
+
self.state_manager.set_context("inference")
|
| 75 |
+
|
| 76 |
+
encoder_hidden_states = kwargs.get("encoder_hidden_states")
|
| 77 |
+
if encoder_hidden_states is not None:
|
| 78 |
+
state: TextKVCacheState = self.state_manager.get_state()
|
| 79 |
+
state.key = encoder_hidden_states.data_ptr()
|
| 80 |
+
return self.fn_ref.original_forward(*args, **kwargs)
|
| 81 |
+
|
| 82 |
+
def reset_state(self, module: torch.nn.Module):
|
| 83 |
+
self.state_manager.reset()
|
| 84 |
+
return module
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class TextKVCacheBlockHook(ModelHook):
|
| 88 |
+
"""Caches ``(txt_key, txt_value)`` per block per unique prompt using
|
| 89 |
+
the stable cache key from the shared state."""
|
| 90 |
+
|
| 91 |
+
_is_stateful = True
|
| 92 |
+
|
| 93 |
+
def __init__(self, state_manager: StateManager, block_state_manager: StateManager):
|
| 94 |
+
super().__init__()
|
| 95 |
+
self.state_manager = state_manager
|
| 96 |
+
self.block_state_manager = block_state_manager
|
| 97 |
+
|
| 98 |
+
def new_forward(self, module: torch.nn.Module, *args, **kwargs):
|
| 99 |
+
from ..models.transformers.transformer_nucleusmoe_image import _apply_rotary_emb_nucleus
|
| 100 |
+
|
| 101 |
+
if self.state_manager._current_context is None:
|
| 102 |
+
self.state_manager.set_context("inference")
|
| 103 |
+
|
| 104 |
+
if self.block_state_manager._current_context is None:
|
| 105 |
+
self.block_state_manager.set_context("inference")
|
| 106 |
+
|
| 107 |
+
if "encoder_hidden_states" in kwargs:
|
| 108 |
+
encoder_hidden_states = kwargs["encoder_hidden_states"]
|
| 109 |
+
else:
|
| 110 |
+
encoder_hidden_states = args[1]
|
| 111 |
+
|
| 112 |
+
if "image_rotary_emb" in kwargs:
|
| 113 |
+
image_rotary_emb = kwargs["image_rotary_emb"]
|
| 114 |
+
elif len(args) > 3:
|
| 115 |
+
image_rotary_emb = args[3]
|
| 116 |
+
else:
|
| 117 |
+
image_rotary_emb = None
|
| 118 |
+
|
| 119 |
+
state: TextKVCacheState = self.state_manager.get_state()
|
| 120 |
+
cache_key = state.key
|
| 121 |
+
|
| 122 |
+
block_state: TextKVCacheBlockState = self.block_state_manager.get_state()
|
| 123 |
+
|
| 124 |
+
if cache_key not in block_state.kv_cache:
|
| 125 |
+
context = module.encoder_proj(encoder_hidden_states)
|
| 126 |
+
|
| 127 |
+
attn = module.attn
|
| 128 |
+
head_dim = attn.inner_dim // attn.heads
|
| 129 |
+
num_kv_heads = attn.inner_kv_dim // head_dim
|
| 130 |
+
|
| 131 |
+
txt_key = attn.add_k_proj(context).unflatten(-1, (num_kv_heads, -1))
|
| 132 |
+
txt_value = attn.add_v_proj(context).unflatten(-1, (num_kv_heads, -1))
|
| 133 |
+
|
| 134 |
+
if attn.norm_added_k is not None:
|
| 135 |
+
txt_key = attn.norm_added_k(txt_key)
|
| 136 |
+
|
| 137 |
+
if image_rotary_emb is not None:
|
| 138 |
+
_, txt_freqs = image_rotary_emb
|
| 139 |
+
txt_key = _apply_rotary_emb_nucleus(txt_key, txt_freqs, use_real=False)
|
| 140 |
+
|
| 141 |
+
block_state.kv_cache[cache_key] = (txt_key, txt_value)
|
| 142 |
+
|
| 143 |
+
txt_key, txt_value = block_state.kv_cache[cache_key]
|
| 144 |
+
|
| 145 |
+
attn_kwargs = kwargs.get("attention_kwargs") or {}
|
| 146 |
+
attn_kwargs["cached_txt_key"] = txt_key
|
| 147 |
+
attn_kwargs["cached_txt_value"] = txt_value
|
| 148 |
+
kwargs["attention_kwargs"] = attn_kwargs
|
| 149 |
+
|
| 150 |
+
return self.fn_ref.original_forward(*args, **kwargs)
|
| 151 |
+
|
| 152 |
+
def reset_state(self, module: torch.nn.Module):
|
| 153 |
+
self.block_state_manager.reset()
|
| 154 |
+
return module
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def apply_text_kv_cache(module: torch.nn.Module, config: TextKVCacheConfig) -> None:
|
| 158 |
+
from ..models.transformers.transformer_nucleusmoe_image import NucleusMoEImageTransformerBlock
|
| 159 |
+
|
| 160 |
+
HookRegistry.check_if_exists_or_initialize(module)
|
| 161 |
+
|
| 162 |
+
state_manager = StateManager(TextKVCacheState)
|
| 163 |
+
|
| 164 |
+
transformer_hook = TextKVCacheTransformerHook(state_manager)
|
| 165 |
+
registry = HookRegistry.check_if_exists_or_initialize(module)
|
| 166 |
+
registry.register_hook(transformer_hook, _TEXT_KV_CACHE_TRANSFORMER_HOOK)
|
| 167 |
+
|
| 168 |
+
for _, submodule in module.named_modules():
|
| 169 |
+
if isinstance(submodule, NucleusMoEImageTransformerBlock):
|
| 170 |
+
block_state_manager = StateManager(TextKVCacheBlockState)
|
| 171 |
+
hook = TextKVCacheBlockHook(state_manager, block_state_manager)
|
| 172 |
+
block_registry = HookRegistry.check_if_exists_or_initialize(submodule)
|
| 173 |
+
block_registry.register_hook(hook, _TEXT_KV_CACHE_BLOCK_HOOK)
|