Skip to content

Commit f008f6e

Browse files
authored
Merge pull request #345 from NVIDIA/nvbug6233471-fr-discovery
fix(attribution): discover FR dumps beside app logs
2 parents f714e60 + d798b30 commit f008f6e

2 files changed

Lines changed: 146 additions & 57 deletions

File tree

src/nvidia_resiliency_ext/attribution/trace_analyzer/fr_support.py

Lines changed: 108 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
# Log scan: optional activation (Megatron prints TORCH_FR_DUMP_TEMP_FILE=...)
2929
# ---------------------------------------------------------------------------
3030

31-
FR_DUMP_PATH_LOG_LINE_PATTERN = re.compile(r"TORCH_FR_DUMP_TEMP_FILE=(\S+)")
31+
FR_DUMP_PATH_ENV = "TORCH_FR_DUMP_TEMP_FILE"
32+
FR_DUMP_PATH_LOG_LINE_PATTERN = re.compile(rf"{FR_DUMP_PATH_ENV}=(\S+)")
3233
FR_DUMP_PATH_LOG_SCAN_LINES = 1000
3334

3435

@@ -57,44 +58,111 @@ def fr_path_resolvable_for_collective_analyzer(fr_path: str) -> bool:
5758
return _fr_traces_exist_for_prefix(fr_path)
5859

5960

