Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
9406110
feat(serverless): add VolumeCache skeleton with availability gating
deanq Jul 6, 2026
83c3790
feat(serverless): VolumeCache.sync writes collision-free delta shards
deanq Jul 6, 2026
1d9a037
fix(serverless): tolerate coarse network-fs mtime granularity in Volu…
deanq Jul 6, 2026
709b24b
feat(serverless): VolumeCache.hydrate extracts shards with idempotent…
deanq Jul 6, 2026
0c7a99a
fix(serverless): drop 3.12-only tarfile filter kwarg to honor py3.10 …
deanq Jul 6, 2026
cf5e634
fix(serverless): apply tar data filter when available, honoring py3.1…
deanq Jul 6, 2026
757fb1a
feat(serverless): harden VolumeCache extract against traversal and sy…
deanq Jul 6, 2026
1c7edf0
fix(serverless): realpath-normalize cache dirs so symlinked mounts match
deanq Jul 6, 2026
b6477f5
feat(serverless): add size-capped retention to VolumeCache
deanq Jul 6, 2026
c507959
feat(serverless): add best-effort guard and warm() context manager
deanq Jul 6, 2026
d1957cc
feat(serverless): export VolumeCache from runpod.serverless
deanq Jul 6, 2026
280e142
feat(serverless): auto-hydrate model cache from network volume in wor…
deanq Jul 6, 2026
1310330
fix(serverless): guard build_default_cache and isolate worker-wiring …
deanq Jul 6, 2026
75394fc
fix(serverless): guard cache sync dispatch, sweep temp shards, cover …
deanq Jul 6, 2026
7047644
fix(serverless): address review - streaming sync, retention resilienc…
deanq Jul 7, 2026
ee7d536
feat(serverless): make network-volume warm cache opt-in; add usage docs
deanq Jul 7, 2026
1aff13c
refactor(serverless): VolumeCache mirror+reconcile closure; drop env-…
deanq Jul 7, 2026
17ef5e1
fix(serverless): bound atexit sync join, unique temp names, exclude t…
deanq Jul 7, 2026
544b979
fix(serverless): clear CodeQL alerts (empty-except, unreachable test,…
deanq Jul 8, 2026
5f5570d
docs(serverless): add VolumeCache warm-cache handler example
deanq Jul 8, 2026
a8f0afc
feat(volumecache): add size-partition, max_workers, and packed-format…
deanq Jul 10, 2026
e612661
feat(volumecache): add versioned manifest read/write and change detec…
deanq Jul 10, 2026
5b332dd
feat(volumecache): pack small-file bucket via tar with tarfile fallback
deanq Jul 10, 2026
c34dd22
feat(volumecache): extract small bucket via tarfile with per-member s…
deanq Jul 10, 2026
3a2d36d
fix(volumecache): harden _extract_small against corrupt archives
deanq Jul 10, 2026
f76773b
test(volume-cache): target inner getmembers guard in _extract_small
deanq Jul 10, 2026
f17840a
feat(volumecache): add thread-pool parallel copy helper
deanq Jul 10, 2026
ddbaa9e
feat(volumecache): mirror and hydrate via size-bucketed tar + paralle…
deanq Jul 10, 2026
cc64103
fix(volumecache): gate small-archive extract on manifest + prune stal…
deanq Jul 10, 2026
ca51c75
docs(volumecache): document size-bucketed adaptive transport
deanq Jul 10, 2026
c242dd2
fix(volume-cache): harden manifest reads and pack_small cleanup
deanq Jul 10, 2026
0a40258
feat(serverless): add VolumeCache skeleton with availability gating
deanq Jul 6, 2026
a2d900d
feat(serverless): VolumeCache.sync writes collision-free delta shards
deanq Jul 6, 2026
c9a01c0
fix(serverless): tolerate coarse network-fs mtime granularity in Volu…
deanq Jul 6, 2026
1e941b0
feat(serverless): VolumeCache.hydrate extracts shards with idempotent…
deanq Jul 6, 2026
43435c6
fix(serverless): drop 3.12-only tarfile filter kwarg to honor py3.10 …
deanq Jul 6, 2026
f55ffc5
fix(serverless): apply tar data filter when available, honoring py3.1…
deanq Jul 6, 2026
94c2d3b
feat(serverless): harden VolumeCache extract against traversal and sy…
deanq Jul 6, 2026
1f35d08
fix(serverless): realpath-normalize cache dirs so symlinked mounts match
deanq Jul 6, 2026
870123a
feat(serverless): add size-capped retention to VolumeCache
deanq Jul 6, 2026
a1a70dd
feat(serverless): add best-effort guard and warm() context manager
deanq Jul 6, 2026
3647b82
feat(serverless): export VolumeCache from runpod.serverless
deanq Jul 6, 2026
5e127b3
feat(serverless): auto-hydrate model cache from network volume in wor…
deanq Jul 6, 2026
9bc61b0
fix(serverless): guard build_default_cache and isolate worker-wiring …
deanq Jul 6, 2026
c3f6894
fix(serverless): guard cache sync dispatch, sweep temp shards, cover …
deanq Jul 6, 2026
3d1a82a
fix(serverless): address review - streaming sync, retention resilienc…
deanq Jul 7, 2026
3e52b19
feat(serverless): make network-volume warm cache opt-in; add usage docs
deanq Jul 7, 2026
f53fc51
refactor(serverless): VolumeCache mirror+reconcile closure; drop env-…
deanq Jul 7, 2026
573d8de
fix(serverless): bound atexit sync join, unique temp names, exclude t…
deanq Jul 7, 2026
6832654
fix(serverless): clear CodeQL alerts (empty-except, unreachable test,…
deanq Jul 8, 2026
0883400
docs(serverless): add VolumeCache warm-cache handler example
deanq Jul 8, 2026
996cb1f
fix(volumecache): repack small archive when a small file is deleted
deanq Jul 11, 2026
d261f73
fix(volumecache): treat a scalar dirs path as a single directory
deanq Jul 11, 2026
2f38d64
merge: sync adaptive-transport branch with #531 (scalar-dirs fix)
deanq Jul 11, 2026
3284bc8
refactor(volumecache): stream copy count and confine hydrate source t…
deanq Jul 11, 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
110 changes: 110 additions & 0 deletions docs/serverless/volume_cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 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. |
| `max_workers` | `min(32, (os.cpu_count() or 4) * 4)` | Thread count for parallel copy of large files (I/O-bound). |

## How it works

- **Size-bucketed mirror.** Cached files live at
`{volume_path}/.cache/{namespace}`, split by size: files below 256 KiB are
packed into a single `small.tar` archive (collapsing per-file metadata
round-trips on the network volume), and larger files are copied unpacked
into a `big/` subdirectory, preserving their original relative path. A
versioned `manifest.json` — written last — records file metadata (size,
mtime) for every cached file and is the commit marker for a complete
mirror; a mirror without a valid, current-version manifest is treated as
absent.
- **Incremental large files, whole-archive small files.** `big/` transfers
are diffed per file by size/mtime against the manifest, so unchanged large
files are skipped. The `small.tar` archive is re-packed as a whole whenever
any small file has changed, since unpacking and re-diffing many tiny files
individually is slower than the network volume's per-file overhead.
- **Parallel copy.** Large-file transfers run across a thread pool sized by
`max_workers` (default `min(32, (os.cpu_count() or 4) * 4)`), since the
work is I/O-bound.
- **Packing/extraction.** Packing the small-file archive uses the `tar`
binary when available (falling back to the stdlib `tarfile` module
otherwise). Extraction always goes through `tarfile`, validating every
member's resolved path against the configured `dirs` before writing.
- **Idempotent.** Re-running `hydrate`/`sync` after nothing has changed
copies zero files and does not repack `small.tar`. `manifest.json` is still
refreshed on every `sync` call — a small atomic write that also drops
entries for files deleted locally since the last sync.
- **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. Every archive
member and every big-file destination is 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.
- **Orphaned big files are never pruned.** If a large file is deleted or
renamed locally, its `big/<relpath>` copy stays on the volume — hydrate
is manifest-driven and simply ignores it, but volume space grows across
model-version swaps. Same behavior as the prior flat-mirror design.
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