@@ -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 )
5363class 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