Skip to content

Studio: queue media generation behind model teardown - #8866

Open
divagr18 wants to merge 10 commits into
unslothai:mainfrom
divagr18:fix-studio-generation-teardown-race
Open

Studio: queue media generation behind model teardown#8866
divagr18 wants to merge 10 commits into
unslothai:mainfrom
divagr18:fix-studio-generation-teardown-race

Conversation

@divagr18

Copy link
Copy Markdown

Summary

Fixes #8309.

Studio image and video generation could race with a model replacement or unload. Because Python lock acquisition is not FIFO, a newly queued generation could acquire the generation lock after teardown had been reserved but before teardown completed.

This caused generation requests to incorrectly report that they were cancelled or, for video replacements, observe the temporary unloaded state while the replacement model was still being constructed.

Changes

  • Add condition-based generation admission to the image and video backends.
  • Queue generation while teardown reservations are active.
  • Keep standard video replacement reservations active until the replacement commits or fails.
  • Wake queued generations only after every teardown reservation has drained.
  • Preserve existing explicit user-cancellation behavior.
  • Report the truthful model-not-loaded error after unload or failed replacement.
  • Add invariant checks for teardown reservation underflow.

Regression coverage

Added deterministic threaded tests covering:

  • Generation waiting behind multiple teardown reservations.
  • Successful generation against a replacement model.
  • Generation after unload reporting model-not-loaded.
  • Teardown failures draining their reservations.
  • Video background generation reaching a completed terminal state after replacement.
  • Explicit cancellation behavior remaining unchanged.

Validation

  • 226 runnable video backend tests passed.
  • 29 focused teardown and cancellation tests passed.
  • Ruff checks passed.
  • Python compilation checks passed.
  • git diff --check passed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c90dcd4cca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

while self._teardown_waiters and not cancel.is_set():
# Cancellation uses its independent lock and cannot notify this condition
# while a load owns _lock, so wake periodically only while actually queued.
self._teardown_drained.wait(timeout = 0.1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Let queued cancellation bypass the load's state lock

When a generation wins _generate_lock after a replacement has reserved teardown, it releases that lock and waits on this condition. The replacement can then acquire both _generate_lock and _lock and hold _lock throughout model construction. If the user calls cancel_generate() in that ordering, the independent cancellation lock sets the event, but Condition.wait() must reacquire _lock before returning, so the request remains blocked until the potentially minutes-long load completes; the 100 ms timeout does not make cancellation responsive. Use cancellation waiting that does not require reacquiring the load-held state lock, or avoid holding that lock during construction.

Useful? React with 👍 / 👎.

Comment thread studio/backend/core/inference/video.py Outdated
if self._state is None and self._teardown_waiters == 0:
raise RuntimeError(VIDEO_NOT_LOADED_MSG)
state = self._state
if state is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve request validation while a replacement is loading

When a standard replacement has torn down the old pipeline but not committed the new one, _state is None while _teardown_waiters is nonzero, so this conditional skips _resolve_keyframes, _resolve_references, and _resolve_flow_shifts. Consequently malformed image data, unsupported references, or invalid flow shifts receive a successful /video/generate response instead of the route's synchronous 400/422; the daemon worker only reports a failed job later through polling. Queuing behind replacement should not bypass the input-validation contract.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0a1a3cc6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4848 to 4849
if self._state is None and self._teardown_waiters == 0:
raise RuntimeError(VIDEO_NOT_LOADED_MSG)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require an active replacement before accepting an unloaded job

When an unload supersedes a standard replacement after that replacement has torn down the old state, _loading is cleared but the stale load can retain a teardown reservation until its lengthy construction reaches the next token check. During that interval _state is None and _teardown_waiters is nonzero, so /video/generate is accepted with no incoming model, waits behind the stale worker, and eventually fails through polling with VIDEO_NOT_LOADED_MSG instead of returning the synchronous 409. Treat the waiter as a queueable replacement only when _loading still describes an active load.

Useful? React with 👍 / 👎.

Comment on lines +5111 to +5115
if _defer_shape_validation or (
_validated_state is not None and state is not _validated_state
):
validate_video_request_shape(
fam, width = width, height = height, num_frames = num_frames

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Revalidate all conditioning after the admitted model changes

If begin_generate() validates against the currently loaded family and a replacement reserves teardown before the worker acquires the generation slot, the job now intentionally survives and runs on the replacement. The new _validated_state mismatch branch is fresh evidence that this ordering is handled, but it repeats only shape validation; keyframes, references, and flow shifts are first checked against the old family and are checked against the replacement only later inside the daemon worker. Inputs unsupported by the replacement therefore receive a successful generate response and fail asynchronously rather than preserving the route's synchronous 400/422 contract, so the full conditioning validation must be repeated when the state identity changes.

Useful? React with 👍 / 👎.

Comment on lines +5872 to +5879
with self._generation_cancel_lock:
cancels = set(self._queued_generate_cancels)
if self._active_generate_cancel is not None:
cancels.add(self._active_generate_cancel)
if not cancels:
return False
cancel.set()
for cancel in cancels:
cancel.set()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve ordinary image requests when cancelling the active one

When two image requests reach either generation endpoint concurrently, the second waits on _generate_lock solely because the first is denoising; neither route has a busy-request guard. _generation_slot() now adds that second request to _queued_generate_cancels before it knows whether any teardown exists, and cancel_generate() sets every event in that set. Pressing Stop for the active request therefore also makes unrelated serialized requests fail with the cancellation sentinel, whereas the previous implementation signalled only the active generation. Track teardown/load waiters separately from ordinary generation-lock waiters, or restrict cancellation fan-out to requests actually queued by lifecycle teardown.

Useful? React with 👍 / 👎.

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.

Images: first Generate after a model swap fails with 409 "Diffusion generation was cancelled."

1 participant