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
257 lines
11 KiB
Python
257 lines
11 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 numpy as np
|
|
import torch
|
|
from tokenspeed_scheduler import PD, Forward
|
|
|
|
from tokenspeed.runtime.pd.base.bootstrap import BootstrapInfo
|
|
from tokenspeed.runtime.pd.base.status import TransferPoll
|
|
from tokenspeed.runtime.pd.mooncake.decode import MooncakeKVManagerDecode
|
|
from tokenspeed.runtime.pd.mooncake.receiver import MooncakeKVReceiver
|
|
from tokenspeed.runtime.pd.utils import (
|
|
TransferBackend,
|
|
poll_and_all_reduce,
|
|
)
|
|
from tokenspeed.runtime.utils import get_colorful_logger
|
|
from tokenspeed.runtime.utils.dispatch import TypeBasedDispatcher
|
|
|
|
logger = get_colorful_logger(__name__)
|
|
|
|
|
|
class DisaggDecodeExecutor:
|
|
def __init__(
|
|
self, backend: TransferBackend, args, kv_args, gloo_group, page_size: int
|
|
):
|
|
self.transfer_backend = backend
|
|
self.bootstrap_port = args.bootstrap_port
|
|
self.page_size = page_size
|
|
self._dispatcher = TypeBasedDispatcher(
|
|
[
|
|
(Forward.FlatForwardOp, self._prefill),
|
|
]
|
|
)
|
|
self.receivers: dict[int, MooncakeKVReceiver] = {}
|
|
self.kv_manager = MooncakeKVManagerDecode(args, kv_args)
|
|
self.gloo_group = gloo_group
|
|
self._local_states = {}
|
|
self._request_pool_indices: dict[str, int] = {}
|
|
self._remote_spec_candidate_ids: dict[str, tuple[int, list[int]]] = {}
|
|
|
|
def _bootstrap(self, request_id, info):
|
|
self.receivers[request_id] = MooncakeKVReceiver(
|
|
mgr=self.kv_manager,
|
|
bootstrap_addr=f"{info.bootstrap_host}:{info.bootstrap_port}",
|
|
bootstrap_room=info.bootstrap_room,
|
|
)
|
|
|
|
@staticmethod
|
|
def _mamba_indices(op, index: int):
|
|
indices = getattr(op, "mamba_pool_indices", None)
|
|
if indices is None or index >= len(indices):
|
|
return None
|
|
slot = int(indices[index])
|
|
if slot < 0:
|
|
return None
|
|
return np.array([slot], dtype=np.int64)
|
|
|
|
@staticmethod
|
|
def _mamba_checkpoint_indices(op, index: int):
|
|
indices = getattr(op, "mamba_checkpoint_dst_indices", None)
|
|
if indices is None or index >= len(indices):
|
|
return None
|
|
slot = int(indices[index])
|
|
if slot < 0:
|
|
return None
|
|
return np.array([slot], dtype=np.int64)
|
|
|
|
@classmethod
|
|
def _mamba_transfer_indices(cls, op, index: int):
|
|
working = cls._mamba_indices(op, index)
|
|
if working is None:
|
|
return None
|
|
checkpoint = cls._mamba_checkpoint_indices(op, index)
|
|
if checkpoint is None:
|
|
return working
|
|
|
|
slots = [int(x) for x in working.tolist()]
|
|
for slot in checkpoint.tolist():
|
|
slot = int(slot)
|
|
if slot >= 0 and slot not in slots:
|
|
slots.append(slot)
|
|
return np.array(slots, dtype=np.int64)
|
|
|
|
def _prefill(self, op):
|
|
logger.debug(
|
|
"[decode][_prefill] op: request_ids=%s occupied_pages=%s "
|
|
"begins=%s sizes=%s request_pool_indices=%s extend_prefix_lens=%s",
|
|
list(op.request_ids),
|
|
[list(p) for p in op.occupied_pages],
|
|
list(op.begins),
|
|
list(op.sizes),
|
|
list(op.request_pool_indices),
|
|
list(op.extend_prefix_lens),
|
|
)
|
|
|
|
for i, request_id in enumerate(op.request_ids):
|
|
if request_id not in self.receivers:
|
|
# Request failed and its receiver was cleaned up in generate_events;
|
|
# the scheduler may still dispatch its forward op one last time.
|
|
continue
|
|
extend_prefix_len = op.extend_prefix_lens[i]
|
|
kv_indices = np.array(
|
|
op.occupied_pages[i][extend_prefix_len // self.page_size :],
|
|
dtype=np.int64,
|
|
)
|
|
aux_index = op.request_pool_indices[i]
|
|
mamba_indices = self._mamba_transfer_indices(op, i)
|
|
self._request_pool_indices[request_id] = aux_index
|
|
self.receivers[request_id].prefill(
|
|
kv_indices,
|
|
aux_index,
|
|
extend_prefix_len,
|
|
None, # mla_l1_5_args
|
|
mamba_indices,
|
|
)
|
|
|
|
def register(self, request_id: str, bootstrap_info: BootstrapInfo):
|
|
self._local_states[request_id] = TransferPoll.Bootstrapping
|
|
self._bootstrap(request_id, bootstrap_info)
|
|
|
|
def execute(self, op):
|
|
if not isinstance(op, Forward.FlatForwardOp):
|
|
raise TypeError(f"Expected FlatForwardOp, got {type(op).__name__}.")
|
|
self._dispatcher(op)
|
|
|
|
def generate_events(self):
|
|
if not self.receivers:
|
|
return []
|
|
polls = poll_and_all_reduce(self.receivers.values(), self.gloo_group)
|
|
|
|
events = []
|
|
to_remove = []
|
|
for req_id, poll in zip(list(self.receivers.keys()), polls):
|
|
if (
|
|
self._local_states[req_id] == TransferPoll.Bootstrapping
|
|
and poll == TransferPoll.Bootstrapped
|
|
):
|
|
logger.debug(
|
|
"[decode][generate_events] rid=%s -> BootstrappedEvent", req_id
|
|
)
|
|
events.append(PD.BootstrappedEvent(req_id))
|
|
self._local_states[req_id] = TransferPoll.Bootstrapped
|
|
elif poll == TransferPoll.Failed:
|
|
logger.warning(
|
|
"[decode][generate_events] rid=%s -> FailedEvent", req_id
|
|
)
|
|
events.append(PD.FailedEvent(req_id))
|
|
# Drop the failed receiver so it is not polled again. Without this
|
|
# a single failed request keeps re-emitting FailedEvent every loop
|
|
# (poll stays Failed), wedging the whole conn-1 scheduler.
|
|
to_remove.append(req_id)
|
|
elif (
|
|
self._local_states[req_id] == TransferPoll.Bootstrapped
|
|
and poll == TransferPoll.Success
|
|
):
|
|
# Read bootstrap_token from the ZMQ-delivered table in kv_manager.
|
|
# The decode_thread stored it there when it received the Success status
|
|
# message from the prefill side. bootstrap_room == bootstrap_info.bootstrap_room,
|
|
# which is the key used in MooncakeKVReceiver.
|
|
self._local_states[req_id] = TransferPoll.Success
|
|
bootstrap_room = self.receivers[req_id].bootstrap_room
|
|
bootstrap_token, spec_candidate_ids = (
|
|
self.kv_manager.pop_prefill_metadata(bootstrap_room)
|
|
)
|
|
receiver = self.receivers[req_id]
|
|
if (
|
|
spec_candidate_ids is not None
|
|
and req_id in self._request_pool_indices
|
|
and getattr(
|
|
receiver,
|
|
"supports_remote_spec_candidates",
|
|
True,
|
|
)
|
|
):
|
|
self._remote_spec_candidate_ids[req_id] = (
|
|
self._request_pool_indices[req_id],
|
|
spec_candidate_ids,
|
|
)
|
|
logger.debug(
|
|
"[decode][generate_events] rid=%s -> RemotePrefillDoneEvent bootstrap_token=%s",
|
|
req_id,
|
|
bootstrap_token,
|
|
)
|
|
# Use RemotePrefillDoneEvent to carry the bootstrap_token to event_loop;
|
|
# the C++ FSM will extend it into the TokenContainer via
|
|
# fsm::RemotePrefillDoneEvent::operator()(Prefilling&&).
|
|
event = PD.RemotePrefillDoneEvent(
|
|
req_id, bootstrap_token if bootstrap_token != -1 else -1
|
|
)
|
|
events.append(event)
|
|
to_remove.append(req_id)
|
|
else:
|
|
pass
|
|
for req_id in to_remove:
|
|
# Best-effort cleanup mirroring prefill side; request_id is stable
|
|
# so without explicit pop these dicts would grow unbounded across
|
|
# failed requests. NOTE: _remote_spec_candidate_ids must NOT be
|
|
# popped here — its consumer pop_remote_spec_candidate_ids runs
|
|
# later inside event_loop._process_kv_transfer_events, after we return.
|
|
# That dict is small (one tuple per Success request, between
|
|
# generate_events emitting RemotePrefillDoneEvent and event_loop
|
|
# consuming it) and is naturally drained by the pop path; an
|
|
# eager pop here drops the spec candidates on the floor and the
|
|
# next decode forward reads uninitialized future_input_map tail,
|
|
# causing CUDA illegal memory access on embedding lookup.
|
|
self.receivers.pop(req_id, None)
|
|
self._request_pool_indices.pop(req_id, None)
|
|
self._local_states.pop(req_id, None)
|
|
|
|
return events
|
|
|
|
def pop_remote_spec_candidate_ids(self, request_id: str):
|
|
return self._remote_spec_candidate_ids.pop(request_id, None)
|
|
|
|
def reset_valid_cache_length(
|
|
self, forward_op, runtime_states, execution_stream, device
|
|
):
|
|
num_extends = forward_op.num_extends()
|
|
extend_request_pool_indices = torch.tensor(
|
|
forward_op.request_pool_indices[:num_extends],
|
|
dtype=torch.int64,
|
|
device="cpu",
|
|
pin_memory=True,
|
|
).to(device, non_blocking=True)
|
|
extend_prefix_lens = torch.tensor(
|
|
forward_op.prefill_lengths[:num_extends],
|
|
dtype=torch.int32,
|
|
device="cpu",
|
|
pin_memory=True,
|
|
).to(device, non_blocking=True)
|
|
# HostTodevice segment ends
|
|
|
|
execution_stream.wait_stream(torch.cuda.current_stream())
|
|
with torch.cuda.stream(execution_stream):
|
|
if num_extends > 0:
|
|
runtime_states.reset_states(
|
|
extend_request_pool_indices, extend_prefix_lens
|
|
)
|