Skip to Content
Ⅱ · 执行引擎07 · 列表、懒执行、异步与子图

07 · 列表、懒执行、异步与运行时子图

ComfyUI 节点表面上只需定义一个函数,运行时却允许它一次处理列表、延迟索取某个输入、返回未完成 coroutine、生成新节点,或阻断整条分支。复杂性集中在 _async_map_node_over_list() 与 execute(),使普通节点保持简单。

能力矩阵

能力节点声明 / 返回调度器动作
列表映射普通输入收到多个元素逐 index 调函数并合并输出
整列表输入INPUT_IS_LIST = True只调一次,传完整列表
列表输出OUTPUT_IS_LIST合并时展平对应 slot
懒输入check_lazy_status需要时把弱边变强,节点 PENDING
异步async def executetask 未完成则 external block + PENDING
子图expand注册 ephemeral nodes 和依赖,父节点 PENDING
阻断ExecutionBlocker / block_execution不调用下游计算,传播阻断语义

统一调用适配器

execution.pyL243-319列表切片、V1/V3 调用、CurrentNodeContext 与 coroutine task。

_async_map_node_over_list() 接收对象、归一后的 input_data_all、要调用的方法名和 V3 元数据。验证、fingerprint、lazy status 与真正 execute 都复用它。

这保证一个节点的列表语义不会在四条代码路径中各自实现。

列表映射

所有输入在执行适配层里都是“值列表”。若长度分别为:

images = [A, B, C] strength = [0.2] mask = [M1, M2]

普通节点会调用三次:

call 0: A, 0.2, M1 call 1: B, 0.2, M2 call 2: C, 0.2, M2

短输入通过“重复最后一个元素”补齐,而不是循环或报错。这个规则允许标量自然广播,但也意味着长度不匹配可能被静默接受;节点若要求严格一一对应,应自己验证。

空输入

没有输入时函数仍调用一次,参数为空。否则一个无输入生成器节点将永远没有执行机会。

INPUT_IS_LIST

声明后,运行时不切片,只调用一次并把每个输入的完整 list 传入。它适合需要跨元素聚合、排序或批量决策的节点。

输出合并

每次调用返回一个 tuple。merge_result_data() 按 output slot 转置:

  • 普通 output:收集为 [call0_value, call1_value, ...];
  • OUTPUT_IS_LIST=True 的 slot:把每次返回的列表 extend 成一层。
execution.pyL320-350列表映射结果按输出插槽合并。

输入列表与张量 batch 不是一回事。前者由执行器多次调用节点;后者可能是一次调用里的 IMAGE tensor 第一维。节点作者要明确自己需要哪一层批处理。

V3 调用为什么要 clone 和 lock

V3 节点通过 class method 描述 schema 和执行。适配器在每次调用前验证类、准备 class clone,并用 locked method 包装调用。动态嵌套输入还会从扁平形式重建为预期的嵌套 dict。

这样能:

  • 避免把跨调用可变状态意外留在共享 class 上;
  • 让每个 list index 拥有正确的调用上下文;
  • 在兼容 V1 执行器时仍保持 V3 schema 语义;
  • 将协议保留方法锁定,防止节点覆盖破坏执行契约。

CurrentNodeContext

同步与异步调用都包在 CurrentNodeContext(prompt_id, node_id, list_index) 中。底层服务或 API 节点可读取当前执行身份,而不用把三项参数塞进每个节点签名。

这是一种 context-local 注入。异步任务尤其需要它,否则多个挂起任务恢复时容易读到错误的“当前节点”。

懒输入:先看条件,再决定依赖

典型节点:若条件为真使用 if_true,否则使用 if_false。静态把两个分支都设为强依赖,会浪费一半计算。

节点定义 check_lazy_status(**kwargs),未求值的懒输入以 None(list 模式下是对应空结构)出现。函数返回当前真正需要的输入名。

execution.pyL484-511lazy status 结果转为强依赖并返回 PENDING。

为什么懒边起初不能计入环

如果两个条件分支里有一个形成环,但运行时永远不选它,静态把它作为强边会错误拒绝合法执行。TopologicalSort 区分连接存在与 strong dependency,等节点明确索取后才阻塞。

防止无限索取

执行器只接受字符串输入名,并过滤掉已存在且不缺失的输入。节点若一直请求同一个已提供输入,不会无限新增边;错误实现仍可能导致反复 PENDING,需要测试覆盖。

异步节点

当执行方法是 coroutine:

  1. 适配器用 asyncio.create_task() 启动;
  2. await asyncio.sleep(0) 给它一次立即完成机会;
  3. 已完成则直接取 result;
  4. 未完成则把 Task 留在返回列表。

