From 652d55e69018433e711fe09e984b3ceeca6a987c Mon Sep 17 00:00:00 2001 From: TinyTerra Date: Sat, 11 May 2024 17:06:13 +1000 Subject: [PATCH] Fixed recursive execute for xyPlot --- ttNpy/tinyterraNodes.py | 177 ++++++---------- ttNpy/ttNexecutor.py | 434 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 498 insertions(+), 113 deletions(-) create mode 100644 ttNpy/ttNexecutor.py diff --git a/ttNpy/tinyterraNodes.py b/ttNpy/tinyterraNodes.py index 5b02668..22fb5b3 100644 --- a/ttNpy/tinyterraNodes.py +++ b/ttNpy/tinyterraNodes.py @@ -53,6 +53,7 @@ from nodes import MAX_RESOLUTION, ControlNetApplyAdvanced from nodes import NODE_CLASS_MAPPINGS as COMFY_CLASS_MAPPINGS from .utils import CC, ttNl, ttNpaths, AnyType +from .ttNexecutor import xyExecutor class ttNloader: def __init__(self): @@ -439,18 +440,14 @@ class ttNsampler: ) class ttNadv_xyPlot: - def __init__(self, adv_xyPlot, unique_id, prompt, extra_pnginfo, save_prefix, image_output): + def __init__(self, adv_xyPlot, unique_id, prompt, extra_pnginfo, save_prefix, image_output, executor): + self.executor = executor self.unique_id = str(unique_id) self.prompt = prompt self.extra_pnginfo = extra_pnginfo self.save_prefix = save_prefix self.image_output = image_output - self.outputs = {} - self.outputs_ui = {} - self.object_storage = {} - - self.results = {} self.latent_list = [] self.image_list = [] self.ui_list = [] @@ -471,6 +468,13 @@ class ttNadv_xyPlot: self.num = 0 self.total = (self.num_cols if self.num_cols > 0 else 1) * (self.num_rows if self.num_rows > 0 else 1) + def reset(self): + self.executor.reset() + self.executor = None + self.latent_list = [] + self.image_list = [] + self.ui_list = [] + @staticmethod def get_font(font_size): return ImageFont.truetype(str(Path(ttNpaths.font_path)), font_size) @@ -574,6 +578,17 @@ class ttNadv_xyPlot: def getRelevantPrompt(self): nodes_to_keep = self._getNodesToKeep(self.unique_id, self.prompt) new_prompt = {node_id: self.prompt[node_id] for node_id in nodes_to_keep} + + if self.save_individuals == True: + if self.image_output in ["Hide", "Hide/Save"]: + new_prompt[self.unique_id]["inputs"]["image_output"] = "Hide/Save" + else: + new_prompt[self.unique_id]["inputs"]["image_output"] = "Save" + elif self.image_output in ["Preview", "Save"]: + new_prompt[self.unique_id]["inputs"]["image_output"] = "Preview" + else: + new_prompt[self.unique_id]["inputs"]["image_output"] = "Hide" + return new_prompt def plot_images(self): @@ -620,104 +635,28 @@ class ttNadv_xyPlot: return int(initial_font_size * (label_width / text_width) * scaling_factor) else: return initial_font_size - - def execute_node(self, unique_id, prompt, object_storage, extra_data): - inputs = prompt[str(unique_id)]["inputs"] - class_type = prompt[str(unique_id)]["class_type"] - if class_type == "ttN advanced xyPlot": - class_def = ttN_Plotting #Fake class to avoid recursive execute of xy_plot node - else: - class_def = COMFY_CLASS_MAPPINGS[class_type] - - for x in inputs: - input_data = inputs[x] - - if isinstance(input_data, list): - input_unique_id = input_data[0] - output_index = input_data[1] - if input_unique_id not in self.outputs: - if input_unique_id not in prompt: - input_data = None - else: - self.outputs = self.execute_node(input_unique_id, prompt, object_storage, extra_data) - - input_data = self.outputs[input_unique_id][output_index] - - input_data_all = None - try: - input_data_all = execution.get_input_data(inputs, class_def, unique_id, self.outputs, prompt, extra_data) - - obj = object_storage.get((unique_id, class_type), None) - if obj is None: - obj = class_def() - object_storage[(unique_id, class_type)] = obj - - output_data, output_ui = execution.get_output_data(obj, input_data_all) - self.outputs[unique_id] = output_data - self.outputs_ui[unique_id] = output_ui - except comfy.model_management.InterruptProcessingException as iex: - logging.info("Processing interrupted") - raise iex - - return self.outputs - def execute_prompt(self, prompt, extra_data, xpoint, ypoint, x_label, y_label): - if self.save_individuals == True: - if self.image_output in ["Hide", "Hide/Save"]: - prompt[self.unique_id]["inputs"]["image_output"] = "Hide/Save" - else: - prompt[self.unique_id]["inputs"]["image_output"] = "Save" - - elif self.image_output in ["Preview", "Save"]: - prompt[self.unique_id]["inputs"]["image_output"] = "Preview" - else: - prompt[self.unique_id]["inputs"]["image_output"] = "Hide" - - self.num += 1 - ttNl(f'{CC.GREY}X: {x_label}, Y: {y_label}').t(f'Plot Values {self.num}/{self.total} ->').p() + def execute_prompt(self, prompt, extra_data, x_label, y_label): + valid = execution.validate_prompt(prompt) - with torch.inference_mode(): - #delete cached outputs if nodes don't exist for them - to_delete = [] - for o in self.outputs: - if o not in prompt: - to_delete += [o] - for o in to_delete: - d = self.outputs.pop(o) - d2 = self.outputs_ui.pop(o) - del d - del d2 - to_delete = [] - for o in self.object_storage: - if o[0] not in prompt: - to_delete += [o] - else: - p = prompt[o[0]] - if o[1] != p['class_type']: - to_delete += [o] - for o in to_delete: - d = self.object_storage.pop(o) - del d - - comfy.model_management.cleanup_models(keep_clone_weights_loaded=True) + if valid[0]: + ttNl(f'{CC.GREY}X: {x_label}, Y: {y_label}').t(f'Plot Values {self.num}/{self.total} ->').p() - for node_id in prompt: - self.execute_node(node_id, prompt, self.object_storage, extra_data) + self.executor.execute(prompt, self.num, extra_data, valid[2]) - if comfy.model_management.DISABLE_SMART_MEMORY: - comfy.model_management.unload_all_models() + self.latent_list.append(self.executor.outputs[self.unique_id][-6][0]["samples"]) + ui_out = self.executor.outputs_ui[self.unique_id].get('images') + if ui_out is not None and len(ui_out) > 0: + self.ui_list.append(ui_out[0]) - self.latent_list.append(self.outputs[self.unique_id][-6][0]["samples"]) - ui_out = self.outputs_ui[self.unique_id].get('images') - if ui_out is not None and len(ui_out) > 0: - self.ui_list.append(ui_out[0]) + image = self.executor.outputs[self.unique_id][-3][0] + pil_image = ttNsampler.tensor2pil(image) + self.image_list.append(pil_image) - image = self.outputs[self.unique_id][-3][0] - pil_image = ttNsampler.tensor2pil(image) - self.image_list.append(pil_image) - - self.max_width = max(self.max_width, pil_image.width) - self.max_height = max(self.max_height, pil_image.height) + self.max_width = max(self.max_width, pil_image.width) + self.max_height = max(self.max_height, pil_image.height) + else: + raise Exception(valid[1]) @staticmethod def _parse_value(input_name, value, node_inputs, input_types, regex): @@ -750,13 +689,14 @@ class ttNadv_xyPlot: value = False value = bool(value) return input_name, value - + def xy_plot_process(self): if self.x_points is None and self.y_points is None: return None, None, None, None regex = re.compile(r'%(.*?);(.*?)%') - + + x_label, y_label = None, None base_prompt = self.getRelevantPrompt() for xpoint, nodes in self.x_points.items(): @@ -799,14 +739,16 @@ class ttNadv_xyPlot: for input_name, value in inputs.items(): input_name, value = self._parse_value(input_name, value, y_node_inputs, y_input_types, regex) y_node_inputs[input_name] = value - - self.execute_prompt(y_prompt, self.extra_pnginfo, xpoint, ypoint, x_label, y_label) + + self.num += 1 + self.execute_prompt(y_prompt, self.extra_pnginfo, x_label, y_label) else: - self.execute_prompt(x_prompt, self.extra_pnginfo, xpoint, "1", x_label, None) + self.num += 1 + self.execute_prompt(x_prompt, self.extra_pnginfo, x_label, y_label) # Rearrange latent array to match preview image grid self.latent_list = self.rearrange_tensors(self.latent_list, self.num_cols, self.num_rows) - rearranged_image_list = self.rearrange_tensors(self.image_list, self.num_cols, self.num_rows) + self.image_list = self.rearrange_tensors(self.image_list, self.num_cols, self.num_rows) if self.image_output in ["Preview", "Save"]: rearranged_ui_list = self.rearrange_tensors(self.ui_list, self.num_cols, self.num_rows) @@ -818,7 +760,7 @@ class ttNadv_xyPlot: plot_image = self.plot_images() images = [] - for image in rearranged_image_list: + for image in self.image_list: images.append(sampler.pil2tensor(image)) images_out = torch.cat(images, dim=0) @@ -1312,8 +1254,11 @@ class ttN_pipeKSampler_v2: random.seed(seed) - plotter = ttNadv_xyPlot(adv_xyPlot, my_unique_id, prompt, extra_pnginfo, save_prefix, image_output) + executor = xyExecutor() + plotter = ttNadv_xyPlot(adv_xyPlot, my_unique_id, prompt, extra_pnginfo, save_prefix, image_output, executor) plot_image, images, samples, ui_results = plotter.xy_plot_process() + plotter.reset() + del executor, plotter if samples is None and images is None: return process_sample_state(samp_model, samp_images, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, lora_name, lora_model_strength, lora_clip_strength, @@ -1323,7 +1268,7 @@ class ttN_pipeKSampler_v2: plot_result = ttN_save.images(plot_image, save_prefix, image_output, embed_workflow, file_type) - plot_result.extend(ui_results) + #plot_result.extend(ui_results) new_pipe = { "model": samp_model, @@ -1793,9 +1738,12 @@ class ttN_pipeKSamplerSDXL_v2: image_output, save_prefix, file_type, embed_workflow, prompt, extra_pnginfo, my_unique_id, preview_latent, adv_xyPlot): random.seed(seed) - - plotter = ttNadv_xyPlot(adv_xyPlot, my_unique_id, prompt, extra_pnginfo, save_prefix, image_output) + + executor = xyExecutor() + plotter = ttNadv_xyPlot(adv_xyPlot, my_unique_id, prompt, extra_pnginfo, save_prefix, image_output, executor) plot_image, images, samples, ui_results = plotter.xy_plot_process() + plotter.reset() + del executor, plotter if samples is None and images is None: return process_sample_state(sdxl_model, sdxl_images, sdxl_clip, sdxl_samples, sdxl_vae, sdxl_seed, sdxl_positive, sdxl_negative, lora_name, lora_model_strength, lora_clip_strength, @@ -1806,7 +1754,7 @@ class ttN_pipeKSamplerSDXL_v2: plot_result = ttN_save.images(plot_image, save_prefix, image_output, embed_workflow, file_type) - plot_result.extend(ui_results) + #plot_result.extend(ui_results) new_sdxl_pipe = { "model": sdxl_model, @@ -2412,9 +2360,12 @@ class ttN_KSampler_v2: image_output, save_prefix, file_type, embed_workflow, prompt, extra_pnginfo, my_unique_id, preview_latent, adv_xyPlot): random.seed(seed) - - plotter = ttNadv_xyPlot(adv_xyPlot, my_unique_id, prompt, extra_pnginfo, save_prefix, image_output) + + executor = xyExecutor() + plotter = ttNadv_xyPlot(adv_xyPlot, my_unique_id, prompt, extra_pnginfo, save_prefix, image_output, executor) plot_image, images, samples, ui_results = plotter.xy_plot_process() + plotter.reset() + del executor, plotter if samples is None and images is None: return process_sample_state(model, images, clip, samples, vae, seed, positive, negative, lora_name, lora_model_strength, lora_clip_strength, @@ -2424,7 +2375,7 @@ class ttN_KSampler_v2: plot_result = ttN_save.images(plot_image, save_prefix, image_output, embed_workflow, file_type) - plot_result.extend(ui_results) + #plot_result.extend(ui_results) if image_output in ("Hide", "Hide/Save"): return (model, positive, negative, samples, vae, clip, images, seed, plot_image) diff --git a/ttNpy/ttNexecutor.py b/ttNpy/ttNexecutor.py new file mode 100644 index 0000000..0c69d0b --- /dev/null +++ b/ttNpy/ttNexecutor.py @@ -0,0 +1,434 @@ +import nodes +import torch +import comfy.model_management +import copy +import logging +import sys +import traceback +from execution import full_type_name, get_input_data, get_output_data + +class ttN_advanced_XYPlot: + version = '1.1.0' + plotPlaceholder = "_PLOT\nExample:\n\n\n[node_ID:widget_Name='value']\n\n\n[node_ID:widget_Name='value2']\n[node_ID:widget2_Name='value']\n[node_ID2:widget_Name='value']\n\netc..." + + def get_plot_points(plot_data, unique_id): + if plot_data is None or plot_data.strip() == '': + return None + else: + try: + axis_dict = {} + lines = plot_data.split('<') + new_lines = [] + temp_line = '' + + for line in lines: + if line.startswith('lora'): + temp_line += '<' + line + new_lines[-1] = temp_line + else: + new_lines.append(line) + temp_line = line + + for line in new_lines: + if line: + values_label = [] + line = line.split('>', 1) + num, label = line[0].split(':', 1) + axis_dict[num] = {"label": label} + for point in line[1].split('['): + if point.strip() != '': + node_id = point.split(':', 1)[0] + axis_dict[num][node_id] = {} + input_name = point.split(':', 1)[1].split('=')[0] + value = point.split("'")[1].split("'")[0] + values_label.append((value, input_name, node_id)) + + axis_dict[num][node_id][input_name] = value + + if label in ['v_label', 'tv_label', 'idtv_label']: + new_label = [] + for value, input_name, node_id in values_label: + if label == 'v_label': + new_label.append(value) + elif label == 'tv_label': + new_label.append(f'{input_name}: {value}') + elif label == 'idtv_label': + new_label.append(f'[{node_id}] {input_name}: {value}') + axis_dict[num]['label'] = ', '.join(new_label) + + except ValueError: + ttNl('Invalid Plot - defaulting to None...').t(f'advanced_XYPlot[{unique_id}]').warn().p() + return None + return axis_dict + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "grid_spacing": ("INT",{"min": 0, "max": 500, "step": 5, "default": 0,}), + "save_individuals": ("BOOLEAN", {"default": False}), + "flip_xy": ("BOOLEAN", {"default": False}), + + "x_plot": ("STRING",{"default": '', "multiline": True, "placeholder": 'X' + ttN_advanced_XYPlot.plotPlaceholder, "pysssss.autocomplete": False}), + "y_plot": ("STRING",{"default": '', "multiline": True, "placeholder": 'Y' + ttN_advanced_XYPlot.plotPlaceholder, "pysssss.autocomplete": False}), + }, + "hidden": { + "prompt": ("PROMPT",), + "extra_pnginfo": ("EXTRA_PNGINFO",), + "my_unique_id": ("MY_UNIQUE_ID",), + "ttNnodeVersion": ttN_advanced_XYPlot.version, + }, + } + + RETURN_TYPES = ("ADV_XYPLOT", ) + RETURN_NAMES = ("adv_xyPlot", ) + FUNCTION = "plot" + + CATEGORY = "🌏 tinyterra/xyPlot" + + def plot(self, grid_spacing, save_individuals, flip_xy, x_plot=None, y_plot=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + x_plot = ttN_advanced_XYPlot.get_plot_points(x_plot, my_unique_id) + y_plot = ttN_advanced_XYPlot.get_plot_points(y_plot, my_unique_id) + + if x_plot == {}: + x_plot = None + if y_plot == {}: + y_plot = None + + if flip_xy == "True": + x_plot, y_plot = y_plot, x_plot + + xy_plot = {"x_plot": x_plot, + "y_plot": y_plot, + "grid_spacing": grid_spacing, + "save_individuals": save_individuals,} + + return (xy_plot, ) + +class ttN_Plotting(ttN_advanced_XYPlot): + def plot(self, **args): + xy_plot = None + return (xy_plot, ) + + +def map_node_over_list(obj, input_data_all, func, allow_interrupt=False): + # check if node wants the lists + input_is_list = False + if hasattr(obj, "INPUT_IS_LIST"): + input_is_list = obj.INPUT_IS_LIST + + if len(input_data_all) == 0: + max_len_input = 0 + else: + max_len_input = max([len(x) for x in input_data_all.values()]) + + # get a slice of inputs, repeat last input when list isn't long enough + def slice_dict(d, i): + d_new = dict() + for k,v in d.items(): + d_new[k] = v[i if len(v) > i else -1] + return d_new + + results = [] + if input_is_list: + if allow_interrupt: + nodes.before_node_execution() + results.append(getattr(obj, func)(**input_data_all)) + elif max_len_input == 0: + if allow_interrupt: + nodes.before_node_execution() + results.append(getattr(obj, func)()) + else: + for i in range(max_len_input): + if allow_interrupt: + nodes.before_node_execution() + results.append(getattr(obj, func)(**slice_dict(input_data_all, i))) + return results + +def format_value(x): + if x is None: + return None + elif isinstance(x, (int, float, bool, str)): + return x + else: + return str(x) + +def recursive_execute(prompt, outputs, current_item, extra_data, executed, prompt_id, outputs_ui, object_storage): + unique_id = current_item + inputs = prompt[unique_id]['inputs'] + class_type = prompt[unique_id]['class_type'] + if class_type == "ttN advanced xyPlot": + class_def = ttN_Plotting #Fake class to avoid recursive execute of xy_plot node + else: + class_def = nodes.NODE_CLASS_MAPPINGS[class_type] + + if unique_id in outputs: + print('returning already executed', unique_id) + return (True, None, None) + + for x in inputs: + input_data = inputs[x] + + if isinstance(input_data, list): + input_unique_id = input_data[0] + output_index = input_data[1] + if input_unique_id not in outputs: + result = recursive_execute(prompt, outputs, input_unique_id, extra_data, executed, prompt_id, outputs_ui, object_storage) + if result[0] is not True: + # Another node failed further upstream + return result + + input_data_all = None + try: + input_data_all = get_input_data(inputs, class_def, unique_id, outputs, prompt, extra_data) + + obj = object_storage.get((unique_id, class_type), None) + if obj is None: + obj = class_def() + object_storage[(unique_id, class_type)] = obj + + output_data, output_ui = get_output_data(obj, input_data_all) + outputs[unique_id] = output_data + if len(output_ui) > 0: + outputs_ui[unique_id] = output_ui + + except comfy.model_management.InterruptProcessingException as iex: + logging.info("Processing interrupted") + + # skip formatting inputs/outputs + error_details = { + "node_id": unique_id, + } + + return (False, error_details, iex) + except Exception as ex: + typ, _, tb = sys.exc_info() + exception_type = full_type_name(typ) + input_data_formatted = {} + if input_data_all is not None: + input_data_formatted = {} + for name, inputs in input_data_all.items(): + input_data_formatted[name] = [format_value(x) for x in inputs] + + output_data_formatted = {} + for node_id, node_outputs in outputs.items(): + output_data_formatted[node_id] = [[format_value(x) for x in l] for l in node_outputs] + + logging.error(f"!!! Exception during xyPlot processing!!! {ex}") + logging.error(traceback.format_exc()) + + error_details = { + "node_id": unique_id, + "exception_message": str(ex), + "exception_type": exception_type, + "traceback": traceback.format_tb(tb), + "current_inputs": input_data_formatted, + "current_outputs": output_data_formatted + } + return (False, error_details, ex) + + executed.add(unique_id) + + return (True, None, None) + +def recursive_will_execute(prompt, outputs, current_item, memo={}): + unique_id = current_item + + if unique_id in memo: + return memo[unique_id] + + inputs = prompt[unique_id]['inputs'] + will_execute = [] + if unique_id in outputs: + return [] + + for x in inputs: + input_data = inputs[x] + if isinstance(input_data, list): + input_unique_id = input_data[0] + output_index = input_data[1] + if input_unique_id not in outputs: + will_execute += recursive_will_execute(prompt, outputs, input_unique_id, memo) + + memo[unique_id] = will_execute + [unique_id] + return memo[unique_id] + +def recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item): + unique_id = current_item + inputs = prompt[unique_id]['inputs'] + class_type = prompt[unique_id]['class_type'] + class_def = nodes.NODE_CLASS_MAPPINGS[class_type] + + is_changed_old = '' + is_changed = '' + to_delete = False + if hasattr(class_def, 'IS_CHANGED'): + if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: + is_changed_old = old_prompt[unique_id]['is_changed'] + if 'is_changed' not in prompt[unique_id]: + input_data_all = get_input_data(inputs, class_def, unique_id, outputs) + if input_data_all is not None: + try: + #is_changed = class_def.IS_CHANGED(**input_data_all) + is_changed = map_node_over_list(class_def, input_data_all, "IS_CHANGED") + prompt[unique_id]['is_changed'] = is_changed + except: + to_delete = True + else: + is_changed = prompt[unique_id]['is_changed'] + + if unique_id not in outputs: + return True + + if not to_delete: + if is_changed != is_changed_old: + to_delete = True + elif unique_id not in old_prompt: + to_delete = True + elif inputs == old_prompt[unique_id]['inputs']: + for x in inputs: + input_data = inputs[x] + + if isinstance(input_data, list): + input_unique_id = input_data[0] + output_index = input_data[1] + if input_unique_id in outputs: + to_delete = recursive_output_delete_if_changed(prompt, old_prompt, outputs, input_unique_id) + else: + to_delete = True + if to_delete: + break + else: + to_delete = True + + if to_delete: + d = outputs.pop(unique_id) + del d + return to_delete + + +class xyExecutor: + def __init__(self): + self.reset() + + def reset(self): + self.outputs = {} + self.object_storage = {} + self.outputs_ui = {} + self.status_messages = [] + self.success = True + self.old_prompt = {} + + def add_message(self, event, data, broadcast: bool): + self.status_messages.append((event, data)) + + def handle_execution_error(self, prompt_id, prompt, current_outputs, executed, error, ex): + node_id = error["node_id"] + class_type = prompt[node_id]["class_type"] + + # First, send back the status to the frontend depending + # on the exception type + if isinstance(ex, comfy.model_management.InterruptProcessingException): + mes = { + "prompt_id": prompt_id, + "node_id": node_id, + "node_type": class_type, + "executed": list(executed), + } + self.add_message("execution_interrupted", mes, broadcast=True) + else: + mes = { + "prompt_id": prompt_id, + "node_id": node_id, + "node_type": class_type, + "executed": list(executed), + + "exception_message": error["exception_message"], + "exception_type": error["exception_type"], + "traceback": error["traceback"], + "current_inputs": error["current_inputs"], + "current_outputs": error["current_outputs"], + } + self.add_message("execution_error", mes, broadcast=False) + + # Next, remove the subsequent outputs since they will not be executed + to_delete = [] + for o in self.outputs: + if (o not in current_outputs) and (o not in executed): + to_delete += [o] + if o in self.old_prompt: + d = self.old_prompt.pop(o) + del d + for o in to_delete: + d = self.outputs.pop(o) + del d + + def execute(self, prompt, prompt_id, extra_data={}, execute_outputs=[]): + nodes.interrupt_processing(False) + + self.status_messages = [] + self.add_message("execution_start", { "prompt_id": prompt_id}, broadcast=False) + + with torch.inference_mode(): + #delete cached outputs if nodes don't exist for them + to_delete = [] + for o in self.outputs: + if o not in prompt: + to_delete += [o] + for o in to_delete: + d = self.outputs.pop(o) + del d + to_delete = [] + for o in self.object_storage: + if o[0] not in prompt: + to_delete += [o] + else: + p = prompt[o[0]] + if o[1] != p['class_type']: + to_delete += [o] + for o in to_delete: + d = self.object_storage.pop(o) + del d + + for x in prompt: + recursive_output_delete_if_changed(prompt, self.old_prompt, self.outputs, x) + + current_outputs = set(self.outputs.keys()) + for x in list(self.outputs_ui.keys()): + if x not in current_outputs: + d = self.outputs_ui.pop(x) + del d + + comfy.model_management.cleanup_models(keep_clone_weights_loaded=True) + self.add_message("execution_cached", + { "nodes": list(current_outputs) , "prompt_id": prompt_id}, + broadcast=False) + executed = set() + output_node_id = None + to_execute = [] + + for node_id in list(execute_outputs): + to_execute += [(0, node_id)] + + while len(to_execute) > 0: + #always execute the output that depends on the least amount of unexecuted nodes first + memo = {} + to_execute = sorted(list(map(lambda a: (len(recursive_will_execute(prompt, self.outputs, a[-1], memo)), a[-1]), to_execute))) + output_node_id = to_execute.pop(0)[-1] + + # This call shouldn't raise anything if there's an error deep in + # the actual SD code, instead it will report the node where the + # error was raised + self.success, error, ex = recursive_execute(prompt, self.outputs, output_node_id, extra_data, executed, prompt_id, self.outputs_ui, self.object_storage) + if self.success is not True: + self.handle_execution_error(prompt_id, prompt, current_outputs, executed, error, ex) + break + + for x in executed: + self.old_prompt[x] = copy.deepcopy(prompt[x]) + + if comfy.model_management.DISABLE_SMART_MEMORY: + comfy.model_management.unload_all_models()