[rust-server] PD disaggregation support (#33125)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Rain Jiang <rain-jiang@outlook.com>
This commit is contained in:
co-authored by
Claude Fable 5
Cursor
Rain Jiang
parent
c844244da5
commit
5d2dbb35a6
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -101,6 +102,9 @@ def handle_pd_disaggregation(server_args: ServerArgs) -> None:
|
||||
server_args.disaggregation_transfer_backend != "fake"
|
||||
), "Prefill server does not support 'fake' as the transfer backend"
|
||||
|
||||
if envs.SGLANG_RUST_SERVER.get():
|
||||
_alias_bootstrap_port_to_api_port(server_args)
|
||||
|
||||
if server_args.disaggregation_mode in ("prefill", "decode"):
|
||||
if (
|
||||
envs.SGLANG_DISAGG_STAGING_BUFFER.get()
|
||||
@@ -111,3 +115,36 @@ def handle_pd_disaggregation(server_args: ServerArgs) -> None:
|
||||
f"disaggregation_transfer_backend='mooncake' or 'nixl', "
|
||||
f"got '{server_args.disaggregation_transfer_backend}'."
|
||||
)
|
||||
|
||||
|
||||
def _alias_bootstrap_port_to_api_port(server_args: ServerArgs) -> None:
|
||||
"""Rust-server prefill serves the KV bootstrap registry on the api listener
|
||||
itself, so the resolved bootstrap port must BE the api port — every internal
|
||||
consumer (KVManager registration, PrefillBootstrapQueue) reads the resolved
|
||||
field and agrees automatically. Decode is untouched: there the field names
|
||||
the PREFILL side's bootstrap port and must stay as the operator set it.
|
||||
"""
|
||||
default_port = next(
|
||||
f.default
|
||||
for f in dataclasses.fields(server_args)
|
||||
if f.name == "disaggregation_bootstrap_port"
|
||||
)
|
||||
if server_args.disaggregation_bootstrap_port not in (
|
||||
default_port,
|
||||
server_args.port,
|
||||
):
|
||||
raise ValueError(
|
||||
"SGLANG_RUST_SERVER serves the PD KV bootstrap registry on the api "
|
||||
"port itself; --disaggregation-bootstrap-port "
|
||||
f"{server_args.disaggregation_bootstrap_port} conflicts with --port "
|
||||
f"{server_args.port}. Drop --disaggregation-bootstrap-port (decode "
|
||||
"nodes and the PD router must then target the prefill api port)."
|
||||
)
|
||||
if server_args.disaggregation_bootstrap_port != server_args.port:
|
||||
logger.info(
|
||||
"SGLANG_RUST_SERVER: KV bootstrap registry is served on the api "
|
||||
"port; disaggregation_bootstrap_port %d -> %d",
|
||||
server_args.disaggregation_bootstrap_port,
|
||||
server_args.port,
|
||||
)
|
||||
server_args.disaggregation_bootstrap_port = server_args.port
|
||||
|
||||
@@ -2251,10 +2251,6 @@ def _execute_server_warmup(server_args: ServerArgs):
|
||||
_global_state.tokenizer_manager.server_status = ServerStatus.Up
|
||||
|
||||
else:
|
||||
# TODO: @rainj-me fix this when Rust server supports disaggregation
|
||||
assert (
|
||||
not envs.SGLANG_RUST_SERVER.get()
|
||||
), "Rust server is not supported for disaggregation warmup for now"
|
||||
logger.info(f"Start of pd disaggregation warmup ...")
|
||||
status_codes = asyncio.run(
|
||||
_send_disaggregation_warmup_requests(
|
||||
@@ -2272,14 +2268,20 @@ def _execute_server_warmup(server_args: ServerArgs):
|
||||
server_args.dp_size,
|
||||
)
|
||||
logger.info("End of disaggregation warmup")
|
||||
_global_state.tokenizer_manager.server_status = ServerStatus.Up
|
||||
else:
|
||||
logger.info(
|
||||
"Disaggregation warmup failed (mode=%s), status codes: %s",
|
||||
server_args.disaggregation_mode,
|
||||
failed_status_codes,
|
||||
)
|
||||
_global_state.tokenizer_manager.server_status = ServerStatus.UnHealthy
|
||||
# In rust-server mode there is no TokenizerManager (readiness is
|
||||
# the Rust server's own /health), so skip the status update.
|
||||
if not envs.SGLANG_RUST_SERVER.get():
|
||||
_global_state.tokenizer_manager.server_status = (
|
||||
ServerStatus.Up
|
||||
if not failed_status_codes
|
||||
else ServerStatus.UnHealthy
|
||||
)
|
||||
|
||||
except Exception:
|
||||
last_traceback = get_exception_traceback()
|
||||
|
||||
@@ -27,18 +27,30 @@ def start_disagg_service(
|
||||
host=server_args.host,
|
||||
port=server_args.disaggregation_bootstrap_port,
|
||||
)
|
||||
is_create_store = (
|
||||
server_args.node_rank == 0 and transfer_backend == TransferBackend.ASCEND
|
||||
maybe_create_ascend_config_store(
|
||||
server_args=server_args, transfer_backend=transfer_backend
|
||||
)
|
||||
if is_create_store:
|
||||
try:
|
||||
from memfabric_hybrid import create_config_store
|
||||
|
||||
ascend_url = os.getenv("ASCEND_MF_STORE_URL")
|
||||
create_config_store(ascend_url)
|
||||
except Exception as e:
|
||||
error_message = f"Failed create mf store, invalid ascend_url."
|
||||
error_message += f" With exception {e}"
|
||||
raise error_message
|
||||
|
||||
return bootstrap_server
|
||||
|
||||
|
||||
def maybe_create_ascend_config_store(
|
||||
server_args: ServerArgs, transfer_backend: TransferBackend
|
||||
) -> None:
|
||||
"""Also called directly by the rust-server scheduler: there the KV
|
||||
bootstrap registry is served by the embedded rust server's api listener
|
||||
(one rust implementation covers every transfer backend — their
|
||||
bootstrap-server subclasses are all plain ``CommonKVBootstrapServer``,
|
||||
which the rust registry ports verbatim), leaving this store as the only
|
||||
``start_disagg_service`` duty left to perform."""
|
||||
if not (server_args.node_rank == 0 and transfer_backend == TransferBackend.ASCEND):
|
||||
return
|
||||
try:
|
||||
from memfabric_hybrid import create_config_store
|
||||
|
||||
ascend_url = os.getenv("ASCEND_MF_STORE_URL")
|
||||
create_config_store(ascend_url)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"Failed create mf store, invalid ascend_url. With exception {e}"
|
||||
)
|
||||
|
||||
@@ -99,6 +99,7 @@ from sglang.srt.layers.quantization.fp8_utils import initialize_fp8_gemm_config
|
||||
from sglang.srt.layers.quantization.unquant import initialize_bf16_gemm_config
|
||||
from sglang.srt.lora.lora_drainer import LoRADrainer
|
||||
from sglang.srt.lora.lora_overlap_loader import LoRAOverlapLoader
|
||||
from sglang.srt.managers.disagg_service import maybe_create_ascend_config_store
|
||||
from sglang.srt.managers.hisparse_coordinator import HiSparseCoordinator
|
||||
from sglang.srt.managers.io_struct import (
|
||||
AbortReq,
|
||||
@@ -590,6 +591,15 @@ class Scheduler(
|
||||
# Init profiler
|
||||
self.init_profiler()
|
||||
|
||||
# Start the embedded Rust frontend (rank 0). Must precede
|
||||
# init_disaggregation: on PD prefill the rust api listener also serves
|
||||
# the KV bootstrap registry, and the KVManagers built there register to
|
||||
# it synchronously. (The listener is bound synchronously inside launch,
|
||||
# so the registry is accepting once this returns.) Must also precede
|
||||
# the request receiver, which reads self.recv_from_tokenizer to pick
|
||||
# its ingress transport.
|
||||
self.maybe_init_rust_server()
|
||||
|
||||
# Init prefill-decodedisaggregation
|
||||
self.init_disaggregation()
|
||||
|
||||
@@ -618,10 +628,6 @@ class Scheduler(
|
||||
|
||||
self.maybe_init_scripted_scheduler_hook()
|
||||
|
||||
# Start the embedded Rust frontend (rank 0) before the request receiver,
|
||||
# which reads self.rust_ring_recv to pick its ingress transport.
|
||||
self.maybe_init_rust_server()
|
||||
|
||||
self.init_request_receiver()
|
||||
|
||||
self.init_dp_attn_adapter()
|
||||
@@ -1214,6 +1220,21 @@ class Scheduler(
|
||||
get_disagg().disaggregation_transfer_backend
|
||||
)
|
||||
|
||||
# In rust-server mode the KV bootstrap registry is already serving on
|
||||
# the rust api listener (maybe_init_rust_server runs before this
|
||||
# method — the PrefillBootstrapQueue's KVManager below registers to it
|
||||
# synchronously, and a failed registration only retries ~60s then logs,
|
||||
# leaving every PD request unroutable). Only the ascend config store,
|
||||
# which start_disagg_service would otherwise create, is left to do.
|
||||
if (
|
||||
self.disaggregation_mode == DisaggregationMode.PREFILL
|
||||
and self._hosts_rust_server()
|
||||
):
|
||||
maybe_create_ascend_config_store(
|
||||
server_args=self.server_args,
|
||||
transfer_backend=self.transfer_backend,
|
||||
)
|
||||
|
||||
# todo: should we fix this when enabling mtp or it doesn't matter since we only enable mtp in decode node thus we don't transfer draft kvs between P and D?
|
||||
draft_token_to_kv_pool = kv_cache_builder.get_draft_kv_pool(
|
||||
draft_worker=self.draft_worker,
|
||||
@@ -1844,17 +1865,22 @@ class Scheduler(
|
||||
else:
|
||||
self.scripted_scheduler_hook = None
|
||||
|
||||
def _hosts_rust_server(self) -> bool:
|
||||
"""Whether this scheduler rank embeds the Rust server (rank 0 only) —
|
||||
and with it the server-process duties a Python ``TokenizerManager``
|
||||
would otherwise own (e.g. serving the PD KV bootstrap registry)."""
|
||||
return envs.SGLANG_RUST_SERVER.get() and (
|
||||
self.ps.pp_rank == 0
|
||||
and self.ps.attn_tp_rank == 0
|
||||
and self.ps.attn_cp_rank == 0
|
||||
)
|
||||
|
||||
def maybe_init_rust_server(self) -> None:
|
||||
"""Start the embedded Rust server (rank 0) if ``SGLANG_RUST_SERVER`` is
|
||||
set, and point the ingress receiver at it. All the plumbing lives in
|
||||
``RustServer`` (scheduler_components/rust_scheduler.py)."""
|
||||
|
||||
is_rank_zero = (
|
||||
self.ps.pp_rank == 0
|
||||
and self.ps.attn_tp_rank == 0
|
||||
and self.ps.attn_cp_rank == 0
|
||||
)
|
||||
if not (envs.SGLANG_RUST_SERVER.get() and is_rank_zero):
|
||||
if not self._hosts_rust_server():
|
||||
# Always define the attribute: init_output_streamer and the
|
||||
# process_input_requests hook read self.rust_server unconditionally.
|
||||
self.rust_server = None
|
||||
@@ -2303,9 +2329,10 @@ class Scheduler(
|
||||
f"bootstrap room id. {req.rid=}"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
recv_req.time_stats.trace_ctx.abort(
|
||||
abort_info={"reason": error_msg}
|
||||
)
|
||||
if not envs.SGLANG_RUST_SERVER.get():
|
||||
recv_req.time_stats.trace_ctx.abort(
|
||||
abort_info={"reason": error_msg}
|
||||
)
|
||||
prepare_abort(req, error_msg, status_code=HTTPStatus.BAD_REQUEST)
|
||||
self.output_streamer.stream_output([req], req.return_logprob)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user