Spaces:
Running on Zero
Running on Zero
MiniMax-H3 ref2va, the denoising half of the split deployment
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +144 -7
- app.py +420 -0
- diffusers/__init__.py +1750 -0
- diffusers/callbacks.py +244 -0
- diffusers/commands/__init__.py +27 -0
- diffusers/commands/custom_blocks.py +140 -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 +1227 -0
- diffusers/commands/schema.py +287 -0
- diffusers/commands/skills.py +344 -0
- diffusers/configuration_utils.py +752 -0
- diffusers/dependency_versions_check.py +34 -0
- diffusers/dependency_versions_table.py +57 -0
- diffusers/experimental/README.md +5 -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 +31 -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/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 +30 -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 +312 -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/text_kv_cache.py +173 -0
- diffusers/hooks/utils.py +43 -0
- diffusers/image_processor.py +1468 -0
- diffusers/loaders/__init__.py +159 -0
README.md
CHANGED
|
@@ -1,13 +1,150 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: MiniMax-H3 references (split, bf16)
|
| 3 |
+
emoji: 🎭
|
| 4 |
+
colorFrom: pink
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.20.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
short_description: Unquantized MiniMax-H3 ref2va with AoTI blocks, split in two
|
| 11 |
+
suggested_hardware: zero-a10g
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# MiniMax-H3 — omni-references, unquantized, split across two Spaces
|
| 15 |
+
|
| 16 |
+
Joint video **and** soundtrack out of a single denoising pass, conditioned on an ordered list of image, video and
|
| 17 |
+
audio references, at **bfloat16 with no quantization anywhere**.
|
| 18 |
+
|
| 19 |
+
This Space is the denoising half of the `ref2va` task: the 61.73 GiB `transformer_ref` partition and the two
|
| 20 |
+
autoencoders. The 62.14 GiB Qwen3-VL conditioner runs in
|
| 21 |
+
[`minimax-h3-conditioner`](https://huggingface.co/spaces/diffusers-internal-dev/minimax-h3-conditioner), which this
|
| 22 |
+
Space calls over the gradio API for every request — the same conditioner Space, and the same resident weights, that
|
| 23 |
+
the keyframe half [`minimax-h3-generator`](https://huggingface.co/spaces/diffusers-internal-dev/minimax-h3-generator)
|
| 24 |
+
uses.
|
| 25 |
+
|
| 26 |
+
## Why split
|
| 27 |
+
|
| 28 |
+
MiniMax-H3 is 195.9 GiB in bfloat16 and a ZeroGPU Space is evicted at **150 GB of storage**. An unquantized single
|
| 29 |
+
Space is therefore impossible. Cut the `MiniMaxH3Ref2VABlocks` sequence at its `text_encoder` step and both halves
|
| 30 |
+
fit unquantized:
|
| 31 |
+
|
| 32 |
+
| Space | Subfolders | Download | Resident |
|
| 33 |
+
|---|---|---|---|
|
| 34 |
+
| [`minimax-h3-conditioner`](https://huggingface.co/spaces/diffusers-internal-dev/minimax-h3-conditioner) | `text_encoder/` + `tokenizer/` + `processor/` | 66.7 GB | 62.15 GiB bf16 |
|
| 35 |
+
| this one | `transformer_ref/` + `vae/` + `audio_vae/` | 77.3 GB | 61.73 GiB bf16 + 10.43 GiB float32 |
|
| 36 |
+
|
| 37 |
+
## References
|
| 38 |
+
|
| 39 |
+
A request carries up to **12** references — at most 9 images, 3 videos and 3 audio clips — **in the order the model
|
| 40 |
+
reads them**. The order is semantic: it numbers the labels of MiniMax-H3's prompt presentation (`<Picture 1>`,
|
| 41 |
+
`<Video 1>`, `<Audio 1>`) and it advances the shared audio/video rotary clock, so the same references in a different
|
| 42 |
+
order are a different request. This demo exposes the first four slots in a fixed order — one video, two images, one
|
| 43 |
+
audio clip — and assembles them video, images, audio.
|
| 44 |
+
|
| 45 |
+
Rules the model imposes, enforced here before anything is uploaded:
|
| 46 |
+
|
| 47 |
+
* an audio reference cannot be the only one; it needs an image or a video alongside it,
|
| 48 |
+
* a reference video runs 2 to 15 seconds, and brings its own soundtrack with it,
|
| 49 |
+
* the generated duration may be left to the references, but only when exactly one of them carries a soundtrack —
|
| 50 |
+
which is why the duration slider disappears when a single reference can set it, and comes back when two can or
|
| 51 |
+
when the one that could is out of range.
|
| 52 |
+
|
| 53 |
+
## How the split is expressed
|
| 54 |
+
|
| 55 |
+
`MiniMaxH3Ref2VABlocks` is a `SequentialPipelineBlocks` of eight steps:
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
setup -> text_encoder -> reference_encoder -> prepare_layout -> prepare_latents -> set_timesteps -> denoise -> decode
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
`h3_split_blocks.py` subclasses it with the `text_encoder` step removed. Dropping the step drops the three
|
| 62 |
+
components it declares, so `load_components` resolves `transformer_ref` / `vae` / `audio_vae` / the two schedulers
|
| 63 |
+
out of the shared `modular_model_index.json` and never fetches the conditioner — and `prompt_embeds` and
|
| 64 |
+
`text_token_tags` become ordinary required inputs of the pipeline call:
|
| 65 |
+
|
| 66 |
+
```py
|
| 67 |
+
pipe = MiniMaxH3Ref2VAGeneratorBlocks().init_pipeline("diffusers-internal-dev/MiniMax-H3")
|
| 68 |
+
pipe.load_components(dtype=torch.bfloat16)
|
| 69 |
+
state = pipe(prompt_embeds=..., text_token_tags=..., references=[...], height=544, width=960, num_frames=124,
|
| 70 |
+
num_inference_steps=28)
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
Only **text** encoding is remote. `reference_encoder` is the `ref2va` blockset's own encoder step — it runs the video
|
| 74 |
+
VAE over the image and video references and the audio VAE over the soundtracks, and it is where the references'
|
| 75 |
+
latent geometry is resolved — so it stays on this side, next to the autoencoders the conditioner Space does not hold.
|
| 76 |
+
|
| 77 |
+
The wire format is the same two tensors as the keyframe half: `(1, num_text_tokens, 5120)` bfloat16 and
|
| 78 |
+
`(num_text_tokens,)` int64, carried as one safetensors file with the resolved `height` / `width` / `num_frames` in
|
| 79 |
+
its metadata header. What differs is only what the conditioner is shown, so the references travel to it as files:
|
| 80 |
+
`ref2va`'s presentation puts a vision block in front of the prompt for every image and every merged video frame
|
| 81 |
+
pair. An audio reference contributes its `"<Audio j>: "` label and nothing else — a waveform never reaches the
|
| 82 |
+
conditioner — but it still goes over, because a single audio-bearing reference is what resolves `num_frames` when
|
| 83 |
+
the request leaves it open.
|
| 84 |
+
|
| 85 |
+
The `setup` step runs on **both** halves. It owns no component (PIL, PyAV-decoded media and arithmetic) and it
|
| 86 |
+
resolves the canvas, the `17 * n + 5` frame count and the references prepared at their own resolutions. It is
|
| 87 |
+
deterministic over the same files, and the conditioner returns the plan it resolved so this Space pins the same
|
| 88 |
+
canvas and frame count rather than re-deriving them.
|
| 89 |
+
|
| 90 |
+
## AoTI-compiled blocks
|
| 91 |
+
|
| 92 |
+
With `H3_AOTI=1` the 50 repeated transformer blocks run from a compiled package,
|
| 93 |
+
`diffusers-internal-dev/minimax-h3-aoti:bf16/torch2.11/sm120/dynamic` — a single dynamic-sequence artifact that serves
|
| 94 |
+
every canvas, duration, reference set and prompt length.
|
| 95 |
+
|
| 96 |
+
It is the **same package the `transformer/` partition runs**, and nothing about it is partition-specific. The two
|
| 97 |
+
`config.json` files are identical field for field, and the package carries no weights at all: `LazyAOTIModel` binds
|
| 98 |
+
each block's own live `state_dict()` by name on its first forward. Patching it in is startup CPU work and costs no
|
| 99 |
+
GPU time.
|
| 100 |
+
|
| 101 |
+
It removes a near-constant ~0.5 s/step — 50 blocks' worth of kernel-launch overhead plus the norm / rotary / AdaLN
|
| 102 |
+
epilogues around the matmuls — and cannot touch the matmuls themselves, so it pays best where the block is not
|
| 103 |
+
compute bound. `ref2va` packs the reference rows in front of the generated ones, which makes the sequence longer
|
| 104 |
+
than a keyframe request at the same canvas and moves it further toward compute bound.
|
| 105 |
+
|
| 106 |
+
## Nothing is paid for with GPU time
|
| 107 |
+
|
| 108 |
+
The 77.3 GB download and the load happen at **startup**: `import spaces` at module top patches `torch.cuda` before
|
| 109 |
+
any GPU is attached, so nothing about the load needs a card. The conditioner round trip is a network call on this
|
| 110 |
+
Space's CPU. A `@spaces.GPU` call is therefore only the placement (once), the two reference encoders, the denoise
|
| 111 |
+
loop and the two decoders.
|
| 112 |
+
|
| 113 |
+
One thing does *not* happen at startup: the move onto the card. `spaces`' startup `torch.pack()` writes every
|
| 114 |
+
startup-resident CUDA tensor to a second copy on disk before deleting the downloaded originals, and 77.3 GB of
|
| 115 |
+
weights plus a 77.3 GB pack is 154.6 GB against a 150 GB quota — the Space is evicted mid-pack with `OSError:
|
| 116 |
+
[Errno 28] No space left on device`. Placement therefore happens on the **first GPU call**, `PIPE.to("cuda")` at the
|
| 117 |
+
top of the `@spaces.GPU` function: about 10 s of PCIe once, then a no-op walk, and the denoise loop runs with
|
| 118 |
+
everything resident and no offloading at all.
|
| 119 |
+
|
| 120 |
+
The references are decoded inside that call too, from their paths rather than as decoded media. A `@spaces.GPU`
|
| 121 |
+
argument crosses a process boundary by pickling, and a 5 s 1344x768 reference video is 370 MB of frames once PyAV
|
| 122 |
+
has expanded it.
|
| 123 |
+
|
| 124 |
+
## Generation constraints
|
| 125 |
+
|
| 126 |
+
Fixed by the checkpoint: 24 fps, a 768 pixel short edge, 5 to 15 s, `num_frames` snapped up to the next `17 * n + 5`,
|
| 127 |
+
no CFG and no negative prompt (it is guidance-distilled, so every step is one forward pass). The duration slider
|
| 128 |
+
stops at 14 s because it is the *snapped* count that has to hold for the ceiling: 15 s is 360 frames, which rounds
|
| 129 |
+
up to 362, i.e. 15.083 s, and is refused.
|
| 130 |
+
|
| 131 |
+
## Space variables
|
| 132 |
+
|
| 133 |
+
| Variable | Default | Meaning |
|
| 134 |
+
|---|---|---|
|
| 135 |
+
| `H3_CONDITIONER` | `diffusers-internal-dev/minimax-h3-conditioner` | The Space this one asks for embeddings. |
|
| 136 |
+
| `H3_AOTI` | `0` | `1` loads the compiled block package. |
|
| 137 |
+
| `H3_PLACEMENT` | `lazy` | `lazy` moves all 72.16 GiB onto the card on the first GPU call and leaves it there; `offload` hands placement to `ComponentsManager.enable_auto_cpu_offload` instead. |
|
| 138 |
+
| `H3_ATTENTION` | `_native_cudnn` | cuDNN's fused kernel, 10–20% faster than the SDPA default and needs nothing installed. flash-attention 3 is sm90-only and this pool is sm120. |
|
| 139 |
+
| `H3_GPU_DURATION` | `900` | Seconds per request; the pool applies a 1.5 duration factor. |
|
| 140 |
+
| `H3_GPU_SIZE` | `xlarge` | ZeroGPU allocation size. `large` does not fit. |
|
| 141 |
+
|
| 142 |
+
## Required secret
|
| 143 |
+
|
| 144 |
+
`HF_TOKEN` — `diffusers-internal-dev/MiniMax-H3` is private, and so is the conditioner Space this one calls.
|
| 145 |
+
|
| 146 |
+
## Where diffusers comes from
|
| 147 |
+
|
| 148 |
+
MiniMax-H3 is modular-only and not in a released `diffusers`, so the integration branch's `src/diffusers` tree is
|
| 149 |
+
vendored here as a top-level `diffusers/` package; the working directory comes first on `sys.path`, so there is no
|
| 150 |
+
install step. `requirements.txt` only carries what that tree imports.
|
app.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MiniMax-H3 `ref2va`, split deployment — **the denoising half**.
|
| 2 |
+
|
| 3 |
+
This Space holds the `transformer_ref` partition of the checkpoint and the two autoencoders, **unquantized
|
| 4 |
+
bfloat16**, and nothing else. The 62.14 GiB Qwen3-VL conditioner lives in its own Space,
|
| 5 |
+
[`minimax-h3-conditioner`](https://huggingface.co/spaces/diffusers-internal-dev/minimax-h3-conditioner), which this
|
| 6 |
+
one calls over the gradio API for every request; what comes back is a safetensors file holding the two tensors the
|
| 7 |
+
denoiser needs, `prompt_embeds` and `text_token_tags`.
|
| 8 |
+
|
| 9 |
+
Why split at all: MiniMax-H3 is 195.9 GiB in bfloat16 and a ZeroGPU Space is evicted at 150 GB of storage, so an
|
| 10 |
+
unquantized single Space is impossible. Cut at the text-encoder step, this half pulls 77.3 GB (`transformer_ref/`
|
| 11 |
+
61.73 GiB + `vae/` 9.70 + `audio_vae/` 0.56) and the conditioner 66.7 GB, and neither is quantized.
|
| 12 |
+
|
| 13 |
+
The blockset is `MiniMaxH3Ref2VABlocks` with its `text_encoder` step removed — see `h3_split_blocks.py`. Only *text*
|
| 14 |
+
encoding is remote: `reference_encoder` is the `ref2va` blockset's own encoder step and runs here, next to the two
|
| 15 |
+
autoencoders it needs.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import os
|
| 21 |
+
import tempfile
|
| 22 |
+
import time
|
| 23 |
+
import traceback
|
| 24 |
+
|
| 25 |
+
# First, and at module level. `import spaces` patches `torch.cuda` before any GPU is attached, which is what lets the
|
| 26 |
+
# 72 GiB load happen at **startup** rather than on GPU time; it also has to precede anything that initializes CUDA.
|
| 27 |
+
import spaces
|
| 28 |
+
import gradio as gr
|
| 29 |
+
|
| 30 |
+
MODEL_REPO = os.environ.get("H3_MODEL_REPO", "diffusers-internal-dev/MiniMax-H3")
|
| 31 |
+
CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "diffusers-internal-dev/minimax-h3-conditioner")
|
| 32 |
+
# `lazy` moves all 72.16 GiB onto the card on the first GPU call and leaves it there; `offload` hands placement to
|
| 33 |
+
# `ComponentsManager.enable_auto_cpu_offload` instead. Neither puts anything on the card at *startup*, which is
|
| 34 |
+
# deliberate — see `load_models`: the 150 GB storage quota, not the 95 GiB card, is what rules that out here.
|
| 35 |
+
PLACEMENT = os.environ.get("H3_PLACEMENT", "lazy").lower()
|
| 36 |
+
# cuDNN's fused attention is 10-20% faster than the SDPA default on this pool and needs nothing installed.
|
| 37 |
+
# flash-attention 3 is sm90-only and this card is sm120 (the `zero-a10g` flavour name is legacy).
|
| 38 |
+
ATTENTION = os.environ.get("H3_ATTENTION", "_native_cudnn").lower()
|
| 39 |
+
GPU_DURATION = int(os.environ.get("H3_GPU_DURATION", "900"))
|
| 40 |
+
GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge")
|
| 41 |
+
|
| 42 |
+
# MiniMax-H3's own canvases, i.e. `resolve_canvas_size` from `diffusers.modular_pipelines.minimax_h3.packing`
|
| 43 |
+
# evaluated for the six released aspect ratios. Hardcoded so the UI renders before `diffusers` is importable.
|
| 44 |
+
# Must stay identical to the conditioner's table: this Space forwards the *label* to the conditioner, so a canvas
|
| 45 |
+
# that half does not know is rejected there and surfaces as a failure here.
|
| 46 |
+
CANVASES = {
|
| 47 |
+
"960x544 · 16:9 fast (default)": (544, 960),
|
| 48 |
+
"1024x576 · 16:9 fast": (576, 1024),
|
| 49 |
+
"1152x640 · 16:9": (640, 1152),
|
| 50 |
+
"1280x704 · 16:9": (704, 1280),
|
| 51 |
+
"1344x768 · 16:9 full": (768, 1344),
|
| 52 |
+
"544x960 · 9:16 fast": (960, 544),
|
| 53 |
+
"640x1152 · 9:16": (1152, 640),
|
| 54 |
+
"768x1344 · 9:16 full": (1344, 768),
|
| 55 |
+
"768x768 · 1:1 full": (768, 768),
|
| 56 |
+
"1024x768 · 4:3 full": (768, 1024),
|
| 57 |
+
"768x1024 · 3:4 full": (1024, 768),
|
| 58 |
+
"1536x672 · 21:9 full": (672, 1536),
|
| 59 |
+
}
|
| 60 |
+
DEFAULT_CANVAS = "960x544 · 16:9 fast (default)"
|
| 61 |
+
FPS, FRAMES_PER_CHUNK, LATENTS_PER_CHUNK = 24, 17, 5
|
| 62 |
+
# 15 s is the checkpoint's ceiling, but it is the *snapped* frame count that has to hold for it: 15 s is 360 frames,
|
| 63 |
+
# which rounds up to 362, i.e. 15.083 s, and is refused. 14 is the last whole second that survives the snap.
|
| 64 |
+
MAX_UI_DURATION = 14
|
| 65 |
+
MIN_DURATION = 5
|
| 66 |
+
# A reference video shorter than 2 s gives the model almost no motion to read, and 15 s is the checkpoint's ceiling.
|
| 67 |
+
MIN_REFERENCE_VIDEO, MAX_REFERENCE_VIDEO = 2.0, 15.0
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def snap_frames(seconds: float) -> int:
|
| 71 |
+
"""The frame count MiniMax-H3's video VAE can decode: the next `17 * n + 5` at 24 fps."""
|
| 72 |
+
frames = max(1, round(float(seconds) * FPS))
|
| 73 |
+
while frames % FRAMES_PER_CHUNK != LATENTS_PER_CHUNK:
|
| 74 |
+
frames += 1
|
| 75 |
+
return frames
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
PIPE = None
|
| 79 |
+
MANAGER = None
|
| 80 |
+
LOAD_ERROR: str | None = None
|
| 81 |
+
CLIENT = None
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def load_models() -> str | None:
|
| 85 |
+
"""Load the denoising half. At **startup**, but *not* onto the card.
|
| 86 |
+
|
| 87 |
+
`MiniMaxH3Ref2VAGeneratorBlocks` declares `transformer_ref`, `vae`, `audio_vae`, `scheduler`, `audio_scheduler`
|
| 88 |
+
and `video_processor`, so `load_components` fetches exactly those subfolders out of the shared
|
| 89 |
+
`modular_model_index.json` — `text_encoder/` and the `transformer/` partition are never touched.
|
| 90 |
+
|
| 91 |
+
Both autoencoders carry `_keep_in_fp32_modules` over every module, so the `dtype` below is refused for them and
|
| 92 |
+
they stay float32: a bfloat16 audio VAE decodes the soundtrack roughly 20 dB too quiet.
|
| 93 |
+
|
| 94 |
+
Nothing is moved onto the card here, which is the one place this Space departs from the ZeroGPU idiom, and the
|
| 95 |
+
reason is storage rather than memory. `spaces`' startup `torch.pack()` writes every startup-resident CUDA tensor
|
| 96 |
+
to a **second copy on disk** and only deletes the downloaded originals afterwards; 77.3 GB of weights plus a
|
| 97 |
+
77.3 GB pack is 154.6 GB against a 150 GB quota, and the Space is evicted mid-pack with `OSError: [Errno 28] No
|
| 98 |
+
space left on device` out of `os.posix_fallocate`. Placement therefore happens on the first GPU call, where it
|
| 99 |
+
costs about 10 s of PCIe and then persists across every later request in the same worker.
|
| 100 |
+
"""
|
| 101 |
+
global PIPE, MANAGER, LOAD_ERROR
|
| 102 |
+
|
| 103 |
+
if PIPE is not None or LOAD_ERROR is not None:
|
| 104 |
+
return LOAD_ERROR
|
| 105 |
+
|
| 106 |
+
token = os.environ.get("HF_TOKEN")
|
| 107 |
+
if not token:
|
| 108 |
+
LOAD_ERROR = f"**`HF_TOKEN` secret is missing** and `{MODEL_REPO}` is private. Add it and restart."
|
| 109 |
+
return LOAD_ERROR
|
| 110 |
+
|
| 111 |
+
started = time.time()
|
| 112 |
+
try:
|
| 113 |
+
import torch
|
| 114 |
+
from diffusers import ComponentsManager
|
| 115 |
+
|
| 116 |
+
from h3_split_blocks import MiniMaxH3Ref2VAGeneratorBlocks
|
| 117 |
+
|
| 118 |
+
manager = ComponentsManager()
|
| 119 |
+
blocks = MiniMaxH3Ref2VAGeneratorBlocks()
|
| 120 |
+
print(f"[ref2va] loading {[c.name for c in blocks.expected_components]} from {MODEL_REPO} ...", flush=True)
|
| 121 |
+
pipe = blocks.init_pipeline(MODEL_REPO, components_manager=manager, collection="h3")
|
| 122 |
+
pipe.load_components(dtype=torch.bfloat16, token=token)
|
| 123 |
+
pipe.transformer_ref.set_attention_backend(ATTENTION)
|
| 124 |
+
|
| 125 |
+
# Still startup, still free: an AoTI package carries no weights and opens its compiled archive lazily inside
|
| 126 |
+
# the GPU worker, so pointing the 50-block stack at it is CPU work. Off unless `H3_AOTI=1`.
|
| 127 |
+
#
|
| 128 |
+
# It is the *same* package the `transformer/` partition runs, `bf16/torch2.11/sm120/dynamic`. Nothing about
|
| 129 |
+
# it is partition-specific: the two `config.json` files are identical field for field, and `LazyAOTIModel`
|
| 130 |
+
# binds each block's own live `state_dict()` by name on its first forward, so the compiled code carries no
|
| 131 |
+
# weights of either partition.
|
| 132 |
+
import h3_aoti
|
| 133 |
+
|
| 134 |
+
h3_aoti.maybe_load(pipe.transformer_ref)
|
| 135 |
+
|
| 136 |
+
if PLACEMENT == "offload":
|
| 137 |
+
manager.enable_auto_cpu_offload(device="cuda")
|
| 138 |
+
_arm_decode_hooks(pipe)
|
| 139 |
+
|
| 140 |
+
PIPE, MANAGER = pipe, manager
|
| 141 |
+
print(f"[ref2va] ready in {time.time() - started:.0f}s", flush=True)
|
| 142 |
+
except Exception as error:
|
| 143 |
+
traceback.print_exc()
|
| 144 |
+
LOAD_ERROR = (
|
| 145 |
+
f"**Loading `{MODEL_REPO}` failed** after {time.time() - started:.0f}s: "
|
| 146 |
+
f"`{type(error).__name__}: {error}`"
|
| 147 |
+
)
|
| 148 |
+
return LOAD_ERROR
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _arm_decode_hooks(pipe):
|
| 152 |
+
"""Make the offload hooks fire for the two VAEs.
|
| 153 |
+
|
| 154 |
+
`enable_auto_cpu_offload` installs accelerate hooks, which wrap `forward`. The reference-encoder and decode
|
| 155 |
+
blocks call `components.vae.encode/decode(...)` and `components.audio_vae.encode/decode(...)` directly, so the
|
| 156 |
+
hook never runs and the VAE is still on the host when the latents arrive on the card.
|
| 157 |
+
"""
|
| 158 |
+
for name in ("vae", "audio_vae"):
|
| 159 |
+
module = getattr(pipe, name)
|
| 160 |
+
for method in ("encode", "decode"):
|
| 161 |
+
inner = getattr(module, method)
|
| 162 |
+
|
| 163 |
+
def armed(*args, _module=module, _inner=inner, **kwargs):
|
| 164 |
+
hook = getattr(_module, "_hf_hook", None)
|
| 165 |
+
if hook is not None:
|
| 166 |
+
hook.pre_forward(_module)
|
| 167 |
+
return _inner(*args, **kwargs)
|
| 168 |
+
|
| 169 |
+
setattr(module, method, armed)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def conditioner():
|
| 173 |
+
"""The other half, over the gradio API. Cached — building a `Client` costs a round trip to the Space config."""
|
| 174 |
+
global CLIENT
|
| 175 |
+
if CLIENT is None:
|
| 176 |
+
from gradio_client import Client
|
| 177 |
+
|
| 178 |
+
CLIENT = Client(CONDITIONER_SPACE, token=os.environ.get("HF_TOKEN"))
|
| 179 |
+
return CLIENT
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def probe(path: str) -> tuple[float | None, float | None]:
|
| 183 |
+
"""`(video seconds, audio seconds)` of a media file, either being `None` when the stream is absent."""
|
| 184 |
+
import av
|
| 185 |
+
|
| 186 |
+
def seconds(stream, container):
|
| 187 |
+
if stream.duration is not None and stream.time_base is not None:
|
| 188 |
+
return float(stream.duration * stream.time_base)
|
| 189 |
+
return None if container.duration is None else container.duration / av.time_base
|
| 190 |
+
|
| 191 |
+
with av.open(path) as container:
|
| 192 |
+
video = seconds(container.streams.video[0], container) if container.streams.video else None
|
| 193 |
+
audio = seconds(container.streams.audio[0], container) if container.streams.audio else None
|
| 194 |
+
return video, audio
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def collect(video_path, first_image_path, second_image_path, audio_path) -> list[tuple[str, str]]:
|
| 198 |
+
"""The `(kind, path)` references of a request, **in the order the model reads them**.
|
| 199 |
+
|
| 200 |
+
That order is semantic rather than cosmetic: it numbers the labels of MiniMax-H3's prompt presentation and it
|
| 201 |
+
advances the shared audio/video rotary clock, so the same references in a different order are a different
|
| 202 |
+
request. Video first, then images, then a standalone audio clip — which is also the order the packed sequence
|
| 203 |
+
lays them out in.
|
| 204 |
+
"""
|
| 205 |
+
ordered = [("video", video_path)] if video_path else []
|
| 206 |
+
ordered += [("image", path) for path in (first_image_path, second_image_path) if path]
|
| 207 |
+
if audio_path:
|
| 208 |
+
ordered.append(("audio", audio_path))
|
| 209 |
+
return ordered
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def audio_bearing(references: list[tuple[str, str]]) -> list[tuple[str, float]]:
|
| 213 |
+
"""The references that carry a waveform, and how long it is. A video reference brings its own soundtrack."""
|
| 214 |
+
carried = []
|
| 215 |
+
for kind, path in references:
|
| 216 |
+
if kind == "image":
|
| 217 |
+
continue
|
| 218 |
+
_, audio_seconds = probe(path)
|
| 219 |
+
if audio_seconds is not None:
|
| 220 |
+
carried.append((kind, audio_seconds))
|
| 221 |
+
return carried
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def duration_controls(video_path, first_image_path, second_image_path, audio_path, match: bool):
|
| 225 |
+
"""Show the duration slider unless a single soundtrack can set it, which is when MiniMax-H3 lets it be left out."""
|
| 226 |
+
try:
|
| 227 |
+
carried = audio_bearing(collect(video_path, first_image_path, second_image_path, audio_path))
|
| 228 |
+
except Exception:
|
| 229 |
+
carried = []
|
| 230 |
+
# Exactly one soundtrack, and one long enough to be a duration MiniMax-H3 generates. Anything else and the
|
| 231 |
+
# request is ambiguous or out of range, so the slider stays and nothing is derived.
|
| 232 |
+
derivable = len(carried) == 1 and MIN_DURATION <= snap_frames(carried[0][1]) / FPS <= MAX_REFERENCE_VIDEO
|
| 233 |
+
return gr.update(visible=derivable), gr.update(visible=not (derivable and match))
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def check(prompt: str, references: list[tuple[str, str]]) -> None:
|
| 237 |
+
"""The model's own rules, before anything is uploaded or a card is allocated."""
|
| 238 |
+
if not prompt or not prompt.strip():
|
| 239 |
+
raise gr.Error("MiniMax-H3 always takes a prompt, references or not.")
|
| 240 |
+
if not references:
|
| 241 |
+
raise gr.Error("Add at least one reference — an image or a video for the model to condition on.")
|
| 242 |
+
if {kind for kind, _ in references} == {"audio"}:
|
| 243 |
+
raise gr.Error("An audio reference needs an image or a video alongside it; it cannot go on its own.")
|
| 244 |
+
for kind, path in references:
|
| 245 |
+
if kind != "video":
|
| 246 |
+
continue
|
| 247 |
+
video_seconds, _ = probe(path)
|
| 248 |
+
if video_seconds is None:
|
| 249 |
+
raise gr.Error("That reference video has no video stream. Drop it in the audio slot instead.")
|
| 250 |
+
if not MIN_REFERENCE_VIDEO <= video_seconds <= MAX_REFERENCE_VIDEO:
|
| 251 |
+
raise gr.Error(
|
| 252 |
+
f"The reference video is {video_seconds:.1f} s. Use a clip between "
|
| 253 |
+
f"{MIN_REFERENCE_VIDEO:g} and {MAX_REFERENCE_VIDEO:g} seconds."
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def encode_remote(prompt, references, canvas, num_frames):
|
| 258 |
+
"""Ask the conditioner Space for `prompt_embeds` + `text_token_tags`. Off this Space's GPU time entirely.
|
| 259 |
+
|
| 260 |
+
The references go over with the request: `ref2va`'s presentation puts a vision block in front of the prompt for
|
| 261 |
+
every image and every merged video frame pair, so the conditioner has to see them. It decodes the very same
|
| 262 |
+
files this Space does, which is what keeps the two `setup` runs in agreement.
|
| 263 |
+
"""
|
| 264 |
+
from gradio_client import handle_file
|
| 265 |
+
from safetensors import safe_open
|
| 266 |
+
|
| 267 |
+
path, plan = conditioner().predict(
|
| 268 |
+
prompt=prompt,
|
| 269 |
+
media=[handle_file(path) for _, path in references],
|
| 270 |
+
kinds=",".join(kind for kind, _ in references),
|
| 271 |
+
canvas=canvas,
|
| 272 |
+
num_frames=num_frames,
|
| 273 |
+
api_name="/encode_ref2va",
|
| 274 |
+
)
|
| 275 |
+
with safe_open(path, framework="pt") as handle:
|
| 276 |
+
return handle.get_tensor("prompt_embeds"), handle.get_tensor("text_token_tags"), handle.metadata(), plan
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
@spaces.GPU(duration=GPU_DURATION, size=GPU_SIZE)
|
| 280 |
+
def _generate(prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed):
|
| 281 |
+
"""The only thing on GPU time: the two reference encoders, the packed-sequence denoise loop and the decoders.
|
| 282 |
+
|
| 283 |
+
The references are built here rather than handed over already decoded. A `@spaces.GPU` argument crosses a
|
| 284 |
+
process boundary by pickling, and a 5 s 1344x768 reference video is 370 MB of frames once PyAV has expanded it;
|
| 285 |
+
the file path is a few bytes and the decode is CPU work either way.
|
| 286 |
+
|
| 287 |
+
Only the three generated outputs come back, for the same reason: the full `PipelineState` still holds the packed
|
| 288 |
+
latents, the rotary grid and the row indices on the card.
|
| 289 |
+
"""
|
| 290 |
+
import torch
|
| 291 |
+
|
| 292 |
+
from diffusers.modular_pipelines.minimax_h3 import MiniMaxH3Reference
|
| 293 |
+
|
| 294 |
+
if PLACEMENT == "lazy":
|
| 295 |
+
# 72.16 GiB across PCIe on the first request of a worker, a no-op walk on every one after it. Startup
|
| 296 |
+
# placement is not an option here — see `load_models` — and this is what buys the offload-free denoise loop.
|
| 297 |
+
PIPE.to("cuda")
|
| 298 |
+
|
| 299 |
+
state = PIPE(
|
| 300 |
+
prompt_embeds=prompt_embeds.to("cuda"),
|
| 301 |
+
text_token_tags=text_token_tags,
|
| 302 |
+
references=[MiniMaxH3Reference(**{kind: path}) for kind, path in references],
|
| 303 |
+
height=height,
|
| 304 |
+
width=width,
|
| 305 |
+
num_frames=num_frames,
|
| 306 |
+
num_inference_steps=int(steps),
|
| 307 |
+
generator=torch.Generator("cpu").manual_seed(int(seed)),
|
| 308 |
+
)
|
| 309 |
+
return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def generate(
|
| 313 |
+
prompt,
|
| 314 |
+
video_path,
|
| 315 |
+
first_image_path,
|
| 316 |
+
second_image_path,
|
| 317 |
+
audio_path,
|
| 318 |
+
canvas,
|
| 319 |
+
match,
|
| 320 |
+
duration,
|
| 321 |
+
steps,
|
| 322 |
+
seed,
|
| 323 |
+
progress=gr.Progress(track_tqdm=True),
|
| 324 |
+
):
|
| 325 |
+
if LOAD_ERROR:
|
| 326 |
+
raise gr.Error(LOAD_ERROR)
|
| 327 |
+
if PIPE is None:
|
| 328 |
+
raise gr.Error("The denoiser is still loading.")
|
| 329 |
+
|
| 330 |
+
from diffusers.utils import encode_video
|
| 331 |
+
|
| 332 |
+
references = collect(video_path, first_image_path, second_image_path, audio_path)
|
| 333 |
+
check(prompt, references)
|
| 334 |
+
|
| 335 |
+
# `0` is "leave it to the references" over the wire, which MiniMax-H3 accepts when exactly one of them carries a
|
| 336 |
+
# soundtrack. The conditioner resolves it either way and this Space pins whatever comes back.
|
| 337 |
+
derivable = len(audio_bearing(references)) == 1
|
| 338 |
+
requested = 0 if (match and derivable) else snap_frames(duration)
|
| 339 |
+
|
| 340 |
+
progress(0.0, desc="Reading the prompt and references ...")
|
| 341 |
+
conditioned = time.time()
|
| 342 |
+
try:
|
| 343 |
+
prompt_embeds, text_token_tags, metadata, plan = encode_remote(prompt, references, canvas, requested)
|
| 344 |
+
except gr.Error:
|
| 345 |
+
raise
|
| 346 |
+
except Exception as error:
|
| 347 |
+
raise gr.Error(str(error).strip().splitlines()[-1] if str(error).strip() else repr(error)) from error
|
| 348 |
+
condition_seconds = time.time() - conditioned
|
| 349 |
+
height, width, num_frames = (int(metadata[key]) for key in ("height", "width", "num_frames"))
|
| 350 |
+
|
| 351 |
+
progress(0.1, desc=f"Generating {num_frames / FPS:.1f} s at {width}x{height} ...")
|
| 352 |
+
started = time.time()
|
| 353 |
+
frames, audio, sampling_rate = _generate(
|
| 354 |
+
prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed
|
| 355 |
+
)
|
| 356 |
+
generate_seconds = time.time() - started
|
| 357 |
+
|
| 358 |
+
directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
|
| 359 |
+
os.makedirs(directory, exist_ok=True)
|
| 360 |
+
path = os.path.join(directory, f"h3-ref2va-{int(time.time() * 1000)}.mp4")
|
| 361 |
+
encode_video(frames, fps=FPS, output_path=path, audio=audio, audio_sample_rate=sampling_rate)
|
| 362 |
+
|
| 363 |
+
print(
|
| 364 |
+
f"[ref2va] {[kind for kind, _ in references]} · `{width}x{height}`, {num_frames} frames "
|
| 365 |
+
f"({num_frames / FPS:.3f} s), {int(steps)} steps · conditioner {condition_seconds:.0f}s "
|
| 366 |
+
f"({plan['num_text_tokens']} tokens) · denoise + decode {generate_seconds:.0f}s "
|
| 367 |
+
f"({generate_seconds / int(steps):.1f} s/step) · seed {int(seed)}",
|
| 368 |
+
flush=True,
|
| 369 |
+
)
|
| 370 |
+
return path
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
load_models()
|
| 374 |
+
|
| 375 |
+
INTRO = """# MiniMax-H3 · references
|
| 376 |
+
|
| 377 |
+
Bring a subject, a style, a camera move or a voice, and generate video with a synchronized soundtrack in one pass.
|
| 378 |
+
References are read **in the order below** — the video first, then the images, then the audio.
|
| 379 |
+
"""
|
| 380 |
+
|
| 381 |
+
with gr.Blocks(title="MiniMax-H3 references") as demo:
|
| 382 |
+
gr.Markdown(INTRO)
|
| 383 |
+
|
| 384 |
+
with gr.Row():
|
| 385 |
+
with gr.Column():
|
| 386 |
+
prompt = gr.Textbox(
|
| 387 |
+
label="Prompt",
|
| 388 |
+
lines=3,
|
| 389 |
+
value="The character walks through a neon-lit street in the rain, humming to themselves",
|
| 390 |
+
)
|
| 391 |
+
video = gr.Video(label="Motion & camera reference (optional, 2–15 s)")
|
| 392 |
+
with gr.Row():
|
| 393 |
+
first_image = gr.Image(label="Reference image", type="filepath")
|
| 394 |
+
second_image = gr.Image(label="Reference image", type="filepath")
|
| 395 |
+
audio = gr.Audio(label="Voice or music reference (optional)", type="filepath")
|
| 396 |
+
canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
|
| 397 |
+
match = gr.Checkbox(label="Match the reference soundtrack", value=True, visible=False)
|
| 398 |
+
duration = gr.Slider(
|
| 399 |
+
label="Duration (s)", minimum=MIN_DURATION, maximum=MAX_UI_DURATION, step=1, value=5
|
| 400 |
+
)
|
| 401 |
+
steps = gr.Slider(label="Steps", minimum=10, maximum=40, step=1, value=28)
|
| 402 |
+
seed = gr.Number(label="Seed", value=42, precision=0)
|
| 403 |
+
run = gr.Button("Generate", variant="primary")
|
| 404 |
+
with gr.Column():
|
| 405 |
+
result = gr.Video(label="Video + soundtrack")
|
| 406 |
+
|
| 407 |
+
slots = [video, first_image, second_image, audio]
|
| 408 |
+
for control in [*slots, match]:
|
| 409 |
+
control.change(duration_controls, [*slots, match], [match, duration], show_progress="hidden")
|
| 410 |
+
|
| 411 |
+
run.click(
|
| 412 |
+
generate,
|
| 413 |
+
[prompt, *slots, canvas, match, duration, steps, seed],
|
| 414 |
+
result,
|
| 415 |
+
api_name="generate",
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
if __name__ == "__main__":
|
| 420 |
+
demo.queue(max_size=4).launch(show_error=True)
|
diffusers/__init__.py
ADDED
|
@@ -0,0 +1,1750 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__version__ = "0.40.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 |
+
"PerturbedAttentionGuidance",
|
| 193 |
+
"SkipLayerGuidance",
|
| 194 |
+
"SmoothedEnergyGuidance",
|
| 195 |
+
"TangentialClassifierFreeGuidance",
|
| 196 |
+
]
|
| 197 |
+
)
|
| 198 |
+
_import_structure["hooks"].extend(
|
| 199 |
+
[
|
| 200 |
+
"FasterCacheConfig",
|
| 201 |
+
"FirstBlockCacheConfig",
|
| 202 |
+
"HookRegistry",
|
| 203 |
+
"LayerSkipConfig",
|
| 204 |
+
"MagCacheConfig",
|
| 205 |
+
"PyramidAttentionBroadcastConfig",
|
| 206 |
+
"SmoothedEnergyGuidanceConfig",
|
| 207 |
+
"TaylorSeerCacheConfig",
|
| 208 |
+
"TextKVCacheConfig",
|
| 209 |
+
"apply_faster_cache",
|
| 210 |
+
"apply_first_block_cache",
|
| 211 |
+
"apply_layer_skip",
|
| 212 |
+
"apply_mag_cache",
|
| 213 |
+
"apply_pyramid_attention_broadcast",
|
| 214 |
+
"apply_taylorseer_cache",
|
| 215 |
+
"apply_text_kv_cache",
|
| 216 |
+
]
|
| 217 |
+
)
|
| 218 |
+
_import_structure["image_processor"] = [
|
| 219 |
+
"InpaintProcessor",
|
| 220 |
+
"IPAdapterMaskProcessor",
|
| 221 |
+
"PixArtImageProcessor",
|
| 222 |
+
"VaeImageProcessor",
|
| 223 |
+
"VaeImageProcessorLDM3D",
|
| 224 |
+
]
|
| 225 |
+
_import_structure["models"].extend(
|
| 226 |
+
[
|
| 227 |
+
"AceStepTransformer1DModel",
|
| 228 |
+
"AllegroTransformer3DModel",
|
| 229 |
+
"AnimaTextConditioner",
|
| 230 |
+
"AnyFlowFARTransformer3DModel",
|
| 231 |
+
"AnyFlowTransformer3DModel",
|
| 232 |
+
"AsymmetricAutoencoderKL",
|
| 233 |
+
"AttentionBackendName",
|
| 234 |
+
"AuraFlowTransformer2DModel",
|
| 235 |
+
"AutoencoderDC",
|
| 236 |
+
"AutoencoderKL",
|
| 237 |
+
"AutoencoderKLAllegro",
|
| 238 |
+
"AutoencoderKLCogVideoX",
|
| 239 |
+
"AutoencoderKLCosmos",
|
| 240 |
+
"AutoencoderKLFlux2",
|
| 241 |
+
"AutoencoderKLHunyuanImage",
|
| 242 |
+
"AutoencoderKLHunyuanImageRefiner",
|
| 243 |
+
"AutoencoderKLHunyuanVideo",
|
| 244 |
+
"AutoencoderKLHunyuanVideo15",
|
| 245 |
+
"AutoencoderKLKVAE",
|
| 246 |
+
"AutoencoderKLKVAEVideo",
|
| 247 |
+
"AutoencoderKLLTX2Audio",
|
| 248 |
+
"AutoencoderKLLTX2Video",
|
| 249 |
+
"AutoencoderKLLTXVideo",
|
| 250 |
+
"AutoencoderKLMagvit",
|
| 251 |
+
"AutoencoderKLMiniMaxH3",
|
| 252 |
+
"AutoencoderKLMiniMaxH3Audio",
|
| 253 |
+
"AutoencoderKLMochi",
|
| 254 |
+
"AutoencoderKLQwenImage",
|
| 255 |
+
"AutoencoderKLTemporalDecoder",
|
| 256 |
+
"AutoencoderKLWan",
|
| 257 |
+
"AutoencoderOobleck",
|
| 258 |
+
"AutoencoderRAE",
|
| 259 |
+
"AutoencoderTiny",
|
| 260 |
+
"AutoencoderVidTok",
|
| 261 |
+
"AutoModel",
|
| 262 |
+
"BriaFiboTransformer2DModel",
|
| 263 |
+
"BriaTransformer2DModel",
|
| 264 |
+
"CacheMixin",
|
| 265 |
+
"ChromaTransformer2DModel",
|
| 266 |
+
"ChronoEditTransformer3DModel",
|
| 267 |
+
"CogVideoXTransformer3DModel",
|
| 268 |
+
"CogView3PlusTransformer2DModel",
|
| 269 |
+
"CogView4Transformer2DModel",
|
| 270 |
+
"ConsisIDTransformer3DModel",
|
| 271 |
+
"ConsistencyDecoderVAE",
|
| 272 |
+
"ContextParallelConfig",
|
| 273 |
+
"ControlNetModel",
|
| 274 |
+
"ControlNetUnionModel",
|
| 275 |
+
"ControlNetXSAdapter",
|
| 276 |
+
"Cosmos3AVAEAudioTokenizer",
|
| 277 |
+
"Cosmos3OmniTransformer",
|
| 278 |
+
"CosmosControlNetModel",
|
| 279 |
+
"CosmosTransformer3DModel",
|
| 280 |
+
"DiTTransformer2DModel",
|
| 281 |
+
"DreamLiteTransformer2DModel",
|
| 282 |
+
"DreamLiteUNetModel",
|
| 283 |
+
"EasyAnimateTransformer3DModel",
|
| 284 |
+
"ErnieImageTransformer2DModel",
|
| 285 |
+
"Flux2Transformer2DModel",
|
| 286 |
+
"FluxControlNetModel",
|
| 287 |
+
"FluxMultiControlNetModel",
|
| 288 |
+
"FluxTransformer2DModel",
|
| 289 |
+
"GlmImageTransformer2DModel",
|
| 290 |
+
"HeliosTransformer3DModel",
|
| 291 |
+
"HiDreamImageTransformer2DModel",
|
| 292 |
+
"HunyuanDiT2DControlNetModel",
|
| 293 |
+
"HunyuanDiT2DModel",
|
| 294 |
+
"HunyuanDiT2DMultiControlNetModel",
|
| 295 |
+
"HunyuanImageTransformer2DModel",
|
| 296 |
+
"HunyuanVideo15Transformer3DModel",
|
| 297 |
+
"HunyuanVideoFramepackTransformer3DModel",
|
| 298 |
+
"HunyuanVideoTransformer3DModel",
|
| 299 |
+
"I2VGenXLUNet",
|
| 300 |
+
"Ideogram4Transformer2DModel",
|
| 301 |
+
"JoyImageEditPlusTransformer3DModel",
|
| 302 |
+
"JoyImageEditTransformer3DModel",
|
| 303 |
+
"Kandinsky3UNet",
|
| 304 |
+
"Kandinsky5Transformer3DModel",
|
| 305 |
+
"Krea2Transformer2DModel",
|
| 306 |
+
"LatteTransformer3DModel",
|
| 307 |
+
"LongCatAudioDiTTransformer",
|
| 308 |
+
"LongCatAudioDiTVae",
|
| 309 |
+
"LongCatImageTransformer2DModel",
|
| 310 |
+
"LTX2VideoTransformer3DModel",
|
| 311 |
+
"LTXVideoTransformer3DModel",
|
| 312 |
+
"Lumina2Transformer2DModel",
|
| 313 |
+
"LuminaNextDiT2DModel",
|
| 314 |
+
"MiniMaxH3Transformer3DModel",
|
| 315 |
+
"MochiTransformer3DModel",
|
| 316 |
+
"ModelMixin",
|
| 317 |
+
"MotifVideoTransformer3DModel",
|
| 318 |
+
"MotionAdapter",
|
| 319 |
+
"MultiAdapter",
|
| 320 |
+
"MultiControlNetModel",
|
| 321 |
+
"NucleusMoEImageTransformer2DModel",
|
| 322 |
+
"OmniGenTransformer2DModel",
|
| 323 |
+
"OvisImageTransformer2DModel",
|
| 324 |
+
"ParallelConfig",
|
| 325 |
+
"PixArtTransformer2DModel",
|
| 326 |
+
"PriorTransformer",
|
| 327 |
+
"PRXTransformer2DModel",
|
| 328 |
+
"QwenImageControlNetModel",
|
| 329 |
+
"QwenImageMultiControlNetModel",
|
| 330 |
+
"QwenImageTransformer2DModel",
|
| 331 |
+
"SanaControlNetModel",
|
| 332 |
+
"SanaTransformer2DModel",
|
| 333 |
+
"SanaVideoTransformer3DModel",
|
| 334 |
+
"SD3ControlNetModel",
|
| 335 |
+
"SD3MultiControlNetModel",
|
| 336 |
+
"SD3Transformer2DModel",
|
| 337 |
+
"SkyReelsV2Transformer3DModel",
|
| 338 |
+
"SparseControlNetModel",
|
| 339 |
+
"StableAudioDiTModel",
|
| 340 |
+
"StableCascadeUNet",
|
| 341 |
+
"T2IAdapter",
|
| 342 |
+
"T5FilmDecoder",
|
| 343 |
+
"Transformer2DModel",
|
| 344 |
+
"TransformerTemporalModel",
|
| 345 |
+
"UNet1DModel",
|
| 346 |
+
"UNet2DConditionModel",
|
| 347 |
+
"UNet2DModel",
|
| 348 |
+
"UNet3DConditionModel",
|
| 349 |
+
"UNetControlNetXSModel",
|
| 350 |
+
"UNetMotionModel",
|
| 351 |
+
"UNetSpatioTemporalConditionModel",
|
| 352 |
+
"UVit2DModel",
|
| 353 |
+
"VQModel",
|
| 354 |
+
"WanAnimateTransformer3DModel",
|
| 355 |
+
"WanTransformer3DModel",
|
| 356 |
+
"WanVACETransformer3DModel",
|
| 357 |
+
"ZImageControlNetModel",
|
| 358 |
+
"ZImageTransformer2DModel",
|
| 359 |
+
"attention_backend",
|
| 360 |
+
]
|
| 361 |
+
)
|
| 362 |
+
_import_structure["modular_pipelines"].extend(
|
| 363 |
+
[
|
| 364 |
+
"AutoPipelineBlocks",
|
| 365 |
+
"ComponentsManager",
|
| 366 |
+
"ComponentSpec",
|
| 367 |
+
"ConditionalPipelineBlocks",
|
| 368 |
+
"ConfigSpec",
|
| 369 |
+
"InputParam",
|
| 370 |
+
"LoopSequentialPipelineBlocks",
|
| 371 |
+
"ModularPipeline",
|
| 372 |
+
"ModularPipelineBlocks",
|
| 373 |
+
"OutputParam",
|
| 374 |
+
"SequentialPipelineBlocks",
|
| 375 |
+
]
|
| 376 |
+
)
|
| 377 |
+
_import_structure["optimization"] = [
|
| 378 |
+
"get_constant_schedule",
|
| 379 |
+
"get_constant_schedule_with_warmup",
|
| 380 |
+
"get_cosine_schedule_with_warmup",
|
| 381 |
+
"get_cosine_with_hard_restarts_schedule_with_warmup",
|
| 382 |
+
"get_linear_schedule_with_warmup",
|
| 383 |
+
"get_polynomial_decay_schedule_with_warmup",
|
| 384 |
+
"get_scheduler",
|
| 385 |
+
]
|
| 386 |
+
_import_structure["pipelines"].extend(
|
| 387 |
+
[
|
| 388 |
+
"AudioPipelineOutput",
|
| 389 |
+
"AutoPipelineForImage2Image",
|
| 390 |
+
"AutoPipelineForInpainting",
|
| 391 |
+
"AutoPipelineForText2Audio",
|
| 392 |
+
"AutoPipelineForText2Image",
|
| 393 |
+
"ConsistencyModelPipeline",
|
| 394 |
+
"DanceDiffusionPipeline",
|
| 395 |
+
"DDIMPipeline",
|
| 396 |
+
"DDPMPipeline",
|
| 397 |
+
"DiffusionPipeline",
|
| 398 |
+
"DiTPipeline",
|
| 399 |
+
"ImagePipelineOutput",
|
| 400 |
+
"KarrasVePipeline",
|
| 401 |
+
"LDMPipeline",
|
| 402 |
+
"LDMSuperResolutionPipeline",
|
| 403 |
+
"PNDMPipeline",
|
| 404 |
+
"RePaintPipeline",
|
| 405 |
+
"ScoreSdeVePipeline",
|
| 406 |
+
"StableDiffusionMixin",
|
| 407 |
+
]
|
| 408 |
+
)
|
| 409 |
+
_import_structure["quantizers"] = ["DiffusersQuantizer"]
|
| 410 |
+
_import_structure["schedulers"].extend(
|
| 411 |
+
[
|
| 412 |
+
"AmusedScheduler",
|
| 413 |
+
"BlockRefinementScheduler",
|
| 414 |
+
"BlockRefinementSchedulerOutput",
|
| 415 |
+
"CMStochasticIterativeScheduler",
|
| 416 |
+
"CogVideoXDDIMScheduler",
|
| 417 |
+
"CogVideoXDPMScheduler",
|
| 418 |
+
"DDIMInverseScheduler",
|
| 419 |
+
"DDIMParallelScheduler",
|
| 420 |
+
"DDIMScheduler",
|
| 421 |
+
"DDPMParallelScheduler",
|
| 422 |
+
"DDPMScheduler",
|
| 423 |
+
"DDPMWuerstchenScheduler",
|
| 424 |
+
"DEISMultistepScheduler",
|
| 425 |
+
"DiscreteDDIMScheduler",
|
| 426 |
+
"DiscreteDDIMSchedulerOutput",
|
| 427 |
+
"DPMSolverMultistepInverseScheduler",
|
| 428 |
+
"DPMSolverMultistepScheduler",
|
| 429 |
+
"DPMSolverSinglestepScheduler",
|
| 430 |
+
"EDMDPMSolverMultistepScheduler",
|
| 431 |
+
"EDMEulerScheduler",
|
| 432 |
+
"EntropyBoundScheduler",
|
| 433 |
+
"EntropyBoundSchedulerOutput",
|
| 434 |
+
"EulerAncestralDiscreteScheduler",
|
| 435 |
+
"EulerDiscreteScheduler",
|
| 436 |
+
"FlowMapEulerDiscreteScheduler",
|
| 437 |
+
"FlowMatchEulerDiscreteScheduler",
|
| 438 |
+
"FlowMatchHeunDiscreteScheduler",
|
| 439 |
+
"FlowMatchLCMScheduler",
|
| 440 |
+
"HeliosDMDScheduler",
|
| 441 |
+
"HeliosScheduler",
|
| 442 |
+
"HeunDiscreteScheduler",
|
| 443 |
+
"IPNDMScheduler",
|
| 444 |
+
"KarrasVeScheduler",
|
| 445 |
+
"KDPM2AncestralDiscreteScheduler",
|
| 446 |
+
"KDPM2DiscreteScheduler",
|
| 447 |
+
"LCMScheduler",
|
| 448 |
+
"LTXEulerAncestralRFScheduler",
|
| 449 |
+
"MiniMaxH3Scheduler",
|
| 450 |
+
"PNDMScheduler",
|
| 451 |
+
"RePaintScheduler",
|
| 452 |
+
"SASolverScheduler",
|
| 453 |
+
"SchedulerMixin",
|
| 454 |
+
"SCMScheduler",
|
| 455 |
+
"ScoreSdeVeScheduler",
|
| 456 |
+
"TCDScheduler",
|
| 457 |
+
"UnCLIPScheduler",
|
| 458 |
+
"UniPCMultistepScheduler",
|
| 459 |
+
"VQDiffusionScheduler",
|
| 460 |
+
]
|
| 461 |
+
)
|
| 462 |
+
_import_structure["training_utils"] = ["EMAModel"]
|
| 463 |
+
_import_structure["video_processor"] = ["VideoProcessor"]
|
| 464 |
+
|
| 465 |
+
try:
|
| 466 |
+
if not (is_torch_available() and is_scipy_available()):
|
| 467 |
+
raise OptionalDependencyNotAvailable()
|
| 468 |
+
except OptionalDependencyNotAvailable:
|
| 469 |
+
from .utils import dummy_torch_and_scipy_objects # noqa F403
|
| 470 |
+
|
| 471 |
+
_import_structure["utils.dummy_torch_and_scipy_objects"] = [
|
| 472 |
+
name for name in dir(dummy_torch_and_scipy_objects) if not name.startswith("_")
|
| 473 |
+
]
|
| 474 |
+
|
| 475 |
+
else:
|
| 476 |
+
_import_structure["schedulers"].extend(["LMSDiscreteScheduler"])
|
| 477 |
+
|
| 478 |
+
try:
|
| 479 |
+
if not (is_torch_available() and is_torchsde_available()):
|
| 480 |
+
raise OptionalDependencyNotAvailable()
|
| 481 |
+
except OptionalDependencyNotAvailable:
|
| 482 |
+
from .utils import dummy_torch_and_torchsde_objects # noqa F403
|
| 483 |
+
|
| 484 |
+
_import_structure["utils.dummy_torch_and_torchsde_objects"] = [
|
| 485 |
+
name for name in dir(dummy_torch_and_torchsde_objects) if not name.startswith("_")
|
| 486 |
+
]
|
| 487 |
+
|
| 488 |
+
else:
|
| 489 |
+
_import_structure["schedulers"].extend(["CosineDPMSolverMultistepScheduler", "DPMSolverSDEScheduler"])
|
| 490 |
+
|
| 491 |
+
try:
|
| 492 |
+
if not (is_torch_available() and is_transformers_available()):
|
| 493 |
+
raise OptionalDependencyNotAvailable()
|
| 494 |
+
except OptionalDependencyNotAvailable:
|
| 495 |
+
from .utils import dummy_torch_and_transformers_objects # noqa F403
|
| 496 |
+
|
| 497 |
+
_import_structure["utils.dummy_torch_and_transformers_objects"] = [
|
| 498 |
+
name for name in dir(dummy_torch_and_transformers_objects) if not name.startswith("_")
|
| 499 |
+
]
|
| 500 |
+
|
| 501 |
+
else:
|
| 502 |
+
_import_structure["modular_pipelines"].extend(
|
| 503 |
+
[
|
| 504 |
+
"AnimaAutoBlocks",
|
| 505 |
+
"AnimaModularPipeline",
|
| 506 |
+
"Cosmos3DistilledBlocks",
|
| 507 |
+
"Cosmos3DistilledModularPipeline",
|
| 508 |
+
"Cosmos3OmniBlocks",
|
| 509 |
+
"Cosmos3OmniModularPipeline",
|
| 510 |
+
"ErnieImageAutoBlocks",
|
| 511 |
+
"ErnieImageModularPipeline",
|
| 512 |
+
"Flux2AutoBlocks",
|
| 513 |
+
"Flux2KleinAutoBlocks",
|
| 514 |
+
"Flux2KleinBaseAutoBlocks",
|
| 515 |
+
"Flux2KleinBaseModularPipeline",
|
| 516 |
+
"Flux2KleinModularPipeline",
|
| 517 |
+
"Flux2ModularPipeline",
|
| 518 |
+
"FluxAutoBlocks",
|
| 519 |
+
"FluxKontextAutoBlocks",
|
| 520 |
+
"FluxKontextModularPipeline",
|
| 521 |
+
"FluxModularPipeline",
|
| 522 |
+
"HeliosAutoBlocks",
|
| 523 |
+
"HeliosModularPipeline",
|
| 524 |
+
"HeliosPyramidAutoBlocks",
|
| 525 |
+
"HeliosPyramidDistilledAutoBlocks",
|
| 526 |
+
"HeliosPyramidDistilledModularPipeline",
|
| 527 |
+
"HeliosPyramidModularPipeline",
|
| 528 |
+
"HunyuanVideo15AutoBlocks",
|
| 529 |
+
"HunyuanVideo15ModularPipeline",
|
| 530 |
+
"Ideogram4AutoBlocks",
|
| 531 |
+
"Ideogram4ModularPipeline",
|
| 532 |
+
"Krea2AutoBlocks",
|
| 533 |
+
"Krea2ModularPipeline",
|
| 534 |
+
"Krea2TurboAutoBlocks",
|
| 535 |
+
"Krea2TurboModularPipeline",
|
| 536 |
+
"LTXAutoBlocks",
|
| 537 |
+
"LTXModularPipeline",
|
| 538 |
+
"MiniMaxH3Blocks",
|
| 539 |
+
"MiniMaxH3ModularPipeline",
|
| 540 |
+
"MiniMaxH3Ref2VABlocks",
|
| 541 |
+
"MiniMaxH3Ref2VAModularPipeline",
|
| 542 |
+
"QwenImageAutoBlocks",
|
| 543 |
+
"QwenImageEditAutoBlocks",
|
| 544 |
+
"QwenImageEditModularPipeline",
|
| 545 |
+
"QwenImageEditPlusAutoBlocks",
|
| 546 |
+
"QwenImageEditPlusModularPipeline",
|
| 547 |
+
"QwenImageLayeredAutoBlocks",
|
| 548 |
+
"QwenImageLayeredModularPipeline",
|
| 549 |
+
"QwenImageModularPipeline",
|
| 550 |
+
"StableDiffusion3AutoBlocks",
|
| 551 |
+
"StableDiffusion3ModularPipeline",
|
| 552 |
+
"StableDiffusionXLAutoBlocks",
|
| 553 |
+
"StableDiffusionXLModularPipeline",
|
| 554 |
+
"Wan22Blocks",
|
| 555 |
+
"Wan22Image2VideoBlocks",
|
| 556 |
+
"Wan22Image2VideoModularPipeline",
|
| 557 |
+
"Wan22ModularPipeline",
|
| 558 |
+
"WanBlocks",
|
| 559 |
+
"WanImage2VideoAutoBlocks",
|
| 560 |
+
"WanImage2VideoModularPipeline",
|
| 561 |
+
"WanModularPipeline",
|
| 562 |
+
"ZImageAutoBlocks",
|
| 563 |
+
"ZImageModularPipeline",
|
| 564 |
+
]
|
| 565 |
+
)
|
| 566 |
+
_import_structure["pipelines"].extend(
|
| 567 |
+
[
|
| 568 |
+
"AceStepAudioTokenDetokenizer",
|
| 569 |
+
"AceStepAudioTokenizer",
|
| 570 |
+
"AceStepConditionEncoder",
|
| 571 |
+
"AceStepPipeline",
|
| 572 |
+
"AllegroPipeline",
|
| 573 |
+
"AltDiffusionImg2ImgPipeline",
|
| 574 |
+
"AltDiffusionPipeline",
|
| 575 |
+
"AmusedImg2ImgPipeline",
|
| 576 |
+
"AmusedInpaintPipeline",
|
| 577 |
+
"AmusedPipeline",
|
| 578 |
+
"AnimateDiffControlNetPipeline",
|
| 579 |
+
"AnimateDiffPAGPipeline",
|
| 580 |
+
"AnimateDiffPipeline",
|
| 581 |
+
"AnimateDiffSDXLPipeline",
|
| 582 |
+
"AnimateDiffSparseControlNetPipeline",
|
| 583 |
+
"AnimateDiffVideoToVideoControlNetPipeline",
|
| 584 |
+
"AnimateDiffVideoToVideoPipeline",
|
| 585 |
+
"AnyFlowFARPipeline",
|
| 586 |
+
"AnyFlowPipeline",
|
| 587 |
+
"AudioLDM2Pipeline",
|
| 588 |
+
"AudioLDM2ProjectionModel",
|
| 589 |
+
"AudioLDM2UNet2DConditionModel",
|
| 590 |
+
"AudioLDMPipeline",
|
| 591 |
+
"AuraFlowPipeline",
|
| 592 |
+
"BlipDiffusionControlNetPipeline",
|
| 593 |
+
"BlipDiffusionPipeline",
|
| 594 |
+
"BriaFiboEditPipeline",
|
| 595 |
+
"BriaFiboPipeline",
|
| 596 |
+
"BriaPipeline",
|
| 597 |
+
"ChromaImg2ImgPipeline",
|
| 598 |
+
"ChromaInpaintPipeline",
|
| 599 |
+
"ChromaPipeline",
|
| 600 |
+
"ChronoEditPipeline",
|
| 601 |
+
"CLIPImageProjection",
|
| 602 |
+
"CogVideoXFunControlPipeline",
|
| 603 |
+
"CogVideoXImageToVideoPipeline",
|
| 604 |
+
"CogVideoXPipeline",
|
| 605 |
+
"CogVideoXVideoToVideoPipeline",
|
| 606 |
+
"CogView3PlusPipeline",
|
| 607 |
+
"CogView4ControlPipeline",
|
| 608 |
+
"CogView4Pipeline",
|
| 609 |
+
"ConsisIDPipeline",
|
| 610 |
+
"Cosmos2_5_PredictBasePipeline",
|
| 611 |
+
"Cosmos2_5_TransferPipeline",
|
| 612 |
+
"Cosmos2TextToImagePipeline",
|
| 613 |
+
"Cosmos2VideoToWorldPipeline",
|
| 614 |
+
"Cosmos3OmniPipeline",
|
| 615 |
+
"CosmosActionCondition",
|
| 616 |
+
"CosmosTextToWorldPipeline",
|
| 617 |
+
"CosmosVideoToWorldPipeline",
|
| 618 |
+
"CycleDiffusionPipeline",
|
| 619 |
+
"DiffusionGemmaPipeline",
|
| 620 |
+
"DiffusionGemmaPipelineOutput",
|
| 621 |
+
"DreamLiteMobilePipeline",
|
| 622 |
+
"DreamLitePipeline",
|
| 623 |
+
"DreamLitePipelineOutput",
|
| 624 |
+
"EasyAnimateControlPipeline",
|
| 625 |
+
"EasyAnimateInpaintPipeline",
|
| 626 |
+
"EasyAnimatePipeline",
|
| 627 |
+
"ErnieImagePipeline",
|
| 628 |
+
"Flux2KleinInpaintPipeline",
|
| 629 |
+
"Flux2KleinKVPipeline",
|
| 630 |
+
"Flux2KleinPipeline",
|
| 631 |
+
"Flux2Pipeline",
|
| 632 |
+
"FluxControlImg2ImgPipeline",
|
| 633 |
+
"FluxControlInpaintPipeline",
|
| 634 |
+
"FluxControlNetImg2ImgPipeline",
|
| 635 |
+
"FluxControlNetInpaintPipeline",
|
| 636 |
+
"FluxControlNetPipeline",
|
| 637 |
+
"FluxControlPipeline",
|
| 638 |
+
"FluxFillPipeline",
|
| 639 |
+
"FluxImg2ImgPipeline",
|
| 640 |
+
"FluxInpaintPipeline",
|
| 641 |
+
"FluxKontextInpaintPipeline",
|
| 642 |
+
"FluxKontextPipeline",
|
| 643 |
+
"FluxPipeline",
|
| 644 |
+
"FluxPriorReduxPipeline",
|
| 645 |
+
"GlmImagePipeline",
|
| 646 |
+
"HeliosPipeline",
|
| 647 |
+
"HeliosPyramidPipeline",
|
| 648 |
+
"HiDreamImagePipeline",
|
| 649 |
+
"HunyuanDiTControlNetPipeline",
|
| 650 |
+
"HunyuanDiTPAGPipeline",
|
| 651 |
+
"HunyuanDiTPipeline",
|
| 652 |
+
"HunyuanImagePipeline",
|
| 653 |
+
"HunyuanImageRefinerPipeline",
|
| 654 |
+
"HunyuanSkyreelsImageToVideoPipeline",
|
| 655 |
+
"HunyuanVideo15ImageToVideoPipeline",
|
| 656 |
+
"HunyuanVideo15Pipeline",
|
| 657 |
+
"HunyuanVideoFramepackPipeline",
|
| 658 |
+
"HunyuanVideoImageToVideoPipeline",
|
| 659 |
+
"HunyuanVideoPipeline",
|
| 660 |
+
"I2VGenXLPipeline",
|
| 661 |
+
"Ideogram4Pipeline",
|
| 662 |
+
"Ideogram4PromptEnhancerHead",
|
| 663 |
+
"IFImg2ImgPipeline",
|
| 664 |
+
"IFImg2ImgSuperResolutionPipeline",
|
| 665 |
+
"IFInpaintingPipeline",
|
| 666 |
+
"IFInpaintingSuperResolutionPipeline",
|
| 667 |
+
"IFPipeline",
|
| 668 |
+
"IFSuperResolutionPipeline",
|
| 669 |
+
"ImageTextPipelineOutput",
|
| 670 |
+
"JoyImageEditPipeline",
|
| 671 |
+
"JoyImageEditPipelineOutput",
|
| 672 |
+
"JoyImageEditPlusPipeline",
|
| 673 |
+
"JoyImageEditPlusPipelineOutput",
|
| 674 |
+
"Kandinsky3Img2ImgPipeline",
|
| 675 |
+
"Kandinsky3Pipeline",
|
| 676 |
+
"Kandinsky5I2IPipeline",
|
| 677 |
+
"Kandinsky5I2VPipeline",
|
| 678 |
+
"Kandinsky5T2IPipeline",
|
| 679 |
+
"Kandinsky5T2VPipeline",
|
| 680 |
+
"KandinskyCombinedPipeline",
|
| 681 |
+
"KandinskyImg2ImgCombinedPipeline",
|
| 682 |
+
"KandinskyImg2ImgPipeline",
|
| 683 |
+
"KandinskyInpaintCombinedPipeline",
|
| 684 |
+
"KandinskyInpaintPipeline",
|
| 685 |
+
"KandinskyPipeline",
|
| 686 |
+
"KandinskyPriorPipeline",
|
| 687 |
+
"KandinskyV22CombinedPipeline",
|
| 688 |
+
"KandinskyV22ControlnetImg2ImgPipeline",
|
| 689 |
+
"KandinskyV22ControlnetPipeline",
|
| 690 |
+
"KandinskyV22Img2ImgCombinedPipeline",
|
| 691 |
+
"KandinskyV22Img2ImgPipeline",
|
| 692 |
+
"KandinskyV22InpaintCombinedPipeline",
|
| 693 |
+
"KandinskyV22InpaintPipeline",
|
| 694 |
+
"KandinskyV22Pipeline",
|
| 695 |
+
"KandinskyV22PriorEmb2EmbPipeline",
|
| 696 |
+
"KandinskyV22PriorPipeline",
|
| 697 |
+
"Krea2Pipeline",
|
| 698 |
+
"LatentConsistencyModelImg2ImgPipeline",
|
| 699 |
+
"LatentConsistencyModelPipeline",
|
| 700 |
+
"LattePipeline",
|
| 701 |
+
"LDMTextToImagePipeline",
|
| 702 |
+
"LEditsPPPipelineStableDiffusion",
|
| 703 |
+
"LEditsPPPipelineStableDiffusionXL",
|
| 704 |
+
"LLaDA2Pipeline",
|
| 705 |
+
"LLaDA2PipelineOutput",
|
| 706 |
+
"LongCatAudioDiTPipeline",
|
| 707 |
+
"LongCatImageEditPipeline",
|
| 708 |
+
"LongCatImagePipeline",
|
| 709 |
+
"LTX2ConditionPipeline",
|
| 710 |
+
"LTX2HDRPipeline",
|
| 711 |
+
"LTX2ImageToVideoPipeline",
|
| 712 |
+
"LTX2InContextPipeline",
|
| 713 |
+
"LTX2LatentUpsamplePipeline",
|
| 714 |
+
"LTX2Pipeline",
|
| 715 |
+
"LTXConditionPipeline",
|
| 716 |
+
"LTXI2VLongMultiPromptPipeline",
|
| 717 |
+
"LTXImageToVideoPipeline",
|
| 718 |
+
"LTXLatentUpsamplePipeline",
|
| 719 |
+
"LTXPipeline",
|
| 720 |
+
"LucyEditPipeline",
|
| 721 |
+
"Lumina2Pipeline",
|
| 722 |
+
"Lumina2Text2ImgPipeline",
|
| 723 |
+
"LuminaPipeline",
|
| 724 |
+
"LuminaText2ImgPipeline",
|
| 725 |
+
"MarigoldDepthPipeline",
|
| 726 |
+
"MarigoldIntrinsicsPipeline",
|
| 727 |
+
"MarigoldNormalsPipeline",
|
| 728 |
+
"MochiPipeline",
|
| 729 |
+
"MotifVideoImage2VideoPipeline",
|
| 730 |
+
"MotifVideoPipeline",
|
| 731 |
+
"MotifVideoPipelineOutput",
|
| 732 |
+
"MusicLDMPipeline",
|
| 733 |
+
"NucleusMoEImagePipeline",
|
| 734 |
+
"OmniGenPipeline",
|
| 735 |
+
"OvisImagePipeline",
|
| 736 |
+
"PaintByExamplePipeline",
|
| 737 |
+
"PIAPipeline",
|
| 738 |
+
"PixArtAlphaPipeline",
|
| 739 |
+
"PixArtSigmaPAGPipeline",
|
| 740 |
+
"PixArtSigmaPipeline",
|
| 741 |
+
"PRXPipeline",
|
| 742 |
+
"PRXPixelPipeline",
|
| 743 |
+
"QwenImageControlNetInpaintPipeline",
|
| 744 |
+
"QwenImageControlNetPipeline",
|
| 745 |
+
"QwenImageEditInpaintPipeline",
|
| 746 |
+
"QwenImageEditPipeline",
|
| 747 |
+
"QwenImageEditPlusPipeline",
|
| 748 |
+
"QwenImageImg2ImgPipeline",
|
| 749 |
+
"QwenImageInpaintPipeline",
|
| 750 |
+
"QwenImageLayeredPipeline",
|
| 751 |
+
"QwenImagePipeline",
|
| 752 |
+
"ReduxImageEncoder",
|
| 753 |
+
"SanaControlNetPipeline",
|
| 754 |
+
"SanaImageToVideoPipeline",
|
| 755 |
+
"SanaPAGPipeline",
|
| 756 |
+
"SanaPipeline",
|
| 757 |
+
"SanaSprintImg2ImgPipeline",
|
| 758 |
+
"SanaSprintPipeline",
|
| 759 |
+
"SanaVideoPipeline",
|
| 760 |
+
"SanaVideoPipeline",
|
| 761 |
+
"SemanticStableDiffusionPipeline",
|
| 762 |
+
"ShapEImg2ImgPipeline",
|
| 763 |
+
"ShapEPipeline",
|
| 764 |
+
"SkyReelsV2DiffusionForcingImageToVideoPipeline",
|
| 765 |
+
"SkyReelsV2DiffusionForcingPipeline",
|
| 766 |
+
"SkyReelsV2DiffusionForcingVideoToVideoPipeline",
|
| 767 |
+
"SkyReelsV2ImageToVideoPipeline",
|
| 768 |
+
"SkyReelsV2Pipeline",
|
| 769 |
+
"StableAudioPipeline",
|
| 770 |
+
"StableAudioProjectionModel",
|
| 771 |
+
"StableCascadeCombinedPipeline",
|
| 772 |
+
"StableCascadeDecoderPipeline",
|
| 773 |
+
"StableCascadePriorPipeline",
|
| 774 |
+
"StableDiffusion3ControlNetInpaintingPipeline",
|
| 775 |
+
"StableDiffusion3ControlNetPipeline",
|
| 776 |
+
"StableDiffusion3Img2ImgPipeline",
|
| 777 |
+
"StableDiffusion3InpaintPipeline",
|
| 778 |
+
"StableDiffusion3PAGImg2ImgPipeline",
|
| 779 |
+
"StableDiffusion3PAGImg2ImgPipeline",
|
| 780 |
+
"StableDiffusion3PAGPipeline",
|
| 781 |
+
"StableDiffusion3Pipeline",
|
| 782 |
+
"StableDiffusionAdapterPipeline",
|
| 783 |
+
"StableDiffusionAttendAndExcitePipeline",
|
| 784 |
+
"StableDiffusionControlNetImg2ImgPipeline",
|
| 785 |
+
"StableDiffusionControlNetInpaintPipeline",
|
| 786 |
+
"StableDiffusionControlNetPAGInpaintPipeline",
|
| 787 |
+
"StableDiffusionControlNetPAGPipeline",
|
| 788 |
+
"StableDiffusionControlNetPipeline",
|
| 789 |
+
"StableDiffusionControlNetXSPipeline",
|
| 790 |
+
"StableDiffusionDepth2ImgPipeline",
|
| 791 |
+
"StableDiffusionDiffEditPipeline",
|
| 792 |
+
"StableDiffusionGLIGENPipeline",
|
| 793 |
+
"StableDiffusionGLIGENTextImagePipeline",
|
| 794 |
+
"StableDiffusionImageVariationPipeline",
|
| 795 |
+
"StableDiffusionImg2ImgPipeline",
|
| 796 |
+
"StableDiffusionInpaintPipeline",
|
| 797 |
+
"StableDiffusionInpaintPipelineLegacy",
|
| 798 |
+
"StableDiffusionInstructPix2PixPipeline",
|
| 799 |
+
"StableDiffusionLatentUpscalePipeline",
|
| 800 |
+
"StableDiffusionLDM3DPipeline",
|
| 801 |
+
"StableDiffusionModelEditingPipeline",
|
| 802 |
+
"StableDiffusionPAGImg2ImgPipeline",
|
| 803 |
+
"StableDiffusionPAGInpaintPipeline",
|
| 804 |
+
"StableDiffusionPAGPipeline",
|
| 805 |
+
"StableDiffusionPanoramaPipeline",
|
| 806 |
+
"StableDiffusionParadigmsPipeline",
|
| 807 |
+
"StableDiffusionPipeline",
|
| 808 |
+
"StableDiffusionPipelineSafe",
|
| 809 |
+
"StableDiffusionPix2PixZeroPipeline",
|
| 810 |
+
"StableDiffusionSAGPipeline",
|
| 811 |
+
"StableDiffusionUpscalePipeline",
|
| 812 |
+
"StableDiffusionXLAdapterPipeline",
|
| 813 |
+
"StableDiffusionXLControlNetImg2ImgPipeline",
|
| 814 |
+
"StableDiffusionXLControlNetInpaintPipeline",
|
| 815 |
+
"StableDiffusionXLControlNetPAGImg2ImgPipeline",
|
| 816 |
+
"StableDiffusionXLControlNetPAGPipeline",
|
| 817 |
+
"StableDiffusionXLControlNetPipeline",
|
| 818 |
+
"StableDiffusionXLControlNetUnionImg2ImgPipeline",
|
| 819 |
+
"StableDiffusionXLControlNetUnionInpaintPipeline",
|
| 820 |
+
"StableDiffusionXLControlNetUnionPipeline",
|
| 821 |
+
"StableDiffusionXLControlNetXSPipeline",
|
| 822 |
+
"StableDiffusionXLImg2ImgPipeline",
|
| 823 |
+
"StableDiffusionXLInpaintPipeline",
|
| 824 |
+
"StableDiffusionXLInstructPix2PixPipeline",
|
| 825 |
+
"StableDiffusionXLPAGImg2ImgPipeline",
|
| 826 |
+
"StableDiffusionXLPAGInpaintPipeline",
|
| 827 |
+
"StableDiffusionXLPAGPipeline",
|
| 828 |
+
"StableDiffusionXLPipeline",
|
| 829 |
+
"StableUnCLIPImg2ImgPipeline",
|
| 830 |
+
"StableUnCLIPPipeline",
|
| 831 |
+
"StableVideoDiffusionPipeline",
|
| 832 |
+
"TextToVideoSDPipeline",
|
| 833 |
+
"TextToVideoZeroPipeline",
|
| 834 |
+
"TextToVideoZeroSDXLPipeline",
|
| 835 |
+
"UnCLIPImageVariationPipeline",
|
| 836 |
+
"UnCLIPPipeline",
|
| 837 |
+
"UniDiffuserModel",
|
| 838 |
+
"UniDiffuserPipeline",
|
| 839 |
+
"UniDiffuserTextDecoder",
|
| 840 |
+
"VersatileDiffusionDualGuidedPipeline",
|
| 841 |
+
"VersatileDiffusionImageVariationPipeline",
|
| 842 |
+
"VersatileDiffusionPipeline",
|
| 843 |
+
"VersatileDiffusionTextToImagePipeline",
|
| 844 |
+
"VideoToVideoSDPipeline",
|
| 845 |
+
"VisualClozeGenerationPipeline",
|
| 846 |
+
"VisualClozePipeline",
|
| 847 |
+
"VQDiffusionPipeline",
|
| 848 |
+
"WanAnimatePipeline",
|
| 849 |
+
"WanImageToVideoPipeline",
|
| 850 |
+
"WanPipeline",
|
| 851 |
+
"WanVACEPipeline",
|
| 852 |
+
"WanVideoToVideoPipeline",
|
| 853 |
+
"WuerstchenCombinedPipeline",
|
| 854 |
+
"WuerstchenDecoderPipeline",
|
| 855 |
+
"WuerstchenPriorPipeline",
|
| 856 |
+
"ZImageControlNetInpaintPipeline",
|
| 857 |
+
"ZImageControlNetPipeline",
|
| 858 |
+
"ZImageImg2ImgPipeline",
|
| 859 |
+
"ZImageInpaintPipeline",
|
| 860 |
+
"ZImageOmniPipeline",
|
| 861 |
+
"ZImagePipeline",
|
| 862 |
+
]
|
| 863 |
+
)
|
| 864 |
+
|
| 865 |
+
|
| 866 |
+
try:
|
| 867 |
+
if not (is_torch_available() and is_transformers_available() and is_opencv_available()):
|
| 868 |
+
raise OptionalDependencyNotAvailable()
|
| 869 |
+
except OptionalDependencyNotAvailable:
|
| 870 |
+
from .utils import dummy_torch_and_transformers_and_opencv_objects # noqa F403
|
| 871 |
+
|
| 872 |
+
_import_structure["utils.dummy_torch_and_transformers_and_opencv_objects"] = [
|
| 873 |
+
name for name in dir(dummy_torch_and_transformers_and_opencv_objects) if not name.startswith("_")
|
| 874 |
+
]
|
| 875 |
+
|
| 876 |
+
else:
|
| 877 |
+
_import_structure["pipelines"].extend(["ConsisIDPipeline"])
|
| 878 |
+
|
| 879 |
+
try:
|
| 880 |
+
if not (is_torch_available() and is_transformers_available() and is_sentencepiece_available()):
|
| 881 |
+
raise OptionalDependencyNotAvailable()
|
| 882 |
+
except OptionalDependencyNotAvailable:
|
| 883 |
+
from .utils import dummy_torch_and_transformers_and_sentencepiece_objects # noqa F403
|
| 884 |
+
|
| 885 |
+
_import_structure["utils.dummy_torch_and_transformers_and_sentencepiece_objects"] = [
|
| 886 |
+
name for name in dir(dummy_torch_and_transformers_and_sentencepiece_objects) if not name.startswith("_")
|
| 887 |
+
]
|
| 888 |
+
|
| 889 |
+
else:
|
| 890 |
+
_import_structure["pipelines"].extend(["KolorsImg2ImgPipeline", "KolorsPAGPipeline", "KolorsPipeline"])
|
| 891 |
+
|
| 892 |
+
try:
|
| 893 |
+
if not (is_torch_available() and is_transformers_available() and is_onnx_available()):
|
| 894 |
+
raise OptionalDependencyNotAvailable()
|
| 895 |
+
except OptionalDependencyNotAvailable:
|
| 896 |
+
from .utils import dummy_torch_and_transformers_and_onnx_objects # noqa F403
|
| 897 |
+
|
| 898 |
+
_import_structure["utils.dummy_torch_and_transformers_and_onnx_objects"] = [
|
| 899 |
+
name for name in dir(dummy_torch_and_transformers_and_onnx_objects) if not name.startswith("_")
|
| 900 |
+
]
|
| 901 |
+
|
| 902 |
+
else:
|
| 903 |
+
_import_structure["pipelines"].extend(
|
| 904 |
+
[
|
| 905 |
+
"OnnxStableDiffusionImg2ImgPipeline",
|
| 906 |
+
"OnnxStableDiffusionInpaintPipeline",
|
| 907 |
+
"OnnxStableDiffusionInpaintPipelineLegacy",
|
| 908 |
+
"OnnxStableDiffusionPipeline",
|
| 909 |
+
"OnnxStableDiffusionUpscalePipeline",
|
| 910 |
+
"StableDiffusionOnnxPipeline",
|
| 911 |
+
]
|
| 912 |
+
)
|
| 913 |
+
|
| 914 |
+
try:
|
| 915 |
+
if not (is_torch_available() and is_librosa_available()):
|
| 916 |
+
raise OptionalDependencyNotAvailable()
|
| 917 |
+
except OptionalDependencyNotAvailable:
|
| 918 |
+
from .utils import dummy_torch_and_librosa_objects # noqa F403
|
| 919 |
+
|
| 920 |
+
_import_structure["utils.dummy_torch_and_librosa_objects"] = [
|
| 921 |
+
name for name in dir(dummy_torch_and_librosa_objects) if not name.startswith("_")
|
| 922 |
+
]
|
| 923 |
+
|
| 924 |
+
else:
|
| 925 |
+
_import_structure["pipelines"].extend(["AudioDiffusionPipeline", "Mel"])
|
| 926 |
+
|
| 927 |
+
try:
|
| 928 |
+
if not (is_transformers_available() and is_torch_available() and is_note_seq_available()):
|
| 929 |
+
raise OptionalDependencyNotAvailable()
|
| 930 |
+
except OptionalDependencyNotAvailable:
|
| 931 |
+
from .utils import dummy_transformers_and_torch_and_note_seq_objects # noqa F403
|
| 932 |
+
|
| 933 |
+
_import_structure["utils.dummy_transformers_and_torch_and_note_seq_objects"] = [
|
| 934 |
+
name for name in dir(dummy_transformers_and_torch_and_note_seq_objects) if not name.startswith("_")
|
| 935 |
+
]
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
else:
|
| 939 |
+
_import_structure["pipelines"].extend(["SpectrogramDiffusionPipeline"])
|
| 940 |
+
|
| 941 |
+
try:
|
| 942 |
+
if not (is_note_seq_available()):
|
| 943 |
+
raise OptionalDependencyNotAvailable()
|
| 944 |
+
except OptionalDependencyNotAvailable:
|
| 945 |
+
from .utils import dummy_note_seq_objects # noqa F403
|
| 946 |
+
|
| 947 |
+
_import_structure["utils.dummy_note_seq_objects"] = [
|
| 948 |
+
name for name in dir(dummy_note_seq_objects) if not name.startswith("_")
|
| 949 |
+
]
|
| 950 |
+
|
| 951 |
+
|
| 952 |
+
else:
|
| 953 |
+
_import_structure["pipelines"].extend(["MidiProcessor"])
|
| 954 |
+
|
| 955 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 956 |
+
from .configuration_utils import ConfigMixin
|
| 957 |
+
from .quantizers import PipelineQuantizationConfig
|
| 958 |
+
|
| 959 |
+
try:
|
| 960 |
+
if not is_bitsandbytes_available():
|
| 961 |
+
raise OptionalDependencyNotAvailable()
|
| 962 |
+
except OptionalDependencyNotAvailable:
|
| 963 |
+
from .utils.dummy_bitsandbytes_objects import *
|
| 964 |
+
else:
|
| 965 |
+
from .quantizers.quantization_config import BitsAndBytesConfig
|
| 966 |
+
|
| 967 |
+
try:
|
| 968 |
+
if not is_gguf_available():
|
| 969 |
+
raise OptionalDependencyNotAvailable()
|
| 970 |
+
except OptionalDependencyNotAvailable:
|
| 971 |
+
from .utils.dummy_gguf_objects import *
|
| 972 |
+
else:
|
| 973 |
+
from .quantizers.quantization_config import GGUFQuantizationConfig
|
| 974 |
+
|
| 975 |
+
try:
|
| 976 |
+
if not is_torchao_available():
|
| 977 |
+
raise OptionalDependencyNotAvailable()
|
| 978 |
+
except OptionalDependencyNotAvailable:
|
| 979 |
+
from .utils.dummy_torchao_objects import *
|
| 980 |
+
else:
|
| 981 |
+
from .quantizers.quantization_config import TorchAoConfig
|
| 982 |
+
|
| 983 |
+
try:
|
| 984 |
+
if not is_optimum_quanto_available():
|
| 985 |
+
raise OptionalDependencyNotAvailable()
|
| 986 |
+
except OptionalDependencyNotAvailable:
|
| 987 |
+
from .utils.dummy_optimum_quanto_objects import *
|
| 988 |
+
else:
|
| 989 |
+
from .quantizers.quantization_config import QuantoConfig
|
| 990 |
+
|
| 991 |
+
try:
|
| 992 |
+
if not is_nvidia_modelopt_available():
|
| 993 |
+
raise OptionalDependencyNotAvailable()
|
| 994 |
+
except OptionalDependencyNotAvailable:
|
| 995 |
+
from .utils.dummy_nvidia_modelopt_objects import *
|
| 996 |
+
else:
|
| 997 |
+
from .quantizers.quantization_config import NVIDIAModelOptConfig
|
| 998 |
+
|
| 999 |
+
try:
|
| 1000 |
+
if not is_torch_available():
|
| 1001 |
+
raise OptionalDependencyNotAvailable()
|
| 1002 |
+
except OptionalDependencyNotAvailable:
|
| 1003 |
+
from .utils.dummy_nunchaku_lite_objects import *
|
| 1004 |
+
else:
|
| 1005 |
+
from .quantizers.quantization_config import NunchakuLiteQuantizationConfig
|
| 1006 |
+
|
| 1007 |
+
try:
|
| 1008 |
+
if not is_auto_round_available():
|
| 1009 |
+
raise OptionalDependencyNotAvailable()
|
| 1010 |
+
except OptionalDependencyNotAvailable:
|
| 1011 |
+
from .utils.dummy_auto_round_objects import *
|
| 1012 |
+
else:
|
| 1013 |
+
from .quantizers.quantization_config import AutoRoundConfig
|
| 1014 |
+
|
| 1015 |
+
try:
|
| 1016 |
+
if not is_sdnq_available():
|
| 1017 |
+
raise OptionalDependencyNotAvailable()
|
| 1018 |
+
except OptionalDependencyNotAvailable:
|
| 1019 |
+
from .utils.dummy_sdnq_objects import *
|
| 1020 |
+
else:
|
| 1021 |
+
from .quantizers.quantization_config import SDNQConfig
|
| 1022 |
+
|
| 1023 |
+
try:
|
| 1024 |
+
if not is_onnx_available():
|
| 1025 |
+
raise OptionalDependencyNotAvailable()
|
| 1026 |
+
except OptionalDependencyNotAvailable:
|
| 1027 |
+
from .utils.dummy_onnx_objects import * # noqa F403
|
| 1028 |
+
else:
|
| 1029 |
+
from .pipelines import OnnxRuntimeModel
|
| 1030 |
+
|
| 1031 |
+
try:
|
| 1032 |
+
if not is_torch_available():
|
| 1033 |
+
raise OptionalDependencyNotAvailable()
|
| 1034 |
+
except OptionalDependencyNotAvailable:
|
| 1035 |
+
from .utils.dummy_pt_objects import * # noqa F403
|
| 1036 |
+
else:
|
| 1037 |
+
from .guiders import (
|
| 1038 |
+
AdaptiveProjectedGuidance,
|
| 1039 |
+
AdaptiveProjectedMixGuidance,
|
| 1040 |
+
AutoGuidance,
|
| 1041 |
+
BaseGuidance,
|
| 1042 |
+
ClassifierFreeGuidance,
|
| 1043 |
+
ClassifierFreeZeroStarGuidance,
|
| 1044 |
+
FrequencyDecoupledGuidance,
|
| 1045 |
+
PerturbedAttentionGuidance,
|
| 1046 |
+
SkipLayerGuidance,
|
| 1047 |
+
SmoothedEnergyGuidance,
|
| 1048 |
+
TangentialClassifierFreeGuidance,
|
| 1049 |
+
)
|
| 1050 |
+
from .hooks import (
|
| 1051 |
+
FasterCacheConfig,
|
| 1052 |
+
FirstBlockCacheConfig,
|
| 1053 |
+
HookRegistry,
|
| 1054 |
+
LayerSkipConfig,
|
| 1055 |
+
MagCacheConfig,
|
| 1056 |
+
PyramidAttentionBroadcastConfig,
|
| 1057 |
+
SmoothedEnergyGuidanceConfig,
|
| 1058 |
+
TaylorSeerCacheConfig,
|
| 1059 |
+
TextKVCacheConfig,
|
| 1060 |
+
apply_faster_cache,
|
| 1061 |
+
apply_first_block_cache,
|
| 1062 |
+
apply_layer_skip,
|
| 1063 |
+
apply_mag_cache,
|
| 1064 |
+
apply_pyramid_attention_broadcast,
|
| 1065 |
+
apply_taylorseer_cache,
|
| 1066 |
+
apply_text_kv_cache,
|
| 1067 |
+
)
|
| 1068 |
+
from .image_processor import (
|
| 1069 |
+
InpaintProcessor,
|
| 1070 |
+
IPAdapterMaskProcessor,
|
| 1071 |
+
PixArtImageProcessor,
|
| 1072 |
+
VaeImageProcessor,
|
| 1073 |
+
VaeImageProcessorLDM3D,
|
| 1074 |
+
)
|
| 1075 |
+
from .models import (
|
| 1076 |
+
AceStepTransformer1DModel,
|
| 1077 |
+
AllegroTransformer3DModel,
|
| 1078 |
+
AnimaTextConditioner,
|
| 1079 |
+
AnyFlowFARTransformer3DModel,
|
| 1080 |
+
AnyFlowTransformer3DModel,
|
| 1081 |
+
AsymmetricAutoencoderKL,
|
| 1082 |
+
AttentionBackendName,
|
| 1083 |
+
AuraFlowTransformer2DModel,
|
| 1084 |
+
AutoencoderDC,
|
| 1085 |
+
AutoencoderKL,
|
| 1086 |
+
AutoencoderKLAllegro,
|
| 1087 |
+
AutoencoderKLCogVideoX,
|
| 1088 |
+
AutoencoderKLCosmos,
|
| 1089 |
+
AutoencoderKLFlux2,
|
| 1090 |
+
AutoencoderKLHunyuanImage,
|
| 1091 |
+
AutoencoderKLHunyuanImageRefiner,
|
| 1092 |
+
AutoencoderKLHunyuanVideo,
|
| 1093 |
+
AutoencoderKLHunyuanVideo15,
|
| 1094 |
+
AutoencoderKLKVAE,
|
| 1095 |
+
AutoencoderKLKVAEVideo,
|
| 1096 |
+
AutoencoderKLLTX2Audio,
|
| 1097 |
+
AutoencoderKLLTX2Video,
|
| 1098 |
+
AutoencoderKLLTXVideo,
|
| 1099 |
+
AutoencoderKLMagvit,
|
| 1100 |
+
AutoencoderKLMiniMaxH3,
|
| 1101 |
+
AutoencoderKLMiniMaxH3Audio,
|
| 1102 |
+
AutoencoderKLMochi,
|
| 1103 |
+
AutoencoderKLQwenImage,
|
| 1104 |
+
AutoencoderKLTemporalDecoder,
|
| 1105 |
+
AutoencoderKLWan,
|
| 1106 |
+
AutoencoderOobleck,
|
| 1107 |
+
AutoencoderRAE,
|
| 1108 |
+
AutoencoderTiny,
|
| 1109 |
+
AutoencoderVidTok,
|
| 1110 |
+
AutoModel,
|
| 1111 |
+
BriaFiboTransformer2DModel,
|
| 1112 |
+
BriaTransformer2DModel,
|
| 1113 |
+
CacheMixin,
|
| 1114 |
+
ChromaTransformer2DModel,
|
| 1115 |
+
ChronoEditTransformer3DModel,
|
| 1116 |
+
CogVideoXTransformer3DModel,
|
| 1117 |
+
CogView3PlusTransformer2DModel,
|
| 1118 |
+
CogView4Transformer2DModel,
|
| 1119 |
+
ConsisIDTransformer3DModel,
|
| 1120 |
+
ConsistencyDecoderVAE,
|
| 1121 |
+
ContextParallelConfig,
|
| 1122 |
+
ControlNetModel,
|
| 1123 |
+
ControlNetUnionModel,
|
| 1124 |
+
ControlNetXSAdapter,
|
| 1125 |
+
Cosmos3AVAEAudioTokenizer,
|
| 1126 |
+
Cosmos3OmniTransformer,
|
| 1127 |
+
CosmosControlNetModel,
|
| 1128 |
+
CosmosTransformer3DModel,
|
| 1129 |
+
DiTTransformer2DModel,
|
| 1130 |
+
DreamLiteTransformer2DModel,
|
| 1131 |
+
DreamLiteUNetModel,
|
| 1132 |
+
EasyAnimateTransformer3DModel,
|
| 1133 |
+
ErnieImageTransformer2DModel,
|
| 1134 |
+
Flux2Transformer2DModel,
|
| 1135 |
+
FluxControlNetModel,
|
| 1136 |
+
FluxMultiControlNetModel,
|
| 1137 |
+
FluxTransformer2DModel,
|
| 1138 |
+
GlmImageTransformer2DModel,
|
| 1139 |
+
HeliosTransformer3DModel,
|
| 1140 |
+
HiDreamImageTransformer2DModel,
|
| 1141 |
+
HunyuanDiT2DControlNetModel,
|
| 1142 |
+
HunyuanDiT2DModel,
|
| 1143 |
+
HunyuanDiT2DMultiControlNetModel,
|
| 1144 |
+
HunyuanImageTransformer2DModel,
|
| 1145 |
+
HunyuanVideo15Transformer3DModel,
|
| 1146 |
+
HunyuanVideoFramepackTransformer3DModel,
|
| 1147 |
+
HunyuanVideoTransformer3DModel,
|
| 1148 |
+
I2VGenXLUNet,
|
| 1149 |
+
Ideogram4Transformer2DModel,
|
| 1150 |
+
JoyImageEditPlusTransformer3DModel,
|
| 1151 |
+
JoyImageEditTransformer3DModel,
|
| 1152 |
+
Kandinsky3UNet,
|
| 1153 |
+
Kandinsky5Transformer3DModel,
|
| 1154 |
+
Krea2Transformer2DModel,
|
| 1155 |
+
LatteTransformer3DModel,
|
| 1156 |
+
LongCatAudioDiTTransformer,
|
| 1157 |
+
LongCatAudioDiTVae,
|
| 1158 |
+
LongCatImageTransformer2DModel,
|
| 1159 |
+
LTX2VideoTransformer3DModel,
|
| 1160 |
+
LTXVideoTransformer3DModel,
|
| 1161 |
+
Lumina2Transformer2DModel,
|
| 1162 |
+
LuminaNextDiT2DModel,
|
| 1163 |
+
MiniMaxH3Transformer3DModel,
|
| 1164 |
+
MochiTransformer3DModel,
|
| 1165 |
+
ModelMixin,
|
| 1166 |
+
MotifVideoTransformer3DModel,
|
| 1167 |
+
MotionAdapter,
|
| 1168 |
+
MultiAdapter,
|
| 1169 |
+
MultiControlNetModel,
|
| 1170 |
+
NucleusMoEImageTransformer2DModel,
|
| 1171 |
+
OmniGenTransformer2DModel,
|
| 1172 |
+
OvisImageTransformer2DModel,
|
| 1173 |
+
ParallelConfig,
|
| 1174 |
+
PixArtTransformer2DModel,
|
| 1175 |
+
PriorTransformer,
|
| 1176 |
+
PRXTransformer2DModel,
|
| 1177 |
+
QwenImageControlNetModel,
|
| 1178 |
+
QwenImageMultiControlNetModel,
|
| 1179 |
+
QwenImageTransformer2DModel,
|
| 1180 |
+
SanaControlNetModel,
|
| 1181 |
+
SanaTransformer2DModel,
|
| 1182 |
+
SanaVideoTransformer3DModel,
|
| 1183 |
+
SD3ControlNetModel,
|
| 1184 |
+
SD3MultiControlNetModel,
|
| 1185 |
+
SD3Transformer2DModel,
|
| 1186 |
+
SkyReelsV2Transformer3DModel,
|
| 1187 |
+
SparseControlNetModel,
|
| 1188 |
+
StableAudioDiTModel,
|
| 1189 |
+
T2IAdapter,
|
| 1190 |
+
T5FilmDecoder,
|
| 1191 |
+
Transformer2DModel,
|
| 1192 |
+
TransformerTemporalModel,
|
| 1193 |
+
UNet1DModel,
|
| 1194 |
+
UNet2DConditionModel,
|
| 1195 |
+
UNet2DModel,
|
| 1196 |
+
UNet3DConditionModel,
|
| 1197 |
+
UNetControlNetXSModel,
|
| 1198 |
+
UNetMotionModel,
|
| 1199 |
+
UNetSpatioTemporalConditionModel,
|
| 1200 |
+
UVit2DModel,
|
| 1201 |
+
VQModel,
|
| 1202 |
+
WanAnimateTransformer3DModel,
|
| 1203 |
+
WanTransformer3DModel,
|
| 1204 |
+
WanVACETransformer3DModel,
|
| 1205 |
+
ZImageControlNetModel,
|
| 1206 |
+
ZImageTransformer2DModel,
|
| 1207 |
+
attention_backend,
|
| 1208 |
+
)
|
| 1209 |
+
from .modular_pipelines import (
|
| 1210 |
+
AutoPipelineBlocks,
|
| 1211 |
+
ComponentsManager,
|
| 1212 |
+
ComponentSpec,
|
| 1213 |
+
ConditionalPipelineBlocks,
|
| 1214 |
+
ConfigSpec,
|
| 1215 |
+
InputParam,
|
| 1216 |
+
LoopSequentialPipelineBlocks,
|
| 1217 |
+
ModularPipeline,
|
| 1218 |
+
ModularPipelineBlocks,
|
| 1219 |
+
OutputParam,
|
| 1220 |
+
SequentialPipelineBlocks,
|
| 1221 |
+
)
|
| 1222 |
+
from .optimization import (
|
| 1223 |
+
get_constant_schedule,
|
| 1224 |
+
get_constant_schedule_with_warmup,
|
| 1225 |
+
get_cosine_schedule_with_warmup,
|
| 1226 |
+
get_cosine_with_hard_restarts_schedule_with_warmup,
|
| 1227 |
+
get_linear_schedule_with_warmup,
|
| 1228 |
+
get_polynomial_decay_schedule_with_warmup,
|
| 1229 |
+
get_scheduler,
|
| 1230 |
+
)
|
| 1231 |
+
from .pipelines import (
|
| 1232 |
+
AudioPipelineOutput,
|
| 1233 |
+
AutoPipelineForImage2Image,
|
| 1234 |
+
AutoPipelineForInpainting,
|
| 1235 |
+
AutoPipelineForText2Audio,
|
| 1236 |
+
AutoPipelineForText2Image,
|
| 1237 |
+
BlipDiffusionControlNetPipeline,
|
| 1238 |
+
BlipDiffusionPipeline,
|
| 1239 |
+
CLIPImageProjection,
|
| 1240 |
+
ConsistencyModelPipeline,
|
| 1241 |
+
DanceDiffusionPipeline,
|
| 1242 |
+
DDIMPipeline,
|
| 1243 |
+
DDPMPipeline,
|
| 1244 |
+
DiffusionPipeline,
|
| 1245 |
+
DiTPipeline,
|
| 1246 |
+
ImagePipelineOutput,
|
| 1247 |
+
KarrasVePipeline,
|
| 1248 |
+
LDMPipeline,
|
| 1249 |
+
LDMSuperResolutionPipeline,
|
| 1250 |
+
PNDMPipeline,
|
| 1251 |
+
RePaintPipeline,
|
| 1252 |
+
ScoreSdeVePipeline,
|
| 1253 |
+
StableDiffusionMixin,
|
| 1254 |
+
)
|
| 1255 |
+
from .quantizers import DiffusersQuantizer
|
| 1256 |
+
from .schedulers import (
|
| 1257 |
+
AmusedScheduler,
|
| 1258 |
+
BlockRefinementScheduler,
|
| 1259 |
+
BlockRefinementSchedulerOutput,
|
| 1260 |
+
CMStochasticIterativeScheduler,
|
| 1261 |
+
CogVideoXDDIMScheduler,
|
| 1262 |
+
CogVideoXDPMScheduler,
|
| 1263 |
+
DDIMInverseScheduler,
|
| 1264 |
+
DDIMParallelScheduler,
|
| 1265 |
+
DDIMScheduler,
|
| 1266 |
+
DDPMParallelScheduler,
|
| 1267 |
+
DDPMScheduler,
|
| 1268 |
+
DDPMWuerstchenScheduler,
|
| 1269 |
+
DEISMultistepScheduler,
|
| 1270 |
+
DiscreteDDIMScheduler,
|
| 1271 |
+
DiscreteDDIMSchedulerOutput,
|
| 1272 |
+
DPMSolverMultistepInverseScheduler,
|
| 1273 |
+
DPMSolverMultistepScheduler,
|
| 1274 |
+
DPMSolverSinglestepScheduler,
|
| 1275 |
+
EDMDPMSolverMultistepScheduler,
|
| 1276 |
+
EDMEulerScheduler,
|
| 1277 |
+
EntropyBoundScheduler,
|
| 1278 |
+
EntropyBoundSchedulerOutput,
|
| 1279 |
+
EulerAncestralDiscreteScheduler,
|
| 1280 |
+
EulerDiscreteScheduler,
|
| 1281 |
+
FlowMapEulerDiscreteScheduler,
|
| 1282 |
+
FlowMatchEulerDiscreteScheduler,
|
| 1283 |
+
FlowMatchHeunDiscreteScheduler,
|
| 1284 |
+
FlowMatchLCMScheduler,
|
| 1285 |
+
HeliosDMDScheduler,
|
| 1286 |
+
HeliosScheduler,
|
| 1287 |
+
HeunDiscreteScheduler,
|
| 1288 |
+
IPNDMScheduler,
|
| 1289 |
+
KarrasVeScheduler,
|
| 1290 |
+
KDPM2AncestralDiscreteScheduler,
|
| 1291 |
+
KDPM2DiscreteScheduler,
|
| 1292 |
+
LCMScheduler,
|
| 1293 |
+
LTXEulerAncestralRFScheduler,
|
| 1294 |
+
MiniMaxH3Scheduler,
|
| 1295 |
+
PNDMScheduler,
|
| 1296 |
+
RePaintScheduler,
|
| 1297 |
+
SASolverScheduler,
|
| 1298 |
+
SchedulerMixin,
|
| 1299 |
+
SCMScheduler,
|
| 1300 |
+
ScoreSdeVeScheduler,
|
| 1301 |
+
TCDScheduler,
|
| 1302 |
+
UnCLIPScheduler,
|
| 1303 |
+
UniPCMultistepScheduler,
|
| 1304 |
+
VQDiffusionScheduler,
|
| 1305 |
+
)
|
| 1306 |
+
from .training_utils import EMAModel
|
| 1307 |
+
from .video_processor import VideoProcessor
|
| 1308 |
+
|
| 1309 |
+
try:
|
| 1310 |
+
if not (is_torch_available() and is_scipy_available()):
|
| 1311 |
+
raise OptionalDependencyNotAvailable()
|
| 1312 |
+
except OptionalDependencyNotAvailable:
|
| 1313 |
+
from .utils.dummy_torch_and_scipy_objects import * # noqa F403
|
| 1314 |
+
else:
|
| 1315 |
+
from .schedulers import LMSDiscreteScheduler
|
| 1316 |
+
|
| 1317 |
+
try:
|
| 1318 |
+
if not (is_torch_available() and is_torchsde_available()):
|
| 1319 |
+
raise OptionalDependencyNotAvailable()
|
| 1320 |
+
except OptionalDependencyNotAvailable:
|
| 1321 |
+
from .utils.dummy_torch_and_torchsde_objects import * # noqa F403
|
| 1322 |
+
else:
|
| 1323 |
+
from .schedulers import CosineDPMSolverMultistepScheduler, DPMSolverSDEScheduler
|
| 1324 |
+
|
| 1325 |
+
try:
|
| 1326 |
+
if not (is_torch_available() and is_transformers_available()):
|
| 1327 |
+
raise OptionalDependencyNotAvailable()
|
| 1328 |
+
except OptionalDependencyNotAvailable:
|
| 1329 |
+
from .utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 1330 |
+
else:
|
| 1331 |
+
from .modular_pipelines import (
|
| 1332 |
+
AnimaAutoBlocks,
|
| 1333 |
+
AnimaModularPipeline,
|
| 1334 |
+
Cosmos3DistilledBlocks,
|
| 1335 |
+
Cosmos3DistilledModularPipeline,
|
| 1336 |
+
Cosmos3OmniBlocks,
|
| 1337 |
+
Cosmos3OmniModularPipeline,
|
| 1338 |
+
ErnieImageAutoBlocks,
|
| 1339 |
+
ErnieImageModularPipeline,
|
| 1340 |
+
Flux2AutoBlocks,
|
| 1341 |
+
Flux2KleinAutoBlocks,
|
| 1342 |
+
Flux2KleinBaseAutoBlocks,
|
| 1343 |
+
Flux2KleinBaseModularPipeline,
|
| 1344 |
+
Flux2KleinModularPipeline,
|
| 1345 |
+
Flux2ModularPipeline,
|
| 1346 |
+
FluxAutoBlocks,
|
| 1347 |
+
FluxKontextAutoBlocks,
|
| 1348 |
+
FluxKontextModularPipeline,
|
| 1349 |
+
FluxModularPipeline,
|
| 1350 |
+
HeliosAutoBlocks,
|
| 1351 |
+
HeliosModularPipeline,
|
| 1352 |
+
HeliosPyramidAutoBlocks,
|
| 1353 |
+
HeliosPyramidDistilledAutoBlocks,
|
| 1354 |
+
HeliosPyramidDistilledModularPipeline,
|
| 1355 |
+
HeliosPyramidModularPipeline,
|
| 1356 |
+
HunyuanVideo15AutoBlocks,
|
| 1357 |
+
HunyuanVideo15ModularPipeline,
|
| 1358 |
+
Ideogram4AutoBlocks,
|
| 1359 |
+
Ideogram4ModularPipeline,
|
| 1360 |
+
Krea2AutoBlocks,
|
| 1361 |
+
Krea2ModularPipeline,
|
| 1362 |
+
Krea2TurboAutoBlocks,
|
| 1363 |
+
Krea2TurboModularPipeline,
|
| 1364 |
+
LTXAutoBlocks,
|
| 1365 |
+
LTXModularPipeline,
|
| 1366 |
+
MiniMaxH3Blocks,
|
| 1367 |
+
MiniMaxH3ModularPipeline,
|
| 1368 |
+
MiniMaxH3Ref2VABlocks,
|
| 1369 |
+
MiniMaxH3Ref2VAModularPipeline,
|
| 1370 |
+
QwenImageAutoBlocks,
|
| 1371 |
+
QwenImageEditAutoBlocks,
|
| 1372 |
+
QwenImageEditModularPipeline,
|
| 1373 |
+
QwenImageEditPlusAutoBlocks,
|
| 1374 |
+
QwenImageEditPlusModularPipeline,
|
| 1375 |
+
QwenImageLayeredAutoBlocks,
|
| 1376 |
+
QwenImageLayeredModularPipeline,
|
| 1377 |
+
QwenImageModularPipeline,
|
| 1378 |
+
StableDiffusion3AutoBlocks,
|
| 1379 |
+
StableDiffusion3ModularPipeline,
|
| 1380 |
+
StableDiffusionXLAutoBlocks,
|
| 1381 |
+
StableDiffusionXLModularPipeline,
|
| 1382 |
+
Wan22Blocks,
|
| 1383 |
+
Wan22Image2VideoBlocks,
|
| 1384 |
+
Wan22Image2VideoModularPipeline,
|
| 1385 |
+
Wan22ModularPipeline,
|
| 1386 |
+
WanBlocks,
|
| 1387 |
+
WanImage2VideoAutoBlocks,
|
| 1388 |
+
WanImage2VideoModularPipeline,
|
| 1389 |
+
WanModularPipeline,
|
| 1390 |
+
ZImageAutoBlocks,
|
| 1391 |
+
ZImageModularPipeline,
|
| 1392 |
+
)
|
| 1393 |
+
from .pipelines import (
|
| 1394 |
+
AceStepAudioTokenDetokenizer,
|
| 1395 |
+
AceStepAudioTokenizer,
|
| 1396 |
+
AceStepConditionEncoder,
|
| 1397 |
+
AceStepPipeline,
|
| 1398 |
+
AllegroPipeline,
|
| 1399 |
+
AltDiffusionImg2ImgPipeline,
|
| 1400 |
+
AltDiffusionPipeline,
|
| 1401 |
+
AmusedImg2ImgPipeline,
|
| 1402 |
+
AmusedInpaintPipeline,
|
| 1403 |
+
AmusedPipeline,
|
| 1404 |
+
AnimateDiffControlNetPipeline,
|
| 1405 |
+
AnimateDiffPAGPipeline,
|
| 1406 |
+
AnimateDiffPipeline,
|
| 1407 |
+
AnimateDiffSDXLPipeline,
|
| 1408 |
+
AnimateDiffSparseControlNetPipeline,
|
| 1409 |
+
AnimateDiffVideoToVideoControlNetPipeline,
|
| 1410 |
+
AnimateDiffVideoToVideoPipeline,
|
| 1411 |
+
AnyFlowFARPipeline,
|
| 1412 |
+
AnyFlowPipeline,
|
| 1413 |
+
AudioLDM2Pipeline,
|
| 1414 |
+
AudioLDM2ProjectionModel,
|
| 1415 |
+
AudioLDM2UNet2DConditionModel,
|
| 1416 |
+
AudioLDMPipeline,
|
| 1417 |
+
AuraFlowPipeline,
|
| 1418 |
+
BriaFiboEditPipeline,
|
| 1419 |
+
BriaFiboPipeline,
|
| 1420 |
+
BriaPipeline,
|
| 1421 |
+
ChromaImg2ImgPipeline,
|
| 1422 |
+
ChromaInpaintPipeline,
|
| 1423 |
+
ChromaPipeline,
|
| 1424 |
+
ChronoEditPipeline,
|
| 1425 |
+
CLIPImageProjection,
|
| 1426 |
+
CogVideoXFunControlPipeline,
|
| 1427 |
+
CogVideoXImageToVideoPipeline,
|
| 1428 |
+
CogVideoXPipeline,
|
| 1429 |
+
CogVideoXVideoToVideoPipeline,
|
| 1430 |
+
CogView3PlusPipeline,
|
| 1431 |
+
CogView4ControlPipeline,
|
| 1432 |
+
CogView4Pipeline,
|
| 1433 |
+
ConsisIDPipeline,
|
| 1434 |
+
Cosmos2_5_PredictBasePipeline,
|
| 1435 |
+
Cosmos2_5_TransferPipeline,
|
| 1436 |
+
Cosmos2TextToImagePipeline,
|
| 1437 |
+
Cosmos2VideoToWorldPipeline,
|
| 1438 |
+
Cosmos3OmniPipeline,
|
| 1439 |
+
CosmosActionCondition,
|
| 1440 |
+
CosmosTextToWorldPipeline,
|
| 1441 |
+
CosmosVideoToWorldPipeline,
|
| 1442 |
+
CycleDiffusionPipeline,
|
| 1443 |
+
DiffusionGemmaPipeline,
|
| 1444 |
+
DiffusionGemmaPipelineOutput,
|
| 1445 |
+
DreamLiteMobilePipeline,
|
| 1446 |
+
DreamLitePipeline,
|
| 1447 |
+
DreamLitePipelineOutput,
|
| 1448 |
+
EasyAnimateControlPipeline,
|
| 1449 |
+
EasyAnimateInpaintPipeline,
|
| 1450 |
+
EasyAnimatePipeline,
|
| 1451 |
+
ErnieImagePipeline,
|
| 1452 |
+
Flux2KleinInpaintPipeline,
|
| 1453 |
+
Flux2KleinKVPipeline,
|
| 1454 |
+
Flux2KleinPipeline,
|
| 1455 |
+
Flux2Pipeline,
|
| 1456 |
+
FluxControlImg2ImgPipeline,
|
| 1457 |
+
FluxControlInpaintPipeline,
|
| 1458 |
+
FluxControlNetImg2ImgPipeline,
|
| 1459 |
+
FluxControlNetInpaintPipeline,
|
| 1460 |
+
FluxControlNetPipeline,
|
| 1461 |
+
FluxControlPipeline,
|
| 1462 |
+
FluxFillPipeline,
|
| 1463 |
+
FluxImg2ImgPipeline,
|
| 1464 |
+
FluxInpaintPipeline,
|
| 1465 |
+
FluxKontextInpaintPipeline,
|
| 1466 |
+
FluxKontextPipeline,
|
| 1467 |
+
FluxPipeline,
|
| 1468 |
+
FluxPriorReduxPipeline,
|
| 1469 |
+
GlmImagePipeline,
|
| 1470 |
+
HeliosPipeline,
|
| 1471 |
+
HeliosPyramidPipeline,
|
| 1472 |
+
HiDreamImagePipeline,
|
| 1473 |
+
HunyuanDiTControlNetPipeline,
|
| 1474 |
+
HunyuanDiTPAGPipeline,
|
| 1475 |
+
HunyuanDiTPipeline,
|
| 1476 |
+
HunyuanImagePipeline,
|
| 1477 |
+
HunyuanImageRefinerPipeline,
|
| 1478 |
+
HunyuanSkyreelsImageToVideoPipeline,
|
| 1479 |
+
HunyuanVideo15ImageToVideoPipeline,
|
| 1480 |
+
HunyuanVideo15Pipeline,
|
| 1481 |
+
HunyuanVideoFramepackPipeline,
|
| 1482 |
+
HunyuanVideoImageToVideoPipeline,
|
| 1483 |
+
HunyuanVideoPipeline,
|
| 1484 |
+
I2VGenXLPipeline,
|
| 1485 |
+
Ideogram4Pipeline,
|
| 1486 |
+
Ideogram4PromptEnhancerHead,
|
| 1487 |
+
IFImg2ImgPipeline,
|
| 1488 |
+
IFImg2ImgSuperResolutionPipeline,
|
| 1489 |
+
IFInpaintingPipeline,
|
| 1490 |
+
IFInpaintingSuperResolutionPipeline,
|
| 1491 |
+
IFPipeline,
|
| 1492 |
+
IFSuperResolutionPipeline,
|
| 1493 |
+
ImageTextPipelineOutput,
|
| 1494 |
+
JoyImageEditPipeline,
|
| 1495 |
+
JoyImageEditPipelineOutput,
|
| 1496 |
+
JoyImageEditPlusPipeline,
|
| 1497 |
+
JoyImageEditPlusPipelineOutput,
|
| 1498 |
+
Kandinsky3Img2ImgPipeline,
|
| 1499 |
+
Kandinsky3Pipeline,
|
| 1500 |
+
Kandinsky5I2IPipeline,
|
| 1501 |
+
Kandinsky5I2VPipeline,
|
| 1502 |
+
Kandinsky5T2IPipeline,
|
| 1503 |
+
Kandinsky5T2VPipeline,
|
| 1504 |
+
KandinskyCombinedPipeline,
|
| 1505 |
+
KandinskyImg2ImgCombinedPipeline,
|
| 1506 |
+
KandinskyImg2ImgPipeline,
|
| 1507 |
+
KandinskyInpaintCombinedPipeline,
|
| 1508 |
+
KandinskyInpaintPipeline,
|
| 1509 |
+
KandinskyPipeline,
|
| 1510 |
+
KandinskyPriorPipeline,
|
| 1511 |
+
KandinskyV22CombinedPipeline,
|
| 1512 |
+
KandinskyV22ControlnetImg2ImgPipeline,
|
| 1513 |
+
KandinskyV22ControlnetPipeline,
|
| 1514 |
+
KandinskyV22Img2ImgCombinedPipeline,
|
| 1515 |
+
KandinskyV22Img2ImgPipeline,
|
| 1516 |
+
KandinskyV22InpaintCombinedPipeline,
|
| 1517 |
+
KandinskyV22InpaintPipeline,
|
| 1518 |
+
KandinskyV22Pipeline,
|
| 1519 |
+
KandinskyV22PriorEmb2EmbPipeline,
|
| 1520 |
+
KandinskyV22PriorPipeline,
|
| 1521 |
+
Krea2Pipeline,
|
| 1522 |
+
LatentConsistencyModelImg2ImgPipeline,
|
| 1523 |
+
LatentConsistencyModelPipeline,
|
| 1524 |
+
LattePipeline,
|
| 1525 |
+
LDMTextToImagePipeline,
|
| 1526 |
+
LEditsPPPipelineStableDiffusion,
|
| 1527 |
+
LEditsPPPipelineStableDiffusionXL,
|
| 1528 |
+
LLaDA2Pipeline,
|
| 1529 |
+
LLaDA2PipelineOutput,
|
| 1530 |
+
LongCatAudioDiTPipeline,
|
| 1531 |
+
LongCatImageEditPipeline,
|
| 1532 |
+
LongCatImagePipeline,
|
| 1533 |
+
LTX2ConditionPipeline,
|
| 1534 |
+
LTX2HDRPipeline,
|
| 1535 |
+
LTX2ImageToVideoPipeline,
|
| 1536 |
+
LTX2InContextPipeline,
|
| 1537 |
+
LTX2LatentUpsamplePipeline,
|
| 1538 |
+
LTX2Pipeline,
|
| 1539 |
+
LTXConditionPipeline,
|
| 1540 |
+
LTXI2VLongMultiPromptPipeline,
|
| 1541 |
+
LTXImageToVideoPipeline,
|
| 1542 |
+
LTXLatentUpsamplePipeline,
|
| 1543 |
+
LTXPipeline,
|
| 1544 |
+
LucyEditPipeline,
|
| 1545 |
+
Lumina2Pipeline,
|
| 1546 |
+
Lumina2Text2ImgPipeline,
|
| 1547 |
+
LuminaPipeline,
|
| 1548 |
+
LuminaText2ImgPipeline,
|
| 1549 |
+
MarigoldDepthPipeline,
|
| 1550 |
+
MarigoldIntrinsicsPipeline,
|
| 1551 |
+
MarigoldNormalsPipeline,
|
| 1552 |
+
MochiPipeline,
|
| 1553 |
+
MotifVideoImage2VideoPipeline,
|
| 1554 |
+
MotifVideoPipeline,
|
| 1555 |
+
MotifVideoPipelineOutput,
|
| 1556 |
+
MusicLDMPipeline,
|
| 1557 |
+
NucleusMoEImagePipeline,
|
| 1558 |
+
OmniGenPipeline,
|
| 1559 |
+
OvisImagePipeline,
|
| 1560 |
+
PaintByExamplePipeline,
|
| 1561 |
+
PIAPipeline,
|
| 1562 |
+
PixArtAlphaPipeline,
|
| 1563 |
+
PixArtSigmaPAGPipeline,
|
| 1564 |
+
PixArtSigmaPipeline,
|
| 1565 |
+
PRXPipeline,
|
| 1566 |
+
PRXPixelPipeline,
|
| 1567 |
+
QwenImageControlNetInpaintPipeline,
|
| 1568 |
+
QwenImageControlNetPipeline,
|
| 1569 |
+
QwenImageEditInpaintPipeline,
|
| 1570 |
+
QwenImageEditPipeline,
|
| 1571 |
+
QwenImageEditPlusPipeline,
|
| 1572 |
+
QwenImageImg2ImgPipeline,
|
| 1573 |
+
QwenImageInpaintPipeline,
|
| 1574 |
+
QwenImageLayeredPipeline,
|
| 1575 |
+
QwenImagePipeline,
|
| 1576 |
+
ReduxImageEncoder,
|
| 1577 |
+
SanaControlNetPipeline,
|
| 1578 |
+
SanaImageToVideoPipeline,
|
| 1579 |
+
SanaPAGPipeline,
|
| 1580 |
+
SanaPipeline,
|
| 1581 |
+
SanaSprintImg2ImgPipeline,
|
| 1582 |
+
SanaSprintPipeline,
|
| 1583 |
+
SanaVideoPipeline,
|
| 1584 |
+
SemanticStableDiffusionPipeline,
|
| 1585 |
+
ShapEImg2ImgPipeline,
|
| 1586 |
+
ShapEPipeline,
|
| 1587 |
+
SkyReelsV2DiffusionForcingImageToVideoPipeline,
|
| 1588 |
+
SkyReelsV2DiffusionForcingPipeline,
|
| 1589 |
+
SkyReelsV2DiffusionForcingVideoToVideoPipeline,
|
| 1590 |
+
SkyReelsV2ImageToVideoPipeline,
|
| 1591 |
+
SkyReelsV2Pipeline,
|
| 1592 |
+
StableAudioPipeline,
|
| 1593 |
+
StableAudioProjectionModel,
|
| 1594 |
+
StableCascadeCombinedPipeline,
|
| 1595 |
+
StableCascadeDecoderPipeline,
|
| 1596 |
+
StableCascadePriorPipeline,
|
| 1597 |
+
StableDiffusion3ControlNetInpaintingPipeline,
|
| 1598 |
+
StableDiffusion3ControlNetPipeline,
|
| 1599 |
+
StableDiffusion3Img2ImgPipeline,
|
| 1600 |
+
StableDiffusion3InpaintPipeline,
|
| 1601 |
+
StableDiffusion3PAGImg2ImgPipeline,
|
| 1602 |
+
StableDiffusion3PAGPipeline,
|
| 1603 |
+
StableDiffusion3Pipeline,
|
| 1604 |
+
StableDiffusionAdapterPipeline,
|
| 1605 |
+
StableDiffusionAttendAndExcitePipeline,
|
| 1606 |
+
StableDiffusionControlNetImg2ImgPipeline,
|
| 1607 |
+
StableDiffusionControlNetInpaintPipeline,
|
| 1608 |
+
StableDiffusionControlNetPAGInpaintPipeline,
|
| 1609 |
+
StableDiffusionControlNetPAGPipeline,
|
| 1610 |
+
StableDiffusionControlNetPipeline,
|
| 1611 |
+
StableDiffusionControlNetXSPipeline,
|
| 1612 |
+
StableDiffusionDepth2ImgPipeline,
|
| 1613 |
+
StableDiffusionDiffEditPipeline,
|
| 1614 |
+
StableDiffusionGLIGENPipeline,
|
| 1615 |
+
StableDiffusionGLIGENTextImagePipeline,
|
| 1616 |
+
StableDiffusionImageVariationPipeline,
|
| 1617 |
+
StableDiffusionImg2ImgPipeline,
|
| 1618 |
+
StableDiffusionInpaintPipeline,
|
| 1619 |
+
StableDiffusionInpaintPipelineLegacy,
|
| 1620 |
+
StableDiffusionInstructPix2PixPipeline,
|
| 1621 |
+
StableDiffusionLatentUpscalePipeline,
|
| 1622 |
+
StableDiffusionLDM3DPipeline,
|
| 1623 |
+
StableDiffusionModelEditingPipeline,
|
| 1624 |
+
StableDiffusionPAGImg2ImgPipeline,
|
| 1625 |
+
StableDiffusionPAGInpaintPipeline,
|
| 1626 |
+
StableDiffusionPAGPipeline,
|
| 1627 |
+
StableDiffusionPanoramaPipeline,
|
| 1628 |
+
StableDiffusionParadigmsPipeline,
|
| 1629 |
+
StableDiffusionPipeline,
|
| 1630 |
+
StableDiffusionPipelineSafe,
|
| 1631 |
+
StableDiffusionPix2PixZeroPipeline,
|
| 1632 |
+
StableDiffusionSAGPipeline,
|
| 1633 |
+
StableDiffusionUpscalePipeline,
|
| 1634 |
+
StableDiffusionXLAdapterPipeline,
|
| 1635 |
+
StableDiffusionXLControlNetImg2ImgPipeline,
|
| 1636 |
+
StableDiffusionXLControlNetInpaintPipeline,
|
| 1637 |
+
StableDiffusionXLControlNetPAGImg2ImgPipeline,
|
| 1638 |
+
StableDiffusionXLControlNetPAGPipeline,
|
| 1639 |
+
StableDiffusionXLControlNetPipeline,
|
| 1640 |
+
StableDiffusionXLControlNetUnionImg2ImgPipeline,
|
| 1641 |
+
StableDiffusionXLControlNetUnionInpaintPipeline,
|
| 1642 |
+
StableDiffusionXLControlNetUnionPipeline,
|
| 1643 |
+
StableDiffusionXLControlNetXSPipeline,
|
| 1644 |
+
StableDiffusionXLImg2ImgPipeline,
|
| 1645 |
+
StableDiffusionXLInpaintPipeline,
|
| 1646 |
+
StableDiffusionXLInstructPix2PixPipeline,
|
| 1647 |
+
StableDiffusionXLPAGImg2ImgPipeline,
|
| 1648 |
+
StableDiffusionXLPAGInpaintPipeline,
|
| 1649 |
+
StableDiffusionXLPAGPipeline,
|
| 1650 |
+
StableDiffusionXLPipeline,
|
| 1651 |
+
StableUnCLIPImg2ImgPipeline,
|
| 1652 |
+
StableUnCLIPPipeline,
|
| 1653 |
+
StableVideoDiffusionPipeline,
|
| 1654 |
+
TextToVideoSDPipeline,
|
| 1655 |
+
TextToVideoZeroPipeline,
|
| 1656 |
+
TextToVideoZeroSDXLPipeline,
|
| 1657 |
+
UnCLIPImageVariationPipeline,
|
| 1658 |
+
UnCLIPPipeline,
|
| 1659 |
+
UniDiffuserModel,
|
| 1660 |
+
UniDiffuserPipeline,
|
| 1661 |
+
UniDiffuserTextDecoder,
|
| 1662 |
+
VersatileDiffusionDualGuidedPipeline,
|
| 1663 |
+
VersatileDiffusionImageVariationPipeline,
|
| 1664 |
+
VersatileDiffusionPipeline,
|
| 1665 |
+
VersatileDiffusionTextToImagePipeline,
|
| 1666 |
+
VideoToVideoSDPipeline,
|
| 1667 |
+
VisualClozeGenerationPipeline,
|
| 1668 |
+
VisualClozePipeline,
|
| 1669 |
+
VQDiffusionPipeline,
|
| 1670 |
+
WanAnimatePipeline,
|
| 1671 |
+
WanImageToVideoPipeline,
|
| 1672 |
+
WanPipeline,
|
| 1673 |
+
WanVACEPipeline,
|
| 1674 |
+
WanVideoToVideoPipeline,
|
| 1675 |
+
WuerstchenCombinedPipeline,
|
| 1676 |
+
WuerstchenDecoderPipeline,
|
| 1677 |
+
WuerstchenPriorPipeline,
|
| 1678 |
+
ZImageControlNetInpaintPipeline,
|
| 1679 |
+
ZImageControlNetPipeline,
|
| 1680 |
+
ZImageImg2ImgPipeline,
|
| 1681 |
+
ZImageInpaintPipeline,
|
| 1682 |
+
ZImageOmniPipeline,
|
| 1683 |
+
ZImagePipeline,
|
| 1684 |
+
)
|
| 1685 |
+
|
| 1686 |
+
try:
|
| 1687 |
+
if not (is_torch_available() and is_transformers_available() and is_sentencepiece_available()):
|
| 1688 |
+
raise OptionalDependencyNotAvailable()
|
| 1689 |
+
except OptionalDependencyNotAvailable:
|
| 1690 |
+
from .utils.dummy_torch_and_transformers_and_sentencepiece_objects import * # noqa F403
|
| 1691 |
+
else:
|
| 1692 |
+
from .pipelines import KolorsImg2ImgPipeline, KolorsPAGPipeline, KolorsPipeline
|
| 1693 |
+
|
| 1694 |
+
try:
|
| 1695 |
+
if not (is_torch_available() and is_transformers_available() and is_opencv_available()):
|
| 1696 |
+
raise OptionalDependencyNotAvailable()
|
| 1697 |
+
except OptionalDependencyNotAvailable:
|
| 1698 |
+
from .utils.dummy_torch_and_transformers_and_opencv_objects import * # noqa F403
|
| 1699 |
+
else:
|
| 1700 |
+
from .pipelines import ConsisIDPipeline
|
| 1701 |
+
|
| 1702 |
+
try:
|
| 1703 |
+
if not (is_torch_available() and is_transformers_available() and is_onnx_available()):
|
| 1704 |
+
raise OptionalDependencyNotAvailable()
|
| 1705 |
+
except OptionalDependencyNotAvailable:
|
| 1706 |
+
from .utils.dummy_torch_and_transformers_and_onnx_objects import * # noqa F403
|
| 1707 |
+
else:
|
| 1708 |
+
from .pipelines import (
|
| 1709 |
+
OnnxStableDiffusionImg2ImgPipeline,
|
| 1710 |
+
OnnxStableDiffusionInpaintPipeline,
|
| 1711 |
+
OnnxStableDiffusionInpaintPipelineLegacy,
|
| 1712 |
+
OnnxStableDiffusionPipeline,
|
| 1713 |
+
OnnxStableDiffusionUpscalePipeline,
|
| 1714 |
+
StableDiffusionOnnxPipeline,
|
| 1715 |
+
)
|
| 1716 |
+
|
| 1717 |
+
try:
|
| 1718 |
+
if not (is_torch_available() and is_librosa_available()):
|
| 1719 |
+
raise OptionalDependencyNotAvailable()
|
| 1720 |
+
except OptionalDependencyNotAvailable:
|
| 1721 |
+
from .utils.dummy_torch_and_librosa_objects import * # noqa F403
|
| 1722 |
+
else:
|
| 1723 |
+
from .pipelines import AudioDiffusionPipeline, Mel
|
| 1724 |
+
|
| 1725 |
+
try:
|
| 1726 |
+
if not (is_transformers_available() and is_torch_available() and is_note_seq_available()):
|
| 1727 |
+
raise OptionalDependencyNotAvailable()
|
| 1728 |
+
except OptionalDependencyNotAvailable:
|
| 1729 |
+
from .utils.dummy_transformers_and_torch_and_note_seq_objects import * # noqa F403
|
| 1730 |
+
else:
|
| 1731 |
+
from .pipelines import SpectrogramDiffusionPipeline
|
| 1732 |
+
|
| 1733 |
+
try:
|
| 1734 |
+
if not (is_note_seq_available()):
|
| 1735 |
+
raise OptionalDependencyNotAvailable()
|
| 1736 |
+
except OptionalDependencyNotAvailable:
|
| 1737 |
+
from .utils.dummy_note_seq_objects import * # noqa F403
|
| 1738 |
+
else:
|
| 1739 |
+
from .pipelines import MidiProcessor
|
| 1740 |
+
|
| 1741 |
+
else:
|
| 1742 |
+
import sys
|
| 1743 |
+
|
| 1744 |
+
sys.modules[__name__] = _LazyModule(
|
| 1745 |
+
__name__,
|
| 1746 |
+
globals()["__file__"],
|
| 1747 |
+
_import_structure,
|
| 1748 |
+
module_spec=__spec__,
|
| 1749 |
+
extra_objects={"__version__": __version__},
|
| 1750 |
+
)
|
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,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
getattr(module, child_class)().save_pretrained(os.getcwd())
|
| 107 |
+
|
| 108 |
+
def _choose_block(self, candidates, chosen=None):
|
| 109 |
+
for cls, base in candidates:
|
| 110 |
+
if cls == chosen:
|
| 111 |
+
return cls, base
|
| 112 |
+
return None, None
|
| 113 |
+
|
| 114 |
+
def _get_class_names(self, file_path):
|
| 115 |
+
source = file_path.read_text(encoding="utf-8")
|
| 116 |
+
try:
|
| 117 |
+
tree = ast.parse(source, filename=file_path)
|
| 118 |
+
except SyntaxError as e:
|
| 119 |
+
raise ValueError(f"Could not parse {file_path!r}: {e}") from e
|
| 120 |
+
|
| 121 |
+
results: list[tuple[str, str]] = []
|
| 122 |
+
for node in tree.body:
|
| 123 |
+
if not isinstance(node, ast.ClassDef):
|
| 124 |
+
continue
|
| 125 |
+
|
| 126 |
+
base_names = [bname for b in node.bases if (bname := self._get_base_name(b)) is not None]
|
| 127 |
+
|
| 128 |
+
for allowed in EXPECTED_PARENT_CLASSES:
|
| 129 |
+
if allowed in base_names:
|
| 130 |
+
results.append((node.name, allowed))
|
| 131 |
+
|
| 132 |
+
return results
|
| 133 |
+
|
| 134 |
+
def _get_base_name(self, node: ast.expr):
|
| 135 |
+
if isinstance(node, ast.Name):
|
| 136 |
+
return node.id
|
| 137 |
+
elif isinstance(node, ast.Attribute):
|
| 138 |
+
val = self._get_base_name(node.value)
|
| 139 |
+
return f"{val}.{node.attr}" if val else node.attr
|
| 140 |
+
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,1227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 json
|
| 24 |
+
import os
|
| 25 |
+
import sys
|
| 26 |
+
from argparse import ArgumentParser, Namespace, _SubParsersAction
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Any
|
| 29 |
+
|
| 30 |
+
from huggingface_hub.cli._output import out
|
| 31 |
+
|
| 32 |
+
from diffusers.models.attention_dispatch import _HUB_KERNELS_REGISTRY
|
| 33 |
+
from diffusers.utils import load_image, load_video, logging
|
| 34 |
+
|
| 35 |
+
from . import BaseDiffusersCLICommand
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
logger = logging.get_logger("diffusers-cli/run")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Constants
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
|
| 45 |
+
DEFAULT_OUTPUT_DIR = str(Path.home() / ".diffusers" / "cli" / "run" / "outputs")
|
| 46 |
+
DTYPE_CHOICES = ("auto", "float16", "fp16", "bfloat16", "bf16", "float32", "fp32")
|
| 47 |
+
CPU_OFFLOAD_CHOICES = ("model", "group")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
ATTENTION_BACKEND_CHOICES = ("default", *sorted(b.value for b in _HUB_KERNELS_REGISTRY))
|
| 51 |
+
|
| 52 |
+
# Kwarg keys whose string value gets auto-loaded before being passed to the pipeline call.
|
| 53 |
+
# Images resolve via `diffusers.utils.load_image` → PIL.Image.Image; videos resolve via
|
| 54 |
+
# `diffusers.utils.load_video` → list[PIL.Image.Image].
|
| 55 |
+
_IMAGE_INPUT_KEYS = (
|
| 56 |
+
"image",
|
| 57 |
+
"mask_image",
|
| 58 |
+
"control_image",
|
| 59 |
+
"ip_adapter_image",
|
| 60 |
+
"image_2",
|
| 61 |
+
)
|
| 62 |
+
_VIDEO_INPUT_KEYS = (
|
| 63 |
+
"video",
|
| 64 |
+
"control_video",
|
| 65 |
+
)
|
| 66 |
+
_AUDIO_INPUT_KEYS = (
|
| 67 |
+
"initial_audio_waveforms",
|
| 68 |
+
"reference_audio",
|
| 69 |
+
"src_audio",
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Pipeline attribute prefixes that identify a denoiser submodule. Matches base names
|
| 73 |
+
# (`transformer`, `unet`) and their numbered variants (`transformer_2`, etc.).
|
| 74 |
+
_DENOISER_COMPONENT_KEYS = ("transformer", "unet")
|
| 75 |
+
|
| 76 |
+
_DEFAULT_REMOTE_DEPS = (
|
| 77 |
+
"diffusers",
|
| 78 |
+
"accelerate",
|
| 79 |
+
"transformers",
|
| 80 |
+
"safetensors",
|
| 81 |
+
"sentencepiece", # required by several text-encoder tokenizers (T5, LLaMA, …)
|
| 82 |
+
"ftfy", # required by older CLIP text-encoder paths
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Base sandbox image — provides torch + CUDA so `uv pip install --system`
|
| 86 |
+
# only has to add the small Python deps. cuda12.8 is the highest cuda12.x tag
|
| 87 |
+
# below the HF Jobs host driver's CUDA 12.9 max.
|
| 88 |
+
_DEFAULT_REMOTE_IMAGE = "pytorch/pytorch:2.10.0-cuda12.8-cudnn9-runtime"
|
| 89 |
+
|
| 90 |
+
# Installed console-script name invoked inside the sandbox after the deps land.
|
| 91 |
+
_CONTAINER_CLI_BINARY = "diffusers-cli"
|
| 92 |
+
|
| 93 |
+
# Working directories inside the sandbox: local media from `--pipeline-kwargs` is uploaded
|
| 94 |
+
# under _SANDBOX_INPUTS_DIR, and the sandbox CLI is told to write its outputs under
|
| 95 |
+
# _SANDBOX_OUTPUTS_DIR so we can download them back afterwards.
|
| 96 |
+
_SANDBOX_INPUTS_DIR = "/tmp/diffusers-cli/inputs"
|
| 97 |
+
_SANDBOX_OUTPUTS_DIR = "/tmp/diffusers-cli/outputs"
|
| 98 |
+
|
| 99 |
+
RUN_ID_ENV = "DIFFUSERS_CLI_RUN_ID"
|
| 100 |
+
|
| 101 |
+
# Namespace keys that control *how* a remote run is dispatched, not what the sandbox CLI
|
| 102 |
+
# runs. They are stripped when forwarding argv to the sandbox.
|
| 103 |
+
REMOTE_KEYS = frozenset(
|
| 104 |
+
{
|
| 105 |
+
"remote",
|
| 106 |
+
"flavor",
|
| 107 |
+
"timeout",
|
| 108 |
+
"dependencies",
|
| 109 |
+
"namespace",
|
| 110 |
+
"image",
|
| 111 |
+
"keep_alive",
|
| 112 |
+
"sandbox_id",
|
| 113 |
+
"idle_timeout",
|
| 114 |
+
"volume",
|
| 115 |
+
"func",
|
| 116 |
+
"format", # top-level --format is a local rendering flag; never forward to the sandbox
|
| 117 |
+
}
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ---------------------------------------------------------------------------
|
| 122 |
+
# Argparse helpers
|
| 123 |
+
# ---------------------------------------------------------------------------
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _add_loading_arguments(parser: ArgumentParser) -> None:
|
| 127 |
+
parser.add_argument("--model", "-m", required=True, help="Model id on the Hugging Face Hub or local path.")
|
| 128 |
+
parser.add_argument(
|
| 129 |
+
"--device-map",
|
| 130 |
+
default=None,
|
| 131 |
+
help=(
|
| 132 |
+
"Component placement. Accepts a torch device string (`cuda`, `cuda:0`, `cpu`, `mps`), "
|
| 133 |
+
"`balanced` for pipeline-level auto-split across visible GPUs, or a JSON dict of "
|
| 134 |
+
'`{"<component>": <device>}` for explicit per-component placement. Auto-detected if omitted.'
|
| 135 |
+
),
|
| 136 |
+
)
|
| 137 |
+
parser.add_argument("--dtype", default="auto", choices=DTYPE_CHOICES, help="Torch dtype for pipeline weights.")
|
| 138 |
+
parser.add_argument("--variant", default=None, help='Optional weight variant (e.g. "fp16").')
|
| 139 |
+
parser.add_argument("--revision", default=None, help="Model revision (branch, tag, or commit SHA).")
|
| 140 |
+
parser.add_argument("--token", default=None, help="Hugging Face token for gated/private models.")
|
| 141 |
+
parser.add_argument("--trust-remote-code", action="store_true", help="Allow custom code from the Hub.")
|
| 142 |
+
parser.add_argument(
|
| 143 |
+
"--lora",
|
| 144 |
+
action="append",
|
| 145 |
+
default=None,
|
| 146 |
+
metavar="JSON",
|
| 147 |
+
help=(
|
| 148 |
+
"JSON dict describing a LoRA adapter to attach after the pipeline loads. Repeat to stack "
|
| 149 |
+
'multiple adapters. Format: \'{"lora_id": "<id>", "lora_scale": <float>}\'. `lora_scale` '
|
| 150 |
+
"defaults to 1.0; `adapter_name` is optional (auto-generated as `lora_<i>` when stacking)."
|
| 151 |
+
),
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def _add_optimization_arguments(parser: ArgumentParser) -> None:
|
| 156 |
+
parser.add_argument(
|
| 157 |
+
"--cpu-offload",
|
| 158 |
+
choices=CPU_OFFLOAD_CHOICES,
|
| 159 |
+
default=None,
|
| 160 |
+
help=(
|
| 161 |
+
"Offload pipeline components to CPU during inference. "
|
| 162 |
+
"'model' uses enable_model_cpu_offload, "
|
| 163 |
+
"'group' uses pipeline.enable_group_offload(leaf_level, use_stream=True)."
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
parser.add_argument(
|
| 167 |
+
"--attention-backend",
|
| 168 |
+
choices=ATTENTION_BACKEND_CHOICES,
|
| 169 |
+
default="default",
|
| 170 |
+
help=(
|
| 171 |
+
"Override the attention backend on the transformer/UNet. "
|
| 172 |
+
"Only Hub-hosted kernels are exposed — they auto-download on first use."
|
| 173 |
+
),
|
| 174 |
+
)
|
| 175 |
+
parser.add_argument("--vae-tiling", action="store_true", help="Enable VAE tiling (lower peak VRAM).")
|
| 176 |
+
parser.add_argument("--vae-slicing", action="store_true", help="Enable VAE slicing (lower peak VRAM).")
|
| 177 |
+
parser.add_argument(
|
| 178 |
+
"--context-parallel",
|
| 179 |
+
action="store_true",
|
| 180 |
+
help=(
|
| 181 |
+
"Enable Ulysses-style context parallelism (ulysses_anything mode). "
|
| 182 |
+
"Requires a DiT-based pipeline and launching the CLI under torchrun with ≥2 GPUs."
|
| 183 |
+
),
|
| 184 |
+
)
|
| 185 |
+
parser.add_argument(
|
| 186 |
+
"--compile",
|
| 187 |
+
nargs="?",
|
| 188 |
+
const='{"fullgraph": true}',
|
| 189 |
+
default=None,
|
| 190 |
+
metavar="JSON",
|
| 191 |
+
help=(
|
| 192 |
+
"torch.compile every denoiser submodule on the pipeline. Accepts an optional JSON "
|
| 193 |
+
'object of kwargs forwarded to `torch.compile`, e.g. \'{"mode": "max-autotune", '
|
| 194 |
+
'"fullgraph": true}\'. Bare `--compile` uses `fullgraph=true`. Adds a one-time '
|
| 195 |
+
"compilation cost on the first step but speeds up every subsequent step — worth it "
|
| 196 |
+
"for multi-step generation (50+ steps)."
|
| 197 |
+
),
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _add_output_arguments(parser: ArgumentParser) -> None:
|
| 202 |
+
parser.add_argument(
|
| 203 |
+
"--output",
|
| 204 |
+
"-o",
|
| 205 |
+
default=None,
|
| 206 |
+
help=(
|
| 207 |
+
"Output file or directory. Defaults to "
|
| 208 |
+
"~/.diffusers/cli/run/outputs/diffusers-run-<YYYYMMDDTHHMMSS>-<short-uuid>/<NNNN>.<ext>."
|
| 209 |
+
),
|
| 210 |
+
)
|
| 211 |
+
parser.add_argument(
|
| 212 |
+
"--push-to",
|
| 213 |
+
default=None,
|
| 214 |
+
help=(
|
| 215 |
+
"Upload the generated files to this HF bucket after saving (created if missing). Accepts "
|
| 216 |
+
"an HF bucket id (`<namespace>/<name>`), an `hf://buckets/<namespace>/<name>[/<subpath>]` "
|
| 217 |
+
"URI, or a browser URL for the same — a subpath is used as a folder prefix. Under --remote "
|
| 218 |
+
"the upload runs inside the sandbox; without an explicit --output the bucket becomes the "
|
| 219 |
+
"sole destination and nothing is downloaded back."
|
| 220 |
+
),
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _add_remote_arguments(parser: ArgumentParser) -> None:
|
| 225 |
+
parser.add_argument(
|
| 226 |
+
"--remote",
|
| 227 |
+
action="store_true",
|
| 228 |
+
help="Run this command in a Hugging Face Sandbox instead of on the local machine.",
|
| 229 |
+
)
|
| 230 |
+
parser.add_argument(
|
| 231 |
+
"--flavor",
|
| 232 |
+
default="a10g-small",
|
| 233 |
+
help="HF Sandbox hardware flavor for --remote (e.g. a10g-small, a100-large, cpu-basic).",
|
| 234 |
+
)
|
| 235 |
+
parser.add_argument(
|
| 236 |
+
"--timeout",
|
| 237 |
+
default="10m",
|
| 238 |
+
help="Max wallclock for the run command inside the sandbox (e.g. 30m, 2h). Defaults to 10m.",
|
| 239 |
+
)
|
| 240 |
+
parser.add_argument(
|
| 241 |
+
"--dependencies",
|
| 242 |
+
action="append",
|
| 243 |
+
default=None,
|
| 244 |
+
help="Extra pip dependencies to install in the sandbox. Repeat to add multiple.",
|
| 245 |
+
)
|
| 246 |
+
parser.add_argument(
|
| 247 |
+
"--namespace",
|
| 248 |
+
default=None,
|
| 249 |
+
help="HF namespace to create the sandbox under (defaults to the current user).",
|
| 250 |
+
)
|
| 251 |
+
parser.add_argument(
|
| 252 |
+
"--image",
|
| 253 |
+
default=None,
|
| 254 |
+
help=(
|
| 255 |
+
"Sandbox image for --remote (defaults to "
|
| 256 |
+
f"{_DEFAULT_REMOTE_IMAGE!r}). Must provide torch + CUDA; the CLI installs the "
|
| 257 |
+
"small Python deps on top via `uv pip install --system`."
|
| 258 |
+
),
|
| 259 |
+
)
|
| 260 |
+
parser.add_argument(
|
| 261 |
+
"--keep-alive",
|
| 262 |
+
action="store_true",
|
| 263 |
+
help=(
|
| 264 |
+
"Don't terminate the sandbox after the run. Its id is printed so a later --remote run "
|
| 265 |
+
"can reconnect with --sandbox-id and reuse the warm deps/weights/compile cache."
|
| 266 |
+
),
|
| 267 |
+
)
|
| 268 |
+
parser.add_argument(
|
| 269 |
+
"--sandbox-id",
|
| 270 |
+
default=None,
|
| 271 |
+
help=(
|
| 272 |
+
"Reconnect to an existing sandbox (from a prior --keep-alive run) instead of creating a new "
|
| 273 |
+
"one, reusing its warm deps/weights/compile cache. Implies --keep-alive; stop it with "
|
| 274 |
+
"`hf sandbox kill <id>`."
|
| 275 |
+
),
|
| 276 |
+
)
|
| 277 |
+
parser.add_argument(
|
| 278 |
+
"--idle-timeout",
|
| 279 |
+
default="10m",
|
| 280 |
+
help=(
|
| 281 |
+
"Auto-shutdown the sandbox after this much inactivity (e.g. 30m, 1h). Defaults to 10m. "
|
| 282 |
+
"Only applied on new sandbox creation — ignored when reconnecting via --sandbox-id."
|
| 283 |
+
),
|
| 284 |
+
)
|
| 285 |
+
parser.add_argument(
|
| 286 |
+
"--volume",
|
| 287 |
+
action="append",
|
| 288 |
+
default=None,
|
| 289 |
+
metavar="BUCKET_ID[:MOUNT_PATH]",
|
| 290 |
+
help=(
|
| 291 |
+
"Mount an HF bucket into the sandbox as a read-write directory. Repeatable. Format: "
|
| 292 |
+
"`<namespace>/<name>` (mounts at `/mnt/buckets/<namespace>/<name>`) or "
|
| 293 |
+
"`<namespace>/<name>:/some/path` for a custom path. Reference mounted files from "
|
| 294 |
+
"--pipeline-kwargs like any other local path. Applied only on new sandbox creation — "
|
| 295 |
+
"ignored when reconnecting via --sandbox-id."
|
| 296 |
+
),
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
# ---------------------------------------------------------------------------
|
| 301 |
+
# Pipeline loading + optimization
|
| 302 |
+
# ---------------------------------------------------------------------------
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _resolve_dtype(name: str | None):
|
| 306 |
+
if name in (None, "auto"):
|
| 307 |
+
return "auto"
|
| 308 |
+
import torch
|
| 309 |
+
|
| 310 |
+
mapping = {
|
| 311 |
+
"fp32": torch.float32,
|
| 312 |
+
"float32": torch.float32,
|
| 313 |
+
"fp16": torch.float16,
|
| 314 |
+
"float16": torch.float16,
|
| 315 |
+
"bf16": torch.bfloat16,
|
| 316 |
+
"bfloat16": torch.bfloat16,
|
| 317 |
+
}
|
| 318 |
+
if name not in mapping:
|
| 319 |
+
raise ValueError(f"Unknown dtype: {name}")
|
| 320 |
+
return mapping[name]
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def _resolve_device_map(raw: str | None) -> str | dict:
|
| 324 |
+
"""Parse `--device-map` into a value acceptable by `from_pretrained(device_map=...)`.
|
| 325 |
+
|
| 326 |
+
Returns a JSON dict if the value looks like one, `"balanced"` verbatim, or a single-device string (e.g. `"cuda"`,
|
| 327 |
+
`"cuda:1"`, `"cpu"`, `"mps"`). Auto-detects when `raw is None`, pinning to `cuda:$LOCAL_RANK` under torchrun.
|
| 328 |
+
"""
|
| 329 |
+
if raw is None:
|
| 330 |
+
from diffusers.utils.torch_utils import torch_device
|
| 331 |
+
|
| 332 |
+
if torch_device == "cuda":
|
| 333 |
+
local_rank = os.environ.get("LOCAL_RANK")
|
| 334 |
+
if local_rank is not None:
|
| 335 |
+
import torch
|
| 336 |
+
|
| 337 |
+
torch.cuda.set_device(int(local_rank))
|
| 338 |
+
return f"cuda:{local_rank}"
|
| 339 |
+
return torch_device
|
| 340 |
+
|
| 341 |
+
if raw.strip().startswith("{"):
|
| 342 |
+
try:
|
| 343 |
+
parsed = json.loads(raw)
|
| 344 |
+
except json.JSONDecodeError as e:
|
| 345 |
+
raise SystemExit(f"--device-map must be a device string or a JSON dict: {e}") from e
|
| 346 |
+
if not isinstance(parsed, dict):
|
| 347 |
+
raise SystemExit("--device-map JSON must decode to an object.")
|
| 348 |
+
return parsed
|
| 349 |
+
|
| 350 |
+
return raw
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def _apply_cpu_offload(pipeline: Any, mode: str, device_map: str | dict) -> None:
|
| 354 |
+
"""Apply model or group CPU offload. Requires a single-device target (not balanced or dict)."""
|
| 355 |
+
if not isinstance(device_map, str) or device_map == "balanced":
|
| 356 |
+
raise SystemExit(
|
| 357 |
+
"--cpu-offload requires --device-map to be a single device string (e.g. 'cuda'); "
|
| 358 |
+
f"got {device_map!r}. balanced/dict placement is incompatible with CPU offload."
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
if mode == "model":
|
| 362 |
+
pipeline.enable_model_cpu_offload(device=device_map)
|
| 363 |
+
elif mode == "group":
|
| 364 |
+
import torch
|
| 365 |
+
|
| 366 |
+
pipeline.enable_group_offload(
|
| 367 |
+
onload_device=torch.device(device_map),
|
| 368 |
+
offload_type="leaf_level",
|
| 369 |
+
use_stream=True,
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _set_attention_backend(pipeline: Any, backend: str) -> None:
|
| 374 |
+
transformer = getattr(pipeline, "transformer", None)
|
| 375 |
+
if transformer is None or not hasattr(transformer, "set_attention_backend"):
|
| 376 |
+
logger.warning(
|
| 377 |
+
f"--attention-backend is only supported on transformer-based pipelines; "
|
| 378 |
+
f"{type(pipeline).__name__} uses the legacy UNet attention path."
|
| 379 |
+
)
|
| 380 |
+
return
|
| 381 |
+
try:
|
| 382 |
+
transformer.set_attention_backend(backend)
|
| 383 |
+
except (ValueError, ImportError, RuntimeError) as e:
|
| 384 |
+
logger.warning(
|
| 385 |
+
f"Attention backend {backend!r} could not be set on {type(transformer).__name__}: "
|
| 386 |
+
f"{type(e).__name__}: {e}. Falling back to the model's default backend."
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def _enable_context_parallel(pipeline: Any) -> None:
|
| 391 |
+
import torch
|
| 392 |
+
|
| 393 |
+
if not torch.distributed.is_available():
|
| 394 |
+
raise SystemExit("--context-parallel requires a torch build with distributed support.")
|
| 395 |
+
|
| 396 |
+
if not torch.distributed.is_initialized():
|
| 397 |
+
# Hybrid backend: ulysses_anything's per-rank size coordination wants Gloo on CPU
|
| 398 |
+
# (avoids H2D/D2H for a tiny int tensor); the main attention all-to-all stays on NCCL.
|
| 399 |
+
torch.distributed.init_process_group(backend="cpu:gloo,cuda:nccl")
|
| 400 |
+
|
| 401 |
+
transformer = getattr(pipeline, "transformer", None)
|
| 402 |
+
if transformer is None or not hasattr(transformer, "enable_parallelism"):
|
| 403 |
+
raise SystemExit(
|
| 404 |
+
"--context-parallel requires a DiT-based pipeline. "
|
| 405 |
+
f"{type(pipeline).__name__} does not expose a `transformer` with `enable_parallelism`."
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
from diffusers import ContextParallelConfig
|
| 409 |
+
|
| 410 |
+
transformer.enable_parallelism(
|
| 411 |
+
config=ContextParallelConfig(
|
| 412 |
+
ulysses_degree=torch.distributed.get_world_size(),
|
| 413 |
+
ring_degree=1,
|
| 414 |
+
ulysses_anything=True,
|
| 415 |
+
)
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def _apply_optimizations(pipeline: Any, args: Namespace) -> None:
|
| 420 |
+
"""Apply VAE tiling/slicing, attention backend, context-parallel, and torch.compile toggles."""
|
| 421 |
+
vae = getattr(pipeline, "vae", None)
|
| 422 |
+
if args.vae_tiling and vae is not None and hasattr(vae, "enable_tiling"):
|
| 423 |
+
vae.enable_tiling()
|
| 424 |
+
if args.vae_slicing and vae is not None and hasattr(vae, "enable_slicing"):
|
| 425 |
+
vae.enable_slicing()
|
| 426 |
+
if args.attention_backend != "default":
|
| 427 |
+
_set_attention_backend(pipeline, args.attention_backend)
|
| 428 |
+
if args.context_parallel:
|
| 429 |
+
_enable_context_parallel(pipeline)
|
| 430 |
+
if args.compile is not None:
|
| 431 |
+
if args.context_parallel:
|
| 432 |
+
logger.warning("--compile is currently not supported with --context-parallel; skipping compile.")
|
| 433 |
+
else:
|
| 434 |
+
_compile_denoiser(pipeline, args.compile)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def _compile_denoiser(pipeline: Any, compile_spec: str) -> None:
|
| 438 |
+
"""Compile every `transformer*` and `unet*` submodule on the pipeline.
|
| 439 |
+
|
| 440 |
+
`compile_spec` is the raw JSON string from `--compile` (`"{}"` for bare flag). Decoded into kwargs and forwarded
|
| 441 |
+
verbatim to the compile call.
|
| 442 |
+
|
| 443 |
+
Prefers regional compilation via `module.compile_repeated_blocks(**kwargs)` — only compiles the repeated inner
|
| 444 |
+
blocks (the bulk of the compute), much faster first-step latency than compiling the whole module. Falls back to
|
| 445 |
+
full `torch.compile` if the model doesn't expose `_repeated_blocks`.
|
| 446 |
+
"""
|
| 447 |
+
import torch
|
| 448 |
+
|
| 449 |
+
try:
|
| 450 |
+
compile_kwargs = json.loads(compile_spec)
|
| 451 |
+
except json.JSONDecodeError as e:
|
| 452 |
+
raise SystemExit(f"--compile must be valid JSON: {e}") from e
|
| 453 |
+
if not isinstance(compile_kwargs, dict):
|
| 454 |
+
raise SystemExit("--compile must decode to a JSON object.")
|
| 455 |
+
|
| 456 |
+
for attr in dir(pipeline):
|
| 457 |
+
if not any(attr.startswith(key) for key in _DENOISER_COMPONENT_KEYS):
|
| 458 |
+
continue
|
| 459 |
+
module = getattr(pipeline, attr, None)
|
| 460 |
+
if not isinstance(module, torch.nn.Module):
|
| 461 |
+
continue
|
| 462 |
+
|
| 463 |
+
if getattr(module, "_repeated_blocks", None):
|
| 464 |
+
# Regional compile — only the repeated blocks. Mutates `module` in place.
|
| 465 |
+
module.compile_repeated_blocks(**compile_kwargs)
|
| 466 |
+
else:
|
| 467 |
+
# No regional metadata declared; fall back to compiling the whole module.
|
| 468 |
+
setattr(pipeline, attr, torch.compile(module, **compile_kwargs))
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def _load_lora(pipeline: Any, args: Namespace) -> None:
|
| 472 |
+
"""Attach one or more LoRA adapters. Each `--lora` value is a JSON dict.
|
| 473 |
+
|
| 474 |
+
Per-entry fields: `lora_id` (required), `lora_scale` (optional float, default 1.0), `adapter_name` (optional;
|
| 475 |
+
auto-generated as `lora_<i>` when stacking). Multiple `--lora` flags stack via a single `set_adapters(...)` call at
|
| 476 |
+
the end.
|
| 477 |
+
"""
|
| 478 |
+
if not args.lora:
|
| 479 |
+
return
|
| 480 |
+
specs = []
|
| 481 |
+
for raw in args.lora:
|
| 482 |
+
try:
|
| 483 |
+
parsed = json.loads(raw)
|
| 484 |
+
except json.JSONDecodeError as e:
|
| 485 |
+
raise SystemExit(f"--lora must be valid JSON: {e}") from e
|
| 486 |
+
if not isinstance(parsed, dict):
|
| 487 |
+
raise SystemExit(f"--lora must decode to a JSON object; got {type(parsed).__name__}.")
|
| 488 |
+
specs.append(parsed)
|
| 489 |
+
if not hasattr(pipeline, "load_lora_weights"):
|
| 490 |
+
raise SystemExit(f"{type(pipeline).__name__} does not support LoRA loading.")
|
| 491 |
+
|
| 492 |
+
names: list[str] = []
|
| 493 |
+
scales: list[float] = []
|
| 494 |
+
for i, spec in enumerate(specs):
|
| 495 |
+
lora_id = spec.get("lora_id")
|
| 496 |
+
if not lora_id:
|
| 497 |
+
raise SystemExit(f"--lora entry {i} is missing 'lora_id'.")
|
| 498 |
+
adapter_name = spec.get("adapter_name") or (f"lora_{i}" if len(specs) > 1 else "default")
|
| 499 |
+
pipeline.load_lora_weights(lora_id, adapter_name=adapter_name)
|
| 500 |
+
names.append(adapter_name)
|
| 501 |
+
scales.append(float(spec.get("lora_scale", 1.0)))
|
| 502 |
+
|
| 503 |
+
if hasattr(pipeline, "set_adapters"):
|
| 504 |
+
pipeline.set_adapters(names, adapter_weights=scales)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def _load_pipeline(args: Namespace) -> Any:
|
| 508 |
+
import diffusers
|
| 509 |
+
|
| 510 |
+
# Detect modular repos by trying the standard config; `ModularPipeline` repos ship
|
| 511 |
+
# `modular_model_index.json` instead of `model_index.json`, so `load_config` OSErrors.
|
| 512 |
+
try:
|
| 513 |
+
diffusers.DiffusionPipeline.load_config(args.model, token=args.token, revision=args.revision)
|
| 514 |
+
modular = False
|
| 515 |
+
except OSError:
|
| 516 |
+
modular = True
|
| 517 |
+
|
| 518 |
+
dtype = _resolve_dtype(args.dtype)
|
| 519 |
+
device_map = _resolve_device_map(args.device_map)
|
| 520 |
+
common_kwargs: dict[str, Any] = {
|
| 521 |
+
"trust_remote_code": args.trust_remote_code,
|
| 522 |
+
}
|
| 523 |
+
if dtype != "auto":
|
| 524 |
+
common_kwargs["torch_dtype"] = dtype
|
| 525 |
+
if args.variant:
|
| 526 |
+
common_kwargs["variant"] = args.variant
|
| 527 |
+
if args.token:
|
| 528 |
+
common_kwargs["token"] = args.token
|
| 529 |
+
# CPU offload sets up its own placement hooks, so leave weights on CPU at load time.
|
| 530 |
+
if not args.cpu_offload:
|
| 531 |
+
common_kwargs["device_map"] = device_map
|
| 532 |
+
|
| 533 |
+
if modular:
|
| 534 |
+
# ModularPipeline.from_pretrained fetches only the pipeline config; component
|
| 535 |
+
# weights come in via load_components(). `revision` scopes the config fetch,
|
| 536 |
+
# so it stays on from_pretrained — each ComponentSpec pins its own revision,
|
| 537 |
+
# and forwarding a global `revision` to load_components() would override those.
|
| 538 |
+
pipeline = diffusers.ModularPipeline.from_pretrained(
|
| 539 |
+
args.model,
|
| 540 |
+
trust_remote_code=args.trust_remote_code,
|
| 541 |
+
token=args.token,
|
| 542 |
+
revision=args.revision,
|
| 543 |
+
)
|
| 544 |
+
pipeline.load_components(**common_kwargs)
|
| 545 |
+
else:
|
| 546 |
+
pipeline = diffusers.DiffusionPipeline.from_pretrained(args.model, revision=args.revision, **common_kwargs)
|
| 547 |
+
|
| 548 |
+
_load_lora(pipeline, args)
|
| 549 |
+
if args.cpu_offload:
|
| 550 |
+
_apply_cpu_offload(pipeline, args.cpu_offload, device_map)
|
| 551 |
+
_apply_optimizations(pipeline, args)
|
| 552 |
+
|
| 553 |
+
return pipeline
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
# ---------------------------------------------------------------------------
|
| 557 |
+
# Pipeline call helpers
|
| 558 |
+
# ---------------------------------------------------------------------------
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def _parse_pipeline_kwargs(raw: str | None) -> dict[str, Any]:
|
| 562 |
+
if not raw:
|
| 563 |
+
return {}
|
| 564 |
+
try:
|
| 565 |
+
parsed = json.loads(raw)
|
| 566 |
+
except json.JSONDecodeError as e:
|
| 567 |
+
raise SystemExit(f"--pipeline-kwargs must be valid JSON: {e}") from e
|
| 568 |
+
if not isinstance(parsed, dict):
|
| 569 |
+
raise SystemExit("--pipeline-kwargs must decode to a JSON object.")
|
| 570 |
+
return parsed
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def _load_audio(url_or_path: str) -> tuple[Any, int]:
|
| 574 |
+
"""Load audio from a URL or local path via torchaudio. Returns `(waveform, sampling_rate)`."""
|
| 575 |
+
import torchaudio
|
| 576 |
+
|
| 577 |
+
if url_or_path.startswith(("http://", "https://")):
|
| 578 |
+
import io
|
| 579 |
+
|
| 580 |
+
import httpx
|
| 581 |
+
|
| 582 |
+
from ..utils.constants import DIFFUSERS_REQUEST_TIMEOUT
|
| 583 |
+
|
| 584 |
+
resp = httpx.get(url_or_path, follow_redirects=True, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 585 |
+
resp.raise_for_status()
|
| 586 |
+
return torchaudio.load(io.BytesIO(resp.content))
|
| 587 |
+
return torchaudio.load(url_or_path)
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
def _resolve_media_inputs(call_kwargs: dict[str, Any]) -> None:
|
| 591 |
+
"""Replace string paths/URLs at known media-input keys with loaded tensors.
|
| 592 |
+
|
| 593 |
+
Images resolve to `PIL.Image.Image` via `load_image`; videos to `list[PIL.Image.Image]` via `load_video`; audio to
|
| 594 |
+
a `torch.Tensor` via `_load_audio` (also auto-sets the paired sampling-rate kwarg for `initial_audio_waveforms` if
|
| 595 |
+
the user didn't supply it). A `list[str]` at any key is treated as a batch: each entry is loaded and the value
|
| 596 |
+
becomes a list of loaded objects. Non-string, non-list values pass through untouched.
|
| 597 |
+
"""
|
| 598 |
+
|
| 599 |
+
def _is_string_list(v: Any) -> bool:
|
| 600 |
+
return isinstance(v, list) and bool(v) and all(isinstance(x, str) for x in v)
|
| 601 |
+
|
| 602 |
+
for key in _IMAGE_INPUT_KEYS:
|
| 603 |
+
value = call_kwargs.get(key)
|
| 604 |
+
if isinstance(value, str):
|
| 605 |
+
call_kwargs[key] = load_image(value)
|
| 606 |
+
elif _is_string_list(value):
|
| 607 |
+
call_kwargs[key] = [load_image(v) for v in value]
|
| 608 |
+
for key in _VIDEO_INPUT_KEYS:
|
| 609 |
+
value = call_kwargs.get(key)
|
| 610 |
+
if isinstance(value, str):
|
| 611 |
+
call_kwargs[key] = load_video(value)
|
| 612 |
+
elif _is_string_list(value):
|
| 613 |
+
call_kwargs[key] = [load_video(v) for v in value]
|
| 614 |
+
for key in _AUDIO_INPUT_KEYS:
|
| 615 |
+
value = call_kwargs.get(key)
|
| 616 |
+
if isinstance(value, str):
|
| 617 |
+
waveform, sr = _load_audio(value)
|
| 618 |
+
call_kwargs[key] = waveform
|
| 619 |
+
if key == "initial_audio_waveforms" and "initial_audio_sampling_rate" not in call_kwargs:
|
| 620 |
+
call_kwargs["initial_audio_sampling_rate"] = sr
|
| 621 |
+
elif _is_string_list(value):
|
| 622 |
+
pairs = [_load_audio(v) for v in value]
|
| 623 |
+
call_kwargs[key] = [w for w, _ in pairs]
|
| 624 |
+
if key == "initial_audio_waveforms" and "initial_audio_sampling_rate" not in call_kwargs:
|
| 625 |
+
# All batched waveforms must share a sampling rate; use the first entry's.
|
| 626 |
+
call_kwargs["initial_audio_sampling_rate"] = pairs[0][1]
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
def _get_generator(seed: int | None, device: str):
|
| 630 |
+
if seed is None:
|
| 631 |
+
return None
|
| 632 |
+
import torch
|
| 633 |
+
|
| 634 |
+
generator_device = "cpu" if device == "mps" else device
|
| 635 |
+
return torch.Generator(device=generator_device).manual_seed(seed)
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
def _unwrap_pipeline_output(result: Any) -> Any:
|
| 639 |
+
"""Unwrap a pipeline-output object into the raw payload the saver can dispatch on."""
|
| 640 |
+
if hasattr(result, "images"):
|
| 641 |
+
return result.images
|
| 642 |
+
if hasattr(result, "frames"):
|
| 643 |
+
return result.frames[0]
|
| 644 |
+
if hasattr(result, "audios"):
|
| 645 |
+
return result.audios
|
| 646 |
+
return result
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
# ---------------------------------------------------------------------------
|
| 650 |
+
# Output saving (dispatch by type)
|
| 651 |
+
# ---------------------------------------------------------------------------
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
def _get_or_create_run_id() -> str:
|
| 655 |
+
"""Return the current run's id, creating one if not yet set.
|
| 656 |
+
|
| 657 |
+
Format: `diffusers-run-<YYYYMMDDTHHMMSS>-<6-char-uuid>`. Same id is reused as the local output subdirectory, the
|
| 658 |
+
remote bucket prefix, and the container-side `RUN_ID_ENV` so a run's artifacts are traceable end-to-end.
|
| 659 |
+
"""
|
| 660 |
+
import uuid
|
| 661 |
+
from datetime import datetime
|
| 662 |
+
|
| 663 |
+
existing = os.environ.get(RUN_ID_ENV)
|
| 664 |
+
if existing:
|
| 665 |
+
return existing
|
| 666 |
+
run_id = f"diffusers-run-{datetime.now().strftime('%Y%m%dT%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
| 667 |
+
os.environ[RUN_ID_ENV] = run_id
|
| 668 |
+
return run_id
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
def _resolve_output_paths(task: str, num: int, explicit: str | None, ext: str) -> list[Path]:
|
| 672 |
+
if explicit is None:
|
| 673 |
+
base = Path(DEFAULT_OUTPUT_DIR) / _get_or_create_run_id()
|
| 674 |
+
base.mkdir(parents=True, exist_ok=True)
|
| 675 |
+
return [base / f"{i:04d}.{ext}" for i in range(num)]
|
| 676 |
+
|
| 677 |
+
p = Path(explicit)
|
| 678 |
+
if explicit.endswith(os.sep) or p.is_dir():
|
| 679 |
+
p.mkdir(parents=True, exist_ok=True)
|
| 680 |
+
return [p / f"{i:04d}.{ext}" for i in range(num)]
|
| 681 |
+
|
| 682 |
+
p.parent.mkdir(parents=True, exist_ok=True)
|
| 683 |
+
if num == 1:
|
| 684 |
+
return [p]
|
| 685 |
+
stem, suffix = p.stem, p.suffix or f".{ext}"
|
| 686 |
+
return [p.with_name(f"{stem}-{i:04d}{suffix}") for i in range(num)]
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
def _as_pil_list(value: Any):
|
| 690 |
+
try:
|
| 691 |
+
from PIL.Image import Image as PILImage
|
| 692 |
+
except ImportError:
|
| 693 |
+
return None
|
| 694 |
+
if isinstance(value, PILImage):
|
| 695 |
+
return [value]
|
| 696 |
+
if isinstance(value, (list, tuple)) and value and all(isinstance(v, PILImage) for v in value):
|
| 697 |
+
return list(value)
|
| 698 |
+
return None
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
def _as_frame_sequence(value: Any):
|
| 702 |
+
try:
|
| 703 |
+
from PIL.Image import Image as PILImage
|
| 704 |
+
except ImportError:
|
| 705 |
+
PILImage = None # type: ignore[assignment]
|
| 706 |
+
|
| 707 |
+
if isinstance(value, (list, tuple)) and len(value) >= 2:
|
| 708 |
+
first = value[0]
|
| 709 |
+
if PILImage is not None and isinstance(first, PILImage):
|
| 710 |
+
return list(value)
|
| 711 |
+
try:
|
| 712 |
+
import numpy as np
|
| 713 |
+
|
| 714 |
+
if isinstance(first, np.ndarray):
|
| 715 |
+
return list(value)
|
| 716 |
+
except ImportError:
|
| 717 |
+
pass
|
| 718 |
+
return None
|
| 719 |
+
|
| 720 |
+
|
| 721 |
+
def _as_audio_arrays(value: Any):
|
| 722 |
+
try:
|
| 723 |
+
import numpy as np
|
| 724 |
+
except ImportError:
|
| 725 |
+
return None
|
| 726 |
+
if isinstance(value, np.ndarray) and value.ndim <= 2:
|
| 727 |
+
return [value]
|
| 728 |
+
if isinstance(value, (list, tuple)) and value and all(isinstance(v, np.ndarray) for v in value):
|
| 729 |
+
return list(value)
|
| 730 |
+
return None
|
| 731 |
+
|
| 732 |
+
|
| 733 |
+
def _save_audio_arrays(audios, sampling_rate: int, args: Namespace, task: str) -> list[str]:
|
| 734 |
+
"""Write each numpy audio array to a 16-bit PCM WAV at `sampling_rate` Hz.
|
| 735 |
+
|
| 736 |
+
Uses the stdlib `wave` module so no scipy dependency is required.
|
| 737 |
+
"""
|
| 738 |
+
import wave
|
| 739 |
+
|
| 740 |
+
import numpy as np
|
| 741 |
+
|
| 742 |
+
paths = _resolve_output_paths(task, len(audios), args.output, ext="wav")
|
| 743 |
+
saved: list[str] = []
|
| 744 |
+
for audio, path in zip(audios, paths):
|
| 745 |
+
data = np.asarray(audio)
|
| 746 |
+
if data.dtype.kind == "f":
|
| 747 |
+
data = (np.clip(data, -1.0, 1.0) * 32767).astype(np.int16)
|
| 748 |
+
else:
|
| 749 |
+
data = data.astype(np.int16)
|
| 750 |
+
if data.ndim == 1:
|
| 751 |
+
n_channels = 1
|
| 752 |
+
else:
|
| 753 |
+
# Heuristic: shorter axis is channels (interleaved layout for `wave` is
|
| 754 |
+
# samples × channels, so transpose if needed).
|
| 755 |
+
if data.shape[0] < data.shape[-1]:
|
| 756 |
+
data = data.T
|
| 757 |
+
n_channels = data.shape[1]
|
| 758 |
+
with wave.open(str(path), "wb") as w:
|
| 759 |
+
w.setnchannels(n_channels)
|
| 760 |
+
w.setsampwidth(2) # 16-bit PCM
|
| 761 |
+
w.setframerate(sampling_rate)
|
| 762 |
+
w.writeframes(data.tobytes())
|
| 763 |
+
saved.append(str(path))
|
| 764 |
+
return saved
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
def _save_output(value: Any, args: Namespace, task: str) -> list[str]:
|
| 768 |
+
"""Save `value` by dispatching on its runtime type."""
|
| 769 |
+
pil_images = _as_pil_list(value)
|
| 770 |
+
if pil_images is not None:
|
| 771 |
+
paths = _resolve_output_paths(task, len(pil_images), args.output, ext="png")
|
| 772 |
+
for img, path in zip(pil_images, paths):
|
| 773 |
+
img.save(path)
|
| 774 |
+
return [str(p) for p in paths]
|
| 775 |
+
|
| 776 |
+
frames = _as_frame_sequence(value)
|
| 777 |
+
if frames is not None:
|
| 778 |
+
from diffusers.utils import export_to_video
|
| 779 |
+
|
| 780 |
+
path = _resolve_output_paths(task, 1, args.output, ext="mp4")[0]
|
| 781 |
+
export_to_video(frames, str(path), fps=args.fps)
|
| 782 |
+
return [str(path)]
|
| 783 |
+
|
| 784 |
+
audios = _as_audio_arrays(value)
|
| 785 |
+
if audios is not None:
|
| 786 |
+
return _save_audio_arrays(audios, args.sampling_rate or 16000, args, task)
|
| 787 |
+
|
| 788 |
+
path = _resolve_output_paths(task, 1, args.output, ext="json")[0]
|
| 789 |
+
Path(path).write_text(json.dumps(value, default=str, indent=2))
|
| 790 |
+
return [str(path)]
|
| 791 |
+
|
| 792 |
+
|
| 793 |
+
# ---------------------------------------------------------------------------
|
| 794 |
+
# Hub bucket upload (--push-to)
|
| 795 |
+
# ---------------------------------------------------------------------------
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def _parse_push_to(spec: str) -> tuple[str, str]:
|
| 799 |
+
"""Split `--push-to` into a bucket id and an optional subpath prefix.
|
| 800 |
+
|
| 801 |
+
Accepts an HF bucket id (`<namespace>/<name>[/<subpath>]`), a canonical
|
| 802 |
+
`hf://buckets/<namespace>/<name>[/<subpath>]` URI, or a Hub web URL for the same. Non-bucket URIs (models,
|
| 803 |
+
datasets, spaces) are rejected — `--push-to` targets storage buckets only.
|
| 804 |
+
"""
|
| 805 |
+
from huggingface_hub import parse_hf_uri
|
| 806 |
+
|
| 807 |
+
# Bare shorthand → canonical URI so a single parser handles every accepted form.
|
| 808 |
+
if not spec.startswith(("hf://", "http://", "https://")):
|
| 809 |
+
spec = f"hf://buckets/{spec.strip('/')}"
|
| 810 |
+
uri = parse_hf_uri(spec)
|
| 811 |
+
if not uri.is_bucket:
|
| 812 |
+
raise SystemExit(f"--push-to must point at a bucket; got {uri.type!r} URI {spec!r}.")
|
| 813 |
+
return uri.id, uri.path_in_repo
|
| 814 |
+
|
| 815 |
+
|
| 816 |
+
def _push_outputs(args: Namespace, saved_paths: list[str], task: str) -> dict[str, Any] | None:
|
| 817 |
+
"""Upload `saved_paths` to the `--push-to` bucket. Returns a summary or None."""
|
| 818 |
+
if not args.push_to:
|
| 819 |
+
return None
|
| 820 |
+
|
| 821 |
+
from huggingface_hub import HfApi
|
| 822 |
+
|
| 823 |
+
bucket_id, subpath = _parse_push_to(args.push_to)
|
| 824 |
+
api = HfApi(token=args.token)
|
| 825 |
+
api.create_bucket(bucket_id, exist_ok=True)
|
| 826 |
+
|
| 827 |
+
run_id = _get_or_create_run_id()
|
| 828 |
+
prefix = f"{subpath}/{run_id}" if subpath else run_id
|
| 829 |
+
add = [(local, f"{prefix}/{Path(local).name}") for local in saved_paths]
|
| 830 |
+
api.batch_bucket_files(bucket_id, add=add)
|
| 831 |
+
|
| 832 |
+
uploaded = [f"hf://buckets/{bucket_id}/{dest}" for _, dest in add]
|
| 833 |
+
return {"bucket_id": bucket_id, "uploaded": uploaded}
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
# ---------------------------------------------------------------------------
|
| 837 |
+
# Remote execution (HF Sandbox)
|
| 838 |
+
# ---------------------------------------------------------------------------
|
| 839 |
+
|
| 840 |
+
|
| 841 |
+
def _build_task_kwargs(args: Namespace) -> dict[str, Any]:
|
| 842 |
+
"""Pick out the kwargs the sandbox CLI should invoke the task with."""
|
| 843 |
+
out: dict[str, Any] = {}
|
| 844 |
+
for key, value in vars(args).items():
|
| 845 |
+
if key in REMOTE_KEYS or value is None or value is False:
|
| 846 |
+
continue
|
| 847 |
+
out[key] = value
|
| 848 |
+
return out
|
| 849 |
+
|
| 850 |
+
|
| 851 |
+
def _kwargs_to_argv(task: str, task_kwargs: dict[str, Any]) -> list[str]:
|
| 852 |
+
"""Render `task_kwargs` as the argv list the sandbox CLI's argparse will see."""
|
| 853 |
+
argv: list[str] = [task]
|
| 854 |
+
for key, value in task_kwargs.items():
|
| 855 |
+
flag = "--" + key.replace("_", "-")
|
| 856 |
+
if value is True:
|
| 857 |
+
argv.append(flag)
|
| 858 |
+
elif isinstance(value, list):
|
| 859 |
+
for item in value:
|
| 860 |
+
argv.extend([flag, str(item)])
|
| 861 |
+
else:
|
| 862 |
+
argv.extend([flag, str(value)])
|
| 863 |
+
return argv
|
| 864 |
+
|
| 865 |
+
|
| 866 |
+
def _duration_to_seconds(value: str) -> float:
|
| 867 |
+
"""Parse a duration like `30s`, `10m`, `2h` (or a bare number of seconds) into seconds."""
|
| 868 |
+
value = value.strip()
|
| 869 |
+
units = {"s": 1, "m": 60, "h": 3600}
|
| 870 |
+
if value and value[-1] in units:
|
| 871 |
+
return float(value[:-1]) * units[value[-1]]
|
| 872 |
+
return float(value)
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
def _upload_inputs_to_sandbox(args: Namespace, sbx: Any, run_id: str) -> None:
|
| 876 |
+
"""Upload local media paths in `--pipeline-kwargs` into the sandbox and rewrite the JSON in place.
|
| 877 |
+
|
| 878 |
+
Walks known image/video/audio-input keys; any string value that resolves to a local file is uploaded to
|
| 879 |
+
`<_SANDBOX_INPUTS_DIR>/<run_id>/<key>_<basename>` and the JSON path is rewritten to that in-sandbox path. URLs,
|
| 880 |
+
`hf://` URIs, and non-existent paths pass through untouched.
|
| 881 |
+
"""
|
| 882 |
+
if not args.pipeline_kwargs:
|
| 883 |
+
return
|
| 884 |
+
try:
|
| 885 |
+
parsed = json.loads(args.pipeline_kwargs)
|
| 886 |
+
except json.JSONDecodeError:
|
| 887 |
+
return # the sandbox CLI will fail loudly with a parse error later
|
| 888 |
+
if not isinstance(parsed, dict):
|
| 889 |
+
return
|
| 890 |
+
|
| 891 |
+
def _upload_one(key: str, index: int | None, local_str: str) -> str:
|
| 892 |
+
# `index` is None for scalar entries, an int for list entries (used to disambiguate names).
|
| 893 |
+
local = Path(local_str)
|
| 894 |
+
suffix = f"_{index}" if index is not None else ""
|
| 895 |
+
remote_path = f"{_SANDBOX_INPUTS_DIR}/{run_id}/{key}{suffix}_{local.name}"
|
| 896 |
+
sbx.files.upload(str(local), remote_path)
|
| 897 |
+
return remote_path
|
| 898 |
+
|
| 899 |
+
uploaded = 0
|
| 900 |
+
for key in (*_IMAGE_INPUT_KEYS, *_VIDEO_INPUT_KEYS, *_AUDIO_INPUT_KEYS):
|
| 901 |
+
value = parsed.get(key)
|
| 902 |
+
if isinstance(value, str) and Path(value).is_file():
|
| 903 |
+
parsed[key] = _upload_one(key, None, value)
|
| 904 |
+
uploaded += 1
|
| 905 |
+
elif isinstance(value, list):
|
| 906 |
+
# Batched inputs: upload each local path, leave URLs/hf:// URIs alone.
|
| 907 |
+
new_list = list(value)
|
| 908 |
+
for i, entry in enumerate(value):
|
| 909 |
+
if isinstance(entry, str) and Path(entry).is_file():
|
| 910 |
+
new_list[i] = _upload_one(key, i, entry)
|
| 911 |
+
uploaded += 1
|
| 912 |
+
parsed[key] = new_list
|
| 913 |
+
|
| 914 |
+
if uploaded:
|
| 915 |
+
logger.info(f"uploaded {uploaded} local input file(s) to the sandbox")
|
| 916 |
+
args.pipeline_kwargs = json.dumps(parsed)
|
| 917 |
+
|
| 918 |
+
|
| 919 |
+
def _download_outputs_from_sandbox(sbx: Any, sandbox_dir: str, local_dir: Path) -> list[str]:
|
| 920 |
+
"""Download every file the sandbox CLI wrote under `sandbox_dir` into `local_dir`."""
|
| 921 |
+
local_dir.mkdir(parents=True, exist_ok=True)
|
| 922 |
+
saved: list[str] = []
|
| 923 |
+
for entry in sbx.files.list(sandbox_dir):
|
| 924 |
+
if entry.type != "file":
|
| 925 |
+
continue
|
| 926 |
+
target = local_dir / Path(entry.path).name
|
| 927 |
+
sbx.files.download(entry.path, str(target))
|
| 928 |
+
saved.append(str(target))
|
| 929 |
+
return saved
|
| 930 |
+
|
| 931 |
+
|
| 932 |
+
def _maybe_submit_remote(args: Namespace, task: str) -> bool:
|
| 933 |
+
"""If `--remote` was set, run this invocation inside an HF Sandbox and return True."""
|
| 934 |
+
if not args.remote:
|
| 935 |
+
return False
|
| 936 |
+
|
| 937 |
+
import shlex
|
| 938 |
+
import time
|
| 939 |
+
|
| 940 |
+
from huggingface_hub import get_token
|
| 941 |
+
from huggingface_hub.utils import send_telemetry
|
| 942 |
+
|
| 943 |
+
import diffusers
|
| 944 |
+
|
| 945 |
+
try:
|
| 946 |
+
from huggingface_hub import Sandbox
|
| 947 |
+
except ImportError:
|
| 948 |
+
raise SystemExit(
|
| 949 |
+
"--remote requires huggingface_hub>=1.23 for HF Sandbox support. "
|
| 950 |
+
"Upgrade with `pip install -U huggingface_hub`."
|
| 951 |
+
)
|
| 952 |
+
|
| 953 |
+
if Path(args.model).exists():
|
| 954 |
+
raise SystemExit(
|
| 955 |
+
f"--model {args.model!r} is a local path; the sandbox can't see it. "
|
| 956 |
+
"Pass a Hub repo id so the sandbox can download it."
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
hf_token = args.token or get_token()
|
| 960 |
+
run_id = _get_or_create_run_id()
|
| 961 |
+
|
| 962 |
+
# An explicit --push-to means the bucket is the user's destination, so skip the local
|
| 963 |
+
# download unless they also asked for a local path via --output.
|
| 964 |
+
user_bucket = bool(args.push_to)
|
| 965 |
+
download_locally = (not user_bucket) or (args.output is not None)
|
| 966 |
+
local_dir = Path(args.output) if args.output else Path(DEFAULT_OUTPUT_DIR) / run_id
|
| 967 |
+
|
| 968 |
+
use_existing_sandbox = bool(args.sandbox_id)
|
| 969 |
+
keep_alive = args.keep_alive or use_existing_sandbox
|
| 970 |
+
if use_existing_sandbox and args.volume:
|
| 971 |
+
logger.warning(
|
| 972 |
+
"--volume is ignored when reconnecting to an existing sandbox (mounts are set at creation time)."
|
| 973 |
+
)
|
| 974 |
+
if use_existing_sandbox:
|
| 975 |
+
logger.info(f"reconnecting to sandbox {args.sandbox_id!r}...")
|
| 976 |
+
sbx = Sandbox.connect(args.sandbox_id, token=hf_token)
|
| 977 |
+
else:
|
| 978 |
+
logger.info(f"creating sandbox on flavor={args.flavor!r}...")
|
| 979 |
+
create_kwargs: dict[str, Any] = {
|
| 980 |
+
"image": args.image or _DEFAULT_REMOTE_IMAGE,
|
| 981 |
+
"flavor": args.flavor,
|
| 982 |
+
"forward_hf_token": True,
|
| 983 |
+
"token": hf_token,
|
| 984 |
+
"env": {
|
| 985 |
+
"HF_ENABLE_PARALLEL_LOADING": "1",
|
| 986 |
+
"DIFFUSERS_VERBOSITY": os.environ.get("DIFFUSERS_VERBOSITY", "info"),
|
| 987 |
+
},
|
| 988 |
+
"idle_timeout": args.idle_timeout,
|
| 989 |
+
}
|
| 990 |
+
if args.volume:
|
| 991 |
+
from huggingface_hub import Volume
|
| 992 |
+
|
| 993 |
+
volumes = []
|
| 994 |
+
for spec in args.volume:
|
| 995 |
+
bucket_id, sep, mount_path = spec.partition(":")
|
| 996 |
+
if not sep:
|
| 997 |
+
mount_path = f"/mnt/buckets/{bucket_id}"
|
| 998 |
+
if bucket_id.count("/") != 1:
|
| 999 |
+
raise SystemExit(f"--volume: bucket id must be <namespace>/<name>, got {bucket_id!r}")
|
| 1000 |
+
if not mount_path.startswith("/"):
|
| 1001 |
+
raise SystemExit(f"--volume: mount path must be absolute, got {mount_path!r}")
|
| 1002 |
+
volumes.append(Volume(type="bucket", source=bucket_id, mount_path=mount_path))
|
| 1003 |
+
create_kwargs["volumes"] = volumes
|
| 1004 |
+
if args.namespace is not None:
|
| 1005 |
+
create_kwargs["namespace"] = args.namespace
|
| 1006 |
+
sbx = Sandbox.create(**create_kwargs)
|
| 1007 |
+
|
| 1008 |
+
def _stream(chunk: str) -> None:
|
| 1009 |
+
sys.stderr.write(chunk)
|
| 1010 |
+
sys.stderr.flush()
|
| 1011 |
+
|
| 1012 |
+
exit_code = 0
|
| 1013 |
+
saved: list[str] = []
|
| 1014 |
+
run_seconds = 0.0
|
| 1015 |
+
try:
|
| 1016 |
+
_upload_inputs_to_sandbox(args, sbx, run_id)
|
| 1017 |
+
|
| 1018 |
+
dependencies = list(_DEFAULT_REMOTE_DEPS)
|
| 1019 |
+
if args.dependencies:
|
| 1020 |
+
dependencies.extend(args.dependencies)
|
| 1021 |
+
# --break-system-packages bypasses PEP 668; harmless in a throwaway sandbox. uv is a
|
| 1022 |
+
# near no-op when the deps are already satisfied, so this stays cheap on a reused sandbox.
|
| 1023 |
+
install_cmd = shlex.join(["uv", "pip", "install", "--system", "--break-system-packages", *dependencies])
|
| 1024 |
+
logger.info("installing dependencies in the sandbox...")
|
| 1025 |
+
sbx.run(install_cmd, on_stdout=_stream, on_stderr=_stream)
|
| 1026 |
+
|
| 1027 |
+
# Per-run outputs subdirectory so a reused sandbox doesn't leak files from prior runs
|
| 1028 |
+
# into this run's download set.
|
| 1029 |
+
sandbox_output_dir = f"{_SANDBOX_OUTPUTS_DIR}/{run_id}"
|
| 1030 |
+
task_kwargs = _build_task_kwargs(args)
|
| 1031 |
+
task_kwargs["output"] = sandbox_output_dir + "/"
|
| 1032 |
+
cli_argv = _kwargs_to_argv(task, task_kwargs)
|
| 1033 |
+
# Suppress the container CLI's own `out.result(...)` payload — the outer wrapper owns the
|
| 1034 |
+
# final structured output for --remote runs.
|
| 1035 |
+
format_argv = ["--format", "quiet"]
|
| 1036 |
+
# torchrun wraps the CLI for --context-parallel so torch.distributed initializes across
|
| 1037 |
+
# every visible GPU before the run command starts.
|
| 1038 |
+
if args.context_parallel:
|
| 1039 |
+
cli_argv = [
|
| 1040 |
+
"torchrun",
|
| 1041 |
+
"--nproc-per-node=gpu",
|
| 1042 |
+
"-m",
|
| 1043 |
+
"diffusers.commands.diffusers_cli",
|
| 1044 |
+
*format_argv,
|
| 1045 |
+
*cli_argv,
|
| 1046 |
+
]
|
| 1047 |
+
else:
|
| 1048 |
+
cli_argv = [_CONTAINER_CLI_BINARY, *format_argv, *cli_argv]
|
| 1049 |
+
|
| 1050 |
+
started = time.perf_counter()
|
| 1051 |
+
# Per-invocation env: RUN_ID_ENV must be fresh each run. Sandbox.create-time env is
|
| 1052 |
+
# baked in and would go stale on reused sandboxes, silently reusing the initial run's
|
| 1053 |
+
# bucket prefix in `_push_outputs`.
|
| 1054 |
+
result = sbx.run(
|
| 1055 |
+
cli_argv,
|
| 1056 |
+
env={RUN_ID_ENV: run_id},
|
| 1057 |
+
on_stdout=_stream,
|
| 1058 |
+
on_stderr=_stream,
|
| 1059 |
+
timeout=_duration_to_seconds(args.timeout),
|
| 1060 |
+
check=False,
|
| 1061 |
+
)
|
| 1062 |
+
run_seconds = time.perf_counter() - started
|
| 1063 |
+
exit_code = result.exit_code
|
| 1064 |
+
|
| 1065 |
+
if exit_code == 0 and download_locally:
|
| 1066 |
+
saved = _download_outputs_from_sandbox(sbx, sandbox_output_dir, local_dir)
|
| 1067 |
+
finally:
|
| 1068 |
+
if keep_alive:
|
| 1069 |
+
logger.info(
|
| 1070 |
+
f"sandbox {sbx.id} kept alive — reconnect with "
|
| 1071 |
+
f"`--remote --sandbox-id {sbx.id}`, stop with `hf sandbox kill {sbx.id}`."
|
| 1072 |
+
)
|
| 1073 |
+
else:
|
| 1074 |
+
sbx.kill()
|
| 1075 |
+
|
| 1076 |
+
send_telemetry(
|
| 1077 |
+
topic="diffusers/cli/run/remote",
|
| 1078 |
+
library_name="diffusers",
|
| 1079 |
+
library_version=diffusers.__version__,
|
| 1080 |
+
)
|
| 1081 |
+
|
| 1082 |
+
payload: dict[str, Any] = {
|
| 1083 |
+
"exit_code": exit_code,
|
| 1084 |
+
"run_seconds": round(run_seconds, 1),
|
| 1085 |
+
}
|
| 1086 |
+
if keep_alive:
|
| 1087 |
+
payload["sandbox_id"] = sbx.id
|
| 1088 |
+
if download_locally:
|
| 1089 |
+
payload["outputs"] = saved
|
| 1090 |
+
if args.push_to:
|
| 1091 |
+
bucket_id, subpath = _parse_push_to(args.push_to)
|
| 1092 |
+
prefix = f"{subpath}/{run_id}" if subpath else run_id
|
| 1093 |
+
payload["pushed-to"] = f"hf://buckets/{bucket_id}/{prefix}/"
|
| 1094 |
+
out.result("remote-run", **payload)
|
| 1095 |
+
|
| 1096 |
+
if exit_code != 0:
|
| 1097 |
+
raise SystemExit(f"remote run failed with exit code {exit_code}")
|
| 1098 |
+
return True
|
| 1099 |
+
|
| 1100 |
+
|
| 1101 |
+
# ---------------------------------------------------------------------------
|
| 1102 |
+
# Subcommand
|
| 1103 |
+
# ---------------------------------------------------------------------------
|
| 1104 |
+
|
| 1105 |
+
|
| 1106 |
+
class RunCommand(BaseDiffusersCLICommand):
|
| 1107 |
+
task = "run"
|
| 1108 |
+
|
| 1109 |
+
@staticmethod
|
| 1110 |
+
def register_subcommand(subparsers: _SubParsersAction) -> None:
|
| 1111 |
+
from argparse import RawDescriptionHelpFormatter
|
| 1112 |
+
|
| 1113 |
+
epilog = (
|
| 1114 |
+
"Examples\n"
|
| 1115 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1116 |
+
' --pipeline-kwargs \'{"prompt": "a cat on the moon"}\'\n'
|
| 1117 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1118 |
+
' --pipeline-kwargs \'{"prompt": "make the fur grey", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png"}\'\n'
|
| 1119 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1120 |
+
' --pipeline-kwargs \'{"prompt": "a tiny cat"}\' \\\n'
|
| 1121 |
+
' --lora \'{"lora_id": "alvdansen/littletinies", "lora_scale": 0.8}\'\n'
|
| 1122 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
|
| 1123 |
+
' --pipeline-kwargs \'{"prompt": "a cat"}\' --remote --flavor a100-large\n'
|
| 1124 |
+
" $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 --context-parallel \\\n"
|
| 1125 |
+
' --pipeline-kwargs \'{"prompt": "a cat"}\' --remote --flavor 4xa100-large\n'
|
| 1126 |
+
"\n"
|
| 1127 |
+
"Learn more\n"
|
| 1128 |
+
" Use `diffusers-cli <command> --help` for more information about a command.\n"
|
| 1129 |
+
" Read the documentation at https://huggingface.co/docs/diffusers\n"
|
| 1130 |
+
)
|
| 1131 |
+
|
| 1132 |
+
parser: ArgumentParser = subparsers.add_parser(
|
| 1133 |
+
"run",
|
| 1134 |
+
help="Run any diffusers pipeline locally or remotely in an HF Sandbox.",
|
| 1135 |
+
usage="\n diffusers-cli run [options]",
|
| 1136 |
+
epilog=epilog,
|
| 1137 |
+
formatter_class=RawDescriptionHelpFormatter,
|
| 1138 |
+
)
|
| 1139 |
+
parser._optionals.title = "Options"
|
| 1140 |
+
_add_loading_arguments(parser)
|
| 1141 |
+
_add_optimization_arguments(parser)
|
| 1142 |
+
parser.add_argument(
|
| 1143 |
+
"--pipeline-kwargs",
|
| 1144 |
+
default=None,
|
| 1145 |
+
help=(
|
| 1146 |
+
"JSON object of kwargs passed to the pipeline call. String values at known "
|
| 1147 |
+
f"image-input keys ({', '.join(_IMAGE_INPUT_KEYS)}) are auto-loaded as PIL images; "
|
| 1148 |
+
f"video-input keys ({', '.join(_VIDEO_INPUT_KEYS)}) are auto-loaded as frame lists; "
|
| 1149 |
+
f"audio-input keys ({', '.join(_AUDIO_INPUT_KEYS)}) are auto-loaded via torchaudio."
|
| 1150 |
+
),
|
| 1151 |
+
)
|
| 1152 |
+
parser.add_argument(
|
| 1153 |
+
"--output-key",
|
| 1154 |
+
default=None,
|
| 1155 |
+
help="For modular pipelines: name of the intermediate to extract (passed as `output=` to the call).",
|
| 1156 |
+
)
|
| 1157 |
+
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility.")
|
| 1158 |
+
parser.add_argument(
|
| 1159 |
+
"--fps",
|
| 1160 |
+
type=int,
|
| 1161 |
+
default=8,
|
| 1162 |
+
help="FPS used when the output happens to be a frame sequence.",
|
| 1163 |
+
)
|
| 1164 |
+
parser.add_argument(
|
| 1165 |
+
"--sampling-rate",
|
| 1166 |
+
type=int,
|
| 1167 |
+
default=None,
|
| 1168 |
+
help="Sample rate used when the output happens to be an audio array.",
|
| 1169 |
+
)
|
| 1170 |
+
_add_remote_arguments(parser)
|
| 1171 |
+
_add_output_arguments(parser)
|
| 1172 |
+
parser.set_defaults(func=RunCommand)
|
| 1173 |
+
|
| 1174 |
+
def __init__(self, args: Namespace):
|
| 1175 |
+
self.args = args
|
| 1176 |
+
|
| 1177 |
+
def run(self) -> None:
|
| 1178 |
+
import diffusers
|
| 1179 |
+
|
| 1180 |
+
_get_or_create_run_id() # populate RUN_ID_ENV so local output dir + remote bucket prefix agree
|
| 1181 |
+
|
| 1182 |
+
call_kwargs = _parse_pipeline_kwargs(self.args.pipeline_kwargs)
|
| 1183 |
+
|
| 1184 |
+
if _maybe_submit_remote(self.args, self.task):
|
| 1185 |
+
return
|
| 1186 |
+
|
| 1187 |
+
# Resolve media before loading pipeline weights so dead URLs / missing files fail
|
| 1188 |
+
# fast — cheap to fetch, expensive to load a 20GB model just to hit a 404.
|
| 1189 |
+
_resolve_media_inputs(call_kwargs)
|
| 1190 |
+
pipeline = _load_pipeline(self.args)
|
| 1191 |
+
is_modular = isinstance(pipeline, diffusers.ModularPipeline)
|
| 1192 |
+
|
| 1193 |
+
if self.args.output_key is not None:
|
| 1194 |
+
call_kwargs["output"] = self.args.output_key
|
| 1195 |
+
|
| 1196 |
+
device = pipeline.device.type if hasattr(pipeline, "device") else "cpu"
|
| 1197 |
+
generator = _get_generator(self.args.seed, device)
|
| 1198 |
+
if generator is not None:
|
| 1199 |
+
call_kwargs["generator"] = generator
|
| 1200 |
+
|
| 1201 |
+
try:
|
| 1202 |
+
result = pipeline(**call_kwargs)
|
| 1203 |
+
|
| 1204 |
+
# Under torchrun, ranks > 0 produce identical output to rank 0 (CP shards the
|
| 1205 |
+
# transformer compute but ranks reduce to the same final tensors). Save/push/print
|
| 1206 |
+
# from rank 0 only to avoid clobbering bucket files 4x and printing 4x.
|
| 1207 |
+
if os.environ.get("RANK", "0") == "0":
|
| 1208 |
+
savable = result if is_modular else _unwrap_pipeline_output(result)
|
| 1209 |
+
saved = _save_output(savable, self.args, self.task)
|
| 1210 |
+
pushed = _push_outputs(self.args, saved, self.task)
|
| 1211 |
+
|
| 1212 |
+
out.result(
|
| 1213 |
+
self.task,
|
| 1214 |
+
model=self.args.model,
|
| 1215 |
+
device=device,
|
| 1216 |
+
pipeline_class=type(pipeline).__name__,
|
| 1217 |
+
modular=is_modular,
|
| 1218 |
+
outputs=saved,
|
| 1219 |
+
pushed=pushed,
|
| 1220 |
+
seed=self.args.seed,
|
| 1221 |
+
output_key=self.args.output_key,
|
| 1222 |
+
)
|
| 1223 |
+
finally:
|
| 1224 |
+
import torch
|
| 1225 |
+
|
| 1226 |
+
if torch.distributed.is_available() and torch.distributed.is_initialized():
|
| 1227 |
+
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,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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). Installs to `.agents/skills/<name>/` which Claude, Codex, and Cursor
|
| 18 |
+
all discover.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import os
|
| 24 |
+
import shutil
|
| 25 |
+
from argparse import ArgumentParser, Namespace, _SubParsersAction
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
import httpx
|
| 29 |
+
from huggingface_hub.cli._output import out
|
| 30 |
+
|
| 31 |
+
from ..utils import logging
|
| 32 |
+
from ..utils.constants import DIFFUSERS_REQUEST_TIMEOUT
|
| 33 |
+
from . import BaseDiffusersCLICommand
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
logger = logging.get_logger("diffusers-cli/skills")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
_REGISTRY_BASE = "https://api.github.com/repos/huggingface/diffusers/contents/.ai/skills"
|
| 40 |
+
_REGISTRY_REF = "main"
|
| 41 |
+
|
| 42 |
+
# Native skill-discovery paths per agent. Claude Code reads only `.claude/skills/`; Codex and
|
| 43 |
+
# Cursor read `.agents/skills/` (Cursor also honors `.claude/skills/` via compat, but installing
|
| 44 |
+
# to `.agents/skills/` is the portable choice for both).
|
| 45 |
+
_CLAUDE_SKILLS_DIR = Path(".claude") / "skills"
|
| 46 |
+
_AGENTS_SKILLS_DIR = Path(".agents") / "skills"
|
| 47 |
+
|
| 48 |
+
# Env vars set by each agent when it launches the CLI. Values are the install path to use.
|
| 49 |
+
_AGENT_ENV_TO_DIR: dict[str, Path] = {
|
| 50 |
+
"CLAUDECODE": _CLAUDE_SKILLS_DIR,
|
| 51 |
+
"CLAUDE_CODE": _CLAUDE_SKILLS_DIR,
|
| 52 |
+
"CODEX_SANDBOX": _AGENTS_SKILLS_DIR,
|
| 53 |
+
"CURSOR_AI": _AGENTS_SKILLS_DIR,
|
| 54 |
+
}
|
| 55 |
+
# When no agent env var is set, install to every native path so whichever agent the user
|
| 56 |
+
# later switches to picks the skill up.
|
| 57 |
+
_ALL_INSTALL_DIRS: tuple[Path, ...] = (_CLAUDE_SKILLS_DIR, _AGENTS_SKILLS_DIR)
|
| 58 |
+
|
| 59 |
+
# Empty marker dropped inside each installed skill dir so `update` can distinguish our
|
| 60 |
+
# installs from user-placed skills at the same paths.
|
| 61 |
+
_MANAGED_MARKER_FILE = ".diffusers-skill-managed"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
# Registry fetch
|
| 66 |
+
# ---------------------------------------------------------------------------
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _registry_url(name: str = "") -> str:
|
| 70 |
+
"""API URL for the registry root, or for a single skill bundle when `name` is given."""
|
| 71 |
+
path = f"/{name}" if name else ""
|
| 72 |
+
return f"{_REGISTRY_BASE}{path}?ref={_REGISTRY_REF}"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _fetch_json(url: str) -> list[dict]:
|
| 76 |
+
try:
|
| 77 |
+
resp = httpx.get(url, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 78 |
+
resp.raise_for_status()
|
| 79 |
+
return resp.json()
|
| 80 |
+
except httpx.HTTPStatusError as e:
|
| 81 |
+
if e.response.status_code == 404:
|
| 82 |
+
raise SystemExit(f"Not found in registry: {url}") from e
|
| 83 |
+
raise SystemExit(f"Registry fetch failed: HTTP {e.response.status_code} {e.response.reason_phrase}") from e
|
| 84 |
+
except httpx.HTTPError as e:
|
| 85 |
+
raise SystemExit(f"Could not reach registry: {e}") from e
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _walk_skill_files(name: str) -> list[tuple[str, str]]:
|
| 89 |
+
files: list[tuple[str, str]] = []
|
| 90 |
+
|
| 91 |
+
def _walk(api_url: str, prefix: str) -> None:
|
| 92 |
+
for entry in _fetch_json(api_url):
|
| 93 |
+
if entry["type"] == "file":
|
| 94 |
+
files.append((f"{prefix}{entry['name']}", entry["download_url"]))
|
| 95 |
+
elif entry["type"] == "dir":
|
| 96 |
+
_walk(entry["url"], f"{prefix}{entry['name']}/")
|
| 97 |
+
|
| 98 |
+
_walk(_registry_url(name), "")
|
| 99 |
+
return files
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _download_skill_bundle(name: str) -> dict[str, bytes]:
|
| 103 |
+
files = _walk_skill_files(name)
|
| 104 |
+
if not files:
|
| 105 |
+
raise SystemExit(f"Skill '{name}' has no files in the registry.")
|
| 106 |
+
bundle: dict[str, bytes] = {}
|
| 107 |
+
for rel_path, url in files:
|
| 108 |
+
resp = httpx.get(url, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
| 109 |
+
resp.raise_for_status()
|
| 110 |
+
bundle[rel_path] = resp.content
|
| 111 |
+
return bundle
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# ---------------------------------------------------------------------------
|
| 115 |
+
# Install / discovery
|
| 116 |
+
# ---------------------------------------------------------------------------
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _detect_install_dirs() -> tuple[Path, ...]:
|
| 120 |
+
"""Pick where to install based on the launching agent.
|
| 121 |
+
|
| 122 |
+
If we detect a specific agent from its env var, install only there. If nothing is detected, install to every native
|
| 123 |
+
path so any agent picks the skill up later.
|
| 124 |
+
"""
|
| 125 |
+
for env_var, skills_dir in _AGENT_ENV_TO_DIR.items():
|
| 126 |
+
if os.environ.get(env_var):
|
| 127 |
+
return (skills_dir,)
|
| 128 |
+
return _ALL_INSTALL_DIRS
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _install_skill(name: str, bundle: dict[str, bytes], root: Path, skills_dir: Path, force: bool) -> Path:
|
| 132 |
+
skill_dir = root / skills_dir / name
|
| 133 |
+
if skill_dir.exists():
|
| 134 |
+
if not force:
|
| 135 |
+
raise SystemExit(f"Skill already installed at {skill_dir}. Use --force to reinstall.")
|
| 136 |
+
shutil.rmtree(skill_dir)
|
| 137 |
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
| 138 |
+
for rel_path, data in bundle.items():
|
| 139 |
+
target = skill_dir / rel_path
|
| 140 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 141 |
+
target.write_bytes(data)
|
| 142 |
+
(skill_dir / _MANAGED_MARKER_FILE).touch()
|
| 143 |
+
return skill_dir
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _has_local_changes(skill_dir: Path, bundle: dict[str, bytes]) -> bool:
|
| 147 |
+
"""True if the installed skill has any file that differs from `bundle` or has extra files.
|
| 148 |
+
|
| 149 |
+
The marker file is ignored. Compares raw bytes so a whitespace-only edit still counts as dirty.
|
| 150 |
+
"""
|
| 151 |
+
on_disk: dict[str, bytes] = {}
|
| 152 |
+
for path in skill_dir.rglob("*"):
|
| 153 |
+
if not path.is_file():
|
| 154 |
+
continue
|
| 155 |
+
rel = str(path.relative_to(skill_dir))
|
| 156 |
+
if rel == _MANAGED_MARKER_FILE:
|
| 157 |
+
continue
|
| 158 |
+
on_disk[rel] = path.read_bytes()
|
| 159 |
+
return on_disk != bundle
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _discover_installed(root: Path) -> list[tuple[Path, str]]:
|
| 163 |
+
"""Return `(skills_dir, name)` pairs for every managed install under `root`."""
|
| 164 |
+
found: list[tuple[Path, str]] = []
|
| 165 |
+
for skills_dir in _ALL_INSTALL_DIRS:
|
| 166 |
+
skills_root = root / skills_dir
|
| 167 |
+
if not skills_root.exists():
|
| 168 |
+
continue
|
| 169 |
+
for d in sorted(skills_root.iterdir()):
|
| 170 |
+
if d.is_dir() and (d / _MANAGED_MARKER_FILE).exists():
|
| 171 |
+
found.append((skills_dir, d.name))
|
| 172 |
+
return found
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class SkillsCommand(BaseDiffusersCLICommand):
|
| 176 |
+
@staticmethod
|
| 177 |
+
def register_subcommand(subparsers: _SubParsersAction) -> None:
|
| 178 |
+
parser: ArgumentParser = subparsers.add_parser(
|
| 179 |
+
"skills",
|
| 180 |
+
help="Manage Agent Skills for AI assistants.",
|
| 181 |
+
usage="\n diffusers-cli skills <add|list|update|preview> [options]",
|
| 182 |
+
)
|
| 183 |
+
parser._optionals.title = "Options"
|
| 184 |
+
actions = parser.add_subparsers(dest="skills_action", required=True, metavar="<action>")
|
| 185 |
+
|
| 186 |
+
add = actions.add_parser("add", help="Download and install a skill.")
|
| 187 |
+
add.add_argument(
|
| 188 |
+
"name",
|
| 189 |
+
nargs="?",
|
| 190 |
+
default=None,
|
| 191 |
+
help="Skill name (e.g. diffusers-cli, custom-blocks). Omit and pass --all to install every skill.",
|
| 192 |
+
)
|
| 193 |
+
add.add_argument(
|
| 194 |
+
"--all",
|
| 195 |
+
dest="install_all",
|
| 196 |
+
action="store_true",
|
| 197 |
+
help="Install every skill in the registry. Mutually exclusive with a positional name.",
|
| 198 |
+
)
|
| 199 |
+
add.add_argument(
|
| 200 |
+
"--global",
|
| 201 |
+
"-g",
|
| 202 |
+
dest="install_global",
|
| 203 |
+
action="store_true",
|
| 204 |
+
help="Install globally (user-level) instead of in the current project directory.",
|
| 205 |
+
)
|
| 206 |
+
add.add_argument("--force", action="store_true", help="Overwrite existing skills in the destination.")
|
| 207 |
+
add.set_defaults(func=SkillsCommand)
|
| 208 |
+
|
| 209 |
+
list_action = actions.add_parser("list", help="List available skills in the registry.")
|
| 210 |
+
list_action.set_defaults(func=SkillsCommand)
|
| 211 |
+
|
| 212 |
+
update = actions.add_parser("update", help="Re-download and reinstall managed skills.")
|
| 213 |
+
update.add_argument(
|
| 214 |
+
"name",
|
| 215 |
+
nargs="?",
|
| 216 |
+
default=None,
|
| 217 |
+
help="Optional installed skill name to update. Omit to update every managed skill.",
|
| 218 |
+
)
|
| 219 |
+
update.add_argument(
|
| 220 |
+
"--global",
|
| 221 |
+
"-g",
|
| 222 |
+
dest="install_global",
|
| 223 |
+
action="store_true",
|
| 224 |
+
help="Update skills installed globally (user-level) instead of the current project.",
|
| 225 |
+
)
|
| 226 |
+
update.add_argument(
|
| 227 |
+
"--force",
|
| 228 |
+
action="store_true",
|
| 229 |
+
help="Overwrite skills even if they have local modifications since install.",
|
| 230 |
+
)
|
| 231 |
+
update.set_defaults(func=SkillsCommand)
|
| 232 |
+
|
| 233 |
+
preview = actions.add_parser("preview", help="Print a skill's SKILL.md from the registry.")
|
| 234 |
+
preview.add_argument("name", help="Skill name to preview.")
|
| 235 |
+
preview.set_defaults(func=SkillsCommand)
|
| 236 |
+
|
| 237 |
+
def __init__(self, args: Namespace):
|
| 238 |
+
self.args = args
|
| 239 |
+
|
| 240 |
+
def run(self) -> None:
|
| 241 |
+
if self.args.skills_action == "add":
|
| 242 |
+
self._add()
|
| 243 |
+
elif self.args.skills_action == "list":
|
| 244 |
+
self._list()
|
| 245 |
+
elif self.args.skills_action == "update":
|
| 246 |
+
self._update()
|
| 247 |
+
elif self.args.skills_action == "preview":
|
| 248 |
+
self._preview()
|
| 249 |
+
|
| 250 |
+
def _add(self) -> None:
|
| 251 |
+
if self.args.install_all and self.args.name:
|
| 252 |
+
raise SystemExit("--all and a positional skill name are mutually exclusive.")
|
| 253 |
+
if not self.args.install_all and not self.args.name:
|
| 254 |
+
raise SystemExit("Pass a skill name (e.g. diffusers-cli) or --all to install every skill.")
|
| 255 |
+
|
| 256 |
+
root = Path.home() if self.args.install_global else Path.cwd()
|
| 257 |
+
install_dirs = _detect_install_dirs()
|
| 258 |
+
names = self._resolve_names()
|
| 259 |
+
|
| 260 |
+
installed: list[str] = []
|
| 261 |
+
failed: list[str] = []
|
| 262 |
+
for name in names:
|
| 263 |
+
try:
|
| 264 |
+
bundle = _download_skill_bundle(name)
|
| 265 |
+
for skills_dir in install_dirs:
|
| 266 |
+
_install_skill(name, bundle, root, skills_dir, self.args.force)
|
| 267 |
+
installed.append(name)
|
| 268 |
+
except (SystemExit, httpx.HTTPError) as e:
|
| 269 |
+
# Downgrade to a warning so one broken skill doesn't abort the batch.
|
| 270 |
+
logger.warning(f"Skipping skill {name!r}: {e}")
|
| 271 |
+
failed.append(name)
|
| 272 |
+
|
| 273 |
+
if not installed:
|
| 274 |
+
raise SystemExit(f"No skills installed. Failed: {failed}")
|
| 275 |
+
out.result(
|
| 276 |
+
f"Installed {len(installed)} skill(s)",
|
| 277 |
+
installed=", ".join(installed),
|
| 278 |
+
failed=", ".join(failed) if failed else None,
|
| 279 |
+
paths=", ".join(str(root / d) for d in install_dirs),
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
def _update(self) -> None:
|
| 283 |
+
root = Path.home() if self.args.install_global else Path.cwd()
|
| 284 |
+
installed = _discover_installed(root)
|
| 285 |
+
if self.args.name is not None:
|
| 286 |
+
installed = [entry for entry in installed if entry[1] == self.args.name]
|
| 287 |
+
if not installed:
|
| 288 |
+
raise SystemExit(f"No installed skill named {self.args.name!r} found under {root}.")
|
| 289 |
+
if not installed:
|
| 290 |
+
raise SystemExit(f"No managed skills found under {root}.")
|
| 291 |
+
|
| 292 |
+
# Group by skill name so we redownload each bundle once even if it's installed to
|
| 293 |
+
# multiple locations (e.g. both .claude/skills/ and .agents/skills/).
|
| 294 |
+
by_name: dict[str, list[Path]] = {}
|
| 295 |
+
for skills_dir, name in installed:
|
| 296 |
+
by_name.setdefault(name, []).append(skills_dir)
|
| 297 |
+
|
| 298 |
+
updated: list[str] = []
|
| 299 |
+
failed: list[str] = []
|
| 300 |
+
skipped: list[str] = []
|
| 301 |
+
for name, dirs in sorted(by_name.items()):
|
| 302 |
+
try:
|
| 303 |
+
bundle = _download_skill_bundle(name)
|
| 304 |
+
for skills_dir in dirs:
|
| 305 |
+
skill_dir = root / skills_dir / name
|
| 306 |
+
if not self.args.force and _has_local_changes(skill_dir, bundle):
|
| 307 |
+
logger.warning(
|
| 308 |
+
f"Skill {name!r} at {skill_dir} has local modifications; "
|
| 309 |
+
"skipping. Pass --force to overwrite them."
|
| 310 |
+
)
|
| 311 |
+
skipped.append(name)
|
| 312 |
+
continue
|
| 313 |
+
_install_skill(name, bundle, root, skills_dir, force=True)
|
| 314 |
+
updated.append(name)
|
| 315 |
+
except (SystemExit, httpx.HTTPError) as e:
|
| 316 |
+
logger.warning(f"Skipping skill {name!r}: {e}")
|
| 317 |
+
failed.append(name)
|
| 318 |
+
|
| 319 |
+
out.result(
|
| 320 |
+
f"Updated {len(updated)} skill(s)",
|
| 321 |
+
updated=", ".join(updated),
|
| 322 |
+
skipped=", ".join(skipped) if skipped else None,
|
| 323 |
+
failed=", ".join(failed) if failed else None,
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
def _preview(self) -> None:
|
| 327 |
+
bundle = _download_skill_bundle(self.args.name)
|
| 328 |
+
skill_md = bundle.get("SKILL.md")
|
| 329 |
+
if skill_md is None:
|
| 330 |
+
raise SystemExit(f"Skill {self.args.name!r} has no SKILL.md in the registry.")
|
| 331 |
+
print(skill_md.decode())
|
| 332 |
+
|
| 333 |
+
def _list(self) -> None:
|
| 334 |
+
entries = _fetch_json(_registry_url())
|
| 335 |
+
skills = [{"name": e["name"]} for e in entries if e["type"] == "dir" and not e["name"].startswith(".")]
|
| 336 |
+
if not skills:
|
| 337 |
+
raise SystemExit("No skills found in registry.")
|
| 338 |
+
out.table(skills, headers=["name"])
|
| 339 |
+
|
| 340 |
+
def _resolve_names(self) -> list[str]:
|
| 341 |
+
if self.args.install_all:
|
| 342 |
+
entries = _fetch_json(_registry_url())
|
| 343 |
+
return sorted(e["name"] for e in entries if e["type"] == "dir" and not e["name"].startswith("."))
|
| 344 |
+
return [self.args.name]
|
diffusers/configuration_utils.py
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 DDUFEntry, 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 |
+
dduf_entries: dict[str, DDUFEntry] | None = kwargs.pop("dduf_entries", None)
|
| 387 |
+
|
| 388 |
+
user_agent = {**user_agent, "file_type": "config"}
|
| 389 |
+
user_agent = http_user_agent(user_agent)
|
| 390 |
+
|
| 391 |
+
pretrained_model_name_or_path = str(pretrained_model_name_or_path)
|
| 392 |
+
|
| 393 |
+
if cls.config_name is None:
|
| 394 |
+
raise ValueError(
|
| 395 |
+
"`self.config_name` is not defined. Note that one should not load a config from "
|
| 396 |
+
"`ConfigMixin`. Please make sure to define `config_name` in a class inheriting from `ConfigMixin`"
|
| 397 |
+
)
|
| 398 |
+
# Custom path for now
|
| 399 |
+
if dduf_entries:
|
| 400 |
+
if subfolder is not None:
|
| 401 |
+
raise ValueError(
|
| 402 |
+
"DDUF file only allow for 1 level of directory (e.g transformer/model1/model.safetentors is not allowed). "
|
| 403 |
+
"Please check the DDUF structure"
|
| 404 |
+
)
|
| 405 |
+
config_file = cls._get_config_file_from_dduf(pretrained_model_name_or_path, dduf_entries)
|
| 406 |
+
elif os.path.isfile(pretrained_model_name_or_path):
|
| 407 |
+
config_file = pretrained_model_name_or_path
|
| 408 |
+
elif os.path.isdir(pretrained_model_name_or_path):
|
| 409 |
+
if subfolder is not None and os.path.isfile(
|
| 410 |
+
os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name)
|
| 411 |
+
):
|
| 412 |
+
config_file = os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name)
|
| 413 |
+
elif os.path.isfile(os.path.join(pretrained_model_name_or_path, cls.config_name)):
|
| 414 |
+
# Load from a PyTorch checkpoint
|
| 415 |
+
config_file = os.path.join(pretrained_model_name_or_path, cls.config_name)
|
| 416 |
+
else:
|
| 417 |
+
raise EnvironmentError(
|
| 418 |
+
f"Error no file named {cls.config_name} found in directory {pretrained_model_name_or_path}."
|
| 419 |
+
)
|
| 420 |
+
else:
|
| 421 |
+
try:
|
| 422 |
+
# Load from URL or cache if already cached
|
| 423 |
+
config_file = hf_hub_download(
|
| 424 |
+
pretrained_model_name_or_path,
|
| 425 |
+
filename=cls.config_name,
|
| 426 |
+
cache_dir=cache_dir,
|
| 427 |
+
force_download=force_download,
|
| 428 |
+
proxies=proxies,
|
| 429 |
+
local_files_only=local_files_only,
|
| 430 |
+
token=token,
|
| 431 |
+
user_agent=user_agent,
|
| 432 |
+
subfolder=subfolder,
|
| 433 |
+
revision=revision,
|
| 434 |
+
local_dir=local_dir,
|
| 435 |
+
local_dir_use_symlinks=local_dir_use_symlinks,
|
| 436 |
+
)
|
| 437 |
+
except RepositoryNotFoundError:
|
| 438 |
+
raise EnvironmentError(
|
| 439 |
+
f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier"
|
| 440 |
+
" listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a"
|
| 441 |
+
" token having permission to this repo with `token` or log in with `hf auth login`."
|
| 442 |
+
)
|
| 443 |
+
except RevisionNotFoundError:
|
| 444 |
+
raise EnvironmentError(
|
| 445 |
+
f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for"
|
| 446 |
+
" this model name. Check the model page at"
|
| 447 |
+
f" 'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions."
|
| 448 |
+
)
|
| 449 |
+
except EntryNotFoundError:
|
| 450 |
+
raise EnvironmentError(
|
| 451 |
+
f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}."
|
| 452 |
+
)
|
| 453 |
+
except HfHubHTTPError as err:
|
| 454 |
+
raise EnvironmentError(
|
| 455 |
+
"There was a specific connection error when trying to load"
|
| 456 |
+
f" {pretrained_model_name_or_path}:\n{err}"
|
| 457 |
+
)
|
| 458 |
+
except ValueError:
|
| 459 |
+
raise EnvironmentError(
|
| 460 |
+
f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it"
|
| 461 |
+
f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a"
|
| 462 |
+
f" directory containing a {cls.config_name} file.\nCheckout your internet connection or see how to"
|
| 463 |
+
" run the library in offline mode at"
|
| 464 |
+
" 'https://huggingface.co/docs/diffusers/installation#offline-mode'."
|
| 465 |
+
)
|
| 466 |
+
except EnvironmentError:
|
| 467 |
+
raise EnvironmentError(
|
| 468 |
+
f"Can't load config for '{pretrained_model_name_or_path}'. If you were trying to load it from "
|
| 469 |
+
"'https://huggingface.co/models', make sure you don't have a local directory with the same name. "
|
| 470 |
+
f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory "
|
| 471 |
+
f"containing a {cls.config_name} file"
|
| 472 |
+
)
|
| 473 |
+
try:
|
| 474 |
+
config_dict = cls._dict_from_json_file(config_file, dduf_entries=dduf_entries)
|
| 475 |
+
|
| 476 |
+
commit_hash = extract_commit_hash(config_file)
|
| 477 |
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
| 478 |
+
raise EnvironmentError(f"It looks like the config file at '{config_file}' is not a valid JSON file.")
|
| 479 |
+
|
| 480 |
+
if not (return_unused_kwargs or return_commit_hash):
|
| 481 |
+
return config_dict
|
| 482 |
+
|
| 483 |
+
outputs = (config_dict,)
|
| 484 |
+
|
| 485 |
+
if return_unused_kwargs:
|
| 486 |
+
outputs += (kwargs,)
|
| 487 |
+
|
| 488 |
+
if return_commit_hash:
|
| 489 |
+
outputs += (commit_hash,)
|
| 490 |
+
|
| 491 |
+
return outputs
|
| 492 |
+
|
| 493 |
+
@staticmethod
|
| 494 |
+
def _get_init_keys(input_class):
|
| 495 |
+
return set(dict(inspect.signature(input_class.__init__).parameters).keys())
|
| 496 |
+
|
| 497 |
+
@classmethod
|
| 498 |
+
def extract_init_dict(cls, config_dict, **kwargs):
|
| 499 |
+
# Skip keys that were not present in the original config, so default __init__ values were used
|
| 500 |
+
used_defaults = config_dict.get("_use_default_values", [])
|
| 501 |
+
config_dict = {k: v for k, v in config_dict.items() if k not in used_defaults and k != "_use_default_values"}
|
| 502 |
+
|
| 503 |
+
# 0. Copy origin config dict
|
| 504 |
+
original_dict = dict(config_dict.items())
|
| 505 |
+
|
| 506 |
+
# 1. Retrieve expected config attributes from __init__ signature
|
| 507 |
+
expected_keys = cls._get_init_keys(cls)
|
| 508 |
+
expected_keys.remove("self")
|
| 509 |
+
# remove general kwargs if present in dict
|
| 510 |
+
if "kwargs" in expected_keys:
|
| 511 |
+
expected_keys.remove("kwargs")
|
| 512 |
+
|
| 513 |
+
# 2. Remove attributes that cannot be expected from expected config attributes
|
| 514 |
+
# remove keys to be ignored
|
| 515 |
+
if len(cls.ignore_for_config) > 0:
|
| 516 |
+
expected_keys = expected_keys - set(cls.ignore_for_config)
|
| 517 |
+
|
| 518 |
+
# load diffusers library to import compatible and original scheduler
|
| 519 |
+
diffusers_library = importlib.import_module(__name__.split(".")[0])
|
| 520 |
+
|
| 521 |
+
if cls.has_compatibles:
|
| 522 |
+
compatible_classes = [c for c in cls._get_compatibles() if not isinstance(c, DummyObject)]
|
| 523 |
+
else:
|
| 524 |
+
compatible_classes = []
|
| 525 |
+
|
| 526 |
+
expected_keys_comp_cls = set()
|
| 527 |
+
for c in compatible_classes:
|
| 528 |
+
expected_keys_c = cls._get_init_keys(c)
|
| 529 |
+
expected_keys_comp_cls = expected_keys_comp_cls.union(expected_keys_c)
|
| 530 |
+
expected_keys_comp_cls = expected_keys_comp_cls - cls._get_init_keys(cls)
|
| 531 |
+
config_dict = {k: v for k, v in config_dict.items() if k not in expected_keys_comp_cls}
|
| 532 |
+
|
| 533 |
+
# remove attributes from orig class that cannot be expected
|
| 534 |
+
orig_cls_name = config_dict.pop("_class_name", cls.__name__)
|
| 535 |
+
if (
|
| 536 |
+
isinstance(orig_cls_name, str)
|
| 537 |
+
and orig_cls_name != cls.__name__
|
| 538 |
+
and hasattr(diffusers_library, orig_cls_name)
|
| 539 |
+
):
|
| 540 |
+
orig_cls = getattr(diffusers_library, orig_cls_name)
|
| 541 |
+
unexpected_keys_from_orig = cls._get_init_keys(orig_cls) - expected_keys
|
| 542 |
+
config_dict = {k: v for k, v in config_dict.items() if k not in unexpected_keys_from_orig}
|
| 543 |
+
elif not isinstance(orig_cls_name, str) and not isinstance(orig_cls_name, (list, tuple)):
|
| 544 |
+
raise ValueError(
|
| 545 |
+
"Make sure that the `_class_name` is of type string or list of string (for custom pipelines)."
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
# remove private attributes
|
| 549 |
+
config_dict = {k: v for k, v in config_dict.items() if not k.startswith("_")}
|
| 550 |
+
|
| 551 |
+
# remove quantization_config
|
| 552 |
+
config_dict = {k: v for k, v in config_dict.items() if k != "quantization_config"}
|
| 553 |
+
|
| 554 |
+
# 3. Create keyword arguments that will be passed to __init__ from expected keyword arguments
|
| 555 |
+
init_dict = {}
|
| 556 |
+
for key in expected_keys:
|
| 557 |
+
# if config param is passed to kwarg and is present in config dict
|
| 558 |
+
# it should overwrite existing config dict key
|
| 559 |
+
if key in kwargs and key in config_dict:
|
| 560 |
+
config_dict[key] = kwargs.pop(key)
|
| 561 |
+
|
| 562 |
+
if key in kwargs:
|
| 563 |
+
# overwrite key
|
| 564 |
+
init_dict[key] = kwargs.pop(key)
|
| 565 |
+
elif key in config_dict:
|
| 566 |
+
# use value from config dict
|
| 567 |
+
init_dict[key] = config_dict.pop(key)
|
| 568 |
+
|
| 569 |
+
# 4. Give nice warning if unexpected values have been passed
|
| 570 |
+
if len(config_dict) > 0:
|
| 571 |
+
logger.warning(
|
| 572 |
+
f"The config attributes {config_dict} were passed to {cls.__name__}, "
|
| 573 |
+
"but are not expected and will be ignored. Please verify your "
|
| 574 |
+
f"{cls.config_name} configuration file."
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
# 5. Give nice info if config attributes are initialized to default because they have not been passed
|
| 578 |
+
passed_keys = set(init_dict.keys())
|
| 579 |
+
if len(expected_keys - passed_keys) > 0:
|
| 580 |
+
logger.info(
|
| 581 |
+
f"{expected_keys - passed_keys} was not found in config. Values will be initialized to default values."
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
# 6. Define unused keyword arguments
|
| 585 |
+
unused_kwargs = {**config_dict, **kwargs}
|
| 586 |
+
|
| 587 |
+
# 7. Define "hidden" config parameters that were saved for compatible classes
|
| 588 |
+
hidden_config_dict = {k: v for k, v in original_dict.items() if k not in init_dict}
|
| 589 |
+
|
| 590 |
+
return init_dict, unused_kwargs, hidden_config_dict
|
| 591 |
+
|
| 592 |
+
@classmethod
|
| 593 |
+
def _dict_from_json_file(cls, json_file: str | os.PathLike, dduf_entries: dict[str, DDUFEntry] | None = None):
|
| 594 |
+
if dduf_entries:
|
| 595 |
+
text = dduf_entries[json_file].read_text()
|
| 596 |
+
else:
|
| 597 |
+
with open(json_file, "r", encoding="utf-8") as reader:
|
| 598 |
+
text = reader.read()
|
| 599 |
+
return json.loads(text)
|
| 600 |
+
|
| 601 |
+
def __repr__(self):
|
| 602 |
+
return f"{self.__class__.__name__} {self.to_json_string()}"
|
| 603 |
+
|
| 604 |
+
@property
|
| 605 |
+
def config(self) -> dict[str, Any]:
|
| 606 |
+
"""
|
| 607 |
+
Returns the config of the class as a frozen dictionary
|
| 608 |
+
|
| 609 |
+
Returns:
|
| 610 |
+
`dict[str, Any]`: Config of the class.
|
| 611 |
+
"""
|
| 612 |
+
return self._internal_dict
|
| 613 |
+
|
| 614 |
+
def to_json_string(self) -> str:
|
| 615 |
+
"""
|
| 616 |
+
Serializes the configuration instance to a JSON string.
|
| 617 |
+
|
| 618 |
+
Returns:
|
| 619 |
+
`str`:
|
| 620 |
+
String containing all the attributes that make up the configuration instance in JSON format.
|
| 621 |
+
"""
|
| 622 |
+
config_dict = self._internal_dict if hasattr(self, "_internal_dict") else {}
|
| 623 |
+
config_dict["_class_name"] = self.__class__.__name__
|
| 624 |
+
config_dict["_diffusers_version"] = __version__
|
| 625 |
+
|
| 626 |
+
def to_json_saveable(value):
|
| 627 |
+
if isinstance(value, np.ndarray):
|
| 628 |
+
value = value.tolist()
|
| 629 |
+
elif isinstance(value, Path):
|
| 630 |
+
value = value.as_posix()
|
| 631 |
+
elif hasattr(value, "to_dict") and callable(value.to_dict):
|
| 632 |
+
value = value.to_dict()
|
| 633 |
+
elif isinstance(value, list):
|
| 634 |
+
value = [to_json_saveable(v) for v in value]
|
| 635 |
+
return value
|
| 636 |
+
|
| 637 |
+
if "quantization_config" in config_dict:
|
| 638 |
+
config_dict["quantization_config"] = (
|
| 639 |
+
config_dict.quantization_config.to_dict()
|
| 640 |
+
if not isinstance(config_dict.quantization_config, dict)
|
| 641 |
+
else config_dict.quantization_config
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
config_dict = {k: to_json_saveable(v) for k, v in config_dict.items()}
|
| 645 |
+
# Don't save "_ignore_files" or "_use_default_values"
|
| 646 |
+
config_dict.pop("_ignore_files", None)
|
| 647 |
+
config_dict.pop("_use_default_values", None)
|
| 648 |
+
# pop the `_pre_quantization_dtype` as torch.dtypes are not serializable.
|
| 649 |
+
_ = config_dict.pop("_pre_quantization_dtype", None)
|
| 650 |
+
|
| 651 |
+
if getattr(self, "_auto_class", None) is not None:
|
| 652 |
+
module = self.__class__.__module__.split(".")[-1]
|
| 653 |
+
auto_map = config_dict.get("auto_map", {})
|
| 654 |
+
auto_map[self._auto_class] = f"{module}.{self.__class__.__name__}"
|
| 655 |
+
config_dict["auto_map"] = auto_map
|
| 656 |
+
|
| 657 |
+
return json.dumps(config_dict, indent=2, sort_keys=True) + "\n"
|
| 658 |
+
|
| 659 |
+
def to_json_file(self, json_file_path: str | os.PathLike):
|
| 660 |
+
"""
|
| 661 |
+
Save the configuration instance's parameters to a JSON file.
|
| 662 |
+
|
| 663 |
+
Args:
|
| 664 |
+
json_file_path (`str` or `os.PathLike`):
|
| 665 |
+
Path to the JSON file to save a configuration instance's parameters.
|
| 666 |
+
"""
|
| 667 |
+
with open(json_file_path, "w", encoding="utf-8") as writer:
|
| 668 |
+
writer.write(self.to_json_string())
|
| 669 |
+
|
| 670 |
+
@classmethod
|
| 671 |
+
def _get_config_file_from_dduf(cls, pretrained_model_name_or_path: str, dduf_entries: dict[str, DDUFEntry]):
|
| 672 |
+
# paths inside a DDUF file must always be "/"
|
| 673 |
+
config_file = (
|
| 674 |
+
cls.config_name
|
| 675 |
+
if pretrained_model_name_or_path == ""
|
| 676 |
+
else "/".join([pretrained_model_name_or_path, cls.config_name])
|
| 677 |
+
)
|
| 678 |
+
if config_file not in dduf_entries:
|
| 679 |
+
raise ValueError(
|
| 680 |
+
f"We did not manage to find the file {config_file} in the dduf file. We only have the following files {dduf_entries.keys()}"
|
| 681 |
+
)
|
| 682 |
+
return config_file
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def register_to_config(init):
|
| 686 |
+
r"""
|
| 687 |
+
Decorator to apply on the init of classes inheriting from [`ConfigMixin`] so that all the arguments are
|
| 688 |
+
automatically sent to `self.register_for_config`. To ignore a specific argument accepted by the init but that
|
| 689 |
+
shouldn't be registered in the config, use the `ignore_for_config` class variable
|
| 690 |
+
|
| 691 |
+
Warning: Once decorated, all private arguments (beginning with an underscore) are trashed and not sent to the init!
|
| 692 |
+
"""
|
| 693 |
+
|
| 694 |
+
@functools.wraps(init)
|
| 695 |
+
def inner_init(self, *args, **kwargs):
|
| 696 |
+
# Ignore private kwargs in the init.
|
| 697 |
+
init_kwargs = {k: v for k, v in kwargs.items() if not k.startswith("_")}
|
| 698 |
+
config_init_kwargs = {k: v for k, v in kwargs.items() if k.startswith("_")}
|
| 699 |
+
if not isinstance(self, ConfigMixin):
|
| 700 |
+
raise RuntimeError(
|
| 701 |
+
f"`@register_for_config` was applied to {self.__class__.__name__} init method, but this class does "
|
| 702 |
+
"not inherit from `ConfigMixin`."
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
ignore = getattr(self, "ignore_for_config", [])
|
| 706 |
+
# Get positional arguments aligned with kwargs
|
| 707 |
+
new_kwargs = {}
|
| 708 |
+
signature = inspect.signature(init)
|
| 709 |
+
parameters = {
|
| 710 |
+
name: p.default for i, (name, p) in enumerate(signature.parameters.items()) if i > 0 and name not in ignore
|
| 711 |
+
}
|
| 712 |
+
for arg, name in zip(args, parameters.keys()):
|
| 713 |
+
new_kwargs[name] = arg
|
| 714 |
+
|
| 715 |
+
# Then add all kwargs
|
| 716 |
+
new_kwargs.update(
|
| 717 |
+
{
|
| 718 |
+
k: init_kwargs.get(k, default)
|
| 719 |
+
for k, default in parameters.items()
|
| 720 |
+
if k not in ignore and k not in new_kwargs
|
| 721 |
+
}
|
| 722 |
+
)
|
| 723 |
+
|
| 724 |
+
# Take note of the parameters that were not present in the loaded config
|
| 725 |
+
if len(set(new_kwargs.keys()) - set(init_kwargs)) > 0:
|
| 726 |
+
new_kwargs["_use_default_values"] = list(set(new_kwargs.keys()) - set(init_kwargs))
|
| 727 |
+
|
| 728 |
+
new_kwargs = {**config_init_kwargs, **new_kwargs}
|
| 729 |
+
getattr(self, "register_to_config")(**new_kwargs)
|
| 730 |
+
init(self, *args, **init_kwargs)
|
| 731 |
+
|
| 732 |
+
return inner_init
|
| 733 |
+
|
| 734 |
+
|
| 735 |
+
class LegacyConfigMixin(ConfigMixin):
|
| 736 |
+
r"""
|
| 737 |
+
A subclass of `ConfigMixin` to resolve class mapping from legacy classes (like `Transformer2DModel`) to more
|
| 738 |
+
pipeline-specific classes (like `DiTTransformer2DModel`).
|
| 739 |
+
"""
|
| 740 |
+
|
| 741 |
+
@classmethod
|
| 742 |
+
def from_config(cls, config: FrozenDict | dict[str, Any] = None, return_unused_kwargs=False, **kwargs):
|
| 743 |
+
# To prevent dependency import problem.
|
| 744 |
+
from .models.model_loading_utils import _fetch_remapped_cls_from_config
|
| 745 |
+
|
| 746 |
+
# resolve remapping
|
| 747 |
+
remapped_class = _fetch_remapped_cls_from_config(config, cls)
|
| 748 |
+
|
| 749 |
+
if remapped_class is cls:
|
| 750 |
+
return super(LegacyConfigMixin, remapped_class).from_config(config, return_unused_kwargs, **kwargs)
|
| 751 |
+
else:
|
| 752 |
+
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,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"httpx": "httpx<1.0.0",
|
| 12 |
+
"huggingface-hub": "huggingface-hub>=1.23.0,<2.0",
|
| 13 |
+
"requests-mock": "requests-mock==1.10.0",
|
| 14 |
+
"importlib_metadata": "importlib_metadata",
|
| 15 |
+
"invisible-watermark": "invisible-watermark>=0.2.0",
|
| 16 |
+
"isort": "isort>=5.5.4",
|
| 17 |
+
"Jinja2": "Jinja2",
|
| 18 |
+
"torchsde": "torchsde",
|
| 19 |
+
"note_seq": "note_seq",
|
| 20 |
+
"librosa": "librosa",
|
| 21 |
+
"llvmlite": "llvmlite>=0.40.0",
|
| 22 |
+
"numba": "numba>=0.57.0",
|
| 23 |
+
"numpy": "numpy",
|
| 24 |
+
"parameterized": "parameterized",
|
| 25 |
+
"peft": "peft>=0.17.0",
|
| 26 |
+
"protobuf": "protobuf>=3.20.3,<4",
|
| 27 |
+
"pytest": "pytest",
|
| 28 |
+
"pytest-timeout": "pytest-timeout",
|
| 29 |
+
"pytest-xdist": "pytest-xdist",
|
| 30 |
+
"python": "python>=3.10.0",
|
| 31 |
+
"ruff": "ruff==0.9.10",
|
| 32 |
+
"safetensors": "safetensors>=0.8.0",
|
| 33 |
+
"sentencepiece": "sentencepiece>=0.1.91,!=0.1.92",
|
| 34 |
+
"GitPython": "GitPython<3.1.19",
|
| 35 |
+
"scipy": "scipy",
|
| 36 |
+
"onnx": "onnx",
|
| 37 |
+
"optimum_quanto": "optimum_quanto>=0.2.6",
|
| 38 |
+
"gguf": "gguf>=0.10.0",
|
| 39 |
+
"auto-round": "auto-round>=0.13.0",
|
| 40 |
+
"torchao": "torchao>=0.7.0",
|
| 41 |
+
"bitsandbytes": "bitsandbytes>=0.43.3",
|
| 42 |
+
"nvidia_modelopt[hf]": "nvidia_modelopt[hf]>=0.33.1",
|
| 43 |
+
"sdnq": "sdnq>=0.2.2",
|
| 44 |
+
"regex": "regex!=2019.12.17",
|
| 45 |
+
"requests": "requests",
|
| 46 |
+
"tensorboard": "tensorboard",
|
| 47 |
+
"tiktoken": "tiktoken>=0.7.0",
|
| 48 |
+
"torch": "torch>=2.6",
|
| 49 |
+
"torchvision": "torchvision",
|
| 50 |
+
"transformers": "transformers>=4.41.2",
|
| 51 |
+
"urllib3": "urllib3<=2.0.0",
|
| 52 |
+
"black": "black",
|
| 53 |
+
"phonemizer": "phonemizer",
|
| 54 |
+
"opencv-python": "opencv-python",
|
| 55 |
+
"timm": "timm",
|
| 56 |
+
"flashpack": "flashpack",
|
| 57 |
+
}
|
diffusers/experimental/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🧨 Diffusers Experimental
|
| 2 |
+
|
| 3 |
+
We are adding experimental code to support novel applications and usages of the Diffusers library.
|
| 4 |
+
Currently, the following experiments are supported:
|
| 5 |
+
* Reinforcement learning via an implementation of the [Diffuser](https://huggingface.co/papers/2205.09991) model.
|
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,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 .magnitude_aware_guidance import MagnitudeAwareGuidance
|
| 28 |
+
from .perturbed_attention_guidance import PerturbedAttentionGuidance
|
| 29 |
+
from .skip_layer_guidance import SkipLayerGuidance
|
| 30 |
+
from .smoothed_energy_guidance import SmoothedEnergyGuidance
|
| 31 |
+
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/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 supression 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,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 .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,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 _get_child_registries(self) -> list["HookRegistry"]:
|
| 280 |
+
"""Return registries of child modules, using a cached list when available.
|
| 281 |
+
|
| 282 |
+
The cache is built on first call and reused for subsequent calls. This avoids the cost of walking the full
|
| 283 |
+
module tree via named_modules() on every _set_context call, which is significant for large models (e.g. ~2.7ms
|
| 284 |
+
per call on Flux2).
|
| 285 |
+
"""
|
| 286 |
+
if not hasattr(self, "_child_registries_cache"):
|
| 287 |
+
self._child_registries_cache = None
|
| 288 |
+
|
| 289 |
+
if self._child_registries_cache is not None:
|
| 290 |
+
return self._child_registries_cache
|
| 291 |
+
|
| 292 |
+
registries = []
|
| 293 |
+
for module_name, module in unwrap_module(self._module_ref).named_modules():
|
| 294 |
+
if module_name == "":
|
| 295 |
+
continue
|
| 296 |
+
module = unwrap_module(module)
|
| 297 |
+
if hasattr(module, "_diffusers_hook"):
|
| 298 |
+
registries.append(module._diffusers_hook)
|
| 299 |
+
self._child_registries_cache = registries
|
| 300 |
+
return registries
|
| 301 |
+
|
| 302 |
+
def __repr__(self) -> str:
|
| 303 |
+
registry_repr = ""
|
| 304 |
+
for i, hook_name in enumerate(self._hook_order):
|
| 305 |
+
if self.hooks[hook_name].__class__.__repr__ is not object.__repr__:
|
| 306 |
+
hook_repr = self.hooks[hook_name].__repr__()
|
| 307 |
+
else:
|
| 308 |
+
hook_repr = self.hooks[hook_name].__class__.__name__
|
| 309 |
+
registry_repr += f" ({i}) {hook_name} - {hook_repr}"
|
| 310 |
+
if i < len(self._hook_order) - 1:
|
| 311 |
+
registry_repr += "\n"
|
| 312 |
+
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, is_peft_version
|
| 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() and is_peft_version(">", "0.14.0")
|
| 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/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)
|
diffusers/hooks/utils.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ._common import _ALL_TRANSFORMER_BLOCK_IDENTIFIERS, _ATTENTION_CLASSES, _FEEDFORWARD_CLASSES
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _get_identifiable_transformer_blocks_in_module(module: torch.nn.Module):
|
| 21 |
+
module_list_with_transformer_blocks = []
|
| 22 |
+
for name, submodule in module.named_modules():
|
| 23 |
+
name_endswith_identifier = any(name.endswith(identifier) for identifier in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS)
|
| 24 |
+
is_ModuleList = isinstance(submodule, torch.nn.ModuleList)
|
| 25 |
+
if name_endswith_identifier and is_ModuleList:
|
| 26 |
+
module_list_with_transformer_blocks.append((name, submodule))
|
| 27 |
+
return module_list_with_transformer_blocks
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _get_identifiable_attention_layers_in_module(module: torch.nn.Module):
|
| 31 |
+
attention_layers = []
|
| 32 |
+
for name, submodule in module.named_modules():
|
| 33 |
+
if isinstance(submodule, _ATTENTION_CLASSES):
|
| 34 |
+
attention_layers.append((name, submodule))
|
| 35 |
+
return attention_layers
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _get_identifiable_feedforward_layers_in_module(module: torch.nn.Module):
|
| 39 |
+
feedforward_layers = []
|
| 40 |
+
for name, submodule in module.named_modules():
|
| 41 |
+
if isinstance(submodule, _FEEDFORWARD_CLASSES):
|
| 42 |
+
feedforward_layers.append((name, submodule))
|
| 43 |
+
return feedforward_layers
|
diffusers/image_processor.py
ADDED
|
@@ -0,0 +1,1468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
import warnings
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
import PIL.Image
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
from PIL import Image, ImageFilter, ImageOps
|
| 23 |
+
|
| 24 |
+
from .configuration_utils import ConfigMixin, register_to_config
|
| 25 |
+
from .utils import CONFIG_NAME, PIL_INTERPOLATION, deprecate
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
PipelineImageInput = (
|
| 29 |
+
PIL.Image.Image | np.ndarray | torch.Tensor | list[PIL.Image.Image] | list[np.ndarray] | list[torch.Tensor]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
PipelineDepthInput = PipelineImageInput
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def is_valid_image(image) -> bool:
|
| 36 |
+
r"""
|
| 37 |
+
Checks if the input is a valid image.
|
| 38 |
+
|
| 39 |
+
A valid image can be:
|
| 40 |
+
- A `PIL.Image.Image`.
|
| 41 |
+
- A 2D or 3D `np.ndarray` or `torch.Tensor` (grayscale or color image).
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
image (`PIL.Image.Image | np.ndarray | torch.Tensor`):
|
| 45 |
+
The image to validate. It can be a PIL image, a NumPy array, or a torch tensor.
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
`bool`:
|
| 49 |
+
`True` if the input is a valid image, `False` otherwise.
|
| 50 |
+
"""
|
| 51 |
+
return isinstance(image, PIL.Image.Image) or isinstance(image, (np.ndarray, torch.Tensor)) and image.ndim in (2, 3)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def is_valid_image_imagelist(images):
|
| 55 |
+
r"""
|
| 56 |
+
Checks if the input is a valid image or list of images.
|
| 57 |
+
|
| 58 |
+
The input can be one of the following formats:
|
| 59 |
+
- A 4D tensor or numpy array (batch of images).
|
| 60 |
+
- A valid single image: `PIL.Image.Image`, 2D `np.ndarray` or `torch.Tensor` (grayscale image), 3D `np.ndarray` or
|
| 61 |
+
`torch.Tensor`.
|
| 62 |
+
- A list of valid images.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
images (`np.ndarray | torch.Tensor | PIL.Image.Image | list`):
|
| 66 |
+
The image(s) to check. Can be a batch of images (4D tensor/array), a single image, or a list of valid
|
| 67 |
+
images.
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
`bool`:
|
| 71 |
+
`True` if the input is valid, `False` otherwise.
|
| 72 |
+
"""
|
| 73 |
+
if isinstance(images, (np.ndarray, torch.Tensor)) and images.ndim == 4:
|
| 74 |
+
return True
|
| 75 |
+
elif is_valid_image(images):
|
| 76 |
+
return True
|
| 77 |
+
elif isinstance(images, list):
|
| 78 |
+
return all(is_valid_image(image) for image in images)
|
| 79 |
+
return False
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class VaeImageProcessor(ConfigMixin):
|
| 83 |
+
"""
|
| 84 |
+
Image processor for VAE.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
do_resize (`bool`, *optional*, defaults to `True`):
|
| 88 |
+
Whether to downscale the image's (height, width) dimensions to multiples of `vae_scale_factor`. Can accept
|
| 89 |
+
`height` and `width` arguments from [`image_processor.VaeImageProcessor.preprocess`] method.
|
| 90 |
+
vae_scale_factor (`int`, *optional*, defaults to `8`):
|
| 91 |
+
VAE scale factor. If `do_resize` is `True`, the image is automatically resized to multiples of this factor.
|
| 92 |
+
resample (`str`, *optional*, defaults to `lanczos`):
|
| 93 |
+
Resampling filter to use when resizing the image.
|
| 94 |
+
do_normalize (`bool`, *optional*, defaults to `True`):
|
| 95 |
+
Whether to normalize the image to [-1,1].
|
| 96 |
+
do_binarize (`bool`, *optional*, defaults to `False`):
|
| 97 |
+
Whether to binarize the image to 0/1.
|
| 98 |
+
do_convert_rgb (`bool`, *optional*, defaults to be `False`):
|
| 99 |
+
Whether to convert the images to RGB format.
|
| 100 |
+
do_convert_grayscale (`bool`, *optional*, defaults to be `False`):
|
| 101 |
+
Whether to convert the images to grayscale format.
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
config_name = CONFIG_NAME
|
| 105 |
+
|
| 106 |
+
@register_to_config
|
| 107 |
+
def __init__(
|
| 108 |
+
self,
|
| 109 |
+
do_resize: bool = True,
|
| 110 |
+
vae_scale_factor: int = 8,
|
| 111 |
+
vae_latent_channels: int = 4,
|
| 112 |
+
resample: str = "lanczos",
|
| 113 |
+
reducing_gap: int | None = None,
|
| 114 |
+
do_normalize: bool = True,
|
| 115 |
+
do_binarize: bool = False,
|
| 116 |
+
do_convert_rgb: bool = False,
|
| 117 |
+
do_convert_grayscale: bool = False,
|
| 118 |
+
):
|
| 119 |
+
super().__init__()
|
| 120 |
+
if do_convert_rgb and do_convert_grayscale:
|
| 121 |
+
raise ValueError(
|
| 122 |
+
"`do_convert_rgb` and `do_convert_grayscale` can not both be set to `True`,"
|
| 123 |
+
" if you intended to convert the image into RGB format, please set `do_convert_grayscale = False`.",
|
| 124 |
+
" if you intended to convert the image into grayscale format, please set `do_convert_rgb = False`",
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
@staticmethod
|
| 128 |
+
def numpy_to_pil(images: np.ndarray) -> list[PIL.Image.Image]:
|
| 129 |
+
r"""
|
| 130 |
+
Convert a numpy image or a batch of images to a PIL image.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
images (`np.ndarray`):
|
| 134 |
+
The image array to convert to PIL format.
|
| 135 |
+
|
| 136 |
+
Returns:
|
| 137 |
+
`list[PIL.Image.Image]`:
|
| 138 |
+
A list of PIL images.
|
| 139 |
+
"""
|
| 140 |
+
if images.ndim == 3:
|
| 141 |
+
images = images[None, ...]
|
| 142 |
+
images = (images * 255).round().astype("uint8")
|
| 143 |
+
if images.shape[-1] == 1:
|
| 144 |
+
# special case for grayscale (single channel) images
|
| 145 |
+
pil_images = [Image.fromarray(image.squeeze(), mode="L") for image in images]
|
| 146 |
+
else:
|
| 147 |
+
pil_images = [Image.fromarray(image) for image in images]
|
| 148 |
+
|
| 149 |
+
return pil_images
|
| 150 |
+
|
| 151 |
+
@staticmethod
|
| 152 |
+
def pil_to_numpy(images: list[PIL.Image.Image] | PIL.Image.Image) -> np.ndarray:
|
| 153 |
+
r"""
|
| 154 |
+
Convert a PIL image or a list of PIL images to NumPy arrays.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
images (`PIL.Image.Image` or `list[PIL.Image.Image]`):
|
| 158 |
+
The PIL image or list of images to convert to NumPy format.
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
`np.ndarray`:
|
| 162 |
+
A NumPy array representation of the images.
|
| 163 |
+
"""
|
| 164 |
+
if not isinstance(images, list):
|
| 165 |
+
images = [images]
|
| 166 |
+
images = [np.array(image).astype(np.float32) / 255.0 for image in images]
|
| 167 |
+
images = np.stack(images, axis=0)
|
| 168 |
+
|
| 169 |
+
return images
|
| 170 |
+
|
| 171 |
+
@staticmethod
|
| 172 |
+
def numpy_to_pt(images: np.ndarray) -> torch.Tensor:
|
| 173 |
+
r"""
|
| 174 |
+
Convert a NumPy image to a PyTorch tensor.
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
images (`np.ndarray`):
|
| 178 |
+
The NumPy image array to convert to PyTorch format.
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
`torch.Tensor`:
|
| 182 |
+
A PyTorch tensor representation of the images.
|
| 183 |
+
"""
|
| 184 |
+
if images.ndim == 3:
|
| 185 |
+
images = images[..., None]
|
| 186 |
+
|
| 187 |
+
images = torch.from_numpy(images.transpose(0, 3, 1, 2))
|
| 188 |
+
return images
|
| 189 |
+
|
| 190 |
+
@staticmethod
|
| 191 |
+
def pt_to_numpy(images: torch.Tensor) -> np.ndarray:
|
| 192 |
+
r"""
|
| 193 |
+
Convert a PyTorch tensor to a NumPy image.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
images (`torch.Tensor`):
|
| 197 |
+
The PyTorch tensor to convert to NumPy format.
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
`np.ndarray`:
|
| 201 |
+
A NumPy array representation of the images.
|
| 202 |
+
"""
|
| 203 |
+
images = images.cpu().permute(0, 2, 3, 1).float().numpy()
|
| 204 |
+
return images
|
| 205 |
+
|
| 206 |
+
@staticmethod
|
| 207 |
+
def normalize(images: np.ndarray | torch.Tensor) -> np.ndarray | torch.Tensor:
|
| 208 |
+
r"""
|
| 209 |
+
Normalize an image array to [-1,1].
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
images (`np.ndarray` or `torch.Tensor`):
|
| 213 |
+
The image array to normalize.
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
`np.ndarray` or `torch.Tensor`:
|
| 217 |
+
The normalized image array.
|
| 218 |
+
"""
|
| 219 |
+
return 2.0 * images - 1.0
|
| 220 |
+
|
| 221 |
+
@staticmethod
|
| 222 |
+
def denormalize(images: np.ndarray | torch.Tensor) -> np.ndarray | torch.Tensor:
|
| 223 |
+
r"""
|
| 224 |
+
Denormalize an image array to [0,1].
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
images (`np.ndarray` or `torch.Tensor`):
|
| 228 |
+
The image array to denormalize.
|
| 229 |
+
|
| 230 |
+
Returns:
|
| 231 |
+
`np.ndarray` or `torch.Tensor`:
|
| 232 |
+
The denormalized image array.
|
| 233 |
+
"""
|
| 234 |
+
return (images * 0.5 + 0.5).clamp(0, 1)
|
| 235 |
+
|
| 236 |
+
@staticmethod
|
| 237 |
+
def convert_to_rgb(image: PIL.Image.Image) -> PIL.Image.Image:
|
| 238 |
+
r"""
|
| 239 |
+
Converts a PIL image to RGB format.
|
| 240 |
+
|
| 241 |
+
Args:
|
| 242 |
+
image (`PIL.Image.Image`):
|
| 243 |
+
The PIL image to convert to RGB.
|
| 244 |
+
|
| 245 |
+
Returns:
|
| 246 |
+
`PIL.Image.Image`:
|
| 247 |
+
The RGB-converted PIL image.
|
| 248 |
+
"""
|
| 249 |
+
image = image.convert("RGB")
|
| 250 |
+
|
| 251 |
+
return image
|
| 252 |
+
|
| 253 |
+
@staticmethod
|
| 254 |
+
def convert_to_grayscale(image: PIL.Image.Image) -> PIL.Image.Image:
|
| 255 |
+
r"""
|
| 256 |
+
Converts a given PIL image to grayscale.
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
image (`PIL.Image.Image`):
|
| 260 |
+
The input image to convert.
|
| 261 |
+
|
| 262 |
+
Returns:
|
| 263 |
+
`PIL.Image.Image`:
|
| 264 |
+
The image converted to grayscale.
|
| 265 |
+
"""
|
| 266 |
+
image = image.convert("L")
|
| 267 |
+
|
| 268 |
+
return image
|
| 269 |
+
|
| 270 |
+
@staticmethod
|
| 271 |
+
def blur(image: PIL.Image.Image, blur_factor: int = 4) -> PIL.Image.Image:
|
| 272 |
+
r"""
|
| 273 |
+
Applies Gaussian blur to an image.
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
image (`PIL.Image.Image`):
|
| 277 |
+
The PIL image to convert to grayscale.
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
`PIL.Image.Image`:
|
| 281 |
+
The grayscale-converted PIL image.
|
| 282 |
+
"""
|
| 283 |
+
image = image.filter(ImageFilter.GaussianBlur(blur_factor))
|
| 284 |
+
|
| 285 |
+
return image
|
| 286 |
+
|
| 287 |
+
@staticmethod
|
| 288 |
+
def get_crop_region(mask_image: PIL.Image.Image, width: int, height: int, pad=0):
|
| 289 |
+
r"""
|
| 290 |
+
Finds a rectangular region that contains all masked ares in an image, and expands region to match the aspect
|
| 291 |
+
ratio of the original image; for example, if user drew mask in a 128x32 region, and the dimensions for
|
| 292 |
+
processing are 512x512, the region will be expanded to 128x128.
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
mask_image (PIL.Image.Image): Mask image.
|
| 296 |
+
width (int): Width of the image to be processed.
|
| 297 |
+
height (int): Height of the image to be processed.
|
| 298 |
+
pad (int, optional): Padding to be added to the crop region. Defaults to 0.
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
tuple: (x1, y1, x2, y2) represent a rectangular region that contains all masked ares in an image and
|
| 302 |
+
matches the original aspect ratio.
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
mask_image = mask_image.convert("L")
|
| 306 |
+
mask = np.array(mask_image)
|
| 307 |
+
|
| 308 |
+
# 1. find a rectangular region that contains all masked ares in an image
|
| 309 |
+
h, w = mask.shape
|
| 310 |
+
crop_left = 0
|
| 311 |
+
for i in range(w):
|
| 312 |
+
if not (mask[:, i] == 0).all():
|
| 313 |
+
break
|
| 314 |
+
crop_left += 1
|
| 315 |
+
|
| 316 |
+
crop_right = 0
|
| 317 |
+
for i in reversed(range(w)):
|
| 318 |
+
if not (mask[:, i] == 0).all():
|
| 319 |
+
break
|
| 320 |
+
crop_right += 1
|
| 321 |
+
|
| 322 |
+
crop_top = 0
|
| 323 |
+
for i in range(h):
|
| 324 |
+
if not (mask[i] == 0).all():
|
| 325 |
+
break
|
| 326 |
+
crop_top += 1
|
| 327 |
+
|
| 328 |
+
crop_bottom = 0
|
| 329 |
+
for i in reversed(range(h)):
|
| 330 |
+
if not (mask[i] == 0).all():
|
| 331 |
+
break
|
| 332 |
+
crop_bottom += 1
|
| 333 |
+
|
| 334 |
+
# 2. add padding to the crop region
|
| 335 |
+
x1, y1, x2, y2 = (
|
| 336 |
+
int(max(crop_left - pad, 0)),
|
| 337 |
+
int(max(crop_top - pad, 0)),
|
| 338 |
+
int(min(w - crop_right + pad, w)),
|
| 339 |
+
int(min(h - crop_bottom + pad, h)),
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# 3. expands crop region to match the aspect ratio of the image to be processed
|
| 343 |
+
ratio_crop_region = (x2 - x1) / (y2 - y1)
|
| 344 |
+
ratio_processing = width / height
|
| 345 |
+
|
| 346 |
+
if ratio_crop_region > ratio_processing:
|
| 347 |
+
desired_height = (x2 - x1) / ratio_processing
|
| 348 |
+
desired_height_diff = int(desired_height - (y2 - y1))
|
| 349 |
+
y1 -= desired_height_diff // 2
|
| 350 |
+
y2 += desired_height_diff - desired_height_diff // 2
|
| 351 |
+
if y2 >= mask_image.height:
|
| 352 |
+
diff = y2 - mask_image.height
|
| 353 |
+
y2 -= diff
|
| 354 |
+
y1 -= diff
|
| 355 |
+
if y1 < 0:
|
| 356 |
+
y2 -= y1
|
| 357 |
+
y1 -= y1
|
| 358 |
+
if y2 >= mask_image.height:
|
| 359 |
+
y2 = mask_image.height
|
| 360 |
+
else:
|
| 361 |
+
desired_width = (y2 - y1) * ratio_processing
|
| 362 |
+
desired_width_diff = int(desired_width - (x2 - x1))
|
| 363 |
+
x1 -= desired_width_diff // 2
|
| 364 |
+
x2 += desired_width_diff - desired_width_diff // 2
|
| 365 |
+
if x2 >= mask_image.width:
|
| 366 |
+
diff = x2 - mask_image.width
|
| 367 |
+
x2 -= diff
|
| 368 |
+
x1 -= diff
|
| 369 |
+
if x1 < 0:
|
| 370 |
+
x2 -= x1
|
| 371 |
+
x1 -= x1
|
| 372 |
+
if x2 >= mask_image.width:
|
| 373 |
+
x2 = mask_image.width
|
| 374 |
+
|
| 375 |
+
return x1, y1, x2, y2
|
| 376 |
+
|
| 377 |
+
def _resize_and_fill(
|
| 378 |
+
self,
|
| 379 |
+
image: PIL.Image.Image,
|
| 380 |
+
width: int,
|
| 381 |
+
height: int,
|
| 382 |
+
) -> PIL.Image.Image:
|
| 383 |
+
r"""
|
| 384 |
+
Resize the image to fit within the specified width and height, maintaining the aspect ratio, and then center
|
| 385 |
+
the image within the dimensions, filling empty with data from image.
|
| 386 |
+
|
| 387 |
+
Args:
|
| 388 |
+
image (`PIL.Image.Image`):
|
| 389 |
+
The image to resize and fill.
|
| 390 |
+
width (`int`):
|
| 391 |
+
The width to resize the image to.
|
| 392 |
+
height (`int`):
|
| 393 |
+
The height to resize the image to.
|
| 394 |
+
|
| 395 |
+
Returns:
|
| 396 |
+
`PIL.Image.Image`:
|
| 397 |
+
The resized and filled image.
|
| 398 |
+
"""
|
| 399 |
+
|
| 400 |
+
ratio = width / height
|
| 401 |
+
src_ratio = image.width / image.height
|
| 402 |
+
|
| 403 |
+
src_w = width if ratio < src_ratio else image.width * height // image.height
|
| 404 |
+
src_h = height if ratio >= src_ratio else image.height * width // image.width
|
| 405 |
+
|
| 406 |
+
resized = image.resize((src_w, src_h), resample=PIL_INTERPOLATION[self.config.resample])
|
| 407 |
+
res = Image.new("RGB", (width, height))
|
| 408 |
+
res.paste(resized, box=(width // 2 - src_w // 2, height // 2 - src_h // 2))
|
| 409 |
+
|
| 410 |
+
if ratio < src_ratio:
|
| 411 |
+
fill_height = height // 2 - src_h // 2
|
| 412 |
+
if fill_height > 0:
|
| 413 |
+
res.paste(resized.resize((width, fill_height), box=(0, 0, width, 0)), box=(0, 0))
|
| 414 |
+
res.paste(
|
| 415 |
+
resized.resize((width, fill_height), box=(0, resized.height, width, resized.height)),
|
| 416 |
+
box=(0, fill_height + src_h),
|
| 417 |
+
)
|
| 418 |
+
elif ratio > src_ratio:
|
| 419 |
+
fill_width = width // 2 - src_w // 2
|
| 420 |
+
if fill_width > 0:
|
| 421 |
+
res.paste(resized.resize((fill_width, height), box=(0, 0, 0, height)), box=(0, 0))
|
| 422 |
+
res.paste(
|
| 423 |
+
resized.resize((fill_width, height), box=(resized.width, 0, resized.width, height)),
|
| 424 |
+
box=(fill_width + src_w, 0),
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
return res
|
| 428 |
+
|
| 429 |
+
def _resize_and_crop(
|
| 430 |
+
self,
|
| 431 |
+
image: PIL.Image.Image,
|
| 432 |
+
width: int,
|
| 433 |
+
height: int,
|
| 434 |
+
) -> PIL.Image.Image:
|
| 435 |
+
r"""
|
| 436 |
+
Resize the image to fit within the specified width and height, maintaining the aspect ratio, and then center
|
| 437 |
+
the image within the dimensions, cropping the excess.
|
| 438 |
+
|
| 439 |
+
Args:
|
| 440 |
+
image (`PIL.Image.Image`):
|
| 441 |
+
The image to resize and crop.
|
| 442 |
+
width (`int`):
|
| 443 |
+
The width to resize the image to.
|
| 444 |
+
height (`int`):
|
| 445 |
+
The height to resize the image to.
|
| 446 |
+
|
| 447 |
+
Returns:
|
| 448 |
+
`PIL.Image.Image`:
|
| 449 |
+
The resized and cropped image.
|
| 450 |
+
"""
|
| 451 |
+
ratio = width / height
|
| 452 |
+
src_ratio = image.width / image.height
|
| 453 |
+
|
| 454 |
+
src_w = width if ratio > src_ratio else image.width * height // image.height
|
| 455 |
+
src_h = height if ratio <= src_ratio else image.height * width // image.width
|
| 456 |
+
|
| 457 |
+
resized = image.resize((src_w, src_h), resample=PIL_INTERPOLATION[self.config.resample])
|
| 458 |
+
res = Image.new("RGB", (width, height))
|
| 459 |
+
res.paste(resized, box=(width // 2 - src_w // 2, height // 2 - src_h // 2))
|
| 460 |
+
return res
|
| 461 |
+
|
| 462 |
+
def resize(
|
| 463 |
+
self,
|
| 464 |
+
image: PIL.Image.Image | np.ndarray | torch.Tensor,
|
| 465 |
+
height: int,
|
| 466 |
+
width: int,
|
| 467 |
+
resize_mode: str = "default", # "default", "fill", "crop"
|
| 468 |
+
) -> PIL.Image.Image | np.ndarray | torch.Tensor:
|
| 469 |
+
"""
|
| 470 |
+
Resize image.
|
| 471 |
+
|
| 472 |
+
Args:
|
| 473 |
+
image (`PIL.Image.Image`, `np.ndarray` or `torch.Tensor`):
|
| 474 |
+
The image input, can be a PIL image, numpy array or pytorch tensor.
|
| 475 |
+
height (`int`):
|
| 476 |
+
The height to resize to.
|
| 477 |
+
width (`int`):
|
| 478 |
+
The width to resize to.
|
| 479 |
+
resize_mode (`str`, *optional*, defaults to `default`):
|
| 480 |
+
The resize mode to use, can be one of `default` or `fill`. If `default`, will resize the image to fit
|
| 481 |
+
within the specified width and height, and it may not maintaining the original aspect ratio. If `fill`,
|
| 482 |
+
will resize the image to fit within the specified width and height, maintaining the aspect ratio, and
|
| 483 |
+
then center the image within the dimensions, filling empty with data from image. If `crop`, will resize
|
| 484 |
+
the image to fit within the specified width and height, maintaining the aspect ratio, and then center
|
| 485 |
+
the image within the dimensions, cropping the excess. Note that resize_mode `fill` and `crop` are only
|
| 486 |
+
supported for PIL image input.
|
| 487 |
+
|
| 488 |
+
Returns:
|
| 489 |
+
`PIL.Image.Image`, `np.ndarray` or `torch.Tensor`:
|
| 490 |
+
The resized image.
|
| 491 |
+
"""
|
| 492 |
+
if resize_mode != "default" and not isinstance(image, PIL.Image.Image):
|
| 493 |
+
raise ValueError(f"Only PIL image input is supported for resize_mode {resize_mode}")
|
| 494 |
+
if isinstance(image, PIL.Image.Image):
|
| 495 |
+
if resize_mode == "default":
|
| 496 |
+
image = image.resize(
|
| 497 |
+
(width, height),
|
| 498 |
+
resample=PIL_INTERPOLATION[self.config.resample],
|
| 499 |
+
reducing_gap=self.config.reducing_gap,
|
| 500 |
+
)
|
| 501 |
+
elif resize_mode == "fill":
|
| 502 |
+
image = self._resize_and_fill(image, width, height)
|
| 503 |
+
elif resize_mode == "crop":
|
| 504 |
+
image = self._resize_and_crop(image, width, height)
|
| 505 |
+
else:
|
| 506 |
+
raise ValueError(f"resize_mode {resize_mode} is not supported")
|
| 507 |
+
|
| 508 |
+
elif isinstance(image, torch.Tensor):
|
| 509 |
+
image = torch.nn.functional.interpolate(
|
| 510 |
+
image,
|
| 511 |
+
size=(height, width),
|
| 512 |
+
)
|
| 513 |
+
elif isinstance(image, np.ndarray):
|
| 514 |
+
image = self.numpy_to_pt(image)
|
| 515 |
+
image = torch.nn.functional.interpolate(
|
| 516 |
+
image,
|
| 517 |
+
size=(height, width),
|
| 518 |
+
)
|
| 519 |
+
image = self.pt_to_numpy(image)
|
| 520 |
+
|
| 521 |
+
return image
|
| 522 |
+
|
| 523 |
+
def binarize(self, image: PIL.Image.Image) -> PIL.Image.Image:
|
| 524 |
+
"""
|
| 525 |
+
Create a mask.
|
| 526 |
+
|
| 527 |
+
Args:
|
| 528 |
+
image (`PIL.Image.Image`):
|
| 529 |
+
The image input, should be a PIL image.
|
| 530 |
+
|
| 531 |
+
Returns:
|
| 532 |
+
`PIL.Image.Image`:
|
| 533 |
+
The binarized image. Values less than 0.5 are set to 0, values greater than 0.5 are set to 1.
|
| 534 |
+
"""
|
| 535 |
+
image[image < 0.5] = 0
|
| 536 |
+
image[image >= 0.5] = 1
|
| 537 |
+
|
| 538 |
+
return image
|
| 539 |
+
|
| 540 |
+
def _denormalize_conditionally(
|
| 541 |
+
self, images: torch.Tensor, do_denormalize: list[bool] | None = None
|
| 542 |
+
) -> torch.Tensor:
|
| 543 |
+
r"""
|
| 544 |
+
Denormalize a batch of images based on a condition list.
|
| 545 |
+
|
| 546 |
+
Args:
|
| 547 |
+
images (`torch.Tensor`):
|
| 548 |
+
The input image tensor.
|
| 549 |
+
do_denormalize (`Optional[list[bool]`, *optional*, defaults to `None`):
|
| 550 |
+
A list of booleans indicating whether to denormalize each image in the batch. If `None`, will use the
|
| 551 |
+
value of `do_normalize` in the `VaeImageProcessor` config.
|
| 552 |
+
"""
|
| 553 |
+
if do_denormalize is None:
|
| 554 |
+
return self.denormalize(images) if self.config.do_normalize else images
|
| 555 |
+
|
| 556 |
+
return torch.stack(
|
| 557 |
+
[self.denormalize(images[i]) if do_denormalize[i] else images[i] for i in range(images.shape[0])]
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
def get_default_height_width(
|
| 561 |
+
self,
|
| 562 |
+
image: PIL.Image.Image | np.ndarray | torch.Tensor,
|
| 563 |
+
height: int | None = None,
|
| 564 |
+
width: int | None = None,
|
| 565 |
+
) -> tuple[int, int]:
|
| 566 |
+
r"""
|
| 567 |
+
Returns the height and width of the image, downscaled to the next integer multiple of `vae_scale_factor`.
|
| 568 |
+
|
| 569 |
+
Args:
|
| 570 |
+
image (`PIL.Image.Image | np.ndarray | torch.Tensor`):
|
| 571 |
+
The image input, which can be a PIL image, NumPy array, or PyTorch tensor. If it is a NumPy array, it
|
| 572 |
+
should have shape `[batch, height, width]` or `[batch, height, width, channels]`. If it is a PyTorch
|
| 573 |
+
tensor, it should have shape `[batch, channels, height, width]`.
|
| 574 |
+
height (`int | None`, *optional*, defaults to `None`):
|
| 575 |
+
The height of the preprocessed image. If `None`, the height of the `image` input will be used.
|
| 576 |
+
width (`int | None`, *optional*, defaults to `None`):
|
| 577 |
+
The width of the preprocessed image. If `None`, the width of the `image` input will be used.
|
| 578 |
+
|
| 579 |
+
Returns:
|
| 580 |
+
`tuple[int, int]`:
|
| 581 |
+
A tuple containing the height and width, both resized to the nearest integer multiple of
|
| 582 |
+
`vae_scale_factor`.
|
| 583 |
+
"""
|
| 584 |
+
|
| 585 |
+
if height is None:
|
| 586 |
+
if isinstance(image, PIL.Image.Image):
|
| 587 |
+
height = image.height
|
| 588 |
+
elif isinstance(image, torch.Tensor):
|
| 589 |
+
height = image.shape[2]
|
| 590 |
+
else:
|
| 591 |
+
height = image.shape[1]
|
| 592 |
+
|
| 593 |
+
if width is None:
|
| 594 |
+
if isinstance(image, PIL.Image.Image):
|
| 595 |
+
width = image.width
|
| 596 |
+
elif isinstance(image, torch.Tensor):
|
| 597 |
+
width = image.shape[3]
|
| 598 |
+
else:
|
| 599 |
+
width = image.shape[2]
|
| 600 |
+
|
| 601 |
+
width, height = (
|
| 602 |
+
x - x % self.config.vae_scale_factor for x in (width, height)
|
| 603 |
+
) # resize to integer multiple of vae_scale_factor
|
| 604 |
+
|
| 605 |
+
return height, width
|
| 606 |
+
|
| 607 |
+
def preprocess(
|
| 608 |
+
self,
|
| 609 |
+
image: PipelineImageInput,
|
| 610 |
+
height: int | None = None,
|
| 611 |
+
width: int | None = None,
|
| 612 |
+
resize_mode: str = "default", # "default", "fill", "crop"
|
| 613 |
+
crops_coords: tuple[int, int, int, int] | None = None,
|
| 614 |
+
) -> torch.Tensor:
|
| 615 |
+
"""
|
| 616 |
+
Preprocess the image input.
|
| 617 |
+
|
| 618 |
+
Args:
|
| 619 |
+
image (`PipelineImageInput`):
|
| 620 |
+
The image input, accepted formats are PIL images, NumPy arrays, PyTorch tensors; Also accept list of
|
| 621 |
+
supported formats.
|
| 622 |
+
height (`int`, *optional*):
|
| 623 |
+
The height in preprocessed image. If `None`, will use the `get_default_height_width()` to get default
|
| 624 |
+
height.
|
| 625 |
+
width (`int`, *optional*):
|
| 626 |
+
The width in preprocessed. If `None`, will use get_default_height_width()` to get the default width.
|
| 627 |
+
resize_mode (`str`, *optional*, defaults to `default`):
|
| 628 |
+
The resize mode, can be one of `default` or `fill`. If `default`, will resize the image to fit within
|
| 629 |
+
the specified width and height, and it may not maintaining the original aspect ratio. If `fill`, will
|
| 630 |
+
resize the image to fit within the specified width and height, maintaining the aspect ratio, and then
|
| 631 |
+
center the image within the dimensions, filling empty with data from image. If `crop`, will resize the
|
| 632 |
+
image to fit within the specified width and height, maintaining the aspect ratio, and then center the
|
| 633 |
+
image within the dimensions, cropping the excess. Note that resize_mode `fill` and `crop` are only
|
| 634 |
+
supported for PIL image input.
|
| 635 |
+
crops_coords (`list[tuple[int, int, int, int]]`, *optional*, defaults to `None`):
|
| 636 |
+
The crop coordinates for each image in the batch. If `None`, will not crop the image.
|
| 637 |
+
|
| 638 |
+
Returns:
|
| 639 |
+
`torch.Tensor`:
|
| 640 |
+
The preprocessed image.
|
| 641 |
+
"""
|
| 642 |
+
supported_formats = (PIL.Image.Image, np.ndarray, torch.Tensor)
|
| 643 |
+
|
| 644 |
+
# Expand the missing dimension for 3-dimensional pytorch tensor or numpy array that represents grayscale image
|
| 645 |
+
if self.config.do_convert_grayscale and isinstance(image, (torch.Tensor, np.ndarray)) and image.ndim == 3:
|
| 646 |
+
if isinstance(image, torch.Tensor):
|
| 647 |
+
# if image is a pytorch tensor could have 2 possible shapes:
|
| 648 |
+
# 1. batch x height x width: we should insert the channel dimension at position 1
|
| 649 |
+
# 2. channel x height x width: we should insert batch dimension at position 0,
|
| 650 |
+
# however, since both channel and batch dimension has same size 1, it is same to insert at position 1
|
| 651 |
+
# for simplicity, we insert a dimension of size 1 at position 1 for both cases
|
| 652 |
+
image = image.unsqueeze(1)
|
| 653 |
+
else:
|
| 654 |
+
# if it is a numpy array, it could have 2 possible shapes:
|
| 655 |
+
# 1. batch x height x width: insert channel dimension on last position
|
| 656 |
+
# 2. height x width x channel: insert batch dimension on first position
|
| 657 |
+
if image.shape[-1] == 1:
|
| 658 |
+
image = np.expand_dims(image, axis=0)
|
| 659 |
+
else:
|
| 660 |
+
image = np.expand_dims(image, axis=-1)
|
| 661 |
+
|
| 662 |
+
if isinstance(image, list) and isinstance(image[0], np.ndarray) and image[0].ndim == 4:
|
| 663 |
+
warnings.warn(
|
| 664 |
+
"Passing `image` as a list of 4d np.ndarray is deprecated."
|
| 665 |
+
"Please concatenate the list along the batch dimension and pass it as a single 4d np.ndarray",
|
| 666 |
+
FutureWarning,
|
| 667 |
+
)
|
| 668 |
+
image = np.concatenate(image, axis=0)
|
| 669 |
+
if isinstance(image, list) and isinstance(image[0], torch.Tensor) and image[0].ndim == 4:
|
| 670 |
+
warnings.warn(
|
| 671 |
+
"Passing `image` as a list of 4d torch.Tensor is deprecated."
|
| 672 |
+
"Please concatenate the list along the batch dimension and pass it as a single 4d torch.Tensor",
|
| 673 |
+
FutureWarning,
|
| 674 |
+
)
|
| 675 |
+
image = torch.cat(image, axis=0)
|
| 676 |
+
|
| 677 |
+
if not is_valid_image_imagelist(image):
|
| 678 |
+
raise ValueError(
|
| 679 |
+
f"Input is in incorrect format. Currently, we only support {', '.join(str(x) for x in supported_formats)}"
|
| 680 |
+
)
|
| 681 |
+
if not isinstance(image, list):
|
| 682 |
+
image = [image]
|
| 683 |
+
|
| 684 |
+
if isinstance(image[0], PIL.Image.Image):
|
| 685 |
+
if crops_coords is not None:
|
| 686 |
+
image = [i.crop(crops_coords) for i in image]
|
| 687 |
+
if self.config.do_resize:
|
| 688 |
+
height, width = self.get_default_height_width(image[0], height, width)
|
| 689 |
+
image = [self.resize(i, height, width, resize_mode=resize_mode) for i in image]
|
| 690 |
+
if self.config.do_convert_rgb:
|
| 691 |
+
image = [self.convert_to_rgb(i) for i in image]
|
| 692 |
+
elif self.config.do_convert_grayscale:
|
| 693 |
+
image = [self.convert_to_grayscale(i) for i in image]
|
| 694 |
+
image = self.pil_to_numpy(image) # to np
|
| 695 |
+
image = self.numpy_to_pt(image) # to pt
|
| 696 |
+
|
| 697 |
+
elif isinstance(image[0], np.ndarray):
|
| 698 |
+
image = np.concatenate(image, axis=0) if image[0].ndim == 4 else np.stack(image, axis=0)
|
| 699 |
+
|
| 700 |
+
image = self.numpy_to_pt(image)
|
| 701 |
+
|
| 702 |
+
height, width = self.get_default_height_width(image, height, width)
|
| 703 |
+
if self.config.do_resize:
|
| 704 |
+
image = self.resize(image, height, width)
|
| 705 |
+
|
| 706 |
+
elif isinstance(image[0], torch.Tensor):
|
| 707 |
+
image = torch.cat(image, axis=0) if image[0].ndim == 4 else torch.stack(image, axis=0)
|
| 708 |
+
|
| 709 |
+
if self.config.do_convert_grayscale and image.ndim == 3:
|
| 710 |
+
image = image.unsqueeze(1)
|
| 711 |
+
|
| 712 |
+
channel = image.shape[1]
|
| 713 |
+
# don't need any preprocess if the image is latents
|
| 714 |
+
if channel == self.config.vae_latent_channels:
|
| 715 |
+
return image
|
| 716 |
+
|
| 717 |
+
height, width = self.get_default_height_width(image, height, width)
|
| 718 |
+
if self.config.do_resize:
|
| 719 |
+
image = self.resize(image, height, width)
|
| 720 |
+
|
| 721 |
+
# expected range [0,1], normalize to [-1,1]
|
| 722 |
+
do_normalize = self.config.do_normalize
|
| 723 |
+
if do_normalize and image.min() < 0:
|
| 724 |
+
warnings.warn(
|
| 725 |
+
"Passing `image` as torch tensor with value range in [-1,1] is deprecated. The expected value range for image tensor is [0,1] "
|
| 726 |
+
f"when passing as pytorch tensor or numpy Array. You passed `image` with value range [{image.min()},{image.max()}]",
|
| 727 |
+
FutureWarning,
|
| 728 |
+
)
|
| 729 |
+
do_normalize = False
|
| 730 |
+
if do_normalize:
|
| 731 |
+
image = self.normalize(image)
|
| 732 |
+
|
| 733 |
+
if self.config.do_binarize:
|
| 734 |
+
image = self.binarize(image)
|
| 735 |
+
|
| 736 |
+
return image
|
| 737 |
+
|
| 738 |
+
def postprocess(
|
| 739 |
+
self,
|
| 740 |
+
image: torch.Tensor,
|
| 741 |
+
output_type: str = "pil",
|
| 742 |
+
do_denormalize: list[bool] | None = None,
|
| 743 |
+
) -> PIL.Image.Image | np.ndarray | torch.Tensor:
|
| 744 |
+
"""
|
| 745 |
+
Postprocess the image output from tensor to `output_type`.
|
| 746 |
+
|
| 747 |
+
Args:
|
| 748 |
+
image (`torch.Tensor`):
|
| 749 |
+
The image input, should be a pytorch tensor with shape `B x C x H x W`.
|
| 750 |
+
output_type (`str`, *optional*, defaults to `pil`):
|
| 751 |
+
The output type of the image, can be one of `pil`, `np`, `pt`, `latent`.
|
| 752 |
+
do_denormalize (`list[bool]`, *optional*, defaults to `None`):
|
| 753 |
+
Whether to denormalize the image to [0,1]. If `None`, will use the value of `do_normalize` in the
|
| 754 |
+
`VaeImageProcessor` config.
|
| 755 |
+
|
| 756 |
+
Returns:
|
| 757 |
+
`PIL.Image.Image`, `np.ndarray` or `torch.Tensor`:
|
| 758 |
+
The postprocessed image.
|
| 759 |
+
"""
|
| 760 |
+
if not isinstance(image, torch.Tensor):
|
| 761 |
+
raise ValueError(
|
| 762 |
+
f"Input for postprocessing is in incorrect format: {type(image)}. We only support pytorch tensor"
|
| 763 |
+
)
|
| 764 |
+
if output_type not in ["latent", "pt", "np", "pil"]:
|
| 765 |
+
deprecation_message = (
|
| 766 |
+
f"the output_type {output_type} is outdated and has been set to `np`. Please make sure to set it to one of these instead: "
|
| 767 |
+
"`pil`, `np`, `pt`, `latent`"
|
| 768 |
+
)
|
| 769 |
+
deprecate("Unsupported output_type", "1.0.0", deprecation_message, standard_warn=False)
|
| 770 |
+
output_type = "np"
|
| 771 |
+
|
| 772 |
+
if output_type == "latent":
|
| 773 |
+
return image
|
| 774 |
+
|
| 775 |
+
image = self._denormalize_conditionally(image, do_denormalize)
|
| 776 |
+
|
| 777 |
+
if output_type == "pt":
|
| 778 |
+
return image
|
| 779 |
+
|
| 780 |
+
image = self.pt_to_numpy(image)
|
| 781 |
+
|
| 782 |
+
if output_type == "np":
|
| 783 |
+
return image
|
| 784 |
+
|
| 785 |
+
if output_type == "pil":
|
| 786 |
+
return self.numpy_to_pil(image)
|
| 787 |
+
|
| 788 |
+
def apply_overlay(
|
| 789 |
+
self,
|
| 790 |
+
mask: PIL.Image.Image,
|
| 791 |
+
init_image: PIL.Image.Image,
|
| 792 |
+
image: PIL.Image.Image,
|
| 793 |
+
crop_coords: tuple[int, int, int, int] | None = None,
|
| 794 |
+
) -> PIL.Image.Image:
|
| 795 |
+
r"""
|
| 796 |
+
Applies an overlay of the mask and the inpainted image on the original image.
|
| 797 |
+
|
| 798 |
+
Args:
|
| 799 |
+
mask (`PIL.Image.Image`):
|
| 800 |
+
The mask image that highlights regions to overlay.
|
| 801 |
+
init_image (`PIL.Image.Image`):
|
| 802 |
+
The original image to which the overlay is applied.
|
| 803 |
+
image (`PIL.Image.Image`):
|
| 804 |
+
The image to overlay onto the original.
|
| 805 |
+
crop_coords (`tuple[int, int, int, int]`, *optional*):
|
| 806 |
+
Coordinates to crop the image. If provided, the image will be cropped accordingly.
|
| 807 |
+
|
| 808 |
+
Returns:
|
| 809 |
+
`PIL.Image.Image`:
|
| 810 |
+
The final image with the overlay applied.
|
| 811 |
+
"""
|
| 812 |
+
|
| 813 |
+
width, height = init_image.width, init_image.height
|
| 814 |
+
|
| 815 |
+
init_image_masked = PIL.Image.new("RGBa", (width, height))
|
| 816 |
+
init_image_masked.paste(init_image.convert("RGBA").convert("RGBa"), mask=ImageOps.invert(mask.convert("L")))
|
| 817 |
+
|
| 818 |
+
init_image_masked = init_image_masked.convert("RGBA")
|
| 819 |
+
|
| 820 |
+
if crop_coords is not None:
|
| 821 |
+
x, y, x2, y2 = crop_coords
|
| 822 |
+
w = x2 - x
|
| 823 |
+
h = y2 - y
|
| 824 |
+
base_image = PIL.Image.new("RGBA", (width, height))
|
| 825 |
+
image = self.resize(image, height=h, width=w, resize_mode="crop")
|
| 826 |
+
base_image.paste(image, (x, y))
|
| 827 |
+
image = base_image.convert("RGB")
|
| 828 |
+
|
| 829 |
+
image = image.convert("RGBA")
|
| 830 |
+
image.alpha_composite(init_image_masked)
|
| 831 |
+
image = image.convert("RGB")
|
| 832 |
+
|
| 833 |
+
return image
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
class InpaintProcessor(ConfigMixin):
|
| 837 |
+
"""
|
| 838 |
+
Image processor for inpainting image and mask.
|
| 839 |
+
"""
|
| 840 |
+
|
| 841 |
+
config_name = CONFIG_NAME
|
| 842 |
+
|
| 843 |
+
@register_to_config
|
| 844 |
+
def __init__(
|
| 845 |
+
self,
|
| 846 |
+
do_resize: bool = True,
|
| 847 |
+
vae_scale_factor: int = 8,
|
| 848 |
+
vae_latent_channels: int = 4,
|
| 849 |
+
resample: str = "lanczos",
|
| 850 |
+
reducing_gap: int | None = None,
|
| 851 |
+
do_normalize: bool = True,
|
| 852 |
+
do_binarize: bool = False,
|
| 853 |
+
do_convert_grayscale: bool = False,
|
| 854 |
+
mask_do_normalize: bool = False,
|
| 855 |
+
mask_do_binarize: bool = True,
|
| 856 |
+
mask_do_convert_grayscale: bool = True,
|
| 857 |
+
):
|
| 858 |
+
super().__init__()
|
| 859 |
+
|
| 860 |
+
self._image_processor = VaeImageProcessor(
|
| 861 |
+
do_resize=do_resize,
|
| 862 |
+
vae_scale_factor=vae_scale_factor,
|
| 863 |
+
vae_latent_channels=vae_latent_channels,
|
| 864 |
+
resample=resample,
|
| 865 |
+
reducing_gap=reducing_gap,
|
| 866 |
+
do_normalize=do_normalize,
|
| 867 |
+
do_binarize=do_binarize,
|
| 868 |
+
do_convert_grayscale=do_convert_grayscale,
|
| 869 |
+
)
|
| 870 |
+
self._mask_processor = VaeImageProcessor(
|
| 871 |
+
do_resize=do_resize,
|
| 872 |
+
vae_scale_factor=vae_scale_factor,
|
| 873 |
+
vae_latent_channels=vae_latent_channels,
|
| 874 |
+
resample=resample,
|
| 875 |
+
reducing_gap=reducing_gap,
|
| 876 |
+
do_normalize=mask_do_normalize,
|
| 877 |
+
do_binarize=mask_do_binarize,
|
| 878 |
+
do_convert_grayscale=mask_do_convert_grayscale,
|
| 879 |
+
)
|
| 880 |
+
|
| 881 |
+
def preprocess(
|
| 882 |
+
self,
|
| 883 |
+
image: PIL.Image.Image,
|
| 884 |
+
mask: PIL.Image.Image | None = None,
|
| 885 |
+
height: int | None = None,
|
| 886 |
+
width: int | None = None,
|
| 887 |
+
padding_mask_crop: int | None = None,
|
| 888 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 889 |
+
"""
|
| 890 |
+
Preprocess the image and mask.
|
| 891 |
+
"""
|
| 892 |
+
if mask is None and padding_mask_crop is not None:
|
| 893 |
+
raise ValueError("mask must be provided if padding_mask_crop is provided")
|
| 894 |
+
|
| 895 |
+
# if mask is None, same behavior as regular image processor
|
| 896 |
+
if mask is None:
|
| 897 |
+
return self._image_processor.preprocess(image, height=height, width=width)
|
| 898 |
+
|
| 899 |
+
if padding_mask_crop is not None:
|
| 900 |
+
crops_coords = self._image_processor.get_crop_region(mask, width, height, pad=padding_mask_crop)
|
| 901 |
+
resize_mode = "fill"
|
| 902 |
+
else:
|
| 903 |
+
crops_coords = None
|
| 904 |
+
resize_mode = "default"
|
| 905 |
+
|
| 906 |
+
processed_image = self._image_processor.preprocess(
|
| 907 |
+
image,
|
| 908 |
+
height=height,
|
| 909 |
+
width=width,
|
| 910 |
+
crops_coords=crops_coords,
|
| 911 |
+
resize_mode=resize_mode,
|
| 912 |
+
)
|
| 913 |
+
|
| 914 |
+
processed_mask = self._mask_processor.preprocess(
|
| 915 |
+
mask,
|
| 916 |
+
height=height,
|
| 917 |
+
width=width,
|
| 918 |
+
resize_mode=resize_mode,
|
| 919 |
+
crops_coords=crops_coords,
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
if crops_coords is not None:
|
| 923 |
+
postprocessing_kwargs = {
|
| 924 |
+
"crops_coords": crops_coords,
|
| 925 |
+
"original_image": image,
|
| 926 |
+
"original_mask": mask,
|
| 927 |
+
}
|
| 928 |
+
else:
|
| 929 |
+
postprocessing_kwargs = {
|
| 930 |
+
"crops_coords": None,
|
| 931 |
+
"original_image": None,
|
| 932 |
+
"original_mask": None,
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
return processed_image, processed_mask, postprocessing_kwargs
|
| 936 |
+
|
| 937 |
+
def postprocess(
|
| 938 |
+
self,
|
| 939 |
+
image: torch.Tensor,
|
| 940 |
+
output_type: str = "pil",
|
| 941 |
+
original_image: PIL.Image.Image | None = None,
|
| 942 |
+
original_mask: PIL.Image.Image | None = None,
|
| 943 |
+
crops_coords: tuple[int, int, int, int] | None = None,
|
| 944 |
+
) -> tuple[PIL.Image.Image, PIL.Image.Image]:
|
| 945 |
+
"""
|
| 946 |
+
Postprocess the image, optionally apply mask overlay
|
| 947 |
+
"""
|
| 948 |
+
image = self._image_processor.postprocess(
|
| 949 |
+
image,
|
| 950 |
+
output_type=output_type,
|
| 951 |
+
)
|
| 952 |
+
# optionally apply the mask overlay
|
| 953 |
+
if crops_coords is not None and (original_image is None or original_mask is None):
|
| 954 |
+
raise ValueError("original_image and original_mask must be provided if crops_coords is provided")
|
| 955 |
+
|
| 956 |
+
elif crops_coords is not None and output_type != "pil":
|
| 957 |
+
raise ValueError("output_type must be 'pil' if crops_coords is provided")
|
| 958 |
+
|
| 959 |
+
elif crops_coords is not None:
|
| 960 |
+
image = [
|
| 961 |
+
self._image_processor.apply_overlay(original_mask, original_image, i, crops_coords) for i in image
|
| 962 |
+
]
|
| 963 |
+
|
| 964 |
+
return image
|
| 965 |
+
|
| 966 |
+
|
| 967 |
+
class VaeImageProcessorLDM3D(VaeImageProcessor):
|
| 968 |
+
"""
|
| 969 |
+
Image processor for VAE LDM3D.
|
| 970 |
+
|
| 971 |
+
Args:
|
| 972 |
+
do_resize (`bool`, *optional*, defaults to `True`):
|
| 973 |
+
Whether to downscale the image's (height, width) dimensions to multiples of `vae_scale_factor`.
|
| 974 |
+
vae_scale_factor (`int`, *optional*, defaults to `8`):
|
| 975 |
+
VAE scale factor. If `do_resize` is `True`, the image is automatically resized to multiples of this factor.
|
| 976 |
+
resample (`str`, *optional*, defaults to `lanczos`):
|
| 977 |
+
Resampling filter to use when resizing the image.
|
| 978 |
+
do_normalize (`bool`, *optional*, defaults to `True`):
|
| 979 |
+
Whether to normalize the image to [-1,1].
|
| 980 |
+
"""
|
| 981 |
+
|
| 982 |
+
config_name = CONFIG_NAME
|
| 983 |
+
|
| 984 |
+
@register_to_config
|
| 985 |
+
def __init__(
|
| 986 |
+
self,
|
| 987 |
+
do_resize: bool = True,
|
| 988 |
+
vae_scale_factor: int = 8,
|
| 989 |
+
resample: str = "lanczos",
|
| 990 |
+
do_normalize: bool = True,
|
| 991 |
+
):
|
| 992 |
+
super().__init__()
|
| 993 |
+
|
| 994 |
+
@staticmethod
|
| 995 |
+
def numpy_to_pil(images: np.ndarray) -> list[PIL.Image.Image]:
|
| 996 |
+
r"""
|
| 997 |
+
Convert a NumPy image or a batch of images to a list of PIL images.
|
| 998 |
+
|
| 999 |
+
Args:
|
| 1000 |
+
images (`np.ndarray`):
|
| 1001 |
+
The input NumPy array of images, which can be a single image or a batch.
|
| 1002 |
+
|
| 1003 |
+
Returns:
|
| 1004 |
+
`list[PIL.Image.Image]`:
|
| 1005 |
+
A list of PIL images converted from the input NumPy array.
|
| 1006 |
+
"""
|
| 1007 |
+
if images.ndim == 3:
|
| 1008 |
+
images = images[None, ...]
|
| 1009 |
+
images = (images * 255).round().astype("uint8")
|
| 1010 |
+
if images.shape[-1] == 1:
|
| 1011 |
+
# special case for grayscale (single channel) images
|
| 1012 |
+
pil_images = [Image.fromarray(image.squeeze(), mode="L") for image in images]
|
| 1013 |
+
else:
|
| 1014 |
+
pil_images = [Image.fromarray(image[:, :, :3]) for image in images]
|
| 1015 |
+
|
| 1016 |
+
return pil_images
|
| 1017 |
+
|
| 1018 |
+
@staticmethod
|
| 1019 |
+
def depth_pil_to_numpy(images: list[PIL.Image.Image] | PIL.Image.Image) -> np.ndarray:
|
| 1020 |
+
r"""
|
| 1021 |
+
Convert a PIL image or a list of PIL images to NumPy arrays.
|
| 1022 |
+
|
| 1023 |
+
Args:
|
| 1024 |
+
images (`list[PIL.Image.Image, PIL.Image.Image]`):
|
| 1025 |
+
The input image or list of images to be converted.
|
| 1026 |
+
|
| 1027 |
+
Returns:
|
| 1028 |
+
`np.ndarray`:
|
| 1029 |
+
A NumPy array of the converted images.
|
| 1030 |
+
"""
|
| 1031 |
+
if not isinstance(images, list):
|
| 1032 |
+
images = [images]
|
| 1033 |
+
|
| 1034 |
+
images = [np.array(image).astype(np.float32) / (2**16 - 1) for image in images]
|
| 1035 |
+
images = np.stack(images, axis=0)
|
| 1036 |
+
return images
|
| 1037 |
+
|
| 1038 |
+
@staticmethod
|
| 1039 |
+
def rgblike_to_depthmap(image: np.ndarray | torch.Tensor) -> np.ndarray | torch.Tensor:
|
| 1040 |
+
r"""
|
| 1041 |
+
Convert an RGB-like depth image to a depth map.
|
| 1042 |
+
"""
|
| 1043 |
+
# 1. Cast the tensor to a larger integer type (e.g., int32)
|
| 1044 |
+
# to safely perform the multiplication by 256.
|
| 1045 |
+
# 2. Perform the 16-bit combination: High-byte * 256 + Low-byte.
|
| 1046 |
+
# 3. Cast the final result to the desired depth map type (uint16) if needed
|
| 1047 |
+
# before returning, though leaving it as int32/int64 is often safer
|
| 1048 |
+
# for return value from a library function.
|
| 1049 |
+
|
| 1050 |
+
if isinstance(image, torch.Tensor):
|
| 1051 |
+
# Cast to a safe dtype (e.g., int32 or int64) for the calculation
|
| 1052 |
+
original_dtype = image.dtype
|
| 1053 |
+
image_safe = image.to(torch.int32)
|
| 1054 |
+
|
| 1055 |
+
# Calculate the depth map
|
| 1056 |
+
depth_map = image_safe[:, :, 1] * 256 + image_safe[:, :, 2]
|
| 1057 |
+
|
| 1058 |
+
# You may want to cast the final result to uint16, but casting to a
|
| 1059 |
+
# larger int type (like int32) is sufficient to fix the overflow.
|
| 1060 |
+
# depth_map = depth_map.to(torch.uint16) # Uncomment if uint16 is strictly required
|
| 1061 |
+
return depth_map.to(original_dtype)
|
| 1062 |
+
|
| 1063 |
+
elif isinstance(image, np.ndarray):
|
| 1064 |
+
# NumPy equivalent: Cast to a safe dtype (e.g., np.int32)
|
| 1065 |
+
original_dtype = image.dtype
|
| 1066 |
+
image_safe = image.astype(np.int32)
|
| 1067 |
+
|
| 1068 |
+
# Calculate the depth map
|
| 1069 |
+
depth_map = image_safe[:, :, 1] * 256 + image_safe[:, :, 2]
|
| 1070 |
+
|
| 1071 |
+
# depth_map = depth_map.astype(np.uint16) # Uncomment if uint16 is strictly required
|
| 1072 |
+
return depth_map.astype(original_dtype)
|
| 1073 |
+
else:
|
| 1074 |
+
raise TypeError("Input image must be a torch.Tensor or np.ndarray")
|
| 1075 |
+
|
| 1076 |
+
def numpy_to_depth(self, images: np.ndarray) -> list[PIL.Image.Image]:
|
| 1077 |
+
r"""
|
| 1078 |
+
Convert a NumPy depth image or a batch of images to a list of PIL images.
|
| 1079 |
+
|
| 1080 |
+
Args:
|
| 1081 |
+
images (`np.ndarray`):
|
| 1082 |
+
The input NumPy array of depth images, which can be a single image or a batch.
|
| 1083 |
+
|
| 1084 |
+
Returns:
|
| 1085 |
+
`list[PIL.Image.Image]`:
|
| 1086 |
+
A list of PIL images converted from the input NumPy depth images.
|
| 1087 |
+
"""
|
| 1088 |
+
if images.ndim == 3:
|
| 1089 |
+
images = images[None, ...]
|
| 1090 |
+
images_depth = images[:, :, :, 3:]
|
| 1091 |
+
if images.shape[-1] == 6:
|
| 1092 |
+
images_depth = (images_depth * 255).round().astype("uint8")
|
| 1093 |
+
pil_images = [
|
| 1094 |
+
Image.fromarray(self.rgblike_to_depthmap(image_depth), mode="I;16") for image_depth in images_depth
|
| 1095 |
+
]
|
| 1096 |
+
elif images.shape[-1] == 4:
|
| 1097 |
+
images_depth = (images_depth * 65535.0).astype(np.uint16)
|
| 1098 |
+
pil_images = [Image.fromarray(image_depth, mode="I;16") for image_depth in images_depth]
|
| 1099 |
+
else:
|
| 1100 |
+
raise Exception("Not supported")
|
| 1101 |
+
|
| 1102 |
+
return pil_images
|
| 1103 |
+
|
| 1104 |
+
def postprocess(
|
| 1105 |
+
self,
|
| 1106 |
+
image: torch.Tensor,
|
| 1107 |
+
output_type: str = "pil",
|
| 1108 |
+
do_denormalize: list[bool] | None = None,
|
| 1109 |
+
) -> PIL.Image.Image | np.ndarray | torch.Tensor:
|
| 1110 |
+
"""
|
| 1111 |
+
Postprocess the image output from tensor to `output_type`.
|
| 1112 |
+
|
| 1113 |
+
Args:
|
| 1114 |
+
image (`torch.Tensor`):
|
| 1115 |
+
The image input, should be a pytorch tensor with shape `B x C x H x W`.
|
| 1116 |
+
output_type (`str`, *optional*, defaults to `pil`):
|
| 1117 |
+
The output type of the image, can be one of `pil`, `np`, `pt`, `latent`.
|
| 1118 |
+
do_denormalize (`list[bool]`, *optional*, defaults to `None`):
|
| 1119 |
+
Whether to denormalize the image to [0,1]. If `None`, will use the value of `do_normalize` in the
|
| 1120 |
+
`VaeImageProcessor` config.
|
| 1121 |
+
|
| 1122 |
+
Returns:
|
| 1123 |
+
`PIL.Image.Image`, `np.ndarray` or `torch.Tensor`:
|
| 1124 |
+
The postprocessed image.
|
| 1125 |
+
"""
|
| 1126 |
+
if not isinstance(image, torch.Tensor):
|
| 1127 |
+
raise ValueError(
|
| 1128 |
+
f"Input for postprocessing is in incorrect format: {type(image)}. We only support pytorch tensor"
|
| 1129 |
+
)
|
| 1130 |
+
if output_type not in ["latent", "pt", "np", "pil"]:
|
| 1131 |
+
deprecation_message = (
|
| 1132 |
+
f"the output_type {output_type} is outdated and has been set to `np`. Please make sure to set it to one of these instead: "
|
| 1133 |
+
"`pil`, `np`, `pt`, `latent`"
|
| 1134 |
+
)
|
| 1135 |
+
deprecate("Unsupported output_type", "1.0.0", deprecation_message, standard_warn=False)
|
| 1136 |
+
output_type = "np"
|
| 1137 |
+
|
| 1138 |
+
image = self._denormalize_conditionally(image, do_denormalize)
|
| 1139 |
+
|
| 1140 |
+
image = self.pt_to_numpy(image)
|
| 1141 |
+
|
| 1142 |
+
if output_type == "np":
|
| 1143 |
+
if image.shape[-1] == 6:
|
| 1144 |
+
image_depth = np.stack([self.rgblike_to_depthmap(im[:, :, 3:]) for im in image], axis=0)
|
| 1145 |
+
else:
|
| 1146 |
+
image_depth = image[:, :, :, 3:]
|
| 1147 |
+
return image[:, :, :, :3], image_depth
|
| 1148 |
+
|
| 1149 |
+
if output_type == "pil":
|
| 1150 |
+
return self.numpy_to_pil(image), self.numpy_to_depth(image)
|
| 1151 |
+
else:
|
| 1152 |
+
raise Exception(f"This type {output_type} is not supported")
|
| 1153 |
+
|
| 1154 |
+
def preprocess(
|
| 1155 |
+
self,
|
| 1156 |
+
rgb: torch.Tensor | PIL.Image.Image | np.ndarray,
|
| 1157 |
+
depth: torch.Tensor | PIL.Image.Image | np.ndarray,
|
| 1158 |
+
height: int | None = None,
|
| 1159 |
+
width: int | None = None,
|
| 1160 |
+
target_res: int | None = None,
|
| 1161 |
+
) -> torch.Tensor:
|
| 1162 |
+
r"""
|
| 1163 |
+
Preprocess the image input. Accepted formats are PIL images, NumPy arrays, or PyTorch tensors.
|
| 1164 |
+
|
| 1165 |
+
Args:
|
| 1166 |
+
rgb (`torch.Tensor | PIL.Image.Image | np.ndarray`):
|
| 1167 |
+
The RGB input image, which can be a single image or a batch.
|
| 1168 |
+
depth (`torch.Tensor | PIL.Image.Image | np.ndarray`):
|
| 1169 |
+
The depth input image, which can be a single image or a batch.
|
| 1170 |
+
height (`int | None`, *optional*, defaults to `None`):
|
| 1171 |
+
The desired height of the processed image. If `None`, defaults to the height of the input image.
|
| 1172 |
+
width (`int | None`, *optional*, defaults to `None`):
|
| 1173 |
+
The desired width of the processed image. If `None`, defaults to the width of the input image.
|
| 1174 |
+
target_res (`int | None`, *optional*, defaults to `None`):
|
| 1175 |
+
Target resolution for resizing the images. If specified, overrides height and width.
|
| 1176 |
+
|
| 1177 |
+
Returns:
|
| 1178 |
+
`tuple[torch.Tensor, torch.Tensor]`:
|
| 1179 |
+
A tuple containing the processed RGB and depth images as PyTorch tensors.
|
| 1180 |
+
"""
|
| 1181 |
+
supported_formats = (PIL.Image.Image, np.ndarray, torch.Tensor)
|
| 1182 |
+
|
| 1183 |
+
# Expand the missing dimension for 3-dimensional pytorch tensor or numpy array that represents grayscale image
|
| 1184 |
+
if self.config.do_convert_grayscale and isinstance(rgb, (torch.Tensor, np.ndarray)) and rgb.ndim == 3:
|
| 1185 |
+
raise Exception("This is not yet supported")
|
| 1186 |
+
|
| 1187 |
+
if isinstance(rgb, supported_formats):
|
| 1188 |
+
rgb = [rgb]
|
| 1189 |
+
depth = [depth]
|
| 1190 |
+
elif not (isinstance(rgb, list) and all(isinstance(i, supported_formats) for i in rgb)):
|
| 1191 |
+
raise ValueError(
|
| 1192 |
+
f"Input is in incorrect format: {[type(i) for i in rgb]}. Currently, we only support {', '.join(supported_formats)}"
|
| 1193 |
+
)
|
| 1194 |
+
|
| 1195 |
+
if isinstance(rgb[0], PIL.Image.Image):
|
| 1196 |
+
if self.config.do_convert_rgb:
|
| 1197 |
+
raise Exception("This is not yet supported")
|
| 1198 |
+
# rgb = [self.convert_to_rgb(i) for i in rgb]
|
| 1199 |
+
# depth = [self.convert_to_depth(i) for i in depth] #TODO define convert_to_depth
|
| 1200 |
+
if self.config.do_resize or target_res:
|
| 1201 |
+
height, width = self.get_default_height_width(rgb[0], height, width) if not target_res else target_res
|
| 1202 |
+
rgb = [self.resize(i, height, width) for i in rgb]
|
| 1203 |
+
depth = [self.resize(i, height, width) for i in depth]
|
| 1204 |
+
rgb = self.pil_to_numpy(rgb) # to np
|
| 1205 |
+
rgb = self.numpy_to_pt(rgb) # to pt
|
| 1206 |
+
|
| 1207 |
+
depth = self.depth_pil_to_numpy(depth) # to np
|
| 1208 |
+
depth = self.numpy_to_pt(depth) # to pt
|
| 1209 |
+
|
| 1210 |
+
elif isinstance(rgb[0], np.ndarray):
|
| 1211 |
+
rgb = np.concatenate(rgb, axis=0) if rgb[0].ndim == 4 else np.stack(rgb, axis=0)
|
| 1212 |
+
rgb = self.numpy_to_pt(rgb)
|
| 1213 |
+
height, width = self.get_default_height_width(rgb, height, width)
|
| 1214 |
+
if self.config.do_resize:
|
| 1215 |
+
rgb = self.resize(rgb, height, width)
|
| 1216 |
+
|
| 1217 |
+
depth = np.concatenate(depth, axis=0) if rgb[0].ndim == 4 else np.stack(depth, axis=0)
|
| 1218 |
+
depth = self.numpy_to_pt(depth)
|
| 1219 |
+
height, width = self.get_default_height_width(depth, height, width)
|
| 1220 |
+
if self.config.do_resize:
|
| 1221 |
+
depth = self.resize(depth, height, width)
|
| 1222 |
+
|
| 1223 |
+
elif isinstance(rgb[0], torch.Tensor):
|
| 1224 |
+
raise Exception("This is not yet supported")
|
| 1225 |
+
# rgb = torch.cat(rgb, axis=0) if rgb[0].ndim == 4 else torch.stack(rgb, axis=0)
|
| 1226 |
+
|
| 1227 |
+
# if self.config.do_convert_grayscale and rgb.ndim == 3:
|
| 1228 |
+
# rgb = rgb.unsqueeze(1)
|
| 1229 |
+
|
| 1230 |
+
# channel = rgb.shape[1]
|
| 1231 |
+
|
| 1232 |
+
# height, width = self.get_default_height_width(rgb, height, width)
|
| 1233 |
+
# if self.config.do_resize:
|
| 1234 |
+
# rgb = self.resize(rgb, height, width)
|
| 1235 |
+
|
| 1236 |
+
# depth = torch.cat(depth, axis=0) if depth[0].ndim == 4 else torch.stack(depth, axis=0)
|
| 1237 |
+
|
| 1238 |
+
# if self.config.do_convert_grayscale and depth.ndim == 3:
|
| 1239 |
+
# depth = depth.unsqueeze(1)
|
| 1240 |
+
|
| 1241 |
+
# channel = depth.shape[1]
|
| 1242 |
+
# # don't need any preprocess if the image is latents
|
| 1243 |
+
# if depth == 4:
|
| 1244 |
+
# return rgb, depth
|
| 1245 |
+
|
| 1246 |
+
# height, width = self.get_default_height_width(depth, height, width)
|
| 1247 |
+
# if self.config.do_resize:
|
| 1248 |
+
# depth = self.resize(depth, height, width)
|
| 1249 |
+
# expected range [0,1], normalize to [-1,1]
|
| 1250 |
+
do_normalize = self.config.do_normalize
|
| 1251 |
+
if rgb.min() < 0 and do_normalize:
|
| 1252 |
+
warnings.warn(
|
| 1253 |
+
"Passing `image` as torch tensor with value range in [-1,1] is deprecated. The expected value range for image tensor is [0,1] "
|
| 1254 |
+
f"when passing as pytorch tensor or numpy Array. You passed `image` with value range [{rgb.min()},{rgb.max()}]",
|
| 1255 |
+
FutureWarning,
|
| 1256 |
+
)
|
| 1257 |
+
do_normalize = False
|
| 1258 |
+
|
| 1259 |
+
if do_normalize:
|
| 1260 |
+
rgb = self.normalize(rgb)
|
| 1261 |
+
depth = self.normalize(depth)
|
| 1262 |
+
|
| 1263 |
+
if self.config.do_binarize:
|
| 1264 |
+
rgb = self.binarize(rgb)
|
| 1265 |
+
depth = self.binarize(depth)
|
| 1266 |
+
|
| 1267 |
+
return rgb, depth
|
| 1268 |
+
|
| 1269 |
+
|
| 1270 |
+
class IPAdapterMaskProcessor(VaeImageProcessor):
|
| 1271 |
+
"""
|
| 1272 |
+
Image processor for IP Adapter image masks.
|
| 1273 |
+
|
| 1274 |
+
Args:
|
| 1275 |
+
do_resize (`bool`, *optional*, defaults to `True`):
|
| 1276 |
+
Whether to downscale the image's (height, width) dimensions to multiples of `vae_scale_factor`.
|
| 1277 |
+
vae_scale_factor (`int`, *optional*, defaults to `8`):
|
| 1278 |
+
VAE scale factor. If `do_resize` is `True`, the image is automatically resized to multiples of this factor.
|
| 1279 |
+
resample (`str`, *optional*, defaults to `lanczos`):
|
| 1280 |
+
Resampling filter to use when resizing the image.
|
| 1281 |
+
do_normalize (`bool`, *optional*, defaults to `False`):
|
| 1282 |
+
Whether to normalize the image to [-1,1].
|
| 1283 |
+
do_binarize (`bool`, *optional*, defaults to `True`):
|
| 1284 |
+
Whether to binarize the image to 0/1.
|
| 1285 |
+
do_convert_grayscale (`bool`, *optional*, defaults to be `True`):
|
| 1286 |
+
Whether to convert the images to grayscale format.
|
| 1287 |
+
|
| 1288 |
+
"""
|
| 1289 |
+
|
| 1290 |
+
config_name = CONFIG_NAME
|
| 1291 |
+
|
| 1292 |
+
@register_to_config
|
| 1293 |
+
def __init__(
|
| 1294 |
+
self,
|
| 1295 |
+
do_resize: bool = True,
|
| 1296 |
+
vae_scale_factor: int = 8,
|
| 1297 |
+
resample: str = "lanczos",
|
| 1298 |
+
do_normalize: bool = False,
|
| 1299 |
+
do_binarize: bool = True,
|
| 1300 |
+
do_convert_grayscale: bool = True,
|
| 1301 |
+
):
|
| 1302 |
+
super().__init__(
|
| 1303 |
+
do_resize=do_resize,
|
| 1304 |
+
vae_scale_factor=vae_scale_factor,
|
| 1305 |
+
resample=resample,
|
| 1306 |
+
do_normalize=do_normalize,
|
| 1307 |
+
do_binarize=do_binarize,
|
| 1308 |
+
do_convert_grayscale=do_convert_grayscale,
|
| 1309 |
+
)
|
| 1310 |
+
|
| 1311 |
+
@staticmethod
|
| 1312 |
+
def downsample(mask: torch.Tensor, batch_size: int, num_queries: int, value_embed_dim: int):
|
| 1313 |
+
"""
|
| 1314 |
+
Downsamples the provided mask tensor to match the expected dimensions for scaled dot-product attention. If the
|
| 1315 |
+
aspect ratio of the mask does not match the aspect ratio of the output image, a warning is issued.
|
| 1316 |
+
|
| 1317 |
+
Args:
|
| 1318 |
+
mask (`torch.Tensor`):
|
| 1319 |
+
The input mask tensor generated with `IPAdapterMaskProcessor.preprocess()`.
|
| 1320 |
+
batch_size (`int`):
|
| 1321 |
+
The batch size.
|
| 1322 |
+
num_queries (`int`):
|
| 1323 |
+
The number of queries.
|
| 1324 |
+
value_embed_dim (`int`):
|
| 1325 |
+
The dimensionality of the value embeddings.
|
| 1326 |
+
|
| 1327 |
+
Returns:
|
| 1328 |
+
`torch.Tensor`:
|
| 1329 |
+
The downsampled mask tensor.
|
| 1330 |
+
|
| 1331 |
+
"""
|
| 1332 |
+
o_h = mask.shape[1]
|
| 1333 |
+
o_w = mask.shape[2]
|
| 1334 |
+
ratio = o_w / o_h
|
| 1335 |
+
mask_h = int(math.sqrt(num_queries / ratio))
|
| 1336 |
+
mask_h = int(mask_h) + int((num_queries % int(mask_h)) != 0)
|
| 1337 |
+
mask_w = num_queries // mask_h
|
| 1338 |
+
|
| 1339 |
+
mask_downsample = F.interpolate(mask.unsqueeze(0), size=(mask_h, mask_w), mode="bicubic").squeeze(0)
|
| 1340 |
+
|
| 1341 |
+
# Repeat batch_size times
|
| 1342 |
+
if mask_downsample.shape[0] < batch_size:
|
| 1343 |
+
mask_downsample = mask_downsample.repeat(batch_size, 1, 1)
|
| 1344 |
+
|
| 1345 |
+
mask_downsample = mask_downsample.view(mask_downsample.shape[0], -1)
|
| 1346 |
+
|
| 1347 |
+
downsampled_area = mask_h * mask_w
|
| 1348 |
+
# If the output image and the mask do not have the same aspect ratio, tensor shapes will not match
|
| 1349 |
+
# Pad tensor if downsampled_mask.shape[1] is smaller than num_queries
|
| 1350 |
+
if downsampled_area < num_queries:
|
| 1351 |
+
warnings.warn(
|
| 1352 |
+
"The aspect ratio of the mask does not match the aspect ratio of the output image. "
|
| 1353 |
+
"Please update your masks or adjust the output size for optimal performance.",
|
| 1354 |
+
UserWarning,
|
| 1355 |
+
)
|
| 1356 |
+
mask_downsample = F.pad(mask_downsample, (0, num_queries - mask_downsample.shape[1]), value=0.0)
|
| 1357 |
+
# Discard last embeddings if downsampled_mask.shape[1] is bigger than num_queries
|
| 1358 |
+
if downsampled_area > num_queries:
|
| 1359 |
+
warnings.warn(
|
| 1360 |
+
"The aspect ratio of the mask does not match the aspect ratio of the output image. "
|
| 1361 |
+
"Please update your masks or adjust the output size for optimal performance.",
|
| 1362 |
+
UserWarning,
|
| 1363 |
+
)
|
| 1364 |
+
mask_downsample = mask_downsample[:, :num_queries]
|
| 1365 |
+
|
| 1366 |
+
# Repeat last dimension to match SDPA output shape
|
| 1367 |
+
mask_downsample = mask_downsample.view(mask_downsample.shape[0], mask_downsample.shape[1], 1).repeat(
|
| 1368 |
+
1, 1, value_embed_dim
|
| 1369 |
+
)
|
| 1370 |
+
|
| 1371 |
+
return mask_downsample
|
| 1372 |
+
|
| 1373 |
+
|
| 1374 |
+
class PixArtImageProcessor(VaeImageProcessor):
|
| 1375 |
+
"""
|
| 1376 |
+
Image processor for PixArt image resize and crop.
|
| 1377 |
+
|
| 1378 |
+
Args:
|
| 1379 |
+
do_resize (`bool`, *optional*, defaults to `True`):
|
| 1380 |
+
Whether to downscale the image's (height, width) dimensions to multiples of `vae_scale_factor`. Can accept
|
| 1381 |
+
`height` and `width` arguments from [`image_processor.VaeImageProcessor.preprocess`] method.
|
| 1382 |
+
vae_scale_factor (`int`, *optional*, defaults to `8`):
|
| 1383 |
+
VAE scale factor. If `do_resize` is `True`, the image is automatically resized to multiples of this factor.
|
| 1384 |
+
resample (`str`, *optional*, defaults to `lanczos`):
|
| 1385 |
+
Resampling filter to use when resizing the image.
|
| 1386 |
+
do_normalize (`bool`, *optional*, defaults to `True`):
|
| 1387 |
+
Whether to normalize the image to [-1,1].
|
| 1388 |
+
do_binarize (`bool`, *optional*, defaults to `False`):
|
| 1389 |
+
Whether to binarize the image to 0/1.
|
| 1390 |
+
do_convert_rgb (`bool`, *optional*, defaults to be `False`):
|
| 1391 |
+
Whether to convert the images to RGB format.
|
| 1392 |
+
do_convert_grayscale (`bool`, *optional*, defaults to be `False`):
|
| 1393 |
+
Whether to convert the images to grayscale format.
|
| 1394 |
+
"""
|
| 1395 |
+
|
| 1396 |
+
@register_to_config
|
| 1397 |
+
def __init__(
|
| 1398 |
+
self,
|
| 1399 |
+
do_resize: bool = True,
|
| 1400 |
+
vae_scale_factor: int = 8,
|
| 1401 |
+
resample: str = "lanczos",
|
| 1402 |
+
do_normalize: bool = True,
|
| 1403 |
+
do_binarize: bool = False,
|
| 1404 |
+
do_convert_grayscale: bool = False,
|
| 1405 |
+
):
|
| 1406 |
+
super().__init__(
|
| 1407 |
+
do_resize=do_resize,
|
| 1408 |
+
vae_scale_factor=vae_scale_factor,
|
| 1409 |
+
resample=resample,
|
| 1410 |
+
do_normalize=do_normalize,
|
| 1411 |
+
do_binarize=do_binarize,
|
| 1412 |
+
do_convert_grayscale=do_convert_grayscale,
|
| 1413 |
+
)
|
| 1414 |
+
|
| 1415 |
+
@staticmethod
|
| 1416 |
+
def classify_height_width_bin(height: int, width: int, ratios: dict) -> tuple[int, int]:
|
| 1417 |
+
r"""
|
| 1418 |
+
Returns the binned height and width based on the aspect ratio.
|
| 1419 |
+
|
| 1420 |
+
Args:
|
| 1421 |
+
height (`int`): The height of the image.
|
| 1422 |
+
width (`int`): The width of the image.
|
| 1423 |
+
ratios (`dict`): A dictionary where keys are aspect ratios and values are tuples of (height, width).
|
| 1424 |
+
|
| 1425 |
+
Returns:
|
| 1426 |
+
`tuple[int, int]`: The closest binned height and width.
|
| 1427 |
+
"""
|
| 1428 |
+
ar = float(height / width)
|
| 1429 |
+
closest_ratio = min(ratios.keys(), key=lambda ratio: abs(float(ratio) - ar))
|
| 1430 |
+
default_hw = ratios[closest_ratio]
|
| 1431 |
+
return int(default_hw[0]), int(default_hw[1])
|
| 1432 |
+
|
| 1433 |
+
@staticmethod
|
| 1434 |
+
def resize_and_crop_tensor(samples: torch.Tensor, new_width: int, new_height: int) -> torch.Tensor:
|
| 1435 |
+
r"""
|
| 1436 |
+
Resizes and crops a tensor of images to the specified dimensions.
|
| 1437 |
+
|
| 1438 |
+
Args:
|
| 1439 |
+
samples (`torch.Tensor`):
|
| 1440 |
+
A tensor of shape (N, C, H, W) where N is the batch size, C is the number of channels, H is the height,
|
| 1441 |
+
and W is the width.
|
| 1442 |
+
new_width (`int`): The desired width of the output images.
|
| 1443 |
+
new_height (`int`): The desired height of the output images.
|
| 1444 |
+
|
| 1445 |
+
Returns:
|
| 1446 |
+
`torch.Tensor`: A tensor containing the resized and cropped images.
|
| 1447 |
+
"""
|
| 1448 |
+
orig_height, orig_width = samples.shape[2], samples.shape[3]
|
| 1449 |
+
|
| 1450 |
+
# Check if resizing is needed
|
| 1451 |
+
if orig_height != new_height or orig_width != new_width:
|
| 1452 |
+
ratio = max(new_height / orig_height, new_width / orig_width)
|
| 1453 |
+
resized_width = int(orig_width * ratio)
|
| 1454 |
+
resized_height = int(orig_height * ratio)
|
| 1455 |
+
|
| 1456 |
+
# Resize
|
| 1457 |
+
samples = F.interpolate(
|
| 1458 |
+
samples, size=(resized_height, resized_width), mode="bilinear", align_corners=False
|
| 1459 |
+
)
|
| 1460 |
+
|
| 1461 |
+
# Center Crop
|
| 1462 |
+
start_x = (resized_width - new_width) // 2
|
| 1463 |
+
end_x = start_x + new_width
|
| 1464 |
+
start_y = (resized_height - new_height) // 2
|
| 1465 |
+
end_y = start_y + new_height
|
| 1466 |
+
samples = samples[:, :, start_y:end_y, start_x:end_x]
|
| 1467 |
+
|
| 1468 |
+
return samples
|
diffusers/loaders/__init__.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING
|
| 2 |
+
|
| 3 |
+
from ..utils import DIFFUSERS_SLOW_IMPORT, _LazyModule, deprecate
|
| 4 |
+
from ..utils.import_utils import is_peft_available, is_torch_available, is_transformers_available
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def text_encoder_lora_state_dict(text_encoder):
|
| 8 |
+
deprecate(
|
| 9 |
+
"text_encoder_load_state_dict in `models`",
|
| 10 |
+
"0.27.0",
|
| 11 |
+
"`text_encoder_lora_state_dict` is deprecated and will be removed in 0.27.0. Make sure to retrieve the weights using `get_peft_model`. See https://huggingface.co/docs/peft/v0.6.2/en/quicktour#peftmodel for more information.",
|
| 12 |
+
)
|
| 13 |
+
state_dict = {}
|
| 14 |
+
|
| 15 |
+
for name, module in text_encoder_attn_modules(text_encoder):
|
| 16 |
+
for k, v in module.q_proj.lora_linear_layer.state_dict().items():
|
| 17 |
+
state_dict[f"{name}.q_proj.lora_linear_layer.{k}"] = v
|
| 18 |
+
|
| 19 |
+
for k, v in module.k_proj.lora_linear_layer.state_dict().items():
|
| 20 |
+
state_dict[f"{name}.k_proj.lora_linear_layer.{k}"] = v
|
| 21 |
+
|
| 22 |
+
for k, v in module.v_proj.lora_linear_layer.state_dict().items():
|
| 23 |
+
state_dict[f"{name}.v_proj.lora_linear_layer.{k}"] = v
|
| 24 |
+
|
| 25 |
+
for k, v in module.out_proj.lora_linear_layer.state_dict().items():
|
| 26 |
+
state_dict[f"{name}.out_proj.lora_linear_layer.{k}"] = v
|
| 27 |
+
|
| 28 |
+
return state_dict
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
if is_transformers_available():
|
| 32 |
+
|
| 33 |
+
def text_encoder_attn_modules(text_encoder):
|
| 34 |
+
deprecate(
|
| 35 |
+
"text_encoder_attn_modules in `models`",
|
| 36 |
+
"0.27.0",
|
| 37 |
+
"`text_encoder_lora_state_dict` is deprecated and will be removed in 0.27.0. Make sure to retrieve the weights using `get_peft_model`. See https://huggingface.co/docs/peft/v0.6.2/en/quicktour#peftmodel for more information.",
|
| 38 |
+
)
|
| 39 |
+
from transformers import CLIPTextModel, CLIPTextModelWithProjection
|
| 40 |
+
|
| 41 |
+
attn_modules = []
|
| 42 |
+
|
| 43 |
+
if isinstance(text_encoder, (CLIPTextModel, CLIPTextModelWithProjection)):
|
| 44 |
+
for i, layer in enumerate(text_encoder.text_model.encoder.layers):
|
| 45 |
+
name = f"text_model.encoder.layers.{i}.self_attn"
|
| 46 |
+
mod = layer.self_attn
|
| 47 |
+
attn_modules.append((name, mod))
|
| 48 |
+
else:
|
| 49 |
+
raise ValueError(f"do not know how to get attention modules for: {text_encoder.__class__.__name__}")
|
| 50 |
+
|
| 51 |
+
return attn_modules
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
_import_structure = {}
|
| 55 |
+
|
| 56 |
+
if is_torch_available():
|
| 57 |
+
_import_structure["single_file_model"] = ["FromOriginalModelMixin"]
|
| 58 |
+
_import_structure["transformer_flux"] = ["FluxTransformer2DLoadersMixin"]
|
| 59 |
+
_import_structure["transformer_sd3"] = ["SD3Transformer2DLoadersMixin"]
|
| 60 |
+
_import_structure["unet"] = ["UNet2DConditionLoadersMixin"]
|
| 61 |
+
_import_structure["utils"] = ["AttnProcsLayers"]
|
| 62 |
+
if is_transformers_available():
|
| 63 |
+
_import_structure["single_file"] = ["FromSingleFileMixin"]
|
| 64 |
+
_import_structure["lora_pipeline"] = [
|
| 65 |
+
"AceStepLoraLoaderMixin",
|
| 66 |
+
"AmusedLoraLoaderMixin",
|
| 67 |
+
"AnimaLoraLoaderMixin",
|
| 68 |
+
"StableDiffusionLoraLoaderMixin",
|
| 69 |
+
"SD3LoraLoaderMixin",
|
| 70 |
+
"AuraFlowLoraLoaderMixin",
|
| 71 |
+
"StableDiffusionXLLoraLoaderMixin",
|
| 72 |
+
"LTX2LoraLoaderMixin",
|
| 73 |
+
"LTXVideoLoraLoaderMixin",
|
| 74 |
+
"LoraLoaderMixin",
|
| 75 |
+
"FluxLoraLoaderMixin",
|
| 76 |
+
"CogVideoXLoraLoaderMixin",
|
| 77 |
+
"CogView4LoraLoaderMixin",
|
| 78 |
+
"Mochi1LoraLoaderMixin",
|
| 79 |
+
"HunyuanVideoLoraLoaderMixin",
|
| 80 |
+
"SanaLoraLoaderMixin",
|
| 81 |
+
"Lumina2LoraLoaderMixin",
|
| 82 |
+
"WanLoraLoaderMixin",
|
| 83 |
+
"HeliosLoraLoaderMixin",
|
| 84 |
+
"KandinskyLoraLoaderMixin",
|
| 85 |
+
"HiDreamImageLoraLoaderMixin",
|
| 86 |
+
"SkyReelsV2LoraLoaderMixin",
|
| 87 |
+
"QwenImageLoraLoaderMixin",
|
| 88 |
+
"Krea2LoraLoaderMixin",
|
| 89 |
+
"ZImageLoraLoaderMixin",
|
| 90 |
+
"Flux2LoraLoaderMixin",
|
| 91 |
+
"Ideogram4LoraLoaderMixin",
|
| 92 |
+
"ErnieImageLoraLoaderMixin",
|
| 93 |
+
"CosmosLoraLoaderMixin",
|
| 94 |
+
]
|
| 95 |
+
_import_structure["textual_inversion"] = ["TextualInversionLoaderMixin"]
|
| 96 |
+
_import_structure["ip_adapter"] = [
|
| 97 |
+
"IPAdapterMixin",
|
| 98 |
+
"FluxIPAdapterMixin",
|
| 99 |
+
"SD3IPAdapterMixin",
|
| 100 |
+
"ModularIPAdapterMixin",
|
| 101 |
+
]
|
| 102 |
+
|
| 103 |
+
_import_structure["peft"] = ["PeftAdapterMixin"]
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 107 |
+
if is_torch_available():
|
| 108 |
+
from .single_file_model import FromOriginalModelMixin
|
| 109 |
+
from .transformer_flux import FluxTransformer2DLoadersMixin
|
| 110 |
+
from .transformer_sd3 import SD3Transformer2DLoadersMixin
|
| 111 |
+
from .unet import UNet2DConditionLoadersMixin
|
| 112 |
+
from .utils import AttnProcsLayers
|
| 113 |
+
|
| 114 |
+
if is_transformers_available():
|
| 115 |
+
from .ip_adapter import (
|
| 116 |
+
FluxIPAdapterMixin,
|
| 117 |
+
IPAdapterMixin,
|
| 118 |
+
ModularIPAdapterMixin,
|
| 119 |
+
SD3IPAdapterMixin,
|
| 120 |
+
)
|
| 121 |
+
from .lora_pipeline import (
|
| 122 |
+
AceStepLoraLoaderMixin,
|
| 123 |
+
AmusedLoraLoaderMixin,
|
| 124 |
+
AnimaLoraLoaderMixin,
|
| 125 |
+
AuraFlowLoraLoaderMixin,
|
| 126 |
+
CogVideoXLoraLoaderMixin,
|
| 127 |
+
CogView4LoraLoaderMixin,
|
| 128 |
+
CosmosLoraLoaderMixin,
|
| 129 |
+
ErnieImageLoraLoaderMixin,
|
| 130 |
+
Flux2LoraLoaderMixin,
|
| 131 |
+
FluxLoraLoaderMixin,
|
| 132 |
+
HeliosLoraLoaderMixin,
|
| 133 |
+
HiDreamImageLoraLoaderMixin,
|
| 134 |
+
HunyuanVideoLoraLoaderMixin,
|
| 135 |
+
Ideogram4LoraLoaderMixin,
|
| 136 |
+
KandinskyLoraLoaderMixin,
|
| 137 |
+
Krea2LoraLoaderMixin,
|
| 138 |
+
LoraLoaderMixin,
|
| 139 |
+
LTX2LoraLoaderMixin,
|
| 140 |
+
LTXVideoLoraLoaderMixin,
|
| 141 |
+
Lumina2LoraLoaderMixin,
|
| 142 |
+
Mochi1LoraLoaderMixin,
|
| 143 |
+
QwenImageLoraLoaderMixin,
|
| 144 |
+
SanaLoraLoaderMixin,
|
| 145 |
+
SD3LoraLoaderMixin,
|
| 146 |
+
SkyReelsV2LoraLoaderMixin,
|
| 147 |
+
StableDiffusionLoraLoaderMixin,
|
| 148 |
+
StableDiffusionXLLoraLoaderMixin,
|
| 149 |
+
WanLoraLoaderMixin,
|
| 150 |
+
ZImageLoraLoaderMixin,
|
| 151 |
+
)
|
| 152 |
+
from .single_file import FromSingleFileMixin
|
| 153 |
+
from .textual_inversion import TextualInversionLoaderMixin
|
| 154 |
+
|
| 155 |
+
from .peft import PeftAdapterMixin
|
| 156 |
+
else:
|
| 157 |
+
import sys
|
| 158 |
+
|
| 159 |
+
sys.modules[__name__] = _LazyModule(__name__, globals()["__file__"], _import_structure, module_spec=__spec__)
|