From fc935f8e127bb8eab53ffd1048c0b53ca7d2b2e4 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Mon, 20 Mar 2023 22:25:03 -0400 Subject: [PATCH 01/16] Readme update. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aab892531..d83174e3c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ This ui will let you design and execute advanced stable diffusion pipelines usin - [Upscale Models (ESRGAN, ESRGAN variants, SwinIR, Swin2SR, etc...)](https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/) - Starts up very fast. - Works fully offline: will never download anything. +- [Config file](extra_model_paths.yaml.example) to set the search paths for models. Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/) @@ -136,9 +137,9 @@ This will let you use: pip3.10 to install all the dependencies. ## How to increase generation speed? -The fp16 model configs in the CheckpointLoader can be used to load them in fp16 mode, depending on your GPU this will increase your gen speed by a significant amount. +Make sure you use the CheckpointLoaderSimple node to load checkpoints. It will auto pick the right settings depending on your GPU. -You can also set this command line setting to disable the upcasting to fp32 in some cross attention operations which will increase your speed. Note that this will very likely give you black images on SD2.x models. +You can set this command line setting to disable the upcasting to fp32 in some cross attention operations which will increase your speed. Note that this doesn't do anything when xformers is enabled and will very likely give you black images on SD2.x models. ```--dont-upcast-attention``` From aa2ddfabb938b90d15d79f25d92917e9f86bf91d Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 21 Mar 2023 03:11:18 -0400 Subject: [PATCH 02/16] Fix bug with CLIPLoader. --- nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index 1658a4c88..cb4d7723e 100644 --- a/nodes.py +++ b/nodes.py @@ -337,7 +337,7 @@ class CLIPLoader: def load_clip(self, clip_name): clip_path = folder_paths.get_full_path("clip", clip_name) - clip = comfy.sd.load_clip(ckpt_path=clip_path, embedding_directory=CheckpointLoader.embedding_directory) + clip = comfy.sd.load_clip(ckpt_path=clip_path, embedding_directory=folder_paths.get_folder_paths("embeddings")) return (clip,) class CLIPVisionLoader: From 0b6ba21f52e176a0a579eda7dbfe9d628ae0f062 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Tue, 21 Mar 2023 08:00:13 +0000 Subject: [PATCH 03/16] Added save image menu item --- web/scripts/app.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/web/scripts/app.js b/web/scripts/app.js index 3f06629ee..dc61c5a66 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -80,10 +80,23 @@ class ComfyApp { img = this.imgs[this.overIndex]; } if (img) { - options.unshift({ - content: "Open Image", - callback: () => window.open(img.src, "_blank"), - }); + options.unshift( + { + content: "Open Image", + callback: () => window.open(img.src, "_blank"), + }, + { + content: "Save Image", + callback: () => { + const a = document.createElement("a"); + a.href = img.src; + a.setAttribute("download", new URLSearchParams(new URL(img.src).search).get("filename")); + document.body.append(a); + a.click(); + requestAnimationFrame(() => a.remove()); + }, + } + ); } } }; From 49705dc947a896b12cdc325c3ea8e101512fe455 Mon Sep 17 00:00:00 2001 From: "Guo Y.K" Date: Tue, 21 Mar 2023 17:42:33 +0800 Subject: [PATCH 04/16] ui: hide decimal fraction for int fields --- web/scripts/widgets.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index e1f637637..d6705d872 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -103,6 +103,7 @@ export const ComfyWidgets = { }, INT(node, inputName, inputData) { const { val, config } = getNumberDefaults(inputData, 1); + Object.assign(config, { precision: 0 }) return { widget: node.addWidget( "number", From eb67d0554016758d04af86375ac284a864924cbe Mon Sep 17 00:00:00 2001 From: xss Date: Tue, 21 Mar 2023 13:31:47 -0500 Subject: [PATCH 05/16] add canvas tab index so it recieves keydown events --- web/scripts/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/scripts/app.js b/web/scripts/app.js index dc61c5a66..fd410cd30 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -494,6 +494,7 @@ class ComfyApp { // Create and mount the LiteGraph in the DOM const canvasEl = (this.canvasEl = Object.assign(document.createElement("canvas"), { id: "graph-canvas" })); + canvasEl.tabIndex = "1" document.body.prepend(canvasEl); this.graph = new LGraph(); From cc309568e103cfff491e281c132cfb322525df61 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 21 Mar 2023 14:51:51 -0400 Subject: [PATCH 06/16] Add support for locon mid weights. --- comfy/sd.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/comfy/sd.py b/comfy/sd.py index 6d1e8bb9b..b8f829664 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -129,12 +129,17 @@ def load_lora(path, to_load): A_name = "{}.lora_up.weight".format(x) B_name = "{}.lora_down.weight".format(x) alpha_name = "{}.alpha".format(x) + mid_name = "{}.lora_mid.weight".format(x) if A_name in lora.keys(): alpha = None if alpha_name in lora.keys(): alpha = lora[alpha_name].item() loaded_keys.add(alpha_name) - patch_dict[to_load[x]] = (lora[A_name], lora[B_name], alpha) + mid = None + if mid_name in lora.keys(): + mid = lora[mid_name] + loaded_keys.add(mid_name) + patch_dict[to_load[x]] = (lora[A_name], lora[B_name], alpha, mid) loaded_keys.add(A_name) loaded_keys.add(B_name) for x in lora.keys(): @@ -279,6 +284,10 @@ class ModelPatcher: mat2 = v[1] if v[2] is not None: alpha *= v[2] / mat2.shape[0] + if v[3] is not None: + #locon mid weights, hopefully the math is fine because I didn't properly test it + final_shape = [mat2.shape[1], mat2.shape[0], v[3].shape[2], v[3].shape[3]] + mat2 = torch.mm(mat2.transpose(0, 1).flatten(start_dim=1).float(), v[3].transpose(0, 1).flatten(start_dim=1).float()).reshape(final_shape).transpose(0, 1) weight += (alpha * torch.mm(mat1.flatten(start_dim=1).float(), mat2.flatten(start_dim=1).float())).reshape(weight.shape).type(weight.dtype).to(weight.device) return self.model def unpatch_model(self): From 9d0665c8d0dec457b87c91a20a1af2083857c988 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 21 Mar 2023 16:57:35 -0400 Subject: [PATCH 07/16] Add laptop quadro cards to fp32 list. --- comfy/model_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index c26d682f7..5c4e97da3 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -231,7 +231,7 @@ def should_use_fp16(): return False #FP32 is faster on those cards? - nvidia_16_series = ["1660", "1650", "1630"] + nvidia_16_series = ["1660", "1650", "1630", "T500", "T550", "T600"] for x in nvidia_16_series: if x in props.name: return False From b810ca49f1eb5856a079f25429fb47003d64c945 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Tue, 21 Mar 2023 21:34:00 +0000 Subject: [PATCH 08/16] Add support for multiple multiline text areas on the same widget --- web/scripts/widgets.js | 79 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index d6705d872..72984b4bf 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -27,7 +27,11 @@ function seedWidget(node, inputName, inputData) { return { widget: seed, randomize }; } -function addMultilineWidget(node, name, defaultVal, app) { +const MultilineSymbol = Symbol(); + +function addMultilineWidget(node, name, opts, app) { + const MIN_SIZE = 50; + const widget = { type: "customtext", name, @@ -43,9 +47,9 @@ function addMultilineWidget(node, name, defaultVal, app) { const margin = 10; Object.assign(this.inputEl.style, { left: `${t.a * margin + t.e}px`, - top: `${t.d * (y + widgetHeight - margin) + t.f}px`, + top: `${t.d * (y + widgetHeight - margin - 3) + t.f}px`, width: `${(widgetWidth - margin * 2 - 3) * t.a}px`, - height: `${(this.parent.size[1] - (y + widgetHeight) - 3) * t.d}px`, + height: `${(this.parent.inputHeight - margin * 2 - 4) * t.d}px`, position: "absolute", zIndex: 1, fontSize: `${t.d * 10.0}px`, @@ -55,7 +59,8 @@ function addMultilineWidget(node, name, defaultVal, app) { }; widget.inputEl = document.createElement("textarea"); widget.inputEl.className = "comfy-multiline-input"; - widget.inputEl.value = defaultVal; + widget.inputEl.value = opts.defaultVal; + widget.inputEl.placeholder = opts.placeholder || ""; document.addEventListener("mousedown", function (event) { if (!widget.inputEl.contains(event.target)) { widget.inputEl.blur(); @@ -91,6 +96,68 @@ function addMultilineWidget(node, name, defaultVal, app) { } }; + if (!(MultilineSymbol in node)) { + node[MultilineSymbol] = true; + const onResize = node.onResize; + + node.onResize = function (size) { + if (node.widgets[0].last_y == null) return; + + let y = node.widgets[0].last_y; + let freeSpace = size[1] - y; + + // Compute the height of all non customtext widgets + let widgetHeight = 0; + const multi = []; + for (let i = 0; i < node.widgets.length; i++) { + const w = node.widgets[i]; + if (w.type === "customtext") { + multi.push(w); + } else { + if (w.computeSize) { + widgetHeight += w.computeSize()[1] + 4; + } else { + widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } + } + + // See how large each text input can be + freeSpace -= widgetHeight; + freeSpace /= multi.length; + + if (freeSpace < MIN_SIZE) { + // There isnt enough space for all the widgets, increase the size of the node + freeSpace = MIN_SIZE; + node.size[1] = y + widgetHeight + freeSpace * multi.length; + } + + // Position each of the widgets + for (const w of node.widgets) { + w.y = y; + if (w.type === "customtext") { + y += freeSpace; + } else if (w.computeSize) { + y += w.computeSize()[1] + 4; + } else { + y += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } + + this.inputHeight = freeSpace; + + // Call original resizer handler + if (onResize) { + onResize.apply(this, arguments); + } + }; + + requestAnimationFrame(() => { + node.onResize(node.size); + app.graph.setDirtyCanvas(true); + }); + } + return { minWidth: 400, minHeight: 200, widget }; } @@ -103,7 +170,7 @@ export const ComfyWidgets = { }, INT(node, inputName, inputData) { const { val, config } = getNumberDefaults(inputData, 1); - Object.assign(config, { precision: 0 }) + Object.assign(config, { precision: 0 }); return { widget: node.addWidget( "number", @@ -122,7 +189,7 @@ export const ComfyWidgets = { const multiline = !!inputData[1].multiline; if (multiline) { - return addMultilineWidget(node, inputName, defaultVal, app); + return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app); } else { return { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) }; } From c692509c2b6286248624dd6ec7ec37244a700a97 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 22 Mar 2023 02:33:27 -0400 Subject: [PATCH 09/16] Try to improve VAEEncode memory usage a bit. --- comfy/ldm/modules/diffusionmodules/model.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/comfy/ldm/modules/diffusionmodules/model.py b/comfy/ldm/modules/diffusionmodules/model.py index 129b86a7f..d36fd59c1 100644 --- a/comfy/ldm/modules/diffusionmodules/model.py +++ b/comfy/ldm/modules/diffusionmodules/model.py @@ -616,19 +616,17 @@ class Encoder(nn.Module): x = torch.nn.functional.pad(x, pad, mode="constant", value=0) already_padded = True # downsampling - hs = [self.conv_in(x)] + h = self.conv_in(x) for i_level in range(self.num_resolutions): for i_block in range(self.num_res_blocks): - h = self.down[i_level].block[i_block](hs[-1], temb) + h = self.down[i_level].block[i_block](h, temb) if len(self.down[i_level].attn) > 0: h = self.down[i_level].attn[i_block](h) - hs.append(h) if i_level != self.num_resolutions-1: - hs.append(self.down[i_level].downsample(hs[-1], already_padded)) + h = self.down[i_level].downsample(h, already_padded) already_padded = False # middle - h = hs[-1] h = self.mid.block_1(h, temb) h = self.mid.attn_1(h) h = self.mid.block_2(h, temb) From 4039616ca673f52d3efadcc6b36260807dc9ba06 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 22 Mar 2023 03:29:09 -0400 Subject: [PATCH 10/16] Less seams in tiled outputs at the cost of more processing. --- comfy/sd.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/comfy/sd.py b/comfy/sd.py index b8f829664..585419f7e 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -393,10 +393,16 @@ class VAE: pixel_samples = pixel_samples.cpu().movedim(1,-1) return pixel_samples - def decode_tiled(self, samples, tile_x=64, tile_y=64, overlap = 8): + def decode_tiled(self, samples, tile_x=64, tile_y=64, overlap = 16): model_management.unload_model() self.first_stage_model = self.first_stage_model.to(self.device) - output = utils.tiled_scale(samples, lambda a: torch.clamp((self.first_stage_model.decode(1. / self.scale_factor * a.to(self.device)) + 1.0) / 2.0, min=0.0, max=1.0), tile_x, tile_y, overlap, upscale_amount = 8) + decode_fn = lambda a: (self.first_stage_model.decode(1. / self.scale_factor * a.to(self.device)) + 1.0) + output = torch.clamp(( + (utils.tiled_scale(samples, decode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = 8) + + utils.tiled_scale(samples, decode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = 8) + + utils.tiled_scale(samples, decode_fn, tile_x, tile_y, overlap, upscale_amount = 8)) + / 3.0) / 2.0, min=0.0, max=1.0) + self.first_stage_model = self.first_stage_model.cpu() return output.movedim(1,-1) @@ -414,6 +420,9 @@ class VAE: self.first_stage_model = self.first_stage_model.to(self.device) pixel_samples = pixel_samples.movedim(-1,1).to(self.device) samples = utils.tiled_scale(pixel_samples, lambda a: self.first_stage_model.encode(2. * a - 1.).sample() * self.scale_factor, tile_x, tile_y, overlap, upscale_amount = (1/8), out_channels=4) + samples += utils.tiled_scale(pixel_samples, lambda a: self.first_stage_model.encode(2. * a - 1.).sample() * self.scale_factor, tile_x * 2, tile_y // 2, overlap, upscale_amount = (1/8), out_channels=4) + samples += utils.tiled_scale(pixel_samples, lambda a: self.first_stage_model.encode(2. * a - 1.).sample() * self.scale_factor, tile_x // 2, tile_y * 2, overlap, upscale_amount = (1/8), out_channels=4) + samples /= 3.0 self.first_stage_model = self.first_stage_model.cpu() samples = samples.cpu() return samples From f67c00622f1259598ce1720bbcb483fbe6e5de68 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 22 Mar 2023 03:48:26 -0400 Subject: [PATCH 11/16] Use inference_mode instead of no_grad. --- execution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/execution.py b/execution.py index 30eeb6304..757e0d9f9 100644 --- a/execution.py +++ b/execution.py @@ -143,7 +143,7 @@ class PromptExecutor: else: self.server.client_id = None - with torch.no_grad(): + with torch.inference_mode(): for x in prompt: recursive_output_delete_if_changed(prompt, self.old_prompt, self.outputs, x) From aae9fe0cf9fe3e430bfdac72acab1a5e092ff229 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 22 Mar 2023 12:22:48 -0400 Subject: [PATCH 12/16] Increase max res to 8192x8192 since 4096x4096 wasn't enough for some. --- nodes.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/nodes.py b/nodes.py index cb4d7723e..530f4cea9 100644 --- a/nodes.py +++ b/nodes.py @@ -31,6 +31,8 @@ def before_node_execution(): def interrupt_processing(value=True): model_management.interrupt_current_processing(value) +MAX_RESOLUTION=8192 + class CLIPTextEncode: @classmethod def INPUT_TYPES(s): @@ -59,10 +61,10 @@ class ConditioningSetArea: @classmethod def INPUT_TYPES(s): return {"required": {"conditioning": ("CONDITIONING", ), - "width": ("INT", {"default": 64, "min": 64, "max": 4096, "step": 64}), - "height": ("INT", {"default": 64, "min": 64, "max": 4096, "step": 64}), - "x": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 64}), - "y": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 64}), + "width": ("INT", {"default": 64, "min": 64, "max": MAX_RESOLUTION, "step": 64}), + "height": ("INT", {"default": 64, "min": 64, "max": MAX_RESOLUTION, "step": 64}), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 64}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 64}), "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), }} RETURN_TYPES = ("CONDITIONING",) @@ -412,8 +414,8 @@ class EmptyLatentImage: @classmethod def INPUT_TYPES(s): - return {"required": { "width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), - "height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), + return {"required": { "width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), + "height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), "batch_size": ("INT", {"default": 1, "min": 1, "max": 64})}} RETURN_TYPES = ("LATENT",) FUNCTION = "generate" @@ -433,8 +435,8 @@ class LatentUpscale: @classmethod def INPUT_TYPES(s): return {"required": { "samples": ("LATENT",), "upscale_method": (s.upscale_methods,), - "width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), - "height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), + "width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), + "height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), "crop": (s.crop_methods,)}} RETURN_TYPES = ("LATENT",) FUNCTION = "upscale" @@ -495,9 +497,9 @@ class LatentComposite: def INPUT_TYPES(s): return {"required": { "samples_to": ("LATENT",), "samples_from": ("LATENT",), - "x": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}), - "y": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}), - "feather": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "feather": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), }} RETURN_TYPES = ("LATENT",) FUNCTION = "composite" @@ -536,10 +538,10 @@ class LatentCrop: @classmethod def INPUT_TYPES(s): return {"required": { "samples": ("LATENT",), - "width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), - "height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), - "x": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}), - "y": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 8}), + "width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), + "height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), }} RETURN_TYPES = ("LATENT",) FUNCTION = "crop" @@ -876,8 +878,8 @@ class ImageScale: @classmethod def INPUT_TYPES(s): return {"required": { "image": ("IMAGE",), "upscale_method": (s.upscale_methods,), - "width": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 1}), - "height": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 1}), + "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), "crop": (s.crop_methods,)}} RETURN_TYPES = ("IMAGE",) FUNCTION = "upscale" From 2b94dee3da50912af83fc89fff7bb92caba34900 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 22 Mar 2023 18:43:43 +0000 Subject: [PATCH 13/16] Calculate sizes when drawing if required --- web/scripts/widgets.js | 99 +++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index 72984b4bf..f822e9faf 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -32,6 +32,53 @@ const MultilineSymbol = Symbol(); function addMultilineWidget(node, name, opts, app) { const MIN_SIZE = 50; + function computeSize(size) { + if (node.widgets[0].last_y == null) return; + + let y = node.widgets[0].last_y; + let freeSpace = size[1] - y; + + // Compute the height of all non customtext widgets + let widgetHeight = 0; + const multi = []; + for (let i = 0; i < node.widgets.length; i++) { + const w = node.widgets[i]; + if (w.type === "customtext") { + multi.push(w); + } else { + if (w.computeSize) { + widgetHeight += w.computeSize()[1] + 4; + } else { + widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } + } + + // See how large each text input can be + freeSpace -= widgetHeight; + freeSpace /= multi.length; + + if (freeSpace < MIN_SIZE) { + // There isnt enough space for all the widgets, increase the size of the node + freeSpace = MIN_SIZE; + node.size[1] = y + widgetHeight + freeSpace * multi.length; + } + + // Position each of the widgets + for (const w of node.widgets) { + w.y = y; + if (w.type === "customtext") { + y += freeSpace; + } else if (w.computeSize) { + y += w.computeSize()[1] + 4; + } else { + y += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } + + node.inputHeight = freeSpace; + } + const widget = { type: "customtext", name, @@ -42,6 +89,11 @@ function addMultilineWidget(node, name, opts, app) { this.inputEl.value = x; }, draw: function (ctx, _, widgetWidth, y, widgetHeight) { + if (!this.parent.inputHeight) { + // If we are initially offscreen when created we wont have received a resize event + // Calculate it here instead + computeSize(node.size); + } const visible = app.canvas.ds.scale > 0.5; const t = ctx.getTransform(); const margin = 10; @@ -101,50 +153,7 @@ function addMultilineWidget(node, name, opts, app) { const onResize = node.onResize; node.onResize = function (size) { - if (node.widgets[0].last_y == null) return; - - let y = node.widgets[0].last_y; - let freeSpace = size[1] - y; - - // Compute the height of all non customtext widgets - let widgetHeight = 0; - const multi = []; - for (let i = 0; i < node.widgets.length; i++) { - const w = node.widgets[i]; - if (w.type === "customtext") { - multi.push(w); - } else { - if (w.computeSize) { - widgetHeight += w.computeSize()[1] + 4; - } else { - widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } - } - - // See how large each text input can be - freeSpace -= widgetHeight; - freeSpace /= multi.length; - - if (freeSpace < MIN_SIZE) { - // There isnt enough space for all the widgets, increase the size of the node - freeSpace = MIN_SIZE; - node.size[1] = y + widgetHeight + freeSpace * multi.length; - } - - // Position each of the widgets - for (const w of node.widgets) { - w.y = y; - if (w.type === "customtext") { - y += freeSpace; - } else if (w.computeSize) { - y += w.computeSize()[1] + 4; - } else { - y += LiteGraph.NODE_WIDGET_HEIGHT + 4; - } - } - - this.inputHeight = freeSpace; + computeSize(size); // Call original resizer handler if (onResize) { @@ -153,7 +162,7 @@ function addMultilineWidget(node, name, opts, app) { }; requestAnimationFrame(() => { - node.onResize(node.size); + computeSize(node.size); app.graph.setDirtyCanvas(true); }); } From 3ed4a4e4e633c2f304f09506abdd5d0eeeb5b03b Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 22 Mar 2023 14:49:00 -0400 Subject: [PATCH 14/16] Try again with vae tiled decoding if regular fails because of OOM. --- comfy/ldm/modules/attention.py | 7 +---- comfy/ldm/modules/diffusionmodules/model.py | 7 +---- comfy/ldm/modules/sub_quadratic_attention.py | 7 ++--- comfy/model_management.py | 5 ++++ comfy/sd.py | 30 +++++++++++++------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/comfy/ldm/modules/attention.py b/comfy/ldm/modules/attention.py index e97badd04..23b047342 100644 --- a/comfy/ldm/modules/attention.py +++ b/comfy/ldm/modules/attention.py @@ -20,11 +20,6 @@ if model_management.xformers_enabled(): import os _ATTN_PRECISION = os.environ.get("ATTN_PRECISION", "fp32") -try: - OOM_EXCEPTION = torch.cuda.OutOfMemoryError -except: - OOM_EXCEPTION = Exception - def exists(val): return val is not None @@ -312,7 +307,7 @@ class CrossAttentionDoggettx(nn.Module): r1[:, i:end] = einsum('b i j, b j d -> b i d', s2, v) del s2 break - except OOM_EXCEPTION as e: + except model_management.OOM_EXCEPTION as e: if first_op_done == False: torch.cuda.empty_cache() torch.cuda.ipc_collect() diff --git a/comfy/ldm/modules/diffusionmodules/model.py b/comfy/ldm/modules/diffusionmodules/model.py index d36fd59c1..94f5510b9 100644 --- a/comfy/ldm/modules/diffusionmodules/model.py +++ b/comfy/ldm/modules/diffusionmodules/model.py @@ -13,11 +13,6 @@ if model_management.xformers_enabled(): import xformers import xformers.ops -try: - OOM_EXCEPTION = torch.cuda.OutOfMemoryError -except: - OOM_EXCEPTION = Exception - def get_timestep_embedding(timesteps, embedding_dim): """ This matches the implementation in Denoising Diffusion Probabilistic Models: @@ -221,7 +216,7 @@ class AttnBlock(nn.Module): r1[:, :, i:end] = torch.bmm(v, s2) del s2 break - except OOM_EXCEPTION as e: + except model_management.OOM_EXCEPTION as e: steps *= 2 if steps > 128: raise e diff --git a/comfy/ldm/modules/sub_quadratic_attention.py b/comfy/ldm/modules/sub_quadratic_attention.py index edbff74a2..f3c83f387 100644 --- a/comfy/ldm/modules/sub_quadratic_attention.py +++ b/comfy/ldm/modules/sub_quadratic_attention.py @@ -24,10 +24,7 @@ except ImportError: from torch import Tensor from typing import List -try: - OOM_EXCEPTION = torch.cuda.OutOfMemoryError -except: - OOM_EXCEPTION = Exception +import model_management def dynamic_slice( x: Tensor, @@ -161,7 +158,7 @@ def _get_attention_scores_no_kv_chunking( try: attn_probs = attn_scores.softmax(dim=-1) del attn_scores - except OOM_EXCEPTION: + except model_management.OOM_EXCEPTION: print("ran out of memory while running softmax in _get_attention_scores_no_kv_chunking, trying slower in place softmax instead") attn_scores -= attn_scores.max(dim=-1, keepdim=True).values torch.exp(attn_scores, out=attn_scores) diff --git a/comfy/model_management.py b/comfy/model_management.py index 5c4e97da3..809b19ea2 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -31,6 +31,11 @@ try: except: pass +try: + OOM_EXCEPTION = torch.cuda.OutOfMemoryError +except: + OOM_EXCEPTION = Exception + if "--disable-xformers" in sys.argv: XFORMERS_IS_AVAILBLE = False else: diff --git a/comfy/sd.py b/comfy/sd.py index 585419f7e..b344cbece 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -383,12 +383,26 @@ class VAE: device = model_management.get_torch_device() self.device = device - def decode(self, samples): + def decode_tiled_(self, samples, tile_x=64, tile_y=64, overlap = 16): + decode_fn = lambda a: (self.first_stage_model.decode(1. / self.scale_factor * a.to(self.device)) + 1.0) + output = torch.clamp(( + (utils.tiled_scale(samples, decode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = 8) + + utils.tiled_scale(samples, decode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = 8) + + utils.tiled_scale(samples, decode_fn, tile_x, tile_y, overlap, upscale_amount = 8)) + / 3.0) / 2.0, min=0.0, max=1.0) + return output + + def decode(self, samples_in): model_management.unload_model() self.first_stage_model = self.first_stage_model.to(self.device) - samples = samples.to(self.device) - pixel_samples = self.first_stage_model.decode(1. / self.scale_factor * samples) - pixel_samples = torch.clamp((pixel_samples + 1.0) / 2.0, min=0.0, max=1.0) + try: + samples = samples_in.to(self.device) + pixel_samples = self.first_stage_model.decode(1. / self.scale_factor * samples) + pixel_samples = torch.clamp((pixel_samples + 1.0) / 2.0, min=0.0, max=1.0) + except model_management.OOM_EXCEPTION as e: + print("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.") + pixel_samples = self.decode_tiled_(samples_in) + self.first_stage_model = self.first_stage_model.cpu() pixel_samples = pixel_samples.cpu().movedim(1,-1) return pixel_samples @@ -396,13 +410,7 @@ class VAE: def decode_tiled(self, samples, tile_x=64, tile_y=64, overlap = 16): model_management.unload_model() self.first_stage_model = self.first_stage_model.to(self.device) - decode_fn = lambda a: (self.first_stage_model.decode(1. / self.scale_factor * a.to(self.device)) + 1.0) - output = torch.clamp(( - (utils.tiled_scale(samples, decode_fn, tile_x // 2, tile_y * 2, overlap, upscale_amount = 8) + - utils.tiled_scale(samples, decode_fn, tile_x * 2, tile_y // 2, overlap, upscale_amount = 8) + - utils.tiled_scale(samples, decode_fn, tile_x, tile_y, overlap, upscale_amount = 8)) - / 3.0) / 2.0, min=0.0, max=1.0) - + output = self.decode_tiled_(samples, tile_x, tile_y, overlap) self.first_stage_model = self.first_stage_model.cpu() return output.movedim(1,-1) From 76f4d65d79639b342750fcc3c459b819eb05f68f Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 22 Mar 2023 18:50:45 +0000 Subject: [PATCH 15/16] Remove initial call as now unnecessary Set canvas to dirty if we grow the node --- web/scripts/widgets.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index f822e9faf..ee7b5d3d6 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -62,6 +62,7 @@ function addMultilineWidget(node, name, opts, app) { // There isnt enough space for all the widgets, increase the size of the node freeSpace = MIN_SIZE; node.size[1] = y + widgetHeight + freeSpace * multi.length; + node.graph.setDirtyCanvas(true); } // Position each of the widgets @@ -151,7 +152,7 @@ function addMultilineWidget(node, name, opts, app) { if (!(MultilineSymbol in node)) { node[MultilineSymbol] = true; const onResize = node.onResize; - + node.onResize = function (size) { computeSize(size); @@ -160,11 +161,6 @@ function addMultilineWidget(node, name, opts, app) { onResize.apply(this, arguments); } }; - - requestAnimationFrame(() => { - computeSize(node.size); - app.graph.setDirtyCanvas(true); - }); } return { minWidth: 400, minHeight: 200, widget }; From 4257e4ce51894e90e692aac56388e59795d80dbd Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 22 Mar 2023 18:52:24 +0000 Subject: [PATCH 16/16] tidy --- web/scripts/widgets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index ee7b5d3d6..30a02e72e 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -152,7 +152,7 @@ function addMultilineWidget(node, name, opts, app) { if (!(MultilineSymbol in node)) { node[MultilineSymbol] = true; const onResize = node.onResize; - + node.onResize = function (size) { computeSize(size);