44
55Environment Variables Used:
66 HOSTNAME: Current worker's hostname
7- REPLICA_ID : Current worker's replica ID (0-based)
7+ MEGASCALE_SLICE_ID : Current worker's slice/ replica ID (0-based)
88 TPU_WORKER_ID: Current worker's ID within the replica (0-based)
99 TPU_WORKER_HOSTNAMES: Comma-separated list of all worker hostnames in current replica
10- NUM_REPLICAS : Total number of replicas in the training job
10+ NUM_TPU_SLICES : Total number of slices/ replicas in the training job
1111
1212Hostname Format:
1313 TPU worker hostnames follow the pattern: {job_name}-"job"-{replica_id}-{worker_id}.{job_name}
1818import os
1919import pathlib
2020import re
21+ import signal
2122import subprocess
2223import threading
2324import time
3536# Client timeout in seconds
3637DEFAULT_CLIENT_TIMEOUT = 10.0
3738
38- # Timeout for graceful training process termination in seconds
39- GRACEFUL_TRAINING_TERMINATION_TIMEOUT = 1
40-
4139# Compiled regex patterns for hostname parsing
4240_WORKER_ID_PATTERN = re .compile (r"^.+?-job-\d+-(\d+)\..+" )
4341_JOB_NAME_PATTERN = re .compile (r"^(.+?)-job-\d+-\d+\.(\1)(?:\.|$)" )
@@ -110,26 +108,43 @@ def _cleanup_tpu_lock_files(self) -> None:
110108 def _do_terminate (self ) -> bool :
111109 """Execute the actual process termination sequence.
112110
111+ Uses process group killing to ensure all threads are terminated.
112+ The trainer subprocess should be started with start_new_session=True.
113+
113114 Returns:
114115 bool: True if termination succeeded, False otherwise
115116 """
116- self .current_process .terminate ()
117+ pid = self .current_process .pid
118+
119+ # Try to get process group ID
117120 try :
118- self .current_process .wait (timeout = GRACEFUL_TRAINING_TERMINATION_TIMEOUT )
119- logging .info ("Process terminated gracefully" )
121+ pgid = os .getpgid (pid )
122+ except ProcessLookupError :
123+ logging .info ("Process %d already dead" , pid )
124+ self ._cleanup_tpu_lock_files ()
120125 return True
121- except subprocess .TimeoutExpired :
122- pass
123126
124- logging .warning ("Process did not terminate gracefully, forcing kill" )
125- self .current_process .kill ()
127+ # If trainer has its own process group, kill the entire group
128+ if pgid == pid :
129+ logging .warning ("Sending SIGKILL to process group %d" , pgid )
130+ try :
131+ os .killpg (pgid , signal .SIGKILL )
132+ except ProcessLookupError :
133+ logging .info ("Process group %d already dead" , pgid )
134+ except PermissionError as e :
135+ logging .warning ("Cannot kill process group %d: %s, killing process only" , pgid , e )
136+ self .current_process .kill ()
137+ else :
138+ logging .warning ("Sending SIGKILL to process %d" , pid )
139+ self .current_process .kill ()
140+
126141 try :
127- self .current_process .wait (timeout = 5 )
142+ self .current_process .wait (timeout = 10 )
128143 logging .info (
129144 "Process killed successfully, exit code: %s" , self .current_process .returncode
130145 )
131146 except subprocess .TimeoutExpired :
132- logging .error ("Process still alive after SIGKILL, pid=%d" , self . current_process . pid )
147+ logging .error ("Process still alive after SIGKILL, pid=%d" , pid )
133148 return False
134149
135150 self ._cleanup_tpu_lock_files ()
@@ -220,19 +235,20 @@ def get_replica_id() -> int:
220235 """Get replica ID from environment variable with validation.
221236
222237 Returns:
223- The replica ID as an integer.
238+ The replica ID as an integer. Defaults to 0 for single-slice jobs.
224239
225240 Raises:
226- ValueError: If REPLICA_ID is not set or not a valid integer.
241+ ValueError: If MEGASCALE_SLICE_ID is not a valid integer.
227242 """
228- replica_id_str = os .environ .get ("REPLICA_ID" )
229- if replica_id_str is None :
230- raise ValueError ("REPLICA_ID environment variable is not set" )
243+ # Default to "0" for single-slice jobs where MEGASCALE_SLICE_ID is not set
244+ replica_id_str = os .environ .get ("MEGASCALE_SLICE_ID" , "0" )
231245
232246 try :
233247 return int (replica_id_str )
234248 except ValueError as e :
235- raise ValueError (f"REPLICA_ID must be a valid integer, got: '{ replica_id_str } '" ) from e
249+ raise ValueError (
250+ f"MEGASCALE_SLICE_ID must be a valid integer, got: '{ replica_id_str } '"
251+ ) from e
236252
237253
238254def get_worker_id () -> int :
@@ -258,23 +274,22 @@ def get_num_replicas() -> int:
258274 """Get total number of replicas from environment variable with validation.
259275
260276 Returns:
261- The total number of replicas as an integer.
277+ The total number of replicas as an integer. Defaults to 1 for single-slice jobs.
262278
263279 Raises:
264- ValueError: If NUM_REPLICAS is not set or not a valid integer.
280+ ValueError: If NUM_TPU_SLICES is not a valid positive integer.
265281 """
266- num_replicas_str = os .environ .get ("NUM_REPLICAS" )
267- if num_replicas_str is None :
268- raise ValueError ("NUM_REPLICAS environment variable is not set" )
282+ # Default to "1" for single-slice jobs where NUM_TPU_SLICES is not set
283+ num_replicas_str = os .environ .get ("NUM_TPU_SLICES" , "1" )
269284
270285 try :
271286 num_replicas = int (num_replicas_str )
272287 if num_replicas < 1 :
273- raise ValueError (f"NUM_REPLICAS must be at least 1, got: { num_replicas } " )
288+ raise ValueError (f"NUM_TPU_SLICES must be at least 1, got: { num_replicas } " )
274289 return num_replicas
275290 except ValueError as e :
276291 raise ValueError (
277- f"NUM_REPLICAS must be a valid positive integer, got: '{ num_replicas_str } '"
292+ f"NUM_TPU_SLICES must be a valid positive integer, got: '{ num_replicas_str } '"
278293 ) from e
279294
280295
0 commit comments