Nixl async transfer (#23967)
Signed-off-by: Ovidiu Mara <ovidium@nvidia.com>
This commit is contained in:
@@ -20,7 +20,10 @@ from sglang.srt.disaggregation.common.conn import (
|
|||||||
CommonKVReceiver,
|
CommonKVReceiver,
|
||||||
CommonKVSender,
|
CommonKVSender,
|
||||||
)
|
)
|
||||||
from sglang.srt.disaggregation.common.utils import group_concurrent_contiguous
|
from sglang.srt.disaggregation.common.utils import (
|
||||||
|
FastQueue,
|
||||||
|
group_concurrent_contiguous,
|
||||||
|
)
|
||||||
from sglang.srt.disaggregation.utils import (
|
from sglang.srt.disaggregation.utils import (
|
||||||
DisaggregationMode,
|
DisaggregationMode,
|
||||||
filter_kv_indices_for_cp_rank,
|
filter_kv_indices_for_cp_rank,
|
||||||
@@ -94,6 +97,17 @@ class TransferInfo:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class TransferKVChunk:
|
||||||
|
room: int
|
||||||
|
prefill_kv_indices: npt.NDArray[np.int32]
|
||||||
|
index_slice: slice
|
||||||
|
is_last: bool
|
||||||
|
chunk_id: int
|
||||||
|
prefill_aux_index: Optional[int]
|
||||||
|
state_indices: Optional[List[int]]
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class KVArgsRegisterInfo:
|
class KVArgsRegisterInfo:
|
||||||
"""Contains base pointers and other info which only needs to be sent once by KVReceiver. Received by prefill bootstrap thread."""
|
"""Contains base pointers and other info which only needs to be sent once by KVReceiver. Received by prefill bootstrap thread."""
|
||||||
@@ -248,6 +262,15 @@ class NixlKVManager(CommonKVManager):
|
|||||||
self.register_buffer_to_engine()
|
self.register_buffer_to_engine()
|
||||||
|
|
||||||
if self.disaggregation_mode == DisaggregationMode.PREFILL:
|
if self.disaggregation_mode == DisaggregationMode.PREFILL:
|
||||||
|
transfer_queue_size = envs.SGLANG_DISAGGREGATION_QUEUE_SIZE.get()
|
||||||
|
self.transfer_queues: List[FastQueue] = [
|
||||||
|
FastQueue() for _ in range(transfer_queue_size)
|
||||||
|
]
|
||||||
|
self.exceptions: Dict[int, Exception] = {}
|
||||||
|
for queue in self.transfer_queues:
|
||||||
|
threading.Thread(
|
||||||
|
target=self.transfer_worker, args=(queue,), daemon=True
|
||||||
|
).start()
|
||||||
self._start_bootstrap_thread()
|
self._start_bootstrap_thread()
|
||||||
elif self.disaggregation_mode == DisaggregationMode.DECODE:
|
elif self.disaggregation_mode == DisaggregationMode.DECODE:
|
||||||
self.transfer_statuses: Dict[int, TransferStatus] = defaultdict(
|
self.transfer_statuses: Dict[int, TransferStatus] = defaultdict(
|
||||||
@@ -345,6 +368,146 @@ class NixlKVManager(CommonKVManager):
|
|||||||
logger.error(f"Let room {room} be failed due to prefill down")
|
logger.error(f"Let room {room} be failed due to prefill down")
|
||||||
self.update_status(room, KVPoll.Failed)
|
self.update_status(room, KVPoll.Failed)
|
||||||
|
|
||||||
|
def check_status(self, bootstrap_room: int):
|
||||||
|
return self.request_status.get(bootstrap_room, KVPoll.WaitingForInput)
|
||||||
|
|
||||||
|
def transfer_worker(self, queue: FastQueue):
|
||||||
|
while True:
|
||||||
|
kv_chunk: TransferKVChunk = queue.get()
|
||||||
|
room = kv_chunk.room
|
||||||
|
try:
|
||||||
|
if self.check_status(room) == KVPoll.Failed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert room in self.transfer_infos
|
||||||
|
|
||||||
|
self.update_status(room, KVPoll.Transferring)
|
||||||
|
|
||||||
|
reqs_to_be_processed = list(self.transfer_infos[room].values())
|
||||||
|
handles: List = []
|
||||||
|
|
||||||
|
for req in reqs_to_be_processed:
|
||||||
|
assert room == req.room
|
||||||
|
if req.is_dummy():
|
||||||
|
continue
|
||||||
|
|
||||||
|
assert req.agent_name in self.decode_kv_args_table
|
||||||
|
decode_tp_size = self.decode_kv_args_table[
|
||||||
|
req.agent_name
|
||||||
|
].decode_tp_size
|
||||||
|
|
||||||
|
# Skip KV RDMA transfer when there are no pages to send
|
||||||
|
# (e.g., decode-side radix cache matched the entire prefix).
|
||||||
|
# Aux data is still sent below when is_last=True.
|
||||||
|
if len(kv_chunk.prefill_kv_indices) > 0:
|
||||||
|
chunked_dst_kv_indice = req.dst_kv_indices[kv_chunk.index_slice]
|
||||||
|
|
||||||
|
# NOTE: This is temporarily a workaround to deal with the case where the prefill_kv_indices
|
||||||
|
# is mismatched with the dst_kv_indices when page size > 1, this should never happen.
|
||||||
|
if len(chunked_dst_kv_indice) < len(
|
||||||
|
kv_chunk.prefill_kv_indices
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
f"len(chunked_dst_kv_indice) = {len(chunked_dst_kv_indice)}, len(kv_chunk.prefill_kv_indices) = {len(kv_chunk.prefill_kv_indices)}"
|
||||||
|
)
|
||||||
|
kv_chunk.prefill_kv_indices = kv_chunk.prefill_kv_indices[
|
||||||
|
: len(chunked_dst_kv_indice)
|
||||||
|
]
|
||||||
|
|
||||||
|
notif = f"{req.room}_kv_{kv_chunk.chunk_id}_{int(kv_chunk.is_last)}_{self.kv_args.engine_rank}"
|
||||||
|
|
||||||
|
if self.is_mla_backend or (decode_tp_size == self.attn_tp_size):
|
||||||
|
kv_xfer_handle = self.send_kvcache(
|
||||||
|
req.agent_name,
|
||||||
|
kv_chunk.prefill_kv_indices,
|
||||||
|
self.decode_kv_args_table[req.agent_name].dst_kv_ptrs,
|
||||||
|
chunked_dst_kv_indice,
|
||||||
|
self.decode_kv_args_table[req.agent_name].gpu_id,
|
||||||
|
notif,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
kv_xfer_handle = self.send_kvcache_slice(
|
||||||
|
req.agent_name,
|
||||||
|
kv_chunk.prefill_kv_indices,
|
||||||
|
self.decode_kv_args_table[req.agent_name].dst_kv_ptrs,
|
||||||
|
chunked_dst_kv_indice,
|
||||||
|
self.decode_kv_args_table[req.agent_name].gpu_id,
|
||||||
|
notif,
|
||||||
|
prefill_tp_size=self.attn_tp_size,
|
||||||
|
decode_tp_size=decode_tp_size,
|
||||||
|
decode_tp_rank=self.decode_kv_args_table[
|
||||||
|
req.agent_name
|
||||||
|
].decode_tp_rank,
|
||||||
|
dst_kv_item_len=self.decode_kv_args_table[
|
||||||
|
req.agent_name
|
||||||
|
].dst_kv_item_len,
|
||||||
|
)
|
||||||
|
|
||||||
|
handles.append(kv_xfer_handle)
|
||||||
|
|
||||||
|
if kv_chunk.is_last:
|
||||||
|
if kv_chunk.state_indices is not None:
|
||||||
|
dst_info = self.decode_kv_args_table[req.agent_name]
|
||||||
|
state_xfer_handle = self.maybe_send_extra(
|
||||||
|
req.agent_name,
|
||||||
|
kv_chunk.state_indices,
|
||||||
|
dst_info.dst_state_data_ptrs,
|
||||||
|
req.dst_state_indices,
|
||||||
|
dst_info.gpu_id,
|
||||||
|
f"{req.room}_state_{self.kv_args.engine_rank}",
|
||||||
|
decode_tp_size,
|
||||||
|
decode_tp_rank=dst_info.decode_tp_rank,
|
||||||
|
dst_state_item_lens=dst_info.dst_state_item_lens,
|
||||||
|
dst_state_dim_per_tensor=dst_info.dst_state_dim_per_tensor,
|
||||||
|
)
|
||||||
|
if state_xfer_handle is not None:
|
||||||
|
handles.append(state_xfer_handle)
|
||||||
|
|
||||||
|
if kv_chunk.prefill_aux_index is None:
|
||||||
|
raise RuntimeError("Missing aux index for last chunk")
|
||||||
|
# When no KV pages were sent (decode-side cache hit),
|
||||||
|
# encode pp_rank in aux notif so receiver can mark
|
||||||
|
# expected_kvs_per_pp[pp_rank] = 0.
|
||||||
|
if len(kv_chunk.prefill_kv_indices) == 0:
|
||||||
|
aux_notif = (
|
||||||
|
f"{req.room}_aux_nokv_{self.kv_args.engine_rank}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
aux_notif = f"{req.room}_aux"
|
||||||
|
aux_xfer_handle = self.send_aux(
|
||||||
|
req.agent_name,
|
||||||
|
kv_chunk.prefill_aux_index,
|
||||||
|
self.decode_kv_args_table[req.agent_name].dst_aux_ptrs,
|
||||||
|
req.dst_aux_index,
|
||||||
|
aux_notif,
|
||||||
|
)
|
||||||
|
handles.append(aux_xfer_handle)
|
||||||
|
|
||||||
|
while handles:
|
||||||
|
states = [self.agent.check_xfer_state(h) for h in handles]
|
||||||
|
if any(s == "ERR" for s in states):
|
||||||
|
raise RuntimeError(f"NIXL transfer encountered ERR room={room}")
|
||||||
|
if all(s == "DONE" for s in states):
|
||||||
|
break
|
||||||
|
time.sleep(0)
|
||||||
|
|
||||||
|
if kv_chunk.is_last:
|
||||||
|
self.update_status(room, KVPoll.Success)
|
||||||
|
else:
|
||||||
|
self.update_status(room, KVPoll.Transferring)
|
||||||
|
except Exception as e:
|
||||||
|
# Catch all exceptions to prevent silently killing this
|
||||||
|
# worker thread, but still propagate via failure_exception().
|
||||||
|
if isinstance(e, _NIXL_TRANSPORT_ERRORS):
|
||||||
|
logger.warning(f"NIXL transport error for room {room}: {e}")
|
||||||
|
else:
|
||||||
|
logger.exception(
|
||||||
|
f"Unexpected transfer worker error for room {room}"
|
||||||
|
)
|
||||||
|
self.exceptions[room] = e
|
||||||
|
self.record_failure(room, str(e))
|
||||||
|
self.update_status(room, KVPoll.Failed)
|
||||||
|
|
||||||
def register_buffer_to_engine(self):
|
def register_buffer_to_engine(self):
|
||||||
kv_addrs = []
|
kv_addrs = []
|
||||||
for kv_data_ptr, kv_data_len in zip(
|
for kv_data_ptr, kv_data_len in zip(
|
||||||
@@ -925,91 +1088,19 @@ class NixlKVManager(CommonKVManager):
|
|||||||
assert self.disaggregation_mode == DisaggregationMode.PREFILL
|
assert self.disaggregation_mode == DisaggregationMode.PREFILL
|
||||||
assert not is_last or (is_last and aux_index is not None)
|
assert not is_last or (is_last and aux_index is not None)
|
||||||
|
|
||||||
reqs_to_be_processed = self.transfer_infos[bootstrap_room].values()
|
shard_idx = bootstrap_room % len(self.transfer_queues)
|
||||||
handles = []
|
self.transfer_queues[shard_idx].put(
|
||||||
for req in reqs_to_be_processed:
|
TransferKVChunk(
|
||||||
assert bootstrap_room == req.room
|
room=bootstrap_room,
|
||||||
if req.is_dummy():
|
prefill_kv_indices=kv_indices,
|
||||||
continue
|
index_slice=index_slice,
|
||||||
|
is_last=is_last,
|
||||||
chunked_dst_kv_indice = req.dst_kv_indices[index_slice]
|
chunk_id=chunk_id,
|
||||||
assert len(chunked_dst_kv_indice) == len(kv_indices)
|
prefill_aux_index=aux_index,
|
||||||
assert req.agent_name in self.decode_kv_args_table
|
state_indices=state_indices,
|
||||||
|
|
||||||
decode_tp_size = self.decode_kv_args_table[req.agent_name].decode_tp_size
|
|
||||||
|
|
||||||
# Skip KV RDMA transfer when there are no pages to send
|
|
||||||
# (e.g., decode-side radix cache matched the entire prefix).
|
|
||||||
# Aux data is still sent below when is_last=True.
|
|
||||||
if len(kv_indices) > 0:
|
|
||||||
notif = (
|
|
||||||
f"{req.room}_kv_{chunk_id}_{int(is_last)}_{self.kv_args.pp_rank}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.is_mla_backend or (decode_tp_size == self.attn_tp_size):
|
|
||||||
kv_xfer_handle = self.send_kvcache(
|
|
||||||
req.agent_name,
|
|
||||||
kv_indices,
|
|
||||||
self.decode_kv_args_table[req.agent_name].dst_kv_ptrs,
|
|
||||||
chunked_dst_kv_indice,
|
|
||||||
self.decode_kv_args_table[req.agent_name].gpu_id,
|
|
||||||
notif,
|
|
||||||
)
|
)
|
||||||
else:
|
return None
|
||||||
kv_xfer_handle = self.send_kvcache_slice(
|
|
||||||
req.agent_name,
|
|
||||||
kv_indices,
|
|
||||||
self.decode_kv_args_table[req.agent_name].dst_kv_ptrs,
|
|
||||||
chunked_dst_kv_indice,
|
|
||||||
self.decode_kv_args_table[req.agent_name].gpu_id,
|
|
||||||
notif,
|
|
||||||
prefill_tp_size=self.attn_tp_size,
|
|
||||||
decode_tp_size=decode_tp_size,
|
|
||||||
decode_tp_rank=self.decode_kv_args_table[
|
|
||||||
req.agent_name
|
|
||||||
].decode_tp_rank,
|
|
||||||
dst_kv_item_len=self.decode_kv_args_table[
|
|
||||||
req.agent_name
|
|
||||||
].dst_kv_item_len,
|
|
||||||
)
|
|
||||||
|
|
||||||
handles.append(kv_xfer_handle)
|
|
||||||
# Only the last chunk we need to send the aux data.
|
|
||||||
if is_last:
|
|
||||||
if state_indices is not None:
|
|
||||||
dst_info = self.decode_kv_args_table[req.agent_name]
|
|
||||||
state_xfer_handle = self.maybe_send_extra(
|
|
||||||
req.agent_name,
|
|
||||||
state_indices,
|
|
||||||
dst_info.dst_state_data_ptrs,
|
|
||||||
req.dst_state_indices,
|
|
||||||
dst_info.gpu_id,
|
|
||||||
f"{req.room}_state_{self.kv_args.engine_rank}",
|
|
||||||
decode_tp_size,
|
|
||||||
decode_tp_rank=dst_info.decode_tp_rank,
|
|
||||||
dst_state_item_lens=dst_info.dst_state_item_lens,
|
|
||||||
dst_state_dim_per_tensor=dst_info.dst_state_dim_per_tensor,
|
|
||||||
)
|
|
||||||
if state_xfer_handle is not None:
|
|
||||||
handles.append(state_xfer_handle)
|
|
||||||
|
|
||||||
assert aux_index is not None
|
|
||||||
# When no KV pages were sent (decode-side cache hit),
|
|
||||||
# encode pp_rank in aux notif so receiver can mark
|
|
||||||
# expected_kvs_per_pp[pp_rank] = 0.
|
|
||||||
if len(kv_indices) == 0:
|
|
||||||
aux_notif = f"{req.room}_aux_nokv_{self.kv_args.pp_rank}"
|
|
||||||
else:
|
|
||||||
aux_notif = f"{req.room}_aux"
|
|
||||||
aux_xfer_handle = self.send_aux(
|
|
||||||
req.agent_name,
|
|
||||||
aux_index,
|
|
||||||
self.decode_kv_args_table[req.agent_name].dst_aux_ptrs,
|
|
||||||
req.dst_aux_index,
|
|
||||||
aux_notif,
|
|
||||||
)
|
|
||||||
handles.append(aux_xfer_handle)
|
|
||||||
return handles
|
|
||||||
|
|
||||||
def update_transfer_status(self):
|
def update_transfer_status(self):
|
||||||
# Process notifications from received transfers.
|
# Process notifications from received transfers.
|
||||||
@@ -1115,7 +1206,6 @@ class NixlKVSender(CommonKVSender):
|
|||||||
pp_rank: int,
|
pp_rank: int,
|
||||||
):
|
):
|
||||||
super().__init__(mgr, bootstrap_addr, bootstrap_room, dest_tp_ranks, pp_rank)
|
super().__init__(mgr, bootstrap_addr, bootstrap_room, dest_tp_ranks, pp_rank)
|
||||||
self.xfer_handles = []
|
|
||||||
self.has_sent = False
|
self.has_sent = False
|
||||||
self.chunk_id = 0
|
self.chunk_id = 0
|
||||||
self._send_failed = False
|
self._send_failed = False
|
||||||
@@ -1159,8 +1249,7 @@ class NixlKVSender(CommonKVSender):
|
|||||||
):
|
):
|
||||||
self._transfer_start_time = time.perf_counter()
|
self._transfer_start_time = time.perf_counter()
|
||||||
|
|
||||||
try:
|
self.kv_mgr.add_transfer_request(
|
||||||
new_xfer_handles = self.kv_mgr.add_transfer_request(
|
|
||||||
self.bootstrap_room,
|
self.bootstrap_room,
|
||||||
kv_indices,
|
kv_indices,
|
||||||
index_slice,
|
index_slice,
|
||||||
@@ -1169,16 +1258,7 @@ class NixlKVSender(CommonKVSender):
|
|||||||
self.aux_index,
|
self.aux_index,
|
||||||
state_indices,
|
state_indices,
|
||||||
)
|
)
|
||||||
except _NIXL_TRANSPORT_ERRORS as e:
|
|
||||||
logger.warning(
|
|
||||||
f"KVSender transfer request failed for room {self.bootstrap_room}: {e}"
|
|
||||||
)
|
|
||||||
self._send_failed = True
|
|
||||||
self._send_error = e
|
|
||||||
return
|
|
||||||
|
|
||||||
self._record_transfer_indices(kv_indices, state_indices)
|
self._record_transfer_indices(kv_indices, state_indices)
|
||||||
self.xfer_handles.extend(new_xfer_handles)
|
|
||||||
self.chunk_id += 1
|
self.chunk_id += 1
|
||||||
if is_last:
|
if is_last:
|
||||||
self.has_sent = True
|
self.has_sent = True
|
||||||
@@ -1186,37 +1266,26 @@ class NixlKVSender(CommonKVSender):
|
|||||||
def poll(self) -> KVPoll:
|
def poll(self) -> KVPoll:
|
||||||
if self._send_failed:
|
if self._send_failed:
|
||||||
return KVPoll.Failed # type: ignore
|
return KVPoll.Failed # type: ignore
|
||||||
if not self.has_sent:
|
status = self.kv_mgr.check_status(self.bootstrap_room)
|
||||||
return self.kv_mgr.check_status(self.bootstrap_room)
|
|
||||||
try:
|
|
||||||
states = [self.kv_mgr.agent.check_xfer_state(x) for x in self.xfer_handles]
|
|
||||||
except _NIXL_TRANSPORT_ERRORS as e:
|
|
||||||
logger.warning(
|
|
||||||
f"KVSender check_xfer_state failed for room {self.bootstrap_room}: {e}"
|
|
||||||
)
|
|
||||||
self._send_failed = True
|
|
||||||
self._send_error = e
|
|
||||||
return KVPoll.Failed # type: ignore
|
|
||||||
if all(x == "DONE" for x in states):
|
|
||||||
if (
|
if (
|
||||||
self._transfer_start_time is not None
|
status == KVPoll.Success
|
||||||
|
and self._transfer_start_time is not None
|
||||||
and self._transfer_metric.transfer_latency_s is None
|
and self._transfer_metric.transfer_latency_s is None
|
||||||
):
|
):
|
||||||
self._transfer_metric.transfer_latency_s = (
|
self._transfer_metric.transfer_latency_s = (
|
||||||
time.perf_counter() - self._transfer_start_time
|
time.perf_counter() - self._transfer_start_time
|
||||||
)
|
)
|
||||||
return KVPoll.Success # type: ignore
|
return status
|
||||||
if any(x == "ERR" for x in states):
|
|
||||||
self._send_failed = True
|
def clear(self):
|
||||||
self._send_error = RuntimeError(
|
super().clear()
|
||||||
f"NIXL transfer error for room {self.bootstrap_room}"
|
|
||||||
)
|
|
||||||
return KVPoll.Failed # type: ignore
|
|
||||||
return KVPoll.WaitingForInput # type: ignore
|
|
||||||
|
|
||||||
def failure_exception(self):
|
def failure_exception(self):
|
||||||
if self._send_error is not None:
|
if self._send_error is not None:
|
||||||
raise self._send_error
|
raise self._send_error
|
||||||
|
exc = self.kv_mgr.exceptions.pop(self.bootstrap_room, None)
|
||||||
|
if exc is not None:
|
||||||
|
raise exc
|
||||||
raise RuntimeError("NIXL KVSender Exception")
|
raise RuntimeError("NIXL KVSender Exception")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user