From c33d26c283ea53b8ba3e42ef3dca1f03ddf4d7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Sepp=C3=A4nen?= <40791699+kijai@users.noreply.github.com> Date: Mon, 4 May 2026 20:20:40 +0300 Subject: [PATCH 1/5] fix: Proper memory estimation for frame interpolation when not using dynamic VRAM (#13698) --- comfy_extras/frame_interpolation_models/film_net.py | 3 +++ comfy_extras/frame_interpolation_models/ifnet.py | 3 +++ comfy_extras/nodes_frame_interpolation.py | 11 ++++------- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/comfy_extras/frame_interpolation_models/film_net.py b/comfy_extras/frame_interpolation_models/film_net.py index cf4f6e1e1..36bc79dc3 100644 --- a/comfy_extras/frame_interpolation_models/film_net.py +++ b/comfy_extras/frame_interpolation_models/film_net.py @@ -199,6 +199,9 @@ class FILMNet(nn.Module): def get_dtype(self): return self.extract.extract_sublevels.convs[0][0].conv.weight.dtype + def memory_used_forward(self, shape, dtype): + return 1700 * shape[1] * shape[2] * dtype.itemsize + def _build_warp_grids(self, H, W, device): """Pre-compute warp grids for all pyramid levels.""" if (H, W) in self._warp_grids: diff --git a/comfy_extras/frame_interpolation_models/ifnet.py b/comfy_extras/frame_interpolation_models/ifnet.py index 03cb34c50..ad6edbec9 100644 --- a/comfy_extras/frame_interpolation_models/ifnet.py +++ b/comfy_extras/frame_interpolation_models/ifnet.py @@ -74,6 +74,9 @@ class IFNet(nn.Module): def get_dtype(self): return self.encode.cnn0.weight.dtype + def memory_used_forward(self, shape, dtype): + return 300 * shape[1] * shape[2] * dtype.itemsize + def _build_warp_grids(self, H, W, device): if (H, W) in self._warp_grids: return diff --git a/comfy_extras/nodes_frame_interpolation.py b/comfy_extras/nodes_frame_interpolation.py index a3b00d36e..fa49c203a 100644 --- a/comfy_extras/nodes_frame_interpolation.py +++ b/comfy_extras/nodes_frame_interpolation.py @@ -37,7 +37,7 @@ class FrameInterpolationModelLoader(io.ComfyNode): model = cls._detect_and_load(sd) dtype = torch.float16 if model_management.should_use_fp16(model_management.get_torch_device()) else torch.float32 model.eval().to(dtype) - patcher = comfy.model_patcher.ModelPatcher( + patcher = comfy.model_patcher.CoreModelPatcher( model, load_device=model_management.get_torch_device(), offload_device=model_management.unet_offload_device(), @@ -98,16 +98,13 @@ class FrameInterpolate(io.ComfyNode): if num_frames < 2 or multiplier < 2: return io.NodeOutput(images) - model_management.load_model_gpu(interp_model) device = interp_model.load_device dtype = interp_model.model_dtype() inference_model = interp_model.model - - # Free VRAM for inference activations (model weights + ~20x a single frame's worth) - H, W = images.shape[1], images.shape[2] - activation_mem = H * W * 3 * images.element_size() * 20 - model_management.free_memory(activation_mem, device) + activation_mem = inference_model.memory_used_forward(images.shape, dtype) + model_management.load_models_gpu([interp_model], memory_required=activation_mem) align = getattr(inference_model, "pad_align", 1) + H, W = images.shape[1], images.shape[2] # Prepare a single padded frame on device for determining output dimensions def prepare_frame(idx): From c47633f3befbc32bf5aeece6a899c20d55a9feb1 Mon Sep 17 00:00:00 2001 From: rattus <46076784+rattus128@users.noreply.github.com> Date: Tue, 5 May 2026 05:56:05 +1000 Subject: [PATCH 2/5] prefetch: guard against no offload (#13703) cast_ will return no stream if there is no work to do. guard against this is the consume logic. --- comfy/model_prefetch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/comfy/model_prefetch.py b/comfy/model_prefetch.py index 0ad35deb5..72e11dec6 100644 --- a/comfy/model_prefetch.py +++ b/comfy/model_prefetch.py @@ -37,7 +37,8 @@ def prefetch_queue_pop(queue, device, module): consumed = queue.pop(0) if consumed is not None: offload_stream, prefetch_state = consumed - offload_stream.wait_stream(comfy.model_management.current_stream(device)) + if offload_stream is not None: + offload_stream.wait_stream(comfy.model_management.current_stream(device)) _, comfy_modules = prefetch_state if comfy_modules is not None: cleanup_prefetched_modules(comfy_modules) From 1ac78180b3f797f09f5805e5a923debe77638889 Mon Sep 17 00:00:00 2001 From: rattus <46076784+rattus128@users.noreply.github.com> Date: Tue, 5 May 2026 05:58:06 +1000 Subject: [PATCH 3/5] make control-net load order deterministic (#13701) Make this deterministic so speeds dont change base of load order. Load them in reverse order so whatever the caller lists first is the top priority. --- comfy/model_management.py | 10 ++++++---- comfy/sampler_helpers.py | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index 02ad66656..21738a4c7 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -721,13 +721,15 @@ def load_models_gpu(models, memory_required=0, force_patch_weights=False, minimu else: minimum_memory_required = max(inference_memory, minimum_memory_required + extra_reserved_memory()) - models_temp = set() + # Order-preserving dedup. A plain set() would randomize iteration order across runs + models_temp = {} for m in models: - models_temp.add(m) + models_temp[m] = None for mm in m.model_patches_models(): - models_temp.add(mm) + models_temp[mm] = None - models = models_temp + models = list(models_temp) + models.reverse() models_to_load = [] diff --git a/comfy/sampler_helpers.py b/comfy/sampler_helpers.py index bbba09e26..3782fd2d5 100644 --- a/comfy/sampler_helpers.py +++ b/comfy/sampler_helpers.py @@ -89,7 +89,8 @@ def get_additional_models(conds, dtype): gligen += get_models_from_cond(conds[k], "gligen") add_models += get_models_from_cond(conds[k], "additional_models") - control_nets = set(cnets) + # Order-preserving dedup. A plain set() would randomize iteration order across runs + control_nets = list(dict.fromkeys(cnets)) inference_memory = 0 control_models = [] From 1265955b34dd63622caecb71fdae550fe5cb44fb Mon Sep 17 00:00:00 2001 From: rattus <46076784+rattus128@users.noreply.github.com> Date: Tue, 5 May 2026 09:40:57 +1000 Subject: [PATCH 4/5] ops: handle multi-compute of the same weight (#13705) If the same weight is used multiple times within the same prefetch window, it should only apply compute state mutations once. Mark the weight as fully resident on the first pass accordingly. --- comfy/ops.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/comfy/ops.py b/comfy/ops.py index 4f0338346..585c185a3 100644 --- a/comfy/ops.py +++ b/comfy/ops.py @@ -253,6 +253,9 @@ def resolve_cast_module_with_vbar(s, dtype, device, bias_dtype, compute_dtype, w if bias is not None: bias = post_cast(s, "bias", bias, bias_dtype, prefetch["resident"], update_weight) + if prefetch["signature"] is not None: + prefetch["resident"] = True + return weight, bias From 15a4494a4e5299e1210ccad6d49d3253555ef3e6 Mon Sep 17 00:00:00 2001 From: Alexis Rolland Date: Tue, 5 May 2026 08:37:25 +0800 Subject: [PATCH 5/5] chore: Update display names and categories (CORE-151) (#13693) * Standardize DEPRECATED label in node display name * Promote category image/video to root level video/ * Update images and masks names and categories --- comfy_api_nodes/nodes_sora.py | 2 +- comfy_extras/nodes_frame_interpolation.py | 2 +- comfy_extras/nodes_image_compare.py | 2 +- comfy_extras/nodes_images.py | 17 ++++++++++----- comfy_extras/nodes_mask.py | 9 ++++++-- comfy_extras/nodes_morphology.py | 6 ++++-- comfy_extras/nodes_post_processing.py | 6 ++++-- comfy_extras/nodes_video.py | 13 ++++++------ nodes.py | 25 ++++++++++++----------- 9 files changed, 50 insertions(+), 32 deletions(-) diff --git a/comfy_api_nodes/nodes_sora.py b/comfy_api_nodes/nodes_sora.py index 4d9075dcf..c1d485188 100644 --- a/comfy_api_nodes/nodes_sora.py +++ b/comfy_api_nodes/nodes_sora.py @@ -33,7 +33,7 @@ class OpenAIVideoSora2(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="OpenAIVideoSora2", - display_name="OpenAI Sora - Video (Deprecated)", + display_name="OpenAI Sora - Video (DEPRECATED)", category="api node/video/Sora", description=( "OpenAI video and audio generation.\n\n" diff --git a/comfy_extras/nodes_frame_interpolation.py b/comfy_extras/nodes_frame_interpolation.py index fa49c203a..9dd34cfb8 100644 --- a/comfy_extras/nodes_frame_interpolation.py +++ b/comfy_extras/nodes_frame_interpolation.py @@ -78,7 +78,7 @@ class FrameInterpolate(io.ComfyNode): return io.Schema( node_id="FrameInterpolate", display_name="Frame Interpolate", - category="image/video", + category="video", search_aliases=["rife", "film", "frame interpolation", "slow motion", "interpolate frames", "vfi"], inputs=[ FrameInterpolationModel.Input("interp_model"), diff --git a/comfy_extras/nodes_image_compare.py b/comfy_extras/nodes_image_compare.py index 3d943be67..58af9ae82 100644 --- a/comfy_extras/nodes_image_compare.py +++ b/comfy_extras/nodes_image_compare.py @@ -11,7 +11,7 @@ class ImageCompare(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="ImageCompare", - display_name="Image Compare", + display_name="Compare Images", description="Compares two images side by side with a slider.", category="image", essentials_category="Image Tools", diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index a77f0641f..68916ab75 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -24,7 +24,7 @@ class ImageCrop(IO.ComfyNode): return IO.Schema( node_id="ImageCrop", search_aliases=["trim"], - display_name="Image Crop (Deprecated)", + display_name="Crop Image (DEPRECATED)", category="image/transform", is_deprecated=True, essentials_category="Image Tools", @@ -56,7 +56,7 @@ class ImageCropV2(IO.ComfyNode): return IO.Schema( node_id="ImageCropV2", search_aliases=["trim"], - display_name="Image Crop", + display_name="Crop Image", category="image/transform", essentials_category="Image Tools", has_intermediate_output=True, @@ -109,6 +109,7 @@ class RepeatImageBatch(IO.ComfyNode): return IO.Schema( node_id="RepeatImageBatch", search_aliases=["duplicate image", "clone image"], + display_name="Repeat Image Batch", category="image/batch", inputs=[ IO.Image.Input("image"), @@ -131,6 +132,7 @@ class ImageFromBatch(IO.ComfyNode): return IO.Schema( node_id="ImageFromBatch", search_aliases=["select image", "pick from batch", "extract image"], + display_name="Get Image from Batch", category="image/batch", inputs=[ IO.Image.Input("image"), @@ -157,7 +159,8 @@ class ImageAddNoise(IO.ComfyNode): return IO.Schema( node_id="ImageAddNoise", search_aliases=["film grain"], - category="image", + display_name="Add Noise to Image", + category="image/postprocessing", inputs=[ IO.Image.Input("image"), IO.Int.Input( @@ -259,7 +262,7 @@ class ImageStitch(IO.ComfyNode): return IO.Schema( node_id="ImageStitch", search_aliases=["combine images", "join images", "concatenate images", "side by side"], - display_name="Image Stitch", + display_name="Stitch Images", description="Stitches image2 to image1 in the specified direction.\n" "If image2 is not provided, returns image1 unchanged.\n" "Optional spacing can be added between images.", @@ -434,6 +437,7 @@ class ResizeAndPadImage(IO.ComfyNode): return IO.Schema( node_id="ResizeAndPadImage", search_aliases=["fit to size"], + display_name="Resize And Pad Image", category="image/transform", inputs=[ IO.Image.Input("image"), @@ -485,6 +489,7 @@ class SaveSVGNode(IO.ComfyNode): return IO.Schema( node_id="SaveSVGNode", search_aliases=["export vector", "save vector graphics"], + display_name="Save SVG", description="Save SVG files on disk.", category="image/save", inputs=[ @@ -591,7 +596,7 @@ class ImageRotate(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="ImageRotate", - display_name="Image Rotate", + display_name="Rotate Image", search_aliases=["turn", "flip orientation"], category="image/transform", essentials_category="Image Tools", @@ -624,6 +629,7 @@ class ImageFlip(IO.ComfyNode): return IO.Schema( node_id="ImageFlip", search_aliases=["mirror", "reflect"], + display_name="Flip Image", category="image/transform", inputs=[ IO.Image.Input("image"), @@ -650,6 +656,7 @@ class ImageScaleToMaxDimension(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="ImageScaleToMaxDimension", + display_name="Scale Image to Max Dimension", category="image/upscaling", inputs=[ IO.Image.Input("image"), diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index 8ca947718..43a933dac 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -80,7 +80,8 @@ class ImageCompositeMasked(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="ImageCompositeMasked", - search_aliases=["paste image", "overlay", "layer"], + search_aliases=["overlay", "layer", "paste image", "images composition"], + display_name="Image Composite Masked", category="image", inputs=[ IO.Image.Input("destination"), @@ -201,6 +202,7 @@ class InvertMask(IO.ComfyNode): return IO.Schema( node_id="InvertMask", search_aliases=["reverse mask", "flip mask"], + display_name="Invert Mask", category="mask", inputs=[ IO.Mask.Input("mask"), @@ -222,6 +224,7 @@ class CropMask(IO.ComfyNode): return IO.Schema( node_id="CropMask", search_aliases=["cut mask", "extract mask region", "mask slice"], + display_name="Crop Mask", category="mask", inputs=[ IO.Mask.Input("mask"), @@ -247,7 +250,8 @@ class MaskComposite(IO.ComfyNode): def define_schema(cls): return IO.Schema( node_id="MaskComposite", - search_aliases=["combine masks", "blend masks", "layer masks"], + search_aliases=["combine masks", "blend masks", "layer masks", "masks composition"], + display_name="Combine Masks", category="mask", inputs=[ IO.Mask.Input("destination"), @@ -298,6 +302,7 @@ class FeatherMask(IO.ComfyNode): return IO.Schema( node_id="FeatherMask", search_aliases=["soft edge mask", "blur mask edges", "gradient mask edge"], + display_name="Feather Mask", category="mask", inputs=[ IO.Mask.Input("mask"), diff --git a/comfy_extras/nodes_morphology.py b/comfy_extras/nodes_morphology.py index 4ab2fb7e8..c01b9436d 100644 --- a/comfy_extras/nodes_morphology.py +++ b/comfy_extras/nodes_morphology.py @@ -59,7 +59,8 @@ class ImageRGBToYUV(io.ComfyNode): return io.Schema( node_id="ImageRGBToYUV", search_aliases=["color space conversion"], - category="image/batch", + display_name="Image RGB to YUV", + category="image/color", inputs=[ io.Image.Input("image"), ], @@ -81,7 +82,8 @@ class ImageYUVToRGB(io.ComfyNode): return io.Schema( node_id="ImageYUVToRGB", search_aliases=["color space conversion"], - category="image/batch", + display_name="Image YUV to RGB", + category="image/color", inputs=[ io.Image.Input("Y"), io.Image.Input("U"), diff --git a/comfy_extras/nodes_post_processing.py b/comfy_extras/nodes_post_processing.py index 345fdb695..d938a2035 100644 --- a/comfy_extras/nodes_post_processing.py +++ b/comfy_extras/nodes_post_processing.py @@ -20,7 +20,8 @@ class Blend(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="ImageBlend", - display_name="Image Blend", + search_aliases=["mix images"], + display_name="Blend Images", category="image/postprocessing", essentials_category="Image Tools", inputs=[ @@ -224,6 +225,7 @@ class ImageScaleToTotalPixels(io.ComfyNode): def define_schema(cls): return io.Schema( node_id="ImageScaleToTotalPixels", + display_name="Scale Image to Total Pixels", category="image/upscaling", inputs=[ io.Image.Input("image"), @@ -568,7 +570,7 @@ class BatchImagesNode(io.ComfyNode): return io.Schema( node_id="BatchImagesNode", display_name="Batch Images", - category="image", + category="image/batch", essentials_category="Image Tools", search_aliases=["batch", "image batch", "batch images", "combine images", "merge images", "stack images"], inputs=[ diff --git a/comfy_extras/nodes_video.py b/comfy_extras/nodes_video.py index 5c096c232..719acf2f1 100644 --- a/comfy_extras/nodes_video.py +++ b/comfy_extras/nodes_video.py @@ -17,7 +17,8 @@ class SaveWEBM(io.ComfyNode): return io.Schema( node_id="SaveWEBM", search_aliases=["export webm"], - category="image/video", + display_name="Save WEBM", + category="video", is_experimental=True, inputs=[ io.Image.Input("images"), @@ -72,7 +73,7 @@ class SaveVideo(io.ComfyNode): node_id="SaveVideo", search_aliases=["export video"], display_name="Save Video", - category="image/video", + category="video", essentials_category="Basics", description="Saves the input images to your ComfyUI output directory.", inputs=[ @@ -121,7 +122,7 @@ class CreateVideo(io.ComfyNode): node_id="CreateVideo", search_aliases=["images to video"], display_name="Create Video", - category="image/video", + category="video", description="Create a video from images.", inputs=[ io.Image.Input("images", tooltip="The images to create a video from."), @@ -146,7 +147,7 @@ class GetVideoComponents(io.ComfyNode): node_id="GetVideoComponents", search_aliases=["extract frames", "split video", "video to images", "demux"], display_name="Get Video Components", - category="image/video", + category="video", description="Extracts all components from a video: frames, audio, and framerate.", inputs=[ io.Video.Input("video", tooltip="The video to extract components from."), @@ -174,7 +175,7 @@ class LoadVideo(io.ComfyNode): node_id="LoadVideo", search_aliases=["import video", "open video", "video file"], display_name="Load Video", - category="image/video", + category="video", essentials_category="Basics", inputs=[ io.Combo.Input("file", options=sorted(files), upload=io.UploadType.video), @@ -216,7 +217,7 @@ class VideoSlice(io.ComfyNode): "frame load cap", "start time", ], - category="image/video", + category="video", essentials_category="Video Tools", inputs=[ io.Video.Input("video"), diff --git a/nodes.py b/nodes.py index 8f8f90cf6..eebc2fa76 100644 --- a/nodes.py +++ b/nodes.py @@ -1887,7 +1887,7 @@ class ImageInvert: RETURN_TYPES = ("IMAGE",) FUNCTION = "invert" - CATEGORY = "image" + CATEGORY = "image/color" def invert(self, image): s = 1.0 - image @@ -1903,7 +1903,7 @@ class ImageBatch: RETURN_TYPES = ("IMAGE",) FUNCTION = "batch" - CATEGORY = "image" + CATEGORY = "image/batch" DEPRECATED = True def batch(self, image1, image2): @@ -1960,7 +1960,7 @@ class ImagePadForOutpaint: RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "expand_image" - CATEGORY = "image" + CATEGORY = "image/transform" def expand_image(self, image, left, top, right, bottom, feathering): d1, d2, d3, d4 = image.size() @@ -2103,7 +2103,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ConditioningSetArea": "Conditioning (Set Area)", "ConditioningSetAreaPercentage": "Conditioning (Set Area with Percentage)", "ConditioningSetMask": "Conditioning (Set Mask)", - "ControlNetApply": "Apply ControlNet (OLD)", + "ControlNetApply": "Apply ControlNet (DEPRECATED)", "ControlNetApplyAdvanced": "Apply ControlNet", # Latent "VAEEncodeForInpaint": "VAE Encode (for Inpainting)", @@ -2121,6 +2121,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "LatentFromBatch" : "Latent From Batch", "RepeatLatentBatch": "Repeat Latent Batch", # Image + "EmptyImage": "Empty Image", "SaveImage": "Save Image", "PreviewImage": "Preview Image", "LoadImage": "Load Image", @@ -2128,15 +2129,15 @@ NODE_DISPLAY_NAME_MAPPINGS = { "LoadImageOutput": "Load Image (from Outputs)", "ImageScale": "Upscale Image", "ImageScaleBy": "Upscale Image By", - "ImageInvert": "Invert Image", + "ImageInvert": "Invert Image Colors", "ImagePadForOutpaint": "Pad Image for Outpainting", - "ImageBatch": "Batch Images", - "ImageCrop": "Image Crop", - "ImageStitch": "Image Stitch", - "ImageBlend": "Image Blend", - "ImageBlur": "Image Blur", - "ImageQuantize": "Image Quantize", - "ImageSharpen": "Image Sharpen", + "ImageBatch": "Batch Images (DEPRECATED)", + "ImageCrop": "Crop Image", + "ImageStitch": "Stitch Images", + "ImageBlend": "Blend Images", + "ImageBlur": "Blur Image", + "ImageQuantize": "Quantize Image", + "ImageSharpen": "Sharpen Image", "ImageScaleToTotalPixels": "Scale Image to Total Pixels", "GetImageSize": "Get Image Size", # _for_testing