Skip to content

Commit 2cdb91b

Browse files
Liang (AIML-Compute) Hechanglan
authored andcommitted
[AXLearnFT] Optimizing the FT restart training gRPC
GitOrigin-RevId: 0abc902
1 parent daf6910 commit 2cdb91b

2 files changed

Lines changed: 139 additions & 58 deletions

File tree

axlearn/ft/manager_server.py

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
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

3440
class 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

axlearn/ft/utils.py

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import logging
1818
import os
19+
import pathlib
1920
import re
2021
import subprocess
2122
import threading
@@ -34,6 +35,9 @@
3435
# Client timeout in seconds
3536
DEFAULT_CLIENT_TIMEOUT = 10.0
3637

38+
# Timeout for graceful training process termination in seconds
39+
GRACEFUL_TRAINING_TERMINATION_TIMEOUT = 1
40+
3741
# Compiled regex patterns for hostname parsing
3842
_WORKER_ID_PATTERN = re.compile(r"^.+?-job-\d+-(\d+)\..+")
3943
_JOB_NAME_PATTERN = re.compile(r"^(.+?)-job-\d+-\d+\.(\1)(?:\.|$)")
@@ -88,6 +92,48 @@ def set_process(self, process: subprocess.Popen):
8892
self.termination_requested = False
8993
self.termination_reason = ""
9094

95+
def _cleanup_tpu_lock_files(self) -> None:
96+
"""Clean up TPU lock files.
97+
98+
See: https://github.com/jax-ml/jax/issues/10192#issuecomment-1509814942
99+
"""
100+
for lock_file in pathlib.Path("/tmp").glob("libtpu_lockfile*"):
101+
try:
102+
lock_file.unlink()
103+
logging.info("Cleaned up stale lock file: %s", lock_file)
104+
except FileNotFoundError:
105+
pass # File already removed, this is fine
106+
except OSError as e:
107+
logging.warning("Failed to remove TPU lock file %s: %s", lock_file, e)
108+
109+
def _do_terminate(self) -> bool:
110+
"""Execute the actual process termination sequence.
111+
112+
Returns:
113+
bool: True if termination succeeded, False otherwise
114+
"""
115+
self.current_process.terminate()
116+
try:
117+
self.current_process.wait(timeout=GRACEFUL_TRAINING_TERMINATION_TIMEOUT)
118+
logging.info("Process terminated gracefully")
119+
return True
120+
except subprocess.TimeoutExpired:
121+
pass
122+
123+
logging.warning("Process did not terminate gracefully, forcing kill")
124+
self.current_process.kill()
125+
try:
126+
self.current_process.wait(timeout=5)
127+
logging.info(
128+
"Process killed successfully, exit code: %s", self.current_process.returncode
129+
)
130+
except subprocess.TimeoutExpired:
131+
logging.error("Process still alive after SIGKILL, pid=%d", self.current_process.pid)
132+
return False
133+
134+
self._cleanup_tpu_lock_files()
135+
return True
136+
91137
def terminate_training(self, reason: str = "Restart requested"):
92138
"""Terminate the current trainer process.
93139
@@ -98,27 +144,20 @@ def terminate_training(self, reason: str = "Restart requested"):
98144
bool: True if termination was initiated, False if no process to terminate
99145
"""
100146
with self.lock:
101-
if self.current_process:
102-
logging.warning("Terminating trainer process: %s", reason)
103-
self.termination_requested = True
104-
self.termination_reason = reason
105-
106-
try:
107-
self.current_process.terminate() # Send SIGTERM first
108-
try:
109-
self.current_process.wait(timeout=5) # Wait for graceful shutdown
110-
logging.info("Process terminated gracefully")
111-
except subprocess.TimeoutExpired:
112-
logging.warning("Process did not terminate gracefully, forcing kill")
113-
self.current_process.kill() # Force kill if timeout
114-
return True
115-
except Exception as e: # pylint: disable=broad-exception-caught
116-
logging.error("Failed to terminate process: %s", e)
117-
return False
118-
else:
147+
if not self.current_process:
119148
logging.warning("No trainer process to terminate")
120149
return False
121150

151+
logging.warning("Terminating trainer process: %s", reason)
152+
self.termination_requested = True
153+
self.termination_reason = reason
154+
155+
try:
156+
return self._do_terminate()
157+
except Exception as e: # pylint: disable=broad-exception-caught
158+
logging.error("Failed to terminate process: %s", e)
159+
return False
160+
122161
def check_termination_requested(self):
123162
"""Check if termination was requested and get reason.
124163

0 commit comments

Comments
 (0)