fix: recover the post-init lock after process death - #248
Conversation
Signed-off-by: iemAnshuman <asquare567@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe post-initialization lock now uses a POSIX record lock with local process and thread guards. Child reinitialization clears lock state. New GPU-free CTest coverage verifies owner-death recovery and thread serialization. ChangesPost-init locking
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant postInit
participant lock_postinit
participant LocalGuard
participant RecordLock
postInit->>lock_postinit: acquire post-init lock
lock_postinit->>LocalGuard: serialize threads in the process
LocalGuard->>RecordLock: acquire fixed-offset fcntl lock
RecordLock-->>lock_postinit: return lock status
lock_postinit-->>postInit: continue or report lock error
postInit->>lock_postinit: release post-init lock
lock_postinit->>RecordLock: release fixed-offset lock
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/libvgpu.c`:
- Around line 897-907: Update the lock failure branch in the postinit handling
around lock_postinit() to assign NVML_ERROR_UNKNOWN instead of
NVML_ERROR_TIMEOUT, while preserving the warning log and successful
set_task_pid()/unlock_postinit() flow.
In `@test/test_postinit_owner_death.c`:
- Around line 361-406: Add the declaration header for log_utils_init by
including src/include/log_utils.h alongside the existing includes in
test_postinit_owner_death.c. Ensure this include is present before main calls
log_utils_init(), without changing the test logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 91d1b4d9-d34b-43c2-a712-58015882370c
📒 Files selected for processing (6)
CMakeLists.txtsrc/libvgpu.csrc/multiprocess/multiprocess_memory_limit.csrc/multiprocess/multiprocess_memory_limit.htest/CMakeLists.txttest/test_postinit_owner_death.c
Signed-off-by: iemAnshuman <asquare567@gmail.com>
Signed-off-by: iemAnshuman <asquare567@gmail.com>
|
Tested this on a 8×RTX 3090 node (driver 580.126.20, CUDA 13.3 Docker). Test results:
The stress test verified that after the lock holder is killed, all 64 concurrent waiters recover correctly and the lock remains reusable post-recovery. The vLLM inference test confirmed no regression in real GPU workload scenarios. |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: archlitchi, iemAnshuman The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
Hi @iemAnshuman , I did some follow-up measurements on the record-lock acquisition strategy in this PR, specifically the choice of non-blocking First, to be clear: the owner-death correctness fix itself looks sound in the case I tested. I am not reporting a failure to recover the lock. The question here is narrower: whether polling is the right default acquisition strategy for the normal local shared-region cache path. The implementation currently explains the choice as:
That made me curious about the cost of this workaround on a normal local filesystem, especially because the shared-region/postInit path is already important to the high-density initialization behavior discussed in Project-HAMi/HAMi#1662. 1. Contention experimentI compared two otherwise equivalent POSIX record-lock loops:
Each worker holds the lock for 300 ms to model a serialized expensive postInit operation. All workers start concurrently. Results:
Both modes completed with zero failures. For the 300-worker run, the latency distribution was: F_SETLKW
F_SETLK + polling
So at high N the unavoidable serialized 300 ms critical section dominates, but polling still adds measurable tail/wall latency. At smaller N the proportional difference is considerably larger. I don't think these numbers by themselves establish a serious regression; they mainly show that the polling policy has a real cost on the local-filesystem case. 2. Owner-death recovery latencyI then tested the specific case this PR is intended to fix. The owner acquires the record lock and is killed while holding it. A waiter is already contending for the same lock. I measured how long the waiter takes to acquire after owner death. 100 trials per mode: F_SETLKW
F_SETLK + polling/backoff
So both mechanisms recovered correctly in all of these trials, but the polling implementation had substantially higher owner-death recovery latency, particularly in the tail:
This behavior makes sense: after the kernel releases the dead owner's record lock, an 3. Existing regression testI also looked through The test does a useful correctness check:
It also separately verifies same-process thread serialization, which is important because POSIX record locks are process-associated. One thing the test currently doesn't exercise is recovery latency.
Therefore a waiter recovering in 20 ms, 150 ms, 1 second, or several seconds would all satisfy the same correctness assertion. The test cache is also created from: so the regression test itself is currently exercising a local 4. Question about the NFS tradeoffI have not tested the NFS behavior described in the source comment yet, so I don't want to claim that If some NFS lock managers really keep blocked What I'm wondering is whether that behavior needs to determine the default strategy for the normal local shared-region cache as well. Would one of these approaches make sense?
I'm particularly interested in (4), because right now I can reproduce the local-filesystem cost of polling, but I have not independently reproduced the NFS failure mode that polling is protecting against. If there is a specific NFS server/client configuration where the delayed-grant behavior was observed, I'm happy to reproduce the same setup and run both acquisition strategies against it. For now my conclusion is only:
Happy to share the benchmark source/raw CSVs or adapt this into a repository benchmark if that would be useful. |
|
@Raja-89 thanks for measuring this. Your /tmp benchmark is a fair lock strategy comparison on a local filesystem. It shows that the current polling policy adds delay. I also agree that the CTest checks correctness, but does not measure recovery time. I checked the validation behind baec149. The first version used F_SETLKW. I tested it on volga:/uno/work/aagrawal, mounted as NFSv4.2 with local_lock=none. The corrected harness placed all 16 waiters on the target lock before killing the owner. Every waiter eventually recovered in the three longer runs. However, the last waiter completed after about 30 seconds in one run and about 60 seconds in two runs. With the final F_SETLK polling code on the same NFS path, all 16 waiters recovered in 10 of 10 runs. This NFS result is why I changed the first version. The two tests answer different questions. Your test measures the cost of polling on a local filesystem. Mine measures delayed wake behavior after owner death on one NFS setup. Together, they show that one policy may not be right for both filesystems. I would test option 1 next. We could use F_SETLKW on known local filesystems and keep fresh F_SETLK attempts for NFS and unknown filesystems. Both methods use the same record lock byte, so they remain compatible. Before changing the code, I want to run the same harness on both paths and define which filesystems should use each method. I would leave the global retry cap unchanged until we measure the NFS request rate with a smaller cap. I also agree that we should measure recovery latency in a separate benchmark. A strict CTest limit may be unreliable on shared CI runners. Please share the benchmark source and raw CSV files. I can publish the NFS harness and traces, then run the same benchmark on both /tmp and the NFSv4.2 path. |
|
@iemAnshuman thanks for the NFS results and for explaining the reason behind switching from F_SETLKW to fresh F_SETLK attempts. I've published the /tmp owner-death benchmark source and the raw 100-run CSVs for both strategies in #250: https://github.com/Raja-89/HAMi-core/tree/evidence/owner-death-benchmark/evidence/owner-death The CSVs preserve the original per-run measurements; I only converted the original text output into mode,recovery_us,recovery_ms columns. The README keeps the scope limited to the local /tmp filesystem. I agree that the /tmp result shouldn't be generalized to NFS. Your NFSv4.2 results make the filesystem-aware split worth testing next: F_SETLKW for known local filesystems while retaining fresh F_SETLK attempts for NFS and unknown filesystems. I'll keep the retry-cap question separate until we have the NFS request-rate measurements. Once you publish the NFS harness/traces, I can use the same setup for further comparison. |
Problem
If a process is killed while holding
sem_postinit, the semaphore stays locked. On a node with four A100 GPUs, none of the 4 waiters and none of the 16 waiters acquired it. Every waiter reached the retry limit after waiting between 327.4 and 328.0 seconds.When this happens in
postInit(), the code setspidfound = 0. Later CUDA kernel launches then skip SM rate limiting.Raw logs, commands, and checksums
Change
This change uses a POSIX record lock on one byte of the existing shared cache file. The lock protects host PID detection in
postInit(). It is released when the ownerprocess exits, including after SIGKILL.
The lock uses a fixed byte outside the mapped shared region. A static assertion makes the build fail if
shared_region_tgrows to that byte.sem_postinitremains inshared_region_t, so its size and field positions do not change.POSIX record locks belong to a process rather than one thread. A local guard inside each process keeps its threads in order. The
fork()handler clears this local state in the child.An earlier version used
F_SETLKW. On the tested NFSv4 mount, some blocked calls took 30 or 60 seconds to wake after the owner died. Other waiters had already acquired and released the lock.The final version uses repeated
F_SETLKcalls that do not block. Each waiter pauses for a slightly different time after a busy result. The pause starts at about 10 milliseconds and grows to at most 1 second. This avoids relying on delayed NFS wake events and reduces the chance that many waiters retry at the same time.The code retries when
fcntl()returnsEACCESorEAGAIN. There is no time limit for normal contention. Other errors are logged and returned. Normal contention can no longer reach the old 330 second path that disables host PID detection and SM rate limiting.Test
Added a CTest that uses the production shared region code and does not need a GPU. It does the following:
It queues two waiters behind a live owner and confirms that neither enters early.
It sends SIGKILL to the original owner and confirms that one waiter acquires the lock.
It sends SIGKILL to that waiter and confirms that the second waiter acquires and releases the lock.
It confirms that threads in the same process never enter at the same time.
It acquires the lock again to confirm that it remains usable.
Linux validation is complete:
The full Linux and CUDA build passed.
CTest passed once through the build and in 10 more direct runs.
The edited source produced no compiler warnings.
Focused Clang checks showed no findings on the changed lines.
On the NFSv4 cache path, all waiters recovered in 3 of 3 runs with 4 waiters, 10 of 10 runs with 16 waiters, and 3 of 3 runs with 64 waiters.
Stress runs recovered 300 of 300 waiters in 0.4509 seconds and 512 of 512 waiters in 1.1021 seconds.
A traced run with 16 waiters recorded 101
F_SETLKcalls and noF_SETLKWcalls at the selected lock byte.The whitespace and checksum checks passed.
Upgrade requirement
All processes that share one cache file must use the same locking method. A binary that only uses the semaphore does not take the record lock. If old and new processes
run together, both could enter the protected section at the same time.
Drain the node or restart the affected processes together. Do not use a rolling upgrade while old and new processes share the cache.
AI disclosure
I used Chatgpt while analyzing the lock protocol, and creating the test. I reviewed the final diff and take responsibility for it.
Refs Project-HAMi/HAMi#1662 and #243.
Summary by CodeRabbit
New Features
Tests