mltrainer 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mltrainer-0.3.2/.remember/archive.md +3 -0
- mltrainer-0.3.2/.remember/recent.md +8 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/PKG-INFO +1 -1
- {mltrainer-0.3.1 → mltrainer-0.3.2}/pyproject.toml +1 -1
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/trainer.py +34 -5
- mltrainer-0.3.2/tests/test_trainer_iteration.py +142 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/uv.lock +1 -1
- mltrainer-0.3.1/.remember/tmp/save-session.pid +0 -1
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.claude/settings.local.json +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.gitignore +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.lefthook.yml +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/.gitignore +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/now.md +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/tmp/last-ndc.ts +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/tmp/last-save-ts +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/.remember/tmp/last-save.json +0 -0
- /mltrainer-0.3.1/.remember/today-2026-07-20.md → /mltrainer-0.3.2/.remember/today-2026-07-20.done.md +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/Makefile +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/README.md +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/__init__.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/imagemodels.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/metrics.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/preprocessors.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/rnn_models.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/settings.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/transformer.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/src/mltrainer/vae.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/__init__.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/conftest.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_metrics.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_preprocessors.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_trainer.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/test_trainer_devices.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/unit/__init__.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/unit/test_trainersettings.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/__init__.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/dummy_data.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/dummy_metrics.py +0 -0
- {mltrainer-0.3.1 → mltrainer-0.3.2}/tests/utils/dummy_model.py +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Recent
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
|
|
5
|
+
# Recent
|
|
6
|
+
|
|
7
|
+
## 2026-07-20
|
|
8
|
+
Refactored mltrainer.report() with Reporter ABC pattern (RayReporter/MLflow/TensorBoard) and added type hints w/ builtin generators, _to_device helper, plus type checking fixes. Cleaned ~115 LOC commented code and updated Makefile (ruff/ty). 47 tests passing—ready v0.3.1.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from collections.abc import Callable, Iterator
|
|
2
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import TYPE_CHECKING, TypeVar
|
|
@@ -126,8 +126,8 @@ class Trainer:
|
|
|
126
126
|
settings: TrainerSettings,
|
|
127
127
|
loss_fn: Callable,
|
|
128
128
|
optimizer: type[Optimizer],
|
|
129
|
-
traindataloader:
|
|
130
|
-
validdataloader:
|
|
129
|
+
traindataloader: Iterable,
|
|
130
|
+
validdataloader: Iterable,
|
|
131
131
|
scheduler: Callable | None,
|
|
132
132
|
device: str | None = None,
|
|
133
133
|
) -> None:
|
|
@@ -137,6 +137,8 @@ class Trainer:
|
|
|
137
137
|
self.loss_fn = loss_fn
|
|
138
138
|
self.traindataloader = traindataloader
|
|
139
139
|
self.validdataloader = validdataloader
|
|
140
|
+
self._train_iter: Iterator | None = None
|
|
141
|
+
self._valid_iter: Iterator | None = None
|
|
140
142
|
self.device = device
|
|
141
143
|
self.writer: "SummaryWriter | None" = None
|
|
142
144
|
|
|
@@ -201,6 +203,33 @@ class Trainer:
|
|
|
201
203
|
x = x.to(self.device)
|
|
202
204
|
return x, y.to(self.device)
|
|
203
205
|
|
|
206
|
+
def _next_batch(
|
|
207
|
+
self, loader: Iterable, cache: str
|
|
208
|
+
) -> tuple[BatchTensor, torch.Tensor]:
|
|
209
|
+
"""Pull one batch, keeping the iterator alive across steps.
|
|
210
|
+
|
|
211
|
+
`next(iter(loader))` per step is only correct for an *iterator* -- a
|
|
212
|
+
streamer -- because `iter()` on an iterator returns itself. A torch
|
|
213
|
+
DataLoader is an *iterable*: it hands out a fresh iterator (and, with
|
|
214
|
+
num_workers > 0, a fresh worker pool) on every call, so a training
|
|
215
|
+
loader would reshuffle and replay its first batch every step, and a
|
|
216
|
+
validation loader would score the same first batch `valid_steps` times.
|
|
217
|
+
|
|
218
|
+
Holding the iterator and re-`iter`ing it only when it is exhausted
|
|
219
|
+
works for both: an infinite streamer never raises StopIteration, and a
|
|
220
|
+
finite DataLoader wraps around into its next epoch.
|
|
221
|
+
"""
|
|
222
|
+
it = getattr(self, cache)
|
|
223
|
+
if it is None:
|
|
224
|
+
it = iter(loader)
|
|
225
|
+
setattr(self, cache, it)
|
|
226
|
+
try:
|
|
227
|
+
return next(it)
|
|
228
|
+
except StopIteration:
|
|
229
|
+
it = iter(loader)
|
|
230
|
+
setattr(self, cache, it)
|
|
231
|
+
return next(it)
|
|
232
|
+
|
|
204
233
|
def __del__(self):
|
|
205
234
|
"""Cleanup method to ensure proper resource handling"""
|
|
206
235
|
if hasattr(self, "writer") and self.writer is not None:
|
|
@@ -234,7 +263,7 @@ class Trainer:
|
|
|
234
263
|
train_loss: float = 0.0
|
|
235
264
|
train_steps = self.settings.train_steps
|
|
236
265
|
for _ in tqdm(range(train_steps), colour="#1e4706"):
|
|
237
|
-
x, y =
|
|
266
|
+
x, y = self._next_batch(self.traindataloader, "_train_iter")
|
|
238
267
|
x, y = self._to_device(x, y)
|
|
239
268
|
self.optimizer.zero_grad()
|
|
240
269
|
yhat = self.model(x)
|
|
@@ -255,7 +284,7 @@ class Trainer:
|
|
|
255
284
|
|
|
256
285
|
with torch.no_grad(): # Prevent gradient computation during evaluation
|
|
257
286
|
for _ in range(valid_steps):
|
|
258
|
-
x, y =
|
|
287
|
+
x, y = self._next_batch(self.validdataloader, "_valid_iter")
|
|
259
288
|
x, y = self._to_device(x, y)
|
|
260
289
|
|
|
261
290
|
# Forward pass
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Regression tests for the per-step iterator (see mltrainer P0.6).
|
|
2
|
+
|
|
3
|
+
`next(iter(loader))` inside the step loop is correct for an *iterator* but not
|
|
4
|
+
for an *iterable* like a torch DataLoader: it rebuilds the iterator every step,
|
|
5
|
+
so training replays a reshuffled first batch and validation scores the same
|
|
6
|
+
first batch `valid_steps` times.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
import torch.optim as optim
|
|
11
|
+
from torch.utils.data import DataLoader, Dataset
|
|
12
|
+
|
|
13
|
+
from mltrainer import Trainer, TrainerSettings
|
|
14
|
+
from tests.utils.dummy_metrics import TestMSE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CountingDataset(Dataset):
|
|
18
|
+
"""Each row is its own index, so a batch is identifiable by its contents."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, size: int = 64) -> None:
|
|
21
|
+
self.size = size
|
|
22
|
+
|
|
23
|
+
def __len__(self) -> int:
|
|
24
|
+
return self.size
|
|
25
|
+
|
|
26
|
+
def __getitem__(self, idx: int):
|
|
27
|
+
x = torch.full((10,), float(idx))
|
|
28
|
+
return x, x
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_trainer(model, loss_fn, temp_logdir, train_loader, valid_loader, **kwargs):
|
|
32
|
+
settings = TrainerSettings(
|
|
33
|
+
epochs=1,
|
|
34
|
+
metrics=[TestMSE()],
|
|
35
|
+
logdir=temp_logdir,
|
|
36
|
+
reporttypes=[],
|
|
37
|
+
optimizer_kwargs={"lr": 0.01},
|
|
38
|
+
**kwargs,
|
|
39
|
+
)
|
|
40
|
+
return Trainer(
|
|
41
|
+
model=model,
|
|
42
|
+
settings=settings,
|
|
43
|
+
loss_fn=loss_fn,
|
|
44
|
+
optimizer=optim.Adam, # type: ignore
|
|
45
|
+
traindataloader=train_loader,
|
|
46
|
+
validdataloader=valid_loader,
|
|
47
|
+
scheduler=None,
|
|
48
|
+
device="cpu",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_valid_loader_walks_the_whole_split(model, loss_fn, temp_logdir):
|
|
53
|
+
"""A shuffle=False DataLoader must yield a *different* batch per step."""
|
|
54
|
+
dataset = CountingDataset(size=64)
|
|
55
|
+
valid_loader = DataLoader(dataset, batch_size=16, shuffle=False)
|
|
56
|
+
trainer = build_trainer(
|
|
57
|
+
model,
|
|
58
|
+
loss_fn,
|
|
59
|
+
temp_logdir,
|
|
60
|
+
DataLoader(dataset, batch_size=16, shuffle=True),
|
|
61
|
+
valid_loader,
|
|
62
|
+
train_steps=1,
|
|
63
|
+
valid_steps=4,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
seen = [
|
|
67
|
+
trainer._next_batch(valid_loader, "_valid_iter")[0][0, 0].item()
|
|
68
|
+
for _ in range(4)
|
|
69
|
+
]
|
|
70
|
+
assert seen == [0.0, 16.0, 32.0, 48.0]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_iterator_wraps_around_at_the_end(model, loss_fn, temp_logdir):
|
|
74
|
+
"""Asking for more steps than the loader holds restarts it, not crashes."""
|
|
75
|
+
dataset = CountingDataset(size=32)
|
|
76
|
+
valid_loader = DataLoader(dataset, batch_size=16, shuffle=False)
|
|
77
|
+
trainer = build_trainer(
|
|
78
|
+
model,
|
|
79
|
+
loss_fn,
|
|
80
|
+
temp_logdir,
|
|
81
|
+
DataLoader(dataset, batch_size=16, shuffle=True),
|
|
82
|
+
valid_loader,
|
|
83
|
+
train_steps=1,
|
|
84
|
+
valid_steps=3,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
seen = [
|
|
88
|
+
trainer._next_batch(valid_loader, "_valid_iter")[0][0, 0].item()
|
|
89
|
+
for _ in range(3)
|
|
90
|
+
]
|
|
91
|
+
assert seen == [0.0, 16.0, 0.0]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_plain_iterator_still_advances(model, loss_fn, temp_logdir):
|
|
95
|
+
"""The old streamer path: an infinite generator, not a DataLoader."""
|
|
96
|
+
|
|
97
|
+
def stream():
|
|
98
|
+
i = 0
|
|
99
|
+
while True:
|
|
100
|
+
yield torch.full((4, 10), float(i)), torch.full((4, 10), float(i))
|
|
101
|
+
i += 1
|
|
102
|
+
|
|
103
|
+
streamer = stream()
|
|
104
|
+
trainer = build_trainer(
|
|
105
|
+
model,
|
|
106
|
+
loss_fn,
|
|
107
|
+
temp_logdir,
|
|
108
|
+
streamer,
|
|
109
|
+
streamer,
|
|
110
|
+
train_steps=1,
|
|
111
|
+
valid_steps=3,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
seen = [
|
|
115
|
+
trainer._next_batch(streamer, "_valid_iter")[0][0, 0].item() for _ in range(3)
|
|
116
|
+
]
|
|
117
|
+
assert seen == [0.0, 1.0, 2.0]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_validation_metrics_cover_more_than_one_batch(model, loss_fn, temp_logdir):
|
|
121
|
+
"""End to end: evalbatches over a loader whose batches differ wildly must
|
|
122
|
+
not report the metric of a single batch."""
|
|
123
|
+
dataset = CountingDataset(size=64)
|
|
124
|
+
valid_loader = DataLoader(dataset, batch_size=16, shuffle=False)
|
|
125
|
+
trainer = build_trainer(
|
|
126
|
+
model,
|
|
127
|
+
loss_fn,
|
|
128
|
+
temp_logdir,
|
|
129
|
+
DataLoader(dataset, batch_size=16, shuffle=True),
|
|
130
|
+
valid_loader,
|
|
131
|
+
train_steps=1,
|
|
132
|
+
valid_steps=4,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
metric_dict, _ = trainer.evalbatches()
|
|
136
|
+
|
|
137
|
+
# what the bug reported: the same model scored on batch 0, four times over
|
|
138
|
+
x, y = next(iter(valid_loader))
|
|
139
|
+
with torch.no_grad():
|
|
140
|
+
first_only = float(TestMSE()(y, trainer.model(x)))
|
|
141
|
+
|
|
142
|
+
assert abs(metric_dict[str(TestMSE())] - first_only) > 1e-3
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
85305
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/mltrainer-0.3.1/.remember/today-2026-07-20.md → /mltrainer-0.3.2/.remember/today-2026-07-20.done.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|