diffusers-workflow 0.4.0__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.4.0.dist-info/METADATA +318 -0
- diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
- diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
- diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
- dw/__init__.py +440 -0
- dw/adapter_compatibility.py +226 -0
- dw/arguments.py +1231 -0
- dw/assessment_rules.py +159 -0
- dw/assets.py +130 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +146 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/content_types.py +150 -0
- dw/dissolve_frame_errors.py +121 -0
- dw/docs/ACCELERATION.md +352 -0
- dw/docs/AGENT_LOOP.md +95 -0
- dw/docs/DEPENDENCIES.md +91 -0
- dw/docs/IP_ADAPTER.md +109 -0
- dw/docs/LORAS.md +131 -0
- dw/docs/MCP.md +517 -0
- dw/docs/PROMPT_WEIGHTING.md +78 -0
- dw/docs/QUANTIZATION.md +230 -0
- dw/docs/RECIPES_24GB.md +201 -0
- dw/docs/RELEASING.md +195 -0
- dw/docs/REMOTE.md +140 -0
- dw/docs/REPL_COMMANDS.md +121 -0
- dw/docs/REPL_WORKER_GUIDE.md +51 -0
- dw/docs/SECURITY.md +272 -0
- dw/docs/SECURITY_QUICKREF.md +112 -0
- dw/docs/SERVER.md +679 -0
- dw/docs/TASKS.md +1741 -0
- dw/docs/TESTING.md +71 -0
- dw/docs/WORKFLOW_GUIDE.md +2038 -0
- dw/docs/WORKSPACES.md +316 -0
- dw/download_watch.py +335 -0
- dw/elision.py +306 -0
- dw/events.py +275 -0
- dw/for_each.py +409 -0
- dw/host_memory.py +258 -0
- dw/host_memory_projection.py +230 -0
- dw/hub_cache.py +432 -0
- dw/introspection.py +1228 -0
- dw/kernel_availability.py +208 -0
- dw/locations.py +599 -0
- dw/log_setup.py +45 -0
- dw/loudness.py +82 -0
- dw/media_audio.py +217 -0
- dw/media_frames.py +367 -0
- dw/media_info.py +297 -0
- dw/pipeline_processors/chain.py +821 -0
- dw/pipeline_processors/config_objects.py +237 -0
- dw/pipeline_processors/pipeline.py +2297 -0
- dw/pipeline_processors/remote.py +46 -0
- dw/plan.py +920 -0
- dw/previous_results.py +411 -0
- dw/probe_paths.py +59 -0
- dw/prompt_schema.json +48 -0
- dw/prompt_weighting.py +378 -0
- dw/prompts.py +159 -0
- dw/realize.py +250 -0
- dw/reference_limits.py +215 -0
- dw/reference_names.py +125 -0
- dw/repl.py +338 -0
- dw/repl_commands.py +836 -0
- dw/repl_worker.py +159 -0
- dw/result.py +1720 -0
- dw/result_fps.py +82 -0
- dw/run.py +162 -0
- dw/runs.py +768 -0
- dw/scalar_result_validation.py +97 -0
- dw/schema.py +283 -0
- dw/security.py +1038 -0
- dw/select_validation.py +115 -0
- dw/serve.py +277 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +4586 -0
- dw/server/assess.py +132 -0
- dw/server/catalog_shape.py +487 -0
- dw/server/enhancers.py +129 -0
- dw/server/exports.py +480 -0
- dw/server/guides.py +257 -0
- dw/server/jobs.py +1561 -0
- dw/server/mcp_mount.py +95 -0
- dw/server/netinfo.py +124 -0
- dw/server/observed_cost.py +379 -0
- dw/server/sysinfo.py +71 -0
- dw/server/ui/assets/abap-08VXUWAP.js +1 -0
- dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
- dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
- dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
- dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
- dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
- dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
- dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
- dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
- dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
- dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
- dw/server/ui/assets/css-DIMkf-bt.js +3 -0
- dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
- dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
- dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
- dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
- dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
- dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
- dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
- dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
- dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
- dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
- dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
- dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- dw/server/ui/assets/go-C-y9NEjX.js +1 -0
- dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
- dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-PhsdjHSr.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
- dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
- dw/server/ui/assets/index-DgrYhQd9.js +43 -0
- dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
- dw/server/ui/assets/java-BEtHBSE6.js +1 -0
- dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
- dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
- dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
- dw/server/ui/assets/less-B9JPFI3C.js +2 -0
- dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
- dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
- dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
- dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
- dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
- dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
- dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
- dw/server/ui/assets/monaco--ixms01u.css +1 -0
- dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
- dw/server/ui/assets/msdax-DauUninz.js +1 -0
- dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
- dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
- dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- dw/server/ui/assets/perl-oz_6vUea.js +1 -0
- dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
- dw/server/ui/assets/php-nr791fC2.js +1 -0
- dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
- dw/server/ui/assets/postiats-43DmfD33.js +1 -0
- dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
- dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
- dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
- dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
- dw/server/ui/assets/python-Bcn70HdC.js +1 -0
- dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
- dw/server/ui/assets/r-BwWrilGY.js +1 -0
- dw/server/ui/assets/razor-D1HmNnby.js +1 -0
- dw/server/ui/assets/redis-ClamHrr6.js +1 -0
- dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
- dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
- dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
- dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
- dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
- dw/server/ui/assets/scheme-BeGwcela.js +1 -0
- dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
- dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
- dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
- dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
- dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- dw/server/ui/assets/sql-NEE52Syq.js +1 -0
- dw/server/ui/assets/st-DbInun42.js +1 -0
- dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
- dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
- dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
- dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
- dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
- dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
- dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
- dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
- dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
- dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/server/updater.py +192 -0
- dw/settings.py +98 -0
- dw/shot_span_preflight.py +116 -0
- dw/shots.py +359 -0
- dw/slice_preflight.py +148 -0
- dw/step.py +187 -0
- dw/step_cache.py +442 -0
- dw/subfolders.py +107 -0
- dw/task_domains.py +307 -0
- dw/tasks/assess.py +826 -0
- dw/tasks/audio_transcription.py +88 -0
- dw/tasks/audio_utils.py +1862 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/compose_text.py +74 -0
- dw/tasks/concat_videos.py +300 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/dissolve_videos.py +342 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +173 -0
- dw/tasks/grade.py +97 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +764 -0
- dw/tasks/interpolate_frames.py +252 -0
- dw/tasks/judge.py +68 -0
- dw/tasks/model_cache.py +55 -0
- dw/tasks/pair_audio.py +268 -0
- dw/tasks/qr_code.py +19 -0
- dw/tasks/restore_faces.py +175 -0
- dw/tasks/rife_model.py +192 -0
- dw/tasks/segment.py +121 -0
- dw/tasks/select.py +111 -0
- dw/tasks/speech_generation.py +228 -0
- dw/tasks/stabilize.py +129 -0
- dw/tasks/task.py +920 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +169 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +624 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +381 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +231 -0
- dw/validate.py +68 -0
- dw/variable_constraints.py +444 -0
- dw/variables.py +443 -0
- dw/video_extensions.py +141 -0
- dw/vram_estimate.py +116 -0
- dw/worker.py +764 -0
- dw/workflow.py +2007 -0
- dw/workflow_schema.json +1346 -0
- dw/workflow_sources.py +383 -0
- dw/workflows/h3_context_ir.json +57 -0
- dw/workflows/test.json +31 -0
- dw/workspace.py +730 -0
- dw_mcp/__init__.py +6 -0
- dw_mcp/__main__.py +133 -0
- dw_mcp/assets.py +336 -0
- dw_mcp/authoring.py +114 -0
- dw_mcp/catalog.py +360 -0
- dw_mcp/client.py +486 -0
- dw_mcp/diagnose.py +371 -0
- dw_mcp/exports.py +84 -0
- dw_mcp/guides.py +35 -0
- dw_mcp/media.py +638 -0
- dw_mcp/models.py +97 -0
- dw_mcp/prompts.py +104 -0
- dw_mcp/server.py +1343 -0
- dw_mcp/workspaces.py +212 -0
dw/step_cache.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Skip re-executing a step whose resolved definition, seed, and upstream
|
|
2
|
+
results are all unchanged since the last run in this process.
|
|
3
|
+
|
|
4
|
+
Reusing a loaded pipeline (Workflow.run's previous_pipelines) is only half
|
|
5
|
+
of what makes REPL iteration fast - the other half is not re-running a
|
|
6
|
+
step's forward pass at all when nothing feeding it changed, the way
|
|
7
|
+
Mellon's NodeBase skips a node whose resolved params match its last call
|
|
8
|
+
(deep value equality, not just identity - a step's arguments are plain
|
|
9
|
+
dicts/lists/scalars after variable substitution, not hashable).
|
|
10
|
+
|
|
11
|
+
A step is safe to skip only if:
|
|
12
|
+
1. its own resolved definition (step_data) matches last run's, AND
|
|
13
|
+
2. its seed matches last run's - step_data does NOT carry the seed
|
|
14
|
+
(Workflow.run resolves it separately, and draws a fresh random one
|
|
15
|
+
per run when the workflow sets none), so seed must be compared
|
|
16
|
+
explicitly or two differently-seeded runs would wrongly look identical
|
|
17
|
+
3. every previous_result: it reads was ITSELF served from cache this run
|
|
18
|
+
AND the upstream entry now in the cache is the same generation this
|
|
19
|
+
entry was computed from - "hit this run" alone is not enough, because
|
|
20
|
+
a run cancelled between an upstream's put and this step's leaves this
|
|
21
|
+
entry describing an upstream that has since been recomputed
|
|
22
|
+
4. the effective output directory matches last run's - like seed, this
|
|
23
|
+
is out-of-band (not part of step_data), and a cache hit reuses the
|
|
24
|
+
entry's saved_files/manifest paths verbatim, so a mismatch must force
|
|
25
|
+
a miss or a changed output dir would silently keep pointing at the
|
|
26
|
+
old directory's files
|
|
27
|
+
5. every file the cached result names still exists - a hit republishes
|
|
28
|
+
those paths into the manifest and job history, so a file deleted
|
|
29
|
+
since (gallery delete button, or by hand) must force a re-run
|
|
30
|
+
6. the entry retained the step's Result if this run needs one - an entry
|
|
31
|
+
stored for a step nothing downstream read holds only its saved_files
|
|
32
|
+
|
|
33
|
+
Entries are keyed by (workflow_id, step_name), not by step name alone:
|
|
34
|
+
saved files are named "{workflow_id}-{step_name}.{index}", so a bare-name
|
|
35
|
+
key would let a different workflow (or the same one after an id rename, or
|
|
36
|
+
a sub-workflow sharing a name with its parent) hit and republish the other
|
|
37
|
+
workflow's file paths while writing none of its own.
|
|
38
|
+
|
|
39
|
+
The cache is per-process and bounded two ways, LRU either way: by entry
|
|
40
|
+
count (DEFAULT_MAX_ENTRIES), sized so a maximal for_each run (32 entries
|
|
41
|
+
over two groups plus fixed steps, ~70 members) never evicts its own earlier
|
|
42
|
+
members before it ends - a smaller cap would turn a long list-driven run
|
|
43
|
+
into one that thrashes its own cache - and by the approximate byte size of
|
|
44
|
+
the retained media itself (DEFAULT_MAX_RETAINED_BYTES). A for_each group
|
|
45
|
+
whose members are each a full decoded video (dialogue-short's `shot`, gathered
|
|
46
|
+
by `episode`) is retained in full for every member - correctly, since the
|
|
47
|
+
final step genuinely reads all of them - but nothing ever released that once
|
|
48
|
+
the run finished, so a handful of members held several GB resident
|
|
49
|
+
indefinitely (#368). The byte cap does not know which entries a future run
|
|
50
|
+
would most want back; it just keeps the newest-used bytes under budget the
|
|
51
|
+
same way the entry cap keeps the newest-used count under one, oldest first.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
import copy
|
|
55
|
+
import dataclasses
|
|
56
|
+
import itertools
|
|
57
|
+
import logging
|
|
58
|
+
import os
|
|
59
|
+
from collections import OrderedDict
|
|
60
|
+
|
|
61
|
+
import numpy as np
|
|
62
|
+
import torch
|
|
63
|
+
from PIL import Image
|
|
64
|
+
|
|
65
|
+
logger = logging.getLogger("dw")
|
|
66
|
+
|
|
67
|
+
# Monotonic across the process: every put stamps its entry, and a downstream
|
|
68
|
+
# entry records the stamp of each upstream it was computed from. Never reset
|
|
69
|
+
# (clear() included) - a reused number would make a stale entry look fresh.
|
|
70
|
+
_generations = itertools.count(1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def reference_resolves_to(reference, name):
|
|
74
|
+
"""Whether a previous_result reference resolves to the result `name`.
|
|
75
|
+
|
|
76
|
+
A reference either names a result outright or extends it with a property
|
|
77
|
+
('segment.mask'). The step cache, release_unreferenced_results and
|
|
78
|
+
get_previous_results all ask some form of this question.
|
|
79
|
+
"""
|
|
80
|
+
return reference == name or reference.startswith(name + ".")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def referenced_result_names(steps):
|
|
84
|
+
"""Every previous_result reference the given steps make, as full names.
|
|
85
|
+
|
|
86
|
+
Scans nested dicts and lists, so references inside pipeline arguments,
|
|
87
|
+
task arguments and sub-workflow argument maps are all found - including a
|
|
88
|
+
constructed object's 'from_previous_result', which names a step without
|
|
89
|
+
the 'previous_result:' prefix.
|
|
90
|
+
"""
|
|
91
|
+
prefix = "previous_result:"
|
|
92
|
+
names = set()
|
|
93
|
+
|
|
94
|
+
def scan(value):
|
|
95
|
+
if isinstance(value, str) and value.startswith(prefix):
|
|
96
|
+
names.add(value[len(prefix) :])
|
|
97
|
+
elif isinstance(value, dict):
|
|
98
|
+
reference = value.get("from_previous_result")
|
|
99
|
+
if isinstance(reference, str):
|
|
100
|
+
names.add(reference)
|
|
101
|
+
for item in value.values():
|
|
102
|
+
scan(item)
|
|
103
|
+
elif isinstance(value, list):
|
|
104
|
+
for item in value:
|
|
105
|
+
scan(item)
|
|
106
|
+
|
|
107
|
+
for step in steps:
|
|
108
|
+
scan(step)
|
|
109
|
+
return names
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Tasks that reset a result's level before anything downstream ships it -
|
|
113
|
+
# a result only these read is not itself a headroom concern (dw/result.py,
|
|
114
|
+
# warn_without_headroom)
|
|
115
|
+
NORMALIZING_COMMANDS = {"normalize_audio", "match_levels"}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def normalized_downstream(steps, name):
|
|
119
|
+
"""Whether a later normalize_audio/match_levels step consumes result `name`.
|
|
120
|
+
|
|
121
|
+
write_song's raw Music 3 mp3 (templates/minimax/music-video) always lands
|
|
122
|
+
at or over full scale and is always normalized before the deliverable
|
|
123
|
+
mux - the pre-ship level is the template's documented, designed-in input
|
|
124
|
+
condition, not a mistake, so a headroom warning on that intermediate
|
|
125
|
+
save trains the reader to ignore job.warnings (#286). Scoped to the two
|
|
126
|
+
tasks that actually reset level, not to any downstream consumer: a step
|
|
127
|
+
that only reads the raw result (a conditioning slice, say) does not
|
|
128
|
+
change what its own written file will sound like.
|
|
129
|
+
"""
|
|
130
|
+
for step in steps:
|
|
131
|
+
if not isinstance(step, dict):
|
|
132
|
+
continue
|
|
133
|
+
task = step.get("task")
|
|
134
|
+
if (
|
|
135
|
+
not isinstance(task, dict)
|
|
136
|
+
or task.get("command") not in NORMALIZING_COMMANDS
|
|
137
|
+
):
|
|
138
|
+
continue
|
|
139
|
+
if any(
|
|
140
|
+
reference_resolves_to(ref, name) for ref in referenced_result_names([step])
|
|
141
|
+
):
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def deep_equal(a, b):
|
|
147
|
+
"""Value equality across the JSON-ish types a resolved step definition holds.
|
|
148
|
+
|
|
149
|
+
realize_args runs before the step loop (and before for_each expansion's
|
|
150
|
+
per-member `from_file` construction), so a resolved argument can be a
|
|
151
|
+
diffusers reference dataclass wrapping in-memory media - a
|
|
152
|
+
MiniMaxH3ImageReference, an LTX2ReferenceCondition - built fresh by
|
|
153
|
+
`from_file()` on every run from the same source file. Two such instances
|
|
154
|
+
hold identical media but are never `==`: PIL.Image has no value equality
|
|
155
|
+
(falls back to identity), and comparing torch.Tensor/np.ndarray fields
|
|
156
|
+
with `==` yields an array rather than a bool, which the dataclass's
|
|
157
|
+
generated `__eq__` cannot resolve to True/False and which the fallback
|
|
158
|
+
below (correctly) treats as "cannot tell, so unequal". Left unhandled,
|
|
159
|
+
that made every for_each member carrying an image/audio/video reference
|
|
160
|
+
an unconditional cache miss - identical inputs included (#253). These
|
|
161
|
+
checks give those types real value equality before the generic fallback.
|
|
162
|
+
"""
|
|
163
|
+
if a is b:
|
|
164
|
+
return True
|
|
165
|
+
if type(a) is not type(b):
|
|
166
|
+
return False
|
|
167
|
+
if isinstance(a, dict):
|
|
168
|
+
return a.keys() == b.keys() and all(deep_equal(a[k], b[k]) for k in a)
|
|
169
|
+
if isinstance(a, (list, tuple)):
|
|
170
|
+
return len(a) == len(b) and all(deep_equal(x, y) for x, y in zip(a, b))
|
|
171
|
+
if isinstance(a, Image.Image):
|
|
172
|
+
return a.mode == b.mode and a.size == b.size and a.tobytes() == b.tobytes()
|
|
173
|
+
if isinstance(a, np.ndarray):
|
|
174
|
+
return a.shape == b.shape and a.dtype == b.dtype and bool(np.array_equal(a, b))
|
|
175
|
+
if isinstance(a, torch.Tensor):
|
|
176
|
+
return (
|
|
177
|
+
a.shape == b.shape
|
|
178
|
+
and a.dtype == b.dtype
|
|
179
|
+
and bool(torch.equal(a.cpu(), b.cpu()))
|
|
180
|
+
)
|
|
181
|
+
if dataclasses.is_dataclass(a) and not isinstance(a, type):
|
|
182
|
+
return all(
|
|
183
|
+
deep_equal(getattr(a, f.name), getattr(b, f.name))
|
|
184
|
+
for f in dataclasses.fields(a)
|
|
185
|
+
)
|
|
186
|
+
try:
|
|
187
|
+
# An exotic object's == can still raise, or (for a type not caught
|
|
188
|
+
# above) return something other than a bool. A value that cannot
|
|
189
|
+
# answer "are these equal" cleanly is treated as unequal: a cache
|
|
190
|
+
# miss just re-runs the step, where a raised exception would abort
|
|
191
|
+
# the whole run
|
|
192
|
+
return bool(a == b)
|
|
193
|
+
except (ValueError, TypeError, RuntimeError):
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _approx_bytes(value, seen):
|
|
198
|
+
"""Approximate resident size of a retained result item, in bytes.
|
|
199
|
+
|
|
200
|
+
Walks the same shapes a pipeline output actually takes - a dict/dataclass
|
|
201
|
+
of arrays wrapping frames and audio, nested lists of per-frame images -
|
|
202
|
+
rather than every Python object, so an unrecognized type (a bare string,
|
|
203
|
+
a plain number) costs nothing rather than raising. `seen` is shared
|
|
204
|
+
across one Result's whole result_list so an object two artifacts both
|
|
205
|
+
reference (get_artifact_list's fitted-in-place audio, say) is not
|
|
206
|
+
double-counted.
|
|
207
|
+
"""
|
|
208
|
+
key = id(value)
|
|
209
|
+
if key in seen:
|
|
210
|
+
return 0
|
|
211
|
+
seen.add(key)
|
|
212
|
+
if isinstance(value, torch.Tensor):
|
|
213
|
+
return value.element_size() * value.nelement()
|
|
214
|
+
if isinstance(value, np.ndarray):
|
|
215
|
+
return value.nbytes
|
|
216
|
+
if isinstance(value, Image.Image):
|
|
217
|
+
bands = len(value.getbands()) or 1
|
|
218
|
+
return value.width * value.height * bands
|
|
219
|
+
if isinstance(value, (bytes, bytearray)):
|
|
220
|
+
return len(value)
|
|
221
|
+
if isinstance(value, dict):
|
|
222
|
+
return sum(_approx_bytes(v, seen) for v in value.values())
|
|
223
|
+
if isinstance(value, (list, tuple)):
|
|
224
|
+
return sum(_approx_bytes(v, seen) for v in value)
|
|
225
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
226
|
+
return sum(
|
|
227
|
+
_approx_bytes(getattr(value, f.name), seen)
|
|
228
|
+
for f in dataclasses.fields(value)
|
|
229
|
+
)
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _result_bytes(result):
|
|
234
|
+
seen = set()
|
|
235
|
+
return sum(_approx_bytes(item, seen) for item in result.result_list)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class StepCache:
|
|
239
|
+
"""Per-process cache of the last Result produced for each
|
|
240
|
+
(workflow_id, step_name).
|
|
241
|
+
|
|
242
|
+
Bounded: entries hold realized media, so an unbounded cache would work
|
|
243
|
+
directly against release_unreferenced_results' OOM avoidance. The
|
|
244
|
+
least-recently-used entry is evicted once the cap is reached. The
|
|
245
|
+
default is sized so a maximal for_each run - 32 entries over two groups
|
|
246
|
+
plus fixed steps, ~70 members - fits without evicting its own earlier
|
|
247
|
+
members.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
DEFAULT_MAX_ENTRIES = 128
|
|
251
|
+
# 4 GiB: enough for several retained shot@ videos at once, small next to
|
|
252
|
+
# the VRAM/RAM a generation step itself needs, and never the only thing
|
|
253
|
+
# standing between a run and OOM - release_unreferenced_results and the
|
|
254
|
+
# entry cap both still apply
|
|
255
|
+
DEFAULT_MAX_RETAINED_BYTES = 4 * 1024**3
|
|
256
|
+
|
|
257
|
+
def __init__(self, max_entries=None, max_retained_bytes=None):
|
|
258
|
+
# (workflow_id, step_name) -> {"step_data", "step_seed", "result",
|
|
259
|
+
# "output_dir", "generation", "upstream_generations", "retained",
|
|
260
|
+
# "size"}, ordered least- to most-recently-used
|
|
261
|
+
self._entries = OrderedDict()
|
|
262
|
+
self.max_entries = (
|
|
263
|
+
self.DEFAULT_MAX_ENTRIES if max_entries is None else max_entries
|
|
264
|
+
)
|
|
265
|
+
self.max_retained_bytes = (
|
|
266
|
+
self.DEFAULT_MAX_RETAINED_BYTES
|
|
267
|
+
if max_retained_bytes is None
|
|
268
|
+
else max_retained_bytes
|
|
269
|
+
)
|
|
270
|
+
self._retained_bytes = 0
|
|
271
|
+
|
|
272
|
+
def clear(self):
|
|
273
|
+
# The generation counter deliberately survives: it only has to be
|
|
274
|
+
# monotonic, and restarting it could make a stale reference match
|
|
275
|
+
self._entries.clear()
|
|
276
|
+
self._retained_bytes = 0
|
|
277
|
+
|
|
278
|
+
def stats(self):
|
|
279
|
+
"""A read-only snapshot for a caller that cannot see inside the
|
|
280
|
+
worker process otherwise (#418) - entries/retained_bytes are exactly
|
|
281
|
+
the eviction accounting above, so this is how a stale-drop or an
|
|
282
|
+
LRU-evict actually freeing bytes becomes externally observable."""
|
|
283
|
+
return {
|
|
284
|
+
"entries": len(self._entries),
|
|
285
|
+
"max_entries": self.max_entries,
|
|
286
|
+
"retained_bytes": self._retained_bytes,
|
|
287
|
+
"max_retained_bytes": self.max_retained_bytes,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
def get(
|
|
291
|
+
self, workflow_id, step_data, step_seed, hits_this_run, output_dir, needs_result
|
|
292
|
+
):
|
|
293
|
+
"""Return the cached Result for this step if it's still valid, else None.
|
|
294
|
+
|
|
295
|
+
`needs_result` says whether this run reads the step's Result (a later
|
|
296
|
+
step references it, or it is the workflow's return value); an entry
|
|
297
|
+
stored without one cannot serve such a run.
|
|
298
|
+
"""
|
|
299
|
+
name = step_data["name"]
|
|
300
|
+
key = (workflow_id, name)
|
|
301
|
+
entry = self._entries.get(key)
|
|
302
|
+
if entry is None:
|
|
303
|
+
logger.debug(f"No cache entry for step '{name}' - treating as a miss")
|
|
304
|
+
return None
|
|
305
|
+
if entry["step_seed"] != step_seed:
|
|
306
|
+
logger.debug(
|
|
307
|
+
f"Cached result for step '{name}' used a different seed - treating as a miss"
|
|
308
|
+
)
|
|
309
|
+
return None
|
|
310
|
+
# The output *root* a run was told to write to. A run directory is
|
|
311
|
+
# new every execution and would defeat the cache; the root changing
|
|
312
|
+
# means the caller asked for output somewhere the cached files are not
|
|
313
|
+
if entry["output_dir"] != output_dir:
|
|
314
|
+
logger.debug(
|
|
315
|
+
f"Cached result for step '{name}' used a different output_dir - treating as a miss"
|
|
316
|
+
)
|
|
317
|
+
return None
|
|
318
|
+
if needs_result and not entry["retained"]:
|
|
319
|
+
logger.debug(
|
|
320
|
+
f"Cached result for step '{name}' did not retain its Result, and this run needs one - treating as a miss"
|
|
321
|
+
)
|
|
322
|
+
return None
|
|
323
|
+
if not deep_equal(entry["step_data"], step_data):
|
|
324
|
+
logger.debug(
|
|
325
|
+
f"Cached result for step '{name}' has different resolved arguments - treating as a miss"
|
|
326
|
+
)
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
upstream = referenced_result_names([step_data])
|
|
330
|
+
if not all(self._is_hit(ref, hits_this_run) for ref in upstream):
|
|
331
|
+
return None
|
|
332
|
+
# Hitting this run is not the same as being the run this entry was
|
|
333
|
+
# computed from - compare the upstream generations too
|
|
334
|
+
for upstream_name, generation in entry["upstream_generations"].items():
|
|
335
|
+
current = self._entries.get((workflow_id, upstream_name))
|
|
336
|
+
if current is None or current["generation"] != generation:
|
|
337
|
+
logger.debug(
|
|
338
|
+
f"Cached result for step '{name}' was computed from an "
|
|
339
|
+
f"older '{upstream_name}' - treating as a miss"
|
|
340
|
+
)
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
# A hit reports the entry's saved_files verbatim into the manifest
|
|
344
|
+
# and job history - if the user deleted one of them (gallery delete
|
|
345
|
+
# button, or by hand), the entry is stale and the step must re-run
|
|
346
|
+
if not self._saved_files_exist(entry["result"]):
|
|
347
|
+
logger.debug(
|
|
348
|
+
f"Cached result for step '{name}' names a file that no longer "
|
|
349
|
+
"exists - treating as a miss"
|
|
350
|
+
)
|
|
351
|
+
stale = self._entries.pop(key, None)
|
|
352
|
+
if stale is not None:
|
|
353
|
+
self._retained_bytes -= stale["size"]
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
self._entries.move_to_end(key)
|
|
357
|
+
return entry["result"]
|
|
358
|
+
|
|
359
|
+
def put(self, workflow_id, step_data, step_seed, result, output_dir, retain_result):
|
|
360
|
+
"""Record this step's outcome.
|
|
361
|
+
|
|
362
|
+
`retain_result` says whether anything reads the Result itself. When
|
|
363
|
+
False only a stripped copy is kept - saved_files and definition, but
|
|
364
|
+
no result_list - so decoded frames and latent tensors are not pinned
|
|
365
|
+
for the life of the cache, which is exactly what
|
|
366
|
+
release_unreferenced_results drops them to avoid.
|
|
367
|
+
|
|
368
|
+
A Result whose result_list holds an artifact save() already spilled
|
|
369
|
+
and cleaned up (a chain step's save_segments - see Result.retainable)
|
|
370
|
+
is downgraded to the same stripped storage even when retain_result is
|
|
371
|
+
True: its files are gone already by the time put() runs (save()
|
|
372
|
+
happens before put() in the step loop), so keeping the result_list
|
|
373
|
+
would let a later cache hit fail opening files that no longer exist.
|
|
374
|
+
"""
|
|
375
|
+
retain_result = retain_result and getattr(result, "retainable", True)
|
|
376
|
+
name = step_data["name"]
|
|
377
|
+
key = (workflow_id, name)
|
|
378
|
+
size = _result_bytes(result) if retain_result else 0
|
|
379
|
+
previous = self._entries.get(key)
|
|
380
|
+
if previous is not None:
|
|
381
|
+
self._retained_bytes -= previous["size"]
|
|
382
|
+
self._entries[key] = {
|
|
383
|
+
"step_data": step_data,
|
|
384
|
+
"step_seed": step_seed,
|
|
385
|
+
"result": result if retain_result else self._strip_result(result),
|
|
386
|
+
"retained": retain_result,
|
|
387
|
+
"output_dir": output_dir,
|
|
388
|
+
"generation": next(_generations),
|
|
389
|
+
"upstream_generations": self._upstream_generations(workflow_id, step_data),
|
|
390
|
+
"size": size,
|
|
391
|
+
}
|
|
392
|
+
self._retained_bytes += size
|
|
393
|
+
self._entries.move_to_end(key)
|
|
394
|
+
while len(self._entries) > 1 and (
|
|
395
|
+
len(self._entries) > self.max_entries
|
|
396
|
+
or self._retained_bytes > self.max_retained_bytes
|
|
397
|
+
):
|
|
398
|
+
(evicted_workflow, evicted_step), evicted = self._entries.popitem(
|
|
399
|
+
last=False
|
|
400
|
+
)
|
|
401
|
+
self._retained_bytes -= evicted["size"]
|
|
402
|
+
logger.debug(
|
|
403
|
+
"Step cache full - evicting least recently used "
|
|
404
|
+
f"'{evicted_workflow}/{evicted_step}'"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def _upstream_generations(self, workflow_id, step_data):
|
|
408
|
+
"""The generation of every cached step this step's references read."""
|
|
409
|
+
generations = {}
|
|
410
|
+
for reference in referenced_result_names([step_data]):
|
|
411
|
+
for (entry_workflow, entry_step), entry in self._entries.items():
|
|
412
|
+
if entry_workflow == workflow_id and reference_resolves_to(
|
|
413
|
+
reference, entry_step
|
|
414
|
+
):
|
|
415
|
+
generations[entry_step] = entry["generation"]
|
|
416
|
+
return generations
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def _strip_result(result):
|
|
420
|
+
"""A shallow copy of the Result with its realized media dropped."""
|
|
421
|
+
stripped = copy.copy(result)
|
|
422
|
+
stripped.result_list = []
|
|
423
|
+
# A fresh cache, not the shallow copy's shared one: save() fills
|
|
424
|
+
# _artifact_cache with the artifacts it extracted - decoded frames, a
|
|
425
|
+
# waveform, possibly still on the GPU - so carrying it over would pin
|
|
426
|
+
# exactly what dropping result_list exists to release. The original
|
|
427
|
+
# Result keeps its own cache; only this entry's copy starts empty.
|
|
428
|
+
stripped._artifact_cache = {}
|
|
429
|
+
return stripped
|
|
430
|
+
|
|
431
|
+
@staticmethod
|
|
432
|
+
def _saved_files_exist(result):
|
|
433
|
+
# Read straight through: a result-like object with no saved_files is
|
|
434
|
+
# a programming error, not an entry that quietly skips the file check
|
|
435
|
+
return all(os.path.exists(path) for path in result.saved_files)
|
|
436
|
+
|
|
437
|
+
@staticmethod
|
|
438
|
+
def _is_hit(ref, hits_this_run):
|
|
439
|
+
return any(reference_resolves_to(ref, n) for n in hits_this_run)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
step_cache = StepCache()
|
dw/subfolders.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""A step's result 'subfolder': where under the run directory its files go.
|
|
2
|
+
|
|
3
|
+
A run writes everything into one directory, so a finished episode sits
|
|
4
|
+
beside the twenty scratch files that went into it, distinguished only by
|
|
5
|
+
the step name in each file name. 'subfolder' on a step's result block puts
|
|
6
|
+
that step's files into a subfolder of the run directory instead - by
|
|
7
|
+
convention 'final' or 'intermediate', though the engine treats no name
|
|
8
|
+
specially. Nothing else about placement changes: a step without one writes
|
|
9
|
+
where it always did.
|
|
10
|
+
|
|
11
|
+
This module owns the shape check and the static pass over an expanded
|
|
12
|
+
definition. Containment - that the joined path really is inside the run
|
|
13
|
+
directory - is the engine's, at the moment it joins (see
|
|
14
|
+
Workflow.step_output_dir).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .for_each import MEMBER_SEPARATOR, render_path
|
|
18
|
+
from .security import (
|
|
19
|
+
InvalidInputError,
|
|
20
|
+
validate_file_base_name,
|
|
21
|
+
validate_subfolder,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
SUBFOLDER_KEY = "subfolder"
|
|
25
|
+
FILE_BASE_NAME_KEY = "file_base_name"
|
|
26
|
+
|
|
27
|
+
# Reference prefixes substitution resolves before this pass runs. One still
|
|
28
|
+
# spelled out here is one nothing resolved, and that is the undeclared-
|
|
29
|
+
# variable pass's complaint rather than a shape error
|
|
30
|
+
_UNRESOLVED_PREFIXES = ("variable:", "item:")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def step_subfolder(step_definition):
|
|
34
|
+
"""The validated subfolder a step's result names, or '' when it names
|
|
35
|
+
none.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
InvalidInputError: If the value is not a string or not a valid
|
|
39
|
+
subfolder
|
|
40
|
+
"""
|
|
41
|
+
result = step_definition.get("result")
|
|
42
|
+
if not isinstance(result, dict) or SUBFOLDER_KEY not in result:
|
|
43
|
+
return ""
|
|
44
|
+
value = result[SUBFOLDER_KEY]
|
|
45
|
+
if not isinstance(value, str):
|
|
46
|
+
raise InvalidInputError(
|
|
47
|
+
f"Invalid subfolder: {value!r} - a subfolder is a string like 'final'"
|
|
48
|
+
)
|
|
49
|
+
return validate_subfolder(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def subfolder_errors(workflow_definition, source_indices=None):
|
|
53
|
+
"""Every result 'subfolder' or 'file_base_name' that cannot be written,
|
|
54
|
+
as [{path, message}].
|
|
55
|
+
|
|
56
|
+
The definition handed here has already been substituted and expanded,
|
|
57
|
+
so every value in it is literal; a 'variable:' or 'item:' still spelled
|
|
58
|
+
out is left alone. `source_indices`, when given, is the source step
|
|
59
|
+
index of each step - a 'for_each' group turns one written step into
|
|
60
|
+
several, and the path an error carries has to be one the author can
|
|
61
|
+
find in the file they wrote; the member is named in the message.
|
|
62
|
+
"""
|
|
63
|
+
steps = workflow_definition.get("steps")
|
|
64
|
+
if not isinstance(steps, list):
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
errors = []
|
|
68
|
+
for index, step in enumerate(steps):
|
|
69
|
+
if not isinstance(step, dict):
|
|
70
|
+
continue
|
|
71
|
+
result = step.get("result")
|
|
72
|
+
if not isinstance(result, dict):
|
|
73
|
+
continue
|
|
74
|
+
source = (
|
|
75
|
+
source_indices[index]
|
|
76
|
+
if source_indices is not None and index < len(source_indices)
|
|
77
|
+
else index
|
|
78
|
+
)
|
|
79
|
+
name = step.get("name")
|
|
80
|
+
where = (
|
|
81
|
+
f" in member '{name}'"
|
|
82
|
+
if isinstance(name, str) and MEMBER_SEPARATOR in name
|
|
83
|
+
else ""
|
|
84
|
+
)
|
|
85
|
+
for key, check in (
|
|
86
|
+
(SUBFOLDER_KEY, validate_subfolder),
|
|
87
|
+
(FILE_BASE_NAME_KEY, validate_file_base_name),
|
|
88
|
+
):
|
|
89
|
+
if key not in result:
|
|
90
|
+
continue
|
|
91
|
+
value = result[key]
|
|
92
|
+
if isinstance(value, str) and value.startswith(_UNRESOLVED_PREFIXES):
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
if not isinstance(value, str):
|
|
96
|
+
raise InvalidInputError(
|
|
97
|
+
f"Invalid {key}: {value!r} - expected a string"
|
|
98
|
+
)
|
|
99
|
+
check(value)
|
|
100
|
+
except InvalidInputError as e:
|
|
101
|
+
errors.append(
|
|
102
|
+
{
|
|
103
|
+
"path": render_path(("steps", source, "result", key)),
|
|
104
|
+
"message": f"{e}{where}",
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return errors
|