Skip to content

Commit a680424

Browse files
fluffy314cursoragent
authored andcommitted
perf(prefill): export only final MLX snapshot
Eliminate quadratic full-cache serialization by computing every token normally but encoding and compressing only the final chained-prefix snapshot. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d682674 commit a680424

6 files changed

Lines changed: 119 additions & 21 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ prevents adaptive shrink from consuming active reservations, then atomically
361361
publishes and leases the final snapshot before adding optional intermediate
362362
boundaries. The 16GB allens deployment uses a 1 GiB cache floor and 0.5 GiB
363363
memory reserve. Capacity failures are rejected before Prefill starts.
364+
The MLX worker exports and compresses only the final chained-prefix snapshot.
365+
The final hash commits every preceding token block, so exporting a growing full
366+
snapshot at every 64-token boundary is redundant and creates quadratic
367+
serialization/compression work for long Critic contexts.
364368
Interactive prompt templates are deterministic and contain no per-run nonce, so
365369
repeating the same task can reuse allens cold-tier and Primary hot-tier KV.
366370

inference_engine/backends/mlx/prefill_worker.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,7 @@ def compute_prefill(
4545
raise InterruptedError("prefill job cancelled")
4646
first_end = min(size, len(tokens))
4747
self.verifier.prefill(tokens[:first_end])
48-
blocks: list[CacheBlock] = [
49-
self._snapshot(
50-
token_count=first_end,
51-
block_hash=block_hashes[0],
52-
compression=compression,
53-
),
54-
]
55-
for block_index, start in enumerate(
56-
range(first_end, len(tokens), size),
57-
start=1,
58-
):
48+
for start in range(first_end, len(tokens), size):
5949
if cancelled.is_set():
6050
raise InterruptedError("prefill job cancelled")
6151
block = tokens[start:start + size]
@@ -65,12 +55,17 @@ def compute_prefill(
6555
accepted=len(block),
6656
)
6757
self.verifier.next_token_logits = logits[-1].clone()
68-
blocks.append(self._snapshot(
69-
token_count=min(start + size, len(tokens)),
70-
block_hash=block_hashes[block_index],
58+
# Export exactly once. Intermediate full snapshots make encoding
59+
# and compression quadratic in prompt length and are not required
60+
# for correctness because the final chained hash commits every
61+
# preceding token block.
62+
return (
63+
self._snapshot(
64+
token_count=len(tokens),
65+
block_hash=block_hashes[-1],
7166
compression=compression,
72-
))
73-
return blocks
67+
),
68+
)
7469

7570
def _snapshot(
7671
self,

inference_engine/distributed/prefill_cache.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,11 @@ def publish_and_lease(
297297
lease_seconds: float = DEFAULT_LEASE_SECONDS,
298298
) -> PrefixLease:
299299
"""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")
300+
if not blocks or len(blocks) not in (1, len(block_hashes)):
301+
raise ValueError(
302+
"computed snapshots must contain only the final snapshot "
303+
"or one snapshot per block hash",
304+
)
302305
if lease_seconds <= 0:
303306
raise ValueError("lease_seconds must be > 0")
304307
final = blocks[-1]

inference_engine/distributed/prefill_worker.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,10 @@ def _run(self, job_id: str) -> None:
282282
))
283283
if job.cancelled.is_set():
284284
raise InterruptedError("prefill job cancelled")
285-
if len(blocks) != len(job.block_hashes):
285+
if len(blocks) not in (1, len(job.block_hashes)):
286286
raise RuntimeError(
287-
"prefill engine must return one snapshot per block hash",
287+
"prefill engine must return only the final snapshot "
288+
"or one snapshot per block hash",
288289
)
289290
lease = self.cache_store.publish_and_lease(
290291
blocks,

tests/inference_engine/distributed/test_prefill_cache.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,21 @@ def test_publish_and_lease_atomically_pins_final_snapshot():
189189
assert not store.resize(1)
190190

191191

192+
def test_publish_and_lease_accepts_final_snapshot_only():
193+
store = PrefixCacheStore(_compat(), max_bytes=10, node_id="x")
194+
hashes = chained_block_hashes([1, 2, 3, 4], _compat())
195+
final = CacheBlock.create(hashes[-1], 4, b"final!")
196+
store.reserve("job", 6)
197+
lease = store.publish_and_lease(
198+
[final],
199+
hashes,
200+
reservation_id="job",
201+
)
202+
assert lease.hit_block_count == 2
203+
assert lease.hit_token_count == 4
204+
assert store.fetch(lease.lease_id) == (final,)
205+
206+
192207
def test_reservation_and_atomic_publish_validation_guards():
193208
store = PrefixCacheStore(_compat(), max_bytes=10, node_id="x")
194209
with pytest.raises(ValueError, match="reservation id"):
@@ -197,7 +212,7 @@ def test_reservation_and_atomic_publish_validation_guards():
197212
with pytest.raises(ValueError, match="duplicate"):
198213
store.reserve("job", 1)
199214
block = CacheBlock.create(bytes(32), 1, b"abc")
200-
with pytest.raises(ValueError, match="one computed snapshot"):
215+
with pytest.raises(ValueError, match="computed snapshots"):
201216
store.publish_and_lease([], [], reservation_id="job")
202217
with pytest.raises(ValueError, match="lease_seconds"):
203218
store.publish_and_lease(

tests/inference_engine/distributed/test_prefill_worker.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99
import pytest_asyncio
1010

11+
from inference_engine.backends.mlx.prefill_worker import MLXPrefillComputeEngine
1112
from inference_engine.distributed.capability import (
1213
CacheCompatibility,
1314
CompressionCodec,
@@ -55,6 +56,55 @@ def compute_prefill(self, token_ids, block_hashes, *, compression, cancelled):
5556
]
5657

5758

59+
def test_mlx_worker_exports_only_final_snapshot(monkeypatch):
60+
class Logit:
61+
def clone(self):
62+
return self
63+
64+
class Verifier:
65+
def __init__(self):
66+
self.next_token_logits = None
67+
self.forwarded = []
68+
69+
def prefill(self, tokens):
70+
self.forwarded.extend(tokens)
71+
72+
def forward_block(self, tokens):
73+
self.forwarded.extend(tokens)
74+
return [Logit() for _ in tokens]
75+
76+
def commit_or_truncate(self, *, forwarded, accepted):
77+
assert forwarded == accepted
78+
79+
verifier = Verifier()
80+
engine = MLXPrefillComputeEngine(verifier, COMPAT)
81+
snapshots = []
82+
83+
def snapshot(**kwargs):
84+
snapshots.append(kwargs)
85+
return CacheBlock.create(
86+
kwargs["block_hash"],
87+
kwargs["token_count"],
88+
b"final",
89+
)
90+
91+
monkeypatch.setattr(engine, "_snapshot", snapshot)
92+
hashes = [b"a" * 32, b"b" * 32, b"c" * 32]
93+
blocks = engine.compute_prefill(
94+
[1, 2, 3, 4, 5],
95+
hashes,
96+
compression=CompressionCodec.NONE,
97+
cancelled=threading.Event(),
98+
)
99+
assert verifier.forwarded == [1, 2, 3, 4, 5]
100+
assert len(blocks) == 1
101+
assert snapshots == [{
102+
"token_count": 5,
103+
"block_hash": hashes[-1],
104+
"compression": CompressionCodec.NONE,
105+
}]
106+
107+
58108
@pytest_asyncio.fixture
59109
async def worker():
60110
engine = _Engine()
@@ -282,6 +332,36 @@ def test_job_reservation_preserves_unrelated_restore_snapshot():
282332
jobs.close()
283333

284334

335+
def test_job_accepts_final_snapshot_only():
336+
class FinalOnlyEngine:
337+
def compute_prefill(self, token_ids, block_hashes, **_kwargs):
338+
return [
339+
CacheBlock.create(
340+
block_hashes[-1],
341+
len(token_ids),
342+
b"final-only",
343+
),
344+
]
345+
346+
cache = PrefixCacheStore(COMPAT, max_bytes=1024, node_id="final-only")
347+
jobs = PrefillJobStore(FinalOnlyEngine(), cache)
348+
try:
349+
job = jobs.submit(
350+
request_id="final-only",
351+
tenant_id="tenant",
352+
token_ids=[1, 2, 3, 4],
353+
block_hashes=[b"a" * 32, b"b" * 32],
354+
compatibility=COMPAT,
355+
compression=CompressionCodec.NONE,
356+
)
357+
job.future.result(timeout=1)
358+
assert job.state == PrefillJobState.COMPLETED
359+
assert cache.block_hashes() == (b"b" * 32,)
360+
assert cache.fetch(job.lease_id)[0].payload == b"final-only"
361+
finally:
362+
jobs.close()
363+
364+
285365
def test_factory_engine_is_warmed_and_used_on_same_compute_thread():
286366
cache = PrefixCacheStore(COMPAT, max_bytes=1024, node_id="w")
287367
created_on = []

0 commit comments

Comments
 (0)