Length one processing

Internally, the Comfy server represents data flowing from one node to the next as a Python list, normally length 1, of the relevant datatype. In normal operation, when a node returns an output, each element in the output tuple is separately wrapped in a list (length 1); then when the next node is called, the data is unwrapped and passed to the main function.

You generally don’t need to worry about this, since Comfy does the wrapping and unwrapping.
This isn’t about batches. A batch (of, for instance, latents, or images) is a single entry in the list (see tensor datatypes)

List processing

In some circumstance, multiple data instances are processed in a single workflow, in which case the internal data will be a list containing the data instances. An example of this might be processing a series of images one at a time to avoid running out of VRAM, or handling images of different sizes.

By default, Comfy will process the values in the list sequentially:

  • if the inputs are lists of different lengths, the shorter ones are padded by repeating the last value
  • the main method is called once for each value in the input lists
  • the outputs are lists, each of which is the same length as the longest input

The relevant code can be found in the method map_node_over_list in execution.py.

However, as Comfy wraps node outputs into a list of length one, if the tuple returned by a custom node contains a list, that list will be wrapped, and treated as a single piece of data. In order to tell Comfy that the list being returned should not be wrapped, but treated as a series of data for sequential processing, the node should provide a class attribute OUTPUT_IS_LIST, which is a tuple[bool], of the same length as RETURN_TYPES, specifying which outputs which should be so treated.

A node can also override the default input behaviour and receive the whole list in a single call. This is done by setting a class attribute INPUT_IS_LIST to True.

Here’s a (lightly annotated) example from the built in nodes - ImageRebatch takes one or more batches of images (received as a list, because INPUT_IS_LIST - True) and rebatches them into batches of the requested size.

INPUT_IS_LIST is node level - all inputs get the same treatment. So the value of the batch_size widget is given by batch_size[0].

class ImageRebatch:
    @classmethod
    def INPUT_TYPES(s):
        return {"required": { "images": ("IMAGE",),
                              "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}) }}
    RETURN_TYPES = ("IMAGE",)
    INPUT_IS_LIST = True
    OUTPUT_IS_LIST = (True, )
    FUNCTION = "rebatch"
    CATEGORY = "image/batch"

    def rebatch(self, images, batch_size):
        batch_size = batch_size[0]    # everything comes as a list, so batch_size is list[int]

        output_list = []
        all_images = []
        for img in images:                    # each img is a batch of images
            for i in range(img.shape[0]):     # each i is a single image
                all_images.append(img[i:i+1])

        for i in range(0, len(all_images), batch_size): # take batch_size chunks and turn each into a new batch
            output_list.append(torch.cat(all_images[i:i+batch_size], dim=0))  # will die horribly if the image batches had different width or height!

        return (output_list,)

INPUT_IS_LIST