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

785 lines
30 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.
"""AsyncLLM is the main-process async frontend.
Owns request intake, per-request state, scheduler IPC, and the
output-dispatch loop. Inherits from ``EngineClient`` (explicit
structural conformance) and ``SchedulerControlClient`` (scheduler
control-plane helpers).
"""
import asyncio
import copy
import logging
import os
import signal
import sys
import threading
import time
import uuid
from collections import deque
from collections.abc import Awaitable
from enum import Enum
from http import HTTPStatus
from typing import (
Any,
Generic,
TypeVar,
)
import uvloop
from tokenspeed.runtime.configs.model_config import ModelConfig
from tokenspeed.runtime.engine.aio_rwlock import RWLock
from tokenspeed.runtime.engine.collector import RequestOutputCollector
from tokenspeed.runtime.engine.core_client import EngineCoreClient
from tokenspeed.runtime.engine.exceptions import EngineGenerateError
from tokenspeed.runtime.engine.input_processor import InputProcessor
from tokenspeed.runtime.engine.io_struct import (
AbortReq,
BatchEmbeddingOut,
BatchStrOut,
BatchTokenIDOut,
CloseSessionReqInput,
ConfigureLoggingReq,
EmbeddingReqInput,
FlushCacheReqInput,
FlushCacheReqOutput,
GenerateReqInput,
GetLoadReqInput,
HealthCheckOutput,
OpenSessionReqInput,
OpenSessionReqOutput,
TokenizedEmbeddingReqInput,
TokenizedGenerateReqInput,
UpdateWeightFromDiskReqInput,
UpdateWeightFromDiskReqOutput,
WatchLoadUpdateReq,
)
from tokenspeed.runtime.engine.output_processor import OutputProcessor, ReqState
from tokenspeed.runtime.engine.parallel_sampling import (
prepare_parallel_sampling_replica,
prepare_prefix_warmup,
)
from tokenspeed.runtime.engine.protocol import EngineClient
from tokenspeed.runtime.engine.scheduler_control_client import (
SchedulerControlClient,
)
from tokenspeed.runtime.metrics.collector import RequestMetrics
from tokenspeed.runtime.pd.utils import (
DisaggregationMode,
KVClassType,
TransferBackend,
get_kv_class,
)
from tokenspeed.runtime.utils import (
dataclass_to_string_truncated,
get_colorful_logger,
)
from tokenspeed.runtime.utils.dispatch import TypeBasedDispatcher
from tokenspeed.runtime.utils.exceptions import get_exception_traceback
from tokenspeed.runtime.utils.hf_transformers_utils import get_tokenizer
from tokenspeed.runtime.utils.process import kill_process_tree
from tokenspeed.runtime.utils.server_args import PortArgs, ServerArgs
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
logger = get_colorful_logger(__name__)
def _ignore_health_check_output(_: HealthCheckOutput) -> None:
return None
class ServerStatus(Enum):
Up = "Up"
Starting = "Starting"
UnHealthy = "UnHealthy"
Crashed = "Crashed"
class AsyncLLM(SchedulerControlClient, EngineClient):
"""Main-process async frontend for the tokenspeed runtime.
Owns request intake, per-request state, scheduler IPC, and the
output-dispatch loop. Structurally satisfies :class:`EngineClient`
via the explicit inheritance declaration above.
"""
def __init__(
self,
server_args: ServerArgs,
port_args: PortArgs,
):
# Parse args
self.server_args = server_args
self.enable_metrics = server_args.enable_metrics
self.log_requests = server_args.enable_log_requests
self.log_requests_level = server_args.log_requests_level
self.logger = logger
# Init inter-process communication (scheduler IPC owned by EngineCoreClient).
self.engine_core_client = EngineCoreClient(port_args)
# Read model args
self.model_path = server_args.model
self.served_model_name = server_args.served_model_name
self.model_config = ModelConfig(
server_args.model,
trust_remote_code=server_args.trust_remote_code,
revision=server_args.revision,
context_length=server_args.max_model_len,
model_override_args=server_args.hf_overrides,
dtype=server_args.dtype,
quantization=server_args.quantization,
server_args=server_args,
)
self.is_generation = self.model_config.is_generation
self.is_image_gen = self.model_config.is_image_gen
self.context_len = self.model_config.context_len
self.image_token_id = self.model_config.image_token_id
# Create tokenizer. The engine never preprocesses images -- the SMG
# gateway ships precomputed multimodal inputs -- so even multimodal
# models only need the tokenizer, not the full HF AutoProcessor.
if server_args.skip_tokenizer_init:
self.tokenizer = None
else:
self.tokenizer = get_tokenizer(
server_args.tokenizer,
tokenizer_mode=server_args.tokenizer_mode,
trust_remote_code=server_args.trust_remote_code,
revision=server_args.revision,
architectures=self.model_config.hf_config.architectures,
)
if self.model_config.is_multimodal:
os.environ["TOKENIZERS_PARALLELISM"] = "false"
# Store states
self.no_create_loop = False
self.rid_to_state: dict[str, ReqState] = {}
self.gracefully_exit = False
self.last_receive_tstamp = 0
self.dump_requests_folder = "" # By default do not dump
self.dump_requests_threshold = 1000
self.dump_request_list: list[tuple] = []
self.log_request_metadata = self.get_log_request_metadata()
self.server_status = ServerStatus.Starting
# The event to notify the weight sync is finished.
self.model_update_lock = RWLock()
self.model_update_result: Awaitable[UpdateWeightFromDiskReqOutput] | None = None
self.asyncio_tasks = set()
# For session info
self.session_futures = {} # session_id -> asyncio event
# Set after scheduler is initialized
self.max_req_input_len = None
self.metrics = RequestMetrics(
labels={
"model_name": self.server_args.served_model_name,
"app_key": self.server_args.app_key,
},
enabled=(
self.enable_metrics
and "prometheus" in (server_args.metrics_reporters or [])
),
)
self.output_processor = OutputProcessor(self)
self._result_dispatcher = TypeBasedDispatcher(
[
(
(
BatchStrOut,
BatchEmbeddingOut,
BatchTokenIDOut,
),
self.output_processor.handle_batch_output,
),
(OpenSessionReqOutput, self._handle_open_session_req_output),
(
UpdateWeightFromDiskReqOutput,
self._handle_update_weights_from_disk_req_output,
),
(HealthCheckOutput, _ignore_health_check_output),
]
)
self.disaggregation_mode = DisaggregationMode(
self.server_args.disaggregation_mode
)
self.transfer_backend = TransferBackend(
self.server_args.disaggregation_transfer_backend
)
# for disaggregation, start kv bootstrap server on prefill
if self.disaggregation_mode == DisaggregationMode.PREFILL:
# only start bootstrap server on prefill tm
kv_bootstrap_server_class = get_kv_class(
self.transfer_backend, KVClassType.BOOTSTRAP_SERVER
)
self.bootstrap_server = kv_bootstrap_server_class(
self.server_args.disaggregation_bootstrap_port
)
self.init_communicators(server_args)
# Tokenization lives in :class:`InputProcessor`; see
# :meth:`_tokenize_one_request` for the delegation.
self.input_processor = InputProcessor(self)
async def generate_request(
self,
obj: GenerateReqInput | EmbeddingReqInput,
):
created_time = time.time()
self.auto_create_handle_loop()
self.input_processor.validate_request(obj)
obj.normalize_batch_and_arguments()
if self.log_requests:
max_length, skip_names, _ = self.log_request_metadata
logger.info(
"Receive: obj=%s",
dataclass_to_string_truncated(obj, max_length, skip_names=skip_names),
)
async with self.model_update_lock.reader_lock:
is_single = obj.is_single
if is_single:
tokenized_obj = await self._tokenize_one_request(obj)
self._send_one_request(obj, tokenized_obj, created_time)
async for response in self._wait_one_response(obj):
yield response
else:
async for response in self._handle_batch_request(obj, created_time):
yield response
async def _tokenize_one_request(
self,
obj: GenerateReqInput | EmbeddingReqInput,
) -> TokenizedGenerateReqInput | TokenizedEmbeddingReqInput:
"""Delegate to :class:`InputProcessor`.
The tokenization body lives in
``InputProcessor.tokenize_one_request``. If the input-side
surface ever grows (e.g. multimodal routing), it happens in
that module — not here.
"""
return await self.input_processor.tokenize_one_request(obj)
def _send_one_request(
self,
obj: GenerateReqInput | EmbeddingReqInput,
tokenized_obj: TokenizedGenerateReqInput | TokenizedEmbeddingReqInput,
created_time: float | None = None,
):
state = ReqState(
RequestOutputCollector(),
False,
asyncio.Event(),
obj,
created_time=created_time,
tokenized_time=tokenized_obj.created_time,
)
self.rid_to_state[obj.rid] = state
mm_inputs = getattr(tokenized_obj, "multimodal_inputs", None)
if mm_inputs is not None:
mm_inputs.publish_shm_features()
self.engine_core_client.send_to_scheduler.send_pyobj(tokenized_obj)
def submit_encode(self, encode_request) -> None:
"""Send an EPD encode request to the encode-worker scheduler subprocess.
Fire-and-forget over the same scheduler-input channel the LM uses (the
encode subprocess runs ``run_encode_loop``, not the LM EventLoop). The
encode worker runs the vision tower and ships the embeddings to prefill
over Mooncake; the gateway gets no streamed response, only the gRPC ack.
``encode_request`` is an ``encode_worker.EncodeRequest`` whose multimodal
tensors are CPU (gateway-reconstructed) and pickle over ZMQ directly; shm
publishing of the pixels is a follow-up optimization.
"""
self.engine_core_client.send_to_scheduler.send_pyobj(encode_request)
async def _wait_one_response(
self,
obj: GenerateReqInput | EmbeddingReqInput,
):
"""Wait for the response of one request.
Cancellation contract: callers (FastAPI route handlers, the sync
``LLM`` bridge, RL-trainer drivers, etc.) signal client disconnect
via ``asyncio.CancelledError`` — not via a polled
``request.is_disconnected()`` check. If the task driving this
generator is cancelled mid-wait, the ``finally`` below drops the
rid from ``rid_to_state`` and fires an ``AbortReq`` at the
scheduler so no per-request state leaks.
"""
state = self.rid_to_state[obj.rid]
try:
while True:
await state.event.wait()
out = state.collector.take()
if out is None:
state.event.clear()
continue
if state.finished:
if self.log_requests:
max_length, skip_names, out_skip_names = (
self.log_request_metadata
)
if self.model_config.is_multimodal_gen:
msg = f"Finish: obj={dataclass_to_string_truncated(obj, max_length, skip_names=skip_names)}"
else:
if (
isinstance(obj, GenerateReqInput)
and obj.input_ids is not None
and obj.text is None
):
if self.tokenizer is not None:
obj.text = self.tokenizer.decode(
obj.input_ids,
skip_special_tokens=getattr(
obj.sampling_params,
"skip_special_tokens",
False,
),
)
else:
obj.text = ""
msg = f"Finish: obj={dataclass_to_string_truncated(obj, max_length, skip_names=skip_names)}, out={dataclass_to_string_truncated(out, max_length, skip_names=out_skip_names)}"
logger.info(msg)
del self.rid_to_state[obj.rid]
# Check if this was an abort/error created by scheduler
if isinstance(out["meta_info"].get("finish_reason"), dict):
finish_reason = out["meta_info"]["finish_reason"]
if finish_reason.get("type") == "abort":
if (
finish_reason.get("status_code")
== HTTPStatus.BAD_REQUEST
):
raise EngineGenerateError(finish_reason["message"])
yield out
break
state.event.clear()
if state.collector.has_pending():
state.event.set()
if obj.stream:
yield out
# else: non-stream path falls through and waits for the
# final chunk; external cancellation wakes us via
# ``asyncio.CancelledError`` from ``state.event.wait()``.
finally:
# Idempotent cleanup split on ``state.finished``:
#
# * Normal-finish path: the yield loop above already did
# ``del self.rid_to_state[obj.rid]`` at ``state.finished``.
# We defensively ``pop`` with a default so a second exit
# through ``finally`` (e.g. when the yield itself raised
# after the del) is a no-op.
#
# * Abandoned path (CancelledError / unexpected exception):
# the rid is still in ``rid_to_state``. Call
# ``abort_request`` which **both** removes it from the
# state map **and** sends ``AbortReq`` to the scheduler.
# Ordering matters: ``abort_request`` early-returns if
# the rid is already gone, so we must not pop first.
if state.finished:
self.rid_to_state.pop(obj.rid, None)
else:
self.abort_request(obj.rid)
async def _handle_batch_request(
self,
obj: GenerateReqInput | EmbeddingReqInput,
created_time: float | None = None,
):
batch_size = obj.batch_size
generators = []
rids = []
if getattr(obj, "parallel_sample_num", 1) == 1:
# Send all requests
for i in range(batch_size):
tmp_obj = obj[i]
tokenized_obj = await self._tokenize_one_request(tmp_obj)
self._send_one_request(tmp_obj, tokenized_obj, created_time)
generators.append(self._wait_one_response(tmp_obj))
rids.append(tmp_obj.rid)
else:
# Batched parallel sampling still follows a conservative path and
# can be slower than duplicating requests explicitly.
if batch_size > 128:
logger.warning(
"Sending a single large batch with parallel sampling (n > 1) has not been well optimized. "
"The performance might be better if you just duplicate the requests n times or use "
"many threads to send them one by one with parallel sampling (n > 1)."
)
# Tokenize all requests
objs = [obj[i] for i in range(batch_size)]
tokenized_objs = await self.input_processor.tokenize_batch(objs)
# Cache the common prefix for parallel sampling
for i in range(batch_size):
tmp_obj = copy.copy(objs[i])
warmup_obj = prepare_prefix_warmup(tmp_obj, tokenized_objs[i])
self._send_one_request(tmp_obj, warmup_obj, created_time)
await self._wait_one_response(tmp_obj).__anext__()
# Expand requests, assign new rids for them, and send them
for i in range(batch_size):
for _ in range(obj.parallel_sample_num):
tmp_obj = copy.copy(objs[i])
replica_obj = prepare_parallel_sampling_replica(
tmp_obj, tokenized_objs[i]
)
self._send_one_request(tmp_obj, replica_obj, created_time)
generators.append(self._wait_one_response(tmp_obj))
rids.append(tmp_obj.rid)
# Wait for all requests
is_stream = hasattr(obj, "stream") and obj.stream
if not is_stream:
outputs = await asyncio.gather(*(gen.__anext__() for gen in generators))
yield outputs
else:
rid_to_index = {rid: i for i, rid in enumerate(rids)}
task_map = {asyncio.create_task(gen.__anext__()): gen for gen in generators}
while task_map:
done, _ = await asyncio.wait(
task_map.keys(), return_when=asyncio.FIRST_COMPLETED
)
for task in done:
gen = task_map.pop(task)
try:
result = task.result()
result["index"] = rid_to_index[result["meta_info"]["id"]]
yield result
new_task = asyncio.create_task(gen.__anext__())
task_map[new_task] = gen
except StopAsyncIteration:
pass
async def flush_cache(self) -> FlushCacheReqOutput:
return (await self.flush_cache_communicator(FlushCacheReqInput()))[0]
def abort_request(self, rid: str):
if rid not in self.rid_to_state:
return
del self.rid_to_state[rid]
req = AbortReq(rid)
self.engine_core_client.send_to_scheduler.send_pyobj(req)
async def update_weights_from_disk(
self,
obj: UpdateWeightFromDiskReqInput,
) -> tuple[bool, str, Any]:
self.auto_create_handle_loop()
# default the load format to the server_args
if obj.load_format is None:
obj.load_format = self.server_args.load_format
logger.info("Start update_weights. Load format=%s", obj.load_format)
# Hold the lock if it is not async. This means that weight sync
# cannot run while requests are in progress.
async with self.model_update_lock.writer_lock:
return await self._wait_for_model_update_from_disk(obj)
async def _wait_for_model_update_from_disk(
self, obj: UpdateWeightFromDiskReqInput
) -> tuple[bool, str]:
self.engine_core_client.send_to_scheduler.send_pyobj(obj)
self.model_update_result = asyncio.Future()
if not self.server_args.mapping.attn.has_dp:
result = await self.model_update_result
if result.success:
self.served_model_name = obj.model_path
self.server_args.model = obj.model_path
self.server_args.load_format = obj.load_format
self.model_path = obj.model_path
return result.success, result.message, result.num_paused_requests
else: # self.server_args.mapping.has_attn_dp
self.model_update_tmp = []
result = await self.model_update_result
all_success = all([r.success for r in result])
if all_success is True:
self.server_args.model = obj.model_path
self.server_args.load_format = obj.load_format
self.model_path = obj.model_path
all_message = [r.message for r in result]
all_message = " | ".join(all_message)
all_paused_requests = [r.num_paused_requests for r in result]
return all_success, all_message, all_paused_requests
async def open_session(self, obj: OpenSessionReqInput) -> str | None:
self.auto_create_handle_loop()
if obj.session_id is None:
obj.session_id = uuid.uuid4().hex
elif obj.session_id in self.session_futures:
return None
self.engine_core_client.send_to_scheduler.send_pyobj(obj)
self.session_futures[obj.session_id] = asyncio.Future()
session_id = await self.session_futures[obj.session_id]
del self.session_futures[obj.session_id]
return session_id
async def close_session(self, obj: CloseSessionReqInput) -> None:
await self.engine_core_client.send_to_scheduler.send_pyobj(obj)
async def watch_load_thread(self):
# Only for dp_controller when dp_size > 1
if (
not self.server_args.mapping.attn.has_dp
or self.server_args.load_balance_method == "round_robin"
):
return
while True:
await asyncio.sleep(self.server_args.load_watch_interval)
loads = await self.get_load_communicator(GetLoadReqInput())
load_udpate_req = WatchLoadUpdateReq(loads=loads)
self.engine_core_client.send_to_scheduler.send_pyobj(load_udpate_req)
def get_log_request_metadata(self):
max_length = None
skip_names = None
out_skip_names = None
if self.log_requests:
if self.log_requests_level == 0:
max_length = 1 << 30
skip_names = set(
[
"text",
"input_ids",
"input_embeds",
"image_data",
"audio_data",
"precomputed_multimodal_inputs",
"input_multi_ids",
]
)
out_skip_names = set(
[
"text",
"output_ids",
]
)
elif self.log_requests_level == 1:
max_length = 2048
elif self.log_requests_level == 2:
max_length = 1 << 30
else:
raise ValueError(
f"Invalid --log-requests-level: {self.log_requests_level=}"
)
return max_length, skip_names, out_skip_names
def configure_logging(self, obj: ConfigureLoggingReq):
if obj.log_requests is not None:
self.log_requests = obj.log_requests
if obj.log_requests_level is not None:
self.log_requests_level = obj.log_requests_level
if obj.dump_requests_folder is not None:
self.dump_requests_folder = obj.dump_requests_folder
if obj.dump_requests_threshold is not None:
self.dump_requests_threshold = obj.dump_requests_threshold
logging.info("Config logging: obj=%r", obj)
self.log_request_metadata = self.get_log_request_metadata()
# ---- Server lifecycle / health -------------------------------
# Intent-revealing wrappers around the private ``server_status``
# field. Callers (notably ``http_server.py``) drive transitions
# through these methods so the ``ServerStatus`` enum and the
# attribute name stay implementation-private.
def is_server_starting(self) -> bool:
return self.server_status == ServerStatus.Starting
def mark_server_up(self) -> None:
self.server_status = ServerStatus.Up
def mark_server_unhealthy(self) -> None:
self.server_status = ServerStatus.UnHealthy
def drop_request_state(self, rid: str) -> None:
"""Discard the per-request state for ``rid`` if present.
Used by health probes that synthesize a request, await one
token through ``generate_request``, and then need to clean
up the state slot regardless of whether the probe succeeded
or timed out.
"""
self.rid_to_state.pop(rid, None)
def auto_create_handle_loop(self):
if self.no_create_loop:
return
self.no_create_loop = True
loop = asyncio.get_event_loop()
self.asyncio_tasks.add(
loop.create_task(print_exception_wrapper(self.handle_loop))
)
# We cannot add signal handler when the tokenizer manager is not in
# the main thread due to the CPython limitation.
if threading.current_thread() is threading.main_thread():
signal_handler = SignalHandler(self)
loop.add_signal_handler(signal.SIGTERM, signal_handler.signal_handler)
else:
logger.warning(
"Signal handler is not added because the tokenizer manager is "
"not in the main thread. This disables graceful shutdown of the "
"tokenizer manager when SIGTERM is received."
)
self.asyncio_tasks.add(
loop.create_task(print_exception_wrapper(self.sigterm_watchdog))
)
self.asyncio_tasks.add(
loop.create_task(print_exception_wrapper(self.watch_load_thread))
)
async def sigterm_watchdog(self):
while not self.gracefully_exit:
await asyncio.sleep(5)
# Drain requests
while True:
remain_num_req = len(self.rid_to_state)
logger.info(
"Gracefully exiting... remaining number of requests %s", remain_num_req
)
if remain_num_req > 0:
await asyncio.sleep(5)
else:
break
kill_process_tree(os.getpid(), include_parent=True)
sys.exit(0)
async def handle_loop(self):
"""The event loop that handles requests"""
while True:
recv_obj = await self.engine_core_client.recv_from_detokenizer.recv_pyobj()
self._result_dispatcher(recv_obj)
self.last_receive_tstamp = time.time()
def _handle_open_session_req_output(self, recv_obj):
self.session_futures[recv_obj.session_id].set_result(
recv_obj.session_id if recv_obj.success else None
)
def _handle_update_weights_from_disk_req_output(self, recv_obj):
if not self.server_args.mapping.attn.has_dp:
self.model_update_result.set_result(recv_obj)
else: # self.server_args.mapping.has_attn_dp
self.model_update_tmp.append(recv_obj)
# set future if the all results are received
if len(self.model_update_tmp) == self.server_args.mapping.attn.dp_size:
self.model_update_result.set_result(self.model_update_tmp)
async def print_exception_wrapper(func):
"""
Sometimes an asyncio function does not print exception.
We do another wrapper to handle the exception.
"""
try:
await func()
except Exception:
traceback = get_exception_traceback()
logger.error("AsyncLLM hit an exception: %s", traceback)
kill_process_tree(os.getpid(), include_parent=True)
sys.exit(1)
class SignalHandler:
def __init__(self, tokenizer_manager):
self.tokenizer_manager = tokenizer_manager
def signal_handler(self, signum=None, frame=None):
logger.warning(
"SIGTERM received. signum=%r frame=%r. Draining requests and shutting down...",
signum,
frame,
)
self.tokenizer_manager.gracefully_exit = True
T = TypeVar("T")
class _Communicator(Generic[T]):
"""Note: The communicator now only run up to 1 in-flight request at any time."""
def __init__(self, sender, fan_out: int):
self._sender = sender
self._fan_out = fan_out
self._result_event: asyncio.Event | None = None
self._result_values: list[T] | None = None
self._ready_queue: deque[asyncio.Future] = deque()
async def __call__(self, obj):
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
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()