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
589 lines
22 KiB
Python
Executable File
589 lines
22 KiB
Python
Executable File
# SPDX-License-Identifier: MIT AND Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
|
|
# SPDX-FileCopyrightText: Copyright 2023-2024 SGLang Team
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
The entry point of inference server.
|
|
|
|
This file implements python APIs for the inference engine.
|
|
"""
|
|
|
|
# ruff: noqa: E402
|
|
import asyncio
|
|
import atexit
|
|
import copy
|
|
import dataclasses
|
|
import multiprocessing as mp
|
|
import os
|
|
import signal
|
|
import threading
|
|
from collections.abc import AsyncIterator, Iterator
|
|
|
|
import zmq
|
|
import zmq.asyncio
|
|
|
|
from tokenspeed.runtime.engine.async_llm import AsyncLLM
|
|
from tokenspeed.runtime.engine.llm import LLM
|
|
|
|
|
|
def _ignore_threading_atexit(*args, **kwargs) -> None:
|
|
return None
|
|
|
|
|
|
# Fix a bug of Python threading
|
|
setattr(threading, "_register_atexit", _ignore_threading_atexit)
|
|
|
|
import torch
|
|
import uvloop
|
|
|
|
from tokenspeed.runtime.engine.data_parallel_controller import (
|
|
run_data_parallel_controller_process,
|
|
)
|
|
from tokenspeed.runtime.engine.event_loop import run_event_loop
|
|
from tokenspeed.runtime.engine.io_struct import (
|
|
GenerateReqInput,
|
|
GetWeightsByNameReqInput,
|
|
InitWeightsUpdateGroupReqInput,
|
|
ReleaseMemoryOccupationReqInput,
|
|
ResumeMemoryOccupationReqInput,
|
|
RpcReqInput,
|
|
RpcReqOutput,
|
|
UpdateWeightFromDiskReqInput,
|
|
UpdateWeightsFromDistributedReqInput,
|
|
UpdateWeightsFromTensorReqInput,
|
|
)
|
|
from tokenspeed.runtime.entrypoints.engine_base import EngineBase
|
|
from tokenspeed.runtime.utils import (
|
|
MultiprocessingSerializer,
|
|
configure_logger,
|
|
get_colorful_logger,
|
|
launch_dummy_health_check_server,
|
|
prepare_model_and_tokenizer,
|
|
set_prometheus_multiproc_dir,
|
|
set_ulimit,
|
|
)
|
|
from tokenspeed.runtime.utils.env import envs
|
|
from tokenspeed.runtime.utils.process import kill_process_tree
|
|
from tokenspeed.runtime.utils.server_args import PortArgs, ServerArgs
|
|
from tokenspeed.runtime.utils.torch_memory_saver_adapter import TorchMemorySaverAdapter
|
|
from tokenspeed.version import __version__
|
|
|
|
logger = get_colorful_logger(__name__)
|
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|
|
|
|
|
class Engine(EngineBase):
|
|
"""
|
|
The entry point to the inference engine.
|
|
|
|
- The engine consists of three components:
|
|
1. TokenizerManager: Tokenizes the requests and sends them to the scheduler.
|
|
2. Scheduler (subprocess): Receives requests from the Tokenizer Manager, schedules batches, forwards them, and sends the output tokens to the Detokenizer Manager.
|
|
3. DetokenizerManager (subprocess): Detokenizes the output tokens and sends the result back to the Tokenizer Manager.
|
|
|
|
Note:
|
|
1. The HTTP server, Engine, and TokenizerManager both run in the main process.
|
|
2. Inter-process communication is done through ICP (each process uses a different port) via the ZMQ library.
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
"""
|
|
The arguments of this function is the same as `tokenspeed/runtime/utils/server_args.py::ServerArgs`.
|
|
Please refer to `ServerArgs` for the documentation.
|
|
"""
|
|
if "server_args" in kwargs:
|
|
# Directly load server_args
|
|
server_args = kwargs["server_args"]
|
|
else:
|
|
# Construct server_args from kwargs
|
|
if "log_level" not in kwargs:
|
|
# Do not print logs by default
|
|
kwargs["log_level"] = "error"
|
|
server_args = ServerArgs(**kwargs)
|
|
|
|
# Shutdown the subprocesses automatically when the program exits
|
|
atexit.register(self.shutdown)
|
|
|
|
# Allocate ports for inter-process communications
|
|
self.port_args = PortArgs.init_new(server_args)
|
|
logger.info("server_args=%r", server_args)
|
|
|
|
# Launch subprocesses
|
|
tokenizer_manager, _, scheduler_info = _launch_subprocesses(
|
|
server_args=server_args,
|
|
port_args=self.port_args,
|
|
)
|
|
self.server_args = server_args
|
|
self.tokenizer_manager = tokenizer_manager
|
|
self.scheduler_info = scheduler_info
|
|
|
|
# Sync facade for blocking callers. Owns its own bg event-loop thread; see runtime/engine/llm.py
|
|
# for the queue-bridge semantics.
|
|
self.llm = LLM(self.tokenizer_manager)
|
|
|
|
def generate(
|
|
self,
|
|
# The input prompt. It can be a single prompt or a batch of prompts.
|
|
prompt: list[str] | str | None = None,
|
|
sampling_params: list[dict] | dict | None = None,
|
|
# The token ids for text; one can either specify text or input_ids.
|
|
input_ids: list[list[int]] | list[int] | None = None,
|
|
# SGLang-compatible logprob controls; vLLM-compatible requests use
|
|
# sampling_params["logprobs"].
|
|
return_logprob: list[bool] | bool | None = None,
|
|
logprob_start_len: list[int] | int | None = None,
|
|
top_logprobs_num: list[int] | int | None = None,
|
|
token_ids_logprob: list[list[int]] | list[int] | None = None,
|
|
return_text_in_logprobs: bool = False,
|
|
logprob_format: list[str | None] | str | None = None,
|
|
custom_logit_processor: list[str] | str | None = None,
|
|
return_hidden_states: bool = False,
|
|
stream: bool = False,
|
|
bootstrap_host: list[str] | str | None = None,
|
|
bootstrap_port: list[int] | int | None = None,
|
|
bootstrap_room: list[int] | int | None = None,
|
|
data_parallel_rank: int | None = None,
|
|
) -> dict | Iterator[dict]:
|
|
"""
|
|
The arguments of this function match
|
|
``tokenspeed.runtime.engine.io_struct.GenerateReqInput``.
|
|
Please refer to ``GenerateReqInput`` for the documentation.
|
|
"""
|
|
if self.server_args.mapping.has_attn_dp:
|
|
if data_parallel_rank is None:
|
|
logger.debug("data_parallel_rank not provided, using default dispatch")
|
|
elif data_parallel_rank < 0:
|
|
raise ValueError("data_parallel_rank must be non-negative")
|
|
elif data_parallel_rank >= self.server_args.mapping.attn.dp_size:
|
|
raise ValueError(
|
|
f"data_parallel_rank must be less than dp_size: {self.server_args.mapping.attn.dp_size}"
|
|
)
|
|
|
|
obj = GenerateReqInput(
|
|
text=prompt,
|
|
input_ids=input_ids,
|
|
sampling_params=sampling_params,
|
|
return_logprob=return_logprob,
|
|
logprob_start_len=logprob_start_len,
|
|
top_logprobs_num=top_logprobs_num,
|
|
token_ids_logprob=token_ids_logprob,
|
|
return_text_in_logprobs=return_text_in_logprobs,
|
|
logprob_format=logprob_format,
|
|
custom_logit_processor=custom_logit_processor,
|
|
return_hidden_states=return_hidden_states,
|
|
stream=stream,
|
|
bootstrap_host=bootstrap_host,
|
|
bootstrap_port=bootstrap_port,
|
|
bootstrap_room=bootstrap_room,
|
|
)
|
|
if stream:
|
|
return self.llm.generate_stream(obj)
|
|
else:
|
|
return self.llm.generate(obj)
|
|
|
|
async def async_generate(
|
|
self,
|
|
# The input prompt. It can be a single prompt or a batch of prompts.
|
|
prompt: list[str] | str | None = None,
|
|
sampling_params: list[dict] | dict | None = None,
|
|
# The token ids for text; one can either specify text or input_ids.
|
|
input_ids: list[list[int]] | list[int] | None = None,
|
|
input_embeds: torch.Tensor = None,
|
|
input_multi_ids: list[list[int]] = None,
|
|
input_extra_infos: list[dict] = None,
|
|
# Same legacy logprob controls as generate().
|
|
return_logprob: list[bool] | bool | None = None,
|
|
logprob_start_len: list[int] | int | None = None,
|
|
top_logprobs_num: list[int] | int | None = None,
|
|
token_ids_logprob: list[list[int]] | list[int] | None = None,
|
|
return_text_in_logprobs: bool = False,
|
|
logprob_format: list[str | None] | str | None = None,
|
|
custom_logit_processor: list[str] | str | None = None,
|
|
return_hidden_states: bool = False,
|
|
stream: bool = False,
|
|
bootstrap_host: list[str] | str | None = None,
|
|
bootstrap_port: list[int] | int | None = None,
|
|
bootstrap_room: list[int] | int | None = None,
|
|
user_rid: list[str] | str | None = None,
|
|
) -> dict | AsyncIterator[dict]:
|
|
"""
|
|
The arguments of this function match
|
|
``tokenspeed.runtime.engine.io_struct.GenerateReqInput``.
|
|
Please refer to ``GenerateReqInput`` for the documentation.
|
|
"""
|
|
|
|
obj = GenerateReqInput(
|
|
text=prompt,
|
|
input_ids=input_ids,
|
|
input_embeds=input_embeds,
|
|
input_multi_ids=input_multi_ids,
|
|
input_extra_infos=input_extra_infos,
|
|
sampling_params=sampling_params,
|
|
return_logprob=return_logprob,
|
|
logprob_start_len=logprob_start_len,
|
|
top_logprobs_num=top_logprobs_num,
|
|
token_ids_logprob=token_ids_logprob,
|
|
return_text_in_logprobs=return_text_in_logprobs,
|
|
logprob_format=logprob_format,
|
|
return_hidden_states=return_hidden_states,
|
|
stream=stream,
|
|
custom_logit_processor=custom_logit_processor,
|
|
bootstrap_host=bootstrap_host,
|
|
bootstrap_port=bootstrap_port,
|
|
bootstrap_room=bootstrap_room,
|
|
user_rid=user_rid,
|
|
)
|
|
generator = self.tokenizer_manager.generate_request(obj)
|
|
|
|
async def wrapped_output_generator(original_async_gen):
|
|
async for item in original_async_gen:
|
|
yield item
|
|
|
|
await asyncio.sleep(1)
|
|
self.tokenizer_manager.abort_request(obj.rid[0])
|
|
|
|
if stream is True:
|
|
return wrapped_output_generator(generator)
|
|
else:
|
|
return await generator.__anext__()
|
|
|
|
def shutdown(self):
|
|
"""Shutdown the engine"""
|
|
# Stop the sync-facade event loop before subprocess teardown so any
|
|
# in-flight blocking callers see a clean loop close instead of a
|
|
# stale-reference error.
|
|
if getattr(self, "llm", None) is not None:
|
|
self.llm.shutdown()
|
|
kill_process_tree(os.getpid(), include_parent=False)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
self.shutdown()
|
|
return False
|
|
|
|
def flush_cache(self):
|
|
return self.llm.run(self.tokenizer_manager.flush_cache())
|
|
|
|
def pause_scheduler(self, mode: str = "abort"):
|
|
"""Pause generation (e.g. to swap weights). See AsyncLLM.pause_scheduler."""
|
|
return self.llm.run(self.tokenizer_manager.pause_scheduler(mode=mode))
|
|
|
|
def resume_scheduler(self):
|
|
"""Resume generation after :meth:`pause_scheduler`."""
|
|
return self.llm.run(self.tokenizer_manager.resume_scheduler())
|
|
|
|
def is_scheduler_paused(self):
|
|
"""Return whether the scheduler is currently paused."""
|
|
return self.llm.run(self.tokenizer_manager.is_scheduler_paused())
|
|
|
|
def start_profile(self):
|
|
self.llm.run(self.tokenizer_manager.start_profile())
|
|
|
|
def stop_profile(self):
|
|
self.llm.run(self.tokenizer_manager.stop_profile())
|
|
|
|
def start_expert_distribution_record(self):
|
|
self.llm.run(self.tokenizer_manager.start_expert_distribution_record())
|
|
|
|
def stop_expert_distribution_record(self):
|
|
self.llm.run(self.tokenizer_manager.stop_expert_distribution_record())
|
|
|
|
def dump_expert_distribution_record(self):
|
|
self.llm.run(self.tokenizer_manager.dump_expert_distribution_record())
|
|
|
|
def get_server_info(self):
|
|
internal_states = self.llm.run(self.tokenizer_manager.get_internal_state())
|
|
return {
|
|
**dataclasses.asdict(self.tokenizer_manager.server_args),
|
|
**self.scheduler_info,
|
|
"internal_states": internal_states,
|
|
"version": __version__,
|
|
}
|
|
|
|
def init_weights_update_group(
|
|
self,
|
|
master_address: str,
|
|
master_port: int,
|
|
rank_offset: int,
|
|
world_size: int,
|
|
group_name: str,
|
|
backend: str = "nccl",
|
|
):
|
|
"""Initialize parameter update group."""
|
|
obj = InitWeightsUpdateGroupReqInput(
|
|
master_address=master_address,
|
|
master_port=master_port,
|
|
rank_offset=rank_offset,
|
|
world_size=world_size,
|
|
group_name=group_name,
|
|
backend=backend,
|
|
)
|
|
return self.llm.run(self.tokenizer_manager.init_weights_update_group(obj))
|
|
|
|
def update_weights_from_distributed(
|
|
self,
|
|
names: list[str],
|
|
dtypes: list[str],
|
|
shapes: list[list[int]],
|
|
group_name: str = "weight_update_group",
|
|
flush_cache: bool = True,
|
|
):
|
|
"""Update weights from distributed source."""
|
|
obj = UpdateWeightsFromDistributedReqInput(
|
|
names=names,
|
|
dtypes=dtypes,
|
|
shapes=shapes,
|
|
group_name=group_name,
|
|
flush_cache=flush_cache,
|
|
)
|
|
return self.llm.run(self.tokenizer_manager.update_weights_from_distributed(obj))
|
|
|
|
def update_weights_from_tensor(
|
|
self,
|
|
named_tensors: list[tuple[str, torch.Tensor]],
|
|
load_format: str | None = None,
|
|
flush_cache: bool = True,
|
|
):
|
|
"""Update weights from distributed source. If there are going to be more updates, set `flush_cache` to be false
|
|
to avoid duplicated cache cleaning operation."""
|
|
obj = UpdateWeightsFromTensorReqInput(
|
|
serialized_named_tensors=[
|
|
MultiprocessingSerializer.serialize(named_tensors)
|
|
for _ in range(self.server_args.mapping.world_size)
|
|
],
|
|
load_format=load_format,
|
|
flush_cache=flush_cache,
|
|
)
|
|
return self.llm.run(self.tokenizer_manager.update_weights_from_tensor(obj))
|
|
|
|
def update_weights_from_disk(
|
|
self,
|
|
model_path: str,
|
|
load_format: str | None = None,
|
|
):
|
|
"""Update the weights from disk inplace without re-launching the engine.
|
|
|
|
This method allows updating the model weights from disk without restarting
|
|
the engine. It can be used to load a different model or update weights with
|
|
new training.
|
|
"""
|
|
obj = UpdateWeightFromDiskReqInput(
|
|
model_path=model_path,
|
|
load_format=load_format,
|
|
)
|
|
|
|
return self.llm.run(self.tokenizer_manager.update_weights_from_disk(obj))
|
|
|
|
def get_weights_by_name(self, name: str, truncate_size: int = 100):
|
|
"""Get weights by parameter name."""
|
|
obj = GetWeightsByNameReqInput(name=name, truncate_size=truncate_size)
|
|
return self.llm.run(self.tokenizer_manager.get_weights_by_name(obj))
|
|
|
|
def release_memory_occupation(self, tags: list[str] | None = None):
|
|
obj = ReleaseMemoryOccupationReqInput(tags=tags)
|
|
return self.llm.run(self.tokenizer_manager.release_memory_occupation(obj))
|
|
|
|
def resume_memory_occupation(self, tags: list[str] | None = None):
|
|
obj = ResumeMemoryOccupationReqInput(tags=tags)
|
|
return self.llm.run(self.tokenizer_manager.resume_memory_occupation(obj))
|
|
|
|
def is_sleeping(self) -> bool:
|
|
"""Return whether any GPU memory is currently released (data-plane sleep)."""
|
|
return self.llm.run(self.tokenizer_manager.is_sleeping())
|
|
|
|
"""
|
|
Execute an RPC call on all scheduler processes.
|
|
"""
|
|
|
|
def collective_rpc(self, method: str, **kwargs):
|
|
obj = RpcReqInput(method=method, parameters=kwargs)
|
|
self.send_to_rpc.send_pyobj(obj)
|
|
recv_req = self.send_to_rpc.recv_pyobj(zmq.BLOCKY)
|
|
if not isinstance(recv_req, RpcReqOutput):
|
|
raise TypeError(f"Expected RpcReqOutput, got {type(recv_req).__name__}.")
|
|
if not recv_req.success:
|
|
raise RuntimeError(recv_req.message)
|
|
|
|
def save_remote_model(self, **kwargs):
|
|
self.collective_rpc("save_remote_model", **kwargs)
|
|
|
|
def save_sharded_model(self, **kwargs):
|
|
self.collective_rpc("save_sharded_model", **kwargs)
|
|
|
|
|
|
def _set_envs_and_config(server_args: ServerArgs):
|
|
# Set global environments
|
|
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
|
|
os.environ["NCCL_CUMEM_ENABLE"] = str(int(server_args.enable_symm_mem))
|
|
if not server_args.enable_symm_mem:
|
|
os.environ["NCCL_NVLS_ENABLE"] = str(int(server_args.enable_nccl_nvls))
|
|
os.environ["CUDA_DEVICE_MAX_CONNECTIONS"] = "4"
|
|
os.environ["CUDA_MODULE_LOADING"] = "AUTO"
|
|
if not server_args.disable_tf32:
|
|
# Force TF32 on for cuBLAS/cuDNN matmuls. setdefault so a user's
|
|
# explicit env wins; --disable-tf32 is the documented opt-out.
|
|
os.environ.setdefault("NVIDIA_TF32_OVERRIDE", "1")
|
|
os.environ.setdefault("TORCH_ALLOW_TF32_CUBLAS_OVERRIDE", "1")
|
|
|
|
# Set prometheus env vars
|
|
if server_args.enable_metrics:
|
|
set_prometheus_multiproc_dir()
|
|
|
|
# Set ulimit
|
|
set_ulimit()
|
|
|
|
# Install a launch-phase SIGQUIT handler so a failing child tears down the
|
|
# whole local process tree instead of leaving orphaned workers behind.
|
|
# TokenizerManager may replace this handler later during steady-state
|
|
# serving.
|
|
def launch_phase_sigquit_handler(signum, frame):
|
|
logger.error(
|
|
"Received sigquit from a child process. It usually means the child failed."
|
|
)
|
|
kill_process_tree(os.getpid())
|
|
|
|
signal.signal(signal.SIGQUIT, launch_phase_sigquit_handler)
|
|
|
|
# Set mp start method
|
|
mp.set_start_method("spawn", force=True)
|
|
|
|
|
|
def _launch_subprocesses(
|
|
server_args: ServerArgs, port_args: PortArgs | None = None
|
|
) -> tuple[AsyncLLM, None, dict]:
|
|
"""
|
|
Launch the TokenizerManager in the main process, the Scheduler in a subprocess, and the DetokenizerManager in another subprocess.
|
|
"""
|
|
# Configure global environment
|
|
configure_logger(server_args)
|
|
_set_envs_and_config(server_args)
|
|
|
|
# Allocate ports for inter-process communications
|
|
if port_args is None:
|
|
port_args = PortArgs.init_new(server_args)
|
|
logger.info("server_args=%r", server_args)
|
|
|
|
# If using model from www.modelscope.cn, first download the model.
|
|
server_args.model, server_args.tokenizer = prepare_model_and_tokenizer(
|
|
server_args.model, server_args.tokenizer
|
|
)
|
|
|
|
scheduler_procs = []
|
|
if not server_args.mapping.attn.has_dp:
|
|
# Launch tensor parallel scheduler processes
|
|
memory_saver_adapter = TorchMemorySaverAdapter.create(
|
|
enable=server_args.enable_memory_saver
|
|
)
|
|
|
|
scheduler_pipe_readers = []
|
|
rank_start = server_args.mapping.nprocs_per_node * server_args.node_rank
|
|
rank_end = rank_start + server_args.mapping.nprocs_per_node
|
|
for rank in range(rank_start, rank_end):
|
|
# Create per-rank server_args with rank-initialized mapping
|
|
rank_server_args = copy.copy(server_args)
|
|
rank_server_args.mapping = copy.deepcopy(server_args.mapping)
|
|
rank_server_args.mapping.rank = rank
|
|
|
|
reader, writer = mp.Pipe(duplex=False)
|
|
|
|
proc = mp.Process(
|
|
target=run_event_loop,
|
|
args=(
|
|
rank_server_args,
|
|
port_args,
|
|
writer,
|
|
),
|
|
)
|
|
with memory_saver_adapter.configure_subprocess():
|
|
proc.start()
|
|
scheduler_procs.append(proc)
|
|
scheduler_pipe_readers.append(reader)
|
|
else:
|
|
# Launch the data parallel controller
|
|
reader, writer = mp.Pipe(duplex=False)
|
|
scheduler_pipe_readers = [reader]
|
|
proc = mp.Process(
|
|
target=run_data_parallel_controller_process,
|
|
args=(server_args, port_args, writer),
|
|
)
|
|
proc.start()
|
|
scheduler_procs.append(proc)
|
|
|
|
if server_args.node_rank >= 1:
|
|
# In multi-node cases, non-zero rank nodes do not need to run tokenizer or detokenizer,
|
|
# so they can just wait here.
|
|
|
|
for reader in scheduler_pipe_readers:
|
|
data = reader.recv()
|
|
if data.get("status") != "ready":
|
|
raise RuntimeError(
|
|
"Initialization failed. Please see the error messages above."
|
|
)
|
|
|
|
if not envs.TOKENSPEED_BLOCK_NONZERO_RANK_CHILDREN.get():
|
|
# When using `Engine` as a Python API, we don't want to block here.
|
|
return None, None, None
|
|
|
|
launch_dummy_health_check_server(
|
|
server_args.host, server_args.port, server_args.enable_metrics
|
|
)
|
|
|
|
for proc in scheduler_procs:
|
|
proc.join()
|
|
logger.error(
|
|
"Scheduler or DataParallelController %s terminated with %s",
|
|
proc.pid,
|
|
proc.exitcode,
|
|
)
|
|
return None, None, None
|
|
|
|
# Launch the main-process async frontend. The detokenizer runs
|
|
# inline inside AsyncLLM — no separate subprocess.
|
|
tokenizer_manager = AsyncLLM(server_args, port_args)
|
|
|
|
# Wait for the model to finish loading
|
|
scheduler_infos = []
|
|
for i in range(len(scheduler_pipe_readers)):
|
|
try:
|
|
data = scheduler_pipe_readers[i].recv()
|
|
except EOFError:
|
|
logger.error(
|
|
"Rank %s scheduler is dead. Please check if there are relevant logs.", i
|
|
)
|
|
scheduler_procs[i].join()
|
|
logger.error("Exit code: %s", scheduler_procs[i].exitcode)
|
|
raise
|
|
|
|
if data["status"] != "ready":
|
|
raise RuntimeError(
|
|
"Initialization failed. Please see the error messages above."
|
|
)
|
|
scheduler_infos.append(data)
|
|
|
|
# Assume all schedulers have the same scheduler_info
|
|
scheduler_info = scheduler_infos[0]
|
|
tokenizer_manager.max_req_input_len = scheduler_info["max_req_input_len"]
|
|
return tokenizer_manager, None, scheduler_info
|