linoyts HF Staff commited on
Commit
3db1cbc
·
verified ·
1 Parent(s): 579e4f8

FLUX.3 layout-control canvas (custom HTML) — initial

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -35
  2. README.md +27 -7
  3. app.py +624 -0
  4. diffusers/__init__.py +1812 -0
  5. diffusers/callbacks.py +244 -0
  6. diffusers/commands/__init__.py +27 -0
  7. diffusers/commands/custom_blocks.py +145 -0
  8. diffusers/commands/diffusers_cli.py +69 -0
  9. diffusers/commands/env.py +185 -0
  10. diffusers/commands/fp16_safetensors.py +144 -0
  11. diffusers/commands/run.py +1335 -0
  12. diffusers/commands/schema.py +287 -0
  13. diffusers/commands/skills.py +459 -0
  14. diffusers/configuration_utils.py +726 -0
  15. diffusers/dependency_versions_check.py +34 -0
  16. diffusers/dependency_versions_table.py +56 -0
  17. diffusers/experimental/__init__.py +1 -0
  18. diffusers/experimental/rl/__init__.py +1 -0
  19. diffusers/experimental/rl/value_guided_sampling.py +153 -0
  20. diffusers/guiders/__init__.py +32 -0
  21. diffusers/guiders/adaptive_projected_guidance.py +253 -0
  22. diffusers/guiders/adaptive_projected_guidance_mix.py +297 -0
  23. diffusers/guiders/auto_guidance.py +198 -0
  24. diffusers/guiders/classifier_free_guidance.py +156 -0
  25. diffusers/guiders/classifier_free_zero_star_guidance.py +164 -0
  26. diffusers/guiders/frequency_decoupled_guidance.py +335 -0
  27. diffusers/guiders/guider_utils.py +396 -0
  28. diffusers/guiders/ltx2_guidance.py +171 -0
  29. diffusers/guiders/magnitude_aware_guidance.py +159 -0
  30. diffusers/guiders/perturbed_attention_guidance.py +289 -0
  31. diffusers/guiders/skip_layer_guidance.py +280 -0
  32. diffusers/guiders/smoothed_energy_guidance.py +269 -0
  33. diffusers/guiders/tangential_classifier_free_guidance.py +151 -0
  34. diffusers/hooks/__init__.py +31 -0
  35. diffusers/hooks/_common.py +61 -0
  36. diffusers/hooks/_helpers.py +401 -0
  37. diffusers/hooks/context_parallel.py +382 -0
  38. diffusers/hooks/faster_cache.py +654 -0
  39. diffusers/hooks/first_block_cache.py +258 -0
  40. diffusers/hooks/group_offloading.py +1056 -0
  41. diffusers/hooks/hooks.py +326 -0
  42. diffusers/hooks/layer_skip.py +263 -0
  43. diffusers/hooks/layerwise_casting.py +240 -0
  44. diffusers/hooks/mag_cache.py +468 -0
  45. diffusers/hooks/pyramid_attention_broadcast.py +314 -0
  46. diffusers/hooks/smoothed_energy_guidance_utils.py +166 -0
  47. diffusers/hooks/taylorseer_cache.py +345 -0
  48. diffusers/hooks/tensor_parallel.py +273 -0
  49. diffusers/hooks/tensor_parallel_neuron.py +178 -0
  50. diffusers/hooks/text_kv_cache.py +173 -0
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.webp filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,13 +1,33 @@
1
  ---
2
- title: Flux3 Image Small Canvas
3
- emoji: 🏃
4
- colorFrom: blue
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.30.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FLUX.3 Layout Canvas
3
+ emoji: 🎛️
4
+ colorFrom: indigo
5
+ colorTo: gray
6
  sdk: gradio
7
+ sdk_version: 5.49.1
 
8
  app_file: app.py
9
  pinned: false
10
+ python_version: 3.12.12
11
+ short_description: FLUX.3 image-small bounding-box layout canvas (ZeroGPU)
12
  ---
13
 
14
+ # FLUX.3 image-small — layout-control canvas (private)
15
+
16
+ A custom-HTML canvas on top of the **diffusers-native FLUX.3 modular pipeline**
17
+ ([`diffusers-new-model-addition-f3` PR #23](https://github.com/huggingface/diffusers-new-model-addition-f3/pull/23)),
18
+ on ZeroGPU.
19
+
20
+ Drag a bounding box for every element that matters, give each box an id and a
21
+ description, write a one-line scene prompt, and FLUX.3 renders the scene with
22
+ every element placed inside its box. Toggle the **Bounding boxes** overlay on the
23
+ result, or **Replay** to regenerate.
24
+
25
+ > **Private.** Weights are read at runtime from the private repo
26
+ > `diffusers-internal-dev/f3-small-img` (never copied here). Keep this Space private.
27
+
28
+ Layout control uses FLUX.3's region format: the final prompt is the scene caption
29
+ followed by a JSON list of regions, each `{"id", "bbox": [y0, x0, y1, x1] (0-1000), "desc"}`.
30
+
31
+ ## Setup
32
+
33
+ Needs an `HF_TOKEN` secret with read access to `diffusers-internal-dev/f3-small-img`.
app.py ADDED
@@ -0,0 +1,624 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FLUX.3 image-small — layout-control canvas (private ZeroGPU demo, diffusers modular).
2
+
3
+ A custom HTML/JS canvas on top of the diffusers-native FLUX.3 modular pipeline
4
+ (PR #23, vendored `diffusers/`). You drag bounding boxes on an aspect-ratio
5
+ artboard, give each box an id + a description, write a one-line scene prompt, and
6
+ FLUX.3 renders the scene with every element placed inside its box.
7
+
8
+ Layout control uses FLUX.3's region format (see `integration-notes/flux3_examples.py`):
9
+ the final prompt is the scene caption followed by a JSON list of regions, each
10
+ `{"id", "bbox": [y0, x0, y1, x1] (normalized 0-1000), "desc"}`.
11
+ """
12
+
13
+ import importlib.util
14
+ import os
15
+
16
+ os.environ.setdefault("TORCH_ALLOW_TF32_CUBLAS_OVERRIDE", "1")
17
+
18
+ spaces = None
19
+ if importlib.util.find_spec("spaces"):
20
+ import spaces # before torch so ZeroGPU can patch torch
21
+
22
+ import base64
23
+ import io
24
+ import json
25
+ import random
26
+
27
+ import gradio as gr
28
+ import torch
29
+
30
+ from diffusers import ModularPipeline
31
+
32
+ REPO = os.environ.get("F3_REPO", "diffusers-internal-dev/f3-small-img")
33
+ HF_TOKEN = os.environ.get("HF_TOKEN")
34
+
35
+ MAX_SEED = 2**63 - 1
36
+ DEFAULT_STEPS = int(os.environ.get("F3_STEPS", "50"))
37
+ DEFAULT_GUIDANCE = 3.5
38
+
39
+ torch.backends.cuda.matmul.allow_tf32 = True
40
+ torch.backends.cudnn.allow_tf32 = True
41
+
42
+ # Aspect ratio -> pixel dims (multiples of 16, ~1 MP, the model's default area).
43
+ RATIOS = {
44
+ "1:1": (1024, 1024),
45
+ "3:2": (1216, 816),
46
+ "2:3": (816, 1216),
47
+ "4:3": (1152, 864),
48
+ "3:4": (864, 1152),
49
+ "16:9": (1344, 768),
50
+ "9:16": (768, 1344),
51
+ }
52
+
53
+ # ---- load the pipeline on CPU at import; place on cuda under ZeroGPU ----
54
+ print(f"[app] loading modular pipeline from {REPO} ...", flush=True)
55
+ PIPE = ModularPipeline.from_pretrained(REPO, token=HF_TOKEN)
56
+ PIPE.load_components(dtype={"vae": torch.float32, "default": torch.bfloat16}, token=HF_TOKEN)
57
+ print("[app] components loaded on CPU.", flush=True)
58
+ if spaces is not None:
59
+ PIPE.to("cuda")
60
+ print("[app] pipeline on cuda; ready.", flush=True)
61
+
62
+
63
+ def _build_prompt(scene: str, elements: list) -> str:
64
+ """Scene caption + JSON region list, FLUX.3's layout-control format."""
65
+ scene = (scene or "").strip()
66
+ regions = []
67
+ for e in elements:
68
+ desc = (e.get("desc") or "").strip()
69
+ if not desc:
70
+ continue
71
+ bbox = e.get("bbox")
72
+ if not (isinstance(bbox, list) and len(bbox) == 4):
73
+ continue
74
+ regions.append({
75
+ "id": e.get("id") or "element",
76
+ "bbox": [int(round(v)) for v in bbox],
77
+ "desc": desc,
78
+ })
79
+ if not regions:
80
+ return scene
81
+ return (scene + " " if scene else "") + json.dumps(regions)
82
+
83
+
84
+ def _duration(state_json, *_):
85
+ try:
86
+ st = json.loads(state_json) if state_json else {}
87
+ w, h = RATIOS.get(st.get("aspect", "1:1"), (1024, 1024))
88
+ except Exception:
89
+ w, h = 1024, 1024
90
+ px = w * h
91
+ return int(min(300, 45 + DEFAULT_STEPS * 2.6 * max(1.0, px / (1024 * 1024))))
92
+
93
+
94
+ def _generate(state_json):
95
+ try:
96
+ st = json.loads(state_json) if state_json else {}
97
+ except Exception as e:
98
+ raise gr.Error(f"Bad canvas state: {e}")
99
+
100
+ elements = st.get("elements") or []
101
+ scene = st.get("scene") or ""
102
+ aspect = st.get("aspect") or "1:1"
103
+ w, h = RATIOS.get(aspect, (1024, 1024))
104
+
105
+ full_prompt = _build_prompt(scene, elements)
106
+ if not full_prompt.strip():
107
+ raise gr.Error("Add at least a scene prompt or one described element.")
108
+
109
+ seed = st.get("seed")
110
+ seed = int(seed) if isinstance(seed, (int, float)) and seed >= 0 else random.randint(0, MAX_SEED)
111
+
112
+ PIPE.guider = PIPE.guider.new(guidance_scale=DEFAULT_GUIDANCE)
113
+ image = PIPE(
114
+ prompt=full_prompt,
115
+ height=h,
116
+ width=w,
117
+ num_inference_steps=DEFAULT_STEPS,
118
+ generator=torch.Generator().manual_seed(seed),
119
+ output="images",
120
+ )[0]
121
+ if torch.cuda.is_available():
122
+ torch.cuda.synchronize()
123
+
124
+ buf = io.BytesIO()
125
+ image.save(buf, format="WEBP", quality=92)
126
+ data_uri = "data:image/webp;base64," + base64.b64encode(buf.getvalue()).decode()
127
+ print(f"[app] generate {w}x{h} steps={DEFAULT_STEPS} seed={seed} "
128
+ f"elements={len(elements)} prompt_len={len(full_prompt)}", flush=True)
129
+ return json.dumps({
130
+ "image": data_uri,
131
+ "aspect": aspect,
132
+ "w": w, "h": h,
133
+ "seed": seed,
134
+ "elements": [{"id": e.get("id"), "bbox": e.get("bbox"), "color": e.get("color")}
135
+ for e in elements if e.get("bbox")],
136
+ "prompt": full_prompt,
137
+ })
138
+
139
+
140
+ generate = spaces.GPU(_generate, duration=_duration) if spaces is not None else _generate
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Preset scenes (the top tabs), reconstructed from BFL's FLUX.3 layout examples.
145
+ # bbox is [y0, x0, y1, x1] normalized 0-1000.
146
+ # ---------------------------------------------------------------------------
147
+ PRESETS = {
148
+ "Le Festival du Soleil": {
149
+ "aspect": "3:4",
150
+ "scene": "A glowing concrete dome rises from a twilight bay, a crowd on the beach before it.",
151
+ "elements": [
152
+ {"id": "Fr_Text_1", "bbox": [90, 230, 230, 780],
153
+ "desc": "\"LE FESTIVAL DU SOLEIL\" written in a thin, elegant, serif typeface in a light cream color."},
154
+ {"id": "dome_1", "bbox": [250, 150, 650, 850], "desc": "a massive, smooth parabolic dome of pale concrete"},
155
+ {"id": "town_1", "bbox": [250, 640, 430, 900], "desc": "faint lights and small buildings at the foot of the hills"},
156
+ {"id": "swimmers_1", "bbox": [580, 200, 720, 800],
157
+ "desc": "dozens of small, silhouetted figures scattered in the dark water, wading"},
158
+ {"id": "crowd_1", "bbox": [740, 0, 1000, 1000],
159
+ "desc": "a large crowd of people seated on the beach in casual, light-colored summer attire"},
160
+ ],
161
+ },
162
+ "Five frames of winter": {
163
+ "aspect": "16:9",
164
+ "scene": "Five vertical frames of a quiet winter day, left to right from dawn to dusk.",
165
+ "elements": [
166
+ {"id": "frame_1", "bbox": [0, 0, 1000, 190], "desc": "a bare birch forest at pale blue dawn, frost on every branch"},
167
+ {"id": "frame_2", "bbox": [0, 200, 1000, 390], "desc": "a frozen lake under flat grey light, a lone figure skating"},
168
+ {"id": "frame_3", "bbox": [0, 400, 1000, 590], "desc": "heavy snowfall over a red wooden cabin with a lit window"},
169
+ {"id": "frame_4", "bbox": [0, 600, 1000, 790], "desc": "a snowy pine ridge at golden hour, long blue shadows"},
170
+ {"id": "frame_5", "bbox": [0, 800, 1000, 1000], "desc": "the same valley at dusk, deep indigo sky and the first stars"},
171
+ ],
172
+ },
173
+ "Knitted iceberg village": {
174
+ "aspect": "4:3",
175
+ "scene": "A knitted diorama: three cable-knit icebergs with tiny houses, a banner and a magenta whale tail.",
176
+ "elements": [
177
+ {"id": "En_Text_1", "bbox": [130, 220, 220, 780],
178
+ "desc": "The exact words \"KNITTED ICEBERG VILLAGE\" embroidered in thick, raised navy-blue yarn across a banner."},
179
+ {"id": "iceberg_3", "bbox": [200, 300, 650, 700],
180
+ "desc": "The largest central iceberg, a bulky mound of white and pale grey ribbed knitting, dominating the midground with thick, vertical columns of stitches."},
181
+ {"id": "iceberg_1", "bbox": [230, 60, 650, 320],
182
+ "desc": "A conical soft sculpture made of stark white, chunky roving yarn in a thick cable-knit pattern. The left side is brightly lit, while the right side falls into deep, fuzzy shadow."},
183
+ {"id": "iceberg_2", "bbox": [230, 680, 640, 960],
184
+ "desc": "A tall, asymmetrical peak of white cable-knit yarn, featuring a few visibly dropped stitches near the base and slightly uneven tension in the cables."},
185
+ {"id": "whale_tail_1", "bbox": [500, 380, 850, 620],
186
+ "desc": "A giant whale tail knitted from bright magenta and coral-pink yarn, bursting vertically from the teal sea."},
187
+ ],
188
+ },
189
+ "Up from the subway": {
190
+ "aspect": "3:2",
191
+ "scene": "Looking up a subway staircase at dusk, a blurred figure climbing toward the skyline.",
192
+ "elements": [
193
+ {"id": "building_1", "bbox": [50, 120, 630, 470],
194
+ "desc": "a modern, red-brick high-rise skyscraper stretching into the sky"},
195
+ {"id": "building_2", "bbox": [0, 450, 650, 850],
196
+ "desc": "a tall, classical white stone skyscraper with a tiered, set-back top"},
197
+ {"id": "person_1", "bbox": [300, 380, 820, 640],
198
+ "desc": "a heavily blurred, dark shape of a person seen from behind"},
199
+ {"id": "stairs_1", "bbox": [600, 200, 1000, 800],
200
+ "desc": "a flight of concrete subway stairs filling the bottom center of the frame"},
201
+ {"id": "En_Text_1", "bbox": [300, 760, 640, 1000],
202
+ "desc": "a partially visible, glowing green and white sign on the building"},
203
+ ],
204
+ },
205
+ }
206
+
207
+ RATIO_LIST = list(RATIOS.keys())
208
+
209
+ CONFIG_JS = (
210
+ "<script>window.F3_PRESETS = " + json.dumps(PRESETS) + ";"
211
+ "window.F3_RATIOS = " + json.dumps(RATIO_LIST) + ";"
212
+ "window.F3_PRESET_ORDER = " + json.dumps(list(PRESETS.keys())) + ";</script>"
213
+ )
214
+
215
+ # Styles + behavior live in <head>; the component mounts into #f3-root (gr.HTML).
216
+ STYLE = """
217
+ <style>
218
+ #f3-root, #f3-root * { box-sizing: border-box; }
219
+ #f3-root {
220
+ --bg:#09090b; --panel:#101013; --panel2:#0c0c0f; --line:#26262b; --line2:#1b1b1f;
221
+ --txt:#e7e7ea; --dim:#8a8a93; --dim2:#5f5f68; --violet:#8b7cf6; --violetbg:rgba(139,124,246,.14);
222
+ --green:#3fd19b; --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
223
+ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;
224
+ color:var(--txt); width:100%;
225
+ }
226
+ #f3-root .tabs{display:flex;gap:4px;background:var(--panel);border:1px solid var(--line2);
227
+ border-radius:12px;padding:5px;width:max-content;max-width:100%;overflow:auto;margin-bottom:14px;}
228
+ #f3-root .tab{padding:9px 16px;border-radius:8px;color:var(--dim);font-size:14px;cursor:pointer;
229
+ white-space:nowrap;transition:.15s;}
230
+ #f3-root .tab:hover{color:var(--txt);}
231
+ #f3-root .tab.active{background:#222228;color:var(--txt);}
232
+ #f3-root .main{display:grid;grid-template-columns:1fr 390px;gap:16px;align-items:start;}
233
+ @media (max-width:980px){#f3-root .main{grid-template-columns:1fr;}}
234
+
235
+ #f3-root .canvas-wrap{background:var(--panel2);border:1px solid var(--line2);border-radius:14px;
236
+ padding:22px;position:relative;min-height:560px;
237
+ background-image:radial-gradient(var(--line) 1px,transparent 1px);background-size:17px 17px;}
238
+ #f3-root .board{position:relative;margin:0 auto;background:#0b0b0e;border:1px solid #34343c;
239
+ box-shadow:0 0 0 1px #000 inset; user-select:none; touch-action:none;}
240
+ #f3-root .board.empty{background:#0b0b0e;}
241
+ #f3-root .board img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:1px;}
242
+ #f3-root .ratio-tag{position:absolute;left:4px;bottom:-24px;color:var(--dim);font:12px/1 var(--mono);}
243
+ #f3-root .board-tools{position:absolute;right:22px;bottom:22px;display:flex;gap:8px;align-items:center;z-index:30;}
244
+ #f3-root .btn{display:inline-flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--line);
245
+ color:var(--dim);border-radius:9px;padding:8px 13px;font-size:13px;cursor:pointer;transition:.15s;}
246
+ #f3-root .btn:hover{color:var(--txt);border-color:#3a3a42;}
247
+ #f3-root .btn.on{color:var(--txt);}
248
+ #f3-root .toggle{width:34px;height:19px;border-radius:12px;background:#2a2a30;position:relative;transition:.15s;flex:none;}
249
+ #f3-root .toggle::after{content:'';position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;
250
+ background:#777;transition:.15s;}
251
+ #f3-root .btn.on .toggle{background:var(--violet);}
252
+ #f3-root .btn.on .toggle::after{left:17px;background:#fff;}
253
+
254
+ #f3-root .box{position:absolute;border:1.5px solid var(--violet);background:var(--violetbg);cursor:move;}
255
+ #f3-root .box.sel{box-shadow:0 0 0 1px var(--violet);}
256
+ #f3-root .box .tag{position:absolute;left:-1px;top:-22px;display:flex;gap:7px;align-items:center;
257
+ background:var(--violet);color:#0a0a0c;font:600 11px/1.4 var(--mono);padding:3px 7px;border-radius:4px;
258
+ white-space:nowrap;max-width:420px;}
259
+ #f3-root .box .tag .id{outline:none;}
260
+ #f3-root .box .tag .coords{color:rgba(10,10,12,.62);font-weight:500;}
261
+ #f3-root .box .desc{position:absolute;inset:7px;overflow:auto;color:var(--txt);font-size:12.5px;line-height:1.35;
262
+ outline:none;background:transparent;}
263
+ #f3-root .box .desc:empty::before{content:'describe this element…';color:var(--dim2);}
264
+ #f3-root .box .rm{position:absolute;right:-9px;top:-9px;width:18px;height:18px;border-radius:50%;background:#222;
265
+ border:1px solid var(--line);color:var(--dim);font-size:12px;line-height:16px;text-align:center;cursor:pointer;
266
+ display:none;}
267
+ #f3-root .box:hover .rm{display:block;}
268
+ #f3-root .box .h{position:absolute;width:10px;height:10px;background:#0b0b0e;border:1.5px solid var(--violet);border-radius:2px;}
269
+ #f3-root .box .h.se{right:-6px;bottom:-6px;cursor:se-resize;}
270
+ #f3-root .box .h.ne{right:-6px;top:-6px;cursor:ne-resize;}
271
+ #f3-root .box .h.sw{left:-6px;bottom:-6px;cursor:sw-resize;}
272
+ #f3-root .box .h.nw{left:-6px;top:-6px;cursor:nw-resize;}
273
+ #f3-root .board.rendered .box{background:transparent;}
274
+ #f3-root .board.rendered .box .desc{display:none;}
275
+ #f3-root .board.hideboxes .box{display:none;}
276
+ #f3-root .board .hint{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
277
+ color:var(--dim2);font-size:13px;text-align:center;padding:30px;pointer-events:none;}
278
+ #f3-root .board .spin{position:absolute;inset:0;display:none;align-items:center;justify-content:center;
279
+ background:rgba(8,8,10,.55);color:var(--txt);font-size:13px;z-index:40;}
280
+ #f3-root .board.busy .spin{display:flex;}
281
+
282
+ #f3-root .side{background:var(--panel);border:1px solid var(--line2);border-radius:14px;overflow:hidden;}
283
+ #f3-root .step{padding:15px 18px;border-bottom:1px solid var(--line2);cursor:pointer;}
284
+ #f3-root .step:last-child{border-bottom:none;}
285
+ #f3-root .step .row{display:flex;gap:14px;align-items:center;}
286
+ #f3-root .step .n{font:12px/1 var(--mono);color:var(--dim2);}
287
+ #f3-root .step .t{font-size:15px;color:var(--dim);}
288
+ #f3-root .step.active{background:#17171b;}
289
+ #f3-root .step.active .t{color:var(--txt);}
290
+ #f3-root .step .hint{margin:9px 0 0 26px;color:var(--dim);font-size:13px;line-height:1.45;display:none;}
291
+ #f3-root .step.active .hint{display:block;}
292
+ #f3-root .summary{border-top:1px solid var(--line2);padding:16px 18px;max-height:230px;overflow:auto;}
293
+ #f3-root .summary .e{margin-bottom:13px;}
294
+ #f3-root .summary .e .h{font:600 12.5px/1.5 var(--mono);color:var(--violet);}
295
+ #f3-root .summary .e .h .c{color:var(--dim2);font-weight:500;}
296
+ #f3-root .summary .e .d{color:var(--dim);font-size:13px;line-height:1.4;}
297
+ #f3-root .summary .empty{color:var(--dim2);font-size:13px;}
298
+ #f3-root .scene{border-top:1px solid var(--line2);padding:16px 18px;}
299
+ #f3-root .scene label{display:block;font:11px/1 var(--mono);letter-spacing:.08em;color:var(--dim2);margin-bottom:9px;}
300
+ #f3-root .scene textarea{width:100%;background:transparent;border:none;color:var(--txt);font-size:15.5px;
301
+ line-height:1.4;resize:none;outline:none;min-height:46px;font-family:inherit;}
302
+ #f3-root .scene textarea::placeholder{color:var(--dim2);}
303
+ #f3-root .ratios{display:flex;flex-wrap:wrap;gap:7px;margin:11px 0 2px 26px;}
304
+ #f3-root .chip{padding:6px 11px;border:1px solid var(--line);border-radius:7px;color:var(--dim);
305
+ font:12px/1 var(--mono);cursor:pointer;}
306
+ #f3-root .chip.sel{border-color:var(--violet);color:var(--txt);background:var(--violetbg);}
307
+ #f3-root .gen{margin:12px 18px 18px;}
308
+ #f3-root .genbtn{width:100%;background:var(--violet);color:#0a0a0c;border:none;border-radius:10px;
309
+ padding:13px;font-size:14.5px;font-weight:600;cursor:pointer;transition:.15s;}
310
+ #f3-root .genbtn:hover{filter:brightness(1.08);}
311
+ #f3-root .genbtn[disabled]{opacity:.55;cursor:default;}
312
+ #f3-root .table-bar{margin-top:16px;background:var(--panel);border:1px solid var(--line2);border-radius:12px;
313
+ padding:16px 18px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;}
314
+ #f3-root .table-bar .lab{font:12px/1 var(--mono);letter-spacing:.06em;color:var(--dim);}
315
+ #f3-root .table-bar .cnt{font:12px/1 var(--mono);color:var(--dim2);}
316
+ #f3-root .etable{margin-top:8px;background:var(--panel);border:1px solid var(--line2);border-radius:12px;
317
+ overflow:auto;display:none;}
318
+ #f3-root .etable.open{display:block;}
319
+ #f3-root table{width:100%;border-collapse:collapse;font-size:13px;}
320
+ #f3-root th,#f3-root td{text-align:left;padding:10px 14px;border-bottom:1px solid var(--line2);vertical-align:top;}
321
+ #f3-root th{font:11px/1 var(--mono);letter-spacing:.06em;color:var(--dim2);text-transform:uppercase;}
322
+ #f3-root td.id{font:600 12.5px/1.4 var(--mono);color:var(--violet);white-space:nowrap;}
323
+ #f3-root td.bb{font:12px/1.4 var(--mono);color:var(--dim);white-space:nowrap;}
324
+ #f3-root td.d{color:var(--txt);}
325
+ </style>
326
+ """
327
+
328
+ # Selection accent (green) + small extras, appended to the head styles.
329
+ STYLE2 = """
330
+ <style>
331
+ #f3-root .box.sel{border-color:var(--green);}
332
+ #f3-root .box.sel .tag{background:var(--green);}
333
+ #f3-root .box.sel .tag .coords{color:rgba(10,12,11,.6);}
334
+ #f3-root .box.sel .h{border-color:var(--green);}
335
+ </style>
336
+ """
337
+
338
+ SCRIPT = r"""
339
+ <script>
340
+ (function(){
341
+ const NS = window.F3 = window.F3 || {};
342
+ const RATIOS = {"1:1":[1024,1024],"3:2":[1216,816],"2:3":[816,1216],"4:3":[1152,864],
343
+ "3:4":[864,1152],"16:9":[1344,768],"9:16":[768,1344]};
344
+ const STEP_HINTS = [
345
+ ["Aspect ratio","Pick the frame your scene lives in."],
346
+ ["Elements","Drag out a box for every element that matters, then describe what goes in it."],
347
+ ["Scene prompt","Give the scene one line that ties the elements together."],
348
+ ["Generate","FLUX.3 renders the scene with every element inside its box."],
349
+ ];
350
+ let S = {aspect:"1:1", elements:[], scene:"", seed:-1, rendered:false, step:1, showBoxes:true};
351
+ let seq = 0, sel = null;
352
+
353
+ function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
354
+ function uid(base){ base=(base||"element").replace(/[^A-Za-z0-9_]+/g,"_").replace(/^_+|_+$/g,"")||"element";
355
+ let id=base, n=1; const ids=new Set(S.elements.map(e=>e.id));
356
+ while(ids.has(id)){ id=base+"_"+(++n);} return id; }
357
+
358
+ function setHidden(id,val){
359
+ const w=document.getElementById(id); if(!w) return;
360
+ const el=w.querySelector('textarea,input'); if(!el) return;
361
+ const proto = el.tagName==='TEXTAREA'?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype;
362
+ const setter=Object.getOwnPropertyDescriptor(proto,'value').set;
363
+ setter.call(el,val); el.dispatchEvent(new Event('input',{bubbles:true}));
364
+ }
365
+ function syncState(){
366
+ setHidden('f3-state', JSON.stringify({
367
+ aspect:S.aspect, scene:S.scene, seed:S.seed,
368
+ elements:S.elements.map(e=>({id:e.id,bbox:e.bbox,desc:e.desc,color:e.color})),
369
+ }));
370
+ }
371
+
372
+ // ---------- board geometry ----------
373
+ function boardSize(){
374
+ const wrap=NS.el.wrap; const availW=wrap.clientWidth-44; const maxH=600;
375
+ const [W,H]=RATIOS[S.aspect]; let w=availW, h=w*H/W;
376
+ if(h>maxH){ h=maxH; w=h*W/H; } if(w>availW){ w=availW; h=w*H/W; }
377
+ return [Math.round(w),Math.round(h)];
378
+ }
379
+ function layoutBoard(){
380
+ const [w,h]=boardSize(); const b=NS.el.board; b.style.width=w+'px'; b.style.height=h+'px';
381
+ NS.el.ratioTag.textContent=S.aspect;
382
+ }
383
+
384
+ // ---------- rendering ----------
385
+ function boxEl(e){
386
+ const d=document.createElement('div'); d.className='box'; d.dataset.id=e.id;
387
+ d.innerHTML='<div class="tag"><span class="id" contenteditable="true" spellcheck="false"></span>'
388
+ +'<span class="coords"></span></div>'
389
+ +'<div class="desc" contenteditable="true" spellcheck="false"></div>'
390
+ +'<div class="rm">×</div>'
391
+ +'<div class="h nw"></div><div class="h ne"></div><div class="h sw"></div><div class="h se"></div>';
392
+ d.querySelector('.id').textContent=e.id;
393
+ d.querySelector('.desc').textContent=e.desc||'';
394
+ wireBox(d,e); return d;
395
+ }
396
+ function posBox(d,e){
397
+ const [y0,x0,y1,x1]=e.bbox;
398
+ d.style.left=(x0/10)+'%'; d.style.top=(y0/10)+'%';
399
+ d.style.width=((x1-x0)/10)+'%'; d.style.height=((y1-y0)/10)+'%';
400
+ d.querySelector('.coords').textContent='['+[y0,x0,y1,x1].map(Math.round).join(', ')+']';
401
+ }
402
+ function renderBoxes(){
403
+ const b=NS.el.board;
404
+ b.querySelectorAll('.box').forEach(n=>n.remove());
405
+ S.elements.forEach(e=>{ const d=boxEl(e); posBox(d,e); if(e===sel)d.classList.add('sel'); b.appendChild(d); });
406
+ }
407
+ function renderSummary(){
408
+ const s=NS.el.summary;
409
+ if(!S.elements.length){ s.innerHTML='<div class="empty">No elements yet — drag a box on the canvas.</div>'; return; }
410
+ s.innerHTML=S.elements.map(e=>'<div class="e"><div class="h">'+esc(e.id)
411
+ +' <span class="c">['+e.bbox.map(Math.round).join(', ')+']</span></div>'
412
+ +'<div class="d">'+esc(e.desc||'')+'</div></div>').join('');
413
+ }
414
+ function renderTable(){
415
+ NS.el.cnt.textContent=S.elements.length+' ROW'+(S.elements.length===1?'':'S');
416
+ const body=S.elements.map(e=>'<tr><td class="id">'+esc(e.id)+'</td><td class="bb">['
417
+ +e.bbox.map(Math.round).join(', ')+']</td><td class="d">'+esc(e.desc||'')+'</td></tr>').join('');
418
+ NS.el.etable.innerHTML='<table><thead><tr><th>Element</th><th>bbox [y0,x0,y1,x1]</th><th>Description</th></tr></thead><tbody>'
419
+ +(body||'<tr><td colspan="3" class="d" style="color:var(--dim2)">empty</td></tr>')+'</tbody></table>';
420
+ }
421
+ function renderSteps(){
422
+ NS.el.steps.forEach((st,i)=>st.classList.toggle('active', (i+1)===S.step));
423
+ NS.el.ratios.style.display = S.step===1?'flex':'none';
424
+ }
425
+ function renderAll(){ layoutBoard(); renderBoxes(); renderSummary(); renderTable(); renderSteps();
426
+ NS.el.ratioChips.forEach(c=>c.classList.toggle('sel', c.dataset.r===S.aspect)); syncState(); }
427
+ function esc(s){return (s||'').replace(/[&<>]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[m]));}
428
+
429
+ function select(e){ sel=e; NS.el.board.querySelectorAll('.box').forEach(d=>
430
+ d.classList.toggle('sel', d.dataset.id===(e&&e.id))); }
431
+
432
+ // ---------- box interactions ----------
433
+ function px2norm(ev){
434
+ const r=NS.el.board.getBoundingClientRect();
435
+ return [clamp((ev.clientY-r.top)/r.height*1000,0,1000), clamp((ev.clientX-r.left)/r.width*1000,0,1000)];
436
+ }
437
+ function wireBox(d,e){
438
+ const idEl=d.querySelector('.id'), descEl=d.querySelector('.desc');
439
+ d.addEventListener('mousedown',ev=>{
440
+ if(ev.target.classList.contains('h')||ev.target.classList.contains('rm')||
441
+ ev.target.isContentEditable) { select(e); return; }
442
+ ev.preventDefault(); select(e); startMove(ev,e,d);
443
+ });
444
+ d.querySelectorAll('.h').forEach(h=>h.addEventListener('mousedown',ev=>{
445
+ ev.preventDefault(); ev.stopPropagation(); select(e);
446
+ startResize(ev,e,d,[...h.classList].find(c=>['nw','ne','sw','se'].includes(c)));
447
+ }));
448
+ d.querySelector('.rm').addEventListener('click',ev=>{ ev.stopPropagation();
449
+ S.elements=S.elements.filter(x=>x!==e); if(sel===e)sel=null; renderAll(); });
450
+ idEl.addEventListener('blur',()=>{ let v=idEl.textContent.trim().replace(/[^A-Za-z0-9_]+/g,'_');
451
+ if(!v){v=e.id;} if(v!==e.id){ const ids=new Set(S.elements.filter(x=>x!==e).map(x=>x.id));
452
+ let f=v,n=1; while(ids.has(f)){f=v+'_'+(++n);} e.id=f;} idEl.textContent=e.id; renderAll(); });
453
+ idEl.addEventListener('keydown',ev=>{ if(ev.key==='Enter'){ev.preventDefault();idEl.blur();} });
454
+ descEl.addEventListener('input',()=>{ e.desc=descEl.textContent; renderSummaryTable(); syncState(); });
455
+ descEl.addEventListener('focus',()=>select(e));
456
+ }
457
+ function renderSummaryTable(){ renderSummary(); renderTable(); }
458
+
459
+ function startMove(ev,e,d){
460
+ const [y,x]=px2norm(ev); const b=e.bbox.slice(); const oy=y-b[0], ox=x-b[1];
461
+ const w=b[3]-b[1], h=b[2]-b[0];
462
+ function mv(ev2){ const [ny,nx]=px2norm(ev2); let x0=clamp(nx-ox,0,1000-w), y0=clamp(ny-oy,0,1000-h);
463
+ e.bbox=[y0,x0,y0+h,x0+w]; posBox(d,e); liveMeta(e); }
464
+ function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); renderAll(); }
465
+ document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
466
+ }
467
+ function startResize(ev,e,d,corner){
468
+ function mv(ev2){ const [ny,nx]=px2norm(ev2); let [y0,x0,y1,x1]=e.bbox;
469
+ if(corner.includes('n'))y0=ny; if(corner.includes('s'))y1=ny;
470
+ if(corner.includes('w'))x0=nx; if(corner.includes('e'))x1=nx;
471
+ e.bbox=[Math.min(y0,y1),Math.min(x0,x1),Math.max(y0,y1),Math.max(x0,x1)]; posBox(d,e); liveMeta(e); }
472
+ function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); renderAll(); }
473
+ document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
474
+ }
475
+ function liveMeta(e){ renderSummary(); renderTable(); }
476
+
477
+ function startDraw(ev){
478
+ if(ev.target!==NS.el.board) return; // only empty canvas
479
+ if(S.rendered) return; // don't draw over a render
480
+ ev.preventDefault();
481
+ const [sy,sx]=px2norm(ev);
482
+ const e={id:uid('element'), color:'violet', bbox:[sy,sx,sy,sx], desc:''};
483
+ const d=boxEl(e); NS.el.board.appendChild(d);
484
+ function mv(ev2){ const [ny,nx]=px2norm(ev2);
485
+ e.bbox=[Math.min(sy,ny),Math.min(sx,nx),Math.max(sy,ny),Math.max(sx,nx)]; posBox(d,e); }
486
+ function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up);
487
+ if((e.bbox[2]-e.bbox[0])<30||(e.bbox[3]-e.bbox[1])<30){ d.remove(); return; }
488
+ S.elements.push(e); select(e); S.step=2; renderAll();
489
+ const de=NS.el.board.querySelector('.box[data-id="'+e.id+'"] .desc'); if(de)de.focus();
490
+ }
491
+ document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
492
+ }
493
+
494
+ // ---------- presets / tabs ----------
495
+ function loadPreset(name){
496
+ const p=window.F3_PRESETS[name]; if(!p) return;
497
+ S.aspect=p.aspect; S.scene=p.scene; S.seed=-1; S.rendered=false; S.step=2; sel=null;
498
+ S.elements=p.elements.map(e=>({id:e.id,color:'violet',bbox:e.bbox.slice(),desc:e.desc}));
499
+ NS.el.board.classList.remove('rendered','hideboxes','busy');
500
+ const img=NS.el.board.querySelector('img'); if(img)img.remove();
501
+ NS.el.scene.value=S.scene; NS.el.active=name;
502
+ NS.el.tabs.forEach(t=>t.classList.toggle('active', t.dataset.name===name));
503
+ NS.el.toggleBtn.style.display='none';
504
+ renderAll();
505
+ }
506
+
507
+ // ---------- generate / render ----------
508
+ function doGenerate(){
509
+ if(!S.elements.some(e=>(e.desc||'').trim()) && !(S.scene||'').trim()){
510
+ NS.el.scene.focus(); return;
511
+ }
512
+ S.seed=-1; S.step=4; renderSteps(); syncState();
513
+ NS.el.board.classList.add('busy');
514
+ NS.el.genbtn.setAttribute('disabled','');
515
+ setTimeout(()=>{ const b=document.querySelector('#f3-gen button'); if(b)b.click(); },70);
516
+ clearTimeout(NS._to); NS._to=setTimeout(()=>{ NS.el.board.classList.remove('busy');
517
+ NS.el.genbtn.removeAttribute('disabled'); }, 300000);
518
+ }
519
+ window.f3_render=function(resStr){
520
+ clearTimeout(NS._to);
521
+ NS.el.board.classList.remove('busy'); NS.el.genbtn.removeAttribute('disabled');
522
+ let r; try{ r=JSON.parse(resStr);}catch(e){ return; }
523
+ if(!r||!r.image) return;
524
+ layoutBoard();
525
+ let img=NS.el.board.querySelector('img'); if(!img){ img=document.createElement('img');
526
+ NS.el.board.insertBefore(img,NS.el.board.firstChild); } img.src=r.image;
527
+ S.rendered=true; S.seed=r.seed;
528
+ NS.el.board.classList.add('rendered'); NS.el.board.classList.remove('hideboxes');
529
+ S.showBoxes=true; updateToggle();
530
+ NS.el.toggleBtn.style.display='inline-flex';
531
+ renderBoxes();
532
+ };
533
+ function updateToggle(){
534
+ NS.el.toggleBtn.classList.toggle('on',S.showBoxes);
535
+ NS.el.board.classList.toggle('hideboxes',!S.showBoxes);
536
+ }
537
+
538
+ // ---------- build DOM ----------
539
+ function build(root){
540
+ const order=window.F3_PRESET_ORDER||Object.keys(window.F3_PRESETS||{});
541
+ root.innerHTML=''
542
+ +'<div class="tabs"></div>'
543
+ +'<div class="main">'
544
+ + '<div class="canvas-wrap"><div class="board empty"><div class="hint">Drag to draw your first element box, or pick a scene above.</div><div class="spin">Rendering with FLUX.3…</div></div>'
545
+ + '<div class="ratio-tag"></div>'
546
+ + '<div class="board-tools">'
547
+ + '<div class="btn toggleBtn on" style="display:none"><span class="toggle"></span>Bounding boxes</div>'
548
+ + '<div class="btn replayBtn">↻ Replay</div>'
549
+ + '</div></div>'
550
+ + '<div class="side">'
551
+ + '<div class="steps"></div>'
552
+ + '<div class="summary"></div>'
553
+ + '<div class="scene"><label>SCENE PROMPT</label><textarea rows="2" placeholder="Describe the whole scene in one line."></textarea></div>'
554
+ + '<div class="gen"><button class="genbtn">Generate</button></div>'
555
+ + '</div>'
556
+ +'</div>'
557
+ +'<div class="table-bar"><span class="lab">+ VIEW THE ELEMENT TABLE</span><span class="cnt">0 ROWS</span></div>'
558
+ +'<div class="etable"></div>';
559
+
560
+ // tabs
561
+ const tabsEl=root.querySelector('.tabs');
562
+ const tabs=order.map(name=>{ const t=document.createElement('div'); t.className='tab'; t.dataset.name=name;
563
+ t.textContent=name; t.addEventListener('click',()=>loadPreset(name)); tabsEl.appendChild(t); return t; });
564
+ // steps
565
+ const stepsWrap=root.querySelector('.steps'); const steps=STEP_HINTS.map((h,i)=>{
566
+ const st=document.createElement('div'); st.className='step';
567
+ st.innerHTML='<div class="row"><span class="n">0'+(i+1)+'</span><span class="t">'+h[0]+'</span></div>'
568
+ +'<div class="hint">'+h[1]+'</div>'+(i===0?'<div class="ratios"></div>':'');
569
+ st.addEventListener('click',()=>{ S.step=i+1; renderSteps(); }); stepsWrap.appendChild(st); return st; });
570
+ // ratio chips in step 1
571
+ const ratiosEl=steps[0].querySelector('.ratios');
572
+ const ratioChips=(window.F3_RATIOS||Object.keys(RATIOS)).map(r=>{ const c=document.createElement('div');
573
+ c.className='chip'; c.dataset.r=r; c.textContent=r;
574
+ c.addEventListener('click',ev=>{ ev.stopPropagation(); S.aspect=r; S.rendered=false;
575
+ const img=NS.el.board.querySelector('img'); if(img)img.remove();
576
+ NS.el.board.classList.remove('rendered','hideboxes'); NS.el.toggleBtn.style.display='none';
577
+ renderAll(); }); ratiosEl.appendChild(c); return c; });
578
+
579
+ NS.el={ root, wrap:root.querySelector('.canvas-wrap'), board:root.querySelector('.board'),
580
+ ratioTag:root.querySelector('.ratio-tag'), tabs, steps, ratios:ratiosEl, ratioChips,
581
+ summary:root.querySelector('.summary'), scene:root.querySelector('.scene textarea'),
582
+ genbtn:root.querySelector('.genbtn'), toggleBtn:root.querySelector('.toggleBtn'),
583
+ replayBtn:root.querySelector('.replayBtn'), cnt:root.querySelector('.table-bar .cnt'),
584
+ etable:root.querySelector('.etable') };
585
+
586
+ NS.el.board.addEventListener('mousedown',startDraw);
587
+ NS.el.scene.addEventListener('input',()=>{ S.scene=NS.el.scene.value; S.step=3; syncState(); });
588
+ NS.el.genbtn.addEventListener('click',doGenerate);
589
+ NS.el.replayBtn.addEventListener('click',doGenerate);
590
+ NS.el.toggleBtn.addEventListener('click',()=>{ S.showBoxes=!S.showBoxes; updateToggle(); });
591
+ root.querySelector('.table-bar').addEventListener('click',()=>NS.el.etable.classList.toggle('open'));
592
+ window.addEventListener('resize',()=>{ layoutBoard(); });
593
+
594
+ loadPreset(order[order.length-1]); // start on "Up from the subway" like the screenshots
595
+ }
596
+
597
+ function init(){
598
+ const root=document.getElementById('f3-root');
599
+ if(!root || !document.getElementById('f3-state') || !document.querySelector('#f3-gen button')){
600
+ return false;
601
+ }
602
+ if(root.dataset.ready) return true;
603
+ root.dataset.ready='1'; build(root); return true;
604
+ }
605
+ const iv=setInterval(()=>{ if(init()) clearInterval(iv); }, 120);
606
+ })();
607
+ </script>
608
+ """
609
+
610
+ HEAD = STYLE + STYLE2 + CONFIG_JS + SCRIPT
611
+
612
+ with gr.Blocks(head=HEAD, title="FLUX.3 layout canvas",
613
+ css="footer{display:none!important} .gradio-container{max-width:1240px!important}") as demo:
614
+ state_box = gr.Textbox(visible=False, elem_id="f3-state")
615
+ result_box = gr.Textbox(visible=False, elem_id="f3-result")
616
+ gen_btn = gr.Button("gen", visible=False, elem_id="f3-gen")
617
+ gr.HTML('<div id="f3-root"></div>')
618
+
619
+ ev = gen_btn.click(generate, [state_box], [result_box], api_name="generate",
620
+ show_progress="hidden")
621
+ ev.then(None, [result_box], None, js="(r)=>{ if(window.f3_render) window.f3_render(r); }")
622
+
623
+ if __name__ == "__main__":
624
+ demo.queue(max_size=12).launch(show_error=True)
diffusers/__init__.py ADDED
@@ -0,0 +1,1812 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __version__ = "0.41.0.dev0"
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .utils import (
6
+ DIFFUSERS_SLOW_IMPORT,
7
+ OptionalDependencyNotAvailable,
8
+ _LazyModule,
9
+ is_accelerate_available,
10
+ is_auto_round_available,
11
+ is_bitsandbytes_available,
12
+ is_gguf_available,
13
+ is_librosa_available,
14
+ is_note_seq_available,
15
+ is_nvidia_modelopt_available,
16
+ is_onnx_available,
17
+ is_opencv_available,
18
+ is_optimum_quanto_available,
19
+ is_scipy_available,
20
+ is_sdnq_available,
21
+ is_sentencepiece_available,
22
+ is_torch_available,
23
+ is_torchao_available,
24
+ is_torchsde_available,
25
+ is_transformers_available,
26
+ is_transformers_version,
27
+ )
28
+
29
+
30
+ # Lazy Import based on
31
+ # https://github.com/huggingface/transformers/blob/main/src/transformers/__init__.py
32
+
33
+ # When adding a new object to this init, please add it to `_import_structure`. The `_import_structure` is a dictionary submodule to list of object names,
34
+ # and is used to defer the actual importing for when the objects are requested.
35
+ # This way `import diffusers` provides the names in the namespace without actually importing anything (and especially none of the backends).
36
+
37
+ _import_structure = {
38
+ "configuration_utils": ["ConfigMixin"],
39
+ "guiders": [],
40
+ "hooks": [],
41
+ "loaders": ["FromOriginalModelMixin"],
42
+ "models": [],
43
+ "modular_pipelines": [],
44
+ "pipelines": [],
45
+ "quantizers.pipe_quant_config": ["PipelineQuantizationConfig"],
46
+ "quantizers.quantization_config": [],
47
+ "schedulers": [],
48
+ "utils": [
49
+ "OptionalDependencyNotAvailable",
50
+ "is_inflect_available",
51
+ "is_invisible_watermark_available",
52
+ "is_librosa_available",
53
+ "is_note_seq_available",
54
+ "is_onnx_available",
55
+ "is_scipy_available",
56
+ "is_torch_available",
57
+ "is_torchsde_available",
58
+ "is_transformers_available",
59
+ "is_transformers_version",
60
+ "is_unidecode_available",
61
+ "logging",
62
+ ],
63
+ }
64
+
65
+ try:
66
+ if not is_torch_available() and not is_accelerate_available() and not is_bitsandbytes_available():
67
+ raise OptionalDependencyNotAvailable()
68
+ except OptionalDependencyNotAvailable:
69
+ from .utils import dummy_bitsandbytes_objects
70
+
71
+ _import_structure["utils.dummy_bitsandbytes_objects"] = [
72
+ name for name in dir(dummy_bitsandbytes_objects) if not name.startswith("_")
73
+ ]
74
+ else:
75
+ _import_structure["quantizers.quantization_config"].append("BitsAndBytesConfig")
76
+
77
+ try:
78
+ if not is_torch_available() and not is_accelerate_available() and not is_gguf_available():
79
+ raise OptionalDependencyNotAvailable()
80
+ except OptionalDependencyNotAvailable:
81
+ from .utils import dummy_gguf_objects
82
+
83
+ _import_structure["utils.dummy_gguf_objects"] = [
84
+ name for name in dir(dummy_gguf_objects) if not name.startswith("_")
85
+ ]
86
+ else:
87
+ _import_structure["quantizers.quantization_config"].append("GGUFQuantizationConfig")
88
+
89
+ try:
90
+ if not is_torch_available() and not is_accelerate_available() and not is_torchao_available():
91
+ raise OptionalDependencyNotAvailable()
92
+ except OptionalDependencyNotAvailable:
93
+ from .utils import dummy_torchao_objects
94
+
95
+ _import_structure["utils.dummy_torchao_objects"] = [
96
+ name for name in dir(dummy_torchao_objects) if not name.startswith("_")
97
+ ]
98
+ else:
99
+ _import_structure["quantizers.quantization_config"].append("TorchAoConfig")
100
+
101
+ try:
102
+ if not is_torch_available() and not is_accelerate_available() and not is_optimum_quanto_available():
103
+ raise OptionalDependencyNotAvailable()
104
+ except OptionalDependencyNotAvailable:
105
+ from .utils import dummy_optimum_quanto_objects
106
+
107
+ _import_structure["utils.dummy_optimum_quanto_objects"] = [
108
+ name for name in dir(dummy_optimum_quanto_objects) if not name.startswith("_")
109
+ ]
110
+ else:
111
+ _import_structure["quantizers.quantization_config"].append("QuantoConfig")
112
+
113
+ try:
114
+ if not is_torch_available() and not is_accelerate_available() and not is_nvidia_modelopt_available():
115
+ raise OptionalDependencyNotAvailable()
116
+ except OptionalDependencyNotAvailable:
117
+ from .utils import dummy_nvidia_modelopt_objects
118
+
119
+ _import_structure["utils.dummy_nvidia_modelopt_objects"] = [
120
+ name for name in dir(dummy_nvidia_modelopt_objects) if not name.startswith("_")
121
+ ]
122
+ else:
123
+ _import_structure["quantizers.quantization_config"].append("NVIDIAModelOptConfig")
124
+
125
+ try:
126
+ if not is_torch_available():
127
+ raise OptionalDependencyNotAvailable()
128
+ except OptionalDependencyNotAvailable:
129
+ from .utils import dummy_nunchaku_lite_objects
130
+
131
+ _import_structure["utils.dummy_nunchaku_lite_objects"] = [
132
+ name for name in dir(dummy_nunchaku_lite_objects) if not name.startswith("_")
133
+ ]
134
+ else:
135
+ _import_structure["quantizers.quantization_config"].append("NunchakuLiteQuantizationConfig")
136
+
137
+ try:
138
+ if not is_auto_round_available():
139
+ raise OptionalDependencyNotAvailable()
140
+ except OptionalDependencyNotAvailable:
141
+ from .utils import dummy_auto_round_objects
142
+
143
+ _import_structure["utils.dummy_auto_round_objects"] = [
144
+ name for name in dir(dummy_auto_round_objects) if not name.startswith("_")
145
+ ]
146
+ else:
147
+ _import_structure["quantizers.quantization_config"].append("AutoRoundConfig")
148
+
149
+ try:
150
+ if not is_torch_available() and not is_accelerate_available() and not is_sdnq_available():
151
+ raise OptionalDependencyNotAvailable()
152
+ except OptionalDependencyNotAvailable:
153
+ from .utils import dummy_sdnq_objects
154
+
155
+ _import_structure["utils.dummy_sdnq_objects"] = [
156
+ name for name in dir(dummy_sdnq_objects) if not name.startswith("_")
157
+ ]
158
+ else:
159
+ _import_structure["quantizers.quantization_config"].append("SDNQConfig")
160
+
161
+ try:
162
+ if not is_onnx_available():
163
+ raise OptionalDependencyNotAvailable()
164
+ except OptionalDependencyNotAvailable:
165
+ from .utils import dummy_onnx_objects # noqa F403
166
+
167
+ _import_structure["utils.dummy_onnx_objects"] = [
168
+ name for name in dir(dummy_onnx_objects) if not name.startswith("_")
169
+ ]
170
+
171
+ else:
172
+ _import_structure["pipelines"].extend(["OnnxRuntimeModel"])
173
+
174
+ try:
175
+ if not is_torch_available():
176
+ raise OptionalDependencyNotAvailable()
177
+ except OptionalDependencyNotAvailable:
178
+ from .utils import dummy_pt_objects # noqa F403
179
+
180
+ _import_structure["utils.dummy_pt_objects"] = [name for name in dir(dummy_pt_objects) if not name.startswith("_")]
181
+
182
+ else:
183
+ _import_structure["guiders"].extend(
184
+ [
185
+ "AdaptiveProjectedGuidance",
186
+ "AdaptiveProjectedMixGuidance",
187
+ "AutoGuidance",
188
+ "BaseGuidance",
189
+ "ClassifierFreeGuidance",
190
+ "ClassifierFreeZeroStarGuidance",
191
+ "FrequencyDecoupledGuidance",
192
+ "LTX2Guidance",
193
+ "PerturbedAttentionGuidance",
194
+ "SkipLayerGuidance",
195
+ "SmoothedEnergyGuidance",
196
+ "TangentialClassifierFreeGuidance",
197
+ ]
198
+ )
199
+ _import_structure["hooks"].extend(
200
+ [
201
+ "FasterCacheConfig",
202
+ "FirstBlockCacheConfig",
203
+ "HookRegistry",
204
+ "LayerSkipConfig",
205
+ "MagCacheConfig",
206
+ "PyramidAttentionBroadcastConfig",
207
+ "SmoothedEnergyGuidanceConfig",
208
+ "TaylorSeerCacheConfig",
209
+ "TextKVCacheConfig",
210
+ "apply_faster_cache",
211
+ "apply_first_block_cache",
212
+ "apply_layer_skip",
213
+ "apply_mag_cache",
214
+ "apply_pyramid_attention_broadcast",
215
+ "apply_taylorseer_cache",
216
+ "apply_text_kv_cache",
217
+ ]
218
+ )
219
+ _import_structure["image_processor"] = [
220
+ "InpaintProcessor",
221
+ "IPAdapterMaskProcessor",
222
+ "PixArtImageProcessor",
223
+ "VaeImageProcessor",
224
+ "VaeImageProcessorLDM3D",
225
+ ]
226
+ _import_structure["models"].extend(
227
+ [
228
+ "AceStepTransformer1DModel",
229
+ "AllegroTransformer3DModel",
230
+ "AnimaTextConditioner",
231
+ "AnyFlowFARTransformer3DModel",
232
+ "AnyFlowTransformer3DModel",
233
+ "AsymmetricAutoencoderKL",
234
+ "AttentionBackendName",
235
+ "AuraFlowTransformer2DModel",
236
+ "AutoencoderDC",
237
+ "AutoencoderKL",
238
+ "AutoencoderKLAllegro",
239
+ "AutoencoderKLCogVideoX",
240
+ "AutoencoderKLCosmos",
241
+ "AutoencoderKLFlux2",
242
+ "AutoencoderKLHunyuanImage",
243
+ "AutoencoderKLHunyuanImageRefiner",
244
+ "AutoencoderKLHunyuanVideo",
245
+ "AutoencoderKLHunyuanVideo15",
246
+ "AutoencoderKLKVAE",
247
+ "AutoencoderKLKVAEVideo",
248
+ "AutoencoderKLLTX2Audio",
249
+ "AutoencoderKLLTX2Video",
250
+ "AutoencoderKLLTXVideo",
251
+ "AutoencoderKLMagvit",
252
+ "AutoencoderKLMiniMaxH3",
253
+ "AutoencoderKLMiniMaxH3Audio",
254
+ "AutoencoderKLMochi",
255
+ "AutoencoderKLQwenImage",
256
+ "AutoencoderKLTemporalDecoder",
257
+ "AutoencoderKLWan",
258
+ "AutoencoderOobleck",
259
+ "AutoencoderRAE",
260
+ "AutoencoderSAME",
261
+ "AutoencoderTiny",
262
+ "AutoencoderVidTok",
263
+ "AutoModel",
264
+ "BriaFiboTransformer2DModel",
265
+ "BriaTransformer2DModel",
266
+ "CacheMixin",
267
+ "ChromaTransformer2DModel",
268
+ "ChronoEditTransformer3DModel",
269
+ "CogVideoXTransformer3DModel",
270
+ "CogView3PlusTransformer2DModel",
271
+ "CogView4Transformer2DModel",
272
+ "ConsisIDTransformer3DModel",
273
+ "ConsistencyDecoderVAE",
274
+ "ContextParallelConfig",
275
+ "ControlNetModel",
276
+ "ControlNetUnionModel",
277
+ "ControlNetXSAdapter",
278
+ "Cosmos3AVAEAudioTokenizer",
279
+ "Cosmos3OmniTransformer",
280
+ "CosmosControlNetModel",
281
+ "CosmosTransformer3DModel",
282
+ "DiTTransformer2DModel",
283
+ "DreamLiteTransformer2DModel",
284
+ "DreamLiteUNetModel",
285
+ "EasyAnimateTransformer3DModel",
286
+ "ErnieImageTransformer2DModel",
287
+ "Flux2Transformer2DModel",
288
+ "Flux3Transformer2DModel",
289
+ "FluxControlNetModel",
290
+ "FluxMultiControlNetModel",
291
+ "FluxTransformer2DModel",
292
+ "GlmImageTransformer2DModel",
293
+ "HeliosTransformer3DModel",
294
+ "HiDreamImageTransformer2DModel",
295
+ "HunyuanDiT2DControlNetModel",
296
+ "HunyuanDiT2DModel",
297
+ "HunyuanDiT2DMultiControlNetModel",
298
+ "HunyuanImageTransformer2DModel",
299
+ "HunyuanVideo15Transformer3DModel",
300
+ "HunyuanVideoFramepackTransformer3DModel",
301
+ "HunyuanVideoTransformer3DModel",
302
+ "I2VGenXLUNet",
303
+ "Ideogram4Transformer2DModel",
304
+ "JoyImageEditPlusTransformer3DModel",
305
+ "JoyImageEditTransformer3DModel",
306
+ "Kandinsky3UNet",
307
+ "Kandinsky5Transformer3DModel",
308
+ "Krea2Transformer2DModel",
309
+ "LatteTransformer3DModel",
310
+ "LongCatAudioDiTTransformer",
311
+ "LongCatAudioDiTVae",
312
+ "LongCatImageTransformer2DModel",
313
+ "LTX2VideoDiffusionDecoderModel",
314
+ "LTX2VideoTransformer3DModel",
315
+ "LTXVideoTransformer3DModel",
316
+ "Lumina2Transformer2DModel",
317
+ "LuminaNextDiT2DModel",
318
+ "MiniMaxH3Transformer3DModel",
319
+ "MiniMaxMusic3ConditionEncoder",
320
+ "MiniMaxMusic3RVQDepthDecoder",
321
+ "MiniMaxMusic3Transformer1DModel",
322
+ "MiniMaxMusic3Vocoder",
323
+ "MochiTransformer3DModel",
324
+ "ModelMixin",
325
+ "MotifVideoTransformer3DModel",
326
+ "MotionAdapter",
327
+ "MultiAdapter",
328
+ "MultiControlNetModel",
329
+ "NucleusMoEImageTransformer2DModel",
330
+ "OmniGenTransformer2DModel",
331
+ "OvisImageTransformer2DModel",
332
+ "ParallelConfig",
333
+ "PixArtTransformer2DModel",
334
+ "PriorTransformer",
335
+ "PRXTransformer2DModel",
336
+ "QwenImageControlNetModel",
337
+ "QwenImageMultiControlNetModel",
338
+ "QwenImageTransformer2DModel",
339
+ "SanaControlNetModel",
340
+ "SanaTransformer2DModel",
341
+ "SanaVideoTransformer3DModel",
342
+ "SD3ControlNetModel",
343
+ "SD3MultiControlNetModel",
344
+ "SD3Transformer2DModel",
345
+ "SkyReelsV2Transformer3DModel",
346
+ "SparseControlNetModel",
347
+ "StableAudio3DiTModel",
348
+ "StableAudioDiTModel",
349
+ "StableCascadeUNet",
350
+ "T2IAdapter",
351
+ "T5FilmDecoder",
352
+ "TensorParallelConfig",
353
+ "Transformer2DModel",
354
+ "TransformerTemporalModel",
355
+ "UNet1DModel",
356
+ "UNet2DConditionModel",
357
+ "UNet2DModel",
358
+ "UNet3DConditionModel",
359
+ "UNetControlNetXSModel",
360
+ "UNetMotionModel",
361
+ "UNetSpatioTemporalConditionModel",
362
+ "UVit2DModel",
363
+ "VQModel",
364
+ "WanAnimate2Transformer3DModel",
365
+ "WanAnimateTransformer3DModel",
366
+ "WanTransformer3DModel",
367
+ "WanVACETransformer3DModel",
368
+ "ZImageControlNetModel",
369
+ "ZImageTransformer2DModel",
370
+ "attention_backend",
371
+ ]
372
+ )
373
+ _import_structure["modular_pipelines"].extend(
374
+ [
375
+ "AutoPipelineBlocks",
376
+ "ComponentsManager",
377
+ "ComponentSpec",
378
+ "ConditionalPipelineBlocks",
379
+ "ConfigSpec",
380
+ "InputParam",
381
+ "LoopSequentialPipelineBlocks",
382
+ "ModularPipeline",
383
+ "ModularPipelineBlocks",
384
+ "OutputParam",
385
+ "SequentialPipelineBlocks",
386
+ ]
387
+ )
388
+ _import_structure["optimization"] = [
389
+ "get_constant_schedule",
390
+ "get_constant_schedule_with_warmup",
391
+ "get_cosine_schedule_with_warmup",
392
+ "get_cosine_with_hard_restarts_schedule_with_warmup",
393
+ "get_linear_schedule_with_warmup",
394
+ "get_polynomial_decay_schedule_with_warmup",
395
+ "get_scheduler",
396
+ ]
397
+ _import_structure["pipelines"].extend(
398
+ [
399
+ "AudioPipelineOutput",
400
+ "AutoPipelineForImage2Image",
401
+ "AutoPipelineForInpainting",
402
+ "AutoPipelineForText2Audio",
403
+ "AutoPipelineForText2Image",
404
+ "ConsistencyModelPipeline",
405
+ "DanceDiffusionPipeline",
406
+ "DDIMPipeline",
407
+ "DDPMPipeline",
408
+ "DiffusionPipeline",
409
+ "DiTPipeline",
410
+ "ImagePipelineOutput",
411
+ "KarrasVePipeline",
412
+ "LDMPipeline",
413
+ "LDMSuperResolutionPipeline",
414
+ "PNDMPipeline",
415
+ "RePaintPipeline",
416
+ "ScoreSdeVePipeline",
417
+ "StableDiffusionMixin",
418
+ ]
419
+ )
420
+ _import_structure["quantizers"] = ["DiffusersQuantizer"]
421
+ _import_structure["schedulers"].extend(
422
+ [
423
+ "AmusedScheduler",
424
+ "BlockRefinementScheduler",
425
+ "BlockRefinementSchedulerOutput",
426
+ "CMStochasticIterativeScheduler",
427
+ "CogVideoXDDIMScheduler",
428
+ "CogVideoXDPMScheduler",
429
+ "DDIMInverseScheduler",
430
+ "DDIMParallelScheduler",
431
+ "DDIMScheduler",
432
+ "DDPMParallelScheduler",
433
+ "DDPMScheduler",
434
+ "DDPMWuerstchenScheduler",
435
+ "DEISMultistepScheduler",
436
+ "DiscreteDDIMScheduler",
437
+ "DiscreteDDIMSchedulerOutput",
438
+ "DPMSolverMultistepInverseScheduler",
439
+ "DPMSolverMultistepScheduler",
440
+ "DPMSolverSinglestepScheduler",
441
+ "EDMDPMSolverMultistepScheduler",
442
+ "EDMEulerScheduler",
443
+ "EntropyBoundScheduler",
444
+ "EntropyBoundSchedulerOutput",
445
+ "EulerAncestralDiscreteScheduler",
446
+ "EulerDiscreteScheduler",
447
+ "FlowMapEulerDiscreteScheduler",
448
+ "FlowMatchEulerDiscreteScheduler",
449
+ "FlowMatchHeunDiscreteScheduler",
450
+ "FlowMatchLCMScheduler",
451
+ "HeliosDMDScheduler",
452
+ "HeliosScheduler",
453
+ "HeunDiscreteScheduler",
454
+ "IPNDMScheduler",
455
+ "KarrasVeScheduler",
456
+ "KDPM2AncestralDiscreteScheduler",
457
+ "KDPM2DiscreteScheduler",
458
+ "LCMScheduler",
459
+ "LTXEulerAncestralRFScheduler",
460
+ "MiniMaxH3Scheduler",
461
+ "PNDMScheduler",
462
+ "RePaintScheduler",
463
+ "SASolverScheduler",
464
+ "SchedulerMixin",
465
+ "SCMScheduler",
466
+ "ScoreSdeVeScheduler",
467
+ "TCDScheduler",
468
+ "UnCLIPScheduler",
469
+ "UniPCMultistepScheduler",
470
+ "VQDiffusionScheduler",
471
+ ]
472
+ )
473
+ _import_structure["training_utils"] = ["EMAModel"]
474
+ _import_structure["video_processor"] = ["VideoProcessor"]
475
+
476
+ try:
477
+ if not (is_torch_available() and is_scipy_available()):
478
+ raise OptionalDependencyNotAvailable()
479
+ except OptionalDependencyNotAvailable:
480
+ from .utils import dummy_torch_and_scipy_objects # noqa F403
481
+
482
+ _import_structure["utils.dummy_torch_and_scipy_objects"] = [
483
+ name for name in dir(dummy_torch_and_scipy_objects) if not name.startswith("_")
484
+ ]
485
+
486
+ else:
487
+ _import_structure["schedulers"].extend(["LMSDiscreteScheduler"])
488
+
489
+ try:
490
+ if not (is_torch_available() and is_torchsde_available()):
491
+ raise OptionalDependencyNotAvailable()
492
+ except OptionalDependencyNotAvailable:
493
+ from .utils import dummy_torch_and_torchsde_objects # noqa F403
494
+
495
+ _import_structure["utils.dummy_torch_and_torchsde_objects"] = [
496
+ name for name in dir(dummy_torch_and_torchsde_objects) if not name.startswith("_")
497
+ ]
498
+
499
+ else:
500
+ _import_structure["schedulers"].extend(["CosineDPMSolverMultistepScheduler", "DPMSolverSDEScheduler"])
501
+
502
+ try:
503
+ if not (is_torch_available() and is_transformers_available()):
504
+ raise OptionalDependencyNotAvailable()
505
+ except OptionalDependencyNotAvailable:
506
+ from .utils import dummy_torch_and_transformers_objects # noqa F403
507
+
508
+ _import_structure["utils.dummy_torch_and_transformers_objects"] = [
509
+ name for name in dir(dummy_torch_and_transformers_objects) if not name.startswith("_")
510
+ ]
511
+
512
+ else:
513
+ _import_structure["modular_pipelines"].extend(
514
+ [
515
+ "AnimaAutoBlocks",
516
+ "AnimaModularPipeline",
517
+ "Cosmos3DistilledBlocks",
518
+ "Cosmos3DistilledModularPipeline",
519
+ "Cosmos3OmniBlocks",
520
+ "Cosmos3OmniModularPipeline",
521
+ "ErnieImageAutoBlocks",
522
+ "ErnieImageModularPipeline",
523
+ "Flux2AutoBlocks",
524
+ "Flux2KleinAutoBlocks",
525
+ "Flux2KleinBaseAutoBlocks",
526
+ "Flux2KleinBaseModularPipeline",
527
+ "Flux2KleinModularPipeline",
528
+ "Flux2ModularPipeline",
529
+ "Flux3AutoBlocks",
530
+ "Flux3ModularPipeline",
531
+ "FluxAutoBlocks",
532
+ "FluxKontextAutoBlocks",
533
+ "FluxKontextModularPipeline",
534
+ "FluxModularPipeline",
535
+ "HeliosAutoBlocks",
536
+ "HeliosModularPipeline",
537
+ "HeliosPyramidAutoBlocks",
538
+ "HeliosPyramidDistilledAutoBlocks",
539
+ "HeliosPyramidDistilledModularPipeline",
540
+ "HeliosPyramidModularPipeline",
541
+ "HunyuanVideo15AutoBlocks",
542
+ "HunyuanVideo15ModularPipeline",
543
+ "Ideogram4AutoBlocks",
544
+ "Ideogram4ModularPipeline",
545
+ "Krea2AutoBlocks",
546
+ "Krea2ModularPipeline",
547
+ "Krea2TurboAutoBlocks",
548
+ "Krea2TurboModularPipeline",
549
+ "LTX25AutoBlocks",
550
+ "LTX25ModularPipeline",
551
+ "LTX2AutoBlocks",
552
+ "LTX2ModularPipeline",
553
+ "LTXAutoBlocks",
554
+ "LTXModularPipeline",
555
+ "MiniMaxH3Blocks",
556
+ "MiniMaxH3ModularPipeline",
557
+ "MiniMaxMusic3Blocks",
558
+ "MiniMaxMusic3ModularPipeline",
559
+ "QwenImageAutoBlocks",
560
+ "QwenImageEditAutoBlocks",
561
+ "QwenImageEditModularPipeline",
562
+ "QwenImageEditPlusAutoBlocks",
563
+ "QwenImageEditPlusModularPipeline",
564
+ "QwenImageLayeredAutoBlocks",
565
+ "QwenImageLayeredModularPipeline",
566
+ "QwenImageModularPipeline",
567
+ "StableDiffusion3AutoBlocks",
568
+ "StableDiffusion3ModularPipeline",
569
+ "StableDiffusionXLAutoBlocks",
570
+ "StableDiffusionXLModularPipeline",
571
+ "Wan22Blocks",
572
+ "Wan22Image2VideoBlocks",
573
+ "Wan22Image2VideoModularPipeline",
574
+ "Wan22ModularPipeline",
575
+ "Wan22VaceBlocks",
576
+ "Wan22VaceModularPipeline",
577
+ "WanAnimate2Blocks",
578
+ "WanAnimate2DistilledBlocks",
579
+ "WanAnimate2DistilledModularPipeline",
580
+ "WanAnimate2ModularPipeline",
581
+ "WanBlocks",
582
+ "WanImage2VideoAutoBlocks",
583
+ "WanImage2VideoModularPipeline",
584
+ "WanModularPipeline",
585
+ "ZImageAutoBlocks",
586
+ "ZImageModularPipeline",
587
+ ]
588
+ )
589
+ _import_structure["pipelines"].extend(
590
+ [
591
+ "AceStepAudioTokenDetokenizer",
592
+ "AceStepAudioTokenizer",
593
+ "AceStepConditionEncoder",
594
+ "AceStepPipeline",
595
+ "AllegroPipeline",
596
+ "AltDiffusionImg2ImgPipeline",
597
+ "AltDiffusionPipeline",
598
+ "AmusedImg2ImgPipeline",
599
+ "AmusedInpaintPipeline",
600
+ "AmusedPipeline",
601
+ "AnimateDiffControlNetPipeline",
602
+ "AnimateDiffPAGPipeline",
603
+ "AnimateDiffPipeline",
604
+ "AnimateDiffSDXLPipeline",
605
+ "AnimateDiffSparseControlNetPipeline",
606
+ "AnimateDiffVideoToVideoControlNetPipeline",
607
+ "AnimateDiffVideoToVideoPipeline",
608
+ "AnyFlowFARPipeline",
609
+ "AnyFlowPipeline",
610
+ "AudioLDM2Pipeline",
611
+ "AudioLDM2ProjectionModel",
612
+ "AudioLDM2UNet2DConditionModel",
613
+ "AudioLDMPipeline",
614
+ "AuraFlowPipeline",
615
+ "BlipDiffusionControlNetPipeline",
616
+ "BlipDiffusionPipeline",
617
+ "BriaFiboEditPipeline",
618
+ "BriaFiboPipeline",
619
+ "BriaPipeline",
620
+ "ChromaImg2ImgPipeline",
621
+ "ChromaInpaintPipeline",
622
+ "ChromaPipeline",
623
+ "ChronoEditPipeline",
624
+ "CLIPImageProjection",
625
+ "CogVideoXFunControlPipeline",
626
+ "CogVideoXImageToVideoPipeline",
627
+ "CogVideoXPipeline",
628
+ "CogVideoXVideoToVideoPipeline",
629
+ "CogView3PlusPipeline",
630
+ "CogView4ControlPipeline",
631
+ "CogView4Pipeline",
632
+ "ConsisIDPipeline",
633
+ "Cosmos2_5_PredictBasePipeline",
634
+ "Cosmos2_5_TransferPipeline",
635
+ "Cosmos2TextToImagePipeline",
636
+ "Cosmos2VideoToWorldPipeline",
637
+ "Cosmos3OmniPipeline",
638
+ "CosmosActionCondition",
639
+ "CosmosTextToWorldPipeline",
640
+ "CosmosVideoToWorldPipeline",
641
+ "CycleDiffusionPipeline",
642
+ "DiffusionGemmaPipeline",
643
+ "DiffusionGemmaPipelineOutput",
644
+ "DreamLiteMobilePipeline",
645
+ "DreamLitePipeline",
646
+ "DreamLitePipelineOutput",
647
+ "EasyAnimateControlPipeline",
648
+ "EasyAnimateInpaintPipeline",
649
+ "EasyAnimatePipeline",
650
+ "ErnieImagePipeline",
651
+ "Flux2KleinInpaintPipeline",
652
+ "Flux2KleinKVPipeline",
653
+ "Flux2KleinPipeline",
654
+ "Flux2Pipeline",
655
+ "FluxControlImg2ImgPipeline",
656
+ "FluxControlInpaintPipeline",
657
+ "FluxControlNetImg2ImgPipeline",
658
+ "FluxControlNetInpaintPipeline",
659
+ "FluxControlNetPipeline",
660
+ "FluxControlPipeline",
661
+ "FluxFillPipeline",
662
+ "FluxImg2ImgPipeline",
663
+ "FluxInpaintPipeline",
664
+ "FluxKontextInpaintPipeline",
665
+ "FluxKontextPipeline",
666
+ "FluxPipeline",
667
+ "FluxPriorReduxPipeline",
668
+ "GlmImagePipeline",
669
+ "HeliosPipeline",
670
+ "HeliosPyramidPipeline",
671
+ "HiDreamImagePipeline",
672
+ "HunyuanDiTControlNetPipeline",
673
+ "HunyuanDiTPAGPipeline",
674
+ "HunyuanDiTPipeline",
675
+ "HunyuanImagePipeline",
676
+ "HunyuanImageRefinerPipeline",
677
+ "HunyuanSkyreelsImageToVideoPipeline",
678
+ "HunyuanVideo15ImageToVideoPipeline",
679
+ "HunyuanVideo15Pipeline",
680
+ "HunyuanVideoFramepackPipeline",
681
+ "HunyuanVideoImageToVideoPipeline",
682
+ "HunyuanVideoPipeline",
683
+ "I2VGenXLPipeline",
684
+ "Ideogram4Pipeline",
685
+ "Ideogram4PromptEnhancerHead",
686
+ "IFImg2ImgPipeline",
687
+ "IFImg2ImgSuperResolutionPipeline",
688
+ "IFInpaintingPipeline",
689
+ "IFInpaintingSuperResolutionPipeline",
690
+ "IFPipeline",
691
+ "IFSuperResolutionPipeline",
692
+ "ImageTextPipelineOutput",
693
+ "JoyImageEditPipeline",
694
+ "JoyImageEditPipelineOutput",
695
+ "JoyImageEditPlusPipeline",
696
+ "JoyImageEditPlusPipelineOutput",
697
+ "Kandinsky3Img2ImgPipeline",
698
+ "Kandinsky3Pipeline",
699
+ "Kandinsky5I2IPipeline",
700
+ "Kandinsky5I2VPipeline",
701
+ "Kandinsky5T2IPipeline",
702
+ "Kandinsky5T2VPipeline",
703
+ "KandinskyCombinedPipeline",
704
+ "KandinskyImg2ImgCombinedPipeline",
705
+ "KandinskyImg2ImgPipeline",
706
+ "KandinskyInpaintCombinedPipeline",
707
+ "KandinskyInpaintPipeline",
708
+ "KandinskyPipeline",
709
+ "KandinskyPriorPipeline",
710
+ "KandinskyV22CombinedPipeline",
711
+ "KandinskyV22ControlnetImg2ImgPipeline",
712
+ "KandinskyV22ControlnetPipeline",
713
+ "KandinskyV22Img2ImgCombinedPipeline",
714
+ "KandinskyV22Img2ImgPipeline",
715
+ "KandinskyV22InpaintCombinedPipeline",
716
+ "KandinskyV22InpaintPipeline",
717
+ "KandinskyV22Pipeline",
718
+ "KandinskyV22PriorEmb2EmbPipeline",
719
+ "KandinskyV22PriorPipeline",
720
+ "Krea2Pipeline",
721
+ "LatentConsistencyModelImg2ImgPipeline",
722
+ "LatentConsistencyModelPipeline",
723
+ "LattePipeline",
724
+ "LDMTextToImagePipeline",
725
+ "LEditsPPPipelineStableDiffusion",
726
+ "LEditsPPPipelineStableDiffusionXL",
727
+ "LLaDA2Pipeline",
728
+ "LLaDA2PipelineOutput",
729
+ "LongCatAudioDiTPipeline",
730
+ "LongCatImageEditPipeline",
731
+ "LongCatImagePipeline",
732
+ "LTX2ConditionPipeline",
733
+ "LTX2DFRPipeline",
734
+ "LTX2DFRPipelineOutput",
735
+ "LTX2DFRTemporalRefinePipeline",
736
+ "LTX2HDRPipeline",
737
+ "LTX2ImageToVideoPipeline",
738
+ "LTX2InContextPipeline",
739
+ "LTX2LatentUpsamplePipeline",
740
+ "LTX2Pipeline",
741
+ "LTX2VideoDiffusionDecodePipeline",
742
+ "LTXConditionPipeline",
743
+ "LTXI2VLongMultiPromptPipeline",
744
+ "LTXImageToVideoPipeline",
745
+ "LTXLatentUpsamplePipeline",
746
+ "LTXPipeline",
747
+ "LucyEditPipeline",
748
+ "Lumina2Pipeline",
749
+ "Lumina2Text2ImgPipeline",
750
+ "LuminaPipeline",
751
+ "LuminaText2ImgPipeline",
752
+ "MarigoldDepthPipeline",
753
+ "MarigoldIntrinsicsPipeline",
754
+ "MarigoldNormalsPipeline",
755
+ "MochiPipeline",
756
+ "MotifVideoImage2VideoPipeline",
757
+ "MotifVideoPipeline",
758
+ "MotifVideoPipelineOutput",
759
+ "MusicLDMPipeline",
760
+ "NucleusMoEImagePipeline",
761
+ "OmniGenPipeline",
762
+ "OvisImagePipeline",
763
+ "PaintByExamplePipeline",
764
+ "PIAPipeline",
765
+ "PixArtAlphaPipeline",
766
+ "PixArtSigmaPAGPipeline",
767
+ "PixArtSigmaPipeline",
768
+ "PRXPipeline",
769
+ "PRXPixelPipeline",
770
+ "QwenImageControlNetInpaintPipeline",
771
+ "QwenImageControlNetPipeline",
772
+ "QwenImageEditInpaintPipeline",
773
+ "QwenImageEditPipeline",
774
+ "QwenImageEditPlusPipeline",
775
+ "QwenImageImg2ImgPipeline",
776
+ "QwenImageInpaintPipeline",
777
+ "QwenImageLayeredPipeline",
778
+ "QwenImagePipeline",
779
+ "ReduxImageEncoder",
780
+ "SanaControlNetPipeline",
781
+ "SanaImageToVideoPipeline",
782
+ "SanaPAGPipeline",
783
+ "SanaPipeline",
784
+ "SanaSprintImg2ImgPipeline",
785
+ "SanaSprintPipeline",
786
+ "SanaVideoPipeline",
787
+ "SanaVideoPipeline",
788
+ "SemanticStableDiffusionPipeline",
789
+ "ShapEImg2ImgPipeline",
790
+ "ShapEPipeline",
791
+ "SkyReelsV2DiffusionForcingImageToVideoPipeline",
792
+ "SkyReelsV2DiffusionForcingPipeline",
793
+ "SkyReelsV2DiffusionForcingVideoToVideoPipeline",
794
+ "SkyReelsV2ImageToVideoPipeline",
795
+ "SkyReelsV2Pipeline",
796
+ "StableAudio3AudioToAudioPipeline",
797
+ "StableAudio3DurationEmbedder",
798
+ "StableAudio3InpaintPipeline",
799
+ "StableAudio3Pipeline",
800
+ "StableAudioPipeline",
801
+ "StableAudioProjectionModel",
802
+ "StableCascadeCombinedPipeline",
803
+ "StableCascadeDecoderPipeline",
804
+ "StableCascadePriorPipeline",
805
+ "StableDiffusion3ControlNetInpaintingPipeline",
806
+ "StableDiffusion3ControlNetPipeline",
807
+ "StableDiffusion3Img2ImgPipeline",
808
+ "StableDiffusion3InpaintPipeline",
809
+ "StableDiffusion3PAGImg2ImgPipeline",
810
+ "StableDiffusion3PAGImg2ImgPipeline",
811
+ "StableDiffusion3PAGPipeline",
812
+ "StableDiffusion3Pipeline",
813
+ "StableDiffusionAdapterPipeline",
814
+ "StableDiffusionAttendAndExcitePipeline",
815
+ "StableDiffusionControlNetImg2ImgPipeline",
816
+ "StableDiffusionControlNetInpaintPipeline",
817
+ "StableDiffusionControlNetPAGInpaintPipeline",
818
+ "StableDiffusionControlNetPAGPipeline",
819
+ "StableDiffusionControlNetPipeline",
820
+ "StableDiffusionControlNetXSPipeline",
821
+ "StableDiffusionDepth2ImgPipeline",
822
+ "StableDiffusionDiffEditPipeline",
823
+ "StableDiffusionGLIGENPipeline",
824
+ "StableDiffusionGLIGENTextImagePipeline",
825
+ "StableDiffusionImageVariationPipeline",
826
+ "StableDiffusionImg2ImgPipeline",
827
+ "StableDiffusionInpaintPipeline",
828
+ "StableDiffusionInpaintPipelineLegacy",
829
+ "StableDiffusionInstructPix2PixPipeline",
830
+ "StableDiffusionLatentUpscalePipeline",
831
+ "StableDiffusionLDM3DPipeline",
832
+ "StableDiffusionModelEditingPipeline",
833
+ "StableDiffusionPAGImg2ImgPipeline",
834
+ "StableDiffusionPAGInpaintPipeline",
835
+ "StableDiffusionPAGPipeline",
836
+ "StableDiffusionPanoramaPipeline",
837
+ "StableDiffusionParadigmsPipeline",
838
+ "StableDiffusionPipeline",
839
+ "StableDiffusionPipelineSafe",
840
+ "StableDiffusionPix2PixZeroPipeline",
841
+ "StableDiffusionSAGPipeline",
842
+ "StableDiffusionUpscalePipeline",
843
+ "StableDiffusionXLAdapterPipeline",
844
+ "StableDiffusionXLControlNetImg2ImgPipeline",
845
+ "StableDiffusionXLControlNetInpaintPipeline",
846
+ "StableDiffusionXLControlNetPAGImg2ImgPipeline",
847
+ "StableDiffusionXLControlNetPAGPipeline",
848
+ "StableDiffusionXLControlNetPipeline",
849
+ "StableDiffusionXLControlNetUnionImg2ImgPipeline",
850
+ "StableDiffusionXLControlNetUnionInpaintPipeline",
851
+ "StableDiffusionXLControlNetUnionPipeline",
852
+ "StableDiffusionXLControlNetXSPipeline",
853
+ "StableDiffusionXLImg2ImgPipeline",
854
+ "StableDiffusionXLInpaintPipeline",
855
+ "StableDiffusionXLInstructPix2PixPipeline",
856
+ "StableDiffusionXLPAGImg2ImgPipeline",
857
+ "StableDiffusionXLPAGInpaintPipeline",
858
+ "StableDiffusionXLPAGPipeline",
859
+ "StableDiffusionXLPipeline",
860
+ "StableUnCLIPImg2ImgPipeline",
861
+ "StableUnCLIPPipeline",
862
+ "StableVideoDiffusionPipeline",
863
+ "TextToVideoSDPipeline",
864
+ "TextToVideoZeroPipeline",
865
+ "TextToVideoZeroSDXLPipeline",
866
+ "UnCLIPImageVariationPipeline",
867
+ "UnCLIPPipeline",
868
+ "UniDiffuserModel",
869
+ "UniDiffuserPipeline",
870
+ "UniDiffuserTextDecoder",
871
+ "VersatileDiffusionDualGuidedPipeline",
872
+ "VersatileDiffusionImageVariationPipeline",
873
+ "VersatileDiffusionPipeline",
874
+ "VersatileDiffusionTextToImagePipeline",
875
+ "VideoToVideoSDPipeline",
876
+ "VisualClozeGenerationPipeline",
877
+ "VisualClozePipeline",
878
+ "VQDiffusionPipeline",
879
+ "WanAnimatePipeline",
880
+ "WanImageToVideoPipeline",
881
+ "WanPipeline",
882
+ "WanVACEPipeline",
883
+ "WanVideoToVideoPipeline",
884
+ "WuerstchenCombinedPipeline",
885
+ "WuerstchenDecoderPipeline",
886
+ "WuerstchenPriorPipeline",
887
+ "ZImageControlNetInpaintPipeline",
888
+ "ZImageControlNetPipeline",
889
+ "ZImageImg2ImgPipeline",
890
+ "ZImageInpaintPipeline",
891
+ "ZImageOmniPipeline",
892
+ "ZImagePipeline",
893
+ ]
894
+ )
895
+
896
+
897
+ try:
898
+ if not (is_torch_available() and is_transformers_available() and is_opencv_available()):
899
+ raise OptionalDependencyNotAvailable()
900
+ except OptionalDependencyNotAvailable:
901
+ from .utils import dummy_torch_and_transformers_and_opencv_objects # noqa F403
902
+
903
+ _import_structure["utils.dummy_torch_and_transformers_and_opencv_objects"] = [
904
+ name for name in dir(dummy_torch_and_transformers_and_opencv_objects) if not name.startswith("_")
905
+ ]
906
+
907
+ else:
908
+ _import_structure["pipelines"].extend(["ConsisIDPipeline"])
909
+
910
+ try:
911
+ if not (is_torch_available() and is_transformers_available() and is_sentencepiece_available()):
912
+ raise OptionalDependencyNotAvailable()
913
+ except OptionalDependencyNotAvailable:
914
+ from .utils import dummy_torch_and_transformers_and_sentencepiece_objects # noqa F403
915
+
916
+ _import_structure["utils.dummy_torch_and_transformers_and_sentencepiece_objects"] = [
917
+ name for name in dir(dummy_torch_and_transformers_and_sentencepiece_objects) if not name.startswith("_")
918
+ ]
919
+
920
+ else:
921
+ _import_structure["pipelines"].extend(["KolorsImg2ImgPipeline", "KolorsPAGPipeline", "KolorsPipeline"])
922
+
923
+ try:
924
+ if not (is_torch_available() and is_transformers_available() and is_onnx_available()):
925
+ raise OptionalDependencyNotAvailable()
926
+ except OptionalDependencyNotAvailable:
927
+ from .utils import dummy_torch_and_transformers_and_onnx_objects # noqa F403
928
+
929
+ _import_structure["utils.dummy_torch_and_transformers_and_onnx_objects"] = [
930
+ name for name in dir(dummy_torch_and_transformers_and_onnx_objects) if not name.startswith("_")
931
+ ]
932
+
933
+ else:
934
+ _import_structure["pipelines"].extend(
935
+ [
936
+ "OnnxStableDiffusionImg2ImgPipeline",
937
+ "OnnxStableDiffusionInpaintPipeline",
938
+ "OnnxStableDiffusionInpaintPipelineLegacy",
939
+ "OnnxStableDiffusionPipeline",
940
+ "OnnxStableDiffusionUpscalePipeline",
941
+ "StableDiffusionOnnxPipeline",
942
+ ]
943
+ )
944
+
945
+ try:
946
+ if not (is_torch_available() and is_librosa_available()):
947
+ raise OptionalDependencyNotAvailable()
948
+ except OptionalDependencyNotAvailable:
949
+ from .utils import dummy_torch_and_librosa_objects # noqa F403
950
+
951
+ _import_structure["utils.dummy_torch_and_librosa_objects"] = [
952
+ name for name in dir(dummy_torch_and_librosa_objects) if not name.startswith("_")
953
+ ]
954
+
955
+ else:
956
+ _import_structure["pipelines"].extend(["AudioDiffusionPipeline", "Mel"])
957
+
958
+ try:
959
+ if not (is_transformers_available() and is_torch_available() and is_note_seq_available()):
960
+ raise OptionalDependencyNotAvailable()
961
+ except OptionalDependencyNotAvailable:
962
+ from .utils import dummy_transformers_and_torch_and_note_seq_objects # noqa F403
963
+
964
+ _import_structure["utils.dummy_transformers_and_torch_and_note_seq_objects"] = [
965
+ name for name in dir(dummy_transformers_and_torch_and_note_seq_objects) if not name.startswith("_")
966
+ ]
967
+
968
+
969
+ else:
970
+ _import_structure["pipelines"].extend(["SpectrogramDiffusionPipeline"])
971
+
972
+ try:
973
+ if not (is_note_seq_available()):
974
+ raise OptionalDependencyNotAvailable()
975
+ except OptionalDependencyNotAvailable:
976
+ from .utils import dummy_note_seq_objects # noqa F403
977
+
978
+ _import_structure["utils.dummy_note_seq_objects"] = [
979
+ name for name in dir(dummy_note_seq_objects) if not name.startswith("_")
980
+ ]
981
+
982
+
983
+ else:
984
+ _import_structure["pipelines"].extend(["MidiProcessor"])
985
+
986
+ if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
987
+ from .configuration_utils import ConfigMixin
988
+ from .quantizers import PipelineQuantizationConfig
989
+
990
+ try:
991
+ if not is_bitsandbytes_available():
992
+ raise OptionalDependencyNotAvailable()
993
+ except OptionalDependencyNotAvailable:
994
+ from .utils.dummy_bitsandbytes_objects import *
995
+ else:
996
+ from .quantizers.quantization_config import BitsAndBytesConfig
997
+
998
+ try:
999
+ if not is_gguf_available():
1000
+ raise OptionalDependencyNotAvailable()
1001
+ except OptionalDependencyNotAvailable:
1002
+ from .utils.dummy_gguf_objects import *
1003
+ else:
1004
+ from .quantizers.quantization_config import GGUFQuantizationConfig
1005
+
1006
+ try:
1007
+ if not is_torchao_available():
1008
+ raise OptionalDependencyNotAvailable()
1009
+ except OptionalDependencyNotAvailable:
1010
+ from .utils.dummy_torchao_objects import *
1011
+ else:
1012
+ from .quantizers.quantization_config import TorchAoConfig
1013
+
1014
+ try:
1015
+ if not is_optimum_quanto_available():
1016
+ raise OptionalDependencyNotAvailable()
1017
+ except OptionalDependencyNotAvailable:
1018
+ from .utils.dummy_optimum_quanto_objects import *
1019
+ else:
1020
+ from .quantizers.quantization_config import QuantoConfig
1021
+
1022
+ try:
1023
+ if not is_nvidia_modelopt_available():
1024
+ raise OptionalDependencyNotAvailable()
1025
+ except OptionalDependencyNotAvailable:
1026
+ from .utils.dummy_nvidia_modelopt_objects import *
1027
+ else:
1028
+ from .quantizers.quantization_config import NVIDIAModelOptConfig
1029
+
1030
+ try:
1031
+ if not is_torch_available():
1032
+ raise OptionalDependencyNotAvailable()
1033
+ except OptionalDependencyNotAvailable:
1034
+ from .utils.dummy_nunchaku_lite_objects import *
1035
+ else:
1036
+ from .quantizers.quantization_config import NunchakuLiteQuantizationConfig
1037
+
1038
+ try:
1039
+ if not is_auto_round_available():
1040
+ raise OptionalDependencyNotAvailable()
1041
+ except OptionalDependencyNotAvailable:
1042
+ from .utils.dummy_auto_round_objects import *
1043
+ else:
1044
+ from .quantizers.quantization_config import AutoRoundConfig
1045
+
1046
+ try:
1047
+ if not is_sdnq_available():
1048
+ raise OptionalDependencyNotAvailable()
1049
+ except OptionalDependencyNotAvailable:
1050
+ from .utils.dummy_sdnq_objects import *
1051
+ else:
1052
+ from .quantizers.quantization_config import SDNQConfig
1053
+
1054
+ try:
1055
+ if not is_onnx_available():
1056
+ raise OptionalDependencyNotAvailable()
1057
+ except OptionalDependencyNotAvailable:
1058
+ from .utils.dummy_onnx_objects import * # noqa F403
1059
+ else:
1060
+ from .pipelines import OnnxRuntimeModel
1061
+
1062
+ try:
1063
+ if not is_torch_available():
1064
+ raise OptionalDependencyNotAvailable()
1065
+ except OptionalDependencyNotAvailable:
1066
+ from .utils.dummy_pt_objects import * # noqa F403
1067
+ else:
1068
+ from .guiders import (
1069
+ AdaptiveProjectedGuidance,
1070
+ AdaptiveProjectedMixGuidance,
1071
+ AutoGuidance,
1072
+ BaseGuidance,
1073
+ ClassifierFreeGuidance,
1074
+ ClassifierFreeZeroStarGuidance,
1075
+ FrequencyDecoupledGuidance,
1076
+ LTX2Guidance,
1077
+ PerturbedAttentionGuidance,
1078
+ SkipLayerGuidance,
1079
+ SmoothedEnergyGuidance,
1080
+ TangentialClassifierFreeGuidance,
1081
+ )
1082
+ from .hooks import (
1083
+ FasterCacheConfig,
1084
+ FirstBlockCacheConfig,
1085
+ HookRegistry,
1086
+ LayerSkipConfig,
1087
+ MagCacheConfig,
1088
+ PyramidAttentionBroadcastConfig,
1089
+ SmoothedEnergyGuidanceConfig,
1090
+ TaylorSeerCacheConfig,
1091
+ TextKVCacheConfig,
1092
+ apply_faster_cache,
1093
+ apply_first_block_cache,
1094
+ apply_layer_skip,
1095
+ apply_mag_cache,
1096
+ apply_pyramid_attention_broadcast,
1097
+ apply_taylorseer_cache,
1098
+ apply_text_kv_cache,
1099
+ )
1100
+ from .image_processor import (
1101
+ InpaintProcessor,
1102
+ IPAdapterMaskProcessor,
1103
+ PixArtImageProcessor,
1104
+ VaeImageProcessor,
1105
+ VaeImageProcessorLDM3D,
1106
+ )
1107
+ from .models import (
1108
+ AceStepTransformer1DModel,
1109
+ AllegroTransformer3DModel,
1110
+ AnimaTextConditioner,
1111
+ AnyFlowFARTransformer3DModel,
1112
+ AnyFlowTransformer3DModel,
1113
+ AsymmetricAutoencoderKL,
1114
+ AttentionBackendName,
1115
+ AuraFlowTransformer2DModel,
1116
+ AutoencoderDC,
1117
+ AutoencoderKL,
1118
+ AutoencoderKLAllegro,
1119
+ AutoencoderKLCogVideoX,
1120
+ AutoencoderKLCosmos,
1121
+ AutoencoderKLFlux2,
1122
+ AutoencoderKLHunyuanImage,
1123
+ AutoencoderKLHunyuanImageRefiner,
1124
+ AutoencoderKLHunyuanVideo,
1125
+ AutoencoderKLHunyuanVideo15,
1126
+ AutoencoderKLKVAE,
1127
+ AutoencoderKLKVAEVideo,
1128
+ AutoencoderKLLTX2Audio,
1129
+ AutoencoderKLLTX2Video,
1130
+ AutoencoderKLLTXVideo,
1131
+ AutoencoderKLMagvit,
1132
+ AutoencoderKLMiniMaxH3,
1133
+ AutoencoderKLMiniMaxH3Audio,
1134
+ AutoencoderKLMochi,
1135
+ AutoencoderKLQwenImage,
1136
+ AutoencoderKLTemporalDecoder,
1137
+ AutoencoderKLWan,
1138
+ AutoencoderOobleck,
1139
+ AutoencoderRAE,
1140
+ AutoencoderSAME,
1141
+ AutoencoderTiny,
1142
+ AutoencoderVidTok,
1143
+ AutoModel,
1144
+ BriaFiboTransformer2DModel,
1145
+ BriaTransformer2DModel,
1146
+ CacheMixin,
1147
+ ChromaTransformer2DModel,
1148
+ ChronoEditTransformer3DModel,
1149
+ CogVideoXTransformer3DModel,
1150
+ CogView3PlusTransformer2DModel,
1151
+ CogView4Transformer2DModel,
1152
+ ConsisIDTransformer3DModel,
1153
+ ConsistencyDecoderVAE,
1154
+ ContextParallelConfig,
1155
+ ControlNetModel,
1156
+ ControlNetUnionModel,
1157
+ ControlNetXSAdapter,
1158
+ Cosmos3AVAEAudioTokenizer,
1159
+ Cosmos3OmniTransformer,
1160
+ CosmosControlNetModel,
1161
+ CosmosTransformer3DModel,
1162
+ DiTTransformer2DModel,
1163
+ DreamLiteTransformer2DModel,
1164
+ DreamLiteUNetModel,
1165
+ EasyAnimateTransformer3DModel,
1166
+ ErnieImageTransformer2DModel,
1167
+ Flux2Transformer2DModel,
1168
+ Flux3Transformer2DModel,
1169
+ FluxControlNetModel,
1170
+ FluxMultiControlNetModel,
1171
+ FluxTransformer2DModel,
1172
+ GlmImageTransformer2DModel,
1173
+ HeliosTransformer3DModel,
1174
+ HiDreamImageTransformer2DModel,
1175
+ HunyuanDiT2DControlNetModel,
1176
+ HunyuanDiT2DModel,
1177
+ HunyuanDiT2DMultiControlNetModel,
1178
+ HunyuanImageTransformer2DModel,
1179
+ HunyuanVideo15Transformer3DModel,
1180
+ HunyuanVideoFramepackTransformer3DModel,
1181
+ HunyuanVideoTransformer3DModel,
1182
+ I2VGenXLUNet,
1183
+ Ideogram4Transformer2DModel,
1184
+ JoyImageEditPlusTransformer3DModel,
1185
+ JoyImageEditTransformer3DModel,
1186
+ Kandinsky3UNet,
1187
+ Kandinsky5Transformer3DModel,
1188
+ Krea2Transformer2DModel,
1189
+ LatteTransformer3DModel,
1190
+ LongCatAudioDiTTransformer,
1191
+ LongCatAudioDiTVae,
1192
+ LongCatImageTransformer2DModel,
1193
+ LTX2VideoDiffusionDecoderModel,
1194
+ LTX2VideoTransformer3DModel,
1195
+ LTXVideoTransformer3DModel,
1196
+ Lumina2Transformer2DModel,
1197
+ LuminaNextDiT2DModel,
1198
+ MiniMaxH3Transformer3DModel,
1199
+ MiniMaxMusic3ConditionEncoder,
1200
+ MiniMaxMusic3RVQDepthDecoder,
1201
+ MiniMaxMusic3Transformer1DModel,
1202
+ MiniMaxMusic3Vocoder,
1203
+ MochiTransformer3DModel,
1204
+ ModelMixin,
1205
+ MotifVideoTransformer3DModel,
1206
+ MotionAdapter,
1207
+ MultiAdapter,
1208
+ MultiControlNetModel,
1209
+ NucleusMoEImageTransformer2DModel,
1210
+ OmniGenTransformer2DModel,
1211
+ OvisImageTransformer2DModel,
1212
+ ParallelConfig,
1213
+ PixArtTransformer2DModel,
1214
+ PriorTransformer,
1215
+ PRXTransformer2DModel,
1216
+ QwenImageControlNetModel,
1217
+ QwenImageMultiControlNetModel,
1218
+ QwenImageTransformer2DModel,
1219
+ SanaControlNetModel,
1220
+ SanaTransformer2DModel,
1221
+ SanaVideoTransformer3DModel,
1222
+ SD3ControlNetModel,
1223
+ SD3MultiControlNetModel,
1224
+ SD3Transformer2DModel,
1225
+ SkyReelsV2Transformer3DModel,
1226
+ SparseControlNetModel,
1227
+ StableAudio3DiTModel,
1228
+ StableAudioDiTModel,
1229
+ T2IAdapter,
1230
+ T5FilmDecoder,
1231
+ TensorParallelConfig,
1232
+ Transformer2DModel,
1233
+ TransformerTemporalModel,
1234
+ UNet1DModel,
1235
+ UNet2DConditionModel,
1236
+ UNet2DModel,
1237
+ UNet3DConditionModel,
1238
+ UNetControlNetXSModel,
1239
+ UNetMotionModel,
1240
+ UNetSpatioTemporalConditionModel,
1241
+ UVit2DModel,
1242
+ VQModel,
1243
+ WanAnimate2Transformer3DModel,
1244
+ WanAnimateTransformer3DModel,
1245
+ WanTransformer3DModel,
1246
+ WanVACETransformer3DModel,
1247
+ ZImageControlNetModel,
1248
+ ZImageTransformer2DModel,
1249
+ attention_backend,
1250
+ )
1251
+ from .modular_pipelines import (
1252
+ AutoPipelineBlocks,
1253
+ ComponentsManager,
1254
+ ComponentSpec,
1255
+ ConditionalPipelineBlocks,
1256
+ ConfigSpec,
1257
+ InputParam,
1258
+ LoopSequentialPipelineBlocks,
1259
+ ModularPipeline,
1260
+ ModularPipelineBlocks,
1261
+ OutputParam,
1262
+ SequentialPipelineBlocks,
1263
+ )
1264
+ from .optimization import (
1265
+ get_constant_schedule,
1266
+ get_constant_schedule_with_warmup,
1267
+ get_cosine_schedule_with_warmup,
1268
+ get_cosine_with_hard_restarts_schedule_with_warmup,
1269
+ get_linear_schedule_with_warmup,
1270
+ get_polynomial_decay_schedule_with_warmup,
1271
+ get_scheduler,
1272
+ )
1273
+ from .pipelines import (
1274
+ AudioPipelineOutput,
1275
+ AutoPipelineForImage2Image,
1276
+ AutoPipelineForInpainting,
1277
+ AutoPipelineForText2Audio,
1278
+ AutoPipelineForText2Image,
1279
+ BlipDiffusionControlNetPipeline,
1280
+ BlipDiffusionPipeline,
1281
+ CLIPImageProjection,
1282
+ ConsistencyModelPipeline,
1283
+ DanceDiffusionPipeline,
1284
+ DDIMPipeline,
1285
+ DDPMPipeline,
1286
+ DiffusionPipeline,
1287
+ DiTPipeline,
1288
+ ImagePipelineOutput,
1289
+ KarrasVePipeline,
1290
+ LDMPipeline,
1291
+ LDMSuperResolutionPipeline,
1292
+ PNDMPipeline,
1293
+ RePaintPipeline,
1294
+ ScoreSdeVePipeline,
1295
+ StableDiffusionMixin,
1296
+ )
1297
+ from .quantizers import DiffusersQuantizer
1298
+ from .schedulers import (
1299
+ AmusedScheduler,
1300
+ BlockRefinementScheduler,
1301
+ BlockRefinementSchedulerOutput,
1302
+ CMStochasticIterativeScheduler,
1303
+ CogVideoXDDIMScheduler,
1304
+ CogVideoXDPMScheduler,
1305
+ DDIMInverseScheduler,
1306
+ DDIMParallelScheduler,
1307
+ DDIMScheduler,
1308
+ DDPMParallelScheduler,
1309
+ DDPMScheduler,
1310
+ DDPMWuerstchenScheduler,
1311
+ DEISMultistepScheduler,
1312
+ DiscreteDDIMScheduler,
1313
+ DiscreteDDIMSchedulerOutput,
1314
+ DPMSolverMultistepInverseScheduler,
1315
+ DPMSolverMultistepScheduler,
1316
+ DPMSolverSinglestepScheduler,
1317
+ EDMDPMSolverMultistepScheduler,
1318
+ EDMEulerScheduler,
1319
+ EntropyBoundScheduler,
1320
+ EntropyBoundSchedulerOutput,
1321
+ EulerAncestralDiscreteScheduler,
1322
+ EulerDiscreteScheduler,
1323
+ FlowMapEulerDiscreteScheduler,
1324
+ FlowMatchEulerDiscreteScheduler,
1325
+ FlowMatchHeunDiscreteScheduler,
1326
+ FlowMatchLCMScheduler,
1327
+ HeliosDMDScheduler,
1328
+ HeliosScheduler,
1329
+ HeunDiscreteScheduler,
1330
+ IPNDMScheduler,
1331
+ KarrasVeScheduler,
1332
+ KDPM2AncestralDiscreteScheduler,
1333
+ KDPM2DiscreteScheduler,
1334
+ LCMScheduler,
1335
+ LTXEulerAncestralRFScheduler,
1336
+ MiniMaxH3Scheduler,
1337
+ PNDMScheduler,
1338
+ RePaintScheduler,
1339
+ SASolverScheduler,
1340
+ SchedulerMixin,
1341
+ SCMScheduler,
1342
+ ScoreSdeVeScheduler,
1343
+ TCDScheduler,
1344
+ UnCLIPScheduler,
1345
+ UniPCMultistepScheduler,
1346
+ VQDiffusionScheduler,
1347
+ )
1348
+ from .training_utils import EMAModel
1349
+ from .video_processor import VideoProcessor
1350
+
1351
+ try:
1352
+ if not (is_torch_available() and is_scipy_available()):
1353
+ raise OptionalDependencyNotAvailable()
1354
+ except OptionalDependencyNotAvailable:
1355
+ from .utils.dummy_torch_and_scipy_objects import * # noqa F403
1356
+ else:
1357
+ from .schedulers import LMSDiscreteScheduler
1358
+
1359
+ try:
1360
+ if not (is_torch_available() and is_torchsde_available()):
1361
+ raise OptionalDependencyNotAvailable()
1362
+ except OptionalDependencyNotAvailable:
1363
+ from .utils.dummy_torch_and_torchsde_objects import * # noqa F403
1364
+ else:
1365
+ from .schedulers import CosineDPMSolverMultistepScheduler, DPMSolverSDEScheduler
1366
+
1367
+ try:
1368
+ if not (is_torch_available() and is_transformers_available()):
1369
+ raise OptionalDependencyNotAvailable()
1370
+ except OptionalDependencyNotAvailable:
1371
+ from .utils.dummy_torch_and_transformers_objects import * # noqa F403
1372
+ else:
1373
+ from .modular_pipelines import (
1374
+ AnimaAutoBlocks,
1375
+ AnimaModularPipeline,
1376
+ Cosmos3DistilledBlocks,
1377
+ Cosmos3DistilledModularPipeline,
1378
+ Cosmos3OmniBlocks,
1379
+ Cosmos3OmniModularPipeline,
1380
+ ErnieImageAutoBlocks,
1381
+ ErnieImageModularPipeline,
1382
+ Flux2AutoBlocks,
1383
+ Flux2KleinAutoBlocks,
1384
+ Flux2KleinBaseAutoBlocks,
1385
+ Flux2KleinBaseModularPipeline,
1386
+ Flux2KleinModularPipeline,
1387
+ Flux2ModularPipeline,
1388
+ Flux3AutoBlocks,
1389
+ Flux3ModularPipeline,
1390
+ FluxAutoBlocks,
1391
+ FluxKontextAutoBlocks,
1392
+ FluxKontextModularPipeline,
1393
+ FluxModularPipeline,
1394
+ HeliosAutoBlocks,
1395
+ HeliosModularPipeline,
1396
+ HeliosPyramidAutoBlocks,
1397
+ HeliosPyramidDistilledAutoBlocks,
1398
+ HeliosPyramidDistilledModularPipeline,
1399
+ HeliosPyramidModularPipeline,
1400
+ HunyuanVideo15AutoBlocks,
1401
+ HunyuanVideo15ModularPipeline,
1402
+ Ideogram4AutoBlocks,
1403
+ Ideogram4ModularPipeline,
1404
+ Krea2AutoBlocks,
1405
+ Krea2ModularPipeline,
1406
+ Krea2TurboAutoBlocks,
1407
+ Krea2TurboModularPipeline,
1408
+ LTX2AutoBlocks,
1409
+ LTX2ModularPipeline,
1410
+ LTX25AutoBlocks,
1411
+ LTX25ModularPipeline,
1412
+ LTXAutoBlocks,
1413
+ LTXModularPipeline,
1414
+ MiniMaxH3Blocks,
1415
+ MiniMaxH3ModularPipeline,
1416
+ MiniMaxMusic3Blocks,
1417
+ MiniMaxMusic3ModularPipeline,
1418
+ QwenImageAutoBlocks,
1419
+ QwenImageEditAutoBlocks,
1420
+ QwenImageEditModularPipeline,
1421
+ QwenImageEditPlusAutoBlocks,
1422
+ QwenImageEditPlusModularPipeline,
1423
+ QwenImageLayeredAutoBlocks,
1424
+ QwenImageLayeredModularPipeline,
1425
+ QwenImageModularPipeline,
1426
+ StableDiffusion3AutoBlocks,
1427
+ StableDiffusion3ModularPipeline,
1428
+ StableDiffusionXLAutoBlocks,
1429
+ StableDiffusionXLModularPipeline,
1430
+ Wan22Blocks,
1431
+ Wan22Image2VideoBlocks,
1432
+ Wan22Image2VideoModularPipeline,
1433
+ Wan22ModularPipeline,
1434
+ Wan22VaceBlocks,
1435
+ Wan22VaceModularPipeline,
1436
+ WanAnimate2Blocks,
1437
+ WanAnimate2DistilledBlocks,
1438
+ WanAnimate2DistilledModularPipeline,
1439
+ WanAnimate2ModularPipeline,
1440
+ WanBlocks,
1441
+ WanImage2VideoAutoBlocks,
1442
+ WanImage2VideoModularPipeline,
1443
+ WanModularPipeline,
1444
+ ZImageAutoBlocks,
1445
+ ZImageModularPipeline,
1446
+ )
1447
+ from .pipelines import (
1448
+ AceStepAudioTokenDetokenizer,
1449
+ AceStepAudioTokenizer,
1450
+ AceStepConditionEncoder,
1451
+ AceStepPipeline,
1452
+ AllegroPipeline,
1453
+ AltDiffusionImg2ImgPipeline,
1454
+ AltDiffusionPipeline,
1455
+ AmusedImg2ImgPipeline,
1456
+ AmusedInpaintPipeline,
1457
+ AmusedPipeline,
1458
+ AnimateDiffControlNetPipeline,
1459
+ AnimateDiffPAGPipeline,
1460
+ AnimateDiffPipeline,
1461
+ AnimateDiffSDXLPipeline,
1462
+ AnimateDiffSparseControlNetPipeline,
1463
+ AnimateDiffVideoToVideoControlNetPipeline,
1464
+ AnimateDiffVideoToVideoPipeline,
1465
+ AnyFlowFARPipeline,
1466
+ AnyFlowPipeline,
1467
+ AudioLDM2Pipeline,
1468
+ AudioLDM2ProjectionModel,
1469
+ AudioLDM2UNet2DConditionModel,
1470
+ AudioLDMPipeline,
1471
+ AuraFlowPipeline,
1472
+ BriaFiboEditPipeline,
1473
+ BriaFiboPipeline,
1474
+ BriaPipeline,
1475
+ ChromaImg2ImgPipeline,
1476
+ ChromaInpaintPipeline,
1477
+ ChromaPipeline,
1478
+ ChronoEditPipeline,
1479
+ CLIPImageProjection,
1480
+ CogVideoXFunControlPipeline,
1481
+ CogVideoXImageToVideoPipeline,
1482
+ CogVideoXPipeline,
1483
+ CogVideoXVideoToVideoPipeline,
1484
+ CogView3PlusPipeline,
1485
+ CogView4ControlPipeline,
1486
+ CogView4Pipeline,
1487
+ ConsisIDPipeline,
1488
+ Cosmos2_5_PredictBasePipeline,
1489
+ Cosmos2_5_TransferPipeline,
1490
+ Cosmos2TextToImagePipeline,
1491
+ Cosmos2VideoToWorldPipeline,
1492
+ Cosmos3OmniPipeline,
1493
+ CosmosActionCondition,
1494
+ CosmosTextToWorldPipeline,
1495
+ CosmosVideoToWorldPipeline,
1496
+ CycleDiffusionPipeline,
1497
+ DiffusionGemmaPipeline,
1498
+ DiffusionGemmaPipelineOutput,
1499
+ DreamLiteMobilePipeline,
1500
+ DreamLitePipeline,
1501
+ DreamLitePipelineOutput,
1502
+ EasyAnimateControlPipeline,
1503
+ EasyAnimateInpaintPipeline,
1504
+ EasyAnimatePipeline,
1505
+ ErnieImagePipeline,
1506
+ Flux2KleinInpaintPipeline,
1507
+ Flux2KleinKVPipeline,
1508
+ Flux2KleinPipeline,
1509
+ Flux2Pipeline,
1510
+ FluxControlImg2ImgPipeline,
1511
+ FluxControlInpaintPipeline,
1512
+ FluxControlNetImg2ImgPipeline,
1513
+ FluxControlNetInpaintPipeline,
1514
+ FluxControlNetPipeline,
1515
+ FluxControlPipeline,
1516
+ FluxFillPipeline,
1517
+ FluxImg2ImgPipeline,
1518
+ FluxInpaintPipeline,
1519
+ FluxKontextInpaintPipeline,
1520
+ FluxKontextPipeline,
1521
+ FluxPipeline,
1522
+ FluxPriorReduxPipeline,
1523
+ GlmImagePipeline,
1524
+ HeliosPipeline,
1525
+ HeliosPyramidPipeline,
1526
+ HiDreamImagePipeline,
1527
+ HunyuanDiTControlNetPipeline,
1528
+ HunyuanDiTPAGPipeline,
1529
+ HunyuanDiTPipeline,
1530
+ HunyuanImagePipeline,
1531
+ HunyuanImageRefinerPipeline,
1532
+ HunyuanSkyreelsImageToVideoPipeline,
1533
+ HunyuanVideo15ImageToVideoPipeline,
1534
+ HunyuanVideo15Pipeline,
1535
+ HunyuanVideoFramepackPipeline,
1536
+ HunyuanVideoImageToVideoPipeline,
1537
+ HunyuanVideoPipeline,
1538
+ I2VGenXLPipeline,
1539
+ Ideogram4Pipeline,
1540
+ Ideogram4PromptEnhancerHead,
1541
+ IFImg2ImgPipeline,
1542
+ IFImg2ImgSuperResolutionPipeline,
1543
+ IFInpaintingPipeline,
1544
+ IFInpaintingSuperResolutionPipeline,
1545
+ IFPipeline,
1546
+ IFSuperResolutionPipeline,
1547
+ ImageTextPipelineOutput,
1548
+ JoyImageEditPipeline,
1549
+ JoyImageEditPipelineOutput,
1550
+ JoyImageEditPlusPipeline,
1551
+ JoyImageEditPlusPipelineOutput,
1552
+ Kandinsky3Img2ImgPipeline,
1553
+ Kandinsky3Pipeline,
1554
+ Kandinsky5I2IPipeline,
1555
+ Kandinsky5I2VPipeline,
1556
+ Kandinsky5T2IPipeline,
1557
+ Kandinsky5T2VPipeline,
1558
+ KandinskyCombinedPipeline,
1559
+ KandinskyImg2ImgCombinedPipeline,
1560
+ KandinskyImg2ImgPipeline,
1561
+ KandinskyInpaintCombinedPipeline,
1562
+ KandinskyInpaintPipeline,
1563
+ KandinskyPipeline,
1564
+ KandinskyPriorPipeline,
1565
+ KandinskyV22CombinedPipeline,
1566
+ KandinskyV22ControlnetImg2ImgPipeline,
1567
+ KandinskyV22ControlnetPipeline,
1568
+ KandinskyV22Img2ImgCombinedPipeline,
1569
+ KandinskyV22Img2ImgPipeline,
1570
+ KandinskyV22InpaintCombinedPipeline,
1571
+ KandinskyV22InpaintPipeline,
1572
+ KandinskyV22Pipeline,
1573
+ KandinskyV22PriorEmb2EmbPipeline,
1574
+ KandinskyV22PriorPipeline,
1575
+ Krea2Pipeline,
1576
+ LatentConsistencyModelImg2ImgPipeline,
1577
+ LatentConsistencyModelPipeline,
1578
+ LattePipeline,
1579
+ LDMTextToImagePipeline,
1580
+ LEditsPPPipelineStableDiffusion,
1581
+ LEditsPPPipelineStableDiffusionXL,
1582
+ LLaDA2Pipeline,
1583
+ LLaDA2PipelineOutput,
1584
+ LongCatAudioDiTPipeline,
1585
+ LongCatImageEditPipeline,
1586
+ LongCatImagePipeline,
1587
+ LTX2ConditionPipeline,
1588
+ LTX2DFRPipeline,
1589
+ LTX2DFRPipelineOutput,
1590
+ LTX2DFRTemporalRefinePipeline,
1591
+ LTX2HDRPipeline,
1592
+ LTX2ImageToVideoPipeline,
1593
+ LTX2InContextPipeline,
1594
+ LTX2LatentUpsamplePipeline,
1595
+ LTX2Pipeline,
1596
+ LTX2VideoDiffusionDecodePipeline,
1597
+ LTXConditionPipeline,
1598
+ LTXI2VLongMultiPromptPipeline,
1599
+ LTXImageToVideoPipeline,
1600
+ LTXLatentUpsamplePipeline,
1601
+ LTXPipeline,
1602
+ LucyEditPipeline,
1603
+ Lumina2Pipeline,
1604
+ Lumina2Text2ImgPipeline,
1605
+ LuminaPipeline,
1606
+ LuminaText2ImgPipeline,
1607
+ MarigoldDepthPipeline,
1608
+ MarigoldIntrinsicsPipeline,
1609
+ MarigoldNormalsPipeline,
1610
+ MochiPipeline,
1611
+ MotifVideoImage2VideoPipeline,
1612
+ MotifVideoPipeline,
1613
+ MotifVideoPipelineOutput,
1614
+ MusicLDMPipeline,
1615
+ NucleusMoEImagePipeline,
1616
+ OmniGenPipeline,
1617
+ OvisImagePipeline,
1618
+ PaintByExamplePipeline,
1619
+ PIAPipeline,
1620
+ PixArtAlphaPipeline,
1621
+ PixArtSigmaPAGPipeline,
1622
+ PixArtSigmaPipeline,
1623
+ PRXPipeline,
1624
+ PRXPixelPipeline,
1625
+ QwenImageControlNetInpaintPipeline,
1626
+ QwenImageControlNetPipeline,
1627
+ QwenImageEditInpaintPipeline,
1628
+ QwenImageEditPipeline,
1629
+ QwenImageEditPlusPipeline,
1630
+ QwenImageImg2ImgPipeline,
1631
+ QwenImageInpaintPipeline,
1632
+ QwenImageLayeredPipeline,
1633
+ QwenImagePipeline,
1634
+ ReduxImageEncoder,
1635
+ SanaControlNetPipeline,
1636
+ SanaImageToVideoPipeline,
1637
+ SanaPAGPipeline,
1638
+ SanaPipeline,
1639
+ SanaSprintImg2ImgPipeline,
1640
+ SanaSprintPipeline,
1641
+ SanaVideoPipeline,
1642
+ SemanticStableDiffusionPipeline,
1643
+ ShapEImg2ImgPipeline,
1644
+ ShapEPipeline,
1645
+ SkyReelsV2DiffusionForcingImageToVideoPipeline,
1646
+ SkyReelsV2DiffusionForcingPipeline,
1647
+ SkyReelsV2DiffusionForcingVideoToVideoPipeline,
1648
+ SkyReelsV2ImageToVideoPipeline,
1649
+ SkyReelsV2Pipeline,
1650
+ StableAudio3AudioToAudioPipeline,
1651
+ StableAudio3DurationEmbedder,
1652
+ StableAudio3InpaintPipeline,
1653
+ StableAudio3Pipeline,
1654
+ StableAudioPipeline,
1655
+ StableAudioProjectionModel,
1656
+ StableCascadeCombinedPipeline,
1657
+ StableCascadeDecoderPipeline,
1658
+ StableCascadePriorPipeline,
1659
+ StableDiffusion3ControlNetInpaintingPipeline,
1660
+ StableDiffusion3ControlNetPipeline,
1661
+ StableDiffusion3Img2ImgPipeline,
1662
+ StableDiffusion3InpaintPipeline,
1663
+ StableDiffusion3PAGImg2ImgPipeline,
1664
+ StableDiffusion3PAGPipeline,
1665
+ StableDiffusion3Pipeline,
1666
+ StableDiffusionAdapterPipeline,
1667
+ StableDiffusionAttendAndExcitePipeline,
1668
+ StableDiffusionControlNetImg2ImgPipeline,
1669
+ StableDiffusionControlNetInpaintPipeline,
1670
+ StableDiffusionControlNetPAGInpaintPipeline,
1671
+ StableDiffusionControlNetPAGPipeline,
1672
+ StableDiffusionControlNetPipeline,
1673
+ StableDiffusionControlNetXSPipeline,
1674
+ StableDiffusionDepth2ImgPipeline,
1675
+ StableDiffusionDiffEditPipeline,
1676
+ StableDiffusionGLIGENPipeline,
1677
+ StableDiffusionGLIGENTextImagePipeline,
1678
+ StableDiffusionImageVariationPipeline,
1679
+ StableDiffusionImg2ImgPipeline,
1680
+ StableDiffusionInpaintPipeline,
1681
+ StableDiffusionInpaintPipelineLegacy,
1682
+ StableDiffusionInstructPix2PixPipeline,
1683
+ StableDiffusionLatentUpscalePipeline,
1684
+ StableDiffusionLDM3DPipeline,
1685
+ StableDiffusionModelEditingPipeline,
1686
+ StableDiffusionPAGImg2ImgPipeline,
1687
+ StableDiffusionPAGInpaintPipeline,
1688
+ StableDiffusionPAGPipeline,
1689
+ StableDiffusionPanoramaPipeline,
1690
+ StableDiffusionParadigmsPipeline,
1691
+ StableDiffusionPipeline,
1692
+ StableDiffusionPipelineSafe,
1693
+ StableDiffusionPix2PixZeroPipeline,
1694
+ StableDiffusionSAGPipeline,
1695
+ StableDiffusionUpscalePipeline,
1696
+ StableDiffusionXLAdapterPipeline,
1697
+ StableDiffusionXLControlNetImg2ImgPipeline,
1698
+ StableDiffusionXLControlNetInpaintPipeline,
1699
+ StableDiffusionXLControlNetPAGImg2ImgPipeline,
1700
+ StableDiffusionXLControlNetPAGPipeline,
1701
+ StableDiffusionXLControlNetPipeline,
1702
+ StableDiffusionXLControlNetUnionImg2ImgPipeline,
1703
+ StableDiffusionXLControlNetUnionInpaintPipeline,
1704
+ StableDiffusionXLControlNetUnionPipeline,
1705
+ StableDiffusionXLControlNetXSPipeline,
1706
+ StableDiffusionXLImg2ImgPipeline,
1707
+ StableDiffusionXLInpaintPipeline,
1708
+ StableDiffusionXLInstructPix2PixPipeline,
1709
+ StableDiffusionXLPAGImg2ImgPipeline,
1710
+ StableDiffusionXLPAGInpaintPipeline,
1711
+ StableDiffusionXLPAGPipeline,
1712
+ StableDiffusionXLPipeline,
1713
+ StableUnCLIPImg2ImgPipeline,
1714
+ StableUnCLIPPipeline,
1715
+ StableVideoDiffusionPipeline,
1716
+ TextToVideoSDPipeline,
1717
+ TextToVideoZeroPipeline,
1718
+ TextToVideoZeroSDXLPipeline,
1719
+ UnCLIPImageVariationPipeline,
1720
+ UnCLIPPipeline,
1721
+ UniDiffuserModel,
1722
+ UniDiffuserPipeline,
1723
+ UniDiffuserTextDecoder,
1724
+ VersatileDiffusionDualGuidedPipeline,
1725
+ VersatileDiffusionImageVariationPipeline,
1726
+ VersatileDiffusionPipeline,
1727
+ VersatileDiffusionTextToImagePipeline,
1728
+ VideoToVideoSDPipeline,
1729
+ VisualClozeGenerationPipeline,
1730
+ VisualClozePipeline,
1731
+ VQDiffusionPipeline,
1732
+ WanAnimatePipeline,
1733
+ WanImageToVideoPipeline,
1734
+ WanPipeline,
1735
+ WanVACEPipeline,
1736
+ WanVideoToVideoPipeline,
1737
+ WuerstchenCombinedPipeline,
1738
+ WuerstchenDecoderPipeline,
1739
+ WuerstchenPriorPipeline,
1740
+ ZImageControlNetInpaintPipeline,
1741
+ ZImageControlNetPipeline,
1742
+ ZImageImg2ImgPipeline,
1743
+ ZImageInpaintPipeline,
1744
+ ZImageOmniPipeline,
1745
+ ZImagePipeline,
1746
+ )
1747
+
1748
+ try:
1749
+ if not (is_torch_available() and is_transformers_available() and is_sentencepiece_available()):
1750
+ raise OptionalDependencyNotAvailable()
1751
+ except OptionalDependencyNotAvailable:
1752
+ from .utils.dummy_torch_and_transformers_and_sentencepiece_objects import * # noqa F403
1753
+ else:
1754
+ from .pipelines import KolorsImg2ImgPipeline, KolorsPAGPipeline, KolorsPipeline
1755
+
1756
+ try:
1757
+ if not (is_torch_available() and is_transformers_available() and is_opencv_available()):
1758
+ raise OptionalDependencyNotAvailable()
1759
+ except OptionalDependencyNotAvailable:
1760
+ from .utils.dummy_torch_and_transformers_and_opencv_objects import * # noqa F403
1761
+ else:
1762
+ from .pipelines import ConsisIDPipeline
1763
+
1764
+ try:
1765
+ if not (is_torch_available() and is_transformers_available() and is_onnx_available()):
1766
+ raise OptionalDependencyNotAvailable()
1767
+ except OptionalDependencyNotAvailable:
1768
+ from .utils.dummy_torch_and_transformers_and_onnx_objects import * # noqa F403
1769
+ else:
1770
+ from .pipelines import (
1771
+ OnnxStableDiffusionImg2ImgPipeline,
1772
+ OnnxStableDiffusionInpaintPipeline,
1773
+ OnnxStableDiffusionInpaintPipelineLegacy,
1774
+ OnnxStableDiffusionPipeline,
1775
+ OnnxStableDiffusionUpscalePipeline,
1776
+ StableDiffusionOnnxPipeline,
1777
+ )
1778
+
1779
+ try:
1780
+ if not (is_torch_available() and is_librosa_available()):
1781
+ raise OptionalDependencyNotAvailable()
1782
+ except OptionalDependencyNotAvailable:
1783
+ from .utils.dummy_torch_and_librosa_objects import * # noqa F403
1784
+ else:
1785
+ from .pipelines import AudioDiffusionPipeline, Mel
1786
+
1787
+ try:
1788
+ if not (is_transformers_available() and is_torch_available() and is_note_seq_available()):
1789
+ raise OptionalDependencyNotAvailable()
1790
+ except OptionalDependencyNotAvailable:
1791
+ from .utils.dummy_transformers_and_torch_and_note_seq_objects import * # noqa F403
1792
+ else:
1793
+ from .pipelines import SpectrogramDiffusionPipeline
1794
+
1795
+ try:
1796
+ if not (is_note_seq_available()):
1797
+ raise OptionalDependencyNotAvailable()
1798
+ except OptionalDependencyNotAvailable:
1799
+ from .utils.dummy_note_seq_objects import * # noqa F403
1800
+ else:
1801
+ from .pipelines import MidiProcessor
1802
+
1803
+ else:
1804
+ import sys
1805
+
1806
+ sys.modules[__name__] = _LazyModule(
1807
+ __name__,
1808
+ globals()["__file__"],
1809
+ _import_structure,
1810
+ module_spec=__spec__,
1811
+ extra_objects={"__version__": __version__},
1812
+ )
diffusers/callbacks.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ from .configuration_utils import ConfigMixin, register_to_config
4
+ from .utils import CONFIG_NAME
5
+
6
+
7
+ class PipelineCallback(ConfigMixin):
8
+ """
9
+ Base class for all the official callbacks used in a pipeline. This class provides a structure for implementing
10
+ custom callbacks and ensures that all callbacks have a consistent interface.
11
+
12
+ Please implement the following:
13
+ `tensor_inputs`: This should return a list of tensor inputs specific to your callback. You will only be able to
14
+ include
15
+ variables listed in the `._callback_tensor_inputs` attribute of your pipeline class.
16
+ `callback_fn`: This method defines the core functionality of your callback.
17
+ """
18
+
19
+ config_name = CONFIG_NAME
20
+
21
+ @register_to_config
22
+ def __init__(self, cutoff_step_ratio=1.0, cutoff_step_index=None):
23
+ super().__init__()
24
+
25
+ if (cutoff_step_ratio is None and cutoff_step_index is None) or (
26
+ cutoff_step_ratio is not None and cutoff_step_index is not None
27
+ ):
28
+ raise ValueError("Either cutoff_step_ratio or cutoff_step_index should be provided, not both or none.")
29
+
30
+ if cutoff_step_ratio is not None and (
31
+ not isinstance(cutoff_step_ratio, float) or not (0.0 <= cutoff_step_ratio <= 1.0)
32
+ ):
33
+ raise ValueError("cutoff_step_ratio must be a float between 0.0 and 1.0.")
34
+
35
+ @property
36
+ def tensor_inputs(self) -> list[str]:
37
+ raise NotImplementedError(f"You need to set the attribute `tensor_inputs` for {self.__class__}")
38
+
39
+ def callback_fn(self, pipeline, step_index, timesteps, callback_kwargs) -> dict[str, Any]:
40
+ raise NotImplementedError(f"You need to implement the method `callback_fn` for {self.__class__}")
41
+
42
+ def __call__(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
43
+ return self.callback_fn(pipeline, step_index, timestep, callback_kwargs)
44
+
45
+
46
+ class MultiPipelineCallbacks:
47
+ """
48
+ This class is designed to handle multiple pipeline callbacks. It accepts a list of PipelineCallback objects and
49
+ provides a unified interface for calling all of them.
50
+ """
51
+
52
+ def __init__(self, callbacks: list[PipelineCallback]):
53
+ self.callbacks = callbacks
54
+
55
+ @property
56
+ def tensor_inputs(self) -> list[str]:
57
+ return [input for callback in self.callbacks for input in callback.tensor_inputs]
58
+
59
+ def __call__(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
60
+ """
61
+ Calls all the callbacks in order with the given arguments and returns the final callback_kwargs.
62
+ """
63
+ for callback in self.callbacks:
64
+ callback_kwargs = callback(pipeline, step_index, timestep, callback_kwargs)
65
+
66
+ return callback_kwargs
67
+
68
+
69
+ class SDCFGCutoffCallback(PipelineCallback):
70
+ """
71
+ Callback function for Stable Diffusion Pipelines. After certain number of steps (set by `cutoff_step_ratio` or
72
+ `cutoff_step_index`), this callback will disable the CFG.
73
+
74
+ Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
75
+ """
76
+
77
+ tensor_inputs = ["prompt_embeds"]
78
+
79
+ def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
80
+ cutoff_step_ratio = self.config.cutoff_step_ratio
81
+ cutoff_step_index = self.config.cutoff_step_index
82
+
83
+ # Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
84
+ cutoff_step = (
85
+ cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
86
+ )
87
+
88
+ if step_index == cutoff_step:
89
+ prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
90
+ prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
91
+
92
+ pipeline._guidance_scale = 0.0
93
+
94
+ callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
95
+ return callback_kwargs
96
+
97
+
98
+ class SDXLCFGCutoffCallback(PipelineCallback):
99
+ """
100
+ Callback function for the base Stable Diffusion XL Pipelines. After certain number of steps (set by
101
+ `cutoff_step_ratio` or `cutoff_step_index`), this callback will disable the CFG.
102
+
103
+ Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
104
+ """
105
+
106
+ tensor_inputs = [
107
+ "prompt_embeds",
108
+ "add_text_embeds",
109
+ "add_time_ids",
110
+ ]
111
+
112
+ def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
113
+ cutoff_step_ratio = self.config.cutoff_step_ratio
114
+ cutoff_step_index = self.config.cutoff_step_index
115
+
116
+ # Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
117
+ cutoff_step = (
118
+ cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
119
+ )
120
+
121
+ if step_index == cutoff_step:
122
+ prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
123
+ prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
124
+
125
+ add_text_embeds = callback_kwargs[self.tensor_inputs[1]]
126
+ add_text_embeds = add_text_embeds[-1:] # "-1" denotes the embeddings for conditional pooled text tokens
127
+
128
+ add_time_ids = callback_kwargs[self.tensor_inputs[2]]
129
+ add_time_ids = add_time_ids[-1:] # "-1" denotes the embeddings for conditional added time vector
130
+
131
+ pipeline._guidance_scale = 0.0
132
+
133
+ callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
134
+ callback_kwargs[self.tensor_inputs[1]] = add_text_embeds
135
+ callback_kwargs[self.tensor_inputs[2]] = add_time_ids
136
+
137
+ return callback_kwargs
138
+
139
+
140
+ class SDXLControlnetCFGCutoffCallback(PipelineCallback):
141
+ """
142
+ Callback function for the Controlnet Stable Diffusion XL Pipelines. After certain number of steps (set by
143
+ `cutoff_step_ratio` or `cutoff_step_index`), this callback will disable the CFG.
144
+
145
+ Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
146
+ """
147
+
148
+ tensor_inputs = [
149
+ "prompt_embeds",
150
+ "add_text_embeds",
151
+ "add_time_ids",
152
+ "image",
153
+ ]
154
+
155
+ def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
156
+ cutoff_step_ratio = self.config.cutoff_step_ratio
157
+ cutoff_step_index = self.config.cutoff_step_index
158
+
159
+ # Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
160
+ cutoff_step = (
161
+ cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
162
+ )
163
+
164
+ if step_index == cutoff_step:
165
+ prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
166
+ prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
167
+
168
+ add_text_embeds = callback_kwargs[self.tensor_inputs[1]]
169
+ add_text_embeds = add_text_embeds[-1:] # "-1" denotes the embeddings for conditional pooled text tokens
170
+
171
+ add_time_ids = callback_kwargs[self.tensor_inputs[2]]
172
+ add_time_ids = add_time_ids[-1:] # "-1" denotes the embeddings for conditional added time vector
173
+
174
+ # For Controlnet
175
+ image = callback_kwargs[self.tensor_inputs[3]]
176
+ image = image[-1:]
177
+
178
+ pipeline._guidance_scale = 0.0
179
+
180
+ callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
181
+ callback_kwargs[self.tensor_inputs[1]] = add_text_embeds
182
+ callback_kwargs[self.tensor_inputs[2]] = add_time_ids
183
+ callback_kwargs[self.tensor_inputs[3]] = image
184
+
185
+ return callback_kwargs
186
+
187
+
188
+ class IPAdapterScaleCutoffCallback(PipelineCallback):
189
+ """
190
+ Callback function for any pipeline that inherits `IPAdapterMixin`. After certain number of steps (set by
191
+ `cutoff_step_ratio` or `cutoff_step_index`), this callback will set the IP Adapter scale to `0.0`.
192
+
193
+ Note: This callback mutates the IP Adapter attention processors by setting the scale to 0.0 after the cutoff step.
194
+ """
195
+
196
+ tensor_inputs = []
197
+
198
+ def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
199
+ cutoff_step_ratio = self.config.cutoff_step_ratio
200
+ cutoff_step_index = self.config.cutoff_step_index
201
+
202
+ # Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
203
+ cutoff_step = (
204
+ cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
205
+ )
206
+
207
+ if step_index == cutoff_step:
208
+ pipeline.set_ip_adapter_scale(0.0)
209
+ return callback_kwargs
210
+
211
+
212
+ class SD3CFGCutoffCallback(PipelineCallback):
213
+ """
214
+ Callback function for Stable Diffusion 3 Pipelines. After certain number of steps (set by `cutoff_step_ratio` or
215
+ `cutoff_step_index`), this callback will disable the CFG.
216
+
217
+ Note: This callback mutates the pipeline by changing the `_guidance_scale` attribute to 0.0 after the cutoff step.
218
+ """
219
+
220
+ tensor_inputs = ["prompt_embeds", "pooled_prompt_embeds"]
221
+
222
+ def callback_fn(self, pipeline, step_index, timestep, callback_kwargs) -> dict[str, Any]:
223
+ cutoff_step_ratio = self.config.cutoff_step_ratio
224
+ cutoff_step_index = self.config.cutoff_step_index
225
+
226
+ # Use cutoff_step_index if it's not None, otherwise use cutoff_step_ratio
227
+ cutoff_step = (
228
+ cutoff_step_index if cutoff_step_index is not None else int(pipeline.num_timesteps * cutoff_step_ratio)
229
+ )
230
+
231
+ if step_index == cutoff_step:
232
+ prompt_embeds = callback_kwargs[self.tensor_inputs[0]]
233
+ prompt_embeds = prompt_embeds[-1:] # "-1" denotes the embeddings for conditional text tokens.
234
+
235
+ pooled_prompt_embeds = callback_kwargs[self.tensor_inputs[1]]
236
+ pooled_prompt_embeds = pooled_prompt_embeds[
237
+ -1:
238
+ ] # "-1" denotes the embeddings for conditional pooled text tokens.
239
+
240
+ pipeline._guidance_scale = 0.0
241
+
242
+ callback_kwargs[self.tensor_inputs[0]] = prompt_embeds
243
+ callback_kwargs[self.tensor_inputs[1]] = pooled_prompt_embeds
244
+ return callback_kwargs
diffusers/commands/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from abc import ABC, abstractmethod
16
+ from argparse import ArgumentParser
17
+
18
+
19
+ class BaseDiffusersCLICommand(ABC):
20
+ @staticmethod
21
+ @abstractmethod
22
+ def register_subcommand(parser: ArgumentParser):
23
+ raise NotImplementedError()
24
+
25
+ @abstractmethod
26
+ def run(self):
27
+ raise NotImplementedError()
diffusers/commands/custom_blocks.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """`diffusers-cli custom_blocks` — package a local `ModularPipelineBlocks` subclass for the Hub.
16
+
17
+ Parses `block.py` (or `--block_module_name`), instantiates the chosen block, and calls `save_pretrained` in the current
18
+ working directory.
19
+ """
20
+
21
+ import ast
22
+ import importlib.util
23
+ import os
24
+ from argparse import ArgumentParser, Namespace
25
+ from pathlib import Path
26
+
27
+ from ..utils import logging
28
+ from . import BaseDiffusersCLICommand
29
+
30
+
31
+ EXPECTED_PARENT_CLASSES = ["ModularPipelineBlocks"]
32
+
33
+
34
+ def conversion_command_factory(args: Namespace):
35
+ return CustomBlocksCommand(args.block_module_name, args.block_class_name)
36
+
37
+
38
+ class CustomBlocksCommand(BaseDiffusersCLICommand):
39
+ @staticmethod
40
+ def register_subcommand(parser: ArgumentParser):
41
+ from argparse import RawDescriptionHelpFormatter
42
+
43
+ epilog = (
44
+ "Examples\n"
45
+ " $ diffusers-cli custom_blocks\n"
46
+ " $ diffusers-cli custom_blocks --block_module_name my_block.py\n"
47
+ " $ diffusers-cli custom_blocks --block_module_name my_block.py --block_class_name MyDenoiseBlock\n"
48
+ "\n"
49
+ "Learn more\n"
50
+ " Use `diffusers-cli <command> --help` for more information about a command.\n"
51
+ " Read the documentation at https://huggingface.co/docs/diffusers\n"
52
+ )
53
+
54
+ conversion_parser = parser.add_parser(
55
+ "custom_blocks",
56
+ help="Package a local ModularPipelineBlocks subclass for the Hub.",
57
+ usage="\n diffusers-cli custom_blocks [options]",
58
+ epilog=epilog,
59
+ formatter_class=RawDescriptionHelpFormatter,
60
+ )
61
+ conversion_parser._optionals.title = "Options"
62
+ conversion_parser.add_argument(
63
+ "--block_module_name",
64
+ type=str,
65
+ default="block.py",
66
+ help="Module filename in which the custom block will be implemented.",
67
+ )
68
+ conversion_parser.add_argument(
69
+ "--block_class_name",
70
+ type=str,
71
+ default=None,
72
+ help="Name of the custom block. If provided None, we will try to infer it.",
73
+ )
74
+ conversion_parser.set_defaults(func=conversion_command_factory)
75
+
76
+ def __init__(self, block_module_name: str = "block.py", block_class_name: str = None):
77
+ self.logger = logging.get_logger("diffusers-cli/custom_blocks")
78
+ self.block_module_name = Path(block_module_name)
79
+ self.block_class_name = block_class_name
80
+
81
+ def run(self):
82
+ # determine the block to be saved.
83
+ out = self._get_class_names(self.block_module_name)
84
+ classes_found = list({cls for cls, _ in out})
85
+
86
+ if self.block_class_name is not None:
87
+ child_class, parent_class = self._choose_block(out, self.block_class_name)
88
+ if child_class is None and parent_class is None:
89
+ raise ValueError(
90
+ "`block_class_name` could not be retrieved. Available classes from "
91
+ f"{self.block_module_name}:\n{classes_found}"
92
+ )
93
+ else:
94
+ self.logger.info(
95
+ f"Found classes: {classes_found} will be using {classes_found[0]}. "
96
+ "If this needs to be changed, re-run the command specifying `block_class_name`."
97
+ )
98
+ child_class, parent_class = out[0][0], out[0][1]
99
+
100
+ # dynamically get the custom block and initialize it to call `save_pretrained` in the current directory.
101
+ # the user is responsible for running it, so I guess that is safe?
102
+ module_name = f"__dynamic__{self.block_module_name.stem}"
103
+ spec = importlib.util.spec_from_file_location(module_name, str(self.block_module_name))
104
+ module = importlib.util.module_from_spec(spec)
105
+ spec.loader.exec_module(module)
106
+ block = getattr(module, child_class)()
107
+ block.save_pretrained(os.getcwd())
108
+ # `ModularPipeline.from_pretrained` (and therefore `diffusers-cli run`) loads a repo
109
+ # through `modular_model_index.json`, which only the pipeline-level save writes — without
110
+ # it the packaged repo is importable as blocks but not runnable as a pipeline.
111
+ block.init_pipeline().save_pretrained(os.getcwd())
112
+
113
+ def _choose_block(self, candidates, chosen=None):
114
+ for cls, base in candidates:
115
+ if cls == chosen:
116
+ return cls, base
117
+ return None, None
118
+
119
+ def _get_class_names(self, file_path):
120
+ source = file_path.read_text(encoding="utf-8")
121
+ try:
122
+ tree = ast.parse(source, filename=file_path)
123
+ except SyntaxError as e:
124
+ raise ValueError(f"Could not parse {file_path!r}: {e}") from e
125
+
126
+ results: list[tuple[str, str]] = []
127
+ for node in tree.body:
128
+ if not isinstance(node, ast.ClassDef):
129
+ continue
130
+
131
+ base_names = [bname for b in node.bases if (bname := self._get_base_name(b)) is not None]
132
+
133
+ for allowed in EXPECTED_PARENT_CLASSES:
134
+ if allowed in base_names:
135
+ results.append((node.name, allowed))
136
+
137
+ return results
138
+
139
+ def _get_base_name(self, node: ast.expr):
140
+ if isinstance(node, ast.Name):
141
+ return node.id
142
+ elif isinstance(node, ast.Attribute):
143
+ val = self._get_base_name(node.value)
144
+ return f"{val}.{node.attr}" if val else node.attr
145
+ return None
diffusers/commands/diffusers_cli.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from argparse import ArgumentParser
17
+
18
+ from huggingface_hub.cli._output import OutputFormat, out
19
+
20
+ from .custom_blocks import CustomBlocksCommand
21
+ from .env import EnvironmentCommand
22
+ from .fp16_safetensors import FP16SafetensorsCommand
23
+ from .run import RunCommand
24
+ from .schema import SchemaCommand
25
+ from .skills import SkillsCommand
26
+
27
+
28
+ def main():
29
+ parser = ArgumentParser(
30
+ prog="diffusers-cli",
31
+ usage="\n diffusers-cli [--format <fmt>] <command> [options]",
32
+ )
33
+ parser._optionals.title = "Options"
34
+ parser.add_argument(
35
+ "--format",
36
+ choices=[m.value for m in OutputFormat],
37
+ default=OutputFormat.auto.value,
38
+ help=(
39
+ "Output format. 'auto' (default) picks 'agent' when an AI coding agent is detected "
40
+ "(via CLAUDECODE/CURSOR_AI/AIDER_AI_CONTEXT/... env vars) and 'human' otherwise. "
41
+ "Must appear before the subcommand."
42
+ ),
43
+ )
44
+ commands_parser = parser.add_subparsers(title="Commands", metavar="<command>")
45
+
46
+ # Register commands
47
+ EnvironmentCommand.register_subcommand(commands_parser)
48
+ FP16SafetensorsCommand.register_subcommand(commands_parser)
49
+ CustomBlocksCommand.register_subcommand(commands_parser)
50
+ RunCommand.register_subcommand(commands_parser)
51
+ SchemaCommand.register_subcommand(commands_parser)
52
+ SkillsCommand.register_subcommand(commands_parser)
53
+
54
+ # Let's go
55
+ args = parser.parse_args()
56
+
57
+ out.set_mode(OutputFormat(args.format))
58
+
59
+ if not hasattr(args, "func"):
60
+ parser.print_help()
61
+ exit(1)
62
+
63
+ # Run
64
+ service = args.func(args)
65
+ service.run()
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
diffusers/commands/env.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import importlib.metadata
16
+ import platform
17
+ import subprocess
18
+ from argparse import ArgumentParser
19
+
20
+ import huggingface_hub
21
+
22
+ from .. import __version__ as version
23
+ from ..utils import (
24
+ is_accelerate_available,
25
+ is_bitsandbytes_available,
26
+ is_gguf_available,
27
+ is_google_colab,
28
+ is_nvidia_modelopt_available,
29
+ is_optimum_quanto_available,
30
+ is_peft_available,
31
+ is_safetensors_available,
32
+ is_torch_available,
33
+ is_torchao_available,
34
+ is_transformers_available,
35
+ is_xformers_available,
36
+ )
37
+ from . import BaseDiffusersCLICommand
38
+
39
+
40
+ # (display name, availability_fn, pypi distribution name for importlib.metadata.version)
41
+ _QUANTIZATION_BACKENDS = (
42
+ ("bitsandbytes", is_bitsandbytes_available, "bitsandbytes"),
43
+ ("gguf", is_gguf_available, "gguf"),
44
+ ("optimum-quanto", is_optimum_quanto_available, "optimum-quanto"),
45
+ ("torchao", is_torchao_available, "torchao"),
46
+ ("nvidia-modelopt", is_nvidia_modelopt_available, "nvidia-modelopt"),
47
+ )
48
+
49
+
50
+ def info_command_factory(_):
51
+ return EnvironmentCommand()
52
+
53
+
54
+ class EnvironmentCommand(BaseDiffusersCLICommand):
55
+ @staticmethod
56
+ def register_subcommand(parser: ArgumentParser) -> None:
57
+ download_parser = parser.add_parser(
58
+ "env",
59
+ help="Print versions of diffusers and its dependencies (for bug reports).",
60
+ usage="\n diffusers-cli env",
61
+ )
62
+ download_parser._optionals.title = "Options"
63
+ download_parser.set_defaults(func=info_command_factory)
64
+
65
+ def run(self) -> dict:
66
+ hub_version = huggingface_hub.__version__
67
+
68
+ safetensors_version = "not installed"
69
+ if is_safetensors_available():
70
+ import safetensors
71
+
72
+ safetensors_version = safetensors.__version__
73
+
74
+ pt_version = "not installed"
75
+ pt_cuda_available = "NA"
76
+ if is_torch_available():
77
+ import torch
78
+
79
+ pt_version = torch.__version__
80
+ pt_cuda_available = torch.cuda.is_available()
81
+
82
+ transformers_version = "not installed"
83
+ if is_transformers_available():
84
+ import transformers
85
+
86
+ transformers_version = transformers.__version__
87
+
88
+ accelerate_version = "not installed"
89
+ if is_accelerate_available():
90
+ import accelerate
91
+
92
+ accelerate_version = accelerate.__version__
93
+
94
+ peft_version = "not installed"
95
+ if is_peft_available():
96
+ import peft
97
+
98
+ peft_version = peft.__version__
99
+
100
+ quantization_versions = {}
101
+ for backend_name, is_available_fn, dist_name in _QUANTIZATION_BACKENDS:
102
+ if not is_available_fn():
103
+ continue
104
+ try:
105
+ quantization_versions[backend_name] = importlib.metadata.version(dist_name)
106
+ except importlib.metadata.PackageNotFoundError:
107
+ quantization_versions[backend_name] = "N/A"
108
+
109
+ xformers_version = "not installed"
110
+ if is_xformers_available():
111
+ import xformers
112
+
113
+ xformers_version = xformers.__version__
114
+
115
+ platform_info = platform.platform()
116
+
117
+ is_google_colab_str = "Yes" if is_google_colab() else "No"
118
+
119
+ accelerator = "NA"
120
+ if platform.system() in {"Linux", "Windows"}:
121
+ try:
122
+ sp = subprocess.Popen(
123
+ ["nvidia-smi", "--query-gpu=gpu_name,memory.total", "--format=csv,noheader"],
124
+ stdout=subprocess.PIPE,
125
+ stderr=subprocess.PIPE,
126
+ )
127
+ out_str, _ = sp.communicate()
128
+ out_str = out_str.decode("utf-8")
129
+
130
+ if len(out_str) > 0:
131
+ accelerator = out_str.strip()
132
+ except FileNotFoundError:
133
+ pass
134
+ elif platform.system() == "Darwin": # Mac OS
135
+ try:
136
+ sp = subprocess.Popen(
137
+ ["system_profiler", "SPDisplaysDataType"],
138
+ stdout=subprocess.PIPE,
139
+ stderr=subprocess.PIPE,
140
+ )
141
+ out_str, _ = sp.communicate()
142
+ out_str = out_str.decode("utf-8")
143
+
144
+ start = out_str.find("Chipset Model:")
145
+ if start != -1:
146
+ start += len("Chipset Model:")
147
+ end = out_str.find("\n", start)
148
+ accelerator = out_str[start:end].strip()
149
+
150
+ start = out_str.find("VRAM (Total):")
151
+ if start != -1:
152
+ start += len("VRAM (Total):")
153
+ end = out_str.find("\n", start)
154
+ accelerator += " VRAM: " + out_str[start:end].strip()
155
+ except FileNotFoundError:
156
+ pass
157
+ else:
158
+ print("It seems you are running an unusual OS. Could you fill in the accelerator manually?")
159
+
160
+ info = {
161
+ "🤗 Diffusers version": version,
162
+ "Platform": platform_info,
163
+ "Running on Google Colab?": is_google_colab_str,
164
+ "Python version": platform.python_version(),
165
+ "PyTorch version (GPU?)": f"{pt_version} ({pt_cuda_available})",
166
+ "Huggingface_hub version": hub_version,
167
+ "Transformers version": transformers_version,
168
+ "Accelerate version": accelerate_version,
169
+ "PEFT version": peft_version,
170
+ **{f"{name} version": ver for name, ver in quantization_versions.items()},
171
+ "Safetensors version": safetensors_version,
172
+ "xFormers version": xformers_version,
173
+ "Accelerator": accelerator,
174
+ "Using GPU in script?": "<fill in>",
175
+ "Using distributed or parallel set-up in script?": "<fill in>",
176
+ }
177
+
178
+ print("\nCopy-and-paste the text below in your GitHub issue and FILL OUT the two last points.\n")
179
+ print(self.format_dict(info))
180
+
181
+ return info
182
+
183
+ @staticmethod
184
+ def format_dict(d: dict) -> str:
185
+ return "\n".join([f"- {prop}: {val}" for prop, val in d.items()]) + "\n"
diffusers/commands/fp16_safetensors.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Usage example:
17
+ diffusers-cli fp16_safetensors --ckpt_id=openai/shap-e --fp16 --use_safetensors
18
+ """
19
+
20
+ import glob
21
+ import json
22
+ import warnings
23
+ from argparse import ArgumentParser, Namespace
24
+ from importlib import import_module
25
+
26
+ import huggingface_hub
27
+ import torch
28
+ from huggingface_hub import hf_hub_download
29
+ from packaging import version
30
+
31
+ from ..utils import logging
32
+ from . import BaseDiffusersCLICommand
33
+
34
+
35
+ def conversion_command_factory(args: Namespace):
36
+ warnings.warn(
37
+ "`diffusers-cli fp16_safetensors` is deprecated and will be removed in a future version. "
38
+ "Convert weights to fp16 safetensors directly with `safetensors.torch.save_file` or via "
39
+ "`pipeline.save_pretrained(..., safe_serialization=True, variant='fp16')`.",
40
+ FutureWarning,
41
+ stacklevel=2,
42
+ )
43
+ if args.use_auth_token:
44
+ warnings.warn(
45
+ "The `--use_auth_token` flag is deprecated and will be removed in a future version."
46
+ "Authentication is now handled automatically if the user is logged in."
47
+ )
48
+ return FP16SafetensorsCommand(args.ckpt_id, args.fp16, args.use_safetensors)
49
+
50
+
51
+ class FP16SafetensorsCommand(BaseDiffusersCLICommand):
52
+ @staticmethod
53
+ def register_subcommand(parser: ArgumentParser):
54
+ conversion_parser = parser.add_parser(
55
+ "fp16_safetensors",
56
+ help="[DEPRECATED] Convert a Hub checkpoint's weights to fp16 safetensors and push back as a PR.",
57
+ usage="\n diffusers-cli fp16_safetensors [options]",
58
+ )
59
+ conversion_parser._optionals.title = "Options"
60
+ conversion_parser.add_argument(
61
+ "--ckpt_id",
62
+ type=str,
63
+ help="Repo id of the checkpoints on which to run the conversion. Example: 'openai/shap-e'.",
64
+ )
65
+ conversion_parser.add_argument(
66
+ "--fp16", action="store_true", help="If serializing the variables in FP16 precision."
67
+ )
68
+ conversion_parser.add_argument(
69
+ "--use_safetensors", action="store_true", help="If serializing in the safetensors format."
70
+ )
71
+ conversion_parser.add_argument(
72
+ "--use_auth_token",
73
+ action="store_true",
74
+ help="When working with checkpoints having private visibility. When used `hf auth login` needs to be run beforehand.",
75
+ )
76
+ conversion_parser.set_defaults(func=conversion_command_factory)
77
+
78
+ def __init__(self, ckpt_id: str, fp16: bool, use_safetensors: bool):
79
+ self.logger = logging.get_logger("diffusers-cli/fp16_safetensors")
80
+ self.ckpt_id = ckpt_id
81
+ self.local_ckpt_dir = f"/tmp/{ckpt_id}"
82
+ self.fp16 = fp16
83
+
84
+ self.use_safetensors = use_safetensors
85
+
86
+ if not self.use_safetensors and not self.fp16:
87
+ raise NotImplementedError(
88
+ "When `use_safetensors` and `fp16` both are False, then this command is of no use."
89
+ )
90
+
91
+ def run(self):
92
+ if version.parse(huggingface_hub.__version__) < version.parse("0.9.0"):
93
+ raise ImportError(
94
+ "The huggingface_hub version must be >= 0.9.0 to use this command. Please update your huggingface_hub"
95
+ " installation."
96
+ )
97
+ else:
98
+ from huggingface_hub import create_commit
99
+ from huggingface_hub._commit_api import CommitOperationAdd
100
+
101
+ model_index = hf_hub_download(repo_id=self.ckpt_id, filename="model_index.json")
102
+ with open(model_index, "r") as f:
103
+ pipeline_class_name = json.load(f)["_class_name"]
104
+ pipeline_class = getattr(import_module("diffusers"), pipeline_class_name)
105
+ self.logger.info(f"Pipeline class imported: {pipeline_class_name}.")
106
+
107
+ # Load the appropriate pipeline. We could have used `DiffusionPipeline`
108
+ # here, but just to avoid potential edge cases.
109
+ pipeline = pipeline_class.from_pretrained(
110
+ self.ckpt_id, torch_dtype=torch.float16 if self.fp16 else torch.float32
111
+ )
112
+ pipeline.save_pretrained(
113
+ self.local_ckpt_dir,
114
+ safe_serialization=True if self.use_safetensors else False,
115
+ variant="fp16" if self.fp16 else None,
116
+ )
117
+ self.logger.info(f"Pipeline locally saved to {self.local_ckpt_dir}.")
118
+
119
+ # Fetch all the paths.
120
+ if self.fp16:
121
+ modified_paths = glob.glob(f"{self.local_ckpt_dir}/*/*.fp16.*")
122
+ elif self.use_safetensors:
123
+ modified_paths = glob.glob(f"{self.local_ckpt_dir}/*/*.safetensors")
124
+
125
+ # Prepare for the PR.
126
+ commit_message = f"Serialize variables with FP16: {self.fp16} and safetensors: {self.use_safetensors}."
127
+ operations = []
128
+ for path in modified_paths:
129
+ operations.append(CommitOperationAdd(path_in_repo="/".join(path.split("/")[4:]), path_or_fileobj=path))
130
+
131
+ # Open the PR.
132
+ commit_description = (
133
+ "Variables converted by the [`diffusers`' `fp16_safetensors`"
134
+ " CLI](https://github.com/huggingface/diffusers/blob/main/src/diffusers/commands/fp16_safetensors.py)."
135
+ )
136
+ hub_pr_url = create_commit(
137
+ repo_id=self.ckpt_id,
138
+ operations=operations,
139
+ commit_message=commit_message,
140
+ commit_description=commit_description,
141
+ repo_type="model",
142
+ create_pr=True,
143
+ ).pr_url
144
+ self.logger.info(f"PR created here: {hub_pr_url}.")
diffusers/commands/run.py ADDED
@@ -0,0 +1,1335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """`diffusers-cli run` — single agentic entry point.
16
+
17
+ Runs any diffusers pipeline (standard or modular) by forwarding `--pipeline-kwargs` verbatim, saves the output by
18
+ detecting its runtime type, and can submit the same call to an HF Sandbox via `--remote`.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import io
24
+ import json
25
+ import os
26
+ import shlex
27
+ import sys
28
+ import time
29
+ import uuid
30
+ import wave
31
+ from argparse import ArgumentParser, Namespace, RawDescriptionHelpFormatter, _SubParsersAction
32
+ from datetime import datetime
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ import numpy as np
37
+ import torch
38
+ from huggingface_hub import HfApi, Sandbox, Volume, get_token, parse_hf_uri
39
+ from huggingface_hub.cli._output import out
40
+ from huggingface_hub.utils import httpx, send_telemetry
41
+ from PIL import Image
42
+
43
+ import diffusers
44
+ from diffusers import ContextParallelConfig
45
+ from diffusers.models.attention_dispatch import _HUB_KERNELS_REGISTRY
46
+ from diffusers.utils import export_to_video, load_image, load_video, logging
47
+ from diffusers.utils.constants import DIFFUSERS_REQUEST_TIMEOUT
48
+ from diffusers.utils.torch_utils import torch_device
49
+
50
+ from . import BaseDiffusersCLICommand
51
+
52
+
53
+ logger = logging.get_logger("diffusers-cli/run")
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Constants
58
+ # ---------------------------------------------------------------------------
59
+
60
+ DEFAULT_OUTPUT_DIR = str(Path.home() / ".diffusers" / "cli" / "run" / "outputs")
61
+ DTYPE_CHOICES = ("auto", "float16", "fp16", "bfloat16", "bf16", "float32", "fp32")
62
+ CPU_OFFLOAD_CHOICES = ("model", "group", "auto")
63
+
64
+
65
+ ATTENTION_BACKEND_CHOICES = ("default", *sorted(b.value for b in _HUB_KERNELS_REGISTRY))
66
+
67
+ # Kwarg keys whose string value gets auto-loaded before being passed to the pipeline call.
68
+ # Images resolve via `diffusers.utils.load_image` → PIL.Image.Image; videos resolve via
69
+ # `diffusers.utils.load_video` → list[PIL.Image.Image].
70
+ _IMAGE_INPUT_KEYS = (
71
+ "image",
72
+ "last_image",
73
+ "mask_image",
74
+ "control_image",
75
+ "ip_adapter_image",
76
+ "image_2",
77
+ )
78
+ _VIDEO_INPUT_KEYS = (
79
+ "video",
80
+ "control_video",
81
+ )
82
+ _AUDIO_INPUT_KEYS = (
83
+ "initial_audio_waveforms",
84
+ "reference_audio",
85
+ "src_audio",
86
+ )
87
+
88
+ # Pipeline attribute prefixes that identify a denoiser submodule. Matches base names
89
+ # (`transformer`, `unet`) and their numbered variants (`transformer_2`, etc.).
90
+ _DENOISER_COMPONENT_KEYS = ("transformer", "unet")
91
+
92
+ _DEFAULT_REMOTE_DEPS = (
93
+ "diffusers",
94
+ "accelerate",
95
+ "transformers",
96
+ "safetensors",
97
+ "sentencepiece", # required by several text-encoder tokenizers (T5, LLaMA, …)
98
+ "ftfy", # required by older CLIP text-encoder paths
99
+ "peft", # required by `load_lora_weights` when `--lora` is passed
100
+ "imageio", # preferred `export_to_video` backend
101
+ "imageio-ffmpeg", # bundles a static ffmpeg; the cv2 fallback needs system libs the slim image lacks
102
+ )
103
+
104
+ # Base sandbox image — provides torch + CUDA so `uv pip install --system`
105
+ # only has to add the small Python deps. cuda12.8 is the highest cuda12.x tag
106
+ # below the HF Jobs host driver's CUDA 12.9 max.
107
+ _DEFAULT_REMOTE_IMAGE = "pytorch/pytorch:2.10.0-cuda12.8-cudnn9-runtime"
108
+
109
+ # Installed console-script name invoked inside the sandbox after the deps land.
110
+ _CONTAINER_CLI_BINARY = "diffusers-cli"
111
+
112
+ # Working directories inside the sandbox: local media from `--pipeline-kwargs` is uploaded
113
+ # under _SANDBOX_INPUTS_DIR, and the sandbox CLI is told to write its outputs under
114
+ # _SANDBOX_OUTPUTS_DIR so we can download them back afterwards.
115
+ _SANDBOX_INPUTS_DIR = "/tmp/diffusers-cli/inputs"
116
+ _SANDBOX_OUTPUTS_DIR = "/tmp/diffusers-cli/outputs"
117
+
118
+ RUN_ID_ENV = "DIFFUSERS_CLI_RUN_ID"
119
+
120
+ # Namespace keys that control *how* a remote run is dispatched, not what the sandbox CLI
121
+ # runs. They are stripped when forwarding argv to the sandbox.
122
+ REMOTE_KEYS = frozenset(
123
+ {
124
+ "remote",
125
+ "flavor",
126
+ "timeout",
127
+ "dependencies",
128
+ "namespace",
129
+ "image",
130
+ "keep_alive",
131
+ "sandbox_id",
132
+ "idle_timeout",
133
+ "volume",
134
+ "func",
135
+ "format", # top-level --format is a local rendering flag; never forward to the sandbox
136
+ }
137
+ )
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Argparse helpers
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ def _add_loading_arguments(parser: ArgumentParser) -> None:
146
+ parser.add_argument("--model", "-m", required=True, help="Model id on the Hugging Face Hub or local path.")
147
+ parser.add_argument(
148
+ "--device-map",
149
+ default=None,
150
+ help=(
151
+ "Component placement. Accepts a torch device string (`cuda`, `cuda:0`, `cpu`, `mps`), "
152
+ "`balanced` for pipeline-level auto-split across visible GPUs, or a JSON dict of "
153
+ '`{"<component>": <device>}` for explicit per-component placement. Auto-detected if omitted.'
154
+ ),
155
+ )
156
+ parser.add_argument("--dtype", default="auto", choices=DTYPE_CHOICES, help="Torch dtype for pipeline weights.")
157
+ parser.add_argument("--variant", default=None, help='Optional weight variant (e.g. "fp16").')
158
+ parser.add_argument("--revision", default=None, help="Model revision (branch, tag, or commit SHA).")
159
+ parser.add_argument("--token", default=None, help="Hugging Face token for gated/private models.")
160
+ parser.add_argument("--trust-remote-code", action="store_true", help="Allow custom code from the Hub.")
161
+ parser.add_argument(
162
+ "--lora",
163
+ action="append",
164
+ default=None,
165
+ metavar="JSON",
166
+ help=(
167
+ "JSON dict describing a LoRA adapter to attach after the pipeline loads. Repeat to stack "
168
+ 'multiple adapters. Format: \'{"lora_id": "<id>", "lora_scale": <float>}\'. `lora_scale` '
169
+ "defaults to 1.0; `adapter_name` is optional (auto-generated as `lora_<i>` when stacking); "
170
+ "`weight_name` picks the weight file when the repo ships more than one."
171
+ ),
172
+ )
173
+
174
+
175
+ def _add_optimization_arguments(parser: ArgumentParser) -> None:
176
+ parser.add_argument(
177
+ "--cpu-offload",
178
+ choices=CPU_OFFLOAD_CHOICES,
179
+ default=None,
180
+ help=(
181
+ "Offload pipeline components to CPU during inference. "
182
+ "'model' uses enable_model_cpu_offload, "
183
+ "'group' uses pipeline.enable_group_offload(leaf_level, use_stream=True). "
184
+ "Modular pipelines only support 'auto', which offloads through a ComponentsManager "
185
+ "via enable_auto_cpu_offload."
186
+ ),
187
+ )
188
+ parser.add_argument(
189
+ "--attention-backend",
190
+ choices=ATTENTION_BACKEND_CHOICES,
191
+ default="default",
192
+ help=(
193
+ "Override the attention backend on the transformer/UNet. "
194
+ "Only Hub-hosted kernels are exposed — they auto-download on first use."
195
+ ),
196
+ )
197
+ parser.add_argument("--vae-tiling", action="store_true", help="Enable VAE tiling (lower peak VRAM).")
198
+ parser.add_argument("--vae-slicing", action="store_true", help="Enable VAE slicing (lower peak VRAM).")
199
+ parser.add_argument(
200
+ "--context-parallel",
201
+ action="store_true",
202
+ help=(
203
+ "Enable Ulysses-style context parallelism (ulysses_anything mode). "
204
+ "Requires a DiT-based pipeline and launching the CLI under torchrun with ≥2 GPUs."
205
+ ),
206
+ )
207
+ parser.add_argument(
208
+ "--compile",
209
+ nargs="?",
210
+ const='{"fullgraph": true}',
211
+ default=None,
212
+ metavar="JSON",
213
+ help=(
214
+ "torch.compile every denoiser submodule on the pipeline. Accepts an optional JSON "
215
+ 'object of kwargs forwarded to `torch.compile`, e.g. \'{"mode": "max-autotune", '
216
+ '"fullgraph": true}\'. Bare `--compile` uses `fullgraph=true`. Adds a one-time '
217
+ "compilation cost on the first step but speeds up every subsequent step — worth it "
218
+ "for multi-step generation (50+ steps)."
219
+ ),
220
+ )
221
+
222
+
223
+ def _add_output_arguments(parser: ArgumentParser) -> None:
224
+ parser.add_argument(
225
+ "--output",
226
+ "-o",
227
+ default=None,
228
+ help=(
229
+ "Output file or directory. Defaults to "
230
+ "~/.diffusers/cli/run/outputs/diffusers-run-<YYYYMMDDTHHMMSS>-<short-uuid>/<NNNN>.<ext>."
231
+ ),
232
+ )
233
+ parser.add_argument(
234
+ "--push-to",
235
+ default=None,
236
+ help=(
237
+ "Upload the generated files to this HF bucket after saving (created if missing). Accepts "
238
+ "an HF bucket id (`<namespace>/<name>`), an `hf://buckets/<namespace>/<name>[/<subpath>]` "
239
+ "URI, or a browser URL for the same — a subpath is used as a folder prefix. Under --remote "
240
+ "the upload runs inside the sandbox; without an explicit --output the bucket becomes the "
241
+ "sole destination and nothing is downloaded back."
242
+ ),
243
+ )
244
+
245
+
246
+ def _add_remote_arguments(parser: ArgumentParser) -> None:
247
+ parser.add_argument(
248
+ "--remote",
249
+ action="store_true",
250
+ help="Run this command in a Hugging Face Sandbox instead of on the local machine.",
251
+ )
252
+ parser.add_argument(
253
+ "--flavor",
254
+ default="a10g-small",
255
+ help="HF Sandbox hardware flavor for --remote (e.g. a10g-small, a100-large, cpu-basic).",
256
+ )
257
+ parser.add_argument(
258
+ "--timeout",
259
+ default="10m",
260
+ help="Max wallclock for the run command inside the sandbox (e.g. 30m, 2h). Defaults to 10m.",
261
+ )
262
+ parser.add_argument(
263
+ "--dependencies",
264
+ action="append",
265
+ default=None,
266
+ help="Extra pip dependencies to install in the sandbox. Repeat to add multiple.",
267
+ )
268
+ parser.add_argument(
269
+ "--namespace",
270
+ default=None,
271
+ help="HF namespace to create the sandbox under (defaults to the current user).",
272
+ )
273
+ parser.add_argument(
274
+ "--image",
275
+ default=None,
276
+ help=(
277
+ "Sandbox image for --remote (defaults to "
278
+ f"{_DEFAULT_REMOTE_IMAGE!r}). Must provide torch + CUDA; the CLI installs the "
279
+ "small Python deps on top via `uv pip install --system`."
280
+ ),
281
+ )
282
+ parser.add_argument(
283
+ "--keep-alive",
284
+ action="store_true",
285
+ help=(
286
+ "Don't terminate the sandbox after the run. Its id is printed so a later --remote run "
287
+ "can reconnect with --sandbox-id and reuse the warm deps/weights/compile cache."
288
+ ),
289
+ )
290
+ parser.add_argument(
291
+ "--sandbox-id",
292
+ default=None,
293
+ help=(
294
+ "Reconnect to an existing sandbox (from a prior --keep-alive run) instead of creating a new "
295
+ "one, reusing its warm deps/weights/compile cache. Implies --keep-alive; stop it with "
296
+ "`hf sandbox kill <id>`."
297
+ ),
298
+ )
299
+ parser.add_argument(
300
+ "--idle-timeout",
301
+ default="10m",
302
+ help=(
303
+ "Auto-shutdown the sandbox after this much inactivity (e.g. 30m, 1h). Defaults to 10m. "
304
+ "Only applied on new sandbox creation — ignored when reconnecting via --sandbox-id."
305
+ ),
306
+ )
307
+ parser.add_argument(
308
+ "--volume",
309
+ action="append",
310
+ default=None,
311
+ metavar="BUCKET_ID[:MOUNT_PATH]",
312
+ help=(
313
+ "Mount an HF bucket into the sandbox as a read-write directory. Repeatable. Format: "
314
+ "`<namespace>/<name>` (mounts at `/mnt/buckets/<namespace>/<name>`) or "
315
+ "`<namespace>/<name>:/some/path` for a custom path. Reference mounted files from "
316
+ "--pipeline-kwargs like any other local path. Applied only on new sandbox creation — "
317
+ "ignored when reconnecting via --sandbox-id."
318
+ ),
319
+ )
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # Pipeline loading + optimization
324
+ # ---------------------------------------------------------------------------
325
+
326
+
327
+ def _resolve_dtype(name: str | None):
328
+ if name in (None, "auto"):
329
+ return "auto"
330
+
331
+ mapping = {
332
+ "fp32": torch.float32,
333
+ "float32": torch.float32,
334
+ "fp16": torch.float16,
335
+ "float16": torch.float16,
336
+ "bf16": torch.bfloat16,
337
+ "bfloat16": torch.bfloat16,
338
+ }
339
+ if name not in mapping:
340
+ raise ValueError(f"Unknown dtype: {name}")
341
+ return mapping[name]
342
+
343
+
344
+ def _resolve_device_map(raw: str | None) -> str | dict:
345
+ """Parse `--device-map` into a value acceptable by `from_pretrained(device_map=...)`.
346
+
347
+ Returns a JSON dict if the value looks like one, `"balanced"` verbatim, or a single-device string (e.g. `"cuda"`,
348
+ `"cuda:1"`, `"cpu"`, `"mps"`). Auto-detects when `raw is None`, pinning to `cuda:$LOCAL_RANK` under torchrun.
349
+ """
350
+ if raw is None:
351
+ if torch_device == "cuda":
352
+ local_rank = os.environ.get("LOCAL_RANK")
353
+ if local_rank is not None:
354
+ torch.cuda.set_device(int(local_rank))
355
+ return f"cuda:{local_rank}"
356
+ return torch_device
357
+
358
+ if raw.strip().startswith("{"):
359
+ try:
360
+ parsed = json.loads(raw)
361
+ except json.JSONDecodeError as e:
362
+ raise SystemExit(f"--device-map must be a device string or a JSON dict: {e}") from e
363
+ if not isinstance(parsed, dict):
364
+ raise SystemExit("--device-map JSON must decode to an object.")
365
+ return parsed
366
+
367
+ return raw
368
+
369
+
370
+ def _apply_cpu_offload(pipeline: Any, mode: str, device_map: str | dict, offload_margin: str | None = None) -> None:
371
+ """Apply CPU offload. Requires a single-device target (not balanced or dict).
372
+
373
+ Standard pipelines support 'model' and 'group'; modular pipelines offload through the ComponentsManager they were
374
+ loaded with ('auto').
375
+ """
376
+ if not isinstance(device_map, str) or device_map == "balanced":
377
+ raise SystemExit(
378
+ "--cpu-offload requires --device-map to be a single device string (e.g. 'cuda'); "
379
+ f"got {device_map!r}. balanced/dict placement is incompatible with CPU offload."
380
+ )
381
+
382
+ if isinstance(pipeline, diffusers.ModularPipeline):
383
+ offload_kwargs = {"memory_reserve_margin": offload_margin} if offload_margin is not None else {}
384
+ pipeline._components_manager.enable_auto_cpu_offload(device=device_map, **offload_kwargs)
385
+ return
386
+
387
+ if mode == "auto":
388
+ raise SystemExit(
389
+ "--cpu-offload auto only applies to modular pipelines (it offloads through a "
390
+ "ComponentsManager). Use 'model' or 'group' for standard pipelines."
391
+ )
392
+ if offload_margin is not None:
393
+ logger.warning(
394
+ f"--offload-margin {offload_margin!r} only applies to `--cpu-offload auto`; "
395
+ f"ignoring it for `--cpu-offload {mode}`."
396
+ )
397
+ if mode == "model":
398
+ pipeline.enable_model_cpu_offload(device=device_map)
399
+ elif mode == "group":
400
+ pipeline.enable_group_offload(
401
+ onload_device=torch.device(device_map),
402
+ offload_type="leaf_level",
403
+ use_stream=True,
404
+ )
405
+
406
+
407
+ def _set_attention_backend(pipeline: Any, backend: str) -> None:
408
+ transformer = getattr(pipeline, "transformer", None)
409
+ if transformer is None or not hasattr(transformer, "set_attention_backend"):
410
+ logger.warning(
411
+ f"--attention-backend is only supported on transformer-based pipelines; "
412
+ f"{type(pipeline).__name__} uses the legacy UNet attention path."
413
+ )
414
+ return
415
+ try:
416
+ transformer.set_attention_backend(backend)
417
+ except (ValueError, ImportError, RuntimeError) as e:
418
+ logger.warning(
419
+ f"Attention backend {backend!r} could not be set on {type(transformer).__name__}: "
420
+ f"{type(e).__name__}: {e}. Falling back to the model's default backend."
421
+ )
422
+
423
+
424
+ def _enable_context_parallel(pipeline: Any) -> None:
425
+ if not torch.distributed.is_available():
426
+ raise SystemExit("--context-parallel requires a torch build with distributed support.")
427
+
428
+ if not torch.distributed.is_initialized():
429
+ # Hybrid backend: ulysses_anything's per-rank size coordination wants Gloo on CPU
430
+ # (avoids H2D/D2H for a tiny int tensor); the main attention all-to-all stays on NCCL.
431
+ torch.distributed.init_process_group(backend="cpu:gloo,cuda:nccl")
432
+
433
+ transformer = getattr(pipeline, "transformer", None)
434
+ if transformer is None or not hasattr(transformer, "enable_parallelism"):
435
+ raise SystemExit(
436
+ "--context-parallel requires a DiT-based pipeline. "
437
+ f"{type(pipeline).__name__} does not expose a `transformer` with `enable_parallelism`."
438
+ )
439
+
440
+ transformer.enable_parallelism(
441
+ config=ContextParallelConfig(
442
+ ulysses_degree=torch.distributed.get_world_size(),
443
+ ring_degree=1,
444
+ ulysses_anything=True,
445
+ )
446
+ )
447
+
448
+
449
+ def _apply_optimizations(pipeline: Any, args: Namespace) -> None:
450
+ """Apply VAE tiling/slicing, attention backend, context-parallel, and torch.compile toggles."""
451
+ vae = getattr(pipeline, "vae", None)
452
+ if args.vae_tiling and vae is not None and hasattr(vae, "enable_tiling"):
453
+ vae.enable_tiling()
454
+ if args.vae_slicing and vae is not None and hasattr(vae, "enable_slicing"):
455
+ vae.enable_slicing()
456
+ if args.attention_backend != "default":
457
+ _set_attention_backend(pipeline, args.attention_backend)
458
+ if args.context_parallel:
459
+ _enable_context_parallel(pipeline)
460
+ if args.compile is not None:
461
+ if args.context_parallel:
462
+ logger.warning("--compile is currently not supported with --context-parallel; skipping compile.")
463
+ else:
464
+ _compile_denoiser(pipeline, args.compile)
465
+
466
+
467
+ def _compile_denoiser(pipeline: Any, compile_spec: str) -> None:
468
+ """Compile every `transformer*` and `unet*` submodule on the pipeline.
469
+
470
+ `compile_spec` is the raw JSON string from `--compile` (`"{}"` for bare flag). Decoded into kwargs and forwarded
471
+ verbatim to the compile call.
472
+
473
+ Prefers regional compilation via `module.compile_repeated_blocks(**kwargs)` — only compiles the repeated inner
474
+ blocks (the bulk of the compute), much faster first-step latency than compiling the whole module. Falls back to
475
+ full `torch.compile` if the model doesn't expose `_repeated_blocks`.
476
+ """
477
+
478
+ try:
479
+ compile_kwargs = json.loads(compile_spec)
480
+ except json.JSONDecodeError as e:
481
+ raise SystemExit(f"--compile must be valid JSON: {e}") from e
482
+ if not isinstance(compile_kwargs, dict):
483
+ raise SystemExit("--compile must decode to a JSON object.")
484
+
485
+ for attr in dir(pipeline):
486
+ if not any(attr.startswith(key) for key in _DENOISER_COMPONENT_KEYS):
487
+ continue
488
+ module = getattr(pipeline, attr, None)
489
+ if not isinstance(module, torch.nn.Module):
490
+ continue
491
+
492
+ if getattr(module, "_repeated_blocks", None):
493
+ # Regional compile — only the repeated blocks. Mutates `module` in place.
494
+ module.compile_repeated_blocks(**compile_kwargs)
495
+ else:
496
+ # No regional metadata declared; fall back to compiling the whole module.
497
+ setattr(pipeline, attr, torch.compile(module, **compile_kwargs))
498
+
499
+
500
+ def _load_lora(pipeline: Any, args: Namespace) -> None:
501
+ """Attach one or more LoRA adapters. Each `--lora` value is a JSON dict.
502
+
503
+ Per-entry fields: `lora_id` (required), `lora_scale` (optional float, default 1.0), `adapter_name` (optional;
504
+ auto-generated as `lora_<i>` when stacking), `weight_name` (optional; required when the repo ships more than one
505
+ weight file, e.g. a ComfyUI variant alongside the diffusers one). Multiple `--lora` flags stack via a single
506
+ `set_adapters(...)` call at the end.
507
+ """
508
+ if not args.lora:
509
+ return
510
+ specs = []
511
+ for raw in args.lora:
512
+ try:
513
+ parsed = json.loads(raw)
514
+ except json.JSONDecodeError as e:
515
+ raise SystemExit(f"--lora must be valid JSON: {e}") from e
516
+ if not isinstance(parsed, dict):
517
+ raise SystemExit(f"--lora must decode to a JSON object; got {type(parsed).__name__}.")
518
+ specs.append(parsed)
519
+ if not hasattr(pipeline, "load_lora_weights"):
520
+ raise SystemExit(f"{type(pipeline).__name__} does not support LoRA loading.")
521
+
522
+ names: list[str] = []
523
+ scales: list[float] = []
524
+ for i, spec in enumerate(specs):
525
+ lora_id = spec.get("lora_id")
526
+ if not lora_id:
527
+ raise SystemExit(f"--lora entry {i} is missing 'lora_id'.")
528
+ adapter_name = spec.get("adapter_name") or (f"lora_{i}" if len(specs) > 1 else "default")
529
+ pipeline.load_lora_weights(lora_id, adapter_name=adapter_name, weight_name=spec.get("weight_name", None))
530
+ names.append(adapter_name)
531
+ scales.append(float(spec.get("lora_scale", 1.0)))
532
+
533
+ if hasattr(pipeline, "set_adapters"):
534
+ pipeline.set_adapters(names, adapter_weights=scales)
535
+
536
+
537
+ def _load_pipeline(args: Namespace) -> Any:
538
+ # Detect modular repos by trying the standard config; `ModularPipeline` repos ship
539
+ # `modular_model_index.json` instead of `model_index.json`, so `load_config` OSErrors.
540
+ # A repo can also ship a `model_index.json` whose `_class_name` is a modular pipeline
541
+ # (e.g. MiniMax-H3's repository-level entry), so route on the class, not just the file.
542
+ try:
543
+ config = diffusers.DiffusionPipeline.load_config(args.model, token=args.token, revision=args.revision)
544
+ cls = getattr(diffusers, str(config.get("_class_name")), None)
545
+ modular = isinstance(cls, type) and issubclass(cls, diffusers.ModularPipeline)
546
+ except OSError:
547
+ modular = True
548
+
549
+ dtype = _resolve_dtype(args.dtype)
550
+ device_map = _resolve_device_map(args.device_map)
551
+ common_kwargs: dict[str, Any] = {
552
+ "trust_remote_code": args.trust_remote_code,
553
+ }
554
+ if dtype != "auto":
555
+ common_kwargs["torch_dtype"] = dtype
556
+ if args.variant:
557
+ common_kwargs["variant"] = args.variant
558
+ if args.token:
559
+ common_kwargs["token"] = args.token
560
+ # CPU offload sets up its own placement hooks, so leave weights on CPU at load time.
561
+ if not args.cpu_offload:
562
+ common_kwargs["device_map"] = device_map
563
+
564
+ if modular:
565
+ if args.cpu_offload and args.cpu_offload != "auto":
566
+ raise SystemExit(
567
+ f"--cpu-offload {args.cpu_offload!r} is not supported for modular pipelines — they "
568
+ "offload through a ComponentsManager. Use `--cpu-offload auto`."
569
+ )
570
+ components_manager = diffusers.ComponentsManager() if args.cpu_offload else None
571
+ # ModularPipeline.from_pretrained fetches only the pipeline config; component
572
+ # weights come in via load_components(). `revision` scopes the config fetch,
573
+ # so it stays on from_pretrained — each ComponentSpec pins its own revision,
574
+ # and forwarding a global `revision` to load_components() would override those.
575
+ pipeline = diffusers.ModularPipeline.from_pretrained(
576
+ args.model,
577
+ trust_remote_code=args.trust_remote_code,
578
+ token=args.token,
579
+ revision=args.revision,
580
+ components_manager=components_manager,
581
+ workflow=args.workflow,
582
+ )
583
+ pipeline.load_components(**common_kwargs)
584
+ else:
585
+ if args.workflow:
586
+ logger.warning(
587
+ f"--workflow {args.workflow!r} only applies to modular pipelines; "
588
+ f"ignoring it — '{args.model}' loads as a standard DiffusionPipeline."
589
+ )
590
+ pipeline = diffusers.DiffusionPipeline.from_pretrained(args.model, revision=args.revision, **common_kwargs)
591
+
592
+ _load_lora(pipeline, args)
593
+ if args.cpu_offload:
594
+ _apply_cpu_offload(pipeline, args.cpu_offload, device_map, args.offload_margin)
595
+ _apply_optimizations(pipeline, args)
596
+
597
+ return pipeline
598
+
599
+
600
+ # ---------------------------------------------------------------------------
601
+ # Pipeline call helpers
602
+ # ---------------------------------------------------------------------------
603
+
604
+
605
+ def _parse_pipeline_kwargs(raw: str | None) -> dict[str, Any]:
606
+ if not raw:
607
+ return {}
608
+ try:
609
+ parsed = json.loads(raw)
610
+ except json.JSONDecodeError as e:
611
+ raise SystemExit(f"--pipeline-kwargs must be valid JSON: {e}") from e
612
+ if not isinstance(parsed, dict):
613
+ raise SystemExit("--pipeline-kwargs must decode to a JSON object.")
614
+ return parsed
615
+
616
+
617
+ def _load_audio(url_or_path: str) -> tuple[Any, int]:
618
+ """Load audio from a URL or local path via torchaudio. Returns `(waveform, sampling_rate)`."""
619
+ import torchaudio
620
+
621
+ if url_or_path.startswith(("http://", "https://")):
622
+ resp = httpx.get(url_or_path, follow_redirects=True, timeout=DIFFUSERS_REQUEST_TIMEOUT)
623
+ resp.raise_for_status()
624
+ return torchaudio.load(io.BytesIO(resp.content))
625
+ return torchaudio.load(url_or_path)
626
+
627
+
628
+ def _resolve_media_inputs(call_kwargs: dict[str, Any]) -> None:
629
+ """Replace string paths/URLs at known media-input keys with loaded tensors.
630
+
631
+ Images resolve to `PIL.Image.Image` via `load_image`; videos to `list[PIL.Image.Image]` via `load_video`; audio to
632
+ a `torch.Tensor` via `_load_audio` (also auto-sets the paired sampling-rate kwarg for `initial_audio_waveforms` if
633
+ the user didn't supply it). A `list[str]` at any key is treated as a batch: each entry is loaded and the value
634
+ becomes a list of loaded objects. Non-string, non-list values pass through untouched.
635
+ """
636
+
637
+ def _is_string_list(v: Any) -> bool:
638
+ return isinstance(v, list) and bool(v) and all(isinstance(x, str) for x in v)
639
+
640
+ for key in _IMAGE_INPUT_KEYS:
641
+ value = call_kwargs.get(key)
642
+ if isinstance(value, str):
643
+ call_kwargs[key] = load_image(value)
644
+ elif _is_string_list(value):
645
+ call_kwargs[key] = [load_image(v) for v in value]
646
+ for key in _VIDEO_INPUT_KEYS:
647
+ value = call_kwargs.get(key)
648
+ if isinstance(value, str):
649
+ call_kwargs[key] = load_video(value)
650
+ elif _is_string_list(value):
651
+ call_kwargs[key] = [load_video(v) for v in value]
652
+ for key in _AUDIO_INPUT_KEYS:
653
+ value = call_kwargs.get(key)
654
+ if isinstance(value, str):
655
+ waveform, sr = _load_audio(value)
656
+ call_kwargs[key] = waveform
657
+ if key == "initial_audio_waveforms" and "initial_audio_sampling_rate" not in call_kwargs:
658
+ call_kwargs["initial_audio_sampling_rate"] = sr
659
+ elif _is_string_list(value):
660
+ pairs = [_load_audio(v) for v in value]
661
+ call_kwargs[key] = [w for w, _ in pairs]
662
+ if key == "initial_audio_waveforms" and "initial_audio_sampling_rate" not in call_kwargs:
663
+ # All batched waveforms must share a sampling rate; use the first entry's.
664
+ call_kwargs["initial_audio_sampling_rate"] = pairs[0][1]
665
+
666
+
667
+ def _get_generator(seed: int | None, device: str):
668
+ if seed is None:
669
+ return None
670
+
671
+ generator_device = "cpu" if device == "mps" else device
672
+ return torch.Generator(device=generator_device).manual_seed(seed)
673
+
674
+
675
+ def _unwrap_pipeline_output(result: Any) -> list[Any]:
676
+ """Resolve a pipeline-output object into the media payloads the saver dispatches on.
677
+
678
+ An output can carry more than one media field (e.g. LTX2 returns video in `frames` and a waveform in `audio`), so
679
+ every known field that is present is saved, not just the first match. Payloads keep their batch dimension —
680
+ `_save_output` dispatches on the full batched shape.
681
+ """
682
+ payloads = [
683
+ getattr(result, name)
684
+ for name in ("images", "frames", "audios", "audio")
685
+ if getattr(result, name, None) is not None
686
+ ]
687
+ return payloads or [result]
688
+
689
+
690
+ # ---------------------------------------------------------------------------
691
+ # Output saving (dispatch by type)
692
+ # ---------------------------------------------------------------------------
693
+
694
+
695
+ def _get_or_create_run_id() -> str:
696
+ """Return the current run's id, creating one if not yet set.
697
+
698
+ Format: `diffusers-run-<YYYYMMDDTHHMMSS>-<6-char-uuid>`. Same id is reused as the local output subdirectory, the
699
+ remote bucket prefix, and the container-side `RUN_ID_ENV` so a run's artifacts are traceable end-to-end.
700
+ """
701
+
702
+ existing = os.environ.get(RUN_ID_ENV)
703
+ if existing:
704
+ return existing
705
+ run_id = f"diffusers-run-{datetime.now().strftime('%Y%m%dT%H%M%S')}-{uuid.uuid4().hex[:6]}"
706
+ os.environ[RUN_ID_ENV] = run_id
707
+ return run_id
708
+
709
+
710
+ def _resolve_output_paths(num: int, explicit: str | None, ext: str) -> list[Path]:
711
+ if explicit is None:
712
+ base = Path(DEFAULT_OUTPUT_DIR) / _get_or_create_run_id()
713
+ base.mkdir(parents=True, exist_ok=True)
714
+ return [base / f"{i:04d}.{ext}" for i in range(num)]
715
+
716
+ p = Path(explicit)
717
+ if explicit.endswith(os.sep) or p.is_dir():
718
+ p.mkdir(parents=True, exist_ok=True)
719
+ return [p / f"{i:04d}.{ext}" for i in range(num)]
720
+
721
+ p.parent.mkdir(parents=True, exist_ok=True)
722
+ if num == 1:
723
+ return [p]
724
+ stem, suffix = p.stem, p.suffix or f".{ext}"
725
+ return [p.with_name(f"{stem}-{i:04d}{suffix}") for i in range(num)]
726
+
727
+
728
+ def _as_pil_list(value: Any):
729
+ if isinstance(value, Image.Image):
730
+ return [value]
731
+ if isinstance(value, (list, tuple)) and value and all(isinstance(v, Image.Image) for v in value):
732
+ return list(value)
733
+ return None
734
+
735
+
736
+ def _as_frame_sequence(value: Any):
737
+ if isinstance(value, (list, tuple)) and len(value) >= 2 and isinstance(value[0], (Image.Image, np.ndarray)):
738
+ return list(value)
739
+ return None
740
+
741
+
742
+ def _as_audio_arrays(value: Any):
743
+ if isinstance(value, np.ndarray) and value.ndim <= 2:
744
+ return [value]
745
+ if isinstance(value, np.ndarray) and value.ndim == 3:
746
+ return list(value)
747
+ if isinstance(value, (list, tuple)) and value and all(isinstance(v, np.ndarray) for v in value):
748
+ return list(value)
749
+ return None
750
+
751
+
752
+ def _save_audio_arrays(audios, sampling_rate: int, args: Namespace) -> list[str]:
753
+ """Write each numpy audio array to a 16-bit PCM WAV at `sampling_rate` Hz.
754
+
755
+ Uses the stdlib `wave` module so no scipy dependency is required.
756
+ """
757
+
758
+ paths = _resolve_output_paths(len(audios), args.output, ext="wav")
759
+ saved: list[str] = []
760
+ for audio, path in zip(audios, paths):
761
+ data = np.asarray(audio)
762
+ if data.dtype.kind == "f":
763
+ data = (np.clip(data, -1.0, 1.0) * 32767).astype(np.int16)
764
+ else:
765
+ data = data.astype(np.int16)
766
+ if data.ndim == 1:
767
+ n_channels = 1
768
+ else:
769
+ # Heuristic: shorter axis is channels (interleaved layout for `wave` is
770
+ # samples × channels, so transpose if needed).
771
+ if data.shape[0] < data.shape[-1]:
772
+ data = data.T
773
+ n_channels = data.shape[1]
774
+ with wave.open(str(path), "wb") as w:
775
+ w.setnchannels(n_channels)
776
+ w.setsampwidth(2) # 16-bit PCM
777
+ w.setframerate(sampling_rate)
778
+ w.writeframes(data.tobytes())
779
+ saved.append(str(path))
780
+ return saved
781
+
782
+
783
+ def _save_videos(videos: list[Any], args: Namespace) -> list[str]:
784
+ """Write each frame sequence to mp4, plus every frame as `<video-stem>-frames/<NNNN>.png` beside it.
785
+
786
+ The per-video subfolder ties each frame to its video and keeps paths unique across a batch. Frames are written
787
+ first and need no video backend, so they double as the safety net: if `export_to_video` fails (e.g. `imageio`
788
+ missing), the frames are already on disk and only the mp4 is skipped.
789
+ """
790
+ mp4_paths = _resolve_output_paths(len(videos), args.output, ext="mp4")
791
+ saved: list[str] = []
792
+ for frames, path in zip(videos, mp4_paths):
793
+ frames = list(frames)
794
+ frames_dir = path.with_name(f"{path.stem}-frames")
795
+ frames_dir.mkdir(parents=True, exist_ok=True)
796
+ for i, frame in enumerate(frames):
797
+ if not isinstance(frame, Image.Image):
798
+ arr = np.asarray(frame)
799
+ if arr.dtype != np.uint8:
800
+ arr = (np.clip(arr, 0.0, 1.0) * 255).round().astype(np.uint8)
801
+ frame = Image.fromarray(arr)
802
+ frame_path = frames_dir / f"{i:04d}.png"
803
+ frame.save(frame_path)
804
+ saved.append(str(frame_path))
805
+ try:
806
+ export_to_video(frames, str(path), fps=args.fps)
807
+ saved.append(str(path))
808
+ except Exception as e:
809
+ logger.warning(
810
+ f"Video export failed ({e}); the individual frames of {path.stem} are saved next to it as PNGs. "
811
+ "Install a video backend with: pip install imageio imageio-ffmpeg"
812
+ )
813
+ return saved
814
+
815
+
816
+ def _save_output(value: Any, args: Namespace) -> list[str]:
817
+ """Save `value` by dispatching on its runtime type and, for arrays, its shape."""
818
+ # Tensors arrive only when the user explicitly asked for `output_type="pt"` (or the pipeline
819
+ # natively defaults to it, e.g. StableAudio). Postprocessed pt outputs are channels-first per
820
+ # frame — (B, C, H, W) images, (B, F, C, H, W) video from `postprocess_video` — while the array
821
+ # branches below expect channels-last, so convert here.
822
+ if isinstance(value, torch.Tensor):
823
+ arr = value.detach().to(torch.float32).cpu().numpy()
824
+ if arr.ndim == 5:
825
+ arr = arr.transpose(0, 1, 3, 4, 2)
826
+ elif arr.ndim == 4:
827
+ arr = arr.transpose(0, 2, 3, 1)
828
+ value = arr
829
+
830
+ # Array shapes are unambiguous where PIL lists are not: (B, F, H, W, C) is batched video,
831
+ # (B, H, W, C) is batched images.
832
+ if isinstance(value, np.ndarray):
833
+ if value.ndim == 5:
834
+ return _save_videos(list(value), args)
835
+ if value.ndim == 4:
836
+ paths = _resolve_output_paths(len(value), args.output, ext="png")
837
+ for arr, path in zip(value, paths):
838
+ if arr.dtype != np.uint8:
839
+ arr = (np.clip(arr, 0.0, 1.0) * 255).round().astype(np.uint8)
840
+ Image.fromarray(arr).save(path)
841
+ return [str(p) for p in paths]
842
+
843
+ pil_images = _as_pil_list(value)
844
+ if pil_images is not None:
845
+ paths = _resolve_output_paths(len(pil_images), args.output, ext="png")
846
+ for img, path in zip(pil_images, paths):
847
+ img.save(path)
848
+ return [str(p) for p in paths]
849
+
850
+ frames = _as_frame_sequence(value)
851
+ if frames is not None:
852
+ return _save_videos([frames], args)
853
+
854
+ # A batch of PIL frame sequences — what video pipelines return for an explicit
855
+ # `output_type="pil"`. Previously this matched no branch and fell through to the JSON dump.
856
+ if isinstance(value, (list, tuple)) and value and all(_as_frame_sequence(v) is not None for v in value):
857
+ return _save_videos([list(v) for v in value], args)
858
+
859
+ audios = _as_audio_arrays(value)
860
+ if audios is not None:
861
+ return _save_audio_arrays(audios, args.sampling_rate or 16000, args)
862
+
863
+ raise ValueError(
864
+ f"Cannot save pipeline output of type {type(value).__name__!r}: not a recognized image, video, or audio "
865
+ "payload. For modular pipelines, name the media outputs to save with `--output-key` (repeat it for "
866
+ "several, e.g. `--output-key videos --output-key audio`); non-media values like a sample rate are not "
867
+ "saved and are set with their own flag (`--sampling-rate`)."
868
+ )
869
+
870
+
871
+ # ---------------------------------------------------------------------------
872
+ # Hub bucket upload (--push-to)
873
+ # ---------------------------------------------------------------------------
874
+
875
+
876
+ def _parse_push_to(spec: str) -> tuple[str, str]:
877
+ """Split `--push-to` into a bucket id and an optional subpath prefix.
878
+
879
+ Accepts an HF bucket id (`<namespace>/<name>[/<subpath>]`), a canonical
880
+ `hf://buckets/<namespace>/<name>[/<subpath>]` URI, or a Hub web URL for the same. Non-bucket URIs (models,
881
+ datasets, spaces) are rejected — `--push-to` targets storage buckets only.
882
+ """
883
+
884
+ # Bare shorthand → canonical URI so a single parser handles every accepted form.
885
+ if not spec.startswith(("hf://", "http://", "https://")):
886
+ spec = f"hf://buckets/{spec.strip('/')}"
887
+ uri = parse_hf_uri(spec)
888
+ if not uri.is_bucket:
889
+ raise SystemExit(f"--push-to must point at a bucket; got {uri.type!r} URI {spec!r}.")
890
+ return uri.id, uri.path_in_repo
891
+
892
+
893
+ def _collapse_frame_dirs(paths: list[str]) -> list[str]:
894
+ """Replace each `<stem>-frames/` group with its directory path for reporting.
895
+
896
+ A video's frames are hundreds of files; listing them all buries the mp4 and floods the terminal scrollback. Entries
897
+ stay real paths, so the reported list is still usable programmatically. The files themselves are untouched — only
898
+ what is reported changes.
899
+ """
900
+ collapsed: list[str] = []
901
+ for path in paths:
902
+ parent = path.rsplit("/", 1)[0] if "/" in path else ""
903
+ if not parent.endswith("-frames"):
904
+ collapsed.append(path)
905
+ elif parent not in collapsed:
906
+ collapsed.append(parent)
907
+ return collapsed
908
+
909
+
910
+ def _push_outputs(args: Namespace, saved_paths: list[str]) -> dict[str, Any] | None:
911
+ """Upload `saved_paths` to the `--push-to` bucket. Returns a summary or None."""
912
+ if not args.push_to:
913
+ return None
914
+
915
+ bucket_id, subpath = _parse_push_to(args.push_to)
916
+ api = HfApi(token=args.token)
917
+ api.create_bucket(bucket_id, exist_ok=True)
918
+
919
+ run_id = _get_or_create_run_id()
920
+ prefix = f"{subpath}/{run_id}" if subpath else run_id
921
+ # Destination paths keep their structure relative to the common output dir, so files in
922
+ # subfolders (e.g. `0000-frames/0003.png`) don't collide on basename in the bucket.
923
+ base = Path(os.path.commonpath([str(Path(p).parent) for p in saved_paths]))
924
+ add = [(local, f"{prefix}/{Path(local).relative_to(base).as_posix()}") for local in saved_paths]
925
+ api.batch_bucket_files(bucket_id, add=add)
926
+
927
+ uploaded = _collapse_frame_dirs([f"hf://buckets/{bucket_id}/{dest}" for _, dest in add])
928
+ return {"bucket_id": bucket_id, "uploaded": uploaded}
929
+
930
+
931
+ # ---------------------------------------------------------------------------
932
+ # Remote execution (HF Sandbox)
933
+ # ---------------------------------------------------------------------------
934
+
935
+
936
+ def _build_task_kwargs(args: Namespace) -> dict[str, Any]:
937
+ """Pick out the kwargs the sandbox CLI should invoke the task with."""
938
+ out: dict[str, Any] = {}
939
+ for key, value in vars(args).items():
940
+ if key in REMOTE_KEYS or value is None or value is False:
941
+ continue
942
+ out[key] = value
943
+ return out
944
+
945
+
946
+ def _kwargs_to_argv(task: str, task_kwargs: dict[str, Any]) -> list[str]:
947
+ """Render `task_kwargs` as the argv list the sandbox CLI's argparse will see."""
948
+ argv: list[str] = [task]
949
+ for key, value in task_kwargs.items():
950
+ flag = "--" + key.replace("_", "-")
951
+ if value is True:
952
+ argv.append(flag)
953
+ elif isinstance(value, list):
954
+ for item in value:
955
+ argv.extend([flag, str(item)])
956
+ else:
957
+ argv.extend([flag, str(value)])
958
+ return argv
959
+
960
+
961
+ def _duration_to_seconds(value: str) -> float:
962
+ """Parse a duration like `30s`, `10m`, `2h` (or a bare number of seconds) into seconds."""
963
+ value = value.strip()
964
+ units = {"s": 1, "m": 60, "h": 3600}
965
+ if value and value[-1] in units:
966
+ return float(value[:-1]) * units[value[-1]]
967
+ return float(value)
968
+
969
+
970
+ def _upload_inputs_to_sandbox(args: Namespace, sbx: Any, run_id: str) -> None:
971
+ """Upload local media paths in `--pipeline-kwargs` into the sandbox and rewrite the JSON in place.
972
+
973
+ Walks known image/video/audio-input keys; any string value that resolves to a local file is uploaded to
974
+ `<_SANDBOX_INPUTS_DIR>/<run_id>/<key>_<basename>` and the JSON path is rewritten to that in-sandbox path. URLs,
975
+ `hf://` URIs, and non-existent paths pass through untouched.
976
+ """
977
+ if not args.pipeline_kwargs:
978
+ return
979
+ try:
980
+ parsed = json.loads(args.pipeline_kwargs)
981
+ except json.JSONDecodeError:
982
+ return # the sandbox CLI will fail loudly with a parse error later
983
+ if not isinstance(parsed, dict):
984
+ return
985
+
986
+ def _upload_one(key: str, index: int | None, local_str: str) -> str:
987
+ # `index` is None for scalar entries, an int for list entries (used to disambiguate names).
988
+ local = Path(local_str)
989
+ suffix = f"_{index}" if index is not None else ""
990
+ remote_path = f"{_SANDBOX_INPUTS_DIR}/{run_id}/{key}{suffix}_{local.name}"
991
+ sbx.files.upload(str(local), remote_path)
992
+ return remote_path
993
+
994
+ uploaded = 0
995
+ for key in (*_IMAGE_INPUT_KEYS, *_VIDEO_INPUT_KEYS, *_AUDIO_INPUT_KEYS):
996
+ value = parsed.get(key)
997
+ if isinstance(value, str) and Path(value).is_file():
998
+ parsed[key] = _upload_one(key, None, value)
999
+ uploaded += 1
1000
+ elif isinstance(value, list):
1001
+ # Batched inputs: upload each local path, leave URLs/hf:// URIs alone.
1002
+ new_list = list(value)
1003
+ for i, entry in enumerate(value):
1004
+ if isinstance(entry, str) and Path(entry).is_file():
1005
+ new_list[i] = _upload_one(key, i, entry)
1006
+ uploaded += 1
1007
+ parsed[key] = new_list
1008
+
1009
+ if uploaded:
1010
+ logger.info(f"uploaded {uploaded} local input file(s) to the sandbox")
1011
+ args.pipeline_kwargs = json.dumps(parsed)
1012
+
1013
+
1014
+ def _download_outputs_from_sandbox(sbx: Any, sandbox_dir: str, local_dir: Path) -> list[str]:
1015
+ """Download everything the sandbox CLI wrote under `sandbox_dir` into `local_dir`, recursing into subfolders."""
1016
+ local_dir.mkdir(parents=True, exist_ok=True)
1017
+ saved: list[str] = []
1018
+ for entry in sbx.files.list(sandbox_dir):
1019
+ if entry.type == "dir":
1020
+ saved.extend(_download_outputs_from_sandbox(sbx, entry.path, local_dir / Path(entry.path).name))
1021
+ elif entry.type == "file":
1022
+ target = local_dir / Path(entry.path).name
1023
+ sbx.files.download(entry.path, str(target))
1024
+ saved.append(str(target))
1025
+ return saved
1026
+
1027
+
1028
+ def _maybe_submit_remote(args: Namespace, task: str) -> bool:
1029
+ """If `--remote` was set, run this invocation inside an HF Sandbox and return True."""
1030
+ if not args.remote:
1031
+ return False
1032
+
1033
+ if Path(args.model).exists():
1034
+ raise SystemExit(
1035
+ f"--model {args.model!r} is a local path; the sandbox can't see it. "
1036
+ "Pass a Hub repo id so the sandbox can download it."
1037
+ )
1038
+
1039
+ hf_token = args.token or get_token()
1040
+ run_id = _get_or_create_run_id()
1041
+
1042
+ # An explicit --push-to means the bucket is the user's destination, so skip the local
1043
+ # download unless they also asked for a local path via --output.
1044
+ user_bucket = bool(args.push_to)
1045
+ download_locally = (not user_bucket) or (args.output is not None)
1046
+ local_dir = Path(args.output) if args.output else Path(DEFAULT_OUTPUT_DIR) / run_id
1047
+
1048
+ use_existing_sandbox = bool(args.sandbox_id)
1049
+ keep_alive = args.keep_alive or use_existing_sandbox
1050
+ if use_existing_sandbox and args.volume:
1051
+ logger.warning(
1052
+ "--volume is ignored when reconnecting to an existing sandbox (mounts are set at creation time)."
1053
+ )
1054
+ if use_existing_sandbox:
1055
+ logger.info(f"reconnecting to sandbox {args.sandbox_id!r}...")
1056
+ sbx = Sandbox.connect(args.sandbox_id, token=hf_token)
1057
+ else:
1058
+ logger.info(f"creating sandbox on flavor={args.flavor!r}...")
1059
+ create_kwargs: dict[str, Any] = {
1060
+ "image": args.image or _DEFAULT_REMOTE_IMAGE,
1061
+ "flavor": args.flavor,
1062
+ "forward_hf_token": True,
1063
+ "token": hf_token,
1064
+ "env": {
1065
+ "HF_ENABLE_PARALLEL_LOADING": "1",
1066
+ "DIFFUSERS_VERBOSITY": os.environ.get("DIFFUSERS_VERBOSITY", "info"),
1067
+ },
1068
+ "idle_timeout": args.idle_timeout,
1069
+ # The 120s default expires while a cold node pulls the multi-GB pytorch base image or
1070
+ # waits for GPU capacity, which fails the run before it starts.
1071
+ "start_timeout": 600.0,
1072
+ }
1073
+ if args.volume:
1074
+ volumes = []
1075
+ for spec in args.volume:
1076
+ bucket_id, sep, mount_path = spec.partition(":")
1077
+ if not sep:
1078
+ mount_path = f"/mnt/buckets/{bucket_id}"
1079
+ if bucket_id.count("/") != 1:
1080
+ raise SystemExit(f"--volume: bucket id must be <namespace>/<name>, got {bucket_id!r}")
1081
+ if not mount_path.startswith("/"):
1082
+ raise SystemExit(f"--volume: mount path must be absolute, got {mount_path!r}")
1083
+ volumes.append(Volume(type="bucket", source=bucket_id, mount_path=mount_path))
1084
+ create_kwargs["volumes"] = volumes
1085
+ if args.namespace is not None:
1086
+ create_kwargs["namespace"] = args.namespace
1087
+ sbx = Sandbox.create(**create_kwargs)
1088
+
1089
+ def _stream(chunk: str) -> None:
1090
+ sys.stderr.write(chunk)
1091
+ sys.stderr.flush()
1092
+
1093
+ exit_code = 0
1094
+ saved: list[str] = []
1095
+ run_seconds = 0.0
1096
+ try:
1097
+ _upload_inputs_to_sandbox(args, sbx, run_id)
1098
+
1099
+ dependencies = list(_DEFAULT_REMOTE_DEPS)
1100
+ if args.dependencies:
1101
+ dependencies.extend(args.dependencies)
1102
+ # --break-system-packages bypasses PEP 668; harmless in a throwaway sandbox. uv is a
1103
+ # near no-op when the deps are already satisfied, so this stays cheap on a reused sandbox.
1104
+ install_cmd = shlex.join(["uv", "pip", "install", "--system", "--break-system-packages", *dependencies])
1105
+ logger.info("installing dependencies in the sandbox...")
1106
+ sbx.run(install_cmd, on_stdout=_stream, on_stderr=_stream)
1107
+
1108
+ # Per-run outputs subdirectory so a reused sandbox doesn't leak files from prior runs
1109
+ # into this run's download set.
1110
+ sandbox_output_dir = f"{_SANDBOX_OUTPUTS_DIR}/{run_id}"
1111
+ task_kwargs = _build_task_kwargs(args)
1112
+ task_kwargs["output"] = sandbox_output_dir + "/"
1113
+ cli_argv = _kwargs_to_argv(task, task_kwargs)
1114
+ # Suppress the container CLI's own `out.result(...)` payload — the outer wrapper owns the
1115
+ # final structured output for --remote runs.
1116
+ format_argv = ["--format", "quiet"]
1117
+ # torchrun wraps the CLI for --context-parallel so torch.distributed initializes across
1118
+ # every visible GPU before the run command starts.
1119
+ if args.context_parallel:
1120
+ cli_argv = [
1121
+ "torchrun",
1122
+ "--nproc-per-node=gpu",
1123
+ "-m",
1124
+ "diffusers.commands.diffusers_cli",
1125
+ *format_argv,
1126
+ *cli_argv,
1127
+ ]
1128
+ else:
1129
+ cli_argv = [_CONTAINER_CLI_BINARY, *format_argv, *cli_argv]
1130
+
1131
+ started = time.perf_counter()
1132
+ # Per-invocation env: RUN_ID_ENV must be fresh each run. Sandbox.create-time env is
1133
+ # baked in and would go stale on reused sandboxes, silently reusing the initial run's
1134
+ # bucket prefix in `_push_outputs`.
1135
+ result = sbx.run(
1136
+ cli_argv,
1137
+ env={RUN_ID_ENV: run_id},
1138
+ on_stdout=_stream,
1139
+ on_stderr=_stream,
1140
+ timeout=_duration_to_seconds(args.timeout),
1141
+ check=False,
1142
+ )
1143
+ run_seconds = time.perf_counter() - started
1144
+ exit_code = result.exit_code
1145
+
1146
+ if exit_code == 0 and download_locally:
1147
+ saved = _download_outputs_from_sandbox(sbx, sandbox_output_dir, local_dir)
1148
+ finally:
1149
+ if keep_alive:
1150
+ logger.info(
1151
+ f"sandbox {sbx.id} kept alive — reconnect with "
1152
+ f"`--remote --sandbox-id {sbx.id}`, stop with `hf sandbox kill {sbx.id}`."
1153
+ )
1154
+ else:
1155
+ sbx.kill()
1156
+
1157
+ send_telemetry(
1158
+ topic="diffusers/cli/run/remote",
1159
+ library_name="diffusers",
1160
+ library_version=diffusers.__version__,
1161
+ )
1162
+
1163
+ payload: dict[str, Any] = {
1164
+ "exit_code": exit_code,
1165
+ "run_seconds": round(run_seconds, 1),
1166
+ }
1167
+ if keep_alive:
1168
+ payload["sandbox_id"] = sbx.id
1169
+ if download_locally:
1170
+ payload["outputs"] = _collapse_frame_dirs(saved)
1171
+ if args.push_to:
1172
+ bucket_id, subpath = _parse_push_to(args.push_to)
1173
+ prefix = f"{subpath}/{run_id}" if subpath else run_id
1174
+ payload["pushed-to"] = f"hf://buckets/{bucket_id}/{prefix}/"
1175
+ out.result("remote-run", **payload)
1176
+
1177
+ if exit_code != 0:
1178
+ raise SystemExit(f"remote run failed with exit code {exit_code}")
1179
+ return True
1180
+
1181
+
1182
+ # ---------------------------------------------------------------------------
1183
+ # Subcommand
1184
+ # ---------------------------------------------------------------------------
1185
+
1186
+
1187
+ class RunCommand(BaseDiffusersCLICommand):
1188
+ task = "run"
1189
+
1190
+ @staticmethod
1191
+ def register_subcommand(subparsers: _SubParsersAction) -> None:
1192
+ epilog = (
1193
+ "Examples\n"
1194
+ " $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
1195
+ ' --pipeline-kwargs \'{"prompt": "a cat on the moon"}\'\n'
1196
+ " $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
1197
+ ' --pipeline-kwargs \'{"prompt": "make the fur grey", "image": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png"}\'\n'
1198
+ " $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
1199
+ ' --pipeline-kwargs \'{"prompt": "a tiny cat"}\' \\\n'
1200
+ ' --lora \'{"lora_id": "alvdansen/littletinies", "lora_scale": 0.8}\'\n'
1201
+ " $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 \\\n"
1202
+ ' --pipeline-kwargs \'{"prompt": "a cat"}\' --remote --flavor a100-large\n'
1203
+ " $ diffusers-cli run -m black-forest-labs/FLUX.1-dev --dtype bf16 --context-parallel \\\n"
1204
+ ' --pipeline-kwargs \'{"prompt": "a cat"}\' --remote --flavor 4xa100-large\n'
1205
+ "\n"
1206
+ "Learn more\n"
1207
+ " Use `diffusers-cli <command> --help` for more information about a command.\n"
1208
+ " Read the documentation at https://huggingface.co/docs/diffusers\n"
1209
+ )
1210
+
1211
+ parser: ArgumentParser = subparsers.add_parser(
1212
+ "run",
1213
+ help="Run any diffusers pipeline locally or remotely in an HF Sandbox.",
1214
+ usage="\n diffusers-cli run [options]",
1215
+ epilog=epilog,
1216
+ formatter_class=RawDescriptionHelpFormatter,
1217
+ )
1218
+ parser._optionals.title = "Options"
1219
+ _add_loading_arguments(parser)
1220
+ _add_optimization_arguments(parser)
1221
+ parser.add_argument(
1222
+ "--pipeline-kwargs",
1223
+ default=None,
1224
+ help=(
1225
+ "JSON object of kwargs passed to the pipeline call. String values at known "
1226
+ f"image-input keys ({', '.join(_IMAGE_INPUT_KEYS)}) are auto-loaded as PIL images; "
1227
+ f"video-input keys ({', '.join(_VIDEO_INPUT_KEYS)}) are auto-loaded as frame lists; "
1228
+ f"audio-input keys ({', '.join(_AUDIO_INPUT_KEYS)}) are auto-loaded via torchaudio."
1229
+ ),
1230
+ )
1231
+ parser.add_argument(
1232
+ "--output-key",
1233
+ action="append",
1234
+ default=None,
1235
+ metavar="NAME",
1236
+ help=(
1237
+ "For modular pipelines: name of the intermediate to extract (passed as `output=` to the call). "
1238
+ "Repeat to request several, e.g. `--output-key videos --output-key audio`; each is saved by its "
1239
+ "own media type."
1240
+ ),
1241
+ )
1242
+ parser.add_argument(
1243
+ "--workflow",
1244
+ default=None,
1245
+ help=(
1246
+ "For modular pipelines: workflow to load (passed to `ModularPipeline.from_pretrained`). "
1247
+ "Prunes the blocks to that workflow so `load_components` only fetches its components."
1248
+ ),
1249
+ )
1250
+ parser.add_argument(
1251
+ "--offload-margin",
1252
+ default=None,
1253
+ help=(
1254
+ "For `--cpu-offload auto`: device memory kept free for activations "
1255
+ "(passed to `ComponentsManager.enable_auto_cpu_offload` as `memory_reserve_margin`, "
1256
+ "default 3GB). Raise it when a large canvas OOMs mid-forward."
1257
+ ),
1258
+ )
1259
+ parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility.")
1260
+ parser.add_argument(
1261
+ "--fps",
1262
+ type=int,
1263
+ default=8,
1264
+ help="FPS used when the output happens to be a frame sequence.",
1265
+ )
1266
+ parser.add_argument(
1267
+ "--sampling-rate",
1268
+ type=int,
1269
+ default=None,
1270
+ help="Sample rate used when the output happens to be an audio array.",
1271
+ )
1272
+ _add_remote_arguments(parser)
1273
+ _add_output_arguments(parser)
1274
+ parser.set_defaults(func=RunCommand)
1275
+
1276
+ def __init__(self, args: Namespace):
1277
+ self.args = args
1278
+
1279
+ def run(self) -> None:
1280
+ _get_or_create_run_id() # populate RUN_ID_ENV so local output dir + remote bucket prefix agree
1281
+
1282
+ call_kwargs = _parse_pipeline_kwargs(self.args.pipeline_kwargs)
1283
+
1284
+ if _maybe_submit_remote(self.args, self.task):
1285
+ return
1286
+
1287
+ # Resolve media before loading pipeline weights so dead URLs / missing files fail
1288
+ # fast — cheap to fetch, expensive to load a 20GB model just to hit a 404.
1289
+ _resolve_media_inputs(call_kwargs)
1290
+ pipeline = _load_pipeline(self.args)
1291
+ is_modular = isinstance(pipeline, diffusers.ModularPipeline)
1292
+
1293
+ if self.args.output_key is not None:
1294
+ # One key returns that value directly; several return a dict keyed by name.
1295
+ keys = self.args.output_key
1296
+ call_kwargs["output"] = keys[0] if len(keys) == 1 else keys
1297
+
1298
+ device = pipeline.device.type if hasattr(pipeline, "device") else "cpu"
1299
+ generator = _get_generator(self.args.seed, device)
1300
+ if generator is not None:
1301
+ call_kwargs["generator"] = generator
1302
+
1303
+ try:
1304
+ result = pipeline(**call_kwargs)
1305
+
1306
+ # Under torchrun, ranks > 0 produce identical output to rank 0 (CP shards the
1307
+ # transformer compute but ranks reduce to the same final tensors). Save/push/print
1308
+ # from rank 0 only to avoid clobbering bucket files 4x and printing 4x.
1309
+ if os.environ.get("RANK", "0") == "0":
1310
+ if not is_modular:
1311
+ savables = _unwrap_pipeline_output(result)
1312
+ elif isinstance(result, dict):
1313
+ # Several `--output-key`s: one payload per requested name.
1314
+ savables = list(result.values())
1315
+ else:
1316
+ savables = [result]
1317
+ saved = []
1318
+ for savable in savables:
1319
+ saved.extend(_save_output(savable, self.args))
1320
+ pushed = _push_outputs(self.args, saved)
1321
+
1322
+ out.result(
1323
+ self.task,
1324
+ model=self.args.model,
1325
+ device=device,
1326
+ pipeline_class=type(pipeline).__name__,
1327
+ modular=is_modular,
1328
+ outputs=_collapse_frame_dirs(saved),
1329
+ pushed=pushed,
1330
+ seed=self.args.seed,
1331
+ output_key=self.args.output_key,
1332
+ )
1333
+ finally:
1334
+ if torch.distributed.is_available() and torch.distributed.is_initialized():
1335
+ torch.distributed.destroy_process_group()
diffusers/commands/schema.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """`diffusers-cli schema` — print the input schema for any pipeline repo.
16
+
17
+ Tries `DiffusionPipeline.config_name` first (so standard repos get their `__call__` signature introspected); falls back
18
+ to `ModularPipelineBlocks.from_pretrained` for modular repos. No weights are downloaded — only the small index file
19
+ (and any custom block code if `--trust-remote-code` is set).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import inspect
25
+ import re
26
+ from argparse import ArgumentParser, Namespace, _SubParsersAction
27
+ from typing import Any
28
+
29
+ from huggingface_hub.cli._output import OutputFormat, out
30
+
31
+ from ..utils import logging
32
+ from . import BaseDiffusersCLICommand
33
+
34
+
35
+ logger = logging.get_logger("diffusers-cli/schema")
36
+
37
+
38
+ def _schema(args: Namespace) -> None:
39
+ """Print the pipeline's input schema.
40
+
41
+ Tries `DiffusionPipeline.config_name` (= `model_index.json`) first; if present, introspects the declared pipeline
42
+ class's `__call__` signature. Otherwise falls back to `ModularPipelineBlocks.from_pretrained` and reads the
43
+ block-declared `inputs`. No weights downloaded either way.
44
+ """
45
+ import diffusers
46
+
47
+ try:
48
+ index = diffusers.DiffusionPipeline.load_config(args.model, token=args.token, revision=args.revision)
49
+ except OSError:
50
+ index = None
51
+
52
+ if index is not None:
53
+ class_name = index.get("_class_name")
54
+ if class_name is None:
55
+ raise SystemExit(
56
+ f"{diffusers.DiffusionPipeline.config_name} for {args.model!r} has no `_class_name` field."
57
+ )
58
+ pipeline_cls = getattr(diffusers, class_name, None)
59
+ if pipeline_cls is None:
60
+ raise SystemExit(
61
+ f"Pipeline class {class_name!r} declared in {diffusers.DiffusionPipeline.config_name} "
62
+ "is not exported by the installed diffusers."
63
+ )
64
+
65
+ sig = inspect.signature(pipeline_cls.__call__)
66
+ descriptions = _parse_docstring_args(pipeline_cls.__call__.__doc__) if args.verbose else {}
67
+ schema: list[dict[str, Any]] = []
68
+ for name, param in sig.parameters.items():
69
+ if name == "self":
70
+ continue
71
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
72
+ continue
73
+ has_default = param.default is not inspect.Parameter.empty
74
+ schema.append(
75
+ {
76
+ "name": name,
77
+ "type_hint": str(param.annotation) if param.annotation is not inspect.Parameter.empty else None,
78
+ "default": param.default if has_default else None,
79
+ "required": not has_default,
80
+ "description": descriptions.get(name, ""),
81
+ }
82
+ )
83
+ else:
84
+ kwargs: dict[str, Any] = {"trust_remote_code": args.trust_remote_code}
85
+ if args.revision:
86
+ kwargs["revision"] = args.revision
87
+ if args.token:
88
+ kwargs["token"] = args.token
89
+
90
+ # If the repo declares custom code + external dependencies, surface them upfront so
91
+ # the user knows what to install before we hit an ImportError inside from_pretrained.
92
+ _warn_custom_block_requirements(args)
93
+
94
+ try:
95
+ blocks = diffusers.ModularPipelineBlocks.from_pretrained(args.model, **kwargs)
96
+ except Exception as e:
97
+ hint = "\nPass --trust-remote-code if it ships custom block code." if not args.trust_remote_code else ""
98
+ raise SystemExit(
99
+ f"Could not read schema for {args.model!r}: no {diffusers.DiffusionPipeline.config_name} and "
100
+ f"loading as a modular pipeline failed with:\n {type(e).__name__}: {e}{hint}"
101
+ ) from e
102
+
103
+ class_name = type(blocks).__name__
104
+ schema = [
105
+ {
106
+ "name": p.name,
107
+ "type_hint": str(p.type_hint) if p.type_hint is not None else None,
108
+ "default": p.default,
109
+ "required": p.required,
110
+ "description": p.description,
111
+ }
112
+ for p in blocks.inputs
113
+ ]
114
+
115
+ if out.mode == OutputFormat.json:
116
+ out.dict({"task": "schema", "model": args.model, "pipeline_class": class_name, "inputs": schema})
117
+ elif out.mode == OutputFormat.agent:
118
+ out.table(schema, headers=["name", "required", "type_hint", "default", "description"])
119
+ else:
120
+ out.text(f"{class_name} ({args.model}) inputs:")
121
+ for entry in schema:
122
+ tag = "required" if entry["required"] else f"optional, default={entry['default']!r}"
123
+ out.text(f" {entry['name']} ({tag})")
124
+ if entry["type_hint"]:
125
+ out.text(f" type: {entry['type_hint']}")
126
+ if entry["description"]:
127
+ out.text(f" desc: {entry['description']}")
128
+
129
+
130
+ def _warn_custom_block_requirements(args: Namespace) -> None:
131
+ """Warn upfront when a modular block ships custom code with declared external dependencies.
132
+
133
+ Reads `modular_config.json` if present; if it has an `auto_map` (custom code) and a non-empty `requirements`
134
+ list/dict, prints a heads-up. `from_pretrained` will otherwise fail with an `ImportError` deep in the loader stack
135
+ when a listed dep is missing.
136
+ """
137
+ import diffusers
138
+
139
+ try:
140
+ config = diffusers.ModularPipelineBlocks.load_config(args.model, token=args.token, revision=args.revision)
141
+ except Exception:
142
+ return # no modular_config.json or unreachable — nothing to warn about
143
+ if not isinstance(config, dict):
144
+ return
145
+ if not config.get("auto_map"):
146
+ return
147
+ requirements = config.get("requirements")
148
+ if not requirements:
149
+ return
150
+
151
+ # `requirements` may be a dict {name: version} or (older repos) a list of [name, version] pairs.
152
+ if isinstance(requirements, dict):
153
+ pairs = list(requirements.items())
154
+ elif isinstance(requirements, list):
155
+ pairs = [(item[0], item[1]) for item in requirements if isinstance(item, (list, tuple)) and len(item) >= 2]
156
+ else:
157
+ pairs = []
158
+ if not pairs:
159
+ return
160
+
161
+ formatted = ", ".join(f"{name}=={version}" for name, version in pairs)
162
+ logger.warning(
163
+ f"{args.model!r} ships custom block code with external dependencies: {formatted}. "
164
+ "You will need to install these in order to determine the pipeline schema."
165
+ )
166
+
167
+
168
+ def _parse_docstring_args(docstring: str | None) -> dict[str, str]:
169
+ """Extract per-argument descriptions from a Google-style `Args:` block.
170
+
171
+ Returns a `{name: description}` mapping. Best-effort — unrecognised formats just yield an empty dict rather than
172
+ raising.
173
+ """
174
+ if not docstring:
175
+ return {}
176
+
177
+ lines = docstring.expandtabs().splitlines()
178
+ start = None
179
+ section_indent = 0
180
+ for i, line in enumerate(lines):
181
+ if line.strip() in ("Args:", "Arguments:", "Parameters:"):
182
+ start = i + 1
183
+ section_indent = len(line) - len(line.lstrip())
184
+ break
185
+ if start is None:
186
+ return {}
187
+
188
+ descriptions: dict[str, str] = {}
189
+ current_name: str | None = None
190
+ current_lines: list[str] = []
191
+ arg_indent: int | None = None
192
+ name_pattern = re.compile(r"^(\w+)\s*(?:\([^)]*\))?\s*:?\s*(.*)$")
193
+
194
+ def _flush() -> None:
195
+ if current_name and current_lines:
196
+ descriptions[current_name] = " ".join(s.strip() for s in current_lines).strip()
197
+
198
+ for line in lines[start:]:
199
+ if not line.strip():
200
+ continue
201
+ indent = len(line) - len(line.lstrip())
202
+ # A new top-level section ends the Args block.
203
+ if indent <= section_indent and line.strip().endswith(":"):
204
+ break
205
+ if arg_indent is None:
206
+ arg_indent = indent
207
+ if indent == arg_indent:
208
+ _flush()
209
+ current_lines = []
210
+ match = name_pattern.match(line.strip())
211
+ if match:
212
+ current_name = match.group(1)
213
+ tail = match.group(2).strip()
214
+ if tail:
215
+ current_lines.append(tail)
216
+ else:
217
+ current_name = None
218
+ elif current_name is not None and indent > arg_indent:
219
+ current_lines.append(line.strip())
220
+ _flush()
221
+ return descriptions
222
+
223
+
224
+ class SchemaCommand(BaseDiffusersCLICommand):
225
+ task = "schema"
226
+
227
+ @staticmethod
228
+ def register_subcommand(subparsers: _SubParsersAction) -> None:
229
+ from argparse import RawDescriptionHelpFormatter
230
+
231
+ epilog = (
232
+ "Examples\n"
233
+ " $ diffusers-cli schema -m stabilityai/stable-diffusion-xl-base-1.0\n"
234
+ " $ diffusers-cli schema -m black-forest-labs/FLUX.1-dev --verbose\n"
235
+ " $ diffusers-cli --format json schema -m stabilityai/stable-diffusion-xl-base-1.0\n"
236
+ "\n"
237
+ "Learn more\n"
238
+ " Use `diffusers-cli <command> --help` for more information about a command.\n"
239
+ " Read the documentation at https://huggingface.co/docs/diffusers\n"
240
+ )
241
+
242
+ parser: ArgumentParser = subparsers.add_parser(
243
+ "schema",
244
+ help="Print the input schema for a diffusers pipeline repo. No weights downloaded.",
245
+ usage="\n diffusers-cli schema [options]",
246
+ epilog=epilog,
247
+ formatter_class=RawDescriptionHelpFormatter,
248
+ )
249
+ parser._optionals.title = "Options"
250
+ parser.add_argument(
251
+ "--model",
252
+ "-m",
253
+ required=True,
254
+ help="Model id on the Hugging Face Hub or local path.",
255
+ )
256
+ parser.add_argument(
257
+ "--revision",
258
+ default=None,
259
+ help="Model revision (branch, tag, or commit SHA).",
260
+ )
261
+ parser.add_argument(
262
+ "--token",
263
+ default=None,
264
+ help="Hugging Face token for gated/private models.",
265
+ )
266
+ parser.add_argument(
267
+ "--trust-remote-code",
268
+ action="store_true",
269
+ help="Allow custom code from the Hub (required for modular pipelines that ship block code).",
270
+ )
271
+ parser.add_argument(
272
+ "--verbose",
273
+ "-v",
274
+ action="store_true",
275
+ help=(
276
+ "Also include per-argument descriptions from the pipeline's __call__ docstring. "
277
+ "Modular pipelines always include block-declared descriptions; --verbose populates "
278
+ "the equivalent field for standard pipelines by parsing the Google-style Args: block."
279
+ ),
280
+ )
281
+ parser.set_defaults(func=SchemaCommand)
282
+
283
+ def __init__(self, args: Namespace):
284
+ self.args = args
285
+
286
+ def run(self) -> None:
287
+ _schema(self.args)
diffusers/commands/skills.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """`diffusers-cli skills` — install Agent Skills bundles.
15
+
16
+ Skill bundles live under `.ai/skills/<name>/` in the diffusers repo and follow the Agent Skills standard: a directory
17
+ containing `SKILL.md` (plus optional resources), including the `references/` copies of the guides it cites. Claude Code
18
+ gets a plugin bundle under `.claude/skills/`; Codex and Cursor get `.agents/skills/<name>/`.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import re
26
+ import shutil
27
+ import textwrap
28
+ from argparse import ArgumentParser, Namespace, _SubParsersAction
29
+ from pathlib import Path
30
+
31
+ from huggingface_hub.cli._output import out
32
+ from huggingface_hub.utils import httpx
33
+
34
+ from ..utils import logging
35
+ from ..utils.constants import DIFFUSERS_REQUEST_TIMEOUT
36
+ from . import BaseDiffusersCLICommand
37
+
38
+
39
+ logger = logging.get_logger("diffusers-cli/skills")
40
+
41
+
42
+ _REGISTRY_BASE = "https://api.github.com/repos/huggingface/diffusers/contents/.ai/skills"
43
+ _RAW_BASE = "https://raw.githubusercontent.com/huggingface/diffusers"
44
+ _REGISTRY_REF = "main"
45
+
46
+ # How a skill cites a guide, e.g. `](references/models.md)` or `` `references/testing.md` ``. The install copies exactly
47
+ # the guides named this way, so a skill only has to cite one to get it.
48
+ _CITATION = re.compile(r"(?<![./\w])references/([\w-]+\.md)")
49
+
50
+ # Native skill-discovery paths per agent. Claude Code reads only `.claude/skills/`; Codex and
51
+ # Cursor read `.agents/skills/` (Cursor also honors `.claude/skills/` via compat, but installing
52
+ # to `.agents/skills/` is the portable choice for both).
53
+ _CLAUDE_SKILLS_DIR = Path(".claude") / "skills"
54
+ _AGENTS_SKILLS_DIR = Path(".agents") / "skills"
55
+
56
+ # Env vars set by each agent when it launches the CLI. Values are the install path to use.
57
+ _AGENT_ENV_TO_DIR: dict[str, Path] = {
58
+ "CLAUDECODE": _CLAUDE_SKILLS_DIR,
59
+ "CLAUDE_CODE": _CLAUDE_SKILLS_DIR,
60
+ "CODEX_SANDBOX": _AGENTS_SKILLS_DIR,
61
+ "CURSOR_AI": _AGENTS_SKILLS_DIR,
62
+ }
63
+ # When no agent env var is set, install to every native path so whichever agent the user
64
+ # later switches to picks the skill up.
65
+ _ALL_INSTALL_DIRS: tuple[Path, ...] = (_CLAUDE_SKILLS_DIR, _AGENTS_SKILLS_DIR)
66
+
67
+ # Empty marker dropped inside each installed skill dir so `update` can distinguish our
68
+ # installs from user-placed skills at the same paths.
69
+ _MANAGED_MARKER_FILE = ".diffusers-skill-managed"
70
+
71
+ # Claude Code loads any directory under a skills root that carries a `.claude-plugin/plugin.json`
72
+ # as a plugin, which namespaces the skills it holds as `/diffusers:<name>` and matches what the
73
+ # published plugin gives contributors. Codex and Cursor have no equivalent, so they keep the plain
74
+ # one-skill-per-directory layout their discovery expects.
75
+ _PLUGIN_NAME = "diffusers"
76
+ _PLUGIN_MANIFEST = {
77
+ "name": _PLUGIN_NAME,
78
+ "description": "Conventions and task skills for contributing to diffusers",
79
+ }
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Registry fetch
84
+ # ---------------------------------------------------------------------------
85
+
86
+
87
+ def _registry_url(name: str = "") -> str:
88
+ """API URL for the registry root, or for a single skill bundle when `name` is given."""
89
+ path = f"/{name}" if name else ""
90
+ return f"{_REGISTRY_BASE}{path}?ref={_REGISTRY_REF}"
91
+
92
+
93
+ def _skill_md_url(name: str) -> str:
94
+ """Raw URL of a skill's SKILL.md, so `list` can read descriptions without walking each bundle."""
95
+ return f"{_RAW_BASE}/{_REGISTRY_REF}/.ai/skills/{name}/SKILL.md"
96
+
97
+
98
+ def _fetch_json(url: str) -> list[dict]:
99
+ try:
100
+ resp = httpx.get(url, timeout=DIFFUSERS_REQUEST_TIMEOUT)
101
+ resp.raise_for_status()
102
+ return resp.json()
103
+ except httpx.HTTPStatusError as e:
104
+ if e.response.status_code == 404:
105
+ raise SystemExit(f"Not found in registry: {url}") from e
106
+ raise SystemExit(f"Registry fetch failed: HTTP {e.response.status_code} {e.response.reason_phrase}") from e
107
+ except httpx.HTTPError as e:
108
+ raise SystemExit(f"Could not reach registry: {e}") from e
109
+
110
+
111
+ def _walk_skill_files(name: str) -> list[tuple[str, str]]:
112
+ files: list[tuple[str, str]] = []
113
+
114
+ def _walk(api_url: str, prefix: str) -> None:
115
+ for entry in _fetch_json(api_url):
116
+ if entry["type"] == "file":
117
+ files.append((f"{prefix}{entry['name']}", entry["download_url"]))
118
+ elif entry["type"] == "dir":
119
+ _walk(entry["url"], f"{prefix}{entry['name']}/")
120
+
121
+ _walk(_registry_url(name), "")
122
+ return files
123
+
124
+
125
+ def _skill_description(skill_md: str) -> str:
126
+ """The `description` from a SKILL.md YAML frontmatter, collapsed onto one line.
127
+
128
+ Handles both `description: text` and the folded `description: >` form the skills in this repo use. Returns an empty
129
+ string when the file has no frontmatter or no description.
130
+ """
131
+ lines = skill_md.splitlines()
132
+ if not lines or lines[0].strip() != "---":
133
+ return ""
134
+ for index, line in enumerate(lines[1:], start=1):
135
+ if line.strip() == "---":
136
+ return ""
137
+ if not line.startswith("description:"):
138
+ continue
139
+ value = line[len("description:") :].strip()
140
+ if value and value not in (">", "|", ">-", "|-"):
141
+ return value
142
+ folded = []
143
+ for continuation in lines[index + 1 :]:
144
+ if continuation.strip() == "---" or not continuation.startswith((" ", "\t")):
145
+ break
146
+ folded.append(continuation.strip())
147
+ return " ".join(folded)
148
+ return ""
149
+
150
+
151
+ def _download_skill_bundle(name: str) -> dict[str, bytes]:
152
+ files = _walk_skill_files(name)
153
+ if not files:
154
+ raise SystemExit(f"Skill '{name}' has no files in the registry.")
155
+ bundle: dict[str, bytes] = {}
156
+ for rel_path, url in files:
157
+ resp = httpx.get(url, timeout=DIFFUSERS_REQUEST_TIMEOUT)
158
+ resp.raise_for_status()
159
+ bundle[rel_path] = resp.content
160
+ return bundle
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Install / discovery
165
+ # ---------------------------------------------------------------------------
166
+
167
+
168
+ def _detect_install_dirs() -> tuple[Path, ...]:
169
+ """Pick where to install based on the launching agent.
170
+
171
+ If we detect a specific agent from its env var, install only there. If nothing is detected, install to every native
172
+ path so any agent picks the skill up later.
173
+ """
174
+ for env_var, skills_dir in _AGENT_ENV_TO_DIR.items():
175
+ if os.environ.get(env_var):
176
+ return (skills_dir,)
177
+ return _ALL_INSTALL_DIRS
178
+
179
+
180
+ def _skill_dir(root: Path, skills_dir: Path, name: str) -> Path:
181
+ """Where a single skill lands, which differs by target: a plugin bundle for Claude Code, a bare
182
+ directory for Codex and Cursor."""
183
+ if skills_dir == _CLAUDE_SKILLS_DIR:
184
+ return root / skills_dir / _PLUGIN_NAME / "skills" / name
185
+ return root / skills_dir / name
186
+
187
+
188
+ def _install_references(skill_dir: Path, skill_md: bytes) -> None:
189
+ """Copy the guides a skill cites into its own `references/` directory.
190
+
191
+ A skill cites a guide as `references/<guide>.md`, so the guides it needs are exactly the ones its SKILL.md names.
192
+ The repo keeps one copy of each under `.ai/references/`; an install gets its own so the skill is self-contained.
193
+ """
194
+ references_dir = skill_dir / "references"
195
+ for guide in sorted(set(_CITATION.findall(skill_md.decode()))):
196
+ resp = httpx.get(f"{_RAW_BASE}/{_REGISTRY_REF}/.ai/references/{guide}", timeout=DIFFUSERS_REQUEST_TIMEOUT)
197
+ resp.raise_for_status()
198
+ references_dir.mkdir(parents=True, exist_ok=True)
199
+ (references_dir / guide).write_bytes(resp.content)
200
+
201
+
202
+ def _install_skill(name: str, bundle: dict[str, bytes], root: Path, skills_dir: Path, force: bool) -> Path:
203
+ skill_dir = _skill_dir(root, skills_dir, name)
204
+ if skill_dir.exists():
205
+ if not force:
206
+ raise SystemExit(f"Skill already installed at {skill_dir}. Use --force to reinstall.")
207
+ shutil.rmtree(skill_dir)
208
+ skill_dir.mkdir(parents=True, exist_ok=True)
209
+ for rel_path, data in bundle.items():
210
+ target = skill_dir / rel_path
211
+ target.parent.mkdir(parents=True, exist_ok=True)
212
+ target.write_bytes(data)
213
+ (skill_dir / _MANAGED_MARKER_FILE).touch()
214
+ _install_references(skill_dir, bundle["SKILL.md"])
215
+
216
+ if skills_dir == _CLAUDE_SKILLS_DIR:
217
+ manifest = root / skills_dir / _PLUGIN_NAME / ".claude-plugin" / "plugin.json"
218
+ manifest.parent.mkdir(parents=True, exist_ok=True)
219
+ manifest.write_text(json.dumps(_PLUGIN_MANIFEST, indent=2) + "\n")
220
+
221
+ return skill_dir
222
+
223
+
224
+ def _has_local_changes(skill_dir: Path, bundle: dict[str, bytes]) -> bool:
225
+ """True if the installed skill has any file that differs from `bundle` or has extra files.
226
+
227
+ The marker file and the copied `references/` guides are ignored — neither comes from the skill bundle. Compares raw
228
+ bytes so a whitespace-only edit still counts as dirty.
229
+ """
230
+ on_disk: dict[str, bytes] = {}
231
+ for path in skill_dir.rglob("*"):
232
+ if not path.is_file():
233
+ continue
234
+ rel = str(path.relative_to(skill_dir))
235
+ if rel.startswith("references/"):
236
+ continue
237
+ if rel == _MANAGED_MARKER_FILE:
238
+ continue
239
+ on_disk[rel] = path.read_bytes()
240
+ return on_disk != bundle
241
+
242
+
243
+ def _discover_installed(root: Path) -> list[tuple[Path, str]]:
244
+ """Return `(skills_dir, name)` pairs for every managed install under `root`."""
245
+ found: list[tuple[Path, str]] = []
246
+ for skills_dir in _ALL_INSTALL_DIRS:
247
+ skills_root = root / skills_dir
248
+ if skills_dir == _CLAUDE_SKILLS_DIR:
249
+ skills_root = skills_root / _PLUGIN_NAME / "skills"
250
+ if not skills_root.exists():
251
+ continue
252
+ for d in sorted(skills_root.iterdir()):
253
+ if d.is_dir() and (d / _MANAGED_MARKER_FILE).exists():
254
+ found.append((skills_dir, d.name))
255
+ return found
256
+
257
+
258
+ class SkillsCommand(BaseDiffusersCLICommand):
259
+ @staticmethod
260
+ def register_subcommand(subparsers: _SubParsersAction) -> None:
261
+ parser: ArgumentParser = subparsers.add_parser(
262
+ "skills",
263
+ help="Manage Agent Skills for AI assistants.",
264
+ usage="\n diffusers-cli skills <add|list|update|preview> [options]",
265
+ )
266
+ parser._optionals.title = "Options"
267
+ actions = parser.add_subparsers(dest="skills_action", required=True, metavar="<action>")
268
+
269
+ add = actions.add_parser("add", help="Download and install a skill.")
270
+ add.add_argument(
271
+ "name",
272
+ nargs="?",
273
+ default=None,
274
+ help="Skill name (e.g. diffusers-cli, custom-blocks). Omit and pass --all to install every skill.",
275
+ )
276
+ add.add_argument(
277
+ "--all",
278
+ dest="install_all",
279
+ action="store_true",
280
+ help="Install every skill in the registry. Mutually exclusive with a positional name.",
281
+ )
282
+ target = add.add_mutually_exclusive_group()
283
+ target.add_argument(
284
+ "--claude",
285
+ dest="install_dirs",
286
+ action="store_const",
287
+ const=(_CLAUDE_SKILLS_DIR,),
288
+ help="Install for Claude Code only, instead of detecting the agent from the environment.",
289
+ )
290
+ target.add_argument(
291
+ "--codex",
292
+ dest="install_dirs",
293
+ action="store_const",
294
+ const=(_AGENTS_SKILLS_DIR,),
295
+ help="Install for Codex only.",
296
+ )
297
+ target.add_argument(
298
+ "--cursor",
299
+ dest="install_dirs",
300
+ action="store_const",
301
+ const=(_AGENTS_SKILLS_DIR,),
302
+ help="Install for Cursor only.",
303
+ )
304
+ add.add_argument(
305
+ "--global",
306
+ "-g",
307
+ dest="install_global",
308
+ action="store_true",
309
+ help="Install globally (user-level) instead of in the current project directory.",
310
+ )
311
+ add.add_argument("--force", action="store_true", help="Overwrite existing skills in the destination.")
312
+ add.set_defaults(func=SkillsCommand)
313
+
314
+ list_action = actions.add_parser("list", help="List available skills in the registry.")
315
+ list_action.set_defaults(func=SkillsCommand)
316
+
317
+ update = actions.add_parser("update", help="Re-download and reinstall managed skills.")
318
+ update.add_argument(
319
+ "name",
320
+ nargs="?",
321
+ default=None,
322
+ help="Optional installed skill name to update. Omit to update every managed skill.",
323
+ )
324
+ update.add_argument(
325
+ "--global",
326
+ "-g",
327
+ dest="install_global",
328
+ action="store_true",
329
+ help="Update skills installed globally (user-level) instead of the current project.",
330
+ )
331
+ update.add_argument(
332
+ "--force",
333
+ action="store_true",
334
+ help="Overwrite skills even if they have local modifications since install.",
335
+ )
336
+ update.set_defaults(func=SkillsCommand)
337
+
338
+ preview = actions.add_parser("preview", help="Print a skill's SKILL.md from the registry.")
339
+ preview.add_argument("name", help="Skill name to preview.")
340
+ preview.set_defaults(func=SkillsCommand)
341
+
342
+ def __init__(self, args: Namespace):
343
+ self.args = args
344
+
345
+ def run(self) -> None:
346
+ if self.args.skills_action == "add":
347
+ self._add()
348
+ elif self.args.skills_action == "list":
349
+ self._list()
350
+ elif self.args.skills_action == "update":
351
+ self._update()
352
+ elif self.args.skills_action == "preview":
353
+ self._preview()
354
+
355
+ def _add(self) -> None:
356
+ if self.args.install_all and self.args.name:
357
+ raise SystemExit("--all and a positional skill name are mutually exclusive.")
358
+ if not self.args.install_all and not self.args.name:
359
+ raise SystemExit("Pass a skill name (e.g. diffusers-cli) or --all to install every skill.")
360
+
361
+ root = Path.home() if self.args.install_global else Path.cwd()
362
+ install_dirs = self.args.install_dirs or _detect_install_dirs()
363
+ names = self._resolve_names()
364
+
365
+ installed: list[str] = []
366
+ failed: list[str] = []
367
+ for name in names:
368
+ try:
369
+ bundle = _download_skill_bundle(name)
370
+ for skills_dir in install_dirs:
371
+ _install_skill(name, bundle, root, skills_dir, self.args.force)
372
+ installed.append(name)
373
+ except (SystemExit, httpx.HTTPError) as e:
374
+ # Downgrade to a warning so one broken skill doesn't abort the batch.
375
+ logger.warning(f"Skipping skill {name!r}: {e}")
376
+ failed.append(name)
377
+
378
+ if not installed:
379
+ raise SystemExit(f"No skills installed. Failed: {failed}")
380
+
381
+ out.result(
382
+ f"Installed {len(installed)} skill(s)",
383
+ installed=", ".join(installed),
384
+ failed=", ".join(failed) if failed else None,
385
+ paths=", ".join(str(root / d) for d in install_dirs),
386
+ )
387
+
388
+ def _update(self) -> None:
389
+ root = Path.home() if self.args.install_global else Path.cwd()
390
+ installed = _discover_installed(root)
391
+ if self.args.name is not None:
392
+ installed = [entry for entry in installed if entry[1] == self.args.name]
393
+ if not installed:
394
+ raise SystemExit(f"No installed skill named {self.args.name!r} found under {root}.")
395
+ if not installed:
396
+ raise SystemExit(f"No managed skills found under {root}.")
397
+
398
+ # Group by skill name so we redownload each bundle once even if it's installed to
399
+ # multiple locations (e.g. both .claude/skills/ and .agents/skills/).
400
+ by_name: dict[str, list[Path]] = {}
401
+ for skills_dir, name in installed:
402
+ by_name.setdefault(name, []).append(skills_dir)
403
+
404
+ updated: list[str] = []
405
+ failed: list[str] = []
406
+ skipped: list[str] = []
407
+ for name, dirs in sorted(by_name.items()):
408
+ try:
409
+ bundle = _download_skill_bundle(name)
410
+ for skills_dir in dirs:
411
+ skill_dir = _skill_dir(root, skills_dir, name)
412
+ if not self.args.force and _has_local_changes(skill_dir, bundle):
413
+ logger.warning(
414
+ f"Skill {name!r} at {skill_dir} has local modifications; "
415
+ "skipping. Pass --force to overwrite them."
416
+ )
417
+ skipped.append(name)
418
+ continue
419
+ _install_skill(name, bundle, root, skills_dir, force=True)
420
+ updated.append(name)
421
+ except (SystemExit, httpx.HTTPError) as e:
422
+ logger.warning(f"Skipping skill {name!r}: {e}")
423
+ failed.append(name)
424
+
425
+ out.result(
426
+ f"Updated {len(updated)} skill(s)",
427
+ updated=", ".join(updated),
428
+ skipped=", ".join(skipped) if skipped else None,
429
+ failed=", ".join(failed) if failed else None,
430
+ )
431
+
432
+ def _preview(self) -> None:
433
+ bundle = _download_skill_bundle(self.args.name)
434
+ skill_md = bundle.get("SKILL.md")
435
+ if skill_md is None:
436
+ raise SystemExit(f"Skill {self.args.name!r} has no SKILL.md in the registry.")
437
+ print(skill_md.decode())
438
+
439
+ def _list(self) -> None:
440
+ entries = _fetch_json(_registry_url())
441
+ names = sorted(e["name"] for e in entries if e["type"] == "dir" and not e["name"].startswith("."))
442
+ if not names:
443
+ raise SystemExit("No skills found in registry.")
444
+
445
+ # Descriptions run several sentences, so they get a wrapped block under each name rather than a table cell.
446
+ width = min(shutil.get_terminal_size().columns, 100)
447
+ installed = {name for _, name in _discover_installed(Path.cwd())}
448
+ for name in names:
449
+ resp = httpx.get(_skill_md_url(name), timeout=DIFFUSERS_REQUEST_TIMEOUT)
450
+ resp.raise_for_status()
451
+ description = _skill_description(resp.text)
452
+ print(f"\n{name}{' (installed)' if name in installed else ''}")
453
+ print(textwrap.fill(description, width=width, initial_indent=" ", subsequent_indent=" "))
454
+
455
+ def _resolve_names(self) -> list[str]:
456
+ if self.args.install_all:
457
+ entries = _fetch_json(_registry_url())
458
+ return sorted(e["name"] for e in entries if e["type"] == "dir" and not e["name"].startswith("."))
459
+ return [self.args.name]
diffusers/configuration_utils.py ADDED
@@ -0,0 +1,726 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025 The HuggingFace Inc. team.
3
+ # Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ """ConfigMixin base class and utilities."""
17
+
18
+ import functools
19
+ import importlib
20
+ import inspect
21
+ import json
22
+ import os
23
+ import re
24
+ from collections import OrderedDict
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ import numpy as np
29
+ from huggingface_hub import create_repo, hf_hub_download
30
+ from huggingface_hub.utils import (
31
+ EntryNotFoundError,
32
+ HfHubHTTPError,
33
+ RepositoryNotFoundError,
34
+ RevisionNotFoundError,
35
+ validate_hf_hub_args,
36
+ )
37
+ from typing_extensions import Self
38
+
39
+ from . import __version__
40
+ from .utils import (
41
+ HUGGINGFACE_CO_RESOLVE_ENDPOINT,
42
+ DummyObject,
43
+ deprecate,
44
+ extract_commit_hash,
45
+ http_user_agent,
46
+ logging,
47
+ )
48
+
49
+
50
+ logger = logging.get_logger(__name__)
51
+
52
+ _re_configuration_file = re.compile(r"config\.(.*)\.json")
53
+
54
+
55
+ class FrozenDict(OrderedDict):
56
+ def __init__(self, *args, **kwargs):
57
+ super().__init__(*args, **kwargs)
58
+
59
+ for key, value in self.items():
60
+ setattr(self, key, value)
61
+
62
+ self.__frozen = True
63
+
64
+ def __delitem__(self, *args, **kwargs):
65
+ raise Exception(f"You cannot use ``__delitem__`` on a {self.__class__.__name__} instance.")
66
+
67
+ def setdefault(self, *args, **kwargs):
68
+ raise Exception(f"You cannot use ``setdefault`` on a {self.__class__.__name__} instance.")
69
+
70
+ def pop(self, *args, **kwargs):
71
+ raise Exception(f"You cannot use ``pop`` on a {self.__class__.__name__} instance.")
72
+
73
+ def update(self, *args, **kwargs):
74
+ raise Exception(f"You cannot use ``update`` on a {self.__class__.__name__} instance.")
75
+
76
+ def __setattr__(self, name, value):
77
+ if hasattr(self, "__frozen") and self.__frozen:
78
+ raise Exception(f"You cannot use ``__setattr__`` on a {self.__class__.__name__} instance.")
79
+ super().__setattr__(name, value)
80
+
81
+ def __setitem__(self, name, value):
82
+ if hasattr(self, "__frozen") and self.__frozen:
83
+ raise Exception(f"You cannot use ``__setattr__`` on a {self.__class__.__name__} instance.")
84
+ super().__setitem__(name, value)
85
+
86
+
87
+ class ConfigMixin:
88
+ r"""
89
+ Base class for all configuration classes. All configuration parameters are stored under `self.config`. Also
90
+ provides the [`~ConfigMixin.from_config`] and [`~ConfigMixin.save_config`] methods for loading, downloading, and
91
+ saving classes that inherit from [`ConfigMixin`].
92
+
93
+ Class attributes:
94
+ - **config_name** (`str`) -- A filename under which the config should stored when calling
95
+ [`~ConfigMixin.save_config`] (should be overridden by parent class).
96
+ - **ignore_for_config** (`list[str]`) -- A list of attributes that should not be saved in the config (should be
97
+ overridden by subclass).
98
+ - **has_compatibles** (`bool`) -- Whether the class has compatible classes (should be overridden by subclass).
99
+ - **_deprecated_kwargs** (`list[str]`) -- Keyword arguments that are deprecated. Note that the `init` function
100
+ should only have a `kwargs` argument if at least one argument is deprecated (should be overridden by
101
+ subclass).
102
+ """
103
+
104
+ config_name = None
105
+ ignore_for_config = []
106
+ has_compatibles = False
107
+
108
+ _deprecated_kwargs = []
109
+ _auto_class = None
110
+
111
+ @classmethod
112
+ def register_for_auto_class(cls, auto_class="AutoModel"):
113
+ """
114
+ Register this class with the given auto class so that it can be loaded with `AutoModel.from_pretrained(...,
115
+ trust_remote_code=True)`.
116
+
117
+ When the config is saved, the resulting `config.json` will include an `auto_map` entry mapping the auto class
118
+ to this class's module and class name.
119
+
120
+ Args:
121
+ auto_class (`str` or type, *optional*, defaults to `"AutoModel"`):
122
+ The auto class to register this class with. Can be a string (e.g. `"AutoModel"`) or the class itself.
123
+ Currently only `"AutoModel"` is supported.
124
+
125
+ Example:
126
+
127
+ ```python
128
+ from diffusers import ModelMixin, ConfigMixin
129
+
130
+
131
+ class MyCustomModel(ModelMixin, ConfigMixin): ...
132
+
133
+
134
+ MyCustomModel.register_for_auto_class("AutoModel")
135
+ ```
136
+ """
137
+ if auto_class != "AutoModel":
138
+ raise ValueError(f"Only 'AutoModel' is supported, got '{auto_class}'.")
139
+
140
+ cls._auto_class = auto_class
141
+
142
+ def register_to_config(self, **kwargs):
143
+ if self.config_name is None:
144
+ raise NotImplementedError(f"Make sure that {self.__class__} has defined a class name `config_name`")
145
+ # Special case for `kwargs` used in deprecation warning added to schedulers
146
+ # TODO: remove this when we remove the deprecation warning, and the `kwargs` argument,
147
+ # or solve in a more general way.
148
+ kwargs.pop("kwargs", None)
149
+
150
+ if not hasattr(self, "_internal_dict"):
151
+ internal_dict = kwargs
152
+ else:
153
+ previous_dict = dict(self._internal_dict)
154
+ internal_dict = {**self._internal_dict, **kwargs}
155
+ logger.debug(f"Updating config from {previous_dict} to {internal_dict}")
156
+
157
+ self._internal_dict = FrozenDict(internal_dict)
158
+
159
+ def __getattr__(self, name: str) -> Any:
160
+ """The only reason we overwrite `getattr` here is to gracefully deprecate accessing
161
+ config attributes directly. See https://github.com/huggingface/diffusers/pull/3129
162
+
163
+ This function is mostly copied from PyTorch's __getattr__ overwrite:
164
+ https://pytorch.org/docs/stable/_modules/torch/nn/modules/module.html#Module
165
+ """
166
+
167
+ is_in_config = "_internal_dict" in self.__dict__ and hasattr(self.__dict__["_internal_dict"], name)
168
+ is_attribute = name in self.__dict__
169
+
170
+ if is_in_config and not is_attribute:
171
+ deprecation_message = f"Accessing config attribute `{name}` directly via '{type(self).__name__}' object attribute is deprecated. Please access '{name}' over '{type(self).__name__}'s config object instead, e.g. 'scheduler.config.{name}'."
172
+ deprecate("direct config name access", "1.0.0", deprecation_message, standard_warn=False)
173
+ return self._internal_dict[name]
174
+
175
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
176
+
177
+ def save_config(self, save_directory: str | os.PathLike, push_to_hub: bool = False, **kwargs):
178
+ """
179
+ Save a configuration object to the directory specified in `save_directory` so that it can be reloaded using the
180
+ [`~ConfigMixin.from_config`] class method.
181
+
182
+ Args:
183
+ save_directory (`str` or `os.PathLike`):
184
+ Directory where the configuration JSON file is saved (will be created if it does not exist).
185
+ push_to_hub (`bool`, *optional*, defaults to `False`):
186
+ Whether or not to push your model to the Hugging Face Hub after saving it. You can specify the
187
+ repository you want to push to with `repo_id` (will default to the name of `save_directory` in your
188
+ namespace).
189
+ kwargs (`dict[str, Any]`, *optional*):
190
+ Additional keyword arguments passed along to the [`~utils.PushToHubMixin.push_to_hub`] method.
191
+ """
192
+ if os.path.isfile(save_directory):
193
+ raise AssertionError(f"Provided path ({save_directory}) should be a directory, not a file")
194
+
195
+ os.makedirs(save_directory, exist_ok=True)
196
+
197
+ # If we save using the predefined names, we can load using `from_config`
198
+ output_config_file = os.path.join(save_directory, self.config_name)
199
+
200
+ self.to_json_file(output_config_file)
201
+ logger.info(f"Configuration saved in {output_config_file}")
202
+
203
+ if push_to_hub:
204
+ commit_message = kwargs.pop("commit_message", None)
205
+ private = kwargs.pop("private", None)
206
+ create_pr = kwargs.pop("create_pr", False)
207
+ token = kwargs.pop("token", None)
208
+ repo_id = kwargs.pop("repo_id", save_directory.split(os.path.sep)[-1])
209
+ repo_id = create_repo(repo_id, exist_ok=True, private=private, token=token).repo_id
210
+ subfolder = kwargs.pop("subfolder", None)
211
+
212
+ self._upload_folder(
213
+ save_directory,
214
+ repo_id,
215
+ token=token,
216
+ commit_message=commit_message,
217
+ create_pr=create_pr,
218
+ subfolder=subfolder,
219
+ )
220
+
221
+ @classmethod
222
+ def from_config(
223
+ cls, config: FrozenDict | dict[str, Any] = None, return_unused_kwargs=False, **kwargs
224
+ ) -> Self | tuple[Self, dict[str, Any]]:
225
+ r"""
226
+ Instantiate a Python class from a config dictionary.
227
+
228
+ Parameters:
229
+ config (`dict[str, Any]`):
230
+ A config dictionary from which the Python class is instantiated. Make sure to only load configuration
231
+ files of compatible classes.
232
+ return_unused_kwargs (`bool`, *optional*, defaults to `False`):
233
+ Whether kwargs that are not consumed by the Python class should be returned or not.
234
+ kwargs (remaining dictionary of keyword arguments, *optional*):
235
+ Can be used to update the configuration object (after it is loaded) and initiate the Python class.
236
+ `**kwargs` are passed directly to the underlying scheduler/model's `__init__` method and eventually
237
+ overwrite the same named arguments in `config`.
238
+
239
+ Returns:
240
+ [`ModelMixin`] or [`SchedulerMixin`]:
241
+ A model or scheduler object instantiated from a config dictionary.
242
+
243
+ Examples:
244
+
245
+ ```python
246
+ >>> from diffusers import DDPMScheduler, DDIMScheduler, PNDMScheduler
247
+
248
+ >>> # Download scheduler from huggingface.co and cache.
249
+ >>> scheduler = DDPMScheduler.from_pretrained("google/ddpm-cifar10-32")
250
+
251
+ >>> # Instantiate DDIM scheduler class with same config as DDPM
252
+ >>> scheduler = DDIMScheduler.from_config(scheduler.config)
253
+
254
+ >>> # Instantiate PNDM scheduler class with same config as DDPM
255
+ >>> scheduler = PNDMScheduler.from_config(scheduler.config)
256
+ ```
257
+ """
258
+ # <===== TO BE REMOVED WITH DEPRECATION
259
+ # TODO(Patrick) - make sure to remove the following lines when config=="model_path" is deprecated
260
+ if "pretrained_model_name_or_path" in kwargs:
261
+ config = kwargs.pop("pretrained_model_name_or_path")
262
+
263
+ if config is None:
264
+ raise ValueError("Please make sure to provide a config as the first positional argument.")
265
+ # ======>
266
+
267
+ if not isinstance(config, dict):
268
+ deprecation_message = "It is deprecated to pass a pretrained model name or path to `from_config`."
269
+ if "Scheduler" in cls.__name__:
270
+ deprecation_message += (
271
+ f"If you were trying to load a scheduler, please use {cls}.from_pretrained(...) instead."
272
+ " Otherwise, please make sure to pass a configuration dictionary instead. This functionality will"
273
+ " be removed in v1.0.0."
274
+ )
275
+ elif "Model" in cls.__name__:
276
+ deprecation_message += (
277
+ f"If you were trying to load a model, please use {cls}.load_config(...) followed by"
278
+ f" {cls}.from_config(...) instead. Otherwise, please make sure to pass a configuration dictionary"
279
+ " instead. This functionality will be removed in v1.0.0."
280
+ )
281
+ deprecate("config-passed-as-path", "1.0.0", deprecation_message, standard_warn=False)
282
+ config, kwargs = cls.load_config(pretrained_model_name_or_path=config, return_unused_kwargs=True, **kwargs)
283
+
284
+ init_dict, unused_kwargs, hidden_dict = cls.extract_init_dict(config, **kwargs)
285
+
286
+ # Allow dtype to be specified on initialization
287
+ if "dtype" in unused_kwargs:
288
+ init_dict["dtype"] = unused_kwargs.pop("dtype")
289
+
290
+ # add possible deprecated kwargs
291
+ for deprecated_kwarg in cls._deprecated_kwargs:
292
+ if deprecated_kwarg in unused_kwargs:
293
+ init_dict[deprecated_kwarg] = unused_kwargs.pop(deprecated_kwarg)
294
+
295
+ # Return model and optionally state and/or unused_kwargs
296
+ model = cls(**init_dict)
297
+
298
+ # make sure to also save config parameters that might be used for compatible classes
299
+ # update _class_name
300
+ if "_class_name" in hidden_dict:
301
+ hidden_dict["_class_name"] = cls.__name__
302
+
303
+ model.register_to_config(**hidden_dict)
304
+
305
+ # add hidden kwargs of compatible classes to unused_kwargs
306
+ unused_kwargs = {**unused_kwargs, **hidden_dict}
307
+
308
+ if return_unused_kwargs:
309
+ return (model, unused_kwargs)
310
+ else:
311
+ return model
312
+
313
+ @classmethod
314
+ def get_config_dict(cls, *args, **kwargs):
315
+ deprecation_message = (
316
+ f" The function get_config_dict is deprecated. Please use {cls}.load_config instead. This function will be"
317
+ " removed in version v1.0.0"
318
+ )
319
+ deprecate("get_config_dict", "1.0.0", deprecation_message, standard_warn=False)
320
+ return cls.load_config(*args, **kwargs)
321
+
322
+ @classmethod
323
+ @validate_hf_hub_args
324
+ def load_config(
325
+ cls,
326
+ pretrained_model_name_or_path: str | os.PathLike,
327
+ return_unused_kwargs=False,
328
+ return_commit_hash=False,
329
+ **kwargs,
330
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
331
+ r"""
332
+ Load a model or scheduler configuration.
333
+
334
+ Parameters:
335
+ pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*):
336
+ Can be either:
337
+
338
+ - A string, the *model id* (for example `google/ddpm-celebahq-256`) of a pretrained model hosted on
339
+ the Hub.
340
+ - A path to a *directory* (for example `./my_model_directory`) containing model weights saved with
341
+ [`~ConfigMixin.save_config`].
342
+
343
+ cache_dir (`str | os.PathLike`, *optional*):
344
+ Path to a directory where a downloaded pretrained model configuration is cached if the standard cache
345
+ is not used.
346
+ force_download (`bool`, *optional*, defaults to `False`):
347
+ Whether or not to force the (re-)download of the model weights and configuration files, overriding the
348
+ cached versions if they exist.
349
+ proxies (`dict[str, str]`, *optional*):
350
+ A dictionary of proxy servers to use by protocol or endpoint, for example, `{'http': 'foo.bar:3128',
351
+ 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request.
352
+ output_loading_info(`bool`, *optional*, defaults to `False`):
353
+ Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages.
354
+ local_files_only (`bool`, *optional*, defaults to `False`):
355
+ Whether to only load local model weights and configuration files or not. If set to `True`, the model
356
+ won't be downloaded from the Hub.
357
+ token (`str` or *bool*, *optional*):
358
+ The token to use as HTTP bearer authorization for remote files. If `True`, the token generated from
359
+ `diffusers-cli login` (stored in `~/.huggingface`) is used.
360
+ revision (`str`, *optional*, defaults to `"main"`):
361
+ The specific model version to use. It can be a branch name, a tag name, a commit id, or any identifier
362
+ allowed by Git.
363
+ subfolder (`str`, *optional*, defaults to `""`):
364
+ The subfolder location of a model file within a larger model repository on the Hub or locally.
365
+ return_unused_kwargs (`bool`, *optional*, defaults to `False):
366
+ Whether unused keyword arguments of the config are returned.
367
+ return_commit_hash (`bool`, *optional*, defaults to `False):
368
+ Whether the `commit_hash` of the loaded configuration are returned.
369
+
370
+ Returns:
371
+ `dict`:
372
+ A dictionary of all the parameters stored in a JSON configuration file.
373
+
374
+ """
375
+ cache_dir = kwargs.pop("cache_dir", None)
376
+ local_dir = kwargs.pop("local_dir", None)
377
+ local_dir_use_symlinks = kwargs.pop("local_dir_use_symlinks", "auto")
378
+ force_download = kwargs.pop("force_download", False)
379
+ proxies = kwargs.pop("proxies", None)
380
+ token = kwargs.pop("token", None)
381
+ local_files_only = kwargs.pop("local_files_only", False)
382
+ revision = kwargs.pop("revision", None)
383
+ _ = kwargs.pop("mirror", None)
384
+ subfolder = kwargs.pop("subfolder", None)
385
+ user_agent = kwargs.pop("user_agent", {})
386
+
387
+ user_agent = {**user_agent, "file_type": "config"}
388
+ user_agent = http_user_agent(user_agent)
389
+
390
+ pretrained_model_name_or_path = str(pretrained_model_name_or_path)
391
+
392
+ if cls.config_name is None:
393
+ raise ValueError(
394
+ "`self.config_name` is not defined. Note that one should not load a config from "
395
+ "`ConfigMixin`. Please make sure to define `config_name` in a class inheriting from `ConfigMixin`"
396
+ )
397
+ if os.path.isfile(pretrained_model_name_or_path):
398
+ config_file = pretrained_model_name_or_path
399
+ elif os.path.isdir(pretrained_model_name_or_path):
400
+ if subfolder is not None and os.path.isfile(
401
+ os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name)
402
+ ):
403
+ config_file = os.path.join(pretrained_model_name_or_path, subfolder, cls.config_name)
404
+ elif os.path.isfile(os.path.join(pretrained_model_name_or_path, cls.config_name)):
405
+ # Load from a PyTorch checkpoint
406
+ config_file = os.path.join(pretrained_model_name_or_path, cls.config_name)
407
+ else:
408
+ raise EnvironmentError(
409
+ f"Error no file named {cls.config_name} found in directory {pretrained_model_name_or_path}."
410
+ )
411
+ else:
412
+ try:
413
+ # Load from URL or cache if already cached
414
+ config_file = hf_hub_download(
415
+ pretrained_model_name_or_path,
416
+ filename=cls.config_name,
417
+ cache_dir=cache_dir,
418
+ force_download=force_download,
419
+ proxies=proxies,
420
+ local_files_only=local_files_only,
421
+ token=token,
422
+ user_agent=user_agent,
423
+ subfolder=subfolder,
424
+ revision=revision,
425
+ local_dir=local_dir,
426
+ local_dir_use_symlinks=local_dir_use_symlinks,
427
+ )
428
+ except RepositoryNotFoundError:
429
+ raise EnvironmentError(
430
+ f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier"
431
+ " listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a"
432
+ " token having permission to this repo with `token` or log in with `hf auth login`."
433
+ )
434
+ except RevisionNotFoundError:
435
+ raise EnvironmentError(
436
+ f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for"
437
+ " this model name. Check the model page at"
438
+ f" 'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions."
439
+ )
440
+ except EntryNotFoundError:
441
+ raise EnvironmentError(
442
+ f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}."
443
+ )
444
+ except HfHubHTTPError as err:
445
+ raise EnvironmentError(
446
+ "There was a specific connection error when trying to load"
447
+ f" {pretrained_model_name_or_path}:\n{err}"
448
+ )
449
+ except ValueError:
450
+ raise EnvironmentError(
451
+ f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it"
452
+ f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a"
453
+ f" directory containing a {cls.config_name} file.\nCheckout your internet connection or see how to"
454
+ " run the library in offline mode at"
455
+ " 'https://huggingface.co/docs/diffusers/installation#offline-mode'."
456
+ )
457
+ except EnvironmentError:
458
+ raise EnvironmentError(
459
+ f"Can't load config for '{pretrained_model_name_or_path}'. If you were trying to load it from "
460
+ "'https://huggingface.co/models', make sure you don't have a local directory with the same name. "
461
+ f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory "
462
+ f"containing a {cls.config_name} file"
463
+ )
464
+ try:
465
+ config_dict = cls._dict_from_json_file(config_file)
466
+
467
+ commit_hash = extract_commit_hash(config_file)
468
+ except (json.JSONDecodeError, UnicodeDecodeError):
469
+ raise EnvironmentError(f"It looks like the config file at '{config_file}' is not a valid JSON file.")
470
+
471
+ if not (return_unused_kwargs or return_commit_hash):
472
+ return config_dict
473
+
474
+ outputs = (config_dict,)
475
+
476
+ if return_unused_kwargs:
477
+ outputs += (kwargs,)
478
+
479
+ if return_commit_hash:
480
+ outputs += (commit_hash,)
481
+
482
+ return outputs
483
+
484
+ @staticmethod
485
+ def _get_init_keys(input_class):
486
+ return set(dict(inspect.signature(input_class.__init__).parameters).keys())
487
+
488
+ @classmethod
489
+ def extract_init_dict(cls, config_dict, **kwargs):
490
+ # Skip keys that were not present in the original config, so default __init__ values were used
491
+ used_defaults = config_dict.get("_use_default_values", [])
492
+ config_dict = {k: v for k, v in config_dict.items() if k not in used_defaults and k != "_use_default_values"}
493
+
494
+ # 0. Copy origin config dict
495
+ original_dict = dict(config_dict.items())
496
+
497
+ # 1. Retrieve expected config attributes from __init__ signature
498
+ expected_keys = cls._get_init_keys(cls)
499
+ expected_keys.remove("self")
500
+ # remove general kwargs if present in dict
501
+ if "kwargs" in expected_keys:
502
+ expected_keys.remove("kwargs")
503
+
504
+ # 2. Remove attributes that cannot be expected from expected config attributes
505
+ # remove keys to be ignored
506
+ if len(cls.ignore_for_config) > 0:
507
+ expected_keys = expected_keys - set(cls.ignore_for_config)
508
+
509
+ # load diffusers library to import compatible and original scheduler
510
+ diffusers_library = importlib.import_module(__name__.split(".")[0])
511
+
512
+ if cls.has_compatibles:
513
+ compatible_classes = [c for c in cls._get_compatibles() if not isinstance(c, DummyObject)]
514
+ else:
515
+ compatible_classes = []
516
+
517
+ expected_keys_comp_cls = set()
518
+ for c in compatible_classes:
519
+ expected_keys_c = cls._get_init_keys(c)
520
+ expected_keys_comp_cls = expected_keys_comp_cls.union(expected_keys_c)
521
+ expected_keys_comp_cls = expected_keys_comp_cls - cls._get_init_keys(cls)
522
+ config_dict = {k: v for k, v in config_dict.items() if k not in expected_keys_comp_cls}
523
+
524
+ # remove attributes from orig class that cannot be expected
525
+ orig_cls_name = config_dict.pop("_class_name", cls.__name__)
526
+ if (
527
+ isinstance(orig_cls_name, str)
528
+ and orig_cls_name != cls.__name__
529
+ and hasattr(diffusers_library, orig_cls_name)
530
+ ):
531
+ orig_cls = getattr(diffusers_library, orig_cls_name)
532
+ unexpected_keys_from_orig = cls._get_init_keys(orig_cls) - expected_keys
533
+ config_dict = {k: v for k, v in config_dict.items() if k not in unexpected_keys_from_orig}
534
+ elif not isinstance(orig_cls_name, str) and not isinstance(orig_cls_name, (list, tuple)):
535
+ raise ValueError(
536
+ "Make sure that the `_class_name` is of type string or list of string (for custom pipelines)."
537
+ )
538
+
539
+ # remove private attributes
540
+ config_dict = {k: v for k, v in config_dict.items() if not k.startswith("_")}
541
+
542
+ # remove quantization_config
543
+ config_dict = {k: v for k, v in config_dict.items() if k != "quantization_config"}
544
+
545
+ # 3. Create keyword arguments that will be passed to __init__ from expected keyword arguments
546
+ init_dict = {}
547
+ for key in expected_keys:
548
+ # if config param is passed to kwarg and is present in config dict
549
+ # it should overwrite existing config dict key
550
+ if key in kwargs and key in config_dict:
551
+ config_dict[key] = kwargs.pop(key)
552
+
553
+ if key in kwargs:
554
+ # overwrite key
555
+ init_dict[key] = kwargs.pop(key)
556
+ elif key in config_dict:
557
+ # use value from config dict
558
+ init_dict[key] = config_dict.pop(key)
559
+
560
+ # 4. Give nice warning if unexpected values have been passed
561
+ if len(config_dict) > 0:
562
+ logger.warning(
563
+ f"The config attributes {config_dict} were passed to {cls.__name__}, "
564
+ "but are not expected and will be ignored. Please verify your "
565
+ f"{cls.config_name} configuration file."
566
+ )
567
+
568
+ # 5. Give nice info if config attributes are initialized to default because they have not been passed
569
+ passed_keys = set(init_dict.keys())
570
+ if len(expected_keys - passed_keys) > 0:
571
+ logger.info(
572
+ f"{expected_keys - passed_keys} was not found in config. Values will be initialized to default values."
573
+ )
574
+
575
+ # 6. Define unused keyword arguments
576
+ unused_kwargs = {**config_dict, **kwargs}
577
+
578
+ # 7. Define "hidden" config parameters that were saved for compatible classes
579
+ hidden_config_dict = {k: v for k, v in original_dict.items() if k not in init_dict}
580
+
581
+ return init_dict, unused_kwargs, hidden_config_dict
582
+
583
+ @classmethod
584
+ def _dict_from_json_file(cls, json_file: str | os.PathLike):
585
+ with open(json_file, "r", encoding="utf-8") as reader:
586
+ text = reader.read()
587
+ return json.loads(text)
588
+
589
+ def __repr__(self):
590
+ return f"{self.__class__.__name__} {self.to_json_string()}"
591
+
592
+ @property
593
+ def config(self) -> dict[str, Any]:
594
+ """
595
+ Returns the config of the class as a frozen dictionary
596
+
597
+ Returns:
598
+ `dict[str, Any]`: Config of the class.
599
+ """
600
+ return self._internal_dict
601
+
602
+ def to_json_string(self) -> str:
603
+ """
604
+ Serializes the configuration instance to a JSON string.
605
+
606
+ Returns:
607
+ `str`:
608
+ String containing all the attributes that make up the configuration instance in JSON format.
609
+ """
610
+ config_dict = self._internal_dict if hasattr(self, "_internal_dict") else {}
611
+ config_dict["_class_name"] = self.__class__.__name__
612
+ config_dict["_diffusers_version"] = __version__
613
+
614
+ def to_json_saveable(value):
615
+ if isinstance(value, np.ndarray):
616
+ value = value.tolist()
617
+ elif isinstance(value, Path):
618
+ value = value.as_posix()
619
+ elif hasattr(value, "to_dict") and callable(value.to_dict):
620
+ value = value.to_dict()
621
+ elif isinstance(value, list):
622
+ value = [to_json_saveable(v) for v in value]
623
+ return value
624
+
625
+ if "quantization_config" in config_dict:
626
+ config_dict["quantization_config"] = (
627
+ config_dict.quantization_config.to_dict()
628
+ if not isinstance(config_dict.quantization_config, dict)
629
+ else config_dict.quantization_config
630
+ )
631
+
632
+ config_dict = {k: to_json_saveable(v) for k, v in config_dict.items()}
633
+ # Don't save "_ignore_files" or "_use_default_values"
634
+ config_dict.pop("_ignore_files", None)
635
+ config_dict.pop("_use_default_values", None)
636
+ # pop the `_pre_quantization_dtype` as torch.dtypes are not serializable.
637
+ _ = config_dict.pop("_pre_quantization_dtype", None)
638
+
639
+ if getattr(self, "_auto_class", None) is not None:
640
+ module = self.__class__.__module__.split(".")[-1]
641
+ auto_map = config_dict.get("auto_map", {})
642
+ auto_map[self._auto_class] = f"{module}.{self.__class__.__name__}"
643
+ config_dict["auto_map"] = auto_map
644
+
645
+ return json.dumps(config_dict, indent=2, sort_keys=True) + "\n"
646
+
647
+ def to_json_file(self, json_file_path: str | os.PathLike):
648
+ """
649
+ Save the configuration instance's parameters to a JSON file.
650
+
651
+ Args:
652
+ json_file_path (`str` or `os.PathLike`):
653
+ Path to the JSON file to save a configuration instance's parameters.
654
+ """
655
+ with open(json_file_path, "w", encoding="utf-8") as writer:
656
+ writer.write(self.to_json_string())
657
+
658
+
659
+ def register_to_config(init):
660
+ r"""
661
+ Decorator to apply on the init of classes inheriting from [`ConfigMixin`] so that all the arguments are
662
+ automatically sent to `self.register_for_config`. To ignore a specific argument accepted by the init but that
663
+ shouldn't be registered in the config, use the `ignore_for_config` class variable
664
+
665
+ Warning: Once decorated, all private arguments (beginning with an underscore) are trashed and not sent to the init!
666
+ """
667
+
668
+ @functools.wraps(init)
669
+ def inner_init(self, *args, **kwargs):
670
+ # Ignore private kwargs in the init.
671
+ init_kwargs = {k: v for k, v in kwargs.items() if not k.startswith("_")}
672
+ config_init_kwargs = {k: v for k, v in kwargs.items() if k.startswith("_")}
673
+ if not isinstance(self, ConfigMixin):
674
+ raise RuntimeError(
675
+ f"`@register_for_config` was applied to {self.__class__.__name__} init method, but this class does "
676
+ "not inherit from `ConfigMixin`."
677
+ )
678
+
679
+ ignore = getattr(self, "ignore_for_config", [])
680
+ # Get positional arguments aligned with kwargs
681
+ new_kwargs = {}
682
+ signature = inspect.signature(init)
683
+ parameters = {
684
+ name: p.default for i, (name, p) in enumerate(signature.parameters.items()) if i > 0 and name not in ignore
685
+ }
686
+ for arg, name in zip(args, parameters.keys()):
687
+ new_kwargs[name] = arg
688
+
689
+ # Then add all kwargs
690
+ new_kwargs.update(
691
+ {
692
+ k: init_kwargs.get(k, default)
693
+ for k, default in parameters.items()
694
+ if k not in ignore and k not in new_kwargs
695
+ }
696
+ )
697
+
698
+ # Take note of the parameters that were not present in the loaded config
699
+ if len(set(new_kwargs.keys()) - set(init_kwargs)) > 0:
700
+ new_kwargs["_use_default_values"] = list(set(new_kwargs.keys()) - set(init_kwargs))
701
+
702
+ new_kwargs = {**config_init_kwargs, **new_kwargs}
703
+ getattr(self, "register_to_config")(**new_kwargs)
704
+ init(self, *args, **init_kwargs)
705
+
706
+ return inner_init
707
+
708
+
709
+ class LegacyConfigMixin(ConfigMixin):
710
+ r"""
711
+ A subclass of `ConfigMixin` to resolve class mapping from legacy classes (like `Transformer2DModel`) to more
712
+ pipeline-specific classes (like `DiTTransformer2DModel`).
713
+ """
714
+
715
+ @classmethod
716
+ def from_config(cls, config: FrozenDict | dict[str, Any] = None, return_unused_kwargs=False, **kwargs):
717
+ # To prevent dependency import problem.
718
+ from .models.model_loading_utils import _fetch_remapped_cls_from_config
719
+
720
+ # resolve remapping
721
+ remapped_class = _fetch_remapped_cls_from_config(config, cls)
722
+
723
+ if remapped_class is cls:
724
+ return super(LegacyConfigMixin, remapped_class).from_config(config, return_unused_kwargs, **kwargs)
725
+ else:
726
+ return remapped_class.from_config(config, return_unused_kwargs, **kwargs)
diffusers/dependency_versions_check.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from .dependency_versions_table import deps
16
+ from .utils.versions import require_version, require_version_core
17
+
18
+
19
+ # define which module versions we always want to check at run time
20
+ # (usually the ones defined in `install_requires` in setup.py)
21
+ #
22
+ # order specific notes:
23
+ # - tqdm must be checked before tokenizers
24
+
25
+ pkgs_to_check_at_runtime = "python requests filelock numpy".split()
26
+ for pkg in pkgs_to_check_at_runtime:
27
+ if pkg in deps:
28
+ require_version_core(deps[pkg])
29
+ else:
30
+ raise ValueError(f"can't find {pkg} in {deps.keys()}, check dependency_versions_table.py")
31
+
32
+
33
+ def dep_version_check(pkg, hint=None):
34
+ require_version(deps[pkg], hint)
diffusers/dependency_versions_table.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # THIS FILE HAS BEEN AUTOGENERATED. To update:
2
+ # 1. modify the `_deps` dict in setup.py
3
+ # 2. run `make deps_table_update`
4
+ deps = {
5
+ "Pillow": "Pillow",
6
+ "accelerate": "accelerate>=0.31.0",
7
+ "datasets": "datasets",
8
+ "filelock": "filelock",
9
+ "ftfy": "ftfy",
10
+ "hf-doc-builder": "hf-doc-builder>=0.3.0",
11
+ "huggingface-hub": "huggingface-hub>=1.31.0,<2.0",
12
+ "requests-mock": "requests-mock==1.10.0",
13
+ "importlib_metadata": "importlib_metadata",
14
+ "invisible-watermark": "invisible-watermark>=0.2.0",
15
+ "isort": "isort>=5.5.4",
16
+ "Jinja2": "Jinja2",
17
+ "torchsde": "torchsde",
18
+ "note_seq": "note_seq",
19
+ "librosa": "librosa",
20
+ "llvmlite": "llvmlite>=0.40.0",
21
+ "numba": "numba>=0.57.0",
22
+ "numpy": "numpy",
23
+ "parameterized": "parameterized",
24
+ "peft": "peft>=0.17.0",
25
+ "protobuf": "protobuf>=3.20.3,<4",
26
+ "pytest": "pytest",
27
+ "pytest-timeout": "pytest-timeout",
28
+ "pytest-xdist": "pytest-xdist",
29
+ "python": "python>=3.10.0",
30
+ "ruff": "ruff==0.9.10",
31
+ "safetensors": "safetensors>=0.8.0",
32
+ "sentencepiece": "sentencepiece>=0.1.91,!=0.1.92",
33
+ "GitPython": "GitPython<3.1.19",
34
+ "scipy": "scipy",
35
+ "onnx": "onnx",
36
+ "optimum_quanto": "optimum_quanto>=0.2.6",
37
+ "gguf": "gguf>=0.10.0",
38
+ "auto-round": "auto-round>=0.13.0",
39
+ "torchao": "torchao>=0.7.0",
40
+ "bitsandbytes": "bitsandbytes>=0.43.3",
41
+ "nvidia_modelopt[hf]": "nvidia_modelopt[hf]>=0.33.1",
42
+ "sdnq": "sdnq>=0.2.2",
43
+ "regex": "regex!=2019.12.17",
44
+ "requests": "requests",
45
+ "tensorboard": "tensorboard",
46
+ "tiktoken": "tiktoken>=0.7.0",
47
+ "torch": "torch>=2.6",
48
+ "torchvision": "torchvision",
49
+ "transformers": "transformers>=4.41.2",
50
+ "urllib3": "urllib3<=2.0.0",
51
+ "black": "black",
52
+ "phonemizer": "phonemizer",
53
+ "opencv-python": "opencv-python",
54
+ "timm": "timm",
55
+ "flashpack": "flashpack",
56
+ }
diffusers/experimental/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .rl import ValueGuidedRLPipeline
diffusers/experimental/rl/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .value_guided_sampling import ValueGuidedRLPipeline
diffusers/experimental/rl/value_guided_sampling.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import numpy as np
16
+ import torch
17
+ import tqdm
18
+
19
+ from ...models.unets.unet_1d import UNet1DModel
20
+ from ...pipelines import DiffusionPipeline
21
+ from ...utils.dummy_pt_objects import DDPMScheduler
22
+ from ...utils.torch_utils import randn_tensor
23
+
24
+
25
+ class ValueGuidedRLPipeline(DiffusionPipeline):
26
+ r"""
27
+ Pipeline for value-guided sampling from a diffusion model trained to predict sequences of states.
28
+
29
+ This model inherits from [`DiffusionPipeline`]. Check the superclass documentation for the generic methods
30
+ implemented for all pipelines (downloading, saving, running on a particular device, etc.).
31
+
32
+ Parameters:
33
+ value_function ([`UNet1DModel`]):
34
+ A specialized UNet for fine-tuning trajectories base on reward.
35
+ unet ([`UNet1DModel`]):
36
+ UNet architecture to denoise the encoded trajectories.
37
+ scheduler ([`SchedulerMixin`]):
38
+ A scheduler to be used in combination with `unet` to denoise the encoded trajectories. Default for this
39
+ application is [`DDPMScheduler`].
40
+ env ():
41
+ An environment following the OpenAI gym API to act in. For now only Hopper has pretrained models.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ value_function: UNet1DModel,
47
+ unet: UNet1DModel,
48
+ scheduler: DDPMScheduler,
49
+ env,
50
+ ):
51
+ super().__init__()
52
+
53
+ self.register_modules(value_function=value_function, unet=unet, scheduler=scheduler, env=env)
54
+
55
+ self.data = env.get_dataset()
56
+ self.means = {}
57
+ for key in self.data.keys():
58
+ try:
59
+ self.means[key] = self.data[key].mean()
60
+ except: # noqa: E722
61
+ pass
62
+ self.stds = {}
63
+ for key in self.data.keys():
64
+ try:
65
+ self.stds[key] = self.data[key].std()
66
+ except: # noqa: E722
67
+ pass
68
+ self.state_dim = env.observation_space.shape[0]
69
+ self.action_dim = env.action_space.shape[0]
70
+
71
+ def normalize(self, x_in, key):
72
+ return (x_in - self.means[key]) / self.stds[key]
73
+
74
+ def de_normalize(self, x_in, key):
75
+ return x_in * self.stds[key] + self.means[key]
76
+
77
+ def to_torch(self, x_in):
78
+ if isinstance(x_in, dict):
79
+ return {k: self.to_torch(v) for k, v in x_in.items()}
80
+ elif torch.is_tensor(x_in):
81
+ return x_in.to(self.unet.device)
82
+ return torch.tensor(x_in, device=self.unet.device)
83
+
84
+ def reset_x0(self, x_in, cond, act_dim):
85
+ for key, val in cond.items():
86
+ x_in[:, key, act_dim:] = val.clone()
87
+ return x_in
88
+
89
+ def run_diffusion(self, x, conditions, n_guide_steps, scale):
90
+ batch_size = x.shape[0]
91
+ y = None
92
+ for i in tqdm.tqdm(self.scheduler.timesteps):
93
+ # create batch of timesteps to pass into model
94
+ timesteps = torch.full((batch_size,), i, device=self.unet.device, dtype=torch.long)
95
+ for _ in range(n_guide_steps):
96
+ with torch.enable_grad():
97
+ x.requires_grad_()
98
+
99
+ # permute to match dimension for pre-trained models
100
+ y = self.value_function(x.permute(0, 2, 1), timesteps).sample
101
+ grad = torch.autograd.grad([y.sum()], [x])[0]
102
+
103
+ posterior_variance = self.scheduler._get_variance(i)
104
+ model_std = torch.exp(0.5 * posterior_variance)
105
+ grad = model_std * grad
106
+
107
+ grad[timesteps < 2] = 0
108
+ x = x.detach()
109
+ x = x + scale * grad
110
+ x = self.reset_x0(x, conditions, self.action_dim)
111
+
112
+ prev_x = self.unet(x.permute(0, 2, 1), timesteps).sample.permute(0, 2, 1)
113
+
114
+ # TODO: verify deprecation of this kwarg
115
+ x = self.scheduler.step(prev_x, i, x)["prev_sample"]
116
+
117
+ # apply conditions to the trajectory (set the initial state)
118
+ x = self.reset_x0(x, conditions, self.action_dim)
119
+ x = self.to_torch(x)
120
+ return x, y
121
+
122
+ def __call__(self, obs, batch_size=64, planning_horizon=32, n_guide_steps=2, scale=0.1):
123
+ # normalize the observations and create batch dimension
124
+ obs = self.normalize(obs, "observations")
125
+ obs = obs[None].repeat(batch_size, axis=0)
126
+
127
+ conditions = {0: self.to_torch(obs)}
128
+ shape = (batch_size, planning_horizon, self.state_dim + self.action_dim)
129
+
130
+ # generate initial noise and apply our conditions (to make the trajectories start at current state)
131
+ x1 = randn_tensor(shape, device=self.unet.device)
132
+ x = self.reset_x0(x1, conditions, self.action_dim)
133
+ x = self.to_torch(x)
134
+
135
+ # run the diffusion process
136
+ x, y = self.run_diffusion(x, conditions, n_guide_steps, scale)
137
+
138
+ # sort output trajectories by value
139
+ sorted_idx = y.argsort(0, descending=True).squeeze()
140
+ sorted_values = x[sorted_idx]
141
+ actions = sorted_values[:, :, : self.action_dim]
142
+ actions = actions.detach().cpu().numpy()
143
+ denorm_actions = self.de_normalize(actions, key="actions")
144
+
145
+ # select the action with the highest value
146
+ if y is not None:
147
+ selected_index = 0
148
+ else:
149
+ # if we didn't run value guiding, select a random action
150
+ selected_index = np.random.randint(0, batch_size)
151
+
152
+ denorm_actions = denorm_actions[selected_index, 0]
153
+ return denorm_actions
diffusers/guiders/__init__.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ..utils import is_torch_available, logging
17
+
18
+
19
+ if is_torch_available():
20
+ from .adaptive_projected_guidance import AdaptiveProjectedGuidance
21
+ from .adaptive_projected_guidance_mix import AdaptiveProjectedMixGuidance
22
+ from .auto_guidance import AutoGuidance
23
+ from .classifier_free_guidance import ClassifierFreeGuidance
24
+ from .classifier_free_zero_star_guidance import ClassifierFreeZeroStarGuidance
25
+ from .frequency_decoupled_guidance import FrequencyDecoupledGuidance
26
+ from .guider_utils import BaseGuidance
27
+ from .ltx2_guidance import LTX2Guidance
28
+ from .magnitude_aware_guidance import MagnitudeAwareGuidance
29
+ from .perturbed_attention_guidance import PerturbedAttentionGuidance
30
+ from .skip_layer_guidance import SkipLayerGuidance
31
+ from .smoothed_energy_guidance import SmoothedEnergyGuidance
32
+ from .tangential_classifier_free_guidance import TangentialClassifierFreeGuidance
diffusers/guiders/adaptive_projected_guidance.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from ..modular_pipelines.modular_pipeline import BlockState
28
+
29
+
30
+ class AdaptiveProjectedGuidance(BaseGuidance):
31
+ """
32
+ Adaptive Projected Guidance (APG): https://huggingface.co/papers/2410.02416
33
+
34
+ Args:
35
+ guidance_scale (`float`, defaults to `7.5`):
36
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
37
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
38
+ deterioration of image quality.
39
+ adaptive_projected_guidance_momentum (`float`, defaults to `None`):
40
+ The momentum parameter for the adaptive projected guidance. Disabled if set to `None`.
41
+ adaptive_projected_guidance_rescale (`float`, defaults to `15.0`):
42
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
43
+ adaptive_projected_guidance_norm_dim (`int` or `tuple[int]`, *optional*):
44
+ Dimension(s) over which to compute the APG norm and projection. If omitted, all non-batch dimensions are
45
+ used, preserving the original behavior.
46
+ guidance_rescale (`float`, defaults to `0.0`):
47
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
48
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
49
+ Flawed](https://huggingface.co/papers/2305.08891).
50
+ use_original_formulation (`bool`, defaults to `False`):
51
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
52
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
53
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
54
+ start (`float`, defaults to `0.0`):
55
+ The fraction of the total number of denoising steps after which guidance starts.
56
+ stop (`float`, defaults to `1.0`):
57
+ The fraction of the total number of denoising steps after which guidance stops.
58
+ """
59
+
60
+ _input_predictions = ["pred_cond", "pred_uncond"]
61
+
62
+ @register_to_config
63
+ def __init__(
64
+ self,
65
+ guidance_scale: float = 7.5,
66
+ adaptive_projected_guidance_momentum: float | None = None,
67
+ adaptive_projected_guidance_rescale: float = 15.0,
68
+ adaptive_projected_guidance_norm_dim: int | tuple[int, ...] | None = None,
69
+ eta: float = 1.0,
70
+ guidance_rescale: float = 0.0,
71
+ use_original_formulation: bool = False,
72
+ start: float = 0.0,
73
+ stop: float = 1.0,
74
+ enabled: bool = True,
75
+ ):
76
+ super().__init__(start, stop, enabled)
77
+
78
+ self.guidance_scale = guidance_scale
79
+ self.adaptive_projected_guidance_momentum = adaptive_projected_guidance_momentum
80
+ self.adaptive_projected_guidance_rescale = adaptive_projected_guidance_rescale
81
+ self.adaptive_projected_guidance_norm_dim = adaptive_projected_guidance_norm_dim
82
+ self.eta = eta
83
+ self.guidance_rescale = guidance_rescale
84
+ self.use_original_formulation = use_original_formulation
85
+ self.momentum_buffer = None
86
+
87
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
88
+ if self._step == 0:
89
+ if self.adaptive_projected_guidance_momentum is not None:
90
+ self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
91
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
92
+ data_batches = []
93
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
94
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
95
+ data_batches.append(data_batch)
96
+ return data_batches
97
+
98
+ def prepare_inputs_from_block_state(
99
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
100
+ ) -> list["BlockState"]:
101
+ if self._step == 0:
102
+ if self.adaptive_projected_guidance_momentum is not None:
103
+ self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
104
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
105
+ data_batches = []
106
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
107
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
108
+ data_batches.append(data_batch)
109
+ return data_batches
110
+
111
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
112
+ pred = None
113
+
114
+ if not self._is_apg_enabled():
115
+ pred = pred_cond
116
+ else:
117
+ pred = normalized_guidance(
118
+ pred_cond,
119
+ pred_uncond,
120
+ self.guidance_scale,
121
+ self.momentum_buffer,
122
+ self.eta,
123
+ self.adaptive_projected_guidance_rescale,
124
+ self.use_original_formulation,
125
+ self.adaptive_projected_guidance_norm_dim,
126
+ )
127
+
128
+ if self.guidance_rescale > 0.0:
129
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
130
+
131
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
132
+
133
+ @property
134
+ def is_conditional(self) -> bool:
135
+ return self._count_prepared == 1
136
+
137
+ @property
138
+ def num_conditions(self) -> int:
139
+ num_conditions = 1
140
+ if self._is_apg_enabled():
141
+ num_conditions += 1
142
+ return num_conditions
143
+
144
+ def _is_apg_enabled(self) -> bool:
145
+ if not self._enabled:
146
+ return False
147
+
148
+ is_within_range = True
149
+ if self._num_inference_steps is not None:
150
+ skip_start_step = int(self._start * self._num_inference_steps)
151
+ skip_stop_step = int(self._stop * self._num_inference_steps)
152
+ is_within_range = skip_start_step <= self._step < skip_stop_step
153
+
154
+ is_close = False
155
+ if self.use_original_formulation:
156
+ is_close = math.isclose(self.guidance_scale, 0.0)
157
+ else:
158
+ is_close = math.isclose(self.guidance_scale, 1.0)
159
+
160
+ return is_within_range and not is_close
161
+
162
+
163
+ class MomentumBuffer:
164
+ def __init__(self, momentum: float):
165
+ self.momentum = momentum
166
+ self.running_average = 0
167
+
168
+ def update(self, update_value: torch.Tensor):
169
+ new_average = self.momentum * self.running_average
170
+ self.running_average = update_value + new_average
171
+
172
+ def __repr__(self) -> str:
173
+ """
174
+ Returns a string representation showing momentum, shape, statistics, and a slice of the running_average.
175
+ """
176
+ if isinstance(self.running_average, torch.Tensor):
177
+ shape = tuple(self.running_average.shape)
178
+
179
+ # Calculate statistics
180
+ with torch.no_grad():
181
+ stats = {
182
+ "mean": self.running_average.mean().item(),
183
+ "std": self.running_average.std().item(),
184
+ "min": self.running_average.min().item(),
185
+ "max": self.running_average.max().item(),
186
+ }
187
+
188
+ # Get a slice (max 3 elements per dimension)
189
+ slice_indices = tuple(slice(None, min(3, dim)) for dim in shape)
190
+ sliced_data = self.running_average[slice_indices]
191
+
192
+ # Format the slice for display (convert to float32 for numpy compatibility with bfloat16)
193
+ slice_str = str(sliced_data.detach().float().cpu().numpy())
194
+ if len(slice_str) > 200: # Truncate if too long
195
+ slice_str = slice_str[:200] + "..."
196
+
197
+ stats_str = ", ".join([f"{k}={v:.4f}" for k, v in stats.items()])
198
+
199
+ return (
200
+ f"MomentumBuffer(\n"
201
+ f" momentum={self.momentum},\n"
202
+ f" shape={shape},\n"
203
+ f" stats=[{stats_str}],\n"
204
+ f" slice={slice_str}\n"
205
+ f")"
206
+ )
207
+ else:
208
+ return f"MomentumBuffer(momentum={self.momentum}, running_average={self.running_average})"
209
+
210
+
211
+ def normalized_guidance(
212
+ pred_cond: torch.Tensor,
213
+ pred_uncond: torch.Tensor,
214
+ guidance_scale: float,
215
+ momentum_buffer: MomentumBuffer | None = None,
216
+ eta: float = 1.0,
217
+ norm_threshold: float = 0.0,
218
+ use_original_formulation: bool = False,
219
+ norm_dim: int | tuple[int, ...] | None = None,
220
+ ):
221
+ diff = pred_cond - pred_uncond
222
+ if norm_dim is None:
223
+ dim = [-i for i in range(1, len(diff.shape))]
224
+ elif isinstance(norm_dim, int):
225
+ dim = [norm_dim]
226
+ else:
227
+ dim = list(norm_dim)
228
+
229
+ if momentum_buffer is not None:
230
+ momentum_buffer.update(diff)
231
+ diff = momentum_buffer.running_average
232
+
233
+ if norm_threshold > 0:
234
+ ones = torch.ones_like(diff)
235
+ diff_norm = diff.norm(p=2, dim=dim, keepdim=True)
236
+ scale_factor = torch.minimum(ones, norm_threshold / diff_norm)
237
+ diff = diff * scale_factor
238
+
239
+ if diff.device.type in {"mps", "npu"}:
240
+ v0, v1 = diff.cpu().double(), pred_cond.cpu().double()
241
+ else:
242
+ v0, v1 = diff.double(), pred_cond.double()
243
+ v1 = torch.nn.functional.normalize(v1, dim=dim)
244
+ v0_parallel = (v0 * v1).sum(dim=dim, keepdim=True) * v1
245
+ v0_orthogonal = v0 - v0_parallel
246
+ diff_parallel = v0_parallel.to(device=diff.device, dtype=diff.dtype)
247
+ diff_orthogonal = v0_orthogonal.to(device=diff.device, dtype=diff.dtype)
248
+ normalized_update = diff_orthogonal + eta * diff_parallel
249
+
250
+ pred = pred_cond if use_original_formulation else pred_uncond
251
+ pred = pred + guidance_scale * normalized_update
252
+
253
+ return pred
diffusers/guiders/adaptive_projected_guidance_mix.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import math
16
+ from typing import TYPE_CHECKING
17
+
18
+ import torch
19
+
20
+ from ..configuration_utils import register_to_config
21
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from ..modular_pipelines.modular_pipeline import BlockState
26
+
27
+
28
+ class AdaptiveProjectedMixGuidance(BaseGuidance):
29
+ """
30
+ Adaptive Projected Guidance (APG) https://huggingface.co/papers/2410.02416 combined with Classifier-Free Guidance
31
+ (CFG). This guider is used in HunyuanImage2.1 https://github.com/Tencent-Hunyuan/HunyuanImage-2.1
32
+
33
+ Args:
34
+ guidance_scale (`float`, defaults to `7.5`):
35
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
36
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
37
+ deterioration of image quality.
38
+ adaptive_projected_guidance_momentum (`float`, defaults to `None`):
39
+ The momentum parameter for the adaptive projected guidance. Disabled if set to `None`.
40
+ adaptive_projected_guidance_rescale (`float`, defaults to `15.0`):
41
+ The rescale factor applied to the noise predictions for adaptive projected guidance. This is used to
42
+ improve image quality and fix
43
+ guidance_rescale (`float`, defaults to `0.0`):
44
+ The rescale factor applied to the noise predictions for classifier-free guidance. This is used to improve
45
+ image quality and fix overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample
46
+ Steps are Flawed](https://huggingface.co/papers/2305.08891).
47
+ use_original_formulation (`bool`, defaults to `False`):
48
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
49
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
50
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
51
+ start (`float`, defaults to `0.0`):
52
+ The fraction of the total number of denoising steps after which the classifier-free guidance starts.
53
+ stop (`float`, defaults to `1.0`):
54
+ The fraction of the total number of denoising steps after which the classifier-free guidance stops.
55
+ adaptive_projected_guidance_start_step (`int`, defaults to `5`):
56
+ The step at which the adaptive projected guidance starts (before this step, classifier-free guidance is
57
+ used, and momentum buffer is updated).
58
+ enabled (`bool`, defaults to `True`):
59
+ Whether this guidance is enabled.
60
+ """
61
+
62
+ _input_predictions = ["pred_cond", "pred_uncond"]
63
+
64
+ @register_to_config
65
+ def __init__(
66
+ self,
67
+ guidance_scale: float = 3.5,
68
+ guidance_rescale: float = 0.0,
69
+ adaptive_projected_guidance_scale: float = 10.0,
70
+ adaptive_projected_guidance_momentum: float = -0.5,
71
+ adaptive_projected_guidance_rescale: float = 10.0,
72
+ eta: float = 0.0,
73
+ use_original_formulation: bool = False,
74
+ start: float = 0.0,
75
+ stop: float = 1.0,
76
+ adaptive_projected_guidance_start_step: int = 5,
77
+ enabled: bool = True,
78
+ ):
79
+ super().__init__(start, stop, enabled)
80
+
81
+ self.guidance_scale = guidance_scale
82
+ self.guidance_rescale = guidance_rescale
83
+ self.adaptive_projected_guidance_scale = adaptive_projected_guidance_scale
84
+ self.adaptive_projected_guidance_momentum = adaptive_projected_guidance_momentum
85
+ self.adaptive_projected_guidance_rescale = adaptive_projected_guidance_rescale
86
+ self.eta = eta
87
+ self.adaptive_projected_guidance_start_step = adaptive_projected_guidance_start_step
88
+ self.use_original_formulation = use_original_formulation
89
+ self.momentum_buffer = None
90
+
91
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
92
+ if self._step == 0:
93
+ if self.adaptive_projected_guidance_momentum is not None:
94
+ self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
95
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
96
+ data_batches = []
97
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
98
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
99
+ data_batches.append(data_batch)
100
+ return data_batches
101
+
102
+ def prepare_inputs_from_block_state(
103
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
104
+ ) -> list["BlockState"]:
105
+ if self._step == 0:
106
+ if self.adaptive_projected_guidance_momentum is not None:
107
+ self.momentum_buffer = MomentumBuffer(self.adaptive_projected_guidance_momentum)
108
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
109
+ data_batches = []
110
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
111
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
112
+ data_batches.append(data_batch)
113
+ return data_batches
114
+
115
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
116
+ pred = None
117
+
118
+ # no guidance
119
+ if not self._is_cfg_enabled():
120
+ pred = pred_cond
121
+
122
+ # CFG + update momentum buffer
123
+ elif not self._is_apg_enabled():
124
+ if self.momentum_buffer is not None:
125
+ update_momentum_buffer(pred_cond, pred_uncond, self.momentum_buffer)
126
+ # CFG + update momentum buffer
127
+ shift = pred_cond - pred_uncond
128
+ pred = pred_cond if self.use_original_formulation else pred_uncond
129
+ pred = pred + self.guidance_scale * shift
130
+
131
+ # APG
132
+ elif self._is_apg_enabled():
133
+ pred = normalized_guidance(
134
+ pred_cond,
135
+ pred_uncond,
136
+ self.adaptive_projected_guidance_scale,
137
+ self.momentum_buffer,
138
+ self.eta,
139
+ self.adaptive_projected_guidance_rescale,
140
+ self.use_original_formulation,
141
+ )
142
+
143
+ if self.guidance_rescale > 0.0:
144
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
145
+
146
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
147
+
148
+ @property
149
+ def is_conditional(self) -> bool:
150
+ return self._count_prepared == 1
151
+
152
+ @property
153
+ def num_conditions(self) -> int:
154
+ num_conditions = 1
155
+ if self._is_apg_enabled() or self._is_cfg_enabled():
156
+ num_conditions += 1
157
+ return num_conditions
158
+
159
+ # Copied from diffusers.guiders.classifier_free_guidance.ClassifierFreeGuidance._is_cfg_enabled
160
+ def _is_cfg_enabled(self) -> bool:
161
+ if not self._enabled:
162
+ return False
163
+
164
+ is_within_range = True
165
+ if self._num_inference_steps is not None:
166
+ skip_start_step = int(self._start * self._num_inference_steps)
167
+ skip_stop_step = int(self._stop * self._num_inference_steps)
168
+ is_within_range = skip_start_step <= self._step < skip_stop_step
169
+
170
+ is_close = False
171
+ if self.use_original_formulation:
172
+ is_close = math.isclose(self.guidance_scale, 0.0)
173
+ else:
174
+ is_close = math.isclose(self.guidance_scale, 1.0)
175
+
176
+ return is_within_range and not is_close
177
+
178
+ def _is_apg_enabled(self) -> bool:
179
+ if not self._enabled:
180
+ return False
181
+
182
+ if not self._is_cfg_enabled():
183
+ return False
184
+
185
+ is_within_range = False
186
+ if self._step is not None:
187
+ is_within_range = self._step > self.adaptive_projected_guidance_start_step
188
+
189
+ is_close = False
190
+ if self.use_original_formulation:
191
+ is_close = math.isclose(self.adaptive_projected_guidance_scale, 0.0)
192
+ else:
193
+ is_close = math.isclose(self.adaptive_projected_guidance_scale, 1.0)
194
+
195
+ return is_within_range and not is_close
196
+
197
+ def get_state(self):
198
+ state = super().get_state()
199
+ state["momentum_buffer"] = self.momentum_buffer
200
+ state["is_apg_enabled"] = self._is_apg_enabled()
201
+ state["is_cfg_enabled"] = self._is_cfg_enabled()
202
+ return state
203
+
204
+
205
+ # Copied from diffusers.guiders.adaptive_projected_guidance.MomentumBuffer
206
+ class MomentumBuffer:
207
+ def __init__(self, momentum: float):
208
+ self.momentum = momentum
209
+ self.running_average = 0
210
+
211
+ def update(self, update_value: torch.Tensor):
212
+ new_average = self.momentum * self.running_average
213
+ self.running_average = update_value + new_average
214
+
215
+ def __repr__(self) -> str:
216
+ """
217
+ Returns a string representation showing momentum, shape, statistics, and a slice of the running_average.
218
+ """
219
+ if isinstance(self.running_average, torch.Tensor):
220
+ shape = tuple(self.running_average.shape)
221
+
222
+ # Calculate statistics
223
+ with torch.no_grad():
224
+ stats = {
225
+ "mean": self.running_average.mean().item(),
226
+ "std": self.running_average.std().item(),
227
+ "min": self.running_average.min().item(),
228
+ "max": self.running_average.max().item(),
229
+ }
230
+
231
+ # Get a slice (max 3 elements per dimension)
232
+ slice_indices = tuple(slice(None, min(3, dim)) for dim in shape)
233
+ sliced_data = self.running_average[slice_indices]
234
+
235
+ # Format the slice for display (convert to float32 for numpy compatibility with bfloat16)
236
+ slice_str = str(sliced_data.detach().float().cpu().numpy())
237
+ if len(slice_str) > 200: # Truncate if too long
238
+ slice_str = slice_str[:200] + "..."
239
+
240
+ stats_str = ", ".join([f"{k}={v:.4f}" for k, v in stats.items()])
241
+
242
+ return (
243
+ f"MomentumBuffer(\n"
244
+ f" momentum={self.momentum},\n"
245
+ f" shape={shape},\n"
246
+ f" stats=[{stats_str}],\n"
247
+ f" slice={slice_str}\n"
248
+ f")"
249
+ )
250
+ else:
251
+ return f"MomentumBuffer(momentum={self.momentum}, running_average={self.running_average})"
252
+
253
+
254
+ def update_momentum_buffer(
255
+ pred_cond: torch.Tensor,
256
+ pred_uncond: torch.Tensor,
257
+ momentum_buffer: MomentumBuffer | None = None,
258
+ ):
259
+ diff = pred_cond - pred_uncond
260
+ if momentum_buffer is not None:
261
+ momentum_buffer.update(diff)
262
+
263
+
264
+ def normalized_guidance(
265
+ pred_cond: torch.Tensor,
266
+ pred_uncond: torch.Tensor,
267
+ guidance_scale: float,
268
+ momentum_buffer: MomentumBuffer | None = None,
269
+ eta: float = 1.0,
270
+ norm_threshold: float = 0.0,
271
+ use_original_formulation: bool = False,
272
+ ):
273
+ if momentum_buffer is not None:
274
+ update_momentum_buffer(pred_cond, pred_uncond, momentum_buffer)
275
+ diff = momentum_buffer.running_average
276
+ else:
277
+ diff = pred_cond - pred_uncond
278
+
279
+ dim = [-i for i in range(1, len(diff.shape))]
280
+
281
+ if norm_threshold > 0:
282
+ ones = torch.ones_like(diff)
283
+ diff_norm = diff.norm(p=2, dim=dim, keepdim=True)
284
+ scale_factor = torch.minimum(ones, norm_threshold / diff_norm)
285
+ diff = diff * scale_factor
286
+
287
+ v0, v1 = diff.double(), pred_cond.double()
288
+ v1 = torch.nn.functional.normalize(v1, dim=dim)
289
+ v0_parallel = (v0 * v1).sum(dim=dim, keepdim=True) * v1
290
+ v0_orthogonal = v0 - v0_parallel
291
+ diff_parallel, diff_orthogonal = v0_parallel.type_as(diff), v0_orthogonal.type_as(diff)
292
+ normalized_update = diff_orthogonal + eta * diff_parallel
293
+
294
+ pred = pred_cond if use_original_formulation else pred_uncond
295
+ pred = pred + guidance_scale * normalized_update
296
+
297
+ return pred
diffusers/guiders/auto_guidance.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from ..hooks import HookRegistry, LayerSkipConfig
24
+ from ..hooks.layer_skip import _apply_layer_skip_hook
25
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from ..modular_pipelines.modular_pipeline import BlockState
30
+
31
+
32
+ class AutoGuidance(BaseGuidance):
33
+ """
34
+ AutoGuidance: https://huggingface.co/papers/2406.02507
35
+
36
+ Args:
37
+ guidance_scale (`float`, defaults to `7.5`):
38
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
39
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
40
+ deterioration of image quality.
41
+ auto_guidance_layers (`int` or `list[int]`, *optional*):
42
+ The layer indices to apply skip layer guidance to. Can be a single integer or a list of integers. If not
43
+ provided, `skip_layer_config` must be provided.
44
+ auto_guidance_config (`LayerSkipConfig` or `list[LayerSkipConfig]`, *optional*):
45
+ The configuration for the skip layer guidance. Can be a single `LayerSkipConfig` or a list of
46
+ `LayerSkipConfig`. If not provided, `skip_layer_guidance_layers` must be provided.
47
+ dropout (`float`, *optional*):
48
+ The dropout probability for autoguidance on the enabled skip layers (either with `auto_guidance_layers` or
49
+ `auto_guidance_config`). If not provided, the dropout probability will be set to 1.0.
50
+ guidance_rescale (`float`, defaults to `0.0`):
51
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
52
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
53
+ Flawed](https://huggingface.co/papers/2305.08891).
54
+ use_original_formulation (`bool`, defaults to `False`):
55
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
56
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
57
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
58
+ start (`float`, defaults to `0.0`):
59
+ The fraction of the total number of denoising steps after which guidance starts.
60
+ stop (`float`, defaults to `1.0`):
61
+ The fraction of the total number of denoising steps after which guidance stops.
62
+ """
63
+
64
+ _input_predictions = ["pred_cond", "pred_uncond"]
65
+
66
+ @register_to_config
67
+ def __init__(
68
+ self,
69
+ guidance_scale: float = 7.5,
70
+ auto_guidance_layers: int | list[int] | None = None,
71
+ auto_guidance_config: LayerSkipConfig | list[LayerSkipConfig] | dict[str, Any] = None,
72
+ dropout: float | None = None,
73
+ guidance_rescale: float = 0.0,
74
+ use_original_formulation: bool = False,
75
+ start: float = 0.0,
76
+ stop: float = 1.0,
77
+ enabled: bool = True,
78
+ ):
79
+ super().__init__(start, stop, enabled)
80
+
81
+ self.guidance_scale = guidance_scale
82
+ self.auto_guidance_layers = auto_guidance_layers
83
+ self.auto_guidance_config = auto_guidance_config
84
+ self.dropout = dropout
85
+ self.guidance_rescale = guidance_rescale
86
+ self.use_original_formulation = use_original_formulation
87
+
88
+ is_layer_or_config_provided = auto_guidance_layers is not None or auto_guidance_config is not None
89
+ is_layer_and_config_provided = auto_guidance_layers is not None and auto_guidance_config is not None
90
+ if not is_layer_or_config_provided:
91
+ raise ValueError(
92
+ "Either `auto_guidance_layers` or `auto_guidance_config` must be provided to enable AutoGuidance."
93
+ )
94
+ if is_layer_and_config_provided:
95
+ raise ValueError("Only one of `auto_guidance_layers` or `auto_guidance_config` can be provided.")
96
+ if auto_guidance_config is None and dropout is None:
97
+ raise ValueError("`dropout` must be provided if `auto_guidance_layers` is provided.")
98
+
99
+ if auto_guidance_layers is not None:
100
+ if isinstance(auto_guidance_layers, int):
101
+ auto_guidance_layers = [auto_guidance_layers]
102
+ if not isinstance(auto_guidance_layers, list):
103
+ raise ValueError(
104
+ f"Expected `auto_guidance_layers` to be an int or a list of ints, but got {type(auto_guidance_layers)}."
105
+ )
106
+ auto_guidance_config = [
107
+ LayerSkipConfig(layer, fqn="auto", dropout=dropout) for layer in auto_guidance_layers
108
+ ]
109
+
110
+ if isinstance(auto_guidance_config, dict):
111
+ auto_guidance_config = LayerSkipConfig.from_dict(auto_guidance_config)
112
+
113
+ if isinstance(auto_guidance_config, LayerSkipConfig):
114
+ auto_guidance_config = [auto_guidance_config]
115
+
116
+ if not isinstance(auto_guidance_config, list):
117
+ raise ValueError(
118
+ f"Expected `auto_guidance_config` to be a LayerSkipConfig or a list of LayerSkipConfig, but got {type(auto_guidance_config)}."
119
+ )
120
+ elif isinstance(next(iter(auto_guidance_config), None), dict):
121
+ auto_guidance_config = [LayerSkipConfig.from_dict(config) for config in auto_guidance_config]
122
+
123
+ self.auto_guidance_config = auto_guidance_config
124
+ self._auto_guidance_hook_names = [f"AutoGuidance_{i}" for i in range(len(self.auto_guidance_config))]
125
+
126
+ def prepare_models(self, denoiser: torch.nn.Module) -> None:
127
+ self._count_prepared += 1
128
+ if self._is_ag_enabled() and self.is_unconditional:
129
+ for name, config in zip(self._auto_guidance_hook_names, self.auto_guidance_config):
130
+ _apply_layer_skip_hook(denoiser, config, name=name)
131
+
132
+ def cleanup_models(self, denoiser: torch.nn.Module) -> None:
133
+ if self._is_ag_enabled() and self.is_unconditional:
134
+ for name in self._auto_guidance_hook_names:
135
+ registry = HookRegistry.check_if_exists_or_initialize(denoiser)
136
+ registry.remove_hook(name, recurse=True)
137
+
138
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
139
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
140
+ data_batches = []
141
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
142
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
143
+ data_batches.append(data_batch)
144
+ return data_batches
145
+
146
+ def prepare_inputs_from_block_state(
147
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
148
+ ) -> list["BlockState"]:
149
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
150
+ data_batches = []
151
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
152
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
153
+ data_batches.append(data_batch)
154
+ return data_batches
155
+
156
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
157
+ pred = None
158
+
159
+ if not self._is_ag_enabled():
160
+ pred = pred_cond
161
+ else:
162
+ shift = pred_cond - pred_uncond
163
+ pred = pred_cond if self.use_original_formulation else pred_uncond
164
+ pred = pred + self.guidance_scale * shift
165
+
166
+ if self.guidance_rescale > 0.0:
167
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
168
+
169
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
170
+
171
+ @property
172
+ def is_conditional(self) -> bool:
173
+ return self._count_prepared == 1
174
+
175
+ @property
176
+ def num_conditions(self) -> int:
177
+ num_conditions = 1
178
+ if self._is_ag_enabled():
179
+ num_conditions += 1
180
+ return num_conditions
181
+
182
+ def _is_ag_enabled(self) -> bool:
183
+ if not self._enabled:
184
+ return False
185
+
186
+ is_within_range = True
187
+ if self._num_inference_steps is not None:
188
+ skip_start_step = int(self._start * self._num_inference_steps)
189
+ skip_stop_step = int(self._stop * self._num_inference_steps)
190
+ is_within_range = skip_start_step <= self._step < skip_stop_step
191
+
192
+ is_close = False
193
+ if self.use_original_formulation:
194
+ is_close = math.isclose(self.guidance_scale, 0.0)
195
+ else:
196
+ is_close = math.isclose(self.guidance_scale, 1.0)
197
+
198
+ return is_within_range and not is_close
diffusers/guiders/classifier_free_guidance.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from ..modular_pipelines.modular_pipeline import BlockState
28
+
29
+
30
+ class ClassifierFreeGuidance(BaseGuidance):
31
+ """
32
+ Implements Classifier-Free Guidance (CFG) for diffusion models.
33
+
34
+ Reference: https://huggingface.co/papers/2207.12598
35
+
36
+ CFG improves generation quality and prompt adherence by jointly training models on both conditional and
37
+ unconditional data, then combining predictions during inference. This allows trading off between quality (high
38
+ guidance) and diversity (low guidance).
39
+
40
+ **Two CFG Formulations:**
41
+
42
+ 1. **Original formulation** (from paper):
43
+ ```
44
+ x_pred = x_cond + guidance_scale * (x_cond - x_uncond)
45
+ ```
46
+ Moves conditional predictions further from unconditional ones.
47
+
48
+ 2. **Diffusers-native formulation** (default, from Imagen paper):
49
+ ```
50
+ x_pred = x_uncond + guidance_scale * (x_cond - x_uncond)
51
+ ```
52
+ Moves unconditional predictions toward conditional ones, effectively suppressing negative features (e.g., "bad
53
+ quality", "watermarks"). Equivalent in theory but more intuitive.
54
+
55
+ Use `use_original_formulation=True` to switch to the original formulation.
56
+
57
+ Args:
58
+ guidance_scale (`float`, defaults to `7.5`):
59
+ CFG scale applied by this guider during post-processing. Higher values = stronger prompt conditioning but
60
+ may reduce quality. Typical range: 1.0-20.0.
61
+ guidance_rescale (`float`, defaults to `0.0`):
62
+ Rescaling factor to prevent overexposure from high guidance scales. Based on [Common Diffusion Noise
63
+ Schedules and Sample Steps are Flawed](https://huggingface.co/papers/2305.08891). Range: 0.0 (no rescaling)
64
+ to 1.0 (full rescaling).
65
+ use_original_formulation (`bool`, defaults to `False`):
66
+ If `True`, uses the original CFG formulation from the paper. If `False` (default), uses the
67
+ diffusers-native formulation from the Imagen paper.
68
+ start (`float`, defaults to `0.0`):
69
+ Fraction of denoising steps (0.0-1.0) after which CFG starts. Use > 0.0 to disable CFG in early denoising
70
+ steps.
71
+ stop (`float`, defaults to `1.0`):
72
+ Fraction of denoising steps (0.0-1.0) after which CFG stops. Use < 1.0 to disable CFG in late denoising
73
+ steps.
74
+ enabled (`bool`, defaults to `True`):
75
+ Whether CFG is enabled. Set to `False` to disable CFG entirely (uses only conditional predictions).
76
+ """
77
+
78
+ _input_predictions = ["pred_cond", "pred_uncond"]
79
+
80
+ @register_to_config
81
+ def __init__(
82
+ self,
83
+ guidance_scale: float = 7.5,
84
+ guidance_rescale: float = 0.0,
85
+ use_original_formulation: bool = False,
86
+ start: float = 0.0,
87
+ stop: float = 1.0,
88
+ enabled: bool = True,
89
+ ):
90
+ super().__init__(start, stop, enabled)
91
+
92
+ self.guidance_scale = guidance_scale
93
+ self.guidance_rescale = guidance_rescale
94
+ self.use_original_formulation = use_original_formulation
95
+
96
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
97
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
98
+ data_batches = []
99
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
100
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
101
+ data_batches.append(data_batch)
102
+ return data_batches
103
+
104
+ def prepare_inputs_from_block_state(
105
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
106
+ ) -> list["BlockState"]:
107
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
108
+ data_batches = []
109
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
110
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
111
+ data_batches.append(data_batch)
112
+ return data_batches
113
+
114
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
115
+ pred = None
116
+
117
+ if not self._is_cfg_enabled():
118
+ pred = pred_cond
119
+ else:
120
+ shift = pred_cond - pred_uncond
121
+ pred = pred_cond if self.use_original_formulation else pred_uncond
122
+ pred = pred + self.guidance_scale * shift
123
+
124
+ if self.guidance_rescale > 0.0:
125
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
126
+
127
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
128
+
129
+ @property
130
+ def is_conditional(self) -> bool:
131
+ return self._count_prepared == 1
132
+
133
+ @property
134
+ def num_conditions(self) -> int:
135
+ num_conditions = 1
136
+ if self._is_cfg_enabled():
137
+ num_conditions += 1
138
+ return num_conditions
139
+
140
+ def _is_cfg_enabled(self) -> bool:
141
+ if not self._enabled:
142
+ return False
143
+
144
+ is_within_range = True
145
+ if self._num_inference_steps is not None:
146
+ skip_start_step = int(self._start * self._num_inference_steps)
147
+ skip_stop_step = int(self._stop * self._num_inference_steps)
148
+ is_within_range = skip_start_step <= self._step < skip_stop_step
149
+
150
+ is_close = False
151
+ if self.use_original_formulation:
152
+ is_close = math.isclose(self.guidance_scale, 0.0)
153
+ else:
154
+ is_close = math.isclose(self.guidance_scale, 1.0)
155
+
156
+ return is_within_range and not is_close
diffusers/guiders/classifier_free_zero_star_guidance.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from ..modular_pipelines.modular_pipeline import BlockState
28
+
29
+
30
+ class ClassifierFreeZeroStarGuidance(BaseGuidance):
31
+ """
32
+ Classifier-free Zero* (CFG-Zero*): https://huggingface.co/papers/2503.18886
33
+
34
+ This is an implementation of the Classifier-Free Zero* guidance technique, which is a variant of classifier-free
35
+ guidance. It proposes zero initialization of the noise predictions for the first few steps of the diffusion
36
+ process, and also introduces an optimal rescaling factor for the noise predictions, which can help in improving the
37
+ quality of generated images.
38
+
39
+ The authors of the paper suggest setting zero initialization in the first 4% of the inference steps.
40
+
41
+ Args:
42
+ guidance_scale (`float`, defaults to `7.5`):
43
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
44
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
45
+ deterioration of image quality.
46
+ zero_init_steps (`int`, defaults to `1`):
47
+ The number of inference steps for which the noise predictions are zeroed out (see Section 4.2).
48
+ guidance_rescale (`float`, defaults to `0.0`):
49
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
50
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
51
+ Flawed](https://huggingface.co/papers/2305.08891).
52
+ use_original_formulation (`bool`, defaults to `False`):
53
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
54
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
55
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
56
+ start (`float`, defaults to `0.01`):
57
+ The fraction of the total number of denoising steps after which guidance starts.
58
+ stop (`float`, defaults to `0.2`):
59
+ The fraction of the total number of denoising steps after which guidance stops.
60
+ """
61
+
62
+ _input_predictions = ["pred_cond", "pred_uncond"]
63
+
64
+ @register_to_config
65
+ def __init__(
66
+ self,
67
+ guidance_scale: float = 7.5,
68
+ zero_init_steps: int = 1,
69
+ guidance_rescale: float = 0.0,
70
+ use_original_formulation: bool = False,
71
+ start: float = 0.0,
72
+ stop: float = 1.0,
73
+ enabled: bool = True,
74
+ ):
75
+ super().__init__(start, stop, enabled)
76
+
77
+ self.guidance_scale = guidance_scale
78
+ self.zero_init_steps = zero_init_steps
79
+ self.guidance_rescale = guidance_rescale
80
+ self.use_original_formulation = use_original_formulation
81
+
82
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
83
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
84
+ data_batches = []
85
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
86
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
87
+ data_batches.append(data_batch)
88
+ return data_batches
89
+
90
+ def prepare_inputs_from_block_state(
91
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
92
+ ) -> list["BlockState"]:
93
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
94
+ data_batches = []
95
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
96
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
97
+ data_batches.append(data_batch)
98
+ return data_batches
99
+
100
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
101
+ pred = None
102
+
103
+ # YiYi Notes: add default behavior for self._enabled == False
104
+ if not self._enabled:
105
+ pred = pred_cond
106
+
107
+ elif self._step < self.zero_init_steps:
108
+ pred = torch.zeros_like(pred_cond)
109
+ elif not self._is_cfg_enabled():
110
+ pred = pred_cond
111
+ else:
112
+ pred_cond_flat = pred_cond.flatten(1)
113
+ pred_uncond_flat = pred_uncond.flatten(1)
114
+ alpha = cfg_zero_star_scale(pred_cond_flat, pred_uncond_flat)
115
+ alpha = alpha.view(-1, *(1,) * (len(pred_cond.shape) - 1))
116
+ pred_uncond = pred_uncond * alpha
117
+ shift = pred_cond - pred_uncond
118
+ pred = pred_cond if self.use_original_formulation else pred_uncond
119
+ pred = pred + self.guidance_scale * shift
120
+
121
+ if self.guidance_rescale > 0.0:
122
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
123
+
124
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
125
+
126
+ @property
127
+ def is_conditional(self) -> bool:
128
+ return self._count_prepared == 1
129
+
130
+ @property
131
+ def num_conditions(self) -> int:
132
+ num_conditions = 1
133
+ if self._is_cfg_enabled():
134
+ num_conditions += 1
135
+ return num_conditions
136
+
137
+ def _is_cfg_enabled(self) -> bool:
138
+ if not self._enabled:
139
+ return False
140
+
141
+ is_within_range = True
142
+ if self._num_inference_steps is not None:
143
+ skip_start_step = int(self._start * self._num_inference_steps)
144
+ skip_stop_step = int(self._stop * self._num_inference_steps)
145
+ is_within_range = skip_start_step <= self._step < skip_stop_step
146
+
147
+ is_close = False
148
+ if self.use_original_formulation:
149
+ is_close = math.isclose(self.guidance_scale, 0.0)
150
+ else:
151
+ is_close = math.isclose(self.guidance_scale, 1.0)
152
+
153
+ return is_within_range and not is_close
154
+
155
+
156
+ def cfg_zero_star_scale(cond: torch.Tensor, uncond: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
157
+ cond_dtype = cond.dtype
158
+ cond = cond.float()
159
+ uncond = uncond.float()
160
+ dot_product = torch.sum(cond * uncond, dim=1, keepdim=True)
161
+ squared_norm = torch.sum(uncond**2, dim=1, keepdim=True) + eps
162
+ # st_star = v_cond^T * v_uncond / ||v_uncond||^2
163
+ scale = dot_product / squared_norm
164
+ return scale.to(dtype=cond_dtype)
diffusers/guiders/frequency_decoupled_guidance.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from ..utils import is_kornia_available
24
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ from ..modular_pipelines.modular_pipeline import BlockState
29
+
30
+
31
+ _CAN_USE_KORNIA = is_kornia_available()
32
+
33
+
34
+ if _CAN_USE_KORNIA:
35
+ from kornia.geometry import pyrup as upsample_and_blur_func
36
+ from kornia.geometry.transform import build_laplacian_pyramid as build_laplacian_pyramid_func
37
+ else:
38
+ upsample_and_blur_func = None
39
+ build_laplacian_pyramid_func = None
40
+
41
+
42
+ def project(v0: torch.Tensor, v1: torch.Tensor, upcast_to_double: bool = True) -> tuple[torch.Tensor, torch.Tensor]:
43
+ """
44
+ Project vector v0 onto vector v1, returning the parallel and orthogonal components of v0. Implementation from paper
45
+ (Algorithm 2).
46
+ """
47
+ # v0 shape: [B, ...]
48
+ # v1 shape: [B, ...]
49
+ # Assume first dim is a batch dim and all other dims are channel or "spatial" dims
50
+ all_dims_but_first = list(range(1, len(v0.shape)))
51
+ if upcast_to_double:
52
+ dtype = v0.dtype
53
+ v0, v1 = v0.double(), v1.double()
54
+ v1 = torch.nn.functional.normalize(v1, dim=all_dims_but_first)
55
+ v0_parallel = (v0 * v1).sum(dim=all_dims_but_first, keepdim=True) * v1
56
+ v0_orthogonal = v0 - v0_parallel
57
+ if upcast_to_double:
58
+ v0_parallel = v0_parallel.to(dtype)
59
+ v0_orthogonal = v0_orthogonal.to(dtype)
60
+ return v0_parallel, v0_orthogonal
61
+
62
+
63
+ def build_image_from_pyramid(pyramid: list[torch.Tensor]) -> torch.Tensor:
64
+ """
65
+ Recovers the data space latents from the Laplacian pyramid frequency space. Implementation from the paper
66
+ (Algorithm 2).
67
+ """
68
+ # pyramid shapes: [[B, C, H, W], [B, C, H/2, W/2], ...]
69
+ img = pyramid[-1]
70
+ for i in range(len(pyramid) - 2, -1, -1):
71
+ img = upsample_and_blur_func(img) + pyramid[i]
72
+ return img
73
+
74
+
75
+ class FrequencyDecoupledGuidance(BaseGuidance):
76
+ """
77
+ Frequency-Decoupled Guidance (FDG): https://huggingface.co/papers/2506.19713
78
+
79
+ FDG is a technique similar to (and based on) classifier-free guidance (CFG) which is used to improve generation
80
+ quality and condition-following in diffusion models. Like CFG, during training we jointly train the model on both
81
+ conditional and unconditional data, and use a combination of the two during inference. (If you want more details on
82
+ how CFG works, you can check out the CFG guider.)
83
+
84
+ FDG differs from CFG in that the normal CFG prediction is instead decoupled into low- and high-frequency components
85
+ using a frequency transform (such as a Laplacian pyramid). The CFG update is then performed in frequency space
86
+ separately for the low- and high-frequency components with different guidance scales. Finally, the inverse
87
+ frequency transform is used to map the CFG frequency predictions back to data space (e.g. pixel space for images)
88
+ to form the final FDG prediction.
89
+
90
+ For images, the FDG authors found that using low guidance scales for the low-frequency components retains sample
91
+ diversity and realistic color composition, while using high guidance scales for high-frequency components enhances
92
+ sample quality (such as better visual details). Therefore, they recommend using low guidance scales (low w_low) for
93
+ the low-frequency components and high guidance scales (high w_high) for the high-frequency components. As an
94
+ example, they suggest w_low = 5.0 and w_high = 10.0 for Stable Diffusion XL (see Table 8 in the paper).
95
+
96
+ As with CFG, Diffusers implements the scaling and shifting on the unconditional prediction based on the [Imagen
97
+ paper](https://huggingface.co/papers/2205.11487), which is equivalent to what the original CFG paper proposed in
98
+ theory. [x_pred = x_uncond + scale * (x_cond - x_uncond)]
99
+
100
+ The `use_original_formulation` argument can be set to `True` to use the original CFG formulation mentioned in the
101
+ paper. By default, we use the diffusers-native implementation that has been in the codebase for a long time.
102
+
103
+ Args:
104
+ guidance_scales (`list[float]`, defaults to `[10.0, 5.0]`):
105
+ The scale parameter for frequency-decoupled guidance for each frequency component, listed from highest
106
+ frequency level to lowest. Higher values result in stronger conditioning on the text prompt, while lower
107
+ values allow for more freedom in generation. Higher values may lead to saturation and deterioration of
108
+ image quality. The FDG authors recommend using higher guidance scales for higher frequency components and
109
+ lower guidance scales for lower frequency components (so `guidance_scales` should typically be sorted in
110
+ descending order).
111
+ guidance_rescale (`float` or `list[float]`, defaults to `0.0`):
112
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
113
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
114
+ Flawed](https://huggingface.co/papers/2305.08891). If a list is supplied, it should be the same length as
115
+ `guidance_scales`.
116
+ parallel_weights (`float` or `list[float]`, *optional*):
117
+ Optional weights for the parallel component of each frequency component of the projected CFG shift. If not
118
+ set, the weights will default to `1.0` for all components, which corresponds to using the normal CFG shift
119
+ (that is, equal weights for the parallel and orthogonal components). If set, a value in `[0, 1]` is
120
+ recommended. If a list is supplied, it should be the same length as `guidance_scales`.
121
+ use_original_formulation (`bool`, defaults to `False`):
122
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
123
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
124
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
125
+ start (`float` or `list[float]`, defaults to `0.0`):
126
+ The fraction of the total number of denoising steps after which guidance starts. If a list is supplied, it
127
+ should be the same length as `guidance_scales`.
128
+ stop (`float` or `list[float]`, defaults to `1.0`):
129
+ The fraction of the total number of denoising steps after which guidance stops. If a list is supplied, it
130
+ should be the same length as `guidance_scales`.
131
+ guidance_rescale_space (`str`, defaults to `"data"`):
132
+ Whether to performance guidance rescaling in `"data"` space (after the full FDG update in data space) or in
133
+ `"freq"` space (right after the CFG update, for each freq level). Note that frequency space rescaling is
134
+ speculative and may not produce expected results. If `"data"` is set, the first `guidance_rescale` value
135
+ will be used; otherwise, per-frequency-level guidance rescale values will be used if available.
136
+ upcast_to_double (`bool`, defaults to `True`):
137
+ Whether to upcast certain operations, such as the projection operation when using `parallel_weights`, to
138
+ float64 when performing guidance. This may result in better performance at the cost of increased runtime.
139
+ """
140
+
141
+ _input_predictions = ["pred_cond", "pred_uncond"]
142
+
143
+ @register_to_config
144
+ def __init__(
145
+ self,
146
+ guidance_scales: list[float] | tuple[float] = [10.0, 5.0],
147
+ guidance_rescale: float | list[float] | tuple[float] = 0.0,
148
+ parallel_weights: float | list[float] | tuple[float] | None = None,
149
+ use_original_formulation: bool = False,
150
+ start: float | list[float] | tuple[float] = 0.0,
151
+ stop: float | list[float] | tuple[float] = 1.0,
152
+ guidance_rescale_space: str = "data",
153
+ upcast_to_double: bool = True,
154
+ enabled: bool = True,
155
+ ):
156
+ if not _CAN_USE_KORNIA:
157
+ raise ImportError(
158
+ "The `FrequencyDecoupledGuidance` guider cannot be instantiated because the `kornia` library on which "
159
+ "it depends is not available in the current environment. You can install `kornia` with `pip install "
160
+ "kornia`."
161
+ )
162
+
163
+ # Set start to earliest start for any freq component and stop to latest stop for any freq component
164
+ min_start = start if isinstance(start, float) else min(start)
165
+ max_stop = stop if isinstance(stop, float) else max(stop)
166
+ super().__init__(min_start, max_stop, enabled)
167
+
168
+ self.guidance_scales = guidance_scales
169
+ self.levels = len(guidance_scales)
170
+
171
+ if isinstance(guidance_rescale, float):
172
+ self.guidance_rescale = [guidance_rescale] * self.levels
173
+ elif len(guidance_rescale) == self.levels:
174
+ self.guidance_rescale = guidance_rescale
175
+ else:
176
+ raise ValueError(
177
+ f"`guidance_rescale` has length {len(guidance_rescale)} but should have the same length as "
178
+ f"`guidance_scales` ({len(self.guidance_scales)})"
179
+ )
180
+ # Whether to perform guidance rescaling in frequency space (right after the CFG update) or data space (after
181
+ # transforming from frequency space back to data space)
182
+ if guidance_rescale_space not in ["data", "freq"]:
183
+ raise ValueError(
184
+ f"Guidance rescale space is {guidance_rescale_space} but must be one of `data` or `freq`."
185
+ )
186
+ self.guidance_rescale_space = guidance_rescale_space
187
+
188
+ if parallel_weights is None:
189
+ # Use normal CFG shift (equal weights for parallel and orthogonal components)
190
+ self.parallel_weights = [1.0] * self.levels
191
+ elif isinstance(parallel_weights, float):
192
+ self.parallel_weights = [parallel_weights] * self.levels
193
+ elif len(parallel_weights) == self.levels:
194
+ self.parallel_weights = parallel_weights
195
+ else:
196
+ raise ValueError(
197
+ f"`parallel_weights` has length {len(parallel_weights)} but should have the same length as "
198
+ f"`guidance_scales` ({len(self.guidance_scales)})"
199
+ )
200
+
201
+ self.use_original_formulation = use_original_formulation
202
+ self.upcast_to_double = upcast_to_double
203
+
204
+ if isinstance(start, float):
205
+ self.guidance_start = [start] * self.levels
206
+ elif len(start) == self.levels:
207
+ self.guidance_start = start
208
+ else:
209
+ raise ValueError(
210
+ f"`start` has length {len(start)} but should have the same length as `guidance_scales` "
211
+ f"({len(self.guidance_scales)})"
212
+ )
213
+ if isinstance(stop, float):
214
+ self.guidance_stop = [stop] * self.levels
215
+ elif len(stop) == self.levels:
216
+ self.guidance_stop = stop
217
+ else:
218
+ raise ValueError(
219
+ f"`stop` has length {len(stop)} but should have the same length as `guidance_scales` "
220
+ f"({len(self.guidance_scales)})"
221
+ )
222
+
223
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
224
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
225
+ data_batches = []
226
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
227
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
228
+ data_batches.append(data_batch)
229
+ return data_batches
230
+
231
+ def prepare_inputs_from_block_state(
232
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
233
+ ) -> list["BlockState"]:
234
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
235
+ data_batches = []
236
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
237
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
238
+ data_batches.append(data_batch)
239
+ return data_batches
240
+
241
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
242
+ pred = None
243
+
244
+ if not self._is_fdg_enabled():
245
+ pred = pred_cond
246
+ else:
247
+ # Apply the frequency transform (e.g. Laplacian pyramid) to the conditional and unconditional predictions.
248
+ pred_cond_pyramid = build_laplacian_pyramid_func(pred_cond, self.levels)
249
+ pred_uncond_pyramid = build_laplacian_pyramid_func(pred_uncond, self.levels)
250
+
251
+ # From high frequencies to low frequencies, following the paper implementation
252
+ pred_guided_pyramid = []
253
+ parameters = zip(self.guidance_scales, self.parallel_weights, self.guidance_rescale)
254
+ for level, (guidance_scale, parallel_weight, guidance_rescale) in enumerate(parameters):
255
+ if self._is_fdg_enabled_for_level(level):
256
+ # Get the cond/uncond preds (in freq space) at the current frequency level
257
+ pred_cond_freq = pred_cond_pyramid[level]
258
+ pred_uncond_freq = pred_uncond_pyramid[level]
259
+
260
+ shift = pred_cond_freq - pred_uncond_freq
261
+
262
+ # Apply parallel weights, if used (1.0 corresponds to using the normal CFG shift)
263
+ if not math.isclose(parallel_weight, 1.0):
264
+ shift_parallel, shift_orthogonal = project(shift, pred_cond_freq, self.upcast_to_double)
265
+ shift = parallel_weight * shift_parallel + shift_orthogonal
266
+
267
+ # Apply CFG update for the current frequency level
268
+ pred = pred_cond_freq if self.use_original_formulation else pred_uncond_freq
269
+ pred = pred + guidance_scale * shift
270
+
271
+ if self.guidance_rescale_space == "freq" and guidance_rescale > 0.0:
272
+ pred = rescale_noise_cfg(pred, pred_cond_freq, guidance_rescale)
273
+
274
+ # Add the current FDG guided level to the FDG prediction pyramid
275
+ pred_guided_pyramid.append(pred)
276
+ else:
277
+ # Add the current pred_cond_pyramid level as the "non-FDG" prediction
278
+ pred_guided_pyramid.append(pred_cond_freq)
279
+
280
+ # Convert from frequency space back to data (e.g. pixel) space by applying inverse freq transform
281
+ pred = build_image_from_pyramid(pred_guided_pyramid)
282
+
283
+ # If rescaling in data space, use the first elem of self.guidance_rescale as the "global" rescale value
284
+ # across all freq levels
285
+ if self.guidance_rescale_space == "data" and self.guidance_rescale[0] > 0.0:
286
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale[0])
287
+
288
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
289
+
290
+ @property
291
+ def is_conditional(self) -> bool:
292
+ return self._count_prepared == 1
293
+
294
+ @property
295
+ def num_conditions(self) -> int:
296
+ num_conditions = 1
297
+ if self._is_fdg_enabled():
298
+ num_conditions += 1
299
+ return num_conditions
300
+
301
+ def _is_fdg_enabled(self) -> bool:
302
+ if not self._enabled:
303
+ return False
304
+
305
+ is_within_range = True
306
+ if self._num_inference_steps is not None:
307
+ skip_start_step = int(self._start * self._num_inference_steps)
308
+ skip_stop_step = int(self._stop * self._num_inference_steps)
309
+ is_within_range = skip_start_step <= self._step < skip_stop_step
310
+
311
+ is_close = False
312
+ if self.use_original_formulation:
313
+ is_close = all(math.isclose(guidance_scale, 0.0) for guidance_scale in self.guidance_scales)
314
+ else:
315
+ is_close = all(math.isclose(guidance_scale, 1.0) for guidance_scale in self.guidance_scales)
316
+
317
+ return is_within_range and not is_close
318
+
319
+ def _is_fdg_enabled_for_level(self, level: int) -> bool:
320
+ if not self._enabled:
321
+ return False
322
+
323
+ is_within_range = True
324
+ if self._num_inference_steps is not None:
325
+ skip_start_step = int(self.guidance_start[level] * self._num_inference_steps)
326
+ skip_stop_step = int(self.guidance_stop[level] * self._num_inference_steps)
327
+ is_within_range = skip_start_step <= self._step < skip_stop_step
328
+
329
+ is_close = False
330
+ if self.use_original_formulation:
331
+ is_close = math.isclose(self.guidance_scales[level], 0.0)
332
+ else:
333
+ is_close = math.isclose(self.guidance_scales[level], 1.0)
334
+
335
+ return is_within_range and not is_close
diffusers/guiders/guider_utils.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ import torch
21
+ from huggingface_hub.utils import validate_hf_hub_args
22
+ from typing_extensions import Self
23
+
24
+ from ..configuration_utils import ConfigMixin
25
+ from ..utils import BaseOutput, PushToHubMixin, get_logger
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from ..modular_pipelines.modular_pipeline import BlockState
30
+
31
+
32
+ GUIDER_CONFIG_NAME = "guider_config.json"
33
+
34
+
35
+ logger = get_logger(__name__) # pylint: disable=invalid-name
36
+
37
+
38
+ class BaseGuidance(ConfigMixin, PushToHubMixin):
39
+ r"""Base class providing the skeleton for implementing guidance techniques."""
40
+
41
+ config_name = GUIDER_CONFIG_NAME
42
+ _input_predictions = None
43
+ _identifier_key = "__guidance_identifier__"
44
+
45
+ def __init__(self, start: float = 0.0, stop: float = 1.0, enabled: bool = True):
46
+ logger.warning(
47
+ "Guiders are currently an experimental feature under active development. The API is subject to breaking changes in future releases."
48
+ )
49
+
50
+ self._start = start
51
+ self._stop = stop
52
+ self._step: int = None
53
+ self._num_inference_steps: int = None
54
+ self._timestep: torch.LongTensor = None
55
+ self._count_prepared = 0
56
+ self._input_fields: dict[str, str | tuple[str, str]] = None
57
+ self._enabled = enabled
58
+
59
+ if not (0.0 <= start < 1.0):
60
+ raise ValueError(f"Expected `start` to be between 0.0 and 1.0, but got {start}.")
61
+ if not (start <= stop <= 1.0):
62
+ raise ValueError(f"Expected `stop` to be between {start} and 1.0, but got {stop}.")
63
+
64
+ if self._input_predictions is None or not isinstance(self._input_predictions, list):
65
+ raise ValueError(
66
+ "`_input_predictions` must be a list of required prediction names for the guidance technique."
67
+ )
68
+
69
+ def new(self, **kwargs):
70
+ """
71
+ Creates a copy of this guider instance, optionally with modified configuration parameters.
72
+
73
+ Args:
74
+ **kwargs: Configuration parameters to override in the new instance. If no kwargs are provided,
75
+ returns an exact copy with the same configuration.
76
+
77
+ Returns:
78
+ A new guider instance with the same (or updated) configuration.
79
+
80
+ Example:
81
+ ```python
82
+ # Create a CFG guider
83
+ guider = ClassifierFreeGuidance(guidance_scale=3.5)
84
+
85
+ # Create an exact copy
86
+ same_guider = guider.new()
87
+
88
+ # Create a copy with different start step, keeping other config the same
89
+ new_guider = guider.new(guidance_scale=5)
90
+ ```
91
+ """
92
+ return self.__class__.from_config(self.config, **kwargs)
93
+
94
+ def disable(self):
95
+ self._enabled = False
96
+
97
+ def enable(self):
98
+ self._enabled = True
99
+
100
+ def set_state(self, step: int, num_inference_steps: int, timestep: torch.LongTensor) -> None:
101
+ self._step = step
102
+ self._num_inference_steps = num_inference_steps
103
+ self._timestep = timestep
104
+ self._count_prepared = 0
105
+
106
+ def get_state(self) -> dict[str, Any]:
107
+ """
108
+ Returns the current state of the guidance technique as a dictionary. The state variables will be included in
109
+ the __repr__ method. Returns:
110
+ `dict[str, Any]`: A dictionary containing the current state variables including:
111
+ - step: Current inference step
112
+ - num_inference_steps: Total number of inference steps
113
+ - timestep: Current timestep tensor
114
+ - count_prepared: Number of times prepare_models has been called
115
+ - enabled: Whether the guidance is enabled
116
+ - num_conditions: Number of conditions
117
+ """
118
+ state = {
119
+ "step": self._step,
120
+ "num_inference_steps": self._num_inference_steps,
121
+ "timestep": self._timestep,
122
+ "count_prepared": self._count_prepared,
123
+ "enabled": self._enabled,
124
+ "num_conditions": self.num_conditions,
125
+ }
126
+ return state
127
+
128
+ def __repr__(self) -> str:
129
+ """
130
+ Returns a string representation of the guidance object including both config and current state.
131
+ """
132
+ # Get ConfigMixin's __repr__
133
+ str_repr = super().__repr__()
134
+
135
+ # Get current state
136
+ state = self.get_state()
137
+
138
+ # Format each state variable on its own line with indentation
139
+ state_lines = []
140
+ for k, v in state.items():
141
+ # Convert value to string and handle multi-line values
142
+ v_str = str(v)
143
+ if "\n" in v_str:
144
+ # For multi-line values (like MomentumBuffer), indent subsequent lines
145
+ v_lines = v_str.split("\n")
146
+ v_str = v_lines[0] + "\n" + "\n".join([" " + line for line in v_lines[1:]])
147
+ state_lines.append(f" {k}: {v_str}")
148
+
149
+ state_str = "\n".join(state_lines)
150
+
151
+ return f"{str_repr}\nState:\n{state_str}"
152
+
153
+ def prepare_models(self, denoiser: torch.nn.Module) -> None:
154
+ """
155
+ Prepares the models for the guidance technique on a given batch of data. This method should be overridden in
156
+ subclasses to implement specific model preparation logic.
157
+ """
158
+ self._count_prepared += 1
159
+
160
+ def cleanup_models(self, denoiser: torch.nn.Module) -> None:
161
+ """
162
+ Cleans up the models for the guidance technique after a given batch of data. This method should be overridden
163
+ in subclasses to implement specific model cleanup logic. It is useful for removing any hooks or other stateful
164
+ modifications made during `prepare_models`.
165
+ """
166
+ pass
167
+
168
+ def prepare_inputs(self, data: "BlockState") -> list["BlockState"]:
169
+ raise NotImplementedError("BaseGuidance::prepare_inputs must be implemented in subclasses.")
170
+
171
+ def prepare_inputs_from_block_state(
172
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
173
+ ) -> list["BlockState"]:
174
+ raise NotImplementedError("BaseGuidance::prepare_inputs_from_block_state must be implemented in subclasses.")
175
+
176
+ def __call__(self, data: list["BlockState"]) -> Any:
177
+ if not all(hasattr(d, "noise_pred") for d in data):
178
+ raise ValueError("Expected all data to have `noise_pred` attribute.")
179
+ if len(data) != self.num_conditions:
180
+ raise ValueError(
181
+ f"Expected {self.num_conditions} data items, but got {len(data)}. Please check the input data."
182
+ )
183
+ forward_inputs = {getattr(d, self._identifier_key): d.noise_pred for d in data}
184
+ return self.forward(**forward_inputs)
185
+
186
+ def forward(self, *args, **kwargs) -> Any:
187
+ raise NotImplementedError("BaseGuidance::forward must be implemented in subclasses.")
188
+
189
+ @property
190
+ def is_conditional(self) -> bool:
191
+ raise NotImplementedError("BaseGuidance::is_conditional must be implemented in subclasses.")
192
+
193
+ @property
194
+ def is_unconditional(self) -> bool:
195
+ return not self.is_conditional
196
+
197
+ @property
198
+ def num_conditions(self) -> int:
199
+ raise NotImplementedError("BaseGuidance::num_conditions must be implemented in subclasses.")
200
+
201
+ @classmethod
202
+ def _prepare_batch(
203
+ cls,
204
+ data: dict[str, tuple[torch.Tensor, torch.Tensor]],
205
+ tuple_index: int,
206
+ identifier: str,
207
+ ) -> "BlockState":
208
+ """
209
+ Prepares a batch of data for the guidance technique. This method is used in the `prepare_inputs` method of the
210
+ `BaseGuidance` class. It prepares the batch based on the provided tuple index.
211
+
212
+ Args:
213
+ input_fields (`dict[str, str | tuple[str, str]]`):
214
+ A dictionary where the keys are the names of the fields that will be used to store the data once it is
215
+ prepared with `prepare_inputs`. The values can be either a string or a tuple of length 2, which is used
216
+ to look up the required data provided for preparation. If a string is provided, it will be used as the
217
+ conditional data (or unconditional if used with a guidance method that requires it). If a tuple of
218
+ length 2 is provided, the first element must be the conditional data identifier and the second element
219
+ must be the unconditional data identifier or None.
220
+ data (`BlockState`):
221
+ The input data to be prepared.
222
+ tuple_index (`int`):
223
+ The index to use when accessing input fields that are tuples.
224
+
225
+ Returns:
226
+ `BlockState`: The prepared batch of data.
227
+ """
228
+ from ..modular_pipelines.modular_pipeline import BlockState
229
+
230
+ data_batch = {}
231
+ for key, value in data.items():
232
+ try:
233
+ if isinstance(value, torch.Tensor):
234
+ data_batch[key] = value
235
+ elif isinstance(value, tuple):
236
+ data_batch[key] = value[tuple_index]
237
+ else:
238
+ raise ValueError(f"Invalid value type: {type(value)}")
239
+ except ValueError:
240
+ logger.debug(f"`data` does not have attribute(s) {value}, skipping.")
241
+ data_batch[cls._identifier_key] = identifier
242
+ return BlockState(**data_batch)
243
+
244
+ @classmethod
245
+ def _prepare_batch_from_block_state(
246
+ cls,
247
+ input_fields: dict[str, str | tuple[str, str]],
248
+ data: "BlockState",
249
+ tuple_index: int,
250
+ identifier: str,
251
+ ) -> "BlockState":
252
+ """
253
+ Prepares a batch of data for the guidance technique. This method is used in the `prepare_inputs` method of the
254
+ `BaseGuidance` class. It prepares the batch based on the provided tuple index.
255
+
256
+ Args:
257
+ input_fields (`dict[str, str | tuple[str, str]]`):
258
+ A dictionary where the keys are the names of the fields that will be used to store the data once it is
259
+ prepared with `prepare_inputs`. The values can be either a string or a tuple of length 2, which is used
260
+ to look up the required data provided for preparation. If a string is provided, it will be used as the
261
+ conditional data (or unconditional if used with a guidance method that requires it). If a tuple of
262
+ length 2 is provided, the first element must be the conditional data identifier and the second element
263
+ must be the unconditional data identifier or None.
264
+ data (`BlockState`):
265
+ The input data to be prepared.
266
+ tuple_index (`int`):
267
+ The index to use when accessing input fields that are tuples.
268
+
269
+ Returns:
270
+ `BlockState`: The prepared batch of data.
271
+ """
272
+ from ..modular_pipelines.modular_pipeline import BlockState
273
+
274
+ data_batch = {}
275
+ for key, value in input_fields.items():
276
+ try:
277
+ if isinstance(value, str):
278
+ data_batch[key] = getattr(data, value)
279
+ elif isinstance(value, tuple):
280
+ data_batch[key] = getattr(data, value[tuple_index])
281
+ else:
282
+ # We've already checked that value is a string or a tuple of strings with length 2
283
+ pass
284
+ except AttributeError:
285
+ logger.debug(f"`data` does not have attribute(s) {value}, skipping.")
286
+ data_batch[cls._identifier_key] = identifier
287
+ return BlockState(**data_batch)
288
+
289
+ @classmethod
290
+ @validate_hf_hub_args
291
+ def from_pretrained(
292
+ cls,
293
+ pretrained_model_name_or_path: str | os.PathLike | None = None,
294
+ subfolder: str | None = None,
295
+ return_unused_kwargs=False,
296
+ **kwargs,
297
+ ) -> Self:
298
+ r"""
299
+ Instantiate a guider from a pre-defined JSON configuration file in a local directory or Hub repository.
300
+
301
+ Parameters:
302
+ pretrained_model_name_or_path (`str` or `os.PathLike`, *optional*):
303
+ Can be either:
304
+
305
+ - A string, the *model id* (for example `google/ddpm-celebahq-256`) of a pretrained model hosted on
306
+ the Hub.
307
+ - A path to a *directory* (for example `./my_model_directory`) containing the guider configuration
308
+ saved with [`~BaseGuidance.save_pretrained`].
309
+ subfolder (`str`, *optional*):
310
+ The subfolder location of a model file within a larger model repository on the Hub or locally.
311
+ return_unused_kwargs (`bool`, *optional*, defaults to `False`):
312
+ Whether kwargs that are not consumed by the Python class should be returned or not.
313
+ cache_dir (`str | os.PathLike`, *optional*):
314
+ Path to a directory where a downloaded pretrained model configuration is cached if the standard cache
315
+ is not used.
316
+ force_download (`bool`, *optional*, defaults to `False`):
317
+ Whether or not to force the (re-)download of the model weights and configuration files, overriding the
318
+ cached versions if they exist.
319
+
320
+ proxies (`dict[str, str]`, *optional*):
321
+ A dictionary of proxy servers to use by protocol or endpoint, for example, `{'http': 'foo.bar:3128',
322
+ 'http://hostname': 'foo.bar:4012'}`. The proxies are used on each request.
323
+ output_loading_info(`bool`, *optional*, defaults to `False`):
324
+ Whether or not to also return a dictionary containing missing keys, unexpected keys and error messages.
325
+ local_files_only(`bool`, *optional*, defaults to `False`):
326
+ Whether to only load local model weights and configuration files or not. If set to `True`, the model
327
+ won't be downloaded from the Hub.
328
+ token (`str` or *bool*, *optional*):
329
+ The token to use as HTTP bearer authorization for remote files. If `True`, the token generated from
330
+ `diffusers-cli login` (stored in `~/.huggingface`) is used.
331
+ revision (`str`, *optional*, defaults to `"main"`):
332
+ The specific model version to use. It can be a branch name, a tag name, a commit id, or any identifier
333
+ allowed by Git.
334
+
335
+ > [!TIP] > To use private or [gated models](https://huggingface.co/docs/hub/models-gated#gated-models), log-in
336
+ with `hf > auth login`. You can also activate the special >
337
+ ["offline-mode"](https://huggingface.co/diffusers/installation.html#offline-mode) to use this method in a >
338
+ firewalled environment.
339
+
340
+ """
341
+ config, kwargs, commit_hash = cls.load_config(
342
+ pretrained_model_name_or_path=pretrained_model_name_or_path,
343
+ subfolder=subfolder,
344
+ return_unused_kwargs=True,
345
+ return_commit_hash=True,
346
+ **kwargs,
347
+ )
348
+ return cls.from_config(config, return_unused_kwargs=return_unused_kwargs, **kwargs)
349
+
350
+ def save_pretrained(self, save_directory: str | os.PathLike, push_to_hub: bool = False, **kwargs):
351
+ """
352
+ Save a guider configuration object to a directory so that it can be reloaded using the
353
+ [`~BaseGuidance.from_pretrained`] class method.
354
+
355
+ Args:
356
+ save_directory (`str` or `os.PathLike`):
357
+ Directory where the configuration JSON file will be saved (will be created if it does not exist).
358
+ push_to_hub (`bool`, *optional*, defaults to `False`):
359
+ Whether or not to push your model to the Hugging Face Hub after saving it. You can specify the
360
+ repository you want to push to with `repo_id` (will default to the name of `save_directory` in your
361
+ namespace).
362
+ kwargs (`dict[str, Any]`, *optional*):
363
+ Additional keyword arguments passed along to the [`~utils.PushToHubMixin.push_to_hub`] method.
364
+ """
365
+ self.save_config(save_directory=save_directory, push_to_hub=push_to_hub, **kwargs)
366
+
367
+
368
+ class GuiderOutput(BaseOutput):
369
+ pred: torch.Tensor
370
+ pred_cond: torch.Tensor | None
371
+ pred_uncond: torch.Tensor | None
372
+
373
+
374
+ def rescale_noise_cfg(noise_cfg, noise_pred_text, guidance_rescale=0.0):
375
+ r"""
376
+ Rescales `noise_cfg` tensor based on `guidance_rescale` to improve image quality and fix overexposure. Based on
377
+ Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
378
+ Flawed](https://huggingface.co/papers/2305.08891).
379
+
380
+ Args:
381
+ noise_cfg (`torch.Tensor`):
382
+ The predicted noise tensor for the guided diffusion process.
383
+ noise_pred_text (`torch.Tensor`):
384
+ The predicted noise tensor for the text-guided diffusion process.
385
+ guidance_rescale (`float`, *optional*, defaults to 0.0):
386
+ A rescale factor applied to the noise predictions.
387
+ Returns:
388
+ noise_cfg (`torch.Tensor`): The rescaled noise prediction tensor.
389
+ """
390
+ std_text = noise_pred_text.std(dim=list(range(1, noise_pred_text.ndim)), keepdim=True)
391
+ std_cfg = noise_cfg.std(dim=list(range(1, noise_cfg.ndim)), keepdim=True)
392
+ # rescale the results from guidance (fixes overexposure)
393
+ noise_pred_rescaled = noise_cfg * (std_text / std_cfg)
394
+ # mix with the original results from guidance by factor guidance_rescale to avoid "plain looking" images
395
+ noise_cfg = guidance_rescale * noise_pred_rescaled + (1 - guidance_rescale) * noise_cfg
396
+ return noise_cfg
diffusers/guiders/ltx2_guidance.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import math
16
+ from typing import TYPE_CHECKING
17
+
18
+ import torch
19
+
20
+ from ..configuration_utils import register_to_config
21
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from ..modular_pipelines.modular_pipeline import BlockState
26
+
27
+
28
+ class LTX2Guidance(BaseGuidance):
29
+ """
30
+ Single-modality guider for LTX-2.X. Combines up to three terms via the delta formulation:
31
+ - classifier-free guidance (CFG),
32
+ - spatio-temporal guidance (STG), an extra pass with a set of transformer blocks perturbed,
33
+ - modality-isolation guidance, an extra pass with A2V/V2A cross-attention disabled.
34
+
35
+ One instance drives one modality, so a multimodal checkpoint carries several: LTX-2.X pairs a video `guider`
36
+ with an `audio_guider`, each holding its own scales. The final guidance estimate is computed in whatever space
37
+ the caller feeds it — the LTX-2 denoiser converts velocity->x0 before calling this and back afterwards, so
38
+ `forward` operates on x0 predictions.
39
+
40
+ Unlike `SkipLayerGuidance`, which applies its perturbations with hooks, `LTX2Guidance` expects its non-CFG
41
+ perturbations to be configurable through model forward arguments (`spatio_temporal_guidance_blocks` and
42
+ `isolate_modalities` in `LTX2VideoTransformer3DModel`). The calling denoiser block is responsible for setting
43
+ those per pass identifier; see `LTX2LoopDenoiser` for a usage example.
44
+
45
+ Args:
46
+ guidance_scale (`float`, defaults to `1.0`):
47
+ CFG scale for this modality. The CFG pass is skipped entirely at `1.0`.
48
+ stg_scale (`float`, defaults to `0.0`):
49
+ Spatio-temporal guidance scale for this modality. The STG pass is skipped entirely at `0.0`.
50
+ modality_scale (`float`, defaults to `1.0`):
51
+ Modality-isolation guidance scale for this modality. That pass is skipped entirely at `1.0`.
52
+ guidance_rescale (`float`, defaults to `0.0`):
53
+ Rescaling factor to prevent overexposure from high guidance scales. Based on [Common Diffusion Noise
54
+ Schedules and Sample Steps are Flawed](https://huggingface.co/papers/2305.08891). Range: 0.0 (no rescaling)
55
+ to 1.0 (full rescaling).
56
+ spatio_temporal_guidance_blocks (`list[int]`, *optional*):
57
+ Transformer blocks to perturb on the STG pass. How the value is consumed is up to the denoiser block:
58
+ LTX-2's perturbs whole blocks in a single forward that feeds every modality, so it reads this off the video
59
+ guider alone — setting it on the audio guider has no effect, and that guider joins the same STG forward
60
+ through its own `stg_scale`.
61
+ start (`float`, defaults to `0.0`):
62
+ Fraction of denoising steps (0.0-1.0) after which guidance starts.
63
+ stop (`float`, defaults to `1.0`):
64
+ Fraction of denoising steps (0.0-1.0) after which guidance stops.
65
+ enabled (`bool`, defaults to `True`):
66
+ Whether this guider applies guidance at all. When `False`, only the conditional pass runs.
67
+ """
68
+
69
+ # `pred_cond` is the base; the others are the guidance passes.
70
+ _input_predictions = ["pred_cond", "pred_uncond", "pred_cond_stg", "pred_cond_modality"]
71
+
72
+ @register_to_config
73
+ def __init__(
74
+ self,
75
+ guidance_scale: float = 1.0,
76
+ stg_scale: float = 0.0,
77
+ modality_scale: float = 1.0,
78
+ guidance_rescale: float = 0.0,
79
+ spatio_temporal_guidance_blocks: list[int] | None = None,
80
+ start: float = 0.0,
81
+ stop: float = 1.0,
82
+ enabled: bool = True,
83
+ ):
84
+ super().__init__(start, stop, enabled)
85
+ self.guidance_scale = guidance_scale
86
+ self.stg_scale = stg_scale
87
+ self.modality_scale = modality_scale
88
+ self.guidance_rescale = guidance_rescale
89
+ self.spatio_temporal_guidance_blocks = spatio_temporal_guidance_blocks
90
+
91
+ # --- per-term enabled checks (public: the denoiser ORs these across the two guiders) --------------------
92
+ def _within_window(self) -> bool:
93
+ if self._num_inference_steps is None:
94
+ return True
95
+ start_step = int(self._start * self._num_inference_steps)
96
+ stop_step = int(self._stop * self._num_inference_steps)
97
+ return start_step <= self._step < stop_step
98
+
99
+ def is_cfg_enabled(self) -> bool:
100
+ return self._enabled and self._within_window() and not math.isclose(self.guidance_scale, 1.0)
101
+
102
+ def is_stg_enabled(self) -> bool:
103
+ return self._enabled and self._within_window() and not math.isclose(self.stg_scale, 0.0)
104
+
105
+ def is_modality_enabled(self) -> bool:
106
+ return self._enabled and self._within_window() and not math.isclose(self.modality_scale, 1.0)
107
+
108
+ def active_predictions(self) -> list[str]:
109
+ preds = ["pred_cond"]
110
+ if self.is_cfg_enabled():
111
+ preds.append("pred_uncond")
112
+ if self.is_stg_enabled():
113
+ preds.append("pred_cond_stg")
114
+ if self.is_modality_enabled():
115
+ preds.append("pred_cond_modality")
116
+ return preds
117
+
118
+ @property
119
+ def num_conditions(self) -> int:
120
+ return len(self.active_predictions())
121
+
122
+ @property
123
+ def is_conditional(self) -> bool:
124
+ # Not load-bearing here: the denoiser owns pass execution and carries the STG/modality flags on the
125
+ # batches, so we never drive `prepare_models`/hooks off `_count_prepared` the way SkipLayerGuidance does.
126
+ return True
127
+
128
+ # Each pass reads its own slot ([cond, uncond, stg, modality]) of every `guider_input_fields` tuple, selected
129
+ # by this per-identifier index. Distinct slots (rather than cond/uncond only) let STG/modality pull the same
130
+ # conditioning as `pred_cond` while the denoiser overrides their per-pass model flags after preparation.
131
+ _PREDICTION_INDEX = {"pred_cond": 0, "pred_uncond": 1, "pred_cond_stg": 2, "pred_cond_modality": 3}
132
+
133
+ def prepare_inputs(self, guider_inputs: dict) -> list["BlockState"]:
134
+ # One identifier-tagged batch per active pass. Every value in `guider_inputs` is a 4-tuple indexed by
135
+ # `_PREDICTION_INDEX` ([cond, uncond, stg, modality]); each pass reads its own slot, so the per-pass model
136
+ # flags (`spatio_temporal_guidance_blocks`, `isolate_modalities`) are carried exactly like the encoder
137
+ # inputs. The denoiser fills each returned batch's `noise_pred` before calling the guider to combine.
138
+ return [
139
+ self._prepare_batch(guider_inputs, self._PREDICTION_INDEX[pred], pred)
140
+ for pred in self.active_predictions()
141
+ ]
142
+
143
+ def prepare_inputs_from_block_state(self, data: "BlockState", input_fields: dict) -> list["BlockState"]:
144
+ # One identifier-tagged batch per active pass. Each value in `input_fields` maps a transformer argument to a
145
+ # 4-tuple of block-state attribute names indexed by `_PREDICTION_INDEX` ([cond, uncond, stg, modality]); the
146
+ # base helper reads the pass's slot off `data`. The denoiser then sets the per-pass model flags and fills
147
+ # each batch's `noise_pred` before calling the guider to combine.
148
+ return [
149
+ self._prepare_batch_from_block_state(input_fields, data, self._PREDICTION_INDEX[pred], pred)
150
+ for pred in self.active_predictions()
151
+ ]
152
+
153
+ def forward(
154
+ self,
155
+ pred_cond: torch.Tensor,
156
+ pred_uncond: torch.Tensor | None = None,
157
+ pred_cond_stg: torch.Tensor | None = None,
158
+ pred_cond_modality: torch.Tensor | None = None,
159
+ ) -> GuiderOutput:
160
+ # Delta formulation (matches the reference LTX-2 combine, applied in the x0 space the denoiser feeds):
161
+ # x0_cond + (gs-1)*(cond-uncond) + stg*(cond-stg) + (mod-1)*(cond-modality), then optional rescale.
162
+ pred = pred_cond
163
+ if self.is_cfg_enabled():
164
+ pred = pred + (self.guidance_scale - 1.0) * (pred_cond - pred_uncond)
165
+ if self.is_stg_enabled():
166
+ pred = pred + self.stg_scale * (pred_cond - pred_cond_stg)
167
+ if self.is_modality_enabled():
168
+ pred = pred + (self.modality_scale - 1.0) * (pred_cond - pred_cond_modality)
169
+ if self.guidance_rescale > 0.0:
170
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
171
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
diffusers/guiders/magnitude_aware_guidance.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import math
16
+ from typing import TYPE_CHECKING
17
+
18
+ import torch
19
+
20
+ from ..configuration_utils import register_to_config
21
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from ..modular_pipelines.modular_pipeline import BlockState
26
+
27
+
28
+ class MagnitudeAwareGuidance(BaseGuidance):
29
+ """
30
+ Magnitude-Aware Mitigation for Boosted Guidance (MAMBO-G): https://huggingface.co/papers/2508.03442
31
+
32
+ Args:
33
+ guidance_scale (`float`, defaults to `10.0`):
34
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
35
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
36
+ deterioration of image quality.
37
+ alpha (`float`, defaults to `8.0`):
38
+ The alpha parameter for the magnitude-aware guidance. Higher values cause more aggressive suppression of
39
+ guidance scale when the magnitude of the guidance update is large.
40
+ guidance_rescale (`float`, defaults to `0.0`):
41
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
42
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
43
+ Flawed](https://huggingface.co/papers/2305.08891).
44
+ use_original_formulation (`bool`, defaults to `False`):
45
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
46
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
47
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
48
+ start (`float`, defaults to `0.0`):
49
+ The fraction of the total number of denoising steps after which guidance starts.
50
+ stop (`float`, defaults to `1.0`):
51
+ The fraction of the total number of denoising steps after which guidance stops.
52
+ """
53
+
54
+ _input_predictions = ["pred_cond", "pred_uncond"]
55
+
56
+ @register_to_config
57
+ def __init__(
58
+ self,
59
+ guidance_scale: float = 10.0,
60
+ alpha: float = 8.0,
61
+ guidance_rescale: float = 0.0,
62
+ use_original_formulation: bool = False,
63
+ start: float = 0.0,
64
+ stop: float = 1.0,
65
+ enabled: bool = True,
66
+ ):
67
+ super().__init__(start, stop, enabled)
68
+
69
+ self.guidance_scale = guidance_scale
70
+ self.alpha = alpha
71
+ self.guidance_rescale = guidance_rescale
72
+ self.use_original_formulation = use_original_formulation
73
+
74
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
75
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
76
+ data_batches = []
77
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
78
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
79
+ data_batches.append(data_batch)
80
+ return data_batches
81
+
82
+ def prepare_inputs_from_block_state(
83
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
84
+ ) -> list["BlockState"]:
85
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
86
+ data_batches = []
87
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
88
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
89
+ data_batches.append(data_batch)
90
+ return data_batches
91
+
92
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
93
+ pred = None
94
+
95
+ if not self._is_mambo_g_enabled():
96
+ pred = pred_cond
97
+ else:
98
+ pred = mambo_guidance(
99
+ pred_cond,
100
+ pred_uncond,
101
+ self.guidance_scale,
102
+ self.alpha,
103
+ self.use_original_formulation,
104
+ )
105
+
106
+ if self.guidance_rescale > 0.0:
107
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
108
+
109
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
110
+
111
+ @property
112
+ def is_conditional(self) -> bool:
113
+ return self._count_prepared == 1
114
+
115
+ @property
116
+ def num_conditions(self) -> int:
117
+ num_conditions = 1
118
+ if self._is_mambo_g_enabled():
119
+ num_conditions += 1
120
+ return num_conditions
121
+
122
+ def _is_mambo_g_enabled(self) -> bool:
123
+ if not self._enabled:
124
+ return False
125
+
126
+ is_within_range = True
127
+ if self._num_inference_steps is not None:
128
+ skip_start_step = int(self._start * self._num_inference_steps)
129
+ skip_stop_step = int(self._stop * self._num_inference_steps)
130
+ is_within_range = skip_start_step <= self._step < skip_stop_step
131
+
132
+ is_close = False
133
+ if self.use_original_formulation:
134
+ is_close = math.isclose(self.guidance_scale, 0.0)
135
+ else:
136
+ is_close = math.isclose(self.guidance_scale, 1.0)
137
+
138
+ return is_within_range and not is_close
139
+
140
+
141
+ def mambo_guidance(
142
+ pred_cond: torch.Tensor,
143
+ pred_uncond: torch.Tensor,
144
+ guidance_scale: float,
145
+ alpha: float = 8.0,
146
+ use_original_formulation: bool = False,
147
+ ):
148
+ dim = list(range(1, len(pred_cond.shape)))
149
+ diff = pred_cond - pred_uncond
150
+ ratio = torch.norm(diff, dim=dim, keepdim=True) / torch.norm(pred_uncond, dim=dim, keepdim=True)
151
+ guidance_scale_final = (
152
+ guidance_scale * torch.exp(-alpha * ratio)
153
+ if use_original_formulation
154
+ else 1.0 + (guidance_scale - 1.0) * torch.exp(-alpha * ratio)
155
+ )
156
+ pred = pred_cond if use_original_formulation else pred_uncond
157
+ pred = pred + guidance_scale_final * diff
158
+
159
+ return pred
diffusers/guiders/perturbed_attention_guidance.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from ..hooks import HookRegistry, LayerSkipConfig
24
+ from ..hooks.layer_skip import _apply_layer_skip_hook
25
+ from ..utils import get_logger
26
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
27
+
28
+
29
+ if TYPE_CHECKING:
30
+ from ..modular_pipelines.modular_pipeline import BlockState
31
+
32
+
33
+ logger = get_logger(__name__) # pylint: disable=invalid-name
34
+
35
+
36
+ class PerturbedAttentionGuidance(BaseGuidance):
37
+ """
38
+ Perturbed Attention Guidance (PAG): https://huggingface.co/papers/2403.17377
39
+
40
+ The intution behind PAG can be thought of as moving the CFG predicted distribution estimates further away from
41
+ worse versions of the conditional distribution estimates. PAG was one of the first techniques to introduce the idea
42
+ of using a worse version of the trained model for better guiding itself in the denoising process. It perturbs the
43
+ attention scores of the latent stream by replacing the score matrix with an identity matrix for selectively chosen
44
+ layers.
45
+
46
+ Additional reading:
47
+ - [Guiding a Diffusion Model with a Bad Version of Itself](https://huggingface.co/papers/2406.02507)
48
+
49
+ PAG is implemented with similar implementation to SkipLayerGuidance due to overlap in the configuration parameters
50
+ and implementation details.
51
+
52
+ Args:
53
+ guidance_scale (`float`, defaults to `7.5`):
54
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
55
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
56
+ deterioration of image quality.
57
+ perturbed_guidance_scale (`float`, defaults to `2.8`):
58
+ The scale parameter for perturbed attention guidance.
59
+ perturbed_guidance_start (`float`, defaults to `0.01`):
60
+ The fraction of the total number of denoising steps after which perturbed attention guidance starts.
61
+ perturbed_guidance_stop (`float`, defaults to `0.2`):
62
+ The fraction of the total number of denoising steps after which perturbed attention guidance stops.
63
+ perturbed_guidance_layers (`int` or `list[int]`, *optional*):
64
+ The layer indices to apply perturbed attention guidance to. Can be a single integer or a list of integers.
65
+ If not provided, `perturbed_guidance_config` must be provided.
66
+ perturbed_guidance_config (`LayerSkipConfig` or `list[LayerSkipConfig]`, *optional*):
67
+ The configuration for the perturbed attention guidance. Can be a single `LayerSkipConfig` or a list of
68
+ `LayerSkipConfig`. If not provided, `perturbed_guidance_layers` must be provided.
69
+ guidance_rescale (`float`, defaults to `0.0`):
70
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
71
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
72
+ Flawed](https://huggingface.co/papers/2305.08891).
73
+ use_original_formulation (`bool`, defaults to `False`):
74
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
75
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
76
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
77
+ start (`float`, defaults to `0.01`):
78
+ The fraction of the total number of denoising steps after which guidance starts.
79
+ stop (`float`, defaults to `0.2`):
80
+ The fraction of the total number of denoising steps after which guidance stops.
81
+ """
82
+
83
+ # NOTE: The current implementation does not account for joint latent conditioning (text + image/video tokens in
84
+ # the same latent stream). It assumes the entire latent is a single stream of visual tokens. It would be very
85
+ # complex to support joint latent conditioning in a model-agnostic manner without specializing the implementation
86
+ # for each model architecture.
87
+
88
+ _input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
89
+
90
+ @register_to_config
91
+ def __init__(
92
+ self,
93
+ guidance_scale: float = 7.5,
94
+ perturbed_guidance_scale: float = 2.8,
95
+ perturbed_guidance_start: float = 0.01,
96
+ perturbed_guidance_stop: float = 0.2,
97
+ perturbed_guidance_layers: int | list[int] | None = None,
98
+ perturbed_guidance_config: LayerSkipConfig | list[LayerSkipConfig] | dict[str, Any] = None,
99
+ guidance_rescale: float = 0.0,
100
+ use_original_formulation: bool = False,
101
+ start: float = 0.0,
102
+ stop: float = 1.0,
103
+ enabled: bool = True,
104
+ ):
105
+ super().__init__(start, stop, enabled)
106
+
107
+ self.guidance_scale = guidance_scale
108
+ self.skip_layer_guidance_scale = perturbed_guidance_scale
109
+ self.skip_layer_guidance_start = perturbed_guidance_start
110
+ self.skip_layer_guidance_stop = perturbed_guidance_stop
111
+ self.guidance_rescale = guidance_rescale
112
+ self.use_original_formulation = use_original_formulation
113
+
114
+ if perturbed_guidance_config is None:
115
+ if perturbed_guidance_layers is None:
116
+ raise ValueError(
117
+ "`perturbed_guidance_layers` must be provided if `perturbed_guidance_config` is not specified."
118
+ )
119
+ perturbed_guidance_config = LayerSkipConfig(
120
+ indices=perturbed_guidance_layers,
121
+ fqn="auto",
122
+ skip_attention=False,
123
+ skip_attention_scores=True,
124
+ skip_ff=False,
125
+ )
126
+ else:
127
+ if perturbed_guidance_layers is not None:
128
+ raise ValueError(
129
+ "`perturbed_guidance_layers` should not be provided if `perturbed_guidance_config` is specified."
130
+ )
131
+
132
+ if isinstance(perturbed_guidance_config, dict):
133
+ perturbed_guidance_config = LayerSkipConfig.from_dict(perturbed_guidance_config)
134
+
135
+ if isinstance(perturbed_guidance_config, LayerSkipConfig):
136
+ perturbed_guidance_config = [perturbed_guidance_config]
137
+
138
+ if not isinstance(perturbed_guidance_config, list):
139
+ raise ValueError(
140
+ "`perturbed_guidance_config` must be a `LayerSkipConfig`, a list of `LayerSkipConfig`, or a dict that can be converted to a `LayerSkipConfig`."
141
+ )
142
+ elif isinstance(next(iter(perturbed_guidance_config), None), dict):
143
+ perturbed_guidance_config = [LayerSkipConfig.from_dict(config) for config in perturbed_guidance_config]
144
+
145
+ for config in perturbed_guidance_config:
146
+ if config.skip_attention or not config.skip_attention_scores or config.skip_ff:
147
+ logger.warning(
148
+ "Perturbed Attention Guidance is designed to perturb attention scores, so `skip_attention` should be False, `skip_attention_scores` should be True, and `skip_ff` should be False. "
149
+ "Please check your configuration. Modifying the config to match the expected values."
150
+ )
151
+ config.skip_attention = False
152
+ config.skip_attention_scores = True
153
+ config.skip_ff = False
154
+
155
+ self.skip_layer_config = perturbed_guidance_config
156
+ self._skip_layer_hook_names = [f"SkipLayerGuidance_{i}" for i in range(len(self.skip_layer_config))]
157
+
158
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.prepare_models
159
+ def prepare_models(self, denoiser: torch.nn.Module) -> None:
160
+ self._count_prepared += 1
161
+ if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
162
+ for name, config in zip(self._skip_layer_hook_names, self.skip_layer_config):
163
+ _apply_layer_skip_hook(denoiser, config, name=name)
164
+
165
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.cleanup_models
166
+ def cleanup_models(self, denoiser: torch.nn.Module) -> None:
167
+ if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
168
+ registry = HookRegistry.check_if_exists_or_initialize(denoiser)
169
+ # Remove the hooks after inference
170
+ for hook_name in self._skip_layer_hook_names:
171
+ registry.remove_hook(hook_name, recurse=True)
172
+
173
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.prepare_inputs
174
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
175
+ if self.num_conditions == 1:
176
+ tuple_indices = [0]
177
+ input_predictions = ["pred_cond"]
178
+ elif self.num_conditions == 2:
179
+ tuple_indices = [0, 1]
180
+ input_predictions = (
181
+ ["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
182
+ )
183
+ else:
184
+ tuple_indices = [0, 1, 0]
185
+ input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
186
+ data_batches = []
187
+ for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
188
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
189
+ data_batches.append(data_batch)
190
+ return data_batches
191
+
192
+ def prepare_inputs_from_block_state(
193
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
194
+ ) -> list["BlockState"]:
195
+ if self.num_conditions == 1:
196
+ tuple_indices = [0]
197
+ input_predictions = ["pred_cond"]
198
+ elif self.num_conditions == 2:
199
+ tuple_indices = [0, 1]
200
+ input_predictions = (
201
+ ["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
202
+ )
203
+ else:
204
+ tuple_indices = [0, 1, 0]
205
+ input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
206
+ data_batches = []
207
+ for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
208
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
209
+ data_batches.append(data_batch)
210
+ return data_batches
211
+
212
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.forward
213
+ def forward(
214
+ self,
215
+ pred_cond: torch.Tensor,
216
+ pred_uncond: torch.Tensor | None = None,
217
+ pred_cond_skip: torch.Tensor | None = None,
218
+ ) -> GuiderOutput:
219
+ pred = None
220
+
221
+ if not self._is_cfg_enabled() and not self._is_slg_enabled():
222
+ pred = pred_cond
223
+ elif not self._is_cfg_enabled():
224
+ shift = pred_cond - pred_cond_skip
225
+ pred = pred_cond if self.use_original_formulation else pred_cond_skip
226
+ pred = pred + self.skip_layer_guidance_scale * shift
227
+ elif not self._is_slg_enabled():
228
+ shift = pred_cond - pred_uncond
229
+ pred = pred_cond if self.use_original_formulation else pred_uncond
230
+ pred = pred + self.guidance_scale * shift
231
+ else:
232
+ shift = pred_cond - pred_uncond
233
+ shift_skip = pred_cond - pred_cond_skip
234
+ pred = pred_cond if self.use_original_formulation else pred_uncond
235
+ pred = pred + self.guidance_scale * shift + self.skip_layer_guidance_scale * shift_skip
236
+
237
+ if self.guidance_rescale > 0.0:
238
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
239
+
240
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
241
+
242
+ @property
243
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.is_conditional
244
+ def is_conditional(self) -> bool:
245
+ return self._count_prepared == 1 or self._count_prepared == 3
246
+
247
+ @property
248
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance.num_conditions
249
+ def num_conditions(self) -> int:
250
+ num_conditions = 1
251
+ if self._is_cfg_enabled():
252
+ num_conditions += 1
253
+ if self._is_slg_enabled():
254
+ num_conditions += 1
255
+ return num_conditions
256
+
257
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance._is_cfg_enabled
258
+ def _is_cfg_enabled(self) -> bool:
259
+ if not self._enabled:
260
+ return False
261
+
262
+ is_within_range = True
263
+ if self._num_inference_steps is not None:
264
+ skip_start_step = int(self._start * self._num_inference_steps)
265
+ skip_stop_step = int(self._stop * self._num_inference_steps)
266
+ is_within_range = skip_start_step <= self._step < skip_stop_step
267
+
268
+ is_close = False
269
+ if self.use_original_formulation:
270
+ is_close = math.isclose(self.guidance_scale, 0.0)
271
+ else:
272
+ is_close = math.isclose(self.guidance_scale, 1.0)
273
+
274
+ return is_within_range and not is_close
275
+
276
+ # Copied from diffusers.guiders.skip_layer_guidance.SkipLayerGuidance._is_slg_enabled
277
+ def _is_slg_enabled(self) -> bool:
278
+ if not self._enabled:
279
+ return False
280
+
281
+ is_within_range = True
282
+ if self._num_inference_steps is not None:
283
+ skip_start_step = int(self.skip_layer_guidance_start * self._num_inference_steps)
284
+ skip_stop_step = int(self.skip_layer_guidance_stop * self._num_inference_steps)
285
+ is_within_range = skip_start_step < self._step < skip_stop_step
286
+
287
+ is_zero = math.isclose(self.skip_layer_guidance_scale, 0.0)
288
+
289
+ return is_within_range and not is_zero
diffusers/guiders/skip_layer_guidance.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from ..hooks import HookRegistry, LayerSkipConfig
24
+ from ..hooks.layer_skip import _apply_layer_skip_hook
25
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from ..modular_pipelines.modular_pipeline import BlockState
30
+
31
+
32
+ class SkipLayerGuidance(BaseGuidance):
33
+ """
34
+ Skip Layer Guidance (SLG): https://github.com/Stability-AI/sd3.5
35
+
36
+ Spatio-Temporal Guidance (STG): https://huggingface.co/papers/2411.18664
37
+
38
+ SLG was introduced by StabilityAI for improving structure and anotomy coherence in generated images. It works by
39
+ skipping the forward pass of specified transformer blocks during the denoising process on an additional conditional
40
+ batch of data, apart from the conditional and unconditional batches already used in CFG
41
+ ([~guiders.classifier_free_guidance.ClassifierFreeGuidance]), and then scaling and shifting the CFG predictions
42
+ based on the difference between conditional without skipping and conditional with skipping predictions.
43
+
44
+ The intution behind SLG can be thought of as moving the CFG predicted distribution estimates further away from
45
+ worse versions of the conditional distribution estimates (because skipping layers is equivalent to using a worse
46
+ version of the model for the conditional prediction).
47
+
48
+ STG is an improvement and follow-up work combining ideas from SLG, PAG and similar techniques for improving
49
+ generation quality in video diffusion models.
50
+
51
+ Additional reading:
52
+ - [Guiding a Diffusion Model with a Bad Version of Itself](https://huggingface.co/papers/2406.02507)
53
+
54
+ The values for `skip_layer_guidance_scale`, `skip_layer_guidance_start`, and `skip_layer_guidance_stop` are
55
+ defaulted to the recommendations by StabilityAI for Stable Diffusion 3.5 Medium.
56
+
57
+ Args:
58
+ guidance_scale (`float`, defaults to `7.5`):
59
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
60
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
61
+ deterioration of image quality.
62
+ skip_layer_guidance_scale (`float`, defaults to `2.8`):
63
+ The scale parameter for skip layer guidance. Anatomy and structure coherence may improve with higher
64
+ values, but it may also lead to overexposure and saturation.
65
+ skip_layer_guidance_start (`float`, defaults to `0.01`):
66
+ The fraction of the total number of denoising steps after which skip layer guidance starts.
67
+ skip_layer_guidance_stop (`float`, defaults to `0.2`):
68
+ The fraction of the total number of denoising steps after which skip layer guidance stops.
69
+ skip_layer_guidance_layers (`int` or `list[int]`, *optional*):
70
+ The layer indices to apply skip layer guidance to. Can be a single integer or a list of integers. If not
71
+ provided, `skip_layer_config` must be provided. The recommended values are `[7, 8, 9]` for Stable Diffusion
72
+ 3.5 Medium.
73
+ skip_layer_config (`LayerSkipConfig` or `list[LayerSkipConfig]`, *optional*):
74
+ The configuration for the skip layer guidance. Can be a single `LayerSkipConfig` or a list of
75
+ `LayerSkipConfig`. If not provided, `skip_layer_guidance_layers` must be provided.
76
+ guidance_rescale (`float`, defaults to `0.0`):
77
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
78
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
79
+ Flawed](https://huggingface.co/papers/2305.08891).
80
+ use_original_formulation (`bool`, defaults to `False`):
81
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
82
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
83
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
84
+ start (`float`, defaults to `0.01`):
85
+ The fraction of the total number of denoising steps after which guidance starts.
86
+ stop (`float`, defaults to `0.2`):
87
+ The fraction of the total number of denoising steps after which guidance stops.
88
+ """
89
+
90
+ _input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
91
+
92
+ @register_to_config
93
+ def __init__(
94
+ self,
95
+ guidance_scale: float = 7.5,
96
+ skip_layer_guidance_scale: float = 2.8,
97
+ skip_layer_guidance_start: float = 0.01,
98
+ skip_layer_guidance_stop: float = 0.2,
99
+ skip_layer_guidance_layers: int | list[int] | None = None,
100
+ skip_layer_config: LayerSkipConfig | list[LayerSkipConfig] | dict[str, Any] = None,
101
+ guidance_rescale: float = 0.0,
102
+ use_original_formulation: bool = False,
103
+ start: float = 0.0,
104
+ stop: float = 1.0,
105
+ enabled: bool = True,
106
+ ):
107
+ super().__init__(start, stop, enabled)
108
+
109
+ self.guidance_scale = guidance_scale
110
+ self.skip_layer_guidance_scale = skip_layer_guidance_scale
111
+ self.skip_layer_guidance_start = skip_layer_guidance_start
112
+ self.skip_layer_guidance_stop = skip_layer_guidance_stop
113
+ self.guidance_rescale = guidance_rescale
114
+ self.use_original_formulation = use_original_formulation
115
+
116
+ if not (0.0 <= skip_layer_guidance_start < 1.0):
117
+ raise ValueError(
118
+ f"Expected `skip_layer_guidance_start` to be between 0.0 and 1.0, but got {skip_layer_guidance_start}."
119
+ )
120
+ if not (skip_layer_guidance_start <= skip_layer_guidance_stop <= 1.0):
121
+ raise ValueError(
122
+ f"Expected `skip_layer_guidance_stop` to be between 0.0 and 1.0, but got {skip_layer_guidance_stop}."
123
+ )
124
+
125
+ if skip_layer_guidance_layers is None and skip_layer_config is None:
126
+ raise ValueError(
127
+ "Either `skip_layer_guidance_layers` or `skip_layer_config` must be provided to enable Skip Layer Guidance."
128
+ )
129
+ if skip_layer_guidance_layers is not None and skip_layer_config is not None:
130
+ raise ValueError("Only one of `skip_layer_guidance_layers` or `skip_layer_config` can be provided.")
131
+
132
+ if skip_layer_guidance_layers is not None:
133
+ if isinstance(skip_layer_guidance_layers, int):
134
+ skip_layer_guidance_layers = [skip_layer_guidance_layers]
135
+ if not isinstance(skip_layer_guidance_layers, list):
136
+ raise ValueError(
137
+ f"Expected `skip_layer_guidance_layers` to be an int or a list of ints, but got {type(skip_layer_guidance_layers)}."
138
+ )
139
+ skip_layer_config = [LayerSkipConfig(layer, fqn="auto") for layer in skip_layer_guidance_layers]
140
+
141
+ if isinstance(skip_layer_config, dict):
142
+ skip_layer_config = LayerSkipConfig.from_dict(skip_layer_config)
143
+
144
+ if isinstance(skip_layer_config, LayerSkipConfig):
145
+ skip_layer_config = [skip_layer_config]
146
+
147
+ if not isinstance(skip_layer_config, list):
148
+ raise ValueError(
149
+ f"Expected `skip_layer_config` to be a LayerSkipConfig or a list of LayerSkipConfig, but got {type(skip_layer_config)}."
150
+ )
151
+ elif isinstance(next(iter(skip_layer_config), None), dict):
152
+ skip_layer_config = [LayerSkipConfig.from_dict(config) for config in skip_layer_config]
153
+
154
+ self.skip_layer_config = skip_layer_config
155
+ self._skip_layer_hook_names = [f"SkipLayerGuidance_{i}" for i in range(len(self.skip_layer_config))]
156
+
157
+ def prepare_models(self, denoiser: torch.nn.Module) -> None:
158
+ self._count_prepared += 1
159
+ if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
160
+ for name, config in zip(self._skip_layer_hook_names, self.skip_layer_config):
161
+ _apply_layer_skip_hook(denoiser, config, name=name)
162
+
163
+ def cleanup_models(self, denoiser: torch.nn.Module) -> None:
164
+ if self._is_slg_enabled() and self.is_conditional and self._count_prepared > 1:
165
+ registry = HookRegistry.check_if_exists_or_initialize(denoiser)
166
+ # Remove the hooks after inference
167
+ for hook_name in self._skip_layer_hook_names:
168
+ registry.remove_hook(hook_name, recurse=True)
169
+
170
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
171
+ if self.num_conditions == 1:
172
+ tuple_indices = [0]
173
+ input_predictions = ["pred_cond"]
174
+ elif self.num_conditions == 2:
175
+ tuple_indices = [0, 1]
176
+ input_predictions = (
177
+ ["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
178
+ )
179
+ else:
180
+ tuple_indices = [0, 1, 0]
181
+ input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
182
+ data_batches = []
183
+ for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
184
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
185
+ data_batches.append(data_batch)
186
+ return data_batches
187
+
188
+ def prepare_inputs_from_block_state(
189
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
190
+ ) -> list["BlockState"]:
191
+ if self.num_conditions == 1:
192
+ tuple_indices = [0]
193
+ input_predictions = ["pred_cond"]
194
+ elif self.num_conditions == 2:
195
+ tuple_indices = [0, 1]
196
+ input_predictions = (
197
+ ["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_skip"]
198
+ )
199
+ else:
200
+ tuple_indices = [0, 1, 0]
201
+ input_predictions = ["pred_cond", "pred_uncond", "pred_cond_skip"]
202
+ data_batches = []
203
+ for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
204
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
205
+ data_batches.append(data_batch)
206
+ return data_batches
207
+
208
+ def forward(
209
+ self,
210
+ pred_cond: torch.Tensor,
211
+ pred_uncond: torch.Tensor | None = None,
212
+ pred_cond_skip: torch.Tensor | None = None,
213
+ ) -> GuiderOutput:
214
+ pred = None
215
+
216
+ if not self._is_cfg_enabled() and not self._is_slg_enabled():
217
+ pred = pred_cond
218
+ elif not self._is_cfg_enabled():
219
+ shift = pred_cond - pred_cond_skip
220
+ pred = pred_cond if self.use_original_formulation else pred_cond_skip
221
+ pred = pred + self.skip_layer_guidance_scale * shift
222
+ elif not self._is_slg_enabled():
223
+ shift = pred_cond - pred_uncond
224
+ pred = pred_cond if self.use_original_formulation else pred_uncond
225
+ pred = pred + self.guidance_scale * shift
226
+ else:
227
+ shift = pred_cond - pred_uncond
228
+ shift_skip = pred_cond - pred_cond_skip
229
+ pred = pred_cond if self.use_original_formulation else pred_uncond
230
+ pred = pred + self.guidance_scale * shift + self.skip_layer_guidance_scale * shift_skip
231
+
232
+ if self.guidance_rescale > 0.0:
233
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
234
+
235
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
236
+
237
+ @property
238
+ def is_conditional(self) -> bool:
239
+ return self._count_prepared == 1 or self._count_prepared == 3
240
+
241
+ @property
242
+ def num_conditions(self) -> int:
243
+ num_conditions = 1
244
+ if self._is_cfg_enabled():
245
+ num_conditions += 1
246
+ if self._is_slg_enabled():
247
+ num_conditions += 1
248
+ return num_conditions
249
+
250
+ def _is_cfg_enabled(self) -> bool:
251
+ if not self._enabled:
252
+ return False
253
+
254
+ is_within_range = True
255
+ if self._num_inference_steps is not None:
256
+ skip_start_step = int(self._start * self._num_inference_steps)
257
+ skip_stop_step = int(self._stop * self._num_inference_steps)
258
+ is_within_range = skip_start_step <= self._step < skip_stop_step
259
+
260
+ is_close = False
261
+ if self.use_original_formulation:
262
+ is_close = math.isclose(self.guidance_scale, 0.0)
263
+ else:
264
+ is_close = math.isclose(self.guidance_scale, 1.0)
265
+
266
+ return is_within_range and not is_close
267
+
268
+ def _is_slg_enabled(self) -> bool:
269
+ if not self._enabled:
270
+ return False
271
+
272
+ is_within_range = True
273
+ if self._num_inference_steps is not None:
274
+ skip_start_step = int(self.skip_layer_guidance_start * self._num_inference_steps)
275
+ skip_stop_step = int(self.skip_layer_guidance_stop * self._num_inference_steps)
276
+ is_within_range = skip_start_step < self._step < skip_stop_step
277
+
278
+ is_zero = math.isclose(self.skip_layer_guidance_scale, 0.0)
279
+
280
+ return is_within_range and not is_zero
diffusers/guiders/smoothed_energy_guidance.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from ..hooks import HookRegistry
24
+ from ..hooks.smoothed_energy_guidance_utils import SmoothedEnergyGuidanceConfig, _apply_smoothed_energy_guidance_hook
25
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from ..modular_pipelines.modular_pipeline import BlockState
30
+
31
+
32
+ class SmoothedEnergyGuidance(BaseGuidance):
33
+ """
34
+ Smoothed Energy Guidance (SEG): https://huggingface.co/papers/2408.00760
35
+
36
+ SEG is only supported as an experimental prototype feature for now, so the implementation may be modified in the
37
+ future without warning or guarantee of reproducibility. This implementation assumes:
38
+ - Generated images are square (height == width)
39
+ - The model does not combine different modalities together (e.g., text and image latent streams are not combined
40
+ together such as Flux)
41
+
42
+ Args:
43
+ guidance_scale (`float`, defaults to `7.5`):
44
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
45
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
46
+ deterioration of image quality.
47
+ seg_guidance_scale (`float`, defaults to `3.0`):
48
+ The scale parameter for smoothed energy guidance. Anatomy and structure coherence may improve with higher
49
+ values, but it may also lead to overexposure and saturation.
50
+ seg_blur_sigma (`float`, defaults to `9999999.0`):
51
+ The amount by which we blur the attention weights. Setting this value greater than 9999.0 results in
52
+ infinite blur, which means uniform queries. Controlling it exponentially is empirically effective.
53
+ seg_blur_threshold_inf (`float`, defaults to `9999.0`):
54
+ The threshold above which the blur is considered infinite.
55
+ seg_guidance_start (`float`, defaults to `0.0`):
56
+ The fraction of the total number of denoising steps after which smoothed energy guidance starts.
57
+ seg_guidance_stop (`float`, defaults to `1.0`):
58
+ The fraction of the total number of denoising steps after which smoothed energy guidance stops.
59
+ seg_guidance_layers (`int` or `list[int]`, *optional*):
60
+ The layer indices to apply smoothed energy guidance to. Can be a single integer or a list of integers. If
61
+ not provided, `seg_guidance_config` must be provided. The recommended values are `[7, 8, 9]` for Stable
62
+ Diffusion 3.5 Medium.
63
+ seg_guidance_config (`SmoothedEnergyGuidanceConfig` or `list[SmoothedEnergyGuidanceConfig]`, *optional*):
64
+ The configuration for the smoothed energy layer guidance. Can be a single `SmoothedEnergyGuidanceConfig` or
65
+ a list of `SmoothedEnergyGuidanceConfig`. If not provided, `seg_guidance_layers` must be provided.
66
+ guidance_rescale (`float`, defaults to `0.0`):
67
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
68
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
69
+ Flawed](https://huggingface.co/papers/2305.08891).
70
+ use_original_formulation (`bool`, defaults to `False`):
71
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
72
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
73
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
74
+ start (`float`, defaults to `0.01`):
75
+ The fraction of the total number of denoising steps after which guidance starts.
76
+ stop (`float`, defaults to `0.2`):
77
+ The fraction of the total number of denoising steps after which guidance stops.
78
+ """
79
+
80
+ _input_predictions = ["pred_cond", "pred_uncond", "pred_cond_seg"]
81
+
82
+ @register_to_config
83
+ def __init__(
84
+ self,
85
+ guidance_scale: float = 7.5,
86
+ seg_guidance_scale: float = 2.8,
87
+ seg_blur_sigma: float = 9999999.0,
88
+ seg_blur_threshold_inf: float = 9999.0,
89
+ seg_guidance_start: float = 0.0,
90
+ seg_guidance_stop: float = 1.0,
91
+ seg_guidance_layers: int | list[int] | None = None,
92
+ seg_guidance_config: SmoothedEnergyGuidanceConfig | list[SmoothedEnergyGuidanceConfig] = None,
93
+ guidance_rescale: float = 0.0,
94
+ use_original_formulation: bool = False,
95
+ start: float = 0.0,
96
+ stop: float = 1.0,
97
+ enabled: bool = True,
98
+ ):
99
+ super().__init__(start, stop, enabled)
100
+
101
+ self.guidance_scale = guidance_scale
102
+ self.seg_guidance_scale = seg_guidance_scale
103
+ self.seg_blur_sigma = seg_blur_sigma
104
+ self.seg_blur_threshold_inf = seg_blur_threshold_inf
105
+ self.seg_guidance_start = seg_guidance_start
106
+ self.seg_guidance_stop = seg_guidance_stop
107
+ self.guidance_rescale = guidance_rescale
108
+ self.use_original_formulation = use_original_formulation
109
+
110
+ if not (0.0 <= seg_guidance_start < 1.0):
111
+ raise ValueError(f"Expected `seg_guidance_start` to be between 0.0 and 1.0, but got {seg_guidance_start}.")
112
+ if not (seg_guidance_start <= seg_guidance_stop <= 1.0):
113
+ raise ValueError(f"Expected `seg_guidance_stop` to be between 0.0 and 1.0, but got {seg_guidance_stop}.")
114
+
115
+ if seg_guidance_layers is None and seg_guidance_config is None:
116
+ raise ValueError(
117
+ "Either `seg_guidance_layers` or `seg_guidance_config` must be provided to enable Smoothed Energy Guidance."
118
+ )
119
+ if seg_guidance_layers is not None and seg_guidance_config is not None:
120
+ raise ValueError("Only one of `seg_guidance_layers` or `seg_guidance_config` can be provided.")
121
+
122
+ if seg_guidance_layers is not None:
123
+ if isinstance(seg_guidance_layers, int):
124
+ seg_guidance_layers = [seg_guidance_layers]
125
+ if not isinstance(seg_guidance_layers, list):
126
+ raise ValueError(
127
+ f"Expected `seg_guidance_layers` to be an int or a list of ints, but got {type(seg_guidance_layers)}."
128
+ )
129
+ seg_guidance_config = [SmoothedEnergyGuidanceConfig(layer, fqn="auto") for layer in seg_guidance_layers]
130
+
131
+ if isinstance(seg_guidance_config, dict):
132
+ seg_guidance_config = SmoothedEnergyGuidanceConfig.from_dict(seg_guidance_config)
133
+
134
+ if isinstance(seg_guidance_config, SmoothedEnergyGuidanceConfig):
135
+ seg_guidance_config = [seg_guidance_config]
136
+
137
+ if not isinstance(seg_guidance_config, list):
138
+ raise ValueError(
139
+ f"Expected `seg_guidance_config` to be a SmoothedEnergyGuidanceConfig or a list of SmoothedEnergyGuidanceConfig, but got {type(seg_guidance_config)}."
140
+ )
141
+ elif isinstance(next(iter(seg_guidance_config), None), dict):
142
+ seg_guidance_config = [SmoothedEnergyGuidanceConfig.from_dict(config) for config in seg_guidance_config]
143
+
144
+ self.seg_guidance_config = seg_guidance_config
145
+ self._seg_layer_hook_names = [f"SmoothedEnergyGuidance_{i}" for i in range(len(self.seg_guidance_config))]
146
+
147
+ def prepare_models(self, denoiser: torch.nn.Module) -> None:
148
+ if self._is_seg_enabled() and self.is_conditional and self._count_prepared > 1:
149
+ for name, config in zip(self._seg_layer_hook_names, self.seg_guidance_config):
150
+ _apply_smoothed_energy_guidance_hook(denoiser, config, self.seg_blur_sigma, name=name)
151
+
152
+ def cleanup_models(self, denoiser: torch.nn.Module):
153
+ if self._is_seg_enabled() and self.is_conditional and self._count_prepared > 1:
154
+ registry = HookRegistry.check_if_exists_or_initialize(denoiser)
155
+ # Remove the hooks after inference
156
+ for hook_name in self._seg_layer_hook_names:
157
+ registry.remove_hook(hook_name, recurse=True)
158
+
159
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
160
+ if self.num_conditions == 1:
161
+ tuple_indices = [0]
162
+ input_predictions = ["pred_cond"]
163
+ elif self.num_conditions == 2:
164
+ tuple_indices = [0, 1]
165
+ input_predictions = (
166
+ ["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_seg"]
167
+ )
168
+ else:
169
+ tuple_indices = [0, 1, 0]
170
+ input_predictions = ["pred_cond", "pred_uncond", "pred_cond_seg"]
171
+ data_batches = []
172
+ for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
173
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
174
+ data_batches.append(data_batch)
175
+ return data_batches
176
+
177
+ def prepare_inputs_from_block_state(
178
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
179
+ ) -> list["BlockState"]:
180
+ if self.num_conditions == 1:
181
+ tuple_indices = [0]
182
+ input_predictions = ["pred_cond"]
183
+ elif self.num_conditions == 2:
184
+ tuple_indices = [0, 1]
185
+ input_predictions = (
186
+ ["pred_cond", "pred_uncond"] if self._is_cfg_enabled() else ["pred_cond", "pred_cond_seg"]
187
+ )
188
+ else:
189
+ tuple_indices = [0, 1, 0]
190
+ input_predictions = ["pred_cond", "pred_uncond", "pred_cond_seg"]
191
+ data_batches = []
192
+ for tuple_idx, input_prediction in zip(tuple_indices, input_predictions):
193
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
194
+ data_batches.append(data_batch)
195
+ return data_batches
196
+
197
+ def forward(
198
+ self,
199
+ pred_cond: torch.Tensor,
200
+ pred_uncond: torch.Tensor | None = None,
201
+ pred_cond_seg: torch.Tensor | None = None,
202
+ ) -> GuiderOutput:
203
+ pred = None
204
+
205
+ if not self._is_cfg_enabled() and not self._is_seg_enabled():
206
+ pred = pred_cond
207
+ elif not self._is_cfg_enabled():
208
+ shift = pred_cond - pred_cond_seg
209
+ pred = pred_cond if self.use_original_formulation else pred_cond_seg
210
+ pred = pred + self.seg_guidance_scale * shift
211
+ elif not self._is_seg_enabled():
212
+ shift = pred_cond - pred_uncond
213
+ pred = pred_cond if self.use_original_formulation else pred_uncond
214
+ pred = pred + self.guidance_scale * shift
215
+ else:
216
+ shift = pred_cond - pred_uncond
217
+ shift_seg = pred_cond - pred_cond_seg
218
+ pred = pred_cond if self.use_original_formulation else pred_uncond
219
+ pred = pred + self.guidance_scale * shift + self.seg_guidance_scale * shift_seg
220
+
221
+ if self.guidance_rescale > 0.0:
222
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
223
+
224
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
225
+
226
+ @property
227
+ def is_conditional(self) -> bool:
228
+ return self._count_prepared == 1 or self._count_prepared == 3
229
+
230
+ @property
231
+ def num_conditions(self) -> int:
232
+ num_conditions = 1
233
+ if self._is_cfg_enabled():
234
+ num_conditions += 1
235
+ if self._is_seg_enabled():
236
+ num_conditions += 1
237
+ return num_conditions
238
+
239
+ def _is_cfg_enabled(self) -> bool:
240
+ if not self._enabled:
241
+ return False
242
+
243
+ is_within_range = True
244
+ if self._num_inference_steps is not None:
245
+ skip_start_step = int(self._start * self._num_inference_steps)
246
+ skip_stop_step = int(self._stop * self._num_inference_steps)
247
+ is_within_range = skip_start_step <= self._step < skip_stop_step
248
+
249
+ is_close = False
250
+ if self.use_original_formulation:
251
+ is_close = math.isclose(self.guidance_scale, 0.0)
252
+ else:
253
+ is_close = math.isclose(self.guidance_scale, 1.0)
254
+
255
+ return is_within_range and not is_close
256
+
257
+ def _is_seg_enabled(self) -> bool:
258
+ if not self._enabled:
259
+ return False
260
+
261
+ is_within_range = True
262
+ if self._num_inference_steps is not None:
263
+ skip_start_step = int(self.seg_guidance_start * self._num_inference_steps)
264
+ skip_stop_step = int(self.seg_guidance_stop * self._num_inference_steps)
265
+ is_within_range = skip_start_step < self._step < skip_stop_step
266
+
267
+ is_zero = math.isclose(self.seg_guidance_scale, 0.0)
268
+
269
+ return is_within_range and not is_zero
diffusers/guiders/tangential_classifier_free_guidance.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import TYPE_CHECKING
19
+
20
+ import torch
21
+
22
+ from ..configuration_utils import register_to_config
23
+ from .guider_utils import BaseGuidance, GuiderOutput, rescale_noise_cfg
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from ..modular_pipelines.modular_pipeline import BlockState
28
+
29
+
30
+ class TangentialClassifierFreeGuidance(BaseGuidance):
31
+ """
32
+ Tangential Classifier Free Guidance (TCFG): https://huggingface.co/papers/2503.18137
33
+
34
+ Args:
35
+ guidance_scale (`float`, defaults to `7.5`):
36
+ The scale parameter for classifier-free guidance. Higher values result in stronger conditioning on the text
37
+ prompt, while lower values allow for more freedom in generation. Higher values may lead to saturation and
38
+ deterioration of image quality.
39
+ guidance_rescale (`float`, defaults to `0.0`):
40
+ The rescale factor applied to the noise predictions. This is used to improve image quality and fix
41
+ overexposure. Based on Section 3.4 from [Common Diffusion Noise Schedules and Sample Steps are
42
+ Flawed](https://huggingface.co/papers/2305.08891).
43
+ use_original_formulation (`bool`, defaults to `False`):
44
+ Whether to use the original formulation of classifier-free guidance as proposed in the paper. By default,
45
+ we use the diffusers-native implementation that has been in the codebase for a long time. See
46
+ [~guiders.classifier_free_guidance.ClassifierFreeGuidance] for more details.
47
+ start (`float`, defaults to `0.0`):
48
+ The fraction of the total number of denoising steps after which guidance starts.
49
+ stop (`float`, defaults to `1.0`):
50
+ The fraction of the total number of denoising steps after which guidance stops.
51
+ """
52
+
53
+ _input_predictions = ["pred_cond", "pred_uncond"]
54
+
55
+ @register_to_config
56
+ def __init__(
57
+ self,
58
+ guidance_scale: float = 7.5,
59
+ guidance_rescale: float = 0.0,
60
+ use_original_formulation: bool = False,
61
+ start: float = 0.0,
62
+ stop: float = 1.0,
63
+ enabled: bool = True,
64
+ ):
65
+ super().__init__(start, stop, enabled)
66
+
67
+ self.guidance_scale = guidance_scale
68
+ self.guidance_rescale = guidance_rescale
69
+ self.use_original_formulation = use_original_formulation
70
+
71
+ def prepare_inputs(self, data: dict[str, tuple[torch.Tensor, torch.Tensor]]) -> list["BlockState"]:
72
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
73
+ data_batches = []
74
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
75
+ data_batch = self._prepare_batch(data, tuple_idx, input_prediction)
76
+ data_batches.append(data_batch)
77
+ return data_batches
78
+
79
+ def prepare_inputs_from_block_state(
80
+ self, data: "BlockState", input_fields: dict[str, str | tuple[str, str]]
81
+ ) -> list["BlockState"]:
82
+ tuple_indices = [0] if self.num_conditions == 1 else [0, 1]
83
+ data_batches = []
84
+ for tuple_idx, input_prediction in zip(tuple_indices, self._input_predictions):
85
+ data_batch = self._prepare_batch_from_block_state(input_fields, data, tuple_idx, input_prediction)
86
+ data_batches.append(data_batch)
87
+ return data_batches
88
+
89
+ def forward(self, pred_cond: torch.Tensor, pred_uncond: torch.Tensor | None = None) -> GuiderOutput:
90
+ pred = None
91
+
92
+ if not self._is_tcfg_enabled():
93
+ pred = pred_cond
94
+ else:
95
+ pred = normalized_guidance(pred_cond, pred_uncond, self.guidance_scale, self.use_original_formulation)
96
+
97
+ if self.guidance_rescale > 0.0:
98
+ pred = rescale_noise_cfg(pred, pred_cond, self.guidance_rescale)
99
+
100
+ return GuiderOutput(pred=pred, pred_cond=pred_cond, pred_uncond=pred_uncond)
101
+
102
+ @property
103
+ def is_conditional(self) -> bool:
104
+ return self._num_outputs_prepared == 1
105
+
106
+ @property
107
+ def num_conditions(self) -> int:
108
+ num_conditions = 1
109
+ if self._is_tcfg_enabled():
110
+ num_conditions += 1
111
+ return num_conditions
112
+
113
+ def _is_tcfg_enabled(self) -> bool:
114
+ if not self._enabled:
115
+ return False
116
+
117
+ is_within_range = True
118
+ if self._num_inference_steps is not None:
119
+ skip_start_step = int(self._start * self._num_inference_steps)
120
+ skip_stop_step = int(self._stop * self._num_inference_steps)
121
+ is_within_range = skip_start_step <= self._step < skip_stop_step
122
+
123
+ is_close = False
124
+ if self.use_original_formulation:
125
+ is_close = math.isclose(self.guidance_scale, 0.0)
126
+ else:
127
+ is_close = math.isclose(self.guidance_scale, 1.0)
128
+
129
+ return is_within_range and not is_close
130
+
131
+
132
+ def normalized_guidance(
133
+ pred_cond: torch.Tensor, pred_uncond: torch.Tensor, guidance_scale: float, use_original_formulation: bool = False
134
+ ) -> torch.Tensor:
135
+ cond_dtype = pred_cond.dtype
136
+ preds = torch.stack([pred_cond, pred_uncond], dim=1).float()
137
+ preds = preds.flatten(2)
138
+ U, S, Vh = torch.linalg.svd(preds, full_matrices=False)
139
+ Vh_modified = Vh.clone()
140
+ Vh_modified[:, 1] = 0
141
+
142
+ uncond_flat = pred_uncond.reshape(pred_uncond.size(0), 1, -1).float()
143
+ x_Vh = torch.matmul(uncond_flat, Vh.transpose(-2, -1))
144
+ x_Vh_V = torch.matmul(x_Vh, Vh_modified)
145
+ pred_uncond = x_Vh_V.reshape(pred_uncond.shape).to(cond_dtype)
146
+
147
+ pred = pred_cond if use_original_formulation else pred_uncond
148
+ shift = pred_cond - pred_uncond
149
+ pred = pred + guidance_scale * shift
150
+
151
+ return pred
diffusers/hooks/__init__.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from ..utils import is_torch_available
16
+
17
+
18
+ if is_torch_available():
19
+ from .context_parallel import apply_context_parallel
20
+ from .faster_cache import FasterCacheConfig, apply_faster_cache
21
+ from .first_block_cache import FirstBlockCacheConfig, apply_first_block_cache
22
+ from .group_offloading import apply_group_offloading
23
+ from .hooks import HookRegistry, ModelHook
24
+ from .layer_skip import LayerSkipConfig, apply_layer_skip
25
+ from .layerwise_casting import apply_layerwise_casting, apply_layerwise_casting_hook
26
+ from .mag_cache import MagCacheConfig, apply_mag_cache
27
+ from .pyramid_attention_broadcast import PyramidAttentionBroadcastConfig, apply_pyramid_attention_broadcast
28
+ from .smoothed_energy_guidance_utils import SmoothedEnergyGuidanceConfig
29
+ from .taylorseer_cache import TaylorSeerCacheConfig, apply_taylorseer_cache
30
+ from .tensor_parallel import apply_tensor_parallel
31
+ from .text_kv_cache import TextKVCacheConfig, apply_text_kv_cache
diffusers/hooks/_common.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+
17
+ from ..models.attention import AttentionModuleMixin, FeedForward, LuminaFeedForward
18
+ from ..models.attention_processor import Attention, MochiAttention
19
+
20
+
21
+ _ATTENTION_CLASSES = (Attention, MochiAttention, AttentionModuleMixin)
22
+ _FEEDFORWARD_CLASSES = (FeedForward, LuminaFeedForward)
23
+
24
+ _SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS = (
25
+ "blocks",
26
+ "transformer_blocks",
27
+ "single_transformer_blocks",
28
+ "layers",
29
+ "visual_transformer_blocks",
30
+ )
31
+ _TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS = ("temporal_transformer_blocks",)
32
+ _CROSS_TRANSFORMER_BLOCK_IDENTIFIERS = ("blocks", "transformer_blocks", "layers")
33
+
34
+ _ALL_TRANSFORMER_BLOCK_IDENTIFIERS = tuple(
35
+ {
36
+ *_SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS,
37
+ *_TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS,
38
+ *_CROSS_TRANSFORMER_BLOCK_IDENTIFIERS,
39
+ }
40
+ )
41
+
42
+ # Layers supported for group offloading and layerwise casting
43
+ _GO_LC_SUPPORTED_PYTORCH_LAYERS = (
44
+ torch.nn.Conv1d,
45
+ torch.nn.Conv2d,
46
+ torch.nn.Conv3d,
47
+ torch.nn.ConvTranspose1d,
48
+ torch.nn.ConvTranspose2d,
49
+ torch.nn.ConvTranspose3d,
50
+ torch.nn.Linear,
51
+ torch.nn.Embedding,
52
+ # TODO(aryan): look into torch.nn.LayerNorm, torch.nn.GroupNorm later, seems to be causing some issues with CogVideoX
53
+ # because of double invocation of the same norm layer in CogVideoXLayerNorm
54
+ )
55
+
56
+
57
+ def _get_submodule_from_fqn(module: torch.nn.Module, fqn: str) -> torch.nn.Module | None:
58
+ for submodule_name, submodule in module.named_modules():
59
+ if submodule_name == fqn:
60
+ return submodule
61
+ return None
diffusers/hooks/_helpers.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ from dataclasses import dataclass
17
+ from typing import Any, Callable, Type
18
+
19
+
20
+ @dataclass
21
+ class AttentionProcessorMetadata:
22
+ skip_processor_output_fn: Callable[[Any], Any]
23
+
24
+
25
+ @dataclass
26
+ class TransformerBlockMetadata:
27
+ return_hidden_states_index: int = None
28
+ return_encoder_hidden_states_index: int = None
29
+ hidden_states_argument_name: str = "hidden_states"
30
+
31
+ _cls: Type = None
32
+ _cached_parameter_indices: dict[str, int] = None
33
+
34
+ def _get_parameter_from_args_kwargs(self, identifier: str, args=(), kwargs=None):
35
+ kwargs = kwargs or {}
36
+ if identifier in kwargs:
37
+ return kwargs[identifier]
38
+ if self._cached_parameter_indices is not None:
39
+ return args[self._cached_parameter_indices[identifier]]
40
+ if self._cls is None:
41
+ raise ValueError("Model class is not set for metadata.")
42
+ parameters = list(inspect.signature(self._cls.forward).parameters.keys())
43
+ parameters = parameters[1:] # skip `self`
44
+ self._cached_parameter_indices = {param: i for i, param in enumerate(parameters)}
45
+ if identifier not in self._cached_parameter_indices:
46
+ raise ValueError(f"Parameter '{identifier}' not found in function signature but was requested.")
47
+ index = self._cached_parameter_indices[identifier]
48
+ if index >= len(args):
49
+ raise ValueError(f"Expected {index} arguments but got {len(args)}.")
50
+ return args[index]
51
+
52
+
53
+ class AttentionProcessorRegistry:
54
+ _registry = {}
55
+ # TODO(aryan): this is only required for the time being because we need to do the registrations
56
+ # for classes. If we do it eagerly, i.e. call the functions in global scope, we will get circular
57
+ # import errors because of the models imported in this file.
58
+ _is_registered = False
59
+
60
+ @classmethod
61
+ def register(cls, model_class: Type, metadata: AttentionProcessorMetadata):
62
+ cls._register()
63
+ cls._registry[model_class] = metadata
64
+
65
+ @classmethod
66
+ def get(cls, model_class: Type) -> AttentionProcessorMetadata:
67
+ cls._register()
68
+ if model_class not in cls._registry:
69
+ raise ValueError(f"Model class {model_class} not registered.")
70
+ return cls._registry[model_class]
71
+
72
+ @classmethod
73
+ def _register(cls):
74
+ if cls._is_registered:
75
+ return
76
+ cls._is_registered = True
77
+ _register_attention_processors_metadata()
78
+
79
+
80
+ class TransformerBlockRegistry:
81
+ _registry = {}
82
+ # TODO(aryan): this is only required for the time being because we need to do the registrations
83
+ # for classes. If we do it eagerly, i.e. call the functions in global scope, we will get circular
84
+ # import errors because of the models imported in this file.
85
+ _is_registered = False
86
+
87
+ @classmethod
88
+ def register(cls, model_class: Type, metadata: TransformerBlockMetadata):
89
+ cls._register()
90
+ metadata._cls = model_class
91
+ cls._registry[model_class] = metadata
92
+
93
+ @classmethod
94
+ def get(cls, model_class: Type) -> TransformerBlockMetadata:
95
+ cls._register()
96
+ if model_class not in cls._registry:
97
+ raise ValueError(f"Model class {model_class} not registered.")
98
+ return cls._registry[model_class]
99
+
100
+ @classmethod
101
+ def _register(cls):
102
+ if cls._is_registered:
103
+ return
104
+ cls._is_registered = True
105
+ _register_transformer_blocks_metadata()
106
+
107
+
108
+ def _register_attention_processors_metadata():
109
+ from ..models.attention_processor import AttnProcessor2_0
110
+ from ..models.transformers.transformer_cogview4 import CogView4AttnProcessor
111
+ from ..models.transformers.transformer_flux import FluxAttnProcessor
112
+ from ..models.transformers.transformer_hunyuanimage import HunyuanImageAttnProcessor
113
+ from ..models.transformers.transformer_qwenimage import QwenDoubleStreamAttnProcessor2_0
114
+ from ..models.transformers.transformer_wan import WanAttnProcessor2_0
115
+ from ..models.transformers.transformer_z_image import ZSingleStreamAttnProcessor
116
+
117
+ # AttnProcessor2_0
118
+ AttentionProcessorRegistry.register(
119
+ model_class=AttnProcessor2_0,
120
+ metadata=AttentionProcessorMetadata(
121
+ skip_processor_output_fn=_skip_proc_output_fn_Attention_AttnProcessor2_0,
122
+ ),
123
+ )
124
+
125
+ # CogView4AttnProcessor
126
+ AttentionProcessorRegistry.register(
127
+ model_class=CogView4AttnProcessor,
128
+ metadata=AttentionProcessorMetadata(
129
+ skip_processor_output_fn=_skip_proc_output_fn_Attention_CogView4AttnProcessor,
130
+ ),
131
+ )
132
+
133
+ # WanAttnProcessor2_0
134
+ AttentionProcessorRegistry.register(
135
+ model_class=WanAttnProcessor2_0,
136
+ metadata=AttentionProcessorMetadata(
137
+ skip_processor_output_fn=_skip_proc_output_fn_Attention_WanAttnProcessor2_0,
138
+ ),
139
+ )
140
+
141
+ # FluxAttnProcessor
142
+ AttentionProcessorRegistry.register(
143
+ model_class=FluxAttnProcessor,
144
+ metadata=AttentionProcessorMetadata(skip_processor_output_fn=_skip_proc_output_fn_Attention_FluxAttnProcessor),
145
+ )
146
+
147
+ # QwenDoubleStreamAttnProcessor2
148
+ AttentionProcessorRegistry.register(
149
+ model_class=QwenDoubleStreamAttnProcessor2_0,
150
+ metadata=AttentionProcessorMetadata(
151
+ skip_processor_output_fn=_skip_proc_output_fn_Attention_QwenDoubleStreamAttnProcessor2_0
152
+ ),
153
+ )
154
+
155
+ # HunyuanImageAttnProcessor
156
+ AttentionProcessorRegistry.register(
157
+ model_class=HunyuanImageAttnProcessor,
158
+ metadata=AttentionProcessorMetadata(
159
+ skip_processor_output_fn=_skip_proc_output_fn_Attention_HunyuanImageAttnProcessor,
160
+ ),
161
+ )
162
+
163
+ # ZSingleStreamAttnProcessor
164
+ AttentionProcessorRegistry.register(
165
+ model_class=ZSingleStreamAttnProcessor,
166
+ metadata=AttentionProcessorMetadata(
167
+ skip_processor_output_fn=_skip_proc_output_fn_Attention_ZSingleStreamAttnProcessor,
168
+ ),
169
+ )
170
+
171
+
172
+ def _register_transformer_blocks_metadata():
173
+ from ..models.attention import BasicTransformerBlock, JointTransformerBlock
174
+ from ..models.transformers.cogvideox_transformer_3d import CogVideoXBlock
175
+ from ..models.transformers.transformer_bria import BriaTransformerBlock
176
+ from ..models.transformers.transformer_cogview4 import CogView4TransformerBlock
177
+ from ..models.transformers.transformer_flux import FluxSingleTransformerBlock, FluxTransformerBlock
178
+ from ..models.transformers.transformer_hunyuan_video import (
179
+ HunyuanVideoSingleTransformerBlock,
180
+ HunyuanVideoTokenReplaceSingleTransformerBlock,
181
+ HunyuanVideoTokenReplaceTransformerBlock,
182
+ HunyuanVideoTransformerBlock,
183
+ )
184
+ from ..models.transformers.transformer_hunyuanimage import (
185
+ HunyuanImageSingleTransformerBlock,
186
+ HunyuanImageTransformerBlock,
187
+ )
188
+ from ..models.transformers.transformer_kandinsky import Kandinsky5TransformerDecoderBlock
189
+ from ..models.transformers.transformer_ltx import LTXVideoTransformerBlock
190
+ from ..models.transformers.transformer_mochi import MochiTransformerBlock
191
+ from ..models.transformers.transformer_motif_video import (
192
+ MotifVideoSingleTransformerBlock,
193
+ MotifVideoTransformerBlock,
194
+ )
195
+ from ..models.transformers.transformer_qwenimage import QwenImageTransformerBlock
196
+ from ..models.transformers.transformer_wan import WanTransformerBlock
197
+ from ..models.transformers.transformer_z_image import ZImageTransformerBlock
198
+
199
+ # BasicTransformerBlock
200
+ TransformerBlockRegistry.register(
201
+ model_class=BasicTransformerBlock,
202
+ metadata=TransformerBlockMetadata(
203
+ return_hidden_states_index=0,
204
+ return_encoder_hidden_states_index=None,
205
+ ),
206
+ )
207
+ TransformerBlockRegistry.register(
208
+ model_class=BriaTransformerBlock,
209
+ metadata=TransformerBlockMetadata(
210
+ return_hidden_states_index=0,
211
+ return_encoder_hidden_states_index=None,
212
+ ),
213
+ )
214
+
215
+ # CogVideoX
216
+ TransformerBlockRegistry.register(
217
+ model_class=CogVideoXBlock,
218
+ metadata=TransformerBlockMetadata(
219
+ return_hidden_states_index=0,
220
+ return_encoder_hidden_states_index=1,
221
+ ),
222
+ )
223
+
224
+ # CogView4
225
+ TransformerBlockRegistry.register(
226
+ model_class=CogView4TransformerBlock,
227
+ metadata=TransformerBlockMetadata(
228
+ return_hidden_states_index=0,
229
+ return_encoder_hidden_states_index=1,
230
+ ),
231
+ )
232
+
233
+ # Flux
234
+ TransformerBlockRegistry.register(
235
+ model_class=FluxTransformerBlock,
236
+ metadata=TransformerBlockMetadata(
237
+ return_hidden_states_index=1,
238
+ return_encoder_hidden_states_index=0,
239
+ ),
240
+ )
241
+ TransformerBlockRegistry.register(
242
+ model_class=FluxSingleTransformerBlock,
243
+ metadata=TransformerBlockMetadata(
244
+ return_hidden_states_index=1,
245
+ return_encoder_hidden_states_index=0,
246
+ ),
247
+ )
248
+
249
+ # HunyuanVideo
250
+ TransformerBlockRegistry.register(
251
+ model_class=HunyuanVideoTransformerBlock,
252
+ metadata=TransformerBlockMetadata(
253
+ return_hidden_states_index=0,
254
+ return_encoder_hidden_states_index=1,
255
+ ),
256
+ )
257
+ TransformerBlockRegistry.register(
258
+ model_class=HunyuanVideoSingleTransformerBlock,
259
+ metadata=TransformerBlockMetadata(
260
+ return_hidden_states_index=0,
261
+ return_encoder_hidden_states_index=1,
262
+ ),
263
+ )
264
+ TransformerBlockRegistry.register(
265
+ model_class=HunyuanVideoTokenReplaceTransformerBlock,
266
+ metadata=TransformerBlockMetadata(
267
+ return_hidden_states_index=0,
268
+ return_encoder_hidden_states_index=1,
269
+ ),
270
+ )
271
+ TransformerBlockRegistry.register(
272
+ model_class=HunyuanVideoTokenReplaceSingleTransformerBlock,
273
+ metadata=TransformerBlockMetadata(
274
+ return_hidden_states_index=0,
275
+ return_encoder_hidden_states_index=1,
276
+ ),
277
+ )
278
+
279
+ # LTXVideo
280
+ TransformerBlockRegistry.register(
281
+ model_class=LTXVideoTransformerBlock,
282
+ metadata=TransformerBlockMetadata(
283
+ return_hidden_states_index=0,
284
+ return_encoder_hidden_states_index=None,
285
+ ),
286
+ )
287
+
288
+ # Mochi
289
+ TransformerBlockRegistry.register(
290
+ model_class=MochiTransformerBlock,
291
+ metadata=TransformerBlockMetadata(
292
+ return_hidden_states_index=0,
293
+ return_encoder_hidden_states_index=1,
294
+ ),
295
+ )
296
+
297
+ # MotifVideo
298
+ TransformerBlockRegistry.register(
299
+ model_class=MotifVideoTransformerBlock,
300
+ metadata=TransformerBlockMetadata(
301
+ return_hidden_states_index=0,
302
+ return_encoder_hidden_states_index=1,
303
+ ),
304
+ )
305
+ TransformerBlockRegistry.register(
306
+ model_class=MotifVideoSingleTransformerBlock,
307
+ metadata=TransformerBlockMetadata(
308
+ return_hidden_states_index=0,
309
+ return_encoder_hidden_states_index=1,
310
+ ),
311
+ )
312
+
313
+ # Wan
314
+ TransformerBlockRegistry.register(
315
+ model_class=WanTransformerBlock,
316
+ metadata=TransformerBlockMetadata(
317
+ return_hidden_states_index=0,
318
+ return_encoder_hidden_states_index=None,
319
+ ),
320
+ )
321
+
322
+ # QwenImage
323
+ TransformerBlockRegistry.register(
324
+ model_class=QwenImageTransformerBlock,
325
+ metadata=TransformerBlockMetadata(
326
+ return_hidden_states_index=1,
327
+ return_encoder_hidden_states_index=0,
328
+ ),
329
+ )
330
+
331
+ # HunyuanImage2.1
332
+ TransformerBlockRegistry.register(
333
+ model_class=HunyuanImageTransformerBlock,
334
+ metadata=TransformerBlockMetadata(
335
+ return_hidden_states_index=0,
336
+ return_encoder_hidden_states_index=1,
337
+ ),
338
+ )
339
+ TransformerBlockRegistry.register(
340
+ model_class=HunyuanImageSingleTransformerBlock,
341
+ metadata=TransformerBlockMetadata(
342
+ return_hidden_states_index=0,
343
+ return_encoder_hidden_states_index=1,
344
+ ),
345
+ )
346
+
347
+ # ZImage
348
+ TransformerBlockRegistry.register(
349
+ model_class=ZImageTransformerBlock,
350
+ metadata=TransformerBlockMetadata(
351
+ return_hidden_states_index=0,
352
+ return_encoder_hidden_states_index=None,
353
+ ),
354
+ )
355
+
356
+ TransformerBlockRegistry.register(
357
+ model_class=JointTransformerBlock,
358
+ metadata=TransformerBlockMetadata(
359
+ return_hidden_states_index=1,
360
+ return_encoder_hidden_states_index=0,
361
+ ),
362
+ )
363
+
364
+ # Kandinsky 5.0 (Kandinsky5TransformerDecoderBlock)
365
+ TransformerBlockRegistry.register(
366
+ model_class=Kandinsky5TransformerDecoderBlock,
367
+ metadata=TransformerBlockMetadata(
368
+ return_hidden_states_index=0,
369
+ return_encoder_hidden_states_index=None,
370
+ hidden_states_argument_name="visual_embed",
371
+ ),
372
+ )
373
+
374
+
375
+ # fmt: off
376
+ def _skip_attention___ret___hidden_states(self, *args, **kwargs):
377
+ hidden_states = kwargs.get("hidden_states", None)
378
+ if hidden_states is None and len(args) > 0:
379
+ hidden_states = args[0]
380
+ return hidden_states
381
+
382
+
383
+ def _skip_attention___ret___hidden_states___encoder_hidden_states(self, *args, **kwargs):
384
+ hidden_states = kwargs.get("hidden_states", None)
385
+ encoder_hidden_states = kwargs.get("encoder_hidden_states", None)
386
+ if hidden_states is None and len(args) > 0:
387
+ hidden_states = args[0]
388
+ if encoder_hidden_states is None and len(args) > 1:
389
+ encoder_hidden_states = args[1]
390
+ return hidden_states, encoder_hidden_states
391
+
392
+
393
+ _skip_proc_output_fn_Attention_AttnProcessor2_0 = _skip_attention___ret___hidden_states
394
+ _skip_proc_output_fn_Attention_CogView4AttnProcessor = _skip_attention___ret___hidden_states___encoder_hidden_states
395
+ _skip_proc_output_fn_Attention_WanAttnProcessor2_0 = _skip_attention___ret___hidden_states
396
+ # not sure what this is yet.
397
+ _skip_proc_output_fn_Attention_FluxAttnProcessor = _skip_attention___ret___hidden_states
398
+ _skip_proc_output_fn_Attention_QwenDoubleStreamAttnProcessor2_0 = _skip_attention___ret___hidden_states
399
+ _skip_proc_output_fn_Attention_HunyuanImageAttnProcessor = _skip_attention___ret___hidden_states
400
+ _skip_proc_output_fn_Attention_ZSingleStreamAttnProcessor = _skip_attention___ret___hidden_states
401
+ # fmt: on
diffusers/hooks/context_parallel.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import copy
15
+ import inspect
16
+ from dataclasses import dataclass
17
+ from typing import Type
18
+
19
+ import torch
20
+ import torch.distributed as dist
21
+
22
+
23
+ if torch.distributed.is_available():
24
+ import torch.distributed._functional_collectives as funcol
25
+
26
+ from ..models._modeling_parallel import (
27
+ ContextParallelConfig,
28
+ ContextParallelInput,
29
+ ContextParallelModelPlan,
30
+ ContextParallelOutput,
31
+ gather_size_by_comm,
32
+ )
33
+ from ..utils import get_logger
34
+ from ..utils.torch_utils import lru_cache_unless_export, maybe_allow_in_graph, unwrap_module
35
+ from .hooks import HookRegistry, ModelHook
36
+
37
+
38
+ logger = get_logger(__name__) # pylint: disable=invalid-name
39
+
40
+ _CONTEXT_PARALLEL_INPUT_HOOK_TEMPLATE = "cp_input---{}"
41
+ _CONTEXT_PARALLEL_OUTPUT_HOOK_TEMPLATE = "cp_output---{}"
42
+
43
+
44
+ # TODO(aryan): consolidate with ._helpers.TransformerBlockMetadata
45
+ @dataclass
46
+ class ModuleForwardMetadata:
47
+ cached_parameter_indices: dict[str, int] = None
48
+ _cls: Type = None
49
+
50
+ def _get_parameter_from_args_kwargs(self, identifier: str, args=(), kwargs=None):
51
+ kwargs = kwargs or {}
52
+
53
+ if identifier in kwargs:
54
+ return kwargs[identifier], True, None
55
+
56
+ if self.cached_parameter_indices is not None:
57
+ index = self.cached_parameter_indices.get(identifier, None)
58
+ if index is None:
59
+ raise ValueError(f"Parameter '{identifier}' not found in cached indices.")
60
+ return args[index], False, index
61
+
62
+ if self._cls is None:
63
+ raise ValueError("Model class is not set for metadata.")
64
+
65
+ parameters = list(inspect.signature(self._cls.forward).parameters.keys())
66
+ parameters = parameters[1:] # skip `self`
67
+ self.cached_parameter_indices = {param: i for i, param in enumerate(parameters)}
68
+
69
+ if identifier not in self.cached_parameter_indices:
70
+ raise ValueError(f"Parameter '{identifier}' not found in function signature but was requested.")
71
+
72
+ index = self.cached_parameter_indices[identifier]
73
+
74
+ if index >= len(args):
75
+ raise ValueError(f"Expected {index} arguments but got {len(args)}.")
76
+
77
+ return args[index], False, index
78
+
79
+
80
+ def apply_context_parallel(
81
+ module: torch.nn.Module,
82
+ parallel_config: ContextParallelConfig,
83
+ plan: dict[str, ContextParallelModelPlan],
84
+ ) -> None:
85
+ """Apply context parallel on a model."""
86
+ logger.debug(f"Applying context parallel with CP mesh: {parallel_config._mesh} and plan: {plan}")
87
+
88
+ for module_id, cp_model_plan in plan.items():
89
+ submodule = _get_submodule_by_name(module, module_id)
90
+ if not isinstance(submodule, list):
91
+ submodule = [submodule]
92
+
93
+ logger.debug(f"Applying ContextParallelHook to {module_id=} identifying a total of {len(submodule)} modules")
94
+
95
+ for m in submodule:
96
+ if isinstance(cp_model_plan, dict):
97
+ hook = ContextParallelSplitHook(cp_model_plan, parallel_config)
98
+ hook_name = _CONTEXT_PARALLEL_INPUT_HOOK_TEMPLATE.format(module_id)
99
+ elif isinstance(cp_model_plan, (ContextParallelOutput, list, tuple)):
100
+ if isinstance(cp_model_plan, ContextParallelOutput):
101
+ cp_model_plan = [cp_model_plan]
102
+ if not all(isinstance(x, ContextParallelOutput) for x in cp_model_plan):
103
+ raise ValueError(f"Expected all elements of cp_model_plan to be CPOutput, but got {cp_model_plan}")
104
+ hook = ContextParallelGatherHook(cp_model_plan, parallel_config)
105
+ hook_name = _CONTEXT_PARALLEL_OUTPUT_HOOK_TEMPLATE.format(module_id)
106
+ else:
107
+ raise ValueError(f"Unsupported context parallel model plan type: {type(cp_model_plan)}")
108
+ registry = HookRegistry.check_if_exists_or_initialize(m)
109
+ registry.register_hook(hook, hook_name)
110
+
111
+
112
+ def remove_context_parallel(module: torch.nn.Module, plan: dict[str, ContextParallelModelPlan]) -> None:
113
+ for module_id, cp_model_plan in plan.items():
114
+ submodule = _get_submodule_by_name(module, module_id)
115
+ if not isinstance(submodule, list):
116
+ submodule = [submodule]
117
+
118
+ for m in submodule:
119
+ registry = HookRegistry.check_if_exists_or_initialize(m)
120
+ if isinstance(cp_model_plan, dict):
121
+ hook_name = _CONTEXT_PARALLEL_INPUT_HOOK_TEMPLATE.format(module_id)
122
+ elif isinstance(cp_model_plan, (ContextParallelOutput, list, tuple)):
123
+ hook_name = _CONTEXT_PARALLEL_OUTPUT_HOOK_TEMPLATE.format(module_id)
124
+ else:
125
+ raise ValueError(f"Unsupported context parallel model plan type: {type(cp_model_plan)}")
126
+ registry.remove_hook(hook_name)
127
+
128
+
129
+ class ContextParallelSplitHook(ModelHook):
130
+ def __init__(self, metadata: ContextParallelModelPlan, parallel_config: ContextParallelConfig) -> None:
131
+ super().__init__()
132
+ self.metadata = metadata
133
+ self.parallel_config = parallel_config
134
+ self.module_forward_metadata = None
135
+
136
+ def initialize_hook(self, module):
137
+ cls = unwrap_module(module).__class__
138
+ self.module_forward_metadata = ModuleForwardMetadata(_cls=cls)
139
+ return module
140
+
141
+ def pre_forward(self, module, *args, **kwargs):
142
+ args_list = list(args)
143
+
144
+ for name, cpm in self.metadata.items():
145
+ if isinstance(cpm, ContextParallelInput) and cpm.split_output:
146
+ continue
147
+
148
+ # Maybe the parameter was passed as a keyword argument
149
+ input_val, is_kwarg, index = self.module_forward_metadata._get_parameter_from_args_kwargs(
150
+ name, args_list, kwargs
151
+ )
152
+
153
+ if input_val is None:
154
+ continue
155
+
156
+ # The input_val may be a tensor or list/tuple of tensors. In certain cases, user may specify to shard
157
+ # the output instead of input for a particular layer by setting split_output=True
158
+ if isinstance(input_val, torch.Tensor):
159
+ input_val = self._prepare_cp_input(input_val, cpm)
160
+ elif isinstance(input_val, (list, tuple)):
161
+ if len(input_val) != len(cpm):
162
+ raise ValueError(
163
+ f"Expected input model plan to have {len(input_val)} elements, but got {len(cpm)}."
164
+ )
165
+ sharded_input_val = []
166
+ for i, x in enumerate(input_val):
167
+ if torch.is_tensor(x) and not cpm[i].split_output:
168
+ x = self._prepare_cp_input(x, cpm[i])
169
+ sharded_input_val.append(x)
170
+ input_val = sharded_input_val
171
+ else:
172
+ raise ValueError(f"Unsupported input type: {type(input_val)}")
173
+
174
+ if is_kwarg:
175
+ kwargs[name] = input_val
176
+ elif index is not None and index < len(args_list):
177
+ args_list[index] = input_val
178
+ else:
179
+ raise ValueError(
180
+ f"An unexpected error occurred while processing the input '{name}'. Please open an "
181
+ f"issue at https://github.com/huggingface/diffusers/issues and provide a minimal reproducible "
182
+ f"example along with the full stack trace."
183
+ )
184
+
185
+ return tuple(args_list), kwargs
186
+
187
+ def post_forward(self, module, output):
188
+ is_tensor = isinstance(output, torch.Tensor)
189
+ is_tensor_list = isinstance(output, (list, tuple)) and all(isinstance(x, torch.Tensor) for x in output)
190
+
191
+ if not is_tensor and not is_tensor_list:
192
+ raise ValueError(f"Expected output to be a tensor or a list/tuple of tensors, but got {type(output)}.")
193
+
194
+ output = [output] if is_tensor else list(output)
195
+ for index, cpm in self.metadata.items():
196
+ if not isinstance(cpm, ContextParallelInput) or not cpm.split_output:
197
+ continue
198
+ if index >= len(output):
199
+ raise ValueError(f"Index {index} out of bounds for output of length {len(output)}.")
200
+ current_output = output[index]
201
+ current_output = self._prepare_cp_input(current_output, cpm)
202
+ output[index] = current_output
203
+
204
+ return output[0] if is_tensor else tuple(output)
205
+
206
+ def _prepare_cp_input(self, x: torch.Tensor, cp_input: ContextParallelInput) -> torch.Tensor:
207
+ if cp_input.expected_dims is not None and x.dim() != cp_input.expected_dims:
208
+ logger.warning_once(
209
+ f"Expected input tensor to have {cp_input.expected_dims} dimensions, but got {x.dim()} dimensions, split will not be applied."
210
+ )
211
+ return x
212
+ else:
213
+ if self.parallel_config.ulysses_anything or self.parallel_config.ring_anything:
214
+ return PartitionAnythingSharder.shard_anything(
215
+ x, cp_input.split_dim, self.parallel_config._flattened_mesh
216
+ )
217
+ return EquipartitionSharder.shard(x, cp_input.split_dim, self.parallel_config._flattened_mesh)
218
+
219
+
220
+ class ContextParallelGatherHook(ModelHook):
221
+ def __init__(self, metadata: ContextParallelModelPlan, parallel_config: ContextParallelConfig) -> None:
222
+ super().__init__()
223
+ self.metadata = metadata
224
+ self.parallel_config = parallel_config
225
+
226
+ def post_forward(self, module, output):
227
+ is_tensor = isinstance(output, torch.Tensor)
228
+
229
+ if is_tensor:
230
+ output = [output]
231
+ elif not (isinstance(output, (list, tuple)) and all(isinstance(x, torch.Tensor) for x in output)):
232
+ raise ValueError(f"Expected output to be a tensor or a list/tuple of tensors, but got {type(output)}.")
233
+
234
+ output = list(output)
235
+
236
+ if len(output) != len(self.metadata):
237
+ raise ValueError(f"Expected output to have {len(self.metadata)} elements, but got {len(output)}.")
238
+
239
+ for i, cpm in enumerate(self.metadata):
240
+ if cpm is None:
241
+ continue
242
+ if self.parallel_config.ulysses_anything or self.parallel_config.ring_anything:
243
+ output[i] = PartitionAnythingSharder.unshard_anything(
244
+ output[i], cpm.gather_dim, self.parallel_config._flattened_mesh
245
+ )
246
+ else:
247
+ output[i] = EquipartitionSharder.unshard(
248
+ output[i], cpm.gather_dim, self.parallel_config._flattened_mesh
249
+ )
250
+
251
+ return output[0] if is_tensor else tuple(output)
252
+
253
+
254
+ class AllGatherFunction(torch.autograd.Function):
255
+ @staticmethod
256
+ def forward(ctx, tensor, dim, group):
257
+ ctx.dim = dim
258
+ ctx.group = group
259
+ ctx.world_size = torch.distributed.get_world_size(group)
260
+ ctx.rank = torch.distributed.get_rank(group)
261
+ return funcol.all_gather_tensor(tensor, dim, group=group)
262
+
263
+ @staticmethod
264
+ def backward(ctx, grad_output):
265
+ grad_chunks = torch.chunk(grad_output, ctx.world_size, dim=ctx.dim)
266
+ return grad_chunks[ctx.rank], None, None
267
+
268
+
269
+ class EquipartitionSharder:
270
+ @classmethod
271
+ def shard(cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh) -> torch.Tensor:
272
+ # NOTE: the following assertion does not have to be true in general. We simply enforce it for now
273
+ # because the alternate case has not yet been tested/required for any model.
274
+ assert tensor.size()[dim] % mesh.size() == 0, (
275
+ "Tensor size along dimension to be sharded must be divisible by mesh size"
276
+ )
277
+
278
+ # The following is not fullgraph compatible with Dynamo (fails in DeviceMesh.get_rank)
279
+ # return tensor.chunk(mesh.size(), dim=dim)[mesh.get_rank()]
280
+
281
+ return tensor.chunk(mesh.size(), dim=dim)[torch.distributed.get_rank(mesh.get_group())]
282
+
283
+ @classmethod
284
+ def unshard(cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh) -> torch.Tensor:
285
+ tensor = tensor.contiguous()
286
+ tensor = AllGatherFunction.apply(tensor, dim, mesh.get_group())
287
+ return tensor
288
+
289
+
290
+ class AllGatherAnythingFunction(torch.autograd.Function):
291
+ @staticmethod
292
+ def forward(ctx, tensor: torch.Tensor, dim: int, group: dist.device_mesh.DeviceMesh):
293
+ ctx.dim = dim
294
+ ctx.group = group
295
+ ctx.world_size = dist.get_world_size(group)
296
+ ctx.rank = dist.get_rank(group)
297
+ gathered_tensor = _all_gather_anything(tensor, dim, group)
298
+ return gathered_tensor
299
+
300
+ @staticmethod
301
+ def backward(ctx, grad_output):
302
+ # NOTE: We use `tensor_split` instead of chunk, because the `chunk`
303
+ # function may return fewer than the specified number of chunks!
304
+ grad_splits = torch.tensor_split(grad_output, ctx.world_size, dim=ctx.dim)
305
+ return grad_splits[ctx.rank], None, None
306
+
307
+
308
+ class PartitionAnythingSharder:
309
+ @classmethod
310
+ def shard_anything(
311
+ cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh
312
+ ) -> torch.Tensor:
313
+ assert tensor.size()[dim] >= mesh.size(), (
314
+ f"Cannot shard tensor of size {tensor.size()} along dim {dim} across mesh of size {mesh.size()}."
315
+ )
316
+ # NOTE: We use `tensor_split` instead of chunk, because the `chunk`
317
+ # function may return fewer than the specified number of chunks!
318
+ return tensor.tensor_split(mesh.size(), dim=dim)[dist.get_rank(mesh.get_group())]
319
+
320
+ @classmethod
321
+ def unshard_anything(
322
+ cls, tensor: torch.Tensor, dim: int, mesh: torch.distributed.device_mesh.DeviceMesh
323
+ ) -> torch.Tensor:
324
+ tensor = tensor.contiguous()
325
+ tensor = AllGatherAnythingFunction.apply(tensor, dim, mesh.get_group())
326
+ return tensor
327
+
328
+
329
+ @lru_cache_unless_export(maxsize=64)
330
+ def _fill_gather_shapes(shape: tuple[int], gather_dims: tuple[int], dim: int, world_size: int) -> list[list[int]]:
331
+ gather_shapes = []
332
+ for i in range(world_size):
333
+ rank_shape = list(copy.deepcopy(shape))
334
+ rank_shape[dim] = gather_dims[i]
335
+ gather_shapes.append(rank_shape)
336
+ return gather_shapes
337
+
338
+
339
+ @maybe_allow_in_graph
340
+ def _all_gather_anything(tensor: torch.Tensor, dim: int, group: dist.device_mesh.DeviceMesh) -> torch.Tensor:
341
+ world_size = dist.get_world_size(group=group)
342
+
343
+ tensor = tensor.contiguous()
344
+ shape = tensor.shape
345
+ rank_dim = shape[dim]
346
+ gather_dims = gather_size_by_comm(rank_dim, group)
347
+
348
+ gather_shapes = _fill_gather_shapes(tuple(shape), tuple(gather_dims), dim, world_size)
349
+
350
+ gathered_tensors = [torch.empty(shape, device=tensor.device, dtype=tensor.dtype) for shape in gather_shapes]
351
+
352
+ dist.all_gather(gathered_tensors, tensor, group=group)
353
+ gathered_tensor = torch.cat(gathered_tensors, dim=dim)
354
+ return gathered_tensor
355
+
356
+
357
+ def _get_submodule_by_name(model: torch.nn.Module, name: str) -> torch.nn.Module | list[torch.nn.Module]:
358
+ if name.count("*") > 1:
359
+ raise ValueError("Wildcard '*' can only be used once in the name")
360
+ return _find_submodule_by_name(model, name)
361
+
362
+
363
+ def _find_submodule_by_name(model: torch.nn.Module, name: str) -> torch.nn.Module | list[torch.nn.Module]:
364
+ if name == "":
365
+ return model
366
+ first_atom, remaining_name = name.split(".", 1) if "." in name else (name, "")
367
+ if first_atom == "*":
368
+ if not isinstance(model, torch.nn.ModuleList):
369
+ raise ValueError("Wildcard '*' can only be used with ModuleList")
370
+ submodules = []
371
+ for submodule in model:
372
+ subsubmodules = _find_submodule_by_name(submodule, remaining_name)
373
+ if not isinstance(subsubmodules, list):
374
+ subsubmodules = [subsubmodules]
375
+ submodules.extend(subsubmodules)
376
+ return submodules
377
+ else:
378
+ if hasattr(model, first_atom):
379
+ submodule = getattr(model, first_atom)
380
+ return _find_submodule_by_name(submodule, remaining_name)
381
+ else:
382
+ raise ValueError(f"'{first_atom}' is not a submodule of '{model.__class__.__name__}'")
diffusers/hooks/faster_cache.py ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+ from dataclasses import dataclass
17
+ from typing import Any, Callable
18
+
19
+ import torch
20
+
21
+ from ..models.attention import AttentionModuleMixin
22
+ from ..models.modeling_outputs import Transformer2DModelOutput
23
+ from ..utils import logging
24
+ from ._common import _ATTENTION_CLASSES
25
+ from .hooks import HookRegistry, ModelHook
26
+
27
+
28
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
29
+
30
+
31
+ _FASTER_CACHE_DENOISER_HOOK = "faster_cache_denoiser"
32
+ _FASTER_CACHE_BLOCK_HOOK = "faster_cache_block"
33
+ _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS = (
34
+ "^blocks.*attn",
35
+ "^transformer_blocks.*attn",
36
+ "^single_transformer_blocks.*attn",
37
+ )
38
+ _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS = ("^temporal_transformer_blocks.*attn",)
39
+ _TRANSFORMER_BLOCK_IDENTIFIERS = _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS + _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS
40
+ _UNCOND_COND_INPUT_KWARGS_IDENTIFIERS = (
41
+ "hidden_states",
42
+ "encoder_hidden_states",
43
+ "timestep",
44
+ "attention_mask",
45
+ "encoder_attention_mask",
46
+ )
47
+
48
+
49
+ @dataclass
50
+ class FasterCacheConfig:
51
+ r"""
52
+ Configuration for [FasterCache](https://huggingface.co/papers/2410.19355).
53
+
54
+ Attributes:
55
+ spatial_attention_block_skip_range (`int`, defaults to `2`):
56
+ Calculate the attention states every `N` iterations. If this is set to `N`, the attention computation will
57
+ be skipped `N - 1` times (i.e., cached attention states will be reused) before computing the new attention
58
+ states again.
59
+ temporal_attention_block_skip_range (`int`, *optional*, defaults to `None`):
60
+ Calculate the attention states every `N` iterations. If this is set to `N`, the attention computation will
61
+ be skipped `N - 1` times (i.e., cached attention states will be reused) before computing the new attention
62
+ states again.
63
+ spatial_attention_timestep_skip_range (`tuple[float, float]`, defaults to `(-1, 681)`):
64
+ The timestep range within which the spatial attention computation can be skipped without a significant loss
65
+ in quality. This is to be determined by the user based on the underlying model. The first value in the
66
+ tuple is the lower bound and the second value is the upper bound. Typically, diffusion timesteps for
67
+ denoising are in the reversed range of 0 to 1000 (i.e. denoising starts at timestep 1000 and ends at
68
+ timestep 0). For the default values, this would mean that the spatial attention computation skipping will
69
+ be applicable only after denoising timestep 681 is reached, and continue until the end of the denoising
70
+ process.
71
+ temporal_attention_timestep_skip_range (`tuple[float, float]`, *optional*, defaults to `None`):
72
+ The timestep range within which the temporal attention computation can be skipped without a significant
73
+ loss in quality. This is to be determined by the user based on the underlying model. The first value in the
74
+ tuple is the lower bound and the second value is the upper bound. Typically, diffusion timesteps for
75
+ denoising are in the reversed range of 0 to 1000 (i.e. denoising starts at timestep 1000 and ends at
76
+ timestep 0).
77
+ low_frequency_weight_update_timestep_range (`tuple[int, int]`, defaults to `(99, 901)`):
78
+ The timestep range within which the low frequency weight scaling update is applied. The first value in the
79
+ tuple is the lower bound and the second value is the upper bound of the timestep range. The callback
80
+ function for the update is called only within this range.
81
+ high_frequency_weight_update_timestep_range (`tuple[int, int]`, defaults to `(-1, 301)`):
82
+ The timestep range within which the high frequency weight scaling update is applied. The first value in the
83
+ tuple is the lower bound and the second value is the upper bound of the timestep range. The callback
84
+ function for the update is called only within this range.
85
+ alpha_low_frequency (`float`, defaults to `1.1`):
86
+ The weight to scale the low frequency updates by. This is used to approximate the unconditional branch from
87
+ the conditional branch outputs.
88
+ alpha_high_frequency (`float`, defaults to `1.1`):
89
+ The weight to scale the high frequency updates by. This is used to approximate the unconditional branch
90
+ from the conditional branch outputs.
91
+ unconditional_batch_skip_range (`int`, defaults to `5`):
92
+ Process the unconditional branch every `N` iterations. If this is set to `N`, the unconditional branch
93
+ computation will be skipped `N - 1` times (i.e., cached unconditional branch states will be reused) before
94
+ computing the new unconditional branch states again.
95
+ unconditional_batch_timestep_skip_range (`tuple[float, float]`, defaults to `(-1, 641)`):
96
+ The timestep range within which the unconditional branch computation can be skipped without a significant
97
+ loss in quality. This is to be determined by the user based on the underlying model. The first value in the
98
+ tuple is the lower bound and the second value is the upper bound.
99
+ spatial_attention_block_identifiers (`tuple[str, ...]`, defaults to `("blocks.*attn1", "transformer_blocks.*attn1", "single_transformer_blocks.*attn1")`):
100
+ The identifiers to match the spatial attention blocks in the model. If the name of the block contains any
101
+ of these identifiers, FasterCache will be applied to that block. This can either be the full layer names,
102
+ partial layer names, or regex patterns. Matching will always be done using a regex match.
103
+ temporal_attention_block_identifiers (`tuple[str, ...]`, defaults to `("temporal_transformer_blocks.*attn1",)`):
104
+ The identifiers to match the temporal attention blocks in the model. If the name of the block contains any
105
+ of these identifiers, FasterCache will be applied to that block. This can either be the full layer names,
106
+ partial layer names, or regex patterns. Matching will always be done using a regex match.
107
+ attention_weight_callback (`Callable[[torch.nn.Module], float]`, defaults to `None`):
108
+ The callback function to determine the weight to scale the attention outputs by. This function should take
109
+ the attention module as input and return a float value. This is used to approximate the unconditional
110
+ branch from the conditional branch outputs. If not provided, the default weight is 0.5 for all timesteps.
111
+ Typically, as described in the paper, this weight should gradually increase from 0 to 1 as the inference
112
+ progresses. Users are encouraged to experiment and provide custom weight schedules that take into account
113
+ the number of inference steps and underlying model behaviour as denoising progresses.
114
+ low_frequency_weight_callback (`Callable[[torch.nn.Module], float]`, defaults to `None`):
115
+ The callback function to determine the weight to scale the low frequency updates by. If not provided, the
116
+ default weight is 1.1 for timesteps within the range specified (as described in the paper).
117
+ high_frequency_weight_callback (`Callable[[torch.nn.Module], float]`, defaults to `None`):
118
+ The callback function to determine the weight to scale the high frequency updates by. If not provided, the
119
+ default weight is 1.1 for timesteps within the range specified (as described in the paper).
120
+ tensor_format (`str`, defaults to `"BCFHW"`):
121
+ The format of the input tensors. This should be one of `"BCFHW"`, `"BFCHW"`, or `"BCHW"`. The format is
122
+ used to split individual latent frames in order for low and high frequency components to be computed.
123
+ is_guidance_distilled (`bool`, defaults to `False`):
124
+ Whether the model is guidance distilled or not. If the model is guidance distilled, FasterCache will not be
125
+ applied at the denoiser-level to skip the unconditional branch computation (as there is none).
126
+ _unconditional_conditional_input_kwargs_identifiers (`list[str]`, defaults to `("hidden_states", "encoder_hidden_states", "timestep", "attention_mask", "encoder_attention_mask")`):
127
+ The identifiers to match the input kwargs that contain the batchwise-concatenated unconditional and
128
+ conditional inputs. If the name of the input kwargs contains any of these identifiers, FasterCache will
129
+ split the inputs into unconditional and conditional branches. This must be a list of exact input kwargs
130
+ names that contain the batchwise-concatenated unconditional and conditional inputs.
131
+ """
132
+
133
+ # In the paper and codebase, they hardcode these values to 2. However, it can be made configurable
134
+ # after some testing. We default to 2 if these parameters are not provided.
135
+ spatial_attention_block_skip_range: int = 2
136
+ temporal_attention_block_skip_range: int | None = None
137
+
138
+ spatial_attention_timestep_skip_range: tuple[int, int] = (-1, 681)
139
+ temporal_attention_timestep_skip_range: tuple[int, int] = (-1, 681)
140
+
141
+ # Indicator functions for low/high frequency as mentioned in Equation 11 of the paper
142
+ low_frequency_weight_update_timestep_range: tuple[int, int] = (99, 901)
143
+ high_frequency_weight_update_timestep_range: tuple[int, int] = (-1, 301)
144
+
145
+ # ⍺1 and ⍺2 as mentioned in Equation 11 of the paper
146
+ alpha_low_frequency: float = 1.1
147
+ alpha_high_frequency: float = 1.1
148
+
149
+ # n as described in CFG-Cache explanation in the paper - dependent on the model
150
+ unconditional_batch_skip_range: int = 5
151
+ unconditional_batch_timestep_skip_range: tuple[int, int] = (-1, 641)
152
+
153
+ spatial_attention_block_identifiers: tuple[str, ...] = _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS
154
+ temporal_attention_block_identifiers: tuple[str, ...] = _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS
155
+
156
+ attention_weight_callback: Callable[[torch.nn.Module], float] = None
157
+ low_frequency_weight_callback: Callable[[torch.nn.Module], float] = None
158
+ high_frequency_weight_callback: Callable[[torch.nn.Module], float] = None
159
+
160
+ tensor_format: str = "BCFHW"
161
+ is_guidance_distilled: bool = False
162
+
163
+ current_timestep_callback: Callable[[], int] = None
164
+
165
+ _unconditional_conditional_input_kwargs_identifiers: list[str] = _UNCOND_COND_INPUT_KWARGS_IDENTIFIERS
166
+
167
+ def __repr__(self) -> str:
168
+ return (
169
+ f"FasterCacheConfig(\n"
170
+ f" spatial_attention_block_skip_range={self.spatial_attention_block_skip_range},\n"
171
+ f" temporal_attention_block_skip_range={self.temporal_attention_block_skip_range},\n"
172
+ f" spatial_attention_timestep_skip_range={self.spatial_attention_timestep_skip_range},\n"
173
+ f" temporal_attention_timestep_skip_range={self.temporal_attention_timestep_skip_range},\n"
174
+ f" low_frequency_weight_update_timestep_range={self.low_frequency_weight_update_timestep_range},\n"
175
+ f" high_frequency_weight_update_timestep_range={self.high_frequency_weight_update_timestep_range},\n"
176
+ f" alpha_low_frequency={self.alpha_low_frequency},\n"
177
+ f" alpha_high_frequency={self.alpha_high_frequency},\n"
178
+ f" unconditional_batch_skip_range={self.unconditional_batch_skip_range},\n"
179
+ f" unconditional_batch_timestep_skip_range={self.unconditional_batch_timestep_skip_range},\n"
180
+ f" spatial_attention_block_identifiers={self.spatial_attention_block_identifiers},\n"
181
+ f" temporal_attention_block_identifiers={self.temporal_attention_block_identifiers},\n"
182
+ f" tensor_format={self.tensor_format},\n"
183
+ f")"
184
+ )
185
+
186
+
187
+ class FasterCacheDenoiserState:
188
+ r"""
189
+ State for [FasterCache](https://huggingface.co/papers/2410.19355) top-level denoiser module.
190
+ """
191
+
192
+ def __init__(self) -> None:
193
+ self.iteration: int = 0
194
+ self.low_frequency_delta: torch.Tensor = None
195
+ self.high_frequency_delta: torch.Tensor = None
196
+
197
+ def reset(self):
198
+ self.iteration = 0
199
+ self.low_frequency_delta = None
200
+ self.high_frequency_delta = None
201
+
202
+
203
+ class FasterCacheBlockState:
204
+ r"""
205
+ State for [FasterCache](https://huggingface.co/papers/2410.19355). Every underlying block that FasterCache is
206
+ applied to will have an instance of this state.
207
+ """
208
+
209
+ def __init__(self) -> None:
210
+ self.iteration: int = 0
211
+ self.batch_size: int = None
212
+ self.cache: tuple[torch.Tensor, torch.Tensor] = None
213
+
214
+ def reset(self):
215
+ self.iteration = 0
216
+ self.batch_size = None
217
+ self.cache = None
218
+
219
+
220
+ class FasterCacheDenoiserHook(ModelHook):
221
+ _is_stateful = True
222
+
223
+ def __init__(
224
+ self,
225
+ unconditional_batch_skip_range: int,
226
+ unconditional_batch_timestep_skip_range: tuple[int, int],
227
+ tensor_format: str,
228
+ is_guidance_distilled: bool,
229
+ uncond_cond_input_kwargs_identifiers: list[str],
230
+ current_timestep_callback: Callable[[], int],
231
+ low_frequency_weight_callback: Callable[[torch.nn.Module], torch.Tensor],
232
+ high_frequency_weight_callback: Callable[[torch.nn.Module], torch.Tensor],
233
+ ) -> None:
234
+ super().__init__()
235
+
236
+ self.unconditional_batch_skip_range = unconditional_batch_skip_range
237
+ self.unconditional_batch_timestep_skip_range = unconditional_batch_timestep_skip_range
238
+ # We can't easily detect what args are to be split in unconditional and conditional branches. We
239
+ # can only do it for kwargs, hence they are the only ones we split. The args are passed as-is.
240
+ # If a model is to be made compatible with FasterCache, the user must ensure that the inputs that
241
+ # contain batchwise-concatenated unconditional and conditional inputs are passed as kwargs.
242
+ self.uncond_cond_input_kwargs_identifiers = uncond_cond_input_kwargs_identifiers
243
+ self.tensor_format = tensor_format
244
+ self.is_guidance_distilled = is_guidance_distilled
245
+
246
+ self.current_timestep_callback = current_timestep_callback
247
+ self.low_frequency_weight_callback = low_frequency_weight_callback
248
+ self.high_frequency_weight_callback = high_frequency_weight_callback
249
+
250
+ def initialize_hook(self, module):
251
+ self.state = FasterCacheDenoiserState()
252
+ return module
253
+
254
+ @staticmethod
255
+ def _get_cond_input(input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
256
+ # Note: this method assumes that the input tensor is batchwise-concatenated with unconditional inputs
257
+ # followed by conditional inputs.
258
+ _, cond = input.chunk(2, dim=0)
259
+ return cond
260
+
261
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs) -> Any:
262
+ # Split the unconditional and conditional inputs. We only want to infer the conditional branch if the
263
+ # requirements for skipping the unconditional branch are met as described in the paper.
264
+ # We skip the unconditional branch only if the following conditions are met:
265
+ # 1. We have completed at least one iteration of the denoiser
266
+ # 2. The current timestep is within the range specified by the user. This is the optimal timestep range
267
+ # where approximating the unconditional branch from the computation of the conditional branch is possible
268
+ # without a significant loss in quality.
269
+ # 3. The current iteration is not a multiple of the unconditional batch skip range. This is done so that
270
+ # we compute the unconditional branch at least once every few iterations to ensure minimal quality loss.
271
+ is_within_timestep_range = (
272
+ self.unconditional_batch_timestep_skip_range[0]
273
+ < self.current_timestep_callback()
274
+ < self.unconditional_batch_timestep_skip_range[1]
275
+ )
276
+ should_skip_uncond = (
277
+ self.state.iteration > 0
278
+ and is_within_timestep_range
279
+ and self.state.iteration % self.unconditional_batch_skip_range != 0
280
+ and not self.is_guidance_distilled
281
+ )
282
+
283
+ if should_skip_uncond:
284
+ is_any_kwarg_uncond = any(k in self.uncond_cond_input_kwargs_identifiers for k in kwargs.keys())
285
+ if is_any_kwarg_uncond:
286
+ logger.debug("FasterCache - Skipping unconditional branch computation")
287
+ args = tuple([self._get_cond_input(arg) if torch.is_tensor(arg) else arg for arg in args])
288
+ kwargs = {
289
+ k: v if k not in self.uncond_cond_input_kwargs_identifiers else self._get_cond_input(v)
290
+ for k, v in kwargs.items()
291
+ }
292
+
293
+ output = self.fn_ref.original_forward(*args, **kwargs)
294
+
295
+ if self.is_guidance_distilled:
296
+ self.state.iteration += 1
297
+ return output
298
+
299
+ if torch.is_tensor(output):
300
+ hidden_states = output
301
+ elif isinstance(output, (tuple, Transformer2DModelOutput)):
302
+ hidden_states = output[0]
303
+
304
+ batch_size = hidden_states.size(0)
305
+
306
+ if should_skip_uncond:
307
+ self.state.low_frequency_delta = self.state.low_frequency_delta * self.low_frequency_weight_callback(
308
+ module
309
+ )
310
+ self.state.high_frequency_delta = self.state.high_frequency_delta * self.high_frequency_weight_callback(
311
+ module
312
+ )
313
+
314
+ if self.tensor_format == "BCFHW":
315
+ hidden_states = hidden_states.permute(0, 2, 1, 3, 4)
316
+ if self.tensor_format == "BCFHW" or self.tensor_format == "BFCHW":
317
+ hidden_states = hidden_states.flatten(0, 1)
318
+
319
+ low_freq_cond, high_freq_cond = _split_low_high_freq(hidden_states.float())
320
+
321
+ # Approximate/compute the unconditional branch outputs as described in Equation 9 and 10 of the paper
322
+ low_freq_uncond = self.state.low_frequency_delta + low_freq_cond
323
+ high_freq_uncond = self.state.high_frequency_delta + high_freq_cond
324
+ uncond_freq = low_freq_uncond + high_freq_uncond
325
+
326
+ uncond_states = torch.fft.ifftshift(uncond_freq)
327
+ uncond_states = torch.fft.ifft2(uncond_states).real
328
+
329
+ if self.tensor_format == "BCFHW" or self.tensor_format == "BFCHW":
330
+ uncond_states = uncond_states.unflatten(0, (batch_size, -1))
331
+ hidden_states = hidden_states.unflatten(0, (batch_size, -1))
332
+ if self.tensor_format == "BCFHW":
333
+ uncond_states = uncond_states.permute(0, 2, 1, 3, 4)
334
+ hidden_states = hidden_states.permute(0, 2, 1, 3, 4)
335
+
336
+ # Concatenate the approximated unconditional and predicted conditional branches
337
+ uncond_states = uncond_states.to(hidden_states.dtype)
338
+ hidden_states = torch.cat([uncond_states, hidden_states], dim=0)
339
+ else:
340
+ uncond_states, cond_states = hidden_states.chunk(2, dim=0)
341
+ if self.tensor_format == "BCFHW":
342
+ uncond_states = uncond_states.permute(0, 2, 1, 3, 4)
343
+ cond_states = cond_states.permute(0, 2, 1, 3, 4)
344
+ if self.tensor_format == "BCFHW" or self.tensor_format == "BFCHW":
345
+ uncond_states = uncond_states.flatten(0, 1)
346
+ cond_states = cond_states.flatten(0, 1)
347
+
348
+ low_freq_uncond, high_freq_uncond = _split_low_high_freq(uncond_states.float())
349
+ low_freq_cond, high_freq_cond = _split_low_high_freq(cond_states.float())
350
+ self.state.low_frequency_delta = low_freq_uncond - low_freq_cond
351
+ self.state.high_frequency_delta = high_freq_uncond - high_freq_cond
352
+
353
+ self.state.iteration += 1
354
+ if torch.is_tensor(output):
355
+ output = hidden_states
356
+ elif isinstance(output, tuple):
357
+ output = (hidden_states, *output[1:])
358
+ else:
359
+ output.sample = hidden_states
360
+
361
+ return output
362
+
363
+ def reset_state(self, module: torch.nn.Module) -> torch.nn.Module:
364
+ self.state.reset()
365
+ return module
366
+
367
+
368
+ class FasterCacheBlockHook(ModelHook):
369
+ _is_stateful = True
370
+
371
+ def __init__(
372
+ self,
373
+ block_skip_range: int,
374
+ timestep_skip_range: tuple[int, int],
375
+ is_guidance_distilled: bool,
376
+ weight_callback: Callable[[torch.nn.Module], float],
377
+ current_timestep_callback: Callable[[], int],
378
+ ) -> None:
379
+ super().__init__()
380
+
381
+ self.block_skip_range = block_skip_range
382
+ self.timestep_skip_range = timestep_skip_range
383
+ self.is_guidance_distilled = is_guidance_distilled
384
+
385
+ self.weight_callback = weight_callback
386
+ self.current_timestep_callback = current_timestep_callback
387
+
388
+ def initialize_hook(self, module):
389
+ self.state = FasterCacheBlockState()
390
+ return module
391
+
392
+ def _compute_approximated_attention_output(
393
+ self, t_2_output: torch.Tensor, t_output: torch.Tensor, weight: float, batch_size: int
394
+ ) -> torch.Tensor:
395
+ if t_2_output.size(0) != batch_size:
396
+ # The cache t_2_output contains both batchwise-concatenated unconditional-conditional branch outputs. Just
397
+ # take the conditional branch outputs.
398
+ assert t_2_output.size(0) == 2 * batch_size
399
+ t_2_output = t_2_output[batch_size:]
400
+ if t_output.size(0) != batch_size:
401
+ # The cache t_output contains both batchwise-concatenated unconditional-conditional branch outputs. Just
402
+ # take the conditional branch outputs.
403
+ assert t_output.size(0) == 2 * batch_size
404
+ t_output = t_output[batch_size:]
405
+ return t_output + (t_output - t_2_output) * weight
406
+
407
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs) -> Any:
408
+ batch_size = [
409
+ *[arg.size(0) for arg in args if torch.is_tensor(arg)],
410
+ *[v.size(0) for v in kwargs.values() if torch.is_tensor(v)],
411
+ ][0]
412
+ if self.state.batch_size is None:
413
+ # Will be updated on first forward pass through the denoiser
414
+ self.state.batch_size = batch_size
415
+
416
+ # If we have to skip due to the skip conditions, then let's skip as expected.
417
+ # But, we can't skip if the denoiser wants to infer both unconditional and conditional branches. This
418
+ # is because the expected output shapes of attention layer will not match if we only return values from
419
+ # the cache (which only caches conditional branch outputs). So, if state.batch_size (which is the true
420
+ # unconditional-conditional batch size) is same as the current batch size, we don't perform the layer
421
+ # skip. Otherwise, we conditionally skip the layer based on what state.skip_callback returns.
422
+ is_within_timestep_range = (
423
+ self.timestep_skip_range[0] < self.current_timestep_callback() < self.timestep_skip_range[1]
424
+ )
425
+ if not is_within_timestep_range:
426
+ should_skip_attention = False
427
+ else:
428
+ should_compute_attention = self.state.iteration > 0 and self.state.iteration % self.block_skip_range == 0
429
+ should_skip_attention = not should_compute_attention
430
+ if should_skip_attention:
431
+ should_skip_attention = self.is_guidance_distilled or self.state.batch_size != batch_size
432
+
433
+ if should_skip_attention:
434
+ logger.debug("FasterCache - Skipping attention and using approximation")
435
+ if torch.is_tensor(self.state.cache[-1]):
436
+ t_2_output, t_output = self.state.cache
437
+ weight = self.weight_callback(module)
438
+ output = self._compute_approximated_attention_output(t_2_output, t_output, weight, batch_size)
439
+ else:
440
+ # The cache contains multiple tensors from past N iterations (N=2 for FasterCache). We need to handle all of them.
441
+ # Diffusers blocks can return multiple tensors - let's call them [A, B, C, ...] for simplicity.
442
+ # In our cache, we would have [[A_1, B_1, C_1, ...], [A_2, B_2, C_2, ...], ...] where each list is the output from
443
+ # a forward pass of the block. We need to compute the approximated output for each of these tensors.
444
+ # The zip(*state.cache) operation will give us [(A_1, A_2, ...), (B_1, B_2, ...), (C_1, C_2, ...), ...] which
445
+ # allows us to compute the approximated attention output for each tensor in the cache.
446
+ output = ()
447
+ for t_2_output, t_output in zip(*self.state.cache):
448
+ result = self._compute_approximated_attention_output(
449
+ t_2_output, t_output, self.weight_callback(module), batch_size
450
+ )
451
+ output += (result,)
452
+ else:
453
+ logger.debug("FasterCache - Computing attention")
454
+ output = self.fn_ref.original_forward(*args, **kwargs)
455
+
456
+ # Note that the following condition for getting hidden_states should suffice since Diffusers blocks either return
457
+ # a single hidden_states tensor, or a tuple of (hidden_states, encoder_hidden_states) tensors. We need to handle
458
+ # both cases.
459
+ if torch.is_tensor(output):
460
+ cache_output = output
461
+ if not self.is_guidance_distilled and cache_output.size(0) == self.state.batch_size:
462
+ # The output here can be both unconditional-conditional branch outputs or just conditional branch outputs.
463
+ # This is determined at the higher-level denoiser module. We only want to cache the conditional branch outputs.
464
+ cache_output = cache_output.chunk(2, dim=0)[1]
465
+ else:
466
+ # Cache all return values and perform the same operation as above
467
+ cache_output = ()
468
+ for out in output:
469
+ if not self.is_guidance_distilled and out.size(0) == self.state.batch_size:
470
+ out = out.chunk(2, dim=0)[1]
471
+ cache_output += (out,)
472
+
473
+ if self.state.cache is None:
474
+ self.state.cache = [cache_output, cache_output]
475
+ else:
476
+ self.state.cache = [self.state.cache[-1], cache_output]
477
+
478
+ self.state.iteration += 1
479
+ return output
480
+
481
+ def reset_state(self, module: torch.nn.Module) -> torch.nn.Module:
482
+ self.state.reset()
483
+ return module
484
+
485
+
486
+ def apply_faster_cache(module: torch.nn.Module, config: FasterCacheConfig) -> None:
487
+ r"""
488
+ Applies [FasterCache](https://huggingface.co/papers/2410.19355) to a given pipeline.
489
+
490
+ Args:
491
+ module (`torch.nn.Module`):
492
+ The pytorch module to apply FasterCache to. Typically, this should be a transformer architecture supported
493
+ in Diffusers, such as `CogVideoXTransformer3DModel`, but external implementations may also work.
494
+ config (`FasterCacheConfig`):
495
+ The configuration to use for FasterCache.
496
+
497
+ Example:
498
+ ```python
499
+ >>> import torch
500
+ >>> from diffusers import CogVideoXPipeline, FasterCacheConfig, apply_faster_cache
501
+
502
+ >>> pipe = CogVideoXPipeline.from_pretrained("THUDM/CogVideoX-5b", torch_dtype=torch.bfloat16)
503
+ >>> pipe.to("cuda")
504
+
505
+ >>> config = FasterCacheConfig(
506
+ ... spatial_attention_block_skip_range=2,
507
+ ... spatial_attention_timestep_skip_range=(-1, 681),
508
+ ... low_frequency_weight_update_timestep_range=(99, 641),
509
+ ... high_frequency_weight_update_timestep_range=(-1, 301),
510
+ ... spatial_attention_block_identifiers=["transformer_blocks"],
511
+ ... attention_weight_callback=lambda _: 0.3,
512
+ ... tensor_format="BFCHW",
513
+ ... )
514
+ >>> apply_faster_cache(pipe.transformer, config)
515
+ ```
516
+ """
517
+
518
+ logger.warning(
519
+ "FasterCache is a purely experimental feature and may not work as expected. Not all models support FasterCache. "
520
+ "The API is subject to change in future releases, with no guarantee of backward compatibility. Please report any issues at "
521
+ "https://github.com/huggingface/diffusers/issues."
522
+ )
523
+
524
+ if config.attention_weight_callback is None:
525
+ # If the user has not provided a weight callback, we default to 0.5 for all timesteps.
526
+ # In the paper, they recommend using a gradually increasing weight from 0 to 1 as the inference progresses, but
527
+ # this depends from model-to-model. It is required by the user to provide a weight callback if they want to
528
+ # use a different weight function. Defaulting to 0.5 works well in practice for most cases.
529
+ logger.warning(
530
+ "No `attention_weight_callback` provided when enabling FasterCache. Defaulting to using a weight of 0.5 for all timesteps."
531
+ )
532
+ config.attention_weight_callback = lambda _: 0.5
533
+
534
+ if config.low_frequency_weight_callback is None:
535
+ logger.debug(
536
+ "Low frequency weight callback not provided when enabling FasterCache. Defaulting to behaviour described in the paper."
537
+ )
538
+
539
+ def low_frequency_weight_callback(module: torch.nn.Module) -> float:
540
+ is_within_range = (
541
+ config.low_frequency_weight_update_timestep_range[0]
542
+ < config.current_timestep_callback()
543
+ < config.low_frequency_weight_update_timestep_range[1]
544
+ )
545
+ return config.alpha_low_frequency if is_within_range else 1.0
546
+
547
+ config.low_frequency_weight_callback = low_frequency_weight_callback
548
+
549
+ if config.high_frequency_weight_callback is None:
550
+ logger.debug(
551
+ "High frequency weight callback not provided when enabling FasterCache. Defaulting to behaviour described in the paper."
552
+ )
553
+
554
+ def high_frequency_weight_callback(module: torch.nn.Module) -> float:
555
+ is_within_range = (
556
+ config.high_frequency_weight_update_timestep_range[0]
557
+ < config.current_timestep_callback()
558
+ < config.high_frequency_weight_update_timestep_range[1]
559
+ )
560
+ return config.alpha_high_frequency if is_within_range else 1.0
561
+
562
+ config.high_frequency_weight_callback = high_frequency_weight_callback
563
+
564
+ supported_tensor_formats = ["BCFHW", "BFCHW", "BCHW"] # TODO(aryan): Support BSC for LTX Video
565
+ if config.tensor_format not in supported_tensor_formats:
566
+ raise ValueError(f"`tensor_format` must be one of {supported_tensor_formats}, but got {config.tensor_format}.")
567
+
568
+ _apply_faster_cache_on_denoiser(module, config)
569
+
570
+ for name, submodule in module.named_modules():
571
+ if not isinstance(submodule, _ATTENTION_CLASSES):
572
+ continue
573
+ if any(re.search(identifier, name) is not None for identifier in _TRANSFORMER_BLOCK_IDENTIFIERS):
574
+ _apply_faster_cache_on_attention_class(name, submodule, config)
575
+
576
+
577
+ def _apply_faster_cache_on_denoiser(module: torch.nn.Module, config: FasterCacheConfig) -> None:
578
+ hook = FasterCacheDenoiserHook(
579
+ config.unconditional_batch_skip_range,
580
+ config.unconditional_batch_timestep_skip_range,
581
+ config.tensor_format,
582
+ config.is_guidance_distilled,
583
+ config._unconditional_conditional_input_kwargs_identifiers,
584
+ config.current_timestep_callback,
585
+ config.low_frequency_weight_callback,
586
+ config.high_frequency_weight_callback,
587
+ )
588
+ registry = HookRegistry.check_if_exists_or_initialize(module)
589
+ registry.register_hook(hook, _FASTER_CACHE_DENOISER_HOOK)
590
+
591
+
592
+ def _apply_faster_cache_on_attention_class(name: str, module: AttentionModuleMixin, config: FasterCacheConfig) -> None:
593
+ is_spatial_self_attention = (
594
+ any(re.search(identifier, name) is not None for identifier in config.spatial_attention_block_identifiers)
595
+ and config.spatial_attention_block_skip_range is not None
596
+ and not getattr(module, "is_cross_attention", False)
597
+ )
598
+ is_temporal_self_attention = (
599
+ any(re.search(identifier, name) is not None for identifier in config.temporal_attention_block_identifiers)
600
+ and config.temporal_attention_block_skip_range is not None
601
+ and not module.is_cross_attention
602
+ )
603
+
604
+ block_skip_range, timestep_skip_range, block_type = None, None, None
605
+ if is_spatial_self_attention:
606
+ block_skip_range = config.spatial_attention_block_skip_range
607
+ timestep_skip_range = config.spatial_attention_timestep_skip_range
608
+ block_type = "spatial"
609
+ elif is_temporal_self_attention:
610
+ block_skip_range = config.temporal_attention_block_skip_range
611
+ timestep_skip_range = config.temporal_attention_timestep_skip_range
612
+ block_type = "temporal"
613
+
614
+ if block_skip_range is None or timestep_skip_range is None:
615
+ logger.debug(
616
+ f'Unable to apply FasterCache to the selected layer: "{name}" because it does '
617
+ f"not match any of the required criteria for spatial or temporal attention layers. Note, "
618
+ f"however, that this layer may still be valid for applying PAB. Please specify the correct "
619
+ f"block identifiers in the configuration or use the specialized `apply_faster_cache_on_module` "
620
+ f"function to apply FasterCache to this layer."
621
+ )
622
+ return
623
+
624
+ logger.debug(f"Enabling FasterCache ({block_type}) for layer: {name}")
625
+ hook = FasterCacheBlockHook(
626
+ block_skip_range,
627
+ timestep_skip_range,
628
+ config.is_guidance_distilled,
629
+ config.attention_weight_callback,
630
+ config.current_timestep_callback,
631
+ )
632
+ registry = HookRegistry.check_if_exists_or_initialize(module)
633
+ registry.register_hook(hook, _FASTER_CACHE_BLOCK_HOOK)
634
+
635
+
636
+ # Reference: https://github.com/Vchitect/FasterCache/blob/fab32c15014636dc854948319c0a9a8d92c7acb4/scripts/latte/faster_cache_sample_latte.py#L127C1-L143C39
637
+ @torch.no_grad()
638
+ def _split_low_high_freq(x):
639
+ fft = torch.fft.fft2(x)
640
+ fft_shifted = torch.fft.fftshift(fft)
641
+ height, width = x.shape[-2:]
642
+ radius = min(height, width) // 5
643
+
644
+ y_grid, x_grid = torch.meshgrid(torch.arange(height), torch.arange(width))
645
+ center_x, center_y = width // 2, height // 2
646
+ mask = (x_grid - center_x) ** 2 + (y_grid - center_y) ** 2 <= radius**2
647
+
648
+ low_freq_mask = mask.unsqueeze(0).unsqueeze(0).to(x.device)
649
+ high_freq_mask = ~low_freq_mask
650
+
651
+ low_freq_fft = fft_shifted * low_freq_mask
652
+ high_freq_fft = fft_shifted * high_freq_mask
653
+
654
+ return low_freq_fft, high_freq_fft
diffusers/hooks/first_block_cache.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from dataclasses import dataclass
16
+
17
+ import torch
18
+
19
+ from ..utils import get_logger
20
+ from ..utils.torch_utils import unwrap_module
21
+ from ._common import _ALL_TRANSFORMER_BLOCK_IDENTIFIERS
22
+ from ._helpers import TransformerBlockRegistry
23
+ from .hooks import BaseState, HookRegistry, ModelHook, StateManager
24
+
25
+
26
+ logger = get_logger(__name__) # pylint: disable=invalid-name
27
+
28
+ _FBC_LEADER_BLOCK_HOOK = "fbc_leader_block_hook"
29
+ _FBC_BLOCK_HOOK = "fbc_block_hook"
30
+
31
+
32
+ @dataclass
33
+ class FirstBlockCacheConfig:
34
+ r"""
35
+ Configuration for [First Block
36
+ Cache](https://github.com/chengzeyi/ParaAttention/blob/7a266123671b55e7e5a2fe9af3121f07a36afc78/README.md#first-block-cache-our-dynamic-caching).
37
+
38
+ Args:
39
+ threshold (`float`, defaults to `0.05`):
40
+ The threshold to determine whether or not a forward pass through all layers of the model is required. A
41
+ higher threshold usually results in a forward pass through a lower number of layers and faster inference,
42
+ but might lead to poorer generation quality. A lower threshold may not result in significant generation
43
+ speedup. The threshold is compared against the absmean difference of the residuals between the current and
44
+ cached outputs from the first transformer block. If the difference is below the threshold, the forward pass
45
+ is skipped.
46
+ """
47
+
48
+ threshold: float = 0.05
49
+
50
+
51
+ class FBCSharedBlockState(BaseState):
52
+ def __init__(self) -> None:
53
+ super().__init__()
54
+
55
+ self.head_block_output: torch.Tensor | tuple[torch.Tensor, ...] = None
56
+ self.head_block_residual: torch.Tensor = None
57
+ self.tail_block_residuals: torch.Tensor | tuple[torch.Tensor, ...] = None
58
+ self.should_compute: bool = True
59
+
60
+ def reset(self):
61
+ self.tail_block_residuals = None
62
+ self.should_compute = True
63
+
64
+
65
+ class FBCHeadBlockHook(ModelHook):
66
+ _is_stateful = True
67
+
68
+ def __init__(self, state_manager: StateManager, threshold: float):
69
+ self.state_manager = state_manager
70
+ self.threshold = threshold
71
+ self._metadata = None
72
+
73
+ def initialize_hook(self, module):
74
+ unwrapped_module = unwrap_module(module)
75
+ self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
76
+ return module
77
+
78
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
79
+ original_hidden_states = self._metadata._get_parameter_from_args_kwargs("hidden_states", args, kwargs)
80
+
81
+ output = self.fn_ref.original_forward(*args, **kwargs)
82
+ is_output_tuple = isinstance(output, tuple)
83
+
84
+ if is_output_tuple:
85
+ hidden_states_residual = output[self._metadata.return_hidden_states_index] - original_hidden_states
86
+ else:
87
+ hidden_states_residual = output - original_hidden_states
88
+
89
+ shared_state: FBCSharedBlockState = self.state_manager.get_state()
90
+ hidden_states = encoder_hidden_states = None
91
+ should_compute = self._should_compute_remaining_blocks(hidden_states_residual)
92
+ shared_state.should_compute = should_compute
93
+
94
+ if not should_compute:
95
+ # Apply caching
96
+ if is_output_tuple:
97
+ hidden_states = (
98
+ shared_state.tail_block_residuals[0] + output[self._metadata.return_hidden_states_index]
99
+ )
100
+ else:
101
+ hidden_states = shared_state.tail_block_residuals[0] + output
102
+
103
+ if self._metadata.return_encoder_hidden_states_index is not None:
104
+ assert is_output_tuple
105
+ encoder_hidden_states = (
106
+ shared_state.tail_block_residuals[1] + output[self._metadata.return_encoder_hidden_states_index]
107
+ )
108
+
109
+ if is_output_tuple:
110
+ return_output = [None] * len(output)
111
+ return_output[self._metadata.return_hidden_states_index] = hidden_states
112
+ return_output[self._metadata.return_encoder_hidden_states_index] = encoder_hidden_states
113
+ return_output = tuple(return_output)
114
+ else:
115
+ return_output = hidden_states
116
+ output = return_output
117
+ else:
118
+ if is_output_tuple:
119
+ head_block_output = [None] * len(output)
120
+ head_block_output[0] = output[self._metadata.return_hidden_states_index]
121
+ head_block_output[1] = output[self._metadata.return_encoder_hidden_states_index]
122
+ else:
123
+ head_block_output = output
124
+ shared_state.head_block_output = head_block_output
125
+ shared_state.head_block_residual = hidden_states_residual
126
+
127
+ return output
128
+
129
+ def reset_state(self, module):
130
+ self.state_manager.reset()
131
+ return module
132
+
133
+ @torch.compiler.disable
134
+ def _should_compute_remaining_blocks(self, hidden_states_residual: torch.Tensor) -> bool:
135
+ shared_state = self.state_manager.get_state()
136
+ if shared_state.head_block_residual is None:
137
+ return True
138
+ prev_hidden_states_residual = shared_state.head_block_residual
139
+ absmean = (hidden_states_residual - prev_hidden_states_residual).abs().mean()
140
+ prev_hidden_states_absmean = prev_hidden_states_residual.abs().mean()
141
+ diff = (absmean / prev_hidden_states_absmean).item()
142
+ return diff > self.threshold
143
+
144
+
145
+ class FBCBlockHook(ModelHook):
146
+ def __init__(self, state_manager: StateManager, is_tail: bool = False):
147
+ super().__init__()
148
+ self.state_manager = state_manager
149
+ self.is_tail = is_tail
150
+ self._metadata = None
151
+
152
+ def initialize_hook(self, module):
153
+ unwrapped_module = unwrap_module(module)
154
+ self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
155
+ return module
156
+
157
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
158
+ original_hidden_states = self._metadata._get_parameter_from_args_kwargs("hidden_states", args, kwargs)
159
+ original_encoder_hidden_states = None
160
+ if self._metadata.return_encoder_hidden_states_index is not None:
161
+ original_encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
162
+ "encoder_hidden_states", args, kwargs
163
+ )
164
+
165
+ shared_state = self.state_manager.get_state()
166
+
167
+ if shared_state.should_compute:
168
+ output = self.fn_ref.original_forward(*args, **kwargs)
169
+ if self.is_tail:
170
+ hidden_states_residual = encoder_hidden_states_residual = None
171
+ if isinstance(output, tuple):
172
+ hidden_states_residual = (
173
+ output[self._metadata.return_hidden_states_index] - shared_state.head_block_output[0]
174
+ )
175
+ encoder_hidden_states_residual = (
176
+ output[self._metadata.return_encoder_hidden_states_index] - shared_state.head_block_output[1]
177
+ )
178
+ else:
179
+ hidden_states_residual = output - shared_state.head_block_output
180
+ shared_state.tail_block_residuals = (hidden_states_residual, encoder_hidden_states_residual)
181
+ return output
182
+
183
+ if original_encoder_hidden_states is None:
184
+ return_output = original_hidden_states
185
+ else:
186
+ return_output = [None, None]
187
+ return_output[self._metadata.return_hidden_states_index] = original_hidden_states
188
+ return_output[self._metadata.return_encoder_hidden_states_index] = original_encoder_hidden_states
189
+ return_output = tuple(return_output)
190
+ return return_output
191
+
192
+
193
+ def apply_first_block_cache(module: torch.nn.Module, config: FirstBlockCacheConfig) -> None:
194
+ """
195
+ Applies [First Block
196
+ Cache](https://github.com/chengzeyi/ParaAttention/blob/4de137c5b96416489f06e43e19f2c14a772e28fd/README.md#first-block-cache-our-dynamic-caching)
197
+ to a given module.
198
+
199
+ First Block Cache builds on the ideas of [TeaCache](https://huggingface.co/papers/2411.19108). It is much simpler
200
+ to implement generically for a wide range of models and has been integrated first for experimental purposes.
201
+
202
+ Args:
203
+ module (`torch.nn.Module`):
204
+ The pytorch module to apply FBCache to. Typically, this should be a transformer architecture supported in
205
+ Diffusers, such as `CogVideoXTransformer3DModel`, but external implementations may also work.
206
+ config (`FirstBlockCacheConfig`):
207
+ The configuration to use for applying the FBCache method.
208
+
209
+ Example:
210
+ ```python
211
+ >>> import torch
212
+ >>> from diffusers import CogView4Pipeline
213
+ >>> from diffusers.hooks import apply_first_block_cache, FirstBlockCacheConfig
214
+
215
+ >>> pipe = CogView4Pipeline.from_pretrained("THUDM/CogView4-6B", torch_dtype=torch.bfloat16)
216
+ >>> pipe.to("cuda")
217
+
218
+ >>> apply_first_block_cache(pipe.transformer, FirstBlockCacheConfig(threshold=0.2))
219
+
220
+ >>> prompt = "A photo of an astronaut riding a horse on mars"
221
+ >>> image = pipe(prompt, generator=torch.Generator().manual_seed(42)).images[0]
222
+ >>> image.save("output.png")
223
+ ```
224
+ """
225
+
226
+ state_manager = StateManager(FBCSharedBlockState, (), {})
227
+ remaining_blocks = []
228
+
229
+ for name, submodule in module.named_children():
230
+ if name not in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS or not isinstance(submodule, torch.nn.ModuleList):
231
+ continue
232
+ for index, block in enumerate(submodule):
233
+ remaining_blocks.append((f"{name}.{index}", block))
234
+
235
+ head_block_name, head_block = remaining_blocks.pop(0)
236
+ tail_block_name, tail_block = remaining_blocks.pop(-1)
237
+
238
+ logger.debug(f"Applying FBCHeadBlockHook to '{head_block_name}'")
239
+ _apply_fbc_head_block_hook(head_block, state_manager, config.threshold)
240
+
241
+ for name, block in remaining_blocks:
242
+ logger.debug(f"Applying FBCBlockHook to '{name}'")
243
+ _apply_fbc_block_hook(block, state_manager)
244
+
245
+ logger.debug(f"Applying FBCBlockHook to tail block '{tail_block_name}'")
246
+ _apply_fbc_block_hook(tail_block, state_manager, is_tail=True)
247
+
248
+
249
+ def _apply_fbc_head_block_hook(block: torch.nn.Module, state_manager: StateManager, threshold: float) -> None:
250
+ registry = HookRegistry.check_if_exists_or_initialize(block)
251
+ hook = FBCHeadBlockHook(state_manager, threshold)
252
+ registry.register_hook(hook, _FBC_LEADER_BLOCK_HOOK)
253
+
254
+
255
+ def _apply_fbc_block_hook(block: torch.nn.Module, state_manager: StateManager, is_tail: bool = False) -> None:
256
+ registry = HookRegistry.check_if_exists_or_initialize(block)
257
+ hook = FBCBlockHook(state_manager, is_tail)
258
+ registry.register_hook(hook, _FBC_BLOCK_HOOK)
diffusers/hooks/group_offloading.py ADDED
@@ -0,0 +1,1056 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import hashlib
16
+ import os
17
+ from contextlib import contextmanager, nullcontext
18
+ from dataclasses import dataclass, replace
19
+ from enum import Enum
20
+ from typing import Set
21
+
22
+ import safetensors.torch
23
+ import torch
24
+
25
+ from ..utils import get_logger, is_accelerate_available, is_torchao_available
26
+ from ._common import _GO_LC_SUPPORTED_PYTORCH_LAYERS
27
+ from .hooks import HookRegistry, ModelHook
28
+
29
+
30
+ if is_accelerate_available():
31
+ from accelerate.hooks import AlignDevicesHook, CpuOffload
32
+ from accelerate.utils import send_to_device
33
+
34
+
35
+ logger = get_logger(__name__) # pylint: disable=invalid-name
36
+
37
+
38
+ def _is_torchao_tensor(tensor: torch.Tensor) -> bool:
39
+ if not is_torchao_available():
40
+ return False
41
+ from torchao.utils import TorchAOBaseTensor
42
+
43
+ return isinstance(tensor, TorchAOBaseTensor)
44
+
45
+
46
+ def _get_torchao_inner_tensor_names(tensor: torch.Tensor) -> list[str]:
47
+ """Get names of all internal tensor data attributes from a TorchAO tensor."""
48
+ cls = type(tensor)
49
+ names = list(getattr(cls, "tensor_data_names", []))
50
+ for attr_name in getattr(cls, "optional_tensor_data_names", []):
51
+ if getattr(tensor, attr_name, None) is not None:
52
+ names.append(attr_name)
53
+ return names
54
+
55
+
56
+ def _swap_torchao_tensor(param: torch.Tensor, source: torch.Tensor) -> None:
57
+ """Move a TorchAO parameter to the device of `source` via `swap_tensors`.
58
+
59
+ `param.data = source` does not work for `_make_wrapper_subclass` tensors because the `.data` setter only replaces
60
+ the outer wrapper storage while leaving the subclass's internal attributes (e.g. `.qdata`, `.scale`) on the
61
+ original device. `swap_tensors` swaps the full tensor contents in-place, preserving the parameter's identity so
62
+ that any dict keyed by `id(param)` remains valid.
63
+
64
+ Refer to https://github.com/huggingface/diffusers/pull/13276#discussion_r2944471548 for the full discussion.
65
+ """
66
+ torch.utils.swap_tensors(param, source)
67
+
68
+
69
+ def _restore_torchao_tensor(param: torch.Tensor, source: torch.Tensor) -> None:
70
+ """Restore internal tensor data of a TorchAO parameter from `source` without mutating `source`.
71
+
72
+ Unlike `_swap_torchao_tensor` this copies attribute references one-by-one via `setattr` so that `source` is **not**
73
+ modified. Use this when `source` is a cached tensor that must remain unchanged (e.g. a pinned CPU copy in
74
+ `cpu_param_dict`).
75
+ """
76
+ for attr_name in _get_torchao_inner_tensor_names(source):
77
+ setattr(param, attr_name, getattr(source, attr_name))
78
+
79
+
80
+ def _record_stream_torchao_tensor(param: torch.Tensor, stream) -> None:
81
+ """Record stream for all internal tensors of a TorchAO parameter."""
82
+ for attr_name in _get_torchao_inner_tensor_names(param):
83
+ getattr(param, attr_name).record_stream(stream)
84
+
85
+
86
+ # fmt: off
87
+ _GROUP_OFFLOADING = "group_offloading"
88
+ _LAYER_EXECUTION_TRACKER = "layer_execution_tracker"
89
+ _LAZY_PREFETCH_GROUP_OFFLOADING = "lazy_prefetch_group_offloading"
90
+ _GROUP_ID_LAZY_LEAF = "lazy_leafs"
91
+ # fmt: on
92
+
93
+
94
+ class GroupOffloadingType(str, Enum):
95
+ BLOCK_LEVEL = "block_level"
96
+ LEAF_LEVEL = "leaf_level"
97
+
98
+
99
+ @dataclass
100
+ class GroupOffloadingConfig:
101
+ onload_device: torch.device
102
+ offload_device: torch.device
103
+ offload_type: GroupOffloadingType
104
+ non_blocking: bool
105
+ record_stream: bool
106
+ low_cpu_mem_usage: bool
107
+ num_blocks_per_group: int | None = None
108
+ offload_to_disk_path: str | None = None
109
+ stream: torch.cuda.Stream | torch.Stream | None = None
110
+ block_modules: list[str] | None = None
111
+ exclude_kwargs: list[str] | None = None
112
+ module_prefix: str = ""
113
+
114
+
115
+ class ModuleGroup:
116
+ def __init__(
117
+ self,
118
+ modules: list[torch.nn.Module],
119
+ offload_device: torch.device,
120
+ onload_device: torch.device,
121
+ offload_leader: torch.nn.Module,
122
+ onload_leader: torch.nn.Module | None = None,
123
+ parameters: list[torch.nn.Parameter] | None = None,
124
+ buffers: list[torch.Tensor] | None = None,
125
+ non_blocking: bool = False,
126
+ stream: torch.cuda.Stream | torch.Stream | None = None,
127
+ record_stream: bool | None = False,
128
+ low_cpu_mem_usage: bool = False,
129
+ onload_self: bool = True,
130
+ offload_to_disk_path: str | None = None,
131
+ group_id: int | str | None = None,
132
+ ) -> None:
133
+ self.modules = modules
134
+ self.offload_device = offload_device
135
+ self.onload_device = onload_device
136
+ self.offload_leader = offload_leader
137
+ self.onload_leader = onload_leader
138
+ self.parameters = parameters or []
139
+ self.buffers = buffers or []
140
+ self.non_blocking = non_blocking or stream is not None
141
+ self.stream = stream
142
+ self.record_stream = record_stream
143
+ self.onload_self = onload_self
144
+ self.low_cpu_mem_usage = low_cpu_mem_usage
145
+
146
+ self.offload_to_disk_path = offload_to_disk_path
147
+ self._is_offloaded_to_disk = False
148
+
149
+ if self.offload_to_disk_path is not None:
150
+ # Instead of `group_id or str(id(self))` we do this because `group_id` can be "" as well.
151
+ self.group_id = group_id if group_id is not None else str(id(self))
152
+ short_hash = _compute_group_hash(self.group_id)
153
+ self.safetensors_file_path = os.path.join(self.offload_to_disk_path, f"group_{short_hash}.safetensors")
154
+
155
+ all_tensors = []
156
+ for module in self.modules:
157
+ all_tensors.extend(list(module.parameters()))
158
+ all_tensors.extend(list(module.buffers()))
159
+ all_tensors.extend(self.parameters)
160
+ all_tensors.extend(self.buffers)
161
+ all_tensors = list(dict.fromkeys(all_tensors)) # Remove duplicates
162
+
163
+ self.tensor_to_key = {tensor: f"tensor_{i}" for i, tensor in enumerate(all_tensors)}
164
+ self.key_to_tensor = {v: k for k, v in self.tensor_to_key.items()}
165
+ self.cpu_param_dict = {}
166
+ else:
167
+ self.cpu_param_dict = self._init_cpu_param_dict()
168
+
169
+ self._torch_accelerator_module = (
170
+ getattr(torch, torch.accelerator.current_accelerator().type)
171
+ if hasattr(torch, "accelerator")
172
+ else torch.cuda
173
+ )
174
+
175
+ @staticmethod
176
+ def _to_cpu(tensor, low_cpu_mem_usage):
177
+ # For TorchAO tensors, `.data` returns an incomplete wrapper without internal attributes
178
+ # (e.g. `.qdata`, `.scale`), so we must call `.cpu()` on the tensor directly.
179
+ t = tensor.cpu() if _is_torchao_tensor(tensor) else tensor.data.cpu()
180
+ return t if low_cpu_mem_usage else t.pin_memory()
181
+
182
+ def _init_cpu_param_dict(self):
183
+ cpu_param_dict = {}
184
+ if self.stream is None:
185
+ return cpu_param_dict
186
+
187
+ for module in self.modules:
188
+ for param in module.parameters():
189
+ cpu_param_dict[param] = self._to_cpu(param, self.low_cpu_mem_usage)
190
+ for buffer in module.buffers():
191
+ cpu_param_dict[buffer] = self._to_cpu(buffer, self.low_cpu_mem_usage)
192
+
193
+ for param in self.parameters:
194
+ cpu_param_dict[param] = self._to_cpu(param, self.low_cpu_mem_usage)
195
+
196
+ for buffer in self.buffers:
197
+ cpu_param_dict[buffer] = self._to_cpu(buffer, self.low_cpu_mem_usage)
198
+
199
+ return cpu_param_dict
200
+
201
+ @contextmanager
202
+ def _pinned_memory_tensors(self):
203
+ try:
204
+ pinned_dict = {
205
+ param: tensor.pin_memory() if not tensor.is_pinned() else tensor
206
+ for param, tensor in self.cpu_param_dict.items()
207
+ }
208
+ yield pinned_dict
209
+ finally:
210
+ pinned_dict = None
211
+
212
+ def _transfer_tensor_to_device(self, tensor, source_tensor, default_stream):
213
+ moved = source_tensor.to(self.onload_device, non_blocking=self.non_blocking)
214
+ if _is_torchao_tensor(tensor):
215
+ _swap_torchao_tensor(tensor, moved)
216
+ else:
217
+ tensor.data = moved
218
+ if self.record_stream:
219
+ if _is_torchao_tensor(tensor):
220
+ _record_stream_torchao_tensor(tensor, default_stream)
221
+ else:
222
+ tensor.data.record_stream(default_stream)
223
+
224
+ def _process_tensors_from_modules(self, pinned_memory=None, default_stream=None):
225
+ for group_module in self.modules:
226
+ for param in group_module.parameters():
227
+ source = pinned_memory[param] if pinned_memory else param.data
228
+ self._transfer_tensor_to_device(param, source, default_stream)
229
+ for buffer in group_module.buffers():
230
+ source = pinned_memory[buffer] if pinned_memory else buffer.data
231
+ self._transfer_tensor_to_device(buffer, source, default_stream)
232
+
233
+ for param in self.parameters:
234
+ source = pinned_memory[param] if pinned_memory else param.data
235
+ self._transfer_tensor_to_device(param, source, default_stream)
236
+
237
+ for buffer in self.buffers:
238
+ source = pinned_memory[buffer] if pinned_memory else buffer.data
239
+ self._transfer_tensor_to_device(buffer, source, default_stream)
240
+
241
+ def _check_disk_offload_torchao(self):
242
+ all_tensors = list(self.tensor_to_key.keys())
243
+ has_torchao = any(_is_torchao_tensor(t) for t in all_tensors)
244
+ if has_torchao:
245
+ raise ValueError(
246
+ "Disk offloading is not supported for TorchAO quantized tensors because safetensors "
247
+ "cannot serialize TorchAO subclass tensors. Use memory offloading instead by not "
248
+ "setting `offload_to_disk_path`."
249
+ )
250
+
251
+ def _onload_from_disk(self):
252
+ self._check_disk_offload_torchao()
253
+
254
+ if self.stream is not None:
255
+ # Wait for previous Host->Device transfer to complete
256
+ self.stream.synchronize()
257
+
258
+ context = nullcontext() if self.stream is None else self._torch_accelerator_module.stream(self.stream)
259
+ current_stream = self._torch_accelerator_module.current_stream() if self.record_stream else None
260
+
261
+ with context:
262
+ if self.stream is not None:
263
+ # Load to CPU first, pin memory, then async copy to the target device
264
+ loaded_tensors = safetensors.torch.load_file(self.safetensors_file_path, device="cpu")
265
+ for key, tensor_obj in self.key_to_tensor.items():
266
+ pinned_tensor = loaded_tensors[key].pin_memory()
267
+ tensor_obj.data = pinned_tensor.to(self.onload_device, non_blocking=self.non_blocking)
268
+ if self.record_stream:
269
+ tensor_obj.data.record_stream(current_stream)
270
+ else:
271
+ # Load directly to the target device
272
+ onload_device = (
273
+ self.onload_device.type if isinstance(self.onload_device, torch.device) else self.onload_device
274
+ )
275
+ loaded_tensors = safetensors.torch.load_file(self.safetensors_file_path, device=onload_device)
276
+ for key, tensor_obj in self.key_to_tensor.items():
277
+ tensor_obj.data = loaded_tensors[key]
278
+
279
+ def _onload_from_memory(self):
280
+ if self.stream is not None:
281
+ # Wait for previous Host->Device transfer to complete
282
+ self.stream.synchronize()
283
+
284
+ context = nullcontext() if self.stream is None else self._torch_accelerator_module.stream(self.stream)
285
+ default_stream = self._torch_accelerator_module.current_stream() if self.stream is not None else None
286
+
287
+ with context:
288
+ if self.stream is not None:
289
+ with self._pinned_memory_tensors() as pinned_memory:
290
+ self._process_tensors_from_modules(pinned_memory, default_stream=default_stream)
291
+ else:
292
+ self._process_tensors_from_modules(None)
293
+
294
+ def _offload_to_disk(self):
295
+ self._check_disk_offload_torchao()
296
+
297
+ # TODO: we can potentially optimize this code path by checking if the _all_ the desired
298
+ # safetensor files exist on the disk and if so, skip this step entirely, reducing IO
299
+ # overhead. Currently, we just check if the given `safetensors_file_path` exists and if not
300
+ # we perform a write.
301
+ # Check if the file has been saved in this session or if it already exists on disk.
302
+ if not self._is_offloaded_to_disk and not os.path.exists(self.safetensors_file_path):
303
+ os.makedirs(os.path.dirname(self.safetensors_file_path), exist_ok=True)
304
+ tensors_to_save = {key: tensor.data.to(self.offload_device) for tensor, key in self.tensor_to_key.items()}
305
+ safetensors.torch.save_file(tensors_to_save, self.safetensors_file_path)
306
+
307
+ # The group is now considered offloaded to disk for the rest of the session.
308
+ self._is_offloaded_to_disk = True
309
+
310
+ # We do this to free up the RAM which is still holding the up tensor data.
311
+ for tensor_obj in self.tensor_to_key.keys():
312
+ tensor_obj.data = torch.empty_like(tensor_obj.data, device=self.offload_device)
313
+
314
+ def _offload_to_memory(self):
315
+ if self.stream is not None:
316
+ if not self.record_stream:
317
+ self._torch_accelerator_module.current_stream().synchronize()
318
+
319
+ for group_module in self.modules:
320
+ for param in group_module.parameters():
321
+ if _is_torchao_tensor(param):
322
+ _restore_torchao_tensor(param, self.cpu_param_dict[param])
323
+ else:
324
+ param.data = self.cpu_param_dict[param]
325
+ for param in self.parameters:
326
+ if _is_torchao_tensor(param):
327
+ _restore_torchao_tensor(param, self.cpu_param_dict[param])
328
+ else:
329
+ param.data = self.cpu_param_dict[param]
330
+ for buffer in self.buffers:
331
+ if _is_torchao_tensor(buffer):
332
+ _restore_torchao_tensor(buffer, self.cpu_param_dict[buffer])
333
+ else:
334
+ buffer.data = self.cpu_param_dict[buffer]
335
+ else:
336
+ for group_module in self.modules:
337
+ group_module.to(self.offload_device, non_blocking=False)
338
+ for param in self.parameters:
339
+ if _is_torchao_tensor(param):
340
+ moved = param.to(self.offload_device, non_blocking=False)
341
+ _swap_torchao_tensor(param, moved)
342
+ else:
343
+ param.data = param.data.to(self.offload_device, non_blocking=False)
344
+ for buffer in self.buffers:
345
+ if _is_torchao_tensor(buffer):
346
+ moved = buffer.to(self.offload_device, non_blocking=False)
347
+ _swap_torchao_tensor(buffer, moved)
348
+ else:
349
+ buffer.data = buffer.data.to(self.offload_device, non_blocking=False)
350
+
351
+ @torch.compiler.disable()
352
+ def onload_(self):
353
+ r"""Onloads the group of parameters to the onload_device."""
354
+ if self.offload_to_disk_path is not None:
355
+ self._onload_from_disk()
356
+ else:
357
+ self._onload_from_memory()
358
+
359
+ @torch.compiler.disable()
360
+ def offload_(self):
361
+ r"""Offloads the group of parameters to the offload_device."""
362
+ if self.offload_to_disk_path:
363
+ self._offload_to_disk()
364
+ else:
365
+ self._offload_to_memory()
366
+
367
+
368
+ class GroupOffloadingHook(ModelHook):
369
+ r"""
370
+ A hook that offloads groups of torch.nn.Module to the CPU for storage and onloads to accelerator device for
371
+ computation. Each group has one "onload leader" module that is responsible for onloading, and an "offload leader"
372
+ module that is responsible for offloading. If prefetching is enabled, the onload leader of the previous module
373
+ group is responsible for onloading the current module group.
374
+ """
375
+
376
+ _is_stateful = False
377
+
378
+ def __init__(self, group: ModuleGroup, *, config: GroupOffloadingConfig) -> None:
379
+ self.group = group
380
+ self.next_group: ModuleGroup | None = None
381
+ self.config = config
382
+
383
+ def initialize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
384
+ if self.group.offload_leader == module:
385
+ self.group.offload_()
386
+ return module
387
+
388
+ def pre_forward(self, module: torch.nn.Module, *args, **kwargs):
389
+ # If there wasn't an onload_leader assigned, we assume that the submodule that first called its forward
390
+ # method is the onload_leader of the group.
391
+ if self.group.onload_leader is None:
392
+ self.group.onload_leader = module
393
+
394
+ # If the current module is the onload_leader of the group, we onload the group if it is supposed
395
+ # to onload itself. In the case of using prefetching with streams, we onload the next group if
396
+ # it is not supposed to onload itself.
397
+ if self.group.onload_leader == module:
398
+ if self.group.onload_self:
399
+ self.group.onload_()
400
+ else:
401
+ # onload_self=False means this group relies on prefetching from a previous group.
402
+ # However, for conditionally-executed modules (e.g. patch_short/patch_mid/patch_long in Helios),
403
+ # the prefetch chain may not cover them if they were absent during the first forward pass
404
+ # when the execution order was traced. In that case, their weights remain on offload_device,
405
+ # so we fall back to a synchronous onload here.
406
+ params = [p for m in self.group.modules for p in m.parameters()] + list(self.group.parameters)
407
+ if params and params[0].device == self.group.offload_device:
408
+ self.group.onload_()
409
+ if self.group.stream is not None:
410
+ self.group.stream.synchronize()
411
+
412
+ should_onload_next_group = self.next_group is not None and not self.next_group.onload_self
413
+ if should_onload_next_group:
414
+ self.next_group.onload_()
415
+
416
+ should_synchronize = (
417
+ not self.group.onload_self and self.group.stream is not None and not should_onload_next_group
418
+ )
419
+ if should_synchronize:
420
+ # If this group didn't onload itself, it means it was asynchronously onloaded by the
421
+ # previous group. We need to synchronize the side stream to ensure parameters
422
+ # are completely loaded to proceed with forward pass. Without this, uninitialized
423
+ # weights will be used in the computation, leading to incorrect results
424
+ # Also, we should only do this synchronization if we don't already do it from the sync call in
425
+ # self.next_group.onload_, hence the `not should_onload_next_group` check.
426
+ self.group.stream.synchronize()
427
+
428
+ args = send_to_device(args, self.group.onload_device, non_blocking=self.group.non_blocking)
429
+
430
+ # Some Autoencoder models use a feature cache that is passed through submodules
431
+ # and modified in place. The `send_to_device` call returns a copy of this feature cache object
432
+ # which breaks the inplace updates. Use `exclude_kwargs` to mark these cache features
433
+ exclude_kwargs = self.config.exclude_kwargs or []
434
+ if exclude_kwargs:
435
+ moved_kwargs = send_to_device(
436
+ {k: v for k, v in kwargs.items() if k not in exclude_kwargs},
437
+ self.group.onload_device,
438
+ non_blocking=self.group.non_blocking,
439
+ )
440
+ kwargs.update(moved_kwargs)
441
+ else:
442
+ kwargs = send_to_device(kwargs, self.group.onload_device, non_blocking=self.group.non_blocking)
443
+
444
+ return args, kwargs
445
+
446
+ def post_forward(self, module: torch.nn.Module, output):
447
+ if self.group.offload_leader == module:
448
+ self.group.offload_()
449
+ return output
450
+
451
+
452
+ class LazyPrefetchGroupOffloadingHook(ModelHook):
453
+ r"""
454
+ A hook, used in conjunction with GroupOffloadingHook, that applies lazy prefetching to groups of torch.nn.Module.
455
+ This hook is used to determine the order in which the layers are executed during the forward pass. Once the layer
456
+ invocation order is known, assignments of the next_group attribute for prefetching can be made, which allows
457
+ prefetching groups in the correct order.
458
+ """
459
+
460
+ _is_stateful = False
461
+
462
+ def __init__(self):
463
+ self.execution_order: list[tuple[str, torch.nn.Module]] = []
464
+ self._layer_execution_tracker_module_names = set()
465
+
466
+ def initialize_hook(self, module):
467
+ def make_execution_order_update_callback(current_name, current_submodule):
468
+ def callback():
469
+ if not torch.compiler.is_compiling():
470
+ logger.debug(f"Adding {current_name} to the execution order")
471
+ self.execution_order.append((current_name, current_submodule))
472
+
473
+ return callback
474
+
475
+ # To every submodule that contains a group offloading hook (at this point, no prefetching is enabled for any
476
+ # of the groups), we add a layer execution tracker hook that will be used to determine the order in which the
477
+ # layers are executed during the forward pass.
478
+ for name, submodule in module.named_modules():
479
+ if name == "" or not hasattr(submodule, "_diffusers_hook"):
480
+ continue
481
+
482
+ registry = HookRegistry.check_if_exists_or_initialize(submodule)
483
+ group_offloading_hook = registry.get_hook(_GROUP_OFFLOADING)
484
+
485
+ if group_offloading_hook is not None:
486
+ # For the first forward pass, we have to load in a blocking manner
487
+ group_offloading_hook.group.non_blocking = False
488
+ layer_tracker_hook = LayerExecutionTrackerHook(make_execution_order_update_callback(name, submodule))
489
+ registry.register_hook(layer_tracker_hook, _LAYER_EXECUTION_TRACKER)
490
+ self._layer_execution_tracker_module_names.add(name)
491
+
492
+ return module
493
+
494
+ def post_forward(self, module, output):
495
+ # At this point, for the current modules' submodules, we know the execution order of the layers. We can now
496
+ # remove the layer execution tracker hooks and apply prefetching by setting the next_group attribute for each
497
+ # group offloading hook.
498
+ num_executed = len(self.execution_order)
499
+ execution_order_module_names = {name for name, _ in self.execution_order}
500
+
501
+ # It may be possible that some layers were not executed during the forward pass. This can happen if the layer
502
+ # is not used in the forward pass, or if the layer is not executed due to some other reason. In such cases, we
503
+ # may not be able to apply prefetching in the correct order, which can lead to device-mismatch related errors
504
+ # if the missing layers end up being executed in the future.
505
+ if execution_order_module_names != self._layer_execution_tracker_module_names:
506
+ unexecuted_layers = list(self._layer_execution_tracker_module_names - execution_order_module_names)
507
+ if not torch.compiler.is_compiling():
508
+ logger.warning(
509
+ "It seems like some layers were not executed during the forward pass. This may lead to problems when "
510
+ "applying lazy prefetching with automatic tracing and lead to device-mismatch related errors. Please "
511
+ "make sure that all layers are executed during the forward pass. The following layers were not executed:\n"
512
+ f"{unexecuted_layers=}"
513
+ )
514
+
515
+ # Remove the layer execution tracker hooks from the submodules
516
+ base_module_registry = module._diffusers_hook
517
+ registries = [submodule._diffusers_hook for _, submodule in self.execution_order]
518
+ group_offloading_hooks = [registry.get_hook(_GROUP_OFFLOADING) for registry in registries]
519
+
520
+ for i in range(num_executed):
521
+ registries[i].remove_hook(_LAYER_EXECUTION_TRACKER, recurse=False)
522
+
523
+ # Remove the current lazy prefetch group offloading hook so that it doesn't interfere with the next forward pass
524
+ base_module_registry.remove_hook(_LAZY_PREFETCH_GROUP_OFFLOADING, recurse=False)
525
+
526
+ # LazyPrefetchGroupOffloadingHook is only used with streams, so we know that non_blocking should be True.
527
+ # We disable non_blocking for the first forward pass, but need to enable it for the subsequent passes to
528
+ # see the benefits of prefetching.
529
+ for hook in group_offloading_hooks:
530
+ hook.group.non_blocking = True
531
+
532
+ # Set required attributes for prefetching
533
+ if num_executed > 0:
534
+ base_module_group_offloading_hook = base_module_registry.get_hook(_GROUP_OFFLOADING)
535
+ base_module_group_offloading_hook.next_group = group_offloading_hooks[0].group
536
+ base_module_group_offloading_hook.next_group.onload_self = False
537
+
538
+ for i in range(num_executed - 1):
539
+ name1, _ = self.execution_order[i]
540
+ name2, _ = self.execution_order[i + 1]
541
+ if not torch.compiler.is_compiling():
542
+ logger.debug(f"Applying lazy prefetch group offloading from {name1} to {name2}")
543
+ group_offloading_hooks[i].next_group = group_offloading_hooks[i + 1].group
544
+ group_offloading_hooks[i].next_group.onload_self = False
545
+
546
+ return output
547
+
548
+
549
+ class LayerExecutionTrackerHook(ModelHook):
550
+ r"""
551
+ A hook that tracks the order in which the layers are executed during the forward pass by calling back to the
552
+ LazyPrefetchGroupOffloadingHook to update the execution order.
553
+ """
554
+
555
+ _is_stateful = False
556
+
557
+ def __init__(self, execution_order_update_callback):
558
+ self.execution_order_update_callback = execution_order_update_callback
559
+
560
+ def pre_forward(self, module, *args, **kwargs):
561
+ self.execution_order_update_callback()
562
+ return args, kwargs
563
+
564
+
565
+ def apply_group_offloading(
566
+ module: torch.nn.Module,
567
+ onload_device: str | torch.device,
568
+ offload_device: str | torch.device = torch.device("cpu"),
569
+ offload_type: str | GroupOffloadingType = "block_level",
570
+ num_blocks_per_group: int | None = None,
571
+ non_blocking: bool = False,
572
+ use_stream: bool = False,
573
+ record_stream: bool = False,
574
+ low_cpu_mem_usage: bool = False,
575
+ offload_to_disk_path: str | None = None,
576
+ block_modules: list[str] | None = None,
577
+ exclude_kwargs: list[str] | None = None,
578
+ ) -> None:
579
+ r"""
580
+ Applies group offloading to the internal layers of a torch.nn.Module. To understand what group offloading is, and
581
+ where it is beneficial, we need to first provide some context on how other supported offloading methods work.
582
+
583
+ Typically, offloading is done at two levels:
584
+ - Module-level: In Diffusers, this can be enabled using the `ModelMixin::enable_model_cpu_offload()` method. It
585
+ works by offloading each component of a pipeline to the CPU for storage, and onloading to the accelerator device
586
+ when needed for computation. This method is more memory-efficient than keeping all components on the accelerator,
587
+ but the memory requirements are still quite high. For this method to work, one needs memory equivalent to size of
588
+ the model in runtime dtype + size of largest intermediate activation tensors to be able to complete the forward
589
+ pass.
590
+ - Leaf-level: In Diffusers, this can be enabled using the `ModelMixin::enable_sequential_cpu_offload()` method. It
591
+ works by offloading the lowest leaf-level parameters of the computation graph to the CPU for storage, and
592
+ onloading only the leafs to the accelerator device for computation. This uses the lowest amount of accelerator
593
+ memory, but can be slower due to the excessive number of device synchronizations.
594
+
595
+ Group offloading is a middle ground between the two methods. It works by offloading groups of internal layers,
596
+ (either `torch.nn.ModuleList` or `torch.nn.Sequential`). This method uses lower memory than module-level
597
+ offloading. It is also faster than leaf-level/sequential offloading, as the number of device synchronizations is
598
+ reduced.
599
+
600
+ Another supported feature (for CUDA devices with support for asynchronous data transfer streams) is the ability to
601
+ overlap data transfer and computation to reduce the overall execution time compared to sequential offloading. This
602
+ is enabled using layer prefetching with streams, i.e., the layer that is to be executed next starts onloading to
603
+ the accelerator device while the current layer is being executed - this increases the memory requirements slightly.
604
+ Note that this implementation also supports leaf-level offloading but can be made much faster when using streams.
605
+
606
+ Args:
607
+ module (`torch.nn.Module`):
608
+ The module to which group offloading is applied.
609
+ onload_device (`torch.device`):
610
+ The device to which the group of modules are onloaded.
611
+ offload_device (`torch.device`, defaults to `torch.device("cpu")`):
612
+ The device to which the group of modules are offloaded. This should typically be the CPU. Default is CPU.
613
+ offload_type (`str` or `GroupOffloadingType`, defaults to "block_level"):
614
+ The type of offloading to be applied. Can be one of "block_level" or "leaf_level". Default is
615
+ "block_level".
616
+ offload_to_disk_path (`str`, *optional*, defaults to `None`):
617
+ The path to the directory where parameters will be offloaded. Setting this option can be useful in limited
618
+ RAM environment settings where a reasonable speed-memory trade-off is desired.
619
+ num_blocks_per_group (`int`, *optional*):
620
+ The number of blocks per group when using offload_type="block_level". This is required when using
621
+ offload_type="block_level".
622
+ non_blocking (`bool`, defaults to `False`):
623
+ If True, offloading and onloading is done with non-blocking data transfer.
624
+ use_stream (`bool`, defaults to `False`):
625
+ If True, offloading and onloading is done asynchronously using a CUDA stream. This can be useful for
626
+ overlapping computation and data transfer.
627
+ record_stream (`bool`, defaults to `False`): When enabled with `use_stream`, it marks the current tensor
628
+ as having been used by this stream. It is faster at the expense of slightly more memory usage. Refer to the
629
+ [PyTorch official docs](https://pytorch.org/docs/stable/generated/torch.Tensor.record_stream.html) more
630
+ details.
631
+ low_cpu_mem_usage (`bool`, defaults to `False`):
632
+ If True, the CPU memory usage is minimized by pinning tensors on-the-fly instead of pre-pinning them. This
633
+ option only matters when using streamed CPU offloading (i.e. `use_stream=True`). This can be useful when
634
+ the CPU memory is a bottleneck but may counteract the benefits of using streams.
635
+ block_modules (`list[str]`, *optional*):
636
+ List of module names that should be treated as blocks for offloading. If provided, only these modules will
637
+ be considered for block-level offloading. If not provided, the default block detection logic will be used.
638
+ exclude_kwargs (`list[str]`, *optional*):
639
+ List of kwarg keys that should not be processed by send_to_device. This is useful for mutable state like
640
+ caching lists that need to maintain their object identity across forward passes. If not provided, will be
641
+ inferred from the module's `_skip_keys` attribute if it exists.
642
+
643
+ Example:
644
+ ```python
645
+ >>> from diffusers import CogVideoXTransformer3DModel
646
+ >>> from diffusers.hooks import apply_group_offloading
647
+
648
+ >>> transformer = CogVideoXTransformer3DModel.from_pretrained(
649
+ ... "THUDM/CogVideoX-5b", subfolder="transformer", torch_dtype=torch.bfloat16
650
+ ... )
651
+
652
+ >>> apply_group_offloading(
653
+ ... transformer,
654
+ ... onload_device=torch.device("cuda"),
655
+ ... offload_device=torch.device("cpu"),
656
+ ... offload_type="block_level",
657
+ ... num_blocks_per_group=2,
658
+ ... use_stream=True,
659
+ ... )
660
+ ```
661
+ """
662
+
663
+ onload_device = torch.device(onload_device) if isinstance(onload_device, str) else onload_device
664
+ offload_device = torch.device(offload_device) if isinstance(offload_device, str) else offload_device
665
+ offload_type = GroupOffloadingType(offload_type)
666
+
667
+ stream = None
668
+ if use_stream:
669
+ if torch.cuda.is_available():
670
+ stream = torch.cuda.Stream()
671
+ elif hasattr(torch, "xpu") and torch.xpu.is_available():
672
+ stream = torch.Stream()
673
+ else:
674
+ raise ValueError("Using streams for data transfer requires a CUDA device, or an Intel XPU device.")
675
+
676
+ if not use_stream and record_stream:
677
+ raise ValueError("`record_stream` cannot be True when `use_stream=False`.")
678
+ if offload_type == GroupOffloadingType.BLOCK_LEVEL and num_blocks_per_group is None:
679
+ raise ValueError("`num_blocks_per_group` must be provided when using `offload_type='block_level'.")
680
+
681
+ _raise_error_if_accelerate_model_or_sequential_hook_present(module)
682
+
683
+ if block_modules is None:
684
+ block_modules = getattr(module, "_group_offload_block_modules", None)
685
+
686
+ if exclude_kwargs is None:
687
+ exclude_kwargs = getattr(module, "_skip_keys", None)
688
+
689
+ config = GroupOffloadingConfig(
690
+ onload_device=onload_device,
691
+ offload_device=offload_device,
692
+ offload_type=offload_type,
693
+ num_blocks_per_group=num_blocks_per_group,
694
+ non_blocking=non_blocking,
695
+ stream=stream,
696
+ record_stream=record_stream,
697
+ low_cpu_mem_usage=low_cpu_mem_usage,
698
+ offload_to_disk_path=offload_to_disk_path,
699
+ block_modules=block_modules,
700
+ exclude_kwargs=exclude_kwargs,
701
+ )
702
+ _apply_group_offloading(module, config)
703
+
704
+
705
+ def _apply_group_offloading(module: torch.nn.Module, config: GroupOffloadingConfig) -> None:
706
+ if config.offload_type == GroupOffloadingType.BLOCK_LEVEL:
707
+ _apply_group_offloading_block_level(module, config)
708
+ elif config.offload_type == GroupOffloadingType.LEAF_LEVEL:
709
+ _apply_group_offloading_leaf_level(module, config)
710
+ else:
711
+ assert False
712
+
713
+
714
+ def _apply_group_offloading_block_level(module: torch.nn.Module, config: GroupOffloadingConfig) -> None:
715
+ r"""
716
+ This function applies offloading to groups of torch.nn.ModuleList or torch.nn.Sequential blocks, and explicitly
717
+ defined block modules. In comparison to the "leaf_level" offloading, which is more fine-grained, this offloading is
718
+ done at the top-level blocks and modules specified in block_modules.
719
+
720
+ When block_modules is provided, only those modules will be treated as blocks for offloading. For each specified
721
+ module, recursively apply block offloading to it.
722
+ """
723
+ if config.stream is not None and config.num_blocks_per_group != 1:
724
+ logger.warning(
725
+ f"Using streams is only supported for num_blocks_per_group=1. Got {config.num_blocks_per_group=}. Setting it to 1."
726
+ )
727
+ config.num_blocks_per_group = 1
728
+
729
+ block_modules = set(config.block_modules) if config.block_modules is not None else set()
730
+
731
+ # Create module groups for ModuleList and Sequential blocks, and explicitly defined block modules
732
+ modules_with_group_offloading = set()
733
+ unmatched_modules = []
734
+ matched_module_groups = []
735
+
736
+ for name, submodule in module.named_children():
737
+ # Check if this is an explicitly defined block module
738
+ if name in block_modules:
739
+ # Track submodule using a prefix to avoid filename collisions during disk offload.
740
+ # Without this, submodules sharing the same model class would be assigned identical
741
+ # filenames (derived from the class name).
742
+ prefix = f"{config.module_prefix}{name}." if config.module_prefix else f"{name}."
743
+ submodule_config = replace(config, module_prefix=prefix)
744
+
745
+ _apply_group_offloading_block_level(submodule, submodule_config)
746
+ modules_with_group_offloading.add(name)
747
+
748
+ elif isinstance(submodule, (torch.nn.ModuleList, torch.nn.Sequential)):
749
+ # Handle ModuleList and Sequential blocks as before
750
+ for i in range(0, len(submodule), config.num_blocks_per_group):
751
+ current_modules = list(submodule[i : i + config.num_blocks_per_group])
752
+ if len(current_modules) == 0:
753
+ continue
754
+
755
+ group_id = f"{config.module_prefix}{name}_{i}_{i + len(current_modules) - 1}"
756
+ group = ModuleGroup(
757
+ modules=current_modules,
758
+ offload_device=config.offload_device,
759
+ onload_device=config.onload_device,
760
+ offload_to_disk_path=config.offload_to_disk_path,
761
+ offload_leader=current_modules[-1],
762
+ onload_leader=current_modules[0],
763
+ non_blocking=config.non_blocking,
764
+ stream=config.stream,
765
+ record_stream=config.record_stream,
766
+ low_cpu_mem_usage=config.low_cpu_mem_usage,
767
+ onload_self=True,
768
+ group_id=group_id,
769
+ )
770
+ matched_module_groups.append(group)
771
+ for j in range(i, i + len(current_modules)):
772
+ modules_with_group_offloading.add(f"{name}.{j}")
773
+ else:
774
+ # This is an unmatched module
775
+ unmatched_modules.append((name, submodule))
776
+
777
+ # Apply group offloading hooks to the module groups
778
+ for i, group in enumerate(matched_module_groups):
779
+ for group_module in group.modules:
780
+ _apply_group_offloading_hook(group_module, group, config=config)
781
+
782
+ # Parameters and Buffers of the top-level module need to be offloaded/onloaded separately
783
+ # when the forward pass of this module is called. This is because the top-level module is not
784
+ # part of any group (as doing so would lead to no VRAM savings).
785
+ parameters = _gather_parameters_with_no_group_offloading_parent(module, modules_with_group_offloading)
786
+ buffers = _gather_buffers_with_no_group_offloading_parent(module, modules_with_group_offloading)
787
+ parameters = [param for _, param in parameters]
788
+ buffers = [buffer for _, buffer in buffers]
789
+
790
+ # Create a group for the remaining unmatched submodules of the top-level
791
+ # module so that they are on the correct device when the forward pass is called.
792
+ unmatched_modules = [unmatched_module for _, unmatched_module in unmatched_modules]
793
+ if len(unmatched_modules) > 0 or len(parameters) > 0 or len(buffers) > 0:
794
+ unmatched_group = ModuleGroup(
795
+ modules=unmatched_modules,
796
+ offload_device=config.offload_device,
797
+ onload_device=config.onload_device,
798
+ offload_to_disk_path=config.offload_to_disk_path,
799
+ offload_leader=module,
800
+ onload_leader=module,
801
+ parameters=parameters,
802
+ buffers=buffers,
803
+ non_blocking=False,
804
+ stream=None,
805
+ record_stream=False,
806
+ onload_self=True,
807
+ group_id=f"{config.module_prefix}{module.__class__.__name__}_unmatched_group",
808
+ )
809
+ if config.stream is None:
810
+ _apply_group_offloading_hook(module, unmatched_group, config=config)
811
+ else:
812
+ _apply_lazy_group_offloading_hook(module, unmatched_group, config=config)
813
+
814
+
815
+ def _apply_group_offloading_leaf_level(module: torch.nn.Module, config: GroupOffloadingConfig) -> None:
816
+ r"""
817
+ This function applies offloading to groups of leaf modules in a torch.nn.Module. This method has minimal memory
818
+ requirements. However, it can be slower compared to other offloading methods due to the excessive number of device
819
+ synchronizations. When using devices that support streams to overlap data transfer and computation, this method can
820
+ reduce memory usage without any performance degradation.
821
+ """
822
+ # Create module groups for leaf modules and apply group offloading hooks
823
+ modules_with_group_offloading = set()
824
+ for name, submodule in module.named_modules():
825
+ if not isinstance(submodule, _GO_LC_SUPPORTED_PYTORCH_LAYERS):
826
+ continue
827
+ group = ModuleGroup(
828
+ modules=[submodule],
829
+ offload_device=config.offload_device,
830
+ onload_device=config.onload_device,
831
+ offload_to_disk_path=config.offload_to_disk_path,
832
+ offload_leader=submodule,
833
+ onload_leader=submodule,
834
+ non_blocking=config.non_blocking,
835
+ stream=config.stream,
836
+ record_stream=config.record_stream,
837
+ low_cpu_mem_usage=config.low_cpu_mem_usage,
838
+ onload_self=True,
839
+ group_id=name,
840
+ )
841
+ _apply_group_offloading_hook(submodule, group, config=config)
842
+ modules_with_group_offloading.add(name)
843
+
844
+ # Parameters and Buffers at all non-leaf levels need to be offloaded/onloaded separately when the forward pass
845
+ # of the module is called
846
+ module_dict = dict(module.named_modules())
847
+ parameters = _gather_parameters_with_no_group_offloading_parent(module, modules_with_group_offloading)
848
+ buffers = _gather_buffers_with_no_group_offloading_parent(module, modules_with_group_offloading)
849
+
850
+ # Find closest module parent for each parameter and buffer, and attach group hooks
851
+ parent_to_parameters = {}
852
+ for name, param in parameters:
853
+ parent_name = _find_parent_module_in_module_dict(name, module_dict)
854
+ if parent_name in parent_to_parameters:
855
+ parent_to_parameters[parent_name].append(param)
856
+ else:
857
+ parent_to_parameters[parent_name] = [param]
858
+
859
+ parent_to_buffers = {}
860
+ for name, buffer in buffers:
861
+ parent_name = _find_parent_module_in_module_dict(name, module_dict)
862
+ if parent_name in parent_to_buffers:
863
+ parent_to_buffers[parent_name].append(buffer)
864
+ else:
865
+ parent_to_buffers[parent_name] = [buffer]
866
+
867
+ parent_names = set(parent_to_parameters.keys()) | set(parent_to_buffers.keys())
868
+ for name in parent_names:
869
+ parameters = parent_to_parameters.get(name, [])
870
+ buffers = parent_to_buffers.get(name, [])
871
+ parent_module = module_dict[name]
872
+ group = ModuleGroup(
873
+ modules=[],
874
+ offload_device=config.offload_device,
875
+ onload_device=config.onload_device,
876
+ offload_leader=parent_module,
877
+ onload_leader=parent_module,
878
+ offload_to_disk_path=config.offload_to_disk_path,
879
+ parameters=parameters,
880
+ buffers=buffers,
881
+ non_blocking=config.non_blocking,
882
+ stream=config.stream,
883
+ record_stream=config.record_stream,
884
+ low_cpu_mem_usage=config.low_cpu_mem_usage,
885
+ onload_self=True,
886
+ group_id=name,
887
+ )
888
+ _apply_group_offloading_hook(parent_module, group, config=config)
889
+
890
+ if config.stream is not None:
891
+ # When using streams, we need to know the layer execution order for applying prefetching (to overlap data transfer
892
+ # and computation). Since we don't know the order beforehand, we apply a lazy prefetching hook that will find the
893
+ # execution order and apply prefetching in the correct order.
894
+ unmatched_group = ModuleGroup(
895
+ modules=[],
896
+ offload_device=config.offload_device,
897
+ onload_device=config.onload_device,
898
+ offload_to_disk_path=config.offload_to_disk_path,
899
+ offload_leader=module,
900
+ onload_leader=module,
901
+ parameters=None,
902
+ buffers=None,
903
+ non_blocking=False,
904
+ stream=None,
905
+ record_stream=False,
906
+ low_cpu_mem_usage=config.low_cpu_mem_usage,
907
+ onload_self=True,
908
+ group_id=_GROUP_ID_LAZY_LEAF,
909
+ )
910
+ _apply_lazy_group_offloading_hook(module, unmatched_group, config=config)
911
+
912
+
913
+ def _apply_group_offloading_hook(
914
+ module: torch.nn.Module,
915
+ group: ModuleGroup,
916
+ *,
917
+ config: GroupOffloadingConfig,
918
+ ) -> None:
919
+ registry = HookRegistry.check_if_exists_or_initialize(module)
920
+
921
+ # We may have already registered a group offloading hook if the module had a torch.nn.Parameter whose parent
922
+ # is the current module. In such cases, we don't want to overwrite the existing group offloading hook.
923
+ if registry.get_hook(_GROUP_OFFLOADING) is None:
924
+ hook = GroupOffloadingHook(group, config=config)
925
+ registry.register_hook(hook, _GROUP_OFFLOADING)
926
+
927
+
928
+ def _apply_lazy_group_offloading_hook(
929
+ module: torch.nn.Module,
930
+ group: ModuleGroup,
931
+ *,
932
+ config: GroupOffloadingConfig,
933
+ ) -> None:
934
+ registry = HookRegistry.check_if_exists_or_initialize(module)
935
+
936
+ # We may have already registered a group offloading hook if the module had a torch.nn.Parameter whose parent
937
+ # is the current module. In such cases, we don't want to overwrite the existing group offloading hook.
938
+ if registry.get_hook(_GROUP_OFFLOADING) is None:
939
+ hook = GroupOffloadingHook(group, config=config)
940
+ registry.register_hook(hook, _GROUP_OFFLOADING)
941
+
942
+ lazy_prefetch_hook = LazyPrefetchGroupOffloadingHook()
943
+ registry.register_hook(lazy_prefetch_hook, _LAZY_PREFETCH_GROUP_OFFLOADING)
944
+
945
+
946
+ def _gather_parameters_with_no_group_offloading_parent(
947
+ module: torch.nn.Module, modules_with_group_offloading: Set[str]
948
+ ) -> list[torch.nn.Parameter]:
949
+ parameters = []
950
+ for name, parameter in module.named_parameters():
951
+ has_parent_with_group_offloading = False
952
+ atoms = name.split(".")
953
+ while len(atoms) > 0:
954
+ parent_name = ".".join(atoms)
955
+ if parent_name in modules_with_group_offloading:
956
+ has_parent_with_group_offloading = True
957
+ break
958
+ atoms.pop()
959
+ if not has_parent_with_group_offloading:
960
+ parameters.append((name, parameter))
961
+ return parameters
962
+
963
+
964
+ def _gather_buffers_with_no_group_offloading_parent(
965
+ module: torch.nn.Module, modules_with_group_offloading: Set[str]
966
+ ) -> list[torch.Tensor]:
967
+ buffers = []
968
+ for name, buffer in module.named_buffers():
969
+ has_parent_with_group_offloading = False
970
+ atoms = name.split(".")
971
+ while len(atoms) > 0:
972
+ parent_name = ".".join(atoms)
973
+ if parent_name in modules_with_group_offloading:
974
+ has_parent_with_group_offloading = True
975
+ break
976
+ atoms.pop()
977
+ if not has_parent_with_group_offloading:
978
+ buffers.append((name, buffer))
979
+ return buffers
980
+
981
+
982
+ def _find_parent_module_in_module_dict(name: str, module_dict: dict[str, torch.nn.Module]) -> str:
983
+ atoms = name.split(".")
984
+ while len(atoms) > 0:
985
+ parent_name = ".".join(atoms)
986
+ if parent_name in module_dict:
987
+ return parent_name
988
+ atoms.pop()
989
+ return ""
990
+
991
+
992
+ def _raise_error_if_accelerate_model_or_sequential_hook_present(module: torch.nn.Module) -> None:
993
+ if not is_accelerate_available():
994
+ return
995
+ for name, submodule in module.named_modules():
996
+ if not hasattr(submodule, "_hf_hook"):
997
+ continue
998
+ if isinstance(submodule._hf_hook, (AlignDevicesHook, CpuOffload)):
999
+ raise ValueError(
1000
+ f"Cannot apply group offloading to a module that is already applying an alternative "
1001
+ f"offloading strategy from Accelerate. If you want to apply group offloading, please "
1002
+ f"disable the existing offloading strategy first. Offending module: {name} ({type(submodule)})"
1003
+ )
1004
+
1005
+
1006
+ def _get_top_level_group_offload_hook(module: torch.nn.Module) -> GroupOffloadingHook | None:
1007
+ for submodule in module.modules():
1008
+ if hasattr(submodule, "_diffusers_hook"):
1009
+ group_offloading_hook = submodule._diffusers_hook.get_hook(_GROUP_OFFLOADING)
1010
+ if group_offloading_hook is not None:
1011
+ return group_offloading_hook
1012
+ return None
1013
+
1014
+
1015
+ def _is_group_offload_enabled(module: torch.nn.Module) -> bool:
1016
+ top_level_group_offload_hook = _get_top_level_group_offload_hook(module)
1017
+ return top_level_group_offload_hook is not None
1018
+
1019
+
1020
+ def _get_group_onload_device(module: torch.nn.Module) -> torch.device:
1021
+ top_level_group_offload_hook = _get_top_level_group_offload_hook(module)
1022
+ if top_level_group_offload_hook is not None:
1023
+ return top_level_group_offload_hook.config.onload_device
1024
+ raise ValueError("Group offloading is not enabled for the provided module.")
1025
+
1026
+
1027
+ def _compute_group_hash(group_id):
1028
+ hashed_id = hashlib.sha256(group_id.encode("utf-8")).hexdigest()
1029
+ # first 16 characters for a reasonably short but unique name
1030
+ return hashed_id[:16]
1031
+
1032
+
1033
+ def _maybe_remove_and_reapply_group_offloading(module: torch.nn.Module) -> None:
1034
+ r"""
1035
+ Removes the group offloading hook from the module and re-applies it. This is useful when the module has been
1036
+ modified in-place and the group offloading hook references-to-tensors needs to be updated. The in-place
1037
+ modification can happen in a number of ways, for example, fusing QKV or unloading/loading LoRAs on-the-fly.
1038
+
1039
+ In this implementation, we make an assumption that group offloading has only been applied at the top-level module,
1040
+ and therefore all submodules have the same onload and offload devices. If this assumption is not true, say in the
1041
+ case where user has applied group offloading at multiple levels, this function will not work as expected.
1042
+
1043
+ There is some performance penalty associated with doing this when non-default streams are used, because we need to
1044
+ retrace the execution order of the layers with `LazyPrefetchGroupOffloadingHook`.
1045
+ """
1046
+ top_level_group_offload_hook = _get_top_level_group_offload_hook(module)
1047
+
1048
+ if top_level_group_offload_hook is None:
1049
+ return
1050
+
1051
+ registry = HookRegistry.check_if_exists_or_initialize(module)
1052
+ registry.remove_hook(_GROUP_OFFLOADING, recurse=True)
1053
+ registry.remove_hook(_LAYER_EXECUTION_TRACKER, recurse=True)
1054
+ registry.remove_hook(_LAZY_PREFETCH_GROUP_OFFLOADING, recurse=True)
1055
+
1056
+ _apply_group_offloading(module, top_level_group_offload_hook.config)
diffusers/hooks/hooks.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import functools
16
+ from typing import Any
17
+
18
+ import torch
19
+
20
+ from ..utils.logging import get_logger
21
+ from ..utils.torch_utils import unwrap_module
22
+
23
+
24
+ logger = get_logger(__name__) # pylint: disable=invalid-name
25
+
26
+
27
+ class BaseState:
28
+ def reset(self, *args, **kwargs) -> None:
29
+ raise NotImplementedError(
30
+ "BaseState::reset is not implemented. Please implement this method in the derived class."
31
+ )
32
+
33
+
34
+ class StateManager:
35
+ def __init__(self, state_cls: BaseState, init_args=None, init_kwargs=None):
36
+ self._state_cls = state_cls
37
+ self._init_args = init_args if init_args is not None else ()
38
+ self._init_kwargs = init_kwargs if init_kwargs is not None else {}
39
+ self._state_cache = {}
40
+ self._current_context = None
41
+
42
+ def get_state(self):
43
+ if self._current_context is None:
44
+ raise ValueError("No context is set. Please set a context before retrieving the state.")
45
+ if self._current_context not in self._state_cache.keys():
46
+ self._state_cache[self._current_context] = self._state_cls(*self._init_args, **self._init_kwargs)
47
+ return self._state_cache[self._current_context]
48
+
49
+ def set_context(self, name: str) -> None:
50
+ self._current_context = name
51
+
52
+ def reset(self, *args, **kwargs) -> None:
53
+ for name, state in list(self._state_cache.items()):
54
+ state.reset(*args, **kwargs)
55
+ self._state_cache.pop(name)
56
+ self._current_context = None
57
+
58
+
59
+ class ModelHook:
60
+ r"""
61
+ A hook that contains callbacks to be executed just before and after the forward method of a model.
62
+ """
63
+
64
+ _is_stateful = False
65
+
66
+ def __init__(self):
67
+ self.fn_ref: "HookFunctionReference" = None
68
+
69
+ def initialize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
70
+ r"""
71
+ Hook that is executed when a model is initialized.
72
+
73
+ Args:
74
+ module (`torch.nn.Module`):
75
+ The module attached to this hook.
76
+ """
77
+ return module
78
+
79
+ def deinitalize_hook(self, module: torch.nn.Module) -> torch.nn.Module:
80
+ r"""
81
+ Hook that is executed when a model is deinitialized.
82
+
83
+ Args:
84
+ module (`torch.nn.Module`):
85
+ The module attached to this hook.
86
+ """
87
+ return module
88
+
89
+ def pre_forward(self, module: torch.nn.Module, *args, **kwargs) -> tuple[tuple[Any], dict[str, Any]]:
90
+ r"""
91
+ Hook that is executed just before the forward method of the model.
92
+
93
+ Args:
94
+ module (`torch.nn.Module`):
95
+ The module whose forward pass will be executed just after this event.
96
+ args (`tuple[Any]`):
97
+ The positional arguments passed to the module.
98
+ kwargs (`dict[Str, Any]`):
99
+ The keyword arguments passed to the module.
100
+ Returns:
101
+ `tuple[tuple[Any], dict[Str, Any]]`:
102
+ A tuple with the treated `args` and `kwargs`.
103
+ """
104
+ return args, kwargs
105
+
106
+ def post_forward(self, module: torch.nn.Module, output: Any) -> Any:
107
+ r"""
108
+ Hook that is executed just after the forward method of the model.
109
+
110
+ Args:
111
+ module (`torch.nn.Module`):
112
+ The module whose forward pass been executed just before this event.
113
+ output (`Any`):
114
+ The output of the module.
115
+ Returns:
116
+ `Any`: The processed `output`.
117
+ """
118
+ return output
119
+
120
+ def detach_hook(self, module: torch.nn.Module) -> torch.nn.Module:
121
+ r"""
122
+ Hook that is executed when the hook is detached from a module.
123
+
124
+ Args:
125
+ module (`torch.nn.Module`):
126
+ The module detached from this hook.
127
+ """
128
+ return module
129
+
130
+ def reset_state(self, module: torch.nn.Module):
131
+ if self._is_stateful:
132
+ raise NotImplementedError("This hook is stateful and needs to implement the `reset_state` method.")
133
+ return module
134
+
135
+ def _set_context(self, module: torch.nn.Module, name: str) -> None:
136
+ # Iterate over all attributes of the hook to see if any of them have the type `StateManager`. If so, call `set_context` on them.
137
+ for attr_name in dir(self):
138
+ attr = getattr(self, attr_name)
139
+ if isinstance(attr, StateManager):
140
+ attr.set_context(name)
141
+ return module
142
+
143
+
144
+ class HookFunctionReference:
145
+ def __init__(self) -> None:
146
+ """A container class that maintains mutable references to forward pass functions in a hook chain.
147
+
148
+ Its mutable nature allows the hook system to modify the execution chain dynamically without rebuilding the
149
+ entire forward pass structure.
150
+
151
+ Attributes:
152
+ pre_forward: A callable that processes inputs before the main forward pass.
153
+ post_forward: A callable that processes outputs after the main forward pass.
154
+ forward: The current forward function in the hook chain.
155
+ original_forward: The original forward function, stored when a hook provides a custom new_forward.
156
+
157
+ The class enables hook removal by allowing updates to the forward chain through reference modification rather
158
+ than requiring reconstruction of the entire chain. When a hook is removed, only the relevant references need to
159
+ be updated, preserving the execution order of the remaining hooks.
160
+ """
161
+ self.pre_forward = None
162
+ self.post_forward = None
163
+ self.forward = None
164
+ self.original_forward = None
165
+
166
+
167
+ class HookRegistry:
168
+ def __init__(self, module_ref: torch.nn.Module) -> None:
169
+ super().__init__()
170
+
171
+ self.hooks: dict[str, ModelHook] = {}
172
+
173
+ self._module_ref = module_ref
174
+ self._hook_order = []
175
+ self._fn_refs = []
176
+
177
+ def register_hook(self, hook: ModelHook, name: str) -> None:
178
+ if name in self.hooks.keys():
179
+ raise ValueError(
180
+ f"Hook with name {name} already exists in the registry. Please use a different name or "
181
+ f"first remove the existing hook and then add a new one."
182
+ )
183
+
184
+ self._module_ref = hook.initialize_hook(self._module_ref)
185
+
186
+ def create_new_forward(function_reference: HookFunctionReference):
187
+ def new_forward(module, *args, **kwargs):
188
+ args, kwargs = function_reference.pre_forward(module, *args, **kwargs)
189
+ output = function_reference.forward(*args, **kwargs)
190
+ return function_reference.post_forward(module, output)
191
+
192
+ return new_forward
193
+
194
+ forward = self._module_ref.forward
195
+
196
+ fn_ref = HookFunctionReference()
197
+ fn_ref.pre_forward = hook.pre_forward
198
+ fn_ref.post_forward = hook.post_forward
199
+ fn_ref.forward = forward
200
+
201
+ if hasattr(hook, "new_forward"):
202
+ fn_ref.original_forward = forward
203
+ fn_ref.forward = functools.update_wrapper(
204
+ functools.partial(hook.new_forward, self._module_ref), hook.new_forward
205
+ )
206
+
207
+ rewritten_forward = create_new_forward(fn_ref)
208
+ # Wrap from the original `forward` so `inspect.signature` follows `__wrapped__` to the real
209
+ # signature instead of the generic `(module, *args, **kwargs)`, which breaks `torch.export`.
210
+ self._module_ref.forward = functools.update_wrapper(
211
+ functools.partial(rewritten_forward, self._module_ref), forward
212
+ )
213
+
214
+ hook.fn_ref = fn_ref
215
+ self.hooks[name] = hook
216
+ self._hook_order.append(name)
217
+ self._fn_refs.append(fn_ref)
218
+
219
+ def get_hook(self, name: str) -> ModelHook | None:
220
+ return self.hooks.get(name, None)
221
+
222
+ def remove_hook(self, name: str, recurse: bool = True) -> None:
223
+ if name in self.hooks.keys():
224
+ num_hooks = len(self._hook_order)
225
+ hook = self.hooks[name]
226
+ index = self._hook_order.index(name)
227
+ fn_ref = self._fn_refs[index]
228
+
229
+ old_forward = fn_ref.forward
230
+ if fn_ref.original_forward is not None:
231
+ old_forward = fn_ref.original_forward
232
+
233
+ if index == num_hooks - 1:
234
+ self._module_ref.forward = old_forward
235
+ else:
236
+ self._fn_refs[index + 1].forward = old_forward
237
+
238
+ self._module_ref = hook.deinitalize_hook(self._module_ref)
239
+ del self.hooks[name]
240
+ self._hook_order.pop(index)
241
+ self._fn_refs.pop(index)
242
+
243
+ if recurse:
244
+ for module_name, module in self._module_ref.named_modules():
245
+ if module_name == "":
246
+ continue
247
+ if hasattr(module, "_diffusers_hook"):
248
+ module._diffusers_hook.remove_hook(name, recurse=False)
249
+
250
+ def reset_stateful_hooks(self, recurse: bool = True) -> None:
251
+ for hook_name in reversed(self._hook_order):
252
+ hook = self.hooks[hook_name]
253
+ if hook._is_stateful:
254
+ hook.reset_state(self._module_ref)
255
+
256
+ if recurse:
257
+ for module_name, module in unwrap_module(self._module_ref).named_modules():
258
+ if module_name == "":
259
+ continue
260
+ module = unwrap_module(module)
261
+ if hasattr(module, "_diffusers_hook"):
262
+ module._diffusers_hook.reset_stateful_hooks(recurse=False)
263
+
264
+ @classmethod
265
+ def check_if_exists_or_initialize(cls, module: torch.nn.Module) -> "HookRegistry":
266
+ if not hasattr(module, "_diffusers_hook"):
267
+ module._diffusers_hook = cls(module)
268
+ return module._diffusers_hook
269
+
270
+ def _set_context(self, name: str | None = None) -> None:
271
+ for hook_name in reversed(self._hook_order):
272
+ hook = self.hooks[hook_name]
273
+ if hook._is_stateful:
274
+ hook._set_context(self._module_ref, name)
275
+
276
+ for registry in self._get_child_registries():
277
+ registry._set_context(name)
278
+
279
+ def invalidate_child_registries_cache(self) -> None:
280
+ """Invalidate the cached child-registry list across this module's tree.
281
+
282
+ `_get_child_registries` caches the registries it finds by walking `named_modules()`, keyed on the registry that
283
+ built it. Registering or removing hooks on descendant modules (e.g. block hooks added by `enable_cache`)
284
+ changes which modules carry a `_diffusers_hook`, which stales that cache. Call this after any operation that
285
+ adds or removes hooks in the subtree so the list is rebuilt on next use. Clears the cache on every registry in
286
+ the tree, since the same registries appear in ancestor caches.
287
+ """
288
+ for _, module in unwrap_module(self._module_ref).named_modules():
289
+ module = unwrap_module(module)
290
+ if hasattr(module, "_diffusers_hook"):
291
+ module._diffusers_hook._child_registries_cache = None
292
+
293
+ def _get_child_registries(self) -> list["HookRegistry"]:
294
+ """Return registries of child modules, using a cached list when available.
295
+
296
+ The cache is built on first call and reused for subsequent calls. This avoids the cost of walking the full
297
+ module tree via named_modules() on every _set_context call, which is significant for large models (e.g. ~2.7ms
298
+ per call on Flux2).
299
+ """
300
+ if not hasattr(self, "_child_registries_cache"):
301
+ self._child_registries_cache = None
302
+
303
+ if self._child_registries_cache is not None:
304
+ return self._child_registries_cache
305
+
306
+ registries = []
307
+ for module_name, module in unwrap_module(self._module_ref).named_modules():
308
+ if module_name == "":
309
+ continue
310
+ module = unwrap_module(module)
311
+ if hasattr(module, "_diffusers_hook"):
312
+ registries.append(module._diffusers_hook)
313
+ self._child_registries_cache = registries
314
+ return registries
315
+
316
+ def __repr__(self) -> str:
317
+ registry_repr = ""
318
+ for i, hook_name in enumerate(self._hook_order):
319
+ if self.hooks[hook_name].__class__.__repr__ is not object.__repr__:
320
+ hook_repr = self.hooks[hook_name].__repr__()
321
+ else:
322
+ hook_repr = self.hooks[hook_name].__class__.__name__
323
+ registry_repr += f" ({i}) {hook_name} - {hook_repr}"
324
+ if i < len(self._hook_order) - 1:
325
+ registry_repr += "\n"
326
+ return f"HookRegistry(\n{registry_repr}\n)"
diffusers/hooks/layer_skip.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import math
16
+ from dataclasses import asdict, dataclass
17
+ from typing import Callable
18
+
19
+ import torch
20
+
21
+ from ..utils import get_logger
22
+ from ..utils.torch_utils import unwrap_module
23
+ from ._common import (
24
+ _ALL_TRANSFORMER_BLOCK_IDENTIFIERS,
25
+ _ATTENTION_CLASSES,
26
+ _FEEDFORWARD_CLASSES,
27
+ _get_submodule_from_fqn,
28
+ )
29
+ from ._helpers import AttentionProcessorRegistry, TransformerBlockRegistry
30
+ from .hooks import HookRegistry, ModelHook
31
+
32
+
33
+ logger = get_logger(__name__) # pylint: disable=invalid-name
34
+
35
+ _LAYER_SKIP_HOOK = "layer_skip_hook"
36
+
37
+
38
+ # Aryan/YiYi TODO: we need to make guider class a config mixin so I think this is not needed
39
+ # either remove or make it serializable
40
+ @dataclass
41
+ class LayerSkipConfig:
42
+ r"""
43
+ Configuration for skipping internal transformer blocks when executing a transformer model.
44
+
45
+ Args:
46
+ indices (`list[int]`):
47
+ The indices of the layer to skip. This is typically the first layer in the transformer block.
48
+ fqn (`str`, defaults to `"auto"`):
49
+ The fully qualified name identifying the stack of transformer blocks. Typically, this is
50
+ `transformer_blocks`, `single_transformer_blocks`, `blocks`, `layers`, or `temporal_transformer_blocks`.
51
+ For automatic detection, set this to `"auto"`. "auto" only works on DiT models. For UNet models, you must
52
+ provide the correct fqn.
53
+ skip_attention (`bool`, defaults to `True`):
54
+ Whether to skip attention blocks.
55
+ skip_ff (`bool`, defaults to `True`):
56
+ Whether to skip feed-forward blocks.
57
+ skip_attention_scores (`bool`, defaults to `False`):
58
+ Whether to skip attention score computation in the attention blocks. This is equivalent to using `value`
59
+ projections as the output of scaled dot product attention.
60
+ dropout (`float`, defaults to `1.0`):
61
+ The dropout probability for dropping the outputs of the skipped layers. By default, this is set to `1.0`,
62
+ meaning that the outputs of the skipped layers are completely ignored. If set to `0.0`, the outputs of the
63
+ skipped layers are fully retained, which is equivalent to not skipping any layers.
64
+ """
65
+
66
+ indices: list[int]
67
+ fqn: str = "auto"
68
+ skip_attention: bool = True
69
+ skip_attention_scores: bool = False
70
+ skip_ff: bool = True
71
+ dropout: float = 1.0
72
+
73
+ def __post_init__(self):
74
+ if not (0 <= self.dropout <= 1):
75
+ raise ValueError(f"Expected `dropout` to be between 0.0 and 1.0, but got {self.dropout}.")
76
+ if not math.isclose(self.dropout, 1.0) and self.skip_attention_scores:
77
+ raise ValueError(
78
+ "Cannot set `skip_attention_scores` to True when `dropout` is not 1.0. Please set `dropout` to 1.0."
79
+ )
80
+
81
+ def to_dict(self):
82
+ return asdict(self)
83
+
84
+ @staticmethod
85
+ def from_dict(data: dict) -> "LayerSkipConfig":
86
+ return LayerSkipConfig(**data)
87
+
88
+
89
+ class AttentionScoreSkipFunctionMode(torch.overrides.TorchFunctionMode):
90
+ def __torch_function__(self, func, types, args=(), kwargs=None):
91
+ if kwargs is None:
92
+ kwargs = {}
93
+ if func is torch.nn.functional.scaled_dot_product_attention:
94
+ query = kwargs.get("query", None)
95
+ key = kwargs.get("key", None)
96
+ value = kwargs.get("value", None)
97
+ query = query if query is not None else args[0]
98
+ key = key if key is not None else args[1]
99
+ value = value if value is not None else args[2]
100
+ # If the Q sequence length does not match KV sequence length, methods like
101
+ # Perturbed Attention Guidance cannot be used (because the caller expects
102
+ # the same sequence length as Q, but if we return V here, it will not match).
103
+ # When Q.shape[2] != V.shape[2], PAG will essentially not be applied and
104
+ # the overall effect would that be of normal CFG with a scale of (guidance_scale + perturbed_guidance_scale).
105
+ if query.shape[2] == value.shape[2]:
106
+ return value
107
+ return func(*args, **kwargs)
108
+
109
+
110
+ class AttentionProcessorSkipHook(ModelHook):
111
+ def __init__(self, skip_processor_output_fn: Callable, skip_attention_scores: bool = False, dropout: float = 1.0):
112
+ self.skip_processor_output_fn = skip_processor_output_fn
113
+ self.skip_attention_scores = skip_attention_scores
114
+ self.dropout = dropout
115
+
116
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
117
+ if self.skip_attention_scores:
118
+ if not math.isclose(self.dropout, 1.0):
119
+ raise ValueError(
120
+ "Cannot set `skip_attention_scores` to True when `dropout` is not 1.0. Please set `dropout` to 1.0."
121
+ )
122
+ with AttentionScoreSkipFunctionMode():
123
+ output = self.fn_ref.original_forward(*args, **kwargs)
124
+ else:
125
+ if math.isclose(self.dropout, 1.0):
126
+ output = self.skip_processor_output_fn(module, *args, **kwargs)
127
+ else:
128
+ output = self.fn_ref.original_forward(*args, **kwargs)
129
+ output = torch.nn.functional.dropout(output, p=self.dropout)
130
+ return output
131
+
132
+
133
+ class FeedForwardSkipHook(ModelHook):
134
+ def __init__(self, dropout: float):
135
+ super().__init__()
136
+ self.dropout = dropout
137
+
138
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
139
+ if math.isclose(self.dropout, 1.0):
140
+ output = kwargs.get("hidden_states", None)
141
+ if output is None:
142
+ output = kwargs.get("x", None)
143
+ if output is None and len(args) > 0:
144
+ output = args[0]
145
+ else:
146
+ output = self.fn_ref.original_forward(*args, **kwargs)
147
+ output = torch.nn.functional.dropout(output, p=self.dropout)
148
+ return output
149
+
150
+
151
+ class TransformerBlockSkipHook(ModelHook):
152
+ def __init__(self, dropout: float):
153
+ super().__init__()
154
+ self.dropout = dropout
155
+
156
+ def initialize_hook(self, module):
157
+ self._metadata = TransformerBlockRegistry.get(unwrap_module(module).__class__)
158
+ return module
159
+
160
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
161
+ if math.isclose(self.dropout, 1.0):
162
+ original_hidden_states = self._metadata._get_parameter_from_args_kwargs("hidden_states", args, kwargs)
163
+ if self._metadata.return_encoder_hidden_states_index is None:
164
+ output = original_hidden_states
165
+ else:
166
+ original_encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
167
+ "encoder_hidden_states", args, kwargs
168
+ )
169
+ output = (original_hidden_states, original_encoder_hidden_states)
170
+ else:
171
+ output = self.fn_ref.original_forward(*args, **kwargs)
172
+ output = torch.nn.functional.dropout(output, p=self.dropout)
173
+ return output
174
+
175
+
176
+ def apply_layer_skip(module: torch.nn.Module, config: LayerSkipConfig) -> None:
177
+ r"""
178
+ Apply layer skipping to internal layers of a transformer.
179
+
180
+ Args:
181
+ module (`torch.nn.Module`):
182
+ The transformer model to which the layer skip hook should be applied.
183
+ config (`LayerSkipConfig`):
184
+ The configuration for the layer skip hook.
185
+
186
+ Example:
187
+
188
+ ```python
189
+ >>> from diffusers import apply_layer_skip_hook, CogVideoXTransformer3DModel, LayerSkipConfig
190
+
191
+ >>> transformer = CogVideoXTransformer3DModel.from_pretrained("THUDM/CogVideoX-5b", torch_dtype=torch.bfloat16)
192
+ >>> config = LayerSkipConfig(layer_index=[10, 20], fqn="transformer_blocks")
193
+ >>> apply_layer_skip_hook(transformer, config)
194
+ ```
195
+ """
196
+ _apply_layer_skip_hook(module, config)
197
+
198
+
199
+ def _apply_layer_skip_hook(module: torch.nn.Module, config: LayerSkipConfig, name: str | None = None) -> None:
200
+ name = name or _LAYER_SKIP_HOOK
201
+
202
+ if config.skip_attention and config.skip_attention_scores:
203
+ raise ValueError("Cannot set both `skip_attention` and `skip_attention_scores` to True. Please choose one.")
204
+ if not math.isclose(config.dropout, 1.0) and config.skip_attention_scores:
205
+ raise ValueError(
206
+ "Cannot set `skip_attention_scores` to True when `dropout` is not 1.0. Please set `dropout` to 1.0."
207
+ )
208
+
209
+ if config.fqn == "auto":
210
+ for identifier in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS:
211
+ if hasattr(module, identifier):
212
+ config.fqn = identifier
213
+ break
214
+ else:
215
+ raise ValueError(
216
+ "Could not find a suitable identifier for the transformer blocks automatically. Please provide a valid "
217
+ "`fqn` (fully qualified name) that identifies a stack of transformer blocks."
218
+ )
219
+
220
+ transformer_blocks = _get_submodule_from_fqn(module, config.fqn)
221
+ if transformer_blocks is None or not isinstance(transformer_blocks, torch.nn.ModuleList):
222
+ raise ValueError(
223
+ f"Could not find {config.fqn} in the provided module, or configured `fqn` (fully qualified name) does not identify "
224
+ f"a `torch.nn.ModuleList`. Please provide a valid `fqn` that identifies a stack of transformer blocks."
225
+ )
226
+ if len(config.indices) == 0:
227
+ raise ValueError("Layer index list is empty. Please provide a non-empty list of layer indices to skip.")
228
+
229
+ blocks_found = False
230
+ for i, block in enumerate(transformer_blocks):
231
+ if i not in config.indices:
232
+ continue
233
+
234
+ blocks_found = True
235
+
236
+ if config.skip_attention and config.skip_ff:
237
+ logger.debug(f"Applying TransformerBlockSkipHook to '{config.fqn}.{i}'")
238
+ registry = HookRegistry.check_if_exists_or_initialize(block)
239
+ hook = TransformerBlockSkipHook(config.dropout)
240
+ registry.register_hook(hook, name)
241
+
242
+ elif config.skip_attention or config.skip_attention_scores:
243
+ for submodule_name, submodule in block.named_modules():
244
+ if isinstance(submodule, _ATTENTION_CLASSES) and not submodule.is_cross_attention:
245
+ logger.debug(f"Applying AttentionProcessorSkipHook to '{config.fqn}.{i}.{submodule_name}'")
246
+ output_fn = AttentionProcessorRegistry.get(submodule.processor.__class__).skip_processor_output_fn
247
+ registry = HookRegistry.check_if_exists_or_initialize(submodule)
248
+ hook = AttentionProcessorSkipHook(output_fn, config.skip_attention_scores, config.dropout)
249
+ registry.register_hook(hook, name)
250
+
251
+ if config.skip_ff:
252
+ for submodule_name, submodule in block.named_modules():
253
+ if isinstance(submodule, _FEEDFORWARD_CLASSES):
254
+ logger.debug(f"Applying FeedForwardSkipHook to '{config.fqn}.{i}.{submodule_name}'")
255
+ registry = HookRegistry.check_if_exists_or_initialize(submodule)
256
+ hook = FeedForwardSkipHook(config.dropout)
257
+ registry.register_hook(hook, name)
258
+
259
+ if not blocks_found:
260
+ raise ValueError(
261
+ f"Could not find any transformer blocks matching the provided indices {config.indices} and "
262
+ f"fully qualified name '{config.fqn}'. Please check the indices and fqn for correctness."
263
+ )
diffusers/hooks/layerwise_casting.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+ from typing import Type
17
+
18
+ import torch
19
+
20
+ from ..utils import get_logger, is_peft_available
21
+ from ._common import _GO_LC_SUPPORTED_PYTORCH_LAYERS
22
+ from .hooks import HookRegistry, ModelHook
23
+
24
+
25
+ logger = get_logger(__name__) # pylint: disable=invalid-name
26
+
27
+
28
+ # fmt: off
29
+ _LAYERWISE_CASTING_HOOK = "layerwise_casting"
30
+ _PEFT_AUTOCAST_DISABLE_HOOK = "peft_autocast_disable"
31
+ DEFAULT_SKIP_MODULES_PATTERN = ("pos_embed", "patch_embed", "norm", "^proj_in$", "^proj_out$")
32
+ # fmt: on
33
+
34
+ _SHOULD_DISABLE_PEFT_INPUT_AUTOCAST = is_peft_available()
35
+ if _SHOULD_DISABLE_PEFT_INPUT_AUTOCAST:
36
+ from peft.helpers import disable_input_dtype_casting
37
+ from peft.tuners.tuners_utils import BaseTunerLayer
38
+
39
+
40
+ class LayerwiseCastingHook(ModelHook):
41
+ r"""
42
+ A hook that casts the weights of a module to a high precision dtype for computation, and to a low precision dtype
43
+ for storage. This process may lead to quality loss in the output, but can significantly reduce the memory
44
+ footprint.
45
+ """
46
+
47
+ _is_stateful = False
48
+
49
+ def __init__(self, storage_dtype: torch.dtype, compute_dtype: torch.dtype, non_blocking: bool) -> None:
50
+ self.storage_dtype = storage_dtype
51
+ self.compute_dtype = compute_dtype
52
+ self.non_blocking = non_blocking
53
+
54
+ def initialize_hook(self, module: torch.nn.Module):
55
+ module.to(dtype=self.storage_dtype, non_blocking=self.non_blocking)
56
+ return module
57
+
58
+ def deinitalize_hook(self, module: torch.nn.Module):
59
+ raise NotImplementedError(
60
+ "LayerwiseCastingHook does not support deinitialization. A model once enabled with layerwise casting will "
61
+ "have casted its weights to a lower precision dtype for storage. Casting this back to the original dtype "
62
+ "will lead to precision loss, which might have an impact on the model's generation quality. The model should "
63
+ "be re-initialized and loaded in the original dtype."
64
+ )
65
+
66
+ def pre_forward(self, module: torch.nn.Module, *args, **kwargs):
67
+ module.to(dtype=self.compute_dtype, non_blocking=self.non_blocking)
68
+ return args, kwargs
69
+
70
+ def post_forward(self, module: torch.nn.Module, output):
71
+ module.to(dtype=self.storage_dtype, non_blocking=self.non_blocking)
72
+ return output
73
+
74
+
75
+ class PeftInputAutocastDisableHook(ModelHook):
76
+ r"""
77
+ A hook that disables the casting of inputs to the module weight dtype during the forward pass. By default, PEFT
78
+ casts the inputs to the weight dtype of the module, which can lead to precision loss.
79
+
80
+ The reasons for needing this are:
81
+ - If we don't add PEFT layers' weight names to `skip_modules_pattern` when applying layerwise casting, the
82
+ inputs will be casted to the, possibly lower precision, storage dtype. Reference:
83
+ https://github.com/huggingface/peft/blob/0facdebf6208139cbd8f3586875acb378813dd97/src/peft/tuners/lora/layer.py#L706
84
+ - We can, on our end, use something like accelerate's `send_to_device` but for dtypes. This way, we can ensure
85
+ that the inputs are casted to the computation dtype correctly always. However, there are two goals we are
86
+ hoping to achieve:
87
+ 1. Making forward implementations independent of device/dtype casting operations as much as possible.
88
+ 2. Performing inference without losing information from casting to different precisions. With the current
89
+ PEFT implementation (as linked in the reference above), and assuming running layerwise casting inference
90
+ with storage_dtype=torch.float8_e4m3fn and compute_dtype=torch.bfloat16, inputs are cast to
91
+ torch.float8_e4m3fn in the lora layer. We will then upcast back to torch.bfloat16 when we continue the
92
+ forward pass in PEFT linear forward or Diffusers layer forward, with a `send_to_dtype` operation from
93
+ LayerwiseCastingHook. This will be a lossy operation and result in poorer generation quality.
94
+ """
95
+
96
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
97
+ with disable_input_dtype_casting(module):
98
+ return self.fn_ref.original_forward(*args, **kwargs)
99
+
100
+
101
+ def apply_layerwise_casting(
102
+ module: torch.nn.Module,
103
+ storage_dtype: torch.dtype,
104
+ compute_dtype: torch.dtype,
105
+ skip_modules_pattern: str | tuple[str, ...] = "auto",
106
+ skip_modules_classes: tuple[Type[torch.nn.Module], ...] | None = None,
107
+ non_blocking: bool = False,
108
+ ) -> None:
109
+ r"""
110
+ Applies layerwise casting to a given module. The module expected here is a Diffusers ModelMixin but it can be any
111
+ nn.Module using diffusers layers or pytorch primitives.
112
+
113
+ Example:
114
+
115
+ ```python
116
+ >>> import torch
117
+ >>> from diffusers import CogVideoXTransformer3DModel
118
+
119
+ >>> transformer = CogVideoXTransformer3DModel.from_pretrained(
120
+ ... model_id, subfolder="transformer", torch_dtype=torch.bfloat16
121
+ ... )
122
+
123
+ >>> apply_layerwise_casting(
124
+ ... transformer,
125
+ ... storage_dtype=torch.float8_e4m3fn,
126
+ ... compute_dtype=torch.bfloat16,
127
+ ... skip_modules_pattern=["patch_embed", "norm", "proj_out"],
128
+ ... non_blocking=True,
129
+ ... )
130
+ ```
131
+
132
+ Args:
133
+ module (`torch.nn.Module`):
134
+ The module whose leaf modules will be cast to a high precision dtype for computation, and to a low
135
+ precision dtype for storage.
136
+ storage_dtype (`torch.dtype`):
137
+ The dtype to cast the module to before/after the forward pass for storage.
138
+ compute_dtype (`torch.dtype`):
139
+ The dtype to cast the module to during the forward pass for computation.
140
+ skip_modules_pattern (`tuple[str, ...]`, defaults to `"auto"`):
141
+ A list of patterns to match the names of the modules to skip during the layerwise casting process. If set
142
+ to `"auto"`, the default patterns are used. If set to `None`, no modules are skipped. If set to `None`
143
+ alongside `skip_modules_classes` being `None`, the layerwise casting is applied directly to the module
144
+ instead of its internal submodules.
145
+ skip_modules_classes (`tuple[Type[torch.nn.Module], ...]`, defaults to `None`):
146
+ A list of module classes to skip during the layerwise casting process.
147
+ non_blocking (`bool`, defaults to `False`):
148
+ If `True`, the weight casting operations are non-blocking.
149
+ """
150
+ if skip_modules_pattern == "auto":
151
+ skip_modules_pattern = DEFAULT_SKIP_MODULES_PATTERN
152
+
153
+ if skip_modules_classes is None and skip_modules_pattern is None:
154
+ apply_layerwise_casting_hook(module, storage_dtype, compute_dtype, non_blocking)
155
+ return
156
+
157
+ _apply_layerwise_casting(
158
+ module,
159
+ storage_dtype,
160
+ compute_dtype,
161
+ skip_modules_pattern,
162
+ skip_modules_classes,
163
+ non_blocking,
164
+ )
165
+ _disable_peft_input_autocast(module)
166
+
167
+
168
+ def _apply_layerwise_casting(
169
+ module: torch.nn.Module,
170
+ storage_dtype: torch.dtype,
171
+ compute_dtype: torch.dtype,
172
+ skip_modules_pattern: tuple[str, ...] | None = None,
173
+ skip_modules_classes: tuple[Type[torch.nn.Module], ...] | None = None,
174
+ non_blocking: bool = False,
175
+ _prefix: str = "",
176
+ ) -> None:
177
+ should_skip = (skip_modules_classes is not None and isinstance(module, skip_modules_classes)) or (
178
+ skip_modules_pattern is not None and any(re.search(pattern, _prefix) for pattern in skip_modules_pattern)
179
+ )
180
+ if should_skip:
181
+ logger.debug(f'Skipping layerwise casting for layer "{_prefix}"')
182
+ return
183
+
184
+ if isinstance(module, _GO_LC_SUPPORTED_PYTORCH_LAYERS):
185
+ logger.debug(f'Applying layerwise casting to layer "{_prefix}"')
186
+ apply_layerwise_casting_hook(module, storage_dtype, compute_dtype, non_blocking)
187
+ return
188
+
189
+ for name, submodule in module.named_children():
190
+ layer_name = f"{_prefix}.{name}" if _prefix else name
191
+ _apply_layerwise_casting(
192
+ submodule,
193
+ storage_dtype,
194
+ compute_dtype,
195
+ skip_modules_pattern,
196
+ skip_modules_classes,
197
+ non_blocking,
198
+ _prefix=layer_name,
199
+ )
200
+
201
+
202
+ def apply_layerwise_casting_hook(
203
+ module: torch.nn.Module, storage_dtype: torch.dtype, compute_dtype: torch.dtype, non_blocking: bool
204
+ ) -> None:
205
+ r"""
206
+ Applies a `LayerwiseCastingHook` to a given module.
207
+
208
+ Args:
209
+ module (`torch.nn.Module`):
210
+ The module to attach the hook to.
211
+ storage_dtype (`torch.dtype`):
212
+ The dtype to cast the module to before the forward pass.
213
+ compute_dtype (`torch.dtype`):
214
+ The dtype to cast the module to during the forward pass.
215
+ non_blocking (`bool`):
216
+ If `True`, the weight casting operations are non-blocking.
217
+ """
218
+ registry = HookRegistry.check_if_exists_or_initialize(module)
219
+ hook = LayerwiseCastingHook(storage_dtype, compute_dtype, non_blocking)
220
+ registry.register_hook(hook, _LAYERWISE_CASTING_HOOK)
221
+
222
+
223
+ def _is_layerwise_casting_active(module: torch.nn.Module) -> bool:
224
+ for submodule in module.modules():
225
+ if (
226
+ hasattr(submodule, "_diffusers_hook")
227
+ and submodule._diffusers_hook.get_hook(_LAYERWISE_CASTING_HOOK) is not None
228
+ ):
229
+ return True
230
+ return False
231
+
232
+
233
+ def _disable_peft_input_autocast(module: torch.nn.Module) -> None:
234
+ if not _SHOULD_DISABLE_PEFT_INPUT_AUTOCAST:
235
+ return
236
+ for submodule in module.modules():
237
+ if isinstance(submodule, BaseTunerLayer) and _is_layerwise_casting_active(submodule):
238
+ registry = HookRegistry.check_if_exists_or_initialize(submodule)
239
+ hook = PeftInputAutocastDisableHook()
240
+ registry.register_hook(hook, _PEFT_AUTOCAST_DISABLE_HOOK)
diffusers/hooks/mag_cache.py ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from dataclasses import dataclass
16
+ from typing import List, Optional, Tuple, Union
17
+
18
+ import torch
19
+
20
+ from ..utils import get_logger
21
+ from ..utils.torch_utils import unwrap_module
22
+ from ._common import _ALL_TRANSFORMER_BLOCK_IDENTIFIERS
23
+ from ._helpers import TransformerBlockRegistry
24
+ from .hooks import BaseState, HookRegistry, ModelHook, StateManager
25
+
26
+
27
+ logger = get_logger(__name__) # pylint: disable=invalid-name
28
+
29
+ _MAG_CACHE_LEADER_BLOCK_HOOK = "mag_cache_leader_block_hook"
30
+ _MAG_CACHE_BLOCK_HOOK = "mag_cache_block_hook"
31
+
32
+ # Default Mag Ratios for Flux models (Dev/Schnell) are provided for convenience.
33
+ # Users must explicitly pass these to the config if using Flux.
34
+ # Reference: https://github.com/Zehong-Ma/MagCache
35
+ FLUX_MAG_RATIOS = torch.tensor(
36
+ [1.0]
37
+ + [
38
+ 1.21094,
39
+ 1.11719,
40
+ 1.07812,
41
+ 1.0625,
42
+ 1.03906,
43
+ 1.03125,
44
+ 1.03906,
45
+ 1.02344,
46
+ 1.03125,
47
+ 1.02344,
48
+ 0.98047,
49
+ 1.01562,
50
+ 1.00781,
51
+ 1.0,
52
+ 1.00781,
53
+ 1.0,
54
+ 1.00781,
55
+ 1.0,
56
+ 1.0,
57
+ 0.99609,
58
+ 0.99609,
59
+ 0.98047,
60
+ 0.98828,
61
+ 0.96484,
62
+ 0.95703,
63
+ 0.93359,
64
+ 0.89062,
65
+ ]
66
+ )
67
+
68
+
69
+ def nearest_interp(src_array: torch.Tensor, target_length: int) -> torch.Tensor:
70
+ """
71
+ Interpolate the source array to the target length using nearest neighbor interpolation.
72
+ """
73
+ src_length = len(src_array)
74
+ if target_length == 1:
75
+ return src_array[-1:]
76
+
77
+ scale = (src_length - 1) / (target_length - 1)
78
+ grid = torch.arange(target_length, device=src_array.device, dtype=torch.float32)
79
+ mapped_indices = torch.round(grid * scale).long()
80
+ return src_array[mapped_indices]
81
+
82
+
83
+ @dataclass
84
+ class MagCacheConfig:
85
+ r"""
86
+ Configuration for [MagCache](https://github.com/Zehong-Ma/MagCache).
87
+
88
+ Args:
89
+ threshold (`float`, defaults to `0.06`):
90
+ The threshold for the accumulated error. If the accumulated error is below this threshold, the block
91
+ computation is skipped. A higher threshold allows for more aggressive skipping (faster) but may degrade
92
+ quality.
93
+ max_skip_steps (`int`, defaults to `3`):
94
+ The maximum number of consecutive steps that can be skipped (K in the paper).
95
+ retention_ratio (`float`, defaults to `0.2`):
96
+ The fraction of initial steps during which skipping is disabled to ensure stability. For example, if
97
+ `num_inference_steps` is 28 and `retention_ratio` is 0.2, the first 6 steps will never be skipped.
98
+ num_inference_steps (`int`, defaults to `28`):
99
+ The number of inference steps used in the pipeline. This is required to interpolate `mag_ratios` correctly.
100
+ mag_ratios (`torch.Tensor`, *optional*):
101
+ The pre-computed magnitude ratios for the model. These are checkpoint-dependent. If not provided, you must
102
+ set `calibrate=True` to calculate them for your specific model. For Flux models, you can use
103
+ `diffusers.hooks.mag_cache.FLUX_MAG_RATIOS`.
104
+ calibrate (`bool`, defaults to `False`):
105
+ If True, enables calibration mode. In this mode, no blocks are skipped. Instead, the hook calculates the
106
+ magnitude ratios for the current run and logs them at the end. Use this to obtain `mag_ratios` for new
107
+ models or schedulers.
108
+ """
109
+
110
+ threshold: float = 0.06
111
+ max_skip_steps: int = 3
112
+ retention_ratio: float = 0.2
113
+ num_inference_steps: int = 28
114
+ mag_ratios: Optional[Union[torch.Tensor, List[float]]] = None
115
+ calibrate: bool = False
116
+
117
+ def __post_init__(self):
118
+ # User MUST provide ratios OR enable calibration.
119
+ if self.mag_ratios is None and not self.calibrate:
120
+ raise ValueError(
121
+ " `mag_ratios` must be provided for MagCache inference because these ratios are model-dependent.\n"
122
+ "To get them for your model:\n"
123
+ "1. Initialize `MagCacheConfig(calibrate=True, ...)`\n"
124
+ "2. Run inference on your model once.\n"
125
+ "3. Copy the printed ratios array and pass it to `mag_ratios` in the config.\n"
126
+ "For Flux models, you can import `FLUX_MAG_RATIOS` from `diffusers.hooks.mag_cache`."
127
+ )
128
+
129
+ if not self.calibrate and self.mag_ratios is not None:
130
+ if not torch.is_tensor(self.mag_ratios):
131
+ self.mag_ratios = torch.tensor(self.mag_ratios)
132
+
133
+ if len(self.mag_ratios) != self.num_inference_steps:
134
+ logger.debug(
135
+ f"Interpolating mag_ratios from length {len(self.mag_ratios)} to {self.num_inference_steps}"
136
+ )
137
+ self.mag_ratios = nearest_interp(self.mag_ratios, self.num_inference_steps)
138
+
139
+
140
+ class MagCacheState(BaseState):
141
+ def __init__(self) -> None:
142
+ super().__init__()
143
+ # Cache for the residual (output - input) from the *previous* timestep
144
+ self.previous_residual: torch.Tensor = None
145
+
146
+ # State inputs/outputs for the current forward pass
147
+ self.head_block_input: Union[torch.Tensor, Tuple[torch.Tensor, ...]] = None
148
+ self.should_compute: bool = True
149
+
150
+ # MagCache accumulators
151
+ self.accumulated_ratio: float = 1.0
152
+ self.accumulated_err: float = 0.0
153
+ self.accumulated_steps: int = 0
154
+
155
+ # Current step counter (timestep index)
156
+ self.step_index: int = 0
157
+
158
+ # Calibration storage
159
+ self.calibration_ratios: List[float] = []
160
+
161
+ def reset(self):
162
+ self.previous_residual = None
163
+ self.should_compute = True
164
+ self.accumulated_ratio = 1.0
165
+ self.accumulated_err = 0.0
166
+ self.accumulated_steps = 0
167
+ self.step_index = 0
168
+ self.calibration_ratios = []
169
+
170
+
171
+ class MagCacheHeadHook(ModelHook):
172
+ _is_stateful = True
173
+
174
+ def __init__(self, state_manager: StateManager, config: MagCacheConfig):
175
+ self.state_manager = state_manager
176
+ self.config = config
177
+ self._metadata = None
178
+
179
+ def initialize_hook(self, module):
180
+ unwrapped_module = unwrap_module(module)
181
+ self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
182
+ return module
183
+
184
+ @torch.compiler.disable
185
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
186
+ if self.state_manager._current_context is None:
187
+ self.state_manager.set_context("inference")
188
+
189
+ arg_name = self._metadata.hidden_states_argument_name
190
+ hidden_states = self._metadata._get_parameter_from_args_kwargs(arg_name, args, kwargs)
191
+
192
+ state: MagCacheState = self.state_manager.get_state()
193
+ state.head_block_input = hidden_states
194
+
195
+ should_compute = True
196
+
197
+ if self.config.calibrate:
198
+ # Never skip during calibration
199
+ should_compute = True
200
+ else:
201
+ # MagCache Logic
202
+ current_step = state.step_index
203
+ if current_step >= len(self.config.mag_ratios):
204
+ current_scale = 1.0
205
+ else:
206
+ current_scale = self.config.mag_ratios[current_step]
207
+
208
+ retention_step = int(self.config.retention_ratio * self.config.num_inference_steps + 0.5)
209
+
210
+ if current_step >= retention_step:
211
+ state.accumulated_ratio *= current_scale
212
+ state.accumulated_steps += 1
213
+ state.accumulated_err += abs(1.0 - state.accumulated_ratio)
214
+
215
+ if (
216
+ state.previous_residual is not None
217
+ and state.accumulated_err <= self.config.threshold
218
+ and state.accumulated_steps <= self.config.max_skip_steps
219
+ ):
220
+ should_compute = False
221
+ else:
222
+ state.accumulated_ratio = 1.0
223
+ state.accumulated_steps = 0
224
+ state.accumulated_err = 0.0
225
+
226
+ state.should_compute = should_compute
227
+
228
+ if not should_compute:
229
+ logger.debug(f"MagCache: Skipping step {state.step_index}")
230
+ # Apply MagCache: Output = Input + Previous Residual
231
+
232
+ output = hidden_states
233
+ res = state.previous_residual
234
+
235
+ if res.device != output.device:
236
+ res = res.to(output.device)
237
+
238
+ # Attempt to apply residual handling shape mismatches (e.g., text+image vs image only)
239
+ if res.shape == output.shape:
240
+ output = output + res
241
+ elif (
242
+ output.ndim == 3
243
+ and res.ndim == 3
244
+ and output.shape[0] == res.shape[0]
245
+ and output.shape[2] == res.shape[2]
246
+ ):
247
+ # Assuming concatenation where image part is at the end (standard in Flux/SD3)
248
+ diff = output.shape[1] - res.shape[1]
249
+ if diff > 0:
250
+ output = output.clone()
251
+ output[:, diff:, :] = output[:, diff:, :] + res
252
+ else:
253
+ logger.warning(
254
+ f"MagCache: Dimension mismatch. Input {output.shape}, Residual {res.shape}. "
255
+ "Cannot apply residual safely. Returning input without residual."
256
+ )
257
+ else:
258
+ logger.warning(
259
+ f"MagCache: Dimension mismatch. Input {output.shape}, Residual {res.shape}. "
260
+ "Cannot apply residual safely. Returning input without residual."
261
+ )
262
+
263
+ if self._metadata.return_encoder_hidden_states_index is not None:
264
+ original_encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
265
+ "encoder_hidden_states", args, kwargs
266
+ )
267
+ max_idx = max(
268
+ self._metadata.return_hidden_states_index, self._metadata.return_encoder_hidden_states_index
269
+ )
270
+ ret_list = [None] * (max_idx + 1)
271
+ ret_list[self._metadata.return_hidden_states_index] = output
272
+ ret_list[self._metadata.return_encoder_hidden_states_index] = original_encoder_hidden_states
273
+ return tuple(ret_list)
274
+ else:
275
+ return output
276
+
277
+ else:
278
+ # Compute original forward
279
+ output = self.fn_ref.original_forward(*args, **kwargs)
280
+ return output
281
+
282
+ def reset_state(self, module):
283
+ self.state_manager.reset()
284
+ return module
285
+
286
+
287
+ class MagCacheBlockHook(ModelHook):
288
+ def __init__(self, state_manager: StateManager, is_tail: bool = False, config: MagCacheConfig = None):
289
+ super().__init__()
290
+ self.state_manager = state_manager
291
+ self.is_tail = is_tail
292
+ self.config = config
293
+ self._metadata = None
294
+
295
+ def initialize_hook(self, module):
296
+ unwrapped_module = unwrap_module(module)
297
+ self._metadata = TransformerBlockRegistry.get(unwrapped_module.__class__)
298
+ return module
299
+
300
+ @torch.compiler.disable
301
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
302
+ if self.state_manager._current_context is None:
303
+ self.state_manager.set_context("inference")
304
+ state: MagCacheState = self.state_manager.get_state()
305
+
306
+ if not state.should_compute:
307
+ arg_name = self._metadata.hidden_states_argument_name
308
+ hidden_states = self._metadata._get_parameter_from_args_kwargs(arg_name, args, kwargs)
309
+
310
+ if self.is_tail:
311
+ # Still need to advance step index even if we skip
312
+ self._advance_step(state)
313
+
314
+ if self._metadata.return_encoder_hidden_states_index is not None:
315
+ encoder_hidden_states = self._metadata._get_parameter_from_args_kwargs(
316
+ "encoder_hidden_states", args, kwargs
317
+ )
318
+ max_idx = max(
319
+ self._metadata.return_hidden_states_index, self._metadata.return_encoder_hidden_states_index
320
+ )
321
+ ret_list = [None] * (max_idx + 1)
322
+ ret_list[self._metadata.return_hidden_states_index] = hidden_states
323
+ ret_list[self._metadata.return_encoder_hidden_states_index] = encoder_hidden_states
324
+ return tuple(ret_list)
325
+
326
+ return hidden_states
327
+
328
+ output = self.fn_ref.original_forward(*args, **kwargs)
329
+
330
+ if self.is_tail:
331
+ # Calculate residual for next steps
332
+ if isinstance(output, tuple):
333
+ out_hidden = output[self._metadata.return_hidden_states_index]
334
+ else:
335
+ out_hidden = output
336
+
337
+ in_hidden = state.head_block_input
338
+
339
+ if in_hidden is None:
340
+ return output
341
+
342
+ # Determine residual
343
+ if out_hidden.shape == in_hidden.shape:
344
+ residual = out_hidden - in_hidden
345
+ elif out_hidden.ndim == 3 and in_hidden.ndim == 3 and out_hidden.shape[2] == in_hidden.shape[2]:
346
+ diff = in_hidden.shape[1] - out_hidden.shape[1]
347
+ if diff == 0:
348
+ residual = out_hidden - in_hidden
349
+ else:
350
+ residual = out_hidden - in_hidden # Fallback to matching tail
351
+ else:
352
+ # Fallback for completely mismatched shapes
353
+ residual = out_hidden
354
+
355
+ if self.config.calibrate:
356
+ self._perform_calibration_step(state, residual)
357
+
358
+ state.previous_residual = residual
359
+ self._advance_step(state)
360
+
361
+ return output
362
+
363
+ def _perform_calibration_step(self, state: MagCacheState, current_residual: torch.Tensor):
364
+ if state.previous_residual is None:
365
+ # First step has no previous residual to compare against.
366
+ # log 1.0 as a neutral starting point.
367
+ ratio = 1.0
368
+ else:
369
+ # MagCache Calibration Formula: mean(norm(curr) / norm(prev))
370
+ # norm(dim=-1) gives magnitude of each token vector
371
+ curr_norm = torch.linalg.norm(current_residual.float(), dim=-1)
372
+ prev_norm = torch.linalg.norm(state.previous_residual.float(), dim=-1)
373
+
374
+ # Avoid division by zero
375
+ ratio = (curr_norm / (prev_norm + 1e-8)).mean().item()
376
+
377
+ state.calibration_ratios.append(ratio)
378
+
379
+ def _advance_step(self, state: MagCacheState):
380
+ state.step_index += 1
381
+ if state.step_index >= self.config.num_inference_steps:
382
+ # End of inference loop
383
+ if self.config.calibrate:
384
+ print("\n[MagCache] Calibration Complete. Copy these values to MagCacheConfig(mag_ratios=...):")
385
+ print(f"{state.calibration_ratios}\n")
386
+ logger.info(f"MagCache Calibration Results: {state.calibration_ratios}")
387
+
388
+ # Reset state
389
+ state.step_index = 0
390
+ state.accumulated_ratio = 1.0
391
+ state.accumulated_steps = 0
392
+ state.accumulated_err = 0.0
393
+ state.previous_residual = None
394
+ state.calibration_ratios = []
395
+
396
+
397
+ def apply_mag_cache(module: torch.nn.Module, config: MagCacheConfig) -> None:
398
+ """
399
+ Applies MagCache to a given module (typically a Transformer).
400
+
401
+ Args:
402
+ module (`torch.nn.Module`):
403
+ The module to apply MagCache to.
404
+ config (`MagCacheConfig`):
405
+ The configuration for MagCache.
406
+ """
407
+ # Initialize registry on the root module so the Pipeline can set context.
408
+ HookRegistry.check_if_exists_or_initialize(module)
409
+
410
+ state_manager = StateManager(MagCacheState, (), {})
411
+ remaining_blocks = []
412
+
413
+ for name, submodule in module.named_children():
414
+ if name not in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS or not isinstance(submodule, torch.nn.ModuleList):
415
+ continue
416
+ for index, block in enumerate(submodule):
417
+ remaining_blocks.append((f"{name}.{index}", block))
418
+
419
+ if not remaining_blocks:
420
+ logger.warning("MagCache: No transformer blocks found to apply hooks.")
421
+ return
422
+
423
+ # Handle single-block models
424
+ if len(remaining_blocks) == 1:
425
+ name, block = remaining_blocks[0]
426
+ logger.info(f"MagCache: Applying Head+Tail Hooks to single block '{name}'")
427
+ _apply_mag_cache_block_hook(block, state_manager, config, is_tail=True)
428
+ _apply_mag_cache_head_hook(block, state_manager, config)
429
+ return
430
+
431
+ head_block_name, head_block = remaining_blocks.pop(0)
432
+ tail_block_name, tail_block = remaining_blocks.pop(-1)
433
+
434
+ logger.info(f"MagCache: Applying Head Hook to {head_block_name}")
435
+ _apply_mag_cache_head_hook(head_block, state_manager, config)
436
+
437
+ for name, block in remaining_blocks:
438
+ _apply_mag_cache_block_hook(block, state_manager, config)
439
+
440
+ logger.info(f"MagCache: Applying Tail Hook to {tail_block_name}")
441
+ _apply_mag_cache_block_hook(tail_block, state_manager, config, is_tail=True)
442
+
443
+
444
+ def _apply_mag_cache_head_hook(block: torch.nn.Module, state_manager: StateManager, config: MagCacheConfig) -> None:
445
+ registry = HookRegistry.check_if_exists_or_initialize(block)
446
+
447
+ # Automatically remove existing hook to allow re-application (e.g. switching modes)
448
+ if registry.get_hook(_MAG_CACHE_LEADER_BLOCK_HOOK) is not None:
449
+ registry.remove_hook(_MAG_CACHE_LEADER_BLOCK_HOOK)
450
+
451
+ hook = MagCacheHeadHook(state_manager, config)
452
+ registry.register_hook(hook, _MAG_CACHE_LEADER_BLOCK_HOOK)
453
+
454
+
455
+ def _apply_mag_cache_block_hook(
456
+ block: torch.nn.Module,
457
+ state_manager: StateManager,
458
+ config: MagCacheConfig,
459
+ is_tail: bool = False,
460
+ ) -> None:
461
+ registry = HookRegistry.check_if_exists_or_initialize(block)
462
+
463
+ # Automatically remove existing hook to allow re-application
464
+ if registry.get_hook(_MAG_CACHE_BLOCK_HOOK) is not None:
465
+ registry.remove_hook(_MAG_CACHE_BLOCK_HOOK)
466
+
467
+ hook = MagCacheBlockHook(state_manager, is_tail, config)
468
+ registry.register_hook(hook, _MAG_CACHE_BLOCK_HOOK)
diffusers/hooks/pyramid_attention_broadcast.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+ from dataclasses import dataclass
17
+ from typing import Any, Callable
18
+
19
+ import torch
20
+
21
+ from ..models.attention import AttentionModuleMixin
22
+ from ..models.attention_processor import Attention, MochiAttention
23
+ from ..utils import logging
24
+ from ._common import (
25
+ _ATTENTION_CLASSES,
26
+ _CROSS_TRANSFORMER_BLOCK_IDENTIFIERS,
27
+ _SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS,
28
+ _TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS,
29
+ )
30
+ from .hooks import HookRegistry, ModelHook
31
+
32
+
33
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
34
+
35
+
36
+ _PYRAMID_ATTENTION_BROADCAST_HOOK = "pyramid_attention_broadcast"
37
+
38
+
39
+ @dataclass
40
+ class PyramidAttentionBroadcastConfig:
41
+ r"""
42
+ Configuration for Pyramid Attention Broadcast.
43
+
44
+ Args:
45
+ spatial_attention_block_skip_range (`int`, *optional*, defaults to `None`):
46
+ The number of times a specific spatial attention broadcast is skipped before computing the attention states
47
+ to re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times (i.e.,
48
+ old attention states will be reused) before computing the new attention states again.
49
+ temporal_attention_block_skip_range (`int`, *optional*, defaults to `None`):
50
+ The number of times a specific temporal attention broadcast is skipped before computing the attention
51
+ states to re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times
52
+ (i.e., old attention states will be reused) before computing the new attention states again.
53
+ cross_attention_block_skip_range (`int`, *optional*, defaults to `None`):
54
+ The number of times a specific cross-attention broadcast is skipped before computing the attention states
55
+ to re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times (i.e.,
56
+ old attention states will be reused) before computing the new attention states again.
57
+ spatial_attention_timestep_skip_range (`tuple[int, int]`, defaults to `(100, 800)`):
58
+ The range of timesteps to skip in the spatial attention layer. The attention computations will be
59
+ conditionally skipped if the current timestep is within the specified range.
60
+ temporal_attention_timestep_skip_range (`tuple[int, int]`, defaults to `(100, 800)`):
61
+ The range of timesteps to skip in the temporal attention layer. The attention computations will be
62
+ conditionally skipped if the current timestep is within the specified range.
63
+ cross_attention_timestep_skip_range (`tuple[int, int]`, defaults to `(100, 800)`):
64
+ The range of timesteps to skip in the cross-attention layer. The attention computations will be
65
+ conditionally skipped if the current timestep is within the specified range.
66
+ spatial_attention_block_identifiers (`tuple[str, ...]`):
67
+ The identifiers to match against the layer names to determine if the layer is a spatial attention layer.
68
+ temporal_attention_block_identifiers (`tuple[str, ...]`):
69
+ The identifiers to match against the layer names to determine if the layer is a temporal attention layer.
70
+ cross_attention_block_identifiers (`tuple[str, ...]`):
71
+ The identifiers to match against the layer names to determine if the layer is a cross-attention layer.
72
+ """
73
+
74
+ spatial_attention_block_skip_range: int | None = None
75
+ temporal_attention_block_skip_range: int | None = None
76
+ cross_attention_block_skip_range: int | None = None
77
+
78
+ spatial_attention_timestep_skip_range: tuple[int, int] = (100, 800)
79
+ temporal_attention_timestep_skip_range: tuple[int, int] = (100, 800)
80
+ cross_attention_timestep_skip_range: tuple[int, int] = (100, 800)
81
+
82
+ spatial_attention_block_identifiers: tuple[str, ...] = _SPATIAL_TRANSFORMER_BLOCK_IDENTIFIERS
83
+ temporal_attention_block_identifiers: tuple[str, ...] = _TEMPORAL_TRANSFORMER_BLOCK_IDENTIFIERS
84
+ cross_attention_block_identifiers: tuple[str, ...] = _CROSS_TRANSFORMER_BLOCK_IDENTIFIERS
85
+
86
+ current_timestep_callback: Callable[[], int] = None
87
+
88
+ # TODO(aryan): add PAB for MLP layers (very limited speedup from testing with original codebase
89
+ # so not added for now)
90
+
91
+ def __repr__(self) -> str:
92
+ return (
93
+ f"PyramidAttentionBroadcastConfig(\n"
94
+ f" spatial_attention_block_skip_range={self.spatial_attention_block_skip_range},\n"
95
+ f" temporal_attention_block_skip_range={self.temporal_attention_block_skip_range},\n"
96
+ f" cross_attention_block_skip_range={self.cross_attention_block_skip_range},\n"
97
+ f" spatial_attention_timestep_skip_range={self.spatial_attention_timestep_skip_range},\n"
98
+ f" temporal_attention_timestep_skip_range={self.temporal_attention_timestep_skip_range},\n"
99
+ f" cross_attention_timestep_skip_range={self.cross_attention_timestep_skip_range},\n"
100
+ f" spatial_attention_block_identifiers={self.spatial_attention_block_identifiers},\n"
101
+ f" temporal_attention_block_identifiers={self.temporal_attention_block_identifiers},\n"
102
+ f" cross_attention_block_identifiers={self.cross_attention_block_identifiers},\n"
103
+ f" current_timestep_callback={self.current_timestep_callback}\n"
104
+ ")"
105
+ )
106
+
107
+
108
+ class PyramidAttentionBroadcastState:
109
+ r"""
110
+ State for Pyramid Attention Broadcast.
111
+
112
+ Attributes:
113
+ iteration (`int`):
114
+ The current iteration of the Pyramid Attention Broadcast. It is necessary to ensure that `reset_state` is
115
+ called before starting a new inference forward pass for PAB to work correctly.
116
+ cache (`Any`):
117
+ The cached output from the previous forward pass. This is used to re-use the attention states when the
118
+ attention computation is skipped. It is either a tensor or a tuple of tensors, depending on the module.
119
+ """
120
+
121
+ def __init__(self) -> None:
122
+ self.iteration = 0
123
+ self.cache = None
124
+
125
+ def reset(self):
126
+ self.iteration = 0
127
+ self.cache = None
128
+
129
+ def __repr__(self):
130
+ cache_repr = ""
131
+ if self.cache is None:
132
+ cache_repr = "None"
133
+ else:
134
+ cache_repr = f"Tensor(shape={self.cache.shape}, dtype={self.cache.dtype})"
135
+ return f"PyramidAttentionBroadcastState(iteration={self.iteration}, cache={cache_repr})"
136
+
137
+
138
+ class PyramidAttentionBroadcastHook(ModelHook):
139
+ r"""A hook that applies Pyramid Attention Broadcast to a given module."""
140
+
141
+ _is_stateful = True
142
+
143
+ def __init__(
144
+ self, timestep_skip_range: tuple[int, int], block_skip_range: int, current_timestep_callback: Callable[[], int]
145
+ ) -> None:
146
+ super().__init__()
147
+
148
+ self.timestep_skip_range = timestep_skip_range
149
+ self.block_skip_range = block_skip_range
150
+ self.current_timestep_callback = current_timestep_callback
151
+
152
+ def initialize_hook(self, module):
153
+ self.state = PyramidAttentionBroadcastState()
154
+ return module
155
+
156
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs) -> Any:
157
+ is_within_timestep_range = (
158
+ self.timestep_skip_range[0] < self.current_timestep_callback() < self.timestep_skip_range[1]
159
+ )
160
+ should_compute_attention = (
161
+ self.state.cache is None
162
+ or self.state.iteration == 0
163
+ or not is_within_timestep_range
164
+ or self.state.iteration % self.block_skip_range == 0
165
+ )
166
+
167
+ if should_compute_attention:
168
+ output = self.fn_ref.original_forward(*args, **kwargs)
169
+ else:
170
+ output = self.state.cache
171
+
172
+ self.state.cache = output
173
+ self.state.iteration += 1
174
+ return output
175
+
176
+ def reset_state(self, module: torch.nn.Module) -> None:
177
+ self.state.reset()
178
+ return module
179
+
180
+
181
+ def apply_pyramid_attention_broadcast(module: torch.nn.Module, config: PyramidAttentionBroadcastConfig):
182
+ r"""
183
+ Apply [Pyramid Attention Broadcast](https://huggingface.co/papers/2408.12588) to a given pipeline.
184
+
185
+ PAB is an attention approximation method that leverages the similarity in attention states between timesteps to
186
+ reduce the computational cost of attention computation. The key takeaway from the paper is that the attention
187
+ similarity in the cross-attention layers between timesteps is high, followed by less similarity in the temporal and
188
+ spatial layers. This allows for the skipping of attention computation in the cross-attention layers more frequently
189
+ than in the temporal and spatial layers. Applying PAB will, therefore, speedup the inference process.
190
+
191
+ Args:
192
+ module (`torch.nn.Module`):
193
+ The module to apply Pyramid Attention Broadcast to.
194
+ config (`PyramidAttentionBroadcastConfig | None`, `optional`, defaults to `None`):
195
+ The configuration to use for Pyramid Attention Broadcast.
196
+
197
+ Example:
198
+
199
+ ```python
200
+ >>> import torch
201
+ >>> from diffusers import CogVideoXPipeline, PyramidAttentionBroadcastConfig, apply_pyramid_attention_broadcast
202
+ >>> from diffusers.utils import export_to_video
203
+
204
+ >>> pipe = CogVideoXPipeline.from_pretrained("THUDM/CogVideoX-5b", torch_dtype=torch.bfloat16)
205
+ >>> pipe.to("cuda")
206
+
207
+ >>> config = PyramidAttentionBroadcastConfig(
208
+ ... spatial_attention_block_skip_range=2,
209
+ ... spatial_attention_timestep_skip_range=(100, 800),
210
+ ... current_timestep_callback=lambda: pipe.current_timestep,
211
+ ... )
212
+ >>> apply_pyramid_attention_broadcast(pipe.transformer, config)
213
+ ```
214
+ """
215
+ if config.current_timestep_callback is None:
216
+ raise ValueError(
217
+ "The `current_timestep_callback` function must be provided in the configuration to apply Pyramid Attention Broadcast."
218
+ )
219
+
220
+ if (
221
+ config.spatial_attention_block_skip_range is None
222
+ and config.temporal_attention_block_skip_range is None
223
+ and config.cross_attention_block_skip_range is None
224
+ ):
225
+ logger.warning(
226
+ "Pyramid Attention Broadcast requires one or more of `spatial_attention_block_skip_range`, `temporal_attention_block_skip_range` "
227
+ "or `cross_attention_block_skip_range` parameters to be set to an integer, not `None`. Defaulting to using `spatial_attention_block_skip_range=2`. "
228
+ "To avoid this warning, please set one of the above parameters."
229
+ )
230
+ config.spatial_attention_block_skip_range = 2
231
+
232
+ for name, submodule in module.named_modules():
233
+ if not isinstance(submodule, (*_ATTENTION_CLASSES, AttentionModuleMixin)):
234
+ # PAB has been implemented specific to Diffusers' Attention classes. However, this does not mean that PAB
235
+ # cannot be applied to this layer. For custom layers, users can extend this functionality and implement
236
+ # their own PAB logic similar to `_apply_pyramid_attention_broadcast_on_attention_class`.
237
+ continue
238
+ _apply_pyramid_attention_broadcast_on_attention_class(name, submodule, config)
239
+
240
+
241
+ def _apply_pyramid_attention_broadcast_on_attention_class(
242
+ name: str, module: Attention, config: PyramidAttentionBroadcastConfig
243
+ ) -> bool:
244
+ is_spatial_self_attention = (
245
+ any(re.search(identifier, name) is not None for identifier in config.spatial_attention_block_identifiers)
246
+ and config.spatial_attention_block_skip_range is not None
247
+ and not getattr(module, "is_cross_attention", False)
248
+ )
249
+ is_temporal_self_attention = (
250
+ any(re.search(identifier, name) is not None for identifier in config.temporal_attention_block_identifiers)
251
+ and config.temporal_attention_block_skip_range is not None
252
+ and not getattr(module, "is_cross_attention", False)
253
+ )
254
+ is_cross_attention = (
255
+ any(re.search(identifier, name) is not None for identifier in config.cross_attention_block_identifiers)
256
+ and config.cross_attention_block_skip_range is not None
257
+ and getattr(module, "is_cross_attention", False)
258
+ )
259
+
260
+ block_skip_range, timestep_skip_range, block_type = None, None, None
261
+ if is_spatial_self_attention:
262
+ block_skip_range = config.spatial_attention_block_skip_range
263
+ timestep_skip_range = config.spatial_attention_timestep_skip_range
264
+ block_type = "spatial"
265
+ elif is_temporal_self_attention:
266
+ block_skip_range = config.temporal_attention_block_skip_range
267
+ timestep_skip_range = config.temporal_attention_timestep_skip_range
268
+ block_type = "temporal"
269
+ elif is_cross_attention:
270
+ block_skip_range = config.cross_attention_block_skip_range
271
+ timestep_skip_range = config.cross_attention_timestep_skip_range
272
+ block_type = "cross"
273
+
274
+ if block_skip_range is None or timestep_skip_range is None:
275
+ logger.info(
276
+ f'Unable to apply Pyramid Attention Broadcast to the selected layer: "{name}" because it does '
277
+ f"not match any of the required criteria for spatial, temporal or cross attention layers. Note, "
278
+ f"however, that this layer may still be valid for applying PAB. Please specify the correct "
279
+ f"block identifiers in the configuration."
280
+ )
281
+ return False
282
+
283
+ logger.debug(f"Enabling Pyramid Attention Broadcast ({block_type}) in layer: {name}")
284
+ _apply_pyramid_attention_broadcast_hook(
285
+ module, timestep_skip_range, block_skip_range, config.current_timestep_callback
286
+ )
287
+ return True
288
+
289
+
290
+ def _apply_pyramid_attention_broadcast_hook(
291
+ module: Attention | MochiAttention,
292
+ timestep_skip_range: tuple[int, int],
293
+ block_skip_range: int,
294
+ current_timestep_callback: Callable[[], int],
295
+ ):
296
+ r"""
297
+ Apply [Pyramid Attention Broadcast](https://huggingface.co/papers/2408.12588) to a given torch.nn.Module.
298
+
299
+ Args:
300
+ module (`torch.nn.Module`):
301
+ The module to apply Pyramid Attention Broadcast to.
302
+ timestep_skip_range (`tuple[int, int]`):
303
+ The range of timesteps to skip in the attention layer. The attention computations will be conditionally
304
+ skipped if the current timestep is within the specified range.
305
+ block_skip_range (`int`):
306
+ The number of times a specific attention broadcast is skipped before computing the attention states to
307
+ re-use. If this is set to the value `N`, the attention computation will be skipped `N - 1` times (i.e., old
308
+ attention states will be reused) before computing the new attention states again.
309
+ current_timestep_callback (`Callable[[], int]`):
310
+ A callback function that returns the current inference timestep.
311
+ """
312
+ registry = HookRegistry.check_if_exists_or_initialize(module)
313
+ hook = PyramidAttentionBroadcastHook(timestep_skip_range, block_skip_range, current_timestep_callback)
314
+ registry.register_hook(hook, _PYRAMID_ATTENTION_BROADCAST_HOOK)
diffusers/hooks/smoothed_energy_guidance_utils.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import math
16
+ from dataclasses import asdict, dataclass
17
+
18
+ import torch
19
+ import torch.nn.functional as F
20
+
21
+ from ..utils import get_logger
22
+ from ._common import _ALL_TRANSFORMER_BLOCK_IDENTIFIERS, _ATTENTION_CLASSES, _get_submodule_from_fqn
23
+ from .hooks import HookRegistry, ModelHook
24
+
25
+
26
+ logger = get_logger(__name__) # pylint: disable=invalid-name
27
+
28
+ _SMOOTHED_ENERGY_GUIDANCE_HOOK = "smoothed_energy_guidance_hook"
29
+
30
+
31
+ @dataclass
32
+ class SmoothedEnergyGuidanceConfig:
33
+ r"""
34
+ Configuration for skipping internal transformer blocks when executing a transformer model.
35
+
36
+ Args:
37
+ indices (`list[int]`):
38
+ The indices of the layer to skip. This is typically the first layer in the transformer block.
39
+ fqn (`str`, defaults to `"auto"`):
40
+ The fully qualified name identifying the stack of transformer blocks. Typically, this is
41
+ `transformer_blocks`, `single_transformer_blocks`, `blocks`, `layers`, or `temporal_transformer_blocks`.
42
+ For automatic detection, set this to `"auto"`. "auto" only works on DiT models. For UNet models, you must
43
+ provide the correct fqn.
44
+ _query_proj_identifiers (`list[str]`, defaults to `None`):
45
+ The identifiers for the query projection layers. Typically, these are `to_q`, `query`, or `q_proj`. If
46
+ `None`, `to_q` is used by default.
47
+ """
48
+
49
+ indices: list[int]
50
+ fqn: str = "auto"
51
+ _query_proj_identifiers: list[str] = None
52
+
53
+ def to_dict(self):
54
+ return asdict(self)
55
+
56
+ @staticmethod
57
+ def from_dict(data: dict) -> "SmoothedEnergyGuidanceConfig":
58
+ return SmoothedEnergyGuidanceConfig(**data)
59
+
60
+
61
+ class SmoothedEnergyGuidanceHook(ModelHook):
62
+ def __init__(self, blur_sigma: float = 1.0, blur_threshold_inf: float = 9999.9) -> None:
63
+ super().__init__()
64
+ self.blur_sigma = blur_sigma
65
+ self.blur_threshold_inf = blur_threshold_inf
66
+
67
+ def post_forward(self, module: torch.nn.Module, output: torch.Tensor) -> torch.Tensor:
68
+ # Copied from https://github.com/SusungHong/SEG-SDXL/blob/cf8256d640d5373541cfea3b3b6caf93272cf986/pipeline_seg.py#L172C31-L172C102
69
+ kernel_size = math.ceil(6 * self.blur_sigma) + 1 - math.ceil(6 * self.blur_sigma) % 2
70
+ smoothed_output = _gaussian_blur_2d(output, kernel_size, self.blur_sigma, self.blur_threshold_inf)
71
+ return smoothed_output
72
+
73
+
74
+ def _apply_smoothed_energy_guidance_hook(
75
+ module: torch.nn.Module, config: SmoothedEnergyGuidanceConfig, blur_sigma: float, name: str | None = None
76
+ ) -> None:
77
+ name = name or _SMOOTHED_ENERGY_GUIDANCE_HOOK
78
+
79
+ if config.fqn == "auto":
80
+ for identifier in _ALL_TRANSFORMER_BLOCK_IDENTIFIERS:
81
+ if hasattr(module, identifier):
82
+ config.fqn = identifier
83
+ break
84
+ else:
85
+ raise ValueError(
86
+ "Could not find a suitable identifier for the transformer blocks automatically. Please provide a valid "
87
+ "`fqn` (fully qualified name) that identifies a stack of transformer blocks."
88
+ )
89
+
90
+ if config._query_proj_identifiers is None:
91
+ config._query_proj_identifiers = ["to_q"]
92
+
93
+ transformer_blocks = _get_submodule_from_fqn(module, config.fqn)
94
+ blocks_found = False
95
+ for i, block in enumerate(transformer_blocks):
96
+ if i not in config.indices:
97
+ continue
98
+
99
+ blocks_found = True
100
+
101
+ for submodule_name, submodule in block.named_modules():
102
+ if not isinstance(submodule, _ATTENTION_CLASSES) or submodule.is_cross_attention:
103
+ continue
104
+ for identifier in config._query_proj_identifiers:
105
+ query_proj = getattr(submodule, identifier, None)
106
+ if query_proj is None or not isinstance(query_proj, torch.nn.Linear):
107
+ continue
108
+ logger.debug(
109
+ f"Registering smoothed energy guidance hook on {config.fqn}.{i}.{submodule_name}.{identifier}"
110
+ )
111
+ registry = HookRegistry.check_if_exists_or_initialize(query_proj)
112
+ hook = SmoothedEnergyGuidanceHook(blur_sigma)
113
+ registry.register_hook(hook, name)
114
+
115
+ if not blocks_found:
116
+ raise ValueError(
117
+ f"Could not find any transformer blocks matching the provided indices {config.indices} and "
118
+ f"fully qualified name '{config.fqn}'. Please check the indices and fqn for correctness."
119
+ )
120
+
121
+
122
+ # Modified from https://github.com/SusungHong/SEG-SDXL/blob/cf8256d640d5373541cfea3b3b6caf93272cf986/pipeline_seg.py#L71
123
+ def _gaussian_blur_2d(query: torch.Tensor, kernel_size: int, sigma: float, sigma_threshold_inf: float) -> torch.Tensor:
124
+ """
125
+ This implementation assumes that the input query is for visual (image/videos) tokens to apply the 2D gaussian blur.
126
+ However, some models use joint text-visual token attention for which this may not be suitable. Additionally, this
127
+ implementation also assumes that the visual tokens come from a square image/video. In practice, despite these
128
+ assumptions, applying the 2D square gaussian blur on the query projections generates reasonable results for
129
+ Smoothed Energy Guidance.
130
+
131
+ SEG is only supported as an experimental prototype feature for now, so the implementation may be modified in the
132
+ future without warning or guarantee of reproducibility.
133
+ """
134
+ assert query.ndim == 3
135
+
136
+ is_inf = sigma > sigma_threshold_inf
137
+ batch_size, seq_len, embed_dim = query.shape
138
+
139
+ seq_len_sqrt = int(math.sqrt(seq_len))
140
+ num_square_tokens = seq_len_sqrt * seq_len_sqrt
141
+ query_slice = query[:, :num_square_tokens, :]
142
+ query_slice = query_slice.permute(0, 2, 1)
143
+ query_slice = query_slice.reshape(batch_size, embed_dim, seq_len_sqrt, seq_len_sqrt)
144
+
145
+ if is_inf:
146
+ kernel_size = min(kernel_size, seq_len_sqrt - (seq_len_sqrt % 2 - 1))
147
+ kernel_size_half = (kernel_size - 1) / 2
148
+
149
+ x = torch.linspace(-kernel_size_half, kernel_size_half, steps=kernel_size)
150
+ pdf = torch.exp(-0.5 * (x / sigma).pow(2))
151
+ kernel1d = pdf / pdf.sum()
152
+ kernel1d = kernel1d.to(query)
153
+ kernel2d = torch.matmul(kernel1d[:, None], kernel1d[None, :])
154
+ kernel2d = kernel2d.expand(embed_dim, 1, kernel2d.shape[0], kernel2d.shape[1])
155
+
156
+ padding = [kernel_size // 2, kernel_size // 2, kernel_size // 2, kernel_size // 2]
157
+ query_slice = F.pad(query_slice, padding, mode="reflect")
158
+ query_slice = F.conv2d(query_slice, kernel2d, groups=embed_dim)
159
+ else:
160
+ query_slice[:] = query_slice.mean(dim=(-2, -1), keepdim=True)
161
+
162
+ query_slice = query_slice.reshape(batch_size, embed_dim, num_square_tokens)
163
+ query_slice = query_slice.permute(0, 2, 1)
164
+ query[:, :num_square_tokens, :] = query_slice.clone()
165
+
166
+ return query
diffusers/hooks/taylorseer_cache.py ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import re
3
+ from dataclasses import dataclass
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+
8
+ from ..utils import logging
9
+ from .hooks import HookRegistry, ModelHook, StateManager
10
+
11
+
12
+ logger = logging.get_logger(__name__)
13
+ _TAYLORSEER_CACHE_HOOK = "taylorseer_cache"
14
+ _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS = (
15
+ "^blocks.*attn",
16
+ "^transformer_blocks.*attn",
17
+ "^single_transformer_blocks.*attn",
18
+ )
19
+ _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS = ("^temporal_transformer_blocks.*attn",)
20
+ _TRANSFORMER_BLOCK_IDENTIFIERS = _SPATIAL_ATTENTION_BLOCK_IDENTIFIERS + _TEMPORAL_ATTENTION_BLOCK_IDENTIFIERS
21
+ _BLOCK_IDENTIFIERS = ("^[^.]*block[^.]*\\.[^.]+$",)
22
+ _PROJ_OUT_IDENTIFIERS = ("^proj_out$",)
23
+
24
+
25
+ @dataclass
26
+ class TaylorSeerCacheConfig:
27
+ """
28
+ Configuration for TaylorSeer cache. See: https://huggingface.co/papers/2503.06923
29
+
30
+ Attributes:
31
+ cache_interval (`int`, defaults to `5`):
32
+ The interval between full computation steps. After a full computation, the cached (predicted) outputs are
33
+ reused for this many subsequent denoising steps before refreshing with a new full forward pass.
34
+
35
+ disable_cache_before_step (`int`, defaults to `3`):
36
+ The denoising step index before which caching is disabled, meaning full computation is performed for the
37
+ initial steps (0 to disable_cache_before_step - 1) to gather data for Taylor series approximations. During
38
+ these steps, Taylor factors are updated, but caching/predictions are not applied. Caching begins at this
39
+ step.
40
+
41
+ disable_cache_after_step (`int`, *optional*, defaults to `None`):
42
+ The denoising step index after which caching is disabled. If set, for steps >= this value, all modules run
43
+ full computations without predictions or state updates, ensuring accuracy in later stages if needed.
44
+
45
+ max_order (`int`, defaults to `1`):
46
+ The highest order in the Taylor series expansion for approximating module outputs. Higher orders provide
47
+ better approximations but increase computation and memory usage.
48
+
49
+ taylor_factors_dtype (`torch.dtype`, defaults to `torch.bfloat16`):
50
+ Data type used for storing and computing Taylor series factors. Lower precision reduces memory but may
51
+ affect stability; higher precision improves accuracy at the cost of more memory.
52
+
53
+ skip_predict_identifiers (`list[str]`, *optional*, defaults to `None`):
54
+ Regex patterns (using `re.fullmatch`) for module names to place as "skip" in "cache" mode. In this mode,
55
+ the module computes fully during initial or refresh steps but returns a zero tensor (matching recorded
56
+ shape) during prediction steps to skip computation cheaply.
57
+
58
+ cache_identifiers (`list[str]`, *optional*, defaults to `None`):
59
+ Regex patterns (using `re.fullmatch`) for module names to place in Taylor-series caching mode, where
60
+ outputs are approximated and cached for reuse.
61
+
62
+ use_lite_mode (`bool`, *optional*, defaults to `False`):
63
+ Enables a lightweight TaylorSeer variant that minimizes memory usage by applying predefined patterns for
64
+ skipping and caching (e.g., skipping blocks and caching projections). This overrides any custom
65
+ `inactive_identifiers` or `active_identifiers`.
66
+
67
+ Notes:
68
+ - Patterns are matched using `re.fullmatch` on the module name.
69
+ - If `skip_predict_identifiers` or `cache_identifiers` are provided, only matching modules are hooked.
70
+ - If neither is provided, all attention-like modules are hooked by default.
71
+
72
+ Example of inactive and active usage:
73
+
74
+ ```py
75
+ def forward(x):
76
+ x = self.module1(x) # inactive module: returns zeros tensor based on shape recorded during full compute
77
+ x = self.module2(x) # active module: caches output here, avoiding recomputation of prior steps
78
+ return x
79
+ ```
80
+ """
81
+
82
+ cache_interval: int = 5
83
+ disable_cache_before_step: int = 3
84
+ disable_cache_after_step: int | None = None
85
+ max_order: int = 1
86
+ taylor_factors_dtype: torch.dtype | None = torch.bfloat16
87
+ skip_predict_identifiers: list[str] | None = None
88
+ cache_identifiers: list[str] | None = None
89
+ use_lite_mode: bool = False
90
+
91
+ def __repr__(self) -> str:
92
+ return (
93
+ "TaylorSeerCacheConfig("
94
+ f"cache_interval={self.cache_interval}, "
95
+ f"disable_cache_before_step={self.disable_cache_before_step}, "
96
+ f"disable_cache_after_step={self.disable_cache_after_step}, "
97
+ f"max_order={self.max_order}, "
98
+ f"taylor_factors_dtype={self.taylor_factors_dtype}, "
99
+ f"skip_predict_identifiers={self.skip_predict_identifiers}, "
100
+ f"cache_identifiers={self.cache_identifiers}, "
101
+ f"use_lite_mode={self.use_lite_mode})"
102
+ )
103
+
104
+
105
+ class TaylorSeerState:
106
+ def __init__(
107
+ self,
108
+ taylor_factors_dtype: torch.dtype | None = torch.bfloat16,
109
+ max_order: int = 1,
110
+ is_inactive: bool = False,
111
+ ):
112
+ self.taylor_factors_dtype = taylor_factors_dtype
113
+ self.max_order = max_order
114
+ self.is_inactive = is_inactive
115
+
116
+ self.module_dtypes: tuple[torch.dtype, ...] = ()
117
+ self.last_update_step: int | None = None
118
+ self.taylor_factors: dict[int, dict[int, torch.Tensor]] = {}
119
+ self.inactive_shapes: tuple[tuple[int, ...], ...] | None = None
120
+ self.device: torch.device | None = None
121
+ self.current_step: int = -1
122
+
123
+ def reset(self) -> None:
124
+ self.current_step = -1
125
+ self.last_update_step = None
126
+ self.taylor_factors = {}
127
+ self.inactive_shapes = None
128
+ self.device = None
129
+
130
+ def update(
131
+ self,
132
+ outputs: tuple[torch.Tensor, ...],
133
+ ) -> None:
134
+ self.module_dtypes = tuple(output.dtype for output in outputs)
135
+ self.device = outputs[0].device
136
+
137
+ if self.is_inactive:
138
+ self.inactive_shapes = tuple(output.shape for output in outputs)
139
+ else:
140
+ for i, features in enumerate(outputs):
141
+ new_factors: dict[int, torch.Tensor] = {0: features}
142
+ is_first_update = self.last_update_step is None
143
+ if not is_first_update:
144
+ delta_step = self.current_step - self.last_update_step
145
+ if delta_step == 0:
146
+ raise ValueError("Delta step cannot be zero for TaylorSeer update.")
147
+
148
+ # Recursive divided differences up to max_order
149
+ prev_factors = self.taylor_factors.get(i, {})
150
+ for j in range(self.max_order):
151
+ prev = prev_factors.get(j)
152
+ if prev is None:
153
+ break
154
+ new_factors[j + 1] = (new_factors[j] - prev.to(features.dtype)) / delta_step
155
+ self.taylor_factors[i] = {
156
+ order: factor.to(self.taylor_factors_dtype) for order, factor in new_factors.items()
157
+ }
158
+
159
+ self.last_update_step = self.current_step
160
+
161
+ @torch.compiler.disable
162
+ def predict(self) -> list[torch.Tensor]:
163
+ if self.last_update_step is None:
164
+ raise ValueError("Cannot predict without prior initialization/update.")
165
+
166
+ step_offset = self.current_step - self.last_update_step
167
+
168
+ outputs = []
169
+ if self.is_inactive:
170
+ if self.inactive_shapes is None:
171
+ raise ValueError("Inactive shapes not set during prediction.")
172
+ for i in range(len(self.module_dtypes)):
173
+ outputs.append(
174
+ torch.zeros(
175
+ self.inactive_shapes[i],
176
+ dtype=self.module_dtypes[i],
177
+ device=self.device,
178
+ )
179
+ )
180
+ else:
181
+ if not self.taylor_factors:
182
+ raise ValueError("Taylor factors empty during prediction.")
183
+ num_outputs = len(self.taylor_factors)
184
+ num_orders = len(self.taylor_factors[0])
185
+ for i in range(num_outputs):
186
+ output_dtype = self.module_dtypes[i]
187
+ taylor_factors = self.taylor_factors[i]
188
+ output = torch.zeros_like(taylor_factors[0], dtype=output_dtype)
189
+ for order in range(num_orders):
190
+ coeff = (step_offset**order) / math.factorial(order)
191
+ factor = taylor_factors[order]
192
+ output = output + factor.to(output_dtype) * coeff
193
+ outputs.append(output)
194
+ return outputs
195
+
196
+
197
+ class TaylorSeerCacheHook(ModelHook):
198
+ _is_stateful = True
199
+
200
+ def __init__(
201
+ self,
202
+ cache_interval: int,
203
+ disable_cache_before_step: int,
204
+ taylor_factors_dtype: torch.dtype,
205
+ state_manager: StateManager,
206
+ disable_cache_after_step: int | None = None,
207
+ ):
208
+ super().__init__()
209
+ self.cache_interval = cache_interval
210
+ self.disable_cache_before_step = disable_cache_before_step
211
+ self.disable_cache_after_step = disable_cache_after_step
212
+ self.taylor_factors_dtype = taylor_factors_dtype
213
+ self.state_manager = state_manager
214
+
215
+ def initialize_hook(self, module: torch.nn.Module):
216
+ return module
217
+
218
+ def reset_state(self, module: torch.nn.Module) -> None:
219
+ """
220
+ Reset state between sampling runs.
221
+ """
222
+ self.state_manager.reset()
223
+
224
+ @torch.compiler.disable
225
+ def _measure_should_compute(self) -> bool:
226
+ state: TaylorSeerState = self.state_manager.get_state()
227
+ state.current_step += 1
228
+ current_step = state.current_step
229
+ is_warmup_phase = current_step < self.disable_cache_before_step
230
+ is_compute_interval = (current_step - self.disable_cache_before_step - 1) % self.cache_interval == 0
231
+ is_cooldown_phase = self.disable_cache_after_step is not None and current_step >= self.disable_cache_after_step
232
+ should_compute = is_warmup_phase or is_compute_interval or is_cooldown_phase
233
+ return should_compute, state
234
+
235
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
236
+ should_compute, state = self._measure_should_compute()
237
+ if should_compute:
238
+ outputs = self.fn_ref.original_forward(*args, **kwargs)
239
+ wrapped_outputs = (outputs,) if isinstance(outputs, torch.Tensor) else outputs
240
+ state.update(wrapped_outputs)
241
+ return outputs
242
+
243
+ outputs_list = state.predict()
244
+ return outputs_list[0] if len(outputs_list) == 1 else tuple(outputs_list)
245
+
246
+
247
+ def _resolve_patterns(config: TaylorSeerCacheConfig) -> tuple[list[str], list[str]]:
248
+ """
249
+ Resolve effective inactive and active pattern lists from config + templates.
250
+ """
251
+
252
+ inactive_patterns = config.skip_predict_identifiers if config.skip_predict_identifiers is not None else None
253
+ active_patterns = config.cache_identifiers if config.cache_identifiers is not None else None
254
+
255
+ return inactive_patterns or [], active_patterns or []
256
+
257
+
258
+ def apply_taylorseer_cache(module: torch.nn.Module, config: TaylorSeerCacheConfig):
259
+ """
260
+ Applies the TaylorSeer cache to a given pipeline (typically the transformer / UNet).
261
+
262
+ This function hooks selected modules in the model to enable caching or skipping based on the provided
263
+ configuration, reducing redundant computations in diffusion denoising loops.
264
+
265
+ Args:
266
+ module (torch.nn.Module): The model subtree to apply the hooks to.
267
+ config (TaylorSeerCacheConfig): Configuration for the cache.
268
+
269
+ Example:
270
+ ```python
271
+ >>> import torch
272
+ >>> from diffusers import FluxPipeline, TaylorSeerCacheConfig
273
+
274
+ >>> pipe = FluxPipeline.from_pretrained(
275
+ ... "black-forest-labs/FLUX.1-dev",
276
+ ... torch_dtype=torch.bfloat16,
277
+ ... )
278
+ >>> pipe.to("cuda")
279
+
280
+ >>> config = TaylorSeerCacheConfig(
281
+ ... cache_interval=5,
282
+ ... max_order=1,
283
+ ... disable_cache_before_step=3,
284
+ ... taylor_factors_dtype=torch.float32,
285
+ ... )
286
+ >>> pipe.transformer.enable_cache(config)
287
+ ```
288
+ """
289
+ inactive_patterns, active_patterns = _resolve_patterns(config)
290
+
291
+ active_patterns = active_patterns or _TRANSFORMER_BLOCK_IDENTIFIERS
292
+
293
+ if config.use_lite_mode:
294
+ logger.info("Using TaylorSeer Lite variant for cache.")
295
+ active_patterns = _PROJ_OUT_IDENTIFIERS
296
+ inactive_patterns = _BLOCK_IDENTIFIERS
297
+ if config.skip_predict_identifiers or config.cache_identifiers:
298
+ logger.warning("Lite mode overrides user patterns.")
299
+
300
+ for name, submodule in module.named_modules():
301
+ matches_inactive = any(re.fullmatch(pattern, name) for pattern in inactive_patterns)
302
+ matches_active = any(re.fullmatch(pattern, name) for pattern in active_patterns)
303
+ if not (matches_inactive or matches_active):
304
+ continue
305
+ _apply_taylorseer_cache_hook(
306
+ module=submodule,
307
+ config=config,
308
+ is_inactive=matches_inactive,
309
+ )
310
+
311
+
312
+ def _apply_taylorseer_cache_hook(
313
+ module: nn.Module,
314
+ config: TaylorSeerCacheConfig,
315
+ is_inactive: bool,
316
+ ):
317
+ """
318
+ Registers the TaylorSeer hook on the specified nn.Module.
319
+
320
+ Args:
321
+ name: Name of the module.
322
+ module: The nn.Module to be hooked.
323
+ config: Cache configuration.
324
+ is_inactive: Whether this module should operate in "inactive" mode.
325
+ """
326
+ state_manager = StateManager(
327
+ TaylorSeerState,
328
+ init_kwargs={
329
+ "taylor_factors_dtype": config.taylor_factors_dtype,
330
+ "max_order": config.max_order,
331
+ "is_inactive": is_inactive,
332
+ },
333
+ )
334
+
335
+ registry = HookRegistry.check_if_exists_or_initialize(module)
336
+
337
+ hook = TaylorSeerCacheHook(
338
+ cache_interval=config.cache_interval,
339
+ disable_cache_before_step=config.disable_cache_before_step,
340
+ taylor_factors_dtype=config.taylor_factors_dtype,
341
+ disable_cache_after_step=config.disable_cache_after_step,
342
+ state_manager=state_manager,
343
+ )
344
+
345
+ registry.register_hook(hook, _TAYLORSEER_CACHE_HOOK)
diffusers/hooks/tensor_parallel.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+
17
+ from ..models._modeling_parallel import TensorParallelConfig
18
+ from ..utils import get_logger
19
+
20
+
21
+ logger = get_logger(__name__) # pylint: disable=invalid-name
22
+
23
+ _SUPPORTED_TP_DEVICES = ("cuda", "neuron")
24
+
25
+
26
+ class PackedColwiseParallel:
27
+ """Column-wise sharding for fused projections with heterogeneous block structure.
28
+
29
+ `blocks` is a list of proportional integers whose sum divides the weight's row count. For example, `[1, 1]` for a
30
+ SwiGLU gate+linear projection (two equal halves) or `[1, 1, 1, 3, 3]` for a Q+K+V+gate+linear projection with
31
+ `mlp_ratio=3`. If `blocks` is `None`, the Linear module must carry a `_tp_packed_col_blocks` attribute set during
32
+ model `__init__`.
33
+ """
34
+
35
+ def __init__(self, blocks: "list[int] | None" = None):
36
+ self.blocks = blocks
37
+
38
+
39
+ class PackedRowwiseParallel:
40
+ """Row-wise sharding for fused projections with heterogeneous block structure.
41
+
42
+ `blocks` describes the input-column partition of the fused Linear (e.g. `[1, 3]` when the input concatenates an
43
+ attention projection and an MLP projection with `mlp_ratio=3`). If `blocks` is `None`, the module must carry a
44
+ `_tp_packed_row_blocks` attribute.
45
+ """
46
+
47
+ def __init__(self, blocks: "list[int] | None" = None):
48
+ self.blocks = blocks
49
+
50
+
51
+ def _blocks_to_block_sizes(total_size: int, blocks: "list[int]") -> "list[int]":
52
+ """Convert proportional block counts to absolute sizes.
53
+
54
+ `blocks` is a list of positive integers interpreted as proportional weights. Their sum must divide `total_size`
55
+ evenly. Returns a list of absolute sizes that sum to `total_size`. For example, `_blocks_to_block_sizes(1152, [1,
56
+ 1, 1, 3, 3])` returns `[128, 128, 128, 384, 384]`.
57
+ """
58
+ total = sum(blocks)
59
+ if total_size % total != 0:
60
+ raise ValueError(
61
+ f"Cannot split {total_size} into proportional blocks {blocks}: "
62
+ f"sum({blocks})={total} does not divide {total_size}."
63
+ )
64
+ unit = total_size // total
65
+ return [b * unit for b in blocks]
66
+
67
+
68
+ def _resolve_tp_plan(model: torch.nn.Module, tp_plan: dict) -> list:
69
+ """Group a flat `_tp_plan` into per-block `(submodule, {relative_path: style})` plans.
70
+
71
+ Each glob is split at its single `*`; the prefix must resolve to a `ModuleList` and the suffix is the per-element
72
+ key. Grouping by block lets the caller issue one `parallelize_module` call per block, which `RowwiseParallel` needs
73
+ to attach its input redistribution at the block boundary.
74
+
75
+ Example: when `transformer_blocks` is a `ModuleList` of length 2, the input `{"transformer_blocks.*.ff.linear_out":
76
+ "rowwise"}` returns `[(transformer_blocks[0], {"ff.linear_out": "rowwise"}), (transformer_blocks[1],
77
+ {"ff.linear_out": "rowwise"})]`.
78
+ """
79
+ grouped: dict[int, tuple] = {}
80
+ order: list[int] = []
81
+
82
+ for pattern, style in tp_plan.items():
83
+ if pattern.count("*") > 1:
84
+ raise ValueError(f"Wildcard '*' can only be used once in a `_tp_plan` key, got '{pattern}'.")
85
+
86
+ if "*" in pattern:
87
+ prefix, _, suffix = pattern.partition("*")
88
+ container = model
89
+ for atom in prefix.strip(".").split("."):
90
+ container = getattr(container, atom)
91
+ if not isinstance(container, torch.nn.ModuleList):
92
+ raise ValueError(
93
+ f"`_tp_plan` wildcard '{pattern}' must expand over a `ModuleList`, but "
94
+ f"'{prefix.strip('.')}' resolved to '{container.__class__.__name__}'."
95
+ )
96
+ relative, blocks = suffix.strip("."), list(container)
97
+ else:
98
+ relative, blocks = pattern, [model]
99
+
100
+ for block in blocks:
101
+ key = id(block)
102
+ if key not in grouped:
103
+ grouped[key] = (block, {})
104
+ order.append(key)
105
+ grouped[key][1][relative] = style
106
+
107
+ return [grouped[key] for key in order]
108
+
109
+
110
+ def _styles(relative_plan: dict) -> dict:
111
+ """Map a `{relative_path: style}` plan to `parallelize_module` style instances.
112
+
113
+ Values may be plain strings (`"colwise"` / `"rowwise"`) or `PackedColwiseParallel` / `PackedRowwiseParallel` marker
114
+ instances. Returns `{relative_path: ColwiseParallel() | RowwiseParallel() | <packed impl>}`, each subclassed to
115
+ reject a sharded dim that is not divisible by the TP degree.
116
+ """
117
+ import torch.nn as nn
118
+ from torch.distributed.tensor import DTensor, Replicate, Shard, distribute_tensor
119
+ from torch.distributed.tensor.parallel import ColwiseParallel, RowwiseParallel
120
+
121
+ def _make_packed_col(marker: PackedColwiseParallel) -> ColwiseParallel:
122
+ _blocks = marker.blocks
123
+
124
+ class _PackedColwiseImpl(ColwiseParallel):
125
+ def _partition_linear_fn(self, name, module, device_mesh):
126
+ blocks = _blocks if _blocks is not None else getattr(module, "_tp_packed_col_blocks")
127
+ rank = device_mesh.get_local_rank()
128
+ tp_size = device_mesh.size()
129
+ # Both weight (`[out, in]`) and bias (`[out]`) are sharded row-wise (dim 0) with the same per-block
130
+ # slicing so each rank's bias rows line up with its weight rows for the packed layout.
131
+ for param_name, param in module.named_parameters():
132
+ full = distribute_tensor(
133
+ param, device_mesh, [Replicate()], src_data_rank=self.src_data_rank
134
+ ).to_local()
135
+ block_sizes = _blocks_to_block_sizes(full.shape[0], blocks)
136
+ parts, offset = [], 0
137
+ for bs in block_sizes:
138
+ if bs % tp_size != 0:
139
+ raise ValueError(
140
+ f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
141
+ f"{bs} is not divisible by {tp_size}."
142
+ )
143
+ chunk = bs // tp_size
144
+ parts.append(full[offset + rank * chunk : offset + (rank + 1) * chunk].contiguous())
145
+ offset += bs
146
+ local = torch.cat(parts, dim=0)
147
+ dist_param = nn.Parameter(
148
+ DTensor.from_local(local, device_mesh, [Shard(0)], run_check=False),
149
+ requires_grad=param.requires_grad,
150
+ )
151
+ module.register_parameter(param_name, dist_param)
152
+
153
+ return _PackedColwiseImpl()
154
+
155
+ def _make_packed_row(marker: PackedRowwiseParallel) -> RowwiseParallel:
156
+ _blocks = marker.blocks
157
+
158
+ class _PackedRowwiseImpl(RowwiseParallel):
159
+ def _partition_linear_fn(self, name, module, device_mesh):
160
+ blocks = _blocks if _blocks is not None else getattr(module, "_tp_packed_row_blocks")
161
+ rank = device_mesh.get_local_rank()
162
+ tp_size = device_mesh.size()
163
+ for param_name, param in module.named_parameters():
164
+ if param_name == "weight":
165
+ full = distribute_tensor(
166
+ param, device_mesh, [Replicate()], src_data_rank=self.src_data_rank
167
+ ).to_local()
168
+ block_sizes = _blocks_to_block_sizes(full.shape[1], blocks)
169
+ parts, offset = [], 0
170
+ for bs in block_sizes:
171
+ if bs % tp_size != 0:
172
+ raise ValueError(
173
+ f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
174
+ f"{bs} is not divisible by {tp_size}."
175
+ )
176
+ chunk = bs // tp_size
177
+ parts.append(full[:, offset + rank * chunk : offset + (rank + 1) * chunk].contiguous())
178
+ offset += bs
179
+ local = torch.cat(parts, dim=1)
180
+ dist_param = nn.Parameter(
181
+ DTensor.from_local(local, device_mesh, [Shard(1)], run_check=False),
182
+ requires_grad=param.requires_grad,
183
+ )
184
+ else:
185
+ dist_param = nn.Parameter(
186
+ distribute_tensor(param, device_mesh, [Replicate()], src_data_rank=self.src_data_rank),
187
+ requires_grad=param.requires_grad,
188
+ )
189
+ module.register_parameter(param_name, dist_param)
190
+
191
+ return _PackedRowwiseImpl()
192
+
193
+ # `distribute_tensor` accepts an indivisible shard dim and just gives the trailing ranks a smaller (or empty)
194
+ # slice, so an uneven split does not raise here — it surfaces much later as a shape or numerics error, because
195
+ # the attention head split and the paired colwise/rowwise Linear both assume equal shards. Reject it up front,
196
+ # matching what the packed styles above and the Neuron pre-shard path already do.
197
+ def _make_checked_col(path: str) -> ColwiseParallel:
198
+ class _CheckedColwiseImpl(ColwiseParallel):
199
+ def _partition_linear_fn(self, name, module, device_mesh):
200
+ tp_size = device_mesh.size()
201
+ out_features = module.weight.shape[0]
202
+ if out_features % tp_size != 0:
203
+ raise ValueError(
204
+ f"Cannot colwise-shard '{path}' weight rows ({out_features}) across {tp_size} "
205
+ f"tensor-parallel ranks: not divisible by {tp_size}."
206
+ )
207
+ super()._partition_linear_fn(name, module, device_mesh)
208
+
209
+ return _CheckedColwiseImpl()
210
+
211
+ def _make_checked_row(path: str) -> RowwiseParallel:
212
+ class _CheckedRowwiseImpl(RowwiseParallel):
213
+ def _partition_linear_fn(self, name, module, device_mesh):
214
+ tp_size = device_mesh.size()
215
+ in_features = module.weight.shape[1]
216
+ if in_features % tp_size != 0:
217
+ raise ValueError(
218
+ f"Cannot rowwise-shard '{path}' weight columns ({in_features}) across {tp_size} "
219
+ f"tensor-parallel ranks: not divisible by {tp_size}."
220
+ )
221
+ super()._partition_linear_fn(name, module, device_mesh)
222
+
223
+ return _CheckedRowwiseImpl()
224
+
225
+ resolved = {}
226
+ for path, style in relative_plan.items():
227
+ if style == "colwise":
228
+ resolved[path] = _make_checked_col(path)
229
+ elif style == "rowwise":
230
+ resolved[path] = _make_checked_row(path)
231
+ elif isinstance(style, PackedColwiseParallel):
232
+ resolved[path] = _make_packed_col(style)
233
+ elif isinstance(style, PackedRowwiseParallel):
234
+ resolved[path] = _make_packed_row(style)
235
+ else:
236
+ raise ValueError(
237
+ f"Unsupported tensor-parallel style '{style}' for '{path}'. "
238
+ f"Expected 'colwise', 'rowwise', PackedColwiseParallel, or PackedRowwiseParallel."
239
+ )
240
+ return resolved
241
+
242
+
243
+ def apply_tensor_parallel(
244
+ model: torch.nn.Module,
245
+ config: TensorParallelConfig,
246
+ tp_plan: dict,
247
+ ) -> None:
248
+ """Apply tensor parallel on a model from its flat `_tp_plan`."""
249
+ tp_mesh = config._mesh
250
+ if tp_mesh is None:
251
+ raise ValueError("`config._mesh` is None. Call `config.setup(rank, world_size, device)` before applying TP.")
252
+
253
+ if tp_mesh.device_type not in _SUPPORTED_TP_DEVICES:
254
+ raise ValueError(
255
+ f"Tensor parallelism is not supported on device type '{tp_mesh.device_type}'. Supported device types are "
256
+ f"{list(_SUPPORTED_TP_DEVICES)}. The device type comes from the `mesh` passed to `TensorParallelConfig`, "
257
+ f"or from the active accelerator when the mesh is built from `tp_degree`."
258
+ )
259
+
260
+ backend = "neuron" if tp_mesh.device_type == "neuron" else "default"
261
+ groups = _resolve_tp_plan(model, tp_plan)
262
+ logger.debug(f"Applying tensor parallel (backend={backend}) over {len(groups)} module group(s) on mesh {tp_mesh}.")
263
+
264
+ if backend == "neuron":
265
+ from .tensor_parallel_neuron import _apply_tp_neuron
266
+
267
+ _apply_tp_neuron(model, tp_mesh, groups)
268
+ return
269
+
270
+ from torch.distributed.tensor.parallel import parallelize_module
271
+
272
+ for submodule, relative_plan in groups:
273
+ parallelize_module(submodule, tp_mesh, _styles(relative_plan))
diffusers/hooks/tensor_parallel_neuron.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Neuron backend for tensor parallelism, dispatched from `apply_tensor_parallel` when the TP mesh is on Neuron.
16
+
17
+ The difference from the generic path is a workaround for a Neuron NRT bug: consecutive `reduce_scatter` collectives for
18
+ large weight tensors (≥ 5120×5120) can fail when all layers are distributed in a single `parallelize_module` call. The
19
+ fix is to pre-shard each weight locally on CPU via `DTensor.from_local` *before* calling `parallelize_module`; the
20
+ latter then sees already-placed DTensors, skips the collective for weights, but still registers the required
21
+ input/output hooks for the forward pass.
22
+ """
23
+
24
+ import torch
25
+ import torch.distributed as dist
26
+ import torch.nn as nn
27
+
28
+
29
+ def _neuron_styles(relative_plan: dict) -> dict:
30
+ """Map a `{relative_path: style}` plan to no-op-partition styles for Neuron.
31
+
32
+ Weights (and biases) are pre-sharded in `_pre_shard_and_tp`, so `parallelize_module` runs only to register the
33
+ forward hooks; `_partition_linear_fn` must not re-partition. Packed and plain styles share hook behavior, so both
34
+ collapse onto the two no-op styles.
35
+ """
36
+ from torch.distributed.tensor.parallel import ColwiseParallel, RowwiseParallel
37
+
38
+ from .tensor_parallel import PackedColwiseParallel, PackedRowwiseParallel
39
+
40
+ class _NeuronColwise(ColwiseParallel):
41
+ def _partition_linear_fn(self, name, module, device_mesh):
42
+ pass # weight already Shard(0) via DTensor.from_local; parallelize_module runs only for the hooks
43
+
44
+ class _NeuronRowwise(RowwiseParallel):
45
+ def _partition_linear_fn(self, name, module, device_mesh):
46
+ pass # weight already Shard(1) via DTensor.from_local; parallelize_module runs only for the hooks
47
+
48
+ resolved = {}
49
+ for path, style in relative_plan.items():
50
+ if style == "colwise" or isinstance(style, PackedColwiseParallel):
51
+ resolved[path] = _NeuronColwise()
52
+ elif style == "rowwise" or isinstance(style, PackedRowwiseParallel):
53
+ resolved[path] = _NeuronRowwise()
54
+ else:
55
+ raise ValueError(
56
+ f"Unsupported tensor-parallel style '{style}' for '{path}'. "
57
+ f"Expected 'colwise', 'rowwise', PackedColwiseParallel, or PackedRowwiseParallel."
58
+ )
59
+ return resolved
60
+
61
+
62
+ def _pre_shard_and_tp(
63
+ module: nn.Module,
64
+ tp_mesh: "torch.distributed.device_mesh.DeviceMesh",
65
+ original_plan: dict,
66
+ rank: int,
67
+ tp_size: int,
68
+ ) -> None:
69
+ """Pre-shard Linear weights via `DTensor.from_local`, then call `parallelize_module`.
70
+
71
+ Workaround for a Neuron NRT bug where consecutive `reduce_scatter` calls for large weight tensors (≥ 5120×5120)
72
+ fail when all layers are distributed in a single `parallelize_module` call. Pre-sharding each weight on CPU means
73
+ it is already an on-device DTensor when `parallelize_module` runs (via `_neuron_styles`), so the collective is
74
+ skipped while the forward hooks are still registered.
75
+ """
76
+ from torch.distributed.tensor import DTensor, Replicate, Shard
77
+ from torch.distributed.tensor.parallel import parallelize_module
78
+
79
+ from .tensor_parallel import PackedColwiseParallel, PackedRowwiseParallel, _blocks_to_block_sizes
80
+
81
+ device = torch.neuron.current_device()
82
+
83
+ for path, orig_style in original_plan.items():
84
+ # Resolve nested attribute path (e.g. "attn.to_q" or "attn.to_out.0")
85
+ submod = module
86
+ for part in path.split("."):
87
+ submod = getattr(submod, part)
88
+
89
+ if not hasattr(submod, "weight"):
90
+ raise ValueError(f"`_tp_plan` entry '{path}' does not resolve to a module with a `weight` parameter.")
91
+
92
+ w = submod.weight.data # CPU at this point
93
+ b = submod.bias.data if submod.bias is not None else None
94
+ if isinstance(orig_style, PackedColwiseParallel):
95
+ blocks = orig_style.blocks if orig_style.blocks is not None else getattr(submod, "_tp_packed_col_blocks")
96
+ block_sizes = _blocks_to_block_sizes(w.shape[0], blocks)
97
+ parts, bias_parts, offset = [], [], 0
98
+ for bs in block_sizes:
99
+ if bs % tp_size != 0:
100
+ raise ValueError(
101
+ f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
102
+ f"{bs} is not divisible by {tp_size}."
103
+ )
104
+ chunk = bs // tp_size
105
+ sl = slice(offset + rank * chunk, offset + (rank + 1) * chunk)
106
+ parts.append(w[sl, :].contiguous())
107
+ if b is not None:
108
+ bias_parts.append(b[sl].contiguous())
109
+ offset += bs
110
+ shard = torch.cat(parts, dim=0).to(device)
111
+ submod.weight = nn.Parameter(DTensor.from_local(shard, tp_mesh, [Shard(0)]))
112
+ if b is not None:
113
+ bias_shard = torch.cat(bias_parts, dim=0).to(device)
114
+ submod.bias = nn.Parameter(DTensor.from_local(bias_shard, tp_mesh, [Shard(0)]))
115
+ elif isinstance(orig_style, PackedRowwiseParallel):
116
+ blocks = orig_style.blocks if orig_style.blocks is not None else getattr(submod, "_tp_packed_row_blocks")
117
+ block_sizes = _blocks_to_block_sizes(w.shape[1], blocks)
118
+ parts, offset = [], 0
119
+ for bs in block_sizes:
120
+ if bs % tp_size != 0:
121
+ raise ValueError(
122
+ f"Cannot shard packed block of size {bs} across {tp_size} tensor-parallel ranks: "
123
+ f"{bs} is not divisible by {tp_size}."
124
+ )
125
+ chunk = bs // tp_size
126
+ parts.append(w[:, offset + rank * chunk : offset + (rank + 1) * chunk].contiguous())
127
+ offset += bs
128
+ shard = torch.cat(parts, dim=1).to(device)
129
+ submod.weight = nn.Parameter(DTensor.from_local(shard, tp_mesh, [Shard(1)]))
130
+ if b is not None: # rowwise bias is added post-reduction → keep it replicated
131
+ submod.bias = nn.Parameter(DTensor.from_local(b.to(device), tp_mesh, [Replicate()]))
132
+ elif orig_style == "colwise":
133
+ if w.shape[0] % tp_size != 0:
134
+ raise ValueError(
135
+ f"Cannot colwise-shard '{path}' weight rows ({w.shape[0]}) across {tp_size} "
136
+ f"tensor-parallel ranks: not divisible by {tp_size}."
137
+ )
138
+ rows = w.shape[0] // tp_size
139
+ sl = slice(rank * rows, (rank + 1) * rows)
140
+ submod.weight = nn.Parameter(DTensor.from_local(w[sl, :].contiguous().to(device), tp_mesh, [Shard(0)]))
141
+ if b is not None:
142
+ submod.bias = nn.Parameter(DTensor.from_local(b[sl].contiguous().to(device), tp_mesh, [Shard(0)]))
143
+ elif orig_style == "rowwise":
144
+ if w.shape[1] % tp_size != 0:
145
+ raise ValueError(
146
+ f"Cannot rowwise-shard '{path}' weight columns ({w.shape[1]}) across {tp_size} "
147
+ f"tensor-parallel ranks: not divisible by {tp_size}."
148
+ )
149
+ cols = w.shape[1] // tp_size
150
+ shard = w[:, rank * cols : (rank + 1) * cols].contiguous().to(device)
151
+ submod.weight = nn.Parameter(DTensor.from_local(shard, tp_mesh, [Shard(1)]))
152
+ if b is not None: # rowwise bias is added post-reduction → keep it replicated
153
+ submod.bias = nn.Parameter(DTensor.from_local(b.to(device), tp_mesh, [Replicate()]))
154
+
155
+ # parallelize_module is now a no-op for weight distribution (already DTensors)
156
+ # but still registers the input/output hooks required for the forward pass.
157
+ parallelize_module(module, tp_mesh, _neuron_styles(original_plan))
158
+
159
+
160
+ def _apply_tp_neuron(
161
+ model: nn.Module,
162
+ tp_mesh: "torch.distributed.device_mesh.DeviceMesh",
163
+ groups: list,
164
+ ) -> None:
165
+ """Apply tensor parallelism on Neuron from resolved `_tp_plan` groups.
166
+
167
+ `groups` is produced by `diffusers.hooks.tensor_parallel._resolve_tp_plan` — the same source of truth used by the
168
+ generic path, so the two backends shard identical layers. For each `(block, relative_plan)` group this pre-shards
169
+ the weights via `DTensor.from_local` (Neuron NRT consecutive-reduce-scatter workaround), then calls
170
+ `parallelize_module` to register the forward hooks.
171
+
172
+ Model weights must be on CPU when this is called.
173
+ """
174
+ rank = dist.get_rank()
175
+ tp_size = tp_mesh.size()
176
+
177
+ for block, relative_plan in groups:
178
+ _pre_shard_and_tp(block, tp_mesh, relative_plan, rank, tp_size)
diffusers/hooks/text_kv_cache.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from dataclasses import dataclass
16
+
17
+ import torch
18
+
19
+ from .hooks import BaseState, HookRegistry, ModelHook, StateManager
20
+
21
+
22
+ _TEXT_KV_CACHE_TRANSFORMER_HOOK = "text_kv_cache_transformer"
23
+ _TEXT_KV_CACHE_BLOCK_HOOK = "text_kv_cache_block"
24
+
25
+
26
+ @dataclass
27
+ class TextKVCacheConfig:
28
+ """Enable exact (lossless) text K/V caching for transformer models.
29
+
30
+ Pre-computes per-block text key and value projections once before the denoising loop and reuses them across all
31
+ steps. Positive and negative prompts are distinguished via a stable cache key captured by a transformer-level hook
32
+ before any intermediate tensor allocations.
33
+ """
34
+
35
+ pass
36
+
37
+
38
+ class TextKVCacheState(BaseState):
39
+ """Shared state between the transformer-level and block-level hooks.
40
+
41
+ The transformer hook writes the stable ``encoder_hidden_states`` ``data_ptr()`` (captured *before* ``txt_norm``) so
42
+ that block hooks can use it as a reliable cache key across denoising steps.
43
+ """
44
+
45
+ def __init__(self):
46
+ self.key: int | None = None
47
+
48
+ def reset(self):
49
+ self.key = None
50
+
51
+
52
+ class TextKVCacheBlockState(BaseState):
53
+ """Per-block state holding cached text key/value projections."""
54
+
55
+ def __init__(self):
56
+ self.kv_cache: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
57
+
58
+ def reset(self):
59
+ self.kv_cache.clear()
60
+
61
+
62
+ class TextKVCacheTransformerHook(ModelHook):
63
+ """Captures ``encoder_hidden_states.data_ptr()`` before ``txt_norm``
64
+ and writes it to shared state for the block hooks to read."""
65
+
66
+ _is_stateful = True
67
+
68
+ def __init__(self, state_manager: StateManager):
69
+ super().__init__()
70
+ self.state_manager = state_manager
71
+
72
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
73
+ if self.state_manager._current_context is None:
74
+ self.state_manager.set_context("inference")
75
+
76
+ encoder_hidden_states = kwargs.get("encoder_hidden_states")
77
+ if encoder_hidden_states is not None:
78
+ state: TextKVCacheState = self.state_manager.get_state()
79
+ state.key = encoder_hidden_states.data_ptr()
80
+ return self.fn_ref.original_forward(*args, **kwargs)
81
+
82
+ def reset_state(self, module: torch.nn.Module):
83
+ self.state_manager.reset()
84
+ return module
85
+
86
+
87
+ class TextKVCacheBlockHook(ModelHook):
88
+ """Caches ``(txt_key, txt_value)`` per block per unique prompt using
89
+ the stable cache key from the shared state."""
90
+
91
+ _is_stateful = True
92
+
93
+ def __init__(self, state_manager: StateManager, block_state_manager: StateManager):
94
+ super().__init__()
95
+ self.state_manager = state_manager
96
+ self.block_state_manager = block_state_manager
97
+
98
+ def new_forward(self, module: torch.nn.Module, *args, **kwargs):
99
+ from ..models.transformers.transformer_nucleusmoe_image import _apply_rotary_emb_nucleus
100
+
101
+ if self.state_manager._current_context is None:
102
+ self.state_manager.set_context("inference")
103
+
104
+ if self.block_state_manager._current_context is None:
105
+ self.block_state_manager.set_context("inference")
106
+
107
+ if "encoder_hidden_states" in kwargs:
108
+ encoder_hidden_states = kwargs["encoder_hidden_states"]
109
+ else:
110
+ encoder_hidden_states = args[1]
111
+
112
+ if "image_rotary_emb" in kwargs:
113
+ image_rotary_emb = kwargs["image_rotary_emb"]
114
+ elif len(args) > 3:
115
+ image_rotary_emb = args[3]
116
+ else:
117
+ image_rotary_emb = None
118
+
119
+ state: TextKVCacheState = self.state_manager.get_state()
120
+ cache_key = state.key
121
+
122
+ block_state: TextKVCacheBlockState = self.block_state_manager.get_state()
123
+
124
+ if cache_key not in block_state.kv_cache:
125
+ context = module.encoder_proj(encoder_hidden_states)
126
+
127
+ attn = module.attn
128
+ head_dim = attn.inner_dim // attn.heads
129
+ num_kv_heads = attn.inner_kv_dim // head_dim
130
+
131
+ txt_key = attn.add_k_proj(context).unflatten(-1, (num_kv_heads, -1))
132
+ txt_value = attn.add_v_proj(context).unflatten(-1, (num_kv_heads, -1))
133
+
134
+ if attn.norm_added_k is not None:
135
+ txt_key = attn.norm_added_k(txt_key)
136
+
137
+ if image_rotary_emb is not None:
138
+ _, txt_freqs = image_rotary_emb
139
+ txt_key = _apply_rotary_emb_nucleus(txt_key, txt_freqs, use_real=False)
140
+
141
+ block_state.kv_cache[cache_key] = (txt_key, txt_value)
142
+
143
+ txt_key, txt_value = block_state.kv_cache[cache_key]
144
+
145
+ attn_kwargs = kwargs.get("attention_kwargs") or {}
146
+ attn_kwargs["cached_txt_key"] = txt_key
147
+ attn_kwargs["cached_txt_value"] = txt_value
148
+ kwargs["attention_kwargs"] = attn_kwargs
149
+
150
+ return self.fn_ref.original_forward(*args, **kwargs)
151
+
152
+ def reset_state(self, module: torch.nn.Module):
153
+ self.block_state_manager.reset()
154
+ return module
155
+
156
+
157
+ def apply_text_kv_cache(module: torch.nn.Module, config: TextKVCacheConfig) -> None:
158
+ from ..models.transformers.transformer_nucleusmoe_image import NucleusMoEImageTransformerBlock
159
+
160
+ HookRegistry.check_if_exists_or_initialize(module)
161
+
162
+ state_manager = StateManager(TextKVCacheState)
163
+
164
+ transformer_hook = TextKVCacheTransformerHook(state_manager)
165
+ registry = HookRegistry.check_if_exists_or_initialize(module)
166
+ registry.register_hook(transformer_hook, _TEXT_KV_CACHE_TRANSFORMER_HOOK)
167
+
168
+ for _, submodule in module.named_modules():
169
+ if isinstance(submodule, NucleusMoEImageTransformerBlock):
170
+ block_state_manager = StateManager(TextKVCacheBlockState)
171
+ hook = TextKVCacheBlockHook(state_manager, block_state_manager)
172
+ block_registry = HookRegistry.check_if_exists_or_initialize(submodule)
173
+ block_registry.register_hook(hook, _TEXT_KV_CACHE_BLOCK_HOOK)