main
py 988 lines 39.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Worker-side orchestration of Mooncake control, memory, and data planes.
4
5 Consumer Workers expose rank-local reservations and publish received tensors.
6 Producer Workers reserve every destination shard, bind computed sources, run
7 batched Mooncake writes, and report asynchronous completion to the Scheduler.
8 """
9
10 from __future__ import annotations
11
12 import math
13 import threading
14 import time
15 from collections.abc import Callable
16 from concurrent.futures import Future, ThreadPoolExecutor
17 from functools import partial
18 from typing import TYPE_CHECKING, Any, TypeVar, cast
19
20 import torch
21
22 from vllm.distributed.ec_transfer.ec_connector.mooncake.config import (
23 _RESERVATION_TTL_SECONDS,
24 MooncakeECConfig,
25 )
26 from vllm.distributed.ec_transfer.ec_connector.mooncake.control import (
27 ConsumerControlServer,
28 ControlClient,
29 make_cancel_request,
30 )
31 from vllm.distributed.ec_transfer.ec_connector.mooncake.memory import (
32 ConsumerMemoryPool,
33 ProducerMemoryPool,
34 )
35 from vllm.distributed.ec_transfer.ec_connector.mooncake.metadata import (
36 ECMooncakeConnectorMetadata,
37 ECMooncakePushSpec,
38 ECMooncakeWorkerMetadata,
39 )
40 from vllm.distributed.ec_transfer.ec_connector.mooncake.producer import (
41 ProducerPushManager,
42 ProducerPushRecord,
43 )
44 from vllm.distributed.ec_transfer.ec_connector.mooncake.reservation import (
45 ConsumerReservationManager,
46 ConsumerReservationState,
47 )
48 from vllm.distributed.ec_transfer.ec_connector.mooncake.transfer import (
49 MooncakeTransfer,
50 ensure_mooncake_available,
51 )
52 from vllm.distributed.ec_transfer.ec_connector.mooncake_store_embedding.data import (
53 build_embedding_namespace,
54 )
55 from vllm.logger import init_logger
56 from vllm.utils.network_utils import get_ip
57
58 logger = init_logger(__name__)
59
60 _T = TypeVar("_T")
61
62 if TYPE_CHECKING:
63 from vllm.config import VllmConfig
64
65 from ..mooncake_store_embedding.backend import (
66 MooncakeEmbeddingStoreBackend,
67 )
68
69 _RESERVATION_REFRESH_SECONDS = _RESERVATION_TTL_SECONDS / 2
70 _MAX_CANCELLED_TRANSFER_IDS = 1 << 16
71 _CANCEL_ATTEMPTS = 2
72 _TRANSFER_WORKERS = 4
73 _CONTROL_WORKERS = 8
74
75
76 class _FanoutError(RuntimeError):
77 """Retain shard outcomes after every started task settles."""
78
79 def __init__(self, error: BaseException, results: list[Any | None]) -> None:
80 self.results = results
81 super().__init__(str(error))
82
83
84 class ECMooncakeWorker:
85 """Orchestrate consumer reservations and producer push batches.
86
87 Consumers may use TP, PP, and DP. Only the first PP stage receives encoder
88 outputs, and each TP rank exposes a consecutive control port and receives
89 the same source concurrently. Producers remain unsharded and unreplicated.
90 With DP, the caller must route both halves of a request to the same replica
91 and pass that replica's control address to the producer.
92 """
93
94 def __init__(self, vllm_config: VllmConfig) -> None:
95 ensure_mooncake_available()
96 config = MooncakeECConfig.from_vllm_config(vllm_config)
97 self._store_config = (
98 (
99 build_embedding_namespace(vllm_config),
100 config.store_max_pending_items,
101 config.store_max_pending_bytes,
102 config.store_read_buffer_bytes,
103 )
104 if config.cross_encoder_cache
105 else None
106 )
107 self._output_store: MooncakeEmbeddingStoreBackend | None = None
108 self.is_producer = config.is_producer
109 self.is_consumer = config.is_consumer
110 self._buffer_device = config.buffer_device
111 self._control_host = config.control_host
112 self._control_port = config.control_port
113 self._transfer = MooncakeTransfer(get_ip(), config.protocol)
114 self._consumer_memory = ConsumerMemoryPool(
115 config.pool_size,
116 self._transfer,
117 )
118 self._reservations = ConsumerReservationManager(
119 self._consumer_memory,
120 _RESERVATION_TTL_SECONDS,
121 _MAX_CANCELLED_TRANSFER_IDS,
122 )
123 self._consumer_rank_resolved = False
124 self._is_receiving_rank = True
125 self._tp_rank = 0
126 self._tp_size = 1
127 self._control_server: ConsumerControlServer | None = None
128 # Worker producer
129 self._producer_memory = ProducerMemoryPool(
130 config.pool_size,
131 self._transfer,
132 )
133 self._control_client = ControlClient(config.control_timeout_ms)
134 self._shutdown_drain_timeout_s = config.control_timeout_ms / 1000
135 self._io_executor = ThreadPoolExecutor(
136 max_workers=_TRANSFER_WORKERS,
137 thread_name_prefix="ec-mooncake-transfer",
138 )
139 self._control_executor = ThreadPoolExecutor(
140 max_workers=_CONTROL_WORKERS,
141 thread_name_prefix="ec-mooncake-control",
142 )
143 self._shard_pool: ThreadPoolExecutor | None = None
144 self._shard_pool_lock = threading.Lock()
145 self._push_ready = threading.Event()
146 self._producer_pushes = ProducerPushManager(self._push_ready.set)
147 self._dispatch_stop = threading.Event()
148 self._dispatcher: threading.Thread | None = None
149 self._failed_saves: set[str] = set()
150 self._collecting_sources = False
151 self._completed_loads: set[str] = set()
152 self._failed_loads: set[str] = set()
153 self._shutdown = False
154 if self.is_producer:
155 self._dispatcher = threading.Thread(
156 target=self._dispatch_pushes, name="ec-mooncake-ready", daemon=True
157 )
158 self._dispatcher.start()
159
160 def _dispatch_pushes(self) -> None:
161 pending_event = False
162 while not self._dispatch_stop.is_set():
163 self._push_ready.wait(timeout=0.001 if pending_event else None)
164 self._push_ready.clear()
165 if self._dispatch_stop.is_set():
166 break
167 if self._collecting_sources:
168 pending_event = False
169 continue
170 pending_event = self._producer_pushes.submit_batches(
171 self._io_executor, self._push_batch
172 )
173
174 def _resolve_consumer_rank(self) -> None:
175 """Place this worker in the consumer receive topology."""
176 if self._consumer_rank_resolved:
177 return
178 self._consumer_rank_resolved = True
179 try:
180 from vllm.distributed.parallel_state import get_pp_group, get_tp_group
181
182 tp_group = get_tp_group()
183 self._tp_rank = tp_group.rank_in_group
184 self._tp_size = tp_group.world_size
185 self._is_receiving_rank = get_pp_group().is_first_rank
186 except AssertionError:
187 # Groups are only absent outside a distributed run, where this
188 # worker is the whole consumer.
189 self._tp_rank = 0
190 self._tp_size = 1
191 self._is_receiving_rank = True
192
193 def start_services(self) -> None:
194 if self._store_config is not None and self._output_store is None:
195 from ..mooncake_store_embedding.backend import (
196 MooncakeEmbeddingStoreBackend,
197 )
198 from ..mooncake_store_embedding.store_client import (
199 create_mooncake_embedding_store_client,
200 )
201
202 namespace, max_items, max_bytes, read_bytes = self._store_config
203 self._output_store = MooncakeEmbeddingStoreBackend(
204 create_mooncake_embedding_store_client(read_buffer_bytes=read_bytes),
205 namespace,
206 max_pending_items=max_items,
207 max_pending_bytes=max_bytes,
208 )
209 if not self.is_consumer or self._control_server is not None:
210 return
211 self._resolve_consumer_rank()
212 if not self._is_receiving_rank:
213 # Later pipeline stages hold no encoder outputs, so they need
214 # neither a receive pool nor a control channel.
215 return
216 self._consumer_memory.prepare(torch.device(self._buffer_device))
217 consumer_pool = self._consumer_memory.tensor
218 if consumer_pool is None:
219 raise RuntimeError(
220 "Mooncake push mode requires a registered consumer buffer pool."
221 )
222 base_port = self._control_port
223 self._control_server = ConsumerControlServer(
224 # The reservation channel hands out registered-memory addresses
225 # and takes cancellations, so it listens where the Consumer
226 # advertises itself (`ec_ip`), not on every interface.
227 self._control_host,
228 base_port + self._tp_rank,
229 self._reserve_push_destination,
230 self._push_status,
231 self._reservations.complete,
232 self._reservations.cancel,
233 self._reservations.expire,
234 peer_ports=[base_port + rank for rank in range(self._tp_size)],
235 device=consumer_pool.device,
236 drain_ready=self._reservations.drain_ready,
237 )
238 try:
239 self._control_server.start()
240 except Exception:
241 self._control_server.close()
242 self._control_server = None
243 raise
244
245 def _reserve_push_destination(self, payload: dict[str, Any]) -> dict[str, Any]:
246 transfer_id = str(payload["transfer_id"])
247 mm_hash = str(payload["mm_hash"])
248 nbytes = int(payload["nbytes"])
249 shape = tuple(int(value) for value in payload["shape"])
250 dtype_name = str(payload["dtype"])
251 dtype = getattr(torch, dtype_name, None)
252 if not isinstance(dtype, torch.dtype):
253 raise ValueError(f"Unsupported torch dtype string: {dtype_name!r}")
254 expected_nbytes = math.prod(shape) * dtype.itemsize
255 if expected_nbytes != nbytes:
256 raise ValueError("shape and dtype do not match nbytes")
257
258 self._reservations.expire()
259 reservation, should_write = self._reservations.reserve(
260 transfer_id, mm_hash, nbytes, shape, dtype_name, dtype
261 )
262 if reservation is None:
263 raise RuntimeError("EC consumer buffer pool is full")
264 if reservation.state in {
265 ConsumerReservationState.CANCEL_PENDING,
266 ConsumerReservationState.CANCELLED,
267 }:
268 return {
269 "reservation_id": "",
270 "dst_session": "",
271 "dst_ptr": 0,
272 "nbytes": nbytes,
273 "write": False,
274 "ready": False,
275 "cancelled": True,
276 }
277 assert reservation.allocation is not None
278
279 return {
280 "reservation_id": reservation.reservation_id,
281 "dst_session": self._transfer.local_session(),
282 "dst_ptr": reservation.allocation.tensor.data_ptr(),
283 "nbytes": reservation.allocation.tensor.nbytes,
284 "write": should_write,
285 "ready": reservation.state is ConsumerReservationState.READY,
286 "cached": reservation.lease is not None,
287 }
288
289 def _push_status(self, transfer_id: str) -> dict[str, Any] | None:
290 reservation = self._reservations.status(transfer_id)
291 if reservation is None:
292 return None
293 assert reservation.allocation is not None
294 return {
295 "mm_hash": reservation.mm_hash,
296 "ready": reservation.state is ConsumerReservationState.READY,
297 "reservation_id": reservation.reservation_id,
298 "nbytes": reservation.allocation.tensor.nbytes,
299 "shape": list(reservation.shape),
300 "dtype": reservation.dtype,
301 }
302
303 def _shard_executor(self) -> ThreadPoolExecutor:
304 """Use a separate pool so nested shard fan-out cannot deadlock."""
305 with self._shard_pool_lock:
306 if self._shard_pool is None:
307 self._shard_pool = ThreadPoolExecutor(
308 max_workers=32, thread_name_prefix="ec-mooncake-shard"
309 )
310 return self._shard_pool
311
312 def _reserve_one(self, addr: str, spec: ECMooncakePushSpec) -> dict[str, Any]:
313 result = self._control_client.request(
314 addr,
315 {
316 "op": "reserve",
317 "transfer_id": spec.transfer_id,
318 "mm_hash": spec.mm_hash,
319 "nbytes": spec.nbytes,
320 "shape": list(spec.shape),
321 "dtype": spec.dtype,
322 },
323 )
324 if not isinstance(result, dict):
325 raise RuntimeError("Invalid EC reservation response")
326 result["_received_at"] = time.monotonic()
327 result["addr"] = addr
328 return result
329
330 def _run_fanout(
331 self,
332 tasks: list[Callable[[], _T]],
333 on_submit: Callable[[int, Future[_T]], None] | None = None,
334 ) -> list[_T]:
335 """Run every started shard task and retain partial results on failure.
336
337 Waiting for all submitted tasks is what makes source-memory release and
338 partial-reservation cleanup safe after one shard fails.
339 """
340 if not tasks:
341 return []
342 futures: list[tuple[int, Future[_T]]] = []
343 results: list[_T | None] = [None] * len(tasks)
344 error: BaseException | None = None
345 for index, task in enumerate(tasks[1:], 1):
346 try:
347 future = self._shard_executor().submit(task)
348 except Exception as exc:
349 error = exc
350 break
351 futures.append((index, future))
352 if on_submit is not None:
353 on_submit(index, future)
354 if error is None:
355 try:
356 results[0] = tasks[0]()
357 except Exception as exc:
358 error = exc
359 for index, future in futures:
360 try:
361 results[index] = future.result()
362 except Exception as exc:
363 if error is None:
364 error = exc
365 if error is not None:
366 raise _FanoutError(error, results)
367 return cast(list[_T], results)
368
369 def _cancel_reservations(
370 self,
371 spec: ECMooncakePushSpec,
372 reservations: list[dict[str, Any]],
373 *,
374 refresh: bool = False,
375 record: ProducerPushRecord | None = None,
376 ) -> None:
377 reservations = [
378 shard for shard in reservations if not shard.get("cancelled", False)
379 ]
380 if not reservations:
381 return
382
383 def cancel(shard: dict[str, Any]) -> dict[str, Any]:
384 addr = shard.get("addr")
385 if not isinstance(addr, str) or not addr:
386 raise RuntimeError(
387 "EC reservation is missing a confirmed shard address"
388 )
389 result = self._control_client.request(
390 addr,
391 make_cancel_request(
392 spec.transfer_id,
393 str(shard.get("reservation_id", "")),
394 abandon=True,
395 refresh=refresh,
396 ),
397 )
398 if not isinstance(result, dict) or not result.get("cancelled"):
399 raise RuntimeError(
400 f"Could not cancel EC reservation for mm_hash={spec.mm_hash}"
401 )
402 return shard
403
404 def track(_index: int, future: Future[dict[str, Any]]) -> None:
405 if record is not None:
406 self._producer_pushes.track_shard_futures([record], [future])
407
408 self._run_fanout([partial(cancel, shard) for shard in reservations], track)
409
410 def _retry_cancel_reservations(
411 self,
412 spec: ECMooncakePushSpec,
413 reservations: list[dict[str, Any]],
414 *,
415 record: ProducerPushRecord | None = None,
416 ) -> None:
417 pending = [shard for shard in reservations if not shard.get("cancelled", False)]
418 error: _FanoutError | None = None
419 for _ in range(_CANCEL_ATTEMPTS):
420 try:
421 self._cancel_reservations(spec, pending, record=record)
422 except _FanoutError as exc:
423 error = exc
424 pending = [
425 shard
426 for index, shard in enumerate(pending)
427 if exc.results[index] is None
428 ]
429 continue
430 return
431 assert error is not None
432 raise error
433
434 def _reserve_remote(self, spec: ECMooncakePushSpec) -> list[dict[str, Any]]:
435 """Reserve a destination on every shard of the consumer."""
436 result = self._reserve_remote_many([spec])[0]
437 if isinstance(result, Exception):
438 raise result
439 return result
440
441 def _refresh_remote_reservations(
442 self,
443 spec: ECMooncakePushSpec,
444 reservations: list[dict[str, Any]],
445 record: ProducerPushRecord | None = None,
446 ) -> list[dict[str, Any]]:
447 stale = [
448 shard
449 for shard in reservations
450 if not shard.get("ready", False)
451 and not shard.get("cached", False)
452 and not shard.get("cancelled", False)
453 ]
454 try:
455 self._cancel_reservations(spec, stale, refresh=True, record=record)
456 except _FanoutError as exc:
457 pending = [
458 shard for index, shard in enumerate(stale) if exc.results[index] is None
459 ]
460 try:
461 self._retry_cancel_reservations(spec, pending, record=record)
462 except _FanoutError as cleanup_error:
463 raise exc from cleanup_error
464 raise
465 return self._reserve_remote(spec)
466
467 @staticmethod
468 def _validate_push_source(push: ProducerPushRecord) -> None:
469 tensor = push.source_tensor
470 assert tensor is not None
471 spec = push.spec
472 if tuple(tensor.shape) != tuple(spec.shape):
473 raise ValueError(f"EC source shape mismatch for mm_hash={spec.mm_hash}")
474 if str(tensor.dtype).split(".")[-1] != spec.dtype:
475 raise ValueError(f"EC source dtype mismatch for mm_hash={spec.mm_hash}")
476 if not tensor.is_contiguous():
477 raise ValueError(f"EC source must be contiguous for mm_hash={spec.mm_hash}")
478 if tensor.nbytes != spec.nbytes:
479 raise ValueError(f"EC source size mismatch for mm_hash={spec.mm_hash}")
480
481 def start_save_caches(
482 self,
483 metadata: ECMooncakeConnectorMetadata,
484 encoder_cache: dict[str, torch.Tensor] | None = None,
485 **kwargs: Any,
486 ) -> None:
487 self._collecting_sources = True
488 new: dict[str, list[ProducerPushRecord]] = {}
489 for spec in metadata.pushes:
490 try:
491 record, created = self._producer_pushes.reserve(spec, Future)
492 if created:
493 new.setdefault(spec.consumer_zmq, []).append(record)
494 except ValueError:
495 logger.warning("Rejected conflicting EC push", exc_info=True)
496 self._failed_saves.add(spec.request_id)
497 for records in new.values():
498 future = self._control_executor.submit(self._reserve_batch, records)
499 future.add_done_callback(partial(self._reservation_batch_done, records))
500 if not isinstance(encoder_cache, dict):
501 return
502 for mm_hash in dict.fromkeys(spec.mm_hash for spec in metadata.pushes):
503 tensor = encoder_cache.get(mm_hash)
504 if tensor is not None:
505 self._bind_push_source(tensor, mm_hash)
506 if self._output_store is not None:
507 loaded = self._output_store.resolve_inputs(
508 metadata.store_candidates, encoder_cache, self._buffer_device
509 )
510 # Loaded entries precede the runner's new-output snapshot, so they
511 # need explicit P2P source binding here.
512 for mm_hash in loaded:
513 self._bind_push_source(encoder_cache[mm_hash], mm_hash)
514
515 def _reserve_batch(self, records: list[ProducerPushRecord]) -> None:
516 """Resolve independent item futures with one reserve RPC per shard."""
517 if len(records) == 1:
518 record = records[0]
519 try:
520 record.reservation_future.set_result(self._reserve_remote(record.spec))
521 except Exception as exc:
522 record.reservation_future.set_exception(exc)
523 return
524 outcomes = self._reserve_remote_many([record.spec for record in records])
525 self._producer_pushes.finish_reservations(list(zip(records, outcomes)))
526
527 def _reserve_remote_many(
528 self, specs: list[ECMooncakePushSpec]
529 ) -> list[list[dict[str, Any]] | Exception]:
530 shards = None
531 for _ in range(_CANCEL_ATTEMPTS):
532 shards = self._control_client.discover_shards(specs[0].consumer_zmq)
533 if shards is not None:
534 break
535 if shards is None:
536 raise RuntimeError(
537 f"Could not discover every EC consumer shard at {specs[0].consumer_zmq}"
538 )
539
540 def reserve(addr: str) -> list[dict[str, Any]]:
541 if len(specs) == 1:
542 return [{"ok": True, "result": self._reserve_one(addr, specs[0])}]
543 response = self._control_client.request(
544 addr,
545 {
546 "op": "reserve_batch",
547 "items": [
548 {
549 "transfer_id": spec.transfer_id,
550 "mm_hash": spec.mm_hash,
551 "nbytes": spec.nbytes,
552 "shape": list(spec.shape),
553 "dtype": spec.dtype,
554 }
555 for spec in specs
556 ],
557 },
558 )
559 items = response["items"]
560 if len(items) != len(specs):
561 raise RuntimeError("Malformed EC reservation batch response")
562 for item in items:
563 if item.get("ok"):
564 item["result"]["_received_at"] = time.monotonic()
565 return items
566
567 fanout_error = None
568 results: list[Any]
569 try:
570 results = self._run_fanout([partial(reserve, addr) for addr in shards])
571 except _FanoutError as exc:
572 results = exc.results
573 fanout_error = exc
574 outcomes: list[list[dict[str, Any]] | Exception] = []
575 for index, spec in enumerate(specs):
576 reservations: list[dict[str, Any]] = []
577 error = None
578 for addr, items in zip(shards, results):
579 item = items[index] if items is not None else {}
580 if item.get("ok"):
581 reservations.append({**item["result"], "addr": addr})
582 else:
583 error = fanout_error or RuntimeError(item["error"])
584 reservations.append({"addr": addr, "reservation_id": ""})
585 if error is None:
586 outcomes.append(reservations)
587 else:
588 failure = _FanoutError(error, list(reservations))
589 try:
590 self._retry_cancel_reservations(spec, reservations)
591 except Exception as cleanup_error:
592 failure.__cause__ = cleanup_error
593 logger.exception("Failed to release partial EC reservation batch")
594 outcomes.append(failure)
595 return outcomes
596
597 @staticmethod
598 def _reservation_batch_done(
599 records: list[ProducerPushRecord], future: Future[None]
600 ) -> None:
601 error = future.exception()
602 if error is not None:
603 for record in records:
604 if not record.reservation_future.done():
605 record.reservation_future.set_exception(error)
606
607 def start_load_caches(
608 self,
609 metadata: ECMooncakeConnectorMetadata,
610 encoder_cache: dict[str, torch.Tensor],
611 **kwargs: Any,
612 ) -> None:
613 self._resolve_consumer_rank()
614 if not self._is_receiving_rank:
615 # Later pipeline stages never gather multimodal embeddings.
616 return
617 self._transfer.ensure_ready()
618 if self._buffer_device == "cuda" and not torch.accelerator.is_available():
619 raise RuntimeError(
620 "ECMooncakeConnector requires CUDA for ec_buffer_device=cuda"
621 )
622 self._reservations.retire_stale(encoder_cache, metadata.freed)
623
624 for spec in metadata.loads:
625 if spec.mm_hash in encoder_cache:
626 if not spec.local:
627 # The spec's id is one shard's; cancel by transfer.
628 self._reservations.cancel(spec.transfer_id, "")
629 self._completed_loads.add(spec.mm_hash)
630 continue
631 if spec.local:
632 tensor = self._consumer_memory.take_resident(
633 spec.mm_hash, tuple(spec.shape), spec.dtype
634 )
635 else:
636 try:
637 allocation = self._reservations.take(spec.transfer_id, spec.mm_hash)
638 tensor = allocation.tensor
639 except RuntimeError as e:
640 logger.warning("EC Mooncake pushed load failed: %s", e)
641 tensor = None
642 if tensor is None:
643 self._failed_loads.add(spec.mm_hash)
644 else:
645 encoder_cache[spec.mm_hash] = tensor
646 self._completed_loads.add(spec.mm_hash)
647
648 def _push_batch(self, pushes: list[ProducerPushRecord]) -> None:
649 started_at = time.monotonic()
650 ready: list[tuple[ProducerPushRecord, dict[str, Any]]] = []
651 written_pushes: dict[str, ProducerPushRecord] = {}
652 failure: Exception | None = None
653 try:
654 for push in pushes:
655 self._validate_push_source(push)
656 reservations = self._producer_pushes.resolve_reservations(push)
657 stale = [
658 index
659 for index, shard in enumerate(reservations)
660 if not shard.get("ready", False)
661 and not shard.get("cancelled", False)
662 and time.monotonic() - float(shard.get("_received_at", started_at))
663 >= _RESERVATION_REFRESH_SECONDS
664 ]
665 if stale:
666 reservations = self._refresh_remote_reservations(
667 push.spec, reservations, push
668 )
669 self._producer_pushes.replace_reservations(push, reservations)
670 self._producer_pushes.begin_writing(push)
671 writable = [
672 shard
673 for shard in reservations
674 if not shard.get("cached", False)
675 and not shard.get("cancelled", False)
676 and shard.get("write", True)
677 ]
678 source = push.source_tensor
679 assert source is not None
680 for shard in writable:
681 if int(shard["nbytes"]) != source.nbytes:
682 raise RuntimeError(
683 "Reserved EC size does not match tensor for "
684 f"mm_hash={push.spec.mm_hash}"
685 )
686 ready.append((push, shard))
687 written_pushes.setdefault(push.spec.transfer_id, push)
688 if ready:
689 # Stage each source once, then write it to every destination.
690 source_index = {
691 push.spec.transfer_id: index
692 for index, push in enumerate(written_pushes.values())
693 }
694 tensors = [
695 cast(torch.Tensor, push.source_tensor)
696 for push in written_pushes.values()
697 if push.source_tensor is not None
698 ]
699 lengths = [tensor.nbytes for tensor in tensors]
700 staged = self._producer_memory.stage(tensors)
701 registered_sources: list[int] = []
702 if staged is not None:
703 sources = staged.tensors
704 else:
705 sources = tensors
706 registered_sources = self._transfer.acquire_sources(tensors)
707 addresses = [tensor.data_ptr() for tensor in sources]
708 try:
709 by_session: dict[str, list[tuple[int, int]]] = {}
710 session_records: dict[str, dict[str, ProducerPushRecord]] = {}
711 for push, shard in ready:
712 session = str(shard["dst_session"])
713 by_session.setdefault(session, []).append(
714 (source_index[push.spec.transfer_id], int(shard["dst_ptr"]))
715 )
716 session_records.setdefault(session, {})[
717 push.spec.transfer_id
718 ] = push
719
720 def write(session: str, items: list[tuple[int, int]]) -> None:
721 self._transfer.write(
722 session,
723 [addresses[index] for index, _ in items],
724 [dst for _, dst in items],
725 [lengths[index] for index, _ in items],
726 )
727
728 sessions = list(by_session.items())
729
730 # Write shards concurrently to avoid serial TP latency.
731 def track_write(index: int, future: Future[None]) -> None:
732 session = sessions[index][0]
733 self._producer_pushes.track_shard_futures(
734 list(session_records[session].values()), [future]
735 )
736
737 writes: list[Callable[[], None]] = [
738 partial(write, *session) for session in sessions
739 ]
740 self._run_fanout(writes, track_write)
741 finally:
742 if staged is not None:
743 self._producer_memory.release(staged)
744 self._transfer.release_sources(registered_sources)
745
746 self._producer_pushes.begin_notifying(pushes)
747 self._notify_completions(ready)
748 self._producer_pushes.complete(pushes)
749 except Exception as exc:
750 # Report asynchronously; raising here would fail EngineCore.
751 failure = exc
752 logger.exception(
753 "EC Mooncake push batch failed for mm_hashes=%s",
754 [push.spec.mm_hash for push in pushes],
755 )
756 self._producer_pushes.settle_all(pushes)
757 self._abandon_pushes(pushes)
758 finally:
759 if failure is not None:
760 self._producer_pushes.fail(pushes, failure)
761
762 def _notify_completions(
763 self, notifications: list[tuple[ProducerPushRecord, dict[str, Any]]]
764 ) -> None:
765 """Tell the consumer, in one message per destination, what landed."""
766 if not notifications:
767 return
768 by_destination: dict[str, list[tuple[ProducerPushRecord, dict[str, Any]]]] = {}
769 for push, reservation in notifications:
770 by_destination.setdefault(
771 str(reservation.get("addr", push.spec.consumer_zmq)), []
772 ).append((push, reservation))
773 destinations = list(by_destination.items())
774
775 def notify(
776 consumer_zmq: str,
777 items: list[tuple[ProducerPushRecord, dict[str, Any]]],
778 ) -> None:
779 result = self._control_client.request(
780 consumer_zmq,
781 {
782 "op": "complete_batch",
783 "items": [
784 {
785 "transfer_id": push.spec.transfer_id,
786 "reservation_id": reservation["reservation_id"],
787 }
788 for push, reservation in items
789 ],
790 },
791 )
792 completions = result.get("items", []) if isinstance(result, dict) else []
793 if len(completions) != len(items):
794 raise RuntimeError("Malformed EC completion response")
795 for (push, _), completion in zip(items, completions):
796 if not completion.get("completed"):
797 raise RuntimeError(
798 f"Unknown EC reservation for mm_hash={push.spec.mm_hash}"
799 )
800
801 def track(index: int, future: Future[None]) -> None:
802 records = {
803 push.spec.transfer_id: push for push, _ in destinations[index][1]
804 }
805 self._producer_pushes.track_shard_futures(list(records.values()), [future])
806
807 self._run_fanout(
808 [
809 partial(notify, destination, items)
810 for destination, items in destinations
811 ],
812 track,
813 )
814
815 @staticmethod
816 def _known_reservations(record: ProducerPushRecord) -> list[dict[str, Any]]:
817 if record.reservations:
818 return list(record.reservations)
819 try:
820 return list(record.reservation_future.result())
821 except _FanoutError as exc:
822 return [result for result in exc.results if isinstance(result, dict)]
823 except Exception:
824 return []
825
826 def _abandon_pushes(self, pushes: list[ProducerPushRecord]) -> None:
827 """Release the consumer-side reservations of a batch that failed."""
828 for push in pushes:
829 shards = self._known_reservations(push)
830 if not shards:
831 # No confirmed topology means there is no safe address to
832 # cancel. The original push failure remains observable via
833 # ProducerPushManager.fail below.
834 continue
835 try:
836 self._retry_cancel_reservations(push.spec, shards, record=push)
837 except _FanoutError:
838 logger.exception(
839 "Failed to abandon EC reservations for transfer_id=%s",
840 push.spec.transfer_id,
841 )
842
843 def _flush_pending_pushes(self) -> None:
844 self._producer_pushes.submit_batches(
845 self._io_executor,
846 self._push_batch,
847 )
848
849 def _bind_push_source(self, tensor: torch.Tensor, mm_hash: str) -> None:
850 ready_event = None
851 if tensor.device.type == "cuda":
852 ready_event = torch.Event()
853 ready_event.record(torch.accelerator.current_stream(tensor.device))
854 self._producer_pushes.bind_source(mm_hash, tensor, ready_event)
855
856 def _cancel_orphaned_reservation(self, record: ProducerPushRecord) -> None:
857 resolution_error: Exception | None = None
858 try:
859 reservations = self._producer_pushes.resolve_reservations(record)
860 except Exception as exc:
861 resolution_error = exc
862 reservations = self._known_reservations(record)
863 reservations = [
864 shard for shard in reservations if not shard.get("cancelled", False)
865 ]
866 if not reservations:
867 if resolution_error is not None:
868 self._producer_pushes.fail([record], resolution_error)
869 else:
870 self._producer_pushes.finish_cancel(record)
871 return
872 try:
873 self._retry_cancel_reservations(record.spec, reservations, record=record)
874 except Exception as cleanup_error:
875 if resolution_error is not None:
876 combined_error = RuntimeError(
877 f"EC reservation resolution failed ({resolution_error}); "
878 f"cleanup also failed ({cleanup_error})"
879 )
880 combined_error.__cause__ = resolution_error
881 cleanup_error = combined_error
882 self._producer_pushes.fail([record], cleanup_error)
883 return
884 if resolution_error is not None:
885 self._producer_pushes.fail([record], resolution_error)
886 else:
887 self._producer_pushes.finish_cancel(record)
888
889 def get_finished(
890 self, finished_req_ids: set[str]
891 ) -> tuple[set[str] | None, set[str] | None]:
892 if not self.is_producer:
893 return None, None
894
895 if self._output_store is not None:
896 self._output_store.reap()
897 for record in self._producer_pushes.cancel_requests(finished_req_ids):
898 self._producer_pushes.submit_cancel(
899 record,
900 self._io_executor,
901 self._cancel_orphaned_reservation,
902 )
903 return None, None
904
905 def save_caches(
906 self, encoder_cache: dict[str, torch.Tensor], mm_hash: str, **kwargs: Any
907 ) -> None:
908 if not self.is_producer:
909 return
910 tensor = encoder_cache[mm_hash]
911 self._bind_push_source(tensor, mm_hash)
912 if self._output_store is not None:
913 self._output_store.save_output(mm_hash, tensor)
914
915 def build_connector_worker_meta(self) -> ECMooncakeWorkerMetadata | None:
916 if self.is_consumer and not self._is_receiving_rank:
917 # `loaded` is intersected across reporting ranks, so a stage that
918 # never loads must not report at all rather than report nothing.
919 return None
920
921 self._collecting_sources = False
922 self._push_ready.set()
923 self._flush_pending_pushes()
924 failures = self._producer_pushes.poll()
925 for mm_hash, error in failures:
926 logger.error(
927 "EC Mooncake async save failed for mm_hash=%s: %s",
928 mm_hash,
929 error,
930 )
931 reclaimed = self._consumer_memory.drain_reclaimed()
932 meta = ECMooncakeWorkerMetadata(
933 loaded=self._completed_loads,
934 failed_loads=self._failed_loads,
935 reclaimed=reclaimed,
936 pending_saves=self._producer_pushes.pending,
937 failed_saves=self._failed_saves,
938 )
939 self._failed_saves = set()
940 self._completed_loads = set()
941 self._failed_loads = set()
942 return meta
943
944 def close(self) -> None:
945 if self._shutdown:
946 return
947 self._shutdown = True
948 dispatcher = getattr(self, "_dispatcher", None)
949 if dispatcher is not None:
950 self._dispatch_stop.set()
951 self._push_ready.set()
952 dispatcher.join()
953 if self._control_server is not None:
954 self._reservations.begin_shutdown()
955 self._flush_pending_pushes()
956 for record in self._producer_pushes.cancel_requests(None):
957 self._producer_pushes.submit_cancel(
958 record,
959 self._io_executor,
960 self._cancel_orphaned_reservation,
961 )
962 self._control_executor.shutdown(wait=True)
963 self._producer_pushes.submit_batches(
964 self._io_executor, self._push_batch, wait=True
965 )
966 self._io_executor.shutdown(wait=True)
967 if self._shard_pool is not None:
968 self._shard_pool.shutdown(wait=True)
969 # Every producer-side thread that could hold a control socket is stopped.
970 self._control_client.close()
971 drained = True
972 if self._control_server is not None:
973 drained = self._reservations.wait_for_writers(
974 self._shutdown_drain_timeout_s
975 )
976 self._control_server.close()
977 if drained:
978 self._consumer_memory.close()
979 else:
980 logger.error(
981 "Timed out waiting for Mooncake EC writers; keeping the consumer "
982 "receive pool registered"
983 )
984 self._producer_memory.close()
985 self._transfer.close()
986 if self._output_store is not None:
987 self._output_store.shutdown()
988 self._output_store = None