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

567 lines
24 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.
import dataclasses
import threading
import time
from contextlib import contextmanager
from typing import Any
import numpy as np
import numpy.typing as npt
from tokenspeed.runtime.pd.base.status import TransferPoll
from tokenspeed.runtime.pd.mooncake.entities import (
KVArgs,
TransferInfo,
TransferKVChunk,
)
from tokenspeed.runtime.pd.mooncake.prefill import (
MooncakeKVManagerPrefill as MooncakeKVManager,
)
from tokenspeed.runtime.pd.utils import (
DisaggregationMode,
FastQueue,
StepCounter,
group_concurrent_contiguous,
)
from tokenspeed.runtime.utils import get_colorful_logger
from tokenspeed.runtime.utils.server_args import ServerArgs
logger = get_colorful_logger(__name__)
@dataclasses.dataclass
class WriteRequest:
trans_info: TransferInfo
dst_ranks_info: tuple[str, int, int]
prefill_kv_blocks: npt.NDArray[np.int64]
dst_kv_blocks: npt.NDArray[np.int64]
submit_bids: list[int] = dataclasses.field(default_factory=list)
@dataclasses.dataclass
class LayerWiseTask:
kv_chunk: TransferKVChunk
begin_cache_step: int
aux_step: int
next_layer_id: int = 0
write_requests: list[WriteRequest] = dataclasses.field(default_factory=list)
polls: list[bool] = dataclasses.field(default_factory=list)
class MooncakeAsyncKVManager(MooncakeKVManager):
def __init__(
self,
args: KVArgs,
disaggregation_mode: DisaggregationMode,
server_args: ServerArgs,
is_mla_backend: bool | None = False,
draft_is_mla_backend: bool | None = False,
):
super().__init__(
args, disaggregation_mode, server_args, is_mla_backend, draft_is_mla_backend
)
self.target_layer_num = self.kv_args.target_layer_num
self.draft_layer_num = self.kv_args.draft_layer_num
self.layer_num = self.target_layer_num + self.draft_layer_num
self.is_mla_backend = is_mla_backend
self.draft_is_mla_backend = draft_is_mla_backend
self.kv_cache_quant_method = server_args.kv_cache_quant_method
self.submit_interval = server_args.disaggregation_layerwise_interval
if self.submit_interval <= 0:
raise ValueError("submit_interval must be positive")
self.current_transfer_batch: list[tuple[TransferKVChunk, int]] = []
self.offsets = args.offsets
def _init_offsets(self):
if self.is_mla_backend:
if self.kv_cache_quant_method == "per_token_head":
target_offsets = [
[i * self.target_layer_num + layer_id for i in range(3)]
for layer_id in range(self.target_layer_num)
]
draft_offsets = [
[3 * self.target_layer_num + layer_id]
for layer_id in range(self.draft_layer_num)
]
else:
target_offsets = [
[layer_id] for layer_id in range(self.target_layer_num)
]
draft_offsets = [
[self.target_layer_num + layer_id]
for layer_id in range(self.draft_layer_num)
]
else:
target_offsets = [
[i * self.layer_num + layer_id for i in range(2)]
for layer_id in range(self.target_layer_num)
]
draft_offsets = [
[
i * self.layer_num + self.target_layer_num + layer_id
for i in range(2)
]
for layer_id in range(self.draft_layer_num)
]
self.offsets = target_offsets + draft_offsets
def start_transfer_thread(
self, transfer_thread_pool_size: int, transfer_queue_size: int
):
self.transfer_queues: list[FastQueue] = [FastQueue()]
for queue in self.transfer_queues:
threading.Thread(
target=self.async_transfer_worker, args=(queue,), daemon=True
).start()
def register_step_counter(self, step_counter: StepCounter):
self.step_counter = step_counter
@contextmanager
def add_batch(self, is_idle: bool):
yield # add transfer request
if not is_idle:
begin_cache_step, begin_aux_step = self.step_counter.current_step()
for kv_chunk, shard_idx in self.current_transfer_batch:
self.transfer_queues[shard_idx].put(
LayerWiseTask(
kv_chunk=kv_chunk,
begin_cache_step=begin_cache_step,
aux_step=begin_aux_step if kv_chunk.is_last else None,
)
)
# advance to step of next batch no matter if batch is empty
self.step_counter.advance_step(
delta_cache_step=self.layer_num, delta_aux_step=1
)
self.current_transfer_batch.clear()
def add_transfer_request(
self,
bootstrap_room: int,
kv_indices: npt.NDArray[np.int64],
index_slice: slice,
is_last: bool,
aux_index: int | None = None,
mla_l1_5_args: tuple[Any, Any] | None = None,
):
logger.debug("async manager add_transfer_request")
if self.disaggregation_mode != DisaggregationMode.PREFILL:
raise RuntimeError("Transfer requests can only be added in prefill mode.")
if is_last and aux_index is None:
raise ValueError("aux_index must be set for the last transfer chunk.")
if (
bootstrap_room not in self.request_status
or self.check_status(bootstrap_room) == TransferPoll.Failed
):
logger.debug(
"Request with bootstrap_room=%s already failed", bootstrap_room
)
return
if bootstrap_room not in self.transfer_infos:
# This means that the current rank is a dummy rank for this request,
# and it has already been marked as success, so there is no need to
# add further chunks into the transfer queue.
return
# sharding according to the dst_infos to make sure
# requests with the same dst_sessions will be added into the same
# queue, which enables early abort with failed sessions.
dst_infos = self.transfer_infos[bootstrap_room].keys()
session_port_sum = sum(int(session.split(":")[1]) for session in dst_infos)
shard_idx = session_port_sum % len(self.transfer_queues)
kv_chunk = TransferKVChunk(
room=bootstrap_room,
prefill_kv_indices=kv_indices,
index_slice=index_slice,
is_last=is_last,
prefill_aux_index=aux_index,
mla_l1_5_args=mla_l1_5_args,
)
self.current_transfer_batch.append((kv_chunk, shard_idx))
def submit_aux(
self,
mooncake_session_id: str,
prefill_aux_index: int,
dst_aux_ptrs: list[int],
dst_aux_index: int,
):
# Submit transfer for all aux buffers (output_ids, logprobs, cached_tokens, etc.)
batch_ids = []
for aux_data_ptr, aux_item_len, dst_aux_ptr in zip(
self.kv_args.aux_data_ptrs, self.kv_args.aux_item_lens, dst_aux_ptrs
):
prefill_aux_addr = aux_data_ptr + prefill_aux_index * aux_item_len
decode_aux_addr = dst_aux_ptr + dst_aux_index * aux_item_len
bid = self.engine.transfer_submit_write(
mooncake_session_id, prefill_aux_addr, decode_aux_addr, aux_item_len
)
batch_ids.append(bid)
# The public interface returns a single batch id, so expose the last
# submitted id even though this helper may issue multiple writes.
return batch_ids[-1] if batch_ids else -1
def _transfer_data(self, mooncake_session_id, transfer_blocks):
if not transfer_blocks:
return 0
src_addrs, dst_addrs, lengths = zip(*transfer_blocks)
return self.engine.batch_transfer_sync(
mooncake_session_id, list(src_addrs), list(dst_addrs), list(lengths)
)
def submit_layer_cache(
self,
mooncake_session_id: str,
begin_layer_id: int, # [begin_layer_id, end_layer_id)
end_layer_id: int,
prefill_kv_blocks: npt.NDArray[np.int64],
dst_kv_blocks: npt.NDArray[np.int64],
) -> int:
dst_kv_ptrs = self.decode_kv_args_table[mooncake_session_id].dst_kv_ptrs
transfer_blocks = []
def submit_one_cache(ptr_offset):
src_ptr = self.kv_args.kv_data_ptrs[ptr_offset]
dst_ptr = dst_kv_ptrs[ptr_offset]
item_len = self.kv_args.kv_item_lens[ptr_offset]
for prefill_index, decode_index in zip(prefill_kv_blocks, dst_kv_blocks):
src_addr = src_ptr + int(prefill_index[0]) * item_len
dst_addr = dst_ptr + int(decode_index[0]) * item_len
length = item_len * len(prefill_index)
transfer_blocks.append((src_addr, dst_addr, length))
for layer_id in range(begin_layer_id, end_layer_id):
for offset in self.offsets[layer_id]:
submit_one_cache(offset)
return self._transfer_data(mooncake_session_id, transfer_blocks)
def async_transfer_worker(self, transfer_queue: FastQueue):
def discard_finished_bid_inplace(submit_bids: list[int]):
finished_cnt = 0
failed = False
for bid in submit_bids:
status = self.engine.transfer_check_status(bid)
if status == 1:
finished_cnt += 1
elif status == -1:
failed = True
if self.kv_transfer_metrics:
self.kv_transfer_metrics.record_kv_transfer_timeout()
logger.error("Transfer timeout detected!")
break
elif status == -2:
failed = True
if self.kv_transfer_metrics:
self.kv_transfer_metrics.record_kv_transfer_failure()
logger.error("Transfer failed detected")
break
else:
failed = status != 0
break
submit_bids[:] = submit_bids[finished_cnt:]
return not failed
def discard_tasks(
tasks: list[LayerWiseTask], dropped: list[LayerWiseTask]
) -> list[LayerWiseTask]:
dropped_rooms = set(id(task) for task in dropped)
return [task for task in tasks if id(task) not in dropped_rooms]
def query_ready_step(task: LayerWiseTask) -> tuple[int, int]:
if task.next_layer_id < self.layer_num:
ready_cache_step = self.step_counter.query_ready_cache_step()
if not StepCounter.is_step_ready(
ready_cache_step, task.begin_cache_step + task.next_layer_id
):
time.sleep(1e-3)
elif task.kv_chunk.is_last:
ready_aux_step = self.step_counter.query_ready_aux_step()
if not StepCounter.is_step_ready(ready_aux_step, task.aux_step):
time.sleep(1e-3)
ready_cache_step = self.step_counter.query_ready_cache_step()
ready_aux_step = self.step_counter.query_ready_aux_step()
return ready_cache_step, ready_aux_step
def get_new_tasks(blocking: bool) -> list[LayerWiseTask]:
new_tasks: list[LayerWiseTask] = []
if blocking:
new_tasks.append(transfer_queue.get())
while True:
try:
new_tasks.append(transfer_queue.get_nowait())
except FastQueue.Empty:
break
return new_tasks
def initialize(tasks: list[LayerWiseTask]) -> list[LayerWiseTask]:
abort_tasks: list[LayerWiseTask] = []
for task in tasks:
kv_chunk = task.kv_chunk
reqs_to_be_processed = (
self.transfer_infos[kv_chunk.room].values()
if kv_chunk.room in self.transfer_infos
else []
)
for req in reqs_to_be_processed:
if req.is_dummy:
task.polls.append(True)
abort_tasks.append(task)
break
with self.session_lock:
if self._is_session_failed(req.mooncake_session_id):
logger.info(
"Blocked transfer due to failed session (room=%s, session=%s).",
kv_chunk.room,
req.mooncake_session_id,
)
self.record_failure(
kv_chunk.room,
f"Decode instance could be dead, remote mooncake session {req.mooncake_session_id} is not alive",
)
task.polls.append(False)
abort_tasks.append(task)
break
resolved = self.resolve_transfer_indices(kv_chunk, req)
# Group by indices
prefill_kv_blocks, dst_kv_blocks = group_concurrent_contiguous(
resolved.src_indices, resolved.dst_indices
)
task.write_requests.append(
WriteRequest(
trans_info=req,
dst_ranks_info=(req.endpoint, req.dst_port, req.room),
prefill_kv_blocks=prefill_kv_blocks,
dst_kv_blocks=dst_kv_blocks,
)
)
return abort_tasks
def submit_transfer(
tasks: list[LayerWiseTask], ready_cache_step: int, ready_aux_step: int
) -> tuple[list[LayerWiseTask], list[LayerWiseTask]]:
abort_tasks: list[LayerWiseTask] = []
complete_tasks: list[LayerWiseTask] = []
for task in tasks:
kv_chunk = task.kv_chunk
# submit layer cache
if task.next_layer_id < self.layer_num and StepCounter.is_step_ready(
ready_cache_step, task.begin_cache_step + task.next_layer_id
):
for req in task.write_requests:
if (
(task.next_layer_id + 1) % self.submit_interval == 0
or task.next_layer_id == self.layer_num - 1
):
ret = self.submit_layer_cache(
req.trans_info.mooncake_session_id,
(task.next_layer_id // self.submit_interval)
* self.submit_interval,
task.next_layer_id + 1,
req.prefill_kv_blocks,
req.dst_kv_blocks,
)
if ret != 0:
if self.kv_transfer_metrics:
self.kv_transfer_metrics.record_kv_transfer_failure()
logger.error("Transfer failed detected!")
task.polls.append(False)
abort_tasks.append(task)
with self.session_lock:
self.session_failures[
req.trans_info.mooncake_session_id
] += 1
# Failures should never happen if the session is not dead, if the session fails once, mark it as failed
if (
self.session_failures[
req.trans_info.mooncake_session_id
]
>= 1
):
self._mark_session_failed(
req.trans_info.mooncake_session_id,
reason="submit_layer_cache",
)
logger.error(
"Session %s failed.",
req.trans_info.mooncake_session_id,
)
self.record_failure(
kv_chunk.room,
f"Failed to send kv chunk of {kv_chunk.room} to {req.trans_info.endpoint}:{req.trans_info.dst_port}",
)
break
task.next_layer_id += 1
# submit aux data
if (
kv_chunk.is_last
and task.aux_step is not None
and StepCounter.is_step_ready(ready_aux_step, task.aux_step)
):
task.aux_step = None # reset to None to mark aux has been submitted
if kv_chunk.is_last:
for req in task.write_requests:
aux_bid = self.submit_aux(
req.trans_info.mooncake_session_id,
kv_chunk.prefill_aux_index,
self.decode_kv_args_table[
req.trans_info.mooncake_session_id
].dst_aux_ptrs,
req.trans_info.dst_aux_index,
)
req.submit_bids.append(aux_bid)
if task.next_layer_id == self.layer_num and task.aux_step is None:
complete_tasks.append(task)
return complete_tasks, abort_tasks
def pop_transferred(tasks: list[LayerWiseTask]) -> list[LayerWiseTask]:
complete_tasks: list[LayerWiseTask] = []
for task in tasks:
kv_chunk = task.kv_chunk
for req in task.write_requests[
len(task.polls) :
]: # only check the uncompleted requests
success = discard_finished_bid_inplace(req.submit_bids)
if success:
if len(req.submit_bids) == 0:
task.polls.append(True)
else:
task.polls.append(False)
with self.session_lock:
self.session_failures[
req.trans_info.mooncake_session_id
] += 1
# Failures should never happen if the session is not dead, if the session fails once, mark it as failed
if (
self.session_failures[
req.trans_info.mooncake_session_id
]
>= 1
):
self._mark_session_failed(
req.trans_info.mooncake_session_id,
reason="submit_status",
)
logger.error(
"Session %s failed.",
req.trans_info.mooncake_session_id,
)
self.record_failure(
kv_chunk.room,
f"Failed to send kv chunk of {kv_chunk.room} to {req.trans_info.endpoint}:{req.trans_info.dst_port}",
)
break
# all finished or any failed
if len(task.polls) == len(task.write_requests) or (
len(task.polls) > 0 and not all(task.polls)
):
complete_tasks.append(task)
return complete_tasks
def finalize(tasks: list[LayerWiseTask]) -> None:
for task in tasks:
kv_chunk = task.kv_chunk
status = (
TransferPoll.Success if all(task.polls) else TransferPoll.Failed
)
if (
status == TransferPoll.Failed or kv_chunk.is_last
): # last chunk or any failed
self.update_status(kv_chunk.room, status)
for packed_req in task.write_requests:
endpoint, dst_port, room = packed_req.dst_ranks_info
self.sync_status_to_decode_endpoint(
endpoint, dst_port, room, status, self.attn_tp_rank
)
if (
kv_chunk.room not in self.request_status
or self.check_status(kv_chunk.room) == TransferPoll.Success
):
if kv_chunk.room in self.transfer_infos:
self.transfer_infos.pop(kv_chunk.room)
pending_tasks: list[LayerWiseTask] = []
inflight_tasks: list[LayerWiseTask] = []
while True:
try:
if (
len(
new_tasks := get_new_tasks(
blocking=(len(pending_tasks) + len(inflight_tasks) == 0)
)
)
> 0
):
if len(abort_tasks := initialize(new_tasks)) > 0:
finalize(abort_tasks)
new_tasks = discard_tasks(new_tasks, abort_tasks)
pending_tasks.extend(new_tasks)
if len(pending_tasks) > 0:
ready_cache_step, ready_aux_step = query_ready_step(
pending_tasks[0]
) # only wait the first task
submited_tasks, abort_tasks = submit_transfer(
pending_tasks, ready_cache_step, ready_aux_step
)
finalize(abort_tasks)
pending_tasks = discard_tasks(
pending_tasks, submited_tasks + abort_tasks
)
inflight_tasks.extend(submited_tasks)
if len(complete_tasks := pop_transferred(inflight_tasks)) > 0:
finalize(complete_tasks)
inflight_tasks = discard_tasks(inflight_tasks, complete_tasks)
except Exception as exc:
raise RuntimeError(
f"Transfer thread failed because of {exc}. Prefill instance "
f"with bootstrap_port={self.bootstrap_port} is dead."
) from exc