Skip to content

feat(serverless): coordinate early health checks once per container - #578

Draft
justinwlin wants to merge 13 commits into
mainfrom
justinlin/dr-1409-python-sdk-move-health-checks-at-start-up
Draft

feat(serverless): coordinate early health checks once per container#578
justinwlin wants to merge 13 commits into
mainfrom
justinlin/dr-1409-python-sdk-move-health-checks-at-start-up

Conversation

@justinwlin

@justinwlin justinwlin commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Worker health checks previously waited until serverless.start(), often after expensive model loading. This change runs shared hardware checks at the first eligible SDK import and retains network, Python CUDA initialization, GPU computation, and customer checks in the worker-start pass.

Early checks require both RUNPOD_ENDPOINT_ID and RUNPOD_WEBHOOK_GET_JOB, excluding platform/local tests. A Linux file lock and persisted result coordinate RAM, disk, CUDA-version, and native GPU checks across independent Python processes. Results are scoped to host boot, PID namespace, and init-process start time so container restarts do not reuse stale successes. Failures propagate to subsequent processes. Relevant configuration or SDK changes invalidate successful results.

No launcher, PID environment variable, or customer entrypoint changes are required. Coordination unavailability defers checks to worker start. Lock waits are bounded; unresolved contention at worker start reports unhealthy and exits. RUNPOD_DEFER_FITNESS_CHECKS restores worker-start timing; RUNPOD_SKIP_FITNESS_CHECKS disables checks. Network checks retain bounded retries against the worker API. Setup failures, hard termination, realtime serving checks, and logger redaction fixes are retained.

Validation:

  • Latest local full suite: 688 tests + 6 subtests passed, 94.36% coverage after code-quality cleanup. Shared cache key/configuration logic is centralized; lock acquisition, state parsing, and check execution have separate helpers.
  • Real independent subprocesses: once-only execution, saved failure propagation, lock-owner crash recovery; customer/process checks remain deferred.
  • Live Linux Pod: three concurrent processes executed one synthetic check. Restarting the same Pod retained its files but changed the startup identity and executed exactly one fresh check. Temporary Pod deleted and deletion verified.
  • Earlier live Pod/Serverless comparison confirmed the two environment identifiers and their inheritance by child processes.
  • Wheel/source build and targeted correctness lint passed. Live singleton test validates coordination, not the complete GPU hardware-check suite.

Limitations: the first import can occur after model loading. Processes that do not share /tmp or file permissions cannot share results and use worker-start fallback. Broader platform rollout and full real-GPU check validation remain separate from this SDK change.

justinwlin and others added 6 commits August 25, 2026 22:31
Built-in GPU/system fitness checks ran in run_worker, which a handler module
only reaches after loading its model. Run them when runpod.serverless is
imported instead, so a broken environment fails in seconds. User-registered
checks still run at start(); checks that already passed are not repeated.

Adds RUNPOD_SKIP_FITNESS_CHECKS to disable all checks and
RUNPOD_DEFER_FITNESS_CHECKS to restore the previous start()-only timing.
_cuda_init_check and _benchmark_check import torch and allocate on the
device. Running them at import would leave a CUDA context in a process the
handler may later fork, which CUDA does not support and vLLM/DeepSpeed trip
over. Mark them @defer_to_worker_start so only subprocess-based and
non-GPU checks run early.
- run startup pass on a dedicated event loop instead of asyncio.run,
  which resets the loop policy and breaks asyncio.get_event_loop() in
  handler code on Python 3.10+
- set RUNPOD_FITNESS_CHECKS_DONE after the startup pass so children
  re-importing this module under multiprocessing 'spawn' skip the checks
- latch check auto-registration state only on success, so a malformed
  RUNPOD_MIN_*/GPU timeout value re-raises loudly in run_worker instead
  of silently disabling all system checks
- compare completed checks by identity, not equality, so distinct
  registrations that compare equal (bound methods) are not skipped
- bound the nvidia-smi call in rp_cuda.is_available with a 5s timeout
- accept 1/true/yes/on for RUNPOD_SKIP_GPU_CHECK and
  RUNPOD_SKIP_AUTO_SYSTEM_CHECKS, matching the new flags
- tests: pin the worker.py and import-time wiring, the full defer
  behavior, the done marker, the real auto-registration path (guard: no
  torch import), and bound-method re-registration; fix an orphaned
  coroutine in test_unexpected_error_does_not_propagate
- docs: thresholds/skip flags must be set before import runpod, realtime
  API mode runs only the import-time checks, refresh stale
  ARCHITECTURE.md execution flow
…touch-ups

- regression test: malformed RUNPOD_MIN_* must re-raise in run_worker,
  never fail open (latch-on-success)
- fix dormant called/calls typo in the done-marker test
- README: checks run once per check, not once at startup
- ARCHITECTURE.md: failure path is os._exit(1), not sys.exit(1)
- docs: GPU benchmark default timeout is 2s, not 100ms
- rp_gpu_fitness docstring: lazy registration + truthy flag values
The import-time pass consumes RUNPOD_MIN_*/RUNPOD_SKIP_*/RUNPOD_GPU_* at
import; values set from the handler afterwards were silently ignored.
run_fitness_checks now diffs the current env against the values snapshot
at the startup pass and warns with the exact fix (set before import, or
RUNPOD_DEFER_FITNESS_CHECKS=true).
@justinwlin
justinwlin marked this pull request as ready for review September 8, 2026 18:53
@justinwlin
justinwlin requested a review from deanq September 8, 2026 18:53
@deanq
deanq requested a lite review from Copilot September 9, 2026 19:16

Copilot AI 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.

🟡 Changes recommended

Auto-registration failures can currently escape the hard-exit failure path, undermining the “must not hang” operational guarantee during worker startup.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Moves most serverless worker fitness checks earlier (at import runpod.serverless) to fail unhealthy workers before model load, while keeping CUDA-context-creating checks deferred to start() and adding env flags to skip/defer behavior.

Changes:

  • Add an import-time startup pass (run_startup_fitness_checks) gated by worker env, with once-per-process deduping and deferred-check support.
  • Introduce global skip/defer env flags and more consistent “truthy” env parsing; add nvidia-smi timeout for CUDA detection.
  • Add/expand tests and docs to cover startup timing, deduping, deferred checks, and late-config warnings.
File summaries
File Description
tests/test_serverless/test_worker.py Asserts worker loop still runs fitness checks.
tests/test_serverless/test_utils/test_cuda.py Updates CUDA availability test expectations for timeout=5.
tests/test_serverless/test_modules/test_fitness/test_startup.py New test suite validating import/start timing, deferral, dedupe, and config warnings.
tests/test_serverless/test_modules/test_fitness/conftest.py Resets new startup/dedupe global state between tests.
runpod/serverless/utils/rp_cuda.py Adds bounded nvidia-smi probe with timeout to avoid hangs.
runpod/serverless/modules/rp_system_fitness.py Marks CUDA-init and benchmark checks as deferred-to-worker-start.
runpod/serverless/modules/rp_gpu_fitness.py Uses shared truthy env flag parsing for skip behavior.
runpod/serverless/modules/rp_fitness.py Implements startup pass, deduping, skip/defer env flags, and late-config warnings.
runpod/serverless/init.py Triggers startup checks at import (worker-only no-op otherwise).
README.md Updates high-level behavior summary for new timing model.
docs/serverless/worker_fitness_checks.md Documents import-time checks, deferral, and new env flags.
ARCHITECTURE.md Updates architecture docs for new timing and hard-exit behavior.
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread runpod/serverless/modules/rp_fitness.py Outdated
Comment on lines 324 to 329
# Defer GPU check auto-registration until fitness checks are about to run
# This avoids circular import issues during module initialization
_ensure_gpu_check_registered()

# Defer system check auto-registration until fitness checks are about to run
_ensure_system_checks_registered()

