Skip to Content
Ⅰ · 系统骨架01 · 系统总览

01 · 系统总览

一句话心智模型: ComfyUI 后端是一个“以输出为需求、以节点为计算单元、以模型补丁器管理重量级状态”的动态数据流运行时;Web UI 只是它最常用的客户端。

从一个具体问题开始

用户在画布上连接六个节点,点击 Queue Prompt。几秒后界面持续收到预览,最终图片出现在输出目录。看起来是一条普通流水线,但后端至少同时处理了六种不同的问题:

  1. 画布 JSON 如何收缩成可执行协议?
  2. 哪些节点真正需要执行,顺序怎样决定?
  3. 哪些结果可以沿用上次缓存?
  4. checkpoint、LoRA 和 VAE 如何组合,而不复制几 GB 权重?
  5. 模型放不进显存时,哪些模块先留在 CPU?
  6. 工作线程里的进度怎样安全地送到 asyncio WebSocket?

如果只把 ComfyUI 理解为“按线执行节点”,后五个问题都会变成例外。把它看成运行时,这些反而是主体。

五个平面

这些平面不是目录一一对应,而是职责边界:

平面核心对象长生命周期状态
展示前端包、扩展 JS画布、视图、客户端功能协商
控制PromptServer、PromptQueueSocket、待执行堆、运行任务、历史
执行PromptExecutor、ExecutionList、缓存本次 Prompt、跨次节点结果、临时子图
模型ModelPatcher、LoadedModel、CFGGuider权重、补丁、设备驻留、采样状态
数据folder_paths、DB、Assets模型文件、输出、用户设置、资产索引

一次文生图的真实旅程

假设 API Prompt 包含:

CheckpointLoaderSimple ─┬─> CLIPTextEncode(positive) ─┐ ├─> CLIPTextEncode(negative) ─┼─> KSampler ─> VAEDecode ─> SaveImage └─────────────────────────────┘ EmptyLatentImage ─────────────────────────────────────>

1. 请求进入

POST /prompt 解析 JSON,补充 prompt ID 和队列序号,调用异步验证器。验证不会加载模型;它检查节点类型、必需输入、连接类型、范围、组合值和环。

server.pyL1072-1144Prompt 接口:编号、验证、敏感字段剥离与入队。

2. 队列交给工作线程

合法任务作为一个六元组放进最小堆。独立工作线程阻塞在 PromptQueue.get(),取出后调用 PromptExecutor.execute_async()。aiohttp 事件循环不会被几十步 GPU 推理直接堵住。

main.pyL329-424prompt_worker:选择缓存、执行、写历史、回收内存。

3. 从输出反向找需求

执行器不会遍历所有节点,而是把通过验证的输出节点加入 ExecutionList。SaveImage 依赖 VAEDecode,后者依赖 KSampler,依此反向展开。孤立节点自然不在需求闭包中。

4. 缓存先回答“还要不要算”

每个节点的缓存键不是“节点 ID + 当前输入”这么简单。签名递归包含上游结构、常量、插槽位置、节点类型以及 IS_CHANGED / fingerprint_inputs。因此改了正向提示词,Checkpoint Loader 通常可复用,正向编码及其下游失效。

5. 节点调用统一成一个适配层

执行器把链接解析为上游结果,把常量原样传入,把隐藏输入注入,再处理列表映射、V1/V3 协议、同步/异步函数、UI 输出和阻断对象。节点作者看到的 execute() 很简单,是因为复杂度被运行时收拢了。

6. 模型只是“视图”,不是立即常驻的副本

Checkpoint Loader 构造基础模型,再用 ModelPatcher 包装。LoRA、模型选项与 hook 通常记录为补丁元数据;真正采样前,模型管理器按设备与显存预算决定完整加载、部分加载还是 offload。

7. 采样期间实时回传

每个采样步骤调用进度回调。工作线程通过 send_sync() 把消息投递到 asyncio 队列,publish_loop 再向对应 WebSocket 客户端发送 JSON 或带事件号前缀的二进制预览。

8. 完成、历史与输出

