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/elision.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""A step nothing references, and which saves nothing, does not run.
|
|
2
|
+
|
|
3
|
+
`templates/minimax/dialogue-short` draws its two characters with Z-Image and
|
|
4
|
+
references those portraits from every shot. An episode can just as well be
|
|
5
|
+
cast from portraits that already exist - a shot entry's subject reference
|
|
6
|
+
takes `from_file: "asset:cast/priya.jpg"` exactly as its voice references do -
|
|
7
|
+
and then the two draw steps still ran and their output was discarded: roughly
|
|
8
|
+
55 seconds and two model loads on portraits nothing in the run looked at
|
|
9
|
+
(#109, #122). The recurring cast is the headline use of that template, so
|
|
10
|
+
paying for it every episode was the wrong default, and no argument the caller
|
|
11
|
+
could pass avoided it.
|
|
12
|
+
|
|
13
|
+
So: before the first step executes, drop any step whose result no later step
|
|
14
|
+
reads and which writes no file. It is static - it depends only on what the
|
|
15
|
+
realized workflow references, never on a value produced during the run, which
|
|
16
|
+
is what keeps it a different thing from conditional execution (#118 closed the
|
|
17
|
+
step object specifically so an invented `when` is a hard error).
|
|
18
|
+
|
|
19
|
+
Four guardrails, all of them load-bearing:
|
|
20
|
+
|
|
21
|
+
- **A step that saves is kept.** `save` defaults to true, so a step declaring
|
|
22
|
+
a `result` is a deliverable unless it says otherwise - a workflow whose
|
|
23
|
+
whole point is writing three images references nothing.
|
|
24
|
+
- **The last step is kept**, whatever it declares: it is the run's answer.
|
|
25
|
+
- **A release moves rather than disappearing.** `release_pipeline` frees the
|
|
26
|
+
memory the step it sits on took; eliding it silently would leak that for
|
|
27
|
+
the rest of the run. It moves onto the last surviving step before it when
|
|
28
|
+
that step loaded the same pipeline, and is dropped when nothing was loaded
|
|
29
|
+
to release.
|
|
30
|
+
- **Every elision is a warning.** Without one a misspelled reference makes
|
|
31
|
+
the step feeding it vanish and the failure moves from "previous result not
|
|
32
|
+
found" to "the picture is wrong". The static reference check refuses an
|
|
33
|
+
unresolvable literal reference, which contains most of it - the warning is
|
|
34
|
+
what covers the rest. The warning says which of the two happened: a step
|
|
35
|
+
the *written* definition reads through a variable the caller then
|
|
36
|
+
overrode was elided on purpose - it is `music-video`'s supplied singer
|
|
37
|
+
portrait, the saving the feature exists for - and telling that caller
|
|
38
|
+
their reference is probably misspelled puts a false positive on the
|
|
39
|
+
documented path, which is how the warnings channel stops being read
|
|
40
|
+
(#157). A step nothing ever read still gets the diagnosis.
|
|
41
|
+
|
|
42
|
+
Elision is transitive: dropping a step can leave the step it read
|
|
43
|
+
unreferenced in turn, so it runs to a fixed point.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
|
|
48
|
+
from .step_cache import reference_resolves_to, referenced_result_names
|
|
49
|
+
|
|
50
|
+
VARIABLE_PREFIX = "variable:"
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("dw")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _saves(step):
|
|
56
|
+
"""Whether the step writes a file.
|
|
57
|
+
|
|
58
|
+
Exactly what `Result.save` asks: a `content_type` to write, and `save`
|
|
59
|
+
not turned off. `save` defaults to true, so declaring a `result` with a
|
|
60
|
+
content type is declaring a deliverable - and a step with no `result` at
|
|
61
|
+
all writes nothing, whatever it generates, which is what makes the
|
|
62
|
+
portrait steps droppable in the first place.
|
|
63
|
+
|
|
64
|
+
A composing step is the one exception: its child workflow saves on its
|
|
65
|
+
own terms, so the parent's `result` block (or its absence) says nothing
|
|
66
|
+
about whether files are written. It is kept.
|
|
67
|
+
"""
|
|
68
|
+
if "workflow" in step:
|
|
69
|
+
return True
|
|
70
|
+
result = step.get("result")
|
|
71
|
+
if not isinstance(result, dict):
|
|
72
|
+
return False
|
|
73
|
+
if result.get("content_type") is None:
|
|
74
|
+
return False
|
|
75
|
+
return result.get("save", True) is not False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _reused_component_names(steps):
|
|
79
|
+
"""Every component name a later step asks an earlier one to have shared.
|
|
80
|
+
|
|
81
|
+
Component sharing is keyed on the component's name rather than on the
|
|
82
|
+
step's, so the step that shares is referenced without ever being named.
|
|
83
|
+
"""
|
|
84
|
+
names = set()
|
|
85
|
+
for step in steps:
|
|
86
|
+
pipeline = step.get("pipeline")
|
|
87
|
+
if isinstance(pipeline, dict):
|
|
88
|
+
for name in pipeline.get("reused_components") or []:
|
|
89
|
+
names.add(name)
|
|
90
|
+
return names
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _referenced_pipeline_steps(steps):
|
|
94
|
+
"""Every step name a later `pipeline_reference` addresses."""
|
|
95
|
+
names = set()
|
|
96
|
+
for step in steps:
|
|
97
|
+
reference = step.get("pipeline_reference")
|
|
98
|
+
if isinstance(reference, dict):
|
|
99
|
+
name = reference.get("reference_name")
|
|
100
|
+
if isinstance(name, str):
|
|
101
|
+
names.add(name)
|
|
102
|
+
return names
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _needed_by(step, later):
|
|
106
|
+
"""Whether any of `later` reads this step - by result, by pipeline, or by
|
|
107
|
+
a component it shares."""
|
|
108
|
+
name = step.get("name")
|
|
109
|
+
if not isinstance(name, str):
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
if any(reference_resolves_to(ref, name) for ref in referenced_result_names(later)):
|
|
113
|
+
return True
|
|
114
|
+
if name in _referenced_pipeline_steps(later):
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
pipeline = step.get("pipeline")
|
|
118
|
+
shared = set((pipeline or {}).get("shared_components") or [])
|
|
119
|
+
return bool(shared & _reused_component_names(later))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _carry_release(elided, kept):
|
|
123
|
+
"""Move an elided step's release onto the last surviving step before it.
|
|
124
|
+
|
|
125
|
+
`release_pipeline` names the pipeline the elided step itself loaded, so
|
|
126
|
+
it is only meaningful on an earlier step that loaded the same one -
|
|
127
|
+
moving it anywhere else would unload something the workflow did not ask
|
|
128
|
+
to unload. `release_models` frees the process-wide task-model cache, so
|
|
129
|
+
the last step that ran before this one is exactly where it belongs. With
|
|
130
|
+
nothing before it, nothing was loaded and the flag is dropped.
|
|
131
|
+
"""
|
|
132
|
+
from .workflow import pipeline_cache_key
|
|
133
|
+
|
|
134
|
+
if not kept:
|
|
135
|
+
return False
|
|
136
|
+
predecessor = kept[-1]
|
|
137
|
+
carried = False
|
|
138
|
+
if elided.get("release_models"):
|
|
139
|
+
predecessor["release_models"] = True
|
|
140
|
+
carried = True
|
|
141
|
+
if not elided.get("release_pipeline"):
|
|
142
|
+
return carried
|
|
143
|
+
elided_pipeline = elided.get("pipeline")
|
|
144
|
+
kept_pipeline = predecessor.get("pipeline")
|
|
145
|
+
if not elided_pipeline or not kept_pipeline:
|
|
146
|
+
return carried
|
|
147
|
+
if pipeline_cache_key(elided_pipeline) == pipeline_cache_key(kept_pipeline):
|
|
148
|
+
predecessor["release_pipeline"] = True
|
|
149
|
+
carried = True
|
|
150
|
+
return carried
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def overriding_variables(written, substituted_steps):
|
|
154
|
+
"""{step name: variable name} for every step the workflow as *written*
|
|
155
|
+
reads only through a variable whose value no longer names it.
|
|
156
|
+
|
|
157
|
+
`music-video` declares `singer_reference` as
|
|
158
|
+
`{"from_previous_result": "draw_singer"}` and every shot references the
|
|
159
|
+
variable; a caller who passes a portrait instead replaces that value, and
|
|
160
|
+
`draw_singer` becomes unreferenced *because they said so*. Comparing the
|
|
161
|
+
two definitions is what tells that apart from a misspelling, and it costs
|
|
162
|
+
one walk of the variables block (#157).
|
|
163
|
+
|
|
164
|
+
`written` is the definition as the author wrote it - before substitution,
|
|
165
|
+
so the defaults are still there; `substituted_steps` is the expanded step
|
|
166
|
+
list elision is about to judge.
|
|
167
|
+
"""
|
|
168
|
+
variables = (written or {}).get("variables")
|
|
169
|
+
if not isinstance(variables, dict):
|
|
170
|
+
return {}
|
|
171
|
+
written_steps = (written or {}).get("steps")
|
|
172
|
+
if not isinstance(written_steps, list):
|
|
173
|
+
return {}
|
|
174
|
+
still_read = referenced_result_names(substituted_steps)
|
|
175
|
+
overridden = {}
|
|
176
|
+
for variable, value in variables.items():
|
|
177
|
+
# A variable no step reads is not how the step was reached, so its
|
|
178
|
+
# disappearance is nobody's decision - the general diagnosis is
|
|
179
|
+
# right for that one
|
|
180
|
+
if not _reads_variable(written_steps, variable):
|
|
181
|
+
continue
|
|
182
|
+
for name in referenced_result_names([value]):
|
|
183
|
+
if any(reference_resolves_to(read, name) for read in still_read):
|
|
184
|
+
continue
|
|
185
|
+
overridden.setdefault(name, variable)
|
|
186
|
+
return overridden
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _reads_variable(tree, name):
|
|
190
|
+
"""Whether anything in `tree` references 'variable:<name>'."""
|
|
191
|
+
reference = VARIABLE_PREFIX + name
|
|
192
|
+
if isinstance(tree, str):
|
|
193
|
+
return tree == reference
|
|
194
|
+
if isinstance(tree, dict):
|
|
195
|
+
return any(_reads_variable(value, name) for value in tree.values())
|
|
196
|
+
if isinstance(tree, list):
|
|
197
|
+
return any(_reads_variable(value, name) for value in tree)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def elide_unreferenced_steps(steps, overridden=None):
|
|
202
|
+
"""The steps that will run, and what was dropped.
|
|
203
|
+
|
|
204
|
+
Returns (kept, elided) where `elided` is
|
|
205
|
+
[{'step': name, 'reason': str, 'overridden_by': variable | absent}],
|
|
206
|
+
in the order the steps were written. `overridden` is
|
|
207
|
+
`overriding_variables`' answer: a step named there was dropped because a
|
|
208
|
+
caller replaced the reference that read it, which is a different event
|
|
209
|
+
from a step nothing reads by accident, and is reported as one. `steps` is the expanded, substituted
|
|
210
|
+
list - a `for_each` member is a step like any other by then, and `gather:`
|
|
211
|
+
has already become the `previous_result:` list it stands for.
|
|
212
|
+
|
|
213
|
+
The list is not copied: a kept step is the same object that went in, and
|
|
214
|
+
a release carried onto a survivor is written into it.
|
|
215
|
+
"""
|
|
216
|
+
if not isinstance(steps, list) or len(steps) < 2:
|
|
217
|
+
return steps, []
|
|
218
|
+
|
|
219
|
+
kept = list(steps)
|
|
220
|
+
elided = []
|
|
221
|
+
changed = True
|
|
222
|
+
while changed:
|
|
223
|
+
changed = False
|
|
224
|
+
for index, step in enumerate(kept):
|
|
225
|
+
if index == len(kept) - 1:
|
|
226
|
+
# The run's answer, whatever it declares
|
|
227
|
+
continue
|
|
228
|
+
if not isinstance(step, dict) or _saves(step):
|
|
229
|
+
continue
|
|
230
|
+
if _needed_by(step, kept[index + 1 :]):
|
|
231
|
+
continue
|
|
232
|
+
variable = (overridden or {}).get(step.get("name"))
|
|
233
|
+
reason = (
|
|
234
|
+
f"'{variable}' was supplied, so nothing reads its result"
|
|
235
|
+
if variable
|
|
236
|
+
else "nothing after it reads its result and it saves no file"
|
|
237
|
+
)
|
|
238
|
+
if _carry_release(step, kept[:index]):
|
|
239
|
+
reason += "; its release was carried onto the step before it"
|
|
240
|
+
entry = {"step": step.get("name"), "reason": reason}
|
|
241
|
+
if variable:
|
|
242
|
+
entry["overridden_by"] = variable
|
|
243
|
+
elided.append(entry)
|
|
244
|
+
del kept[index]
|
|
245
|
+
changed = True
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
# Written order, not the order the fixed point happened to reach them in
|
|
249
|
+
order = {
|
|
250
|
+
step.get("name"): index
|
|
251
|
+
for index, step in enumerate(steps)
|
|
252
|
+
if isinstance(step, dict)
|
|
253
|
+
}
|
|
254
|
+
elided.sort(key=lambda entry: order.get(entry["step"], 0))
|
|
255
|
+
return kept, elided
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def elide_definition(workflow_def, written=None):
|
|
259
|
+
"""elide_unreferenced_steps over a whole definition, in place.
|
|
260
|
+
|
|
261
|
+
Returns the elided-step records; the definition's `steps` is replaced
|
|
262
|
+
when anything was dropped, so a caller that wants the untouched list must
|
|
263
|
+
keep its own copy. `written` is the same workflow before substitution,
|
|
264
|
+
which is what distinguishes a step a caller deliberately replaced from
|
|
265
|
+
one nothing reads by accident (#157); without it every elision reads as
|
|
266
|
+
the second.
|
|
267
|
+
"""
|
|
268
|
+
steps = workflow_def.get("steps")
|
|
269
|
+
overridden = (
|
|
270
|
+
overriding_variables(written, steps if isinstance(steps, list) else [])
|
|
271
|
+
if written
|
|
272
|
+
else None
|
|
273
|
+
)
|
|
274
|
+
kept, elided = elide_unreferenced_steps(steps, overridden)
|
|
275
|
+
if elided:
|
|
276
|
+
workflow_def["steps"] = kept
|
|
277
|
+
return elided
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def warn_elided(elided):
|
|
281
|
+
"""Say what did not run, where whoever asked for the run can read it.
|
|
282
|
+
|
|
283
|
+
A warning rather than a log line for the reason every run-time warning is
|
|
284
|
+
one: a consumer over the API or MCP sees the job's `warnings` list and
|
|
285
|
+
nothing else (#82), and "the portrait step was skipped" is precisely the
|
|
286
|
+
thing that explains an otherwise inexplicable result.
|
|
287
|
+
"""
|
|
288
|
+
from .events import emit_warning
|
|
289
|
+
|
|
290
|
+
for entry in elided:
|
|
291
|
+
# A step whose reference the caller replaced was elided on purpose -
|
|
292
|
+
# say what was saved, not what might be wrong with it (#157)
|
|
293
|
+
advice = (
|
|
294
|
+
""
|
|
295
|
+
if entry.get("overridden_by")
|
|
296
|
+
else (
|
|
297
|
+
" If it was meant to, either a later step's reference to it "
|
|
298
|
+
"is misspelled or it needs a 'result' to save."
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
emit_warning(
|
|
302
|
+
f"Step '{entry['step']}' did not run: {entry['reason']}.{advice}",
|
|
303
|
+
kind="step_elided",
|
|
304
|
+
step=entry["step"],
|
|
305
|
+
overridden_by=entry.get("overridden_by"),
|
|
306
|
+
)
|
dw/events.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Progress reporting and cooperative cancellation for workflow runs.
|
|
2
|
+
|
|
3
|
+
A RunContext travels ambiently (contextvars) through a run: Workflow.run
|
|
4
|
+
activates it, and Step/Pipeline reach it with get_context() rather than
|
|
5
|
+
threading a parameter through every action signature. Callers that never
|
|
6
|
+
pass a context get a no-op one, so the CLI path pays nothing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import contextvars
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("dw")
|
|
15
|
+
|
|
16
|
+
# How long a phase may go without any event before the watchdog speaks up,
|
|
17
|
+
# and (since emitting a stall event itself counts as an event - see
|
|
18
|
+
# RunContext._watchdog_loop) the cadence it repeats on while the stall
|
|
19
|
+
# continues. 30s is well past the 7-10s lead-in a healthy step-callback
|
|
20
|
+
# pipeline shows before its first `pipeline_step` (see
|
|
21
|
+
# docs/proposals/step-callback-lead-in-instrumentation.md) but short enough
|
|
22
|
+
# that a genuine stall is visible long before a human would give up on it.
|
|
23
|
+
# The report goes to the event log only - the job's persisted `warnings`
|
|
24
|
+
# list filters `kind == "phase_stall"` (dw/server/jobs.py), because a stall
|
|
25
|
+
# that resolved is not a warning about the result.
|
|
26
|
+
PHASE_STALL_THRESHOLD_SECONDS = 30.0
|
|
27
|
+
# How often the watchdog thread wakes to check - independent of the
|
|
28
|
+
# threshold above, just fine-grained enough that the reported
|
|
29
|
+
# seconds_since_phase_start doesn't overshoot by much.
|
|
30
|
+
PHASE_STALL_CHECK_INTERVAL_SECONDS = 5.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WorkflowCancelled(Exception):
|
|
34
|
+
"""Raised inside a run when its RunContext has been cancelled."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RunContext:
|
|
38
|
+
"""Carries the event sink and the cancellation flag for one workflow run.
|
|
39
|
+
|
|
40
|
+
cancel() may be called from any thread (the worker's command watcher);
|
|
41
|
+
everything else runs on the thread executing the workflow. The
|
|
42
|
+
phase-stall watchdog (_watchdog_loop) is the same story - it reads
|
|
43
|
+
_current_phase/_last_event_at from its own background thread with no
|
|
44
|
+
lock, which is fine for simple attribute reads/writes under the GIL.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, on_event=None):
|
|
48
|
+
self._on_event = on_event
|
|
49
|
+
self._cancel = threading.Event()
|
|
50
|
+
# The most recent phase this run reported (see emit_phase/PHASES) -
|
|
51
|
+
# cancel() reads it to tell a client whether the cancel takes effect
|
|
52
|
+
# right away or has to wait out an in-flight, non-interruptible phase
|
|
53
|
+
self._current_phase = None
|
|
54
|
+
# Pipeline cache keys this run resolved - the worker evicts entries a
|
|
55
|
+
# run no longer touches, so an edited workflow drops stale models
|
|
56
|
+
self.touched_pipelines = set()
|
|
57
|
+
# Watchdog state. monotonic, not wall clock, since a stall is
|
|
58
|
+
# measured in elapsed time, not affected by clock adjustments.
|
|
59
|
+
self._last_event_at = time.monotonic()
|
|
60
|
+
self._phase_started_at = time.monotonic()
|
|
61
|
+
# Last *real* event - anything but the watchdog's own phase_stall
|
|
62
|
+
# report - and what kind it was. Tracked separately from
|
|
63
|
+
# _last_event_at (which the stall report itself also bumps, to drive
|
|
64
|
+
# its own repeat cadence) so "how long has it actually been quiet"
|
|
65
|
+
# doesn't reset every time the watchdog speaks.
|
|
66
|
+
self._last_progress_at = self._last_event_at
|
|
67
|
+
self._last_progress_kind = None
|
|
68
|
+
# Reference-counted: a sub-workflow runs inside its parent's
|
|
69
|
+
# RunContext (Workflow.run reuses the ambient one), so the watchdog
|
|
70
|
+
# starts on the outermost run() and stops on the outermost's exit,
|
|
71
|
+
# not on every nested one
|
|
72
|
+
self._run_depth = 0
|
|
73
|
+
self._watchdog_thread = None
|
|
74
|
+
self._watchdog_stop = threading.Event()
|
|
75
|
+
|
|
76
|
+
def emit(self, event_type, counts_as_progress=True, **data):
|
|
77
|
+
"""Send one event to the sink. counts_as_progress=False is for a
|
|
78
|
+
status report that says nothing has moved - a download reporting
|
|
79
|
+
zero new bytes (#343): the caller should see it, but the stall
|
|
80
|
+
watchdog must still see the silence, so it bumps neither clock."""
|
|
81
|
+
now = time.monotonic()
|
|
82
|
+
if counts_as_progress:
|
|
83
|
+
self._last_event_at = now
|
|
84
|
+
if not (event_type == "warning" and data.get("kind") == "phase_stall"):
|
|
85
|
+
self._last_progress_at = now
|
|
86
|
+
self._last_progress_kind = event_type
|
|
87
|
+
if self._on_event is None:
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
self._on_event({"event": event_type, **data})
|
|
91
|
+
except Exception as e:
|
|
92
|
+
# A broken sink must not kill a run that is otherwise fine
|
|
93
|
+
logger.warning(f"Progress event sink failed on '{event_type}': {e}")
|
|
94
|
+
|
|
95
|
+
def note_phase(self, phase):
|
|
96
|
+
"""Record the run's latest phase - called by emit_phase, read by
|
|
97
|
+
cancel() to judge whether the current phase can be interrupted, and
|
|
98
|
+
by the watchdog to time how long the current phase has run."""
|
|
99
|
+
self._current_phase = phase
|
|
100
|
+
self._phase_started_at = time.monotonic()
|
|
101
|
+
|
|
102
|
+
def enter_run(self):
|
|
103
|
+
"""Called around Workflow.run - starts the watchdog on the
|
|
104
|
+
outermost call, a no-op on a nested (sub-workflow) one."""
|
|
105
|
+
self._run_depth += 1
|
|
106
|
+
if self._run_depth == 1:
|
|
107
|
+
self._watchdog_stop.clear()
|
|
108
|
+
self._watchdog_thread = threading.Thread(
|
|
109
|
+
target=self._watchdog_loop, daemon=True, name="dw-phase-stall"
|
|
110
|
+
)
|
|
111
|
+
self._watchdog_thread.start()
|
|
112
|
+
|
|
113
|
+
def exit_run(self):
|
|
114
|
+
"""Matching exit for enter_run - stops the watchdog once the
|
|
115
|
+
outermost run() has left, whether it succeeded or raised."""
|
|
116
|
+
self._run_depth -= 1
|
|
117
|
+
if self._run_depth == 0 and self._watchdog_thread is not None:
|
|
118
|
+
self._watchdog_stop.set()
|
|
119
|
+
self._watchdog_thread.join(timeout=PHASE_STALL_CHECK_INTERVAL_SECONDS * 2)
|
|
120
|
+
self._watchdog_thread = None
|
|
121
|
+
|
|
122
|
+
def _watchdog_loop(self):
|
|
123
|
+
"""Generic, pipeline-agnostic: this only ever looks at 'how long
|
|
124
|
+
since any event' and 'what phase are we in', never at what a
|
|
125
|
+
particular pipeline does inside a phase (see
|
|
126
|
+
docs/proposals/step-callback-lead-in-instrumentation.md, Option A).
|
|
127
|
+
A stall report is itself an event, so it bumps _last_event_at and
|
|
128
|
+
naturally repeats on PHASE_STALL_THRESHOLD_SECONDS while the silence
|
|
129
|
+
continues and stops the moment a real progress event arrives -
|
|
130
|
+
but it deliberately does not bump _last_progress_at, so the
|
|
131
|
+
seconds_since_last_progress it reports keeps climbing across
|
|
132
|
+
repeats rather than resetting itself every 30s (#357). It is a
|
|
133
|
+
watchdog notice, not progress: a caller polling events should not
|
|
134
|
+
read a run of these, or a climbing event_count made of them, as
|
|
135
|
+
liveness.
|
|
136
|
+
"""
|
|
137
|
+
while not self._watchdog_stop.wait(PHASE_STALL_CHECK_INTERVAL_SECONDS):
|
|
138
|
+
phase = self._current_phase
|
|
139
|
+
if phase is None:
|
|
140
|
+
continue
|
|
141
|
+
now = time.monotonic()
|
|
142
|
+
if now - self._last_event_at < PHASE_STALL_THRESHOLD_SECONDS:
|
|
143
|
+
continue
|
|
144
|
+
seconds_since_phase_start = round(now - self._phase_started_at, 1)
|
|
145
|
+
seconds_since_last_progress = round(now - self._last_progress_at, 1)
|
|
146
|
+
last_kind = self._last_progress_kind or "phase start"
|
|
147
|
+
last_offset = round(
|
|
148
|
+
max(0.0, self._last_progress_at - self._phase_started_at), 1
|
|
149
|
+
)
|
|
150
|
+
message = (
|
|
151
|
+
f"no progress event for {seconds_since_last_progress:.1f}s in phase "
|
|
152
|
+
f"'{phase}' (last: {last_kind} at +{last_offset:.1f}s) - informational; "
|
|
153
|
+
"long silent stretches are normal for some models, see the model's skill"
|
|
154
|
+
)
|
|
155
|
+
logger.warning(message)
|
|
156
|
+
self.emit(
|
|
157
|
+
"warning",
|
|
158
|
+
message=message,
|
|
159
|
+
kind="phase_stall",
|
|
160
|
+
phase=phase,
|
|
161
|
+
seconds_since_phase_start=seconds_since_phase_start,
|
|
162
|
+
seconds_since_last_progress=seconds_since_last_progress,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def cancel(self):
|
|
166
|
+
self._cancel.set()
|
|
167
|
+
if self._current_phase in NON_INTERRUPTIBLE_PHASES:
|
|
168
|
+
# A model load or a task step has no checkpoint to catch this
|
|
169
|
+
# flag until it finishes - say so, or the client sees the cancel
|
|
170
|
+
# request go silent for however long that takes
|
|
171
|
+
self.emit("cancel_pending", phase=self._current_phase)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def cancelled(self):
|
|
175
|
+
return self._cancel.is_set()
|
|
176
|
+
|
|
177
|
+
def check_cancelled(self):
|
|
178
|
+
if self._cancel.is_set():
|
|
179
|
+
raise WorkflowCancelled("Workflow run was cancelled")
|
|
180
|
+
|
|
181
|
+
def touch_pipeline(self, cache_key):
|
|
182
|
+
self.touched_pipelines.add(cache_key)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_active_context = contextvars.ContextVar("dw_run_context", default=None)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_context():
|
|
189
|
+
"""The active run's context, or a no-op one outside any run."""
|
|
190
|
+
context = _active_context.get()
|
|
191
|
+
return context if context is not None else RunContext()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def current_context():
|
|
195
|
+
"""The active run's context, or None outside any run."""
|
|
196
|
+
return _active_context.get()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def activate_context(context):
|
|
200
|
+
"""Make a context the active one; returns a token for deactivate_context."""
|
|
201
|
+
return _active_context.set(context)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def deactivate_context(token):
|
|
205
|
+
_active_context.reset(token)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# The coarse states a run passes through. Small on purpose: a phase says what
|
|
209
|
+
# the run is waiting on, not what any one library is doing internally
|
|
210
|
+
PHASES = ("loading", "cached", "generating", "decoding", "saving", "task")
|
|
211
|
+
|
|
212
|
+
# Phases with no checkpoint of their own: nothing inside a from_pretrained()
|
|
213
|
+
# call or a task handler consults the cancel flag, so a cancel requested
|
|
214
|
+
# during one of these can only take effect once the phase finishes on its own
|
|
215
|
+
NON_INTERRUPTIBLE_PHASES = ("loading", "task")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def select_kinds(events, kinds):
|
|
219
|
+
"""The events whose `event` or `kind` is one of `kinds`, in order.
|
|
220
|
+
|
|
221
|
+
Both, because a consumer names what it sees: the bookkeeping events are
|
|
222
|
+
told apart by `event` (`log`, `warning`, `memory`), but a warning's
|
|
223
|
+
own type - `phase_stall`, `audio_clipped` - is its `kind`, and matching
|
|
224
|
+
`event` alone quietly returned an empty page for those. No `kinds`
|
|
225
|
+
means no filter."""
|
|
226
|
+
if not kinds:
|
|
227
|
+
return list(events)
|
|
228
|
+
allowed = set(kinds)
|
|
229
|
+
return [e for e in events if e.get("event") in allowed or e.get("kind") in allowed]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def emit_warning(message, **data):
|
|
233
|
+
"""Report something the run's result carries but its status will not.
|
|
234
|
+
|
|
235
|
+
A warning a step discovers at run time - shots being cut together 10 dB
|
|
236
|
+
apart, a video about to be written at a frame rate nothing chose - is
|
|
237
|
+
only useful where whoever asked for the run can read it. The server's
|
|
238
|
+
own log is not that place: a consumer over the API or MCP sees the event
|
|
239
|
+
stream and the job's `warnings` list and nothing else, so a diagnostic
|
|
240
|
+
that only reaches the log does not exist out there (#82).
|
|
241
|
+
|
|
242
|
+
Logged as well as emitted, because the CLI and the REPL have no event
|
|
243
|
+
sink and the log is the whole of their surface.
|
|
244
|
+
"""
|
|
245
|
+
logger.warning(message)
|
|
246
|
+
get_context().emit("warning", message=message, **data)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def emit_phase(phase, detail=None):
|
|
250
|
+
"""Report a coarse phase change on the active run.
|
|
251
|
+
|
|
252
|
+
A step spends most of its wall clock outside the denoise loop - pulling
|
|
253
|
+
weights, decoding latents, encoding video - and a step counter says
|
|
254
|
+
nothing about any of that. These are the rest of the story. They are rare
|
|
255
|
+
enough (a handful per step) to carry a free-text detail alongside, which
|
|
256
|
+
is what makes 'loading' readable as 'which model'.
|
|
257
|
+
"""
|
|
258
|
+
context = get_context()
|
|
259
|
+
context.note_phase(phase)
|
|
260
|
+
context.emit("phase", phase=phase, detail=detail)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def emit_log(message, **data):
|
|
264
|
+
"""Narrate one step of a long, otherwise silent stretch of a run.
|
|
265
|
+
|
|
266
|
+
A `log` event rather than a phase: `PHASES` is a closed set a consumer
|
|
267
|
+
switches on, and "which file is being written" is a detail inside one of
|
|
268
|
+
them, not a new state. The modular block lead-in (#95) is the same shape
|
|
269
|
+
at the other end of a step.
|
|
270
|
+
|
|
271
|
+
Logged as well as emitted, because the CLI and the REPL have no event
|
|
272
|
+
sink and the log is the whole of their surface.
|
|
273
|
+
"""
|
|
274
|
+
logger.info(message)
|
|
275
|
+
get_context().emit("log", message=message, **data)
|