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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: trainloop
3
- Version: 0.8.0
3
+ Version: 0.9.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.8.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.8.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 JPEG conversion
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