Skip to content

Commit c1c6d0e

Browse files
Address PR review (R2): drop dead code in PoolWorker
Items called out by the second review pass: - _MAX_NOISE_LINES bound is unreachable in practice (select() deadline already terminates the loop); also drop the post-loop "resp_line is None" branch which was only reachable when the bound fired. - _stderr_thread attribute and assignments are never read again after _spawn(); the thread is daemon so it self-terminates when the pipe closes. Drop the attribute and store the Thread on the stack. - _stderr_tail() is called only from _diag(); inline it. - assert self.proc and self.proc.stdin and self.proc.stdout in submit() is dead — _ensure_alive() guarantees proc, and Popen guarantees the pipes when stdin/stdout=PIPE. Pure cleanup; no behavior change. Signed-off-by: Sudhakar Singh <sudhakars@nvidia.com>
1 parent 01db9f5 commit c1c6d0e

1 file changed

Lines changed: 11 additions & 35 deletions

File tree

tests/pytorch/attention/test_attention_with_cp.py

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)