Skip to content

Stop hanging on dead processes and make seeded image edits repeatable - #256

Merged
merceod merged 8 commits into
mainfrom
main-side-fixes
Sep 21, 2026
Merged

merceod merged 8 commits into
mainfrom
main-side-fixes

Conversation

@merceod

@merceod merceod commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Two problems on main that came out of the same test runs, fixed together.

A dead worker or conductor hung the server

Nothing watched the process handles, so a worker that failed to initialize or got killed left the server up but unusable, with clients waiting for the 600 s timeout and /health still green, and a killed conductor left orphaned workers holding their GPUs. A dead worker or conductor is now fatal. The conductor and the API server poll their child handles, waiting requests get a 503 that names the dead process, new ones are refused, the remaining processes are torn down and the entrypoint exits 1. Each worker also runs a small watchdog and leaves on its own if the conductor is gone, and the data worker drops its in-flight requests when it stops so nothing is left in /dev/shm. SIGINT stops still exit 0, and SIGTERM still reports 143 as on main (uvicorn re-raises the signal).

Seeded image-to-image was not repeatable

Two runs of /v1/images/edits with the same seed gave the same picture with about one gray level of noise on most pixels, while text-to-image was bitwise. Stage hashes pointed at the VAE encoder, which samples its posterior with randn_like on the global RNG. It keeps sampling, but the noise now comes from the request seed (offset so it is a different stream from the diffusion noise) and is passed in as a tensor input, so torch.compile and CUDA graphs stay out of it.

Tests

13 CPU tests for the liveness paths and 3 for the seeded VAE draw. The existing API server stub in test_request_failure_propagation.py gained two attributes.

Checked on H200s

  • Every worker fails at init. Exit 1 within 8s with nothing left behind (main hangs with zombie workers and never binds).
  • One worker fails at build. Exit 1 in 5s and the healthy worker is terminated (main hangs and the healthy worker keeps its GPU).
  • SIGKILL a worker mid load. Exit 1 in 3s (main hangs).
  • SIGKILL a worker mid request. The client gets an error chunk in 2s and the server exits 1 in 6s with nothing left in /dev/shm (main leaves the client waiting for the 600s timeout).
  • SIGKILL the conductor during init or mid request. Exit 1 in 2 to 4 s with both workers gone (main hangs and the workers are orphaned holding their GPUs).
  • Edits with seed 99 twice. Identical, and the same image as native i2i with that seed (on main 80%+ of the pixels differ).
  • bagel, cfg-parallel, TP2, qwen3tts and cosmos3 outputs and latencies are unchanged

(1) upon stop, the data worker iterated tensor_uuid_to_metadata_per_request, which is only populated when result chunks appear. Replaced it with an explicit set in_flight_requests.
(2) has the health check return a 503 when the API server detects that the conductor is down
(3) Set up the dynamo integration api server to also check for the dead conductor
(4) added a watchdog thread on the conductor that checks api server health (like the worker -> conductor watchdog

@NSagan271 NSagan271 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed some further bug fixes, think it's good to merge now. @merceod if you have time can you sanity check the changes I made to the dynamo worker and merge? Otherwise I can do that in the morning

Comment thread mstar/api_server/entrypoint.py Outdated
req.error = message
req.error_status = 503
req.event.set()
if self.on_fatal is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the conductor dies after finalize_setup() starts the message thread but before on_fatal is assigned, the thread records the failure and exits without stopping the server. The later callback assignment does not trigger shutdown

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could move the fatal_error check inside the existing request_lock, alongside the pending request registration? That should closes the race

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread mstar/api_server/entrypoint.py Outdated
since the conductor's per-request seed is derived from
``hash(request_id)``); otherwise a fresh uuid4 is generated.
"""
if self.fatal_error is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

submit_request() checks fatal_error before taking request_lock. If the fatal handler runs before this request is registered, it misses the request, and nothing signals its completion

@merceod
merceod merged commit f5aa0d5 into main Sep 21, 2026
3 checks passed
@merceod
merceod deleted the main-side-fixes branch September 21, 2026 09:24
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.

3 participants