trainloop 0.8.0__tar.gz → 0.10.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.
- {trainloop-0.8.0 → trainloop-0.10.0}/PKG-INFO +1 -1
- {trainloop-0.8.0 → trainloop-0.10.0}/pyproject.toml +2 -1
- {trainloop-0.8.0 → trainloop-0.10.0}/pyproject.toml.orig +2 -1
- {trainloop-0.8.0 → trainloop-0.10.0}/src/trainloop/__init__.py +2 -0
- {trainloop-0.8.0 → trainloop-0.10.0}/src/trainloop/hooks.py +120 -3
- {trainloop-0.8.0 → trainloop-0.10.0}/src/trainloop/trainer.py +10 -5
- {trainloop-0.8.0 → trainloop-0.10.0}/README.md +0 -0
- {trainloop-0.8.0 → trainloop-0.10.0}/src/trainloop/py.typed +0 -0
- {trainloop-0.8.0 → trainloop-0.10.0}/src/trainloop/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "trainloop"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
description = "Minimal PyTorch training loop with hooks and checkpointing."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -21,6 +21,7 @@ dev = [
|
|
|
21
21
|
"zensical>=0.0.10",
|
|
22
22
|
"mkdocstrings-python>=2.0.1",
|
|
23
23
|
"pillow>=11.3.0",
|
|
24
|
+
"tensorboard>=2.20.0",
|
|
24
25
|
"wandb>=0.20.1",
|
|
25
26
|
"numpy>=2.2.6",
|
|
26
27
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "trainloop"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
description = "Minimal PyTorch training loop with hooks and checkpointing."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -22,6 +22,7 @@ dev = [
|
|
|
22
22
|
"zensical>=0.0.10",
|
|
23
23
|
"mkdocstrings-python>=2.0.1",
|
|
24
24
|
"pillow>=11.3.0",
|
|
25
|
+
"tensorboard>=2.20.0",
|
|
25
26
|
"wandb>=0.20.1",
|
|
26
27
|
"numpy>=2.2.6",
|
|
27
28
|
]
|
|
@@ -6,6 +6,7 @@ from .hooks import (
|
|
|
6
6
|
ImageFileLoggerHook,
|
|
7
7
|
ProgressHook,
|
|
8
8
|
StatsHook,
|
|
9
|
+
TensorBoardHook,
|
|
9
10
|
TrainingStats,
|
|
10
11
|
WandbHook,
|
|
11
12
|
)
|
|
@@ -21,6 +22,7 @@ __all__ = [
|
|
|
21
22
|
"LossNoneWarning",
|
|
22
23
|
"ProgressHook",
|
|
23
24
|
"StatsHook",
|
|
25
|
+
"TensorBoardHook",
|
|
24
26
|
"TrainingStats",
|
|
25
27
|
"WandbHook",
|
|
26
28
|
"map_nested_tensor",
|
|
@@ -26,11 +26,18 @@ except ImportError:
|
|
|
26
26
|
# only needed for WandbHook
|
|
27
27
|
pass
|
|
28
28
|
|
|
29
|
+
try:
|
|
30
|
+
import numpy as np
|
|
31
|
+
from torch.utils.tensorboard import SummaryWriter
|
|
32
|
+
except ImportError:
|
|
33
|
+
# only needed for TensorBoardHook
|
|
34
|
+
pass
|
|
35
|
+
|
|
29
36
|
try:
|
|
30
37
|
from PIL import Image
|
|
31
38
|
from PIL.Image import Image as PILImage
|
|
32
39
|
except ImportError:
|
|
33
|
-
# only needed for WandbHook
|
|
40
|
+
# only needed for WandbHook and TensorBoardHook image conversion
|
|
34
41
|
pass
|
|
35
42
|
|
|
36
43
|
from .trainer import BaseTrainer, Records
|
|
@@ -76,7 +83,9 @@ class BaseHook:
|
|
|
76
83
|
def on_after_train(self, trainer: BaseTrainer):
|
|
77
84
|
pass
|
|
78
85
|
|
|
79
|
-
def
|
|
86
|
+
def on_log_scalars(
|
|
87
|
+
self, trainer: BaseTrainer, records: dict, dry_run: bool = False
|
|
88
|
+
):
|
|
80
89
|
pass
|
|
81
90
|
|
|
82
91
|
def on_log_images(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
|
|
@@ -620,6 +629,9 @@ class EMAHook(BaseHook):
|
|
|
620
629
|
class WandbHook(BaseHook):
|
|
621
630
|
"""Log metrics and images to Weights & Biases (rank 0 only).
|
|
622
631
|
|
|
632
|
+
Nested scalar key components are joined with ``/``. Avoid ``/`` within an
|
|
633
|
+
individual component because it is also W&B's panel namespace separator.
|
|
634
|
+
|
|
623
635
|
Args:
|
|
624
636
|
project: W&B project name.
|
|
625
637
|
config: Optional config dict or JSON file path to log.
|
|
@@ -671,7 +683,9 @@ class WandbHook(BaseHook):
|
|
|
671
683
|
if _dist_rank() == 0:
|
|
672
684
|
self.wandb.finish()
|
|
673
685
|
|
|
674
|
-
def
|
|
686
|
+
def on_log_scalars(
|
|
687
|
+
self, trainer: BaseTrainer, records: dict, dry_run: bool = False
|
|
688
|
+
):
|
|
675
689
|
if _dist_rank() == 0:
|
|
676
690
|
data = {"/".join(k): v for k, v in flatten_nested_dict(records).items()}
|
|
677
691
|
if not dry_run:
|
|
@@ -738,6 +752,109 @@ class WandbHook(BaseHook):
|
|
|
738
752
|
return None
|
|
739
753
|
|
|
740
754
|
|
|
755
|
+
class TensorBoardHook(BaseHook):
|
|
756
|
+
"""Log metrics and images to TensorBoard (rank 0 only).
|
|
757
|
+
|
|
758
|
+
Nested namespace components are joined with ``namespace_separator`` and
|
|
759
|
+
the final name is separated with ``/``. Avoid these separators within
|
|
760
|
+
individual keys because TensorBoard uses them to organize tags.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
texts: Optional text values to log when training starts.
|
|
764
|
+
namespace_separator: Separator for nested tag prefixes; the final TensorBoard
|
|
765
|
+
name boundary is always "/".
|
|
766
|
+
"""
|
|
767
|
+
|
|
768
|
+
def __init__(
|
|
769
|
+
self,
|
|
770
|
+
texts: dict[str, Any] | None = None,
|
|
771
|
+
namespace_separator: str = "/",
|
|
772
|
+
):
|
|
773
|
+
self.writer = None
|
|
774
|
+
self.purge_step = None
|
|
775
|
+
self.texts = texts
|
|
776
|
+
self.namespace_separator = namespace_separator
|
|
777
|
+
|
|
778
|
+
def on_before_train(self, trainer: BaseTrainer):
|
|
779
|
+
if _dist_rank() == 0:
|
|
780
|
+
self._open_writer(trainer)
|
|
781
|
+
for k, v in (self.texts or {}).items():
|
|
782
|
+
self.writer.add_text(k, str(v))
|
|
783
|
+
self.writer.flush()
|
|
784
|
+
|
|
785
|
+
def on_load_state_dict(self, trainer: BaseTrainer, state_dict: dict):
|
|
786
|
+
self.purge_step = trainer.step + 1 # the step that will be logged next
|
|
787
|
+
if self.writer is not None:
|
|
788
|
+
self.writer.close()
|
|
789
|
+
self._open_writer(trainer)
|
|
790
|
+
|
|
791
|
+
def on_after_train(self, trainer: BaseTrainer):
|
|
792
|
+
if self.writer is None:
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
self.writer.close()
|
|
796
|
+
self.writer = None
|
|
797
|
+
|
|
798
|
+
def on_log_scalars(
|
|
799
|
+
self, trainer: BaseTrainer, records: dict, dry_run: bool = False
|
|
800
|
+
):
|
|
801
|
+
if self.writer is None:
|
|
802
|
+
return
|
|
803
|
+
|
|
804
|
+
data = {self._format_tag(k): v for k, v in flatten_nested_dict(records).items()}
|
|
805
|
+
if not dry_run:
|
|
806
|
+
for k, v in data.items():
|
|
807
|
+
self.writer.add_scalar(k, v, global_step=trainer.step)
|
|
808
|
+
self.writer.flush()
|
|
809
|
+
else:
|
|
810
|
+
trainer.logger.debug(f"Dry run log. Would log: {data}")
|
|
811
|
+
|
|
812
|
+
def on_log_images(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
|
|
813
|
+
if self.writer is None:
|
|
814
|
+
return
|
|
815
|
+
|
|
816
|
+
data = {
|
|
817
|
+
self._format_tag((*k, "image")): np.asarray(self._pil_to_rgb(v))
|
|
818
|
+
for k, v in flatten_nested_dict(records).items()
|
|
819
|
+
}
|
|
820
|
+
if not dry_run:
|
|
821
|
+
for k, v in data.items():
|
|
822
|
+
self.writer.add_image(k, v, global_step=trainer.step, dataformats="HWC")
|
|
823
|
+
self.writer.flush()
|
|
824
|
+
else:
|
|
825
|
+
trainer.logger.debug(f"Dry run log. Would log: {data}")
|
|
826
|
+
|
|
827
|
+
def _open_writer(self, trainer: BaseTrainer):
|
|
828
|
+
self.writer = SummaryWriter(
|
|
829
|
+
log_dir=str(trainer.workspace),
|
|
830
|
+
purge_step=self.purge_step,
|
|
831
|
+
max_queue=1_000, # we already flush after every step
|
|
832
|
+
)
|
|
833
|
+
self.purge_step = None
|
|
834
|
+
|
|
835
|
+
def _format_tag(self, key: tuple[str, ...]):
|
|
836
|
+
*namespace, name = key
|
|
837
|
+
return (
|
|
838
|
+
f"{self.namespace_separator.join(namespace)}/{name}" if namespace else name
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
@staticmethod
|
|
842
|
+
def _pil_to_rgb(img: "PILImage", bg_color: tuple = (255, 255, 255)):
|
|
843
|
+
if img.mode == "RGB":
|
|
844
|
+
return img
|
|
845
|
+
elif img.mode == "L":
|
|
846
|
+
return img.convert("RGB")
|
|
847
|
+
elif img.mode in ("RGBA", "LA"):
|
|
848
|
+
background = Image.new("RGB", img.size, bg_color)
|
|
849
|
+
background.paste(img, mask=img.getchannel("A"))
|
|
850
|
+
return background
|
|
851
|
+
else:
|
|
852
|
+
warnings.warn(
|
|
853
|
+
f"Trying to convert {img.mode} to RGB in a best-effort manner."
|
|
854
|
+
)
|
|
855
|
+
return img.convert("RGB")
|
|
856
|
+
|
|
857
|
+
|
|
741
858
|
class ImageFileLoggerHook(BaseHook):
|
|
742
859
|
"""Persist logged images to ``workspace/visualizations`` on rank 0.
|
|
743
860
|
|
|
@@ -349,17 +349,22 @@ class BaseTrainer:
|
|
|
349
349
|
def unwrapped_model(self):
|
|
350
350
|
return self.unwrap(self.model)
|
|
351
351
|
|
|
352
|
-
def
|
|
352
|
+
def log_scalars(self, records: dict[str, Any], dry_run: bool = False):
|
|
353
353
|
"""
|
|
354
|
-
Dispatch
|
|
354
|
+
Dispatch nested scalar records to logging hooks.
|
|
355
|
+
|
|
356
|
+
Dictionary keys are treated as path components. Logging hooks may join
|
|
357
|
+
nested components with backend-specific namespace separators, such as
|
|
358
|
+
``/``. Avoid those separators within individual keys because distinct
|
|
359
|
+
nested paths may otherwise map to the same backend metric name.
|
|
355
360
|
|
|
356
361
|
Args:
|
|
357
|
-
records: Nested dict of
|
|
362
|
+
records: Nested dict of scalar metrics to log.
|
|
358
363
|
dry_run: If True, hooks should avoid side effects and only report intent.
|
|
359
364
|
"""
|
|
360
|
-
self.logger.debug("
|
|
365
|
+
self.logger.debug("log_scalars()")
|
|
361
366
|
for h in self.hooks:
|
|
362
|
-
h.
|
|
367
|
+
h.on_log_scalars(self, records, dry_run=dry_run)
|
|
363
368
|
|
|
364
369
|
def log_images(self, records: dict[str, Any], dry_run: bool = False):
|
|
365
370
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|