Skip to content

ci: prevent orphaned DeepSeek V4 engines on test interruption - #86

Open
luohuan19 wants to merge 3 commits into
hw-native-sys:mainfrom
luohuan19:chore/dsv4-ci-teardown-hardening
Open

ci: prevent orphaned DeepSeek V4 engines on test interruption#86
luohuan19 wants to merge 3 commits into
hw-native-sys:mainfrom
luohuan19:chore/dsv4-ci-teardown-hardening

Conversation

@luohuan19

Copy link
Copy Markdown

Background

The DeepSeek V4 accuracy guard left ~800 GiB of orphaned engines behind whenever the CI runner interrupted it. A bare SIGTERM takes CPython down at SIG_DFL without unwinding the stack, so the finally that reaps the server (started in its own session via start_new_session) never ran. The server survives the signal, is inherited by init, and keeps holding its NPU cards and shared memory.

Test hardening

  • _raise_on_termination(): trap SIGTERM/SIGINT/SIGHUP and re-raise as TerminatedBySignal (a KeyboardInterrupt subclass) so finally teardown runs; deafen the signals first (SIG_IGN) so the runner's escalation cannot abort cleanup mid-flight.
  • _set_pdeathsig() preexec hook: PR_SET_PDEATHSIG backstops the one signal we cannot catch (SIGKILL of pytest) and closes the fork/prctl race.
  • _stop_process_group(term_grace): escalate to SIGKILL within ~1s on the interrupted path (the runner SIGKILLs us ~3s after SIGTERM) and stay patient (20s) on the normal path.
  • Unit tests cover the new paths.

CI workflow

  • Switch the orphan-cleanup step from task-submit --list (truncates the command column to 77 chars, so the marker grep can silently miss and leave tasks holding devices) to --find, which matches the untruncated command and prints bare task-ids; also report how many tasks were killed.
  • Raise task-submit --timeout 1200 -> 3600 to tolerate longer device acquisition.

Testing

  • Non-integration unit tests pass: pytest tests/test_deepseek_v4_accuracy.py -k "not matches_expected_text" -> 7 passed.
  • The integration case needs NPU + model and runs in CI.

Known non-blocking notes (from review)

  1. The process.wait(timeout=10) after SIGKILL in _stop_process_group is hardcoded and not bounded by term_grace; on the interrupted path a direct child in D state could exceed the ~3s window (the group SIGKILL has already been delivered, so no orphan results).
  2. _set_pdeathsig calls ctypes.CDLL("libc.so.6") inside the preexec_fn (dlopen-after-fork); could be moved to import time.
  3. The CI step assumes task-submit --find exits 0 when there is no match — worth confirming so a clean run is not reported as a failure.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16262404-d3f1-4f4e-b9ee-b31c7ad3ed41

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds signal-aware DeepSeek V4 subprocess teardown, bounded process-group cleanup, and tests for termination behavior. CI guard timeouts increase for Qwen3 and DeepSeek V4, while orphaned task-submit cleanup switches to marker-based task lookup.

Changes

CI guard resilience

Layer / File(s) Summary
Signal handling primitives
tests/test_deepseek_v4_accuracy.py
Adds temporary signal handlers, termination signaling, and parent-death handling for the server subprocess.
Bounded process shutdown
tests/test_deepseek_v4_accuracy.py
Adds configurable termination grace periods and bounds descendant shutdown accordingly.
DeepSeek lifecycle integration and validation
tests/test_deepseek_v4_accuracy.py
Applies signal-aware startup and teardown to the completion guard and tests grace periods, repeated signals, and handler restoration.
CI timeout and orphan cleanup
.github/workflows/ci.yml
Extends Qwen3 and DeepSeek V4 task timeouts and replaces orphan lookup with marker-based cleanup and reporting.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant DeepSeekTest
  participant DeepSeekServer
  participant ProcessGroup
  Runner->>DeepSeekTest: deliver SIGTERM
  DeepSeekTest->>DeepSeekTest: raise TerminatedBySignal
  DeepSeekTest->>ProcessGroup: stop with SIGTERM grace
  ProcessGroup->>DeepSeekServer: send SIGTERM
  ProcessGroup->>DeepSeekServer: escalate to SIGKILL if needed
  DeepSeekTest-->>Runner: complete teardown
Loading

Possibly related PRs

Poem

A rabbit hopped through CI’s bright lane,
Longer timeouts eased the strain.
Signals came, but cleanup stayed,
Orphan tasks were found and laid.
“Hop, hop!” the subprocess cried—
Safe teardown now runs beside!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: hardening DeepSeek V4 CI teardown to prevent orphaned engines on interruption.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the teardown hardening and workflow updates.
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.

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces robust signal handling and process termination mechanisms to prevent orphaned engine processes during test runs, especially when terminated by a CI runner. It adds a context manager to translate termination signals into a custom exception, uses PR_SET_PDEATHSIG to ensure child processes are killed if the parent dies, and adjusts termination grace periods based on whether the run was interrupted. The review feedback suggests loading libc at the module level to avoid unsafe dlopen calls after fork, utilizing this pre-loaded instance in _set_pdeathsig to maintain async-signal safety, and replacing the fragile use of sys.exc_info() in the finally block with an explicit except TerminatedBySignal block to set an interruption flag.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tests/test_deepseek_v4_accuracy.py
Comment thread tests/test_deepseek_v4_accuracy.py Outdated
Comment thread tests/test_deepseek_v4_accuracy.py

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_deepseek_v4_accuracy.py (1)

