Skip to content

Commit 0036763

Browse files
committed
Add CPU-side NCCL H1 probe
1 parent d8fdf3b commit 0036763

20 files changed

Lines changed: 2514 additions & 10 deletions

flagscale/runner/backend/backend_megatron.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
from omegaconf import DictConfig, OmegaConf
1919

2020
from flagscale.runner.backend.backend_base import BackendBase
21+
from flagscale.runner.diagnostics import diagnostic_command_body
2122
from flagscale.runner.heartbeat.config import prepare_heartbeat_launch_config
2223
from flagscale.runner.runner_train import (
2324
_get_args_megatron,
2425
_update_config_train,
2526
)
27+
from flagscale.runner.tracing.config import prepare_trace_launch_config
2628
from flagscale.runner.utils import get_pkg_dir, logger, parse_hostfile, resolve_path
2729

2830
PERF_MONITOR_RUNNER_KEYS = (
@@ -51,6 +53,9 @@ def _prepare(self):
5153
self.user_args = _get_args_megatron(self.config)
5254
self.rdzv_id = datetime.now().strftime("%Y%m%d_%H%M%S.%f")
5355
self.heartbeat_config = prepare_heartbeat_launch_config(self.config, self.rdzv_id)
56+
self.trace_config = prepare_trace_launch_config(
57+
self.config, self.rdzv_id, self.heartbeat_config
58+
)
5459
self.user_envs = self.config.experiment.get("envs", {})
5560
self.user_script = self.config.experiment.task.entrypoint
5661
self.resources = parse_hostfile(self.config.experiment.runner.get("hostfile", None))
@@ -137,6 +142,10 @@ def generate_run_script(
137142
f.write(f"{line}\n")
138143
if self.heartbeat_config.enabled:
139144
f.write("\n")
145+
for line in self.trace_config.shell_setup_lines(node_rank):
146+
f.write(f"{line}\n")
147+
if self.trace_config.enabled:
148+
f.write("\n")
140149
f.write(f'cmd="{cmd}"\n')
141150
f.write("\n")
142151
if enable_monitoring:
@@ -161,7 +170,9 @@ def generate_run_script(
161170
)
162171
f.write("\n")
163172

164-
command_body = self.heartbeat_config.training_command_body(node_rank)
173+
command_body = diagnostic_command_body(
174+
node_rank, self.heartbeat_config, self.trace_config
175+
)
165176
if background:
166177
f.write(
167178
f'nohup bash -c "{command_body}" >> {host_output_file} 2>&1 & echo $! > {host_pid_file}\n'
@@ -206,6 +217,8 @@ def generate_stop_script(self, host, node_rank):
206217
f.write("fi\n")
207218
for line in self.heartbeat_config.stop_shell_lines(node_rank):
208219
f.write(f"{line}\n")
220+
for line in self.trace_config.stop_shell_lines(node_rank):
221+
f.write(f"{line}\n")
209222
f.write(f"{after_stop}\n")
210223
f.flush()
211224
os.fsync(f.fileno())

flagscale/runner/backend/backend_native_train.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
from omegaconf import DictConfig, OmegaConf
1919

2020
from flagscale.runner.backend.backend_base import BackendBase
21+
from flagscale.runner.diagnostics import diagnostic_command_body
22+
from flagscale.runner.heartbeat.config import prepare_heartbeat_launch_config
2123
from flagscale.runner.runner_train import _get_args_native, _update_config_train
24+
from flagscale.runner.tracing.config import prepare_trace_launch_config
2225
from flagscale.runner.utils import get_pkg_dir, logger, parse_hostfile, resolve_path
2326

2427

@@ -33,6 +36,10 @@ def _prepare(self):
3336
_update_config_train(self.config)
3437
self.user_args = _get_args_native(self.config)
3538
self.rdzv_id = datetime.now().strftime("%Y%m%d_%H%M%S.%f")
39+
self.heartbeat_config = prepare_heartbeat_launch_config(self.config, self.rdzv_id)
40+
self.trace_config = prepare_trace_launch_config(
41+
self.config, self.rdzv_id, self.heartbeat_config
42+
)
3643
self.user_envs = self.config.experiment.get("envs", {})
3744
self.user_script = self.config.experiment.task.entrypoint
3845
self.resources = parse_hostfile(self.config.experiment.runner.get("hostfile", None))
@@ -94,6 +101,14 @@ def generate_run_script(
94101
f.write("\n")
95102
f.write(f"export PYTHONPATH={pkg_dir}:{megatron_dir}:${{PYTHONPATH}}\n")
96103
f.write("\n")
104+
for line in self.heartbeat_config.shell_setup_lines(node_rank):
105+
f.write(f"{line}\n")
106+
if self.heartbeat_config.enabled:
107+
f.write("\n")
108+
for line in self.trace_config.shell_setup_lines(node_rank):
109+
f.write(f"{line}\n")
110+
if self.trace_config.enabled:
111+
f.write("\n")
97112
f.write(f'cmd="{cmd}"\n')
98113
f.write("\n")
99114
if enable_monitoring:
@@ -118,13 +133,16 @@ def generate_run_script(
118133
)
119134
f.write("\n")
120135

136+
command_body = diagnostic_command_body(
137+
node_rank, self.heartbeat_config, self.trace_config
138+
)
121139
if background:
122140
f.write(
123-
f'nohup bash -c "$cmd; sync" >> {host_output_file} 2>&1 & echo $! > {host_pid_file}\n'
141+
f'nohup bash -c "{command_body}" >> {host_output_file} 2>&1 & echo $! > {host_pid_file}\n'
124142
)
125143
else:
126144
f.write("set -o pipefail\n")
127-
f.write(f'bash -c "$cmd; sync" 2>&1 | tee -a {host_output_file}\n')
145+
f.write(f'bash -c "{command_body}" 2>&1 | tee -a {host_output_file}\n')
128146
f.write("\n")
129147
f.flush()
130148
os.fsync(f.fileno())
@@ -160,6 +178,10 @@ def generate_stop_script(self, host, node_rank):
160178
# TODO: This is a temporary fix. We need to find a better way to stop the job.
161179
f.write(" pkill -f 'torchrun'\n")
162180
f.write("fi\n")
181+
for line in self.heartbeat_config.stop_shell_lines(node_rank):
182+
f.write(f"{line}\n")
183+
for line in self.trace_config.stop_shell_lines(node_rank):
184+
f.write(f"{line}\n")
163185
f.write(f"{after_stop}\n")
164186
f.flush()
165187
os.fsync(f.fileno())

flagscale/runner/diagnostics.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Shared shell lifecycle helpers for opt-in runner diagnostics."""
16+
17+
from __future__ import annotations
18+
19+
from typing import Protocol
20+
21+
22+
class DiagnosticLaunchConfig(Protocol):
23+
enabled: bool
24+
25+
def command_exit_actions(self, node_rank: int) -> list[str]: ...
26+
27+
28+
def diagnostic_command_body(node_rank: int, *configs: DiagnosticLaunchConfig) -> str:
29+
"""Write every enabled monitor's completion marker without nesting shells."""
30+
31+
actions = [
32+
action
33+
for config in configs
34+
if config.enabled
35+
for action in config.command_exit_actions(node_rank)
36+
]
37+
if not actions:
38+
return "$cmd; sync"
39+
return f"$cmd; rc=\\$?; {'; '.join(dict.fromkeys(actions))}; sync; exit \\$rc"

flagscale/runner/heartbeat/config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,15 @@ def hardware_health_log_file(self, node_rank: int) -> str:
9999

100100
def training_command_body(self, node_rank: int) -> str:
101101
"""Run training and notify the node-zero heartbeat monitor on exit."""
102-
if not self.enabled:
102+
exit_actions = self.command_exit_actions(node_rank)
103+
if not exit_actions:
103104
return "$cmd; sync"
105+
return "$cmd; rc=\\$?; " + "; ".join(exit_actions) + "; sync; exit \\$rc"
106+
107+
def command_exit_actions(self, node_rank: int) -> list[str]:
108+
"""Return cleanup actions for a shared diagnostic command wrapper."""
109+
if not self.enabled:
110+
return []
104111
exit_actions: list[str] = []
105112
if node_rank == 0:
106113
completion_file = shlex.quote(self.completion_file)
@@ -111,9 +118,7 @@ def training_command_body(self, node_rank: int) -> str:
111118
f"if [ -f {health_pid_file} ]; then "
112119
f'kill \\"\\$(cat {health_pid_file})\\" 2>/dev/null || true; fi'
113120
)
114-
if not exit_actions:
115-
return "$cmd; sync"
116-
return "$cmd; rc=\\$?; " + "; ".join(exit_actions) + "; sync; exit \\$rc"
121+
return exit_actions
117122

118123
def shell_setup_lines(self, node_rank: int) -> list[str]:
119124
if not self.enabled:

flagscale/runner/launcher/launcher_ssh.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,11 @@ def _get_runner_cmd_train(
148148
del runner_args["perf_max_log_files"]
149149
if "perf_model_type" in runner_args:
150150
del runner_args["perf_model_type"]
151-
# Heartbeat is consumed by FlagScale and is not a torchrun option.
151+
# Diagnostics are consumed by FlagScale and are not torchrun options.
152152
if "heartbeat" in runner_args:
153153
del runner_args["heartbeat"]
154+
if "tracing" in runner_args:
155+
del runner_args["tracing"]
154156
runner_args["rdzv_id"] = rdzv_id
155157
# runner_args["master_addr"] = master_addr
156158
# runner_args["master_port"] = master_port

flagscale/runner/runner_train.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
from hydra.core.hydra_config import HydraConfig
2222
from omegaconf import DictConfig, OmegaConf
2323

24+
from flagscale.runner.diagnostics import diagnostic_command_body
2425
from flagscale.runner.elastic.monitor_service import MonitorService
2526
from flagscale.runner.heartbeat.config import (
2627
HeartbeatLaunchConfig,
2728
prepare_heartbeat_launch_config,
2829
)
2930
from flagscale.runner.runner_base_legacy import JobStatus, RunnerBase
31+
from flagscale.runner.tracing.config import TraceLaunchConfig, prepare_trace_launch_config
3032
from flagscale.runner.utils import (
3133
find_latest_stdout_log,
3234
flatten_dict_to_args,
@@ -212,6 +214,8 @@ def _get_runner_cmd_train(
212214
del runner_args["enable_monitoring"]
213215
if "heartbeat" in runner_args:
214216
del runner_args["heartbeat"]
217+
if "tracing" in runner_args:
218+
del runner_args["tracing"]
215219
runner_args["rdzv_id"] = rdzv_id
216220
# runner_args["master_addr"] = master_addr
217221
# runner_args["master_port"] = master_port
@@ -245,8 +249,10 @@ def _generate_run_script_train(
245249
pkg_dir=None,
246250
enable_monitoring=False,
247251
heartbeat_config=None,
252+
trace_config=None,
248253
):
249254
heartbeat_config = heartbeat_config or HeartbeatLaunchConfig(enabled=False)
255+
trace_config = trace_config or TraceLaunchConfig(enabled=False)
250256
system_config = config.train.system
251257
logging_config = config.train.system.logging
252258

@@ -293,6 +299,10 @@ def _generate_run_script_train(
293299
f.write(f"{line}\n")
294300
if heartbeat_config.enabled:
295301
f.write("\n")
302+
for line in trace_config.shell_setup_lines(node_rank):
303+
f.write(f"{line}\n")
304+
if trace_config.enabled:
305+
f.write("\n")
296306
f.write(f'cmd="{cmd}"\n')
297307
f.write("\n")
298308
if enable_monitoring:
@@ -315,7 +325,7 @@ def _generate_run_script_train(
315325
f.write(f'echo "Monitor service started in background for {host} (node {node_rank})"\n')
316326
f.write("\n")
317327

318-
command_body = heartbeat_config.training_command_body(node_rank)
328+
command_body = diagnostic_command_body(node_rank, heartbeat_config, trace_config)
319329
if background:
320330
f.write(
321331
f'nohup bash -c "{command_body}" >> {host_output_file} 2>&1 & echo $! > {host_pid_file}\n'
@@ -331,8 +341,9 @@ def _generate_run_script_train(
331341
return host_run_script_file
332342

333343

334-
def _generate_stop_script_train(config, host, node_rank, heartbeat_config=None):
344+
def _generate_stop_script_train(config, host, node_rank, heartbeat_config=None, trace_config=None):
335345
heartbeat_config = heartbeat_config or HeartbeatLaunchConfig(enabled=False)
346+
trace_config = trace_config or TraceLaunchConfig(enabled=False)
336347
if getattr(config, "train", None):
337348
logging_config = config.train.system.logging
338349
else:
@@ -362,6 +373,8 @@ def _generate_stop_script_train(config, host, node_rank, heartbeat_config=None):
362373
f.write("fi\n")
363374
for line in heartbeat_config.stop_shell_lines(node_rank):
364375
f.write(f"{line}\n")
376+
for line in trace_config.stop_shell_lines(node_rank):
377+
f.write(f"{line}\n")
365378
f.write(f"{after_stop}\n")
366379
f.flush()
367380
os.fsync(f.fileno())
@@ -426,6 +439,9 @@ def _prepare(self):
426439
raise ValueError(f"Unsupported backend: {self.config.experiment.task.backend}")
427440
self.rdzv_id = datetime.now().strftime("%Y%m%d_%H%M%S.%f")
428441
self.heartbeat_config = prepare_heartbeat_launch_config(self.config, self.rdzv_id)
442+
self.trace_config = prepare_trace_launch_config(
443+
self.config, self.rdzv_id, self.heartbeat_config
444+
)
429445
self.user_envs = self.config.experiment.get("envs", {})
430446
self.user_script = self.config.experiment.task.entrypoint
431447
self.resources = parse_hostfile(self.config.experiment.runner.get("hostfile", None))
@@ -484,6 +500,7 @@ def _run_each(
484500
pkg_dir=node_specific_config.get("build_dir", None),
485501
enable_monitoring=enable_monitoring,
486502
heartbeat_config=self.heartbeat_config,
503+
trace_config=self.trace_config,
487504
)
488505

489506
if host != "localhost":
@@ -616,6 +633,7 @@ def _stop_each(self, host, node_rank):
616633
host,
617634
node_rank,
618635
self.heartbeat_config,
636+
self.trace_config,
619637
)
620638
logging_config = self.config.train.system.logging
621639

@@ -889,6 +907,9 @@ def _prepare(self):
889907
_update_config_train(self.config)
890908
self.rdzv_id = datetime.now().strftime("%Y%m%d_%H%M%S.%f")
891909
self.heartbeat_config = prepare_heartbeat_launch_config(self.config, self.rdzv_id)
910+
self.trace_config = prepare_trace_launch_config(
911+
self.config, self.rdzv_id, self.heartbeat_config
912+
)
892913
if self.config.experiment.task.backend == "megatron":
893914
self.user_args = _get_args_megatron(self.config)
894915
logger.info("\n************** configuration ***********")
@@ -922,6 +943,7 @@ def _run_each(
922943
cmd,
923944
background=background,
924945
heartbeat_config=self.heartbeat_config,
946+
trace_config=self.trace_config,
925947
)
926948

927949
run_local_command(f"bash {host_run_script_file}", dryrun)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2026 FlagOS Contributors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""CPU-side NCCL tracing and hang detection utilities."""
16+
17+
from .analyzer import Finding, TraceAnalyzer
18+
19+
__all__ = [
20+
"Finding",
21+
"TraceAnalyzer",
22+
]

0 commit comments

Comments
 (0)