Skip to content

fix: unwedge Optimized Cold Start on ComfyUI v0.3.68+ - #146

Open
KarrixLee wants to merge 1 commit into
mainfrom
fix/optimized-cold-start-prompt-worker
Open

fix: unwedge Optimized Cold Start on ComfyUI v0.3.68+#146
KarrixLee wants to merge 1 commit into
mainfrom
fix/optimized-cold-start-prompt-worker

Conversation

@KarrixLee

Copy link
Copy Markdown
Collaborator

Problem

Runs on machines with Optimized Cold Start (optimized_runner) enabled hang forever in running — no error, no traceback, no node output. Reported log ends at:

Executing prompt: 3702ebf1-e065-46d0-ad64-8d9240b448c2
Created global client session with optimized connection pooling
Status: running

Root cause

ComfyUI v0.3.68 changed PromptExecutor(server, cache_type, cache_size)cache_args (a dict), and execute_async began dereferencing self.cache_args["ram"]. Our forked prompt_worker constructed PromptExecutor(server) with no cache config, so cache_args stayed None.

From v0.20.0 that dereference sits above execute_async's try:

742  self.add_message("execution_start", ...)          <- last thing the user sees
745  ram_headroom = int(self.cache_args["ram"] * ...)  <- TypeError
750  try:

So it raised right after execution_start, killed the daemon worker thread, and q.task_done() was never called. The traceback went to bare stderr via threading.excepthook instead of through DualHandler — which is why nothing showed up in machine logs.

Broke at v0.3.68 (2025-11-04), not v0.30. On v0.3.68–v0.4.x the same deref sits at the end of the node loop, so it died after the first node; v0.20.0 hoisted it above the try, turning it into "dies instantly".

Note: ComfyUI renumbered releases — v0.3.77 → v0.4.0 → … → v0.30.2. v0.30.0 is ~9 months newer than v0.3.68.

Blast radius

Optimized runner only. prompt_worker lives on _ComfyDeployRunner, whose sole subclass is ComfyDeployRunnerOptimizedImports. ComfyDeployRunner extends BaseComfyDeployRunner and runs ComfyUI's real main.py as a subprocess, so it never drifted.

Changes

my_app.py

  • build_prompt_executor() introspects PromptExecutor.__init__, passing cache_type=CLASSIC + complete cache_args on v0.3.68+ and falling back to the bare call on ≤v0.3.67. CLASSIC is pinned deliberately rather than adopting v0.30's RAM_PRESSURE default — CLASSIC is what this runner always effectively used, and RAM-pressure eviction is untested against the model caches held across Modal memory snapshots.
  • Guard e.execute() so an executor exception can never again kill the worker: log via logging, emit execution_error so the run is marked FAILED, always call task_done.
  • Merge item[5] (sensitive-keys slot added in v0.3.68) back into extra_data + pass process_item to strip it from /history. Without this, comfy.org API-node auth was silently dropped on optimized runners.

session.py

  • SESSION_OPTIMIZED_RUNNER as single source of truth. Creation hardcoded False (disabled in 1efd895 when GPU memory snapshotting shipped) while increase_timeout read the real DB flag — so on a flagged machine the session ran on ComfyDeployRunner but the timeout extension was spawned against ComfyDeployRunnerOptimizedImports, a different Modal class, so the container was never extended and the session died early. Sessions stay pinned to non-optimized; this only stops the two call sites disagreeing.

Tests

tests/api/modal/test_prompt_worker.py — 6 passing. AST-extracts the real functions (my_app.py can't be imported: needs modal + generated cd_config) and drives them against fakes mirroring v0.30.x contracts. Includes a test reproducing the original wedge: thread dies, item stuck in currently_running, no error to client. The constructor logic was also checked against the real PromptExecutor signature at every tag v0.3.64 → v0.30.2.

Deploy note

my_app.py is re-copied from the API's current source on every machine deploy, so existing machines need a redeploy to pick this up — deploying the API alone won't unstick them.

🤖 Generated with Claude Code

Runs on machines with `optimized_runner` enabled hung forever in "running"
with no error, no traceback, and no node output after "Executing prompt".

ComfyUI v0.3.68 changed PromptExecutor(server, cache_type, cache_size) to
take `cache_args` (a dict), and execute_async began dereferencing
self.cache_args["ram"]. Our forked prompt_worker constructed
PromptExecutor(server) with no cache config, so cache_args stayed None.

From v0.20.0 that dereference sits ABOVE execute_async's try/except:

    742  self.add_message("execution_start", ...)   <- last thing the user sees
    745  ram_headroom = int(self.cache_args["ram"] * ...)   <- TypeError
    750  try:

so it raised immediately after execution_start, killed the daemon worker
thread, and q.task_done() was never called. The traceback went to bare
stderr via threading.excepthook rather than through DualHandler, which is
why nothing surfaced in machine logs.

Only the optimized runner is affected: prompt_worker lives on
_ComfyDeployRunner, whose sole subclass is ComfyDeployRunnerOptimizedImports.
ComfyDeployRunner extends BaseComfyDeployRunner and runs ComfyUI's real
main.py as a subprocess, so it was never touched by the drift.

my_app.py:
- build_prompt_executor() introspects PromptExecutor.__init__ and passes
  cache_type=CLASSIC plus complete cache_args on v0.3.68+, falling back to
  the bare call on <= v0.3.67. CLASSIC is pinned deliberately rather than
  adopting v0.30's RAM_PRESSURE default -- CLASSIC is what this runner has
  always effectively used (cache_type=False falls through to
  init_classic_cache), and RAM-pressure eviction is untested against the
  model caches held across Modal memory snapshots.
- Guard e.execute() so an executor exception can never again kill the worker
  thread: log via logging (so it reaches the log stream), emit
  execution_error so the run is marked FAILED, and always call task_done.
- Merge item[5] (the sensitive-keys slot added in v0.3.68) back into
  extra_data and pass process_item to strip it from /history. Without this
  comfy.org API-node auth was silently dropped on optimized runners; the
  custom node already pushes the 6-tuple correctly.

session.py:
- Introduce SESSION_OPTIMIZED_RUNNER as the single source of truth. Session
  creation hardcoded False (disabled in 1efd895 when GPU memory snapshotting
  shipped) while increase_timeout read the real DB flag, so on a flagged
  machine the session ran on ComfyDeployRunner but the timeout extension was
  spawned against ComfyDeployRunnerOptimizedImports -- a different Modal
  class, so the live container was never extended and the session died early.
  Sessions stay pinned to the non-optimized runner; this only stops the two
  call sites from disagreeing.

Note: my_app.py is re-copied from the API's current source on every machine
deploy, so existing machines must be redeployed to pick this up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant