Skip to content

feat(serverless): network-volume warm cache (VolumeCache)#531

Open
deanq wants to merge 17 commits into
mainfrom
deanquinanola/sls-367-network-volume-warm-cache-for-serverless-volumecache
Open

feat(serverless): network-volume warm cache (VolumeCache)#531
deanq wants to merge 17 commits into
mainfrom
deanquinanola/sls-367-network-volume-warm-cache-for-serverless-volumecache

Conversation

@deanq

@deanq deanq commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes SLS-367.

Summary

Adds a directory-agnostic, bidirectional network-volume warm cache to the serverless SDK, and auto-wires it into the worker loop to cache model weights across worker recycling. This generalizes the mechanism Flash implements in flash-worker (CacheSyncManager) into a reusable runpod-python primitive that any consumer can point at its own directories.

What's in it

VolumeCache (runpod/serverless/utils/rp_volume_cache.py, public as runpod.serverless.VolumeCache) — stdlib-only, best-effort (never raises into the worker loop):

  • hydrate() — extract per-endpoint tar shards from a mounted volume into configured dirs on cold start; idempotent via a container-local marker.
  • sync() — pack the post-baseline delta into a new worker-owned shard ({worker_id}-{seq}.tar) with an atomic publish. Per-worker shards are collision-free under concurrent workers (no shared read-modify-write).
  • warm() context manager — hydrate on enter, sync on exit; places hydration before the model download by construction.
  • Size-capped retention (prune oldest shards), multi-tenant extract safety (reject traversal / symlink / out-of-dir members), coarse-mtime (NFS) tolerance, and orphan .tmp sweep.

Serverless built-in: run_worker hydrates + registers a default cache before jobs start; run_job fires a guarded, threaded sync once after the first successful job. Gated on a mounted volume and RUNPOD_VOLUME_CACHE. Model dirs discovered from HF_HOME / HF_HUB_CACHE / TORCH_HOME, extensible via RUNPOD_CACHE_DIRS; cap via RUNPOD_VOLUME_CACHE_MAX_GB.

Notes

  • No new runtime dependencies; honors the >=3.10 floor (tar data filter applied only when the runtime provides it).
  • Cache glue is fully guarded: a failure anywhere in build/hydrate/sync degrades to a cold worker and never affects job outcome.

Test plan

  • New unit suite tests/test_serverless/test_utils/test_rp_volume_cache.py (34 tests): availability gating, delta detection + exclusions, coarse-mtime tolerance, multi-shard last-writer-wins, cross-worker distinct-shard hydration, retention pruning, extract-safety (traversal / symlink / hardlink / sibling-prefix), best-effort swallow vs re-raise, warm() ordering incl. on-exception, worker-loop wiring + env gating, build_default_cache disabled / no-volume / bad-env / available branches.
  • Full serverless suite green; repo make quality-check passes (557 tests, 94% coverage, above the 90% gate).
  • Tests are stdlib + tmp_path only — no Docker, no network.

Follow-ups (separate)

  • Repoint Flash's CacheSyncManager to this primitive (dirs=["/root/.cache"]), then delete Flash's bespoke tar/find logic.
  • Decide default-on vs opt-in for the built-in when a volume is present; optional periodic shard compaction (retention covers growth for now).

deanq added 14 commits July 6, 2026 01:54
…test state

Wrap build_default_cache() parse+construct in try/except to prevent malformed
RUNPOD_VOLUME_CACHE_MAX_GB from crashing worker startup; degrades to cache-disabled
instead. Add sync() no-op to fake cache in test_run_worker_hydrates_registered_cache
and wrap test body with reset_builtin_state_for_test() to prevent module-state
leakage to subsequent tests (sync_after_job could fire and hit AttributeError).
@capy-ai

capy-ai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Comment thread tests/test_serverless/test_utils/test_rp_volume_cache.py Fixed
Comment thread runpod/serverless/utils/rp_volume_cache.py Fixed
Comment thread runpod/serverless/utils/rp_volume_cache.py Dismissed
Comment thread tests/test_serverless/test_utils/test_rp_volume_cache.py Fixed
Comment thread tests/test_serverless/test_utils/test_rp_volume_cache.py Fixed
Comment thread tests/test_serverless/test_utils/test_rp_volume_cache.py Fixed
Comment thread runpod/serverless/utils/rp_volume_cache.py Fixed
Comment thread tests/test_serverless/test_utils/test_rp_volume_cache.py Fixed
Comment thread tests/test_serverless/test_utils/test_rp_volume_cache.py Fixed
Comment thread runpod/serverless/utils/rp_volume_cache.py
@promptless

promptless Bot commented Jul 6, 2026

Copy link
Copy Markdown

Promptless prepared a documentation update related to this change.

Triggered by runpod-python PR #531

