Skip to content

fix: sweep dead process slots only when slots are scarce - #253

Open
keshav9926 wants to merge 3 commits into
Project-HAMi:mainfrom
keshav9926:fix-join-path-liveness-sweep
Open

fix: sweep dead process slots only when slots are scarce#253
keshav9926 wants to merge 3 commits into
Project-HAMi:mainfrom
keshav9926:fix-join-path-liveness-sweep

Conversation

@keshav9926

@keshav9926 keshav9926 commented Aug 8, 2026

Copy link
Copy Markdown

Fixes the O(N²) join path measured in #252.

What the problem is

init_proc_slot_withlock() calls clear_proc_slot_nolock(1) on every join. That sweep walks every occupied slot and calls proc_alive(), which is an fopen("/proc/<pid>/stat") + read + close per slot (src/include/process_utils.h:17). All of it runs under lock_shrreg(), the single region-wide semaphore.

So the Nth process to join does O(N) filesystem syscalls while holding a lock every other joining process needs. Across N processes starting together that is O(N²) serialised work — the shape a TP=N inference job or a pod whose containers all start at once produces.

What this changes

A join now sweeps for liveness only once occupancy reaches three quarters of the table.

Why that is safe: reclaiming a slot whose process died is not needed for a join to be correct. It matters in two places, and both are still covered.

  • Memory accounting. A dead process's slot inflates get_gpu_memory_usage(), which could cause a spurious OOM. oom_check() already calls clear_proc_slot_nolock(1) and retries before it reports OOM (src/allocator/allocator.c:54), so the reclaim still happens exactly where a stale slot changes an outcome.
  • Slot exhaustion. Handled by the threshold. The sweep now runs before the proc_num >= SHARED_REGION_MAX_PROCESS_NUM capacity check instead of after the insert, so a table filled with dead slots is recovered rather than hitting exit_withlock(-1). That case used to be fatal.

Slots that exit cleanup already marked with PID 0 are still compacted on every join. That path reads no files, so it costs nothing.

Measurements

Harness from #252 — fork N children, hold them at a shared start barrier, release them together, time exactly one ensure_initialized() per child; the region is deleted before every round so each round measures cold first-touch: https://gist.github.com/keshav9926/9e8f4c29eebd104ba02891e4733b9dfb

Both arms were built from this tree, one with the patch and one without, and run interleaved — base, fix, base, fix — for 6 iterations of 10 repeats each, so thermal drift and background load hit both arms equally. Values below are the median across the 6 iterations.

procs init p50 before init p50 after
1 0.35 ms 0.27 ms 1.3×
2 0.43 ms 0.30 ms 1.4×
4 0.64 ms 0.42 ms 1.5×
8 1.03 ms 0.63 ms 1.6×
16 1.42 ms 0.71 ms 2.0×
32 2.66 ms 0.99 ms 2.7×
64 6.06 ms 1.11 ms 5.5×

Before, p50 grows 17× from 1 to 64 processes. After, it grows 4×, and the gap widens with process count — which is what you would expect if the removed term was the quadratic one. The remaining growth is the lockf in try_create_shrreg() and the semaphore itself; this PR does not touch either.

wall_ms, init_p95 and init_max are dominated by fork/exit scheduling noise on this hardware and I would not read anything into them.

Test

test/test_proc_slot_reclaim.c — GPU-free, built against the production shared-region sources the same way test_postinit_owner_death is, and registered with ctest.

It pins both halves of the new contract:

  • below the threshold, a join leaves slots held by dead processes in place (children are SIGKILL'd, so exit cleanup never runs and the slot keeps a PID that no longer exists);
  • at the threshold, a join reclaims them, so repeated join-and-die cycles cannot grow the table without bound.

The target is compiled with SHARED_REGION_SWEEP_THRESHOLD=8 so the reclaim path is reachable without spawning 768 processes. The production constant is unchanged; it is now #ifndef-guarded only so the test can override it.

Verified red/green: against main without the fix the test fails (join below the sweep threshold changed the table: expected 4 occupied slots, saw 2); with the fix it passes 5/5 consecutive runs.

What was tested, and what wasn't

  • Environment: WSL2 (Ubuntu 24.04, kernel 6.6.87), NVIDIA GeForce RTX 3050 Laptop GPU, driver 592.82, single GPU, driver libs via /usr/lib/wsl/lib.
  • This change does not touch device allocation or in-container isolation — it only changes when the slot table is swept — so I did not run the GPU allocation suite against it. I have not run it on a multi-GPU datacenter node, and /proc and file-lock behaviour under WSL2 may differ from bare metal. Happy to re-run anywhere if someone has a node available.
  • Only ensure_initialized() is timed. No CUDA context creation, no real workload.
  • The absolute numbers are small. What I think matters is the shape: the cost scaled with process count, which is the thing operators scale up.

Trade-off worth flagging

Between sweeps, slots held by dead processes stay in the table, so proc_num and the usage totals derived from it can be stale for longer than before. oom_check() covers the case where that changes an allocation outcome. If you would rather also bound the staleness in wall-clock terms, a "sweep if the last one was more than N seconds ago" rule would do it, but that needs a timestamp in the shared region and therefore a layout change — happy to do it that way instead if you prefer.

(Disclosure: I use AI assistance in my workflow. The measurements, the code reading and the reasoning above are my own, and I'm happy to walk through any part of it.)

Summary by CodeRabbit

  • Bug Fixes

    • Improved recovery of shared resources after worker processes terminate unexpectedly.
    • Reduced unnecessary process checks during normal worker cleanup, improving reliability when joining shared regions.
    • Added threshold-based reclamation to prevent exhausted process slots from blocking new workers.
  • Tests

    • Added regression coverage for reclaiming slots from forcibly terminated workers, including timeout and cleanup handling.

init_proc_slot_withlock() swept every occupied slot for liveness on every
join. The sweep reads /proc/<pid>/stat once per slot and runs with the
region lock held, so N processes starting together perform O(N^2)
serialised filesystem work. On the harness from Project-HAMi#252, init p50 grows from
0.35ms to 6.06ms going from 1 to 64 concurrent processes.

Reclaiming a slot whose process already died is not needed for a join to
be correct: oom_check() sweeps before it reports OOM, which is where a
stale slot actually changes an outcome. Sweep on join only once occupancy
reaches three quarters of the table, and do it before the capacity check
so a table filled with dead slots is recovered instead of being fatal.
Slots that exit cleanup already marked with PID 0 are still compacted on
every join; that path reads no files.

With this change init p50 at 64 concurrent processes is 1.11ms.

Signed-off-by: keshav9926 <kkakani160@gmail.com>
Covers both halves of the join-path contract: below the sweep threshold a
join leaves slots held by dead processes in place, and at the threshold a
join reclaims them, so repeated join-and-die cycles cannot grow the table
without bound.

The test is GPU-free and builds against the production shared-region
sources the same way test_postinit_owner_death does, with a small
SHARED_REGION_SWEEP_THRESHOLD so the reclaim path is reachable without
spawning 768 processes.

Signed-off-by: keshav9926 <kkakani160@gmail.com>
@hami-robot

hami-robot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: keshav9926
Once this PR has been reviewed and has the lgtm label, please assign archlitchi for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@hami-robot

hami-robot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Welcome @keshav9926! It looks like this is your first PR to Project-HAMi/HAMi-core 🎉

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f1783ad-ee57-4982-8753-70d54fc6bad6

📥 Commits

Reviewing files that changed from the base of the PR and between 22ef358 and 1401431.

📒 Files selected for processing (1)
  • src/multiprocess/multiprocess_memory_limit.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/multiprocess/multiprocess_memory_limit.h

📝 Walkthrough

Walkthrough

The shared-region process-slot logic now performs configurable threshold-based dead-slot sweeping. A GPU-free regression test validates slot retention below the threshold and reclamation at the threshold.

Changes

Process-slot reclamation

Layer / File(s) Summary
Threshold-based process-slot sweeping
src/multiprocess/multiprocess_memory_limit.h, src/multiprocess/multiprocess_memory_limit.c
Adds the configurable SHARED_REGION_SWEEP_THRESHOLD. Sweeping runs at the threshold, refreshes occupancy, and final cleanup removes only PID-0 slots.
Production-path test wiring
test/CMakeLists.txt
Builds and registers test_proc_slot_reclaim with threshold 8, rt and pthread only, and a 30-second timeout.
Process-slot reclamation regression coverage
test/test_proc_slot_reclaim.c
Adds worker lifecycle control, shared-region observation, timeout handling, and checks for below-threshold retention and threshold-triggered reclamation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: chaunceyjiang, archlitchi

Poem

A rabbit watched the worker slots,
Then swept the ones whose lives were lost.
Below the mark, the slots stayed still,
At the threshold, they cleared by will.
“Hop,” said Bun, “the test is bright!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: it limits dead process-slot sweeping to periods when slots are scarce.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hami-robot hami-robot Bot added the size/L label Aug 8, 2026
@coderabbitai coderabbitai Bot added the enhancement New feature or request label Aug 8, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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/multiprocess/multiprocess_memory_limit.h`:
- Around line 43-48: Validate SHARED_REGION_SWEEP_THRESHOLD after its default or
override is defined, rejecting values below 1 or above
SHARED_REGION_MAX_PROCESS_NUM while preserving the valid range. Ensure the
existing zero-threshold behavior is handled according to the intended
sweep-on-every-join semantics in init_proc_slot_withlock().
🪄 Autofix

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: 4ec2fc95-c803-4727-8fee-dbf8e39df331

📥 Commits

Reviewing files that changed from the base of the PR and between 5496322 and 22ef358.

📒 Files selected for processing (4)
  • src/multiprocess/multiprocess_memory_limit.c
  • src/multiprocess/multiprocess_memory_limit.h
  • test/CMakeLists.txt
  • test/test_proc_slot_reclaim.c

Comment on lines +43 to +48
// Slot-table occupancy at which joining a process performs a full liveness
// sweep. See init_proc_slot_withlock(). Overridable at build time so the
// regression test can reach the sweep without spawning 768 processes.
#ifndef SHARED_REGION_SWEEP_THRESHOLD
#define SHARED_REGION_SWEEP_THRESHOLD ((SHARED_REGION_MAX_PROCESS_NUM * 3) / 4)
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate the sweep-threshold range.

Reject overrides below 1 or above SHARED_REGION_MAX_PROCESS_NUM. A value of 0 restores a sweep on every join. A value above capacity makes the full-table check exit before dead slots are reclaimed.

Proposed fix
 `#ifndef` SHARED_REGION_SWEEP_THRESHOLD
     `#define` SHARED_REGION_SWEEP_THRESHOLD ((SHARED_REGION_MAX_PROCESS_NUM * 3) / 4)
 `#endif`
+
+#if SHARED_REGION_SWEEP_THRESHOLD < 1 || \
+    SHARED_REGION_SWEEP_THRESHOLD > SHARED_REGION_MAX_PROCESS_NUM
+    `#error` "SHARED_REGION_SWEEP_THRESHOLD must be within process-slot capacity"
+#endif
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Slot-table occupancy at which joining a process performs a full liveness
// sweep. See init_proc_slot_withlock(). Overridable at build time so the
// regression test can reach the sweep without spawning 768 processes.
#ifndef SHARED_REGION_SWEEP_THRESHOLD
#define SHARED_REGION_SWEEP_THRESHOLD ((SHARED_REGION_MAX_PROCESS_NUM * 3) / 4)
#endif
// Slot-table occupancy at which joining a process performs a full liveness
// sweep. See init_proc_slot_withlock(). Overridable at build time so the
// regression test can reach the sweep without spawning 768 processes.
`#ifndef` SHARED_REGION_SWEEP_THRESHOLD
`#define` SHARED_REGION_SWEEP_THRESHOLD ((SHARED_REGION_MAX_PROCESS_NUM * 3) / 4)
`#endif`
`#if` SHARED_REGION_SWEEP_THRESHOLD < 1 || \
SHARED_REGION_SWEEP_THRESHOLD > SHARED_REGION_MAX_PROCESS_NUM
`#error` "SHARED_REGION_SWEEP_THRESHOLD must be within process-slot capacity"
`#endif`
🤖 Prompt for 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.

In `@src/multiprocess/multiprocess_memory_limit.h` around lines 43 - 48, Validate
SHARED_REGION_SWEEP_THRESHOLD after its default or override is defined,
rejecting values below 1 or above SHARED_REGION_MAX_PROCESS_NUM while preserving
the valid range. Ensure the existing zero-threshold behavior is handled
according to the intended sweep-on-every-join semantics in
init_proc_slot_withlock().

An override above SHARED_REGION_MAX_PROCESS_NUM would keep the sweep from
ever running, so a table full of dead slots would reach the capacity check
and exit -- the case this branch set out to make recoverable. Zero would
sweep on every join and bring back the cost this branch removes. Catch both
at compile time, since the override only exists for the regression test.

Signed-off-by: keshav9926 <kkakani160@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant