02 · 启动与生命周期
ComfyUI 的启动代码有一个很强的约束:设备可见性、路径、补丁和自定义节点的预启动逻辑,必须在真正初始化模型运行时之前完成。 顺序错了,不只是“配置晚生效”,还可能让 CUDA 枚举、内存策略或模块替换永久落在错误状态。
总调用链
main.py module body → apply_custom_paths → execute_prestartup_script → import torch/comfy → DynamicVRAM probe → start_comfyui → PromptServer → init_extra_nodes → database.setup → worker thread + event loop
阶段 0:为什么参数解析最先导入
main.py 开头先打开 comfy.options.enable_args_parsing(),随后才导入 CLI 参数对象。某些参数决定环境变量和设备选择;如果 torch 已经导入并触发设备探测,再设置它们通常已经太晚。
源码还主动检查 torch 是否过早出现在 sys.modules 中并警告。这是一种很实用的“启动不变量”:不是寄希望于贡献者记住隐含顺序,而是在运行时检测违反顺序的导入。
典型的早期参数
| 参数类别 | 为什么必须早 |
|---|---|
| CUDA device / deterministic | 影响设备可见性和底层库行为 |
--base-directory | 后续所有默认目录都从这里派生 |
| input / output / user / temp | 服务路由和节点读取都依赖 |
| extra model paths | checkpoint、VAE、LoRA 搜索表需先完整 |
| Windows standalone build | 包路径和更新器行为可能不同 |
阶段 1:构造文件系统命名空间
apply_custom_paths() 合并三类来源:基础目录参数、额外模型路径配置 YAML、显式 input/output/user/temp 路径。最终写入 folder_paths 的全局注册表。
这张注册表不只是 UI 展示模型列表:Checkpoint Loader、上传/查看接口、安全包含关系检查以及 Assets 扫描都依赖它。路径初始化因此是服务和推理的共同前置条件。
冲突时谁覆盖谁
代码按顺序应用配置,显式 CLI 目录最接近最终值。理解这一点比背默认目录更重要:部署环境应把“共享模型盘”和“用户输出盘”作为不同的配置层,而不是在节点里拼绝对路径。
阶段 2:自定义节点的 prestartup
在核心运行时完整导入前,ComfyUI 遍历自定义节点目录,寻找 prestartup_script.py 并直接执行。管理器可以借此安装依赖、调整路径或准备环境。
这个机制强大,也意味着明确的信任边界:
- 脚本与 ComfyUI 进程拥有相同权限;
- 它发生在普通节点模块加载之前;
- 单个脚本失败会被记录,不应悄悄吞掉;
- 禁用全部 custom nodes 时,这一阶段应一起跳过;
- 自定义节点目录本身就是代码供应链,而不是纯数据目录。
部署在共享服务器时,审核
custom_nodes/的重要性不低于审核 Python 依赖锁文件。
阶段 3:导入运行时与选择 Patcher
早期环境就绪后才导入 torch、comfy.utils、execution、server 等模块。随后,启动逻辑尝试探测 comfy_aimdo 支持;满足设备与能力条件时,会把核心使用的 Patcher 类切换为 ModelPatcherDynamic。
这里不是给每个节点添加 if dynamic_vram。它利用统一构造点替换抽象实现,让后面的 checkpoint 加载仍然通过 CoreModelPatcher。这是“在 composition root 选择策略”的典型做法。
必须保留的版本边界
Dynamic VRAM 是本快照中的条件路径,不代表所有机器都使用它。文档后面讨论 ModelPatcher 时,会先讲共同协议,再分别标出经典和动态实现。
阶段 4:组装应用对象
start_comfyui() 负责把此前的全局配置变成活跃对象:
- 清理临时目录并注册退出清理;
- 创建新的 asyncio event loop;
- 构造
PromptServer(loop); - 初始化 extra/custom/API nodes;
- 初始化数据库和路由;
- 安装进度回调;
- 创建 PromptQueue;
- 启动后台 worker thread;
- 返回 event loop、server 与启动协程。
PromptServer.instance、PromptQueue 等对象带有进程级单例味道。优点是节点和回调能方便访问服务;代价是同一 Python 进程内运行多个独立 ComfyUI 实例并非自然用法,测试也必须注意清理全局状态。
阶段 5:节点装载
nodes.init_extra_nodes() 先准备核心节点映射,再扫描内置 extras、自定义目录与 API nodes。服务端的 /object_info 读取最终映射,把后端能力暴露给前端。
节点加载失败通常不会让整个应用立即退出;加载器记录失败模块和导入耗时,服务仍可用。这个选择适合插件生态,但也意味着“服务能打开”不能证明所有预期节点已经成功加载。
阶段 6:数据库
数据库默认 URL 指向绝对路径 user/comfyui.db,也可用 --database-url 替换,甚至指向内存 SQLite。初始化包含文件锁、连接设置和迁移。
数据库是应用服务的一部分,而不是 GPU 执行的硬依赖抽象。尤其要区分:
- PromptQueue 的等待项、运行项和 history 是内存结构;
- 用户、设置和可选 Assets 等使用数据库服务;
- 数据库迁移失败与单个 Prompt 执行失败属于不同故障域。
阶段 7:两个并发世界
asyncio 世界
主线程事件循环负责 HTTP、WebSocket、异步验证、消息发布和服务生命周期。run() 同时等待服务启动协程与 publish_loop()。
工作线程世界
prompt_worker() 在 daemon thread 中循环:
get queue item
→ restore sensitive extra_data
→ PromptExecutor.execute
→ register output assets (optional)
→ task_done + history
→ honor unload/free flags
→ maybe GC / cache cleanup重推理没有运行在 aiohttp loop 中,因此前端仍能查询状态、取消任务并收到消息。但 Python 节点函数本身若操作共享全局状态,依然需要自己保证线程语义。
进度桥接安装在哪里
启动期通过 comfy.utils.set_progress_bar_global_hook() 安装全局 hook。采样器只需要报告 (value, total, preview);hook 再读取当前节点与 prompt ID,构造 progress JSON 和二进制预览事件。
这避免了 comfy.samplers 直接依赖 aiohttp 或 Socket。模型代码只理解进度,composition root 负责接线。
正常退出与清理
临时目录通过退出处理注册清理。进程级 daemon worker 会随主进程结束;服务循环异常或键盘中断走外层清理。由于队列和 history 默认在内存,非正常退出不会自动恢复尚未执行的 Prompt。
对于需要可恢复任务的生产部署,这是一项架构边界:应在 ComfyUI 外层增加持久任务编排,而不是把内存 PromptQueue 当 durable queue。
常见启动故障的定位顺序
| 表现 | 优先检查 |
|---|---|
| 指定 GPU 没生效 | torch 是否过早导入;设备参数是否在进程启动前生效 |
| 模型列表为空 | extra model paths 与 folder_paths 注册,而非 sampler |
| 某一组节点消失 | 节点导入日志、禁用策略、模块依赖、V3 entrypoint |
| 页面能开但 API 被拒 | origin/CORS 中间件和监听地址 |
| 数据库被占用 | SQLite .lock、是否启动了第二实例、database URL |
| 第一次启动很慢 | custom node prestartup、节点导入、数据库迁移、Assets 扫描分别计时 |
设计方法
- 把不可逆的全局选择前置。 设备枚举和模块补丁必须发生在第一次使用前。
- 用 composition root 汇合副作用。 进度回调、节点映射、DB 和线程都在启动层接线。
- 插件失败可降级,但要可观测。 记录失败模块和耗时,不让错误消失。
- 把计算与传输放入不同并发域。 GPU 工作线程不直接维护网络连接。
- 把外部代码目录视作执行权限。 “自定义节点”绝不只是配置文件。