feat: add native gRPC sidecar module launcher (#31076)

Signed-off-by: Ishan Dhanani <ishandhanani@gmail.com>
Signed-off-by: Connor Carpenter <connorc@nvidia.com>
Co-authored-by: Connor Carpenter <connorc@nvidia.com>
This commit is contained in:
ishandhanani
2026-07-22 06:39:08 -07:00
committed by GitHub
co-authored by Connor Carpenter
parent 74338e94f1
commit 21065bc862
4 changed files with 309 additions and 0 deletions
@@ -265,6 +265,7 @@ async def init_multi_tokenizer() -> ServerArgs:
@asynccontextmanager
async def lifespan(fast_api_app: FastAPI):
grpc_handle = None
sidecar = None
warmup_thread = None
if getattr(fast_api_app, "is_single_tokenizer_mode", False):
server_args = fast_api_app.server_args
@@ -397,6 +398,10 @@ async def lifespan(fast_api_app: FastAPI):
template_manager=_global_state.template_manager,
scheduler_info=_global_state.scheduler_info,
)
if server_args.sidecar is not None:
from sglang.srt.entrypoints.sidecar import start_sidecar
sidecar = start_sidecar(server_args)
# Execute the general warmup
warmup_thread = threading.Thread(
@@ -408,6 +413,11 @@ async def lifespan(fast_api_app: FastAPI):
# Start the HTTP server
yield
finally:
if sidecar is not None:
try:
sidecar.stop()
except Exception:
logger.exception("Failed to stop sidecar")
_shutdown_native_grpc_server(grpc_handle)
if tool_server is not None and hasattr(tool_server, "aclose"):
await tool_server.aclose()
+130
View File
@@ -0,0 +1,130 @@
# Copyright 2023-2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Lifecycle management for an optional local native gRPC sidecar."""
import argparse
import importlib
import logging
import multiprocessing as mp
import os
from sglang.srt.utils.common import kill_itself_when_parent_died, kill_process_tree
from sglang.srt.utils.network import NetworkAddress
from sglang.srt.utils.watchdog import SubprocessWatchdog
logger = logging.getLogger(__name__)
SGLANG_GRPC_ENDPOINT_ENV = "SGLANG_GRPC_ENDPOINT"
_DEFAULT_SIDECAR_SHUTDOWN_TIMEOUT = 45.0
def _loopback_host(host: str) -> str:
if not host or host == "0.0.0.0":
return "127.0.0.1"
if host in ("::", "[::]"):
return "::1"
return host
def build_sidecar_endpoint(server_args) -> str:
return NetworkAddress(
_loopback_host(server_args.host), server_args.grpc_port
).to_url()
def _parse_sidecar_args(args: list[str] | None) -> tuple[list[str], float]:
parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
parser.add_argument(
"--sidecar-shutdown-timeout",
type=float,
default=_DEFAULT_SIDECAR_SHUTDOWN_TIMEOUT,
)
parsed, provider_args = parser.parse_known_args(args or [])
if parsed.sidecar_shutdown_timeout <= 0:
raise ValueError("--sidecar-shutdown-timeout must be greater than 0.")
return provider_args, parsed.sidecar_shutdown_timeout
def _run_sidecar(module_name: str, args: list[str], endpoint: str) -> None:
kill_itself_when_parent_died()
os.environ[SGLANG_GRPC_ENDPOINT_ENV] = endpoint
try:
main = getattr(importlib.import_module(module_name), "main")
except (AttributeError, ImportError) as e:
raise RuntimeError(
f"--sidecar requires importable module {module_name!r} "
"with a main(argv) function."
) from e
if not callable(main):
raise RuntimeError(
f"--sidecar requires module {module_name!r} to expose "
"a callable main(argv)."
)
main(args)
class Sidecar:
def __init__(
self,
proc,
module_name: str,
shutdown_timeout: float,
):
self.proc = proc
self.module_name = module_name
self.shutdown_timeout = shutdown_timeout
self._watchdog = SubprocessWatchdog(
processes=[proc], process_names=[module_name]
)
def start(self) -> None:
self.proc.start()
self._watchdog.start()
logger.info(
"Sidecar module %s started pid=%s",
self.module_name,
self.proc.pid,
)
def stop(self) -> None:
self._watchdog.stop()
if self.proc.is_alive():
self.proc.terminate()
self.proc.join(timeout=self.shutdown_timeout)
else:
self.proc.join(timeout=0)
if self.proc.is_alive():
logger.warning("Sidecar module did not terminate; killing process tree")
kill_process_tree(self.proc.pid, wait_timeout=self.shutdown_timeout)
def start_sidecar(server_args) -> Sidecar:
module_name = server_args.sidecar
assert module_name is not None
sidecar_args, shutdown_timeout = _parse_sidecar_args(server_args.sidecar_args)
endpoint = build_sidecar_endpoint(server_args)
proc = mp.get_context("spawn").Process(
name=f"sglang_sidecar_{module_name}",
target=_run_sidecar,
args=(module_name, sidecar_args, endpoint),
)
sidecar = Sidecar(
proc,
module_name,
shutdown_timeout=shutdown_timeout,
)
sidecar.start()
return sidecar
+35
View File
@@ -1181,6 +1181,24 @@ class ServerArgs:
"defaults to --port + 10000.",
NS("serving"),
] = None
sidecar: A[
Optional[str],
"Start a locally managed sidecar against the native gRPC server. "
"The selected module must expose main(argv) and read the resolved "
"native gRPC endpoint from SGLANG_GRPC_ENDPOINT. Requires --grpc-port "
"or SGLANG_GRPC_PORT.",
NS("serving"),
] = None
sidecar_args: A[
Optional[List[str]],
Arg(
help="JSON array passed to the selected sidecar module's "
"main(argv) function. --sidecar-shutdown-timeout SECONDS is "
"consumed by SGLang.",
type_parser=json_list_type,
),
NS("serving"),
] = None
skip_server_warmup: A[bool, "If set, skip warmup.", NS("serving")] = False
warmups: A[
Optional[str],
@@ -3730,6 +3748,23 @@ class ServerArgs:
# Native gRPC is incompatible with launch paths it doesn't wire into.
# Legacy takes precedence over grpc_port, keeping re-runs idempotent.
native_grpc = self.grpc_port is not None and not legacy_grpc
if self.sidecar_args is not None:
if self.sidecar is None:
raise ValueError("--sidecar-args requires --sidecar.")
if not isinstance(self.sidecar_args, list) or not all(
isinstance(arg, str) for arg in self.sidecar_args
):
raise ValueError("--sidecar-args must be a JSON array of strings.")
if self.sidecar is not None:
if not self.sidecar.strip():
raise ValueError("--sidecar must not be empty.")
if legacy_grpc:
raise ValueError(
"--sidecar requires SGLang's native gRPC server; "
"it cannot be combined with --smg-grpc-mode/--grpc-mode."
)
if self.grpc_port is None:
raise ValueError("--sidecar requires --grpc-port or SGLANG_GRPC_PORT.")
if native_grpc:
if self.use_ray:
raise ValueError(