SuperRealCo commited on
Commit
b6b8ce8
·
verified ·
1 Parent(s): 8d4115d

Delete execution.py

Browse files
Files changed (1) hide show
  1. execution.py +0 -1139
execution.py DELETED
@@ -1,1139 +0,0 @@
1
- import copy
2
- import heapq
3
- import inspect
4
- import logging
5
- import sys
6
- import threading
7
- import time
8
- import traceback
9
- from enum import Enum
10
- from typing import List, Literal, NamedTuple, Optional
11
- import asyncio
12
-
13
- import torch
14
-
15
- import comfy.model_management
16
- import nodes
17
- from comfy_execution.caching import (
18
- BasicCache,
19
- CacheKeySetID,
20
- CacheKeySetInputSignature,
21
- DependencyAwareCache,
22
- HierarchicalCache,
23
- LRUCache,
24
- )
25
- from comfy_execution.graph import (
26
- DynamicPrompt,
27
- ExecutionBlocker,
28
- ExecutionList,
29
- get_input_info,
30
- )
31
- from comfy_execution.graph_utils import GraphBuilder, is_link
32
- from comfy_execution.validation import validate_node_input
33
- from comfy_execution.progress import get_progress_state, reset_progress_state, add_progress_handler, WebUIProgressHandler
34
- from comfy_execution.utils import CurrentNodeContext
35
-
36
-
37
- class ExecutionResult(Enum):
38
- SUCCESS = 0
39
- FAILURE = 1
40
- PENDING = 2
41
-
42
- class DuplicateNodeError(Exception):
43
- pass
44
-
45
- class IsChangedCache:
46
- def __init__(self, prompt_id: str, dynprompt: DynamicPrompt, outputs_cache: BasicCache):
47
- self.prompt_id = prompt_id
48
- self.dynprompt = dynprompt
49
- self.outputs_cache = outputs_cache
50
- self.is_changed = {}
51
-
52
- async def get(self, node_id):
53
- if node_id in self.is_changed:
54
- return self.is_changed[node_id]
55
-
56
- node = self.dynprompt.get_node(node_id)
57
- class_type = node["class_type"]
58
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
59
- if not hasattr(class_def, "IS_CHANGED"):
60
- self.is_changed[node_id] = False
61
- return self.is_changed[node_id]
62
-
63
- if "is_changed" in node:
64
- self.is_changed[node_id] = node["is_changed"]
65
- return self.is_changed[node_id]
66
-
67
- # Intentionally do not use cached outputs here. We only want constants in IS_CHANGED
68
- input_data_all, _ = get_input_data(node["inputs"], class_def, node_id, None)
69
- try:
70
- is_changed = await _async_map_node_over_list(self.prompt_id, node_id, class_def, input_data_all, "IS_CHANGED")
71
- is_changed = await resolve_map_node_over_list_results(is_changed)
72
- node["is_changed"] = [None if isinstance(x, ExecutionBlocker) else x for x in is_changed]
73
- except Exception as e:
74
- logging.warning("WARNING: {}".format(e))
75
- node["is_changed"] = float("NaN")
76
- finally:
77
- self.is_changed[node_id] = node["is_changed"]
78
- return self.is_changed[node_id]
79
-
80
-
81
- class CacheType(Enum):
82
- CLASSIC = 0
83
- LRU = 1
84
- DEPENDENCY_AWARE = 2
85
-
86
-
87
- class CacheSet:
88
- def __init__(self, cache_type=None, cache_size=None):
89
- if cache_type == CacheType.DEPENDENCY_AWARE:
90
- self.init_dependency_aware_cache()
91
- logging.info("Disabling intermediate node cache.")
92
- elif cache_type == CacheType.LRU:
93
- if cache_size is None:
94
- cache_size = 0
95
- self.init_lru_cache(cache_size)
96
- logging.info("Using LRU cache")
97
- else:
98
- self.init_classic_cache()
99
-
100
- self.all = [self.outputs, self.ui, self.objects]
101
-
102
- # Performs like the old cache -- dump data ASAP
103
- def init_classic_cache(self):
104
- self.outputs = HierarchicalCache(CacheKeySetInputSignature)
105
- self.ui = HierarchicalCache(CacheKeySetInputSignature)
106
- self.objects = HierarchicalCache(CacheKeySetID)
107
-
108
- def init_lru_cache(self, cache_size):
109
- self.outputs = LRUCache(CacheKeySetInputSignature, max_size=cache_size)
110
- self.ui = LRUCache(CacheKeySetInputSignature, max_size=cache_size)
111
- self.objects = HierarchicalCache(CacheKeySetID)
112
-
113
- # only hold cached items while the decendents have not executed
114
- def init_dependency_aware_cache(self):
115
- self.outputs = DependencyAwareCache(CacheKeySetInputSignature)
116
- self.ui = DependencyAwareCache(CacheKeySetInputSignature)
117
- self.objects = DependencyAwareCache(CacheKeySetID)
118
-
119
- def recursive_debug_dump(self):
120
- result = {
121
- "outputs": self.outputs.recursive_debug_dump(),
122
- "ui": self.ui.recursive_debug_dump(),
123
- }
124
- return result
125
-
126
- SENSITIVE_EXTRA_DATA_KEYS = ("auth_token_comfy_org", "api_key_comfy_org")
127
-
128
- def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, extra_data={}):
129
- valid_inputs = class_def.INPUT_TYPES()
130
- input_data_all = {}
131
- missing_keys = {}
132
- for x in inputs:
133
- input_data = inputs[x]
134
- _, input_category, input_info = get_input_info(class_def, x, valid_inputs)
135
- def mark_missing():
136
- missing_keys[x] = True
137
- input_data_all[x] = (None,)
138
- if is_link(input_data) and (not input_info or not input_info.get("rawLink", False)):
139
- input_unique_id = input_data[0]
140
- output_index = input_data[1]
141
- if outputs is None:
142
- mark_missing()
143
- continue # This might be a lazily-evaluated input
144
- cached_output = outputs.get(input_unique_id)
145
- if cached_output is None:
146
- mark_missing()
147
- continue
148
- if output_index >= len(cached_output):
149
- mark_missing()
150
- continue
151
- obj = cached_output[output_index]
152
- input_data_all[x] = obj
153
- elif input_category is not None:
154
- input_data_all[x] = [input_data]
155
-
156
- if "hidden" in valid_inputs:
157
- h = valid_inputs["hidden"]
158
- for x in h:
159
- if h[x] == "PROMPT":
160
- input_data_all[x] = [dynprompt.get_original_prompt() if dynprompt is not None else {}]
161
- if h[x] == "DYNPROMPT":
162
- input_data_all[x] = [dynprompt]
163
- if h[x] == "EXTRA_PNGINFO":
164
- input_data_all[x] = [extra_data.get('extra_pnginfo', None)]
165
- if h[x] == "UNIQUE_ID":
166
- input_data_all[x] = [unique_id]
167
- if h[x] == "AUTH_TOKEN_COMFY_ORG":
168
- input_data_all[x] = [extra_data.get("auth_token_comfy_org", None)]
169
- if h[x] == "API_KEY_COMFY_ORG":
170
- input_data_all[x] = [extra_data.get("api_key_comfy_org", None)]
171
- return input_data_all, missing_keys
172
-
173
- map_node_over_list = None #Don't hook this please
174
-
175
- async def resolve_map_node_over_list_results(results):
176
- remaining = [x for x in results if isinstance(x, asyncio.Task) and not x.done()]
177
- if len(remaining) == 0:
178
- return [x.result() if isinstance(x, asyncio.Task) else x for x in results]
179
- else:
180
- done, pending = await asyncio.wait(remaining)
181
- for task in done:
182
- exc = task.exception()
183
- if exc is not None:
184
- raise exc
185
- return [x.result() if isinstance(x, asyncio.Task) else x for x in results]
186
-
187
- async def _async_map_node_over_list(prompt_id, unique_id, obj, input_data_all, func, allow_interrupt=False, execution_block_cb=None, pre_execute_cb=None):
188
- # check if node wants the lists
189
- input_is_list = getattr(obj, "INPUT_IS_LIST", False)
190
-
191
- if len(input_data_all) == 0:
192
- max_len_input = 0
193
- else:
194
- max_len_input = max(len(x) for x in input_data_all.values())
195
-
196
- # get a slice of inputs, repeat last input when list isn't long enough
197
- def slice_dict(d, i):
198
- return {k: v[i if len(v) > i else -1] for k, v in d.items()}
199
-
200
- results = []
201
- async def process_inputs(inputs, index=None, input_is_list=False):
202
- if allow_interrupt:
203
- nodes.before_node_execution()
204
- execution_block = None
205
- for k, v in inputs.items():
206
- if input_is_list:
207
- for e in v:
208
- if isinstance(e, ExecutionBlocker):
209
- v = e
210
- break
211
- if isinstance(v, ExecutionBlocker):
212
- execution_block = execution_block_cb(v) if execution_block_cb else v
213
- break
214
- if execution_block is None:
215
- if pre_execute_cb is not None and index is not None:
216
- pre_execute_cb(index)
217
- f = getattr(obj, func)
218
- if inspect.iscoroutinefunction(f):
219
- async def async_wrapper(f, prompt_id, unique_id, list_index, args):
220
- with CurrentNodeContext(prompt_id, unique_id, list_index):
221
- return await f(**args)
222
- task = asyncio.create_task(async_wrapper(f, prompt_id, unique_id, index, args=inputs))
223
- # Give the task a chance to execute without yielding
224
- await asyncio.sleep(0)
225
- if task.done():
226
- result = task.result()
227
- results.append(result)
228
- else:
229
- results.append(task)
230
- else:
231
- with CurrentNodeContext(prompt_id, unique_id, index):
232
- result = f(**inputs)
233
- results.append(result)
234
- else:
235
- results.append(execution_block)
236
-
237
- if input_is_list:
238
- await process_inputs(input_data_all, 0, input_is_list=input_is_list)
239
- elif max_len_input == 0:
240
- await process_inputs({})
241
- else:
242
- for i in range(max_len_input):
243
- input_dict = slice_dict(input_data_all, i)
244
- await process_inputs(input_dict, i)
245
- return results
246
-
247
-
248
- def merge_result_data(results, obj):
249
- # check which outputs need concatenating
250
- output = []
251
- output_is_list = [False] * len(results[0])
252
- if hasattr(obj, "OUTPUT_IS_LIST"):
253
- output_is_list = obj.OUTPUT_IS_LIST
254
-
255
- # merge node execution results
256
- for i, is_list in zip(range(len(results[0])), output_is_list):
257
- if is_list:
258
- value = []
259
- for o in results:
260
- if isinstance(o[i], ExecutionBlocker):
261
- value.append(o[i])
262
- else:
263
- value.extend(o[i])
264
- output.append(value)
265
- else:
266
- output.append([o[i] for o in results])
267
- return output
268
-
269
- async def get_output_data(prompt_id, unique_id, obj, input_data_all, execution_block_cb=None, pre_execute_cb=None):
270
- return_values = await _async_map_node_over_list(prompt_id, unique_id, obj, input_data_all, obj.FUNCTION, allow_interrupt=True, execution_block_cb=execution_block_cb, pre_execute_cb=pre_execute_cb)
271
- has_pending_task = any(isinstance(r, asyncio.Task) and not r.done() for r in return_values)
272
- if has_pending_task:
273
- return return_values, {}, False, has_pending_task
274
- output, ui, has_subgraph = get_output_from_returns(return_values, obj)
275
- return output, ui, has_subgraph, False
276
-
277
- def get_output_from_returns(return_values, obj):
278
- results = []
279
- uis = []
280
- subgraph_results = []
281
- has_subgraph = False
282
- for i in range(len(return_values)):
283
- r = return_values[i]
284
- if isinstance(r, dict):
285
- if 'ui' in r:
286
- uis.append(r['ui'])
287
- if 'expand' in r:
288
- # Perform an expansion, but do not append results
289
- has_subgraph = True
290
- new_graph = r['expand']
291
- result = r.get("result", None)
292
- if isinstance(result, ExecutionBlocker):
293
- result = tuple([result] * len(obj.RETURN_TYPES))
294
- subgraph_results.append((new_graph, result))
295
- elif 'result' in r:
296
- result = r.get("result", None)
297
- if isinstance(result, ExecutionBlocker):
298
- result = tuple([result] * len(obj.RETURN_TYPES))
299
- results.append(result)
300
- subgraph_results.append((None, result))
301
- else:
302
- if isinstance(r, ExecutionBlocker):
303
- r = tuple([r] * len(obj.RETURN_TYPES))
304
- results.append(r)
305
- subgraph_results.append((None, r))
306
-
307
- if has_subgraph:
308
- output = subgraph_results
309
- elif len(results) > 0:
310
- output = merge_result_data(results, obj)
311
- else:
312
- output = []
313
- ui = dict()
314
- # TODO: Think there's an existing bug here
315
- # If we're performing a subgraph expansion, we probably shouldn't be returning UI values yet.
316
- # They'll get cached without the completed subgraphs. It's an edge case and I'm not aware of
317
- # any nodes that use both subgraph expansion and custom UI outputs, but might be a problem in the future.
318
- if len(uis) > 0:
319
- ui = {k: [y for x in uis for y in x[k]] for k in uis[0].keys()}
320
- return output, ui, has_subgraph
321
-
322
- def format_value(x):
323
- if x is None:
324
- return None
325
- elif isinstance(x, (int, float, bool, str)):
326
- return x
327
- else:
328
- return str(x)
329
-
330
- async def execute(server, dynprompt, caches, current_item, extra_data, executed, prompt_id, execution_list, pending_subgraph_results, pending_async_nodes):
331
- unique_id = current_item
332
- real_node_id = dynprompt.get_real_node_id(unique_id)
333
- display_node_id = dynprompt.get_display_node_id(unique_id)
334
- parent_node_id = dynprompt.get_parent_node_id(unique_id)
335
- inputs = dynprompt.get_node(unique_id)['inputs']
336
- class_type = dynprompt.get_node(unique_id)['class_type']
337
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
338
- if caches.outputs.get(unique_id) is not None:
339
- if server.client_id is not None:
340
- cached_output = caches.ui.get(unique_id) or {}
341
- server.send_sync("executed", { "node": unique_id, "display_node": display_node_id, "output": cached_output.get("output",None), "prompt_id": prompt_id }, server.client_id)
342
- get_progress_state().finish_progress(unique_id)
343
- return (ExecutionResult.SUCCESS, None, None)
344
-
345
- input_data_all = None
346
- try:
347
- if unique_id in pending_async_nodes:
348
- results = []
349
- for r in pending_async_nodes[unique_id]:
350
- if isinstance(r, asyncio.Task):
351
- try:
352
- results.append(r.result())
353
- except Exception as ex:
354
- # An async task failed - propagate the exception up
355
- del pending_async_nodes[unique_id]
356
- raise ex
357
- else:
358
- results.append(r)
359
- del pending_async_nodes[unique_id]
360
- output_data, output_ui, has_subgraph = get_output_from_returns(results, class_def)
361
- elif unique_id in pending_subgraph_results:
362
- cached_results = pending_subgraph_results[unique_id]
363
- resolved_outputs = []
364
- for is_subgraph, result in cached_results:
365
- if not is_subgraph:
366
- resolved_outputs.append(result)
367
- else:
368
- resolved_output = []
369
- for r in result:
370
- if is_link(r):
371
- source_node, source_output = r[0], r[1]
372
- node_output = caches.outputs.get(source_node)[source_output]
373
- for o in node_output:
374
- resolved_output.append(o)
375
-
376
- else:
377
- resolved_output.append(r)
378
- resolved_outputs.append(tuple(resolved_output))
379
- output_data = merge_result_data(resolved_outputs, class_def)
380
- output_ui = []
381
- has_subgraph = False
382
- else:
383
- get_progress_state().start_progress(unique_id)
384
- input_data_all, missing_keys = get_input_data(inputs, class_def, unique_id, caches.outputs, dynprompt, extra_data)
385
- if server.client_id is not None:
386
- server.last_node_id = display_node_id
387
- server.send_sync("executing", { "node": unique_id, "display_node": display_node_id, "prompt_id": prompt_id }, server.client_id)
388
-
389
- obj = caches.objects.get(unique_id)
390
- if obj is None:
391
- obj = class_def()
392
- caches.objects.set(unique_id, obj)
393
-
394
- if hasattr(obj, "check_lazy_status"):
395
- required_inputs = await _async_map_node_over_list(prompt_id, unique_id, obj, input_data_all, "check_lazy_status", allow_interrupt=True)
396
- required_inputs = await resolve_map_node_over_list_results(required_inputs)
397
- required_inputs = set(sum([r for r in required_inputs if isinstance(r,list)], []))
398
- required_inputs = [x for x in required_inputs if isinstance(x,str) and (
399
- x not in input_data_all or x in missing_keys
400
- )]
401
- if len(required_inputs) > 0:
402
- for i in required_inputs:
403
- execution_list.make_input_strong_link(unique_id, i)
404
- return (ExecutionResult.PENDING, None, None)
405
-
406
- def execution_block_cb(block):
407
- if block.message is not None:
408
- mes = {
409
- "prompt_id": prompt_id,
410
- "node_id": unique_id,
411
- "node_type": class_type,
412
- "executed": list(executed),
413
-
414
- "exception_message": f"Execution Blocked: {block.message}",
415
- "exception_type": "ExecutionBlocked",
416
- "traceback": [],
417
- "current_inputs": [],
418
- "current_outputs": [],
419
- }
420
- server.send_sync("execution_error", mes, server.client_id)
421
- return ExecutionBlocker(None)
422
- else:
423
- return block
424
- def pre_execute_cb(call_index):
425
- # TODO - How to handle this with async functions without contextvars (which requires Python 3.12)?
426
- GraphBuilder.set_default_prefix(unique_id, call_index, 0)
427
- output_data, output_ui, has_subgraph, has_pending_tasks = await get_output_data(prompt_id, unique_id, obj, input_data_all, execution_block_cb=execution_block_cb, pre_execute_cb=pre_execute_cb)
428
- if has_pending_tasks:
429
- pending_async_nodes[unique_id] = output_data
430
- unblock = execution_list.add_external_block(unique_id)
431
- async def await_completion():
432
- tasks = [x for x in output_data if isinstance(x, asyncio.Task)]
433
- await asyncio.gather(*tasks, return_exceptions=True)
434
- unblock()
435
- asyncio.create_task(await_completion())
436
- return (ExecutionResult.PENDING, None, None)
437
- if len(output_ui) > 0:
438
- caches.ui.set(unique_id, {
439
- "meta": {
440
- "node_id": unique_id,
441
- "display_node": display_node_id,
442
- "parent_node": parent_node_id,
443
- "real_node_id": real_node_id,
444
- },
445
- "output": output_ui
446
- })
447
- if server.client_id is not None:
448
- server.send_sync("executed", { "node": unique_id, "display_node": display_node_id, "output": output_ui, "prompt_id": prompt_id }, server.client_id)
449
- if has_subgraph:
450
- cached_outputs = []
451
- new_node_ids = []
452
- new_output_ids = []
453
- new_output_links = []
454
- for i in range(len(output_data)):
455
- new_graph, node_outputs = output_data[i]
456
- if new_graph is None:
457
- cached_outputs.append((False, node_outputs))
458
- else:
459
- # Check for conflicts
460
- for node_id in new_graph.keys():
461
- if dynprompt.has_node(node_id):
462
- raise DuplicateNodeError(f"Attempt to add duplicate node {node_id}. Ensure node ids are unique and deterministic or use graph_utils.GraphBuilder.")
463
- for node_id, node_info in new_graph.items():
464
- new_node_ids.append(node_id)
465
- display_id = node_info.get("override_display_id", unique_id)
466
- dynprompt.add_ephemeral_node(node_id, node_info, unique_id, display_id)
467
- # Figure out if the newly created node is an output node
468
- class_type = node_info["class_type"]
469
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
470
- if hasattr(class_def, 'OUTPUT_NODE') and class_def.OUTPUT_NODE == True:
471
- new_output_ids.append(node_id)
472
- for i in range(len(node_outputs)):
473
- if is_link(node_outputs[i]):
474
- from_node_id, from_socket = node_outputs[i][0], node_outputs[i][1]
475
- new_output_links.append((from_node_id, from_socket))
476
- cached_outputs.append((True, node_outputs))
477
- new_node_ids = set(new_node_ids)
478
- for cache in caches.all:
479
- subcache = await cache.ensure_subcache_for(unique_id, new_node_ids)
480
- subcache.clean_unused()
481
- for node_id in new_output_ids:
482
- execution_list.add_node(node_id)
483
- for link in new_output_links:
484
- execution_list.add_strong_link(link[0], link[1], unique_id)
485
- pending_subgraph_results[unique_id] = cached_outputs
486
- return (ExecutionResult.PENDING, None, None)
487
- caches.outputs.set(unique_id, output_data)
488
- except comfy.model_management.InterruptProcessingException as iex:
489
- logging.info("Processing interrupted")
490
-
491
- # skip formatting inputs/outputs
492
- error_details = {
493
- "node_id": real_node_id,
494
- }
495
-
496
- return (ExecutionResult.FAILURE, error_details, iex)
497
- except Exception as ex:
498
- typ, _, tb = sys.exc_info()
499
- exception_type = full_type_name(typ)
500
- input_data_formatted = {}
501
- if input_data_all is not None:
502
- input_data_formatted = {}
503
- for name, inputs in input_data_all.items():
504
- input_data_formatted[name] = [format_value(x) for x in inputs]
505
-
506
- logging.error(f"!!! Exception during processing !!! {ex}")
507
- logging.error(traceback.format_exc())
508
- tips = ""
509
-
510
- if isinstance(ex, comfy.model_management.OOM_EXCEPTION):
511
- tips = "This error means you ran out of memory on your GPU.\n\nTIPS: If the workflow worked before you might have accidentally set the batch_size to a large number."
512
- logging.error("Got an OOM, unloading all loaded models.")
513
- comfy.model_management.unload_all_models()
514
-
515
- error_details = {
516
- "node_id": real_node_id,
517
- "exception_message": "{}\n{}".format(ex, tips),
518
- "exception_type": exception_type,
519
- "traceback": traceback.format_tb(tb),
520
- "current_inputs": input_data_formatted
521
- }
522
-
523
- return (ExecutionResult.FAILURE, error_details, ex)
524
-
525
- get_progress_state().finish_progress(unique_id)
526
- executed.add(unique_id)
527
-
528
- return (ExecutionResult.SUCCESS, None, None)
529
-
530
- class PromptExecutor:
531
- def __init__(self, server, cache_type=False, cache_size=None):
532
- self.cache_size = cache_size
533
- self.cache_type = cache_type
534
- self.server = server
535
- self.reset()
536
-
537
- def reset(self):
538
- self.caches = CacheSet(cache_type=self.cache_type, cache_size=self.cache_size)
539
- self.status_messages = []
540
- self.success = True
541
-
542
- def add_message(self, event, data: dict, broadcast: bool):
543
- data = {
544
- **data,
545
- "timestamp": int(time.time() * 1000),
546
- }
547
- self.status_messages.append((event, data))
548
- if self.server.client_id is not None or broadcast:
549
- self.server.send_sync(event, data, self.server.client_id)
550
-
551
- def handle_execution_error(self, prompt_id, prompt, current_outputs, executed, error, ex):
552
- node_id = error["node_id"]
553
- class_type = prompt[node_id]["class_type"]
554
-
555
- # First, send back the status to the frontend depending
556
- # on the exception type
557
- if isinstance(ex, comfy.model_management.InterruptProcessingException):
558
- mes = {
559
- "prompt_id": prompt_id,
560
- "node_id": node_id,
561
- "node_type": class_type,
562
- "executed": list(executed),
563
- }
564
- self.add_message("execution_interrupted", mes, broadcast=True)
565
- else:
566
- mes = {
567
- "prompt_id": prompt_id,
568
- "node_id": node_id,
569
- "node_type": class_type,
570
- "executed": list(executed),
571
- "exception_message": error["exception_message"],
572
- "exception_type": error["exception_type"],
573
- "traceback": error["traceback"],
574
- "current_inputs": error["current_inputs"],
575
- "current_outputs": list(current_outputs),
576
- }
577
- self.add_message("execution_error", mes, broadcast=False)
578
-
579
- def execute(self, prompt, prompt_id, extra_data={}, execute_outputs=[]):
580
- asyncio_loop = asyncio.new_event_loop()
581
- asyncio.set_event_loop(asyncio_loop)
582
- asyncio.run(self.execute_async(prompt, prompt_id, extra_data, execute_outputs))
583
-
584
- async def execute_async(self, prompt, prompt_id, extra_data={}, execute_outputs=[]):
585
- nodes.interrupt_processing(False)
586
-
587
- if "client_id" in extra_data:
588
- self.server.client_id = extra_data["client_id"]
589
- else:
590
- self.server.client_id = None
591
-
592
- self.status_messages = []
593
- self.add_message("execution_start", { "prompt_id": prompt_id}, broadcast=False)
594
-
595
- with torch.inference_mode():
596
- dynamic_prompt = DynamicPrompt(prompt)
597
- reset_progress_state(prompt_id, dynamic_prompt)
598
- add_progress_handler(WebUIProgressHandler(self.server))
599
- is_changed_cache = IsChangedCache(prompt_id, dynamic_prompt, self.caches.outputs)
600
- for cache in self.caches.all:
601
- await cache.set_prompt(dynamic_prompt, prompt.keys(), is_changed_cache)
602
- cache.clean_unused()
603
-
604
- cached_nodes = []
605
- for node_id in prompt:
606
- if self.caches.outputs.get(node_id) is not None:
607
- cached_nodes.append(node_id)
608
-
609
- comfy.model_management.cleanup_models_gc()
610
- self.add_message("execution_cached",
611
- { "nodes": cached_nodes, "prompt_id": prompt_id},
612
- broadcast=False)
613
- pending_subgraph_results = {}
614
- pending_async_nodes = {} # TODO - Unify this with pending_subgraph_results
615
- executed = set()
616
- execution_list = ExecutionList(dynamic_prompt, self.caches.outputs)
617
- current_outputs = self.caches.outputs.all_node_ids()
618
- for node_id in list(execute_outputs):
619
- execution_list.add_node(node_id)
620
-
621
- while not execution_list.is_empty():
622
- node_id, error, ex = await execution_list.stage_node_execution()
623
- if error is not None:
624
- self.handle_execution_error(prompt_id, dynamic_prompt.original_prompt, current_outputs, executed, error, ex)
625
- break
626
-
627
- assert node_id is not None, "Node ID should not be None at this point"
628
- result, error, ex = await execute(self.server, dynamic_prompt, self.caches, node_id, extra_data, executed, prompt_id, execution_list, pending_subgraph_results, pending_async_nodes)
629
- self.success = result != ExecutionResult.FAILURE
630
- if result == ExecutionResult.FAILURE:
631
- self.handle_execution_error(prompt_id, dynamic_prompt.original_prompt, current_outputs, executed, error, ex)
632
- break
633
- elif result == ExecutionResult.PENDING:
634
- execution_list.unstage_node_execution()
635
- else: # result == ExecutionResult.SUCCESS:
636
- execution_list.complete_node_execution()
637
- else:
638
- # Only execute when the while-loop ends without break
639
- self.add_message("execution_success", { "prompt_id": prompt_id }, broadcast=False)
640
-
641
- ui_outputs = {}
642
- meta_outputs = {}
643
- all_node_ids = self.caches.ui.all_node_ids()
644
- for node_id in all_node_ids:
645
- ui_info = self.caches.ui.get(node_id)
646
- if ui_info is not None:
647
- ui_outputs[node_id] = ui_info["output"]
648
- meta_outputs[node_id] = ui_info["meta"]
649
- self.history_result = {
650
- "outputs": ui_outputs,
651
- "meta": meta_outputs,
652
- }
653
- self.server.last_node_id = None
654
- if comfy.model_management.DISABLE_SMART_MEMORY:
655
- comfy.model_management.unload_all_models()
656
-
657
-
658
- async def validate_inputs(prompt_id, prompt, item, validated):
659
- unique_id = item
660
- if unique_id in validated:
661
- return validated[unique_id]
662
-
663
- inputs = prompt[unique_id]['inputs']
664
- class_type = prompt[unique_id]['class_type']
665
- obj_class = nodes.NODE_CLASS_MAPPINGS[class_type]
666
-
667
- class_inputs = obj_class.INPUT_TYPES()
668
- valid_inputs = set(class_inputs.get('required',{})).union(set(class_inputs.get('optional',{})))
669
-
670
- errors = []
671
- valid = True
672
-
673
- validate_function_inputs = []
674
- validate_has_kwargs = False
675
- if hasattr(obj_class, "VALIDATE_INPUTS"):
676
- argspec = inspect.getfullargspec(obj_class.VALIDATE_INPUTS)
677
- validate_function_inputs = argspec.args
678
- validate_has_kwargs = argspec.varkw is not None
679
- received_types = {}
680
-
681
- for x in valid_inputs:
682
- input_type, input_category, extra_info = get_input_info(obj_class, x, class_inputs)
683
- assert extra_info is not None
684
- if x not in inputs:
685
- if input_category == "required":
686
- error = {
687
- "type": "required_input_missing",
688
- "message": "Required input is missing",
689
- "details": f"{x}",
690
- "extra_info": {
691
- "input_name": x
692
- }
693
- }
694
- errors.append(error)
695
- continue
696
-
697
- val = inputs[x]
698
- info = (input_type, extra_info)
699
- if isinstance(val, list):
700
- if len(val) != 2:
701
- error = {
702
- "type": "bad_linked_input",
703
- "message": "Bad linked input, must be a length-2 list of [node_id, slot_index]",
704
- "details": f"{x}",
705
- "extra_info": {
706
- "input_name": x,
707
- "input_config": info,
708
- "received_value": val
709
- }
710
- }
711
- errors.append(error)
712
- continue
713
-
714
- o_id = val[0]
715
- o_class_type = prompt[o_id]['class_type']
716
- r = nodes.NODE_CLASS_MAPPINGS[o_class_type].RETURN_TYPES
717
- received_type = r[val[1]]
718
- received_types[x] = received_type
719
- if 'input_types' not in validate_function_inputs and not validate_node_input(received_type, input_type):
720
- details = f"{x}, received_type({received_type}) mismatch input_type({input_type})"
721
- error = {
722
- "type": "return_type_mismatch",
723
- "message": "Return type mismatch between linked nodes",
724
- "details": details,
725
- "extra_info": {
726
- "input_name": x,
727
- "input_config": info,
728
- "received_type": received_type,
729
- "linked_node": val
730
- }
731
- }
732
- errors.append(error)
733
- continue
734
- try:
735
- r = await validate_inputs(prompt_id, prompt, o_id, validated)
736
- if r[0] is False:
737
- # `r` will be set in `validated[o_id]` already
738
- valid = False
739
- continue
740
- except Exception as ex:
741
- typ, _, tb = sys.exc_info()
742
- valid = False
743
- exception_type = full_type_name(typ)
744
- reasons = [{
745
- "type": "exception_during_inner_validation",
746
- "message": "Exception when validating inner node",
747
- "details": str(ex),
748
- "extra_info": {
749
- "input_name": x,
750
- "input_config": info,
751
- "exception_message": str(ex),
752
- "exception_type": exception_type,
753
- "traceback": traceback.format_tb(tb),
754
- "linked_node": val
755
- }
756
- }]
757
- validated[o_id] = (False, reasons, o_id)
758
- continue
759
- else:
760
- try:
761
- # Unwraps values wrapped in __value__ key. This is used to pass
762
- # list widget value to execution, as by default list value is
763
- # reserved to represent the connection between nodes.
764
- if isinstance(val, dict) and "__value__" in val:
765
- val = val["__value__"]
766
- inputs[x] = val
767
-
768
- if input_type == "INT":
769
- val = int(val)
770
- inputs[x] = val
771
- if input_type == "FLOAT":
772
- val = float(val)
773
- inputs[x] = val
774
- if input_type == "STRING":
775
- val = str(val)
776
- inputs[x] = val
777
- if input_type == "BOOLEAN":
778
- val = bool(val)
779
- inputs[x] = val
780
- except Exception as ex:
781
- error = {
782
- "type": "invalid_input_type",
783
- "message": f"Failed to convert an input value to a {input_type} value",
784
- "details": f"{x}, {val}, {ex}",
785
- "extra_info": {
786
- "input_name": x,
787
- "input_config": info,
788
- "received_value": val,
789
- "exception_message": str(ex)
790
- }
791
- }
792
- errors.append(error)
793
- continue
794
-
795
- if x not in validate_function_inputs and not validate_has_kwargs:
796
- if "min" in extra_info and val < extra_info["min"]:
797
- error = {
798
- "type": "value_smaller_than_min",
799
- "message": "Value {} smaller than min of {}".format(val, extra_info["min"]),
800
- "details": f"{x}",
801
- "extra_info": {
802
- "input_name": x,
803
- "input_config": info,
804
- "received_value": val,
805
- }
806
- }
807
- errors.append(error)
808
- continue
809
- if "max" in extra_info and val > extra_info["max"]:
810
- error = {
811
- "type": "value_bigger_than_max",
812
- "message": "Value {} bigger than max of {}".format(val, extra_info["max"]),
813
- "details": f"{x}",
814
- "extra_info": {
815
- "input_name": x,
816
- "input_config": info,
817
- "received_value": val,
818
- }
819
- }
820
- errors.append(error)
821
- continue
822
-
823
- if isinstance(input_type, list):
824
- combo_options = input_type
825
- if val not in combo_options:
826
- input_config = info
827
- list_info = ""
828
-
829
- # Don't send back gigantic lists like if they're lots of
830
- # scanned model filepaths
831
- if len(combo_options) > 20:
832
- list_info = f"(list of length {len(combo_options)})"
833
- input_config = None
834
- else:
835
- list_info = str(combo_options)
836
-
837
- error = {
838
- "type": "value_not_in_list",
839
- "message": "Value not in list",
840
- "details": f"{x}: '{val}' not in {list_info}",
841
- "extra_info": {
842
- "input_name": x,
843
- "input_config": input_config,
844
- "received_value": val,
845
- }
846
- }
847
- errors.append(error)
848
- continue
849
-
850
- if len(validate_function_inputs) > 0 or validate_has_kwargs:
851
- input_data_all, _ = get_input_data(inputs, obj_class, unique_id)
852
- input_filtered = {}
853
- for x in input_data_all:
854
- if x in validate_function_inputs or validate_has_kwargs:
855
- input_filtered[x] = input_data_all[x]
856
- if 'input_types' in validate_function_inputs:
857
- input_filtered['input_types'] = [received_types]
858
-
859
- #ret = obj_class.VALIDATE_INPUTS(**input_filtered)
860
- ret = await _async_map_node_over_list(prompt_id, unique_id, obj_class, input_filtered, "VALIDATE_INPUTS")
861
- ret = await resolve_map_node_over_list_results(ret)
862
- for x in input_filtered:
863
- for i, r in enumerate(ret):
864
- if r is not True and not isinstance(r, ExecutionBlocker):
865
- details = f"{x}"
866
- if r is not False:
867
- details += f" - {str(r)}"
868
-
869
- error = {
870
- "type": "custom_validation_failed",
871
- "message": "Custom validation failed for node",
872
- "details": details,
873
- "extra_info": {
874
- "input_name": x,
875
- }
876
- }
877
- errors.append(error)
878
- continue
879
-
880
- if len(errors) > 0 or valid is not True:
881
- ret = (False, errors, unique_id)
882
- else:
883
- ret = (True, [], unique_id)
884
-
885
- validated[unique_id] = ret
886
- return ret
887
-
888
- def full_type_name(klass):
889
- module = klass.__module__
890
- if module == 'builtins':
891
- return klass.__qualname__
892
- return module + '.' + klass.__qualname__
893
-
894
- async def validate_prompt(prompt_id, prompt):
895
- outputs = set()
896
- for x in prompt:
897
- if 'class_type' not in prompt[x]:
898
- error = {
899
- "type": "invalid_prompt",
900
- "message": "Cannot execute because a node is missing the class_type property.",
901
- "details": f"Node ID '#{x}'",
902
- "extra_info": {}
903
- }
904
- return (False, error, [], {})
905
-
906
- class_type = prompt[x]['class_type']
907
- class_ = nodes.NODE_CLASS_MAPPINGS.get(class_type, None)
908
- if class_ is None:
909
- error = {
910
- "type": "invalid_prompt",
911
- "message": f"Cannot execute because node {class_type} does not exist.",
912
- "details": f"Node ID '#{x}'",
913
- "extra_info": {}
914
- }
915
- return (False, error, [], {})
916
-
917
- if hasattr(class_, 'OUTPUT_NODE') and class_.OUTPUT_NODE is True:
918
- outputs.add(x)
919
-
920
- if len(outputs) == 0:
921
- error = {
922
- "type": "prompt_no_outputs",
923
- "message": "Prompt has no outputs",
924
- "details": "",
925
- "extra_info": {}
926
- }
927
- return (False, error, [], {})
928
-
929
- good_outputs = set()
930
- errors = []
931
- node_errors = {}
932
- validated = {}
933
- for o in outputs:
934
- valid = False
935
- reasons = []
936
- try:
937
- m = await validate_inputs(prompt_id, prompt, o, validated)
938
- valid = m[0]
939
- reasons = m[1]
940
- except Exception as ex:
941
- typ, _, tb = sys.exc_info()
942
- valid = False
943
- exception_type = full_type_name(typ)
944
- reasons = [{
945
- "type": "exception_during_validation",
946
- "message": "Exception when validating node",
947
- "details": str(ex),
948
- "extra_info": {
949
- "exception_type": exception_type,
950
- "traceback": traceback.format_tb(tb)
951
- }
952
- }]
953
- validated[o] = (False, reasons, o)
954
-
955
- if valid is True:
956
- good_outputs.add(o)
957
- else:
958
- logging.error(f"Failed to validate prompt for output {o}:")
959
- if len(reasons) > 0:
960
- logging.error("* (prompt):")
961
- for reason in reasons:
962
- logging.error(f" - {reason['message']}: {reason['details']}")
963
- errors += [(o, reasons)]
964
- for node_id, result in validated.items():
965
- valid = result[0]
966
- reasons = result[1]
967
- # If a node upstream has errors, the nodes downstream will also
968
- # be reported as invalid, but there will be no errors attached.
969
- # So don't return those nodes as having errors in the response.
970
- if valid is not True and len(reasons) > 0:
971
- if node_id not in node_errors:
972
- class_type = prompt[node_id]['class_type']
973
- node_errors[node_id] = {
974
- "errors": reasons,
975
- "dependent_outputs": [],
976
- "class_type": class_type
977
- }
978
- logging.error(f"* {class_type} {node_id}:")
979
- for reason in reasons:
980
- logging.error(f" - {reason['message']}: {reason['details']}")
981
- node_errors[node_id]["dependent_outputs"].append(o)
982
- logging.error("Output will be ignored")
983
-
984
- if len(good_outputs) == 0:
985
- errors_list = []
986
- for o, errors in errors:
987
- for error in errors:
988
- errors_list.append(f"{error['message']}: {error['details']}")
989
- errors_list = "\n".join(errors_list)
990
-
991
- error = {
992
- "type": "prompt_outputs_failed_validation",
993
- "message": "Prompt outputs failed validation",
994
- "details": errors_list,
995
- "extra_info": {}
996
- }
997
-
998
- return (False, error, list(good_outputs), node_errors)
999
-
1000
- return (True, None, list(good_outputs), node_errors)
1001
-
1002
- MAXIMUM_HISTORY_SIZE = 10000
1003
-
1004
- class PromptQueue:
1005
- def __init__(self, server):
1006
- self.server = server
1007
- self.mutex = threading.RLock()
1008
- self.not_empty = threading.Condition(self.mutex)
1009
- self.task_counter = 0
1010
- self.queue = []
1011
- self.currently_running = {}
1012
- self.history = {}
1013
- self.flags = {}
1014
-
1015
- def put(self, item):
1016
- with self.mutex:
1017
- heapq.heappush(self.queue, item)
1018
- self.server.queue_updated()
1019
- self.not_empty.notify()
1020
-
1021
- def get(self, timeout=None):
1022
- with self.not_empty:
1023
- while len(self.queue) == 0:
1024
- self.not_empty.wait(timeout=timeout)
1025
- if timeout is not None and len(self.queue) == 0:
1026
- return None
1027
- item = heapq.heappop(self.queue)
1028
- i = self.task_counter
1029
- self.currently_running[i] = copy.deepcopy(item)
1030
- self.task_counter += 1
1031
- self.server.queue_updated()
1032
- return (item, i)
1033
-
1034
- class ExecutionStatus(NamedTuple):
1035
- status_str: Literal['success', 'error']
1036
- completed: bool
1037
- messages: List[str]
1038
-
1039
- def task_done(self, item_id, history_result,
1040
- status: Optional['PromptQueue.ExecutionStatus']):
1041
- with self.mutex:
1042
- prompt = self.currently_running.pop(item_id)
1043
- if len(self.history) > MAXIMUM_HISTORY_SIZE:
1044
- self.history.pop(next(iter(self.history)))
1045
-
1046
- status_dict: Optional[dict] = None
1047
- if status is not None:
1048
- status_dict = copy.deepcopy(status._asdict())
1049
-
1050
- # Remove sensitive data from extra_data before storing in history
1051
- for sensitive_val in SENSITIVE_EXTRA_DATA_KEYS:
1052
- if sensitive_val in prompt[3]:
1053
- prompt[3].pop(sensitive_val)
1054
-
1055
- self.history[prompt[1]] = {
1056
- "prompt": prompt,
1057
- "outputs": {},
1058
- 'status': status_dict,
1059
- }
1060
- self.history[prompt[1]].update(history_result)
1061
- self.server.queue_updated()
1062
-
1063
- # Note: slow
1064
- def get_current_queue(self):
1065
- with self.mutex:
1066
- out = []
1067
- for x in self.currently_running.values():
1068
- out += [x]
1069
- return (out, copy.deepcopy(self.queue))
1070
-
1071
- # read-safe as long as queue items are immutable
1072
- def get_current_queue_volatile(self):
1073
- with self.mutex:
1074
- running = [x for x in self.currently_running.values()]
1075
- queued = copy.copy(self.queue)
1076
- return (running, queued)
1077
-
1078
- def get_tasks_remaining(self):
1079
- with self.mutex:
1080
- return len(self.queue) + len(self.currently_running)
1081
-
1082
- def wipe_queue(self):
1083
- with self.mutex:
1084
- self.queue = []
1085
- self.server.queue_updated()
1086
-
1087
- def delete_queue_item(self, function):
1088
- with self.mutex:
1089
- for x in range(len(self.queue)):
1090
- if function(self.queue[x]):
1091
- if len(self.queue) == 1:
1092
- self.wipe_queue()
1093
- else:
1094
- self.queue.pop(x)
1095
- heapq.heapify(self.queue)
1096
- self.server.queue_updated()
1097
- return True
1098
- return False
1099
-
1100
- def get_history(self, prompt_id=None, max_items=None, offset=-1):
1101
- with self.mutex:
1102
- if prompt_id is None:
1103
- out = {}
1104
- i = 0
1105
- if offset < 0 and max_items is not None:
1106
- offset = len(self.history) - max_items
1107
- for k in self.history:
1108
- if i >= offset:
1109
- out[k] = self.history[k]
1110
- if max_items is not None and len(out) >= max_items:
1111
- break
1112
- i += 1
1113
- return out
1114
- elif prompt_id in self.history:
1115
- return {prompt_id: copy.deepcopy(self.history[prompt_id])}
1116
- else:
1117
- return {}
1118
-
1119
- def wipe_history(self):
1120
- with self.mutex:
1121
- self.history = {}
1122
-
1123
- def delete_history_item(self, id_to_delete):
1124
- with self.mutex:
1125
- self.history.pop(id_to_delete, None)
1126
-
1127
- def set_flag(self, name, data):
1128
- with self.mutex:
1129
- self.flags[name] = data
1130
- self.not_empty.notify()
1131
-
1132
- def get_flags(self, reset=True):
1133
- with self.mutex:
1134
- if reset:
1135
- ret = self.flags
1136
- self.flags = {}
1137
- return ret
1138
- else:
1139
- return self.flags.copy()