Feature:Reserve HTTP server port before model loading to immediately detect port conflicts instead of failing after several minutes of model loading. (#17754)
Signed-off-by: xrwang8 <xrwang8@gmail.com>
This commit is contained in:
@@ -173,7 +173,7 @@ from sglang.srt.utils import (
|
|||||||
set_uvicorn_logging_configs,
|
set_uvicorn_logging_configs,
|
||||||
)
|
)
|
||||||
from sglang.srt.utils.auth import AuthLevel, app_has_admin_force_endpoints, auth_level
|
from sglang.srt.utils.auth import AuthLevel, app_has_admin_force_endpoints, auth_level
|
||||||
from sglang.utils import get_exception_traceback
|
from sglang.utils import _prebind_listening_socket, get_exception_traceback
|
||||||
from sglang.version import __version__
|
from sglang.version import __version__
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1975,74 +1975,80 @@ def launch_server(
|
|||||||
1. The HTTP server, Engine, and TokenizerManager all run in the main process.
|
1. The HTTP server, Engine, and TokenizerManager all run in the main process.
|
||||||
2. Inter-process communication is done through IPC (each process uses a different port) via the ZMQ library.
|
2. Inter-process communication is done through IPC (each process uses a different port) via the ZMQ library.
|
||||||
"""
|
"""
|
||||||
# Launch subprocesses
|
# Reserve the HTTP port before launching subprocesses to fail fast if port is unavailable.
|
||||||
tokenizer_manager, template_manager, scheduler_infos, port_args = (
|
# This prevents wasting time loading models only to discover port conflicts later.
|
||||||
_launch_subprocesses(
|
reserved_socket = _prebind_listening_socket(server_args.host, server_args.port)
|
||||||
server_args=server_args,
|
|
||||||
init_tokenizer_manager_func=init_tokenizer_manager_func,
|
|
||||||
run_scheduler_process_func=run_scheduler_process_func,
|
|
||||||
run_detokenizer_process_func=run_detokenizer_process_func,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse info got from the schedulers
|
|
||||||
remote_instance_transfer_engine_info = (
|
|
||||||
parse_remote_instance_transfer_engine_info_from_scheduler_infos(scheduler_infos)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set global states
|
|
||||||
set_global_state(
|
|
||||||
_GlobalState(
|
|
||||||
tokenizer_manager=tokenizer_manager,
|
|
||||||
template_manager=template_manager,
|
|
||||||
scheduler_info=scheduler_infos[0],
|
|
||||||
remote_instance_transfer_engine_info=remote_instance_transfer_engine_info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if server_args.enable_metrics:
|
|
||||||
add_prometheus_track_response_middleware(app)
|
|
||||||
|
|
||||||
# Pass additional arguments to the lifespan function.
|
|
||||||
# They will be used for additional initialization setups.
|
|
||||||
if server_args.tokenizer_worker_num == 1:
|
|
||||||
# If it is single tokenizer mode, we can pass the arguments by attributes of the app object.
|
|
||||||
app.is_single_tokenizer_mode = True
|
|
||||||
app.server_args = server_args
|
|
||||||
app.warmup_thread_kwargs = dict(
|
|
||||||
server_args=server_args,
|
|
||||||
launch_callback=launch_callback,
|
|
||||||
execute_warmup_func=execute_warmup_func,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add api key authorization
|
|
||||||
# This is only supported in single tokenizer mode.
|
|
||||||
#
|
|
||||||
# Backward compatibility:
|
|
||||||
# - api_key only: behavior matches legacy (all endpoints require api_key)
|
|
||||||
# - no keys: legacy had no restriction; ADMIN_FORCE endpoints must still be rejected when
|
|
||||||
# admin_api_key is not configured.
|
|
||||||
if (
|
|
||||||
server_args.api_key
|
|
||||||
or server_args.admin_api_key
|
|
||||||
or app_has_admin_force_endpoints(app)
|
|
||||||
):
|
|
||||||
from sglang.srt.utils.auth import add_api_key_middleware
|
|
||||||
|
|
||||||
add_api_key_middleware(
|
|
||||||
app,
|
|
||||||
api_key=server_args.api_key,
|
|
||||||
admin_api_key=server_args.admin_api_key,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# If it is multi-tokenizer mode, we need to write the arguments to shared memory
|
|
||||||
# for other worker processes to read.
|
|
||||||
app.is_single_tokenizer_mode = False
|
|
||||||
multi_tokenizer_args_shm = write_data_for_multi_tokenizer(
|
|
||||||
port_args, server_args, scheduler_infos[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Launch subprocesses
|
||||||
|
tokenizer_manager, template_manager, scheduler_infos, port_args = (
|
||||||
|
_launch_subprocesses(
|
||||||
|
server_args=server_args,
|
||||||
|
init_tokenizer_manager_func=init_tokenizer_manager_func,
|
||||||
|
run_scheduler_process_func=run_scheduler_process_func,
|
||||||
|
run_detokenizer_process_func=run_detokenizer_process_func,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse info got from the schedulers
|
||||||
|
remote_instance_transfer_engine_info = (
|
||||||
|
parse_remote_instance_transfer_engine_info_from_scheduler_infos(
|
||||||
|
scheduler_infos
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set global states
|
||||||
|
set_global_state(
|
||||||
|
_GlobalState(
|
||||||
|
tokenizer_manager=tokenizer_manager,
|
||||||
|
template_manager=template_manager,
|
||||||
|
scheduler_info=scheduler_infos[0],
|
||||||
|
remote_instance_transfer_engine_info=remote_instance_transfer_engine_info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if server_args.enable_metrics:
|
||||||
|
add_prometheus_track_response_middleware(app)
|
||||||
|
|
||||||
|
# Pass additional arguments to the lifespan function.
|
||||||
|
# They will be used for additional initialization setups.
|
||||||
|
if server_args.tokenizer_worker_num == 1:
|
||||||
|
# If it is single tokenizer mode, we can pass the arguments by attributes of the app object.
|
||||||
|
app.is_single_tokenizer_mode = True
|
||||||
|
app.server_args = server_args
|
||||||
|
app.warmup_thread_kwargs = dict(
|
||||||
|
server_args=server_args,
|
||||||
|
launch_callback=launch_callback,
|
||||||
|
execute_warmup_func=execute_warmup_func,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add api key authorization
|
||||||
|
# This is only supported in single tokenizer mode.
|
||||||
|
#
|
||||||
|
# Backward compatibility:
|
||||||
|
# - api_key only: behavior matches legacy (all endpoints require api_key)
|
||||||
|
# - no keys: legacy had no restriction; ADMIN_FORCE endpoints must still be rejected when
|
||||||
|
# admin_api_key is not configured.
|
||||||
|
if (
|
||||||
|
server_args.api_key
|
||||||
|
or server_args.admin_api_key
|
||||||
|
or app_has_admin_force_endpoints(app)
|
||||||
|
):
|
||||||
|
from sglang.srt.utils.auth import add_api_key_middleware
|
||||||
|
|
||||||
|
add_api_key_middleware(
|
||||||
|
app,
|
||||||
|
api_key=server_args.api_key,
|
||||||
|
admin_api_key=server_args.admin_api_key,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# If it is multi-tokenizer mode, we need to write the arguments to shared memory
|
||||||
|
# for other worker processes to read.
|
||||||
|
app.is_single_tokenizer_mode = False
|
||||||
|
multi_tokenizer_args_shm = write_data_for_multi_tokenizer(
|
||||||
|
port_args, server_args, scheduler_infos[0]
|
||||||
|
)
|
||||||
|
|
||||||
# Update logging configs
|
# Update logging configs
|
||||||
set_uvicorn_logging_configs(server_args)
|
set_uvicorn_logging_configs(server_args)
|
||||||
|
|
||||||
@@ -2051,8 +2057,7 @@ def launch_server(
|
|||||||
# Default case, one tokenizer process
|
# Default case, one tokenizer process
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
app,
|
app,
|
||||||
host=server_args.host,
|
fd=reserved_socket.fileno(),
|
||||||
port=server_args.port,
|
|
||||||
root_path=server_args.fastapi_root_path,
|
root_path=server_args.fastapi_root_path,
|
||||||
log_level=server_args.log_level_http or server_args.log_level,
|
log_level=server_args.log_level_http or server_args.log_level,
|
||||||
timeout_keep_alive=5,
|
timeout_keep_alive=5,
|
||||||
@@ -2071,8 +2076,7 @@ def launch_server(
|
|||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"sglang.srt.entrypoints.http_server:app",
|
"sglang.srt.entrypoints.http_server:app",
|
||||||
host=server_args.host,
|
fd=reserved_socket.fileno(),
|
||||||
port=server_args.port,
|
|
||||||
root_path=server_args.fastapi_root_path,
|
root_path=server_args.fastapi_root_path,
|
||||||
log_level=server_args.log_level_http or server_args.log_level,
|
log_level=server_args.log_level_http or server_args.log_level,
|
||||||
timeout_keep_alive=5,
|
timeout_keep_alive=5,
|
||||||
@@ -2080,6 +2084,10 @@ def launch_server(
|
|||||||
workers=server_args.tokenizer_worker_num,
|
workers=server_args.tokenizer_worker_num,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
# Close the reserved socket after uvicorn exits or on any error
|
||||||
|
# This ensures the port is released even if initialization fails
|
||||||
|
reserved_socket.close()
|
||||||
|
|
||||||
if server_args.tokenizer_worker_num > 1:
|
if server_args.tokenizer_worker_num > 1:
|
||||||
multi_tokenizer_args_shm.unlink()
|
multi_tokenizer_args_shm.unlink()
|
||||||
_global_state.tokenizer_manager.socket_mapping.clear_all_sockets()
|
_global_state.tokenizer_manager.socket_mapping.clear_all_sockets()
|
||||||
|
|||||||
@@ -409,6 +409,75 @@ def reserve_port(host, start=30000, end=40000):
|
|||||||
raise RuntimeError("No free port available.")
|
raise RuntimeError("No free port available.")
|
||||||
|
|
||||||
|
|
||||||
|
def _prebind_listening_socket(host: str, port: int) -> socket.socket:
|
||||||
|
"""
|
||||||
|
Create and bind a server socket to reserve the port before model loading.
|
||||||
|
|
||||||
|
This prevents port conflicts that could occur if another process (e.g., a
|
||||||
|
subprocess spawned during model loading) binds to the same port before the
|
||||||
|
HTTP server starts. By binding early, we ensure the port is available and
|
||||||
|
reserved for the HTTP server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: The host address to bind to.
|
||||||
|
port: The port number to bind to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A bound socket object.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the port is already in use or permission is denied.
|
||||||
|
"""
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from sglang.srt.utils.common import is_valid_ipv6_address
|
||||||
|
|
||||||
|
# Determine socket family based on address type
|
||||||
|
if host and is_valid_ipv6_address(host):
|
||||||
|
family = socket.AF_INET6
|
||||||
|
else:
|
||||||
|
family = socket.AF_INET
|
||||||
|
|
||||||
|
sock = socket.socket(family=family, type=socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
# Set socket options to allow port reuse
|
||||||
|
# SO_REUSEADDR: Allows binding to a port in TIME_WAIT state
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
# SO_REUSEPORT: Allows multiple processes to bind to the same port
|
||||||
|
# This is useful for multi-worker mode
|
||||||
|
if hasattr(socket, "SO_REUSEPORT"):
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.bind((host or "", port))
|
||||||
|
sock.listen(128) # Put socket in listening state for uvicorn
|
||||||
|
sock.set_inheritable(True)
|
||||||
|
logger.info(
|
||||||
|
f"Successfully reserved port {port} on host '{host or ('::' if family == socket.AF_INET6 else '0.0.0.0')}'"
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
sock.close()
|
||||||
|
if e.errno == errno.EADDRINUSE:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Port {port} is already in use on host '{host or ('::' if family == socket.AF_INET6 else '0.0.0.0')}'. "
|
||||||
|
f"Please choose a different port with --port or stop the process using this port. "
|
||||||
|
f"You can find the process using: lsof -i :{port} or netstat -tlnp | grep {port}"
|
||||||
|
) from e
|
||||||
|
elif e.errno == errno.EACCES:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Permission denied when trying to bind to port {port}. "
|
||||||
|
f"Ports below 1024 require root/sudo privileges. "
|
||||||
|
f"Consider using a port >= 1024 (e.g., --port 8000) or run with sudo."
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to bind to {host or ('::' if family == socket.AF_INET6 else '0.0.0.0')}:{port}: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
def release_port(lock_socket):
|
def release_port(lock_socket):
|
||||||
"""
|
"""
|
||||||
Release the reserved port by closing the lock socket.
|
Release the reserved port by closing the lock socket.
|
||||||
|
|||||||
Reference in New Issue
Block a user