forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_deepspeed.py
More file actions
237 lines (177 loc) · 8.45 KB
/
Copy pathtest_deepspeed.py
File metadata and controls
237 lines (177 loc) · 8.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
"""Tests for Multi-GPU / DeepSpeed support."""
import json
import os
from unittest.mock import MagicMock, patch
import pytest
class TestDeepSpeedConfigs:
"""Test DeepSpeed configuration templates."""
def test_zero2_config_structure(self):
"""ZeRO Stage 2 config should have correct structure."""
from soup_cli.utils.deepspeed import get_deepspeed_config
config = get_deepspeed_config("zero2")
assert config["zero_optimization"]["stage"] == 2
assert config["bf16"]["enabled"] is True
assert config["gradient_accumulation_steps"] == "auto"
def test_zero3_config_structure(self):
"""ZeRO Stage 3 config should have correct structure."""
from soup_cli.utils.deepspeed import get_deepspeed_config
config = get_deepspeed_config("zero3")
assert config["zero_optimization"]["stage"] == 3
assert config["zero_optimization"]["stage3_gather_16bit_weights_on_model_save"] is True
def test_zero2_offload_config(self):
"""ZeRO Stage 2 with offload should enable CPU offloading."""
from soup_cli.utils.deepspeed import get_deepspeed_config
config = get_deepspeed_config("zero2_offload")
assert config["zero_optimization"]["stage"] == 2
offload = config["zero_optimization"]["offload_optimizer"]
assert offload["device"] == "cpu"
assert offload["pin_memory"] is True
def test_invalid_config_name(self):
"""Should raise ValueError for unknown config name."""
from soup_cli.utils.deepspeed import get_deepspeed_config
with pytest.raises(ValueError, match="Unknown DeepSpeed config"):
get_deepspeed_config("zero99")
def test_get_config_returns_copy(self):
"""Should return a copy, not the original."""
from soup_cli.utils.deepspeed import get_deepspeed_config
config1 = get_deepspeed_config("zero2")
config2 = get_deepspeed_config("zero2")
config1["bf16"]["enabled"] = False
assert config2["bf16"]["enabled"] is True
def test_all_configs_have_auto_fields(self):
"""All configs should have 'auto' for batch sizes."""
from soup_cli.utils.deepspeed import CONFIGS
for name, config in CONFIGS.items():
assert config["train_batch_size"] == "auto", f"{name} missing auto train_batch_size"
assert config["train_micro_batch_size_per_gpu"] == "auto", (
f"{name} missing auto micro batch"
)
class TestWriteDeepSpeedConfig:
"""Test writing DeepSpeed config to temp file."""
def test_write_creates_file(self):
"""Should create a valid JSON file."""
from soup_cli.utils.deepspeed import write_deepspeed_config
path = write_deepspeed_config("zero2")
assert os.path.exists(path)
with open(path) as f:
config = json.load(f)
assert config["zero_optimization"]["stage"] == 2
# Cleanup
os.unlink(path)
def test_write_file_is_valid_json(self):
"""Written file should be parseable JSON."""
from soup_cli.utils.deepspeed import write_deepspeed_config
for stage in ["zero2", "zero3", "zero2_offload"]:
path = write_deepspeed_config(stage)
with open(path) as f:
config = json.load(f)
assert "zero_optimization" in config
os.unlink(path)
class TestDetectMultiGPU:
"""Test multi-GPU detection."""
def test_detect_no_gpu(self):
"""Should return 0 GPUs when CUDA not available."""
from soup_cli.utils.deepspeed import detect_multi_gpu
with patch("torch.cuda.is_available", return_value=False):
result = detect_multi_gpu()
assert result["gpu_count"] == 0
assert result["gpus"] == []
def test_detect_single_gpu(self):
"""Should detect a single GPU."""
from soup_cli.utils.deepspeed import detect_multi_gpu
mock_props = MagicMock()
mock_props.name = "NVIDIA RTX 4090"
mock_props.total_memory = 24 * (1024 ** 3) # 24GB
with patch("torch.cuda.is_available", return_value=True), \
patch("torch.cuda.device_count", return_value=1), \
patch("torch.cuda.get_device_properties", return_value=mock_props):
result = detect_multi_gpu()
assert result["gpu_count"] == 1
assert len(result["gpus"]) == 1
assert result["gpus"][0]["name"] == "NVIDIA RTX 4090"
assert result["gpus"][0]["memory_gb"] == pytest.approx(24.0)
def test_detect_multiple_gpus(self):
"""Should detect multiple GPUs."""
from soup_cli.utils.deepspeed import detect_multi_gpu
mock_props = MagicMock()
mock_props.name = "NVIDIA A100"
mock_props.total_memory = 80 * (1024 ** 3)
with patch("torch.cuda.is_available", return_value=True), \
patch("torch.cuda.device_count", return_value=4), \
patch("torch.cuda.get_device_properties", return_value=mock_props):
result = detect_multi_gpu()
assert result["gpu_count"] == 4
assert len(result["gpus"]) == 4
def test_detect_without_torch(self):
"""Should handle missing torch gracefully."""
from soup_cli.utils.deepspeed import detect_multi_gpu
with patch.dict("sys.modules", {"torch": None}):
# Import error should be caught
result = detect_multi_gpu()
assert result["gpu_count"] == 0
class TestResolveDeepSpeed:
"""Test DeepSpeed config resolution in train command."""
def test_resolve_named_preset(self):
"""Should resolve named presets like 'zero2'."""
from soup_cli.commands.train import _resolve_deepspeed
path = _resolve_deepspeed("zero2")
assert os.path.exists(path)
with open(path) as f:
config = json.load(f)
assert config["zero_optimization"]["stage"] == 2
os.unlink(path)
def test_resolve_json_file(self, tmp_path):
"""Should resolve path to JSON file."""
from soup_cli.commands.train import _resolve_deepspeed
config_file = tmp_path / "ds_config.json"
config_file.write_text(json.dumps({"zero_optimization": {"stage": 2}}))
result = _resolve_deepspeed(str(config_file))
assert result == str(config_file)
def test_resolve_invalid_name(self):
"""Should raise exit for invalid name."""
from click.exceptions import Exit
from soup_cli.commands.train import _resolve_deepspeed
with pytest.raises(Exit):
_resolve_deepspeed("invalid_config")
class TestTrainerDeepSpeedParam:
"""Test that trainers accept deepspeed_config parameter."""
def test_sft_trainer_accepts_deepspeed(self):
"""SFTTrainerWrapper should accept deepspeed_config."""
from soup_cli.config.schema import SoupConfig
from soup_cli.trainer.sft import SFTTrainerWrapper
cfg = SoupConfig(
base="test-model",
data={"train": "test.jsonl"},
)
wrapper = SFTTrainerWrapper(cfg, device="cpu", deepspeed_config="/tmp/ds.json")
assert wrapper.deepspeed_config == "/tmp/ds.json"
def test_dpo_trainer_accepts_deepspeed(self):
"""DPOTrainerWrapper should accept deepspeed_config."""
from soup_cli.config.schema import SoupConfig
from soup_cli.trainer.dpo import DPOTrainerWrapper
cfg = SoupConfig(
base="test-model",
task="dpo",
data={"train": "test.jsonl"},
)
wrapper = DPOTrainerWrapper(cfg, device="cpu", deepspeed_config="/tmp/ds.json")
assert wrapper.deepspeed_config == "/tmp/ds.json"
def test_sft_trainer_default_no_deepspeed(self):
"""SFTTrainerWrapper should default to no DeepSpeed."""
from soup_cli.config.schema import SoupConfig
from soup_cli.trainer.sft import SFTTrainerWrapper
cfg = SoupConfig(
base="test-model",
data={"train": "test.jsonl"},
)
wrapper = SFTTrainerWrapper(cfg, device="cpu")
assert wrapper.deepspeed_config is None
class TestTrainDeepSpeedFlag:
"""Test --deepspeed flag in train command."""
def test_train_help_shows_deepspeed(self):
"""Train help should mention --deepspeed option."""
from typer.testing import CliRunner
from soup_cli.cli import app
runner = CliRunner()
result = runner.invoke(app, ["train", "--help"])
assert "deepspeed" in result.output.lower()