Skip to content

Commit 9ab6652

Browse files
fluffy314cursoragent
authored andcommitted
fix(prefill): atomically reserve and publish snapshots
Reserve worker cache capacity before expensive compute, protect it from adaptive shrink, and atomically lease the final snapshot so completed full-context Prefill cannot be evicted before discovery. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent afb16a4 commit 9ab6652

10 files changed

Lines changed: 229 additions & 28 deletions

deploy/install_prefill_worker_launchd.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ set -euo pipefail
1212
BIND="${KAKEYA_WORKER_BIND:-0.0.0.0:53051}"
1313
TENANT="${KAKEYA_TENANT_ID:-default}"
1414
CACHE_GB="${KAKEYA_WORKER_CACHE_GB:-4}"
15-
CACHE_MIN_GB="${KAKEYA_WORKER_CACHE_MIN_GB:-0.25}"
16-
MEMORY_RESERVE_GB="${KAKEYA_WORKER_MEMORY_RESERVE_GB:-2}"
15+
CACHE_MIN_GB="${KAKEYA_WORKER_CACHE_MIN_GB:-1}"
16+
MEMORY_RESERVE_GB="${KAKEYA_WORKER_MEMORY_RESERVE_GB:-0.5}"
17+
SNAPSHOT_BYTES_PER_TOKEN="${KAKEYA_SNAPSHOT_BYTES_PER_TOKEN:-400000}"
1718
ADAPTIVE_CACHE="${KAKEYA_WORKER_ADAPTIVE_CACHE:-0}"
1819
PSK_FILE="${KAKEYA_FLEET_PSK_FILE:-}"
1920
CACHE_MODEL_ID="${KAKEYA_CACHE_MODEL_ID:-$KAKEYA_WORKER_MODEL}"
@@ -76,6 +77,7 @@ cat > "$PLIST" <<EOF
7677
<string>--cache-gb</string><string>$CACHE_GB</string>
7778
<string>--cache-min-gb</string><string>$CACHE_MIN_GB</string>
7879
<string>--memory-reserve-gb</string><string>$MEMORY_RESERVE_GB</string>
80+
<string>--estimated-snapshot-bytes-per-token</string><string>$SNAPSHOT_BYTES_PER_TOKEN</string>
7981
$adaptive_xml
8082
<string>--sink</string><string>$SINK</string>
8183
<string>--window</string><string>$WINDOW</string>

deploy/launchd/ai.kakeya.grpc-runtime-prefill.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<string>/Users/fluffy314/Documents/Kakeya-LLM-Inference-engine-prefill-kv-network/scripts/start_grpc_runtime_server.py</string>
1010
<string>--backend</string><string>mlx</string>
1111
<string>--verifier-id</string><string>/Users/fluffy314/kakeya-models/gemma-4-26B-A4B-it-mlx-4bit</string>
12-
<string>--bind</string><string>127.0.0.1:51051</string>
12+
<string>--bind</string><string>0.0.0.0:51051</string>
1313
<string>--capacity</string><string>1</string>
1414
<string>--sink</string><string>4</string>
1515
<string>--window</string><string>2048</string>

deploy/launchd/ai.kakeya.prefill-worker-peer.plist

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
<string>--window</string><string>2048</string>
2424
<string>--block-size-tokens</string><string>64</string>
2525
<string>--cache-gb</string><string>8</string>
26-
<string>--cache-min-gb</string><string>0.25</string>
26+
<string>--cache-min-gb</string><string>1</string>
2727
<string>--adaptive-cache</string>
28-
<string>--memory-reserve-gb</string><string>2</string>
28+
<string>--memory-reserve-gb</string><string>0.5</string>
29+
<string>--estimated-snapshot-bytes-per-token</string><string>400000</string>
2930
<string>--prefill-tps</string><string>1</string>
3031
<string>--max-concurrent-jobs</string><string>1</string>
3132
<string>--network</string><string>thunderbolt</string>

