Skip to content

Commit aa3a357

Browse files
committed
fix(kubeflow): stream only rank 0 + last rank, write all ranks to disk
KubeflowExecutor.fetch_logs followed every replica and forwarded all ranks to the caller, so at scale the aggregate output overran CI/runner job-log size limits (a 16-node x 8-GPU run exceeded GitLab's 128MB cap). Now it still tails every rank (kubectl logs -l <jobset> --prefix --max-log-requests num_nodes) and writes the complete multi-rank output to <job_dir>/log-allranks_0.out, but forwards only global rank 0 (node 0, [default0]) and the last global rank (node num_nodes-1, [default nproc_per_node-1]) to stdout. Downstream log validation that globs log*.out still sees every rank via the on-disk file. Signed-off-by: oliver könig <okoenig@nvidia.com>
1 parent 1ad956b commit aa3a357

1 file changed

Lines changed: 45 additions & 10 deletions

File tree

nemo_run/core/execution/kubeflow.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import getpass
1717
import logging
1818
import os
19+
import re
1920
import subprocess
2021
import time
2122
from dataclasses import dataclass, field
@@ -331,6 +332,13 @@ def fetch_logs(
331332
until pods are running (up to 10 minutes). Otherwise it returns the last
332333
*lines* lines from a single ``kubectl logs`` call.
333334
"""
335+
# Tail every rank, but forward only global rank 0 and the last global
336+
# rank to the caller (stdout / CI job log). Streaming all ranks at scale
337+
# overruns CI/runner job-log size limits, yet the full multi-rank output
338+
# is still written to <job_dir>/log-allranks_0.out so downstream log
339+
# validation (which globs log*.out) sees every rank. --prefix tags each
340+
# line with its pod name so we can recover the node (completion index)
341+
# and pair it with torchrun's [defaultN] local-rank marker.
334342
label_selector = f"jobset.sigs.k8s.io/jobset-name={job_name}"
335343
cmd = [
336344
"kubectl",
@@ -339,11 +347,29 @@ def fetch_logs(
339347
label_selector,
340348
"-n",
341349
self.namespace,
350+
"--prefix",
342351
"--tail",
343352
str(lines),
344353
"--max-log-requests",
345354
str(self.num_nodes),
346355
]
356+
last_node = max(self.num_nodes - 1, 0)
357+
last_local = max(self.nproc_per_node() - 1, 0)
358+
node_re = re.compile(r"node-0-(\d+)-")
359+
local_re = re.compile(r"\[default(\d+)\]")
360+
361+
def _forward_to_stdout(log_line: str) -> bool:
362+
"""True for global rank 0 (node 0, local 0) and the last global rank."""
363+
node_match = node_re.search(log_line)
364+
local_match = local_re.search(log_line)
365+
if not node_match or not local_match:
366+
return False
367+
node, local = int(node_match.group(1)), int(local_match.group(1))
368+
return (node == 0 and local == 0) or (node == last_node and local == last_local)
369+
370+
all_ranks_path = os.path.join(self.job_dir, "log-allranks_0.out")
371+
os.makedirs(self.job_dir, exist_ok=True)
372+
347373
if stream:
348374
cmd.append("-f")
349375
# Retry kubectl logs -f until the job reaches a terminal state.
@@ -354,16 +380,21 @@ def fetch_logs(
354380
)
355381
lines_yielded = 0
356382
try:
357-
for line in iter(proc.stdout.readline, ""):
358-
if line:
359-
lines_yielded += 1
360-
yield line
361-
if proc.poll() is not None:
362-
for remaining in proc.stdout:
363-
if remaining:
383+
with open(all_ranks_path, "a") as all_ranks_file:
384+
for line in iter(proc.stdout.readline, ""):
385+
if line:
386+
all_ranks_file.write(line)
387+
if _forward_to_stdout(line):
364388
lines_yielded += 1
365-
yield remaining
366-
break
389+
yield line
390+
if proc.poll() is not None:
391+
for remaining in proc.stdout:
392+
if remaining:
393+
all_ranks_file.write(remaining)
394+
if _forward_to_stdout(remaining):
395+
lines_yielded += 1
396+
yield remaining
397+
break
367398
except Exception as e:
368399
logger.warning("Error streaming logs: %s; retrying", e)
369400
finally:
@@ -381,7 +412,11 @@ def fetch_logs(
381412
time.sleep(5)
382413
else:
383414
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
384-
yield from result.stdout.splitlines()
415+
with open(all_ranks_path, "a") as all_ranks_file:
416+
for line in result.stdout.splitlines():
417+
all_ranks_file.write(line + "\n")
418+
if _forward_to_stdout(line):
419+
yield line
385420

386421
def cancel(
387422
self,

0 commit comments

Comments
 (0)