@deanq deanq left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review from /code-review (correctness + cleanup pass). Four findings, most centered on moving os._exit(1)-capable checks to import time. Lines re-anchored to the current diff.

Comment thread runpod/serverless/modules/rp_fitness.py Outdated
if _env_flag(SKIP_FITNESS_CHECKS_ENV) or _env_flag(DEFER_FITNESS_CHECKS_ENV):
return

if not os.environ.get("RUNPOD_WEBHOOK_GET_JOB"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The import-time gate keys only on RUNPOD_WEBHOOK_GET_JOB, but that misses the _is_local / test_input local-mode guard that previously protected these checks. A handler run with rp_args.test_input used to skip all fitness checks (local mode -> run_worker never called). Now import runpod (eager on main) runs run_startup_fitness_checks(), executes the built-in memory/disk/network/gpu checks, and any failure hard-kills the process via os._exit(1).

Same hazard for any auxiliary CLI/process that imports runpod only for the API client while RUNPOD_WEBHOOK_GET_JOB is inherited in the environment -- it will now run worker fitness checks and can be killed.

Suggest gating the import-time pass on the same local-mode/test-input signal that run_worker uses, so local and non-worker imports stay exempt.

Comment thread runpod/serverless/__init__.py Outdated

# Check the environment here rather than in start(), which a handler module
# only reaches after loading its model. No-op outside a real worker.
run_startup_fitness_checks()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The whole fail-fast benefit assumes import runpod eagerly imports runpod.serverless. That holds on this PR's declared base (main), but the repo's active feat/apps-sdk line lazy-loads serverless via PEP 562 __getattr__. There, a typical handler -- import runpod -> load model -> runpod.serverless.start(...) -- won't trigger serverless/__init__ until the start() line, i.e. after the multi-minute model load.

So on the apps-sdk line the checks fire no earlier than before, silently regressing, while the README/docs added in this PR assert "built-ins at import" / "any import runpod triggers it."

Which branch does this actually merge into? If it's the lazy-import line, either the docs need correcting or the trigger needs an explicit hook that doesn't depend on eager submodule import.

Comment thread runpod/serverless/modules/rp_fitness.py Outdated
# raises RuntimeError on Python 3.10+.
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(run_fitness_checks(include_deferred=False))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Running the network check at import time means a transient failure hard-exits the process mid-import runpod (_terminate_unhealthy -> os._exit(1)). On a cold worker container whose network stack isn't up yet when the handler module is first imported, this turns a recoverable warm-up delay into a boot crash-loop.

Previously this ran in run_worker after start(), giving the container time to become ready. Consider keeping network (and other environment-readiness) checks on the post-start() path, or adding a bounded retry before terminating.

Comment thread runpod/serverless/modules/rp_fitness.py Outdated
return

if _config_snapshot:
_warn_late_config()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

run_startup_fitness_checks populates _config_snapshot microseconds before calling run_fitness_checks, where this if _config_snapshot: _warn_late_config() re-compares all nine env vars against the values just captured -- guaranteed no change, no warning. It's pure overhead on the import path; the late-config warning is only meaningful on the later run_worker pass. Consider skipping _warn_late_config() when invoked from the import-time pass.

@justinwlin
justinwlin marked this pull request as draft September 10, 2026 17:07
@justinwlin justinwlin changed the title feat(serverless): run fitness checks at startup, add skip env var feat(serverless): add safe early fitness checks and skip controls Sep 10, 2026
Comment thread runpod/_logger.py Fixed
Comment thread runpod/_logger.py Fixed
Comment thread runpod/_health/fitness.py Fixed
Comment thread runpod/_startup.py Fixed
@justinwlin justinwlin changed the title feat(serverless): add safe early fitness checks and skip controls feat(serverless): coordinate early health checks once per container Sep 11, 2026
Comment thread tests/test_serverless/test_modules/test_fitness/test_startup.py Fixed
Comment thread tests/test_serverless/test_modules/test_fitness/test_startup.py Fixed
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.

4 participants