Skip to content

Commit 92b1481

Browse files
lzy-eduwanglei19991004zhaoyinglia
authored
Straggler detection rebased (flagos-ai#1215)
### PR Category Train ### PR Types New Features,Improvements ### PR Description This PR ports the straggler detection integration from flagos-ai#1179 onto the latest main branch. It includes: - Straggler detection runner components - Integration with Megatron training arguments and training flow - Unit tests and smoke test utilities from the original PR - Additional updates to align the straggler detection arguments with the latest main branch Based on: - flagos-ai#1179 --------- Co-authored-by: wanglei19991004 <13453558035@163.com> Co-authored-by: zhaoyingli <86812880+zhaoyinglia@users.noreply.github.com>
1 parent d9a6fdf commit 92b1481

21 files changed

Lines changed: 2370 additions & 24 deletions

flagscale/runner/backend/backend_megatron.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ def generate_run_script(
107107
f.write(f"mkdir -p {system_config.logging.details_dir}\n")
108108
f.write(f"mkdir -p {system_config.logging.tensorboard_dir}\n")
109109
f.write(f"mkdir -p {system_config.logging.wandb_save_dir}\n")
110+
f.write(f"mkdir -p {system_config.logging.straggler_dir}\n")
111+
if system_config.get("straggler_log_dir", None):
112+
f.write(f"mkdir -p {system_config.straggler_log_dir}\n")
110113
if system_config.get("perf_log_dir", None):
111114
f.write(f"mkdir -p {system_config.perf_log_dir}\n")
112115
f.write("\n")

flagscale/runner/runner_train.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def _get_args_megatron(config: DictConfig):
4747
new_config_dict.update(config_dict["model"])
4848
new_config_dict.update(config_dict["data"])
4949

50-
ignore_keys = ["log_dir", "details_dir", "scripts_dir", "pids_dir"]
50+
ignore_keys = ["log_dir", "details_dir", "scripts_dir", "pids_dir", "straggler_dir"]
5151
# Flatten the dictionary to a list of arguments
5252
args = flatten_dict_to_args(new_config_dict, ignore_keys)
5353

@@ -118,6 +118,17 @@ def _update_config_train(config: DictConfig):
118118
else os.path.join(exp_dir, "wandb")
119119
)
120120

121+
system.logging.straggler_dir = (
122+
resolve_path(system.logging.straggler_dir, "logging.straggler_dir")
123+
if system.logging.get("straggler_dir", None)
124+
else os.path.join(log_dir, "straggler")
125+
)
126+
system.straggler_log_dir = (
127+
resolve_path(system.straggler_log_dir, "system.straggler_log_dir")
128+
if system.get("straggler_log_dir", None)
129+
else system.logging.straggler_dir
130+
)
131+
121132
# Tokenizer file paths — resolve before passing to the training subprocess,
122133
# which may run with a different cwd (e.g. site-packages when pip-installed).
123134
data = config.train.get("data", None)
@@ -248,6 +259,9 @@ def _generate_run_script_train(
248259
f.write(f"mkdir -p {system_config.logging.details_dir}\n")
249260
f.write(f"mkdir -p {system_config.logging.tensorboard_dir}\n")
250261
f.write(f"mkdir -p {system_config.logging.wandb_save_dir}\n")
262+
f.write(f"mkdir -p {system_config.logging.straggler_dir}\n")
263+
if system_config.get("straggler_log_dir", None):
264+
f.write(f"mkdir -p {system_config.straggler_log_dir}\n")
251265
f.write("\n")
252266
f.write(f"cd {pkg_dir}\n")
253267
f.write("\n")
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Straggler on Metax C550
2+
3+
## Scope
4+
5+
This note documents the current `straggler` integration for the Metax C550 training path on `main-legacy`.
6+
7+
Important:
8+
9+
- The active branch in use is `main-legacy`.
10+
- The current runnable path is still the legacy training stack.
11+
- The new launcher path `flagscale/runner/launcher/launcher_ssh.py` is not used here.
12+
13+
## Current Code Path
14+
15+
- FlagScale detector logic:
16+
- `flagscale/runner/straggler/`
17+
- Train-side integration:
18+
- `flagscale/train/train.py`
19+
20+
This is a FlagScale-side detector layered on top of the existing Megatron training loop. It does not replace Megatron's native `log_straggler`.
21+
22+
## Metax-Specific Notes
23+
24+
- The implementation avoids new CUDA-only assumptions.
25+
- GPU event profiling is not required by default.
26+
- The practical validation path on Metax should use the mini Aquila config that already passed smoke training.
27+
28+
## Known Pitfalls
29+
30+
- Do not validate this first on the original full 7B config. The practical path we already stabilized on Metax is the mini Aquila config.
31+
- Do not reuse old checkpoints while testing. Use a fresh `exp_dir` and force a missing `checkpoint.load`.
32+
- If the straggler keys are not present in YAML, pass them with `++`.
33+
34+
## Smoke Test
35+
36+
Run from the generated Metax build tree:
37+
38+
```bash
39+
cd /workspace/muxi-flagscale-legacy/build/Metax_C550/muxi-flagscale-legacy
40+
41+
TS=$(date +%Y%m%d_%H%M%S)
42+
43+
python run.py \
44+
--config-path ./examples/aquila/conf \
45+
--config-name train \
46+
action=test \
47+
experiment.exp_dir=/workspace/exp/aquila_straggler_smoke_${TS} \
48+
train.system.checkpoint.load=/workspace/exp/__no_ckpt__/does_not_exist \
49+
train.system.checkpoint.save=/workspace/exp/aquila_straggler_smoke_${TS}/checkpoints \
50+
train.system.use_flash_attn=false \
51+
train.model.attention_backend=unfused \
52+
train.model.num_layers=8 \
53+
train.model.hidden_size=1024 \
54+
train.model.num_attention_heads=16 \
55+
train.model.seq_length=512 \
56+
train.model.max_position_embeddings=512 \
57+
train.model.multiple_of=128 \
58+
train.model.micro_batch_size=1 \
59+
train.model.global_batch_size=8 \
60+
train.model.train_samples=16 \
61+
++train.system.enable_straggler_detection=true \
62+
++train.system.straggler_report_interval=2 \
63+
++train.system.straggler_threshold=1.5 \
64+
++train.system.straggler_warmup_steps=0
65+
```
66+
67+
## Expected Result
68+
69+
- Training starts from random initialization.
70+
- `iteration 1/2` and `iteration 2/2` both complete.
71+
- A straggler report is printed near the end of the short run.
72+
- Report files are written under:
73+
74+
```bash
75+
/workspace/exp/aquila_straggler_smoke_${TS}/logs/straggler
76+
```
77+
78+
## Full Run Example
79+
80+
```bash
81+
TS=$(date +%Y%m%d_%H%M%S)
82+
83+
python run.py \
84+
--config-path ./examples/aquila/conf \
85+
--config-name train \
86+
action=run \
87+
experiment.exp_dir=/workspace/exp/aquila_straggler_run_${TS} \
88+
train.system.checkpoint.load=/workspace/exp/__no_ckpt__/does_not_exist \
89+
train.system.checkpoint.save=/workspace/exp/aquila_straggler_run_${TS}/checkpoints \
90+
train.system.use_flash_attn=false \
91+
train.model.attention_backend=unfused \
92+
train.model.num_layers=8 \
93+
train.model.hidden_size=1024 \
94+
train.model.num_attention_heads=16 \
95+
train.model.seq_length=512 \
96+
train.model.max_position_embeddings=512 \
97+
train.model.multiple_of=128 \
98+
train.model.micro_batch_size=1 \
99+
train.model.global_batch_size=8 \
100+
train.model.train_samples=1600 \
101+
++train.system.enable_straggler_detection=true \
102+
++train.system.straggler_report_interval=20 \
103+
++train.system.straggler_threshold=1.5 \
104+
++train.system.straggler_warmup_steps=10
105+
```
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""FlagScale straggler detection utilities."""
2+
3+
from .comm import CommProfiler, CommStatsCollector, GlooCommHook, NCCLCommHook
4+
from .config import StragglerConfig
5+
from .detector import StragglerDetector
6+
from .healthcheck import ElasticTrainingHealthChecker, NetworkHealthChecker
7+
from .report import StragglerReport
8+
from .section import (
9+
OptionalSectionContext,
10+
SectionContext,
11+
SectionProfiler,
12+
create_section_decorator,
13+
)
14+
15+
__all__ = [
16+
"CommProfiler",
17+
"CommStatsCollector",
18+
"ElasticTrainingHealthChecker",
19+
"GlooCommHook",
20+
"NCCLCommHook",
21+
"NetworkHealthChecker",
22+
"OptionalSectionContext",
23+
"SectionContext",
24+
"SectionProfiler",
25+
"StragglerConfig",
26+
"StragglerDetector",
27+
"StragglerReport",
28+
"create_section_decorator",
29+
]

flagscale/runner/straggler/comm.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""Communication monitoring helpers for straggler analysis."""
2+
3+
import time
4+
from collections import defaultdict
5+
from typing import Any
6+
7+
8+
class CommStatsCollector:
9+
"""Collect basic communication timings."""
10+
11+
def __init__(self, enabled: bool = True):
12+
self.enabled = enabled
13+
self.operation_stats: dict[str, dict[str, Any]] = defaultdict(
14+
lambda: {
15+
"count": 0,
16+
"total_time": 0.0,
17+
"min_time": float("inf"),
18+
"max_time": 0.0,
19+
"rank_times": defaultdict(list),
20+
}
21+
)
22+
self.backend = "unknown"
23+
self.world_size = 1
24+
self.rank = 0
25+
26+
def set_backend_info(self, backend: str, world_size: int, rank: int):
27+
self.backend = backend
28+
self.world_size = world_size
29+
self.rank = rank
30+
31+
def record_operation(
32+
self,
33+
op_type: str,
34+
op_name: str,
35+
start_time: float,
36+
end_time: float,
37+
data_size: int | None = None,
38+
target_ranks: list | None = None,
39+
):
40+
if not self.enabled:
41+
return
42+
43+
duration = end_time - start_time
44+
key = f"{op_type}_{op_name}"
45+
stats = self.operation_stats[key]
46+
stats["count"] += 1
47+
stats["total_time"] += duration
48+
stats["min_time"] = min(stats["min_time"], duration)
49+
stats["max_time"] = max(stats["max_time"], duration)
50+
stats["rank_times"][self.rank].append(duration)
51+
52+
if data_size is not None:
53+
stats["total_data_size"] = stats.get("total_data_size", 0) + data_size
54+
55+
if target_ranks is not None:
56+
stats["target_ranks"] = target_ranks
57+
58+
def get_operation_stats(self, op_type: str, op_name: str) -> dict[str, Any]:
59+
return self.operation_stats[f"{op_type}_{op_name}"].copy()
60+
61+
def get_all_stats(self) -> dict[str, dict[str, Any]]:
62+
return dict(self.operation_stats)
63+
64+
def get_straggler_operations(self, threshold: float = 2.0) -> list:
65+
stragglers = []
66+
for op_key, stats in self.operation_stats.items():
67+
if stats["count"] == 0:
68+
continue
69+
avg_time = stats["total_time"] / stats["count"]
70+
max_time = stats["max_time"]
71+
if avg_time > 0 and max_time / avg_time >= threshold:
72+
stragglers.append(
73+
{
74+
"operation": op_key,
75+
"avg_time": avg_time,
76+
"max_time": max_time,
77+
"slowdown_ratio": max_time / avg_time,
78+
"count": stats["count"],
79+
}
80+
)
81+
return stragglers
82+
83+
84+
class NCCLCommHook:
85+
"""Wrap NCCL collectives with timing."""
86+
87+
def __init__(self, collector: CommStatsCollector):
88+
self.collector = collector
89+
90+
def wrap_all_reduce(self, op_func):
91+
def wrapped(*args, **kwargs):
92+
start_time = time.perf_counter()
93+
result = op_func(*args, **kwargs)
94+
end_time = time.perf_counter()
95+
self.collector.record_operation(
96+
"all_reduce",
97+
"default",
98+
start_time,
99+
end_time,
100+
)
101+
return result
102+
103+
return wrapped
104+
105+
def wrap_broadcast(self, op_func):
106+
def wrapped(*args, **kwargs):
107+
start_time = time.perf_counter()
108+
result = op_func(*args, **kwargs)
109+
end_time = time.perf_counter()
110+
self.collector.record_operation(
111+
"broadcast",
112+
"default",
113+
start_time,
114+
end_time,
115+
)
116+
return result
117+
118+
return wrapped
119+
120+
121+
class GlooCommHook:
122+
"""Wrap Gloo collectives with timing."""
123+
124+
def __init__(self, collector: CommStatsCollector):
125+
self.collector = collector
126+
127+
def wrap_all_reduce(self, op_func):
128+
def wrapped(*args, **kwargs):
129+
start_time = time.perf_counter()
130+
result = op_func(*args, **kwargs)
131+
end_time = time.perf_counter()
132+
self.collector.record_operation(
133+
"all_reduce",
134+
"default",
135+
start_time,
136+
end_time,
137+
)
138+
return result
139+
140+
return wrapped
141+
142+
143+
class CommProfiler:
144+
"""Backend-aware communication profiler."""
145+
146+
def __init__(self, backend: str = "auto", enabled: bool = True):
147+
self.collector = CommStatsCollector(enabled=enabled)
148+
self.hooks = {}
149+
150+
if backend == "auto":
151+
backend = self._detect_backend()
152+
153+
if backend == "nccl":
154+
self.hooks["nccl"] = NCCLCommHook(self.collector)
155+
elif backend == "gloo":
156+
self.hooks["gloo"] = GlooCommHook(self.collector)
157+
158+
self.collector.set_backend_info(backend, 1, 0)
159+
160+
def _detect_backend(self) -> str:
161+
try:
162+
import torch
163+
164+
if torch.cuda.is_available():
165+
return "nccl"
166+
except ImportError:
167+
pass
168+
return "gloo"
169+
170+
def wrap_operation(self, op_type: str, op_func):
171+
backend = self.collector.backend
172+
if backend in self.hooks:
173+
if op_type == "all_reduce":
174+
return self.hooks[backend].wrap_all_reduce(op_func)
175+
if op_type == "broadcast" and hasattr(self.hooks[backend], "wrap_broadcast"):
176+
return self.hooks[backend].wrap_broadcast(op_func)
177+
return op_func
178+
179+
def record_custom_operation(
180+
self,
181+
op_type: str,
182+
op_name: str,
183+
start_time: float,
184+
end_time: float,
185+
data_size: int | None = None,
186+
):
187+
self.collector.record_operation(
188+
op_type,
189+
op_name,
190+
start_time,
191+
end_time,
192+
data_size=data_size,
193+
)

0 commit comments

Comments
 (0)