3030 worker_status_record_to_proto ,
3131)
3232
33+ # Maximum parallel threads for forwarding restart requests
34+ MAX_RESTART_FORWARD_THREADS = 100
35+
36+ # Maximum concurrent async termination tasks per worker
37+ MAX_TERMINATION_EXECUTOR_THREADS = 1 # Each worker has only 1 training process
38+
3339
3440class ManagerServer (manager_pb2_grpc .ManagerServiceServicer ):
3541 """FT trainer manager server.
@@ -58,6 +64,12 @@ def __init__(
5864 self .process_controller = process_controller
5965 self ._registry_lock = threading .Lock ()
6066
67+ # Thread pool for async termination tasks (bounded to prevent resource exhaustion)
68+ self ._termination_executor = futures .ThreadPoolExecutor (
69+ max_workers = MAX_TERMINATION_EXECUTOR_THREADS ,
70+ thread_name_prefix = "termination" ,
71+ )
72+
6173 # Initialize registries based on role
6274 if self ._worker_identity .is_replica_manager :
6375 self ._status_registry : Dict [str , WorkerStatusRecord ] = {}
@@ -136,11 +148,15 @@ def start_server(self):
136148 return self .server
137149
138150 def stop_server (self , grace_period : int = 10 ):
139- """Stop the gRPC server."""
151+ """Stop the gRPC server and clean up resources ."""
140152 if self .server :
141153 self .server .stop (grace_period )
142154 logging .info ("Manager server stopped" )
143155
156+ # Shutdown termination executor (wait for pending tasks to complete)
157+ self ._termination_executor .shutdown (wait = True )
158+ logging .info ("Termination executor stopped" )
159+
144160 def ReportStatus (
145161 self , request : manager_pb2 .StatusUpdate , context
146162 ) -> manager_pb2 .StatusUpdateResponse :
@@ -296,6 +312,26 @@ def RestartReplicaTraining(
296312 )
297313 raise
298314
315+ def _terminate_training_async (self , hostname : str , reason : str , is_shutdown : bool ) -> None :
316+ """Terminate training process asynchronously in background thread.
317+
318+ Args:
319+ hostname: Worker hostname for logging
320+ reason: Reason for termination
321+ is_shutdown: Whether this is a shutdown (vs restart) request
322+ """
323+ try :
324+ success = self .process_controller .terminate_training (reason )
325+ if success :
326+ action = "shutdown" if is_shutdown else "restart"
327+ logging .info ("Worker %s training %s completed" , hostname , action )
328+ if is_shutdown :
329+ logging .warning ("Immediate shutdown requested, exiting after termination" )
330+ else :
331+ logging .warning ("Worker %s has no active training process" , hostname )
332+ except Exception as e : # pylint: disable=broad-exception-caught
333+ logging .error ("Async termination failed: %s" , e )
334+
299335 def RestartTraining (
300336 self , request : manager_pb2 .RestartRequest , context
301337 ) -> manager_pb2 .RestartResponse :
@@ -316,32 +352,25 @@ def RestartTraining(
316352 # Check if this is a shutdown request (not a restart)
317353 is_shutdown = request .reason .startswith ("IMMEDIATE_SHUTDOWN:" )
318354
319- # Attempt to terminate the trainer process
320- success = False
321- message = ""
322-
323- if self .process_controller :
324- success = self .process_controller .terminate_training (request .reason )
325- if success :
326- action = "shutdown" if is_shutdown else "restart"
327- message = f"Worker { worker_identity .hostname } training { action } initiated"
328- logging .info (message )
329-
330- # For shutdown requests, exit immediately (don't restart)
331- if is_shutdown :
332- logging .warning ("Immediate shutdown requested, exiting after termination" )
333- # The FT agent should not restart after this termination
334-
335- else :
336- message = f"Worker { worker_identity .hostname } has no active training process"
337- logging .warning (message )
338- else :
355+ if not self .process_controller :
339356 message = f"Worker { worker_identity .hostname } has no process controller"
340357 logging .error (message )
341- success = False
358+ return self ._create_success_response (
359+ manager_pb2 .RestartResponse , acknowledged = False , message = message
360+ )
361+
362+ # Submit termination to thread pool - return immediately
363+ self ._termination_executor .submit (
364+ self ._terminate_training_async ,
365+ worker_identity .hostname ,
366+ request .reason ,
367+ is_shutdown ,
368+ )
342369
370+ action = "shutdown" if is_shutdown else "restart"
371+ message = f"Worker { worker_identity .hostname } training { action } initiated (async)"
343372 return self ._create_success_response (
344- manager_pb2 .RestartResponse , acknowledged = success , message = message
373+ manager_pb2 .RestartResponse , acknowledged = True , message = message
345374 )
346375
347376 except grpc .RpcError :
@@ -435,29 +464,42 @@ def _forward_restart_to_workers(self, reason: str) -> Dict[str, bool]:
435464 logging .error ("Only replica managers can forward restart requests" )
436465 return {}
437466
438- results = {}
439467 worker_hostnames = get_all_worker_hostnames ()
440468
441- with ManagerClient () as client :
442- for hostname in worker_hostnames :
443- try :
444- logging .info ("Forwarding restart to worker: %s" , hostname )
469+ def restart_single_worker (hostname : str , client : ManagerClient ) -> tuple :
470+ """Send restart request to a single worker."""
471+ try :
472+ logging .info ("Forwarding restart to worker: %s" , hostname )
473+ worker_id = extract_worker_id_from_hostname (hostname )
445474
446- worker_id = extract_worker_id_from_hostname (hostname )
475+ success = client .restart_worker (
476+ hostname , self ._worker_identity .replica_id , reason , worker_id
477+ )
447478
448- success = client .restart_worker (
449- hostname , self ._worker_identity .replica_id , reason , worker_id
450- )
451- results [hostname ] = success
479+ if success :
480+ logging .info ("Worker %s acknowledged restart request" , hostname )
481+ else :
482+ logging .warning ("Worker %s failed to acknowledge restart request" , hostname )
483+
484+ return (hostname , success )
452485
453- if success :
454- logging .info ("Worker %s acknowledged restart request" , hostname )
455- else :
456- logging .warning ("Worker %s failed to acknowledge restart request" , hostname )
486+ except Exception as e : # pylint: disable=broad-exception-caught
487+ logging .error ("Failed to forward restart to worker %s: %s" , hostname , e )
488+ return (hostname , False )
457489
458- except Exception as e : # pylint: disable=broad-exception-caught
459- logging .error ("Failed to forward restart to worker %s: %s" , hostname , e )
460- results [hostname ] = False
490+ # Parallelize restart requests to all workers
491+ # Use higher parallelism for restart - these are I/O-bound gRPC calls
492+ results = {}
493+ max_workers = min (len (worker_hostnames ), MAX_RESTART_FORWARD_THREADS )
494+ with ManagerClient () as client :
495+ with futures .ThreadPoolExecutor (max_workers = max_workers ) as executor :
496+ future_to_hostname = {
497+ executor .submit (restart_single_worker , hostname , client ): hostname
498+ for hostname in worker_hostnames
499+ }
500+ for future in futures .as_completed (future_to_hostname ):
501+ hostname , success = future .result ()
502+ results [hostname ] = success
461503
462504 return results
463505
0 commit comments