Skip to content

Commit 7c8bb06

Browse files
authored
feat(models): add UMA (fairchem-core) interatomic-potential wrapper (#117)
* consolidate tests and improve example Signed-off-by: Dallas Foster <dallasf@nvidia.com> * linting Signed-off-by: Dallas Foster <dallasf@nvidia.com> * update ci workflow to account for uma Signed-off-by: Dallas Foster <dallasf@nvidia.com> * update changelog Signed-off-by: Dallas Foster <dallasf@nvidia.com> * greptile comments Signed-off-by: Dallas Foster <dallasf@nvidia.com> * add train=true/false flag to freeze checkpoint weights * review comments Signed-off-by: Dallas Foster <dallasf@nvidia.com> --------- Signed-off-by: Dallas Foster <dallasf@nvidia.com>
1 parent 35f4530 commit 7c8bb06

13 files changed

Lines changed: 4476 additions & 1166 deletions

File tree

.github/workflows/ci.yml

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
# 1. changed-files: Detects which files have changed compared to main branch
2727
# 2. lint: Runs static code checks and linting via pre-commit
2828
# 3. get-pr-labels: Retrieves PR labels for conditional job execution
29-
# 4. test: Runs pytest unit tests with coverage (on GPU runner)
29+
# 4. test: Runs pytest unit tests with coverage (on GPU runner). UMA, whose
30+
# deps conflict with the cu13/mace stack, is tested from a separate
31+
# .venv-uma environment (gated on UMA changes or full runs).
3032
# 5. verify-status: Verifies all jobs completed successfully
3133

3234
name: "CI"
@@ -64,6 +66,7 @@ jobs:
6466
runs-on: ubuntu-latest
6567
outputs:
6668
any_changed: ${{ steps.changed-files.outputs.any_changed }}
69+
uma_changed: ${{ steps.uma-changed.outputs.any_changed }}
6770
steps:
6871
- uses: actions/checkout@v4
6972
with:
@@ -88,9 +91,23 @@ jobs:
8891
!.gitignore
8992
.github/workflows/ci.yml
9093
94+
# UMA lives in its own (conflicting) environment, so its tests run in a
95+
# separate CI step. Detect UMA-specific changes to gate that step on PRs.
96+
- uses: step-security/changed-files@v46
97+
id: uma-changed
98+
with:
99+
base_sha: ${{ steps.merge-base.outputs.merge-base }}
100+
files: |
101+
nvalchemi/models/uma.py
102+
nvalchemi/models/__init__.py
103+
nvalchemi/_optional.py
104+
test/models/test_uma.py
105+
examples/advanced/09_uma_nve.py
106+
91107
- name: Show output
92108
run: |
93109
echo '${{ toJSON(steps.changed-files.outputs) }}'
110+
echo 'uma_changed=${{ steps.uma-changed.outputs.any_changed }}'
94111
95112
# ============================================================================
96113
# LINTING STAGE
@@ -165,7 +182,9 @@ jobs:
165182
- get-pr-labels
166183
- changed-files
167184
runs-on: linux-amd64-gpu-h100-latest-1
168-
timeout-minutes: 30
185+
# Headroom for the extra UMA environment install (a second torch + the
186+
# fairchem stack) when the UMA steps run; well under this otherwise.
187+
timeout-minutes: 45
169188
container:
170189
image: nvcr.io/nvidia/cuda:13.1.0-runtime-ubuntu24.04
171190
options: --gpus all
@@ -214,6 +233,15 @@ jobs:
214233
export PATH="$HOME/.local/bin:$PATH"
215234
uv sync ${CI_UV_EXTRAS}
216235
236+
# UMA's fairchem-core torch pin conflicts with the cu13 / mace stack
237+
# (see pyproject [tool.uv].conflicts), so it is resolved into a dedicated
238+
# environment. ``ase`` is required by the UMA test module.
239+
- name: Install UMA environment (.venv-uma)
240+
if: needs.changed-files.outputs.uma_changed == 'true' || env.IS_FULL_RUN == 'true'
241+
run: |
242+
export PATH="$HOME/.local/bin:$PATH"
243+
UV_PROJECT_ENVIRONMENT=.venv-uma uv sync --extra uma --extra ase
244+
217245
# ========================================================================
218246
# TESTMON + COVERAGE CACHING (PR runs only)
219247
# ========================================================================
@@ -283,6 +311,30 @@ jobs:
283311
uv run ${CI_UV_EXTRAS} pytest --cov=nvalchemi --cov-report= --cov-fail-under=0 test/
284312
fi
285313
314+
# ========================================================================
315+
# UMA TESTS (isolated environment)
316+
# ========================================================================
317+
# UMA can't share the cu13/mace env, so its tests run from .venv-uma and
318+
# append coverage to the active run's data file. Structural tests always
319+
# run; the checkpoint-based tests skip without HF access to facebook/UMA
320+
# (set the HF_TOKEN secret to exercise them). The ``@slow`` NVE / turbo
321+
# tests are not selected (no --slow), matching the main suite.
322+
323+
- name: Run UMA tests (.venv-uma)
324+
if: needs.changed-files.outputs.uma_changed == 'true' || env.IS_FULL_RUN == 'true'
325+
env:
326+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
327+
run: |
328+
export PATH="$HOME/.local/bin:$PATH"
329+
if [ "$IS_FULL_RUN" = "true" ]; then
330+
export COVERAGE_FILE=.coverage
331+
else
332+
export COVERAGE_FILE=coverage_pr.dat
333+
fi
334+
UV_PROJECT_ENVIRONMENT=.venv-uma uv run --extra uma --extra ase \
335+
pytest --cov=nvalchemi --cov-append --cov-report= --cov-fail-under=0 \
336+
test/models/test_uma.py
337+
286338
# ========================================================================
287339
# COVERAGE REPORTING (shared for full runs and PR runs)
288340
# ========================================================================

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@
1515
Transform failures are wrapped in `RuntimeError` with `transform[<i>]`
1616
breadcrumb and `__cause__` preserved.
1717

18+
### Models
19+
20+
- **UMA (fairchem-core) wrapper** — new `UMAWrapper` exposes UMA
21+
(Universal Models for Atoms) foundation models (`uma-s-1p1`,
22+
`uma-s-1p2`, `uma-m-1p1`) through the `BaseModelMixin` interface,
23+
ready for any dynamics engine or standalone inference. UMA is
24+
multi-task; the wrapper is pinned to one head at construction (OMol,
25+
OMat, OC20, ODAC, OMC). Input conversion is tensor-native (no ASE
26+
round trip); energy is the differentiable primitive with forces and
27+
(for periodic tasks) stress from autograd. Install via the new `uma`
28+
optional extra (`pip install 'nvalchemi-toolkit[uma]'`), which is
29+
declared conflicting with the `mace` and `cu12`/`cu13` extras
30+
(incompatible `e3nn` / `torch` pins) and resolves into its own
31+
environment. `from_checkpoint` forwards fairchem's `inference_settings`
32+
(including `"turbo"` for `torch.compile`). See the
33+
`examples/advanced/09_uma_nve.py` NVE/NVT/NPT walkthrough.
34+
1835
### Fixed
1936

2037
- **NVT Nosé-Hoover velocity collapse** (#104) — reset the NHC

docs/models/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ mechanical reference data.
4848
- ✓
4949
- charge
5050
- MATRIX
51+
* - {py:class}`~nvalchemi.models.uma.UMAWrapper`
52+
- ✓
53+
- ✓
54+
- ✓
55+
- ✓
56+
- ✗
57+
- ✓
58+
- charge, spin
59+
- COO
5160
* - {py:class}`~nvalchemi.models.demo.DemoModelWrapper`
5261
- ✓
5362
- ✓

docs/modules/models.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ Machine-learned potentials
5151

5252
AIMNet2Wrapper
5353

54+
.. currentmodule:: nvalchemi.models.uma
55+
56+
.. autosummary::
57+
:toctree: generated
58+
:template: class.rst
59+
:nosignatures:
60+
61+
UMAWrapper
62+
5463
Physical / classical models
5564
---------------------------
5665

docs/userguide/models.md

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,115 @@ potentials:
3636
| {py:class}`~nvalchemi.models.demo.DemoModelWrapper` | {py:class}`~nvalchemi.models.demo.DemoModel` | Non-invariant demo; useful for testing and tutorials |
3737
| {py:class}`~nvalchemi.models.aimnet2.AIMNet2Wrapper` | {py:class}`~aimnet.calculators.AIMNet2Calculator` | Requires the `aimnet2` optional dependency |
3838
| {py:class}`~nvalchemi.models.mace.MACEWrapper` | Any MACE variant | Requires the `mace` optional dependency with a CUDA extra, such as `cu13` or `cu12` |
39+
| {py:class}`~nvalchemi.models.uma.UMAWrapper` | fairchem-core UMA (`MLIPPredictUnit`) | Requires the `uma` optional dependency; conflicts with `mace` (incompatible `e3nn` pins) |
3940

40-
{py:class}`~nvalchemi.models.aimnet2.AIMNet2Wrapper` and {py:class}`~nvalchemi.models.mace.MACEWrapper`
41+
{py:class}`~nvalchemi.models.aimnet2.AIMNet2Wrapper`, {py:class}`~nvalchemi.models.mace.MACEWrapper`,
42+
and {py:class}`~nvalchemi.models.uma.UMAWrapper`
4143
are lazily imported --- they only load when accessed, so missing dependencies will not
4244
break other imports.
4345

46+
````{note}
47+
**UMA resolves to a different torch than the `cu12` / `cu13` GPU stack.**
48+
`fairchem-core` caps torch below 2.9 (`fairchem-core>=2.8` requires
49+
`torch>=2.8,<2.9`), while the `cu12` / `cu13` extras pull
50+
`nvalchemi-toolkit-ops[torch-cuXX]`, which floors torch at `>=2.11`. The
51+
`uma` extra is therefore declared mutually exclusive with `cu12`, `cu13`,
52+
and `mace`, and `uv sync --extra uma` forks a standalone resolution that
53+
installs a PyPI CUDA torch wheel (~2.8) instead of the NVIDIA-indexed
54+
`cuXX` build. Keep UMA in its own environment, e.g.:
55+
56+
```bash
57+
uv venv .venv-uma && uv sync --extra uma # UMA (fairchem's torch)
58+
uv venv .venv-mace && uv sync --extra cu13 --extra mace # MACE on the cu13 GPU stack
59+
```
60+
61+
The core `nvalchemi-toolkit-ops` package (and its Warp kernels) is still
62+
installed in the UMA environment --- only the `cuXX` GPU-acceleration
63+
extras (cuEquivariance, cuML, the NVIDIA-indexed torch build) are dropped,
64+
none of which UMA uses, since it builds its neighbor graph inside
65+
fairchem. The toolkit-ops `torch-cuXX` extras pin `torch>=2.11`, but that
66+
tracks the `cuXX` wheel builds rather than a toolkit-ops API requirement
67+
--- the base package already declares `torch>=2.8`, so the Warp path is
68+
expected to work against the ~2.8 torch in the UMA environment.
69+
````
70+
71+
### Using UMA (fairchem-core)
72+
73+
UMA (Universal Models for Atoms) is a multi-task foundation model: one
74+
checkpoint ships task heads for molecules (`omol`), bulk crystals (`omat`),
75+
catalysis (`oc20`), direct air capture (`odac`), and molecular crystals
76+
(`omc`). {py:class}`~nvalchemi.models.uma.UMAWrapper` pins a single task at
77+
construction; `active_outputs` is `{energy, forces}` for molecular tasks and
78+
`{energy, forces, stress}` for periodic ones.
79+
80+
**1. Install the optional dependency** (in its own environment, per the note
81+
above):
82+
83+
```bash
84+
uv venv .venv-uma && uv sync --extra uma
85+
# or, with pip: pip install 'nvalchemi-toolkit[uma]'
86+
```
87+
88+
**2. Get HuggingFace access.** UMA checkpoints live in the **gated**
89+
[`facebook/UMA`](https://huggingface.co/facebook/UMA) repository, so a (free)
90+
HuggingFace account and a one-time access approval are required:
91+
92+
1. Sign in and click **"Agree and access repository"** on the
93+
[model page](https://huggingface.co/facebook/UMA).
94+
2. Create a **read** token at
95+
<https://huggingface.co/settings/tokens>.
96+
3. Make the token available to your shell, either by logging in once (it is
97+
cached under `~/.cache/huggingface`):
98+
99+
```bash
100+
huggingface-cli login
101+
```
102+
103+
or by exporting it for the session:
104+
105+
```bash
106+
export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxx
107+
```
108+
109+
**3. Load a checkpoint.** The first call downloads and caches the weights
110+
(under `~/.cache/fairchem`); later calls reuse the cache and need no network:
111+
112+
```python
113+
from nvalchemi.models.uma import UMAWrapper
114+
115+
# Molecular potential (OMol head)
116+
mol = UMAWrapper.from_checkpoint("uma-s-1p1", task_name="omol", device="cuda")
117+
118+
# Bulk-crystal potential (OMat head, same checkpoint family)
119+
mat = UMAWrapper.from_checkpoint("uma-m-1p1", task_name="omat", device="cuda")
120+
```
121+
122+
Registered checkpoint names (see
123+
`fairchem.core.calculate.pretrained_mlip.available_models` for the full list):
124+
125+
| Checkpoint | Size | Notes |
126+
|---|---|---|
127+
| `uma-s-1p1` | small | Default in the examples / tests |
128+
| `uma-s-1p2` | small | Updated small release |
129+
| `uma-m-1p1` | medium | Higher accuracy, larger / slower |
130+
131+
**`torch.compile` / turbo.** `UMAWrapper` does not add a `compile_model`
132+
flag (unlike the MACE / AIMNet2 wrappers) because fairchem owns compilation
133+
internally as a field on its `InferenceSettings`. Reach it through
134+
`from_checkpoint`'s `inference_settings` argument --- pass `"turbo"` for
135+
fairchem's compiled preset (`torch.compile` + TF32 + MoLE merge, for runs
136+
with fixed atomic composition), or an `InferenceSettings` instance for finer
137+
control:
138+
139+
```python
140+
fast = UMAWrapper.from_checkpoint(
141+
"uma-s-1p1", task_name="omat", device="cuda", inference_settings="turbo"
142+
)
143+
```
144+
145+
See {doc}`the UMA NVE/NVT example </auto_examples/advanced/09_uma_nve>` for a
146+
runnable end-to-end molecular-dynamics walkthrough.
147+
44148
## Architecture overview
45149

46150
A wrapped model uses **multiple inheritance**: your existing {py:class}`~torch.nn.Module`

0 commit comments

Comments
 (0)