291-306: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Post-SIGKILL reap wait ignores term_grace, undermining the interrupted-path budget.

_stop_process_group correctly bounds the first wait (line 291) and the descendant sweep (line 311) by term_grace, but the fallback process.wait(timeout=10) at line 298 (reached after escalating to SIGKILL) is still hardcoded. On the interrupted path (term_grace=SIGNAL_TERM_GRACE_SECONDS=1.0), if the process doesn't die within 1s of SIGTERM, this can add up to 10 more seconds — blowing well past the "~3s" budget the header comment (lines 47-51) says the whole teardown must fit inside, and letting the runner's own SIGKILL hit pytest before the final descendant-kill sweep (lines 312-321) ever runs. That's exactly the orphan scenario this PR sets out to prevent, and it's most likely to bite precisely when a process is stuck on NPU/device I/O and slow to die.

test_stop_process_group_honours_term_grace (430-449) doesn't catch this: it only asserts waits[0], not the second wait() call.

🔧 Proposed fix: bound the reap-wait by term_grace too
+SIGKILL_REAP_SECONDS = 10.0
+
 def _stop_process_group(
     process: subprocess.Popen, term_grace: float = NORMAL_TERM_GRACE_SECONDS
 ) -> None:
     ...
         try:
-            process.wait(timeout=10)
+            process.wait(timeout=min(SIGKILL_REAP_SECONDS, term_grace))
         except subprocess.TimeoutExpired:

And extend the test to catch a regression:

     _stop_process_group(StuckProcess(), term_grace=SIGNAL_TERM_GRACE_SECONDS)

     assert waits[0] == SIGNAL_TERM_GRACE_SECONDS
+    assert waits[1] == SIGNAL_TERM_GRACE_SECONDS
     assert signals[:2] == [signal.SIGTERM, signal.SIGKILL]

Also applies to: 430-449

🤖 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 `@tests/test_deepseek_v4_accuracy.py` around lines 291 - 306, Update
_stop_process_group so the post-SIGKILL process.wait call uses term_grace
instead of the hardcoded 10-second timeout, preserving the teardown budget and
allowing the descendant sweep to run promptly. Extend
test_stop_process_group_honours_term_grace to assert the second wait also
receives term_grace.
🤖 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 @.github/workflows/ci.yml:
- Around line 277-279: Update the orphan-task cleanup loop around task-submit
--kill so n increments only when the kill command succeeds; preserve the warning
output for failed terminations and ensure the final killed-task summary reflects
successful kills only.

---

Outside diff comments:
In `@tests/test_deepseek_v4_accuracy.py`:
- Around line 291-306: Update _stop_process_group so the post-SIGKILL
process.wait call uses term_grace instead of the hardcoded 10-second timeout,
preserving the teardown budget and allowing the descendant sweep to run
promptly. Extend test_stop_process_group_honours_term_grace to assert the second
wait also receives term_grace.
🪄 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

Run ID: be7a1303-f72a-4d34-a501-8d89333e1a9d

📥 Commits

Reviewing files that changed from the base of the PR and between 49297c2 and 9d33a51.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • tests/test_deepseek_v4_accuracy.py

Comment thread .github/workflows/ci.yml Outdated
@luohuan19
luohuan19 force-pushed the chore/dsv4-ci-teardown-hardening branch 4 times, most recently from a85fd20 to ede9e36 Compare July 15, 2026 01:26
The DeepSeek V4 accuracy guard left ~800 GiB of orphaned engines behind
whenever the CI runner interrupted it: a bare SIGTERM takes CPython down at
SIG_DFL without unwinding the stack, so the `finally` that reaps the server
(started in its own session via start_new_session) never ran, and the engine
lived on holding its NPU cards and shared memory.

Test hardening:
- _raise_on_termination(): trap SIGTERM/SIGINT/SIGHUP and re-raise as
  TerminatedBySignal (a KeyboardInterrupt) so `finally` teardown runs; deafen
  the signals first so the runner's escalation can't abort cleanup mid-flight.
- _set_pdeathsig() preexec hook: PR_SET_PDEATHSIG backstops the one signal we
  cannot catch (SIGKILL of pytest), closing the fork/prctl race.
- _stop_process_group(term_grace): escalate to SIGKILL within ~1s on the
  interrupted path (the runner SIGKILLs us ~3s after SIGTERM) and stay patient
  (20s) on the normal path.
- Cover the new paths with unit tests.

CI workflow:
- Switch the orphan-cleanup step from `task-submit --list` (truncates the
  command column to 77 chars, so the marker grep can silently miss) to
  `--find`, which matches the untruncated command and prints bare task-ids;
  report how many tasks were killed.
- Raise task-submit --timeout 1200 -> 3600 to tolerate longer device
  acquisition.
- Load libc.so.6 at module import instead of inside the preexec_fn, so the
  child does not dlopen after fork (unsafe if another thread held the loader
  lock across the fork); _set_pdeathsig() now uses the pre-loaded handle.
- Replace the fragile sys.exc_info() probe in the finally block with an
  explicit `except TerminatedBySignal: interrupted = True; raise`.
Increment the killed counter inside the success branch so the
"killed N orphaned task(s)" summary matches the warnings and never
overcounts a kill that actually failed.
)


class TerminatedBySignal(KeyboardInterrupt):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

非特定模型相关的部分不太适合放这个文件,是否单独提出去

assert "still alive after SIGKILL" in capsys.readouterr().out


def test_stop_process_group_honours_term_grace(monkeypatch) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这几个用例的作用是什么

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants