Comfy-Org / Comfy-Org/ComfyUI

Treat a group of nodes as a sequential unit

Open
#11,131 4 comments 4 reactions 0 assignees View on GitHub
Feature
Dominant language
Python
Stars
133k
Forks
15.7k
Avg merge
1d 7h
Merged PRs (30d)
158

Description

**Update: This issue has been escalated to a [RFC](https://github.com/Comfy-Org/rfcs/discussions/43)**

### Feature Idea

tl;dr: ComfyUI provides data lists which enable sequential processing for advanced workflows but the problem is that each node is processed multiple times before passing on execution to the next node. ComfyUI should pass on execution immediately instead or provide a feature which allows control over the execution mode.

**Add an option which allows a group of nodes to be treated as a sequential unit.**

I recently began to understand [data lists](https://docs.comfy.org/custom-nodes/backend/lists) and sequential processing in ComfyUI:

https://github.com/user-attachments/assets/6bb38b4c-f00e-4636-8d23-277be547de18

In this example, the node `String OutputList` is marked as `OUTPUT_IS_LIST=True` and tells `KSampler` to fetch and process the items sequentially. I have used this feature to develop a few custom nodes ([outputlists-combiner](https://github.com/geroldmeisinger/ComfyUI-outputlists-combiner)) which help to build XYZ-gridplots, like the following workflow:

Image

But with huge grids you will notice that the `KSampler` processes ALL combinations before the next nodes are executed (`VAE Decode > Save Image`) which can take a very long time and you could loose all progress if something happens. However, you want to see the intermediate results immediately. Thus we would need some way to tell `KSampler` to pass on execution to `Save Image` on each item.

### Existing Solutions

**a) custom node expansion**

The only solution so far I've found is to copy the node pattern `KSampler > VAE Decode > Save Image` as a custom node with node expansion in code (see `KSampler immediate Save Image`):

Image

```
class KSamplerImmediateSave:
DESCRIPTION="""Node Expansion of default KSampler, VAE Decode and Save Image to process as one.
This is useful if you want to save the intermediate images for grids immediately.
'A custom KSampler just to save an image? Now I have become the very thing I sought to destroy!'
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
# from ComfyUI/nodes.py KSampler
"model" : ("MODEL" , { "tooltip" : "The model used for denoising the input latent." } ) ,
"positive" : ("CONDITIONING" , { "tooltip" : "The conditioning describing the attributes you want to include in the image." } ) ,
"negative" : ("CONDITIONING" , { "tooltip" : "The conditioning describing the attributes you want to exclude from the image." } ) ,
"latent_image" : ("LATENT" , { "tooltip" : "The latent image to denoise." } ) ,
"vae" : ("VAE" , { "tooltip" : "The VAE model used for decoding the latent." } ) ,

"seed" : ("INT" , {"default" : 0 , "min" : 0 , "max" : 0xfffffffffffffff , "control_after_generate" : True, "tooltip" : "The random seed used for creating the noise." } ) ,
"steps" : ("INT" , {"default" : 20 , "min" : 1 , "max" : 10000 , "tooltip" : "The number of steps used in the denoising process." } ) ,
"cfg" : ("FLOAT" , {"default" : 8.0 , "min" : 0.0 , "max" : 100.0 , "step" : 0.1, "round" : 0.01, "tooltip" : "The Classifier-Free Guidance scale balances creativity and adherence to the prompt. Higher values result in images more closely matching the prompt however too high values will negatively impact quality." } ) ,
"sampler_name" : (comfy.samplers.KSampler.SAMPLERS , {"tooltip" : "The algorithm used when sampling , this can affect the quality , speed , and style of the generated output." } ) ,
"scheduler" : (comfy.samplers.KSampler.SCHEDULERS , {"tooltip" : "The scheduler controls how noise is gradually removed to form the image." } ) ,
"denoise" : ("FLOAT" , {"default" : 1.0 , "min" : 0.0 , "max" : 1.0 , "step" : 0.01, "tooltip" : "The amount of denoising applied , lower values will maintain the structure of the initial image allowing for image to image sampling." } ) ,

# from ComfyUI/nodes.py SaveImage
"filename_prefix" : ("STRING", {"default" : "ComfyUI", "tooltip" : "The prefix for the file to save. This may include formatting information such as %date :yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."}),
},
# "hidden": {
# "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO",
# },
}

RETURN_NAMES = ("image", )
RETURN_TYPES = ("IMAGE", )
OUTPUT_TOOLTIPS = ("The decoded image.",) # from ComfyUI/nodes.py VAEDecode
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "_for_testing"

def execute(self, model, positive, negative, latent_image, vae, seed, steps, cfg, sampler_name, scheduler, denoise, filename_prefix):
graph = GraphBuilder()
latent = graph.node("KSampler" , model=model, positive=positive, negative=negative, latent_image=latent_image, seed=seed, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, denoise=denoise)
images = graph.node("VAEDecode", samples=latent.out(0), vae=vae)
save = graph.node("SaveImage", images=images.out(0), filename_prefix=filename_prefix)
return {
"result" : (images.out(0),),
"expand" : graph.finalize(),
}
```

While this works with the default pattern, it would require to reimplement each variation as a new custom node, like two-sampler workflows `KSampler > KSampler > VAE Decode > Save Image` or any variation with different output nodes.

**b) ad-hoc node expansion**

Another solution could be to create an ad-hoc node expansion based on the connected nodes (the following is just an draft, I didn't implement this for real):

Image

`SequentialUnitStart` is located between the OutputList and the `KSampler` and just passes the item through. `SequentialUnitEnd` is located after the output node `SaveImage` and passes the value trough downstream. When the prompt is executed, look up the connected starting node (`KSampler`), the connected end node (`SaveImage`) and find the linked graph and nodes in-between, copy all the nodes as a node expansion, set the original nodes as "muted", and execute the original behaviour on the node expansion.

Cons:
1. this is very tedious to setup as a user.
2. it is not clear were the starting node should be (right after the `OUTPUT_IS_LIST=True` (here: `String OutputList`), or is it okay if it's further downstream (here: `TextEncode`). what if there are multiple output lists connected to the `KSampler`?
3. `SaveImage` only has an input knob, so `SequentialUnitEnd` needs to connect to the input, but then no else can connect to it. If `SequentialUnitEnd` is connected before `SaveImage` then intuitively it looks as if `SaveImage` is excluded from the sequential unit. If it should look as if it's inside the unit, it must provide an output knob, which `SaveImage` does not (hence the custom `SaveImage2` in this example).
4. we could forego linking completely and just work with node ids. but this is even more tedious, error prone and makes no sense in ComfyUI.

### Other

Here are some ideas how this could be implemented:

**0) always emit immediately**

The question also arises as to why immediate sequential processing is not the default for all nodes.

**a) Subgraphs**

Add an option for subgraph "treat as sequential unit" which would work like the node expansion above and immediately passes on execution to the next nodes when a datalist item is fetched from outside.

Image

Pro: It already covers the concept of groups
Con: Requires hierachical treatment of data lists, otherwise subgraphs would work differently if they are executed as a standalone workflow. Let's say there is output list outside of the subgraph, then the whole subgraph executes as whole on this item. If there is another output list within the subgraph, the behaviour should work like data lists work now (but what if different hierarchical layers are intertwined?).

**b) Group nodes**

Image

Pro: same as subgraph
Con: deprecated

**c) Add Group (the background boxes)**

When the box is set as "sequential unit" all nodes within the box execute immediately.

Image

Pro: easy to mark
Con: could lead to ambiguous situations when only half of a graph flow is marked

**d) onExecuted/onTrigger**

I don't know what the original intention for this feature was but it could be used to the tell `KSampler`: "after you have executed, trigger this `SaveImage` node immediately.. which then looks upstream until it finds the `KSampler` and fetches the item downstream.

Image

it's not implemented yet:
`Error: SaveImage.save_images() got an unexpected keyword argument 'onTrigger'`

comfyanonymous/ComfyUI#704
comfyanonymous/ComfyUI#1135
comfyanonymous/ComfyUI#7443 (I tried it here: https://github.com/comfyanonymous/ComfyUI/pull/7443#issuecomment-3592346229)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.