Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

723 lines
33 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Mooncake transport for EPD encode->prefill image-embedding transfer.
The encode (vision-tower-only) server is the data source: it calls
``batch_transfer_sync`` to ship a contiguous ``[num_tokens, hidden]`` tensor
per item over the same Mooncake RDMA engine the KV path uses. The prefill side
is the receiver that pre-registers buffers and registers with the bootstrap
server (direction reversed relative to prefill->decode).
Wire-frame dataclasses live in :mod:`tokenspeed.runtime.pd.epd.entities`: they
are pure data plus codecs and import no torch, so the protocol contract stays
importable and unit-testable on CPU.
"""
from __future__ import annotations
import concurrent.futures
import os
import threading
import time
from typing import TYPE_CHECKING
import requests
import zmq
from tokenspeed.runtime.pd.base.status import TransferPoll
from tokenspeed.runtime.pd.epd.conn import (
MooncakeEmbeddingManagerBase,
)
from tokenspeed.runtime.pd.epd.entities import (
REGISTER_ROOM_SENTINEL,
EmbeddingArgs,
EmbeddingArgsRegisterInfo,
EmbeddingChunk,
EmbeddingManagerArgs,
EmbeddingTransferError,
EmbeddingTransferInfo,
)
from tokenspeed.runtime.pd.utils import DisaggregationMode, FastQueue
from tokenspeed.runtime.utils import get_colorful_logger
from tokenspeed.runtime.utils.env import envs
from tokenspeed.runtime.utils.network import get_free_port, get_local_ip_by_remote
logger = get_colorful_logger(__name__)
if TYPE_CHECKING:
import torch
def _route_get(bootstrap_addr: str, engine_rank: int, target_dp_group: int):
"""GET the bootstrap server's /route endpoint; ``None`` on any failure.
Must never raise: callers in the receiver ``__init__`` treat ``None`` as a
per-room failure, but an uncaught exception would escape the prefill
scheduler thread and take the whole engine down via SIGUSR1.
"""
url = (
f"http://{bootstrap_addr}/route?"
f"engine_rank={engine_rank}&target_dp_group={target_dp_group}"
)
try:
resp = requests.get(url, timeout=5)
except Exception as e: # noqa: BLE001 -- any transport failure -> per-room fail
logger.error("EPD bootstrap /route fetch failed (%s): %s", url, e)
return None
if resp.status_code == 200:
return resp.json()
return None
def _b(value: object) -> bytes:
return str(value).encode("ascii")
def validate_fanout_frames(
infos: list[EmbeddingTransferInfo], chunk: "EmbeddingChunk"
) -> str | None:
"""Pre-send contract check over a room's full fanout set; None when valid.
Per frame: hidden/dtype must match the chunk; ``span`` (every frame carries
the image's full row count) must equal the chunk's token count (the
unchecked RDMA write silently truncates on divergence); the shard must stay
inside the chunk's rows; and deepstack presence must agree on both sides.
The set must then be either ALL identity (each frame covers the full span --
full-copy broadcast) or a proper shard set whose non-empty shards, sorted by
``row_start``, tile a contiguous range disjointly (an encode rank under
encode_tp>1 serves a contiguous BLOCK of the global shards). A gap/overlap
means the two sides' shard math diverged, so the room must fail loud.
"""
for info in infos:
if not (info.hidden == chunk.hidden and info.dtype == chunk.dtype):
return "embedding shape/dtype contract violated"
if info.span != chunk.n_tokens:
return (
f"image token-count contract violated: receiver expects "
f"{info.span} rows, encode has {chunk.n_tokens} (G2)"
)
if (
info.row_start < 0
or info.n_tokens < 0
or info.row_start + info.n_tokens > chunk.n_tokens
):
return (
f"embedding shard out of range: rows [{info.row_start}, "
f"{info.row_start + info.n_tokens}) of {chunk.n_tokens}"
)
if info.has_deepstack and not chunk.deepstack_width:
return (
"receiver expects deepstack but the chunk carries none "
"(encode-side cache hit without its deepstack half?)"
)
if chunk.deepstack_width and not info.has_deepstack:
return "chunk carries deepstack but the receiver did not allocate for it"
if all(i.row_start == 0 and i.n_tokens == chunk.n_tokens for i in infos):
return None # identity (full-copy) mode
shards = sorted((i.row_start, i.n_tokens) for i in infos if i.n_tokens > 0)
for (start, count), (next_start, _next_count) in zip(shards, shards[1:]):
if start + count != next_start:
return (
"embedding shard frames do not tile contiguously: "
f"[{start}, {start + count}) then [{next_start}, ...)"
)
return None
def shard_payload(
chunk: "EmbeddingChunk", info: EmbeddingTransferInfo
) -> tuple[int, int, int, int]:
"""Source pointer/length math for one receiver's row shard of a chunk.
Returns ``(src_ptr, nbytes, src_deepstack_ptr, deepstack_nbytes)`` for the
rows ``[info.row_start, info.row_start + info.n_tokens)``. Identity frames
reproduce the whole-chunk payload exactly. Row strides are derived from the
chunk's own byte counts so dtype size never needs decoding here.
"""
row_bytes = chunk.nbytes // chunk.n_tokens if chunk.n_tokens else 0
src = chunk.src_embedding_ptr + info.row_start * row_bytes
nbytes = info.n_tokens * row_bytes
deep_src = deep_nbytes = 0
if chunk.deepstack_width and chunk.deepstack_nbytes:
deep_row_bytes = (
chunk.deepstack_nbytes // chunk.n_tokens if chunk.n_tokens else 0
)
deep_src = chunk.src_deepstack_ptr + info.row_start * deep_row_bytes
deep_nbytes = info.n_tokens * deep_row_bytes
return src, nbytes, deep_src, deep_nbytes
class MooncakeEmbeddingSender:
"""Encode-side per-request sender for one bootstrap room.
``poll`` / ``clear`` / ``failure_exception`` are status-only (they touch
only the manager's status maps). ``send`` queues one contiguous embedding
tensor described by scalar fields, keeping this class free of any torch
dependency.
"""
def __init__(self, mgr, bootstrap_addr: str, bootstrap_room: int):
self.mgr = mgr # MooncakeEmbeddingManagerEncode
self.bootstrap_server_url = bootstrap_addr
self.bootstrap_room = bootstrap_room
self.mgr.update_status(bootstrap_room, TransferPoll.Bootstrapping)
self.conclude_state = None
def send(
self,
*,
src_embedding_ptr: int,
n_tokens: int,
hidden: int,
dtype: str,
nbytes: int,
src_deepstack_ptr: int = 0,
deepstack_width: int = 0,
deepstack_nbytes: int = 0,
copy_event: torch.cuda.Event | None = None,
) -> None:
chunk = EmbeddingChunk(
room=self.bootstrap_room,
src_embedding_ptr=src_embedding_ptr,
n_tokens=n_tokens,
hidden=hidden,
dtype=dtype,
nbytes=nbytes,
src_deepstack_ptr=src_deepstack_ptr,
deepstack_width=deepstack_width,
deepstack_nbytes=deepstack_nbytes,
copy_event=copy_event,
)
self.mgr.add_transfer_request(self.bootstrap_room, chunk)
def poll(self) -> int:
# No Bootstrapping timeout here: the never-registered-receiver case is
# handled by the manager's _park_reaper (it fails rooms whose parked chunk
# outlives bootstrap_time_out), so this only reports terminal status.
if self.conclude_state is None:
status = self.mgr.check_status(self.bootstrap_room)
if status in (TransferPoll.Success, TransferPoll.Failed):
self.conclude_state = status
return status
return self.conclude_state
def clear(self) -> None:
if self.bootstrap_room in self.mgr.request_status:
self.mgr.request_status.pop(self.bootstrap_room)
def failure_exception(self):
if self.conclude_state is None:
self.conclude_state = TransferPoll.Failed
self.clear()
with self.mgr.failure_lock:
failure_reason = self.mgr.failure_records.pop(
self.bootstrap_room, "Failed due to an unknown reason from another rank"
)
raise EmbeddingTransferError(
self.bootstrap_room, failure_reason, self.bootstrap_server_url
)
class MooncakeEmbeddingManagerEncode(MooncakeEmbeddingManagerBase):
"""Encode-side (data source) manager: registers itself to the bootstrap
server so the prefill receiver can find it, listens for the receiver's
registration + per-request pre-alloc frames, and a worker thread issues the
one-sided Mooncake write of the embedding.
"""
def __init__(self, args: EmbeddingManagerArgs, embedding_args: EmbeddingArgs):
super().__init__(args, embedding_args, DisaggregationMode.ENCODE)
# room -> {receiver_session -> EmbeddingTransferInfo}
self.transfer_infos: dict[int, dict[str, EmbeddingTransferInfo]] = {}
# Bootstrap registration-wait timeout (default 120s).
self.bootstrap_time_out = envs.TOKENSPEED_DISAGGREGATION_BOOTSTRAP_TIMEOUT.get()
# room -> [(deadline, chunk), ...] chunks popped before the receiver
# registered; flushed by the bootstrap thread once its info arrives.
self._pending: dict[int, list[tuple]] = {}
self._pending_lock = threading.Lock()
# zmq sockets are not thread-safe and status pushes come from any
# fanout-pool thread: keep one PUSH socket per (thread, endpoint).
self._status_tls = threading.local()
self._start_bootstrap_thread()
self._register_to_bootstrap()
# K queues sharded by ROOM: a single consumer per room preserves the
# park/flush/straggler-drop ordering the parking machinery relies on.
# Each queue's worker uses a pool to issue one room's per-receiver writes
# concurrently (each prefill rank is a distinct Mooncake session, so a
# single batch call cannot span them).
cpu_count = os.cpu_count() or 8
pool_size = envs.TOKENSPEED_DISAGGREGATION_THREAD_POOL_SIZE.get_set_value_or(
min(max(4, int(0.75 * cpu_count) // 8), 12)
)
queue_count = envs.TOKENSPEED_DISAGGREGATION_QUEUE_SIZE.get()
assert pool_size >= queue_count, (
f"TOKENSPEED_DISAGGREGATION_THREAD_POOL_SIZE={pool_size} must be >= "
f"TOKENSPEED_DISAGGREGATION_QUEUE_SIZE={queue_count}"
)
self._queues: list[FastQueue] = [FastQueue() for _ in range(queue_count)]
self._executors = [
concurrent.futures.ThreadPoolExecutor(max(1, pool_size // queue_count))
for _ in range(queue_count)
]
for queue, executor in zip(self._queues, self._executors):
threading.Thread(
target=self._transfer_worker, args=(queue, executor), daemon=True
).start()
threading.Thread(target=self._park_reaper, daemon=True).start()
def _start_bootstrap_thread(self):
self.rank_port = get_free_port()
self.server_socket.bind(f"tcp://{get_local_ip_by_remote()}:{self.rank_port}")
def loop():
while True:
msg = self.server_socket.recv_multipart()
# A malformed frame must not kill this daemon thread: a dead
# listener drops every later registration, parking all chunks
# until the reaper fails them. Log and continue.
try:
if msg[0].decode("ascii") == REGISTER_ROOM_SENTINEL:
# registration frame: consumed, no per-session state needed
pass
else:
info = EmbeddingTransferInfo.from_zmq(msg)
self.transfer_infos.setdefault(info.room, {})[
info.mooncake_session_id
] = info
if (
len(self.transfer_infos[info.room])
>= info.required_dst_info_num
):
self.update_status(info.room, TransferPoll.Bootstrapped)
# Re-drive any chunk parked before this room registered.
self._flush_pending(info.room)
except Exception as exc: # noqa: BLE001
logger.error(
"dropping malformed embedding bootstrap frame "
"(%d parts): %s",
len(msg),
exc,
)
threading.Thread(target=loop, daemon=True).start()
def _register_to_bootstrap(self):
ip = get_local_ip_by_remote()
bootstrap_host = self.args.bootstrap_host or ip
payload = {
"role": "Prefill", # bootstrap-server role string for "discoverable data source"
"world_size": self.world_size,
"dp_size": self.dp_size,
"rank_ip": ip,
"rank_port": self.rank_port,
"engine_rank": self.embedding_args.engine_rank,
}
url = f"http://{bootstrap_host}:{self.bootstrap_port}/route"
# The bootstrap HTTP server starts concurrently with this call, so the
# first PUTs can race it and hit connection-refused. A dropped
# registration leaves /route's parallel-info null and the encode silently
# serves no embeddings, so retry until the server accepts it.
last_err = None
for _ in range(60):
try:
resp = requests.put(url, json=payload, timeout=5)
if resp.ok:
return
last_err = f"status {resp.status_code}"
except Exception as e: # noqa: BLE001
last_err = e
time.sleep(0.5)
logger.error(
"encode failed to register to bootstrap server after retries: %s", last_err
)
def add_transfer_request(self, room: int, chunk: EmbeddingChunk) -> None:
if (
room not in self.request_status
or self.check_status(room) == TransferPoll.Failed
):
return
self._queue_for(room).put(chunk)
def _queue_for(self, room: int) -> FastQueue:
# Room-affinity sharding: one consumer per room keeps the park /
# _flush_pending re-enqueue / Success-straggler-drop sequence
# single-threaded per room.
return self._queues[room % len(self._queues)]
def _transfer_worker(
self, queue: FastQueue, executor: concurrent.futures.ThreadPoolExecutor
):
while True:
chunk: EmbeddingChunk = queue.get()
# Already concluded: a re-enqueued straggler copy from _flush_pending
# (which re-puts a parked chunk on each receiver registration). The
# room was already sent + popped, so drop it.
if self.request_status.get(chunk.room) == TransferPoll.Success:
continue
# Snapshot via list(): the bootstrap listener inserts into this inner
# dict without _pending_lock, so next(iter(...)) could raise "dict changed
# size during iteration". list() is GIL-atomic.
info_vals = list(self.transfer_infos.get(chunk.room, {}).values())
# required_dst_info_num (= fanout) is identical across the N frames for a
# room, so read it off any registered frame; None until one registers.
need = info_vals[0].required_dst_info_num if info_vals else None
if (need is None or len(info_vals) < need) and self.request_status.get(
chunk.room
) != TransferPoll.Failed:
# Fewer than N receivers registered (1->N broadcast not yet complete):
# park (don't drop) until the full set arrives. _flush_pending re-drives
# this chunk on every new registration; the reaper fails it on timeout.
parked = True
with self._pending_lock:
info_vals = list(self.transfer_infos.get(chunk.room, {}).values())
need = info_vals[0].required_dst_info_num if info_vals else None
if info_vals and need is not None and len(info_vals) >= need:
# Full set landed inside the park window: send below instead.
parked = False
else:
self._pending.setdefault(chunk.room, []).append(
(time.time() + self.bootstrap_time_out, chunk)
)
if parked:
continue
# All N receivers registered: serve every one concurrently (full copy
# in identity mode, its row shard otherwise) -- each is a distinct
# Mooncake session, so concurrency must come from threads.
infos = list(self.transfer_infos.get(chunk.room, {}).values())
err = validate_fanout_frames(infos, chunk)
if err is not None:
self._fail_room(chunk.room, err, infos)
continue
# Wait the ring copy's completion HERE on the daemon (not the
# encode-loop thread): the one-sided RDMA reads in _send touch GPU
# memory off any CUDA stream, so they must not precede the device copy
# that filled the slot (ViT->send corruption hazard). One wait per
# chunk covers all N receivers (same slot); event.synchronize()
# releases the GIL while waiting.
if chunk.copy_event is not None:
chunk.copy_event.synchronize()
futures = [executor.submit(self._send, info, chunk) for info in infos]
failed = False
for info, future in zip(infos, futures):
ret = future.result() # _send returns <0 on error, never raises
if ret != 0:
self.record_failure(chunk.room, f"mooncake transfer ret={ret}")
failed = True
else:
# Per-receiver completion sync (each of the N prefill ranks gets
# exactly one, matching its required_response_num == 1).
self._sync_status(info, TransferPoll.Success)
if failed:
# Push Failed to EVERY receiver (idempotent on ones already
# Success'd: the rank-synced admission MIN aborts the request
# everywhere anyway).
self._fail_room(chunk.room, None, infos)
continue
# All N receivers served: mark the room Success ONCE and pop only now,
# so a partial fanout can never conclude/pop early.
self.update_status(chunk.room, TransferPoll.Success)
self.transfer_infos.pop(chunk.room, None)
def _fail_room(
self,
room: int,
reason: str | None,
infos: list[EmbeddingTransferInfo],
) -> None:
if reason is not None:
self.record_failure(room, reason)
self.update_status(room, TransferPoll.Failed)
for info in infos:
self._sync_status(info, TransferPoll.Failed)
def is_parked(self, room: int) -> bool:
"""Whether ``room`` still has a chunk parked awaiting receiver
registration. Public probe for the executor's ring-slot lease: a parked
chunk holds its slot's pointer for re-send, so the slot is not reusable
until the room unparks."""
with self._pending_lock:
return room in self._pending
def fail_room(self, room: int, reason: str | None) -> None:
"""Conclude ``room`` Failed and push Failed to all of its registered
receivers. Public seam for the encode executor, which must not reach into
``transfer_infos`` or the status FSM directly."""
infos = list(self.transfer_infos.get(room, {}).values())
self._fail_room(room, reason, infos)
def _flush_pending(self, room: int) -> None:
# Re-enqueue this room's parked chunks (in order) now that it registered.
# Same room -> same queue, so the re-driven copy is consumed by the same
# single worker that parked it.
with self._pending_lock:
parked = self._pending.pop(room, [])
for _deadline, chunk in parked:
self._queue_for(room).put(chunk)
def _park_reaper(self):
# Fail rooms whose parked chunk outlived bootstrap_time_out (the
# receiver never registered), so an aborted request never hangs.
while True:
time.sleep(1.0)
now = time.time()
with self._pending_lock:
expired = [
room
for room, parked in self._pending.items()
if parked and now >= parked[0][0]
]
for room in expired:
self._pending.pop(room, None)
for room in expired:
self.record_failure(
room, f"receiver never registered within {self.bootstrap_time_out}s"
)
self.update_status(room, TransferPoll.Failed)
def _send(self, info: EmbeddingTransferInfo, chunk: EmbeddingChunk) -> int:
src, nbytes, deep_src, deep_nbytes = shard_payload(chunk, info)
if nbytes == 0:
# Zero-row shard (image span < shard count): nothing to write, but
# the frame already served as this receiver's registration and it
# still gets its Success push so its poll() completes.
return 0
bufs = [src]
dsts = [info.dst_embedding_ptr]
lens = [nbytes]
if deep_nbytes and info.dst_deepstack_ptr:
bufs.append(deep_src)
dsts.append(info.dst_deepstack_ptr)
lens.append(deep_nbytes)
# Always go through the BATCH transfer API, even for a single buffer: its
# binding releases the GIL for the one-sided RDMA write
# (gil_scoped_release), while singular transfer_sync_write does not
# reliably, pinning the GIL for the whole write and freezing this daemon's
# loop + other daemons. A 1-element batch is byte-for-byte the same
# transfer.
return self.engine.batch_transfer_sync(
info.mooncake_session_id, bufs, dsts, lens
)
def _sync_status(self, info: EmbeddingTransferInfo, status: int) -> None:
# One socket per (thread, endpoint): pushes come from fanout-pool threads
# and zmq sockets are not thread-safe.
socks = getattr(self._status_tls, "socks", None)
if socks is None:
socks = self._status_tls.socks = {}
endpoint = f"tcp://{info.endpoint}:{info.dst_port}"
sock = socks.get(endpoint)
if sock is None:
sock = zmq.Context.instance().socket(zmq.PUSH)
sock.connect(endpoint)
socks[endpoint] = sock
sock.send_multipart(
[_b(info.room), _b(int(status)), _b(self.embedding_args.engine_rank)]
)
class MooncakeEmbeddingManagerPrefill(MooncakeEmbeddingManagerBase):
"""Prefill-side (data sink) manager: holds the discovery caches and a thread
that consumes the encode side's per-request completion-status frames, marking
the room Success once all expected responses arrive.
"""
def __init__(self, args: EmbeddingManagerArgs, embedding_args: EmbeddingArgs):
super().__init__(args, embedding_args, DisaggregationMode.PREFILL)
self.required_response_num: dict[int, int] = {}
self.response_tracker: dict[int, set] = {}
self.connection_pool: dict[str, list] = {}
self.prefill_parallel_info: dict[str, dict] = {}
self._start_status_thread()
def _start_status_thread(self):
self.rank_port = get_free_port()
self.server_socket.bind(f"tcp://{get_local_ip_by_remote()}:{self.rank_port}")
def loop():
while True:
parts = self.server_socket.recv_multipart()
room = int(parts[0].decode("ascii"))
status = int(parts[1].decode("ascii"))
rank = int(parts[2].decode("ascii"))
if status == TransferPoll.Success and room in self.request_status:
self.response_tracker.setdefault(room, set()).add(rank)
if len(
self.response_tracker[room]
) >= self.required_response_num.get(room, 1):
self.update_status(room, TransferPoll.Success)
elif status == TransferPoll.Failed:
self.record_failure(room, "encode failed to send embedding")
self.update_status(room, TransferPoll.Failed)
threading.Thread(target=loop, daemon=True).start()
class MooncakeEmbeddingReceiver:
"""Prefill-side per-request receiver: discovers the encode endpoint via the
bootstrap server, registers its receive buffer, and on ``pre_alloc`` tells
the encode side where/how big to write this request's embedding. 1->N
broadcast (prefill_tp a multiple of encode_tp): contiguous blocks of prefill
ranks pair one encode rank; encode_tp=1 -> all prefill ranks pair encode
rank 0 and receive the same TP-gathered embedding.
"""
def __init__(
self,
mgr: MooncakeEmbeddingManagerPrefill,
bootstrap_addr: str,
bootstrap_room: int,
):
self.mgr = mgr
self.bootstrap_addr = bootstrap_addr
self.bootstrap_room = bootstrap_room
self.session_id = mgr.get_session_id()
mgr.update_status(bootstrap_room, TransferPoll.Bootstrapping)
pinfo = mgr.prefill_parallel_info.get(bootstrap_addr)
if pinfo is None or pinfo.get("prefill_tp_size") is None:
# SINGLE-attempt fail-fast: this runs in the prefill scheduler thread,
# so a retry-loop would stall the whole TP group on a slow/unreachable
# encode bootstrap. /route can answer 200 with un-populated
# parallel-info while the encode worker is still registering (startup
# race); fail this room fast and let the client retry once the encode
# has registered. A partial dict is never cached (it would crash later
# requests on int(None)). _route_get bounds its own GET at timeout=5,
# so this attempt cannot block beyond that.
cand = _route_get(bootstrap_addr, -1, -1)
if cand is None or cand.get("prefill_tp_size") is None:
mgr.record_failure(
bootstrap_room, "no (complete) parallel info from bootstrap"
)
mgr.update_status(bootstrap_room, TransferPoll.Failed)
return
pinfo = cand
mgr.prefill_parallel_info[bootstrap_addr] = pinfo
encode_tp = int(pinfo["prefill_tp_size"])
encode_dp = int(pinfo["prefill_dp_size"])
local_tp = mgr.world_size // mgr.dp_size
# prefill_tp must be a whole multiple of encode_tp. The vision tower
# output is TP-gathered (identical on every encode rank) and every prefill
# rank needs the full embedding, so encode_tp=1 -> prefill_tp=N is a 1->N
# broadcast. Contiguous blocks of `fanout` prefill ranks share one encode
# rank.
assert local_tp % encode_tp == 0, (
f"EPD requires prefill_tp to be a multiple of encode_tp "
f"(encode_tp={encode_tp}, prefill_tp={local_tp})"
)
fanout = local_tp // encode_tp # prefill ranks served by one encode rank
# Drives the ENCODE-side gate: how many prefill ranks register with this
# request's encode rank before it may send + conclude (1->N broadcast).
self.required_dst_info_num = fanout
# This prefill rank still pulls from exactly ONE encode rank, so it expects
# a single completion sync. The N-way fan-out is purely the encode side's
# concern; bumping this to N would hang every prefill rank.
mgr.required_response_num[bootstrap_room] = 1
# Which encode rank this prefill rank pulls from: contiguous grouping.
# encode_tp=1 -> fanout=local_tp -> my_encode_rank == 0 for all prefill
# ranks. Use `// fanout`, NOT `% encode_tp`: both are 0 for encode_tp=1,
# but only `// fanout` groups ranks correctly for encode_tp>1.
my_tp_rank = mgr.embedding_args.engine_rank % local_tp
my_encode_rank = my_tp_rank // fanout
target_dp_group = bootstrap_room % encode_dp
key = f"{bootstrap_addr}_{target_dp_group}_{my_encode_rank}"
if key not in mgr.connection_pool:
info = _route_get(bootstrap_addr, my_encode_rank, target_dp_group)
if info is None:
mgr.record_failure(bootstrap_room, "no encode rank info from bootstrap")
mgr.update_status(bootstrap_room, TransferPoll.Failed)
return
self.encode_infos = [info]
mgr.connection_pool[key] = self.encode_infos
self._register_args()
else:
self.encode_infos = mgr.connection_pool[key]
mgr.update_status(bootstrap_room, TransferPoll.Bootstrapped)
def _register_args(self):
ea = self.mgr.embedding_args
reg = EmbeddingArgsRegisterInfo(
room=REGISTER_ROOM_SENTINEL,
endpoint=get_local_ip_by_remote(),
dst_port=self.mgr.rank_port,
mooncake_session_id=self.session_id,
dst_embedding_ptr=ea.embedding_data_ptr,
dst_deepstack_ptr=ea.deepstack_data_ptr,
)
for info in self.encode_infos:
sock = self.mgr._connect(f"tcp://{info['rank_ip']}:{info['rank_port']}")
sock.send_multipart(reg.to_zmq())
def pre_alloc(
self,
*,
dst_embedding_ptr: int,
n_tokens: int,
hidden: int,
dtype: str,
dst_deepstack_ptr: int = 0,
has_deepstack: bool = False,
row_start: int = 0,
span: int = 0,
) -> None:
"""Tell the encode side where/how big to write. In shard mode the caller
passes this rank's row sub-range: ``row_start`` within the image and
``n_tokens`` = the SHARD's row count, both dst pointers already offset to
the shard's first row. ``span`` is the image's FULL row count (pass it in
identity mode too: it is the encode side's token-count tripwire). A
``n_tokens == 0`` frame is still sent: it doubles as this receiver's
registration heartbeat (the encode-side fanout gate counts frames, not
bytes)."""
for info in self.encode_infos:
ti = EmbeddingTransferInfo(
room=self.bootstrap_room,
endpoint=get_local_ip_by_remote(),
dst_port=self.mgr.rank_port,
mooncake_session_id=self.session_id,
dst_embedding_ptr=dst_embedding_ptr,
dst_deepstack_ptr=dst_deepstack_ptr,
n_tokens=n_tokens,
hidden=hidden,
dtype=dtype,
has_deepstack=has_deepstack,
required_dst_info_num=self.required_dst_info_num,
row_start=row_start,
span=span,
)
sock = self.mgr._connect(f"tcp://{info['rank_ip']}:{info['rank_port']}")
sock.send_multipart(ti.to_zmq())
def poll(self) -> int:
return self.mgr.check_status(self.bootstrap_room)
def clear(self) -> None:
"""Drop this room's bookkeeping from the (singleton) prefill manager on a
terminal receive, else every request leaks its status/tracker entries.
Mirrors the KV receiver; called post-terminal so it can't race check_status."""
room = self.bootstrap_room
self.mgr.request_status.pop(room, None)
self.mgr.required_response_num.pop(room, None)
self.mgr.response_tracker.pop(room, None)
with self.mgr.failure_lock:
self.mgr.failure_records.pop(room, None)