Skip to content

Commit 8cf5a99

Browse files
authored
Merge pull request #229 from FluffyAIcode/AgentMemory/decode-worker-sink-window-fix-0722
fix(runtime): stabilize isolated Prefill sessions
2 parents c8b84d1 + 525ec47 commit 8cf5a99

8 files changed

Lines changed: 279 additions & 67 deletions

File tree

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ WORKDIR /app
4444
# pins all the runtime packages we ship.
4545
COPY requirements.txt ./
4646
RUN pip install --upgrade pip \
47+
&& pip install --index-url https://download.pytorch.org/whl/cpu "torch>=2.4,<3.0" \
4748
&& pip install -r requirements.txt
4849

4950
# Application source. `.dockerignore` prunes tests/, results/, .git/,

inference_engine/backends/mlx/decode_worker.py

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def __init__(self, error_type: str, message: str) -> None:
5656
self.message = message
5757

5858

59+
class DecodeWorkerSessionClosed(DecodeWorkerError):
60+
"""A verifier proxy was used after its router session was closed."""
61+
62+
5963
@dataclass(frozen=True)
6064
class DecodeWorkerConfig:
6165
model_id: str
@@ -824,6 +828,15 @@ def _mark_current(self, session_id: str) -> None:
824828
with self._lock:
825829
self._restored_generation[session_id] = self._generation
826830

831+
def _checkpoint_locked(self, session_id: str) -> ProofCheckpoint:
832+
"""Return a live checkpoint while the caller holds ``_lock``."""
833+
try:
834+
return self._checkpoints[session_id]
835+
except KeyError as exc:
836+
raise DecodeWorkerSessionClosed(
837+
f"decode session {session_id!r} is closed"
838+
) from exc
839+
827840

828841
class DecodeWorkerSession:
829842
"""Verifier-shaped proxy used by existing append/generate coordinators."""
@@ -837,6 +850,16 @@ def __init__(self, client: DecodeWorkerClient, session_id: str) -> None:
837850
self.next_global_position = 0
838851
self._kv_live_bytes = 0
839852

