Skip to content

Commit aa12c1a

Browse files
committed
add train config unit cases
1 parent decb039 commit aa12c1a

6 files changed

Lines changed: 1034 additions & 0 deletions

File tree

tests/unit_tests/train/megatron/__init__.py

Whitespace-only changes.

tests/unit_tests/train/megatron/config/__init__.py

Whitespace-only changes.
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
"""
2+
Unit tests for flagscale/train/megatron/training/config/*.py
3+
4+
Coverage targets:
5+
- RNGConfig, ProfilingConfig, DistributedInitConfig (common_config.py)
6+
- TrainingConfig, ValidationConfig, SchedulerConfig, LoggerConfig, CheckpointConfig (training_config.py)
7+
- RerunStateMachineConfig, StragglerDetectionConfig (resilience_config.py)
8+
9+
"""
10+
11+
import importlib.util
12+
import os
13+
import pathlib
14+
import unittest
15+
from dataclasses import fields
16+
17+
_CONFIG_DIR = (
18+
pathlib.Path(__file__).parents[5] / "flagscale/train/megatron/training/config"
19+
)
20+
21+
22+
def _load_module(filename):
23+
path = _CONFIG_DIR / filename
24+
spec = importlib.util.spec_from_file_location(
25+
filename.replace(".py", ""), str(path)
26+
)
27+
mod = importlib.util.module_from_spec(spec)
28+
spec.loader.exec_module(mod)
29+
return mod
30+
31+
32+
_common = _load_module("common_config.py")
33+
_training = _load_module("training_config.py")
34+
_resilience = _load_module("resilience_config.py")
35+
36+
RNGConfig = _common.RNGConfig
37+
ProfilingConfig = _common.ProfilingConfig
38+
DistributedInitConfig = _common.DistributedInitConfig
39+
40+
TrainingConfig = _training.TrainingConfig
41+
ValidationConfig = _training.ValidationConfig
42+
SchedulerConfig = _training.SchedulerConfig
43+
LoggerConfig = _training.LoggerConfig
44+
45+
RerunStateMachineConfig = _resilience.RerunStateMachineConfig
46+
StragglerDetectionConfig = _resilience.StragglerDetectionConfig
47+
48+
49+
class TestRNGConfig(unittest.TestCase):
50+
51+
def test_defaults(self):
52+
cfg = RNGConfig()
53+
self.assertEqual(cfg.seed, 1234)
54+
self.assertFalse(cfg.te_rng_tracker)
55+
self.assertFalse(cfg.inference_rng_tracker)
56+
self.assertFalse(cfg.data_parallel_random_init)
57+
58+
def test_custom_seed(self):
59+
cfg = RNGConfig(seed=42)
60+
self.assertEqual(cfg.seed, 42)
61+
62+
def test_enable_all_flags(self):
63+
cfg = RNGConfig(
64+
seed=0,
65+
te_rng_tracker=True,
66+
inference_rng_tracker=True,
67+
data_parallel_random_init=True,
68+
)
69+
self.assertTrue(cfg.te_rng_tracker)
70+
self.assertTrue(cfg.inference_rng_tracker)
71+
self.assertTrue(cfg.data_parallel_random_init)
72+
73+
def test_kw_only_rejects_positional(self):
74+
with self.assertRaises(TypeError):
75+
RNGConfig(9999) # positional not allowed
76+
77+
78+
class TestProfilingConfig(unittest.TestCase):
79+
80+
def test_defaults(self):
81+
cfg = ProfilingConfig()
82+
self.assertFalse(cfg.use_nsys_profiler)
83+
self.assertEqual(cfg.profile_step_start, 10)
84+
self.assertEqual(cfg.profile_step_end, 12)
85+
self.assertEqual(cfg.profile_ranks, [])
86+
self.assertEqual(cfg.memory_snapshot_path, "snapshot.pickle")
87+
self.assertFalse(cfg.record_memory_history)
88+
89+
def test_custom_step_range(self):
90+
cfg = ProfilingConfig(profile_step_start=5, profile_step_end=20)
91+
self.assertEqual(cfg.profile_step_start, 5)
92+
self.assertEqual(cfg.profile_step_end, 20)
93+
94+
def test_custom_profile_ranks(self):
95+
cfg = ProfilingConfig(profile_ranks=[0, 1, 2])
96+
self.assertEqual(cfg.profile_ranks, [0, 1, 2])
97+
98+
def test_profile_ranks_default_is_independent_per_instance(self):
99+
a = ProfilingConfig()
100+
b = ProfilingConfig()
101+
a.profile_ranks.append(99)
102+
self.assertEqual(
103+
b.profile_ranks, [], "default_factory must create separate list"
104+
)
105+
106+
def test_pytorch_profiler_flags(self):
107+
cfg = ProfilingConfig(
108+
use_pytorch_profiler=True,
109+
pytorch_profiler_collect_shapes=True,
110+
pytorch_profiler_collect_callstack=True,
111+
)
112+
self.assertTrue(cfg.use_pytorch_profiler)
113+
self.assertTrue(cfg.pytorch_profiler_collect_shapes)
114+
self.assertTrue(cfg.pytorch_profiler_collect_callstack)
115+
116+
117+
class TestDistributedInitConfig(unittest.TestCase):
118+
119+
def tearDown(self):
120+
os.environ.pop("LOCAL_RANK", None)
121+
122+
def test_defaults(self):
123+
os.environ.pop("LOCAL_RANK", None)
124+
cfg = DistributedInitConfig()
125+
self.assertEqual(cfg.distributed_backend, "nccl")
126+
self.assertEqual(cfg.distributed_timeout_minutes, 10)
127+
self.assertFalse(cfg.use_megatron_fsdp)
128+
self.assertFalse(cfg.use_torch_fsdp2)
129+
self.assertIsNone(cfg.nccl_communicator_config_path)
130+
131+
def test_local_rank_from_env(self):
132+
os.environ["LOCAL_RANK"] = "3"
133+
cfg = DistributedInitConfig()
134+
self.assertEqual(cfg.local_rank, 3)
135+
136+
def test_local_rank_default_zero_when_env_unset(self):
137+
os.environ.pop("LOCAL_RANK", None)
138+
cfg = DistributedInitConfig()
139+
self.assertEqual(cfg.local_rank, 0)
140+
141+
def test_local_rank_override(self):
142+
os.environ["LOCAL_RANK"] = "7"
143+
cfg = DistributedInitConfig(local_rank=2)
144+
# explicit kwarg overrides env
145+
self.assertEqual(cfg.local_rank, 2)
146+
147+
def test_valid_backends(self):
148+
for backend in ("nccl", "gloo", "flagcx"):
149+
cfg = DistributedInitConfig(distributed_backend=backend)
150+
self.assertEqual(cfg.distributed_backend, backend)
151+
152+
def test_nccl_communicator_config_path(self):
153+
cfg = DistributedInitConfig(nccl_communicator_config_path="/tmp/nccl.yaml")
154+
self.assertEqual(cfg.nccl_communicator_config_path, "/tmp/nccl.yaml")
155+
156+
def test_fsdp_flags(self):
157+
cfg = DistributedInitConfig(use_megatron_fsdp=True)
158+
self.assertTrue(cfg.use_megatron_fsdp)
159+
cfg2 = DistributedInitConfig(use_torch_fsdp2=True)
160+
self.assertTrue(cfg2.use_torch_fsdp2)
161+
162+
163+
class TestTrainingConfig(unittest.TestCase):
164+
165+
def test_defaults(self):
166+
cfg = TrainingConfig()
167+
self.assertIsNone(cfg.micro_batch_size)
168+
self.assertIsNone(cfg.global_batch_size)
169+
self.assertIsNone(cfg.rampup_batch_size)
170+
self.assertIsNone(cfg.train_iters)
171+
self.assertIsNone(cfg.train_samples)
172+
self.assertEqual(cfg.iterations_to_skip, [])
173+
self.assertFalse(cfg.manual_gc)
174+
self.assertEqual(cfg.manual_gc_interval, 0)
175+
self.assertTrue(cfg.manual_gc_eval)
176+
177+
def test_set_train_iters(self):
178+
cfg = TrainingConfig(micro_batch_size=4, global_batch_size=32, train_iters=1000)
179+
self.assertEqual(cfg.train_iters, 1000)
180+
self.assertEqual(cfg.micro_batch_size, 4)
181+
182+
def test_iterations_to_skip_default_is_independent(self):
183+
a = TrainingConfig()
184+
b = TrainingConfig()
185+
a.iterations_to_skip.append(5)
186+
self.assertEqual(b.iterations_to_skip, [])
187+
188+
def test_empty_unused_memory_level_choices(self):
189+
for level in (0, 1, 2):
190+
cfg = TrainingConfig(empty_unused_memory_level=level)
191+
self.assertEqual(cfg.empty_unused_memory_level, level)
192+
193+
def test_exit_signal_default(self):
194+
import signal
195+
196+
cfg = TrainingConfig()
197+
self.assertEqual(cfg.exit_signal, signal.SIGTERM)
198+
199+
def test_rampup_batch_size_list(self):
200+
cfg = TrainingConfig(rampup_batch_size=[16, 8, 300000])
201+
self.assertEqual(cfg.rampup_batch_size, [16, 8, 300000])
202+
203+
204+
class TestValidationConfig(unittest.TestCase):
205+
206+
def test_defaults(self):
207+
cfg = ValidationConfig()
208+
self.assertEqual(cfg.eval_iters, 100)
209+
self.assertIsNone(cfg.eval_interval)
210+
self.assertFalse(cfg.skip_train)
211+
self.assertFalse(cfg.test_mode)
212+
self.assertFalse(cfg.full_validation)
213+
self.assertFalse(cfg.multiple_validation_sets)
214+
215+
def test_skip_train(self):
216+
cfg = ValidationConfig(skip_train=True)
217+
self.assertTrue(cfg.skip_train)
218+
219+
def test_eval_iters_none(self):
220+
cfg = ValidationConfig(eval_iters=None)
221+
self.assertIsNone(cfg.eval_iters)
222+
223+
def test_custom_eval_interval(self):
224+
cfg = ValidationConfig(eval_interval=500)
225+
self.assertEqual(cfg.eval_interval, 500)
226+
227+
228+
class TestSchedulerConfig(unittest.TestCase):
229+
230+
def test_defaults(self):
231+
cfg = SchedulerConfig()
232+
self.assertEqual(cfg.lr_decay_style, "linear")
233+
self.assertEqual(cfg.lr_warmup_iters, 0)
234+
self.assertEqual(cfg.lr_warmup_samples, 0)
235+
self.assertEqual(cfg.lr_warmup_init, 0.0)
236+
self.assertIsNone(cfg.lr_warmup_fraction)
237+
self.assertIsNone(cfg.lr_decay_iters)
238+
self.assertIsNone(cfg.lr_decay_samples)
239+
240+
def test_init_false_fields_are_none(self):
241+
"""lr_decay_steps, lr_warmup_steps, wd_incr_steps are init=False → always None at creation."""
242+
cfg = SchedulerConfig()
243+
self.assertIsNone(cfg.lr_decay_steps)
244+
self.assertIsNone(cfg.lr_warmup_steps)
245+
self.assertIsNone(cfg.wd_incr_steps)
246+
247+
def test_init_false_fields_not_in_init_signature(self):
248+
"""init=False fields must NOT be accepted as constructor kwargs."""
249+
field_map = {f.name: f for f in fields(SchedulerConfig)}
250+
for name in (
251+
"lr_decay_steps",
252+
"lr_warmup_steps",
253+
"wd_incr_steps",
254+
"wsd_decay_steps",
255+
):
256+
self.assertFalse(field_map[name].init, f"{name} should have init=False")
257+
258+
def test_valid_lr_decay_styles(self):
259+
for style in ("constant", "linear", "cosine", "inverse-square-root", "WSD"):
260+
cfg = SchedulerConfig(lr_decay_style=style)
261+
self.assertEqual(cfg.lr_decay_style, style)
262+
263+
def test_lr_warmup_fraction(self):
264+
cfg = SchedulerConfig(lr_warmup_fraction=0.01)
265+
self.assertAlmostEqual(cfg.lr_warmup_fraction, 0.01)
266+
267+
def test_weight_decay_incr_style(self):
268+
for style in ("constant", "linear", "cosine"):
269+
cfg = SchedulerConfig(weight_decay_incr_style=style)
270+
self.assertEqual(cfg.weight_decay_incr_style, style)
271+
272+
273+
class TestLoggerConfig(unittest.TestCase):
274+
275+
def test_defaults(self):
276+
cfg = LoggerConfig()
277+
self.assertEqual(cfg.log_interval, 100)
278+
self.assertEqual(cfg.timing_log_level, 0)
279+
self.assertEqual(cfg.timing_log_option, "minmax")
280+
self.assertIsNone(cfg.tensorboard_dir)
281+
self.assertEqual(cfg.tensorboard_queue_size, 1000)
282+
self.assertFalse(cfg.log_params_norm)
283+
self.assertIsNone(cfg.wandb_project)
284+
285+
def test_valid_timing_log_levels(self):
286+
for level in (0, 1, 2):
287+
cfg = LoggerConfig(timing_log_level=level)
288+
self.assertEqual(cfg.timing_log_level, level)
289+
290+
def test_tensorboard_dir(self):
291+
cfg = LoggerConfig(tensorboard_dir="/tmp/tb")
292+
self.assertEqual(cfg.tensorboard_dir, "/tmp/tb")
293+
294+
def test_timing_log_options(self):
295+
for opt in ("max", "minmax", "all"):
296+
cfg = LoggerConfig(timing_log_option=opt)
297+
self.assertEqual(cfg.timing_log_option, opt)
298+
299+
def test_memory_keys(self):
300+
keys = {"reserved_bytes.all.peak": "peak_reserved"}
301+
cfg = LoggerConfig(memory_keys=keys)
302+
self.assertEqual(cfg.memory_keys, keys)
303+
304+
305+
class TestRerunStateMachineConfig(unittest.TestCase):
306+
307+
def test_defaults(self):
308+
cfg = RerunStateMachineConfig()
309+
self.assertEqual(cfg.error_injection_rate, 0)
310+
self.assertEqual(cfg.error_injection_type, "transient_error")
311+
self.assertEqual(cfg.rerun_mode, "validate_results")
312+
self.assertTrue(cfg.check_for_nan_in_loss)
313+
self.assertFalse(cfg.check_for_spiky_loss)
314+
315+
def test_valid_error_injection_types(self):
316+
for t in ("correct_result", "transient_error", "persistent_error"):
317+
cfg = RerunStateMachineConfig(error_injection_type=t)
318+
self.assertEqual(cfg.error_injection_type, t)
319+
320+
def test_valid_rerun_modes(self):
321+
for mode in ("disabled", "validate_results", "report_stats"):
322+
cfg = RerunStateMachineConfig(rerun_mode=mode)
323+
self.assertEqual(cfg.rerun_mode, mode)
324+
325+
def test_disable_nan_check(self):
326+
cfg = RerunStateMachineConfig(check_for_nan_in_loss=False)
327+
self.assertFalse(cfg.check_for_nan_in_loss)
328+
329+
def test_enable_spiky_loss_check(self):
330+
cfg = RerunStateMachineConfig(check_for_spiky_loss=True)
331+
self.assertTrue(cfg.check_for_spiky_loss)
332+
333+
def test_error_injection_rate(self):
334+
cfg = RerunStateMachineConfig(error_injection_rate=1000)
335+
self.assertEqual(cfg.error_injection_rate, 1000)
336+
337+
338+
class TestStragglerDetectionConfig(unittest.TestCase):
339+
340+
def test_defaults(self):
341+
cfg = StragglerDetectionConfig()
342+
self.assertFalse(cfg.log_straggler)
343+
self.assertEqual(cfg.straggler_ctrlr_port, 65535)
344+
self.assertEqual(cfg.straggler_minmax_count, 1)
345+
self.assertFalse(cfg.disable_straggler_on_startup)
346+
347+
def test_enable_logging(self):
348+
cfg = StragglerDetectionConfig(log_straggler=True)
349+
self.assertTrue(cfg.log_straggler)
350+
351+
def test_custom_port(self):
352+
cfg = StragglerDetectionConfig(straggler_ctrlr_port=8080)
353+
self.assertEqual(cfg.straggler_ctrlr_port, 8080)
354+
355+
def test_disable_on_startup(self):
356+
cfg = StragglerDetectionConfig(disable_straggler_on_startup=True)
357+
self.assertTrue(cfg.disable_straggler_on_startup)
358+
359+
def test_minmax_count(self):
360+
cfg = StragglerDetectionConfig(straggler_minmax_count=4)
361+
self.assertEqual(cfg.straggler_minmax_count, 4)
362+
363+
364+
if __name__ == "__main__":
365+
unittest.main()

tests/unit_tests/train/megatron/training/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)