Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 225 additions & 107 deletions python/sglang/srt/disaggregation/common/conn.py

Large diffs are not rendered by default.

164 changes: 80 additions & 84 deletions python/sglang/srt/disaggregation/mooncake/conn.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

from sglang.srt.disaggregation.base.conn import KVArgs, KVPoll, StateType
from sglang.srt.disaggregation.common.conn import (
ABORT_ACK_TAG,
ABORT_TAG,
AbortNotification,
CommonKVBootstrapServer,
CommonKVManager,
CommonKVReceiver,
Expand Down Expand Up @@ -249,9 +252,6 @@ def __init__(
self.failed_sessions = set()
self.session_lock = threading.Lock()
self.start_prefill_thread()
# Per-room count of chunks not yet transferred; teardown waits for
# zero so a deferred chunk is not dropped by an early conclude.
self._staging_outstanding = defaultdict(int)
# Determine the number of threads to use for kv sender
cpu_count = os.cpu_count()
transfer_thread_pool_size = (
Expand Down Expand Up @@ -2134,8 +2134,9 @@ def transfer_worker(
)

while True:
kv_chunk: Optional[TransferKVChunk] = None
try:
kv_chunk: TransferKVChunk = queue.get()
kv_chunk = queue.get()
# teardown() pushes a None sentinel to unblock get() and stop
# the worker: FastQueue.get() blocks indefinitely, so checking
# _stopped alone can never wake a parked worker during a role switch.
Expand Down Expand Up @@ -2470,10 +2471,83 @@ def transfer_worker(

except Exception as e:
# NOTE(shangming): Remove this when we make sure the transfer thread is bug-free
if kv_chunk is not None:
self.poison_deferred_ack_room(kv_chunk.room)
raise RuntimeError(
f"Transfer thread failed because of {e}. Prefill instance with bootstrap_port={self.bootstrap_port} is dead."
)

def _handle_abort_notification(self, msg: List[bytes]) -> bool:
if not msg or msg[0] != ABORT_TAG:
return False

notification = AbortNotification.from_zmq(msg)
if notification is None:
return True
room = notification.room
status = self.request_status.get(room)
room_active = status is not None and status != KVPoll.Success

if self.enable_deferred_decode_kv_release:
self._handle_deferred_abort_notification(notification, room_active)
else:
self._handle_legacy_abort_notification(notification, room_active)
return True

def _handle_deferred_abort_notification(
self, notification: AbortNotification, room_active: bool
) -> None:
room = notification.room
if room_active:
self.update_status(room, KVPoll.Failed)

ack_target = notification.deferred_ack_target()
if ack_target is None:
return
self.register_deferred_ack_target(room, ack_target)
self._maybe_ack_drained_abort(room)
if room_active:
logger.debug(
"Received abort notification for room %s, marked as Failed; "
"ACK deferred until transfer drains",
room,
)

def _handle_legacy_abort_notification(
self, notification: AbortNotification, room_active: bool
) -> None:
room = notification.room
if room_active:
self.update_status(room, KVPoll.Failed)
logger.debug(
"Received abort notification for room %s, marked as Failed", room
)
else:
logger.debug(
"Received abort notification for room %s, ignoring "
"(already completed or unknown)",
room,
)

if notification.decode_ip is None or notification.decode_port is None:
return
try:
na = NetworkAddress(notification.decode_ip, notification.decode_port)
# Deferred ACKs carry a rank; the feature-off wire format does not.
self._send_multipart_locked(
na.to_tcp(),
[ABORT_ACK_TAG, str(room).encode("ascii")],
is_ipv6=na.is_ipv6,
)
logger.debug(
"Sent ABORT_ACK for room %s to %s:%s",
room,
notification.decode_ip,
notification.decode_port,
)
except Exception as exc:
logger.debug("Failed to send ABORT_ACK for room %s: %s", room, exc)

def start_prefill_thread(self):
recv = self._make_worker_recv(self.server_socket)

Expand All @@ -2493,76 +2567,7 @@ def bootstrap_thread():
if room == "STAGING_RSP":
handle_staging_rsp(waiting_req_bytes, self.transfer_infos)
continue
# Decode-side abort notification: mark room as failed and ACK
if room == "ABORT":
room_to_be_aborted = int(waiting_req_bytes[1].decode("ascii"))
decode_ip = waiting_req_bytes[2].decode("ascii")
decode_port = int(waiting_req_bytes[3].decode("ascii"))
room_active = (
room_to_be_aborted in self.request_status
and self.check_status(room_to_be_aborted) != KVPoll.Success
)
if self.enable_deferred_decode_kv_release:
# Mark Failed FIRST (stops add_transfer_request enqueuing
# new chunks), THEN register the ack target: registering
# first would let the worker drain+ack while the room is
# not yet Failed, so a newly enqueued chunk could still
# write to the freed pages. The worker (not this thread)
# acks once its in-flight write drains.
if (
room_active
or self._staging_outstanding.get(room_to_be_aborted, 0) > 0
):
self.update_status(room_to_be_aborted, KVPoll.Failed)
self.register_deferred_ack_target(
room_to_be_aborted, decode_ip, decode_port
)
# Try once: the room may already be quiescent and
# never revisited by the worker.
self._maybe_ack_drained_abort(room_to_be_aborted)
logger.debug(
f"Received abort notification for room {room_to_be_aborted}, "
f"marked as Failed; ACK deferred until transfer drains"
)
else:
# Concluded/unknown AND quiescent (the branch above
# already took every case with writes outstanding):
# ack now so decode releases without the timeout.
self._send_abort_ack(
decode_ip, decode_port, room_to_be_aborted
)
continue
# No need to abort the room if it has already succeeded
if room_active:
self.update_status(room_to_be_aborted, KVPoll.Failed)
logger.debug(
f"Received abort notification for room {room_to_be_aborted}, "
f"marked as Failed"
)
else:
logger.debug(
f"Received abort notification for room {room_to_be_aborted}, "
f"ignoring (already completed or unknown)"
)
# Send ACK back to decode endpoint
try:
na = NetworkAddress(decode_ip, decode_port)
self._send_multipart_locked(
na.to_tcp(),
[
b"ABORT_ACK",
str(room_to_be_aborted).encode("ascii"),
],
is_ipv6=na.is_ipv6,
)
logger.debug(
f"Sent ABORT_ACK for room {room_to_be_aborted} to "
f"{decode_ip}:{decode_port}"
)
except Exception as e:
logger.debug(
f"Failed to send ABORT_ACK for room {room_to_be_aborted}: {e}"
)
if self._handle_abort_notification(waiting_req_bytes):
continue
mooncake_session_id = waiting_req_bytes[3].decode("ascii")
if room == "None":
Expand Down Expand Up @@ -2659,16 +2664,7 @@ def decode_thread():
self._handle_staging_req(msg)
continue

# Prefill acknowledges abort notification
if msg[0] == b"ABORT_ACK":
ack_aborted_room = int(msg[1].decode("ascii"))
logger.debug(f"Received ABORT_ACK for room {ack_aborted_room}")
# Deferred release: the 3-frame ack carries the prefill rank
# and means its transfer drained; aggregate for is_abort_release_safe.
if self.enable_deferred_decode_kv_release and len(msg) >= 3:
self.note_abort_ack(
ack_aborted_room, int(msg[2].decode("ascii"))
)
if self.handle_abort_ack_message(msg):
continue

parsed = self.parse_kv_status_message(msg)
Expand Down
Loading
Loading