forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_curriculum.py
More file actions
279 lines (212 loc) · 9.72 KB
/
Copy pathtest_curriculum.py
File metadata and controls
279 lines (212 loc) · 9.72 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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""Tests for curriculum learning — config, sorting, bucket creation."""
from soup_cli.config.schema import SoupConfig, TrainingConfig
# ─── Config Tests ─────────────────────────────────────────────────────────
class TestCurriculumConfig:
"""Test curriculum learning fields in TrainingConfig."""
def test_curriculum_default_false(self):
"""curriculum should default to False."""
tcfg = TrainingConfig()
assert tcfg.curriculum is False
def test_curriculum_true(self):
tcfg = TrainingConfig(curriculum=True)
assert tcfg.curriculum is True
def test_curriculum_metric_default(self):
"""curriculum_metric should default to 'length'."""
tcfg = TrainingConfig()
assert tcfg.curriculum_metric == "length"
def test_curriculum_metric_length(self):
tcfg = TrainingConfig(curriculum_metric="length")
assert tcfg.curriculum_metric == "length"
def test_curriculum_metric_perplexity(self):
tcfg = TrainingConfig(curriculum_metric="perplexity")
assert tcfg.curriculum_metric == "perplexity"
def test_curriculum_metric_loss(self):
tcfg = TrainingConfig(curriculum_metric="loss")
assert tcfg.curriculum_metric == "loss"
def test_curriculum_buckets_default(self):
"""curriculum_buckets should default to 4."""
tcfg = TrainingConfig()
assert tcfg.curriculum_buckets == 4
def test_curriculum_buckets_custom(self):
tcfg = TrainingConfig(curriculum_buckets=8)
assert tcfg.curriculum_buckets == 8
def test_curriculum_in_full_config(self):
cfg = SoupConfig(
base="test-model",
data={"train": "data.jsonl"},
training={
"curriculum": True,
"curriculum_metric": "length",
"curriculum_buckets": 4,
},
)
assert cfg.training.curriculum is True
assert cfg.training.curriculum_metric == "length"
assert cfg.training.curriculum_buckets == 4
# ─── YAML Config Loading Tests ────────────────────────────────────────────
class TestCurriculumYamlConfig:
"""Test curriculum via YAML config loading."""
def test_load_config_with_curriculum(self):
from soup_cli.config.loader import load_config_from_string
yaml_str = """
base: test-model
data:
train: data.jsonl
training:
curriculum: true
curriculum_metric: length
curriculum_buckets: 6
"""
cfg = load_config_from_string(yaml_str)
assert cfg.training.curriculum is True
assert cfg.training.curriculum_metric == "length"
assert cfg.training.curriculum_buckets == 6
def test_load_config_without_curriculum(self):
from soup_cli.config.loader import load_config_from_string
yaml_str = """
base: test-model
data:
train: data.jsonl
"""
cfg = load_config_from_string(yaml_str)
assert cfg.training.curriculum is False
# ─── Curriculum Sorting Tests ─────────────────────────────────────────────
class TestCurriculumSorting:
"""Test curriculum sorting by different metrics."""
def test_sort_by_length(self):
"""Sort by length should order short → long."""
from soup_cli.utils.curriculum import sort_by_length
data = [
{"text": "a" * 100},
{"text": "a" * 10},
{"text": "a" * 50},
]
sorted_data = sort_by_length(data)
lengths = [len(row["text"]) for row in sorted_data]
assert lengths == [10, 50, 100]
def test_sort_by_length_messages_format(self):
"""Sort by length should work with messages format."""
from soup_cli.utils.curriculum import sort_by_length
data = [
{"messages": [{"role": "user", "content": "a" * 100}]},
{"messages": [{"role": "user", "content": "short"}]},
{"messages": [{"role": "user", "content": "a" * 50}]},
]
sorted_data = sort_by_length(data)
# First should be shortest
assert len(str(sorted_data[0])) < len(str(sorted_data[-1]))
def test_sort_by_length_empty_list(self):
"""Sort by length should handle empty list."""
from soup_cli.utils.curriculum import sort_by_length
assert sort_by_length([]) == []
def test_sort_by_length_single_item(self):
"""Sort by length should handle single item."""
from soup_cli.utils.curriculum import sort_by_length
data = [{"text": "hello"}]
assert sort_by_length(data) == data
def test_sort_by_length_fallback_json(self):
"""Sort by length should fall back to JSON stringify for unknown formats."""
from soup_cli.utils.curriculum import sort_by_length
data = [
{"custom": "a" * 100, "extra": "b" * 50},
{"custom": "short"},
]
sorted_data = sort_by_length(data)
# Shorter row should come first
assert len(str(sorted_data[0])) < len(str(sorted_data[-1]))
# ─── Curriculum Metric Fallback Tests ────────────────────────────────────
class TestCurriculumMetricFallback:
"""Test non-length metric fallback behavior."""
def test_perplexity_metric_config(self):
"""curriculum_metric=perplexity should be a valid config."""
cfg = SoupConfig(
base="test-model",
data={"train": "data.jsonl"},
training={
"curriculum": True,
"curriculum_metric": "perplexity",
},
)
assert cfg.training.curriculum_metric == "perplexity"
def test_loss_metric_config(self):
"""curriculum_metric=loss should be a valid config."""
cfg = SoupConfig(
base="test-model",
data={"train": "data.jsonl"},
training={
"curriculum": True,
"curriculum_metric": "loss",
},
)
assert cfg.training.curriculum_metric == "loss"
def test_non_length_metric_falls_back_to_length(self):
"""Non-length metrics should fall back to length sorting in SFT trainer."""
from io import StringIO
from rich.console import Console
cfg = SoupConfig(
base="test-model",
data={"train": "data.jsonl"},
training={
"curriculum": True,
"curriculum_metric": "perplexity",
},
)
output = StringIO()
console = Console(file=output)
tcfg = cfg.training
# Simulate the trainer logic
if tcfg.curriculum and tcfg.curriculum_metric != "length":
console.print(
f"[yellow]Curriculum metric '{tcfg.curriculum_metric}' "
"requires pre-computed scores. Using length-based sorting.[/]"
)
text = output.getvalue()
assert "perplexity" in text
assert "length-based" in text
# ─── Bucket Creation Tests ───────────────────────────────────────────────
class TestCurriculumBuckets:
"""Test bucket creation for staged training."""
def test_create_buckets(self):
"""create_buckets should split sorted data into N roughly equal parts."""
from soup_cli.utils.curriculum import create_buckets
data = list(range(100))
buckets = create_buckets(data, num_buckets=4)
assert len(buckets) == 4
# All data should be present
flat = [item for bucket in buckets for item in bucket]
assert sorted(flat) == data
def test_create_buckets_uneven(self):
"""Buckets should handle non-evenly-divisible data."""
from soup_cli.utils.curriculum import create_buckets
data = list(range(10))
buckets = create_buckets(data, num_buckets=3)
assert len(buckets) == 3
flat = [item for bucket in buckets for item in bucket]
assert sorted(flat) == data
def test_create_buckets_single(self):
"""Single bucket should contain all data."""
from soup_cli.utils.curriculum import create_buckets
data = list(range(20))
buckets = create_buckets(data, num_buckets=1)
assert len(buckets) == 1
assert buckets[0] == data
def test_create_buckets_more_than_data(self):
"""More buckets than data should still work."""
from soup_cli.utils.curriculum import create_buckets
data = list(range(3))
buckets = create_buckets(data, num_buckets=5)
# Some buckets may be empty, but all data should be present
flat = [item for bucket in buckets for item in bucket]
assert sorted(flat) == data
# ─── Sweep Integration Tests ─────────────────────────────────────────────
class TestCurriculumSweep:
"""Test curriculum in sweep configurations."""
def test_curriculum_in_sweep_params(self):
from soup_cli.commands.sweep import _parse_sweep_params
params = _parse_sweep_params(["training.curriculum=true,false"])
assert "training.curriculum" in params
def test_curriculum_buckets_in_sweep_params(self):
from soup_cli.commands.sweep import _parse_sweep_params
params = _parse_sweep_params(["training.curriculum_buckets=2,4,8"])
assert "training.curriculum_buckets" in params
assert params["training.curriculum_buckets"] == [2, 4, 8]