Documents the new Serverless network-volume warm cache: a new page covering the VolumeCache API and its warm()/hydrate()/sync() lifecycle, the RUNPOD_VOLUME_CACHE / RUNPOD_CACHE_DIRS / RUNPOD_VOLUME_CACHE_MAX_GB environment variables, and auto-discovered cache directories (HF_HOME / HF_HUB_CACHE / TORCH_HOME), plus a cold-start optimization cross-link. Since this PR is still open, the draft is phrased around the env-var toggle rather than asserting default-on behavior — worth confirming the default-on vs opt-in decision and target SDK version before merge.

Review: Document Serverless network-volume warm cache (VolumeCache)

@deanq deanq requested a review from Copilot July 6, 2026 23:48

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.

Pull request overview

This PR introduces a reusable serverless SDK primitive (VolumeCache) for warming local model/cache directories from a mounted network volume and syncing back deltas as per-worker tar “shards”, then wires it into the serverless worker lifecycle to persist model weights across worker recycling (SLS-367).

Changes:

  • Added runpod.serverless.utils.rp_volume_cache.VolumeCache with hydrate(), sync(), and warm() plus built-in helpers (build_default_cache, sync_after_job).
  • Auto-integrated the cache into the worker loop (run_worker hydrates/registers) and job execution (run_job dispatches a one-time async sync after a successful handler run).
  • Expanded public exports (runpod.serverless.VolumeCache, runpod.serverless.utils.VolumeCache) and added a comprehensive unit test suite.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_serverless/test_utils/test_rp_volume_cache.py New unit test suite covering VolumeCache behavior, safety, retention, and worker/job wiring.
tests/test_serverless/test_utils_init.py Updates runpod.serverless.utils.__all__ expectations to include VolumeCache.
tests/test_serverless/test_init.py Updates runpod.serverless.__all__ expectations to include VolumeCache.
runpod/serverless/worker.py Hydrates and registers the default cache before starting job scaling.
runpod/serverless/utils/rp_volume_cache.py New implementation of VolumeCache plus built-in env-driven wiring helpers.
runpod/serverless/utils/init.py Re-exports VolumeCache from runpod.serverless.utils.
runpod/serverless/modules/rp_job.py Dispatches a one-time background cache sync after non-streaming jobs.
runpod/serverless/init.py Re-exports VolumeCache from runpod.serverless.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread runpod/serverless/utils/rp_volume_cache.py
Comment thread runpod/serverless/utils/rp_volume_cache.py
Comment thread runpod/serverless/modules/rp_job.py
@deanq

deanq commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Addressed review feedback in ede2d5c:

Copilot (behavioral):

  • run_job_generator now dispatches the one-time sync_after_job() on its success path — streaming/generator handlers previously never warmed the cache.
  • _enforce_retention now tolerates a shard vanishing under concurrent prune (getsize no longer raises FileNotFoundError out of best-effort sync()).
  • VolumeCache now rejects an unsafe namespace (absolute / path separators / ./..) to prevent shard/marker paths escaping the volume.

CodeQL / code-quality:

  • Documented the best-effort empty except in the temp-shard sweep; removed the unreachable-code pattern in the warm-on-exception test; consolidated mixed import styles in the test module.
  • The "unused global _SYNCED" alert is a false positive (_SYNCED is read/written in sync_after_job and reset in reset_builtin_state_for_test) — dismissed.

Tests added for streaming sync, retention resilience, and namespace validation. Full suite green (566 tests, 94% coverage).

Still open for a maintainer decision before merge: default-on vs opt-in for the serverless built-in when a volume is mounted (currently on, disabled via RUNPOD_VOLUME_CACHE=0).

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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment on lines +60 to +66
def _list_shards(self):
d = self._shard_dir
if not os.path.isdir(d):
return []
shards = [os.path.join(d, f) for f in os.listdir(d) if f.endswith(".tar")]
return sorted(shards, key=os.path.getmtime)

Comment on lines +163 to +184
shards = self._list_shards()
if not shards:
return False
newest = self._newest_shard_mtime()
if os.path.exists(self._marker_path) and os.path.getmtime(self._marker_path) >= newest:
log.debug("VolumeCache: cache already hydrated, skipping")
return False
extracted = False
# Use the tar data filter for defense-in-depth where the runtime provides
# it (Python 3.12+, and 3.10.12+/3.11.4+ backports) without requiring it --
# the >=3.10 floor may predate the API. _is_safe_member is the primary guard.
extract_kwargs = {"filter": "data"} if hasattr(tarfile, "data_filter") else {}
for shard in shards: # oldest -> newest (last wins)
with tarfile.open(shard) as tar:
safe = [m for m in tar.getmembers() if self._is_safe_member(m)]
tar.extractall(path="/", members=safe, **extract_kwargs)
extracted = extracted or bool(safe)
os.makedirs(os.path.dirname(self._marker_path), exist_ok=True)
with open(self._marker_path, "w") as fh:
fh.write(str(newest))
os.utime(self._marker_path, (newest, newest))
self._baseline = time.time() - _BASELINE_EPSILON_SECONDS
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.

3 participants