docs/ops/distributed-prefill-kv-network.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,11 @@ and semantic fallback are forbidden. A global Critic score is valid only when
349349
`critic_omitted_tokens=0`. Long Prefill operations emit a heartbeat every 30
350350
seconds; on the 16GB allens worker, full-context Critic Prefill may take 15–25
351351
minutes.
352+
The worker reserves estimated final-snapshot capacity before model compute,
353+
prevents adaptive shrink from consuming active reservations, then atomically
354+
publishes and leases the final snapshot before adding optional intermediate
355+
boundaries. The 16GB allens deployment uses a 1 GiB cache floor and 0.5 GiB
356+
memory reserve. Capacity failures are rejected before Prefill starts.
352357
Interactive prompt templates are deterministic and contain no per-run nonce, so
353358
repeating the same task can reuse allens cold-tier and Primary hot-tier KV.
354359

inference_engine/distributed/prefill_cache.py

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def __init__(
159159
self._evictions = 0
160160
self._bytes_evicted = 0
161161
self._put_failures = 0
162+
self._reservations: dict[str, int] = {}
162163
self._lock = threading.RLock()
163164

164165
def put(self, block: CacheBlock) -> bool:
@@ -263,6 +264,80 @@ def fetch(self, lease_id: str, *, now: float | None = None) -> tuple[CacheBlock,
263264
self._bytes_served += lease.transfer_bytes
264265
return tuple(blocks)
265266

267+
def reserve(self, reservation_id: str, byte_count: int) -> None:
268+
"""Reserve cache capacity before an expensive Prefill job starts."""
269+
if not reservation_id or byte_count <= 0:
270+
raise ValueError("reservation id and byte count must be positive")
271+
with self._lock:
272+
if reservation_id in self._reservations:
273+
raise ValueError("duplicate cache reservation")
274+
requested = int(byte_count)
275+
reserved = sum(self._reservations.values()) + requested
276+
if reserved > self.max_bytes:
277+
raise ValueError(
278+
f"snapshot reservation {requested} exceeds available "
279+
f"cache budget {self.max_bytes - sum(self._reservations.values())}",
280+
)
281+
self._expire_leases(time.time())
282+
self._evict_to_limit(self.max_bytes - reserved)
283+
if self._bytes_used > self.max_bytes - reserved:
284+
raise ValueError("cache capacity is pinned by active leases")
285+
self._reservations[reservation_id] = requested
286+
287+
def release_reservation(self, reservation_id: str) -> None:
288+
with self._lock:
289+
self._reservations.pop(reservation_id, None)
290+
291+
def publish_and_lease(
292+
self,
293+
blocks: Sequence[CacheBlock],
294+
block_hashes: Sequence[bytes],
295+
*,
296+
reservation_id: str,
297+
lease_seconds: float = DEFAULT_LEASE_SECONDS,
298+
) -> PrefixLease:
299+
"""Atomically publish and pin the final computed snapshot."""
300+
if not blocks or len(blocks) != len(block_hashes):
301+
raise ValueError("one computed snapshot is required per block hash")
302+
if lease_seconds <= 0:
303+
raise ValueError("lease_seconds must be > 0")
304+
final = blocks[-1]
305+
if final.block_hash != bytes(block_hashes[-1]):
306+
raise ValueError("final snapshot hash does not match request")
307+
now = time.time()
308+
with self._lock:
309+
reserved = self._reservations.get(reservation_id)
310+
if reserved is None:
311+
raise ValueError("unknown cache reservation")
312+
if final.nbytes > reserved:
313+
raise ValueError(
314+
f"final snapshot {final.nbytes} exceeds reservation {reserved}",
315+
)
316+
self._expire_leases(now)
317+
self._evict_to_limit(self.max_bytes - final.nbytes)
318+
if self._bytes_used > self.max_bytes - final.nbytes:
319+
raise ValueError("cache capacity is pinned by active leases")
320+
self._put_locked(final)
321+
lease_id = secrets.token_urlsafe(18)
322+
lease = PrefixLease(
323+
lease_id=lease_id,
324+
block_hashes=(final.block_hash,),
325+
hit_block_count=len(block_hashes),
326+
hit_token_count=final.token_count,
327+
transfer_bytes=final.nbytes,
328+
cache_epoch=self._epoch,
329+
expires_at_unix=now + lease_seconds,
330+
payload_sha256=final.payload_sha256,
331+
)
332+
self._leases[lease_id] = lease
333+
del self._reservations[reservation_id]
334+
# Preserve longest useful boundaries when spare capacity remains.
335+
for block in reversed(blocks[:-1]):
336+
if block.nbytes + self._bytes_used > self.max_bytes:
337+
continue
338+
self._put_locked(block)
339+
return lease
340+
266341
def stats(self) -> CacheStats:
267342
with self._lock:
268343
return CacheStats(
@@ -285,9 +360,12 @@ def resize(self, max_bytes: int) -> bool:
285360
raise ValueError("max_bytes must be > 0")
286361
with self._lock:
287362
previous = self.max_bytes
363+
reserved = sum(self._reservations.values())
364+
if int(max_bytes) < reserved:
365+
return False
288366
self.max_bytes = int(max_bytes)
289-
self._evict_to_budget()
290-
if self._bytes_used > self.max_bytes:
367+
self._evict_to_limit(self.max_bytes - reserved)
368+
if self._bytes_used > self.max_bytes - reserved:
291369
self.max_bytes = max(previous, self._bytes_used)
292370
return False
293371
return True
@@ -322,8 +400,11 @@ def _pinned_hashes(self) -> set[bytes]:
322400
}
323401

324402
def _evict_to_budget(self) -> None:
403+
self._evict_to_limit(self.max_bytes)
404+
405+
def _evict_to_limit(self, limit: int) -> None:
325406
pinned = self._pinned_hashes()
326-
while self._bytes_used > self.max_bytes and self._blocks:
407+
while self._bytes_used > limit and self._blocks:
327408
victim = next((h for h in self._blocks if h not in pinned), None)
328409
if victim is None:
329410
break
@@ -333,6 +414,19 @@ def _evict_to_budget(self) -> None:
333414
self._bytes_evicted += block.nbytes
334415
self._epoch += 1
335416

417+
def _put_locked(self, block: CacheBlock) -> bool:
418+
existing = self._blocks.get(block.block_hash)
419+
if existing is not None:
420+
if existing.payload_sha256 != block.payload_sha256:
421+
self._put_failures += 1
422+
raise ValueError("content-address collision with different payload")
423+
self._blocks.move_to_end(block.block_hash)
424+
return False
425+
self._blocks[block.block_hash] = block
426+
self._bytes_used += block.nbytes
427+
self._epoch += 1
428+
return True
429+
336430

337431
def total_payload_bytes(blocks: Iterable[CacheBlock]) -> int:
338432
return sum(block.nbytes for block in blocks)

inference_engine/distributed/prefill_worker.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,14 @@ def __init__(
9393
max_jobs: int = 128,
9494
completed_ttl_s: float = 600.0,
9595
max_prompt_tokens: int = 131_072,
96+
estimated_snapshot_bytes_per_token: int = 16,
9697
) -> None:
9798
if min(
9899
max_concurrent_jobs,
99100
max_jobs,
100101
completed_ttl_s,
101102
max_prompt_tokens,
103+
estimated_snapshot_bytes_per_token,
102104
) <= 0:
103105
raise ValueError("worker limits must be > 0")
104106
if (engine is None) == (engine_factory is None):
@@ -110,6 +112,9 @@ def __init__(
110112
self.max_jobs = int(max_jobs)
111113
self.completed_ttl_s = float(completed_ttl_s)
112114
self.max_prompt_tokens = int(max_prompt_tokens)
115+
self.estimated_snapshot_bytes_per_token = int(
116+
estimated_snapshot_bytes_per_token,
117+
)
113118
self._jobs: dict[str, PrefillJob] = {}
114119
self._requests: dict[tuple[str, str], str] = {}
115120
self._lock = threading.RLock()
@@ -190,6 +195,14 @@ def submit(
190195
if deadline_ms > 0 else 0.0
191196
),
192197
)
198+
estimated_bytes = (
199+
len(job.token_ids) * self.estimated_snapshot_bytes_per_token
200+
)
201+
if estimated_bytes > self.cache_store.max_bytes:
202+
raise ValueError(
203+
f"estimated final snapshot {estimated_bytes} exceeds "
204+
f"cache capacity {self.cache_store.max_bytes}",
205+
)
193206
self._jobs[job.job_id] = job
194207
self._requests[request_key] = job.job_id
195208
job.future = self._executor.submit(self._run, job.job_id)
@@ -216,6 +229,7 @@ def cancel(self, job_id: str, tenant_id: str) -> bool:
216229
if job.future is not None and job.future.cancel():
217230
job.state = PrefillJobState.CANCELLED
218231
job.finished_at = time.time()
232+
self.cache_store.release_reservation(job.job_id)
219233
return True
220234

221235
def stats(self) -> tuple[int, int, float, int]:
@@ -239,6 +253,7 @@ def _run(self, job_id: str) -> None:
239253
if job.cancelled.is_set():
240254
job.state = PrefillJobState.CANCELLED
241255
job.finished_at = time.time()
256+
self.cache_store.release_reservation(job.job_id)
242257
return
243258
job.state = PrefillJobState.RUNNING
244259
started = time.perf_counter()
@@ -252,6 +267,13 @@ def _run(self, job_id: str) -> None:
252267
timer.daemon = True
253268
timer.start()
254269
try:
270+
# MLX workers are single-job. Reserve the full current budget
271+
# before model compute so adaptive resizing and unrelated
272+
# boundaries cannot evict the final snapshot before leasing.
273+
self.cache_store.reserve(
274+
job.job_id,
275+
self.cache_store.max_bytes,
276+
)
255277
blocks = tuple(self._engine_for_current_thread().compute_prefill(
256278
job.token_ids,
257279
job.block_hashes,
@@ -264,13 +286,11 @@ def _run(self, job_id: str) -> None:
264286
raise RuntimeError(
265287
"prefill engine must return one snapshot per block hash",
266288
)
267-
for block in blocks:
268-
if job.cancelled.is_set():
269-
raise InterruptedError("prefill job cancelled")
270-
self.cache_store.put(block)
271-
lease = self.cache_store.lookup(job.block_hashes)
272-
if not lease.lease_id:
273-
raise RuntimeError("computed snapshot was not discoverable")
289+
lease = self.cache_store.publish_and_lease(
290+
blocks,
291+
job.block_hashes,
292+
reservation_id=job.job_id,
293+
)
274294
if job.cancelled.is_set():
275295
raise InterruptedError("prefill job cancelled")
276296
with self._lock:
@@ -292,6 +312,7 @@ def _run(self, job_id: str) -> None:
292312
job.state = PrefillJobState.FAILED
293313
job.failure_reason = f"{type(exc).__name__}: {exc}"
294314
finally:
315+
self.cache_store.release_reservation(job.job_id)
295316
if timer is not None:
296317
timer.cancel()
297318
with self._lock:

scripts/start_prefill_worker_node.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ def engine_factory() -> MLXPrefillComputeEngine:
136136
max_jobs=args.max_jobs,
137137
completed_ttl_s=args.job_ttl_s,
138138
max_prompt_tokens=args.max_prompt_tokens,
139+
estimated_snapshot_bytes_per_token=(
140+
args.estimated_snapshot_bytes_per_token
141+
),
139142
)
140143
jobs.warmup()
141144

@@ -290,6 +293,11 @@ def main() -> None:
290293
parser.add_argument("--max-concurrent-jobs", type=int, default=1)
291294
parser.add_argument("--max-jobs", type=int, default=128)
292295
parser.add_argument("--max-prompt-tokens", type=int, default=131072)
296+
parser.add_argument(
297+
"--estimated-snapshot-bytes-per-token",
298+
type=int,
299+
default=400_000,
300+
)
293301
parser.add_argument("--job-ttl-s", type=float, default=600.0)
294302
parser.add_argument("--prefill-tps", type=float, default=20.0)
295303
parser.add_argument("--max-concurrent-rpcs", type=int, default=32)

tests/inference_engine/bridge/test_prefill_worker_launchd.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def test_worker_installer_emits_full_cache_compatibility_contract():
2424
"--max-prompt-tokens",
2525
"--cache-min-gb",
2626
"--memory-reserve-gb",
27+
"--estimated-snapshot-bytes-per-token",
2728
):
2829
assert f"<string>{flag}</string>" in source
2930
assert 'PEER="${KAKEYA_WORKER_PEER:-}"' in source
@@ -48,6 +49,7 @@ def test_two_mac_deployment_uses_allens_as_prefill_only():
4849
"<string>--prefill-policy</string><string>remote-required</string>"
4950
in plist
5051
)
52+
assert "<string>--bind</string><string>0.0.0.0:51051</string>" in plist
5153
assert (
5254
"<string>--prefill-worker-timeout-s</string><string>3600</string>"
5355
in plist
@@ -66,7 +68,13 @@ def test_two_mac_deployment_uses_allens_as_prefill_only():
6668
assert "scripts/start_prefill_worker_node.py" in worker
6769
assert "scripts/start_prefill_cache_node.py" not in worker
6870
assert "<string>--cache-gb</string><string>8</string>" in worker
69-
assert "<string>--cache-min-gb</string><string>0.25</string>" in worker
71+
assert "<string>--cache-min-gb</string><string>1</string>" in worker
72+
assert "<string>--memory-reserve-gb</string><string>0.5</string>" in worker
73+
assert (
74+
"<string>--estimated-snapshot-bytes-per-token</string>"
75+
"<string>400000</string>"
76+
in worker
77+
)
7078
assert "<string>--adaptive-cache</string>" in worker
7179
assert "<string>--window</string><string>2048</string>" in worker
7280
assert "<string>--prefill-tps</string><string>1</string>" in worker

tests/inference_engine/distributed/test_prefill_cache.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,36 @@ def test_resize_evicts_cold_blocks_and_preserves_pinned_budget():
154154
pass
155155
else:
156156
raise AssertionError("expected resize validation")
157+
158+
159+
def test_reservation_blocks_adaptive_shrink_and_rejects_upfront():
160+
store = PrefixCacheStore(_compat(), max_bytes=10, node_id="x")
161+
store.reserve("job", 6)
162+
assert not store.resize(5)
163+
assert store.stats().max_bytes == 10
164+
with pytest.raises(ValueError, match="available cache budget"):
165+
store.reserve("too-large", 5)
166+
store.release_reservation("job")
167+
assert store.resize(5)
168+
169+
170+
def test_publish_and_lease_atomically_pins_final_snapshot():
171+
store = PrefixCacheStore(_compat(), max_bytes=10, node_id="x")
172+
old = CacheBlock.create(bytes.fromhex("03" * 32), 1, b"old!")
173+
store.put(old)
174+
hashes = chained_block_hashes([1, 2, 3, 4], _compat())
175+
blocks = (
176+
CacheBlock.create(hashes[0], 2, b"mid"),
177+
CacheBlock.create(hashes[1], 4, b"final!"),
178+
)
179+
store.reserve("job", 6)
180+
lease = store.publish_and_lease(
181+
blocks,
182+
hashes,
183+
reservation_id="job",
184+
)
185+
assert lease.hit_block_count == 2
186+
assert lease.hit_token_count == 4
187+
assert store.fetch(lease.lease_id) == (blocks[-1],)
188+
assert hashes[-1] in store.block_hashes()
189+
assert not store.resize(1)

0 commit comments

Comments
 (0)