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

453 lines
17 KiB
Python
Executable File

# 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.
from __future__ import annotations
import asyncio
import copy
import logging
import time
from collections import deque
from typing import (
TYPE_CHECKING,
Any,
Generic,
TypeVar,
)
import zmq
from tokenspeed.runtime.engine.io_struct import (
ExpertDistributionReq,
ExpertDistributionReqOutput,
FlushCacheReqInput,
FlushCacheReqOutput,
GetInternalStateReq,
GetInternalStateReqOutput,
GetLoadReqInput,
GetLoadReqOutput,
GetWeightsByNameReqInput,
GetWeightsByNameReqOutput,
InitWeightsUpdateGroupReqInput,
InitWeightsUpdateGroupReqOutput,
IsSchedulerPausedReqInput,
IsSchedulerPausedReqOutput,
IsSleepingReqInput,
IsSleepingReqOutput,
PauseMode,
PauseSchedulerReqInput,
PauseSchedulerReqOutput,
ProfileReq,
ProfileReqOutput,
ProfileReqType,
ReleaseMemoryOccupationReqInput,
ReleaseMemoryOccupationReqOutput,
ResumeMemoryOccupationReqInput,
ResumeMemoryOccupationReqOutput,
ResumeSchedulerReqInput,
ResumeSchedulerReqOutput,
SetInternalStateReq,
SetInternalStateReqOutput,
UpdateWeightsFromDistributedReqInput,
UpdateWeightsFromDistributedReqOutput,
UpdateWeightsFromTensorReqInput,
UpdateWeightsFromTensorReqOutput,
)
from tokenspeed.runtime.utils.dispatch import TypeBasedDispatcher
from tokenspeed.runtime.utils.env import envs
from tokenspeed.runtime.utils.server_args import ServerArgs
if TYPE_CHECKING:
from tokenspeed.runtime.engine.async_llm import AsyncLLM
T = TypeVar("T")
logger = logging.getLogger(__name__)
class _Communicator(Generic[T]):
"""Note: The communicator now only run up to 1 in-flight request at any time."""
def __init__(self, sender: zmq.Socket, fan_out: int, mode="queueing"):
self._sender = sender
self._fan_out = fan_out
self._mode = mode
self._result_event: asyncio.Event | None = None
self._result_values: list[T] | None = None
self._ready_queue: deque[asyncio.Future] = deque()
if mode not in ("queueing", "watching"):
raise ValueError(f"Invalid communicator mode: {mode}")
async def queueing_call(self, obj: T):
ready_event = asyncio.Event()
if self._result_event is not None or len(self._ready_queue) > 0:
self._ready_queue.append(ready_event)
await ready_event.wait()
if self._result_event is not None or self._result_values is not None:
raise RuntimeError("Communicator result state was not reset.")
if obj:
self._sender.send_pyobj(obj)
self._result_event = asyncio.Event()
self._result_values = []
await self._result_event.wait()
result_values = self._result_values
self._result_event = self._result_values = None
if len(self._ready_queue) > 0:
self._ready_queue.popleft().set()
return result_values
async def watching_call(self, obj):
if self._result_event is None:
if self._result_values is not None:
raise RuntimeError("Communicator result values were not reset.")
self._result_values = []
self._result_event = asyncio.Event()
if obj:
self._sender.send_pyobj(obj)
await self._result_event.wait()
result_values = copy.deepcopy(self._result_values)
self._result_event = self._result_values = None
return result_values
async def __call__(self, obj):
if self._mode == "queueing":
return await self.queueing_call(obj)
else:
return await self.watching_call(obj)
def handle_recv(self, recv_obj: T):
self._result_values.append(recv_obj)
if len(self._result_values) == self._fan_out:
self._result_event.set()
class SchedulerControlClient:
"""Scheduler control-plane client methods for AsyncLLM."""
def init_communicators(self: AsyncLLM, server_args: ServerArgs):
# Communicators
self.init_weights_update_group_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.update_weights_from_distributed_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.update_weights_from_tensor_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.get_weights_by_name_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.release_memory_occupation_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.resume_memory_occupation_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.flush_cache_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.pause_scheduler_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.resume_scheduler_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.is_scheduler_paused_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.is_sleeping_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.profile_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.get_internal_state_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.set_internal_state_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.expert_distribution_communicator = _Communicator(
self.engine_core_client.send_to_scheduler, server_args.mapping.attn.dp_size
)
self.get_load_communicator = _Communicator(
self.engine_core_client.send_to_scheduler,
server_args.mapping.attn.dp_size,
mode="watching",
)
self._result_dispatcher += self._get_communicator_dispatcher()
def _get_communicator_dispatcher(self: AsyncLLM):
return TypeBasedDispatcher(
[
(
InitWeightsUpdateGroupReqOutput,
self.init_weights_update_group_communicator.handle_recv,
),
(
UpdateWeightsFromDistributedReqOutput,
self.update_weights_from_distributed_communicator.handle_recv,
),
(
UpdateWeightsFromTensorReqOutput,
self.update_weights_from_tensor_communicator.handle_recv,
),
(
GetWeightsByNameReqOutput,
self.get_weights_by_name_communicator.handle_recv,
),
(
ReleaseMemoryOccupationReqOutput,
self.release_memory_occupation_communicator.handle_recv,
),
(
ResumeMemoryOccupationReqOutput,
self.resume_memory_occupation_communicator.handle_recv,
),
(
FlushCacheReqOutput,
self.flush_cache_communicator.handle_recv,
),
(
PauseSchedulerReqOutput,
self.pause_scheduler_communicator.handle_recv,
),
(
ResumeSchedulerReqOutput,
self.resume_scheduler_communicator.handle_recv,
),
(
IsSchedulerPausedReqOutput,
self.is_scheduler_paused_communicator.handle_recv,
),
(
IsSleepingReqOutput,
self.is_sleeping_communicator.handle_recv,
),
(
ProfileReqOutput,
self.profile_communicator.handle_recv,
),
(
GetInternalStateReqOutput,
self.get_internal_state_communicator.handle_recv,
),
(
SetInternalStateReqOutput,
self.set_internal_state_communicator.handle_recv,
),
(
ExpertDistributionReqOutput,
self.expert_distribution_communicator.handle_recv,
),
(
GetLoadReqOutput,
self.get_load_communicator.handle_recv,
),
]
)
async def flush_cache(self: AsyncLLM) -> FlushCacheReqOutput:
return (await self.flush_cache_communicator(FlushCacheReqInput()))[0]
async def pause_scheduler(self: AsyncLLM, *, mode: PauseMode = "abort") -> bool:
"""Pause generation to allow model weight updates.
``mode`` controls in-flight requests: ``"abort"`` cancels them,
``"wait"`` lets them finish, ``"keep"`` freezes them for ``/resume``.
For ``abort``/``wait`` the reply only returns once the scheduler has
drained, so on return no forward work is in flight.
Cache invalidation after a weight swap is the weight-update op's
responsibility (``update_weights_*(flush_cache=...)``), not pause's.
"""
# Pause may be the very first call (e.g. weight swap before serving),
# so ensure the output-dispatch loop is running to receive the reply.
self.auto_create_handle_loop()
result = (
await self.pause_scheduler_communicator(PauseSchedulerReqInput(mode=mode))
)[0]
return result.success
async def resume_scheduler(self: AsyncLLM) -> bool:
"""Resume generation after :meth:`pause_scheduler`."""
self.auto_create_handle_loop()
result = (await self.resume_scheduler_communicator(ResumeSchedulerReqInput()))[
0
]
return result.success
async def is_scheduler_paused(self: AsyncLLM) -> bool:
"""Return whether the scheduler is currently paused."""
self.auto_create_handle_loop()
result = (
await self.is_scheduler_paused_communicator(IsSchedulerPausedReqInput())
)[0]
return result.is_paused
async def start_profile(
self: AsyncLLM,
output_dir: str | None = None,
start_step: int | None = None,
num_steps: int | None = None,
activities: list[str] | None = None,
with_stack: bool | None = None,
record_shapes: bool | None = None,
profile_by_stage: bool = False,
profile_id: str | None = None,
):
self.auto_create_handle_loop()
env_with_stack = envs.TOKENSPEED_PROFILE_WITH_STACK.get()
with_stack = False if with_stack is False or env_with_stack is False else True
req = ProfileReq(
type=ProfileReqType.START_PROFILE,
output_dir=output_dir,
start_step=start_step,
num_steps=num_steps,
activities=activities,
with_stack=with_stack,
record_shapes=record_shapes,
profile_by_stage=profile_by_stage,
profile_id=profile_id or time.strftime("%Y%m%d-%H%M%S"),
)
return await self._execute_profile(req)
async def stop_profile(self: AsyncLLM):
self.auto_create_handle_loop()
req = ProfileReq(type=ProfileReqType.STOP_PROFILE)
return await self._execute_profile(req)
async def _execute_profile(self: AsyncLLM, req: ProfileReq):
result = (await self.profile_communicator(req))[0]
if not result.success:
raise RuntimeError(result.message)
return result
async def start_expert_distribution_record(self: AsyncLLM):
self.auto_create_handle_loop()
await self.expert_distribution_communicator(ExpertDistributionReq.START_RECORD)
async def stop_expert_distribution_record(self: AsyncLLM):
self.auto_create_handle_loop()
await self.expert_distribution_communicator(ExpertDistributionReq.STOP_RECORD)
async def dump_expert_distribution_record(self: AsyncLLM):
self.auto_create_handle_loop()
await self.expert_distribution_communicator(ExpertDistributionReq.DUMP_RECORD)
async def init_weights_update_group(
self: AsyncLLM,
obj: InitWeightsUpdateGroupReqInput,
) -> tuple[bool, str]:
self.auto_create_handle_loop()
if self.server_args.mapping.attn.has_dp:
raise RuntimeError("dp_size must be 1 for init parameter update group")
result = (await self.init_weights_update_group_communicator(obj))[0]
return result.success, result.message
async def update_weights_from_distributed(
self: AsyncLLM,
obj: UpdateWeightsFromDistributedReqInput,
) -> tuple[bool, str]:
self.auto_create_handle_loop()
if self.server_args.mapping.attn.has_dp:
raise RuntimeError("dp_size must be 1 for update weights from distributed")
# This means that weight sync
# cannot run while requests are in progress.
async with self.model_update_lock.writer_lock:
result = (await self.update_weights_from_distributed_communicator(obj))[0]
return result.success, result.message
async def update_weights_from_tensor(
self: AsyncLLM,
obj: UpdateWeightsFromTensorReqInput,
) -> tuple[bool, str]:
self.auto_create_handle_loop()
if self.server_args.mapping.attn.has_dp:
raise RuntimeError("dp_size must be 1 for update weights from tensor")
# This means that weight sync
# cannot run while requests are in progress.
async with self.model_update_lock.writer_lock:
result = (await self.update_weights_from_tensor_communicator(obj))[0]
return result.success, result.message
async def get_weights_by_name(
self: AsyncLLM,
obj: GetWeightsByNameReqInput,
):
self.auto_create_handle_loop()
results = await self.get_weights_by_name_communicator(obj)
all_parameters = [r.parameter for r in results]
if not self.server_args.mapping.attn.has_dp:
return all_parameters[0]
else:
return all_parameters
async def release_memory_occupation(
self: AsyncLLM,
obj: ReleaseMemoryOccupationReqInput,
) -> ReleaseMemoryOccupationReqOutput:
self.auto_create_handle_loop()
return (await self.release_memory_occupation_communicator(obj))[0]
async def resume_memory_occupation(
self: AsyncLLM,
obj: ResumeMemoryOccupationReqInput,
) -> ResumeMemoryOccupationReqOutput:
self.auto_create_handle_loop()
return (await self.resume_memory_occupation_communicator(obj))[0]
async def is_sleeping(self: AsyncLLM) -> bool:
self.auto_create_handle_loop()
result = (await self.is_sleeping_communicator(IsSleepingReqInput()))[0]
return result.is_sleeping
async def get_internal_state(self: AsyncLLM) -> list[dict[Any, Any]]:
req = GetInternalStateReq()
responses: list[GetInternalStateReqOutput] = (
await self.get_internal_state_communicator(req)
)
# Many DP ranks
return [res.internal_state for res in responses]
async def set_internal_state(
self: AsyncLLM, obj: SetInternalStateReq
) -> list[bool]:
responses: list[SetInternalStateReqOutput] = (
await self.set_internal_state_communicator(obj)
)
return [res.updated for res in responses]
async def get_load(self: AsyncLLM) -> list[GetLoadReqOutput]:
req = GetLoadReqInput()
return await self.get_load_communicator(req)