A · 端到端执行实录
这一章不引入新抽象,只把前 15 章压回一次真实执行。目标是能从任一日志或 WebSocket 事件,反推出它位于哪一层、接下来应该发生什么。
场景
工作流包含:
4 CheckpointLoaderSimple
6 CLIPTextEncode (positive)
7 CLIPTextEncode (negative)
5 EmptyLatentImage
3 KSampler
8 VAEDecode
9 SaveImage客户端已经连接 /ws?clientId=browser-42,随后提交 API Prompt。
1. 客户端提交
POST /prompt
Content-Type: application/json
{
"client_id": "browser-42",
"prompt": {
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "example.safetensors"}
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "a quiet glass city", "clip": ["4", 1]}
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "blurry", "clip": ["4", 1]}
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {"width": 1024, "height": 1024, "batch_size": 1}
},
"3": {
"class_type": "KSampler",
"inputs": {
"seed": 42, "steps": 20, "cfg": 7.0,
"sampler_name": "euler", "scheduler": "normal", "denoise": 1.0,
"model": ["4", 0], "positive": ["6", 0],
"negative": ["7", 0], "latent_image": ["5", 0]
}
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["3", 0], "vae": ["4", 2]}
},
"9": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": "ComfyUI", "images": ["8", 0]}
}
}
}注意:上面是 API Prompt,不含节点坐标、尺寸和独立 links 数组。
2. 服务端编号与验证
server.py:
parse JSON
→ prompt_id = UUID
→ number = queue counter
→ extra_data.client_id = browser-42
→ validate_prompt(prompt)验证器找到唯一 output node 9,反向 DFS:
9 → 8 → 3 → {4, 6, 7, 5}
6 → 4
7 → 4
8 → 4检查:
- 每个 class type 已注册;
SaveImage.images收到 IMAGE;VAEDecode.samples收到 LATENT,vae收到 VAE;- KSampler 数值范围和连接类型正确;
- Checkpoint 名在组合选项中;
- DFS 不存在环。
得到 good_outputs = ["9"]。
3. 入 PromptQueue
队列项概念值:
(
number,
prompt_id,
prompt,
{"client_id": "browser-42"},
["9"],
sensitive_extra_data,
)heap push 后 Condition 唤醒 worker。HTTP 立即返回:
{
"prompt_id": "...",
"number": 17,
"node_errors": {}
}此时“接收成功”不等于生成成功,只代表验证通过并已入队。
4. worker 取任务
prompt_worker() 原子地把条目从 waiting heap 移入 currently_running,恢复敏感 extra data,调用:
PromptExecutor.execute(prompt, prompt_id, extra_data, ["9"])同步包装通过 asyncio.run(execute_async(...)) 在 worker 侧执行请求级 async 状态机;网络消息仍经 PromptServer 的 thread-safe bridge 投到主服务 loop。
5. PromptExecutor 准备上下文
依次发生:
- 清除 interrupt flag;
PromptModelTracker.start();- 设置
server.client_id = browser-42; - 记录/发送
execution_start; - 进入
torch.inference_mode(); - 创建
DynamicPrompt; - reset progress state 并添加 WebUI handler;
- 创建
IsChangedCache; - 为 outputs / objects cache 设置当前 Prompt keys;
- 查询所有原始节点的缓存命中。
第一次运行假设全部 miss,于是发送:
{"type":"execution_cached","data":{"nodes":[],"prompt_id":"..."}}6. 建立 ExecutionList
只加入输出 9,拓扑层递归发现强依赖。ready nodes 初始可能是 4 和 5。选择 heuristic 不承诺固定节点 ID 顺序;本例假设先执行 4。
7. Checkpoint 节点
执行入口:
cache miss
→ get_input_data(ckpt_name constant)
→ objects cache: construct CheckpointLoaderSimple
→ list adapter invokes load_checkpoint模型链:
resolve folder path
→ load safetensors state dict
→ detect model config from keys/shapes
→ construct BaseModel
→ wrap CoreModelPatcher
→ construct CLIP
→ construct VAE
→ return (MODEL, CLIP, VAE)结果变成 CacheEntry,并通知 ExecutionList 的所有消费者。PromptModelTracker 从 link callback 观察到 MODEL/Patcher。
浏览器收到 executing,但 Loader 没有自定义 UI,所以不一定收到 executed UI payload。
8. Latent 与文本编码
5 创建空 latent,写缓存。
6 / 7 各自从消费者 local cache 取 CLIP slot 1,解析 text 常量。CLIP 在需要时经模型管理器加载到设备,tokenize + encode 后各返回 CONDITIONING。
同一个 Checkpoint 输出被三个消费者读取,但不会重新运行 Loader。
9. KSampler
输入都 ready,节点 3 被 stage:
prepare deterministic CPU noise(seed=42)
→ construct preview callback
→ build KSampler(sampler=euler, scheduler=normal, steps=20)
→ calculate sigma sequence
→ CFGGuider prepares positive/negative
→ collect main + extra models
→ load_models_gpu with total budget
→ patch/load model weights
→ sampler loop每一步可能做 cond/uncond batch,执行 BaseModel forward,再按 CFG 合成预测并由 Euler 更新 latent。
10. 进度事件
第 7 步回调:
callback(step=7, total=20, x0, x)
→ lightweight latent preview
→ ProgressBar hook
→ send_sync(progress JSON)
→ send_sync(preview binary)
→ publish_loop
→ browser-42 WebSocketJSON 与 preview binary 可能是相邻但独立 frame。前端用 prompt/node ID 归属消息。
11. Decode 与 Save
KSampler output LATENT 让 8 ready。VAE 可能先腾出主模型的一部分显存或按自己的策略加载,然后把 latent decode 为 IMAGE。
9 作为 output node ready 后优先执行:
- 生成安全输出路径与递增文件名;
- 写 PNG;
- 写 Prompt / extra PNG metadata(若配置允许);
- 返回
ui.images; - 可选 Assets enrichment 加 asset ID。
服务发送 executed,前端用 /view?... 拉取最终图像。
12. 成功收尾
ExecutionList 为空,循环 else 分支:
- 重放未执行但有 intermediate UI 的缓存;
- 发送
execution_success; - 汇总
history_result.outputs与meta; - 清
server.last_node_id; - finally 清 RAM release hook;
PromptModelTracker.end();- cache provider 收到 prompt end。
worker 随后 PromptQueue.task_done():从 running 删除,写入内存 history,通知状态。若启用 Assets,登记/扫描输出。
第二次:只改提示词
将节点 6.text 改为 a red glass city:
| 节点 | 缓存 | 原因 |
|---|---|---|
| 4 Checkpoint | hit | 类型、文件、fingerprint 不变 |
| 5 EmptyLatent | hit | 尺寸与 batch 不变 |
| 6 Positive | miss | 常量 text 改变 |
| 7 Negative | hit | 自身与祖先签名不变 |
| 3 KSampler | miss | positive 祖先签名改变 |
| 8 VAEDecode | miss | sampler 祖先改变 |
| 9 SaveImage | 通常执行 | 新上游结果与副作用语义 |
WebSocket 的 execution_cached.nodes 让用户看到哪些节点直接复用。调度器从 miss 闭包开始,不需要先“运行” hit 节点。
失败分支:KSampler OOM
若第一个 model forward OOM:
exception caught in execute(node 3)
→ is_oom(ex) true
→ record memory summary
→ unload_all_models()
→ error_details includes node, type, traceback, inputs, tips
→ ExecutionResult.FAILURE
→ PromptExecutor.handle_execution_error
→ WebSocket execution_error
→ history status = error节点 8、9 不执行;先前缓存的 4、5、6、7 仍可能保留,取决于 OOM 清理与 cache policy。已经发生的外部副作用不会自动回滚。
看到一条日志时先问
| 日志 / 事件 | 所在层 |
|---|---|
Prompt outputs failed validation | HTTP 入队前验证 |
execution_start | PromptExecutor 生命周期开始 |
execution_cached | 当前 input signatures 查询完毕 |
executing node X | 节点已 stage,正解析/调用 |
Requested to load ... | model management 物理驻留 |
Model loaded ... ram_mb/vram_mb | Patcher 装载完成 |
progress value/total | sampler callback |
execution_error | 节点调用或运行时图失败 |
execution_success | ExecutionList 正常耗尽 |
至此,从 /prompt 到最后一个文件写入的每个关键对象都能在前文找到独立章节。