Skip to content

Commit 037d1b6

Browse files
committed
Update composition and scaler to use mts.learn.nn.Module
This simplify the forward that no longer needs to move data to a different dtype. Instead the model is moved once in the trainer and then stays on the same device
1 parent 81324ac commit 037d1b6

52 files changed

Lines changed: 493 additions & 686 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/metatrain/composition/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def train_or_load_composition_model(
3131
atomic_baseline: FixedCompositionWeights | str,
3232
train_datasets: List[Union[Dataset, Subset]],
3333
other_additive_models: List[nn.Module],
34+
device: torch.device,
3435
batch_size: int,
3536
is_distributed: bool,
3637
checkpoint_dir: str = "",
@@ -46,6 +47,8 @@ def train_or_load_composition_model(
4647
:param train_datasets: Training datasets
4748
:param other_additive_models: Other additive models (e.g. ZBL) to
4849
subtract before fitting
50+
:param device: Device on which to train the composition model. This should
51+
be a device that supports float64 (e.g. CPU or CUDA, but not MPS).
4952
:param batch_size: Batch size for data loading
5053
:param is_distributed: Whether training is distributed
5154
:param checkpoint_dir: Directory to save the composition model checkpoint
@@ -86,7 +89,6 @@ def train_or_load_composition_model(
8689
f"'{target_info.unit}'."
8790
)
8891
composition_model.load_state_dict(loaded.state_dict())
89-
composition_model.sync_tensor_maps()
9092
else:
9193
assert isinstance(atomic_baseline, dict)
9294
logging.info("Calculating composition weights")
@@ -98,12 +100,10 @@ def train_or_load_composition_model(
98100
hypers["distributed"] = is_distributed
99101
trainer = Trainer(hypers=hypers)
100102
trainer._additive_models = other_additive_models
101-
# The trainer fits on devices[0]; pass the model's current device so
102-
# embedded training stays where the parent architecture put the model.
103103
trainer.train(
104104
model=composition_model,
105105
dtype=torch.float64,
106-
devices=[composition_model.dummy_buffer.device],
106+
devices=[device],
107107
train_datasets=train_datasets,
108108
val_datasets=train_datasets,
109109
checkpoint_dir=checkpoint_dir,

src/metatrain/composition/_base_composition.py

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
import metatensor.torch as mts
1111
import torch
1212
from metatensor.torch import Labels, LabelsEntry, TensorBlock, TensorMap
13+
from metatensor.torch.learn import nn
1314
from metatomic.torch import ModelOutput, System
1415

1516
from .documentation import FixedCompositionWeights # noqa: F401
1617

1718

18-
class BaseCompositionModel(torch.nn.Module):
19+
class BaseCompositionModel(nn.Module):
1920
"""
2021
Fits a composition model for a dict of targets.
2122
@@ -88,9 +89,11 @@ def __init__(
8889
self.atomic_types = torch.as_tensor(atomic_types, dtype=torch.int32)
8990
self.target_names = []
9091
self.sample_kinds = {}
91-
self.XTX = {}
92-
self.XTY = {}
93-
self.weights = {}
92+
# `XTX` and `XTY` are only used during fitting, not at inference, so they
93+
# are registered as non-persistent buffers to keep them out of the state_dict.
94+
self.register_buffer("XTX", {}, persistent=False)
95+
self.register_buffer("XTY", {}, persistent=False)
96+
self.register_buffer("weights", {})
9497

9598
# go from an atomic type to its position in `self.atomic_types`
9699
self.register_buffer(
@@ -167,7 +170,6 @@ def add_output(self, target_name: str, layout: TensorMap) -> None:
167170
values=torch.zeros(
168171
len(self.atomic_types),
169172
len(self.atomic_types),
170-
dtype=torch.float64,
171173
),
172174
samples=Labels(["center_type"], self.atomic_types.reshape(-1, 1)),
173175
components=[],
@@ -186,7 +188,6 @@ def add_output(self, target_name: str, layout: TensorMap) -> None:
186188
len(self.atomic_types),
187189
*[len(c) for c in block.components],
188190
len(block.properties),
189-
dtype=torch.float64,
190191
),
191192
samples=Labels(["center_type"], self.atomic_types.reshape(-1, 1)),
192193
components=block.components,
@@ -203,7 +204,6 @@ def add_output(self, target_name: str, layout: TensorMap) -> None:
203204
len(self.atomic_types),
204205
*[len(c) for c in block.components],
205206
len(block.properties),
206-
dtype=torch.float64,
207207
),
208208
samples=Labels(["center_type"], self.atomic_types.reshape(-1, 1)),
209209
components=block.components,
@@ -239,10 +239,15 @@ def accumulate(
239239
:param targets: Dict of target names to :py:class:`TensorMap` containing
240240
the target values for each system in the batch.
241241
"""
242-
243242
device = systems[0].positions.device
244243
dtype = systems[0].positions.dtype
245-
self._sync_device_dtype(device, dtype)
244+
245+
if dtype != torch.float64:
246+
raise ValueError(
247+
"Composition model accumulation must be done in float64. "
248+
f"Got systems with dtype {dtype}. Please move the systems to "
249+
"float64 before accumulating."
250+
)
246251

247252
# check that the systems contain no unexpected atom types
248253
for system in systems:
@@ -386,6 +391,15 @@ def fit(
386391
if targets_to_fit is None:
387392
targets_to_fit = self.target_names
388393

394+
if len(targets_to_fit) > 0:
395+
dtype = self.XTX[targets_to_fit[0]].block(0).values.dtype
396+
if dtype != torch.float64:
397+
raise ValueError(
398+
"Composition model fitting must be done in float64. "
399+
f"Got dtype {dtype}. Please move the model to "
400+
"float64 before fitting (e.g. `model.to(dtype=torch.float64)`)."
401+
)
402+
389403
sanitized_fixed_weights = self._sanitize_fixed_weights(fixed_weights)
390404

391405
# fit
@@ -486,10 +500,7 @@ def forward(
486500
:raises ValueError: If no weights have been computed or if `outputs` keys
487501
contain unsupported keys.
488502
"""
489-
490503
device = systems[0].positions.device
491-
dtype = systems[0].positions.dtype
492-
self._sync_device_dtype(device, dtype)
493504

494505
# Build the sample labels that are required
495506
_, sample_labels = _get_system_indices_and_labels(systems, device)
@@ -616,32 +627,6 @@ def _compute_X_per_atom(
616627
)
617628
return one_hot_encoding.to(dtype)
618629

619-
def _sync_device_dtype(self, device: torch.device, dtype: torch.dtype) -> None:
620-
"""
621-
Move the accumulated quantities and the fitted weights to the given
622-
device and dtype.
623-
624-
Needed because they are stored as ``TensorMap`` dicts, which
625-
``torch.nn.Module.to`` does not move.
626-
627-
:param device: Device to move the quantities to.
628-
:param dtype: Dtype to convert the quantities to.
629-
"""
630-
self.atomic_types = self.atomic_types.to(device=device)
631-
self.type_to_index = self.type_to_index.to(device=device)
632-
self.XTX = {
633-
target_name: tm.to(device=device, dtype=dtype)
634-
for target_name, tm in self.XTX.items()
635-
}
636-
self.XTY = {
637-
target_name: tm.to(device=device, dtype=dtype)
638-
for target_name, tm in self.XTY.items()
639-
}
640-
self.weights = {
641-
target_name: tm.to(device=device, dtype=dtype)
642-
for target_name, tm in self.weights.items()
643-
}
644-
645630

646631
def _include_key(key: LabelsEntry) -> bool:
647632
"""

src/metatrain/composition/checkpoints.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,51 @@
1+
import torch
2+
3+
4+
def model_update_v1_v2(checkpoint: dict, prefix: str = "") -> None:
5+
"""
6+
Update model checkpoint from version 1 to version 2.
7+
8+
The model now uses metatensor's ``nn.Module`` and ``register_buffer`` instead of
9+
manually tracking the data.
10+
11+
:param checkpoint: The checkpoint to update.
12+
:param prefix: Prefix prepended to the state_dict keys of the composition model
13+
(e.g. ``"additive_models.0."`` when the composition model is nested inside
14+
another architecture's ``additive_models[0]``).
15+
"""
16+
scaler_key = f"{prefix}model"
17+
18+
for key in ["model_state_dict", "best_model_state_dict"]:
19+
if (state_dict := checkpoint.get(key)) is None:
20+
continue
21+
22+
# If both model_state_dict and best_model_state_dict point to the same
23+
# dict, the upgrade was already applied in the first iteration.
24+
if f"{scaler_key}._mts_helper" in state_dict:
25+
continue
26+
27+
dummy_buffer = state_dict[f"{prefix}dummy_buffer"]
28+
empty_tensor = torch.zeros(
29+
0, dtype=dummy_buffer.dtype, device=dummy_buffer.device
30+
)
31+
32+
extra_state: dict[str, dict] = {"weights": {}}
33+
34+
for target_name in checkpoint["model_data"]["dataset_info"].targets:
35+
buffer_key = f"{prefix}{target_name}_composition_buffer"
36+
if buffer_key not in state_dict:
37+
continue
38+
39+
extra_state["weights"][target_name] = (
40+
"metatensor.TensorMap",
41+
state_dict.pop(buffer_key),
42+
empty_tensor,
43+
)
44+
45+
state_dict[f"{scaler_key}._mts_helper"] = empty_tensor
46+
state_dict[f"{scaler_key}._extra_state"] = extra_state
47+
48+
149
def trainer_update_v1_v2(checkpoint: dict) -> None:
250
"""
351
Update a v1 Trainer checkpoint to v2.

0 commit comments

Comments
 (0)