61+
def _validated_torch_fr_dump_prefix(
62+
prefix: str,
63+
*,
64+
allowed_root: Optional[str] = None,
65+
source: str = FR_DUMP_PATH_ENV,
66+
) -> Optional[str]:
67+
"""Return ``prefix`` when it is a usable ``TORCH_FR_DUMP_TEMP_FILE`` path prefix."""
68+
prefix = prefix.strip().strip("'\"")
69+
if not prefix:
70+
return None
71+
if os.path.isdir(prefix):
72+
logger.warning(
73+
"%s=%r is a directory, not a path prefix; env var is misconfigured — "
74+
"skipping FR analysis",
75+
source,
76+
prefix,
77+
)
78+
return None
79+
if not _fr_traces_exist_for_prefix(prefix):
80+
logger.debug(
81+
"%s prefix %r has no matching trace files; skipping FR analysis",
82+
source,
83+
prefix,
84+
)
85+
return None
86+
if allowed_root is not None and not path_is_under_allowed_root(prefix, allowed_root):
87+
logger.warning(
88+
"%s prefix %r is outside allowed_root %r; skipping FR analysis",
89+
source,
90+
prefix,
91+
allowed_root,
92+
)
93+
return None
94+
return prefix
95+
96+
97+
def _valid_checkpoints_dir(
98+
directory: str,
99+
*,
100+
allowed_root: Optional[str] = None,
101+
source: str,
102+
) -> Optional[str]:
103+
"""Return ``directory`` when it exists, contains FR traces, and passes path policy."""
104+
if not os.path.isdir(directory):
105+
return None
106+
if not _fr_traces_exist_in_dir(directory):
107+
logger.debug(
108+
"%s checkpoints dir %s has no _dump_* traces; skipping candidate",
109+
source,
110+
directory,
111+
)
112+
return None
113+
if allowed_root is not None and not path_is_under_allowed_root(directory, allowed_root):
114+
logger.warning(
115+
"%s FR checkpoints path %r is outside allowed_root %r; skipping candidate",
116+
source,
117+
directory,
118+
allowed_root,
119+
)
120+
return None
121+
logger.debug(
122+
"FR dump path inferred from %s: checkpoints=%s",
123+
source,
124+
directory,
125+
)
126+
return directory
127+
128+
60129
def _infer_checkpoints_dir_from_log_path(
61130
log_path: str, allowed_root: Optional[str] = None
62131
) -> Optional[str]:
63-
"""If ``log_path`` lives under a ``.../logs/...`` tree, return sibling ``.../checkpoints`` when
64-
it exists **and** contains at least one ``_dump_*`` trace file.
132+
"""Infer common shared-filesystem FR dump directories from ``log_path``.
65133
66-
Many training runs use ``<run>/logs/`` for Slurm step logs and ``<run>/checkpoints/`` for FR dumps
67-
(``_dump_<rank>``). The log line ``TORCH_FR_DUMP_TEMP_FILE=`` often points at a container-local path
68-
that attrsvc cannot read; the shared run directory is derivable from the log file path.
134+
Resolution order:
135+
1. ``<log_dir>/checkpoints`` for flat per-cycle logs, e.g.
136+
``/mnt/logs/test_job_cycle0.log`` -> ``/mnt/logs/checkpoints``.
137+
2. ``<run>/checkpoints`` when ``log_path`` is under a ``.../logs/...`` tree, e.g.
138+
``<run>/logs/slurm/job.log`` -> ``<run>/checkpoints``.
139+
140+
These shared path candidates are fallback locations when the explicit
141+
``TORCH_FR_DUMP_TEMP_FILE`` prefix is absent or not resolvable by attrsvc.
69142
"""
70143
try:
71-
d = os.path.dirname(os.path.abspath(log_path))
144+
log_dir = os.path.dirname(os.path.abspath(log_path))
72145
except (OSError, ValueError):
73146
return None
147+
local_checkpoints = _valid_checkpoints_dir(
148+
os.path.join(log_dir, "checkpoints"),
149+
allowed_root=allowed_root,
150+
source="log directory",
151+
)
152+
if local_checkpoints is not None:
153+
return local_checkpoints
154+
d = log_dir
74155
while True:
75156
if os.path.basename(d) == "logs":
76157
run_root = os.path.dirname(d)
77158
cand = os.path.join(run_root, "checkpoints")
78-
if os.path.isdir(cand):
79-
if not _fr_traces_exist_in_dir(cand):
80-
logger.debug(
81-
"Inferred checkpoints dir %s has no _dump_* traces; skipping FR analysis",
82-
cand,
83-
)
84-
return None
85-
if allowed_root is not None and not path_is_under_allowed_root(cand, allowed_root):
86-
logger.warning(
87-
"Inferred FR checkpoints path %r is outside allowed_root %r; skipping FR analysis",
88-
cand,
89-
allowed_root,
90-
)
91-
return None
92-
logger.debug(
93-
"FR dump path inferred from log layout: log_path=%s -> checkpoints=%s",
94-
log_path,
95-
cand,
96-
)
97-
return cand
159+
sibling_checkpoints = _valid_checkpoints_dir(
160+
cand,
161+
allowed_root=allowed_root,
162+
source="logs sibling",
163+
)
164+
if sibling_checkpoints is not None:
165+
return sibling_checkpoints
98166
parent = os.path.dirname(d)
99167
if parent == d:
100168
break
@@ -122,32 +190,11 @@ def _read_torch_fr_dump_from_log(
122190
break
123191
m = FR_DUMP_PATH_LOG_LINE_PATTERN.search(line)
124192
if m:
125-
prefix = m.group(1)
126-
if os.path.isdir(prefix):
127-
logger.warning(
128-
"TORCH_FR_DUMP_TEMP_FILE=%r is a directory, not a path prefix; "
129-
"env var is misconfigured — skipping FR analysis",
130-
prefix,
131-
)
132-
return None
133-
if not _fr_traces_exist_for_prefix(prefix):
134-
logger.debug(
135-
"TORCH_FR_DUMP_TEMP_FILE prefix %r has no matching trace files; "
136-
"skipping FR analysis",
137-
prefix,
138-
)
139-
return None
140-
if allowed_root is not None and not path_is_under_allowed_root(
141-
prefix, allowed_root
142-
):
143-
logger.warning(
144-
"TORCH_FR_DUMP_TEMP_FILE prefix %r is outside allowed_root %r; "
145-
"skipping FR analysis",
146-
prefix,
147-
allowed_root,
148-
)
149-
return None
150-
return prefix
193+
return _validated_torch_fr_dump_prefix(
194+
m.group(1),
195+
allowed_root=allowed_root,
196+
source=f"log {FR_DUMP_PATH_ENV}",
197+
)
151198
except OSError:
152199
pass
153200
return None
@@ -156,20 +203,24 @@ def _read_torch_fr_dump_from_log(
156203
def extract_fr_dump_path(log_path: str, allowed_root: Optional[str] = None) -> Optional[str]:
157204
"""Resolve FR dump path for attrsvc.
158205
159-
Prefer ``<run>/checkpoints`` (directory containing ``_dump_*`` files) when ``log_path`` is
160-
under ``<run>/logs/`` (shared filesystem). Otherwise scan the log for
161-
``TORCH_FR_DUMP_TEMP_FILE=`` (a path prefix; may be container-local).
206+
Apply discovery rules in order:
207+
1. ``TORCH_FR_DUMP_TEMP_FILE=`` scanned from the log (a path prefix).
208+
2. ``<log_dir>/checkpoints`` for flat per-cycle logs.
209+
3. ``<run>/checkpoints`` when ``log_path`` is under ``<run>/logs/...``.
162210
163211
When ``allowed_root`` is set, inferred directories and ``TORCH_FR_DUMP_TEMP_FILE`` prefixes must
164212
resolve under that root (same containment as log path validation); otherwise discovery returns
165213
``None`` so log-injected paths cannot bypass the service path policy.
166214
167215
Returns ``None`` if no valid FR traces are found — analysis should not be triggered.
168216
"""
217+
from_log = _read_torch_fr_dump_from_log(log_path, allowed_root=allowed_root)
218+
if from_log is not None:
219+
return from_log
169220
inferred = _infer_checkpoints_dir_from_log_path(log_path, allowed_root=allowed_root)
170221
if inferred is not None:
171222
return inferred
172-
return _read_torch_fr_dump_from_log(log_path, allowed_root=allowed_root)
223+
return None
173224

174225

175226
# ---------------------------------------------------------------------------

tests/attribution/unit/test_fr_dump_path.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@ def test_torch_fallback_prefix_with_traces(self):
6363

6464
self.assertEqual(extract_fr_dump_path(log_file), prefix)
6565

66+
def test_log_dir_checkpoints_for_flat_cycle_log(self):
67+
"""Infer <log_dir>/checkpoints when TORCH_FR_DUMP_TEMP_FILE is not readable by attrsvc."""
68+
with tempfile.TemporaryDirectory() as tmp:
69+
logs = os.path.join(tmp, "logs")
70+
ckpt = os.path.join(logs, "checkpoints")
71+
os.makedirs(ckpt)
72+
prefix = os.path.join(ckpt, "_dump_")
73+
open(prefix + "0", "w", encoding="utf-8").close()
74+
open(prefix + "1", "w", encoding="utf-8").close()
75+
log_file = os.path.join(logs, "test_job_cycle0.log")
76+
with open(log_file, "w", encoding="utf-8") as f:
77+
f.write("TORCH_FR_DUMP_TEMP_FILE=/container/checkpoints/_dump_\n")
78+
79+
self.assertEqual(
80+
extract_fr_dump_path(log_file, allowed_root=logs),
81+
ckpt,
82+
)
83+
84+
def test_torch_prefix_wins_over_inferred_checkpoints(self):
85+
"""Use TORCH_FR_DUMP_TEMP_FILE when it resolves, before inferred checkpoint dirs."""
86+
with tempfile.TemporaryDirectory() as tmp:
87+
logs = os.path.join(tmp, "logs")
88+
inferred = os.path.join(logs, "checkpoints")
89+
explicit = os.path.join(logs, "explicit_fr")
90+
os.makedirs(inferred)
91+
os.makedirs(explicit)
92+
open(os.path.join(inferred, "_dump_0"), "w", encoding="utf-8").close()
93+
explicit_prefix = os.path.join(explicit, "_dump_")
94+
open(explicit_prefix + "0", "w", encoding="utf-8").close()
95+
log_file = os.path.join(logs, "test_job_cycle0.log")
96+
with open(log_file, "w", encoding="utf-8") as f:
97+
f.write(f"TORCH_FR_DUMP_TEMP_FILE={explicit_prefix}\n")
98+
99+
self.assertEqual(
100+
extract_fr_dump_path(log_file, allowed_root=logs),
101+
explicit_prefix,
102+
)
103+
66104
def test_torch_fallback_prefix_no_traces_returns_none(self):
67105
"""TORCH_FR_DUMP_TEMP_FILE prefix with no matching files — analysis must not be triggered."""
68106
with tempfile.TemporaryDirectory() as tmp:

0 commit comments

Comments
 (0)