Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fec7fc3
feat(serverless): add VolumeCache skeleton with availability gating
deanq Jul 6, 2026
2853f38
feat(serverless): VolumeCache.sync writes collision-free delta shards
deanq Jul 6, 2026
f0a76f8
fix(serverless): tolerate coarse network-fs mtime granularity in Volu…
deanq Jul 6, 2026
4b0f6df
feat(serverless): VolumeCache.hydrate extracts shards with idempotent…
deanq Jul 6, 2026
b95e1f1
fix(serverless): drop 3.12-only tarfile filter kwarg to honor py3.10 …
deanq Jul 6, 2026
6fc8250
fix(serverless): apply tar data filter when available, honoring py3.1…
deanq Jul 6, 2026
5b76d8d
feat(serverless): harden VolumeCache extract against traversal and sy…
deanq Jul 6, 2026
b52bb48
fix(serverless): realpath-normalize cache dirs so symlinked mounts match
deanq Jul 6, 2026
fd19f65
feat(serverless): add size-capped retention to VolumeCache
deanq Jul 6, 2026
68b56d3
feat(serverless): add best-effort guard and warm() context manager
deanq Jul 6, 2026
c3a9e98
feat(serverless): export VolumeCache from runpod.serverless
deanq Jul 6, 2026
a8f466a
feat(serverless): auto-hydrate model cache from network volume in wor…
deanq Jul 6, 2026
1e8fdf1
fix(serverless): guard build_default_cache and isolate worker-wiring …
deanq Jul 6, 2026
f8fe045
fix(serverless): guard cache sync dispatch, sweep temp shards, cover …
deanq Jul 6, 2026
53bb5a6
Merge branch 'main' into deanquinanola/sls-367-network-volume-warm-ca…
deanq Jul 6, 2026
ede2d5c
fix(serverless): address review - streaming sync, retention resilienc…
deanq Jul 7, 2026
eeb26e6
feat(serverless): make network-volume warm cache opt-in; add usage docs
deanq Jul 7, 2026
1e21876
Merge branch 'main' into deanquinanola/sls-367-network-volume-warm-ca…
deanq Jul 7, 2026
b184d21
refactor(serverless): VolumeCache mirror+reconcile closure; drop env-…
deanq Jul 7, 2026
87438a6
fix(serverless): bound atexit sync join, unique temp names, exclude t…
deanq Jul 7, 2026
ca96ab5
Merge branch 'main' into deanquinanola/sls-367-network-volume-warm-ca…
deanq Jul 8, 2026
9469d3a
fix(serverless): clear CodeQL alerts (empty-except, unreachable test,…
deanq Jul 8, 2026
351f35d
docs(serverless): add VolumeCache warm-cache handler example
deanq Jul 8, 2026
fbb27d3
Merge branch 'main' into deanquinanola/sls-367-network-volume-warm-ca…
deanq Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ runpod.serverless.start({"handler": handler})

See [Worker Fitness Checks](https://github.com/runpod/runpod-python/blob/main/docs/serverless/worker_fitness_checks.md) documentation for more examples and best practices.

### Network-Volume Warm Cache

When a network volume is attached, `VolumeCache` warms local directories (such as a model cache) across cold starts — hydrating them on startup and syncing new files back on exit — so a repeated multi-GB model download becomes a one-time cost per endpoint. It is stdlib-only and best-effort.

```python
from runpod.serverless import VolumeCache

with VolumeCache(dirs=["/root/.cache/huggingface"]):
model = load_model()
```

See [Network-Volume Warm Cache](https://github.com/runpod/runpod-python/blob/main/docs/serverless/volume_cache.md) documentation for configuration and details.

## 📚 | API Language Library (GraphQL Wrapper)

When interacting with the Runpod API you can use this library to make requests to the API.
Expand Down
89 changes: 89 additions & 0 deletions docs/serverless/volume_cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Network-Volume Warm Cache (VolumeCache)

`VolumeCache` warms local directories across serverless workers using a mounted
network volume. It keeps a browsable mirror of your cache directories on the
volume and reconciles it against the container on each use: on cold start it
restores previously-cached files into place, and after use it copies newly
written files back to the volume. This turns a repeated multi-GB model
download on every cold start into a one-time cost per endpoint.

It is stdlib-only and best-effort: any failure degrades to a cold worker and
never raises into your handler or worker loop.

## Requirements

- A **network volume** attached to the endpoint (mounted at `/runpod-volume`).
- Set on serverless automatically: `RUNPOD_ENDPOINT_ID` (used to scope the
mirror per endpoint/namespace).

If no volume is mounted, or the namespace is empty, every operation is a safe
no-op.

## Usage

`VolumeCache` is a context-manager closure — using it around a model load
hydrates the cache before the block runs and syncs any changes back after:

```python
from runpod.serverless import VolumeCache

with VolumeCache(dirs=["/root/.cache/huggingface"]):
model = load_model() # downloads land in the cached directory
```

- **On enter**, `hydrate()` copies files that are missing or newer on the
volume mirror into the container.
- **On exit**, `sync()` copies files that are missing or newer in the
container onto the volume mirror. By default this runs on a background
daemon thread and returns immediately, so the `with` block doesn't block on
the sync; a process-exit hook joins any outstanding syncs so short-lived
processes (including local test runs) still complete the sync before
exiting.

You can also call the phases directly when they happen at different points in
your worker's lifecycle:

```python
vc = VolumeCache(dirs=["/data/models"], namespace="my-model-cache")

vc.hydrate() # restore cached files (e.g. at startup)
model = load_model() # populate the cache
vc.sync(background=False) # persist new files back to the volume, inline
```

## Constructor

| Argument | Default | Purpose |
| --- | --- | --- |
| `dirs` | required | Local directories to cache. |
| `namespace` | `RUNPOD_ENDPOINT_ID` | Isolation key for the on-volume mirror. Must be a single safe path component. |
| `volume_path` | `/runpod-volume` | Network-volume mount point. |
| `best_effort` | `True` | Swallow and log errors instead of raising. Set `False` while debugging. |

## How it works

- **Directory mirror.** Cached files live at `{volume_path}/.cache/{namespace}`,
laid out at the same absolute path they occupy in the container (so the
mirror is directly browsable — no archive format to unpack).
- **Per-file, atomic reconcile.** Each direction (`hydrate`/`sync`) walks the
source tree and copies any file whose size differs or whose mtime is newer
than the destination's (beyond a small tolerance for coarse network-filesystem
mtimes). Copies are written to a temp file and atomically renamed into place
(`os.replace`), so a crash mid-copy never leaves a half-written file visible.
- **Idempotent.** Copies preserve mtime (`shutil.copy2`), so re-running
`hydrate`/`sync` after nothing has changed copies zero files.
- **Last-writer-wins.** Under concurrent workers, the mirror simply reflects
whichever worker synced most recently — there's no locking or merge.
- **Safety.** Symlinked sources are never followed/copied. Hydration destinations
are checked to resolve inside one of the configured `dirs` before any write,
so a mirror entry can't be used to write outside the cached directories.

## Limitations

- **Cold-scale write amplification.** If N workers cold-start at the same time,
each may miss the still-empty mirror, download the model, and sync a full
copy back. There's no coordination between concurrent syncs.
- **Background sync on short-lived processes.** `sync()` schedules the copy on
a daemon thread; if the process exits without going through normal
interpreter shutdown (e.g. `os._exit`, `SIGKILL`), the atexit hook never
runs and the sync may not complete.
48 changes: 48 additions & 0 deletions examples/serverless/volume_cache_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Warm-cache model weights across cold starts with VolumeCache.

`VolumeCache` keeps a browsable mirror of local cache directories on a mounted
network volume and reconciles the two on each use: it restores cached files on
cold start (`hydrate`) and syncs new downloads back afterward (`sync`). Wrapping
a model load in `with VolumeCache(...)` turns a repeated multi-GB download into a
one-time cost per endpoint.

Why not just point HF_HOME at the volume? Because then every read hits the
network mount. Here the Hugging Face cache stays on fast local disk and
VolumeCache mirrors it to the volume, so inference reads are local while cold
starts stay warm.

Requirements to run:
- Attach a network volume to the endpoint (mounted at /runpod-volume).
- RUNPOD_ENDPOINT_ID is set automatically on Runpod serverless (it scopes the
cache per endpoint). Without a mounted volume or endpoint id, VolumeCache is a
safe no-op and the handler still works.
- pip install "transformers" "torch" (the model library used below).

Local test:
python volume_cache_handler.py --rp_serve_api
"""

import os

import runpod
from runpod.serverless import VolumeCache

# Hugging Face caches models here by default. Keep it on local disk and let
# VolumeCache mirror it to the network volume.
HF_CACHE = os.environ.get("HF_HOME", os.path.expanduser("~/.cache/huggingface"))

# Hydrate the cache from the volume before loading, then sync new files back on
# exit. On a warm endpoint the model is already local and nothing is downloaded.
with VolumeCache(dirs=[HF_CACHE]):
from transformers import pipeline

classifier = pipeline("sentiment-analysis")


def handler(job):
"""Classify the sentiment of an input string."""
text = job["input"].get("text", "")
return {"input": text, "predictions": classifier(text)}


runpod.serverless.start({"handler": handler})
4 changes: 3 additions & 1 deletion runpod/serverless/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
from .modules.rp_logger import RunPodLogger
from .modules.rp_progress import progress_update
from .modules.rp_fitness import register_fitness_check
from .utils.rp_volume_cache import VolumeCache

__all__ = [
"start",
"progress_update",
"register_fitness_check",
"runpod_version"
"runpod_version",
"VolumeCache",
]

log = RunPodLogger()
Expand Down
4 changes: 3 additions & 1 deletion runpod/serverless/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from .rp_download import download_files_from_urls
from .rp_upload import upload_file_to_bucket, upload_in_memory_object
from .rp_volume_cache import VolumeCache

__all__ = [
"download_files_from_urls",
"upload_file_to_bucket",
"upload_in_memory_object"
"upload_in_memory_object",
"VolumeCache",
]
Loading