trainloop 0.1.0__tar.gz → 0.3.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.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.3
2
+ Name: trainloop
3
+ Version: 0.3.0
4
+ Summary: Minimal PyTorch training loop with hooks and checkpointing.
5
+ Author: Karim Abou Zeid
6
+ Author-email: Karim Abou Zeid <contact@ka.codes>
7
+ Requires-Dist: pillow>=11.3.0
8
+ Requires-Dist: torch>=2.0.0
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # trainloop
13
+
14
+ [![PyPI version](https://img.shields.io/pypi/v/trainloop.svg)](https://pypi.org/project/trainloop/)
15
+
16
+ Minimal PyTorch training loop with hooks for logging, checkpointing, and customization.
17
+
18
+ Docs: https://karimknaebel.github.io/trainloop/
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install trainloop
24
+ ```
25
+
26
+ ## Basic example
27
+
28
+ ```python
29
+ import logging
30
+
31
+ import torch
32
+ import torch.nn as nn
33
+
34
+ from trainloop import BaseTrainer, CheckpointingHook, ProgressHook
35
+
36
+ logging.basicConfig(level=logging.INFO)
37
+
38
+
39
+ class MyTrainer(BaseTrainer):
40
+ def build_data_loader(self):
41
+ class ToyDataset(torch.utils.data.IterableDataset):
42
+ def __iter__(self):
43
+ while True:
44
+ data = torch.randn(784)
45
+ target = torch.randint(0, 10, (1,)).item()
46
+ yield data, target
47
+
48
+ return torch.utils.data.DataLoader(ToyDataset(), batch_size=32)
49
+
50
+ def build_model(self):
51
+ return nn.Sequential(
52
+ nn.Linear(784, 128),
53
+ nn.ReLU(),
54
+ nn.Linear(128, 10),
55
+ ).to(self.device)
56
+
57
+ def build_optimizer(self):
58
+ return torch.optim.AdamW(self.model.parameters(), lr=3e-4)
59
+
60
+ def build_hooks(self):
61
+ return [
62
+ ProgressHook(interval=50, with_records=True),
63
+ CheckpointingHook(interval=500, keep_previous=2),
64
+ ]
65
+
66
+ def forward(self, batch):
67
+ x, y = batch
68
+ x, y = x.to(self.device), y.to(self.device)
69
+ logits = self.model(x)
70
+ loss = nn.functional.cross_entropy(logits, y)
71
+ accuracy = (logits.argmax(1) == y).float().mean().item()
72
+ return loss, {"accuracy": accuracy}
73
+
74
+
75
+ trainer = MyTrainer(max_steps=2000, device="cpu", workspace="runs/demo")
76
+ trainer.train()
77
+ ```
@@ -0,0 +1,66 @@
1
+ # trainloop
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/trainloop.svg)](https://pypi.org/project/trainloop/)
4
+
5
+ Minimal PyTorch training loop with hooks for logging, checkpointing, and customization.
6
+
7
+ Docs: https://karimknaebel.github.io/trainloop/
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install trainloop
13
+ ```
14
+
15
+ ## Basic example
16
+
17
+ ```python
18
+ import logging
19
+
20
+ import torch
21
+ import torch.nn as nn
22
+
23
+ from trainloop import BaseTrainer, CheckpointingHook, ProgressHook
24
+
25
+ logging.basicConfig(level=logging.INFO)
26
+
27
+
28
+ class MyTrainer(BaseTrainer):
29
+ def build_data_loader(self):
30
+ class ToyDataset(torch.utils.data.IterableDataset):
31
+ def __iter__(self):
32
+ while True:
33
+ data = torch.randn(784)
34
+ target = torch.randint(0, 10, (1,)).item()
35
+ yield data, target
36
+
37
+ return torch.utils.data.DataLoader(ToyDataset(), batch_size=32)
38
+
39
+ def build_model(self):
40
+ return nn.Sequential(
41
+ nn.Linear(784, 128),
42
+ nn.ReLU(),
43
+ nn.Linear(128, 10),
44
+ ).to(self.device)
45
+
46
+ def build_optimizer(self):
47
+ return torch.optim.AdamW(self.model.parameters(), lr=3e-4)
48
+
49
+ def build_hooks(self):
50
+ return [
51
+ ProgressHook(interval=50, with_records=True),
52
+ CheckpointingHook(interval=500, keep_previous=2),
53
+ ]
54
+
55
+ def forward(self, batch):
56
+ x, y = batch
57
+ x, y = x.to(self.device), y.to(self.device)
58
+ logits = self.model(x)
59
+ loss = nn.functional.cross_entropy(logits, y)
60
+ accuracy = (logits.argmax(1) == y).float().mean().item()
61
+ return loss, {"accuracy": accuracy}
62
+
63
+
64
+ trainer = MyTrainer(max_steps=2000, device="cpu", workspace="runs/demo")
65
+ trainer.train()
66
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trainloop"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "Minimal PyTorch training loop with hooks and checkpointing."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -18,9 +18,10 @@ build-backend = "uv_build"
18
18
 
19
19
  [dependency-groups]
20
20
  dev = [
21
- "wandb>=0.20.1",
22
21
  "pytest>=8.4.0",
23
22
  "ruff>=0.11.13",
24
23
  "zensical>=0.0.10",
25
24
  "mkdocstrings-python>=2.0.1",
25
+ "wandb>=0.20.1",
26
+ "numpy>=2.2.6",
26
27
  ]
@@ -30,6 +30,23 @@ from .trainer import BaseTrainer, Records
30
30
  from .utils import flatten_nested_dict, key_average
31
31
 
32
32
 
33
+ def _dist_is_initialized() -> bool:
34
+ return dist.is_available() and dist.is_initialized()
35
+
36
+
37
+ def _dist_world_size() -> int:
38
+ return dist.get_world_size() if _dist_is_initialized() else 1
39
+
40
+
41
+ def _dist_rank() -> int:
42
+ return dist.get_rank() if _dist_is_initialized() else 0
43
+
44
+
45
+ def _dist_barrier() -> None:
46
+ if _dist_is_initialized() and dist.get_world_size() > 1:
47
+ dist.barrier()
48
+
49
+
33
50
  class BaseHook:
34
51
  """Lifecycle hooks for `BaseTrainer`."""
35
52
 
@@ -84,6 +101,7 @@ class _StatsHook(BaseHook):
84
101
  self.grad_norms = []
85
102
  self.data_times = []
86
103
  self.step_times = []
104
+ self.non_finite_grad_retry_counts = []
87
105
  self.max_memories = []
88
106
 
89
107
  def on_after_step(self, trainer: BaseTrainer):
@@ -94,6 +112,9 @@ class _StatsHook(BaseHook):
94
112
  self.records_ls.append(key_average(trainer.step_info["records"]))
95
113
  self.data_times.append(sum(trainer.step_info["data_time"])) # total
96
114
  self.step_times.append(trainer.step_info["step_time"])
115
+ self.non_finite_grad_retry_counts.append(
116
+ trainer.step_info["non_finite_grad_retry_count"]
117
+ )
97
118
  if "max_memory" in trainer.step_info:
98
119
  self.max_memories.append(trainer.step_info["max_memory"])
99
120
 
@@ -104,9 +125,12 @@ class _StatsHook(BaseHook):
104
125
  records = key_average(self.records_ls)
105
126
  data_time = sum(self.data_times) / len(self.data_times)
106
127
  step_time = sum(self.step_times) / len(self.step_times)
128
+ non_finite_grad_retry_count = sum(self.non_finite_grad_retry_counts) / len(
129
+ self.non_finite_grad_retry_counts
130
+ )
107
131
  max_memory = max(self.max_memories) if self.max_memories else None
108
132
 
109
- if self.sync:
133
+ if self.sync and _dist_world_size() > 1:
110
134
  # aggregate accross all ranks
111
135
  dist.all_reduce(loss, op=dist.ReduceOp.AVG)
112
136
  if grad_norm is not None:
@@ -119,12 +143,16 @@ class _StatsHook(BaseHook):
119
143
  "records": records,
120
144
  "data_time": data_time,
121
145
  "step_time": step_time,
146
+ "non_finite_grad_retry_count": non_finite_grad_retry_count,
122
147
  "max_memory": max_memory,
123
148
  },
124
149
  )
125
150
  records = key_average([stat["records"] for stat in gathered])
126
151
  data_time = sum(stat["data_time"] for stat in gathered) / len(gathered)
127
152
  step_time = sum(stat["step_time"] for stat in gathered) / len(gathered)
153
+ non_finite_grad_retry_count = sum(
154
+ stat["non_finite_grad_retry_count"] for stat in gathered
155
+ ) / len(gathered)
128
156
  if "max_memory" in trainer.step_info:
129
157
  max_memory = max(stat["max_memory"] for stat in gathered)
130
158
 
@@ -134,6 +162,7 @@ class _StatsHook(BaseHook):
134
162
  grad_norm.item() if grad_norm is not None else None,
135
163
  step_time,
136
164
  data_time,
165
+ non_finite_grad_retry_count,
137
166
  max_memory,
138
167
  records,
139
168
  )
@@ -146,6 +175,7 @@ class _StatsHook(BaseHook):
146
175
  grad_norm: float | None,
147
176
  step_time: float,
148
177
  data_time: float,
178
+ non_finite_grad_retry_count: float,
149
179
  max_memory: float | None,
150
180
  records: Records,
151
181
  ):
@@ -233,6 +263,7 @@ class ProgressHook(_StatsHook):
233
263
  grad_norm: float | None,
234
264
  step_time: float,
235
265
  data_time: float,
266
+ non_finite_grad_retry_count: float,
236
267
  max_memory: float | None,
237
268
  records: Records,
238
269
  ):
@@ -285,6 +316,7 @@ class LoggingHook(_StatsHook):
285
316
  grad_norm: float | None,
286
317
  step_time: float,
287
318
  data_time: float,
319
+ non_finite_grad_retry_count: float,
288
320
  max_memory: float | None,
289
321
  records: Records,
290
322
  ):
@@ -301,6 +333,7 @@ class LoggingHook(_StatsHook):
301
333
  "loss": loss,
302
334
  "data_time": data_time,
303
335
  "step_time": step_time,
336
+ "non_finite_grad_retry_count": non_finite_grad_retry_count,
304
337
  "lr": {f"group_{i}": lr for i, lr in lrs},
305
338
  }
306
339
  }
@@ -326,7 +359,7 @@ class CheckpointingHook(BaseHook):
326
359
  interval: int,
327
360
  keep_previous: int = 0, # keep N previous checkpoints
328
361
  keep_interval: int = 0, # keep checkpoints of every N-th step
329
- path: Path | str = "checkpoint",
362
+ path: Path | str = "checkpoints",
330
363
  load: Path | str | Literal["latest"] | None = "latest",
331
364
  exit_signals: list[signal.Signals] | signal.Signals = None,
332
365
  exit_code: int | Literal["128+signal"] = "128+signal",
@@ -375,9 +408,7 @@ class CheckpointingHook(BaseHook):
375
408
 
376
409
  trainer.logger.info(f"=> Loading checkpoint from {load_path} ...")
377
410
  state_dict = {
378
- file.with_suffix("").name: torch.load(
379
- file, map_location=trainer.device, weights_only=True
380
- )
411
+ file.with_suffix("").name: torch.load(file, map_location=trainer.device)
381
412
  for file in load_path.iterdir()
382
413
  if file.is_file() and file.suffix == ".pt"
383
414
  }
@@ -387,15 +418,19 @@ class CheckpointingHook(BaseHook):
387
418
  def on_before_step(self, trainer: BaseTrainer):
388
419
  if self.has_exit_signal_handlers:
389
420
  self.dist_exit_signal.fill_(self.local_exit_signal)
390
- # micro optimization: reduce async during step and read after step
391
- self.dist_exit_signal_work = dist.all_reduce(
392
- self.dist_exit_signal, op=dist.ReduceOp.MAX, async_op=True
393
- )
421
+ if _dist_world_size() > 1:
422
+ # micro optimization: reduce async during step and read after step
423
+ self.dist_exit_signal_work = dist.all_reduce(
424
+ self.dist_exit_signal, op=dist.ReduceOp.MAX, async_op=True
425
+ )
426
+ else:
427
+ self.dist_exit_signal_work = None
394
428
 
395
429
  def on_after_step(self, trainer: BaseTrainer):
396
430
  save_and_exit = False
397
431
  if self.has_exit_signal_handlers:
398
- self.dist_exit_signal_work.wait()
432
+ if self.dist_exit_signal_work is not None:
433
+ self.dist_exit_signal_work.wait()
399
434
  exit_signal = self.dist_exit_signal.item()
400
435
  save_and_exit = exit_signal != -1
401
436
 
@@ -414,7 +449,7 @@ class CheckpointingHook(BaseHook):
414
449
  keep=self.keep_interval > 0 and trainer.step % self.keep_interval == 0,
415
450
  )
416
451
  if save_and_exit:
417
- dist.barrier()
452
+ _dist_barrier()
418
453
  if self.exit_wait > 0:
419
454
  trainer.logger.info(
420
455
  f"=> Waiting {self.exit_wait:.0f} seconds before exit ..."
@@ -435,7 +470,7 @@ class CheckpointingHook(BaseHook):
435
470
  while retaining or pruning older checkpoints are logged but not raised.
436
471
  """
437
472
 
438
- dist.barrier()
473
+ _dist_barrier()
439
474
 
440
475
  state_dict = trainer.state_dict()
441
476
 
@@ -447,7 +482,7 @@ class CheckpointingHook(BaseHook):
447
482
  # dst=0,
448
483
  # )
449
484
 
450
- if dist.get_rank() == 0:
485
+ if _dist_rank() == 0:
451
486
  # make dir
452
487
  save_path = self.path / str(trainer.step)
453
488
  if not save_path.is_absolute():
@@ -606,7 +641,7 @@ class WandbHook(BaseHook):
606
641
  self.wandb_kwargs = wandb_kwargs
607
642
 
608
643
  def on_before_train(self, trainer: BaseTrainer):
609
- if dist.get_rank() == 0:
644
+ if _dist_rank() == 0:
610
645
  wandb_run_id = self._load_wandb_run_id(trainer)
611
646
 
612
647
  tags = os.getenv("WANDB_TAGS", "")
@@ -627,11 +662,11 @@ class WandbHook(BaseHook):
627
662
  self._save_wandb_run_id(trainer, self.wandb.id)
628
663
 
629
664
  def on_after_train(self, trainer: BaseTrainer):
630
- if dist.get_rank() == 0:
665
+ if _dist_rank() == 0:
631
666
  self.wandb.finish()
632
667
 
633
668
  def on_log(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
634
- if dist.get_rank() == 0:
669
+ if _dist_rank() == 0:
635
670
  data = {"/".join(k): v for k, v in flatten_nested_dict(records).items()}
636
671
  if not dry_run:
637
672
  self.wandb.log(data, step=trainer.step)
@@ -639,7 +674,7 @@ class WandbHook(BaseHook):
639
674
  trainer.logger.debug(f"Dry run log. Would log: {data}")
640
675
 
641
676
  def on_log_images(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
642
- if dist.get_rank() == 0:
677
+ if _dist_rank() == 0:
643
678
  wandb_data = {}
644
679
  for k, img in flatten_nested_dict({"vis": records}).items():
645
680
  file_type = self.image_format(k[-1])
@@ -705,7 +740,7 @@ class ImageFileLoggerHook(BaseHook):
705
740
  self.image_format = lambda _: image_format
706
741
 
707
742
  def on_log_images(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
708
- if dist.get_rank() == 0:
743
+ if _dist_rank() == 0:
709
744
  for k, img in flatten_nested_dict(records).items():
710
745
  p = trainer.workspace / "visualizations" / str(trainer.step) / Path(*k)
711
746
  p = Path(str(p) + "." + self.image_format(k[-1]))
@@ -204,6 +204,7 @@ class BaseTrainer:
204
204
  reset_step_info()
205
205
  self.step_info["data_time"] = []
206
206
  non_finite_grad_retry_count = 0
207
+ self.step_info["non_finite_grad_retry_count"] = non_finite_grad_retry_count
207
208
  i_acc = 0
208
209
  while i_acc < self.gradient_accumulation_steps:
209
210
  is_accumulating = i_acc < self.gradient_accumulation_steps - 1
@@ -275,6 +276,7 @@ class BaseTrainer:
275
276
  < self.max_non_finite_grad_retries
276
277
  ):
277
278
  non_finite_grad_retry_count += 1
279
+ self.step_info["non_finite_grad_retry_count"] = non_finite_grad_retry_count
278
280
  self.logger.warning(
279
281
  f"Gradient is non-finite. Retrying step {self.step} (retry {non_finite_grad_retry_count}"
280
282
  + (
trainloop-0.1.0/PKG-INFO DELETED
@@ -1,24 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: trainloop
3
- Version: 0.1.0
4
- Summary: Minimal PyTorch training loop with hooks and checkpointing.
5
- Author: Karim Abou Zeid
6
- Author-email: Karim Abou Zeid <contact@ka.codes>
7
- Requires-Dist: pillow>=11.3.0
8
- Requires-Dist: torch>=2.0.0
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
-
12
- # trainloop
13
-
14
- [![PyPI version](https://img.shields.io/pypi/v/trainloop.svg)](https://pypi.org/project/trainloop/)
15
-
16
- Minimal PyTorch training loop with hooks for logging, checkpointing, and customization.
17
-
18
- Docs: https://kabouzeid.github.io/trainloop/
19
-
20
- ## Install
21
-
22
- ```bash
23
- pip install trainloop
24
- ```
trainloop-0.1.0/README.md DELETED
@@ -1,13 +0,0 @@
1
- # trainloop
2
-
3
- [![PyPI version](https://img.shields.io/pypi/v/trainloop.svg)](https://pypi.org/project/trainloop/)
4
-
5
- Minimal PyTorch training loop with hooks for logging, checkpointing, and customization.
6
-
7
- Docs: https://kabouzeid.github.io/trainloop/
8
-
9
- ## Install
10
-
11
- ```bash
12
- pip install trainloop
13
- ```