Stop six backend tests waiting on the clock instead of on a signal by danielhanchen · Pull Request #8975 · unslothai/unsloth · GitHub
Skip to content

Stop six backend tests waiting on the clock instead of on a signal - #8975

Merged
danielhanchen merged 2 commits into
mainfrom
ci-sleeps
Aug 16, 2026
Merged

Stop six backend tests waiting on the clock instead of on a signal#8975
danielhanchen merged 2 commits into
mainfrom
ci-sleeps

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Six backend test files spend most of their runtime asleep rather than working, waiting out a fixed window when they could be waiting for the event they actually care about.

Measurement

Same worktree, off current main, six files together:

run 1 run 2
before 118.0s 118.8s
after 56.5s 55.3s

404 passed, 3 skipped in every run, before and after. Backend tests runs on four Python versions, so this is roughly 4 runner-minutes per run.

What changed

test_tool_output_streaming.py, 61.5s to 32.1s, is the bulk of it. Seven grandchild processes were fused with sleep 3 and the test then waited 4s or 11s for them. The fuse is now a gate file the test owns, so the grandchild lives indefinitely and the assertion is stronger than "3 is greater than 1".

test_orphaned_children.py, 17.2s to 4.9s. The test took _terminate_validation_server's grace default of 5.0 and paid it four times. It now passes grace = 1.0; the sibling test added in the same commit already passes 0.2. The production default is untouched.

test_startup_llama_probe_non_blocking.py 7.2s to 1.6s, test_child_lifetime_boundary.py 6.6s to 1.7s, test_openai_auto_download.py 6.35s to 1.9s. All three were sleeping inside fakes the tests define themselves; they now block on an event the test releases, which is an unbounded stall rather than a bounded one, so the guarantee is stronger.

One correctness fix, in test_cloudflare_tunnel.py

This one is worth reading even though it only saves 5s.

monkeypatch.setattr(ct.threading, "Thread", ...) sets the attribute on the threading module object, so it replaced threading.Thread process-wide, not just the tunnel's stdout reader that the test meant to stub. utils/process_lifetime._Spawner then started a helper thread whose start() did nothing, so usable() burned its full _ready.wait(5.0) and returned False, and spawn_on_lifetime_thread fell back to spawning inline on the calling thread. That fallback silently gives up the PR_SET_PDEATHSIG guarantee the helper thread exists to provide, which its own docstring explains.

No assertion is masked today, because Popen is faked too. The point is that the patch reaches far outside what the test intended. It is now a shim that overrides Thread and proxies everything else.

Detection power is measured, not asserted

Neutering _killpg_captured in core/inference/tools.py fails 7 of the 55 tests in test_tool_output_streaming.py on this branch. The identical mutation on unpatched main fails the same 7 by name, 48 passed either way. So the rewrite costs nothing in what it catches.

The others, each a single unique-string replace:

File Mutation Result
test_orphaned_children.py post-SIGKILL group escalation in install_llama_prebuilt.py removed 1 failed, 97 passed
test_cloudflare_tunnel.py _tunnels_pending_stop.discard(tunnel) removed test fails

Worth noting on the grace = 1.0 change: with the kill mutated out the sentinel appears in 20 to 40ms, so 1.0s is a 25 to 50x margin. I did not shave it further for another 4s, since a missed regression costs more than the seconds.

Scope

No production constant is lowered anywhere. The diff is six files, all under studio/backend/tests/.

Attribution, since some of these delays were deliberate and some were not: #7083 added the grandchild fuses, #8170 the installer grace and child lifetime, #6494 the startup probe, #7875 the cloudflare backstop, #7454 the cold scan. Two were sound decisions and are strengthened here rather than removed.

These six files spend most of their runtime asleep rather than working, and the
waiting buys nothing: every one of them is waiting out a fixed window when it
could be waiting for the event it actually cares about.

Measured on a fresh worktree off main, the six files together:
118.0s and 118.8s before, 56.5s and 55.3s after. 404 passed and 3 skipped in
every run, before and after. Backend tests runs on four Python versions, so
that is about 4 runner-minutes a run.

No production constant is lowered. The diff is six files, all under
studio/backend/tests/. Where a delay was reachable from production code the test
now passes an existing parameter at the call site: _terminate_validation_server
already takes grace, defaulted to 5.0, and the sibling test in the same commit
already passes 0.2, so this one passes 1.0 instead of taking the default four
times over. The measured margin there is 25 to 50x, since with the kill mutated
out the sentinel appears in 20 to 40ms.

test_tool_output_streaming.py is the bulk of it, 61.5s to 32.1s. Seven
grandchild processes were fused with sleep 3 and the test then waited 4s or 11s
for them. The fuse is now a gate file the test owns, so the grandchild lives
indefinitely and the assertion is stronger than "3 is greater than 1".

test_cloudflare_tunnel.py is a correctness fix that happens to save 5s.
monkeypatch.setattr(ct.threading, "Thread", ...) sets the attribute on the
threading module itself, so it replaced threading.Thread process-wide rather
than just the tunnel's stdout reader. utils/process_lifetime._Spawner then
started a helper thread that never ran, burned its full 5s readiness backstop,
and fell back to spawning inline, which silently gives up the PR_SET_PDEATHSIG
guarantee that helper thread exists to provide. No assertion is masked today
because Popen is faked too, but the patch reaches far outside what the test
meant to stub. It is now a shim that overrides Thread and proxies the rest.

Detection power is unchanged, and that is measured rather than asserted.
Neutering _killpg_captured in core/inference/tools.py fails 7 of the 55 tests in
test_tool_output_streaming.py on this branch, and the identical mutation on
unpatched main fails the same 7 by name. Same for the others: the installer test
fails when the post-SIGKILL group escalation goes, the probe test fails when the
probe runs inline, the child-lifetime test fails when PDEATHSIG is dropped, and
the cloudflare test fails when the pending-stop record is not cleared.

Attribution, since these were deliberate in some cases and accidental in others:
#7083 added the grandchild fuses, #8170 the installer grace and child lifetime,
#6494 the startup probe, #7875 the cloudflare backstop, #7454 the cold scan.
Two of those were sound decisions and are strengthened here rather than weakened.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c07f1a6a3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +80 to +84
while time.monotonic() < deadline:
assert not sentinel.exists(), (
"a grandchild survived the process-group kill and wrote its sentinel"
)
time.sleep(0.02)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check the sentinel once more after the polling deadline

When a leaked grandchild writes the sentinel during the final sleep(0.02), the loop exits because the deadline has elapsed without checking the file again, so all seven rewritten process-group tests can incorrectly pass. This is especially plausible on a heavily loaded CI runner where the surviving grandchild is scheduled near the end of the one-second window; add a final absence assertion after the loop or structure the polling so every sleep is followed by a check.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e209c263d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound the gate loop when a test exits early

If any rewritten test raises, times out, or is interrupted before _assert_grandchild_was_killed creates the gate, this grandchild now loops forever; unlike the previous three-second fuse, it has no independent exit condition. Because it also inherits and holds the test process's stdout pipe open, such a failure can leave processes contaminating subsequent tests or prevent a CI harness from observing EOF after pytest exits. Keep the gate-based fast path, but add a generous safety timeout or ensure every caller opens the gate and reaps the process in finally.

Useful? React with 👍 / 👎.

@danielhanchen
danielhanchen merged commit 63e371a into main Aug 16, 2026
26 of 44 checks passed
@danielhanchen
danielhanchen deleted the ci-sleeps branch August 16, 2026 10:41
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.

1 participant