-
Notifications
You must be signed in to change notification settings - Fork 6.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Dynamic Typing #5293
base: master
Are you sure you want to change the base?
RFC: Dynamic Typing #5293
Conversation
This is a proof of concept to get feedback. Note that it requires the frontend branch of the same name.
The design is delightfully thorough, but Goal 5 does leave me conflicted. Some current musings:
|
Can the backend obtain what type Dynamic Typing has become? |
@AustinMroz Yeah, for people working on local setups (or those who have low latency to the instance running in the cloud), the delay shouldn't be noticeable at all. Whatever delay they do see is likely thanks to a 500ms debounce I have that can probably be lowered significantly (or entirely removed once a workflow is fully loaded): https://github.com/Comfy-Org/ComfyUI_frontend/pull/1271/files#diff-4755cb4339a71140d692c520a82ab9afd9038b2b8223a0b2c19ebcb264addd42R94-R95
We could definitely do smarter things to reduce the number of calls. There is some complexity, however, in determining whether we should be making a call to the back-end. Let's say that Node X sometimes needs back-end evaluation. We don't just need to make a back-end request if an edge is added directly to (or removed from) Node X -- we also need to make a request if any of those edge types are modified through propagation. It's certainly possible and wouldn't even be particularly difficult -- but it would add complexity that I'm not sure is necessary. Do you have any specific concerns around issues that could be caused by making those extra HTTP requests (assuming most of them will end up as no-ops)? Is it mostly around the CPU usage that could be incurred while also executing a graph?
Resolving dynamic types doesn't need the graph to be valid; despite the identical format of the HTTP request, none of the existing execution/validation logic runs when hitting the
Just to clarify, it isn't that a back-end request is needed for 1% of all cases evenly spread -- it's that 1% of custom nodes would need a back-end request 100% of the time. The most immediate example is For/While loops that need to share types between the Begin and End versions of the node. While we may be able to add full support for entangled nodes to the TemplateType syntax in some way, I'm both hesitant to add that extra complexity to the commonly used syntax and concerned that there are likely other cases that we wouldn't be supporting. The question of how complex it's worth making the declarative TemplateType syntax is definitely a subjective one though -- I might be leaning too far towards keeping it simple 🤔. Really appreciate the thoughtful feedback! |
@mijuku233 Yeah, you can see in my Dynamic Input Count examples that I've added a new hidden input of type With that said, I would somewhat discourage making logic decisions in this way. If the dynamically typed node is connected to a node using old-style |
In addition to dynamically changing types, is it possible to dynamically add types? |
@mijuku233 I'm not totally sure what you're asking. You wouldn't be able to link an If you wanted a dynamic input that accepted any time, but any time it accepted an image you wanted the type to become |
Could it be possible have a "native" switch like the one in Trung0246 nodes ( https://github.com/Trung0246/ComfyUI-0246 ) It's similar to the Impact Pack switch ( https://github.com/ltdrdata/ComfyUI-Impact-Pack ), but with extra features:
|
Thanks for the answer. This is great. I look forward to merging this PR.🥰 |
For frontend side, I did implemented majority of my nodes with some kind of dynamic inputs/outputs. You may want to take a looks since I basically made a light framework for this exact kind of thing. Pin setup is as easy as calling |
I also think that after this PR is merged, it would be appropriate to add |
The This switch can simulate a wire being unplugged in the workflow, in a way other switches can't. Example: Here, I'm switching between
|
Make sure your Impact Pack is up to date. After PR2666 (lazy feature) is merged, Impact Pack's switch also only executes the actually selected branch. Impact Pack's switch determines execution at runtime based on lazy evaluation and ExecutionBlockers, rather than simulation |
I updated all and tried again with I removed all other custom nodes from the workflow. My specs:
It runs both samplers, starting with the 2nd one, even here I selected the first one, as you can see in the image: |
RFC: Dynamic Typing
This Draft PR contains a proposal and initial implementation for adding official support for dynamic inputs/outputs to ComfyUI. This is intended to remove the UX barriers to adding "Loop", "Switch", and other nodes to the default ComfyUI.
ComfyUIDynamicTypingExample.mp4
Note: Getting the benefits of this change will also require the front-end changes located at Comfy-Org/ComfyUI_frontend#1271
The version of the
execution-inversion-demo
node pack (with loops and switches and the like) updated for this PR is located here: https://github.com/BadCafeCode/execution-inversion-demo-comfyui/tree/rfc/dynamic_typingFunctionality
The primary goal of this design is two-fold:
"*"
inputs and outputs.Why current solutions aren't sufficient
Use of
"*"
typesThe most common solution to the lack of dynamic typing is to use
"*"
types. While this functions properly, the user experience is far from ideal. Once you're using a wildcard type, nothing is preventing you from connecting incompatible sockets. When you do make a mistake, the result is a Python error in some node (that may not even be the node where the issue occurred).Custom Frontend Extensions - Dynamic Types
While I haven't seen it done, a custom frontend extension can technically enforce its own type constraints in the UI. While this would work with a single custom node pack in isolation, the propagation of node types through multiple dynamically typed nodes would cause issues. If we're going to start including nodes (like While Loops) in the base ComfyUI, we need a system that allows different node packs to play well with each other.
Custom Frontend Extensions - Variadic Inputs
Custom frontend extensions are frequently used (along with a
kwargs
argument) to allow for a dynamic number of inputs. The issue is that the backend knows nothing at all about these inputs. This means that any functionality that relies on input flags (like lazy evaluation) can't work with these inputs without terrifying hacks (like looking at the callstack to return different results fromINPUT_TYPES
depending on the caller).Design Goals
There were a couple goals going into this:
End While
loops needing types that match the linkedBegin While
node) possible to implement."*"
types, they don't need to change anything.I know that Goal 5 is going to be the most controversial due to the extra call to the back-end, but I believe that it's necessary if we don't want to end up with the ComfyUI back-end being tied inextricably to the default front-end.
Architecture Overview
In order to accomplish the above goals, I've implemented this using a number of layers. The top layer is the easiest to use for custom node authors, but is also the least flexible. Custom nodes that require more complicated behavior can use the same API that the higher layers are built on top of.
Layer 1 - Template Type Syntax
Template type syntax can be activated by using the
@TemplateTypeSupport
decorator imported fromcomfy_execution.node_utils
. The functionality it supports is:<T>
)ACCUMULATION<T>
)Dynamic Types
When specifying a type for an input or output, you can wrap an arbitrary string in angle brackets to indicate that it is dynamic. For example, the type
<FOO>
will be the equivalent of*
(with the commonly used hacks) with the caveat that all inputs/outputs with the same template name (FOO
in this case) must have the same type. Use multiple different template names if you want to allow types to differ. Note that this only applies within a single instance of a node -- different nodes can have different type resolutionsWrapped Types
Rather than using JUST a template type, you can also use a template type with a wrapping type. For example, if you have a node that takes two inputs with the types
<FOO>
andACCUMULATION<FOO>
, any output can be connected to the<FOO>
input. Once that input has a value (let's say anIMAGE
), the other input will resolve as well (toACCUMULATION<IMAGE>
in this example).Dynamic Input Count (Same Type)
Sometimes, you want a node to take a dynamic number of inputs. To do this, create an input value that has a name followed by a number sign and a string (e.g.
input#COUNT
). This will cause additional inputs to be added and removed as the user attaches to those sockets. The string after the '#' can be used to ensure that you have the same number of sockets for two different inputs. For example, having inputs namedimage#FOO
andmask#BAR
will allow the number of images and the number of masks to dynamically increase independently. Having inputs namedimage#FOO
andmask#FOO
will ensure that there are the same number of images as masks.The current dynamic count can be accessed from the node definition.
Dynamic Input Count (Different Types)
If you want to have a variadic input with a dynamic type, you can combine the syntax for the two. For example, if you have an input named
"input#COUNT"
with the type"<FOO#COUNT>"
, each socket for the input can have a different type. (Internally, this is equivalent to making the type<FOO1>
where 1 is the index of this input.)Layer 2 -
resolve_dynamic_types
Behind the scenes, Layer 1 (TemplateType syntax) is implemented using Layer 2. For the more complicated cases where TemplateType syntax is insufficient, custom nodes can use Layer 2 as well.
Layer 2 is used by defining a class function named
resolve_dynamic_types
on your node. This function can only make use of the following information when determining what inputs/outputs it should have:input_types
argument)output_types
argument)"entangleTypes": True
.The return value of
resolve_dynamic_types
should be a dictionary in the form:Example
Here's an example of a 'switch' node.
Note - I don't currently try to handle "unstable"
resolve_dynamic_types
functions. While it would be relatively easy to cause unstable configurations to "fail", identifying the exact node responsible to give a useful error message would be a lot more difficult.Layer 3 (Internal) - Node Definitions
Back-end
Internally to the ComfyUI back-end, I've turned the "node definition" (as returned from the
/object_info
endpoint) into a first-class object. Instead of directly callingINPUT_TYPES
in multiple places, the execution engine makes use of a node definition that is calculated and cached at the beginning of execution (or as part of node expansion in the case of nodes that are created at runtime).Theoretically, this could be extended in the future to making any other part of the node definition dynamic (e.g. whether it's an
OUTPUT_NODE
).These node definitions are iteratively settled across the graph, with a maximum of
O(sockets)
iterations (though you'd have to try hard to actually approach that). The same function is used for both resolving types in response to/resolve_dynamic_types
requests and prior to the beginning of execution, ensuring that the two are consistent.Front-end
The frontend now hits the
/resolve_dynamic_types
endpoint each time edges are created or removed from the graph. This call is non-blocking, but type changes and the addition/removal of inputs/outputs won't occur until it completes. My hope is that by implementing something like the TemplateType syntax on the default front-end, we can make 99% of these calls no-ops.Areas For Improvement
While my back-end changes are solid and could be code reviewed today, my front-end changes are hacky and would almost certainly need some attention from someone who has more experience with the front-end. While I'm posting this PR Draft now to start getting input, there are the following areas for improvement (mostly on the front-end):
"forceInput": True
as I'm not currently creating/destroying widgets as appropriate. This also means that Primitives nodes won't connect to them.displayOrder
option for inputs. This is just intended to sort inputs on the front-end, but it doesn't seem to always work.resolve_dynamic_types
function. (Right now, it'll just infinitely loop.)