execute() 发现 pending tasks 后:

pending_async_nodes[node_id] = tasks unblock = execution_list.add_external_block(node_id) create background await_completion(tasks, then unblock) return PENDING

下一次节点被 stage,执行器不重复调用远端 API,而是从 pending_async_nodes 取 task.result(),再走统一输出解析。

execution.pyL512-537未完成 tasks 注册为 external block。

async 的并发边界

  • coroutine 在服务器 asyncio loop 体系内推进;
  • 同一 Prompt 的调度循环可以去做其他 ready node;
  • 同步 CPU 密集代码即使写在 async def 里仍会阻塞 loop;
  • Task 异常在节点恢复时重新抛出,进入统一 execution error;
  • async 优先 heuristic 只是尽早启动,不承诺固定时序。

运行时子图

节点可返回 V1 dict 或 V3 NodeOutput(expand=..., ...)。get_output_from_returns() 把两种形式归一为 (new_graph, result_links)。

execution.pyL351-414UI、普通 result、ExecutionBlocker 和 expand 的统一解析。

执行器随后:

  1. 把新节点加入 DynamicPrompt,记录 parent / display ID;
  2. 找出子图中的输出节点并加入 ExecutionList;
  3. 为所有 caches 创建父节点下的 subcache;
  4. 把父节点 result 中的 links 设为强依赖;
  5. 保存 pending_subgraph_results[parent];
  6. 父节点返回 PENDING。

子图完成后,父节点再次执行入口,读取 result link 对应的 cached outputs,按列表规则合并,最后把父节点自身结果写入 cache。

execution.pyL555-610ephemeral nodes、subcache、output links 与父节点挂起。

GraphBuilder 的唯一前缀

列表映射可能对同一个节点调用多次,每次都创建本地名 node_1。执行前回调设置默认 prefix,结合父节点 ID 与 call index,避免不同调用的临时节点冲突。

子图缓存为何分层

临时节点只在父节点当前签名下有意义。若把它们扔进顶层平面缓存,父输入改变后旧子节点可能泄漏或被误复用。HierarchicalCache 以父节点 cache key 管理 subcache 生命周期。

ExecutionBlocker

有些情况不是异常,而是“这条数据流不应该继续”。ExecutionBlocker(message) 作为值沿输入传播:

  • 无 message 的 blocker 可安静传播;
  • 有 message 时执行器发送 ExecutionBlocked 错误,并转为空消息 blocker 继续阻断下游;
  • list 输入里任一元素 blocker,可按节点 list 语义阻断该调用;
  • V3 NodeOutput(block_execution=...) 被转成每个输出 slot 的 blocker。
comfy_execution/graph_utils.pyL140-155ExecutionBlocker 是值对象,不是 Python 异常。

异常表示实现失败;Blocker 表示领域控制流。区分两者可以让条件节点停止一条分支,而不把整个运行时当作崩溃。

UI 输出与 expand 的边缘组合

源码注释明确指出一个边缘风险:同一次返回若同时含 expand 和自定义 UI,UI 可能在子图完成前就被缓存。当前没有已知广泛使用的节点依赖这个组合,但节点作者应避免把“依赖子图最终结果的 UI”与 expand 同时提前返回。

这是阅读源码时值得保留的证据:成熟系统也会在注释里标记尚未统一的状态机边界,文档不应把它抹平。

组合示例:异步节点返回动态后处理

同一个父节点可能经历两次 PENDING:一次等待 coroutine,一次等待 expand 子图。调度器不需要为具体能力写死阶段,只需要维护 pending state 和新依赖。

节点作者的选择指南

需求使用
对每张图独立做相同操作默认 list mapping
比较整组图片并选最佳INPUT_IS_LIST
根据条件只计算一个昂贵分支lazy input + check_lazy_status
等待 HTTP / queue / subprocess真正非阻塞的 async def
根据输入生成可视化执行步骤GraphBuilder + expand
合法地停止下游而非崩溃ExecutionBlocker

不要用 expand 代替普通函数分解;它适合需要让运行时看见、缓存和调度的新计算单元。不要用 async 包装阻塞 GPU 调用;那只会把阻塞移到 event loop。

可迁移的设计方法

  1. 把同步/异步、单值/列表的适配集中在调用边界,不污染业务函数。
  2. 懒依赖用“两阶段询问 + 动态强边”,保持调度器可见性。
  3. async task 以显式 pending registry 保存,恢复时绝不重复提交副作用。
  4. 运行时 IR 增量扩展时保留 parent/display 两种身份。
  5. 用领域 blocker 表达可预期的“不产出”,异常只表达失败。
Last updated on