Skip to content

Commit 00044fa

Browse files
committed
add train=true/false flag to freeze checkpoint weights
1 parent 58784e7 commit 00044fa

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

nvalchemi/models/uma.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ class UMAWrapper(nn.Module, BaseModelMixin):
130130
UMA task: one of ``_UMA_TASKS``. Determines which per-task head
131131
in the multi-task model is used and which inputs (charge, spin)
132132
must be populated.
133+
train : bool
134+
``False`` (default) freezes all weights for inference (lossless for
135+
autograd forces); ``True`` keeps fairchem's trainable/frozen split
136+
so weights stay exposed for fine-tuning.
133137
134138
Attributes
135139
----------
@@ -139,7 +143,9 @@ class UMAWrapper(nn.Module, BaseModelMixin):
139143
The UMA task this wrapper is pinned to.
140144
"""
141145

142-
def __init__(self, predict_unit: Any, task_name: str = "omol") -> None:
146+
def __init__(
147+
self, predict_unit: Any, task_name: str = "omol", train: bool = False
148+
) -> None:
143149
super().__init__()
144150
if task_name not in _UMA_TASKS:
145151
raise ValueError(
@@ -198,6 +204,16 @@ def __init__(self, predict_unit: Any, task_name: str = "omol") -> None:
198204
active_outputs=active_outputs,
199205
)
200206

207+
# Inference (train=False): freeze all weights — conservative forces
208+
# come from autograd w.r.t. positions, so this is lossless and avoids
209+
# building a weight-grad graph each forward. Training: leave fairchem's
210+
# loaded trainable/frozen split intact so weights stay exposed.
211+
self._train = train
212+
if not train:
213+
for p in self.predict_unit.model.parameters():
214+
p.requires_grad_(False)
215+
self.train(train)
216+
201217
# ------------------------------------------------------------------
202218
# Construction
203219
# ------------------------------------------------------------------
@@ -210,6 +226,7 @@ def from_checkpoint(
210226
device: str | torch.device = "cpu",
211227
inference_settings: Any = "default",
212228
overrides: dict | None = None,
229+
train: bool = False,
213230
) -> "UMAWrapper":
214231
"""Resolve and load a UMA checkpoint.
215232
@@ -242,6 +259,14 @@ def from_checkpoint(
242259
overrides : dict | None
243260
Optional overrides forwarded to fairchem's inference-settings
244261
builder.
262+
train : bool
263+
If ``False`` (default), freeze all weights for inference —
264+
lossless, since conservative forces come from autograd on
265+
positions. If ``True``, keep fairchem's loaded trainable/frozen
266+
split so weights remain exposed for fine-tuning. Note: the
267+
``forward`` path goes through fairchem's inference ``predict``
268+
(eval mode, detached forces); gradient-based training requires a
269+
separate path through the raw model.
245270
"""
246271
import os as _os
247272

@@ -272,7 +297,7 @@ def from_checkpoint(
272297
f"local file path. Known names: "
273298
f"{sorted(pretrained_mlip.available_models)[:6]}..."
274299
)
275-
return cls(predict_unit, task_name=task_name)
300+
return cls(predict_unit, task_name=task_name, train=train)
276301

277302
def _extract_cutoff(self) -> float:
278303
"""Pull the radial cutoff from the loaded backbone.

test/models/test_uma.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def __init__(self) -> None:
7474
self.r_max = torch.tensor(6.0)
7575
self.sph_feature_size = 16 # (lmax+1)² with lmax=3
7676
self.sphere_channels = 128
77+
# A trainable weight so the train/freeze flag is observable in tests.
78+
self.weight = nn.Parameter(torch.zeros(1))
7779

7880
def forward(self, data: FCAtomicData) -> dict:
7981
n = data.pos.shape[0]
@@ -198,6 +200,18 @@ def test_task_stored(self, mock_omol):
198200
def test_cutoff_from_backbone(self, mock_omol):
199201
assert math.isclose(mock_omol.cutoff, 6.0, abs_tol=1e-6)
200202

203+
def test_inference_freezes_weights(self, mock_omol):
204+
"""train=False (default) freezes the underlying weights for inference."""
205+
params = list(mock_omol.predict_unit.model.parameters())
206+
assert params and all(not p.requires_grad for p in params)
207+
assert mock_omol.training is False
208+
209+
def test_train_keeps_weights_trainable(self, mock_pu):
210+
"""train=True leaves weights trainable/exposed for fine-tuning."""
211+
w = UMAWrapper(mock_pu, task_name="omol", train=True)
212+
assert any(p.requires_grad for p in w.predict_unit.model.parameters())
213+
assert w.training is True
214+
201215

202216
class TestModelConfig:
203217
def test_omol_active_outputs(self, mock_omol):

0 commit comments

Comments
 (0)