Repository navigation
Conversation
In request mode the worker replies COMPUTE_DONE immediately and a background thread does the D2H copy and shared-memory packing before sending OUTPUT_READY. A sleep task releases device memory without waiting for that thread, so any output still in flight can have its tensors freed underneath the copy. OUTPUT_READY is then never sent and the engine-side wait fails with a timeout. This is reachable whenever a colocated trainer alternates generation and sleep/wake cycles: the failure shows up after many healthy steps as a burst of diffusion request timeouts. Track outstanding async outputs and block sleep until packing finishes, mirroring the existing drain on shutdown. If the drain deadline expires the worker logs a warning and proceeds, so a stuck packing thread cannot hang sleep indefinitely. AI assistance (Cursor) was used for this change. Co-authored-by: Cursor Signed-off-by: SamitHuang <285365963@qq.com> Signed-off-by: samithuang <285365963@qq.com>
|
This PR appears to belong to: docs/design/module/diffusion/index.md, docs/design/module/diffusion/offloader.md, docs/design/module/diffusion/diffusion_runtime.md. Module owners: @Isotr0py @princepride @SamitHuang @wtomin @ZJY0516 @RuixiangMa @david6666666 @xuechendi @yuanheng-zhao @lishunyang12 @fhfuih Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment. |
|
Tracking issue: #6024 |
There was a problem hiding this comment.
Pull request overview
Fixes a request-mode race where diffusion outputs may still be undergoing async D2H/shared-memory packing when the worker enters sleep and releases device memory, causing missing OUTPUT_READY messages and engine-side timeouts.
Changes:
- Track in-flight async diffusion outputs via a pending counter + condition variable.
- Add
drain_async_outputs()and invoke it before sleep-related RPCs / sleep tasks so memory isn’t released while packing is active. - Add unit tests covering draining behavior and ensuring non-sleep RPCs don’t drain.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
vllm_omni/diffusion/worker/diffusion_worker.py |
Adds async-output draining and hooks it into sleep entry points to prevent freeing tensors during background packing. |
tests/diffusion/test_async_output_worker.py |
Adds unit tests validating pending tracking, drain semantics, and correct integration with sleep RPC handling. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
fhfuih
left a comment
There was a problem hiding this comment.
Overall LGTM. This PR fixes the conflict between the async output channel and the free-up action of a sleep task.
| # Clamped: only items enqueued by _return_result are counted. | ||
| self._async_output_pending = max(0, self._async_output_pending - 1) |
There was a problem hiding this comment.
Wonder why we have a manual clamp here. It should be matching pairs I think
There was a problem hiding this comment.
it's a defensive safeguard for shutdown and test teardown paths, ensures the counter never underflows to negative values or desyncs the wait_for predicate
…puts-before-sleep
Request-mode rollouts stalled until _ASYNC_OUTPUT_TIMEOUT fired, losing whole batches at once. Three independent defects drop OUTPUT_READY: 1. Per-request tensors are views into one batch tensor. _pack_tensor_if_large sized its decision on the view, so views under the threshold stayed inline -- but pickle serializes a view's entire storage, so a 32-request batch shipped the full batch tensor 32 times (~1.3 GB). That overflows the MessageQueue inline buffer onto the ZMQ XPUB/SUB path, which drops messages under back-pressure. Size the decision on the storage instead; packing still copies only the view. 2. The worker's background D2H/SHM thread can publish OUTPUT_READY before execute_batch registers the batch split map. The pump then cached the batch output under the batch id while consumers waited on per-request ids, hanging every request in the batch. execute_batch now adopts an early-arriving output instead of registering a map nobody consumes. 3. MessageQueue.acquire_write() is single-writer, but the async output thread and the main loop both enqueue on result_mq. Serialize the writes so concurrent enqueues cannot target the same ring block. Also log executor bookkeeping when the engine's async wait times out, so a future stall names the stage that lost the output. AI assistance (Cursor) was used for this change. Co-authored-by: Cursor Signed-off-by: samithuang <285365963@qq.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
…SHM unpack In _result_pump, unpacking SHM for single-request OUTPUT_READY released self._futures_lock between checking _output_futures and saving to _completed_outputs. A concurrent call to wait_output_ready during SHM unpacking would find _completed_outputs empty, register a new future in _output_futures, and get orphaned when the pump cached its own future into _completed_outputs. Unpack SHM first, then atomically pop pending future or cache into _completed_outputs under _futures_lock. Signed-off-by: samithuang <285365963@qq.com> Co-authored-by: Cursor
Agree:
|
|
does this PR solve #5793 ? |
| logger = init_logger(__name__) | ||
|
|
||
| _ASYNC_OUTPUT_THREAD_JOIN_TIMEOUT_S = 10.0 | ||
| _ASYNC_OUTPUT_DRAIN_TIMEOUT_S = 60.0 |
There was a problem hiding this comment.
how is this related to ASYNC_OUTPUT_TIMEOUT?
There was a problem hiding this comment.
They operate at two different layers of the lifecycle and protect against different failure modes:
[ Worker Process ] [ Engine / Client Process ]
Worker finishes forward pass
│
├──> Enqueues D2H/SHM task to background thread
│ and sends COMPUTE_DONE ──────────────────────> Engine waits for OUTPUT_READY
│ (deadline: _ASYNC_OUTPUT_TIMEOUT = 30.0s)
│
Trainer/Engine invokes sleep/wake or memory release
│
Worker calls drain_async_outputs()
(deadline: _ASYNC_OUTPUT_DRAIN_TIMEOUT_S = 10.0s)
│
├── Background thread finishes D2H/SHM (~10-50 ms)
└── Worker safely releases GPU memory
| Parameter | Location | Scope & Role |
|---|---|---|
_ASYNC_OUTPUT_TIMEOUT (30.0s) |
Engine (diffusion_engine.py) |
Total deadline for the engine/client to receive OUTPUT_READY (D2H transfer + SHM write + queue |
| delivery) after compute finishes. | ||
_ASYNC_OUTPUT_DRAIN_TIMEOUT_S (10.0s) |
Worker (diffusion_worker.py) |
Barrier timeout when a worker is asked to sleep or release device memory. It waits for |
| already-enqueued background D2H/SHM tasks to finish so GPU tensors aren't freed while being read. |
Why _ASYNC_OUTPUT_DRAIN_TIMEOUT_S is set to 10.0s:
- Draining is fast: Draining only waits for in-flight D2H memory copies and POSIX SHM writes, which typically complete in under 50–100 ms.
- Preventing timeout inversion: If drain timeout were 60.0s (larger than the engine's 30.0s), a wedged worker background thread could hold the sleep RPC for 60s while the
engine already timed out at 30s, causing desynchronized aborts across processes. Keeping drain timeout at 10.0s guarantees worker memory barriers complete well within the
engine's 30.0s wait window.
…rrency safeguards Update docs/design/feature/async_diffusion_output.md to describe: - Worker drain-before-sleep behavior and timeouts - MessageQueue single-writer synchronization and atomic future resolution - Storage-aware IPC packing and batch-split adoption - LTX-2.3 in verified request-mode models table Co-authored-by: Cursor Signed-off-by: samithuang <285365963@qq.com>
no, #5793 (ResultPump crash on cancelled futures) is addressed directly by #5983, for safe cancellation handling
|
for video generation, I think we need to optimize tensor transmission from engine/server output to client. Also, we should be more concise about what's the meaning of the global env vars and can we avoid them |
Align drain timeout with engine wait window and documentation. Co-authored-by: Cursor Signed-off-by: samithuang <285365963@qq.com>
Add docstring / comment explaining the role and barrier semantics of _ASYNC_OUTPUT_DRAIN_TIMEOUT_S in diffusion_worker.py and document it in docs/design/feature/async_diffusion_output.md. Co-authored-by: Cursor Signed-off-by: samithuang <285365963@qq.com>
Right, for video models (e.g., LTX-2.3, MiniMax-H3), the video payloads (uncompressed raw video tensors) are much larger than image payload. When the Engine/API server sends final outputs over HTTP API to an external user/client, returning raw video arrays (e.g. JSON/base64 of 1.2 GB) over the network is inefficient. A better solution is either compressed video streaming (for web clients) or zero-copy shared-memory pointers (for co-located trainers like verl-omni). There are 2 hops in the transmission pipeline: This PR is a targeted bugfix solving async output drops, worker-to-engine IPC buffer overflow, and pump thread race conditions, and has fixed and optimized Hop 1 via POSIX Shared Memory. For Hop 2 optimization (either compress the video or use zero-copy SHM), I think we should do it in another followup PR.
This PR doesn't add new env var. |
…puts-before-sleep
Please mark it with a new issue and we need to proceed to optimize the transmission. BTW, I remember we have done similar optimization to the WAN model before but it seems that optimization is not repo-wide:) |
track here: #6212 |
…-project#5793) A late result for an aborted/timed-out request could race the result-pump thread's fut.done() check and set_result()/set_exception() call, raising InvalidStateError with nothing to catch it. Since the pump is a singleton, this permanently kills it: the engine stays "healthy" but never completes any job again. Wrap each set_result/set_exception call on a shared future in a narrow try/except InvalidStateError that drops the late resolution instead of crashing the thread. Rebased onto main: vllm-project#6023 rewrote _result_pump/_deliver_batch_split and reverted to plain set_result/set_exception calls, so this race was reproducible again. Reapplied the same guard on the new call sites and added the regression test to tests/diffusion/test_result_pump.py (the test file vllm-project#6023 introduced) instead of a separate file. Signed-off-by: ANURAG KANADE <anuragkanade6@gmail.com>
…llm-project#6023) Signed-off-by: samithuang <285365963@qq.com>
Purpose
Request-level batching (
step_execution=false) stalls: requests hang until_ASYNC_OUTPUT_TIMEOUTfires, and batches fail becauseOUTPUT_READYmessagesare lost or futures orphaned. This PR fixes four independent defects on that
path, all still present on
main.This started as a drain-async-outputs-before-sleep change (the original scope
of this PR). Drain-before-sleep is retained as defensive hardening, but
end-to-end debugging showed it is not what caused the stalls — the four
fixes below are. The PR has been rescoped accordingly.
1. Per-request tensor views ship their entire batch storage (root cause).
_pack_tensor_if_largesized its pack/inline decision on the view:Batch outputs are split into per-request views of one batch tensor. Each view
is under the 1 MB threshold, so it stayed inline — but pickle serializes a
view's whole underlying storage, so a 32-request batch put the full batch
tensor on the wire once per request (~1.3 GB observed). That overflows the
MessageQueueinline buffer (24 MB) onto the ZMQ XPUB/SUB overflow path, andPUB sockets silently drop messages under back-pressure — so
OUTPUT_READYvanished and every request in the batch timed out together.
The fix sizes the decision on
max(view_bytes, storage_bytes). Packing stillcopies only the view, so the SHM payload does not grow.
2. Batch split-map race in
execute_batch.The worker's background D2H/SHM thread can publish
OUTPUT_READYbeforeexecute_batchregisters_batch_split_map[batch_id]. The pump then found nomap and cached the batch-level output under
batch_id, while consumers waitedon per-request ids — hanging every request in the batch.
execute_batchnowadopts an early-arriving output instead of registering a map nobody consumes.
3.
result_mqhas two concurrent writers.MessageQueue.acquire_write()is single-writer: it readscurrent_idx, writesthat block, then advances the index. In async mode the background output thread
enqueues
OUTPUT_READYwhile the main loop enqueuesCOMPUTE_DONE, sounsynchronized writers can target the same ring block and clobber a message.
Writes are now serialized with a lock.
4. Single-request output future race during SHM unpacking.
In
_result_pump, handling single-requestOUTPUT_READYpopped from_output_futuresunder
_futures_lock, then released the lock to performunpack_diffusion_output_shm.If the engine thread called
wait_output_ready()during unpacking, it found_completed_outputsempty and created a new pending future in_output_futures.When
_result_pumpfinished unpacking, it created its own future and stored it in_completed_outputs, leaving the caller's future in_output_futuresorphaned andeternally pending. The fix unpacks SHM first, then atomically resolves
_output_futuresor caches into
_completed_outputsunder_futures_lock.Additionally, the engine now logs executor bookkeeping when the async wait times
out, so a future stall names the stage that lost the output instead of only
reporting the timeout.
Not a duplicate
Per-contribution policy, I checked open PRs for overlap:
(shutdown vs sleep) and does not touch packing or the races.
MessageQueue.dequeuetimeout for payloads thattake the overflow path. Materially different direction: this PR keeps those
payloads off the overflow path entirely by packing them into SHM. The two are
compatible and [codex] Fix diffusion worker timeout for large broadcast payloads #4845 corroborates that the overflow path is fragile.
timeout), [Bugfix][XPU] Fix diffusion output corruption from async D2H copy #5782 / [XPU][Bugfix] Make Omni AR async output path device-agnostic #5569 (XPU async-output correctness), [Model] Cosmos3: Reduce video output transfer overhead #5712 (Cosmos3
model-specific transfer) — all disjoint from these four defects.
I verified defects are still present on current
main.Note on the pickle allowlist
tools/pre_commit/check_pickle_imports.pygained one test-file entry. Theregression test measures the wire size of a diffusion output, and only
picklereproduces the per-view storage duplication under test —
torch.savededuplicates shared storages, so it cannot express the bug. The test serializes
locally constructed tensors and loads no untrusted data.
Test Plan
vLLM-Omni Commit: 3fd9f30 (branch merged with upstream
main@ 5be7541)Unit tests (CPU), regression tests per defect:
End-to-end verification across multiple modalities:
step_execution=false,max_num_seqs=32, 4×GPU),test_freq=save_freq=1exercising continuous rollout → validation → checkpoint (sleep/wake) cycles.step_execution=false, 4×GPU,rollout_tp=2), joint video+audio generation with CLAP + ImageBind reward functions.Test Result
Unit tests:
All unit tests pass cleanly, including the new atomic resolution and IPC storage regression tests.
End-to-end, before vs after:
TimeoutErrorinstep_streaming; all requests in the batch timed out simultaneously.TimeoutErrordue to orphaned futures in_output_futures.ruff checkandruff format --checkclean on all touched files; pre-commit hooks pass.Disclosure
AI assistance (Cursor) was used for the investigation, the patch, and the tests.
A human submitter has reviewed every changed line, ran the commands above, and
can defend the change end-to-end.