Skip to Content
附录A · 端到端执行实录

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 准备上下文

依次发生:

  1. 清除 interrupt flag;
  2. PromptModelTracker.start();
  3. 设置 server.client_id = browser-42;
  4. 记录/发送 execution_start;
  5. 进入 torch.inference_mode();
  6. 创建 DynamicPrompt;
  7. reset progress state 并添加 WebUI handler;
  8. 创建 IsChangedCache;
  9. 为 outputs / objects cache 设置当前 Prompt keys;
  10. 查询所有原始节点的缓存命中。

第一次运行假设全部 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 WebSocket

JSON 与 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 分支:

  1. 重放未执行但有 intermediate UI 的缓存;
  2. 发送 execution_success;
  3. 汇总 history_result.outputs 与 meta;
  4. 清 server.last_node_id;
  5. finally 清 RAM release hook;
  6. PromptModelTracker.end();
  7. cache provider 收到 prompt end。

worker 随后 PromptQueue.task_done():从 running 删除,写入内存 history,通知状态。若启用 Assets,登记/扫描输出。

第二次:只改提示词

将节点 6.text 改为 a red glass city:

节点缓存原因
4 Checkpointhit类型、文件、fingerprint 不变
5 EmptyLatenthit尺寸与 batch 不变
6 Positivemiss常量 text 改变
7 Negativehit自身与祖先签名不变
3 KSamplermisspositive 祖先签名改变
8 VAEDecodemisssampler 祖先改变
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 validationHTTP 入队前验证
execution_startPromptExecutor 生命周期开始
execution_cached当前 input signatures 查询完毕
executing node X节点已 stage,正解析/调用
Requested to load ...model management 物理驻留
Model loaded ... ram_mb/vram_mbPatcher 装载完成
progress value/totalsampler callback
execution_error节点调用或运行时图失败
execution_successExecutionList 正常耗尽

至此,从 /prompt 到最后一个文件写入的每个关键对象都能在前文找到独立章节。

Last updated on