853+
def _sink_window_slice(self, sequence: list[int]) -> list[int]:
854+
"""Mirror the child verifier's bounded token-id cache layout."""
855+
sink_size = int(self.client.config.sink_size)
856+
window_size = int(self.client.config.window_size)
857+
budget = sink_size + window_size
858+
if len(sequence) <= budget:
859+
return list(sequence)
860+
tail = list(sequence[-window_size:]) if window_size else []
861+
return list(sequence[:sink_size]) + tail
862+
840863
def _apply(self, state: dict[str, Any]) -> dict[str, Any]:
841864
self.cached_token_sequence = [
842865
int(token) for token in state.get("cached_token_ids", ())
@@ -853,85 +876,97 @@ def prefill(
853876
cancel_event: threading.Event | None = None,
854877
) -> None:
855878
tokens = [int(token) for token in prompt_ids]
856-
state = self.client._session_request(
857-
self.session_id,
858-
"Init",
859-
{"token_ids": tokens},
860-
cancel_event=cancel_event,
861-
)
862-
checkpoint = self.client._checkpoints[self.session_id]
863-
checkpoint.snapshot = None
864-
checkpoint.compatibility = None
865-
checkpoint.replay_token_ids = list(tokens)
866-
checkpoint.initialized = True
867-
self.client._mark_current(self.session_id)
868-
self._apply(state)
879+
with self.client._lock:
880+
checkpoint = self.client._checkpoint_locked(self.session_id)
881+
state = self.client._session_request(
882+
self.session_id,
883+
"Init",
884+
{"token_ids": tokens},
885+
cancel_event=cancel_event,
886+
)
887+
checkpoint.snapshot = None
888+
checkpoint.compatibility = None
889+
checkpoint.replay_token_ids = list(tokens)
890+
checkpoint.initialized = True
891+
self.client._mark_current(self.session_id)
892+
self._apply(state)
869893

870894
def append_accepted_tokens(
871895
self,
872896
tokens: list[int],
873897
cancel_event: threading.Event | None = None,
874898
) -> None:
875899
committed = [int(token) for token in tokens]
876-
state = self.client._session_request(
877-
self.session_id,
878-
"Append",
879-
{"token_ids": committed},
880-
cancel_event=cancel_event,
881-
)
882-
self.client._checkpoints[self.session_id].replay_token_ids.extend(committed)
883-
self._apply(state)
900+
with self.client._lock:
901+
checkpoint = self.client._checkpoint_locked(self.session_id)
902+
state = self.client._session_request(
903+
self.session_id,
904+
"Append",
905+
{"token_ids": committed},
906+
cancel_event=cancel_event,
907+
)
908+
checkpoint.replay_token_ids.extend(committed)
909+
self._apply(state)
884910

885911
def generate_step(
886912
self,
887913
cancel_event: threading.Event | None = None,
888914
) -> int:
889-
state = self.client._session_request(
890-
self.session_id,
891-
"GenerateStep",
892-
{},
893-
cancel_event=cancel_event,
894-
)
895-
token_id = int(state["token_id"])
896-
self.client._checkpoints[self.session_id].replay_token_ids.append(token_id)
897-
self._apply(state)
898-
return token_id
915+
with self.client._lock:
916+
checkpoint = self.client._checkpoint_locked(self.session_id)
917+
state = self.client._session_request(
918+
self.session_id,
919+
"GenerateStep",
920+
{},
921+
cancel_event=cancel_event,
922+
)
923+
token_id = int(state["token_id"])
924+
checkpoint.replay_token_ids.append(token_id)
925+
self._apply(state)
926+
return token_id
899927

900928
def import_snapshot(
901929
self,
902930
payload: bytes,
903931
compatibility: Any,
904932
) -> dict[str, Any]:
905933
compat = asdict(compatibility)
906-
# Ensure a worker-side session exists before importing its cache.
907-
self.client._session_request(
908-
self.session_id, "Init", {"token_ids": []}
909-
)
910-
state = self.client._session_request(
911-
self.session_id,
912-
"ImportSnapshot",
913-
{"compatibility": compat},
914-
bytes(payload),
915-
)
916-
checkpoint = self.client._checkpoints[self.session_id]
917-
checkpoint.snapshot = bytes(payload)
918-
checkpoint.compatibility = compat
919-
checkpoint.replay_token_ids = []
920-
checkpoint.initialized = True
921-
self.client._mark_current(self.session_id)
922-
return self._apply(state)
934+
snapshot = bytes(payload)
935+
# Pin the router checkpoint and proxy lifecycle across the full
936+
# Init -> ImportSnapshot -> checkpoint-publication transaction.
937+
# Close/cancel cleanup uses the same RLock and therefore cannot remove
938+
# restart state after the child accepted the import but before it is
939+
# made durable on the router.
940+
with self.client._lock:
941+
checkpoint = self.client._checkpoint_locked(self.session_id)
942+
self.client._session_request(
943+
self.session_id, "Init", {"token_ids": []}
944+
)
945+
state = self.client._session_request(
946+
self.session_id,
947+
"ImportSnapshot",
948+
{"compatibility": compat},
949+
snapshot,
950+
)
951+
checkpoint.snapshot = snapshot
952+
checkpoint.compatibility = compat
953+
checkpoint.replay_token_ids = []
954+
checkpoint.initialized = True
955+
self.client._mark_current(self.session_id)
956+
return self._apply(state)
923957

924958
def reset(self) -> None:
925-
state = self.client._session_request(
926-
self.session_id, "Init", {"token_ids": []}
927-
)
928-
checkpoint = self.client._checkpoints[self.session_id]
929-
checkpoint.snapshot = None
930-
checkpoint.compatibility = None
931-
checkpoint.replay_token_ids = []
932-
checkpoint.initialized = True
933-
self.client._mark_current(self.session_id)
934-
self._apply(state)
959+
with self.client._lock:
960+
checkpoint = self.client._checkpoint_locked(self.session_id)
961+
state = self.client._session_request(
962+
self.session_id, "Init", {"token_ids": []}
963+
)
964+
checkpoint.snapshot = None
965+
checkpoint.compatibility = None
966+
checkpoint.replay_token_ids = []
967+
checkpoint.initialized = True
968+
self.client._mark_current(self.session_id)
969+
self._apply(state)
935970

936971
def k_seq_length(self, _session: Any) -> int:
937972
return len(self.cached_token_sequence)

inference_engine/bench/prefill_fleet_report.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
"allens_cold_restore",
1111
"agent_generator",
1212
"agent_critic",
13+
"agent_premise_auditor",
14+
"agent_definition_auditor",
15+
"agent_counterexample_worker",
16+
"agent_decomposer",
17+
"agent_formalizer",
18+
"agent_prover",
19+
"agent_adversarial_proponent",
20+
"agent_judge",
1321
)
1422
HIT_SOURCES = ("remote_worker", "primary_hot", "allens_offload", "unknown")
1523
_PRIVATE_KEYS = {

inference_engine/distributed/prefill_worker.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def __init__(
147147
self._requests: dict[tuple[str, str], str] = {}
148148
self._lock = threading.RLock()
149149
self._thread_local = threading.local()
150+
self._measured_tokens_per_second = 0.0
150151
self._executor = ThreadPoolExecutor(
151152
max_workers=self.max_concurrent_jobs,
152153
thread_name_prefix="kakeya-prefill-worker",
@@ -275,6 +276,11 @@ def stats(self) -> tuple[int, int, float, int]:
275276
load = min(1.0, (running + queued) / self.max_concurrent_jobs)
276277
return running, queued, load, queued_tokens
277278

279+
def measured_tokens_per_second(self, fallback: float = 0.0) -> float:
280+
with self._lock:
281+
measured = self._measured_tokens_per_second
282+
return measured if measured > 0 else max(0.0, float(fallback))
283+
278284
def close(self) -> None:
279285
self._executor.shutdown(wait=False, cancel_futures=True)
280286

@@ -364,8 +370,22 @@ def _run(self, job_id: str) -> None:
364370
if timer is not None:
365371
timer.cancel()
366372
with self._lock:
367-
job.compute_ms = (time.perf_counter() - started) * 1000.0
373+
elapsed_s = time.perf_counter() - started
374+
job.compute_ms = elapsed_s * 1000.0
368375
job.finished_at = time.time()
376+
if (
377+
job.state == PrefillJobState.COMPLETED
378+
and elapsed_s > 0
379+
and job.token_ids
380+
):
381+
sample = len(job.token_ids) / elapsed_s
382+
if self._measured_tokens_per_second <= 0:
383+
self._measured_tokens_per_second = sample
384+
else:
385+
self._measured_tokens_per_second = (
386+
0.25 * sample
387+
+ 0.75 * self._measured_tokens_per_second
388+
)
369389

370390
def _update_job_progress(self, job_id: str, token_count: int) -> None:
371391
with self._lock:
@@ -460,8 +480,14 @@ async def SubmitPrefillJob(self, request, context): # noqa: N802
460480
status=int(job.state),
461481
worker_node_id=self.node_id,
462482
queue_eta_ms=(
463-
self.jobs.stats()[3] / self.tokens_per_second_prefill * 1000.0
464-
if self.tokens_per_second_prefill > 0 else 0.0
483+
self.jobs.stats()[3]
484+
/ self.jobs.measured_tokens_per_second(
485+
self.tokens_per_second_prefill,
486+
)
487+
* 1000.0
488+
if self.jobs.measured_tokens_per_second(
489+
self.tokens_per_second_prefill,
490+
) > 0 else 0.0
465491
),
466492
)
467493

scripts/start_prefill_worker_node.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,17 @@ def refresh_cache_budget() -> tuple[int, int]:
171171
def card() -> NodeCapability:
172172
active_model_bytes, _ = refresh_cache_budget()
173173
inflight, queued, load, queued_tokens = jobs.stats()
174+
measured_prefill_tps = jobs.measured_tokens_per_second(
175+
args.prefill_tps,
176+
)
174177
worker = PrefillWorkerCapability(
175178
compatibility=compatibility,
176179
worker_address=args.advertise,
177180
max_concurrent_jobs=args.max_concurrent_jobs,
178181
inflight_jobs=inflight,
179182
queued_jobs=queued,
180183
load=load,
181-
tokens_per_second_prefill=args.prefill_tps,
184+
tokens_per_second_prefill=measured_prefill_tps,
182185
ram_bytes_free=max(
183186
0,
184187
physical_memory_bytes()
@@ -197,7 +200,7 @@ def card() -> NodeCapability:
197200
args.cache_model_id or args.model_id,
198201
CapabilityRole.PREFILL_COMPUTE,
199202
args.quantization,
200-
args.prefill_tps,
203+
measured_prefill_tps,
201204
),
202205
),
203206
announced_at_unix=time.time(),

0 commit comments

Comments
 (0)