Fully-Async val Deadlock
The first validation hangs forever because do_validate blocks the actor's event loop, pinned with py-spy rather than guessed
Issue
A fully-async run reaches its first test_freq validation and hangs
permanently. The driver .out goes silent, tqdm freezes, vLLM Running drops to
0, the val agent pods are reaped by their 3600 s timeout, and the driver still never
advances. No traceback, no dead node, no OOM. It is a deadlock, not a crash: waiting
longer never helps. Because save_freq is scheduled after validation, such a run
never writes a checkpoint at all.
Confirm it is this deadlock before applying the fix:
ps aux | grep 'ray::FullyAsyncRollouter.do_validate'
py-spy dump --pid <pid>Confirmed when "AsyncIO Thread: default" is parked in ray_utils.py:137 at
result().
Reason
Pinned with py-spy, not inferred:
do_validate(fully_async_rollouter.py:706) is a synchronous method, and Ray runs it on the actor'sAsyncIO Thread: default, the event-loop thread.- It calls the synchronous
_validate(inherited fromRayPPOTrainer,ray_trainer.py:643), which reachesgenerate_sequencesand its@auto_awaitwrapper (verl/utils/ray_utils.py:137). - The wrapper's Case 3 (a running loop exists, the caller is not async) does
pool.submit(asyncio.run, coro)and thenfuture.result(), blocking the calling thread, which is the event-loop thread. The wrapper's own comment says it "cannot block the loop thread without deadlock"; it just never anticipated being entered from the loop thread itself. - With the loop frozen, the training generation tasks that were still in flight when
validation began (
"resuming generation early (active tasks remaining: 21)"in the log) can make no progress, so they never return their workers to the shared_worker_queue. - Validation's
_dispatch_to_workers→queue.get()waits for a worker that a frozen task is holding. Validation never finishes →future.result()never returns → the loop stays blocked. Closed cycle.
Ray's startup warning, "Using blocking ray.get inside async actor. This blocks the event loop", is pointing at exactly this.
Why training is fine and only val hangs: training goes through the normal await
path; validation is the only caller that enters through a synchronous do_validate.
Ruled out. Not over-long prompts — the 131072 ValueError in the log is benign
noise caught by the proxy's _handle_chat, and it fires throughout normal training.
Not a missing patch — the relevant PRs were already applied, and the
threading.Queue change fixes the worker queue, not this val path. Not a missing
per-sample timeout — every val trial did write its result.json.
Fix
Break the cycle at either point; doing both is preferred:
- A. Stop blocking the loop. Make
do_validategenuinely asynchronous (asyncawait), or hand the synchronous work to a real executor thread so the event loop stays free.
- B. Drain before validating. Fully drain in-flight training generation before validation starts, instead of resuming early with tasks still outstanding, so validation owns the worker pool for its duration.
Full write-up:
troubleshooting/training_env/fully-async-val-deadlock-doValidate-blocks-eventloop.md· related:troubleshooting/training_env/221-node-oom-disconnect-fullyasync-deadlock.md(a different route to a stuck fully-async run) and Low val scores §1, where the same validation path merely runs out of time instead of deadlocking.