Skip to content

Commit feb5de7

Browse files
Merge pull request #118 from drunkenbot-ai/develop
fix pretraining
2 parents ff8ad7a + f74014f commit feb5de7

4 files changed

Lines changed: 190 additions & 1 deletion

File tree

interface/core/project_state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ def _default_project_state(self) -> dict[str, Any]:
107107
"use_amp": self.use_amp_default,
108108
"resume": True,
109109
"require_compatible_resume": True,
110+
"early_stopping": True,
111+
"early_stopping_patience": 3,
110112
"benchmark_prompts": "\n\n".join(DEFAULT_BENCHMARK_PROMPTS),
111113
"benchmark_tokens": 128,
112114
"benchmark_temperature": 0.7,
@@ -359,6 +361,7 @@ def _project_state_dict(self, project_name: str, project_dir: Path) -> dict[str,
359361
"resume": self.resume_training.isChecked(),
360362
"require_compatible_resume": self.resume_safety.isChecked(),
361363
"early_stopping": self.early_stopping.isChecked(),
364+
"early_stopping_patience": self.early_stopping_patience.value(),
362365
"benchmark_prompts": self.benchmark_prompts.toPlainText(),
363366
"benchmark_tokens": self.benchmark_tokens.value(),
364367
"benchmark_temperature": self.benchmark_temperature.value(),

interface/core/project_state_apply.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def _apply_project_state(self, data: dict[str, Any]) -> None:
184184
self.resume_training.setChecked(bool(training.get("resume", self.resume_training.isChecked())))
185185
self.resume_safety.setChecked(bool(training.get("require_compatible_resume", True)))
186186
self.early_stopping.setChecked(bool(training.get("early_stopping", True)))
187+
self.early_stopping_patience.setValue(int(training.get("early_stopping_patience", self.early_stopping_patience.value())))
187188
self.benchmark_prompts.setPlainText(str(training.get("benchmark_prompts", self.benchmark_prompts.toPlainText())))
188189
self.benchmark_tokens.setValue(int(training.get("benchmark_tokens", self.benchmark_tokens.value())))
189190
self.benchmark_temperature.setValue(float(training.get("benchmark_temperature", self.benchmark_temperature.value())))

tests/test_pretraining_fixes.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
from __future__ import annotations
2+
3+
import tempfile
4+
import unittest
5+
from pathlib import Path
6+
from unittest.mock import MagicMock
7+
8+
# Ensure interface.app is imported first to populate shared mixin globals
9+
from interface import app as _interface_app # noqa: F401
10+
from interface.core.project_state import ProjectStateMixin
11+
from interface.core.project_state_apply import ProjectStateApplyMixin
12+
13+
from engine.config import DatasetConfig
14+
from engine.data import Document
15+
from engine.dataset_corpus import _StreamingCorpusBuilder
16+
from engine.dataset_mixture import (
17+
MAX_REPETITIVE_UNIT_RATIO,
18+
MIN_UNIQUE_UNITS_FOR_DIVERSITY,
19+
_filter_repetitive_documents,
20+
)
21+
from engine.tokenizer import (
22+
EOS_TOKEN,
23+
encode_file_to_bin,
24+
load_token_memmap,
25+
token_dtype_for_vocab,
26+
train_tokenizer,
27+
)
28+
from engine.training import TokenDataset
29+
30+
31+
class DiversityFilterTests(unittest.TestCase):
32+
"""Tests for low-diversity filtering behavior."""
33+
34+
def test_diversity_filter_accepts_large_code_and_books(self) -> None:
35+
"""Legitimate files with repeated syntax but many unique units must be accepted."""
36+
# Simulate a 1MB-style code file: common boilerplate repeated 10 times,
37+
# but 150 completely unique function definitions and logic blocks.
38+
boilerplate = [
39+
"// Standard project license header and notice block",
40+
"import std.collections.unordered_map;",
41+
"import std.concurrent.atomic_ref;",
42+
"if (error_code != 0) { return error_code; }",
43+
]
44+
unique_lines = [
45+
f"void process_record_item_id_{i}(int param_{i}) {{ execute_job({i}); }}"
46+
for i in range(150)
47+
]
48+
lines = unique_lines + (boilerplate * 10)
49+
content = "\n".join(lines)
50+
doc = Document(path=Path("code_sample.cpp"), text=content, kind="code")
51+
52+
accepted, report = _filter_repetitive_documents([doc])
53+
self.assertEqual(len(accepted), 1)
54+
self.assertEqual(report["removed_documents"], 0)
55+
56+
def test_diversity_filter_rejects_synthetic_padding(self) -> None:
57+
"""Tiny templates repeated over and over to pad corpus size must be excluded."""
58+
template_sentences = [
59+
"The quick brown fox jumps over the lazy dog repeatedly.",
60+
"Synthetic data generation fills space without any real knowledge.",
61+
"This template unit repeats across the document to pad character length.",
62+
]
63+
text = " ".join(template_sentences * 40)
64+
doc = Document(path=Path("synthetic_padding.txt"), text=text, kind="prose")
65+
66+
accepted, report = _filter_repetitive_documents([doc])
67+
self.assertEqual(len(accepted), 0)
68+
self.assertEqual(report["removed_documents"], 1)
69+
70+
def test_streaming_corpus_builder_respects_filter_toggle(self) -> None:
71+
"""When filter_low_diversity is False, repetitive files are not excluded."""
72+
template = "This template repeats indefinitely without variation for testing."
73+
text = " ".join([template] * 80)
74+
doc = Document(path=Path("repetitive.txt"), text=text, kind="prose")
75+
76+
with tempfile.TemporaryDirectory() as temp_dir:
77+
corpus_path = Path(temp_dir) / "corpus.txt"
78+
builder = _StreamingCorpusBuilder(
79+
corpus_path=corpus_path,
80+
code_training_mode=False,
81+
generate_instruction_samples=False,
82+
reasoning_sample_mode="scaffold",
83+
filter_low_diversity=False,
84+
)
85+
builder.submit(doc)
86+
self.assertEqual(builder.stats.low_diversity_removed, 0)
87+
self.assertEqual(builder.stats.accepted_document_count, 1)
88+
builder.close()
89+
90+
91+
class EarlyStoppingPatiencePersistenceTests(unittest.TestCase):
92+
"""Tests for early stopping patience persistence in project state."""
93+
94+
def test_early_stopping_patience_in_default_state(self) -> None:
95+
window = MagicMock()
96+
window.device.currentText.return_value = "cpu"
97+
window.use_amp_default = False
98+
state = ProjectStateMixin._default_project_state(window)
99+
100+
self.assertIn("early_stopping", state["training"])
101+
self.assertTrue(state["training"]["early_stopping"])
102+
self.assertIn("early_stopping_patience", state["training"])
103+
self.assertEqual(state["training"]["early_stopping_patience"], 3)
104+
105+
def test_early_stopping_patience_snapshot_and_apply(self) -> None:
106+
window = MagicMock()
107+
window.theme_name = "dark"
108+
window.persisted_training_process = {}
109+
window.early_stopping.isChecked.return_value = True
110+
window.early_stopping_patience.value.return_value = 7
111+
112+
snap = ProjectStateMixin._project_state_dict(window, "test", Path("."))
113+
self.assertEqual(snap["training"]["early_stopping_patience"], 7)
114+
115+
# Apply should restore patience value
116+
test_state = {
117+
"training": {
118+
"early_stopping": True,
119+
"early_stopping_patience": 5,
120+
}
121+
}
122+
ProjectStateApplyMixin._apply_project_state(window, test_state)
123+
window.early_stopping_patience.setValue.assert_called_with(5)
124+
125+
126+
class TokenizerCorpusEncodingTests(unittest.TestCase):
127+
"""Tests that tokenizing multi-line text does not corrupt lines with <bos>/<eos>."""
128+
129+
def test_encode_file_to_bin_no_per_line_special_tokens(self) -> None:
130+
with tempfile.TemporaryDirectory() as temp_dir:
131+
temp_path = Path(temp_dir)
132+
corpus_file = temp_path / "corpus.txt"
133+
# Two documents, each with two lines, separated by <eos>
134+
corpus_text = (
135+
f"Line one of document alpha\n"
136+
f"Line two of document alpha\n"
137+
f"{EOS_TOKEN}\n"
138+
f"Line one of document beta\n"
139+
f"Line two of document beta\n"
140+
f"{EOS_TOKEN}\n"
141+
)
142+
corpus_file.write_text(corpus_text, encoding="utf-8")
143+
tok_path = temp_path / "tokenizer.json"
144+
tok = train_tokenizer(corpus_file, tok_path, vocab_size=60)
145+
eos_id = tok.token_to_id(EOS_TOKEN)
146+
bos_id = tok.token_to_id("<bos>")
147+
148+
bin_file = temp_path / "tokens.bin"
149+
dtype = token_dtype_for_vocab(60)
150+
encode_file_to_bin(tok, corpus_file, bin_file, dtype=dtype)
151+
152+
tokens = list(load_token_memmap(bin_file, dtype=dtype))
153+
154+
# eos_id should only appear at the document boundaries (2 times total)
155+
eos_indices = [i for i, t in enumerate(tokens) if t == eos_id]
156+
self.assertEqual(len(eos_indices), 2)
157+
158+
# bos_id should NOT appear before every line
159+
bos_indices = [i for i, t in enumerate(tokens) if t == bos_id]
160+
self.assertEqual(len(bos_indices), 0)
161+
162+
163+
class ValidationLoaderStrideTests(unittest.TestCase):
164+
"""Tests for non-overlapping validation loader stride."""
165+
166+
def test_validation_stride_equals_context_length(self) -> None:
167+
context_length = 64
168+
# 257 tokens -> 4 non-overlapping windows of length 64 with stride 64
169+
tokens = list(range(257))
170+
val_dataset = TokenDataset(tokens, context_length=context_length, stride=context_length)
171+
self.assertEqual(len(val_dataset), 4)
172+
173+
# First window covers tokens 0..63 (input) -> 1..64 (target)
174+
x0, y0 = val_dataset[0]
175+
self.assertEqual(x0.tolist(), list(range(0, 64)))
176+
self.assertEqual(y0.tolist(), list(range(1, 65)))
177+
178+
# Second window covers tokens 64..127 (input) -> 65..128 (target)
179+
x1, y1 = val_dataset[1]
180+
self.assertEqual(x1.tolist(), list(range(64, 128)))
181+
self.assertEqual(y1.tolist(), list(range(65, 129)))
182+
183+
184+
if __name__ == "__main__":
185+
unittest.main()

0 commit comments

Comments
 (0)