Skip to content

Commit 1e3a424

Browse files
committed
add gpu workflow and tests
1 parent 71e94d5 commit 1e3a424

20 files changed

Lines changed: 1361 additions & 263 deletions

.github/workflows/bluesky.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: gpu-ci
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
7+
tests:
8+
runs-on: [self-hosted, gpu]
9+
if: github.repository == 'stfc/janus-core'
10+
timeout-minutes: 60
11+
strategy:
12+
matrix:
13+
python-version: ["3.10","3.11","3.12"]
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v6
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
23+
- name: Install "all" dependencies
24+
run: uv sync --extra chgnet --extra dpa3 --extra d3 --extra grace --extra mace --extra orb --extra upet --extra plumed
25+
26+
- name: Install PLUMED
27+
uses: Iximiel/install-plumed@v1
28+
id: plumed
29+
continue-on-error: true
30+
31+
- name: Set environment variable based on plumed success
32+
run: |
33+
if [ "${{ steps.plumed.outcome }}" = "success" ]; then
34+
echo "PLUMED_KERNEL=${{ steps.plumed.outputs.plumed_prefix }}/lib/libplumedKernel.dylib" >> $GITHUB_ENV
35+
fi
36+
37+
- name: Download extra models
38+
run: |
39+
python3 tests/models/extra_models.py tests/models/extra
40+
41+
- name: Run test suite
42+
env:
43+
# show timings of tests
44+
PYTEST_ADDOPTS: "--durations=0"
45+
run: uv run --no-sync pytest -k cuda
46+
47+
- name: Install updated e3nn dependencies
48+
run: |
49+
uv sync --extra mattersim --extra fairchem --extra sevennet --extra nequip --extra d3
50+
uv pip install --reinstall pynvml
51+
uv pip install "fairchem-core[torch-extras]" --no-build-isolation
52+
53+
- name: Run test suite for updated e3nn dependencies
54+
env:
55+
# show timings of tests
56+
PYTEST_ADDOPTS: "--durations=0"
57+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
58+
run: uv run --no-sync pytest tests/test_{mlip_calculators,single_point}.py -k cuda

tests/data/mlip_fine_tune.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ keep_checkpoints: False
3232
save_cpu: True
3333
weight_decay: 1e-8
3434
eval_interval: 2
35+
plot: False

tests/data/mlip_train.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ clip_grad: 100
4444
keep_checkpoints: False
4545
keep_isolated_atoms: True
4646
save_cpu: True
47+
plot: False

tests/test_descriptors.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ase import Atoms
88
from ase.io import read
99
import pytest
10+
import torch
1011

1112
from janus_core.calculations.descriptors import Descriptors
1213
from janus_core.calculations.single_point import SinglePoint
@@ -17,11 +18,15 @@
1718
MODEL_PATH = Path(__file__).parent / "models" / "mace_mp_small.model"
1819

1920

20-
def test_calc_descriptors(tmp_path):
21+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
22+
def test_calc_descriptors(tmp_path, device):
2123
"""Test calculating equation of state from ASE atoms object."""
24+
if device == "cuda" and not torch.cuda.is_available():
25+
pytest.skip("CUDA not available")
26+
2227
struct = read(DATA_PATH / "NaCl.cif")
2328
log_file = tmp_path / "descriptors.log"
24-
struct.calc = choose_calculator(arch="mace_mp", model=MODEL_PATH)
29+
struct.calc = choose_calculator(arch="mace_mp", model=MODEL_PATH, device=device)
2530

2631
descriptors = Descriptors(
2732
struct,
@@ -41,13 +46,18 @@ def test_calc_descriptors(tmp_path):
4146
)
4247

4348

44-
def test_calc_per_element(tmp_path):
49+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
50+
def test_calc_per_element(tmp_path, device):
4551
"""Test calculating descriptors for each element from SinglePoint object."""
52+
if device == "cuda" and not torch.cuda.is_available():
53+
pytest.skip("CUDA not available")
54+
4655
log_file = tmp_path / "descriptors.log"
4756
single_point = SinglePoint(
4857
struct=DATA_PATH / "NaCl.cif",
4958
arch="mace",
5059
model=MODEL_PATH,
60+
device=device,
5161
)
5262

