-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathquickstart_cache.py
More file actions
182 lines (146 loc) · 6.5 KB
/
Copy pathquickstart_cache.py
File metadata and controls
182 lines (146 loc) · 6.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""Request caching quickstart.
Demonstrates Sheaf's in-process LRU response cache. Uses a lightweight
stub backend so no model weights are required — run it immediately after
``pip install sheaf-serve``.
Key points:
- Add ``cache=CacheConfig(enabled=True)`` to any ``ModelSpec``.
- The first call runs inference; identical subsequent calls are served
from the in-process cache.
- ``ttl_s`` expires entries after N seconds.
- ``exclude_fields`` lets callers omit fields from the key (e.g. ``seed``
for diffusion models, so same-seed repeats hit but different seeds miss).
- ``SHEAF_CACHE_DISABLED=1`` env var disables all caches globally (useful
in integration test runs where you need fresh backend output every time).
Good candidates for caching:
- Embedding models: same image / text → same vector.
- Time-series forecasts with fixed history.
- Any model where the input fully determines the output.
Poor candidates:
- Diffusion with random seeds *unless* you include the seed in the key
(omit ``"seed"`` from ``exclude_fields`` and the same seed is cached).
- Models where the caller explicitly needs fresh output each time.
Usage::
python examples/quickstart_cache.py
"""
from __future__ import annotations
import time
from starlette.testclient import TestClient
from sheaf.api.base import ModelType
# ---------------------------------------------------------------------------
# Register a trivial stub backend (no weights needed)
# ---------------------------------------------------------------------------
from sheaf.api.time_series import TimeSeriesRequest, TimeSeriesResponse # noqa: E402
from sheaf.backends.base import ModelBackend # noqa: E402
from sheaf.cache import CacheConfig
from sheaf.modal_server import _build_asgi_app
from sheaf.registry import register_backend # noqa: E402
from sheaf.spec import ModelSpec
@register_backend("_demo_ts")
class _DemoTSBackend(ModelBackend):
"""Minimal time-series backend: returns 0.0 for every horizon step."""
def load(self) -> None:
# Simulate a small load delay so the first predict() is visibly slower.
time.sleep(0.05)
@property
def model_type(self) -> str:
return ModelType.TIME_SERIES
def predict(self, request: TimeSeriesRequest) -> TimeSeriesResponse: # type: ignore[override]
# Simulate inference latency.
time.sleep(0.1)
return TimeSeriesResponse(
request_id=request.request_id,
model_name=request.model_name,
horizon=request.horizon,
frequency=request.frequency.value,
mean=[0.0] * request.horizon,
)
# ---------------------------------------------------------------------------
# 1. ModelSpec with caching enabled
# ---------------------------------------------------------------------------
spec = ModelSpec(
name="forecaster",
model_type=ModelType.TIME_SERIES,
backend="_demo_ts",
cache=CacheConfig(
enabled=True,
max_size=512, # keep up to 512 distinct request fingerprints
ttl_s=300.0, # expire entries after 5 minutes
),
)
# ---------------------------------------------------------------------------
# 2. Build the ASGI app (works without Ray — same HTTP contract as ModelServer)
# ---------------------------------------------------------------------------
print("Building ASGI app and loading backend…")
app = _build_asgi_app([spec])
client = TestClient(app)
# ---------------------------------------------------------------------------
# 3. Define a request payload
# ---------------------------------------------------------------------------
payload = {
"model_type": "time_series",
"model_name": "forecaster",
"history": [312, 298, 275, 260, 255, 263, 285, 320, 368, 402, 421, 435],
"horizon": 6,
"frequency": "1h",
}
# ---------------------------------------------------------------------------
# 4. First call — backend runs (cache miss)
# ---------------------------------------------------------------------------
print("\nCall 1 (cache miss — backend executes):")
t0 = time.perf_counter()
r = client.post("/forecaster/predict", json=payload)
r.raise_for_status()
latency_first = (time.perf_counter() - t0) * 1000
print(f" mean: {r.json()['mean']}")
print(f" latency: {latency_first:.1f} ms")
# ---------------------------------------------------------------------------
# 5. Second call — same payload, different request_id → cache hit
# ---------------------------------------------------------------------------
print("\nCall 2 (cache hit — backend skipped):")
t0 = time.perf_counter()
r = client.post("/forecaster/predict", json=payload)
r.raise_for_status()
latency_cached = (time.perf_counter() - t0) * 1000
print(f" mean: {r.json()['mean']}")
print(f" latency: {latency_cached:.1f} ms")
speedup = latency_first / max(latency_cached, 0.01)
print(
f"\n Speedup: {speedup:.1f}× ({latency_first:.1f} ms → {latency_cached:.1f} ms)"
) # noqa: E501
# ---------------------------------------------------------------------------
# 6. Different payload → cache miss (distinct entry)
# ---------------------------------------------------------------------------
different_payload = {**payload, "history": [100, 110, 120, 130, 140, 150]}
print("\nCall 3 (different history — cache miss, new entry):")
t0 = time.perf_counter()
r = client.post("/forecaster/predict", json=different_payload)
r.raise_for_status()
print(f" mean: {r.json()['mean']}")
print(f" latency: {(time.perf_counter() - t0) * 1000:.1f} ms")
# ---------------------------------------------------------------------------
# 7. exclude_fields — omit a field from the cache key
# ---------------------------------------------------------------------------
print("\n--- exclude_fields demo ---")
print("Two requests that differ only in 'model_name' → treated as the same key.")
spec_excl = ModelSpec(
name="forecaster-excl",
model_type=ModelType.TIME_SERIES,
backend="_demo_ts",
cache=CacheConfig(
enabled=True,
exclude_fields=["model_name"], # model_name excluded from key
),
)
app_excl = _build_asgi_app([spec_excl])
client_excl = TestClient(app_excl)
r1 = client_excl.post(
"/forecaster-excl/predict", json={**payload, "model_name": "version-a"}
)
r2 = client_excl.post(
"/forecaster-excl/predict", json={**payload, "model_name": "version-b"}
)
r1.raise_for_status()
r2.raise_for_status()
print(f" version-a mean: {r1.json()['mean']}")
print(f" version-b mean: {r2.json()['mean']} (served from cache — same result)")
print("\nDone.")