同一套 ComfyUI 工作流第二次为什么快一倍:4 类消息误解 + 互斥阶段账本 + 10 单元实验矩阵 同一套 ComfyUI 工作流第二次为什么快一倍TL;DR场景同一套 ComfyUI 工作流第二次运行耗时明显变短最容易得出的结论是采样器或注意力变快了但缺乏任何阶段归属的耗时数据。结论第二次耗时变短可能来自模型常驻、CUDA kernel 编译预热、节点输入签名缓存命中、实际执行子图收缩也可能来自原生帧之外的后处理被忽略。结论必须建立在互斥的阶段账本之上。产出互斥的阶段账本字典、四种运行模式process_cold / model_cold_process_warm / model_warm_recompute / cache_hit_rerun、十项实验矩阵、阶段绑定的完整 condition_id 字段以及四类最常见错误结论的纠正方法。版本矩阵功能状态说明PyTorch GPU 运算是异步执行✅ 已验证PyTorch 官方 CUDA 语义文档原文默认情况下 GPU 操作都是异步执行的没有同步的时间测量不准确torch.cuda.Event(enable_timingTrue) 测量 GPU 时间✅ 已验证官方推荐方案start.record() / end.record() / end.synchronize() / start.elapsed_time(end)单位毫秒torch.cuda.synchronize() 阻塞 CPU 直到所有 CUDA 任务完成✅ 已验证官方 API用于性能测试、调试与多 GPU 场景不推荐在常规训练中频繁调用CUDA Event 测同设备流上的 GPU 时间✅ 已验证官方说明单设备单 stream 阶段有效多 stream、异步 H2D/D2H、CPU/GPU 并行需 Nsight SystemsComfyUI 节点缓存仅重执行变化的图部分✅ 已验证官方文档原文“Only parts of the graph that change from each execution to the next will be executed, if you submit the same graph twice only the first will be executed”ComfyUI Asynchronous Queue system✅ 已验证ComfyUI GitHub README “Features” 部分明确列出executed 消息在无 UI 输出时不发送✅ 已验证ComfyUI 官方消息文档明确说明executed 不是每个节点完成事件只在节点返回 UI 更新时发送ComfyUI v0.11.0 release date 2026-01-27 EasyCache / Noise_EmptyNoise / WAN-VAE / LTX2 优化⚠️ 待验证上一轮 v0.11.0 摘要未在本轮 web search 结果中直接命中建议核对 GitHub Release tag 原始页面节点缓存默认 TTL / 失效策略⚠️ 待验证节点缓存依赖输入签名 节点定义指纹默认 TTL 与失效策略需查 execution.py 当前实现EasyCache 在采样期间条件变化时的边缘处理⚠️ 待验证v0.11.0 摘要描述正确处理边缘情况但具体场景与配置开关需查 commit diffComfyUI v0.16.3 LTX2 vocoder 修复manual cast to conv_transpose1d⚠️ 本文未引用与本文主题不直接相关列出以避免读者混淆版本号发布边界标题中的快一倍是待解释现象不是作者实测承诺任何性能结果都必须绑定工作流、硬件、版本和运行条件。摘要同一套 ComfyUI 工作流第二次运行明显更快可能来自模型常驻、编译预热、节点缓存或实际执行子图变化而不代表模型本身获得同等幅度加速。本文建立从队列、加载、条件编码、采样、VAE、后处理到文件交付的互斥阶段账本并给出冷启动、热启动、缓存命中和切换模型的可复现比较矩阵。关键词ComfyUI、阶段计时、缓存命中、CUDA Events、性能工程目录一、先区分静态工作流与实际执行子图二、正确理解 ComfyUI 的四类消息三、把总耗时拆成互斥的阶段账本四、CPU 计时不能直接代表 GPU 计时五、冷启动、热启动与模型切换必须分栏六、可直接执行的实验矩阵七、阶段账本必须绑定完整条件八、用阶段占比决定优化顺序九、最小可落地的采集实现十、四类最常见的错误结论结语同一套视频工作流第一次运行很慢第二次却明显变快。最容易得到的结论是优化生效了但这个结论通常没有回答最重要的问题快在哪里第二次运行可能复用了节点输出模型也可能已经驻留显存CUDA 上下文、算子编译、磁盘页缓存和 FFmpeg 初始化也可能已经预热。另一种更隐蔽的情况是测试只计到了原生帧生成没有把插帧、超分、编码、落盘和上传算进去。总耗时变短是真实的但它不能单独证明采样器、注意力实现或模型本身变快。因此从 Queue 到最终视频文件的性能分析不能只记一个开始时间和一个结束时间。需要同时建立三个对象本次请求真正执行了哪一张子图每个工程阶段的边界是什么所有对比运行遵守什么状态与配置协议。只有三者同时固定耗时才可复现、可比较也才能指导优化。一、先区分静态工作流与实际执行子图ComfyUI workflow 是由节点和连接组成的图。[S01][S02] 但保存在 JSON 里的完整图不等于某一次请求真正执行的图。第一层变化来自输出选择。Partial Execution 只执行所选输出节点所依赖的分支完整执行才会请求全部输出分支。[^S04] 如果同一工作流同时有预览图、原生视频、插帧视频和最终交付文件选择不同输出本次请求的祖先节点集合就不同。第二层变化来自缓存。节点是否需要重算不只取决于节点名称而取决于节点类型、输入、上游依赖以及节点定义的变化指纹。当前 ComfyUI 源码会为输入签名构造缓存键并在执行前检查可复用结果。[S05][S06] 因此修改种子可能只让采样及其下游重算文本编码仍然命中修改提示词则可能让条件编码、采样和后续分支全部失效。缓存模式、节点实现和自定义节点版本变化也会改变实际子图。可以把本次请求的候选子图写成候选子图 所选输出的全部祖先节点 实际执行子图 ≈ 候选子图 − 可复用缓存节点 动态展开节点这个公式只能用于设计不应替代观测。最终必须保存三份清单selected_outputs、cached_nodes和actual_executed_nodes。还要记录预期执行而未执行的节点以及意外执行的节点。只看静态 workflow无法解释两次运行为什么不同。二、正确理解 ComfyUI 的四类消息ComfyUI 的 WebSocket 消息适合建立运行状态但不能直接当作通用节点计时器。[^S03]消息可安全解释的语义不能据此推出execution_cached执行开始阶段发现可复用输出的节点清单这些节点构成了本次全部未执行节点或缓存命中等于完整推理executing某节点即将被执行或调度GPU 已开始计算更不能代表 GPU 已完成executed节点产生了需要发给前端的 UI 输出每个节点都已完成无 UI 输出的节点通常没有该消息execution_success本次 ComfyUI prompt 的必需执行无错误结束外部后处理、独立 FFmpeg、上传或最终文件提交一定已经完成官方消息文档明确说明executed不是每个节点完成事件而只在节点返回 UI 更新时发送。[^S03] 当前源码还会为缓存中的 UI 数据发送executed。[^S05] 因此用相邻两个executed的到达时间推算节点耗时会同时漏掉无 UI 节点、混入网络传输并把缓存 UI 误认为真实执行。executing更接近调度前信号。当前源码在调用节点函数前发送它但 GPU 工作可能只是随后被异步入队。[^S05] 它可以帮助重建节点顺序不能作为 GPU 完成边界。当前生命周期消息中的部分时间戳使用服务器墙上时钟而精确耗时应使用同一进程的单调时钟。客户端收到 WebSocket 消息的时间还包含网络、事件循环和序列化延迟。精确的 Queue 等待时间应在服务器端用同一个monotonic/perf_counter时钟记录T_queue t_execution_start_server − t_queue_accepted_server客户端提交时间减服务器事件时间只有在时钟经过同步且明确计入网络延迟时才有意义。否则应命名为客户端观测等待不能冒充服务器队列等待。三、把总耗时拆成互斥的阶段账本端到端边界应从请求被队列接受开始到可交付文件完成提交为止T_total t_delivery_committed − t_queue_accepted最小阶段字典如下。每个阶段都要有明确的开始事件、结束事件、责任组件和产物边界。阶段建议边界需要单独记录的原因queue_wait队列接受到执行开始受并发、优先级和前序任务影响与模型速度无关executor_prepare执行开始到首个实际阶段开始包含图准备、缓存检查、资源清理等调度成本model_load_residency权重读取、反序列化、主机暂存、H2D、驻留或换入结束冷启动和模型切换的主要差异在这里conditioning文本、图像、控制条件和适配器编码可能被上游缓存独立复用sampling首个采样工作开始到最后一个采样工作完成扩散或流模型的迭代主体但占比必须实测vae_decodelatent 解码开始到帧张量完成分辨率、帧数、分块和精度都会改变该阶段native_materialize帧张量到原生输出可用包含 D2H、帧整理、原生文件写入等postprocess插帧、超分、修复、色彩或其他处理可按处理器继续拆分不能并入原生生成encode_muxFFmpeg 启动到成功退出并关闭临时文件编码器、预设、滤镜、像素格式和容器独立影响delivery_commit临时产物完成到最终路径可见且校验通过包含 flush、fsync、原子重命名、复制、上传或校验queue_to_delivery队列接受到最终交付完成用户真正等待的端到端总账如果这些阶段严格串行可以相加得到总耗时。若 GPU 计算、CPU 后处理、磁盘写入或多流复制发生重叠就不能把所有子阶段时长机械相加。此时总账使用区间并集或关键路径资源账本则分别报告 CPU wall、GPU device time、I/O time 和重叠区间。FFmpeg 必须成为显式阶段。父进程用单调时钟记录子进程启动、退出和文件关闭FFmpeg 的-progress可输出机器可读的周期状态-benchmark或-benchmark_all可作为诊断补充。[^S12] 但最终编码墙钟仍应以父进程从启动到成功退出的区间为准。编码完成也不等于交付完成跨文件系统复制、对象存储上传、校验和、原子发布和消费者可见性要放进delivery_commit。四、CPU 计时不能直接代表 GPU 计时CUDA 默认是异步执行。CPU 调用通常只是把 kernel、内存复制或库操作排入 GPU stream然后立即返回。[^S09] 因此t0perf_counter()run_sampling()t1perf_counter()在没有同步边界时往往测到的是提交工作所需的 CPU 时间而不是采样在 GPU 上完成所需的时间。对单设备、单一或明确 stream 的阶段可使用 CUDA Eventstarttorch.cuda.Event(enable_timingTrue)endtorch.cuda.Event(enable_timingTrue)start.record()run_stage()end.record()end.synchronize()gpu_msstart.elapsed_time(end)Event 必须记录在正确的 device 和 stream 上结束 Event 同步后才能读取时间。[^S09] 如果阶段涉及多个 stream、异步 H2D/D2H、模型换入换出、CPU/GPU 并行或第三方 CUDA 库单对 Event 可能覆盖不全。此时应使用 Nsight Systems 捕获 CUDA API、kernel、memory copy、context 和 stream 时间线并用 NVTX range 标注run_id/stage_id/node_id。[^S10]不要在每个节点后强制torch.cuda.synchronize()作为常规基准。它会打破原本的流水和重叠改变被测系统。应建立两套运行低侵入基线运行只记录服务器单调时钟、实际子图、阶段边界、FFmpeg 和产物提交。剖析运行增加 CUDA Event、NVTX 与 Nsight用于解释 GPU 关键路径并单独标记 Profiler 配置和开销。显存也要分层。PyTorch memory snapshot 只能看见由 PyTorch allocator 管理的 CUDA 内存直接 CUDA API、NCCL 或其他库的分配可能不可见。[^S11] 账本应同时保留 PyTorch allocated/reserved 峰值、设备或进程级显存观测以及需要时的 Nsight memory trace三者不能互相冒充。五、冷启动、热启动与模型切换必须分栏冷启动一次、热启动三次仍然太含糊。至少定义以下状态状态操作定义报告名称新进程启动新建 ComfyUI 进程CUDA context、运行时和模型尚未初始化process_cold进程已热、目标模型未驻留服务仍在但目标模型从未加载或已被明确卸载model_cold_process_warm模型驻留且强制重算模型、上下文和必要编译已热通过--cache-none或受控失效保证推理真实执行model_warm_recompute完全相同请求命中缓存保持 prompt 与输入不变允许节点输出复用cache_hit_rerunA→B→A 切换先热 A再运行 B再强制 A 重算model_switch_A_B_Acache_hit_rerun是缓存收益测试不是模型推理速度测试。热模型测试也不能只修改种子后假定全图重算上游条件编码可能仍然命中。必须用execution_cached、executing序列和服务器端埋点核对实际子图。“新进程也不等于物理冷机。操作系统页缓存、模型文件缓存和容器镜像层可能仍然热。如果没有在专用环境中控制这些状态应写成进程冷启动OS 页缓存未控制”不能写成完全冷启动。清理系统页缓存会影响整机不应在共享生产机上作为默认操作。六、可直接执行的实验矩阵先固定一个condition_id再执行以下矩阵。所有单元使用相同输入资产、输出参数和交付边界有意改变的变量只能写在该单元的差异列。单元启动与缓存状态执行范围目的M01process_cold固定缓存模式完整工作流到最终交付测进程冷启动端到端M02与 M01 同进程相同请求允许缓存完整工作流到最终交付测缓存复用收益不参与推理速度排名M03model_warm_recompute完整工作流到最终交付测热模型真实重算基线M04热模型--cache-none完整工作流到最终交付建立不依赖节点输出缓存的执行器基线M05热模型、强制重算Partial Execution 到原生生成输出隔离原生生成链M06热模型、强制重算原生生成加全部后处理编码前停止计算后处理增量M07热模型、强制重算到 FFmpeg 临时文件成功关闭计算编码与封装增量M08热模型、强制重算到最终路径提交并校验计算交付增量M09model_switch_A_B_A每次强制重算完整工作流到最终交付测换模、卸载和重新驻留成本M10固定并发和前序负载完整工作流到最终交付单独研究队列不与单请求算力比较混合执行规则M01 使用五次独立进程启动每次记录 OS 页缓存是否受控。M03—M08 先做三次不计入结果的稳定运行再做十次测量运行。M09 做五个完整 A→B→A 周期按三个位置分别报告。比较两个实现时使用同一输入、种子序列和condition_id配对交错运行两种实现降低温度、后台负载和时间漂移造成的偏差。报告每阶段的 P50、P90、最小值、最大值和离散度同时保留每个原始 run。不能只报最好一次。基线运行与 Nsight 运行分开若必须比较 Profiler 前后差异单独记录 trace 选项和采样开销。这些数字是实验次数与报告规则不是性能结果。任何真正的秒数、帧率或显存数字都必须引用一个完整的condition_id。七、阶段账本必须绑定完整条件每个性能结果至少绑定以下字段run_id, condition_id, prompt_id workflow_hash, api_prompt_hash, selected_outputs expected_nodes, cached_nodes, actual_executed_nodes comfy_commit, frontend_version, custom_node_lock_hash model_hashes, vae_hash, text_encoder_hash, lora_hashes gpu_uuid, gpu_model, vram, power_limit, clocks cpu, ram, driver, cuda, pytorch precision, quantization, attention_backend, compile_mode cache_mode, startup_class, model_residency_before width, height, frames, fps, batch, seed steps, sampler, scheduler, cfg vae_tiling, preview_mode postprocess_chain_and_parameters ffmpeg_version_and_build, codec, preset, quality pixel_format, filters, container, threads, hwaccel temp_storage, final_storage, filesystem, delivery_semantics queue_policy, concurrency, background_load clock_domain, profiler_mode, trace_path artifact_bytes, artifact_hash, status, error_stage事件行还应包含ts_mono_ns、ts_wall、source、event_type、node_id、node_class、stage_id、gpu_device、gpu_stream和产物路径。持续时间只用同一 clock domain 相减墙上时钟只用于跨系统关联。空白结果表可以这样保存run_idcondition_idstage_idstart_mono_nsend_mono_nscpu_wall_msgpu_mscache_stateartifact_hashstatus待测条件哈希阶段配置必须固化到可哈希清单。硬件、模型、分辨率、帧数、步数之外缓存模式、所选输出、FFmpeg 参数、存储位置和最终交付语义同样属于实验条件。少掉任何一个比较都可能失真。八、用阶段占比决定优化顺序拿到阶段账本后先计算阶段占比 s_i T_i / T_total如果阶段i被加速k倍其他阶段不变端到端加速上限为S_total ≤ 1 / ((1 − s_i) s_i / k)如果一个阶段被完全消除理论上限是S_total ≤ 1 / (1 − s_i)这一步把某个 kernel 快了多少转换成用户最终少等多少。如果采样只占本工作流端到端时间的一部分再激进的采样优化也无法消除排队、加载、VAE、后处理、编码和交付。反过来如果换模型主要减少了加载时间它对热模型连续队列可能没有价值却可能显著改善频繁切换的多模型服务。优化报告应同时回答四个问题哪个阶段是当前条件下的主导阶段改动减少了哪个阶段是否把成本转移到另一个阶段实际执行子图、缓存命中和交付边界是否一致速度变化是否伴随质量、失败率、显存、功耗或产物语义变化九、最小可落地的采集实现不需要先修改所有节点。可以从四层探针开始。第一层放在 ComfyUI 服务端入口。请求通过/prompt验证并进入队列时生成或接收run_id记录queue_accepted_mono_ns、队列位置、所选输出和 prompt 哈希。执行器开始处理时记录execution_start_mono_ns成功、错误和中断都写入同一运行记录。这样 Queue 等待不依赖浏览器时间也不会把网络往返混入服务器队列。第二层放在阶段包装器。不要按每个节点强制同步而是给模型加载、条件编码、采样、VAE、后处理等稳定语义阶段建立开始与结束钩子。包装器同时写入节点 ID、节点类型、缓存状态、CPU 单调时间和可选 CUDA Event。自定义节点若把加载、推理、编码塞在一个函数里必须在函数内部继续分段一个节点一个阶段只是实现巧合不是账本原则。第三层放在外部进程和文件系统。FFmpeg 启动时记录命令模板哈希而不是只保存一条可能含敏感路径的完整命令解析-progress记录退出码。输出先写临时文件成功关闭后再执行规定的交付动作。只有最终路径可读、大小非零、容器探测通过并在协议要求时完成校验和或上传确认才写入delivery_committed_mono_ns。第四层是离线归并器。它按run_id合并 ComfyUI 生命周期、阶段事件、CUDA/Nsight trace、FFmpeg 事件和产物记录检查时钟域、区间重叠、缺失结束事件、缓存清单与实际执行节点之间的矛盾。原始事件不可被聚合表覆盖重新定义阶段后应能从原始事件重算历史结果。采集器还应设置失败闭环。节点异常、OOM、取消、FFmpeg 非零退出、文件损坏和上传失败都要保留已经消耗的阶段时间。只统计成功任务会系统性低估不稳定方案的真实成本。失败运行可以不进入成功任务延迟分布但必须进入失败率、浪费 GPU 时间和端到端可靠性报告。十、四类最常见的错误结论第一类是把缓存命中当成模型加速。纠正方法是同时展示cached_nodes与actual_executed_nodes并用model_warm_recompute作为推理比较基线。第二类是把executing到下一条消息的间隔当作节点 GPU 时间。下一条消息可能被异步工作、网络、UI 更新或别的节点调度影响。GPU 阶段使用 Event 或时间线节点消息只负责关联。第三类是只测到execution_success。如果编码、复制或上传在 ComfyUI 之外成功消息只是中间边界。端到端终点必须是双方约定的交付提交事件。第四类是把 Profiler 运行直接与无 Profiler 运行混在同一分布。CUDA trace、Event trace、内存追踪和频繁同步都可能带来开销甚至改变调度。剖析结果用于归因低侵入基线用于排名两者通过相同condition_id关联但不能混算。结语从 Queue 到视频文件不是一个计时点而是一条可审计的执行链。ComfyUI 消息负责说明调度与缓存状态服务器单调时钟负责端到端阶段边界CUDA Event 和 Nsight 负责解释异步 GPU 时间线FFmpeg 与交付提交负责补齐生成系统之外的最后一段。只有把静态 workflow 还原为实际执行子图把冷启动、热重算、缓存命中和模型切换分开把原生生成、后处理、编码和最终交付分账性能数字才具有可复现的条件也才可能指向正确的优化对象。否则第二次快一倍只说明第二次不同不能说明究竟优化了什么。FAQ为什么不能把 executed 当作节点完成事件ComfyUI 官方说明它只在节点返回 UI 更新时发送完整节点边界应结合 executing、缓存列表和执行器侧观测。Python 计时是否完全没用有用但 GPU 阶段需要同步或使用 CUDA Events否则容易只测到异步提交时间。性能报告最少要写哪些条件硬件、软件版本、工作流哈希、模型、分辨率、帧数、步数、缓存与冷暖启动状态。错误速查卡症状根因定位修复第二次明显更快被简单归因为采样器/注意力变快缺乏阶段归属模型常驻、编译预热、节点缓存、执行子图变化都被混在一起用execution_cached/executing序列 阶段账本核对实际子图用model_warm_recompute作为推理速度基线M01/M03 配对报告阶段直接相加得到总耗时实际存在 GPU / CPU / I/O 重叠检查 Nsight 时间线或分段交叉事件使用区间并集或关键路径资源账本分别报告 CPU / GPU / I/O把静态 workflow JSON 当作实际执行子图缓存命中 Partial Execution 动态展开会改变祖先闭包比对selected_outputs/cached_nodes/actual_executed_nodes三份清单每次运行保存三份清单 预期未执行节点 / 意外执行节点CPUperf_counter测得的时间远低于实际 GPU 耗时CUDA 默认异步没有同步边界在阶段前后各加一次torch.cuda.synchronize()验证用 CUDA Event 或 NsightCPU wall clock 仅作低侵入基线t1 - t0测得时间比实际长很多CUDA Event 同步、Profiler 工具或频繁synchronize改变了被测系统跑两套低侵入基线 Profiling run两套分别报告不要在同一分布中混算用相邻两个executed消息的到达间隔当节点 GPU 时间executed只在节点返回 UI 输出时发送可能漏掉无 UI 节点、混入缓存 UI检查execution_cached列表 显式executed节点节点消息只用于关联GPU 时间以 Event / Nsight 为准把客户端executing时间当作 GPU 完成边界WebSocket 消息含网络、事件循环与序列化延迟同时记录服务器端monotonic戳服务器端用t_execution_start_server − t_queue_accepted_serverexecution_success当作视频已可读编码、复制、上传都在 ComfyUI 之外检查delivery_committed_mono_ns与artifact_hash端到端终点必须以交付提交事件为准把 Profiler 运行与无 Profiler 运行混在同一分布Trace/Event/内存追踪都可能改变调度用同一condition_id关联Profiler 结果用于归因基线用于排名cache_hit_rerun被当作模型加速大量节点被缓存命中推理几乎没跑比对cached_nodes与actual_executed_nodes用model_warm_recompute--cache-none测推理速度“新进程被写成完全冷启动”OS 页缓存、模型文件缓存、容器镜像层可能仍然热检查 OS 页缓存是否受控显式标注进程冷启动OS 页缓存未控制失败任务没有阶段数据只统计成功任务检查采集器是否在异常/OOM/取消路径上写记录失败运行进入失败率 浪费 GPU 时间报告阶段间缺乏唯一 ID 关联多 run / 多 stage / 多 stream 事件无法对齐检查run_id/condition_id/stage_id是否贯穿所有事件所有事件必须带三个 ID ts_mono_nssource优化报告采样快 2 倍但端到端几乎没变采样只占端到端时间的一小部分用S_total ≤ 1/((1−s_i)s_i/k)估算上限先看阶段占比再决定优化对象把 PyTorch memory snapshot 当作全部显存snapshot 只能看到 PyTorch allocator 管理的内存同时记录设备级观测与 Nsight memory trace三者分别报告不互相冒充节点缓存默认开被假设成永远命中缓存依赖输入签名 节点定义指纹修改提示词后看execution_cached列表显式记录cache_mode与cached_nodes哈希报告里没有--cache-none也没有--force-fp16等运行参数实验条件字段缺失检查 condition_id 必填字段清单固化所有可哈希条件到 condition_id