最终节点的 UI 元数据进入执行结果;队列把任务从 currently_running 移入内存 history。输出文件已经由节点写入目录;若启用 Assets,任务完成后还会把输出登记为资产引用。

三个关键数据结构

PromptQueue:进程级任务状态

# 概念结构,并非逐字源码 queue: list[(priority, prompt_id, prompt, extra_data, outputs, sensitive)] currently_running: dict[task_id, queue_item] history: dict[prompt_id, execution_record] flags: dict[str, object]

它使用 RLock + Condition 保护状态,用 heap 保证优先级;“队列顺序”与“一个 Prompt 内的节点顺序”是两套机制。

DynamicPrompt:可增长的图

它把用户提交的原始节点与运行时新增的 ephemeral nodes 放在同一查询接口后面,并记录临时节点的父节点与显示 ID。动态扩图不会篡改原始 JSON。

ModelPatcher:共享权重的可变视图

多个 patcher clone 可以共享底层 model 与备份权重,却拥有各自的 patches、options、hooks 和 UUID。这样切换 LoRA 组合不必复制整个 checkpoint。

目录地图:按职责而不是按字母

目录 / 文件读它时要问什么
main.py哪些初始化必须发生在 torch 导入前?线程和事件循环怎样分工?
server.py外部协议如何变成内部队列项?消息怎样跨线程?
execution.py验证、求值、缓存、错误和 UI 输出在哪里汇合?
comfy_execution/动态图、拓扑调度与缓存键怎样独立出来?
nodes.pyV1 节点协议怎样呈现?扩展如何发现?
comfy_api/latest/V3 类型系统如何投影到旧执行器?
comfy/sd.py一袋 state dict 怎样被识别为具体模型?
comfy/model_*权重补丁、设备驻留与显存回收如何协作?
comfy/samplers.py条件批处理、CFG、sigma 和采样算法如何分层?
app/DB、用户、资源、Assets 等应用服务如何接到核心?
comfy_extras/内置扩展节点,很多协议高级用法的活样例
comfy_api_nodes/依赖远端服务的 Partner/API 节点,不等同于纯本地核心

并发模型

这里没有“每个 Prompt 一个进程”的隔离,也没有默认多 GPU 调度器。一个服务实例以共享模型状态换取加载复用;因此自定义节点中的全局可变状态、阻塞 I/O 和线程不安全操作会影响整个实例。

设计取舍

为什么不是简单 DAG 引擎?

因为图会在运行时增长,输入可以懒求值,节点可以挂起等待外部任务,缓存又可能让任意中间节点直接完成。静态拓扑序只能做起点,不能描述完整状态机。

为什么队列和执行器分离?

队列负责跨任务的优先级、取消、历史和状态;执行器负责一个任务内部的依赖与结果。分离后,任务管理不必理解张量,节点调度也不必理解 WebSocket 客户端。

为什么模型管理独立于节点?

如果每个节点自己 .to('cuda'),多个模型、多个设备和低显存策略会互相冲突。统一的 ModelPatcher / model_management 让节点只声明需要的模型,运行时统一预算和回收。

不能从这张总图误推什么

  • 缓存不是磁盘构建缓存。 默认缓存对象主要在进程内,重启后不保留。
  • SQLite 不是 PromptQueue 的持久化替代。 队列和默认 history 仍是内存状态。
  • WebSocket 不负责提交 Prompt。 HTTP 入队,WebSocket 主要推送状态与预览。
  • 所有节点并非都在 GPU 上。 文本、文件、网络或控制节点可能只在 CPU,模型也可能部分 offload。
  • “master 版本”不等于稳定协议。 本文只对固定提交负责。

可以迁移到自己的系统

  1. 把用户编辑格式与机器执行格式分开,避免 UI 元数据污染执行协议。
  2. 从结果反向求需求闭包,而不是默认全图执行。
  3. 让缓存键表达完整语义依赖,不只表达对象身份。
  4. 把重量级资源的“逻辑视图”与“物理驻留”分开。
  5. 用显式消息桥跨越线程/事件循环,不从工作线程直接操作 Socket。

下一章从进程第一行开始,看这些平面如何按正确顺序被组装起来。

Last updated on