trainloop 0.8.0__tar.gz → 0.9.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.9.0}/PKG-INFO +1 -1
- {trainloop-0.8.0 → trainloop-0.9.0}/pyproject.toml +2 -1
- {trainloop-0.8.0 → trainloop-0.9.0}/pyproject.toml.orig +2 -1
- {trainloop-0.8.0 → trainloop-0.9.0}/src/trainloop/__init__.py +2 -0
- {trainloop-0.8.0 → trainloop-0.9.0}/src/trainloop/hooks.py +105 -1
- {trainloop-0.8.0 → trainloop-0.9.0}/README.md +0 -0
- {trainloop-0.8.0 → trainloop-0.9.0}/src/trainloop/py.typed +0 -0
- {trainloop-0.8.0 → trainloop-0.9.0}/src/trainloop/trainer.py +0 -0
- {trainloop-0.8.0 → trainloop-0.9.0}/src/trainloop/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "trainloop"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.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.9.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
|
|
@@ -738,6 +745,103 @@ class WandbHook(BaseHook):
|
|
|
738
745
|
return None
|
|
739
746
|
|
|
740
747
|
|
|
748
|
+
class TensorBoardHook(BaseHook):
|
|
749
|
+
"""Log metrics and images to TensorBoard (rank 0 only).
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
texts: Optional text values to log when training starts.
|
|
753
|
+
namespace_separator: Separator for nested tag prefixes; the final TensorBoard
|
|
754
|
+
name boundary is always "/".
|
|
755
|
+
"""
|
|
756
|
+
|
|
757
|
+
def __init__(
|
|
758
|
+
self,
|
|
759
|
+
texts: dict[str, Any] | None = None,
|
|
760
|
+
namespace_separator: str = "/",
|
|
761
|
+
):
|
|
762
|
+
self.writer = None
|
|
763
|
+
self.purge_step = None
|
|
764
|
+
self.texts = texts
|
|
765
|
+
self.namespace_separator = namespace_separator
|
|
766
|
+
|
|
767
|
+
def on_before_train(self, trainer: BaseTrainer):
|
|
768
|
+
if _dist_rank() == 0:
|
|
769
|
+
self._open_writer(trainer)
|
|
770
|
+
for k, v in (self.texts or {}).items():
|
|
771
|
+
self.writer.add_text(k, str(v))
|
|
772
|
+
self.writer.flush()
|
|
773
|
+
|
|
774
|
+
def on_load_state_dict(self, trainer: BaseTrainer, state_dict: dict):
|
|
775
|
+
self.purge_step = trainer.step + 1 # the step that will be logged next
|
|
776
|
+
if self.writer is not None:
|
|
777
|
+
self.writer.close()
|
|
778
|
+
self._open_writer(trainer)
|
|
779
|
+
|
|
780
|
+
def on_after_train(self, trainer: BaseTrainer):
|
|
781
|
+
if self.writer is None:
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
self.writer.close()
|
|
785
|
+
self.writer = None
|
|
786
|
+
|
|
787
|
+
def on_log(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
|
|
788
|
+
if self.writer is None:
|
|
789
|
+
return
|
|
790
|
+
|
|
791
|
+
data = {self._format_tag(k): v for k, v in flatten_nested_dict(records).items()}
|
|
792
|
+
if not dry_run:
|
|
793
|
+
for k, v in data.items():
|
|
794
|
+
self.writer.add_scalar(k, v, global_step=trainer.step)
|
|
795
|
+
self.writer.flush()
|
|
796
|
+
else:
|
|
797
|
+
trainer.logger.debug(f"Dry run log. Would log: {data}")
|
|
798
|
+
|
|
799
|
+
def on_log_images(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
|
|
800
|
+
if self.writer is None:
|
|
801
|
+
return
|
|
802
|
+
|
|
803
|
+
data = {
|
|
804
|
+
self._format_tag((*k, "image")): np.asarray(self._pil_to_rgb(v))
|
|
805
|
+
for k, v in flatten_nested_dict(records).items()
|
|
806
|
+
}
|
|
807
|
+
if not dry_run:
|
|
808
|
+
for k, v in data.items():
|
|
809
|
+
self.writer.add_image(k, v, global_step=trainer.step, dataformats="HWC")
|
|
810
|
+
self.writer.flush()
|
|
811
|
+
else:
|
|
812
|
+
trainer.logger.debug(f"Dry run log. Would log: {data}")
|
|
813
|
+
|
|
814
|
+
def _open_writer(self, trainer: BaseTrainer):
|
|
815
|
+
self.writer = SummaryWriter(
|
|
816
|
+
log_dir=str(trainer.workspace),
|
|
817
|
+
purge_step=self.purge_step,
|
|
818
|
+
max_queue=1_000, # we already flush after every step
|
|
819
|
+
)
|
|
820
|
+
self.purge_step = None
|
|
821
|
+
|
|
822
|
+
def _format_tag(self, key: tuple[str, ...]):
|
|
823
|
+
*namespace, name = key
|
|
824
|
+
return (
|
|
825
|
+
f"{self.namespace_separator.join(namespace)}/{name}" if namespace else name
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
@staticmethod
|
|
829
|
+
def _pil_to_rgb(img: "PILImage", bg_color: tuple = (255, 255, 255)):
|
|
830
|
+
if img.mode == "RGB":
|
|
831
|
+
return img
|
|
832
|
+
elif img.mode == "L":
|
|
833
|
+
return img.convert("RGB")
|
|
834
|
+
elif img.mode in ("RGBA", "LA"):
|
|
835
|
+
background = Image.new("RGB", img.size, bg_color)
|
|
836
|
+
background.paste(img, mask=img.getchannel("A"))
|
|
837
|
+
return background
|
|
838
|
+
else:
|
|
839
|
+
warnings.warn(
|
|
840
|
+
f"Trying to convert {img.mode} to RGB in a best-effort manner."
|
|
841
|
+
)
|
|
842
|
+
return img.convert("RGB")
|
|
843
|
+
|
|
844
|
+
|
|
741
845
|
class ImageFileLoggerHook(BaseHook):
|
|
742
846
|
"""Persist logged images to ``workspace/visualizations`` on rank 0.
|
|
743
847
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|