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.
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: trainloop
3
- Version: 0.7.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: pillow>=11.3.0
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.7.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
- "pillow>=11.3.0",
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
- CudaMaxMemoryHook,
5
- EmaHook,
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
- "CudaMaxMemoryHook",
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, Callable, Iterable, Literal, Sequence
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, get_ema_avg_fn
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 flatten_nested_dict, key_average
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
- non_finite_grad_retry_count: float
89
- max_memory: float | None
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.non_finite_grad_retry_counts = []
123
- self.max_memories = []
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.non_finite_grad_retry_counts.append(
141
- trainer.step_info["non_finite_grad_retry_count"]
157
+ self.non_finite_grad_retries.append(
158
+ trainer.step_info["non_finite_grad_retries"]
142
159
  )
143
- if "max_memory" in trainer.step_info:
144
- self.max_memories.append(trainer.step_info["max_memory"])
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
- non_finite_grad_retry_count = sum(self.non_finite_grad_retry_counts) / len(
154
- self.non_finite_grad_retry_counts
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
- "non_finite_grad_retry_count": non_finite_grad_retry_count,
172
- "max_memory": max_memory,
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
- non_finite_grad_retry_count = sum(
179
- stat["non_finite_grad_retry_count"] for stat in gathered
197
+ non_finite_grad_retries = sum(
198
+ stat["non_finite_grad_retries"] for stat in gathered
180
199
  ) / len(gathered)
181
- if "max_memory" in trainer.step_info:
182
- max_memory = max(stat["max_memory"] for stat in gathered)
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
- non_finite_grad_retry_count=non_finite_grad_retry_count,
192
- max_memory=max_memory,
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.max_memory:#.3g}{'GiB' if self.show_units else ''}"
281
- if stats.max_memory is not None
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 CudaMaxMemoryHook(BaseHook):
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["max_memory"] = torch.cuda.max_memory_allocated(
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 EmaHook(BaseHook):
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(trainer.model, avg_fn=get_ema_avg_fn(self.decay))
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(self.ema_model, state_dict["ema_model"])
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({"vis": records}).items():
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
- import torch.nn as nn
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
- non_finite_grad_retry_count = 0
207
- self.step_info["non_finite_grad_retry_count"] = non_finite_grad_retry_count
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
- non_finite_grad_retry_count
282
- < self.max_non_finite_grad_retries
286
+ non_finite_grad_retries < self.max_non_finite_grad_retries
283
287
  ):
284
- non_finite_grad_retry_count += 1
285
- self.step_info["non_finite_grad_retry_count"] = (
286
- non_finite_grad_retry_count
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 {non_finite_grad_retry_count}"
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 typing import Any, Generator, MutableMapping
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