From f5a4a5429fd8a88a76ab321ac8e08f081cbef766 Mon Sep 17 00:00:00 2001 From: Kangyan-Zhou Date: Thu, 12 Mar 2026 16:17:33 -0700 Subject: [PATCH] Revert early HTTP port reservation (#17754, #19805) (#20468) Co-authored-by: Claude Opus 4.6 --- python/sglang/srt/entrypoints/http_server.py | 138 +++++++++---------- python/sglang/utils.py | 55 -------- 2 files changed, 63 insertions(+), 130 deletions(-) diff --git a/python/sglang/srt/entrypoints/http_server.py b/python/sglang/srt/entrypoints/http_server.py index d57108c41..853b402e0 100644 --- a/python/sglang/srt/entrypoints/http_server.py +++ b/python/sglang/srt/entrypoints/http_server.py @@ -173,7 +173,7 @@ from sglang.srt.utils import ( set_uvicorn_logging_configs, ) from sglang.srt.utils.auth import AuthLevel, app_has_admin_force_endpoints, auth_level -from sglang.utils import _prebind_listening_socket, get_exception_traceback +from sglang.utils import get_exception_traceback from sglang.version import __version__ logger = logging.getLogger(__name__) @@ -1971,78 +1971,67 @@ def _setup_and_run_http_server( Called by launch_server after subprocesses have been launched. """ - # Reserve the HTTP port before launching subprocesses to fail fast if port is unavailable. - # This prevents wasting time loading models only to discover port conflicts later. - reserved_socket = _prebind_listening_socket(server_args.host, server_args.port) - multi_tokenizer_args_shm = None + # 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: - # 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 set_uvicorn_logging_configs(server_args) - # Delay listen() until uvicorn startup to avoid accepting probe traffic - # while model/subprocess initialization is still in progress. - reserved_socket.listen(128) - if server_args.ssl_certfile: logger.info( f"SSL enabled: certfile={server_args.ssl_certfile}, " @@ -2055,7 +2044,8 @@ def _setup_and_run_http_server( # Use Config/Server API for access to the SSLContext. config = uvicorn.Config( app, - fd=reserved_socket.fileno(), + host=server_args.host, + port=server_args.port, root_path=server_args.fastapi_root_path, log_level=server_args.log_level_http or server_args.log_level, timeout_keep_alive=envs.SGLANG_TIMEOUT_KEEP_ALIVE.get(), @@ -2091,7 +2081,8 @@ def _setup_and_run_http_server( # Default case, one tokenizer process uvicorn.run( app, - fd=reserved_socket.fileno(), + host=server_args.host, + port=server_args.port, root_path=server_args.fastapi_root_path, log_level=server_args.log_level_http or server_args.log_level, timeout_keep_alive=envs.SGLANG_TIMEOUT_KEEP_ALIVE.get(), @@ -2121,7 +2112,8 @@ def _setup_and_run_http_server( uvicorn.run( "sglang.srt.entrypoints.http_server:app", - fd=reserved_socket.fileno(), + host=server_args.host, + port=server_args.port, root_path=server_args.fastapi_root_path, log_level=server_args.log_level_http or server_args.log_level, timeout_keep_alive=envs.SGLANG_TIMEOUT_KEEP_ALIVE.get(), @@ -2133,10 +2125,6 @@ def _setup_and_run_http_server( ssl_keyfile_password=server_args.ssl_keyfile_password, ) 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 multi_tokenizer_args_shm is not None: multi_tokenizer_args_shm.unlink() diff --git a/python/sglang/utils.py b/python/sglang/utils.py index 7dde4d519..751d1af33 100644 --- a/python/sglang/utils.py +++ b/python/sglang/utils.py @@ -409,61 +409,6 @@ def reserve_port(host, start=30000, end=40000): 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. The socket is intentionally not put into - listening mode here to avoid accepting probe connections before uvicorn is - ready to serve requests. - - 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.network import NetworkAddress - - # Determine socket family based on address type - na = NetworkAddress(host or "0.0.0.0", port) - sock = socket.socket(family=na.family, type=socket.SOCK_STREAM) - # SO_REUSEADDR: Allows binding to a port in TIME_WAIT state - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - try: - sock.bind(na.to_bind_tuple()) - sock.set_inheritable(True) - logger.info(f"Successfully reserved port {port} on host '{na.host}'") - except OSError as e: - sock.close() - if e.errno == errno.EADDRINUSE: - raise RuntimeError( - f"Port {port} is already in use on host '{na.host}'. " - 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 {na}: {e}") from e - - return sock - - def release_port(lock_socket): """ Release the reserved port by closing the lock socket.