Skip to content

Commit e39c7be

Browse files
committed
add runner & backend unit cases
1 parent 0ff6c52 commit e39c7be

4 files changed

Lines changed: 1132 additions & 0 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import os
2+
from types import SimpleNamespace
3+
4+
import pytest
5+
from omegaconf import OmegaConf
6+
7+
from flagscale.runner.backend import backend_native_compress, backend_verl
8+
from flagscale.runner.backend.backend_megatron import MegatronBackend
9+
from flagscale.runner.backend.backend_native_compress import NativeCompressBackend
10+
from flagscale.runner.backend.backend_verl import VerlBackend
11+
12+
13+
def _megatron_config(tmp_path):
14+
return OmegaConf.create(
15+
{
16+
"experiment": {
17+
"exp_dir": str(tmp_path / "exp"),
18+
"task": {
19+
"type": "train",
20+
"backend": "megatron",
21+
"entrypoint": "train.py",
22+
},
23+
"runner": {"hostfile": None, "no_shared_fs": False, "ssh_port": 2222},
24+
"envs": {"CUDA_VISIBLE_DEVICES": "0,1"},
25+
"cmds": {"before_start": "echo before", "after_stop": "echo after"},
26+
},
27+
"train": {
28+
"system": {
29+
"checkpoint": {
30+
"save": str(tmp_path / "ckpt-save"),
31+
"load": str(tmp_path / "ckpt-load"),
32+
},
33+
"logging": {
34+
"log_dir": str(tmp_path / "logs"),
35+
"scripts_dir": str(tmp_path / "logs" / "scripts"),
36+
"pids_dir": str(tmp_path / "logs" / "pids"),
37+
"details_dir": str(tmp_path / "logs" / "details"),
38+
"tensorboard_dir": str(tmp_path / "tensorboard"),
39+
"wandb_save_dir": str(tmp_path / "wandb"),
40+
},
41+
},
42+
"model": {},
43+
"data": {},
44+
},
45+
}
46+
)
47+
48+
49+
def _compress_config(tmp_path):
50+
return OmegaConf.create(
51+
{
52+
"experiment": {
53+
"exp_dir": str(tmp_path / "compress-exp"),
54+
"task": {
55+
"type": "compress",
56+
"backend": "native_compress",
57+
"entrypoint": "compress.py",
58+
},
59+
"runner": {"hostfile": None, "no_shared_fs": False},
60+
"envs": {"ENV": "1"},
61+
"cmds": {
62+
"before_start": "echo compress-before",
63+
"after_stop": "echo compress-after",
64+
},
65+
},
66+
"compress": {
67+
"system": {
68+
"save_dir": str(tmp_path / "save"),
69+
"logging": {
70+
"log_dir": str(tmp_path / "compress-logs"),
71+
"scripts_dir": str(tmp_path / "compress-logs" / "scripts"),
72+
"pids_dir": str(tmp_path / "compress-logs" / "pids"),
73+
"tensorboard_dir": str(tmp_path / "tensorboard"),
74+
"wandb_save_dir": str(tmp_path / "wandb"),
75+
},
76+
}
77+
},
78+
}
79+
)
80+
81+
82+
def _verl_config(tmp_path):
83+
return OmegaConf.create(
84+
{
85+
"experiment": {
86+
"exp_dir": str(tmp_path / "rl-exp"),
87+
"task": {"type": "rl", "backend": "verl", "entrypoint": "verl.py"},
88+
"runner": {
89+
"hostfile": None,
90+
"no_shared_fs": False,
91+
"ray_port": 6380,
92+
"ray_dashboard_port": 8266,
93+
},
94+
"envs": {"ENV": "1"},
95+
"cmds": {
96+
"before_start": "echo rl-before",
97+
"after_stop": "echo rl-after",
98+
},
99+
},
100+
"rl": {
101+
"config-path": "conf",
102+
"config-name": "ppo",
103+
"trainer": {"n_gpus_per_node": 8},
104+
"append_kargs": {"data.train_files": ["a", "b"]},
105+
},
106+
"system": {
107+
"logging": {
108+
"log_dir": str(tmp_path / "rl-logs"),
109+
"scripts_dir": str(tmp_path / "rl-logs" / "scripts"),
110+
"pids_dir": str(tmp_path / "rl-logs" / "pids"),
111+
}
112+
},
113+
}
114+
)
115+
116+
117+
def test_megatron_backend_prepare_sets_runtime_fields(tmp_path, mocker):
118+
config = _megatron_config(tmp_path)
119+
update = mocker.patch(
120+
"flagscale.runner.backend.backend_megatron._update_config_train"
121+
)
122+
get_args = mocker.patch(
123+
"flagscale.runner.backend.backend_megatron._get_args_megatron",
124+
return_value=["--num-layers", "2"],
125+
)
126+
parse = mocker.patch(
127+
"flagscale.runner.backend.backend_megatron.parse_hostfile",
128+
return_value={"worker0": {"slots": 8, "type": "A100"}},
129+
)
130+
131+
backend = MegatronBackend(config)
132+
133+
assert backend.task_type == "train"
134+
assert backend.user_args == ["--num-layers", "2"]
135+
assert backend.user_envs == {"CUDA_VISIBLE_DEVICES": "0,1"}
136+
assert backend.user_script == "train.py"
137+
assert backend.resources == {"worker0": {"slots": 8, "type": "A100"}}
138+
assert backend.rdzv_id
139+
update.assert_called_once_with(config)
140+
get_args.assert_called_once_with(config)
141+
parse.assert_called_once_with(None)
142+
143+
144+
def test_megatron_backend_rejects_wrong_task_type(tmp_path):
145+
config = _megatron_config(tmp_path)
146+
config.experiment.task.type = "compress"
147+
148+
with pytest.raises(AssertionError, match="Unsupported task type"):
149+
MegatronBackend(config)
150+
151+
152+
def test_megatron_backend_generate_run_and_stop_scripts(tmp_path, mocker):
153+
config = _megatron_config(tmp_path)
154+
backend = object.__new__(MegatronBackend)
155+
backend.config = config
156+
pkg_dir = tmp_path / "pkg"
157+
pkg_dir.mkdir()
158+
159+
script = MegatronBackend.generate_run_script(
160+
backend,
161+
config,
162+
"worker0",
163+
0,
164+
"torchrun train.py",
165+
background=True,
166+
pkg_dir=str(pkg_dir),
167+
enable_monitoring=True,
168+
)
169+
stop_script = backend.generate_stop_script("worker0", 0)
170+
171+
content = open(script, encoding="utf-8").read()
172+
assert "echo before" in content
173+
assert f"cd {pkg_dir}" in content
174+
assert "flagscale/train" in content
175+
assert "monitor_launcher.py" in content
176+
assert "nohup" in content
177+
assert "host_0_worker0.output" in content
178+
stop_content = open(stop_script, encoding="utf-8").read()
179+
assert "pkill -f 'torchrun'" in stop_content
180+
assert "echo after" in stop_content
181+
182+
183+
def test_native_compress_helpers_update_config_and_args(tmp_path, mocker):
184+
config = _compress_config(tmp_path)
185+
hydra_output = tmp_path / "hydra" / ".hydra"
186+
hydra_output.mkdir(parents=True)
187+
config_file = hydra_output / "config.yaml"
188+
config_file.write_text("x: 1", encoding="utf-8")
189+
mocker.patch(
190+
"flagscale.runner.backend.backend_native_compress.HydraConfig.get",
191+
return_value=SimpleNamespace(
192+
runtime=SimpleNamespace(output_dir=str(tmp_path / "hydra")),
193+
output_subdir=".hydra",
194+
),
195+
)
196+
197+
assert backend_native_compress._get_args_llmcompressor(config) == [
198+
f"--config-path={config_file}"
199+
]
200+
201+
config.compress.system.logging = {}
202+
backend_native_compress._update_config_compress(config)
203+
assert config.compress.system.logging.log_dir.endswith("compress_logs")
204+
assert config.compress.system.logging.tensorboard_dir.endswith("tensorboard")
205+
assert config.compress.system.logging.wandb_save_dir.endswith("wandb")
206+
207+
208+
def test_native_compress_backend_prepare_and_scripts(tmp_path, mocker):
209+
config = _compress_config(tmp_path)
210+
mocker.patch(
211+
"flagscale.runner.backend.backend_native_compress._update_config_compress"
212+
)
213+
mocker.patch(
214+
"flagscale.runner.backend.backend_native_compress._get_args_llmcompressor",
215+
return_value=["--config-path=/tmp/config.yaml"],
216+
)
217+
mocker.patch(
218+
"flagscale.runner.backend.backend_native_compress.parse_hostfile",
219+
return_value=None,
220+
)
221+
mocker.patch(
222+
"flagscale.runner.backend.backend_native_compress.get_pkg_dir",
223+
return_value=str(tmp_path),
224+
)
225+
226+
backend = NativeCompressBackend(config)
227+
assert backend.task_type == "compress"
228+
assert backend.user_args == ["--config-path=/tmp/config.yaml"]
229+
assert backend.cur_envs is None
230+
231+
run_script = backend.generate_run_script(
232+
config, "localhost", 0, "python compress.py"
233+
)
234+
stop_script = backend.generate_stop_script(config, "localhost", 0)
235+
run_content = open(run_script, encoding="utf-8").read()
236+
assert "echo compress-before" in run_content
237+
assert f"mkdir -p {config.compress.system.save_dir}" in run_content
238+
assert "flagscale/compress" in run_content
239+
assert "set -o pipefail" in run_content
240+
assert "pkill -f 'python'" in open(stop_script, encoding="utf-8").read()
241+
242+
243+
def test_verl_args_and_update_config(tmp_path):
244+
config = _verl_config(tmp_path)
245+
246+
args = backend_verl._get_args_verl(config)
247+
248+
assert "--config-path=conf" in args
249+
assert "--config-name=ppo" in args
250+
assert "trainer.n_gpus_per_node=8" in args
251+
assert '+data.train_files=["a", "b"]' in args
252+
253+
config.system = {}
254+
backend_verl._update_config_rl(config)
255+
assert config.system.logging.log_dir.endswith("logs")
256+
assert config.system.logging.scripts_dir.endswith(os.path.join("logs", "scripts"))
257+
258+
259+
def test_verl_backend_prepare_and_ray_scripts(tmp_path, mocker):
260+
config = _verl_config(tmp_path)
261+
mocker.patch("flagscale.runner.backend.backend_verl._update_config_rl")
262+
mocker.patch(
263+
"flagscale.runner.backend.backend_verl._get_args_verl",
264+
return_value=["trainer.x=1"],
265+
)
266+
mocker.patch(
267+
"flagscale.runner.backend.backend_verl.parse_hostfile",
268+
return_value={"worker0": {"slots": 8}},
269+
)
270+
mocker.patch(
271+
"flagscale.runner.backend.backend_verl.get_pkg_dir", return_value=str(tmp_path)
272+
)
273+
274+
backend = VerlBackend(config)
275+
assert backend.task_type == "rl"
276+
assert backend.user_args == ["trainer.x=1"]
277+
assert backend.resources == {"worker0": {"slots": 8}}
278+
279+
resources = {"worker0": {"slots": 8}, "worker1": {"slots": 4}}
280+
script = backend.generate_run_script(
281+
config, "worker0", 0, "python verl.py", background=True, resources=resources
282+
)
283+
stop_script = backend.generate_stop_script(config, "worker0", 0)
284+
content = open(script, encoding="utf-8").read()
285+
assert "ray start --head --port=6380" in content
286+
assert "--dashboard-port=8266" in content
287+
assert "ssh -f -n worker1" in content
288+
assert "--address=worker0:6380" in content
289+
assert "nohup" in content
290+
assert "pkill -f 'torchrun'" in open(stop_script, encoding="utf-8").read()

0 commit comments

Comments
 (0)