1616import threading
1717from typing import List , Optional , Union
1818
19- from ..llmapi .mpi_session import MpiPoolSession , MpiSession
19+ from ..llmapi .mpi_session import (MpiPoolSession , MpiSession ,
20+ validate_session_world_size )
2021from ..llmapi .utils import logger_debug , print_colored
2122from ..logger import logger
2223from .executor import GenerationExecutor
@@ -42,13 +43,12 @@ def __init__(
4243 postproc_worker_config : Optional [PostprocWorkerConfig ] = None ,
4344 is_llm_executor : Optional [bool ] = None ,
4445 ):
45- """
46- Args:
47- worker_kwargs: kwargs for the rpc worker
48- model_world_size: the world size of the model
49- mpi_session: the mpi session to use
50- postproc_worker_config: the postproc worker config
51- is_llm_executor: whether this is an llm executor
46+ """Args:
47+ worker_kwargs: kwargs for the rpc worker
48+ model_world_size: the world size of the model
49+ mpi_session: the mpi session to use
50+ postproc_worker_config: the postproc worker config
51+ is_llm_executor: whether this is an llm executor
5252 """
5353 GenerationExecutorRpcProxy .INSTANCE_COUNTER += 1
5454 self .init_rpc_executor ()
@@ -81,11 +81,13 @@ def __init__(
8181 self ._setup_mainloop_with_tasks ()
8282
8383 def launch_workers (self ):
84- logger .debug (f "Launching workers" )
84+ logger .debug ("Launching workers" )
8585 assert self .mpi_session is not None
86- self .mpi_session .submit (RpcWorker .main_task ,
87- rpc_addr = self .rpc_addr ,
88- ** self .worker_kwargs )
86+ # Keep the futures: on an externally owned (shared) session, shutdown
87+ # waits on them so worker teardown finishes before the pool is reused.
88+ self .worker_futures = self .mpi_session .submit (RpcWorker .main_task ,
89+ rpc_addr = self .rpc_addr ,
90+ ** self .worker_kwargs )
8991
9092 def _setup_mainloop_with_tasks (self ):
9193 """Setup mainloop with tasks needed for RpcProxy.
@@ -256,7 +258,7 @@ def setup_engine_remote(self):
256258 return self .rpc_client .setup_engine ().remote (need_response = True )
257259
258260 def shutdown_remote (self ):
259- logger_debug (f "Shutting down rpc remote" , color = "yellow" )
261+ logger_debug ("Shutting down rpc remote" , color = "yellow" )
260262 self .rpc_client .shutdown ().remote (need_response = False )
261263
262264 def abort_request (self , request_id : int ) -> None :
@@ -266,8 +268,7 @@ def shutdown(self):
266268 if self ._shutdown_event .is_set ():
267269 return
268270 self ._shutdown_event .set ()
269- logger_debug (f"Shutting down GenerationExecutorRpcProxy" ,
270- color = "yellow" )
271+ logger_debug ("Shutting down GenerationExecutorRpcProxy" , color = "yellow" )
271272
272273 # 1. shutdown the rpc server (PyExecutor Rank 0 + RPC server)
273274 self .shutdown_remote ()
@@ -294,9 +295,24 @@ def shutdown(self):
294295 # 3. shutdown the mpi session, this should wait until all the PyExecutor
295296 # processes are shutdown
296297 if self .mpi_session is not None :
297- logger_debug (f"Shutting down mpi session" , color = "yellow" )
298- self .mpi_session .shutdown ()
299- logger_debug (f"Mpi session shutdown" , color = "yellow" )
298+ if self ._owns_mpi_session :
299+ logger_debug ("Shutting down mpi session" , color = "yellow" )
300+ self .mpi_session .shutdown ()
301+ else :
302+ # Externally owned (shared) session: leave the pool alive, but
303+ # wait for this executor's worker tasks to finish so the next
304+ # LLM on the pool doesn't race with PyExecutor teardown. Block
305+ # without a timeout, mirroring the owned path above
306+ # (mpi_session.shutdown() also waits indefinitely); a timeout
307+ # that expires would just reintroduce the race it prevents.
308+ for future in getattr (self , "worker_futures" , []):
309+ try :
310+ future .result ()
311+ except Exception as e :
312+ logger .warning (
313+ f"RPC worker task raised during shutdown on "
314+ f"shared MPI session: { e } " )
315+ logger_debug ("Mpi session shutdown" , color = "yellow" )
300316 self .mpi_session = None
301317
302318 self .rpc_client .close ()
@@ -309,14 +325,20 @@ def __exit__(self, exc_type, exc_value, traceback):
309325
310326 def _create_mpi_session (self , model_world_size : int ,
311327 mpi_session : Optional [MpiSession ]):
328+ # Ownership is decided here, next to the create-vs-adopt branch: a
329+ # session this proxy created is shut down by it; an external one is
330+ # owned (and shut down) by the caller.
312331 mpi_process_pre_spawned : bool = get_spawn_proxy_process_env ()
313332 if mpi_session is None :
333+ self ._owns_mpi_session = True
314334 if mpi_process_pre_spawned :
315335 logger_debug ('[proxy] create comm session ...\n ' , "yellow" )
316336 self .mpi_session = create_mpi_comm_session (model_world_size )
317337 else :
318338 logger_debug ('[proxy] create pool session ...\n ' , "yellow" )
319339 self .mpi_session = MpiPoolSession (n_workers = model_world_size )
320340 else :
341+ validate_session_world_size (mpi_session , model_world_size )
342+ self ._owns_mpi_session = False
321343 logger_debug ('[proxy] using external mpi session ...\n ' , "yellow" )
322344 self .mpi_session = mpi_session
0 commit comments