trainloop 0.7.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.7.0 → trainloop-0.9.0}/PKG-INFO +2 -3
- trainloop-0.9.0/pyproject.toml +27 -0
- trainloop-0.7.0/pyproject.toml → trainloop-0.9.0/pyproject.toml.orig +4 -3
- {trainloop-0.7.0 → trainloop-0.9.0}/src/trainloop/__init__.py +9 -7
- {trainloop-0.7.0 → trainloop-0.9.0}/src/trainloop/hooks.py +175 -35
- {trainloop-0.7.0 → trainloop-0.9.0}/src/trainloop/trainer.py +17 -13
- {trainloop-0.7.0 → trainloop-0.9.0}/src/trainloop/utils.py +21 -2
- {trainloop-0.7.0 → trainloop-0.9.0}/README.md +0 -0
- {trainloop-0.7.0 → trainloop-0.9.0}/src/trainloop/py.typed +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: trainloop
|
|
3
|
-
Version: 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>
|
|
7
|
-
Requires-Dist:
|
|
8
|
-
Requires-Dist: torch>=2.0.0
|
|
7
|
+
Requires-Dist: torch>=2.1.0
|
|
9
8
|
Requires-Python: >=3.10
|
|
10
9
|
Description-Content-Type: text/markdown
|
|
11
10
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "trainloop"
|
|
3
|
+
version = "0.9.0"
|
|
4
|
+
description = "Minimal PyTorch training loop with hooks and checkpointing."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = ["torch>=2.1.0"]
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "Karim Knaebel"
|
|
11
|
+
email = "contact@knaebel.dev"
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["uv_build>=0.9.0,<0.10.0"]
|
|
15
|
+
build-backend = "uv_build"
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.4.0",
|
|
20
|
+
"ruff>=0.11.13",
|
|
21
|
+
"zensical>=0.0.10",
|
|
22
|
+
"mkdocstrings-python>=2.0.1",
|
|
23
|
+
"pillow>=11.3.0",
|
|
24
|
+
"tensorboard>=2.20.0",
|
|
25
|
+
"wandb>=0.20.1",
|
|
26
|
+
"numpy>=2.2.6",
|
|
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 = [
|
|
@@ -8,8 +8,7 @@ authors = [
|
|
|
8
8
|
]
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
|
-
"
|
|
12
|
-
"torch>=2.0.0",
|
|
11
|
+
"torch>=2.1.0",
|
|
13
12
|
]
|
|
14
13
|
|
|
15
14
|
[build-system]
|
|
@@ -22,6 +21,8 @@ dev = [
|
|
|
22
21
|
"ruff>=0.11.13",
|
|
23
22
|
"zensical>=0.0.10",
|
|
24
23
|
"mkdocstrings-python>=2.0.1",
|
|
24
|
+
"pillow>=11.3.0",
|
|
25
|
+
"tensorboard>=2.20.0",
|
|
25
26
|
"wandb>=0.20.1",
|
|
26
27
|
"numpy>=2.2.6",
|
|
27
28
|
]
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
from .hooks import (
|
|
2
2
|
BaseHook,
|
|
3
3
|
CheckpointingHook,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
CUDAMaxMemoryHook,
|
|
5
|
+
EMAHook,
|
|
6
6
|
ImageFileLoggerHook,
|
|
7
7
|
ProgressHook,
|
|
8
8
|
StatsHook,
|
|
9
|
+
TensorBoardHook,
|
|
9
10
|
TrainingStats,
|
|
10
11
|
WandbHook,
|
|
11
12
|
)
|
|
12
13
|
from .trainer import BaseTrainer, LossNoneWarning, map_nested_tensor
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
|
-
"BaseTrainer",
|
|
16
16
|
"BaseHook",
|
|
17
|
+
"BaseTrainer",
|
|
18
|
+
"CUDAMaxMemoryHook",
|
|
17
19
|
"CheckpointingHook",
|
|
18
|
-
"
|
|
20
|
+
"EMAHook",
|
|
21
|
+
"ImageFileLoggerHook",
|
|
22
|
+
"LossNoneWarning",
|
|
19
23
|
"ProgressHook",
|
|
20
24
|
"StatsHook",
|
|
25
|
+
"TensorBoardHook",
|
|
21
26
|
"TrainingStats",
|
|
22
|
-
"EmaHook",
|
|
23
27
|
"WandbHook",
|
|
24
|
-
"ImageFileLoggerHook",
|
|
25
|
-
"LossNoneWarning",
|
|
26
28
|
"map_nested_tensor",
|
|
27
29
|
]
|
|
@@ -5,21 +5,20 @@ import sys
|
|
|
5
5
|
import tempfile
|
|
6
6
|
import time
|
|
7
7
|
import warnings
|
|
8
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from datetime import timedelta
|
|
10
11
|
from numbers import Number
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Any,
|
|
13
|
+
from typing import Any, Literal
|
|
13
14
|
|
|
14
15
|
import torch
|
|
15
16
|
import torch.distributed as dist
|
|
16
|
-
from PIL import Image
|
|
17
|
-
from PIL.Image import Image as PILImage
|
|
18
17
|
from torch.distributed.checkpoint.state_dict import (
|
|
19
18
|
get_model_state_dict,
|
|
20
19
|
set_model_state_dict,
|
|
21
20
|
)
|
|
22
|
-
from torch.optim.swa_utils import AveragedModel,
|
|
21
|
+
from torch.optim.swa_utils import AveragedModel, get_ema_multi_avg_fn
|
|
23
22
|
|
|
24
23
|
try:
|
|
25
24
|
import wandb
|
|
@@ -27,8 +26,26 @@ except ImportError:
|
|
|
27
26
|
# only needed for WandbHook
|
|
28
27
|
pass
|
|
29
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
|
+
|
|
36
|
+
try:
|
|
37
|
+
from PIL import Image
|
|
38
|
+
from PIL.Image import Image as PILImage
|
|
39
|
+
except ImportError:
|
|
40
|
+
# only needed for WandbHook and TensorBoardHook image conversion
|
|
41
|
+
pass
|
|
42
|
+
|
|
30
43
|
from .trainer import BaseTrainer, Records
|
|
31
|
-
from .utils import
|
|
44
|
+
from .utils import (
|
|
45
|
+
flatten_nested_dict,
|
|
46
|
+
key_average,
|
|
47
|
+
log_state_dict_incompatible_keys,
|
|
48
|
+
)
|
|
32
49
|
|
|
33
50
|
|
|
34
51
|
def _dist_is_initialized() -> bool:
|
|
@@ -85,8 +102,8 @@ class TrainingStats:
|
|
|
85
102
|
grad_norm: float | None
|
|
86
103
|
step_time: float
|
|
87
104
|
data_time: float
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
non_finite_grad_retries: float
|
|
106
|
+
cuda_max_memory: float | None
|
|
90
107
|
records: Records
|
|
91
108
|
param_groups: list[dict[str, Any]]
|
|
92
109
|
|
|
@@ -119,8 +136,8 @@ class StatsHook(BaseHook):
|
|
|
119
136
|
self.grad_norms = []
|
|
120
137
|
self.data_times = []
|
|
121
138
|
self.step_times = []
|
|
122
|
-
self.
|
|
123
|
-
self.
|
|
139
|
+
self.non_finite_grad_retries = []
|
|
140
|
+
self.cuda_max_memories = []
|
|
124
141
|
|
|
125
142
|
def on_before_step(self, trainer: BaseTrainer):
|
|
126
143
|
super().on_before_step(trainer)
|
|
@@ -137,11 +154,11 @@ class StatsHook(BaseHook):
|
|
|
137
154
|
self.records_ls.append(key_average(trainer.step_info["records"]))
|
|
138
155
|
self.data_times.append(sum(trainer.step_info["data_time"])) # total
|
|
139
156
|
self.step_times.append(trainer.step_info["step_time"])
|
|
140
|
-
self.
|
|
141
|
-
trainer.step_info["
|
|
157
|
+
self.non_finite_grad_retries.append(
|
|
158
|
+
trainer.step_info["non_finite_grad_retries"]
|
|
142
159
|
)
|
|
143
|
-
if "
|
|
144
|
-
self.
|
|
160
|
+
if "cuda_max_memory" in trainer.step_info:
|
|
161
|
+
self.cuda_max_memories.append(trainer.step_info["cuda_max_memory"])
|
|
145
162
|
|
|
146
163
|
if trainer.step % self.interval == 0 or trainer.step == trainer.max_steps:
|
|
147
164
|
# aggregate over steps
|
|
@@ -150,10 +167,12 @@ class StatsHook(BaseHook):
|
|
|
150
167
|
records = key_average(self.records_ls)
|
|
151
168
|
data_time = sum(self.data_times) / len(self.data_times)
|
|
152
169
|
step_time = sum(self.step_times) / len(self.step_times)
|
|
153
|
-
|
|
154
|
-
self.
|
|
170
|
+
non_finite_grad_retries = sum(self.non_finite_grad_retries) / len(
|
|
171
|
+
self.non_finite_grad_retries
|
|
172
|
+
)
|
|
173
|
+
cuda_max_memory = (
|
|
174
|
+
max(self.cuda_max_memories) if self.cuda_max_memories else None
|
|
155
175
|
)
|
|
156
|
-
max_memory = max(self.max_memories) if self.max_memories else None
|
|
157
176
|
|
|
158
177
|
if self.sync and _dist_world_size() > 1:
|
|
159
178
|
# aggregate accross all ranks
|
|
@@ -168,18 +187,18 @@ class StatsHook(BaseHook):
|
|
|
168
187
|
"records": records,
|
|
169
188
|
"data_time": data_time,
|
|
170
189
|
"step_time": step_time,
|
|
171
|
-
"
|
|
172
|
-
"
|
|
190
|
+
"non_finite_grad_retries": non_finite_grad_retries,
|
|
191
|
+
"cuda_max_memory": cuda_max_memory,
|
|
173
192
|
},
|
|
174
193
|
)
|
|
175
194
|
records = key_average([stat["records"] for stat in gathered])
|
|
176
195
|
data_time = sum(stat["data_time"] for stat in gathered) / len(gathered)
|
|
177
196
|
step_time = sum(stat["step_time"] for stat in gathered) / len(gathered)
|
|
178
|
-
|
|
179
|
-
stat["
|
|
197
|
+
non_finite_grad_retries = sum(
|
|
198
|
+
stat["non_finite_grad_retries"] for stat in gathered
|
|
180
199
|
) / len(gathered)
|
|
181
|
-
if "
|
|
182
|
-
|
|
200
|
+
if "cuda_max_memory" in trainer.step_info:
|
|
201
|
+
cuda_max_memory = max(stat["cuda_max_memory"] for stat in gathered)
|
|
183
202
|
|
|
184
203
|
self.callback(
|
|
185
204
|
trainer,
|
|
@@ -188,8 +207,8 @@ class StatsHook(BaseHook):
|
|
|
188
207
|
grad_norm=grad_norm.item() if grad_norm is not None else None,
|
|
189
208
|
step_time=step_time,
|
|
190
209
|
data_time=data_time,
|
|
191
|
-
|
|
192
|
-
|
|
210
|
+
non_finite_grad_retries=non_finite_grad_retries,
|
|
211
|
+
cuda_max_memory=cuda_max_memory,
|
|
193
212
|
records=records,
|
|
194
213
|
param_groups=self.param_groups,
|
|
195
214
|
),
|
|
@@ -277,8 +296,8 @@ class ProgressHook(StatsHook):
|
|
|
277
296
|
+ f" step {stats.step_time:.4f}{'s' if self.show_units else ''} data {stats.data_time:.4f}{'s' if self.show_units else ''}"
|
|
278
297
|
+ (f" eta {eta}" if eta is not None else "")
|
|
279
298
|
+ (
|
|
280
|
-
f" mem {stats.
|
|
281
|
-
if stats.
|
|
299
|
+
f" mem {stats.cuda_max_memory:#.3g}{'GiB' if self.show_units else ''}"
|
|
300
|
+
if stats.cuda_max_memory is not None
|
|
282
301
|
else ""
|
|
283
302
|
)
|
|
284
303
|
+ f" loss {stats.loss:.4f}"
|
|
@@ -333,7 +352,7 @@ class CheckpointingHook(BaseHook):
|
|
|
333
352
|
| None = None, # save and keep checkpoints at these steps
|
|
334
353
|
path: Path | str = "checkpoints",
|
|
335
354
|
load: Path | str | Literal["latest"] | None = "latest",
|
|
336
|
-
exit_signals: list[signal.Signals] | signal.Signals = None,
|
|
355
|
+
exit_signals: list[signal.Signals] | signal.Signals | None = None,
|
|
337
356
|
exit_code: int | Literal["128+signal"] = "128+signal",
|
|
338
357
|
exit_wait: timedelta | float = 0.0,
|
|
339
358
|
):
|
|
@@ -551,39 +570,54 @@ class CheckpointingHook(BaseHook):
|
|
|
551
570
|
return False
|
|
552
571
|
|
|
553
572
|
|
|
554
|
-
class
|
|
573
|
+
class CUDAMaxMemoryHook(BaseHook):
|
|
555
574
|
"""Record peak CUDA memory per step into ``trainer.step_info``."""
|
|
556
575
|
|
|
557
576
|
def on_before_step(self, trainer: BaseTrainer):
|
|
558
577
|
torch.cuda.reset_peak_memory_stats(trainer.device)
|
|
559
578
|
|
|
560
579
|
def on_after_step(self, trainer: BaseTrainer):
|
|
561
|
-
trainer.step_info["
|
|
580
|
+
trainer.step_info["cuda_max_memory"] = torch.cuda.max_memory_allocated(
|
|
562
581
|
trainer.device
|
|
563
582
|
) / (1024**3) # GiB
|
|
564
583
|
|
|
565
584
|
|
|
566
|
-
class
|
|
585
|
+
class EMAHook(BaseHook):
|
|
567
586
|
"""Maintain an exponential moving average of model weights.
|
|
568
587
|
|
|
569
588
|
Args:
|
|
570
589
|
decay: EMA decay rate.
|
|
590
|
+
use_buffers: Whether to include model buffers in the EMA.
|
|
571
591
|
"""
|
|
572
592
|
|
|
573
|
-
def __init__(self, decay: float):
|
|
593
|
+
def __init__(self, decay: float = 0.999, use_buffers: bool = False):
|
|
574
594
|
self.decay = decay
|
|
595
|
+
self.use_buffers = use_buffers
|
|
575
596
|
|
|
576
597
|
def on_before_train(self, trainer: BaseTrainer):
|
|
577
598
|
trainer.logger.info("=> Creating EMA model ...")
|
|
578
599
|
# Note that AveragedModel does not seem to support FSDP. It will crash here.
|
|
579
|
-
self.ema_model = AveragedModel(
|
|
600
|
+
self.ema_model = AveragedModel(
|
|
601
|
+
trainer.model,
|
|
602
|
+
multi_avg_fn=get_ema_multi_avg_fn(self.decay),
|
|
603
|
+
use_buffers=self.use_buffers,
|
|
604
|
+
)
|
|
605
|
+
# TODO: could be useful to implement a decay warmup via custom avg_fn
|
|
580
606
|
|
|
581
607
|
def on_after_step(self, trainer: BaseTrainer):
|
|
582
608
|
self.ema_model.update_parameters(trainer.model)
|
|
583
609
|
|
|
584
610
|
def on_load_state_dict(self, trainer: BaseTrainer, state_dict: dict):
|
|
585
611
|
trainer.logger.info("=> Loading EMA model state ...")
|
|
586
|
-
set_model_state_dict(
|
|
612
|
+
incompatible_keys = set_model_state_dict(
|
|
613
|
+
self.ema_model, state_dict["ema_model"]
|
|
614
|
+
)
|
|
615
|
+
# This currently doesn't do anything because strict=True is implicit.
|
|
616
|
+
log_state_dict_incompatible_keys(
|
|
617
|
+
trainer.logger,
|
|
618
|
+
incompatible_keys.missing_keys,
|
|
619
|
+
incompatible_keys.unexpected_keys,
|
|
620
|
+
)
|
|
587
621
|
|
|
588
622
|
def on_state_dict(self, trainer: BaseTrainer, state_dict: dict):
|
|
589
623
|
# Note: sadly, we need to keep the AveragedModel wrapper, to save its n_averaged buffer
|
|
@@ -653,9 +687,18 @@ class WandbHook(BaseHook):
|
|
|
653
687
|
trainer.logger.debug(f"Dry run log. Would log: {data}")
|
|
654
688
|
|
|
655
689
|
def on_log_images(self, trainer: BaseTrainer, records: dict, dry_run: bool = False):
|
|
690
|
+
"""Note that the final component of each flattened key becomes the image caption,
|
|
691
|
+
and the remaining components become the W&B key. For example,
|
|
692
|
+
``foo/bar/name1`` and ``foo/bar/name2`` are logged as two images under
|
|
693
|
+
``foo/bar``, so W&B displays them together in one panel.
|
|
694
|
+
|
|
695
|
+
The list is replaced rather than extended when the same W&B key is logged
|
|
696
|
+
again at the same step. Callers should therefore collect all images for a
|
|
697
|
+
panel and pass them in a single call per step.
|
|
698
|
+
"""
|
|
656
699
|
if _dist_rank() == 0:
|
|
657
700
|
wandb_data = {}
|
|
658
|
-
for k, img in flatten_nested_dict(
|
|
701
|
+
for k, img in flatten_nested_dict(records).items():
|
|
659
702
|
file_type = self.image_format(k)
|
|
660
703
|
wandb_data.setdefault("/".join(k[:-1]), []).append(
|
|
661
704
|
wandb.Image(
|
|
@@ -673,7 +716,7 @@ class WandbHook(BaseHook):
|
|
|
673
716
|
trainer.logger.debug(f"Dry run log. Would log: {wandb_data}")
|
|
674
717
|
|
|
675
718
|
@staticmethod
|
|
676
|
-
def _ensure_jpeg_compatible(img: PILImage, bg_color: tuple = (255, 255, 255)):
|
|
719
|
+
def _ensure_jpeg_compatible(img: "PILImage", bg_color: tuple = (255, 255, 255)):
|
|
677
720
|
if img.mode in ("RGB", "L"):
|
|
678
721
|
return img
|
|
679
722
|
elif img.mode in ("RGBA", "LA"):
|
|
@@ -702,6 +745,103 @@ class WandbHook(BaseHook):
|
|
|
702
745
|
return None
|
|
703
746
|
|
|
704
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
|
+
|
|
705
845
|
class ImageFileLoggerHook(BaseHook):
|
|
706
846
|
"""Persist logged images to ``workspace/visualizations`` on rank 0.
|
|
707
847
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
3
|
import warnings
|
|
4
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
4
5
|
from contextlib import closing, nullcontext
|
|
5
6
|
from logging import Logger
|
|
6
7
|
from numbers import Number
|
|
@@ -8,21 +9,20 @@ from pathlib import Path
|
|
|
8
9
|
from typing import (
|
|
9
10
|
TYPE_CHECKING,
|
|
10
11
|
Any,
|
|
11
|
-
Callable,
|
|
12
|
-
Iterable,
|
|
13
|
-
Iterator,
|
|
14
12
|
TypeAlias,
|
|
15
13
|
Union,
|
|
16
14
|
)
|
|
17
15
|
|
|
18
16
|
import torch
|
|
19
|
-
|
|
17
|
+
from torch import nn
|
|
20
18
|
from torch.distributed.checkpoint.state_dict import (
|
|
21
19
|
StateDictOptions,
|
|
22
20
|
get_state_dict,
|
|
23
21
|
set_state_dict,
|
|
24
22
|
)
|
|
25
23
|
|
|
24
|
+
from .utils import log_state_dict_incompatible_keys
|
|
25
|
+
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
27
|
from .hooks import BaseHook
|
|
28
28
|
|
|
@@ -154,13 +154,18 @@ class BaseTrainer:
|
|
|
154
154
|
self.grad_scaler.load_state_dict(training_state["grad_scaler"])
|
|
155
155
|
|
|
156
156
|
self.logger.info("=> Loading model and optimizer state ...")
|
|
157
|
-
set_state_dict(
|
|
157
|
+
incompatible_keys = set_state_dict(
|
|
158
158
|
self.model,
|
|
159
159
|
self.optimizer,
|
|
160
160
|
model_state_dict=state_dict["model"],
|
|
161
161
|
optim_state_dict=training_state["optimizer"],
|
|
162
162
|
options=self.state_dict_options,
|
|
163
163
|
)
|
|
164
|
+
log_state_dict_incompatible_keys(
|
|
165
|
+
self.logger,
|
|
166
|
+
incompatible_keys.missing_keys,
|
|
167
|
+
incompatible_keys.unexpected_keys,
|
|
168
|
+
)
|
|
164
169
|
|
|
165
170
|
self.logger.info("=> Loading hook states ...")
|
|
166
171
|
for h in self.hooks:
|
|
@@ -203,8 +208,8 @@ class BaseTrainer:
|
|
|
203
208
|
|
|
204
209
|
reset_step_info()
|
|
205
210
|
self.step_info["data_time"] = []
|
|
206
|
-
|
|
207
|
-
self.step_info["
|
|
211
|
+
non_finite_grad_retries = 0
|
|
212
|
+
self.step_info["non_finite_grad_retries"] = non_finite_grad_retries
|
|
208
213
|
i_acc = 0
|
|
209
214
|
while i_acc < self.gradient_accumulation_steps:
|
|
210
215
|
is_accumulating = i_acc < self.gradient_accumulation_steps - 1
|
|
@@ -278,15 +283,14 @@ class BaseTrainer:
|
|
|
278
283
|
)
|
|
279
284
|
):
|
|
280
285
|
if self.max_non_finite_grad_retries is None or (
|
|
281
|
-
|
|
282
|
-
< self.max_non_finite_grad_retries
|
|
286
|
+
non_finite_grad_retries < self.max_non_finite_grad_retries
|
|
283
287
|
):
|
|
284
|
-
|
|
285
|
-
self.step_info["
|
|
286
|
-
|
|
288
|
+
non_finite_grad_retries += 1
|
|
289
|
+
self.step_info["non_finite_grad_retries"] = (
|
|
290
|
+
non_finite_grad_retries
|
|
287
291
|
)
|
|
288
292
|
self.logger.warning(
|
|
289
|
-
f"Gradient is non-finite. Retrying step {self.step} (retry {
|
|
293
|
+
f"Gradient is non-finite. Retrying step {self.step} (retry {non_finite_grad_retries}"
|
|
290
294
|
+ (
|
|
291
295
|
f"/{self.max_non_finite_grad_retries})."
|
|
292
296
|
if self.max_non_finite_grad_retries is not None
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# modified from: https://github.com/microsoft/MoGe/blob/6b8b43db567ca4b08615c39b42cffd6c76cada29/moge/utils/tools.py
|
|
2
2
|
|
|
3
3
|
import math
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Generator, MutableMapping
|
|
5
|
+
from logging import Logger
|
|
6
|
+
from typing import Any
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def traverse_nested_dict_keys(
|
|
@@ -50,7 +52,7 @@ def key_average(list_of_dicts: list, exclude_nan: bool = False) -> dict[str, Any
|
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
def flatten_nested_dict(
|
|
53
|
-
d: dict[str, Any], parent_key: tuple[str, ...] = None
|
|
55
|
+
d: dict[str, Any], parent_key: tuple[str, ...] | None = None
|
|
54
56
|
) -> dict[tuple[str, ...], Any]:
|
|
55
57
|
"""
|
|
56
58
|
Flattens a nested dictionary into a single-level dictionary, with keys as tuples.
|
|
@@ -65,3 +67,20 @@ def flatten_nested_dict(
|
|
|
65
67
|
else:
|
|
66
68
|
items.append((new_key, v))
|
|
67
69
|
return dict(items)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def log_state_dict_incompatible_keys(
|
|
73
|
+
logger: Logger, missing_keys: list[str], unexpected_keys: list[str]
|
|
74
|
+
) -> None:
|
|
75
|
+
if missing_keys:
|
|
76
|
+
logger.warning(
|
|
77
|
+
"Missing keys in state_dict: {}.".format(
|
|
78
|
+
", ".join(f'"{key}"' for key in missing_keys)
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
if unexpected_keys:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Unexpected keys in state_dict: {}.".format(
|
|
84
|
+
", ".join(f'"{key}"' for key in unexpected_keys)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
File without changes
|
|
File without changes
|