trainloop 0.5.2__tar.gz → 0.6.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: trainloop
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: Minimal PyTorch training loop with hooks and checkpointing.
5
5
  Author: Karim Knaebel
6
6
  Author-email: Karim Knaebel <contact@knaebel.dev>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trainloop"
3
- version = "0.5.2"
3
+ version = "0.6.0"
4
4
  description = "Minimal PyTorch training loop with hooks and checkpointing."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -4,8 +4,9 @@ from .hooks import (
4
4
  CudaMaxMemoryHook,
5
5
  EmaHook,
6
6
  ImageFileLoggerHook,
7
- LoggingHook,
8
7
  ProgressHook,
8
+ StatsHook,
9
+ TrainingStats,
9
10
  WandbHook,
10
11
  )
11
12
  from .trainer import BaseTrainer, LossNoneWarning, map_nested_tensor
@@ -15,8 +16,9 @@ __all__ = [
15
16
  "BaseHook",
16
17
  "CheckpointingHook",
17
18
  "CudaMaxMemoryHook",
18
- "LoggingHook",
19
19
  "ProgressHook",
20
+ "StatsHook",
21
+ "TrainingStats",
20
22
  "EmaHook",
21
23
  "WandbHook",
22
24
  "ImageFileLoggerHook",
@@ -5,6 +5,7 @@ import sys
5
5
  import tempfile
6
6
  import time
7
7
  import warnings
8
+ from dataclasses import dataclass
8
9
  from datetime import timedelta
9
10
  from numbers import Number
10
11
  from pathlib import Path
@@ -78,21 +79,38 @@ class BaseHook:
78
79
  pass
79
80
 
80
81
 
81
- class _StatsHook(BaseHook):
82
- """Collect step statistics and hand them to subclasses for reporting.
82
+ @dataclass(frozen=True)
83
+ class TrainingStats:
84
+ loss: float
85
+ grad_norm: float | None
86
+ step_time: float
87
+ data_time: float
88
+ non_finite_grad_retry_count: float
89
+ max_memory: float | None
90
+ records: Records
91
+ param_groups: list[dict[str, Any]]
92
+
93
+
94
+ class StatsHook(BaseHook):
95
+ """Aggregate training stats and pass them to a callback.
83
96
 
84
97
  Args:
85
98
  interval: Emit stats every N steps.
86
99
  sync: If True, aggregate stats across distributed ranks.
100
+ callback: Function called with the trainer and aggregated stats.
87
101
  """
88
102
 
89
103
  def __init__(
90
104
  self,
91
- interval: int,
92
- sync: bool,
105
+ callback: Callable[[BaseTrainer, TrainingStats], None],
106
+ interval: int = 10,
107
+ sync: bool = True,
108
+ param_group_keys: Sequence[str] = ("name", "lr"),
93
109
  ):
94
110
  self.interval = interval
95
111
  self.sync = sync
112
+ self.callback = callback
113
+ self.param_group_keys = tuple(param_group_keys)
96
114
  self.reset()
97
115
 
98
116
  def reset(self):
@@ -104,6 +122,13 @@ class _StatsHook(BaseHook):
104
122
  self.non_finite_grad_retry_counts = []
105
123
  self.max_memories = []
106
124
 
125
+ def on_before_step(self, trainer: BaseTrainer):
126
+ super().on_before_step(trainer)
127
+ self.param_groups = [
128
+ {k: param_group[k] for k in self.param_group_keys if k in param_group}
129
+ for param_group in trainer.optimizer.param_groups
130
+ ] # record the LR before the scheduler steps
131
+
107
132
  def on_after_step(self, trainer: BaseTrainer):
108
133
  # collect and aggregate over accumulation steps
109
134
  self.losses.append(torch.stack(trainer.step_info["loss"]).mean())
@@ -156,31 +181,21 @@ class _StatsHook(BaseHook):
156
181
  if "max_memory" in trainer.step_info:
157
182
  max_memory = max(stat["max_memory"] for stat in gathered)
158
183
 
159
- self.process_stats(
184
+ self.callback(
160
185
  trainer,
161
- loss.item(),
162
- grad_norm.item() if grad_norm is not None else None,
163
- step_time,
164
- data_time,
165
- non_finite_grad_retry_count,
166
- max_memory,
167
- records,
186
+ TrainingStats(
187
+ loss=loss.item(),
188
+ grad_norm=grad_norm.item() if grad_norm is not None else None,
189
+ step_time=step_time,
190
+ data_time=data_time,
191
+ non_finite_grad_retry_count=non_finite_grad_retry_count,
192
+ max_memory=max_memory,
193
+ records=records,
194
+ param_groups=self.param_groups,
195
+ ),
168
196
  )
169
197
  self.reset()
170
198
 
171
- def process_stats(
172
- self,
173
- trainer: BaseTrainer,
174
- loss: float,
175
- grad_norm: float | None,
176
- step_time: float,
177
- data_time: float,
178
- non_finite_grad_retry_count: float,
179
- max_memory: float | None,
180
- records: Records,
181
- ):
182
- raise NotImplementedError("Subclasses must implement this method.")
183
-
184
199
 
185
200
  class ETATracker:
186
201
  def __init__(self, warmup_steps: int):
@@ -212,7 +227,7 @@ class ETATracker:
212
227
  return timedelta(seconds=int(eta_seconds))
213
228
 
214
229
 
215
- class ProgressHook(_StatsHook):
230
+ class ProgressHook(StatsHook):
216
231
  """Log progress to stdout with optional metrics, ETA, and memory.
217
232
 
218
233
  Args:
@@ -230,8 +245,14 @@ class ProgressHook(_StatsHook):
230
245
  sync: bool = False,
231
246
  eta_warmup: int = 10,
232
247
  show_units: bool = True,
248
+ param_group_keys: Sequence[str] = ("name", "lr"),
233
249
  ):
