Skip to content

Commit f8171d4

Browse files
committed
Add NCCL H3 internal hang diagnostics
1 parent 5bb0b04 commit f8171d4

8 files changed

Lines changed: 1253 additions & 10 deletions

File tree

flagscale/runner/tracing/analyzer.py

Lines changed: 456 additions & 4 deletions
Large diffs are not rendered by default.

flagscale/runner/tracing/config.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ def _bool(value: Any, name: str) -> bool:
4949
raise ValueError(f"tracing.{name} must be a boolean, got {value!r}")
5050

5151

52+
def _non_negative_int(value: Any, name: str) -> int:
53+
try:
54+
parsed = int(value)
55+
except (TypeError, ValueError) as exc:
56+
raise ValueError(f"tracing.{name} must be a non-negative integer") from exc
57+
if parsed < 0:
58+
raise ValueError(f"tracing.{name} must be a non-negative integer")
59+
return parsed
60+
61+
5262
@dataclass(frozen=True)
5363
class TraceLaunchConfig:
5464
"""Resolved launch-time settings for one trace run."""
@@ -63,9 +73,19 @@ class TraceLaunchConfig:
6373
delayed_enter_threshold_s: float = 30.0
6474
p2p_timeout_s: float = 60.0
6575
p2p_match_window_s: float = 30.0
76+
missing_exit_timeout_s: float = 60.0
6677
failure_grace_period_s: float = 60.0
6778
scan_interval_s: float = 1.0
6879
monitor_nice: int = 10
80+
hardware_health_enabled: bool = False
81+
hardware_health_stale_after_s: float = 180.0
82+
inspector_enabled: bool = False
83+
inspector_plugin_library: str = ""
84+
inspector_dump_dir: str = ""
85+
inspector_dump_interval_us: int = 500_000
86+
inspector_min_size_bytes: int = 8192
87+
inspector_require_kernel_timing: bool = True
88+
inspector_correlation_window_s: float = 5.0
6989

7090
@property
7191
def completion_file(self) -> str:
@@ -106,6 +126,38 @@ def shell_setup_lines(self, node_rank: int) -> list[str]:
106126
f"export FLAGSCALE_TRACE_DIR={qdir}",
107127
f'export LD_PRELOAD={qlib}"${{LD_PRELOAD:+:$LD_PRELOAD}}"',
108128
]
129+
if self.inspector_enabled:
130+
inspector_library = shlex.quote(self.inspector_plugin_library)
131+
inspector_dir = shlex.quote(self.inspector_dump_dir)
132+
lines.extend(
133+
[
134+
f"if [ ! -r {inspector_library} ]; then",
135+
(
136+
" echo "
137+
+ shlex.quote(
138+
"NCCL Inspector plugin not found or unreadable: "
139+
f"{self.inspector_plugin_library}"
140+
)
141+
+ " >&2"
142+
),
143+
" exit 1",
144+
"fi",
145+
f"mkdir -p {inspector_dir}",
146+
f"export NCCL_PROFILER_PLUGIN={inspector_library}",
147+
"export NCCL_INSPECTOR_ENABLE=1",
148+
"export NCCL_INSPECTOR_DUMP_VERBOSE=1",
149+
f"export NCCL_INSPECTOR_DUMP_DIR={inspector_dir}",
150+
(
151+
"export NCCL_INSPECTOR_DUMP_THREAD_INTERVAL_MICROSECONDS="
152+
f"{self.inspector_dump_interval_us}"
153+
),
154+
(f"export NCCL_INSPECTOR_DUMP_MIN_SIZE_BYTES={self.inspector_min_size_bytes}"),
155+
(
156+
"export NCCL_INSPECTOR_REQUIRE_KERNEL_TIMING="
157+
f"{int(self.inspector_require_kernel_timing)}"
158+
),
159+
]
160+
)
109161

