diffusers-workflow 0.11.0__py3-none-any.whl → 0.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/METADATA +1 -1
- {diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/RECORD +24 -24
- dw/docs/ACCELERATION.md +17 -1
- dw/docs/LORAS.md +4 -0
- dw/docs/RECIPES_24GB.md +73 -17
- dw/docs/RELEASING.md +41 -0
- dw/docs/SERVER.md +3 -1
- dw/docs/TASKS.md +1 -1
- dw/docs/WORKER_GUIDE.md +7 -0
- dw/docs/WORKFLOW_GUIDE.md +12 -1
- dw/media.py +23 -5
- dw/pipeline_ownership.py +18 -2
- dw/pipeline_processors/adapters.py +103 -36
- dw/pipeline_processors/components.py +17 -0
- dw/pipeline_processors/pipeline.py +32 -1
- dw/step_cache.py +117 -17
- dw/tasks/restore_faces.py +18 -8
- dw/worker.py +44 -10
- dw/workflow.py +8 -2
- dw/workflow_run.py +41 -7
- {diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/WHEEL +0 -0
- {diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/entry_points.txt +0 -0
- {diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/licenses/LICENSE +0 -0
- {diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
diffusers_workflow-0.11.
|
|
1
|
+
diffusers_workflow-0.11.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
2
2
|
dw/__init__.py,sha256=YKwvXdngbW8iY6xFcfVwDCeiD_oFyCdWb-rRVUogp0Y,19969
|
|
3
3
|
dw/adapter_compatibility.py,sha256=MzNfxK-yOBrn6Mp90m_i_QY-N4zz4C0ZCvKIByUpXAw,10674
|
|
4
4
|
dw/argument_media.py,sha256=fIsvlDRCuTz_DPH4MXFxBZiKTQfgR1vY0-wTU5P9p-I,15145
|
|
@@ -30,13 +30,13 @@ dw/log_setup.py,sha256=ATwSO0CCDtgEA-VoPsS8HVeuDwL2jBg--4QXSZwecQs,1478
|
|
|
30
30
|
dw/lora_catalog.py,sha256=B6RRgdyY0NSCyAwMVoDS3_wxyqF9i3Q3JjEx-hCMVAI,4893
|
|
31
31
|
dw/lora_catalog_schema.json,sha256=i-oh9JR8DbKoNxZ2ufnyPaTaNT8hpeY_I6I3Zrhrl_w,2120
|
|
32
32
|
dw/lora_hub.py,sha256=Px2cV9yAQsKve6WZuvuQCOfgLWjJ1bJx0uDbxeK2kmQ,8500
|
|
33
|
-
dw/media.py,sha256=
|
|
33
|
+
dw/media.py,sha256=bfMHKcqE-STfyS6szwOR0jwpGZjgfwOONqEQF5X9Ul4,43047
|
|
34
34
|
dw/media_frames.py,sha256=2WLLU4Z1NgN4rTxSVDHDs5VZVQqbHXMtOz-ekvonoAA,12789
|
|
35
35
|
dw/media_types.py,sha256=GFnWr8Eq6jU1Y-wccXDKFO5MX-oePAjj8RsE23wvsp8,9770
|
|
36
36
|
dw/outbound.py,sha256=Noaerzmzz-n23JGe9ciMuvo8QQZO-SBOK_gyFXB22Do,20508
|
|
37
37
|
dw/output_extraction.py,sha256=m_uppccZeGPF2lb8UQLCrLHH1PQxmSniB8fuT5TMm_E,10341
|
|
38
38
|
dw/path_redaction.py,sha256=GnZ154Q4X_bvmQEjTVvgcMmlnz1g8N6VkDLj91lLREM,4324
|
|
39
|
-
dw/pipeline_ownership.py,sha256=
|
|
39
|
+
dw/pipeline_ownership.py,sha256=wFzGE6H6tq-2Bzg0EcHkVaNgUU8f_V3HbMMfs1X5woM,13964
|
|
40
40
|
dw/plan.py,sha256=PMu9bRE2tmY69ux6TOwa61f6WHsnC55blS95wZr9r8g,44509
|
|
41
41
|
dw/previous_results.py,sha256=0ReOCNwkHVFhP9VIErvsREVVu3a9RItC33vxhqp4tYw,17361
|
|
42
42
|
dw/probe_paths.py,sha256=iJgy4G256zELyzi4Aol4_qifejjZsUtdeEEDWryAB0o,2683
|
|
@@ -60,7 +60,7 @@ dw/shot_span_preflight.py,sha256=AZP1VY_J9KdjoGdgr3k-H149ojIQr00e6um3XXO3Ga8,431
|
|
|
60
60
|
dw/shots.py,sha256=mLDI_FXHvrG8gIMcY_gJGDLMSUAB8xAomogjA66gcno,21616
|
|
61
61
|
dw/slice_preflight.py,sha256=IJ1NWZPfFBgz_YKU8CvHilasAa6nEIUGu3XW8Vf4gHY,7492
|
|
62
62
|
dw/step.py,sha256=ReYYvfQUKDVtSJ2bcShHDJEmga0YunRIYQV0k1ySSKc,10259
|
|
63
|
-
dw/step_cache.py,sha256=
|
|
63
|
+
dw/step_cache.py,sha256=VDJSH5W6ItzKzFzy0e5rEu7_Rt08WpzXJoyAgaZVbhU,33264
|
|
64
64
|
dw/step_value_checks.py,sha256=60yn46QKVCq4uYwFwkT7z1LB-1C4PpnttlB3m-Yjiag,14212
|
|
65
65
|
dw/subfolders.py,sha256=ZRRxMhHk1Yha9yt_5C_kJfe1YBWJYRba5n2w6Nx30xw,3738
|
|
66
66
|
dw/task_domains.py,sha256=cxfTaECzTOCrFNZQdzgVme6yYk5Hr-P-nHnO8n_zTBo,35379
|
|
@@ -78,46 +78,46 @@ dw/video_size_errors.py,sha256=X6F0LukghxPbOonc7iKVfMNPig2Oiq4sghhhZsR1a4Y,4465
|
|
|
78
78
|
dw/vram_estimate.py,sha256=eTOE9B_9G6sVjZj7mBp0HyxPi09C_uEjQm-qV-hfVOU,22790
|
|
79
79
|
dw/vram_inheritance.py,sha256=JryUJ9k8DduQLKfX2MUkQDea7GsuRYdOty5_qHve2Fc,8369
|
|
80
80
|
dw/window_count_errors.py,sha256=GIifaclu65kV0eiQFP1cscUuHJab80S3KFdP4Beho9k,4876
|
|
81
|
-
dw/worker.py,sha256=
|
|
81
|
+
dw/worker.py,sha256=uh2DIBYxFIOT6vL8j3bxgdsmBUp-jqlZ-NOPmU8cMXA,37032
|
|
82
82
|
dw/worker_manager.py,sha256=MgGZKf_49R8VbyVc5iQrGvCB2PM-rw-J44TyggaTp1Y,12046
|
|
83
83
|
dw/worker_protocol.py,sha256=lz1__T2l6RB72_F7D9atLhEmKOtP7ae0Hydc0TrYTFE,6585
|
|
84
|
-
dw/workflow.py,sha256=
|
|
85
|
-
dw/workflow_run.py,sha256=
|
|
84
|
+
dw/workflow.py,sha256=OyGzzZwtMeF4TLsHuqMTJXsSF1sKNqqNN4sizUjroQc,40640
|
|
85
|
+
dw/workflow_run.py,sha256=PzLhl-4ScKydyAwKtSeKnyYgtSOwpevwW2HrGjBw4cI,42778
|
|
86
86
|
dw/workflow_schema.json,sha256=45oJbjnf3VDor5ZJi00-JJHDKSIduGO9OgD9gFNTjkc,71690
|
|
87
87
|
dw/workspace.py,sha256=8OdAqHjIZGX8MLpuNqzEZcowFXNnHC9kKZot4KNUyyE,25831
|
|
88
88
|
dw/writers.py,sha256=S2-c1zitCHrH7UY8r_O5AmpNFc1eB2IErbAOeSNeWI0,15427
|
|
89
89
|
dw/community_pipelines/pipeline_flux_rf_inversion.py,sha256=XjLO58h5wQn16qP8wEIZKg2Y46mxzQ8qkUu29iQnrrA,50587
|
|
90
90
|
dw/community_pipelines/pipeline_ltx2_refine.py,sha256=kvDBeiaDvt_BX2AjbN6nnYN0EZsEvMzvSC6WzorA_48,6619
|
|
91
|
-
dw/docs/ACCELERATION.md,sha256=
|
|
91
|
+
dw/docs/ACCELERATION.md,sha256=qXqaOROm1RTjYATf1l3kEAnTdcy8aFM6bFCBmRmbI9c,19832
|
|
92
92
|
dw/docs/AGENT_LOOP.md,sha256=rMb_c_r2ucsB7djwtQdp9_VVRagvhevWo-Iep7emmS8,4729
|
|
93
93
|
dw/docs/ARCHITECTURE.md,sha256=WxoTTCSf1oTHdKGij27vL8Irghn5gKvCQ87981lDCUs,62415
|
|
94
94
|
dw/docs/DEPENDENCIES.md,sha256=FKkPm98UeXYBcf26MUpaXkOeoeu3ADyMsC3oP3B4bn4,4455
|
|
95
95
|
dw/docs/IP_ADAPTER.md,sha256=AAHaUIfbnOojHfT0noIeypbhdYPluR7sbZeVn5FpbjU,3730
|
|
96
|
-
dw/docs/LORAS.md,sha256=
|
|
96
|
+
dw/docs/LORAS.md,sha256=iIdMH50hnZtspzG6fXkCkm5-wVmEncb2SvX8SfZyLik,7093
|
|
97
97
|
dw/docs/MCP.md,sha256=_mja-bGds4S4bXHuasTxON1kttOE2RCp86wlRw7Iaok,57495
|
|
98
98
|
dw/docs/PROMPT_WEIGHTING.md,sha256=Pvr25d3yvS8SlnYwX9qc61VvTa8yFMxK_1zMLvrEigI,3031
|
|
99
99
|
dw/docs/QUANTIZATION.md,sha256=NZCL-09bDM-JhzV1WqK2JGkL1la8VoH9fie1X39arsE,7678
|
|
100
|
-
dw/docs/RECIPES_24GB.md,sha256=
|
|
101
|
-
dw/docs/RELEASING.md,sha256=
|
|
100
|
+
dw/docs/RECIPES_24GB.md,sha256=Xu0JNK-PkTnoK0of_0CbjFn-oteVWg2OHNkJ8wLFifE,18867
|
|
101
|
+
dw/docs/RELEASING.md,sha256=7F5be8jR8u_cIa0TvDgJLSFwHVLpxG-9VbaM7Xuu9Tc,54218
|
|
102
102
|
dw/docs/REMOTE.md,sha256=quum2Y54g2bp2rgXg9WkSPY3zhVGHEpMkEAiL6oAQDU,6650
|
|
103
103
|
dw/docs/SECURITY.md,sha256=Y8eIMvJrYPmK6KsMQ9TY6UGE_IjwTU-8EuE13Ikc2uU,20002
|
|
104
104
|
dw/docs/SECURITY_QUICKREF.md,sha256=-xZNIV6j1oWcoy5B-oHKqh3ko2zJgB2fuNMiDV4f1zY,3537
|
|
105
|
-
dw/docs/SERVER.md,sha256=
|
|
106
|
-
dw/docs/TASKS.md,sha256=
|
|
105
|
+
dw/docs/SERVER.md,sha256=UQodcMi5mHkuVQa9qGk2hfPP8bXUsDZD-jCb3OTDD48,57452
|
|
106
|
+
dw/docs/TASKS.md,sha256=gfSzqEJrSspxaewwol5GP0oTknKRqn0vegHpcehF43s,176358
|
|
107
107
|
dw/docs/TESTING.md,sha256=D4zmQp0iVmREM9J1tq4RxTmmIxvxsp5dRtuRGovP96s,3973
|
|
108
|
-
dw/docs/WORKER_GUIDE.md,sha256=
|
|
109
|
-
dw/docs/WORKFLOW_GUIDE.md,sha256=
|
|
108
|
+
dw/docs/WORKER_GUIDE.md,sha256=DHcOrk10Qbb_HBzPzXKsF5lULR4gZJQEWWaa9udx7Mc,2492
|
|
109
|
+
dw/docs/WORKFLOW_GUIDE.md,sha256=wbEvT96wUmOqTDdO0d_nsqzt5o9UjwyBa1D0uUpGodo,149357
|
|
110
110
|
dw/docs/WORKSPACES.md,sha256=PBAuB4zSdPDkkNf8n2D2fc79qClBILpOnzx-CB63FWo,16672
|
|
111
|
-
dw/pipeline_processors/adapters.py,sha256=
|
|
111
|
+
dw/pipeline_processors/adapters.py,sha256=2i9XIbxeDfHWsB5RD4oUbExdg9WJnLTy4VFO8s8lcPE,9705
|
|
112
112
|
dw/pipeline_processors/chain.py,sha256=2KscX3jQttJL6NmEfiQquPGmBjYIGPOaueHbDv0OUHw,41721
|
|
113
|
-
dw/pipeline_processors/components.py,sha256=
|
|
113
|
+
dw/pipeline_processors/components.py,sha256=6NNHoanDpQdiKbNVQ34Aw-B9coOlF8aDPTzXsGsaQho,37001
|
|
114
114
|
dw/pipeline_processors/config_objects.py,sha256=nQYKx00544UzJFlEZ-th45b-8sav1hz3tKJaaBDNJns,10911
|
|
115
115
|
dw/pipeline_processors/h3_guide_steps.py,sha256=wsaV7jFqcoZAEfjjyKYAMust3XgotqMkftlA-Ov5WrE,21580
|
|
116
116
|
dw/pipeline_processors/h3_guides.py,sha256=DAhNOzOUD9i33Iopd-9nG3H5jaJPbDnEtqkP8-_GJDk,12445
|
|
117
117
|
dw/pipeline_processors/h3_hold.py,sha256=dwpUuT8NuAVo77_4YVBAvn3G5oBW32e6M6i-cds66oU,9912
|
|
118
118
|
dw/pipeline_processors/h3_hold_steps.py,sha256=mjoWAYHrVhQbpcXCzDVHhOXtCH50wXSUJjTmRhAGfzw,16437
|
|
119
119
|
dw/pipeline_processors/h3_rules.py,sha256=Kpm4xeO7oiM2hdECjvnPg_NZTgCO-vBxs2jHtNW08iA,9244
|
|
120
|
-
dw/pipeline_processors/pipeline.py,sha256=
|
|
120
|
+
dw/pipeline_processors/pipeline.py,sha256=93ABX25UQ3EfZg4eSk8DonfMv2z94Ww4rmyzLIkefKA,46141
|
|
121
121
|
dw/pipeline_processors/placement.py,sha256=D-7ZMNs2P8t81CTtwc_APVrlk_8bz7Ek0M-Hb8wFhV4,16342
|
|
122
122
|
dw/pipeline_processors/progress.py,sha256=uSKPsMug1AZqDoaFXsF5sRcJNiNLzbBuX3C3H3xVzms,9196
|
|
123
123
|
dw/pipeline_processors/remote.py,sha256=eZl6fPEKUrd1ieFgX94QNKAR4ziPH9JXTV_Hse4VsqY,2450
|
|
@@ -293,7 +293,7 @@ dw/tasks/model_handlers.py,sha256=JbAVIZVQUr3r4y3p8HyQHKw0dUksRPpCPKsw13qwkX4,11
|
|
|
293
293
|
dw/tasks/pair_audio.py,sha256=5q56WfKh5srvjLx_sjXBpl-Pk_0qvdMKULT-CJXKkY8,17842
|
|
294
294
|
dw/tasks/qr_code.py,sha256=xyckWTixcMYasJPI5N7-co4jmyHwfScGWD2D1RbFzuM,548
|
|
295
295
|
dw/tasks/registry.py,sha256=nk8mcqkpg-OBdiN4K7Go9lpJVR2mQREmxv4wTzB7k1c,9107
|
|
296
|
-
dw/tasks/restore_faces.py,sha256=
|
|
296
|
+
dw/tasks/restore_faces.py,sha256=Q9TzuwWdT5H7kZxcgTCitCCLc8clI2T6Qd6azfaoKx0,6926
|
|
297
297
|
dw/tasks/rife_model.py,sha256=eO77oSH4OGY5pXBp6UDaXIDbGPlOHPIiXMrf1RMzYjo,6387
|
|
298
298
|
dw/tasks/script_check.py,sha256=S8YdJ1bPKdiYh-m5YmIXQ_r_aJGGel8TZquMHDYWXm0,28478
|
|
299
299
|
dw/tasks/segment.py,sha256=2g-jndKDlIcVMH4OApWNlWzt-8DzQvhK82_TNMSXoTI,4267
|
|
@@ -333,8 +333,8 @@ dw_mcp/tools_catalog.py,sha256=kpa75deedQEFI90RHKjjqrNY9pFA4E5KtbtOogGwFtY,21962
|
|
|
333
333
|
dw_mcp/tools_jobs.py,sha256=Hk7MNtd-GA-he6VIepOJHkLnHD594IEMNuriqj88MEk,12015
|
|
334
334
|
dw_mcp/tools_media.py,sha256=YovyYnkRzhAjIQ54Sl12c-lOy9Jq5vrQWVRxd015ehU,16909
|
|
335
335
|
dw_mcp/workspaces.py,sha256=OF6oVMQx_pkTuRMLvbOYkpVWwcA6GQwaEf5k0oxqBnI,8449
|
|
336
|
-
diffusers_workflow-0.11.
|
|
337
|
-
diffusers_workflow-0.11.
|
|
338
|
-
diffusers_workflow-0.11.
|
|
339
|
-
diffusers_workflow-0.11.
|
|
340
|
-
diffusers_workflow-0.11.
|
|
336
|
+
diffusers_workflow-0.11.1.dist-info/METADATA,sha256=_ghxhXxXWRSceZi1J8ZmUtvdVSWhep5QWSvAP4Mqwbw,14939
|
|
337
|
+
diffusers_workflow-0.11.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
338
|
+
diffusers_workflow-0.11.1.dist-info/entry_points.txt,sha256=Dem_AOzV-Kcpr9AASlSvJ-S5sn3f6TQu6ABb6BbApho,148
|
|
339
|
+
diffusers_workflow-0.11.1.dist-info/top_level.txt,sha256=o-uFAN_oUOrhXfMKO39HsMs2r19L9LykxoVKj2S9q7U,10
|
|
340
|
+
diffusers_workflow-0.11.1.dist-info/RECORD,,
|
dw/docs/ACCELERATION.md
CHANGED
|
@@ -136,6 +136,18 @@ Select the attention implementation diffusers uses for the duration of each pipe
|
|
|
136
136
|
|
|
137
137
|
Common values: `"flash"`, `"flash_hub"`, `"sage"`, `"sage_hub"`, `"native"`, `"flex"`. The full set is diffusers' `AttentionBackendName` enum - availability depends on what's installed (`flash-attn`, `sageattention`, etc.) and the platform. `_hub`-suffixed backends are fetched from the Hugging Face Hub kernel registry on first use, which needs the `kernels` package installed (`pip install kernels`) - it is not a dw dependency, and no bundled workflow sets a backend, so each runs on a plain install. diffusers also refuses a Hub kernel whose publisher it does not list as trusted - `sage_hub` among them, since SageAttention 2 comes from `SageAttention/sage-attention` - until `DIFFUSERS_TRUST_REMOTE_KERNELS=true`, which allows remote code from any such publisher. On an RTX 3090 `sage_hub` measured about 30% faster per MiniMax-H3 step (13.8 s against 19.2 s at 960x544x124, 54.7 s against 81.2 s at 345 frames); the local `sage` backend needs `sageattention>=2.1.1`, which PyPI does not carry.
|
|
138
138
|
|
|
139
|
+
The MiniMax-H3 baselines expose this as a variable: `attention_backend` on
|
|
140
|
+
[video-with-audio.json](../workflows/templates/minimax/video-with-audio.json),
|
|
141
|
+
[video-with-audio-768p.json](../workflows/templates/minimax/video-with-audio-768p.json),
|
|
142
|
+
[image-to-video.json](../workflows/templates/minimax/image-to-video.json) and
|
|
143
|
+
[reference-to-video.json](../workflows/templates/minimax/reference-to-video.json), pinned
|
|
144
|
+
on the transformer and `null` by default. Pass `attention_backend=sage_hub` to a box with
|
|
145
|
+
the `kernels` package and `DIFFUSERS_TRUST_REMOTE_KERNELS=true` in its `.env`; on MPS
|
|
146
|
+
leave it null. The other H3 templates take the same key under `components.transformer`
|
|
147
|
+
(`transformer_ref` for a ref2va workflow). LTX-2.5 cannot use it: its transformer
|
|
148
|
+
passes an attention mask, and sage attention refuses one ("`attn_mask` is not supported
|
|
149
|
+
for sage attention", measured 2026-10-10), so the LTX templates expose no backend.
|
|
150
|
+
|
|
139
151
|
A component can also pin its backend persistently instead, via `set_attention_backend`:
|
|
140
152
|
|
|
141
153
|
```json
|
|
@@ -232,7 +244,11 @@ Three older per-component knobs, set in the pipeline `configuration` beside
|
|
|
232
244
|
|
|
233
245
|
## Memory Offloading
|
|
234
246
|
|
|
235
|
-
`offload` (`"model"` or `"sequential"`) and `group_offload` trade speed for VRAM by streaming weights between system memory and the accelerator instead of keeping everything resident. `"model"` moves whole submodules and costs the least speed; `"sequential"` moves individual layers and is the slowest but uses the least memory; block/leaf-level `group_offload` sits between the two and is what a modular pipeline's self-loaded components use, since they aren't reachable in time for `offload`. Full configuration syntax is in [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#memory-offloading). Omit both for the fastest run, when VRAM allows it.
|
|
247
|
+
`offload` (`"model"` or `"sequential"`) and `group_offload` trade speed for VRAM by streaming weights between system memory and the accelerator instead of keeping everything resident. `"model"` moves whole submodules and costs the least speed; `"sequential"` moves individual layers and is the slowest but uses the least memory; block/leaf-level `group_offload` sits between the two and is what a modular pipeline's self-loaded components use, since they aren't reachable in time for `offload`. Full configuration syntax is in [WORKFLOW_GUIDE.md](WORKFLOW_GUIDE.md#memory-offloading). Omit both for the fastest run, when VRAM allows it. Measured on an RTX 3090 (RECIPES_24GB.md): sequential
|
|
248
|
+
offload costs about 6x per step on FLUX dev and 3.4x on Z-Image against model offload;
|
|
249
|
+
a block-level, streamed `group_offload` of the transformer from pinned host memory
|
|
250
|
+
matches the resident per-step cost while holding one block on the card, at ~22 GB of
|
|
251
|
+
pinned host RAM per FLUX pipeline - the placement the unquantized FLUX templates use.
|
|
236
252
|
|
|
237
253
|
`"residency": "on_demand"` on a component is the cheap case of the same trade: the model rests in system memory and is moved to the device whole around each of its own calls. That is a bad deal for anything called once per step, and a good one for a VAE called twice a run - it frees the VAE's VRAM for the denoise loop at the cost of two transfers, where group offloading the same VAE would restream it once per decode tile. See [On-demand components](WORKFLOW_GUIDE.md#on-demand-components).
|
|
238
254
|
|
dw/docs/LORAS.md
CHANGED
|
@@ -48,6 +48,10 @@ LoRA (Low-Rank Adaptation) models apply lightweight style or subject modificatio
|
|
|
48
48
|
|
|
49
49
|
Any other property (e.g. `revision`) is forwarded as-is to the underlying `load_lora_weights()` call.
|
|
50
50
|
|
|
51
|
+
`scale` and `alpha` are set in place on the loaded adapter, so a server run that changes
|
|
52
|
+
only them reuses the warm pipeline instead of reloading it; changing `model_name` or
|
|
53
|
+
`weight_name` reloads.
|
|
54
|
+
|
|
51
55
|
## Multiple LoRAs
|
|
52
56
|
|
|
53
57
|
Stack multiple LoRAs. They are blended via weighted adapter composition:
|
dw/docs/RECIPES_24GB.md
CHANGED
|
@@ -12,23 +12,79 @@ The general recipe, in order of impact:
|
|
|
12
12
|
|
|
13
13
|
## Flux dev (12B)
|
|
14
14
|
|
|
15
|
-
The bf16 transformer is ~
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
| **
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
The bf16 transformer is ~22 GB, so on 24 GB it cannot sit beside its own activations
|
|
16
|
+
with any margin, and it cannot sit beside the T5 encoder or the VAE at all. Three
|
|
17
|
+
placements, measured on an RTX 3090 (2026-10-10, cold load included):
|
|
18
|
+
|
|
19
|
+
| Placement | 1024x1024, 25 steps, 1 image, dev + LoRA | 768x768, 25 steps, 4 images, Krea | 1232x1632, 50 steps, Fill |
|
|
20
|
+
| --------- | ----------------------------------------- | --------------------------------- | ------------------------- |
|
|
21
|
+
| `offload: "sequential"` | 273 s (10.8 s/step) | not measured - same per-step cost | not measured |
|
|
22
|
+
| `offload: "model"` | 61 s (1.7 s/step) at **23.3 GB reserved, 491 MB free** | **OOM** at 23.45 GiB | not attempted |
|
|
23
|
+
| **transformer `group_offload`, block-level, streamed, pinned host copies** | 78 s (1.5 s/step), 17.7 GB peak reserved, 6 GB free | 131 s (3.2 s/step) | 152 s denoise (3.0 s/step), 1 s decode |
|
|
24
|
+
|
|
25
|
+
Model offload is the fastest placement that loads, and the one #580 (9c708a12) retired:
|
|
26
|
+
at 1024x1024 it runs with a few hundred MB to spare and fails on anything larger -
|
|
27
|
+
a batch, a second encoder resident for prompt weighting, a cache's state. Sequential
|
|
28
|
+
offload is safe everywhere and 6x slower per step. The streamed group offload is both:
|
|
29
|
+
the transformer's blocks are copied in one at a time from pinned host memory under a
|
|
30
|
+
CUDA stream, so each step costs the same as the resident model's, and the card holds
|
|
31
|
+
one block plus the activations. It is what every unquantized FLUX template now uses:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"configuration": {
|
|
35
|
+
"component_type": "FluxPipeline",
|
|
36
|
+
"components": {
|
|
37
|
+
"transformer": {
|
|
38
|
+
"group_offload": {
|
|
39
|
+
"offload_type": "block_level",
|
|
40
|
+
"num_blocks_per_group": 1,
|
|
41
|
+
"use_stream": true,
|
|
42
|
+
"record_stream": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"text_encoder": { "device": "cuda" },
|
|
46
|
+
"text_encoder_2": { "device": "cuda" },
|
|
47
|
+
"vae": { "device": "cuda" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Two things the shape depends on. The non-offloaded components are placed explicitly:
|
|
53
|
+
with no pipeline-level `offload`, nothing else moves them, and a T5 or VAE left on the
|
|
54
|
+
CPU costs a minute per prompt encode and per decode (measured: 70 s before the first
|
|
55
|
+
step, 185 s decoding). And the host copies are pinned - diffusers' default,
|
|
56
|
+
`low_cpu_mem_usage: false` - which holds ~22 GB of unswappable host memory per loaded
|
|
57
|
+
FLUX pipeline and takes ~35 s once per load; with `low_cpu_mem_usage: true` the copies
|
|
58
|
+
come from pageable memory at 8 s/step, barely ahead of sequential. A host with less
|
|
59
|
+
than 32 GB free should take `offload: "sequential"` instead.
|
|
60
|
+
|
|
61
|
+
Compile and caching stack on top as before: `compile: {repeated_blocks: true}` measured
|
|
62
|
+
52 s vs 55 s per image on the old model-offload placement; `cache: first_block` is
|
|
63
|
+
[step-caching.json](../workflows/templates/step-caching.json). TorchAO int8 is over 60 s
|
|
64
|
+
per *denoising step* on Ampere - do not use it there; float8 needs compute capability
|
|
65
|
+
8.9+ (Ada) and is unmeasured; GGUF Q8 ([flux-gguf.json](../workflows/models/flux-gguf.json))
|
|
66
|
+
loads with the least VRAM and is unmeasured for speed.
|
|
67
|
+
|
|
68
|
+
**Examples:** [lora.json](../workflows/templates/lora.json), [flux-dev-compile.json](../workflows/models/flux-dev-compile.json), [flux-gguf.json](../workflows/models/flux-gguf.json), [step-caching.json](../workflows/templates/step-caching.json)
|
|
69
|
+
|
|
70
|
+
## Z-Image Turbo (6B)
|
|
71
|
+
|
|
72
|
+
Fits a 24 GB card in bf16 with model offload, and that is the configuration to use.
|
|
73
|
+
Measured on an RTX 3090 (960x544, 9 steps, 4 images per prompt, cold load included,
|
|
74
|
+
2026-10-10):
|
|
75
|
+
|
|
76
|
+
| Offload | Load | Denoise | Decode | Total | Peak VRAM reserved |
|
|
77
|
+
| ------- | ---- | ------- | ------ | ----- | ------------------ |
|
|
78
|
+
| `sequential` | 18 s | 84 s | 2 s | 107 s | 1.1 GB |
|
|
79
|
+
| **`model`** | 4 s | 25 s | 9 s | 41 s | 13.0 GB |
|
|
80
|
+
| none (resident) | 11 s | 17 s | 2 s | 33 s | 23.7 GB, 95 MB free at decode |
|
|
81
|
+
|
|
82
|
+
Sequential offload streams the transformer's layers every step and costs 3.4x on the
|
|
83
|
+
denoise for a model that does not need the room. Resident is faster still but decodes
|
|
84
|
+
four images with nothing to spare, so [z-image.json](../workflows/models/z-image.json)
|
|
85
|
+
and the Z-Image portrait steps of the MiniMax templates use `"offload": "model"`. The
|
|
86
|
+
decode's 9 s under model offload is the VAE swapping in; `vae.enable_slicing` would
|
|
87
|
+
trade it for per-image decodes.
|
|
32
88
|
|
|
33
89
|
## Qwen-Image (20B)
|
|
34
90
|
|
dw/docs/RELEASING.md
CHANGED
|
@@ -7,6 +7,47 @@ notes from commits at tag time (see below). This section is a scratch pad
|
|
|
7
7
|
for items a branch's author wants the next release note to name; clear it
|
|
8
8
|
when a release ships.
|
|
9
9
|
|
|
10
|
+
### 0.11.1
|
|
11
|
+
|
|
12
|
+
<!-- Drafted from v0.11.0..develop. Paste into the GitHub release body once the tag has published: gh release edit v0.11.1 --notes-file ... -->
|
|
13
|
+
|
|
14
|
+
A speed release. The unquantized FLUX templates run about 3.5x faster on
|
|
15
|
+
a 24 GB card, Z-Image Turbo about 2.6x, a LoRA scale or scheduler shift
|
|
16
|
+
change no longer reloads the model, and switching workflows keeps the
|
|
17
|
+
models the next one shares. Every number below was measured on an RTX 3090;
|
|
18
|
+
the placement tables are in docs/RECIPES_24GB.md.
|
|
19
|
+
|
|
20
|
+
**Workflow speed**
|
|
21
|
+
|
|
22
|
+
- The 14 unquantized FLUX templates and `models/flux-krea` stream the
|
|
23
|
+
transformer block by block from pinned host memory (`group_offload`)
|
|
24
|
+
instead of `offload: "sequential"`: a 25-step 1024x1024 image takes 78 s
|
|
25
|
+
instead of 273 s (1.5 s/step against 10.8), with 6 GB of VRAM to spare -
|
|
26
|
+
enough for the 4-image batches `offload: "model"` runs out of memory on.
|
|
27
|
+
It holds ~22 GB of pinned host RAM per loaded FLUX pipeline.
|
|
28
|
+
- Z-Image Turbo (`models/z-image` and the MiniMax portrait steps) takes
|
|
29
|
+
`offload: "model"`: four 960x544 images in 41 s instead of 107 s.
|
|
30
|
+
- The four MiniMax-H3 family baselines expose `attention_backend` (null by
|
|
31
|
+
default; `sage_hub` measured about 30% faster per step on a 3090; it needs
|
|
32
|
+
the `kernels` package and `DIFFUSERS_TRUST_REMOTE_KERNELS=true`, see
|
|
33
|
+
docs/ACCELERATION.md). LTX-2.5 cannot use sage attention (its attention
|
|
34
|
+
mask is refused).
|
|
35
|
+
- A LoRA `scale`/`alpha` change or a scheduler `shift` change re-applies on
|
|
36
|
+
the warm pipeline instead of reloading it. Removing an alpha or shift still
|
|
37
|
+
reloads, and a `pipeline_reference` runs at the referenced step's own values.
|
|
38
|
+
- Switching workflows in the worker keeps the warm pipelines the next
|
|
39
|
+
workflow loads anyway, instead of releasing everything first.
|
|
40
|
+
- `restore_faces` builds its face helper once per run instead of once per
|
|
41
|
+
frame, and picture frames decode with libav threading, so video inputs
|
|
42
|
+
process faster.
|
|
43
|
+
- Catalog defaults corrected: compose-workflows video steps run 5 steps
|
|
44
|
+
(was 20) and the prompt-weighting schnell workflow runs 4 steps at guidance
|
|
45
|
+
0 (was 20).
|
|
46
|
+
- The six 9-step minimax ref2va templates drop a dead `first_block` cache,
|
|
47
|
+
and minimax/shots-batch drops its `release_pipeline`: a no-op inside the
|
|
48
|
+
run, but it unloaded H3 at job end, so H3 now stays resident after a
|
|
49
|
+
shots-batch job, which is the win.
|
|
50
|
+
|
|
10
51
|
### 0.11.0
|
|
11
52
|
|
|
12
53
|
<!-- Drafted from v0.10.0..develop. Paste into the GitHub release body once the tag has published: gh release edit v0.11.0 --notes-file ... -->
|
dw/docs/SERVER.md
CHANGED
|
@@ -222,7 +222,9 @@ Among the free cards a job fits, dispatch prefers the one it has an affinity
|
|
|
222
222
|
for: a rerun goes to the card that ran the original, and any job to the card
|
|
223
223
|
whose worker last ran the same workflow (the same file, or an inline
|
|
224
224
|
definition's `id`) - that worker's pipelines and step cache are warm, and a
|
|
225
|
-
worker
|
|
225
|
+
worker's step cache and any model the next workflow does not load are freed
|
|
226
|
+
when the workflow changes, while a model both workflows load (same weights
|
|
227
|
+
identity) stays warm. `clear_memory` still empties the card by hand. A busy preferred card is not
|
|
226
228
|
waited for. A validation's cache probe and its `plan.estimate` ask about the
|
|
227
229
|
card the job would be dispatched to now, and the estimate names it in
|
|
228
230
|
`priced_for`. Memory is per card: `GET /api/memory` and `POST
|
dw/docs/TASKS.md
CHANGED
|
@@ -2632,7 +2632,7 @@ Restore and enhance faces in images using spandrel-compatible face restoration m
|
|
|
2632
2632
|
| `eye_dist_threshold` | No | Skip faces with eye distance below this (default: 5) |
|
|
2633
2633
|
| `upsample_img` | No | Pre-upscaled background image (e.g., from a prior upscale step) |
|
|
2634
2634
|
|
|
2635
|
-
Models are loaded via spandrel, so any `.pth`/`.safetensors` face restoration weights work. CodeFormer requires `pip install spandrel-extra-arches` (non-commercial license).
|
|
2635
|
+
Models are loaded via spandrel, so any `.pth`/`.safetensors` face restoration weights work. CodeFormer requires `pip install spandrel-extra-arches` (non-commercial license). The detector and parser are loaded once per settings/device and reused across frames; `release_models` drops them with the restorer.
|
|
2636
2636
|
|
|
2637
2637
|
**Example:** [restore-faces.json](../workflows/templates/restore-faces.json) — Generate a portrait, then restore faces with GFPGAN v1.4.
|
|
2638
2638
|
|
dw/docs/WORKER_GUIDE.md
CHANGED
|
@@ -28,6 +28,13 @@ The worker cleans up automatically between runs (garbage collection + GPU
|
|
|
28
28
|
cache clearing). If memory grows unexpectedly, `GET /api/memory` reports the
|
|
29
29
|
current reading and `POST /api/memory/clear` resets it.
|
|
30
30
|
|
|
31
|
+
Switching to a different workflow releases only the models the next workflow
|
|
32
|
+
does not load: the worker prepares the incoming definition, and a pipeline
|
|
33
|
+
both workflows load (same weights identity) stays warm. The step cache and
|
|
34
|
+
the task model cache are still emptied, and if the incoming definition cannot
|
|
35
|
+
be prepared everything is released. `clear_memory` (`POST /api/memory/clear`)
|
|
36
|
+
remains the way to empty the card by hand.
|
|
37
|
+
|
|
31
38
|
## Troubleshooting
|
|
32
39
|
|
|
33
40
|
**Worker crashes**: `JobManager` detects it and starts a fresh worker on the
|
dw/docs/WORKFLOW_GUIDE.md
CHANGED
|
@@ -112,6 +112,10 @@ inverted latents:
|
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
`reference_name` must name a step earlier in the same workflow that has a `pipeline`.
|
|
115
|
+
Two steps whose pipelines differ only in a LoRA's `scale` or `alpha` or a scheduler's
|
|
116
|
+
`shift` share one warm model, and a reference always runs at the referenced step's own
|
|
117
|
+
values; `release_pipeline` on either step frees the shared model, as it does for two
|
|
118
|
+
identical steps.
|
|
115
119
|
See [workflows/templates/community-pipeline.json](../workflows/templates/community-pipeline.json) for a full example.
|
|
116
120
|
|
|
117
121
|
### Task Steps
|
|
@@ -1173,7 +1177,7 @@ Control how models use memory:
|
|
|
1173
1177
|
```
|
|
1174
1178
|
|
|
1175
1179
|
- `"model"` — Moves entire models between CPU and GPU. Good balance of speed and memory.
|
|
1176
|
-
- `"sequential"` — Moves individual layers. Slowest but uses least GPU memory. On MPS it is downgraded to `"model"` with a warning: with unified memory there is no separate pool to keep small, so the per-layer copies cost speed and save nothing.
|
|
1180
|
+
- `"sequential"` — Moves individual layers. Slowest but uses least GPU memory (measured at 6x the per-step cost of model offload on FLUX dev and 3.4x on Z-Image; a streamed block-level `group_offload` of the transformer is the usual better answer - see [RECIPES_24GB.md](RECIPES_24GB.md#flux-dev-12b)). On MPS it is downgraded to `"model"` with a warning: with unified memory there is no separate pool to keep small, so the per-layer copies cost speed and save nothing.
|
|
1177
1181
|
`exclude_from_cpu_offload` names components the sweep should leave alone.
|
|
1178
1182
|
- Omit for no offloading (fastest, requires enough VRAM).
|
|
1179
1183
|
|
|
@@ -1325,6 +1329,13 @@ The step-level `release_pipeline` flag unloads the step's pipeline after its res
|
|
|
1325
1329
|
saved. A later `pipeline_reference` to a released step is an error, and the process-wide
|
|
1326
1330
|
step cache will not retain it.
|
|
1327
1331
|
|
|
1332
|
+
What keeps a pipeline warm: the server's worker keeps a pipeline loaded across runs for as
|
|
1333
|
+
long as what it *loads* is unchanged. A LoRA's `scale` and `alpha` and a scheduler's (or
|
|
1334
|
+
`audio_scheduler`'s) `shift` are applied in place on the warm pipeline, so iterating on
|
|
1335
|
+
them costs no reload; removing (nulling) an alpha or a shift reloads, so the checkpoint
|
|
1336
|
+
default comes back. Changing a LoRA's `model_name` or `weight_name`, the quantization,
|
|
1337
|
+
the placement or the scheduler type reloads it.
|
|
1338
|
+
|
|
1328
1339
|
#### Releasing task models mid-workflow
|
|
1329
1340
|
|
|
1330
1341
|
Task models - the checkpoints behind `text_generation`, `segment`, `depth_estimator` and
|
dw/media.py
CHANGED
|
@@ -33,6 +33,15 @@ from .dsp import SILENCE_DBFS, dbfs, integrated_lufs, layout_name, true_peak_dbf
|
|
|
33
33
|
|
|
34
34
|
logger = logging.getLogger("dw")
|
|
35
35
|
|
|
36
|
+
|
|
37
|
+
def _threaded(stream):
|
|
38
|
+
"""A video stream set to decode on every core. libav defaults a
|
|
39
|
+
software h264 decode to one thread; AUTO lets it pick frame and
|
|
40
|
+
slice threading for the codec. Header-only reads do not call this."""
|
|
41
|
+
stream.thread_type = "AUTO"
|
|
42
|
+
return stream
|
|
43
|
+
|
|
44
|
+
|
|
36
45
|
# The most a soundtrack may be as base64 before the gallery route refuses to
|
|
37
46
|
# extract it whole - the twin of dw_mcp/media.py's MAX_RETURNED_BYTES (the
|
|
38
47
|
# MCP package's cap on any inline payload). Two constants because dw_mcp
|
|
@@ -345,7 +354,7 @@ def video_shape(path):
|
|
|
345
354
|
fps = float(stream.average_rate) if stream.average_rate else None
|
|
346
355
|
count = int(stream.frames) if stream.frames else None
|
|
347
356
|
if count is None:
|
|
348
|
-
count = sum(1 for _ in container.decode(stream))
|
|
357
|
+
count = sum(1 for _ in container.decode(_threaded(stream)))
|
|
349
358
|
return {
|
|
350
359
|
"frame_count": count,
|
|
351
360
|
"fps": fps,
|
|
@@ -383,6 +392,8 @@ def read_frames(path, indexes, fit=None):
|
|
|
383
392
|
wanted = sorted(set(int(i) for i in indexes))
|
|
384
393
|
found = {}
|
|
385
394
|
with av.open(path) as container:
|
|
395
|
+
# Not _threaded: frame threads hold frames in flight, and a backward
|
|
396
|
+
# seek after a decoded frame then yields stale ones (tests/test_media_decode_threads.py)
|
|
386
397
|
stream = container.streams.video[0]
|
|
387
398
|
fps = float(stream.average_rate) if stream.average_rate else None
|
|
388
399
|
start_pts = stream.start_time if stream.start_time is not None else 0
|
|
@@ -479,6 +490,8 @@ def read_frame_range(path, start, stop):
|
|
|
479
490
|
)
|
|
480
491
|
out = None
|
|
481
492
|
with av.open(path) as container:
|
|
493
|
+
# Not _threaded: frame threads hold frames in flight, and a backward
|
|
494
|
+
# seek after a decoded frame then yields stale ones (tests/test_media_decode_threads.py)
|
|
482
495
|
stream = container.streams.video[0]
|
|
483
496
|
fps = float(stream.average_rate) if stream.average_rate else None
|
|
484
497
|
start_pts = stream.start_time if stream.start_time is not None else 0
|
|
@@ -513,7 +526,7 @@ def count_video_frames(path):
|
|
|
513
526
|
"""The picture frame count of `path`, by decoding every frame rather
|
|
514
527
|
than reading the header. Nothing is held but the frame in hand."""
|
|
515
528
|
with av.open(path) as container:
|
|
516
|
-
return sum(1 for _ in container.decode(container.streams.video[0]))
|
|
529
|
+
return sum(1 for _ in container.decode(_threaded(container.streams.video[0])))
|
|
517
530
|
|
|
518
531
|
|
|
519
532
|
def decode_audio_video(handle):
|
|
@@ -531,7 +544,7 @@ def decode_audio_video(handle):
|
|
|
531
544
|
sample_rate = None
|
|
532
545
|
|
|
533
546
|
with av.open(handle) as container:
|
|
534
|
-
video_stream = container.streams.video[0]
|
|
547
|
+
video_stream = _threaded(container.streams.video[0])
|
|
535
548
|
frame_rate = (
|
|
536
549
|
float(video_stream.average_rate) if video_stream.average_rate else None
|
|
537
550
|
)
|
|
@@ -559,7 +572,10 @@ def decode_rgb_frames(path):
|
|
|
559
572
|
"""Every picture frame of `path` as an (height, width, 3) uint8 array,
|
|
560
573
|
in order."""
|
|
561
574
|
with av.open(path) as container:
|
|
562
|
-
return [
|
|
575
|
+
return [
|
|
576
|
+
frame.to_ndarray(format="rgb24")
|
|
577
|
+
for frame in container.decode(_threaded(container.streams.video[0]))
|
|
578
|
+
]
|
|
563
579
|
|
|
564
580
|
|
|
565
581
|
def read_thumbnails_and_track(path, thumb_width, thumb_height):
|
|
@@ -575,7 +591,9 @@ def read_thumbnails_and_track(path, thumb_width, thumb_height):
|
|
|
575
591
|
thumbs = []
|
|
576
592
|
chunks = []
|
|
577
593
|
with av.open(path) as container:
|
|
578
|
-
video =
|
|
594
|
+
video = (
|
|
595
|
+
_threaded(container.streams.video[0]) if container.streams.video else None
|
|
596
|
+
)
|
|
579
597
|
audio = container.streams.audio[0] if container.streams.audio else None
|
|
580
598
|
if video is None and audio is None:
|
|
581
599
|
raise ValueError(f"{path} has neither a video nor an audio stream")
|
dw/pipeline_ownership.py
CHANGED
|
@@ -62,6 +62,11 @@ class PipelineOwnership:
|
|
|
62
62
|
# pipeline_reference still address pipelines by the step that made
|
|
63
63
|
# them
|
|
64
64
|
self.keys_by_step = {}
|
|
65
|
+
# Step name -> the step's pipeline definition as the workflow holds
|
|
66
|
+
# it (never a loading copy), so a pipeline_reference can put the
|
|
67
|
+
# referenced step's own scale, alpha and shift back on a model a
|
|
68
|
+
# step differing only in those shares with it
|
|
69
|
+
self.definitions_by_step = {}
|
|
65
70
|
# Step name -> _Deferred for each cache hit whose pipeline was not
|
|
66
71
|
# resident and so was not loaded
|
|
67
72
|
self.deferred = {}
|
|
@@ -92,9 +97,16 @@ class PipelineOwnership:
|
|
|
92
97
|
)
|
|
93
98
|
return self.running[step_name]
|
|
94
99
|
|
|
95
|
-
def record(self, step_name, key):
|
|
96
|
-
"""Record which cache key a step's pipeline lives under this run
|
|
100
|
+
def record(self, step_name, key, pipeline_definition=None):
|
|
101
|
+
"""Record which cache key a step's pipeline lives under this run,
|
|
102
|
+
and the definition it was loaded or wrapped from."""
|
|
97
103
|
self.keys_by_step[step_name] = key
|
|
104
|
+
if pipeline_definition is not None:
|
|
105
|
+
self.definitions_by_step[step_name] = pipeline_definition
|
|
106
|
+
|
|
107
|
+
def definition_for(self, step_name):
|
|
108
|
+
"""The pipeline definition `step_name` recorded this run, or None."""
|
|
109
|
+
return self.definitions_by_step.get(step_name)
|
|
98
110
|
|
|
99
111
|
def key_for(self, step_name):
|
|
100
112
|
"""The cache key `step_name` recorded this run, or None."""
|
|
@@ -286,6 +298,10 @@ def wrap_resident(
|
|
|
286
298
|
new_pipeline_wrapper.pipeline_definition.get("seed", default_seed)
|
|
287
299
|
)
|
|
288
300
|
|
|
301
|
+
# The key the hit matched on is the weights identity: a scale, alpha or
|
|
302
|
+
# shift may differ from the values the resident model last ran with
|
|
303
|
+
new_pipeline_wrapper.apply_runtime_settings()
|
|
304
|
+
|
|
289
305
|
# A cache hit and a cold load look identical from the outside -
|
|
290
306
|
# same step, same dot - and they differ by minutes
|
|
291
307
|
emit_phase("cached", detail=new_pipeline_wrapper.name)
|
|
@@ -101,11 +101,103 @@ def active_loras(loras):
|
|
|
101
101
|
]
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
def adapter_settings(loras):
|
|
105
|
+
"""(adapter_names, adapter_weights, alphas) for the active entries of
|
|
106
|
+
`loras`. An adapter is named by its own adapter_name or its ORIGINAL
|
|
107
|
+
index - a nulled entry keeps its slot, so the name a hit re-applies to
|
|
108
|
+
is the name the load gave. Reads the entries, never pops them.
|
|
109
|
+
|
|
110
|
+
`or` on adapter_name, because a variable nulled by the caller arrives as
|
|
111
|
+
a present None; float() on scale, because the schema takes a 'variable:'
|
|
112
|
+
reference there, and a variable declared as a string default substitutes
|
|
113
|
+
as one."""
|
|
114
|
+
names, weights, alphas = [], [], {}
|
|
115
|
+
for i, lora in enumerate(loras or []):
|
|
116
|
+
if not isinstance(lora, dict) or lora.get("model_name") is None:
|
|
117
|
+
continue
|
|
118
|
+
name = lora.get("adapter_name") or str(i)
|
|
119
|
+
names.append(name)
|
|
120
|
+
scale = lora.get("scale")
|
|
121
|
+
weights.append(1.0 if scale is None else float(scale))
|
|
122
|
+
if lora.get("alpha") is not None:
|
|
123
|
+
alphas[name] = lora["alpha"]
|
|
124
|
+
return names, weights, alphas
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def set_adapter_scales(pipeline, names, weights):
|
|
128
|
+
"""Set each named adapter's scale on every loaded layer that carries it,
|
|
129
|
+
without re-activating the adapters.
|
|
130
|
+
|
|
131
|
+
The warm-pipeline path. diffusers' set_adapters() activates as well as
|
|
132
|
+
scales, and activation goes through peft's set_adapter(), which sets
|
|
133
|
+
requires_grad on the adapter weights - after a run under model offload
|
|
134
|
+
those are inference tensors, and torch refuses ("Setting requires_grad=True
|
|
135
|
+
on inference tensor outside InferenceMode" - seen on a 3090 the first time
|
|
136
|
+
a scale changed on a warm Flux pipeline, 2026-10-10).
|
|
137
|
+
A pipeline that already ran holds the adapters active, so a hit only
|
|
138
|
+
needs each layer's set_scale(), which recomputes scale * alpha / rank and
|
|
139
|
+
touches no tensor.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: if an adapter is carried by no loaded layer - a scale
|
|
143
|
+
that applied to nothing would silently run the old value
|
|
144
|
+
"""
|
|
145
|
+
touched = {name: 0 for name in names}
|
|
146
|
+
for module in _lora_layers(pipeline):
|
|
147
|
+
scaling = getattr(module, "scaling", None)
|
|
148
|
+
if not isinstance(scaling, dict):
|
|
149
|
+
continue
|
|
150
|
+
for name, weight in zip(names, weights):
|
|
151
|
+
if name in scaling:
|
|
152
|
+
module.set_scale(name, weight)
|
|
153
|
+
touched[name] += 1
|
|
154
|
+
missing = [name for name, count in touched.items() if not count]
|
|
155
|
+
if missing:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Cannot set the scale of adapter(s) {missing} - no loaded layer "
|
|
158
|
+
"carries them. Nothing would have been scaled."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def apply_adapter_settings(loras, pipeline, activate=False):
|
|
163
|
+
"""Set each active LoRA's alpha, then every scale, on a pipeline that
|
|
164
|
+
already holds the adapters. Alpha first: the scale pass is what recomputes
|
|
165
|
+
each layer's scaling from it. No-op with no active entry.
|
|
166
|
+
|
|
167
|
+
`activate` is the cold path, right after load_lora_weights: diffusers'
|
|
168
|
+
set_adapters() activates the adapters and sets their scales in one call.
|
|
169
|
+
A warm pipeline (a cache hit) has them active already and takes the
|
|
170
|
+
scale-only path - see set_adapter_scales for why it must.
|
|
171
|
+
"""
|
|
172
|
+
names, weights, alphas = adapter_settings(loras)
|
|
173
|
+
if not names:
|
|
174
|
+
return
|
|
175
|
+
for name, alpha in alphas.items():
|
|
176
|
+
set_adapter_alpha(pipeline, name, alpha)
|
|
177
|
+
logger.info(f"Setting adapter weights: {list(zip(names, weights))}")
|
|
178
|
+
if activate:
|
|
179
|
+
# Positionally - diffusers' mixin calls the second parameter
|
|
180
|
+
# 'adapter_weights' while custom pipelines that delegate to the model
|
|
181
|
+
# (ostris/Krea2OstrisEdit) call it 'weights'
|
|
182
|
+
pipeline.set_adapters(names, weights)
|
|
183
|
+
else:
|
|
184
|
+
set_adapter_scales(pipeline, names, weights)
|
|
185
|
+
|
|
186
|
+
|
|
104
187
|
def load_loras(loras, pipeline):
|
|
105
|
-
"""Load and configure LoRA models.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
188
|
+
"""Load and configure LoRA models.
|
|
189
|
+
|
|
190
|
+
Names, scales and alphas come from adapter_settings, read before the
|
|
191
|
+
entries are popped, so a load and a later cache hit's
|
|
192
|
+
apply_adapter_settings set the same values on the same names."""
|
|
193
|
+
# A snapshot of each entry's runtime keys, taken before the pops below
|
|
194
|
+
settings = [
|
|
195
|
+
{k: lora.get(k) for k in ("model_name", "adapter_name", "scale", "alpha")}
|
|
196
|
+
if isinstance(lora, dict)
|
|
197
|
+
else lora
|
|
198
|
+
for lora in loras or []
|
|
199
|
+
]
|
|
200
|
+
names = iter(adapter_settings(settings)[0])
|
|
109
201
|
|
|
110
202
|
for i, lora in enumerate(loras or []):
|
|
111
203
|
if isinstance(lora, dict) and lora.get("model_name") is None:
|
|
@@ -117,40 +209,15 @@ def load_loras(loras, pipeline):
|
|
|
117
209
|
logger.info(f"Loading LoRA: {model_name}")
|
|
118
210
|
emit_phase("loading", detail=f"LoRA: {model_name}")
|
|
119
211
|
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
212
|
+
# Popped before the load: everything left in the dict is a keyword
|
|
213
|
+
# argument to load_lora_weights; the scale and alpha are applied to
|
|
214
|
+
# the layers afterwards rather than passed to it
|
|
215
|
+
for key in ("adapter_name", "scale", "alpha"):
|
|
216
|
+
lora.pop(key, None)
|
|
124
217
|
|
|
125
|
-
|
|
126
|
-
# 'variable:' reference here, and a variable declared as a string default
|
|
127
|
-
# substitutes as one
|
|
128
|
-
scale = lora.pop("scale", None)
|
|
129
|
-
scale = 1.0 if scale is None else float(scale)
|
|
130
|
-
adapter_weights.append(scale)
|
|
218
|
+
pipeline.load_lora_weights(model_name, adapter_name=next(names), **lora)
|
|
131
219
|
|
|
132
|
-
|
|
133
|
-
# argument to load_lora_weights, and the alpha is applied to the layers
|
|
134
|
-
# afterwards rather than passed to it
|
|
135
|
-
alpha = lora.pop("alpha", None)
|
|
136
|
-
if alpha is not None:
|
|
137
|
-
alphas[adapter_name] = alpha
|
|
138
|
-
|
|
139
|
-
# Load the LoRA with the adapter name
|
|
140
|
-
pipeline.load_lora_weights(model_name, adapter_name=adapter_name, **lora)
|
|
141
|
-
|
|
142
|
-
for adapter_name, alpha in alphas.items():
|
|
143
|
-
set_adapter_alpha(pipeline, adapter_name, alpha)
|
|
144
|
-
|
|
145
|
-
# Set adapter weights for all loaded LoRAs
|
|
146
|
-
if adapter_names:
|
|
147
|
-
logger.info(
|
|
148
|
-
f"Setting adapter weights: {list(zip(adapter_names, adapter_weights))}"
|
|
149
|
-
)
|
|
150
|
-
# Positionally - diffusers' mixin calls the second parameter 'adapter_weights'
|
|
151
|
-
# while custom pipelines that delegate to the model (ostris/Krea2OstrisEdit)
|
|
152
|
-
# call it 'weights'
|
|
153
|
-
pipeline.set_adapters(adapter_names, adapter_weights)
|
|
220
|
+
apply_adapter_settings(settings, pipeline, activate=True)
|
|
154
221
|
|
|
155
222
|
|
|
156
223
|
def load_ip_adapter(ip_adapter_definition, pipeline):
|
|
@@ -447,6 +447,23 @@ def load_and_configure_scheduler(
|
|
|
447
447
|
),
|
|
448
448
|
)
|
|
449
449
|
|
|
450
|
+
apply_scheduler_shift(scheduler_definition, pipeline, component_name)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def apply_scheduler_shift(scheduler_definition, pipeline, component_name="scheduler"):
|
|
454
|
+
"""Set the shift a scheduler definition names on an already-loaded
|
|
455
|
+
scheduler; nothing when the definition is None or names no shift.
|
|
456
|
+
|
|
457
|
+
A load sets it after any scheduler replacement, and a pipeline cache hit
|
|
458
|
+
sets it again (Pipeline.apply_runtime_settings), because the cache key
|
|
459
|
+
leaves the shift out - a run that only moves it reuses the warm pipeline.
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
ValueError: if the scheduler is registered but not loaded, or has no
|
|
463
|
+
set_shift()
|
|
464
|
+
"""
|
|
465
|
+
if scheduler_definition is None:
|
|
466
|
+
return
|
|
450
467
|
shift = scheduler_definition.get("shift", None)
|
|
451
468
|
if shift is None:
|
|
452
469
|
return
|
|
@@ -9,8 +9,14 @@ from .config_objects import (
|
|
|
9
9
|
get_quantization_configuration,
|
|
10
10
|
get_cache_configuration,
|
|
11
11
|
)
|
|
12
|
-
from .adapters import
|
|
12
|
+
from .adapters import (
|
|
13
|
+
active_loras,
|
|
14
|
+
apply_adapter_settings,
|
|
15
|
+
load_ip_adapter,
|
|
16
|
+
load_loras,
|
|
17
|
+
)
|
|
13
18
|
from .components import (
|
|
19
|
+
apply_scheduler_shift,
|
|
14
20
|
apply_sdnq_optimizations,
|
|
15
21
|
configure_components,
|
|
16
22
|
enable_cache_on_transformer,
|
|
@@ -135,6 +141,22 @@ def _loading_copy(pipeline_definition):
|
|
|
135
141
|
return copied
|
|
136
142
|
|
|
137
143
|
|
|
144
|
+
def apply_runtime_settings(pipeline_definition, model):
|
|
145
|
+
"""Set what a pipeline definition states that changes no weights -
|
|
146
|
+
scheduler and audio_scheduler shift, LoRA scales and alphas - on a model
|
|
147
|
+
that already holds those weights.
|
|
148
|
+
|
|
149
|
+
Takes the definition as the workflow holds it, never a Pipeline's
|
|
150
|
+
loading copy: load_loras pops each entry's model_name from that copy,
|
|
151
|
+
and an entry without one reads as switched off and would be skipped.
|
|
152
|
+
"""
|
|
153
|
+
apply_scheduler_shift(pipeline_definition.get("scheduler"), model)
|
|
154
|
+
apply_scheduler_shift(
|
|
155
|
+
pipeline_definition.get("audio_scheduler"), model, "audio_scheduler"
|
|
156
|
+
)
|
|
157
|
+
apply_adapter_settings(pipeline_definition.get("loras"), model)
|
|
158
|
+
|
|
159
|
+
|
|
138
160
|
class Pipeline:
|
|
139
161
|
"""
|
|
140
162
|
Manages pipeline initialization, configuration, and execution.
|
|
@@ -303,6 +325,15 @@ class Pipeline:
|
|
|
303
325
|
for entry in value:
|
|
304
326
|
Pipeline._check_trusted_block(entry, key)
|
|
305
327
|
|
|
328
|
+
def apply_runtime_settings(self):
|
|
329
|
+
"""Re-apply what a run may have changed without changing the weights:
|
|
330
|
+
scheduler and audio_scheduler shift, LoRA scales and alphas. The
|
|
331
|
+
pipeline cache keys on weights_identity, so a hit can carry any of
|
|
332
|
+
these at a new value; load() applies the same values on its way
|
|
333
|
+
through, so a cold load and a hit end in the same state. Only for a
|
|
334
|
+
wrapper that has not loaded (a hit): load() pops from this copy."""
|
|
335
|
+
apply_runtime_settings(self.pipeline_definition, self.pipeline)
|
|
336
|
+
|
|
306
337
|
def load(self, shared_components):
|
|
307
338
|
"""
|
|
308
339
|
Load and configure the pipeline with all components.
|
dw/step_cache.py
CHANGED
|
@@ -121,26 +121,101 @@ def component_names(pipeline_definition, key):
|
|
|
121
121
|
return list(pipeline_definition.get(key, [])) + list(configuration.get(key, []))
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
RUNTIME_LORA_KEYS = ("scale", "alpha")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def weights_identity(pipeline_definition):
|
|
128
|
+
"""The pipeline definition with every runtime-mutable setting removed.
|
|
129
|
+
|
|
130
|
+
A LoRA's scale and alpha (set_adapters / set_adapter_alpha) and a
|
|
131
|
+
scheduler's shift (set_shift) change nothing about what is loaded, so a
|
|
132
|
+
run that only moves one of them must hit the warm pipeline and re-apply
|
|
133
|
+
the value (Pipeline.apply_runtime_settings), not reload the stack.
|
|
134
|
+
Everything else - the model, the LoRA files, quantization, placement,
|
|
135
|
+
the scheduler type - still changes the key. Returns a new dict; the
|
|
136
|
+
definition is the workflow's and is not edited.
|
|
137
|
+
|
|
138
|
+
An alpha's or a shift's *presence* stays in the identity (as True, never
|
|
139
|
+
the value). A hit re-applies only what the definition states, and nothing
|
|
140
|
+
sets an alpha or shift back to the checkpoint's own: a run that drops one
|
|
141
|
+
(or nulls the variable behind it) must reload to get the default back,
|
|
142
|
+
where 16 -> 8 just re-applies. A scale needs no marker - an absent scale
|
|
143
|
+
re-applies as 1.0.
|
|
144
|
+
"""
|
|
145
|
+
identity = {
|
|
146
|
+
k: v
|
|
147
|
+
for k, v in pipeline_definition.items()
|
|
148
|
+
if k not in ("arguments", "seed", "chain")
|
|
149
|
+
}
|
|
150
|
+
loras = identity.get("loras")
|
|
151
|
+
if isinstance(loras, list):
|
|
152
|
+
identity["loras"] = [
|
|
153
|
+
_without_runtime(lora, RUNTIME_LORA_KEYS, "alpha")
|
|
154
|
+
if isinstance(lora, dict)
|
|
155
|
+
else lora
|
|
156
|
+
for lora in loras
|
|
157
|
+
]
|
|
158
|
+
for name in ("scheduler", "audio_scheduler"):
|
|
159
|
+
scheduler = identity.get(name)
|
|
160
|
+
if isinstance(scheduler, dict):
|
|
161
|
+
identity[name] = _without_runtime(scheduler, ("shift",), "shift")
|
|
162
|
+
return identity
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _without_runtime(block, runtime_keys, marked):
|
|
166
|
+
"""`block` without its runtime keys, but with `marked` as True when the
|
|
167
|
+
block states it - see weights_identity for why its presence counts."""
|
|
168
|
+
stripped = {k: v for k, v in block.items() if k not in runtime_keys}
|
|
169
|
+
if block.get(marked) is not None:
|
|
170
|
+
stripped[marked] = True
|
|
171
|
+
return stripped
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def pipeline_definition_key(pipeline_definition):
|
|
175
|
+
"""A hash of everything in a pipeline definition but what varies per call
|
|
176
|
+
(arguments, seed, chain) - runtime settings included, unlike
|
|
177
|
+
pipeline_cache_key.
|
|
178
|
+
|
|
179
|
+
The step cache keys a borrowing step on this, not on the pipeline cache
|
|
180
|
+
key: a step that runs on another step's pipeline (pipeline_reference,
|
|
181
|
+
reused_components) produces a different result when only its source's
|
|
182
|
+
LoRA scale or shift moves, though the loaded weights - and so the warm
|
|
183
|
+
pipeline - are the same.
|
|
184
|
+
"""
|
|
185
|
+
serialized = json.dumps(
|
|
186
|
+
{
|
|
187
|
+
k: v
|
|
188
|
+
for k, v in pipeline_definition.items()
|
|
189
|
+
if k not in ("arguments", "seed", "chain")
|
|
190
|
+
},
|
|
191
|
+
sort_keys=True,
|
|
192
|
+
default=str,
|
|
193
|
+
)
|
|
194
|
+
return hashlib.sha256(serialized.encode()).hexdigest()
|
|
195
|
+
|
|
196
|
+
|
|
124
197
|
def pipeline_cache_key(pipeline_definition):
|
|
125
198
|
"""Stable identity for a loaded pipeline.
|
|
126
199
|
|
|
127
|
-
Hashes everything that shapes loading
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
200
|
+
Hashes the weights identity - everything that shapes loading:
|
|
201
|
+
configuration, components, quantization, the LoRA files, the scheduler
|
|
202
|
+
type - and excludes what varies per call (arguments, seed, chain), so a
|
|
203
|
+
cache hit means "this exact model stack is already loaded". A LoRA's
|
|
204
|
+
scale and alpha and a scheduler's shift are excluded too: each is set in
|
|
205
|
+
place on the loaded model, and a hit re-applies them
|
|
206
|
+
(Pipeline.apply_runtime_settings), so nudging one between runs costs no
|
|
207
|
+
reload. Keying the cache by identity instead of step name means two
|
|
208
|
+
workflows whose steps happen to share a name can no longer collide, and
|
|
209
|
+
a rerun of an edited workflow keeps every pipeline whose definition did
|
|
210
|
+
not change.
|
|
133
211
|
|
|
134
212
|
Computed after variable substitution but the excluded keys keep realized
|
|
135
213
|
per-run values (images, generators) out of the hash; realized types and
|
|
136
214
|
dtypes stringify stably via default=str.
|
|
137
215
|
"""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if k not in ("arguments", "seed", "chain")
|
|
142
|
-
}
|
|
143
|
-
serialized = json.dumps(load_definition, sort_keys=True, default=str)
|
|
216
|
+
serialized = json.dumps(
|
|
217
|
+
weights_identity(pipeline_definition), sort_keys=True, default=str
|
|
218
|
+
)
|
|
144
219
|
return hashlib.sha256(serialized.encode()).hexdigest()
|
|
145
220
|
|
|
146
221
|
|
|
@@ -170,14 +245,34 @@ def step_pipeline_keys(steps):
|
|
|
170
245
|
loaded - a deferred cache hit, or the cache_hits probe. A key that has to
|
|
171
246
|
agree across those cases is read from this table, not re-hashed.
|
|
172
247
|
"""
|
|
248
|
+
return _closure_keys(steps, pipeline_cache_key)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def step_definition_keys(steps):
|
|
252
|
+
"""Step name -> effective definition key for every pipeline step, taken
|
|
253
|
+
before any step runs: step_pipeline_keys over pipeline_definition_key.
|
|
254
|
+
|
|
255
|
+
What the step cache folds in for a borrowed pipeline
|
|
256
|
+
(borrowed_pipeline_keys), so a source step whose only change is a LoRA
|
|
257
|
+
scale, alpha or shift - the same warm pipeline, a different result - is
|
|
258
|
+
a miss for the steps that borrow it. Pipeline ownership keeps to
|
|
259
|
+
step_pipeline_keys.
|
|
260
|
+
"""
|
|
261
|
+
return _closure_keys(steps, pipeline_definition_key)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _closure_keys(steps, own_key_of):
|
|
265
|
+
"""Step name -> the step's own key (`own_key_of` its pipeline
|
|
266
|
+
definition), or, for a step that reuses components, the hash of the
|
|
267
|
+
sorted own keys over its reuse closure - see step_pipeline_keys."""
|
|
173
268
|
keys = {}
|
|
174
|
-
# Step name -> the own
|
|
269
|
+
# Step name -> the own keys of its reuse closure
|
|
175
270
|
closures = {}
|
|
176
271
|
for index, step_data in enumerate(steps):
|
|
177
272
|
if "pipeline" not in step_data:
|
|
178
273
|
continue
|
|
179
274
|
pipeline_definition = step_data["pipeline"]
|
|
180
|
-
own_key =
|
|
275
|
+
own_key = own_key_of(pipeline_definition)
|
|
181
276
|
closure = {own_key}
|
|
182
277
|
reused = component_names(pipeline_definition, "reused_components")
|
|
183
278
|
for name in reused:
|
|
@@ -215,10 +310,15 @@ def borrowed_pipeline_keys(steps, index, pipeline_keys):
|
|
|
215
310
|
step's model changes - its own definition never names the model. Folding
|
|
216
311
|
the source steps' keys into the lookup snapshot makes a source model
|
|
217
312
|
change a miss here too, rather than a stale hit that republishes a
|
|
218
|
-
now-wrong pipeline. The keys are effective keys
|
|
219
|
-
a
|
|
313
|
+
now-wrong pipeline. The keys are effective keys, so a change further up
|
|
314
|
+
a borrow chain than the direct source is a miss too.
|
|
315
|
+
|
|
316
|
+
The step cache passes step_definition_keys, which also moves with a
|
|
317
|
+
source's LoRA scale, alpha or shift - a change that keeps the warm
|
|
318
|
+
pipeline but changes what the borrower produces. Callers that only need
|
|
319
|
+
which steps are borrowed may pass step_pipeline_keys.
|
|
220
320
|
|
|
221
|
-
The keys come from `pipeline_keys` (
|
|
321
|
+
The keys come from `pipeline_keys` (one of those tables), computed from
|
|
222
322
|
the definition before any step ran, never from `steps[:index]` as they
|
|
223
323
|
stand now: a load edits its own definition in place, and a source step
|
|
224
324
|
that loaded before this lookup (a cold run) and one that did not (a
|
dw/tasks/restore_faces.py
CHANGED
|
@@ -66,15 +66,25 @@ def restore_faces(image, model_name, device="cpu", **kwargs):
|
|
|
66
66
|
("restore_faces", model_name, filename, str(device)), load_descriptor
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
# facexlib's helper loads RetinaFace (and ParseNet with use_parse) in
|
|
70
|
+
# its constructor. per_frame calls this once per frame, so a video was
|
|
71
|
+
# reloading detector weights for every frame. The helper keeps per-image
|
|
72
|
+
# state between calls; clean_all() resets it before each read
|
|
73
|
+
def build_helper():
|
|
74
|
+
return FaceRestoreHelper(
|
|
75
|
+
upscale_factor=upscale_factor,
|
|
76
|
+
face_size=face_size,
|
|
77
|
+
crop_ratio=(1, 1),
|
|
78
|
+
det_model="retinaface_resnet50",
|
|
79
|
+
use_parse=use_parse,
|
|
80
|
+
device=torch.device(device),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
face_helper = cached_model(
|
|
84
|
+
("restore_faces_helper", upscale_factor, face_size, use_parse, str(device)),
|
|
85
|
+
build_helper,
|
|
77
86
|
)
|
|
87
|
+
face_helper.clean_all()
|
|
78
88
|
|
|
79
89
|
# Convert PIL to BGR numpy (facexlib format)
|
|
80
90
|
input_bgr = np.array(image.convert("RGB"))[:, :, ::-1].copy()
|
dw/worker.py
CHANGED
|
@@ -357,15 +357,38 @@ class WorkflowWorker:
|
|
|
357
357
|
# what runs out of memory
|
|
358
358
|
if identity != self.workflow_identity:
|
|
359
359
|
if self.workflow_identity is not None:
|
|
360
|
-
self.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
360
|
+
keep = self._keys_worth_keeping(command, job.workflow)
|
|
361
|
+
if keep:
|
|
362
|
+
self._reply(
|
|
363
|
+
Output(
|
|
364
|
+
message=f"Workflow changed - keeping {len(keep)} warm "
|
|
365
|
+
"model(s) the new workflow loads, releasing the rest..."
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
self._reply(
|
|
370
|
+
Output(message="Workflow changed - releasing cached models...")
|
|
371
|
+
)
|
|
372
|
+
self._reply(Output(message=self._cleanup_all(keep=keep)))
|
|
364
373
|
self.workflow_identity = identity
|
|
365
374
|
|
|
366
375
|
self._reply(WorkflowLoaded(workflow_name=job.workflow.name))
|
|
367
376
|
self._reply(Output(message=f"Executing workflow: {job.workflow.name}"))
|
|
368
377
|
|
|
378
|
+
def _keys_worth_keeping(self, command, workflow):
|
|
379
|
+
"""The resident pipeline keys the next workflow loads anyway. Any
|
|
380
|
+
failure to prepare the definition answers the empty set - the run
|
|
381
|
+
itself will report what is wrong, and a full release is the safe
|
|
382
|
+
default on a card that cannot hold two stacks."""
|
|
383
|
+
try:
|
|
384
|
+
wanted = workflow_run.pipeline_keys(
|
|
385
|
+
workflow, command.get("arguments") or {}
|
|
386
|
+
)
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.debug(f"Could not take the next workflow's pipeline keys: {e}")
|
|
389
|
+
return set()
|
|
390
|
+
return wanted & set(self.loaded_pipelines)
|
|
391
|
+
|
|
369
392
|
def _run_job(self, command: Dict[str, Any], job: "_Job"):
|
|
370
393
|
"""Phase 3: run the workflow, streaming its events, then reclaim."""
|
|
371
394
|
workflow = job.workflow
|
|
@@ -635,10 +658,11 @@ class WorkflowWorker:
|
|
|
635
658
|
|
|
636
659
|
logger.debug("Inter-run cleanup complete")
|
|
637
660
|
|
|
638
|
-
def _cleanup_all(self):
|
|
661
|
+
def _cleanup_all(self, keep=frozenset()):
|
|
639
662
|
"""
|
|
640
663
|
Complete cleanup - clear all cached models and components.
|
|
641
|
-
Called when workflow changes or on shutdown.
|
|
664
|
+
Called when workflow changes or on shutdown. Pipelines whose key is
|
|
665
|
+
in `keep` survive; with `keep` empty everything goes.
|
|
642
666
|
|
|
643
667
|
Returns:
|
|
644
668
|
One line saying what host memory looks like on the other side of
|
|
@@ -652,11 +676,20 @@ class WorkflowWorker:
|
|
|
652
676
|
logger.info("Performing full cleanup")
|
|
653
677
|
|
|
654
678
|
# Clear pipeline cache and any models task handlers cached
|
|
679
|
+
kept = {k: v for k, v in self.loaded_pipelines.items() if k in keep}
|
|
680
|
+
dropped = len(self.loaded_pipelines) - len(kept)
|
|
681
|
+
# Cleared and refilled in place, so the kept entries keep their
|
|
682
|
+
# identity
|
|
655
683
|
self.loaded_pipelines.clear()
|
|
684
|
+
self.loaded_pipelines.update(kept)
|
|
685
|
+
# Every run republishes shared components from the pipelines it hits
|
|
686
|
+
# (workflow.py: publish_shared_components on a cache hit)
|
|
656
687
|
self.shared_components.clear()
|
|
657
|
-
#
|
|
658
|
-
#
|
|
659
|
-
self.prior_step_keys
|
|
688
|
+
# A step's prior key addresses an entry that is now gone - unless it
|
|
689
|
+
# was kept, in which case a redefinition next run still releases it
|
|
690
|
+
self.prior_step_keys = {
|
|
691
|
+
name: key for name, key in self.prior_step_keys.items() if key in kept
|
|
692
|
+
}
|
|
660
693
|
clear_model_cache()
|
|
661
694
|
# Drop cached step results too - stale results would otherwise
|
|
662
695
|
# survive a memory clear and keep getting served for steps whose
|
|
@@ -703,9 +736,10 @@ class WorkflowWorker:
|
|
|
703
736
|
f"Released cached models: host RSS {_mb(stats.get('rss_mb'))}, "
|
|
704
737
|
f"{_mb(stats.get('available_mb'))} available"
|
|
705
738
|
+ (f" ({released:.0f} MB returned to the OS)" if released else "")
|
|
739
|
+
+ (f", kept {len(kept)} warm" if kept else "")
|
|
706
740
|
)
|
|
707
741
|
|
|
708
|
-
logger.info(f"Full cleanup complete. {summary}")
|
|
742
|
+
logger.info(f"Full cleanup complete ({dropped} pipeline(s) dropped). {summary}")
|
|
709
743
|
return summary
|
|
710
744
|
|
|
711
745
|
def _parent_is_dead(self) -> bool:
|
dw/workflow.py
CHANGED
|
@@ -27,7 +27,7 @@ from .variables import (
|
|
|
27
27
|
resolve_variable_values,
|
|
28
28
|
set_variables,
|
|
29
29
|
)
|
|
30
|
-
from .pipeline_processors.pipeline import Pipeline
|
|
30
|
+
from .pipeline_processors.pipeline import Pipeline, apply_runtime_settings
|
|
31
31
|
from .tasks.task import Task
|
|
32
32
|
from . import get_device
|
|
33
33
|
from .pipeline_ownership import (
|
|
@@ -730,7 +730,7 @@ class Workflow:
|
|
|
730
730
|
# Pipelines are cached by what they load, not what step loads them
|
|
731
731
|
ownership = self.pipeline_ownership
|
|
732
732
|
cache_key = ownership.load_key(step_definition)
|
|
733
|
-
ownership.record(step_name, cache_key)
|
|
733
|
+
ownership.record(step_name, cache_key, step_definition["pipeline"])
|
|
734
734
|
get_context().touch_pipeline(cache_key)
|
|
735
735
|
|
|
736
736
|
if cache_hit and cache_key not in previous_pipelines:
|
|
@@ -798,6 +798,12 @@ class Workflow:
|
|
|
798
798
|
"earlier pipeline step in this run (or it was released)"
|
|
799
799
|
)
|
|
800
800
|
previous_pipeline = previous_pipelines[referenced_key]
|
|
801
|
+
# Steps whose pipelines differ only in scale, alpha or shift share
|
|
802
|
+
# one model, and the last of them to run left its values on it: put
|
|
803
|
+
# the referenced step's own back
|
|
804
|
+
referenced_definition = self.pipeline_ownership.definition_for(reference_name)
|
|
805
|
+
if referenced_definition is not None:
|
|
806
|
+
apply_runtime_settings(referenced_definition, previous_pipeline.pipeline)
|
|
801
807
|
return Pipeline(
|
|
802
808
|
pipeline_reference,
|
|
803
809
|
default_seed,
|
dw/workflow_run.py
CHANGED
|
@@ -62,6 +62,7 @@ from .step_cache import (
|
|
|
62
62
|
reference_resolves_to,
|
|
63
63
|
referenced_result_names,
|
|
64
64
|
step_cache,
|
|
65
|
+
step_definition_keys,
|
|
65
66
|
step_pipeline_keys,
|
|
66
67
|
)
|
|
67
68
|
from .subfolders import step_subfolder
|
|
@@ -138,6 +139,9 @@ class StepLoop:
|
|
|
138
139
|
hits: set = field(default_factory=set)
|
|
139
140
|
# Final result is the workflow return value
|
|
140
141
|
last_result: object = None
|
|
142
|
+
# step_definition_keys of `steps`, taken before any step loaded - what
|
|
143
|
+
# the step cache folds in for a borrowed pipeline (cache_lookup)
|
|
144
|
+
definition_keys: dict = field(default_factory=dict)
|
|
141
145
|
|
|
142
146
|
|
|
143
147
|
def run_base_dir(workflow):
|
|
@@ -367,14 +371,16 @@ def cache_lookup(
|
|
|
367
371
|
step_seed,
|
|
368
372
|
hits_this_run,
|
|
369
373
|
cache_enabled,
|
|
370
|
-
|
|
374
|
+
definition_keys,
|
|
371
375
|
):
|
|
372
376
|
"""Whether the step cache serves step `index`, as (cached_result or
|
|
373
377
|
None, the step_data snapshot the entry is keyed on or None, whether
|
|
374
378
|
a later step still reads this one's result, the names later steps
|
|
375
379
|
still reference). Shared by run() and cache_hits() - see
|
|
376
|
-
prepare_definition for why. `
|
|
377
|
-
of `steps`, taken before the first step ran
|
|
380
|
+
prepare_definition for why. `definition_keys` is step_definition_keys
|
|
381
|
+
of `steps`, taken before the first step ran - not the pipeline
|
|
382
|
+
ownership table, whose keys leave out a source's LoRA scale, alpha and
|
|
383
|
+
shift, each of which changes what a borrowing step produces.
|
|
378
384
|
"""
|
|
379
385
|
# What later steps still read, which decides both whether this
|
|
380
386
|
# step's result has to be kept alive after the step (release_unreferenced_results
|
|
@@ -411,7 +417,7 @@ def cache_lookup(
|
|
|
411
417
|
# model - only the source step's does - so without this a
|
|
412
418
|
# source model change would leave the borrowing step's
|
|
413
419
|
# snapshot unchanged and serve a stale hit
|
|
414
|
-
borrowed = borrowed_pipeline_keys(steps, index,
|
|
420
|
+
borrowed = borrowed_pipeline_keys(steps, index, definition_keys)
|
|
415
421
|
if borrowed:
|
|
416
422
|
step_data_snapshot["__borrowed_pipelines__"] = borrowed
|
|
417
423
|
except Exception as ex:
|
|
@@ -465,7 +471,7 @@ def cache_hits(workflow, arguments):
|
|
|
465
471
|
realize_args(steps, base_dir)
|
|
466
472
|
# The same table run() takes at the same point, so a borrowed
|
|
467
473
|
# key here is the key the run stored its entry under
|
|
468
|
-
|
|
474
|
+
definition_keys = step_definition_keys(steps)
|
|
469
475
|
hits_this_run = set()
|
|
470
476
|
hits = []
|
|
471
477
|
for index, step_data in enumerate(steps):
|
|
@@ -479,7 +485,7 @@ def cache_hits(workflow, arguments):
|
|
|
479
485
|
step_seed,
|
|
480
486
|
hits_this_run,
|
|
481
487
|
True,
|
|
482
|
-
|
|
488
|
+
definition_keys,
|
|
483
489
|
)
|
|
484
490
|
if cached_result is not None:
|
|
485
491
|
hits_this_run.add(step_data["name"])
|
|
@@ -489,6 +495,33 @@ def cache_hits(workflow, arguments):
|
|
|
489
495
|
deactivate_output_root(output_root_token)
|
|
490
496
|
|
|
491
497
|
|
|
498
|
+
def pipeline_keys(workflow, arguments):
|
|
499
|
+
"""The set of pipeline cache keys a run of `workflow` with `arguments`
|
|
500
|
+
loads its steps under - the table run() and cache_hits() take
|
|
501
|
+
(step_pipeline_keys over the prepared definition). Prepares the
|
|
502
|
+
definition exactly as a run does and executes nothing: the worker asks
|
|
503
|
+
this before releasing the previous workflow's models, so a model the
|
|
504
|
+
next workflow loads anyway stays warm. A sub-workflow step loads its
|
|
505
|
+
own pipelines later and is not in the set; an unseeded workflow still
|
|
506
|
+
answers, since warmth does not depend on the step cache. The probe
|
|
507
|
+
realizes the steps because the run keys on the realized definition, at
|
|
508
|
+
the price of loading step media once per switch (the price cache_hits
|
|
509
|
+
already pays).
|
|
510
|
+
"""
|
|
511
|
+
output_root_token = activate_output_root(workflow.output_dir)
|
|
512
|
+
try:
|
|
513
|
+
workflow_def = copy.deepcopy(workflow.workflow_definition)
|
|
514
|
+
base_dir = run_base_dir(workflow)
|
|
515
|
+
workflow_def, _, _ = prepare_definition(
|
|
516
|
+
workflow, workflow_def, arguments or {}, base_dir
|
|
517
|
+
)
|
|
518
|
+
steps = workflow_def.get("steps", [])
|
|
519
|
+
realize_args(steps, base_dir)
|
|
520
|
+
return set(step_pipeline_keys(steps).values())
|
|
521
|
+
finally:
|
|
522
|
+
deactivate_output_root(output_root_token)
|
|
523
|
+
|
|
524
|
+
|
|
492
525
|
def prepare_run(workflow, record):
|
|
493
526
|
"""Phase B: the definition as this run works from it, and its seed.
|
|
494
527
|
|
|
@@ -678,6 +711,7 @@ def begin_steps(workflow, prepared, previous_pipelines, run_context):
|
|
|
678
711
|
prepared.cache_enabled,
|
|
679
712
|
run_context,
|
|
680
713
|
pipelines,
|
|
714
|
+
definition_keys=step_definition_keys(steps),
|
|
681
715
|
)
|
|
682
716
|
|
|
683
717
|
|
|
@@ -875,7 +909,7 @@ def run_step(workflow, loop, index, step_data, record):
|
|
|
875
909
|
step_seed,
|
|
876
910
|
loop.hits,
|
|
877
911
|
loop.cache_enabled,
|
|
878
|
-
|
|
912
|
+
loop.definition_keys,
|
|
879
913
|
)
|
|
880
914
|
# The last step of a composed child whose parent does the
|
|
881
915
|
# saving (#92) - its files are written once, by the parent
|
|
File without changes
|
{diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{diffusers_workflow-0.11.0.dist-info → diffusers_workflow-0.11.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|