234
- super().__init__(interval=interval, sync=sync)
250
+ super().__init__(
251
+ self.log_progress,
252
+ interval=interval,
253
+ sync=sync,
254
+ param_group_keys=param_group_keys,
255
+ )
235
256
  self.with_records = with_records
236
257
  self.eta_warmup = eta_warmup
237
258
  self.show_units = show_units
@@ -245,47 +266,41 @@ class ProgressHook(_StatsHook):
245
266
  super().on_after_train(trainer)
246
267
  trainer.logger.info("=> Finished training")
247
268
 
248
- def on_before_step(self, trainer: BaseTrainer):
249
- super().on_before_step(trainer)
250
- self.lrs = [
251
- (param_group.get("name", str(i)), param_group["lr"])
252
- for i, param_group in enumerate(trainer.optimizer.param_groups)
253
- ] # record the LR before the scheduler steps
254
-
255
269
  def on_after_step(self, trainer: BaseTrainer):
256
- self.eta_tracker.step() # should be called before process_stats
270
+ self.eta_tracker.step() # should be called before log_progress
257
271
  super().on_after_step(trainer)
258
272
 
259
- def process_stats(
260
- self,
261
- trainer: BaseTrainer,
262
- loss: float,
263
- grad_norm: float | None,
264
- step_time: float,
265
- data_time: float,
266
- non_finite_grad_retry_count: float,
267
- max_memory: float | None,
268
- records: Records,
269
- ):
273
+ def log_progress(self, trainer: BaseTrainer, stats: TrainingStats):
270
274
  eta = self.eta_tracker.get_eta(trainer.max_steps - trainer.step)
271
275
  trainer.logger.info(
272
276
  f"Step {trainer.step}/{trainer.max_steps}:"
273
- + f" step {step_time:.4f}{'s' if self.show_units else ''} data {data_time:.4f}{'s' if self.show_units else ''}"
277
+ + f" step {stats.step_time:.4f}{'s' if self.show_units else ''} data {stats.data_time:.4f}{'s' if self.show_units else ''}"
274
278
  + (f" eta {eta}" if eta is not None else "")
275
279
  + (
276
- f" mem {max_memory:#.3g}{'GiB' if self.show_units else ''}"
277
- if max_memory is not None
280
+ f" mem {stats.max_memory:#.3g}{'GiB' if self.show_units else ''}"
281
+ if stats.max_memory is not None
282
+ else ""
283
+ )
284
+ + f" loss {stats.loss:.4f}"
285
+ + (
286
+ f" grad_norm {stats.grad_norm:.4f}"
287
+ if stats.grad_norm is not None
278
288
  else ""
279
289
  )
280
- + f" loss {loss:.4f}"
281
- + (f" grad_norm {grad_norm:.4f}" if grad_norm is not None else "")
282
- + (" " + " ".join(f"lr/{name} {lr:.2e}" for name, lr in self.lrs))
290
+ + (
291
+ " "
292
+ + " ".join(
293
+ f"lr/{group.get('name', f'group_{i}')} {group['lr']:.2e}"
294
+ for i, group in enumerate(stats.param_groups)
295
+ if "lr" in group
296
+ )
297
+ )
283
298
  + (
284
299
  (
285
300
  " | "
286
301
  + " ".join(
287
302
  f"{'/'.join(k)} {f'{v:#.4g}' if isinstance(v, Number) else v}"
288
- for k, v in flatten_nested_dict(records).items()
303
+ for k, v in flatten_nested_dict(stats.records).items()
289
304
  )
290
305
  )
291
306
  if self.with_records
@@ -294,55 +309,6 @@ class ProgressHook(_StatsHook):
294
309
  )
295
310
 
296
311
 
297
- class LoggingHook(_StatsHook):
298
- """Aggregate stats and forward them to ``trainer.log``.
299
-
300
- Args:
301
- interval: Log every N steps.
302
- sync: If True, aggregate across distributed ranks.
303
- """
304
-
305
- def __init__(
306
- self,
307
- interval: int = 10,
308
- sync: bool = True,
309
- ):
310
- super().__init__(interval, sync)
311
-
312
- def on_before_step(self, trainer: BaseTrainer):
313
- super().on_before_step(trainer)
314
- self.lrs = [
315
- (param_group.get("name", f"group_{i}"), param_group["lr"])
316
- for i, param_group in enumerate(trainer.optimizer.param_groups)
317
- ] # record the LR before the scheduler steps
318
-
319
- def process_stats(
320
- self,
321
- trainer: BaseTrainer,
322
- loss: float,
323
- grad_norm: float | None,
324
- step_time: float,
325
- data_time: float,
326
- non_finite_grad_retry_count: float,
327
- max_memory: float | None,
328
- records: Records,
329
- ):
330
- trainer.log(
331
- {
332
- "train": records
333
- | ({"grad_norm": grad_norm} if grad_norm is not None else {})
334
- | ({"max_memory": max_memory} if max_memory is not None else {})
335
- | {
336
- "loss": loss,
337
- "data_time": data_time,
338
- "step_time": step_time,
339
- "non_finite_grad_retry_count": non_finite_grad_retry_count,
340
- "lr": {name: lr for name, lr in self.lrs},
341
- }
342
- }
343
- )
344
-
345
-
346
312
  class CheckpointingHook(BaseHook):
347
313
  """Save and optionally restore checkpoints at regular intervals.
348
314
 
File without changes