[FS Offloading] Move FSAsyncLookup into a subprocess - #46559
varun-sundar-rabindranath wants to merge 4 commits into
Conversation
Signed-off-by: Varun Sundar Rabindranath <varun-sundar-rabindranath@h100-01.nemg-001.lab.rdu2.dc.redhat.com> Signed-off-by: <>
Signed-off-by: Varun Sundar Rabindranath <varun-sundar-rabindranath@h100-01.nemg-001.lab.rdu2.dc.redhat.com> Signed-off-by: <>
|
Requesting reviews from @tlrmchlsmth @njhill and @robertgshaw2-redhat for FsIOProcess . I ended up using Please advise 🙌 |
tlrmchlsmth
left a comment
There was a problem hiding this comment.
Nice work @varun-sundar-rabindranath
One downside of doing this in a separate process is a lot of added complexity around errors handling and process cleanup. Looks like there are a few things that aren't handled yet in this PR -- I had claude take a look, here are some potential issues:
Details
Confirmed findings- vllm/v1/kv_offload/tiering/fs/io_process.py:75 — wait_until_ready() has no timeout; parent deadlocks if child crashes mid-handshake
If the child process crashes after sending result_handle via the pipe but before completing its wait_until_ready() calls, the parent blocks forever on self._request_queue.wait_until_ready() (line 75) or self._result_queue.wait_until_ready() (line 76).
MessageQueue.wait_until_ready() uses raw socket.recv() with no timeout — it's a collective barrier. The pipe-based timeouts (lines 64, 79) only protect the pipe reads, not the MessageQueue handshake. Trigger: child OOM, segfault, or exception between
sending the handle and completing the barrier.
- vllm/v1/kv_offload/tiering/fs/io_process.py:88 — No child liveness detection; silent hang if subprocess dies at runtime
Once the handshake completes, the parent has no heartbeat or is_alive() check on the child process. dequeue_lookup_results_nowait() returns None on timeout (line 91), which is indistinguishable from "still computing." If the child is OOM-killed or
segfaults mid-lookup, LookupState.result stays None forever — drain_results() just sees no results and breaks (async_lookup.py:137). The only use of is_alive() is in shutdown(). No staleness timeout exists on pending lookups. Trigger: child killed by OS,
unhandled exception in _fs_lookup_worker_main, or NFS hang causing os.path.exists() to block indefinitely.
Plausible findings
- vllm/v1/kv_offload/tiering/fs/io_process.py:85 — enqueue_lookup_batch() has no guard against post-shutdown calls
enqueue_lookup_batch() calls self._request_queue.enqueue(batch) with no check of self._shutting_down. If shutdown() has already been called (setting _shutting_down = True and calling _request_queue.shutdown()), a subsequent flush() → send_lookup_batch()
→ enqueue_lookup_batch() will call enqueue() on a closed MessageQueue. Both operations happen on the scheduler thread, so this depends on the engine shutdown sequence: if scheduling isn't fully quiesced before tier teardown, this sequence is possible.
Full failure mechanism catalog (per your request)
Startup │ Child crashes before sending handle │ Yes — 30s pipe timeout
Startup │ Child crashes after handle, before wait_until_ready() │ No — deadlock (finding 1)
Startup │ Child sends malformed handshake │ Yes — format check + RuntimeError
Runtime │ Child crashes/OOM mid-lookup │ No — silent hang (finding 2)
Runtime │ Filesystem hangs (stale NFS) │ No — os.path.exists() blocks child indefinitely, parent unaware
Runtime │ MessageQueue.enqueue() full/timeout in child │ Partial — child exits, but parent doesn't detect
Shutdown │ Batch in-flight when sentinel sent │ Partial — 5s join then terminate, in-flight results lost
Shutdown │ terminate() doesn't kill child │ No — no fallback to SIGKILL
Shutdown │ enqueue() called after shutdown() │ No (finding 3)
Shutdown │ Double shutdown() │ Yes — _shutting_down guard
Serialization │ FileMapper unpicklable │ Not an issue — simple types, fork context
Resources │ Shared memory leak on unclean exit │ Partial — shutdown() cleans up, but parent crash leaks /dev/shm
Resources │ Zombie child if parent crashes │ Partial — child is daemon=True but blocks in dequeue(indefinite=True)
Ordering │ Results arrive out of order │ Not an issue — keyed by OffloadKey, applied idempotently
Ordering │ Results lost on child crash between dequeue/enqueue │ No — same as runtime crash
I think these might be easier to handle by avoiding the additional process but instead using a lower-level language like Rust to avoid the GIL at a higher granularity. I saw that @orozery had some desire to keep things in Python but I'd like to understand the technical reasons why
|
@varun-sundar-rabindranath thanks for the quick implementation! @tlrmchlsmth I think developing/maintaining python code has many advantages over compiled languages. |
|
closing in favor of C based approach #46713 |
Purpose
TieredOffloading with FS tier on
mainyields poor performance. This is mostly due to lookup delays triggered by the FSAsyncLookupManager.We find that the thread backed FSAsyncLookup is severely impacted by the GIL.
PR #45765 Introduced a timeout for this as a stop-gap. This PR attempts to remove the GIL contention by moving the core part of FSAsyncLookup into a subprocess.
Changes:
Introduce
FsIoProcess. This class,Note that only the core batch lookup (group by requests + os.path.exists) is executed in the subprocess. All other functionality still lives in the main/scheduler thread.
Refactor
AsyncLookupManager.BaseAsyncLookupManager. This class is responsible for,AsyncLookupManagerto be thread-backed, like in main, and specializesend_lookup_batchandtry_recv_result_batchto simply write and read from aSimpleQueue.FSAsyncLookupManagerto interface withFsIoProcessAdditionally
FsIOProcesscan be expanded to handleload_blocks,store_blocksin the future.Test Plan
Test Result