diffusers-workflow 0.4.0a3__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.0a3.dist-info/METADATA +310 -0
- diffusers_workflow-0.4.0a3.dist-info/RECORD +171 -0
- diffusers_workflow-0.4.0a3.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0a3.dist-info/entry_points.txt +6 -0
- diffusers_workflow-0.4.0a3.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0a3.dist-info/top_level.txt +1 -0
- dw/__init__.py +353 -0
- dw/arguments.py +906 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +145 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/events.py +78 -0
- dw/hub_cache.py +289 -0
- dw/introspection.py +458 -0
- dw/log_setup.py +45 -0
- dw/pipeline_processors/chain.py +750 -0
- dw/pipeline_processors/config_objects.py +235 -0
- dw/pipeline_processors/pipeline.py +1687 -0
- dw/pipeline_processors/remote.py +18 -0
- dw/previous_results.py +259 -0
- dw/prompt_weighting.py +378 -0
- dw/repl.py +298 -0
- dw/repl_commands.py +808 -0
- dw/repl_worker.py +129 -0
- dw/result.py +850 -0
- dw/run.py +92 -0
- dw/schema.py +24 -0
- dw/security.py +379 -0
- dw/serve.py +70 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +588 -0
- dw/server/jobs.py +547 -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-CEh6hWi2.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-CExg3_mM.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-DH6orYh2.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-CbrMVW4Q.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-YDNPZw2M.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-B_zSGWO2.js +1 -0
- dw/server/ui/assets/index-B7-VcYS-.css +1 -0
- dw/server/ui/assets/index-D_EiPU3b.js +13 -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-dYuBvioq.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-CUqLM39V.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-D6vxBzMv.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-1WJ2palX.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-SHQb6vmD.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-CP-s5rcP.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-x0_EGHq9.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-BZC4LQDP.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-BTfA6SbD.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-CWA4MsNk.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-CWU0uvj5.js +1 -0
- dw/server/ui/assets/xml-KmfTm3rg.js +1 -0
- dw/server/ui/assets/yaml-nFO_dDS6.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/settings.py +77 -0
- dw/step.py +132 -0
- dw/tasks/audio_utils.py +266 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/concat_videos.py +80 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +139 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +661 -0
- dw/tasks/interpolate_frames.py +227 -0
- dw/tasks/model_cache.py +39 -0
- dw/tasks/pair_audio.py +58 -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/task.py +474 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +168 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +154 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +376 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +68 -0
- dw/validate.py +43 -0
- dw/variables.py +153 -0
- dw/worker.py +517 -0
- dw/workflow.py +553 -0
- dw/workflow_schema.json +1157 -0
- dw/workflows/augment_prompt.json +65 -0
- dw/workflows/describe_image.json +58 -0
- dw/workflows/h3_context_ir.json +57 -0
- dw/workflows/test.json +31 -0
dw/workflow.py
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# Core functionality for loading and executing workflows
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import torch
|
|
5
|
+
import copy
|
|
6
|
+
import gc
|
|
7
|
+
import hashlib
|
|
8
|
+
import logging
|
|
9
|
+
from .arguments import realize_args, realize_constants
|
|
10
|
+
from .events import (
|
|
11
|
+
RunContext,
|
|
12
|
+
WorkflowCancelled,
|
|
13
|
+
get_context,
|
|
14
|
+
current_context,
|
|
15
|
+
activate_context,
|
|
16
|
+
deactivate_context,
|
|
17
|
+
)
|
|
18
|
+
from .step import Step
|
|
19
|
+
from .schema import validate_data, load_schema
|
|
20
|
+
from .variables import replace_variables, set_variables
|
|
21
|
+
from .pipeline_processors.pipeline import Pipeline
|
|
22
|
+
from .tasks.model_cache import clear_model_cache
|
|
23
|
+
from .tasks.task import Task
|
|
24
|
+
from . import get_device, empty_device_cache
|
|
25
|
+
from .security import (
|
|
26
|
+
validate_path,
|
|
27
|
+
validate_workflow_path,
|
|
28
|
+
validate_json_size,
|
|
29
|
+
validate_output_path,
|
|
30
|
+
SecurityError,
|
|
31
|
+
PathTraversalError,
|
|
32
|
+
InvalidInputError,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger("dw")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def workflow_from_file(file_spec, output_dir):
|
|
39
|
+
"""Loads a workflow from a JSON file with security validation"""
|
|
40
|
+
logger.debug(f"Loading workflow from file: {file_spec}")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Validate file path and size
|
|
44
|
+
validated_path = validate_workflow_path(file_spec)
|
|
45
|
+
validate_json_size(validated_path)
|
|
46
|
+
validated_output = validate_output_path(output_dir, None)
|
|
47
|
+
|
|
48
|
+
with open(validated_path, "r") as file:
|
|
49
|
+
workflow_data = json.load(file)
|
|
50
|
+
|
|
51
|
+
return Workflow(workflow_data, validated_output, validated_path)
|
|
52
|
+
|
|
53
|
+
except SecurityError as e:
|
|
54
|
+
logger.error(f"Security validation failed for workflow {file_spec}: {e}")
|
|
55
|
+
raise
|
|
56
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
57
|
+
logger.error(f"Failed to load workflow from {file_spec}: {e}")
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def workflow_from_definition(workflow_definition, output_dir, base_dir=None):
|
|
62
|
+
"""A Workflow from an inline definition (no file on disk).
|
|
63
|
+
|
|
64
|
+
The synthetic '__inline__.json' file_spec exists only to carry the
|
|
65
|
+
directory that relative paths inside the definition resolve against.
|
|
66
|
+
base_dir is caller-supplied (over HTTP, client-supplied) path-shaped
|
|
67
|
+
input, so it goes through the security validator like every other path.
|
|
68
|
+
"""
|
|
69
|
+
validated_output = validate_output_path(output_dir, None)
|
|
70
|
+
if base_dir:
|
|
71
|
+
validated_base = validate_path(base_dir, allow_create=False)
|
|
72
|
+
if not os.path.isdir(validated_base):
|
|
73
|
+
raise InvalidInputError(f"base_dir is not a directory: {base_dir}")
|
|
74
|
+
else:
|
|
75
|
+
validated_base = os.getcwd()
|
|
76
|
+
return Workflow(
|
|
77
|
+
workflow_definition,
|
|
78
|
+
validated_output,
|
|
79
|
+
os.path.join(validated_base, "__inline__.json"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pipeline_cache_key(pipeline_definition):
|
|
84
|
+
"""Stable identity for a loaded pipeline.
|
|
85
|
+
|
|
86
|
+
Hashes everything that shapes loading - configuration, components,
|
|
87
|
+
quantization, loras - and excludes what varies per call (arguments, seed,
|
|
88
|
+
chain), so a cache hit means "this exact model stack is already loaded".
|
|
89
|
+
Keying the cache by identity instead of step name means two workflows
|
|
90
|
+
whose steps happen to share a name can no longer collide, and a rerun of
|
|
91
|
+
an edited workflow keeps every pipeline whose definition did not change.
|
|
92
|
+
|
|
93
|
+
Computed after variable substitution but the excluded keys keep realized
|
|
94
|
+
per-run values (images, generators) out of the hash; realized types and
|
|
95
|
+
dtypes stringify stably via default=str.
|
|
96
|
+
"""
|
|
97
|
+
load_definition = {
|
|
98
|
+
k: v
|
|
99
|
+
for k, v in pipeline_definition.items()
|
|
100
|
+
if k not in ("arguments", "seed", "chain")
|
|
101
|
+
}
|
|
102
|
+
serialized = json.dumps(load_definition, sort_keys=True, default=str)
|
|
103
|
+
return hashlib.sha256(serialized.encode()).hexdigest()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def referenced_result_names(steps):
|
|
107
|
+
"""Every previous_result reference the given steps make, as full names.
|
|
108
|
+
|
|
109
|
+
Scans nested dicts and lists, so references inside pipeline arguments,
|
|
110
|
+
task arguments and sub-workflow argument maps are all found.
|
|
111
|
+
"""
|
|
112
|
+
prefix = "previous_result:"
|
|
113
|
+
names = set()
|
|
114
|
+
|
|
115
|
+
def scan(value):
|
|
116
|
+
if isinstance(value, str) and value.startswith(prefix):
|
|
117
|
+
names.add(value[len(prefix) :])
|
|
118
|
+
elif isinstance(value, dict):
|
|
119
|
+
for item in value.values():
|
|
120
|
+
scan(item)
|
|
121
|
+
elif isinstance(value, list):
|
|
122
|
+
for item in value:
|
|
123
|
+
scan(item)
|
|
124
|
+
|
|
125
|
+
for step in steps:
|
|
126
|
+
scan(step)
|
|
127
|
+
return names
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def release_unreferenced_results(results, remaining_refs):
|
|
131
|
+
"""Drop results no remaining reference can resolve to.
|
|
132
|
+
|
|
133
|
+
A reference resolves to a result whose name it equals or extends with a
|
|
134
|
+
property ('step.mask'), so any result that is such a prefix stays. Saved
|
|
135
|
+
artifacts are already on disk - holding every intermediate image and frame
|
|
136
|
+
list in RAM until the workflow ends is what OOMs long chains.
|
|
137
|
+
"""
|
|
138
|
+
for name in [
|
|
139
|
+
n
|
|
140
|
+
for n in results
|
|
141
|
+
if not any(ref == n or ref.startswith(n + ".") for ref in remaining_refs)
|
|
142
|
+
]:
|
|
143
|
+
logger.debug(f"Releasing result: {name}")
|
|
144
|
+
del results[name]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Workflow:
|
|
148
|
+
"""
|
|
149
|
+
Main class for managing and executing workflows defined in JSON format
|
|
150
|
+
Handles variable substitution, step execution, and result management
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, workflow_definition, output_dir, file_spec):
|
|
154
|
+
self.workflow_definition = workflow_definition
|
|
155
|
+
self.output_dir = output_dir
|
|
156
|
+
self.file_spec = file_spec
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def name(self):
|
|
160
|
+
return self.workflow_definition.get("id", "unknown")
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def argument_template(self):
|
|
164
|
+
return self.workflow_definition.get("argument_template", {})
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def variables(self):
|
|
168
|
+
return self.workflow_definition.get("variables", {})
|
|
169
|
+
|
|
170
|
+
def step_file_prefix(self, step_name):
|
|
171
|
+
"""Naming prefix for files a step writes on its own (chain segment
|
|
172
|
+
spills), matching the workflow-id-step naming its results are saved
|
|
173
|
+
under."""
|
|
174
|
+
return f"{self.name}-{step_name}"
|
|
175
|
+
|
|
176
|
+
def validate(self):
|
|
177
|
+
"""Validates workflow definition against JSON schema"""
|
|
178
|
+
logger.debug(f"Validating workflow: {self.name}")
|
|
179
|
+
status, message = validate_data(
|
|
180
|
+
self.workflow_definition, load_schema("workflow")
|
|
181
|
+
)
|
|
182
|
+
if not status:
|
|
183
|
+
logger.error(f"Validation error: {message}")
|
|
184
|
+
raise Exception(f"Validation error: {message}")
|
|
185
|
+
logger.debug(f"Workflow {self.name} validated successfully")
|
|
186
|
+
|
|
187
|
+
def run(
|
|
188
|
+
self, arguments, previous_pipelines=None, context=None, prior_step_keys=None
|
|
189
|
+
):
|
|
190
|
+
"""
|
|
191
|
+
Executes the workflow by:
|
|
192
|
+
1. Processing variables
|
|
193
|
+
2. Setting up random seed
|
|
194
|
+
3. Running each step in sequence
|
|
195
|
+
4. Managing results between steps
|
|
196
|
+
|
|
197
|
+
An explicit RunContext receives progress events and can cancel the
|
|
198
|
+
run; without one, the ambient context is reused (a sub-workflow
|
|
199
|
+
reports into its parent's run) or a no-op context is created.
|
|
200
|
+
Saved file paths accumulate in self.manifest, one entry per step.
|
|
201
|
+
"""
|
|
202
|
+
run_context = context or current_context() or RunContext()
|
|
203
|
+
context_token = activate_context(run_context)
|
|
204
|
+
# Step name -> cache key for this run, so release_pipeline and
|
|
205
|
+
# pipeline_reference still address pipelines by the step that made them
|
|
206
|
+
self._pipeline_keys_by_step = {}
|
|
207
|
+
# Last run's step->key map: a redefined step's old model is evicted
|
|
208
|
+
# BEFORE its replacement loads, or the transition holds both at once
|
|
209
|
+
self._prior_step_keys = prior_step_keys or {}
|
|
210
|
+
self.manifest = []
|
|
211
|
+
try:
|
|
212
|
+
# CRITICAL: Work on a copy to avoid mutating the original workflow definition
|
|
213
|
+
# This allows the workflow to be run multiple times with different arguments
|
|
214
|
+
workflow_def = copy.deepcopy(self.workflow_definition)
|
|
215
|
+
|
|
216
|
+
workflow_id = workflow_def["id"]
|
|
217
|
+
logger.debug(f"Processing workflow: {workflow_id}")
|
|
218
|
+
|
|
219
|
+
# File paths in workflows are relative to the workflow file
|
|
220
|
+
base_dir = (
|
|
221
|
+
os.path.dirname(os.path.abspath(self.file_spec))
|
|
222
|
+
if self.file_spec
|
|
223
|
+
else None
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Handle variable substitution if variables are defined
|
|
227
|
+
variables = workflow_def.get("variables", None)
|
|
228
|
+
if variables is not None:
|
|
229
|
+
logger.debug(f"Setting variables for workflow: {workflow_id}")
|
|
230
|
+
# a constant is the value a variable declares, so it resolves before
|
|
231
|
+
# anything is converted to the type of that declaration
|
|
232
|
+
realize_constants(variables)
|
|
233
|
+
# first set variable values base don the arguments passed to the workflow
|
|
234
|
+
# these may come form the command line or form a parent workflow
|
|
235
|
+
set_variables(arguments, variables)
|
|
236
|
+
# realize the variables, initialiting downloads of images etc
|
|
237
|
+
realize_args(variables, base_dir)
|
|
238
|
+
## then replace any variable references in the workflow definition with the actual values
|
|
239
|
+
replace_variables(workflow_def, variables)
|
|
240
|
+
|
|
241
|
+
# Set up random seed for reproducibility. Resolved lazily - as a
|
|
242
|
+
# dict.get default, torch.seed() would run on every call and reseed
|
|
243
|
+
# the global RNG even when the workflow names an explicit seed
|
|
244
|
+
default_seed = workflow_def.get("seed")
|
|
245
|
+
if default_seed is None:
|
|
246
|
+
# A fresh generator draws a random seed without touching the
|
|
247
|
+
# global RNG the process may have seeded for reproducibility
|
|
248
|
+
default_seed = torch.Generator().seed()
|
|
249
|
+
workflow_def["seed"] = default_seed
|
|
250
|
+
|
|
251
|
+
# Initialize collections for sharing state between steps
|
|
252
|
+
results = {} # Stores results from each step
|
|
253
|
+
shared_components = {} # Shared resources between steps
|
|
254
|
+
|
|
255
|
+
# Use provided pipelines cache or create new dict
|
|
256
|
+
# This allows pipeline reuse across multiple workflow runs
|
|
257
|
+
if previous_pipelines is None:
|
|
258
|
+
pipelines = {}
|
|
259
|
+
logger.debug("Starting with empty pipeline cache")
|
|
260
|
+
else:
|
|
261
|
+
pipelines = previous_pipelines
|
|
262
|
+
logger.debug(f"Reusing pipeline cache with {len(pipelines)} pipelines")
|
|
263
|
+
|
|
264
|
+
last_result = None # Final result is the workflow return value
|
|
265
|
+
|
|
266
|
+
# realize any arguments for the steps, i.e. load images etc
|
|
267
|
+
# that are referenced directly in the step
|
|
268
|
+
steps = workflow_def.get("steps", [])
|
|
269
|
+
|
|
270
|
+
if not steps:
|
|
271
|
+
logger.warning(f"Workflow {workflow_id} has no steps defined")
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
realize_args(steps, base_dir)
|
|
275
|
+
|
|
276
|
+
run_context.emit(
|
|
277
|
+
"workflow_start",
|
|
278
|
+
workflow=workflow_id,
|
|
279
|
+
total_steps=len(steps),
|
|
280
|
+
steps=[step_data["name"] for step_data in steps],
|
|
281
|
+
seed=default_seed,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Execute each step in sequence
|
|
285
|
+
for i, step_data in enumerate(steps):
|
|
286
|
+
run_context.check_cancelled()
|
|
287
|
+
logger.debug(f"Running step {i+1}/{len(steps)}: {step_data['name']}")
|
|
288
|
+
run_context.emit(
|
|
289
|
+
"step_start",
|
|
290
|
+
workflow=workflow_id,
|
|
291
|
+
step=step_data["name"],
|
|
292
|
+
index=i,
|
|
293
|
+
total_steps=len(steps),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Seeds resolve most-specific-first: pipeline > step > workflow
|
|
297
|
+
step_seed = step_data.get("seed", default_seed)
|
|
298
|
+
|
|
299
|
+
step = Step(step_data, step_seed, self.workflow_definition)
|
|
300
|
+
step_action = self.create_step_action(
|
|
301
|
+
step_data,
|
|
302
|
+
shared_components,
|
|
303
|
+
pipelines,
|
|
304
|
+
step_seed,
|
|
305
|
+
get_device(),
|
|
306
|
+
)
|
|
307
|
+
result = step.run(results, pipelines, step_action)
|
|
308
|
+
last_result = result
|
|
309
|
+
results[step.name] = result
|
|
310
|
+
saved_files = result.save(
|
|
311
|
+
self.output_dir, f"{workflow_id}-{step.name}.{i}"
|
|
312
|
+
)
|
|
313
|
+
self.manifest.append({"step": step.name, "files": saved_files})
|
|
314
|
+
# A sub-workflow's saves land in the child's manifest - roll
|
|
315
|
+
# them up so job history and the gallery see every file
|
|
316
|
+
if isinstance(step_action, Workflow):
|
|
317
|
+
self.manifest.extend(getattr(step_action, "manifest", []))
|
|
318
|
+
run_context.emit(
|
|
319
|
+
"step_end",
|
|
320
|
+
workflow=workflow_id,
|
|
321
|
+
step=step.name,
|
|
322
|
+
index=i,
|
|
323
|
+
total_steps=len(steps),
|
|
324
|
+
files=saved_files,
|
|
325
|
+
)
|
|
326
|
+
logger.debug(f"Step {step.name} completed with result: {result}")
|
|
327
|
+
|
|
328
|
+
# Release results no later step references - saved to disk
|
|
329
|
+
# already, and last_result keeps the workflow's return value
|
|
330
|
+
release_unreferenced_results(
|
|
331
|
+
results, referenced_result_names(steps[i + 1 :])
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# A released pipeline frees its memory for later steps - the
|
|
335
|
+
# alternative on a card that cannot hold two models is offloading
|
|
336
|
+
# everything, which taxes every run to survive one transition
|
|
337
|
+
if step_data.get("release_pipeline", False):
|
|
338
|
+
logger.info(f"Releasing pipeline for step: {step.name}")
|
|
339
|
+
pipelines.pop(self._pipeline_keys_by_step.get(step.name), None)
|
|
340
|
+
|
|
341
|
+
# Task models are cached for the life of the process - the cache
|
|
342
|
+
# exists so a step's cartesian product loads its model once, and
|
|
343
|
+
# nothing else evicts it. A prompt-expanding language model
|
|
344
|
+
# feeding a generation step would otherwise hold its weights on
|
|
345
|
+
# the device for the whole run
|
|
346
|
+
if step_data.get("release_models", False):
|
|
347
|
+
logger.info(f"Releasing task models for step: {step.name}")
|
|
348
|
+
clear_model_cache()
|
|
349
|
+
|
|
350
|
+
# Cleanup between steps (but keep pipelines loaded). Returning
|
|
351
|
+
# cached blocks to the device lets the next step's differently
|
|
352
|
+
# shaped allocations use them
|
|
353
|
+
gc.collect()
|
|
354
|
+
empty_device_cache()
|
|
355
|
+
|
|
356
|
+
logger.debug(f"Workflow {workflow_id} completed successfully")
|
|
357
|
+
run_context.emit(
|
|
358
|
+
"workflow_end", workflow=workflow_id, manifest=self.manifest
|
|
359
|
+
)
|
|
360
|
+
# Return only the last step's results for child workflows
|
|
361
|
+
return last_result.result_list if last_result is not None else []
|
|
362
|
+
|
|
363
|
+
except WorkflowCancelled:
|
|
364
|
+
# The user asked for this - report it without an error traceback
|
|
365
|
+
workflow_id = self.workflow_definition.get("id", "unknown")
|
|
366
|
+
logger.info(f"Workflow {workflow_id} cancelled")
|
|
367
|
+
raise
|
|
368
|
+
except (SecurityError, PathTraversalError, InvalidInputError) as e:
|
|
369
|
+
# Security validation failures - these should fail fast, without the
|
|
370
|
+
# traceback noise of the general handler
|
|
371
|
+
workflow_id = self.workflow_definition.get("id", "unknown")
|
|
372
|
+
logger.error(f"Security error in workflow {workflow_id}: {e}")
|
|
373
|
+
raise
|
|
374
|
+
except Exception as e:
|
|
375
|
+
# One log line with the full traceback - the step already logged its
|
|
376
|
+
# own context, and every clause here did the same log-and-reraise
|
|
377
|
+
workflow_id = self.workflow_definition.get("id", "unknown")
|
|
378
|
+
logger.error(
|
|
379
|
+
f"{type(e).__name__} in workflow {workflow_id}: {e}", exc_info=True
|
|
380
|
+
)
|
|
381
|
+
raise
|
|
382
|
+
finally:
|
|
383
|
+
deactivate_context(context_token)
|
|
384
|
+
|
|
385
|
+
def _step_pipeline_key(self, step_name, cache_key):
|
|
386
|
+
"""Record which cache key a step's pipeline lives under this run."""
|
|
387
|
+
if not hasattr(self, "_pipeline_keys_by_step"):
|
|
388
|
+
self._pipeline_keys_by_step = {}
|
|
389
|
+
self._pipeline_keys_by_step[step_name] = cache_key
|
|
390
|
+
|
|
391
|
+
def create_step_action(
|
|
392
|
+
self,
|
|
393
|
+
step_definition,
|
|
394
|
+
shared_components,
|
|
395
|
+
previous_pipelines,
|
|
396
|
+
default_seed,
|
|
397
|
+
device,
|
|
398
|
+
):
|
|
399
|
+
"""
|
|
400
|
+
Creates the appropriate action object based on step type:
|
|
401
|
+
- Pipeline: Creates new pipeline or reuses cached one
|
|
402
|
+
- Pipeline reference: References existing pipeline
|
|
403
|
+
- Workflow: Loads and validates sub-workflow
|
|
404
|
+
- Task: Creates task object
|
|
405
|
+
"""
|
|
406
|
+
# Handle pipeline creation
|
|
407
|
+
if "pipeline" in step_definition:
|
|
408
|
+
step_name = step_definition["name"]
|
|
409
|
+
|
|
410
|
+
# Pipelines are cached by what they load, not what step loads them
|
|
411
|
+
cache_key = pipeline_cache_key(step_definition["pipeline"])
|
|
412
|
+
self._step_pipeline_key(step_name, cache_key)
|
|
413
|
+
get_context().touch_pipeline(cache_key)
|
|
414
|
+
|
|
415
|
+
# Check if pipeline already loaded in cache (GPU persistence)
|
|
416
|
+
if cache_key in previous_pipelines:
|
|
417
|
+
logger.debug(f"Reusing cached pipeline for step: {step_name}")
|
|
418
|
+
cached_pipeline = previous_pipelines[cache_key]
|
|
419
|
+
# The shared_components dict is fresh every run and only load()
|
|
420
|
+
# fills it - a cache hit must republish or a later step's
|
|
421
|
+
# reused_components finds nothing (impossible under the old
|
|
422
|
+
# whole-file cache, the normal case under identity keys)
|
|
423
|
+
cached_pipeline.publish_shared_components(shared_components)
|
|
424
|
+
# Create new Pipeline wrapper with updated step definition
|
|
425
|
+
# but reuse the loaded model from cache
|
|
426
|
+
new_pipeline_wrapper = Pipeline(
|
|
427
|
+
step_definition["pipeline"],
|
|
428
|
+
default_seed,
|
|
429
|
+
device,
|
|
430
|
+
cached_pipeline.pipeline, # Reuse the actual loaded model
|
|
431
|
+
output_dir=self.output_dir,
|
|
432
|
+
file_prefix=self.step_file_prefix(step_name),
|
|
433
|
+
)
|
|
434
|
+
# Set up generator with potentially new seed. no_generator is a
|
|
435
|
+
# boolean - only an explicit true disables the generator - and the
|
|
436
|
+
# generator lives on the pipeline's own device, which may override
|
|
437
|
+
# the workflow default (the fresh-load path resolves it the same way)
|
|
438
|
+
if not new_pipeline_wrapper.configuration.get("no_generator", False):
|
|
439
|
+
logger.debug(
|
|
440
|
+
"Setting up generator for cached pipeline with new arguments"
|
|
441
|
+
)
|
|
442
|
+
new_pipeline_wrapper.argument_template[
|
|
443
|
+
"generator"
|
|
444
|
+
] = torch.Generator(new_pipeline_wrapper.device).manual_seed(
|
|
445
|
+
new_pipeline_wrapper.pipeline_definition.get(
|
|
446
|
+
"seed", default_seed
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
return new_pipeline_wrapper
|
|
451
|
+
|
|
452
|
+
# Not in cache - a redefined step frees its previous model first,
|
|
453
|
+
# so the swap never holds old and new stacks simultaneously
|
|
454
|
+
prior_key = getattr(self, "_prior_step_keys", {}).get(step_name)
|
|
455
|
+
if prior_key and prior_key != cache_key and prior_key in previous_pipelines:
|
|
456
|
+
logger.info(
|
|
457
|
+
f"Step '{step_name}' was redefined - releasing its previous "
|
|
458
|
+
"pipeline before loading the new one"
|
|
459
|
+
)
|
|
460
|
+
previous_pipelines.pop(prior_key, None)
|
|
461
|
+
gc.collect()
|
|
462
|
+
empty_device_cache()
|
|
463
|
+
|
|
464
|
+
logger.debug(f"Creating pipeline for step: {step_name}")
|
|
465
|
+
pipeline = Pipeline(
|
|
466
|
+
step_definition["pipeline"],
|
|
467
|
+
default_seed,
|
|
468
|
+
device,
|
|
469
|
+
output_dir=self.output_dir,
|
|
470
|
+
file_prefix=self.step_file_prefix(step_name),
|
|
471
|
+
)
|
|
472
|
+
pipeline.load(shared_components)
|
|
473
|
+
previous_pipelines[cache_key] = pipeline
|
|
474
|
+
return pipeline
|
|
475
|
+
|
|
476
|
+
# Handle pipeline reference
|
|
477
|
+
if "pipeline_reference" in step_definition:
|
|
478
|
+
logger.debug(
|
|
479
|
+
f"Referencing existing pipeline for step: {step_definition['name']}"
|
|
480
|
+
)
|
|
481
|
+
pipeline_reference = step_definition["pipeline_reference"]
|
|
482
|
+
reference_name = pipeline_reference["reference_name"]
|
|
483
|
+
referenced_key = self._pipeline_keys_by_step.get(reference_name)
|
|
484
|
+
if referenced_key is None or referenced_key not in previous_pipelines:
|
|
485
|
+
raise ValueError(
|
|
486
|
+
f"pipeline_reference '{reference_name}' does not name an "
|
|
487
|
+
"earlier pipeline step in this run (or it was released)"
|
|
488
|
+
)
|
|
489
|
+
previous_pipeline = previous_pipelines[referenced_key]
|
|
490
|
+
return Pipeline(
|
|
491
|
+
pipeline_reference,
|
|
492
|
+
default_seed,
|
|
493
|
+
device,
|
|
494
|
+
previous_pipeline.pipeline,
|
|
495
|
+
output_dir=self.output_dir,
|
|
496
|
+
file_prefix=self.step_file_prefix(step_definition["name"]),
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Handle sub-workflow
|
|
500
|
+
if "workflow" in step_definition:
|
|
501
|
+
logger.debug(f"Loading sub-workflow for step: {step_definition['name']}")
|
|
502
|
+
workflow_reference = step_definition["workflow"]
|
|
503
|
+
path = workflow_reference["path"]
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
# Handle built-in workflows
|
|
507
|
+
if path.startswith("builtin:"):
|
|
508
|
+
builtin_name = path.replace("builtin:", "")
|
|
509
|
+
# Validate builtin workflow name
|
|
510
|
+
if (
|
|
511
|
+
not builtin_name.endswith(".json")
|
|
512
|
+
or "/" in builtin_name
|
|
513
|
+
or "\\" in builtin_name
|
|
514
|
+
):
|
|
515
|
+
raise InvalidInputError(
|
|
516
|
+
f"Invalid builtin workflow name: {builtin_name}"
|
|
517
|
+
)
|
|
518
|
+
path = os.path.join(
|
|
519
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
520
|
+
"workflows",
|
|
521
|
+
builtin_name,
|
|
522
|
+
)
|
|
523
|
+
# Handle relative paths
|
|
524
|
+
elif not os.path.isabs(path):
|
|
525
|
+
base_dir = os.path.dirname(self.file_spec)
|
|
526
|
+
path = os.path.join(base_dir, path)
|
|
527
|
+
|
|
528
|
+
# Validate the resolved path
|
|
529
|
+
validated_path = validate_workflow_path(path)
|
|
530
|
+
workflow = workflow_from_file(validated_path, self.output_dir)
|
|
531
|
+
|
|
532
|
+
except SecurityError as e:
|
|
533
|
+
logger.error(f"Security validation failed for sub-workflow {path}: {e}")
|
|
534
|
+
raise
|
|
535
|
+
|
|
536
|
+
# this is where the arguments in the paretn script are passed to the child workflow
|
|
537
|
+
# they will already be populated with values from previous steps or parent variables
|
|
538
|
+
workflow.workflow_definition["argument_template"] = workflow_reference.get(
|
|
539
|
+
"arguments", {}
|
|
540
|
+
)
|
|
541
|
+
# A child left to itself draws its own random seed, which makes the
|
|
542
|
+
# parent's seed stop short of the work it delegates. Inheriting it
|
|
543
|
+
# keeps one seed reproducing the whole run; a child that names its
|
|
544
|
+
# own still wins, the same way a step overrides its workflow
|
|
545
|
+
workflow.workflow_definition.setdefault("seed", default_seed)
|
|
546
|
+
workflow.validate()
|
|
547
|
+
return workflow
|
|
548
|
+
|
|
549
|
+
logger.debug(f"Creating task for step: {step_definition['name']}")
|
|
550
|
+
# Handle task creation
|
|
551
|
+
task_definition = step_definition["task"]
|
|
552
|
+
task = Task(task_definition, device)
|
|
553
|
+
return task
|