110162
# With a shared trace directory, a single analyzer on node zero sees all ranks.
111163
if node_rank == 0:
@@ -125,6 +177,8 @@ def shell_setup_lines(self, node_rank: int) -> list[str]:
125177
f"{self.p2p_timeout_s:g}",
126178
"--p2p-match-window",
127179
f"{self.p2p_match_window_s:g}",
180+
"--missing-exit-timeout",
181+
f"{self.missing_exit_timeout_s:g}",
128182
"--failure-grace-period",
129183
f"{self.failure_grace_period_s:g}",
130184
"--scan-interval",
@@ -143,6 +197,19 @@ def shell_setup_lines(self, node_rank: int) -> list[str]:
143197
self.heartbeat_dir,
144198
"--heartbeat-timeout",
145199
f"{self.heartbeat_timeout_s:g}",
200+
"--hardware-health-stale-after",
201+
f"{self.hardware_health_stale_after_s:g}",
202+
]
203+
)
204+
if self.hardware_health_enabled:
205+
monitor_cmd.append("--hardware-health")
206+
if self.inspector_enabled:
207+
monitor_cmd.extend(
208+
[
209+
"--inspector-dir",
210+
self.inspector_dump_dir,
211+
"--inspector-correlation-window",
212+
f"{self.inspector_correlation_window_s:g}",
146213
]
147214
)
148215
lines.extend(
@@ -253,16 +320,51 @@ def prepare_trace_launch_config(
253320
raw_dict.get("p2p_match_window_s", min(30.0, p2p_timeout_s)),
254321
"p2p_match_window_s",
255322
)
323+
missing_exit_timeout_s = _positive_float(
324+
raw_dict.get("missing_exit_timeout_s", collective_timeout_s),
325+
"missing_exit_timeout_s",
326+
)
256327
failure_grace_period_s = _positive_float(
257328
raw_dict.get(
258329
"failure_grace_period_s",
259330
max(
260331
collective_timeout_s,
261332
p2p_timeout_s,
333+
missing_exit_timeout_s,
262334
),
263335
),
264336
"failure_grace_period_s",
265337
)
338+
inspector = raw_dict.get("inspector", {})
339+
if not isinstance(inspector, dict):
340+
raise ValueError("tracing.inspector must be a mapping")
341+
inspector_enabled = _bool(inspector.get("enabled", False), "inspector.enabled")
342+
inspector_plugin_library = str(
343+
inspector.get("plugin_library") or os.getenv("NCCL_PROFILER_PLUGIN") or ""
344+
)
345+
if inspector_enabled and not inspector_plugin_library:
346+
raise ValueError("tracing.inspector.plugin_library is required when Inspector is enabled")
347+
if inspector_plugin_library:
348+
inspector_plugin_library = os.path.abspath(os.path.expanduser(inspector_plugin_library))
349+
inspector_dump_dir = os.path.abspath(
350+
os.path.expanduser(str(inspector.get("dump_dir") or os.path.join(trace_dir, "inspector")))
351+
)
352+
inspector_dump_interval_us = _non_negative_int(
353+
inspector.get("dump_interval_us", 500_000),
354+
"inspector.dump_interval_us",
355+
)
356+
inspector_min_size_bytes = _non_negative_int(
357+
inspector.get("min_size_bytes", 8192),
358+
"inspector.min_size_bytes",
359+
)
360+
inspector_require_kernel_timing = _bool(
361+
inspector.get("require_kernel_timing", True),
362+
"inspector.require_kernel_timing",
363+
)
364+
inspector_correlation_window_s = _positive_float(
365+
inspector.get("correlation_window_s", 5.0),
366+
"inspector.correlation_window_s",
367+
)
266368

267369
resolved = TraceLaunchConfig(
268370
enabled=True,
@@ -286,16 +388,35 @@ def prepare_trace_launch_config(
286388
),
287389
p2p_timeout_s=p2p_timeout_s,
288390
p2p_match_window_s=p2p_match_window_s,
391+
missing_exit_timeout_s=missing_exit_timeout_s,
289392
failure_grace_period_s=failure_grace_period_s,
290393
scan_interval_s=_positive_float(raw_dict.get("scan_interval_s", 1.0), "scan_interval_s"),
291394
monitor_nice=monitor_nice,
395+
hardware_health_enabled=bool(
396+
heartbeat_config is not None
397+
and heartbeat_config.enabled
398+
and heartbeat_config.hardware_health_enabled
399+
),
400+
hardware_health_stale_after_s=(
401+
heartbeat_config.hardware_health_stale_after_s
402+
if heartbeat_config is not None and heartbeat_config.enabled
403+
else 180.0
404+
),
405+
inspector_enabled=inspector_enabled,
406+
inspector_plugin_library=inspector_plugin_library,
407+
inspector_dump_dir=inspector_dump_dir,
408+
inspector_dump_interval_us=inspector_dump_interval_us,
409+
inspector_min_size_bytes=inspector_min_size_bytes,
410+
inspector_require_kernel_timing=inspector_require_kernel_timing,
411+
inspector_correlation_window_s=inspector_correlation_window_s,
292412
)
293413

294414
if resolved.p2p_match_window_s > resolved.p2p_timeout_s:
295415
raise ValueError("tracing.p2p_match_window_s must not exceed p2p_timeout_s")
296416
minimum_failure_grace = max(
297417
resolved.collective_timeout_s,
298418
resolved.p2p_timeout_s,
419+
resolved.missing_exit_timeout_s,
299420
)
300421
if resolved.failure_grace_period_s < minimum_failure_grace:
301422
raise ValueError(

0 commit comments

Comments
 (0)