5363
descriptors = Descriptors(
@@ -68,14 +78,19 @@ def test_calc_per_element(tmp_path):
6878
assert atoms.info["mace_Na_descriptor"] == pytest.approx(-0.0020374985791535563)
6979

7080

71-
def test_logging(tmp_path):
81+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
82+
def test_logging(tmp_path, device):
7283
"""Test attaching logger to Descriptors and emissions are saved to info."""
84+
if device == "cuda" and not torch.cuda.is_available():
85+
pytest.skip("CUDA not available")
86+
7387
log_file = tmp_path / "descriptors.log"
7488

7589
single_point = SinglePoint(
7690
struct=DATA_PATH / "NaCl.cif",
7791
arch="mace_mp",
7892
model=MODEL_PATH,
93+
device=device,
7994
)
8095

8196
descriptors = Descriptors(
@@ -92,12 +107,17 @@ def test_logging(tmp_path):
92107
assert single_point.struct.info["emissions"] > 0
93108

94109

95-
def test_dispersion():
110+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
111+
def test_dispersion(device):
96112
"""Test using mace_mp with dispersion."""
113+
if device == "cuda" and not torch.cuda.is_available():
114+
pytest.skip("CUDA not available")
115+
97116
single_point = SinglePoint(
98117
struct=DATA_PATH / "NaCl.cif",
99118
arch="mace_mp",
100119
calc_kwargs={"dispersion": False},
120+
device=device,
101121
)
102122

103123
descriptors = Descriptors(
@@ -110,6 +130,7 @@ def test_dispersion():
110130
struct=DATA_PATH / "NaCl.cif",
111131
arch="mace_mp",
112132
calc_kwargs={"dispersion": True},
133+
device=device,
113134
)
114135

115136
descriptors_disp = Descriptors(
@@ -118,17 +139,24 @@ def test_dispersion():
118139
)
119140
descriptors_disp.run()
120141

121-
assert (
122-
descriptors_disp.struct.info["mace_mp_d3_descriptor"]
123-
== descriptors.struct.info["mace_mp_descriptor"]
142+
assert descriptors_disp.struct.info["mace_mp_d3_descriptor"] == pytest.approx(
143+
descriptors.struct.info["mace_mp_descriptor"]
124144
)
125145

126146

127-
def test_not_implemented_error():
128-
"""Test correct error raised if descriptors not implemented."""
147+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
148+
def test_not_implemented_error(device):
149+
"""Test correct implemented error raised if descriptors not installed."""
150+
if device == "cuda" and not torch.cuda.is_available():
151+
pytest.skip("CUDA not available")
152+
153+
from tests.utils import skip_extras
154+
155+
skip_extras("chgnet")
129156
single_point = SinglePoint(
130157
struct=DATA_PATH / "NaCl.cif",
131158
arch="chgnet",
159+
device=device,
132160
)
133161
with pytest.raises(NotImplementedError):
134162
Descriptors(

tests/test_elasticity.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from ase.build import bulk
88
from ase.io import read
99
import numpy as np
10+
import pytest
1011
from pytest import approx
12+
import torch
1113

1214
from janus_core.calculations.elasticity import Elasticity
1315
from janus_core.helpers.mlip_calculators import choose_calculator
@@ -17,15 +19,19 @@
1719
MODEL_PATH = Path(__file__).parent / "models" / "mace_mp_small.model"
1820

1921

20-
def test_calc_elasticity(tmp_path):
22+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
23+
def test_calc_elasticity(tmp_path, device):
2124
"""Test calculating elasticity for Aluminium."""
25+
if device == "cuda" and not torch.cuda.is_available():
26+
pytest.skip("CUDA not available")
27+
2228
elasticity_path = tmp_path / "Al-elastic_tensor.dat"
2329
log_file = tmp_path / "Al-elasticity.log"
2430
generated_path = tmp_path / "Al-elasticity-generated.extxyz"
2531
minimized_path = tmp_path / "Al-elasticity-opt.extxyz"
2632

2733
struct = bulk("Al", crystalstructure="fcc")
28-
struct.calc = choose_calculator(arch="mace_mp", model=MODEL_PATH)
34+
struct.calc = choose_calculator(arch="mace_mp", model=MODEL_PATH, device=device)
2935

3036
elasticity = Elasticity(
3137
struct,
@@ -86,15 +92,19 @@ def test_calc_elasticity(tmp_path):
8692
assert struct.info["strain"] == approx(strain.voigt)
8793

8894

89-
def test_no_optimize_no_write_voigt(tmp_path):
95+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
96+
def test_no_optimize_no_write_voigt(tmp_path, device):
9097
"""Test calculating elasticity for Aluminium without optimization."""
98+
if device == "cuda" and not torch.cuda.is_available():
99+
pytest.skip("CUDA not available")
100+
91101
elasticity_path = tmp_path / "Al-elastic_tensor.dat"
92102
log_file = tmp_path / "Al-elasticity.log"
93103
generated_path = tmp_path / "Al-elasticity-generated.extxyz"
94104
minimized_path = tmp_path / "Al-elasticity-opt.extxyz"
95105

96106
struct = bulk("Al", crystalstructure="fcc")
97-
struct.calc = choose_calculator(arch="mace_mp", model=MODEL_PATH)
107+
struct.calc = choose_calculator(arch="mace_mp", model=MODEL_PATH, device=device)
98108

99109
elasticity = Elasticity(
100110
struct,

tests/test_elasticity_cli.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
from ase.io import read
88
import numpy as np
9+
import pytest
910
from pytest import approx
11+
import torch
1012
from typer.testing import CliRunner
1113
import yaml
1214

@@ -26,8 +28,12 @@ def test_help():
2628
assert "Usage: janus elasticity [OPTIONS]" in strip_ansi_codes(result.stdout)
2729

2830

29-
def test_elasticity_opt_none(tmp_path):
31+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
32+
def test_elasticity_opt_none(tmp_path, device):
3033
"""Test calculating the ElasticTensor from the CLI without optimisation."""
34+
if device == "cuda" and not torch.cuda.is_available():
35+
pytest.skip("CUDA not available")
36+
3137
with chdir(tmp_path):
3238
results_dir = Path("./janus_results")
3339
elasticity_path = results_dir / "NaCl-elastic_tensor.dat"
@@ -44,6 +50,8 @@ def test_elasticity_opt_none(tmp_path):
4450
DATA_PATH / "NaCl.cif",
4551
"--arch",
4652
"mace_mp",
53+
"--device",
54+
device,
4755
"--n-strains",
4856
"2",
4957
"--no-minimize",
@@ -95,8 +103,12 @@ def test_elasticity_opt_none(tmp_path):
95103
assert elasticity_summary["config"]["n_strains"] == 2
96104

97105

98-
def test_elasticity_opt_all(tmp_path):
106+
@pytest.mark.parametrize("device", ["cpu", "cuda"])
107+
def test_elasticity_opt_all(tmp_path, device):
99108
"""Test calculating the ElasticTensor from the command line."""
109+
if device == "cuda" and not torch.cuda.is_available():
110+
pytest.skip("CUDA not available")
111+
100112
with chdir(tmp_path):
101113
results_dir = Path("./janus_results")
102114
elasticity_path = results_dir / "NaCl-elastic_tensor.dat"
@@ -113,6 +125,8 @@ def test_elasticity_opt_all(tmp_path):
113125
DATA_PATH / "NaCl.cif",
114126
"--arch",
115127
"mace_mp",
128+
"--device",
129+
device,
116130
"--n-strains",
117131
"2",
118132
"--minimize-all",

0 commit comments

Comments
 (0)