@@ -98,13 +98,11 @@ class PoolWorker:
9898 # output that lands on the shared stdout fd) so the JSON protocol can't
9999 # be corrupted by interleaved chatter.
100100 _RESP_PREFIX = "[CP_POOL_RESP] "
101- _MAX_NOISE_LINES = 1000 # bound the per-submit scan so a chatty worker can't loop forever
102101
103102 def __init__ (self , world_size : int ):
104103 self .world_size = world_size
105104 self .proc : subprocess .Popen | None = None
106105 self ._stderr_buf : deque [str ] = deque (maxlen = self ._STDERR_BUFFER_LINES )
107- self ._stderr_thread : threading .Thread | None = None
108106
109107 def _spawn (self ) -> None :
110108 te_path = os .getenv ("TE_PATH" , "/opt/transformerengine" )
@@ -119,7 +117,8 @@ def _spawn(self) -> None:
119117 ]
120118 # stderr=PIPE so we can capture the tail for crash-path AssertionErrors;
121119 # a daemon drainer thread also echoes each line to sys.stderr so pytest's
122- # per-test stderr capture still works.
120+ # per-test stderr capture still works. The thread is daemon, so it
121+ # self-terminates when the pipe closes — no tracking needed.
123122 self .proc = subprocess .Popen (
124123 cmd ,
125124 stdin = subprocess .PIPE ,
@@ -130,8 +129,7 @@ def _spawn(self) -> None:
130129 env = {** os .environ , "PYTHONUNBUFFERED" : "1" },
131130 )
132131 self ._stderr_buf .clear ()
133- self ._stderr_thread = threading .Thread (target = self ._drain_stderr , daemon = True )
134- self ._stderr_thread .start ()
132+ threading .Thread (target = self ._drain_stderr , daemon = True ).start ()
135133
136134 def _drain_stderr (self ) -> None :
137135 proc = self .proc
@@ -142,12 +140,8 @@ def _drain_stderr(self) -> None:
142140 sys .stderr .write (line )
143141 sys .stderr .flush ()
144142
145- def _stderr_tail (self ) -> str :
146- text = "" .join (self ._stderr_buf )
147- return text [- self ._STDERR_TAIL_CHARS :] if len (text ) > self ._STDERR_TAIL_CHARS else text
148-
149143 def _diag (self , msg : str ) -> str :
150- tail = self . _stderr_tail ()
144+ tail = "" . join ( self . _stderr_buf )[ - self . _STDERR_TAIL_CHARS :]
151145 if not tail .strip ():
152146 return msg
153147 return f"{ msg } \n \n --- pool worker stderr (tail) ---\n { tail } "
@@ -164,13 +158,10 @@ def _kill(self) -> None:
164158 except subprocess .TimeoutExpired :
165159 self .proc .kill ()
166160 self .proc .wait ()
167- # Drainer thread exits on its own when the pipe closes.
168161 self .proc = None
169- self ._stderr_thread = None
170162
171163 def submit (self , kwargs : dict , timeout : float = POOL_SUBMIT_TIMEOUT_SEC ) -> None :
172164 self ._ensure_alive ()
173- assert self .proc and self .proc .stdin and self .proc .stdout
174165 req = json .dumps ({"op" : "run" , "kwargs" : kwargs }) + "\n "
175166 try :
176167 self .proc .stdin .write (req )
@@ -182,20 +173,16 @@ def submit(self, kwargs: dict, timeout: float = POOL_SUBMIT_TIMEOUT_SEC) -> None
182173
183174 # Read lines until we see our sentinel-prefixed JSON response. Anything
184175 # else (torchrun status output, library prints, rank>0 stray writes that
185- # land on the shared stdout fd) is silently discarded — it would corrupt
186- # json.loads if we tried to decode it. Bounded loop so a chatty worker
187- # can't keep us spinning past the deadline.
188- deadline = None # set lazily on first iteration so we honour the original timeout budget
189- resp_line = None
190- scanned = 0
191- while scanned < self ._MAX_NOISE_LINES :
192- remaining = timeout if deadline is None else max (0.0 , deadline - time .monotonic ())
176+ # land on the shared stdout fd) is echoed to stderr and skipped — it
177+ # would corrupt json.loads if we tried to decode it. The select()
178+ # deadline alone bounds the loop; no separate line counter needed.
179+ deadline = time .monotonic () + timeout
180+ while True :
181+ remaining = max (0.0 , deadline - time .monotonic ())
193182 # select() on a pipe fd is Linux/macOS only — on Windows the select
194183 # module only accepts sockets. CP attention tests run on Linux GPU
195184 # hosts so this is fine; flag if portability is ever needed.
196185 ready , _ , _ = select .select ([self .proc .stdout ], [], [], remaining )
197- if deadline is None :
198- deadline = time .monotonic () + timeout
199186 if not ready :
200187 msg = self ._diag (
201188 f"pool worker (world_size={ self .world_size } ) timed out after "
@@ -210,23 +197,13 @@ def submit(self, kwargs: dict, timeout: float = POOL_SUBMIT_TIMEOUT_SEC) -> None
210197 self ._kill ()
211198 raise AssertionError (msg )
212199
213- scanned += 1
214200 if line .startswith (self ._RESP_PREFIX ):
215201 resp_line = line [len (self ._RESP_PREFIX ) :]
216202 break
217- # Otherwise: non-protocol stdout from somewhere. Echo to test stderr
218- # so it's still visible in CI logs, then keep looking.
203+ # Non-protocol stdout — echo to stderr for CI visibility, keep looking.
219204 sys .stderr .write (line )
220205 sys .stderr .flush ()
221206
222- if resp_line is None :
223- msg = self ._diag (
224- f"pool worker (world_size={ self .world_size } ) sent { self ._MAX_NOISE_LINES } + "
225- "stdout lines without a sentinel-prefixed response; assuming protocol corruption"
226- )
227- self ._kill ()
228- raise AssertionError (msg )
229-
230207 resp = json .loads (resp_line )
231208 if not resp ["ok" ]:
232209 # gather_object already carries the full per-rank traceback in
@@ -246,7 +223,6 @@ def shutdown(self) -> None:
246223 except subprocess .TimeoutExpired :
247224 self ._kill ()
248225 self .proc = None
249- self ._stderr_thread = None
250226
251227
252228@pytest .fixture (scope = "session" )
0 commit comments