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 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 09/18] 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 10/18] 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 3c25f5dc0f52788d41b9a44a9e03b845df06b3e8 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 22 Mar 2023 17:32:01 +0000 Subject: [PATCH 11/18] Add content disposition header so images have name --- server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server.py b/server.py index 6615a39e4..73429acca 100644 --- a/server.py +++ b/server.py @@ -129,12 +129,12 @@ class PromptServer(): return web.Response(status=403) output_dir = full_output_dir - file = request.rel_url.query["filename"] - file = os.path.basename(file) - file = os.path.join(output_dir, file) + filename = request.rel_url.query["filename"] + filename = os.path.basename(filename) + file = os.path.join(output_dir, filename) if os.path.isfile(file): - return web.FileResponse(file) + return web.FileResponse(file, headers={"Content-Disposition": f"filename=\"{filename}\""}) return web.Response(status=404) 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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); From cc127eeabd5cdd42f1a472e43938c6a202e89626 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 23 Mar 2023 00:40:48 -0400 Subject: [PATCH 16/18] Lower PNG compression for more speed. After some quick tests compress_level=4 seems to give the best compression performance ratio for stable diffusion images. It's 10x faster than max compression for an image size increase of only 2.5%. --- nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index 530f4cea9..0b8be7659 100644 --- a/nodes.py +++ b/nodes.py @@ -779,7 +779,7 @@ class SaveImage: metadata.add_text(x, json.dumps(extra_pnginfo[x])) file = f"{filename}_{counter:05}_.png" - img.save(os.path.join(full_output_folder, file), pnginfo=metadata, optimize=True) + img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) results.append({ "filename": file, "subfolder": subfolder, From 94a7c895f41944d60fc3f99355064fac8347b006 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 23 Mar 2023 03:40:12 -0400 Subject: [PATCH 17/18] Add loha support. --- README.md | 2 +- comfy/sd.py | 52 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d83174e3c..4ff031331 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This ui will let you design and execute advanced stable diffusion pipelines usin - Works even if you don't have a GPU with: ```--cpu``` (slow) - Can load both ckpt and safetensors models/checkpoints. Standalone VAEs and CLIP models. - Embeddings/Textual inversion -- [Loras (regular and locon)](https://comfyanonymous.github.io/ComfyUI_examples/lora/) +- [Loras (regular, locon and loha)](https://comfyanonymous.github.io/ComfyUI_examples/lora/) - Loading full workflows (with seeds) from generated PNG files. - Saving/Loading workflows as Json files. - Nodes interface can be used to create complex workflows like one for [Hires fix](https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/) or much more advanced ones. diff --git a/comfy/sd.py b/comfy/sd.py index b344cbece..714fa66ba 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -126,15 +126,17 @@ def load_lora(path, to_load): patch_dict = {} loaded_keys = set() for x in to_load: + alpha_name = "{}.alpha".format(x) + alpha = None + if alpha_name in lora.keys(): + alpha = lora[alpha_name].item() + loaded_keys.add(alpha_name) + 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) mid = None if mid_name in lora.keys(): mid = lora[mid_name] @@ -142,6 +144,18 @@ def load_lora(path, to_load): patch_dict[to_load[x]] = (lora[A_name], lora[B_name], alpha, mid) loaded_keys.add(A_name) loaded_keys.add(B_name) + + hada_w1_a_name = "{}.hada_w1_a".format(x) + hada_w1_b_name = "{}.hada_w1_b".format(x) + hada_w2_a_name = "{}.hada_w2_a".format(x) + hada_w2_b_name = "{}.hada_w2_b".format(x) + if hada_w1_a_name in lora.keys(): + patch_dict[to_load[x]] = (lora[hada_w1_a_name], lora[hada_w1_b_name], alpha, lora[hada_w2_a_name], lora[hada_w2_b_name]) + loaded_keys.add(hada_w1_a_name) + loaded_keys.add(hada_w1_b_name) + loaded_keys.add(hada_w2_a_name) + loaded_keys.add(hada_w2_b_name) + for x in lora.keys(): if x not in loaded_keys: print("lora key not loaded", x) @@ -280,15 +294,25 @@ class ModelPatcher: self.backup[key] = weight.clone() alpha = p[0] - mat1 = v[0] - 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) + + if len(v) == 4: #lora/locon + mat1 = v[0] + 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) + else: #loha + w1a = v[0] + w1b = v[1] + if v[2] is not None: + alpha *= v[2] / w1b.shape[0] + w2a = v[3] + w2b = v[4] + weight += (alpha * torch.mm(w1a.float(), w1b.float()) * torch.mm(w2a.float(), w2b.float())).reshape(weight.shape).type(weight.dtype).to(weight.device) return self.model def unpatch_model(self): model_sd = self.model.state_dict() From dd095efc2c172000a840c7ca54c8a01458f6b2a3 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 23 Mar 2023 04:32:25 -0400 Subject: [PATCH 18/18] Support loha that use cp decomposition. --- comfy/sd.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/comfy/sd.py b/comfy/sd.py index 714fa66ba..d767d8671 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -149,8 +149,18 @@ def load_lora(path, to_load): hada_w1_b_name = "{}.hada_w1_b".format(x) hada_w2_a_name = "{}.hada_w2_a".format(x) hada_w2_b_name = "{}.hada_w2_b".format(x) + hada_t1_name = "{}.hada_t1".format(x) + hada_t2_name = "{}.hada_t2".format(x) if hada_w1_a_name in lora.keys(): - patch_dict[to_load[x]] = (lora[hada_w1_a_name], lora[hada_w1_b_name], alpha, lora[hada_w2_a_name], lora[hada_w2_b_name]) + hada_t1 = None + hada_t2 = None + if hada_t1_name in lora.keys(): + hada_t1 = lora[hada_t1_name] + hada_t2 = lora[hada_t2_name] + loaded_keys.add(hada_t1_name) + loaded_keys.add(hada_t2_name) + + patch_dict[to_load[x]] = (lora[hada_w1_a_name], lora[hada_w1_b_name], alpha, lora[hada_w2_a_name], lora[hada_w2_b_name], hada_t1, hada_t2) loaded_keys.add(hada_w1_a_name) loaded_keys.add(hada_w1_b_name) loaded_keys.add(hada_w2_a_name) @@ -312,7 +322,16 @@ class ModelPatcher: alpha *= v[2] / w1b.shape[0] w2a = v[3] w2b = v[4] - weight += (alpha * torch.mm(w1a.float(), w1b.float()) * torch.mm(w2a.float(), w2b.float())).reshape(weight.shape).type(weight.dtype).to(weight.device) + if v[5] is not None: #cp decomposition + t1 = v[5] + t2 = v[6] + m1 = torch.einsum('i j k l, j r, i p -> p r k l', t1.float(), w1b.float(), w1a.float()) + m2 = torch.einsum('i j k l, j r, i p -> p r k l', t2.float(), w2b.float(), w2a.float()) + else: + m1 = torch.mm(w1a.float(), w1b.float()) + m2 = torch.mm(w2a.float(), w2b.float()) + + weight += (alpha * m1 * m2).reshape(weight.shape).type(weight.dtype).to(weight.device) return self.model def unpatch_model(self): model_sd = self.model.state_dict()