OneScience commited on
Commit
a244197
·
verified ·
1 Parent(s): 35d961d

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="center">
2
+ <strong>
3
+ <span style="font-size: 30px;">MACE</span>
4
+ </strong>
5
+ </p>
6
+
7
+ # Model Introduction
8
+
9
+ MACE is a machine-learning interatomic potential (MLIP) model for molecular and materials systems, built on E(3)-equivariant graph neural networks. It predicts energy and forces for atomic structures.
10
+
11
+ Paper: *MACE: Higher order equivariant message passing neural networks for fast and accurate force fields*
12
+ Reference implementation: https://github.com/ACEsuit/mace
13
+
14
+ # Model Description
15
+
16
+ MACE is based on an E(3)-equivariant graph neural network architecture and is trained on HDF5/XYZ format data. It performs energy and force prediction and structure optimization for molecular and materials systems.
17
+
18
+ # Applicable Scenarios
19
+
20
+ | Scenario | Description |
21
+ | :---: | :--- |
22
+ | Interatomic potential training | Train a MACE model using standard configurations that read HDF5/XYZ data |
23
+ | Distributed training pre-check | Check multi-GPU/multi-node training configuration, data paths, and statistics consistency |
24
+ | Validation-set evaluation | Output energy- and force-related error metrics on the validation set during training |
25
+ | Custom data migration | Replace the existing HDF5/XYZ data and statistics files with your own data |
26
+ | Environment connectivity check | Use the pre-check script to verify the OneScience matchem environment, pyyaml, h5py, and data readability |
27
+
28
+ # Usage Instructions
29
+
30
+ ## 1. Using OneCode
31
+
32
+ You can try out intelligent one-click AI4S programming in the OneCode online environment:
33
+
34
+ [Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
35
+
36
+ ## 2. Manual Installation and Usage
37
+
38
+ **Hardware Requirements**
39
+
40
+ - GPU or DCU is recommended for training.
41
+ - CPU can be used for import and small-configuration connectivity checks; full training will be slow.
42
+ - DCU users need to install DTK in advance. DTK 25.04.2 or above, or the OneScience-recommended version matching the current cluster, is suggested.
43
+
44
+ ### Download the Model Package
45
+
46
+ ```bash
47
+ modelscope download --model OneScience/MACE --local_dir ./mace
48
+ cd mace
49
+ ```
50
+
51
+ ### Install the Runtime Environment
52
+
53
+ **DCU Environment**
54
+
55
+ ```bash
56
+ # Please activate DTK and CONDA first
57
+ conda create -n onescience311 python=3.11 -y
58
+ conda activate onescience311
59
+ # uv installation is also supported
60
+ pip install onescience[matchem-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
61
+ ```
62
+
63
+ **GPU Environment**
64
+
65
+ ```bash
66
+ # Please activate CONDA first
67
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
68
+ conda activate onescience311
69
+ # uv installation is also supported
70
+ pip install onescience[matchem-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
71
+ ```
72
+
73
+
74
+ ### Training Data Description
75
+
76
+ This repository does not include built-in training data. Taking the DMC starter dataset as an example, download it from ModelScope and place it under `data/` in the repository root:
77
+
78
+ ```bash
79
+ modelscope download --dataset OneScience/DMC --local_dir ./data
80
+ ```
81
+
82
+ After downloading, the data path will be `data/data/DMC/`. `scripts/demo/run.sh` automatically uses the repository root as `ONESCIENCE_DATASETS_DIR`, so there is no need to manually set this variable to match the paths in the configuration file.
83
+
84
+ For other configurations (e.g., `ani1x_8dcu.yaml`, `water_*.yaml`, etc.), download the corresponding datasets and adjust the data paths in the YAML file.
85
+
86
+
87
+ ### Training
88
+
89
+ Single-GPU:
90
+
91
+ ```bash
92
+ bash scripts/demo/run.sh --config scripts/demo/configs/DMC.yaml
93
+ ```
94
+
95
+ Multi-GPU:
96
+
97
+ ```bash
98
+ # Taking 8 GPUs as an example; launch.launcher in the config should be torchrun
99
+ bash scripts/demo/run.sh --config scripts/demo/configs/ani1x_8dcu.yaml
100
+ ```
101
+
102
+ SLURM submission:
103
+
104
+ ```bash
105
+ bash scripts/demo/run.sh --config scripts/demo/configs/ani1x_8dcu.yaml --submit
106
+ ```
107
+
108
+ ### Training Weights
109
+
110
+ This repository currently does not include built-in trained weights. Weights can be obtained through the training steps above.
111
+
112
+ # OneScience Official Information
113
+
114
+ | Platform | OneScience Main Repository | Skills Repository |
115
+ | --- | --- | --- |
116
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
117
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
118
+
119
+
120
+
121
+ # Citation and License
122
+
123
+ - The MACE-related code comes from the matchem example implementation in the OneScience project and references the upstream MACE project (https://github.com/ACEsuit/mace). The upstream MACE code is released under the [MIT License](https://github.com/ACEsuit/mace/blob/main/LICENSE).
124
+ - If you use MACE training results in scientific research, we recommend citing the original MACE paper, the relevant OneScience project information, and the sources of the datasets actually used.
model/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MACE model definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .__version__ import __version__
6
+ from .mace import AtomicDipolesMACE, EnergyDipolesMACE, MACE, ScaleShiftMACE
7
+
8
+ __all__ = [
9
+ "__version__",
10
+ "AtomicDipolesMACE",
11
+ "EnergyDipolesMACE",
12
+ "MACE",
13
+ "ScaleShiftMACE",
14
+ ]
model/__version__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __version__ = "0.3.13"
2
+
3
+ __all__ = ["__version__"]
model/mace.py ADDED
@@ -0,0 +1,945 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Implementation of MACE models and other models based E(3)-Equivariant MPNNs
3
+
4
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
5
+
6
+ import numpy as np
7
+ import torch
8
+ from e3nn import o3
9
+ from e3nn.util.jit import compile_mode
10
+
11
+ from onescience.modules.layer.mace_radial import ZBLBasis
12
+ #from onescience.utils.mace.tools.scatter import scatter_sum
13
+ from onescience.datapipes.materials.tools.scatter import scatter_sum
14
+
15
+ from onescience.modules.block.mace_block import (
16
+ AtomicEnergiesBlock,
17
+ EquivariantProductBasisBlock,
18
+ InteractionBlock,
19
+ LinearDipoleReadoutBlock,
20
+ LinearNodeEmbeddingBlock,
21
+ LinearReadoutBlock,
22
+ NonLinearDipoleReadoutBlock,
23
+ NonLinearReadoutBlock,
24
+ RadialEmbeddingBlock,
25
+ ScaleShiftBlock,
26
+ )
27
+ from onescience.modules.func_utils.mace_func_utils import (
28
+ compute_fixed_charge_dipole,
29
+ get_atomic_virials_stresses,
30
+ get_edge_vectors_and_lengths,
31
+ get_outputs,
32
+ get_symmetric_displacement,
33
+ prepare_graph,
34
+ )
35
+
36
+ # pylint: disable=C0302
37
+
38
+
39
+ @compile_mode("script")
40
+ class MACE(torch.nn.Module):
41
+ def __init__(
42
+ self,
43
+ r_max: float,
44
+ num_bessel: int,
45
+ num_polynomial_cutoff: int,
46
+ max_ell: int,
47
+ interaction_cls: Type[InteractionBlock],
48
+ interaction_cls_first: Type[InteractionBlock],
49
+ num_interactions: int,
50
+ num_elements: int,
51
+ hidden_irreps: o3.Irreps,
52
+ MLP_irreps: o3.Irreps,
53
+ atomic_energies: np.ndarray,
54
+ avg_num_neighbors: float,
55
+ atomic_numbers: List[int],
56
+ correlation: Union[int, List[int]],
57
+ gate: Optional[Callable],
58
+ pair_repulsion: bool = False,
59
+ distance_transform: str = "None",
60
+ radial_MLP: Optional[List[int]] = None,
61
+ radial_type: Optional[str] = "bessel",
62
+ heads: Optional[List[str]] = None,
63
+ cueq_config: Optional[Dict[str, Any]] = None,
64
+ lammps_mliap: Optional[bool] = False,
65
+ ):
66
+ super().__init__()
67
+ self.register_buffer(
68
+ "atomic_numbers", torch.tensor(atomic_numbers, dtype=torch.int64)
69
+ )
70
+ self.register_buffer(
71
+ "r_max", torch.tensor(r_max, dtype=torch.get_default_dtype())
72
+ )
73
+ self.register_buffer(
74
+ "num_interactions", torch.tensor(num_interactions, dtype=torch.int64)
75
+ )
76
+ if heads is None:
77
+ heads = ["Default"]
78
+ self.heads = heads
79
+ if isinstance(correlation, int):
80
+ correlation = [correlation] * num_interactions
81
+ self.lammps_mliap = lammps_mliap
82
+ # Embedding
83
+ node_attr_irreps = o3.Irreps([(num_elements, (0, 1))])
84
+ node_feats_irreps = o3.Irreps([(hidden_irreps.count(o3.Irrep(0, 1)), (0, 1))])
85
+ self.node_embedding = LinearNodeEmbeddingBlock(
86
+ irreps_in=node_attr_irreps,
87
+ irreps_out=node_feats_irreps,
88
+ cueq_config=cueq_config,
89
+ )
90
+ self.radial_embedding = RadialEmbeddingBlock(
91
+ r_max=r_max,
92
+ num_bessel=num_bessel,
93
+ num_polynomial_cutoff=num_polynomial_cutoff,
94
+ radial_type=radial_type,
95
+ distance_transform=distance_transform,
96
+ )
97
+ edge_feats_irreps = o3.Irreps(f"{self.radial_embedding.out_dim}x0e")
98
+ if pair_repulsion:
99
+ self.pair_repulsion_fn = ZBLBasis(p=num_polynomial_cutoff)
100
+ self.pair_repulsion = True
101
+
102
+ sh_irreps = o3.Irreps.spherical_harmonics(max_ell)
103
+ num_features = hidden_irreps.count(o3.Irrep(0, 1))
104
+ interaction_irreps = (sh_irreps * num_features).sort()[0].simplify()
105
+ self.spherical_harmonics = o3.SphericalHarmonics(
106
+ sh_irreps, normalize=True, normalization="component"
107
+ )
108
+ if radial_MLP is None:
109
+ radial_MLP = [64, 64, 64]
110
+ # Interactions and readout
111
+ self.atomic_energies_fn = AtomicEnergiesBlock(atomic_energies)
112
+
113
+ inter = interaction_cls_first(
114
+ node_attrs_irreps=node_attr_irreps,
115
+ node_feats_irreps=node_feats_irreps,
116
+ edge_attrs_irreps=sh_irreps,
117
+ edge_feats_irreps=edge_feats_irreps,
118
+ target_irreps=interaction_irreps,
119
+ hidden_irreps=hidden_irreps,
120
+ avg_num_neighbors=avg_num_neighbors,
121
+ radial_MLP=radial_MLP,
122
+ cueq_config=cueq_config,
123
+ )
124
+ self.interactions = torch.nn.ModuleList([inter])
125
+
126
+ # Use the appropriate self connection at the first layer for proper E0
127
+ use_sc_first = False
128
+ if "Residual" in str(interaction_cls_first):
129
+ use_sc_first = True
130
+
131
+ node_feats_irreps_out = inter.target_irreps
132
+ prod = EquivariantProductBasisBlock(
133
+ node_feats_irreps=node_feats_irreps_out,
134
+ target_irreps=hidden_irreps,
135
+ correlation=correlation[0],
136
+ num_elements=num_elements,
137
+ use_sc=use_sc_first,
138
+ cueq_config=cueq_config,
139
+ )
140
+ self.products = torch.nn.ModuleList([prod])
141
+
142
+ self.readouts = torch.nn.ModuleList()
143
+ self.readouts.append(
144
+ LinearReadoutBlock(
145
+ hidden_irreps, o3.Irreps(f"{len(heads)}x0e"), cueq_config
146
+ )
147
+ )
148
+
149
+ for i in range(num_interactions - 1):
150
+ if i == num_interactions - 2:
151
+ hidden_irreps_out = str(
152
+ hidden_irreps[0]
153
+ ) # Select only scalars for last layer
154
+ else:
155
+ hidden_irreps_out = hidden_irreps
156
+ inter = interaction_cls(
157
+ node_attrs_irreps=node_attr_irreps,
158
+ node_feats_irreps=hidden_irreps,
159
+ edge_attrs_irreps=sh_irreps,
160
+ edge_feats_irreps=edge_feats_irreps,
161
+ target_irreps=interaction_irreps,
162
+ hidden_irreps=hidden_irreps_out,
163
+ avg_num_neighbors=avg_num_neighbors,
164
+ radial_MLP=radial_MLP,
165
+ cueq_config=cueq_config,
166
+ )
167
+ self.interactions.append(inter)
168
+ prod = EquivariantProductBasisBlock(
169
+ node_feats_irreps=interaction_irreps,
170
+ target_irreps=hidden_irreps_out,
171
+ correlation=correlation[i + 1],
172
+ num_elements=num_elements,
173
+ use_sc=True,
174
+ cueq_config=cueq_config,
175
+ )
176
+ self.products.append(prod)
177
+ if i == num_interactions - 2:
178
+ self.readouts.append(
179
+ NonLinearReadoutBlock(
180
+ hidden_irreps_out,
181
+ (len(heads) * MLP_irreps).simplify(),
182
+ gate,
183
+ o3.Irreps(f"{len(heads)}x0e"),
184
+ len(heads),
185
+ cueq_config,
186
+ )
187
+ )
188
+ else:
189
+ self.readouts.append(
190
+ LinearReadoutBlock(
191
+ hidden_irreps, o3.Irreps(f"{len(heads)}x0e"), cueq_config
192
+ )
193
+ )
194
+
195
+ def forward(
196
+ self,
197
+ data: Dict[str, torch.Tensor],
198
+ training: bool = False,
199
+ compute_force: bool = True,
200
+ compute_virials: bool = False,
201
+ compute_stress: bool = False,
202
+ compute_displacement: bool = False,
203
+ compute_hessian: bool = False,
204
+ compute_edge_forces: bool = False,
205
+ compute_atomic_stresses: bool = False,
206
+ lammps_mliap: bool = False,
207
+ ) -> Dict[str, Optional[torch.Tensor]]:
208
+ # Setup
209
+ ctx = prepare_graph(
210
+ data,
211
+ compute_virials=compute_virials,
212
+ compute_stress=compute_stress,
213
+ compute_displacement=compute_displacement,
214
+ lammps_mliap=lammps_mliap,
215
+ )
216
+ is_lammps = ctx.is_lammps
217
+ num_atoms_arange = ctx.num_atoms_arange
218
+ num_graphs = ctx.num_graphs
219
+ displacement = ctx.displacement
220
+ positions = ctx.positions
221
+ vectors = ctx.vectors
222
+ lengths = ctx.lengths
223
+ cell = ctx.cell
224
+ node_heads = ctx.node_heads
225
+ interaction_kwargs = ctx.interaction_kwargs
226
+ lammps_natoms = interaction_kwargs.lammps_natoms
227
+ lammps_class = interaction_kwargs.lammps_class
228
+
229
+ # Atomic energies
230
+ node_e0 = self.atomic_energies_fn(data["node_attrs"])[
231
+ num_atoms_arange, node_heads
232
+ ]
233
+ e0 = scatter_sum(
234
+ src=node_e0, index=data["batch"], dim=0, dim_size=num_graphs
235
+ ) # [n_graphs, n_heads]
236
+ # Embeddings
237
+ node_feats = self.node_embedding(data["node_attrs"])
238
+ edge_attrs = self.spherical_harmonics(vectors)
239
+ edge_feats = self.radial_embedding(
240
+ lengths, data["node_attrs"], data["edge_index"], self.atomic_numbers
241
+ )
242
+ if hasattr(self, "pair_repulsion"):
243
+ pair_node_energy = self.pair_repulsion_fn(
244
+ lengths, data["node_attrs"], data["edge_index"], self.atomic_numbers
245
+ )
246
+ if is_lammps:
247
+ pair_node_energy = pair_node_energy[: lammps_natoms[0]]
248
+ pair_energy = scatter_sum(
249
+ src=pair_node_energy, index=data["batch"], dim=-1, dim_size=num_graphs
250
+ ) # [n_graphs,]
251
+ else:
252
+ pair_node_energy = torch.zeros_like(node_e0)
253
+ pair_energy = torch.zeros_like(e0)
254
+
255
+ # Interactions
256
+ energies = [e0, pair_energy]
257
+ node_energies_list = [node_e0, pair_node_energy]
258
+ node_feats_concat: List[torch.Tensor] = []
259
+
260
+ for i, (interaction, product, readout) in enumerate(
261
+ zip(self.interactions, self.products, self.readouts)
262
+ ):
263
+ node_attrs_slice = data["node_attrs"]
264
+ if is_lammps and i > 0:
265
+ node_attrs_slice = node_attrs_slice[: lammps_natoms[0]]
266
+ node_feats, sc = interaction(
267
+ node_attrs=node_attrs_slice,
268
+ node_feats=node_feats,
269
+ edge_attrs=edge_attrs,
270
+ edge_feats=edge_feats,
271
+ edge_index=data["edge_index"],
272
+ first_layer=(i == 0),
273
+ lammps_class=lammps_class,
274
+ lammps_natoms=lammps_natoms,
275
+ )
276
+ if is_lammps and i == 0:
277
+ node_attrs_slice = node_attrs_slice[: lammps_natoms[0]]
278
+ node_feats = product(
279
+ node_feats=node_feats, sc=sc, node_attrs=node_attrs_slice
280
+ )
281
+ node_feats_concat.append(node_feats)
282
+ node_es = readout(node_feats, node_heads)[num_atoms_arange, node_heads]
283
+ energy = scatter_sum(node_es, data["batch"], dim=0, dim_size=num_graphs)
284
+ energies.append(energy)
285
+ node_energies_list.append(node_es)
286
+
287
+ contributions = torch.stack(energies, dim=-1)
288
+ total_energy = torch.sum(contributions, dim=-1)
289
+ node_energy = torch.sum(torch.stack(node_energies_list, dim=-1), dim=-1)
290
+ node_feats_out = torch.cat(node_feats_concat, dim=-1)
291
+ node_energy = node_e0.double() + pair_node_energy.double()
292
+
293
+ forces, virials, stress, hessian, edge_forces = get_outputs(
294
+ energy=total_energy,
295
+ positions=positions,
296
+ displacement=displacement,
297
+ vectors=vectors,
298
+ cell=cell,
299
+ training=training,
300
+ compute_force=compute_force,
301
+ compute_virials=compute_virials,
302
+ compute_stress=compute_stress,
303
+ compute_hessian=compute_hessian,
304
+ compute_edge_forces=compute_edge_forces,
305
+ )
306
+
307
+ atomic_virials: Optional[torch.Tensor] = None
308
+ atomic_stresses: Optional[torch.Tensor] = None
309
+ if compute_atomic_stresses and edge_forces is not None:
310
+ atomic_virials, atomic_stresses = get_atomic_virials_stresses(
311
+ edge_forces=edge_forces,
312
+ edge_index=data["edge_index"],
313
+ vectors=vectors,
314
+ num_atoms=positions.shape[0],
315
+ batch=data["batch"],
316
+ cell=cell,
317
+ )
318
+ return {
319
+ "energy": total_energy,
320
+ "node_energy": node_energy,
321
+ "contributions": contributions,
322
+ "forces": forces,
323
+ "edge_forces": edge_forces,
324
+ "virials": virials,
325
+ "stress": stress,
326
+ "atomic_virials": atomic_virials,
327
+ "atomic_stresses": atomic_stresses,
328
+ "displacement": displacement,
329
+ "hessian": hessian,
330
+ "node_feats": node_feats_out,
331
+ }
332
+
333
+
334
+ @compile_mode("script")
335
+ class ScaleShiftMACE(MACE):
336
+ def __init__(
337
+ self,
338
+ atomic_inter_scale: float,
339
+ atomic_inter_shift: float,
340
+ **kwargs,
341
+ ):
342
+ super().__init__(**kwargs)
343
+ self.scale_shift = ScaleShiftBlock(
344
+ scale=atomic_inter_scale, shift=atomic_inter_shift
345
+ )
346
+
347
+ def forward(
348
+ self,
349
+ data: Dict[str, torch.Tensor],
350
+ training: bool = False,
351
+ compute_force: bool = True,
352
+ compute_virials: bool = False,
353
+ compute_stress: bool = False,
354
+ compute_displacement: bool = False,
355
+ compute_hessian: bool = False,
356
+ compute_edge_forces: bool = False,
357
+ compute_atomic_stresses: bool = False,
358
+ lammps_mliap: bool = False,
359
+ ) -> Dict[str, Optional[torch.Tensor]]:
360
+ # Setup
361
+ ctx = prepare_graph(
362
+ data,
363
+ compute_virials=compute_virials,
364
+ compute_stress=compute_stress,
365
+ compute_displacement=compute_displacement,
366
+ lammps_mliap=lammps_mliap,
367
+ )
368
+
369
+ is_lammps = ctx.is_lammps
370
+ num_atoms_arange = ctx.num_atoms_arange
371
+ num_graphs = ctx.num_graphs
372
+ displacement = ctx.displacement
373
+ positions = ctx.positions
374
+ vectors = ctx.vectors
375
+ lengths = ctx.lengths
376
+ cell = ctx.cell
377
+ node_heads = ctx.node_heads
378
+ interaction_kwargs = ctx.interaction_kwargs
379
+ lammps_natoms = interaction_kwargs.lammps_natoms
380
+ lammps_class = interaction_kwargs.lammps_class
381
+
382
+ # Atomic energies
383
+ node_e0 = self.atomic_energies_fn(data["node_attrs"])[
384
+ num_atoms_arange, node_heads
385
+ ]
386
+ e0 = scatter_sum(
387
+ src=node_e0, index=data["batch"], dim=0, dim_size=num_graphs
388
+ ) # [n_graphs, num_heads]
389
+
390
+ # Embeddings
391
+ node_feats = self.node_embedding(data["node_attrs"])
392
+ edge_attrs = self.spherical_harmonics(vectors)
393
+ edge_feats = self.radial_embedding(
394
+ lengths, data["node_attrs"], data["edge_index"], self.atomic_numbers
395
+ )
396
+
397
+ if hasattr(self, "pair_repulsion"):
398
+ pair_node_energy = self.pair_repulsion_fn(
399
+ lengths, data["node_attrs"], data["edge_index"], self.atomic_numbers
400
+ )
401
+ if is_lammps:
402
+ pair_node_energy = pair_node_energy[: lammps_natoms[0]]
403
+ else:
404
+ pair_node_energy = torch.zeros_like(node_e0)
405
+
406
+ # Interactions
407
+ node_es_list = [pair_node_energy]
408
+ node_feats_list: List[torch.Tensor] = []
409
+
410
+ for i, (interaction, product, readout) in enumerate(
411
+ zip(self.interactions, self.products, self.readouts)
412
+ ):
413
+ node_attrs_slice = data["node_attrs"]
414
+ if is_lammps and i > 0:
415
+ node_attrs_slice = node_attrs_slice[: lammps_natoms[0]]
416
+ node_feats, sc = interaction(
417
+ node_attrs=node_attrs_slice,
418
+ node_feats=node_feats,
419
+ edge_attrs=edge_attrs,
420
+ edge_feats=edge_feats,
421
+ edge_index=data["edge_index"],
422
+ first_layer=(i == 0),
423
+ lammps_class=lammps_class,
424
+ lammps_natoms=lammps_natoms,
425
+ )
426
+ if is_lammps and i == 0:
427
+ node_attrs_slice = node_attrs_slice[: lammps_natoms[0]]
428
+ node_feats = product(
429
+ node_feats=node_feats, sc=sc, node_attrs=node_attrs_slice
430
+ )
431
+ node_feats_list.append(node_feats)
432
+ node_es_list.append(
433
+ readout(node_feats, node_heads)[num_atoms_arange, node_heads]
434
+ )
435
+
436
+ node_feats_out = torch.cat(node_feats_list, dim=-1)
437
+ node_inter_es = torch.sum(torch.stack(node_es_list, dim=0), dim=0)
438
+ node_inter_es = self.scale_shift(node_inter_es, node_heads)
439
+ inter_e = scatter_sum(node_inter_es, data["batch"], dim=-1, dim_size=num_graphs)
440
+
441
+ total_energy = e0 + inter_e
442
+ node_energy = node_e0.clone().double() + node_inter_es.clone().double()
443
+
444
+ forces, virials, stress, hessian, edge_forces = get_outputs(
445
+ energy=inter_e,
446
+ positions=positions,
447
+ displacement=displacement,
448
+ vectors=vectors,
449
+ cell=cell,
450
+ training=training,
451
+ compute_force=compute_force,
452
+ compute_virials=compute_virials,
453
+ compute_stress=compute_stress,
454
+ compute_hessian=compute_hessian,
455
+ compute_edge_forces=compute_edge_forces or compute_atomic_stresses,
456
+ )
457
+
458
+ atomic_virials: Optional[torch.Tensor] = None
459
+ atomic_stresses: Optional[torch.Tensor] = None
460
+ if compute_atomic_stresses and edge_forces is not None:
461
+ atomic_virials, atomic_stresses = get_atomic_virials_stresses(
462
+ edge_forces=edge_forces,
463
+ edge_index=data["edge_index"],
464
+ vectors=vectors,
465
+ num_atoms=positions.shape[0],
466
+ batch=data["batch"],
467
+ cell=cell,
468
+ )
469
+ return {
470
+ "energy": total_energy,
471
+ "node_energy": node_energy,
472
+ "interaction_energy": inter_e,
473
+ "forces": forces,
474
+ "edge_forces": edge_forces,
475
+ "virials": virials,
476
+ "stress": stress,
477
+ "atomic_virials": atomic_virials,
478
+ "atomic_stresses": atomic_stresses,
479
+ "hessian": hessian,
480
+ "displacement": displacement,
481
+ "node_feats": node_feats_out,
482
+ }
483
+
484
+
485
+ @compile_mode("script")
486
+ class AtomicDipolesMACE(torch.nn.Module):
487
+ def __init__(
488
+ self,
489
+ r_max: float,
490
+ num_bessel: int,
491
+ num_polynomial_cutoff: int,
492
+ max_ell: int,
493
+ interaction_cls: Type[InteractionBlock],
494
+ interaction_cls_first: Type[InteractionBlock],
495
+ num_interactions: int,
496
+ num_elements: int,
497
+ hidden_irreps: o3.Irreps,
498
+ MLP_irreps: o3.Irreps,
499
+ avg_num_neighbors: float,
500
+ atomic_numbers: List[int],
501
+ correlation: int,
502
+ gate: Optional[Callable],
503
+ atomic_energies: Optional[
504
+ None
505
+ ], # Just here to make it compatible with energy models, MUST be None
506
+ radial_type: Optional[str] = "bessel",
507
+ radial_MLP: Optional[List[int]] = None,
508
+ cueq_config: Optional[Dict[str, Any]] = None, # pylint: disable=unused-argument
509
+ ):
510
+ super().__init__()
511
+ self.register_buffer(
512
+ "atomic_numbers", torch.tensor(atomic_numbers, dtype=torch.int64)
513
+ )
514
+ self.register_buffer("r_max", torch.tensor(r_max, dtype=torch.float64))
515
+ self.register_buffer(
516
+ "num_interactions", torch.tensor(num_interactions, dtype=torch.int64)
517
+ )
518
+ assert atomic_energies is None
519
+
520
+ # Embedding
521
+ node_attr_irreps = o3.Irreps([(num_elements, (0, 1))])
522
+ node_feats_irreps = o3.Irreps([(hidden_irreps.count(o3.Irrep(0, 1)), (0, 1))])
523
+ self.node_embedding = LinearNodeEmbeddingBlock(
524
+ irreps_in=node_attr_irreps, irreps_out=node_feats_irreps
525
+ )
526
+ self.radial_embedding = RadialEmbeddingBlock(
527
+ r_max=r_max,
528
+ num_bessel=num_bessel,
529
+ num_polynomial_cutoff=num_polynomial_cutoff,
530
+ radial_type=radial_type,
531
+ )
532
+ edge_feats_irreps = o3.Irreps(f"{self.radial_embedding.out_dim}x0e")
533
+
534
+ sh_irreps = o3.Irreps.spherical_harmonics(max_ell)
535
+ num_features = hidden_irreps.count(o3.Irrep(0, 1))
536
+ interaction_irreps = (sh_irreps * num_features).sort()[0].simplify()
537
+ self.spherical_harmonics = o3.SphericalHarmonics(
538
+ sh_irreps, normalize=True, normalization="component"
539
+ )
540
+ if radial_MLP is None:
541
+ radial_MLP = [64, 64, 64]
542
+
543
+ # Interactions and readouts
544
+ inter = interaction_cls_first(
545
+ node_attrs_irreps=node_attr_irreps,
546
+ node_feats_irreps=node_feats_irreps,
547
+ edge_attrs_irreps=sh_irreps,
548
+ edge_feats_irreps=edge_feats_irreps,
549
+ target_irreps=interaction_irreps,
550
+ hidden_irreps=hidden_irreps,
551
+ avg_num_neighbors=avg_num_neighbors,
552
+ radial_MLP=radial_MLP,
553
+ )
554
+ self.interactions = torch.nn.ModuleList([inter])
555
+
556
+ # Use the appropriate self connection at the first layer
557
+ use_sc_first = False
558
+ if "Residual" in str(interaction_cls_first):
559
+ use_sc_first = True
560
+
561
+ node_feats_irreps_out = inter.target_irreps
562
+ prod = EquivariantProductBasisBlock(
563
+ node_feats_irreps=node_feats_irreps_out,
564
+ target_irreps=hidden_irreps,
565
+ correlation=correlation,
566
+ num_elements=num_elements,
567
+ use_sc=use_sc_first,
568
+ )
569
+ self.products = torch.nn.ModuleList([prod])
570
+
571
+ self.readouts = torch.nn.ModuleList()
572
+ self.readouts.append(LinearDipoleReadoutBlock(hidden_irreps, dipole_only=True))
573
+
574
+ for i in range(num_interactions - 1):
575
+ if i == num_interactions - 2:
576
+ assert (
577
+ len(hidden_irreps) > 1
578
+ ), "To predict dipoles use at least l=1 hidden_irreps"
579
+ hidden_irreps_out = str(
580
+ hidden_irreps[1]
581
+ ) # Select only l=1 vectors for last layer
582
+ else:
583
+ hidden_irreps_out = hidden_irreps
584
+ inter = interaction_cls(
585
+ node_attrs_irreps=node_attr_irreps,
586
+ node_feats_irreps=hidden_irreps,
587
+ edge_attrs_irreps=sh_irreps,
588
+ edge_feats_irreps=edge_feats_irreps,
589
+ target_irreps=interaction_irreps,
590
+ hidden_irreps=hidden_irreps_out,
591
+ avg_num_neighbors=avg_num_neighbors,
592
+ radial_MLP=radial_MLP,
593
+ )
594
+ self.interactions.append(inter)
595
+ prod = EquivariantProductBasisBlock(
596
+ node_feats_irreps=interaction_irreps,
597
+ target_irreps=hidden_irreps_out,
598
+ correlation=correlation,
599
+ num_elements=num_elements,
600
+ use_sc=True,
601
+ )
602
+ self.products.append(prod)
603
+ if i == num_interactions - 2:
604
+ self.readouts.append(
605
+ NonLinearDipoleReadoutBlock(
606
+ hidden_irreps_out, MLP_irreps, gate, dipole_only=True
607
+ )
608
+ )
609
+ else:
610
+ self.readouts.append(
611
+ LinearDipoleReadoutBlock(hidden_irreps, dipole_only=True)
612
+ )
613
+
614
+ def forward(
615
+ self,
616
+ data: Dict[str, torch.Tensor],
617
+ training: bool = False, # pylint: disable=W0613
618
+ compute_force: bool = False,
619
+ compute_virials: bool = False,
620
+ compute_stress: bool = False,
621
+ compute_displacement: bool = False,
622
+ compute_edge_forces: bool = False, # pylint: disable=W0613
623
+ compute_atomic_stresses: bool = False, # pylint: disable=W0613
624
+ ) -> Dict[str, Optional[torch.Tensor]]:
625
+ assert compute_force is False
626
+ assert compute_virials is False
627
+ assert compute_stress is False
628
+ assert compute_displacement is False
629
+ # Setup
630
+ data["node_attrs"].requires_grad_(True)
631
+ data["positions"].requires_grad_(True)
632
+ num_graphs = data["ptr"].numel() - 1
633
+
634
+ # Embeddings
635
+ node_feats = self.node_embedding(data["node_attrs"])
636
+ vectors, lengths = get_edge_vectors_and_lengths(
637
+ positions=data["positions"],
638
+ edge_index=data["edge_index"],
639
+ shifts=data["shifts"],
640
+ )
641
+ edge_attrs = self.spherical_harmonics(vectors)
642
+ edge_feats = self.radial_embedding(
643
+ lengths, data["node_attrs"], data["edge_index"], self.atomic_numbers
644
+ )
645
+
646
+ # Interactions
647
+ dipoles = []
648
+ for interaction, product, readout in zip(
649
+ self.interactions, self.products, self.readouts
650
+ ):
651
+ node_feats, sc = interaction(
652
+ node_attrs=data["node_attrs"],
653
+ node_feats=node_feats,
654
+ edge_attrs=edge_attrs,
655
+ edge_feats=edge_feats,
656
+ edge_index=data["edge_index"],
657
+ )
658
+ node_feats = product(
659
+ node_feats=node_feats,
660
+ sc=sc,
661
+ node_attrs=data["node_attrs"],
662
+ )
663
+ node_dipoles = readout(node_feats).squeeze(-1) # [n_nodes,3]
664
+ dipoles.append(node_dipoles)
665
+
666
+ # Compute the dipoles
667
+ contributions_dipoles = torch.stack(
668
+ dipoles, dim=-1
669
+ ) # [n_nodes,3,n_contributions]
670
+ atomic_dipoles = torch.sum(contributions_dipoles, dim=-1) # [n_nodes,3]
671
+ total_dipole = scatter_sum(
672
+ src=atomic_dipoles,
673
+ index=data["batch"],
674
+ dim=0,
675
+ dim_size=num_graphs,
676
+ ) # [n_graphs,3]
677
+ baseline = compute_fixed_charge_dipole(
678
+ charges=data["charges"],
679
+ positions=data["positions"],
680
+ batch=data["batch"],
681
+ num_graphs=num_graphs,
682
+ ) # [n_graphs,3]
683
+ total_dipole = total_dipole + baseline
684
+
685
+ output = {
686
+ "dipole": total_dipole,
687
+ "atomic_dipoles": atomic_dipoles,
688
+ }
689
+ return output
690
+
691
+
692
+ @compile_mode("script")
693
+ class EnergyDipolesMACE(torch.nn.Module):
694
+ def __init__(
695
+ self,
696
+ r_max: float,
697
+ num_bessel: int,
698
+ num_polynomial_cutoff: int,
699
+ max_ell: int,
700
+ interaction_cls: Type[InteractionBlock],
701
+ interaction_cls_first: Type[InteractionBlock],
702
+ num_interactions: int,
703
+ num_elements: int,
704
+ hidden_irreps: o3.Irreps,
705
+ MLP_irreps: o3.Irreps,
706
+ avg_num_neighbors: float,
707
+ atomic_numbers: List[int],
708
+ correlation: int,
709
+ gate: Optional[Callable],
710
+ atomic_energies: Optional[np.ndarray],
711
+ radial_MLP: Optional[List[int]] = None,
712
+ cueq_config: Optional[Dict[str, Any]] = None, # pylint: disable=unused-argument
713
+ ):
714
+ super().__init__()
715
+ self.register_buffer(
716
+ "atomic_numbers", torch.tensor(atomic_numbers, dtype=torch.int64)
717
+ )
718
+ self.register_buffer("r_max", torch.tensor(r_max, dtype=torch.float64))
719
+ self.register_buffer(
720
+ "num_interactions", torch.tensor(num_interactions, dtype=torch.int64)
721
+ )
722
+ # Embedding
723
+ node_attr_irreps = o3.Irreps([(num_elements, (0, 1))])
724
+ node_feats_irreps = o3.Irreps([(hidden_irreps.count(o3.Irrep(0, 1)), (0, 1))])
725
+ self.node_embedding = LinearNodeEmbeddingBlock(
726
+ irreps_in=node_attr_irreps, irreps_out=node_feats_irreps
727
+ )
728
+ self.radial_embedding = RadialEmbeddingBlock(
729
+ r_max=r_max,
730
+ num_bessel=num_bessel,
731
+ num_polynomial_cutoff=num_polynomial_cutoff,
732
+ )
733
+ edge_feats_irreps = o3.Irreps(f"{self.radial_embedding.out_dim}x0e")
734
+
735
+ sh_irreps = o3.Irreps.spherical_harmonics(max_ell)
736
+ num_features = hidden_irreps.count(o3.Irrep(0, 1))
737
+ interaction_irreps = (sh_irreps * num_features).sort()[0].simplify()
738
+ self.spherical_harmonics = o3.SphericalHarmonics(
739
+ sh_irreps, normalize=True, normalization="component"
740
+ )
741
+ if radial_MLP is None:
742
+ radial_MLP = [64, 64, 64]
743
+ # Interactions and readouts
744
+ self.atomic_energies_fn = AtomicEnergiesBlock(atomic_energies)
745
+
746
+ inter = interaction_cls_first(
747
+ node_attrs_irreps=node_attr_irreps,
748
+ node_feats_irreps=node_feats_irreps,
749
+ edge_attrs_irreps=sh_irreps,
750
+ edge_feats_irreps=edge_feats_irreps,
751
+ target_irreps=interaction_irreps,
752
+ hidden_irreps=hidden_irreps,
753
+ avg_num_neighbors=avg_num_neighbors,
754
+ radial_MLP=radial_MLP,
755
+ )
756
+ self.interactions = torch.nn.ModuleList([inter])
757
+
758
+ # Use the appropriate self connection at the first layer
759
+ use_sc_first = False
760
+ if "Residual" in str(interaction_cls_first):
761
+ use_sc_first = True
762
+
763
+ node_feats_irreps_out = inter.target_irreps
764
+ prod = EquivariantProductBasisBlock(
765
+ node_feats_irreps=node_feats_irreps_out,
766
+ target_irreps=hidden_irreps,
767
+ correlation=correlation,
768
+ num_elements=num_elements,
769
+ use_sc=use_sc_first,
770
+ )
771
+ self.products = torch.nn.ModuleList([prod])
772
+
773
+ self.readouts = torch.nn.ModuleList()
774
+ self.readouts.append(LinearDipoleReadoutBlock(hidden_irreps, dipole_only=False))
775
+
776
+ for i in range(num_interactions - 1):
777
+ if i == num_interactions - 2:
778
+ assert (
779
+ len(hidden_irreps) > 1
780
+ ), "To predict dipoles use at least l=1 hidden_irreps"
781
+ hidden_irreps_out = str(
782
+ hidden_irreps[:2]
783
+ ) # Select scalars and l=1 vectors for last layer
784
+ else:
785
+ hidden_irreps_out = hidden_irreps
786
+ inter = interaction_cls(
787
+ node_attrs_irreps=node_attr_irreps,
788
+ node_feats_irreps=hidden_irreps,
789
+ edge_attrs_irreps=sh_irreps,
790
+ edge_feats_irreps=edge_feats_irreps,
791
+ target_irreps=interaction_irreps,
792
+ hidden_irreps=hidden_irreps_out,
793
+ avg_num_neighbors=avg_num_neighbors,
794
+ radial_MLP=radial_MLP,
795
+ )
796
+ self.interactions.append(inter)
797
+ prod = EquivariantProductBasisBlock(
798
+ node_feats_irreps=interaction_irreps,
799
+ target_irreps=hidden_irreps_out,
800
+ correlation=correlation,
801
+ num_elements=num_elements,
802
+ use_sc=True,
803
+ )
804
+ self.products.append(prod)
805
+ if i == num_interactions - 2:
806
+ self.readouts.append(
807
+ NonLinearDipoleReadoutBlock(
808
+ hidden_irreps_out, MLP_irreps, gate, dipole_only=False
809
+ )
810
+ )
811
+ else:
812
+ self.readouts.append(
813
+ LinearDipoleReadoutBlock(hidden_irreps, dipole_only=False)
814
+ )
815
+
816
+ def forward(
817
+ self,
818
+ data: Dict[str, torch.Tensor],
819
+ training: bool = False,
820
+ compute_force: bool = True,
821
+ compute_virials: bool = False,
822
+ compute_stress: bool = False,
823
+ compute_displacement: bool = False,
824
+ compute_edge_forces: bool = False, # pylint: disable=W0613
825
+ compute_atomic_stresses: bool = False, # pylint: disable=W0613
826
+ ) -> Dict[str, Optional[torch.Tensor]]:
827
+ # Setup
828
+ data["node_attrs"].requires_grad_(True)
829
+ data["positions"].requires_grad_(True)
830
+ num_graphs = data["ptr"].numel() - 1
831
+ num_atoms_arange = torch.arange(data["positions"].shape[0])
832
+ displacement = torch.zeros(
833
+ (num_graphs, 3, 3),
834
+ dtype=data["positions"].dtype,
835
+ device=data["positions"].device,
836
+ )
837
+ if compute_virials or compute_stress or compute_displacement:
838
+ (
839
+ data["positions"],
840
+ data["shifts"],
841
+ displacement,
842
+ ) = get_symmetric_displacement(
843
+ positions=data["positions"],
844
+ unit_shifts=data["unit_shifts"],
845
+ cell=data["cell"],
846
+ edge_index=data["edge_index"],
847
+ num_graphs=num_graphs,
848
+ batch=data["batch"],
849
+ )
850
+
851
+ # Atomic energies
852
+ node_e0 = self.atomic_energies_fn(data["node_attrs"])[
853
+ num_atoms_arange, data["head"][data["batch"]]
854
+ ]
855
+ e0 = scatter_sum(
856
+ src=node_e0, index=data["batch"], dim=-1, dim_size=num_graphs
857
+ ) # [n_graphs,]
858
+
859
+ # Embeddings
860
+ node_feats = self.node_embedding(data["node_attrs"])
861
+ vectors, lengths = get_edge_vectors_and_lengths(
862
+ positions=data["positions"],
863
+ edge_index=data["edge_index"],
864
+ shifts=data["shifts"],
865
+ )
866
+ edge_attrs = self.spherical_harmonics(vectors)
867
+ edge_feats = self.radial_embedding(
868
+ lengths, data["node_attrs"], data["edge_index"], self.atomic_numbers
869
+ )
870
+
871
+ # Interactions
872
+ energies = [e0]
873
+ node_energies_list = [node_e0]
874
+ dipoles = []
875
+ for interaction, product, readout in zip(
876
+ self.interactions, self.products, self.readouts
877
+ ):
878
+ node_feats, sc = interaction(
879
+ node_attrs=data["node_attrs"],
880
+ node_feats=node_feats,
881
+ edge_attrs=edge_attrs,
882
+ edge_feats=edge_feats,
883
+ edge_index=data["edge_index"],
884
+ )
885
+ node_feats = product(
886
+ node_feats=node_feats,
887
+ sc=sc,
888
+ node_attrs=data["node_attrs"],
889
+ )
890
+ node_out = readout(node_feats).squeeze(-1) # [n_nodes, ]
891
+ # node_energies = readout(node_feats).squeeze(-1) # [n_nodes, ]
892
+ node_energies = node_out[:, 0]
893
+ energy = scatter_sum(
894
+ src=node_energies, index=data["batch"], dim=-1, dim_size=num_graphs
895
+ ) # [n_graphs,]
896
+ energies.append(energy)
897
+ node_dipoles = node_out[:, 1:]
898
+ dipoles.append(node_dipoles)
899
+
900
+ # Compute the energies and dipoles
901
+ contributions = torch.stack(energies, dim=-1)
902
+ total_energy = torch.sum(contributions, dim=-1) # [n_graphs, ]
903
+ node_energy_contributions = torch.stack(node_energies_list, dim=-1)
904
+ node_energy = torch.sum(node_energy_contributions, dim=-1) # [n_nodes, ]
905
+ contributions_dipoles = torch.stack(
906
+ dipoles, dim=-1
907
+ ) # [n_nodes,3,n_contributions]
908
+ atomic_dipoles = torch.sum(contributions_dipoles, dim=-1) # [n_nodes,3]
909
+ total_dipole = scatter_sum(
910
+ src=atomic_dipoles,
911
+ index=data["batch"].unsqueeze(-1),
912
+ dim=0,
913
+ dim_size=num_graphs,
914
+ ) # [n_graphs,3]
915
+ baseline = compute_fixed_charge_dipole(
916
+ charges=data["charges"],
917
+ positions=data["positions"],
918
+ batch=data["batch"],
919
+ num_graphs=num_graphs,
920
+ ) # [n_graphs,3]
921
+ total_dipole = total_dipole + baseline
922
+
923
+ forces, virials, stress, _, _ = get_outputs(
924
+ energy=total_energy,
925
+ positions=data["positions"],
926
+ displacement=displacement,
927
+ cell=data["cell"],
928
+ training=training,
929
+ compute_force=compute_force,
930
+ compute_virials=compute_virials,
931
+ compute_stress=compute_stress,
932
+ )
933
+
934
+ output = {
935
+ "energy": total_energy,
936
+ "node_energy": node_energy,
937
+ "contributions": contributions,
938
+ "forces": forces,
939
+ "virials": virials,
940
+ "stress": stress,
941
+ "displacement": displacement,
942
+ "dipole": total_dipole,
943
+ "atomic_dipoles": atomic_dipoles,
944
+ }
945
+ return output
model/py.typed ADDED
@@ -0,0 +1 @@
 
 
1
+
scripts/demo/README.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MACE 训练 Demo
2
+
3
+ ## 快速上手
4
+
5
+ ### 1. 选择配置
6
+
7
+ `configs/` 目录下提供了多个预定义的实验配置:
8
+
9
+ | 配置文件 | 数据集 | GPU 数 | 说明 |
10
+ | ------------------------------- | ------------ | ------ | ---------------- |
11
+ | `DMC.yaml` | DMC 溶剂 XTB | 1 | 入门示例,最简单 |
12
+ | `water_1dcu.yaml` | Water | 1 | 单卡训练 |
13
+ | `water_4dcu.yaml` | Water | 4 | 4 卡分布式训练 |
14
+ | `water_8dcu.yaml` | Water | 8 | 8 卡分布式训练 |
15
+ | `ani1x_8dcu.yaml` | ANI-1x | 8 | 分布式训练 |
16
+ | `nanotube_l0_8dcu.yaml` | 碳纳米管 | 8 | max_L=0 |
17
+ | `nanotube_l2_8dcu.yaml` | 碳纳米管 | 8 | max_L=2 |
18
+ | `nanotube_l2_16dcu.yaml` | 碳纳米管 | 2x8 | 多节点分布式 |
19
+
20
+ ### 2. 运行训练
21
+
22
+ ```bash
23
+ # 方式一:直接运行(交互式或在已分配的 SLURM 节点上)
24
+ bash run.sh --config configs/DMC.yaml
25
+
26
+ # 方式二:提交 SLURM 作业
27
+ bash run.sh --config configs/DMC.yaml --submit
28
+
29
+ # 方式三:预览命令(不执行)
30
+ bash run.sh --config configs/DMC.yaml --dry-run
31
+ ```
32
+
33
+ ### 3. 查看输出
34
+
35
+ 训练输出自动保存到 `outputs/{实验名}_{时间戳}/` 目录,包含:
36
+ - 模型 checkpoint
37
+ - 训练日志
38
+ - 当次使用的配置快照 (`config.yaml`)
39
+
40
+ ## 创建自定义实验
41
+
42
+ 1. 复制一个最接近的配置文件:
43
+ ```bash
44
+ cp configs/DMC.yaml configs/my_experiment.yaml
45
+ ```
46
+
47
+ 2. 编辑 YAML 中的参数(只需改参数值,不用碰任何 shell 脚本)
48
+
49
+ 3. 运行:
50
+ ```bash
51
+ bash run.sh --config configs/my_experiment.yaml
52
+ ```
53
+
54
+ ## YAML 配置字段说明
55
+
56
+ ### `train_args` - 训练参数
57
+
58
+ 所有字段直接映射为 `train.py` 的命令行参数。布尔值 `true` 转为标志参数(如 `swa: true` -> `--swa`),`false` 则跳过。
59
+
60
+ 常用参数:
61
+
62
+ | 参数 | 说明 | 示例 |
63
+ | -------------- | ---------------- | ----------------------------- |
64
+ | `model` | 模型类型 | `MACE` |
65
+ | `r_max` | 截断半径 (A) | `4.0` - `6.0` |
66
+ | `num_channels` | 通道数 | `64`, `256` |
67
+ | `max_L` | 最大角动量量子数 | `0`, `2` |
68
+ | `batch_size` | 训练批大小 | `2` - `128` |
69
+ | `E0s` | 原子参考能量 | `average`, `isolated`, 显式字典 |
70
+ | `swa` | 启用随机权重平均 | `true` |
71
+ | `ema` | 启用指数移动平均 | `true` |
72
+ | `distributed` | 启用分布式训练 | `true` (多卡时自动添加) |
73
+
74
+ ### `launch` - 启动配置
75
+
76
+ | 参数 | 说明 | 启动方式 |
77
+ | ------------------------------- | ---------- | ------------------------------ |
78
+ | `num_nodes: 1, num_gpus: 1` | 单卡 | `python train.py` |
79
+ | `num_nodes: 1, num_gpus: N` | 单节点多卡 | `torchrun --nproc_per_node=N` |
80
+ | `num_nodes: M, num_gpus: N` | 多节点 | `srun` (需 --submit) |
81
+
82
+ ### `env` - 环境配置
83
+
84
+ | 参数 | 说明 |
85
+ | ----------- | ----------------------- |
86
+ | `conda_env` | conda 环境名 |
87
+ | `modules` | 需要加载的 module 列表 |
88
+
89
+ ### `slurm` - SLURM 作业配置
90
+
91
+ | 参数 | 说明 |
92
+ | --------------- | -------------- |
93
+ | `partition` | SLURM 分区 |
94
+ | `time` | 作业时间限制 |
95
+ | `cpus_per_task` | CPU 核心数 |
96
+
97
+ ### `nccl` - 多节点通信配置(可选)
98
+
99
+ | 参数 | 说明 |
100
+ | --------------- | ------------------- |
101
+ | `socket_ifname` | InfiniBand 网卡名 |
102
+ | `ib_hca` | IB HCA 设备名 |
103
+ | `proto` | NCCL 协议 |
104
+
105
+ ## 目录结构
106
+
107
+ ```
108
+ demo/
109
+ run.sh # 统一入口脚本
110
+ _parse_config.py # 配置解析器(内部使用)
111
+ README.md # 本文件
112
+ configs/ # 实验配置
113
+ templates/ # 脚本模板
114
+ env_setup.sh # 环境初始化
115
+ preflight_check.sh # 训练前预检
116
+ slurm_header.template # SLURM header 模板
117
+ outputs/ # 训练输出(自动创建)
118
+ ```
scripts/demo/_parse_config.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Parse MACE demo YAML and print derived values for run.sh.
3
+
4
+ Usage:
5
+ python _parse_config.py <config.yaml> <action>
6
+
7
+ Actions:
8
+ command Print launch command
9
+ env Print environment exports
10
+ env-args Print arguments for env_setup.sh
11
+ data-files Print input files for preflight check
12
+ slurm Print SLURM variables
13
+ name Print experiment name
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import shlex
20
+ import sys
21
+ from typing import Any
22
+
23
+ import yaml
24
+
25
+
26
+ def load_config(path: str) -> dict[str, Any]:
27
+ with open(path, encoding="utf-8") as f:
28
+ return yaml.safe_load(f) or {}
29
+
30
+
31
+ def _needs_quoting(value: str) -> bool:
32
+ special = set(" {}()[]|&;'\"\\!`<>*?~#")
33
+ return any(c in special for c in value)
34
+
35
+
36
+ def _quote_value(value: Any) -> str:
37
+ s = str(value)
38
+ if "$" in s:
39
+ escaped = s.replace("\\", "\\\\").replace('"', '\\"')
40
+ return f'"{escaped}"'
41
+ return shlex.quote(s)
42
+
43
+
44
+ def _build_cli_args(arg_map: dict[str, Any], auto_name: str | None = None) -> list[str]:
45
+ args: list[str] = []
46
+ if auto_name and "name" not in arg_map:
47
+ args.append(f"--name={_quote_value(auto_name)}")
48
+
49
+ for key, value in arg_map.items():
50
+ if isinstance(value, bool):
51
+ if value:
52
+ args.append(f"--{key}")
53
+ continue
54
+ if isinstance(value, (dict, list)):
55
+ args.append(f"--{key}={_quote_value(value)}")
56
+ continue
57
+
58
+ s = str(value)
59
+ if _needs_quoting(s):
60
+ args.append(f"--{key}={_quote_value(s)}")
61
+ else:
62
+ args.append(f"--{key}={s}")
63
+ return args
64
+
65
+
66
+ def _format_command_with_args(prefix: str, args: list[str]) -> str:
67
+ if not args:
68
+ return prefix
69
+ return f"{prefix} \
70
+ " + " \
71
+ ".join(args)
72
+
73
+
74
+ def _demo_dir(config_path: str) -> str:
75
+ return os.path.dirname(os.path.dirname(os.path.abspath(config_path)))
76
+
77
+
78
+ def _mace_dir(config_path: str) -> str:
79
+ return os.path.dirname(_demo_dir(config_path))
80
+
81
+
82
+ def _get_train_py_path(config_path: str) -> str:
83
+ # train.py 放在 scripts/ 目录下,即 demo 的父目录
84
+ return os.path.join(_mace_dir(config_path), "train.py")
85
+
86
+
87
+ def _resolve_script_path(config_path: str, script_value: str) -> str:
88
+ if os.path.isabs(script_value):
89
+ return script_value
90
+
91
+ config_dir = os.path.dirname(os.path.abspath(config_path))
92
+ demo_dir = _demo_dir(config_path)
93
+ mace_dir = _mace_dir(config_path)
94
+
95
+ candidates = [
96
+ os.path.join(config_dir, script_value),
97
+ os.path.join(demo_dir, script_value),
98
+ os.path.join(mace_dir, script_value),
99
+ os.path.join(mace_dir, "scripts", script_value),
100
+ ]
101
+ for c in candidates:
102
+ if os.path.exists(c):
103
+ return c
104
+
105
+ # Return the most likely path even if it does not exist; preflight will catch it.
106
+ return os.path.join(mace_dir, "scripts", script_value)
107
+
108
+
109
+ def _is_entrypoint_mode(cfg: dict[str, Any]) -> bool:
110
+ entrypoint = cfg.get("entrypoint")
111
+ return isinstance(entrypoint, dict) and bool(entrypoint.get("script"))
112
+
113
+
114
+ def _build_train_command(cfg: dict[str, Any], config_path: str) -> str:
115
+ launch = cfg.get("launch", {}) or {}
116
+ num_nodes = int(launch.get("num_nodes", 1))
117
+ num_gpus = int(launch.get("num_gpus", 1))
118
+ launcher = launch.get("launcher", "python")
119
+
120
+ train_py = _get_train_py_path(config_path)
121
+ train_args = _build_cli_args(
122
+ cfg.get("train_args", {}) or {},
123
+ auto_name=cfg.get("name"),
124
+ )
125
+
126
+ if num_nodes > 1 and "--distributed" not in train_args:
127
+ train_args.append("--distributed")
128
+ if launcher == "torchrun" and "--distributed" not in train_args:
129
+ train_args.append("--distributed")
130
+
131
+ if num_nodes > 1:
132
+ return _format_command_with_args(f"python {train_py}", train_args)
133
+ if launcher == "torchrun":
134
+ prefix = (
135
+ "torchrun \
136
+ --nnodes=1 \
137
+ "
138
+ f"--nproc_per_node={num_gpus} \
139
+ {train_py}"
140
+ )
141
+ return _format_command_with_args(prefix, train_args)
142
+ return _format_command_with_args(f"python {train_py}", train_args)
143
+
144
+
145
+ def _build_entrypoint_command(cfg: dict[str, Any], config_path: str) -> str:
146
+ entrypoint = cfg.get("entrypoint", {}) or {}
147
+ script_path = _resolve_script_path(config_path, str(entrypoint["script"]))
148
+ args = _build_cli_args(entrypoint.get("args", {}) or {})
149
+ return _format_command_with_args(f"python {script_path}", args)
150
+
151
+
152
+ def print_command(cfg: dict[str, Any], config_path: str) -> None:
153
+ if _is_entrypoint_mode(cfg):
154
+ print(_build_entrypoint_command(cfg, config_path))
155
+ else:
156
+ print(_build_train_command(cfg, config_path))
157
+
158
+
159
+ def print_env(cfg: dict[str, Any]) -> None:
160
+ launch = cfg.get("launch", {}) or {}
161
+ num_nodes = int(launch.get("num_nodes", 1))
162
+ num_gpus = int(launch.get("num_gpus", 1))
163
+
164
+ omp = int(launch.get("omp_num_threads", 1))
165
+ print(f"export OMP_NUM_THREADS={omp}")
166
+
167
+ if num_gpus > 1:
168
+ devices = ",".join(str(i) for i in range(num_gpus))
169
+ print(f"export HIP_VISIBLE_DEVICES={devices}")
170
+ elif num_gpus == 1:
171
+ print("export HIP_VISIBLE_DEVICES=0")
172
+
173
+ if num_nodes > 1:
174
+ nccl = cfg.get("nccl", {}) or {}
175
+ print("export HSA_FORCE_FINE_GRAIN_PCIE=1")
176
+ if nccl.get("socket_ifname"):
177
+ print(f"export NCCL_SOCKET_IFNAME={nccl['socket_ifname']}")
178
+ if nccl.get("ib_hca"):
179
+ print(f"export NCCL_IB_HCA={nccl['ib_hca']}")
180
+ if nccl.get("proto"):
181
+ print(f"export NCCL_PROTO={nccl['proto']}")
182
+
183
+ # 用户自定义额外环境变量(可覆盖上面自动生成的变量)
184
+ extra_env = cfg.get("extra_env", {})
185
+ for key, value in extra_env.items():
186
+ if value is None or value == "":
187
+ # 空值时只导出 key,保留当前 shell 中的值
188
+ print(f"export {key}")
189
+ else:
190
+ print(f"export {key}={_quote_value(str(value))}")
191
+
192
+
193
+ def print_env_args(cfg: dict[str, Any]) -> None:
194
+ env = cfg.get("env", {}) or {}
195
+ conda_env = env.get("conda_env", "chem")
196
+ modules = env.get("modules", []) or []
197
+ parts = [str(conda_env)] + [str(m) for m in modules]
198
+ print(" ".join(parts))
199
+
200
+
201
+ def _print_unique(paths: list[str]) -> None:
202
+ seen: set[str] = set()
203
+ for path in paths:
204
+ if not path:
205
+ continue
206
+ if path in seen:
207
+ continue
208
+ seen.add(path)
209
+ print(path)
210
+
211
+
212
+ def print_data_files(cfg: dict[str, Any], config_path: str) -> None:
213
+ explicit = cfg.get("preflight_files")
214
+ if isinstance(explicit, list):
215
+ _print_unique([str(x) for x in explicit])
216
+ return
217
+
218
+ if _is_entrypoint_mode(cfg):
219
+ entrypoint = cfg.get("entrypoint", {}) or {}
220
+ files: list[str] = []
221
+ data_files = entrypoint.get("data_files", [])
222
+ if isinstance(data_files, list):
223
+ files.extend(str(x) for x in data_files)
224
+ script_value = entrypoint.get("script")
225
+ if script_value:
226
+ files.append(_resolve_script_path(config_path, str(script_value)))
227
+ _print_unique(files)
228
+ return
229
+
230
+ train_args = cfg.get("train_args", {}) or {}
231
+ files: list[str] = []
232
+ for key in (
233
+ "train_file",
234
+ "valid_file",
235
+ "test_file",
236
+ "statistics_file",
237
+ "foundation_model",
238
+ "pt_train_file",
239
+ ):
240
+ val = train_args.get(key)
241
+ if val:
242
+ files.append(str(val))
243
+ _print_unique(files)
244
+
245
+
246
+ def print_slurm(cfg: dict[str, Any]) -> None:
247
+ launch = cfg.get("launch", {}) or {}
248
+ slurm = cfg.get("slurm", {}) or {}
249
+ num_nodes = int(launch.get("num_nodes", 1))
250
+ num_gpus = int(launch.get("num_gpus", 1))
251
+
252
+ name = cfg.get("name", "mace_train")
253
+ partition = slurm.get("partition", "newlarge")
254
+ time_limit = slurm.get("time", "8:00:00")
255
+ cpus = int(slurm.get("cpus_per_task", 64))
256
+
257
+ if num_nodes > 1:
258
+ ntasks = num_gpus
259
+ cpus_per_task = max(1, cpus // max(1, num_gpus))
260
+ else:
261
+ ntasks = 1
262
+ cpus_per_task = cpus
263
+
264
+ print(f"JOB_NAME={name}")
265
+ print(f"PARTITION={partition}")
266
+ print(f"NODES={num_nodes}")
267
+ print(f"NTASKS_PER_NODE={ntasks}")
268
+ print(f"CPUS_PER_TASK={cpus_per_task}")
269
+ print(f"GPUS_PER_NODE={num_gpus}")
270
+ print(f"TIME={time_limit}")
271
+
272
+
273
+ def print_name(cfg: dict[str, Any]) -> None:
274
+ print(cfg.get("name", "mace_train"))
275
+
276
+
277
+ def main() -> None:
278
+ if len(sys.argv) < 3:
279
+ print(__doc__, file=sys.stderr)
280
+ sys.exit(1)
281
+
282
+ config_path = sys.argv[1]
283
+ action = sys.argv[2]
284
+ cfg = load_config(config_path)
285
+
286
+ actions = {
287
+ "command": lambda: print_command(cfg, config_path),
288
+ "env": lambda: print_env(cfg),
289
+ "env-args": lambda: print_env_args(cfg),
290
+ "data-files": lambda: print_data_files(cfg, config_path),
291
+ "slurm": lambda: print_slurm(cfg),
292
+ "name": lambda: print_name(cfg),
293
+ }
294
+
295
+ fn = actions.get(action)
296
+ if fn is None:
297
+ print(f"Unknown action: {action}", file=sys.stderr)
298
+ print(f"Available: {', '.join(actions.keys())}", file=sys.stderr)
299
+ sys.exit(1)
300
+ fn()
301
+
302
+
303
+ if __name__ == "__main__":
304
+ main()
scripts/demo/configs/DMC.yaml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # DMC 溶剂 XTB 训练配置
3
+ # 入门级示例:单 GPU,小数据集,参数最少
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: DMC
8
+
9
+ # 实验描述
10
+ description: "DMC solvent XTB - 单 GPU 入门示例 (200 训练帧)"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+
22
+ # --- 数据配置 ---
23
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,
24
+ # 下载 ModelScope DMC 数据集到 ./data 后即可使用 可能需根据实际路径进行调整
25
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/data/DMC/solvent_xtb_train_200.xyz"
26
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/data/DMC/solvent_xtb_test.xyz"
27
+ valid_fraction: 0.10 # 从训练集中划分 10% 作为验证集
28
+ energy_key: energy_xtb # XYZ 文件中能量字段名
29
+ forces_key: forces_xtb # XYZ 文件中力字段名
30
+ E0s: average # 原子参考能量方式: average / isolated / 显式字典
31
+
32
+ # --- 训练超参 ---
33
+ seed: 123 # 随机种子
34
+ device: cuda # 设备: cuda / cpu
35
+ r_max: 4.0 # 截断半径 (Angstrom)
36
+ batch_size: 10 # 训练批大小
37
+ max_num_epochs: 100 # 最大训练轮数
38
+ eval_interval: 10 # 每 10 个 epoch 评估并打印一次
39
+
40
+ # --- SWA (Stochastic Weight Averaging) ---
41
+ swa: true # 启用 SWA
42
+
43
+ # ============================================================
44
+ # 启动配置(控制 python / torchrun / srun 的选择)
45
+ # ============================================================
46
+ launch:
47
+ launcher: python # 启动方式: python / torchrun (多节点时自动用 srun)
48
+ num_nodes: 1 # 节点数
49
+ num_gpus: 8 # 申请的 DCU 卡数
50
+
51
+ # ============================================================
52
+ # 环境配置
53
+ # ============================================================
54
+ env:
55
+ conda_env: matchem # conda 环境名
56
+ modules:
57
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
58
+
59
+ # ============================================================
60
+ # SLURM 配置(仅 --submit 模式使用)
61
+ # ============================================================
62
+ slurm:
63
+ partition: hpctest01 # SLURM 分区
64
+ time: "2:00:00" # 作业时间限制
65
+ cpus_per_task: 128 # CPU 核心数
scripts/demo/configs/ani1x_8dcu.yaml ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # ANI-1x 8 DCU 训练配置
3
+ # 单节点 8 DCU 分布式训练
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: ani1x_8dcu
8
+
9
+ # 实验描述
10
+ description: "ANI-1x - 8 DCU 分布式训练"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+ default_dtype: float64 # 数据精度
22
+ num_interactions: 2 # 交互层数
23
+ num_channels: 64 # 通道数
24
+ max_L: 0 # 最大角动量
25
+ correlation: 3 # 相关阶数
26
+
27
+ # --- 数据配置 ---
28
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,下载对应数据集到 ./data 后即可使用
29
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/ani1x/ANI1x_cc_DFT_rc5_train"
30
+ valid_file: "${ONESCIENCE_DATASETS_DIR}/data/ani1x/ANI1x_cc_DFT_rc5_val"
31
+ statistics_file: "${ONESCIENCE_DATASETS_DIR}/data/ani1x/ANI1x_cc_DFT_rc5_statistics.json"
32
+ E0s: "{1: -13.62222753701504, 6: -1029.4130839658328, 7: -1484.8710358098756, 8: -2041.8396277138045}"
33
+
34
+ # --- 训练超参 ---
35
+ seed: 123 # 随机种子
36
+ device: cuda # 设备: cuda / cpu
37
+ r_max: 4.5 # 截断半径 (Angstrom)
38
+ batch_size: 128 # 训练批大小
39
+ valid_batch_size: 128 # 验证批大小
40
+ max_num_epochs: 20 # 最大训练轮数
41
+ forces_weight: 1000 # 力的损失权重
42
+ energy_weight: 40 # 能量的损失权重
43
+ weight_decay: 1e-7 # 权重衰减
44
+ clip_grad: 1.0 # 梯度裁剪阈值
45
+ scheduler_patience: 20 # 学习率调度器耐心值
46
+ patience: 50 # 早停耐心值
47
+ eval_interval: 1 # 每 10 个 epoch 评估并打印一次
48
+ num_workers: 32 # 数据加载线程数
49
+ error_table: PerAtomMAE # 误差表类型
50
+
51
+ # --- SWA / EMA ---
52
+ ema: true # 启用 EMA
53
+ swa: true # 启用 SWA
54
+ start_swa: 15 # SWA 开始 epoch
55
+ swa_lr: 0.00025 # SWA 学习率
56
+ swa_forces_weight: 10 # SWA 力的损失权重
57
+
58
+ # --- 其他 ---
59
+ restart_latest: true # 从最新检查点重启
60
+ save_cpu: true # 保存模型到 CPU
61
+
62
+ # ============================================================
63
+ # 启动配置(控制 python / torchrun / srun 的选择)
64
+ # ============================================================
65
+ launch:
66
+ launcher: torchrun # 启动方式: python / torchrun (多节点时自动用 srun)
67
+ num_nodes: 1 # 节点数
68
+ num_gpus: 8 # 申请的 DCU 卡数
69
+
70
+ # ============================================================
71
+ # 环境配置
72
+ # ============================================================
73
+ env:
74
+ conda_env: matchem # conda 环境名
75
+ modules:
76
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
77
+
78
+ # ============================================================
79
+ # SLURM 配置(仅 --submit 模式使用)
80
+ # ============================================================
81
+ slurm:
82
+ partition: hpctest02 # SLURM 分区
83
+ time: "8:00:00" # 作业时间限制
84
+ cpus_per_task: 128 # CPU 核心数
scripts/demo/configs/nanotube_eval_l0_1dcu.yaml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Nanotube L0 evaluation (single DCU)
3
+ # ============================================================
4
+ name: nanotube_eval_l0_1dcu
5
+
6
+ description: "Evaluate nanotube L0 model with eval_configs.py"
7
+
8
+ entrypoint:
9
+ script: eval_configs.py
10
+ args:
11
+ configs: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_large.xyz"
12
+ model: "${ONESCIENCE_MODELS_DIR}/mace/nanotube_l0_8dcu.model"
13
+ output: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_large_eavl.xyz"
14
+ device: cuda
15
+ default_dtype: float64
16
+ batch_size: 2
17
+ info_prefix: "MACE_"
18
+ data_files:
19
+ - "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_large.xyz"
20
+ - "${ONESCIENCE_MODELS_DIR}/mace/nanotube_l0_8dcu.model"
21
+
22
+ launch:
23
+ launcher: python
24
+ num_nodes: 1
25
+ num_gpus: 1
26
+
27
+ env:
28
+ conda_env: matchem
29
+ modules:
30
+ - sghpc-mpi-gcc/26.3
31
+
32
+ slurm:
33
+ partition: hpctest01
34
+ time: "2:00:00"
35
+ cpus_per_task: 16
scripts/demo/configs/nanotube_finetune_l0_8dcu.yaml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Nanotube L0 finetuning (8 DCU, single node)
3
+ # ============================================================
4
+ name: nanotube_finetune_l0_8dcu
5
+
6
+ description: "Finetune from nanotube L0 foundation model"
7
+
8
+ train_args:
9
+ foundation_model: "${ONESCIENCE_MODELS_DIR}/mace/nanotube_l0_8dcu.model"
10
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_large.xyz"
11
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_test.xyz"
12
+ valid_fraction: 0.60
13
+ multiheads_finetuning: false
14
+ E0s: average
15
+ energy_key: Energy
16
+ forces_key: forces
17
+
18
+ energy_weight: 1.0
19
+ forces_weight: 1.0
20
+ lr: 0.01
21
+ scaling: rms_forces_scaling
22
+ batch_size: 2
23
+ max_num_epochs: 10
24
+ num_samples_pt: 10
25
+
26
+ ema: true
27
+ swa: true
28
+ ema_decay: 0.99
29
+ amsgrad: true
30
+
31
+ default_dtype: float64
32
+ device: cuda
33
+ seed: 345
34
+
35
+ launch:
36
+ launcher: torchrun
37
+ num_nodes: 1
38
+ num_gpus: 8
39
+
40
+ env:
41
+ conda_env: matchem
42
+ modules:
43
+ - sghpc-mpi-gcc/26.3
44
+
45
+ slurm:
46
+ partition: hpctest01
47
+ time: "2:00:00"
48
+ cpus_per_task: 128
scripts/demo/configs/nanotube_l0_8dcu.yaml ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Nanotube L=0 训练配置
3
+ # 单节点 8 DCU 分布式训练
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: nanotube_l0_8dcu
8
+
9
+ # 实验描述
10
+ description: "Carbon nanotube max_L=0 - 8 DCU 分布式训练"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+ default_dtype: float64 # 数据精度
22
+ num_interactions: 2 # 交互层数
23
+ num_channels: 128 # 通道数
24
+ max_L: 0 # 最大角动量
25
+ correlation: 3 # 相关阶数
26
+
27
+ # --- 数据配置 ---
28
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,下载对应数据集到 ./data 后即可使用
29
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_large.xyz"
30
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_test.xyz"
31
+ valid_fraction: 0.05 # 从训练集中划分 5% 作为验证集
32
+ E0s: average # 原子参考能量方式: average / isolated / 显式字典
33
+ energy_key: Energy # XYZ 文件中能量字段名
34
+ forces_key: forces # XYZ 文件中力字段名
35
+
36
+ # --- 训练超参 ---
37
+ seed: 123 # 随机种子
38
+ device: cuda # 设备: cuda / cpu
39
+ enable_cueq: true # 启用 cuequivariance + FastEq 加速
40
+ r_max: 6.0 # 截断半径 (Angstrom)
41
+ batch_size: 4 # 训练批大小
42
+ valid_batch_size: 8 # 验证批大小
43
+ max_num_epochs: 10 # 最大训练轮数
44
+ forces_weight: 1000 # 力的损失权重
45
+ energy_weight: 10 # 能量的损失权重
46
+ scheduler_patience: 5 # 学习率调度器耐心值
47
+ patience: 15 # 早停耐心值
48
+ eval_interval: 1 # 每 1 个 epoch 评估并打印一次
49
+ error_table: PerAtomMAE # 误差表类型
50
+
51
+ # --- SWA / EMA ---
52
+ ema: true # 启用 EMA
53
+ swa: true # 启用 SWA
54
+ start_swa: 7 # SWA 开始 epoch
55
+ swa_forces_weight: 10 # SWA 力的损失权重
56
+
57
+ # --- 其他 ---
58
+ restart_latest: true # 从最新检查点重启
59
+ save_cpu: true # 保存检查点
60
+
61
+ # ============================================================
62
+ # 启动配置(控制 python / torchrun / srun 的选择)
63
+ # ============================================================
64
+ launch:
65
+ launcher: torchrun # 启动方式: python / torchrun (多节点时自动用 srun)
66
+ num_nodes: 1 # 节点数
67
+ num_gpus: 8 # 申请的 DCU 卡数
68
+
69
+ # ============================================================
70
+ # 环境配置
71
+ # ============================================================
72
+ env:
73
+ conda_env: matchem # conda 环境名
74
+ modules:
75
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
76
+
77
+ # ============================================================
78
+ # SLURM 配置(仅 --submit 模式使用)
79
+ # ============================================================
80
+ slurm:
81
+ partition: hpctest01 # SLURM 分区
82
+ time: "2:00:00" # 作业时间限制
83
+ cpus_per_task: 128 # CPU 核心数
scripts/demo/configs/nanotube_l2_16dcu.yaml ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Nanotube L=2 多节点训练配置
3
+ # 2 节点 x 8 DCU 多节点分布式训练
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: nanotube_l2_16dcu
8
+
9
+ # 实验描述
10
+ description: "Carbon nanotube max_L=2 - 2 节点 x 8 DCU 多节点分布式训练"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+ default_dtype: float64 # 数据精度
22
+ num_interactions: 2 # 交互层数
23
+ num_channels: 256 # 通道数
24
+ max_L: 2 # 最大角动量
25
+ correlation: 3 # 相关阶数
26
+
27
+ # --- 数据配置 ---
28
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,下载对应数据集到 ./data 后即可使用
29
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_large.xyz"
30
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_test.xyz"
31
+ valid_fraction: 0.05 # 从训练集中划分 5% 作为验证集
32
+ E0s: average # 原子参考能量方式: average / isolated / 显式字典
33
+ energy_key: Energy # XYZ 文件中能量字段名
34
+ forces_key: forces # XYZ 文件中力字段名
35
+
36
+ # --- 训练超参 ---
37
+ seed: 123 # 随机种子
38
+ device: cuda # 设备: cuda / cpu
39
+ r_max: 5.0 # 截断半径 (Angstrom)
40
+ batch_size: 2 # 训练批大小 (多节点时适当减小)
41
+ valid_batch_size: 4 # 验证批大小
42
+ max_num_epochs: 10 # 最大训练轮数
43
+ forces_weight: 1000 # 力的损失权重
44
+ energy_weight: 10 # 能量的损失权重
45
+ scheduler_patience: 5 # 学习率调度器耐心值
46
+ patience: 15 # 早停耐心值
47
+ eval_interval: 1 # 每 1 个 epoch 评估并打印一次
48
+ error_table: PerAtomMAE # 误差表类型
49
+
50
+ # --- SWA / EMA ---
51
+ ema: true # 启用 EMA
52
+ swa: true # 启用 SWA
53
+ start_swa: 7 # SWA 开始 epoch
54
+ swa_forces_weight: 10 # SWA 力的损失权重
55
+
56
+ # --- 分布式 ---
57
+ distributed: true # 启用分布式训练
58
+
59
+ # --- 其他 ---
60
+ restart_latest: true # 从最新检查点重启
61
+ save_cpu: true # 保存检查点
62
+
63
+ # ============================================================
64
+ # 启动配置(控制 python / torchrun / srun 的选择)
65
+ # ============================================================
66
+ launch:
67
+ launcher: torchrun # 启动方式: python / torchrun (多节点时自动用 srun)
68
+ num_nodes: 2 # 节点数
69
+ num_gpus: 8 # 每节点申请的 DCU 卡数
70
+
71
+ # ============================================================
72
+ # 环境配置
73
+ # ============================================================
74
+ env:
75
+ conda_env: matchem # conda 环境名
76
+ modules:
77
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
78
+
79
+ # ============================================================
80
+ # 多节点 NCCL 通信配置
81
+ # ============================================================
82
+ nccl:
83
+ socket_ifname: ib0 # NCCL 网络接口
84
+ ib_hca: mlx5_0 # InfiniBand HCA 设备
85
+ proto: Simple # NCCL 协议
86
+
87
+ # ============================================================
88
+ # 自定义环境变量(会覆盖自动生成的变量)
89
+ # ============================================================
90
+ extra_env:
91
+ NCCL_DEBUG: INFO1
92
+ NCCL_DEBUG_SUBSYS: ALL
93
+ NCCL_NET_GDR_LEVEL: 4
94
+ NCCL_NET_GDR_READ: 1
95
+ NCCL_PXN_DISABLE: 0
96
+ RCCL_PXN_GPU_BALANCE: 1
97
+ RCCL_NET_PLANE: "shca_0,shca_3|shca_1,shca_2"
98
+ NCCL_SOCKET_IFNAME: eno1
99
+ HIP_VISIBLE_DEVICES: "0,1,5,4,2,3,7,6"
100
+ NCCL_NET_PLUGIN: shca
101
+ NCCL_PLUGIN_P2P: ib
102
+ NCCL_WORK_FIFO_BYTES1: 4194304
103
+ LD_LIBRARY_PATH: "" # 空值:保持当前 shell 中已有的值
104
+ SHCA_DEBUG_MASK: 0
105
+ NCCL_TOPO_FILE: "" # 空值:保持当前 shell 中已有的值
106
+ UCX_IB_NUM_PATHS: 1
107
+
108
+ # ============================================================
109
+ # SLURM 配置(仅 --submit 模式使用)
110
+ # ============================================================
111
+ slurm:
112
+ partition: hpctest01 # SLURM 分区
113
+ time: "2:00:00" # 作业时间限制
114
+ cpus_per_task: 128 # CPU 核心数
scripts/demo/configs/nanotube_l2_8dcu.yaml ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Nanotube L=2 训练配置
3
+ # 单节点 8 DCU 分布式训练
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: nanotube_l2_8dcu
8
+
9
+ # 实验描述
10
+ description: "Carbon nanotube max_L=2 - 8 DCU 分布式训练"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+ default_dtype: float64 # 数据精度
22
+ num_interactions: 2 # 交互层数
23
+ num_channels: 128 # 通道数
24
+ max_L: 2 # 最大角动量
25
+ correlation: 3 # 相关阶数
26
+
27
+ # --- 数据配置 ---
28
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,下载对应数据集到 ./data 后即可使用
29
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_large.xyz"
30
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_test.xyz"
31
+ valid_fraction: 0.05 # 从训练集中划分 5% 作为验证集
32
+ E0s: average # 原子参考能量方式: average / isolated / 显式字典
33
+ energy_key: Energy # XYZ 文件中能量字段名
34
+ forces_key: forces # XYZ 文件中力字段名
35
+
36
+ # --- 训练超参 ---
37
+ seed: 123 # 随机种子
38
+ device: cuda # 设备: cuda / cpu
39
+ r_max: 5.0 # 截断半径 (Angstrom)
40
+ batch_size: 2 # 训练批大小
41
+ valid_batch_size: 8 # 验证批大小
42
+ max_num_epochs: 10 # 最大训练轮数
43
+ forces_weight: 1000 # 力的损失权重
44
+ energy_weight: 10 # 能量的损失权重
45
+ scheduler_patience: 5 # 学习率调度器耐心值
46
+ patience: 15 # 早停耐心值
47
+ eval_interval: 1 # 每 1 个 epoch 评估并打印一次
48
+ error_table: PerAtomMAE # 误差表类型
49
+
50
+ # --- SWA / EMA ---
51
+ ema: true # 启用 EMA
52
+ swa: true # 启用 SWA
53
+ start_swa: 7 # SWA 开始 epoch
54
+ swa_forces_weight: 10 # SWA 力的损失权重
55
+
56
+ # --- 其他 ---
57
+ restart_latest: true # 从最新检查点重启
58
+ save_cpu: true # 保存检查点
59
+
60
+ # ============================================================
61
+ # 启动配置(控制 python / torchrun / srun 的选择)
62
+ # ============================================================
63
+ launch:
64
+ launcher: torchrun # 启动方式: python / torchrun (多节点时自动用 srun)
65
+ num_nodes: 1 # 节点数
66
+ num_gpus: 8 # 申请的 DCU 卡数
67
+
68
+ # ============================================================
69
+ # 环境配置
70
+ # ============================================================
71
+ env:
72
+ conda_env: matchem # conda 环境名
73
+ modules:
74
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
75
+
76
+ # ============================================================
77
+ # 自定义环境变量(会覆盖自动生成的变量)
78
+ # ============================================================
79
+ extra_env:
80
+ NCCL_DEBUG: INFO1
81
+ NCCL_DEBUG_SUBSYS: ALL
82
+ NCCL_NET_GDR_LEVEL: 4
83
+ NCCL_NET_GDR_READ: 1
84
+ NCCL_PXN_DISABLE: 0
85
+ RCCL_PXN_GPU_BALANCE: 1
86
+ RCCL_NET_PLANE: "shca_0,shca_3|shca_1,shca_2"
87
+ NCCL_SOCKET_IFNAME: eno1
88
+ HIP_VISIBLE_DEVICES: "0,1,5,4,2,3,7,6"
89
+ NCCL_NET_PLUGIN: shca
90
+ NCCL_PLUGIN_P2P: ib
91
+ NCCL_WORK_FIFO_BYTES1: 4194304
92
+ LD_LIBRARY_PATH: "" # 空值:保持当前 shell 中已有的值
93
+ SHCA_DEBUG_MASK: 0
94
+ NCCL_TOPO_FILE: "" # 空值:保持当前 shell 中已有的值
95
+ UCX_IB_NUM_PATHS: 1
96
+
97
+ # ============================================================
98
+ # SLURM 配置(仅 --submit 模式使用)
99
+ # ============================================================
100
+ slurm:
101
+ partition: hpctest02 # SLURM 分区
102
+ time: "8:00:00" # 作业时间限制
103
+ cpus_per_task: 128 # CPU 核心数
scripts/demo/configs/nanotube_run_md_l0_1dcu.yaml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Nanotube L0 MD inference (single DCU)
3
+ # ============================================================
4
+ name: nanotube_run_md_l0_1dcu
5
+
6
+ description: "Run MD using trained nanotube L0 model"
7
+
8
+ entrypoint:
9
+ script: run_md.py
10
+ args:
11
+ init_conf: "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_init.xyz"
12
+ model: "${ONESCIENCE_MODELS_DIR}/mace/nanotube_l0_8dcu.model"
13
+ output_xyz: "md_result.xyz"
14
+ plot_file: "md_plot.png"
15
+ device: cuda
16
+ default_dtype: float32
17
+ temperature_k: 1200
18
+ time_step_fs: 1.0
19
+ friction: 0.1
20
+ save_interval: 10
21
+ steps: 1000
22
+ default_dtype: float64
23
+ data_files:
24
+ - "${ONESCIENCE_DATASETS_DIR}/data/nanotube/nanotube_init.xyz"
25
+ - "${ONESCIENCE_MODELS_DIR}/mace/nanotube_l0_8dcu.model"
26
+
27
+ launch:
28
+ launcher: python
29
+ num_nodes: 1
30
+ num_gpus: 1
31
+
32
+ env:
33
+ conda_env: matchem
34
+ modules:
35
+ - sghpc-mpi-gcc/26.3
36
+
37
+ slurm:
38
+ partition: hpctest01
39
+ time: "2:00:00"
40
+ cpus_per_task: 16
scripts/demo/configs/water_1dcu.yaml ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Water 单 DCU 训练配置
3
+ # 单节点单卡训练
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: water_1dcu
8
+
9
+ # 实验描述
10
+ description: "Water 势能面 - 单 DCU 训练"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+ default_dtype: float64 # 数据精度
22
+ num_interactions: 2 # 交互层数
23
+ num_channels: 64 # 通道数
24
+ max_L: 0 # 最大角动量
25
+ correlation: 3 # 相关阶数
26
+
27
+ # --- 数据配置 ---
28
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,下载对应数据集到 ./data 后即可使用
29
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_train.xyz"
30
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_test.xyz"
31
+ valid_fraction: 0.05 # 从训练集中划分 5% 作为验证集
32
+ E0s: isolated # 原子参考能量方式: average / isolated / 显式字典
33
+ energy_key: TolEnergy # XYZ 文件中能量字段名
34
+ forces_key: force # XYZ 文件中力字段名
35
+
36
+ # --- 训练超参 ---
37
+ seed: 123 # 随机种子
38
+ device: cuda # 设备: cuda / cpu
39
+ r_max: 6.0 # 截断半径 (Angstrom)
40
+ batch_size: 2 # 训练批大小
41
+ valid_batch_size: 4 # 验证批大小
42
+ max_num_epochs: 10 # 最大训练轮数
43
+ forces_weight: 1000 # 力的损失权重
44
+ energy_weight: 10 # 能量的损失权重
45
+ scheduler_patience: 15 # 学习率调度器耐心值
46
+ patience: 30 # 早停耐心值
47
+ eval_interval: 1 # 每 1 个 epoch 评估并打印一次
48
+ error_table: PerAtomMAE # 误差表类型
49
+
50
+ # --- SWA / EMA ---
51
+ ema: true # 启用 EMA
52
+ swa: true # 启用 SWA
53
+
54
+ # --- 其他 ---
55
+ restart_latest: true # 从最新检查点重启
56
+ save_cpu: true # 保存检查点
57
+
58
+ # ============================================================
59
+ # 启动配置(控制 python / torchrun / srun 的选择)
60
+ # ============================================================
61
+ launch:
62
+ launcher: python # 启动方式: python / torchrun (多节点时自动用 srun)
63
+ num_nodes: 1 # 节点数
64
+ num_gpus: 1 # 申请的 DCU 卡数
65
+
66
+ # ============================================================
67
+ # 环境配置
68
+ # ============================================================
69
+ env:
70
+ conda_env: matchem # conda 环境名
71
+ modules:
72
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
73
+
74
+ # ============================================================
75
+ # SLURM 配置(仅 --submit 模式使用)
76
+ # ============================================================
77
+ slurm:
78
+ partition: hpctest02 # SLURM 分区
79
+ time: "8:00:00" # 作业时间限制
80
+ cpus_per_task: 16 # CPU 核心数 (128 / 8 DCU = 16)
scripts/demo/configs/water_4dcu.yaml ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Water 4 DCU 训练配置
3
+ # 单节点 4 DCU 分布式训练
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: water_4dcu
8
+
9
+ # 实验描述
10
+ description: "Water 势能面 - 4 DCU 分布式训练"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+ default_dtype: float64 # 数据精度
22
+ num_interactions: 2 # 交互层数
23
+ num_channels: 64 # 通道数
24
+ max_L: 0 # 最大角动量
25
+ correlation: 3 # 相关阶数
26
+
27
+ # --- 数据配置 ---
28
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,下载对应数据集到 ./data 后即可使用
29
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_train.xyz"
30
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_test.xyz"
31
+ valid_fraction: 0.05 # 从训练集中划分 5% 作为验证集
32
+ E0s: isolated # 原子参考能量方式: average / isolated / 显式字典
33
+ energy_key: TolEnergy # XYZ 文件中能量字段名
34
+ forces_key: force # XYZ 文件中力字段名
35
+
36
+ # --- 训练超参 ---
37
+ seed: 123 # 随机种子
38
+ device: cuda # 设备: cuda / cpu
39
+ r_max: 6.0 # 截断半径 (Angstrom)
40
+ batch_size: 2 # 训练批大小
41
+ valid_batch_size: 4 # 验证批大小
42
+ max_num_epochs: 20 # 最大训练轮数
43
+ forces_weight: 1000 # 力的损失权重
44
+ energy_weight: 10 # 能量的损失权重
45
+ scheduler_patience: 15 # 学习率调度器耐心值
46
+ patience: 30 # 早停耐心值
47
+ eval_interval: 1 # 每 1 个 epoch 评估并打印一次
48
+ error_table: PerAtomMAE # 误差表类型
49
+
50
+ # --- SWA / EMA ---
51
+ ema: true # 启用 EMA
52
+ swa: true # 启用 SWA
53
+ start_swa: 15 # SWA 开始 epoch
54
+
55
+ # --- 其他 ---
56
+ restart_latest: true # 从最新检查点重启
57
+ save_cpu: true # 保存检查点
58
+
59
+ # ============================================================
60
+ # 启动配置(控制 python / torchrun / srun 的选择)
61
+ # ============================================================
62
+ launch:
63
+ launcher: torchrun # 启动方式: python / torchrun (多节点时自动用 srun)
64
+ num_nodes: 1 # 节点数
65
+ num_gpus: 4 # 申请的 DCU 卡数
66
+
67
+ # ============================================================
68
+ # 环境配置
69
+ # ============================================================
70
+ env:
71
+ conda_env: matchem # conda 环境名
72
+ modules:
73
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
74
+
75
+ # ============================================================
76
+ # SLURM 配置(仅 --submit 模式使用)
77
+ # ============================================================
78
+ slurm:
79
+ partition: hpctest02 # SLURM 分区
80
+ time: "8:00:00" # 作业时间限制
81
+ cpus_per_task: 64 # CPU 核心数
scripts/demo/configs/water_8dcu.yaml ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Water 8 DCU 训练配置
3
+ # 单节点 8 DCU 分布式训练
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: water_8dcu
8
+
9
+ # 实验描述
10
+ description: "Water 势能面 - 8 DCU 分布式训练"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # 例如 model: MACE -> --model=MACE
16
+ # swa: true -> --swa (布尔标志)
17
+ # ============================================================
18
+ train_args:
19
+ # --- 模型配置 ---
20
+ model: MACE # 模型类型
21
+ default_dtype: float64 # 数据精度
22
+ num_interactions: 2 # 交互层数
23
+ num_channels: 64 # 通道数
24
+ max_L: 0 # 最大角动量
25
+ correlation: 3 # 相关阶数
26
+
27
+ # --- 数据配置 ---
28
+ # 路径中的 ${ONESCIENCE_DATASETS_DIR} 默认指向仓库根目录,下载对应数据集到 ./data 后即可使用
29
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_train.xyz"
30
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_test.xyz"
31
+ valid_fraction: 0.05 # 从训练集中划分 5% 作为验证集
32
+ E0s: isolated # 原子参考能量方式: average / isolated / 显式字典
33
+ energy_key: TolEnergy # XYZ 文件中能量字段名
34
+ forces_key: force # XYZ 文件中力字段名
35
+
36
+ # --- 训练超参 ---
37
+ seed: 123 # 随机种子
38
+ device: cuda # 设备: cuda / cpu
39
+ r_max: 6.0 # 截断半径 (Angstrom)
40
+ batch_size: 2 # 训练批大小
41
+ valid_batch_size: 4 # 验证批大小
42
+ max_num_epochs: 20 # 最大训练轮数
43
+ forces_weight: 1000 # 力的损失权重
44
+ energy_weight: 10 # 能量的损失权重
45
+ scheduler_patience: 15 # 学习率调度器耐心值
46
+ patience: 30 # 早停耐心值
47
+ eval_interval: 1 # 每 1 个 epoch 评估并打印一次
48
+ error_table: PerAtomMAE # 误差表类型
49
+
50
+ # --- SWA / EMA ---
51
+ ema: true # 启用 EMA
52
+ swa: true # 启用 SWA
53
+ start_swa: 15 # SWA 开始 epoch
54
+
55
+ # --- 其他 ---
56
+ restart_latest: true # 从最新检查点重启
57
+ save_cpu: true # 保存检查点
58
+
59
+ # ============================================================
60
+ # 启动配置(控制 python / torchrun / srun 的选择)
61
+ # ============================================================
62
+ launch:
63
+ launcher: torchrun # 启动方式: python / torchrun (多节点时自动用 srun)
64
+ num_nodes: 1 # 节点数
65
+ num_gpus: 8 # 申请的 DCU 卡数
66
+
67
+ # ============================================================
68
+ # 环境配置
69
+ # ============================================================
70
+ env:
71
+ conda_env: matchem # conda 环境名
72
+ modules:
73
+ - sghpc-mpi-gcc/26.3 # 需要加载的 module
74
+
75
+ # ============================================================
76
+ # SLURM 配置(仅 --submit 模式使用)
77
+ # ============================================================
78
+ slurm:
79
+ partition: hpctest02 # SLURM 分区
80
+ time: "8:00:00" # 作业时间限制
81
+ cpus_per_task: 128 # CPU 核心数
scripts/demo/configs/water_finetune_mp0_8dcu.yaml ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Water 8 DCU 微调配置 —— 基于 MACE-MP-0 基础模型
3
+ # 单节点 8 DCU 分布式微调
4
+ # ============================================================
5
+
6
+ # 实验名称(用于输出目录和模型文件命名)
7
+ name: water_finetune_mp0_8dcu
8
+
9
+ # 实验描述
10
+ description: "Water 势能面 - 基于 MACE-MP-0 (medium) 微调 - 8 DCU"
11
+
12
+ # ============================================================
13
+ # train.py 参数
14
+ # 以下所有字段将被转换为 train.py 的命令行参数
15
+ # ============================================================
16
+ train_args:
17
+ # --- 基础模型配置 ---
18
+ # 使用 mace-mp-0 medium 预训练模型(自动下载或加载本地缓存)
19
+ # 可选: small / medium / large / medium-mpa-0 / medium-omat-0 等
20
+ foundation_model: medium
21
+
22
+ # 保留基础模型的所有元素(不仅限于当前数据集出现的元素)
23
+ # 设置为 true 可防止元素嵌入被截断,有利于泛化
24
+ foundation_model_elements: true
25
+
26
+ # --- 模型配置 ---
27
+ # 当指定 foundation_model 时,以下参数通常会自动从基础模型继承
28
+ # 若需覆盖,可取消注释并修改
29
+ # model: MACE
30
+ # default_dtype: float64
31
+ # num_channels: 128
32
+ # max_L: 1
33
+ # num_interactions: 2
34
+ # correlation: 3
35
+
36
+ # --- 数据配置 ---
37
+ train_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_train.xyz"
38
+ test_file: "${ONESCIENCE_DATASETS_DIR}/data/water/water_test.xyz"
39
+ valid_fraction: 0.05
40
+ E0s: isolated
41
+ energy_key: TolEnergy
42
+ forces_key: force
43
+
44
+ # --- 训练超参(微调建议) ---
45
+ # 微调通常使用较小的学习率和较少的 epoch
46
+ seed: 123
47
+ device: cuda
48
+ r_max: 6.0
49
+ batch_size: 2
50
+ valid_batch_size: 4
51
+
52
+ # 微调 epoch 数通常较少(基础模型已具备先验知识)
53
+ max_num_epochs: 50
54
+
55
+ # 损失权重
56
+ forces_weight: 1000
57
+ energy_weight: 10
58
+
59
+ # 学习率相关(微调建议使用较小学习率)
60
+ lr: 0.001
61
+ scheduler_patience: 10
62
+ patience: 20
63
+ eval_interval: 1
64
+ error_table: PerAtomMAE
65
+
66
+ # --- SWA / EMA ---
67
+ ema: true
68
+ swa: true
69
+ start_swa: 35
70
+
71
+ # --- 其他 ---
72
+ restart_latest: true
73
+ save_cpu: true
74
+
75
+ # ============================================================
76
+ # 启动配置
77
+ # ============================================================
78
+ launch:
79
+ launcher: torchrun
80
+ num_nodes: 1
81
+ num_gpus: 8
82
+
83
+ # ============================================================
84
+ # 环境配置
85
+ # ============================================================
86
+ env:
87
+ conda_env: matchem
88
+ modules:
89
+ - sghpc-mpi-gcc/26.3
90
+
91
+ # ============================================================
92
+ # SLURM 配置(仅 --submit 模式使用)
93
+ # ============================================================
94
+ slurm:
95
+ partition: hpctest02
96
+ time: "4:00:00"
97
+ cpus_per_task: 128
scripts/demo/run.sh ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # run.sh - MACE 训练统一入口脚本
3
+ #
4
+ # 用法:
5
+ # bash run.sh --config configs/DMC.yaml # 直接运行训练
6
+ # bash run.sh --config configs/DMC.yaml --submit # 提交 SLURM 作业
7
+ # bash run.sh --config configs/DMC.yaml --dry-run # 仅打印命令,不执行
8
+ #
9
+ set -euo pipefail
10
+
11
+ # ============================================================
12
+ # 参数解析
13
+ # ============================================================
14
+ CONFIG=""
15
+ SUBMIT=false
16
+ DRY_RUN=false
17
+
18
+ while [[ $# -gt 0 ]]; do
19
+ case "$1" in
20
+ --config)
21
+ CONFIG="$2"; shift 2 ;;
22
+ --config=*)
23
+ CONFIG="${1#*=}"; shift ;;
24
+ --submit)
25
+ SUBMIT=true; shift ;;
26
+ --dry-run)
27
+ DRY_RUN=true; shift ;;
28
+ -h|--help)
29
+ echo "用法: bash run.sh --config <config.yaml> [--submit] [--dry-run]"
30
+ echo ""
31
+ echo "选项:"
32
+ echo " --config <file> YAML 配置文件路径 (必需)"
33
+ echo " --submit 生成 SLURM 脚本并提交作业"
34
+ echo " --dry-run 仅打印训练命令,不执行"
35
+ exit 0 ;;
36
+ *)
37
+ echo "[ERROR] 未知参数: $1"
38
+ exit 1 ;;
39
+ esac
40
+ done
41
+
42
+ if [ -z "$CONFIG" ]; then
43
+ echo "[ERROR] 请指定配置文件: bash run.sh --config configs/xxx.yaml"
44
+ exit 1
45
+ fi
46
+
47
+ if [ ! -f "$CONFIG" ]; then
48
+ echo "[ERROR] 配置文件不存在: $CONFIG"
49
+ exit 1
50
+ fi
51
+
52
+ # ============================================================
53
+ # 路径设置
54
+ # ============================================================
55
+ DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
56
+ REPO_ROOT="$(cd "$DEMO_DIR/../.." && pwd)"
57
+ CONFIG_ABS="$(cd "$(dirname "$CONFIG")" && pwd)/$(basename "$CONFIG")"
58
+ PARSE_PY="$DEMO_DIR/_parse_config.py"
59
+
60
+ # 如果用户没有设置 ONESCIENCE_DATASETS_DIR,默认指向仓库根目录
61
+ export ONESCIENCE_DATASETS_DIR="${ONESCIENCE_DATASETS_DIR:-$REPO_ROOT}"
62
+
63
+ # ============================================================
64
+ # 解析配置
65
+ # ============================================================
66
+ EXP_NAME=$(python3 "$PARSE_PY" "$CONFIG_ABS" name)
67
+ TRAIN_CMD=$(python3 "$PARSE_PY" "$CONFIG_ABS" command)
68
+ ENV_EXPORTS=$(python3 "$PARSE_PY" "$CONFIG_ABS" env)
69
+ ENV_ARGS=$(python3 "$PARSE_PY" "$CONFIG_ABS" env-args)
70
+ DATA_FILES=$(python3 "$PARSE_PY" "$CONFIG_ABS" data-files)
71
+
72
+ # ============================================================
73
+ # Dry-run 模式
74
+ # ============================================================
75
+ if $DRY_RUN; then
76
+ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
77
+ OUTPUT_DIR="$DEMO_DIR/outputs/${EXP_NAME}_${TIMESTAMP}"
78
+ echo "========================================="
79
+ echo "Dry-run: $EXP_NAME"
80
+ echo "Config: $CONFIG_ABS"
81
+ echo "Output: $OUTPUT_DIR (未创建)"
82
+ echo "========================================="
83
+ echo ""
84
+ echo "# 环境变量:"
85
+ echo "$ENV_EXPORTS"
86
+ echo ""
87
+ echo "# 训练命令:"
88
+ echo "$TRAIN_CMD"
89
+ echo ""
90
+ echo "# env_setup.sh 参数: $ENV_ARGS"
91
+ echo "# 数据文件:"
92
+ echo "$DATA_FILES" | sed 's/^/# /'
93
+ exit 0
94
+ fi
95
+
96
+ # ============================================================
97
+ # 创建输出目录
98
+ # ============================================================
99
+ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
100
+ OUTPUT_DIR="$DEMO_DIR/outputs/${EXP_NAME}_${TIMESTAMP}"
101
+ mkdir -p "$OUTPUT_DIR"
102
+ cp "$CONFIG_ABS" "$OUTPUT_DIR/config.yaml"
103
+
104
+ # ============================================================
105
+ # SLURM 提交模式
106
+ # ============================================================
107
+ if $SUBMIT; then
108
+ SLURM_VARS=$(python3 "$PARSE_PY" "$CONFIG_ABS" slurm)
109
+ eval "$SLURM_VARS"
110
+
111
+ # 检查是否是多节点
112
+ SLURM_SCRIPT="$OUTPUT_DIR/submit.sh"
113
+
114
+ # 生成 SLURM header
115
+ sed -e "s|{{JOB_NAME}}|${JOB_NAME}|g" \
116
+ -e "s|{{PARTITION}}|${PARTITION}|g" \
117
+ -e "s|{{NODES}}|${NODES}|g" \
118
+ -e "s|{{NTASKS_PER_NODE}}|${NTASKS_PER_NODE}|g" \
119
+ -e "s|{{CPUS_PER_TASK}}|${CPUS_PER_TASK}|g" \
120
+ -e "s|{{GPUS_PER_NODE}}|${GPUS_PER_NODE}|g" \
121
+ -e "s|{{TIME}}|${TIME}|g" \
122
+ "$DEMO_DIR/templates/slurm_header.template" > "$SLURM_SCRIPT"
123
+
124
+ # 添加环境初始化
125
+ cat >> "$SLURM_SCRIPT" << 'SETUP_BLOCK'
126
+
127
+ # 环境初始化
128
+ SETUP_BLOCK
129
+ cat >> "$SLURM_SCRIPT" << 'ENV_BLOCK'
130
+ set +u
131
+ if [ -n "${MACE_ENV_SCRIPT:-}" ] && [ -f "${MACE_ENV_SCRIPT}" ]; then
132
+ source "${MACE_ENV_SCRIPT}"
133
+ else
134
+ echo "[WARN] MACE_ENV_SCRIPT 未设置或文件不存在,跳过环境初始化。请自行确保 conda/matchem 环境已激活。"
135
+ fi
136
+ set -u
137
+ ENV_BLOCK
138
+
139
+ # 添加预检
140
+ echo "" >> "$SLURM_SCRIPT"
141
+ echo "# 预检" >> "$SLURM_SCRIPT"
142
+ # 将数据文件列表转为参数
143
+ DATA_ARGS=""
144
+ while IFS= read -r line; do
145
+ [ -z "$line" ] && continue
146
+ DATA_ARGS="$DATA_ARGS \"$line\""
147
+ done <<< "$DATA_FILES"
148
+ echo "bash $DEMO_DIR/templates/preflight_check.sh $DATA_ARGS" >> "$SLURM_SCRIPT"
149
+
150
+ # 添加环境变量和训练命令
151
+ echo "" >> "$SLURM_SCRIPT"
152
+ echo "# 工作目录" >> "$SLURM_SCRIPT"
153
+ echo "cd $OUTPUT_DIR" >> "$SLURM_SCRIPT"
154
+ echo "" >> "$SLURM_SCRIPT"
155
+ echo "# 环境变量" >> "$SLURM_SCRIPT"
156
+ echo "$ENV_EXPORTS" >> "$SLURM_SCRIPT"
157
+ echo "" >> "$SLURM_SCRIPT"
158
+ echo "# 将仓库根目录加入 PYTHONPATH,确保能 import 本地 model 包" >> "$SLURM_SCRIPT"
159
+ echo "export PYTHONPATH=\"$REPO_ROOT:\${PYTHONPATH:-}\"" >> "$SLURM_SCRIPT"
160
+ echo "" >> "$SLURM_SCRIPT"
161
+ echo "# 屏蔽 e3nn FutureWarning 和 TorchScript UserWarning" >> "$SLURM_SCRIPT"
162
+ echo 'export PYTHONWARNINGS="ignore::FutureWarning:e3nn.o3._wigner,ignore::UserWarning:torch.jit._check"' >> "$SLURM_SCRIPT"
163
+ echo "" >> "$SLURM_SCRIPT"
164
+ echo '# 屏蔽 PyTorch NCCL C++ INFO 日志' >> "$SLURM_SCRIPT"
165
+ echo 'export TORCH_CPP_LOG_LEVEL=WARNING' >> "$SLURM_SCRIPT"
166
+ echo 'export NCCL_DEBUG=ERROR' >> "$SLURM_SCRIPT"
167
+ echo '' >> "$SLURM_SCRIPT"
168
+ echo '# 屏蔽 glog INFO(ProcessGroupNCCL.cpp 初始化信息)' >> "$SLURM_SCRIPT"
169
+ echo 'export GLOG_minloglevel=1' >> "$SLURM_SCRIPT"
170
+ echo '' >> "$SLURM_SCRIPT"
171
+ echo '# AMD DCU: 避免 RCCL "Missing HSA_FORCE_FINE_GRAIN_PCIE" 警告' >> "$SLURM_SCRIPT"
172
+ echo 'export HSA_FORCE_FINE_GRAIN_PCIE=1' >> "$SLURM_SCRIPT"
173
+
174
+ # 多节点特殊处理
175
+ if [ "$NODES" -gt 1 ]; then
176
+ cat >> "$SLURM_SCRIPT" << 'MULTI_NODE'
177
+
178
+ # 多节点分布式设置
179
+ export MASTER_ADDR=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)
180
+ export MASTER_PORT=29517
181
+ export WORLD_SIZE=$SLURM_NTASKS
182
+
183
+ echo "MASTER_ADDR: $MASTER_ADDR"
184
+ echo "WORLD_SIZE: $WORLD_SIZE"
185
+
186
+ # 使用 srun 启动分布式训练
187
+ MULTI_NODE
188
+ # srun 包裹训练命令
189
+ echo "srun --export=ALL bash -c '" >> "$SLURM_SCRIPT"
190
+ echo " export RANK=\$SLURM_PROCID" >> "$SLURM_SCRIPT"
191
+ echo " export LOCAL_RANK=\$SLURM_LOCALID" >> "$SLURM_SCRIPT"
192
+ echo " exec $TRAIN_CMD" >> "$SLURM_SCRIPT"
193
+ echo "'" >> "$SLURM_SCRIPT"
194
+ else
195
+ echo "" >> "$SLURM_SCRIPT"
196
+ echo "# 训练命令" >> "$SLURM_SCRIPT"
197
+ echo "$TRAIN_CMD" >> "$SLURM_SCRIPT"
198
+ fi
199
+
200
+ echo "========================================="
201
+ echo "SLURM 脚本已生成: $SLURM_SCRIPT"
202
+ echo "配置快照已保存: $OUTPUT_DIR/config.yaml"
203
+ echo "========================================="
204
+ echo ""
205
+ echo "提交作业..."
206
+ sbatch "$SLURM_SCRIPT"
207
+ exit 0
208
+ fi
209
+
210
+ # ============================================================
211
+ # 直接运行模式
212
+ # ============================================================
213
+ echo "========================================="
214
+ echo "实验: $EXP_NAME"
215
+ echo "配置: $CONFIG_ABS"
216
+ echo "输出: $OUTPUT_DIR"
217
+ echo "========================================="
218
+
219
+ # 加载用户环境初始化脚本(路径通过 MACE_ENV_SCRIPT 指定)
220
+ set +u
221
+ if [ -n "${MACE_ENV_SCRIPT:-}" ] && [ -f "${MACE_ENV_SCRIPT}" ]; then
222
+ source "${MACE_ENV_SCRIPT}"
223
+ else
224
+ echo "[WARN] MACE_ENV_SCRIPT 未设置或文件不存在,跳过环境初始化。请自行确保 conda/matchem 环境已激活。"
225
+ fi
226
+ set -u
227
+
228
+ # 预检
229
+ DATA_ARGS=""
230
+ while IFS= read -r line; do
231
+ [ -z "$line" ] && continue
232
+ DATA_ARGS="$DATA_ARGS \"$line\""
233
+ done <<< "$DATA_FILES"
234
+ eval "bash $DEMO_DIR/templates/preflight_check.sh $DATA_ARGS"
235
+
236
+ # 设置环境变量
237
+ eval "$ENV_EXPORTS"
238
+
239
+ # 将仓库根目录加入 PYTHONPATH,确保能 import 本地 model 包
240
+ export PYTHONPATH="$REPO_ROOT:${PYTHONPATH:-}"
241
+
242
+ # 屏蔽 e3nn FutureWarning 和 TorchScript UserWarning
243
+ export PYTHONWARNINGS="ignore::FutureWarning:e3nn.o3._wigner,ignore::UserWarning:torch.jit._check"
244
+
245
+ # 屏蔽 PyTorch NCCL C++ INFO 日志
246
+ export TORCH_CPP_LOG_LEVEL=WARNING
247
+ export NCCL_DEBUG=ERROR
248
+
249
+ # 屏蔽 glog INFO(ProcessGroupNCCL.cpp 初始化信息)
250
+ export GLOG_minloglevel=1
251
+
252
+ # AMD DCU: 避免 RCCL "Missing HSA_FORCE_FINE_GRAIN_PCIE" 警告
253
+ export HSA_FORCE_FINE_GRAIN_PCIE=1
254
+
255
+ # 切换到输出目录
256
+ cd "$OUTPUT_DIR"
257
+
258
+ # 执行训练
259
+ echo "========================================="
260
+ echo "开始训练..."
261
+ echo "========================================="
262
+ if [ -n "${TRAIN_CMD:-}" ]; then
263
+ eval "$TRAIN_CMD"
264
+ else
265
+ echo "[FATAL] TRAIN_CMD 为空,无法执行任务"
266
+ exit 1
267
+ fi
scripts/demo/templates/env_setup.sh ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # env_setup.sh - 公共环境初始化脚本
3
+ # 用法: source templates/env_setup.sh <conda_env> <module1> [module2 ...]
4
+ #
5
+ # 参数:
6
+ # $1 - conda 环境名 (如 chem)
7
+ # $2 ... - 需要加载的 module 列表
8
+
9
+ CONDA_ENV="${1:?请指定 conda 环境名}"
10
+ shift
11
+
12
+ # 系统脚本 (bashrc, conda, module) 可能引用未初始化变量或返回非零退出码,
13
+ # 临时关闭 errexit 和 nounset 避免报错
14
+ set +eu
15
+
16
+ # 清理环境
17
+ module purge
18
+ source ~/.bashrc
19
+
20
+ # 激活 conda 环境
21
+ conda activate "$CONDA_ENV"
22
+
23
+
24
+ # 加载指定 modules
25
+ for mod in "$@"; do
26
+ module load "$mod"
27
+ done
28
+
29
+ # 恢复 errexit 和 nounset
30
+ set -eu
31
+
32
+ # 加载数据集路径
33
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+ DEMO_DIR="$(dirname "$SCRIPT_DIR")"
35
+ REPO_ROOT="$(cd "$DEMO_DIR/../.." && pwd)"
36
+ if [ -f "$REPO_ROOT/env.sh" ]; then
37
+ source "$REPO_ROOT/env.sh"
38
+ fi
scripts/demo/templates/preflight_check.sh ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # preflight_check.sh - 训练前预检脚本
3
+ # 用法: bash templates/preflight_check.sh [data_file1] [data_file2] ...
4
+ #
5
+ # 检查项:
6
+ # 1. ONESCIENCE_DATASETS_DIR 环境变量
7
+ # 2. 数据文件是否存在
8
+ # 3. DCU/GPU 状态
9
+ # 4. 打印节点信息
10
+
11
+ ERRORS=0
12
+
13
+ echo "========================================="
14
+ echo "Node: $(hostname)"
15
+ echo "Time: $(date)"
16
+ echo "========================================="
17
+
18
+ # 检查数据集目录环境变量
19
+ if [ -z "${ONESCIENCE_DATASETS_DIR:-}" ]; then
20
+ echo "[ERROR] ONESCIENCE_DATASETS_DIR 未设置,请先 source env.sh"
21
+ ERRORS=$((ERRORS + 1))
22
+ fi
23
+
24
+ # 检查数据文件是否存在
25
+ for f in "$@"; do
26
+ # 跳过空参数
27
+ [ -z "$f" ] && continue
28
+ # 展开环境变量后检查
29
+ expanded=$(eval echo "$f")
30
+ if [ ! -e "$expanded" ]; then
31
+ echo "[ERROR] 数据文件不存在: $expanded"
32
+ ERRORS=$((ERRORS + 1))
33
+ else
34
+ echo "[OK] 数据文件: $expanded"
35
+ fi
36
+ done
37
+
38
+ # 打印 DCU/GPU 状态
39
+ echo "-----------------------------------------"
40
+ echo ">>> DCU/GPU 状态:"
41
+ if command -v hy-smi &>/dev/null; then
42
+ hy-smi
43
+ elif command -v rocm-smi &>/dev/null; then
44
+ rocm-smi
45
+ elif command -v nvidia-smi &>/dev/null; then
46
+ nvidia-smi
47
+ else
48
+ echo "[WARN] 未找到 GPU 监控工具 (hy-smi/rocm-smi/nvidia-smi)"
49
+ fi
50
+ echo "-----------------------------------------"
51
+
52
+ if [ $ERRORS -gt 0 ]; then
53
+ echo "[FATAL] 预检发现 $ERRORS 个错误,终止训练"
54
+ exit 1
55
+ fi
56
+
57
+ echo "[OK] 预检通过"
scripts/demo/templates/slurm_header.template ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH -J {{JOB_NAME}}
3
+ #SBATCH -p {{PARTITION}}
4
+ #SBATCH --nodes={{NODES}}
5
+ #SBATCH --ntasks-per-node={{NTASKS_PER_NODE}}
6
+ #SBATCH --cpus-per-task={{CPUS_PER_TASK}}
7
+ #SBATCH --gres=dcu:{{GPUS_PER_NODE}}
8
+ #SBATCH --output=%j.log
9
+ #SBATCH --time={{TIME}}
scripts/eval_configs.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Script for evaluating configurations contained in an xyz file with a trained model
2
+
3
+ import time
4
+ import argparse
5
+
6
+ import ase.io
7
+ import numpy as np
8
+ import torch
9
+ from onescience.datapipes.materials.pyg_stack.core.utils import config_from_atoms
10
+ from onescience.datapipes.materials.pyg_stack.core.atomic_data import AtomicData
11
+ from onescience.datapipes.materials.tools import torch_geometric, torch_tools, utils
12
+
13
+
14
+ def parse_args() -> argparse.Namespace:
15
+ parser = argparse.ArgumentParser(
16
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
17
+ )
18
+ parser.add_argument("--configs", help="path to XYZ configurations", required=True)
19
+ parser.add_argument("--model", help="path to model", required=True)
20
+ parser.add_argument("--output", help="output path", required=True)
21
+ parser.add_argument(
22
+ "--device",
23
+ help="select device",
24
+ type=str,
25
+ choices=["cpu", "cuda"],
26
+ default="cpu",
27
+ )
28
+ parser.add_argument(
29
+ "--default_dtype",
30
+ help="set default dtype",
31
+ type=str,
32
+ choices=["float32", "float64"],
33
+ default="float64",
34
+ )
35
+ parser.add_argument("--batch_size", help="batch size", type=int, default=64)
36
+ parser.add_argument(
37
+ "--compute_stress",
38
+ help="compute stress",
39
+ action="store_true",
40
+ default=False,
41
+ )
42
+ parser.add_argument(
43
+ "--return_contributions",
44
+ help="model outputs energy contributions for each body order, only supported for MACE, not ScaleShiftMACE",
45
+ action="store_true",
46
+ default=False,
47
+ )
48
+ parser.add_argument(
49
+ "--info_prefix",
50
+ help="prefix for energy, forces and stress keys",
51
+ type=str,
52
+ default="MACE_",
53
+ )
54
+ parser.add_argument(
55
+ "--head",
56
+ help="Model head used for evaluation",
57
+ type=str,
58
+ required=False,
59
+ default=None,
60
+ )
61
+ return parser.parse_args()
62
+
63
+
64
+ def main() -> None:
65
+ args = parse_args()
66
+ run(args)
67
+
68
+
69
+ def run(args: argparse.Namespace) -> None:
70
+ torch_tools.set_default_dtype(args.default_dtype)
71
+ device = torch_tools.init_device(args.device)
72
+
73
+ # Load model
74
+ model = torch.load(f=args.model, map_location=args.device)
75
+ model = model.to(args.device)
76
+ model.eval() # ✅ 保持 eval,不要关掉 requires_grad
77
+
78
+ # Load data and prepare input
79
+ atoms_list = ase.io.read(args.configs, index=":")
80
+ if args.head is not None:
81
+ for atoms in atoms_list:
82
+ atoms.info["head"] = args.head
83
+ configs = [config_from_atoms(atoms) for atoms in atoms_list]
84
+
85
+ z_table = utils.AtomicNumberTable([int(z) for z in model.atomic_numbers])
86
+
87
+ try:
88
+ heads = model.heads
89
+ except AttributeError:
90
+ heads = None
91
+
92
+ data_loader = torch_geometric.dataloader.DataLoader(
93
+ dataset=[
94
+ AtomicData.from_config(
95
+ config, z_table=z_table, cutoff=float(model.r_max), heads=heads
96
+ )
97
+ for config in configs
98
+ ],
99
+ batch_size=args.batch_size,
100
+ shuffle=False,
101
+ drop_last=False,
102
+ )
103
+
104
+ # Collect data
105
+ energies_list = []
106
+ contributions_list = []
107
+ stresses_list = []
108
+ forces_collection = []
109
+
110
+ total_time = 0.0
111
+ num_batches = 0
112
+ total_cfgs = len(configs)
113
+
114
+ start_total = time.time()
115
+ for batch in data_loader:
116
+ batch = batch.to(device)
117
+
118
+ if args.device == "cuda":
119
+ torch.cuda.synchronize()
120
+ start = time.time()
121
+
122
+ output = model(batch.to_dict(), compute_stress=args.compute_stress)
123
+
124
+ if args.device == "cuda":
125
+ torch.cuda.synchronize()
126
+ end = time.time()
127
+
128
+ total_time += (end - start)
129
+ num_batches += 1
130
+
131
+ energies_list.append(torch_tools.to_numpy(output["energy"]))
132
+ if args.compute_stress:
133
+ stresses_list.append(torch_tools.to_numpy(output["stress"]))
134
+ if args.return_contributions:
135
+ contributions_list.append(torch_tools.to_numpy(output["contributions"]))
136
+
137
+ forces = np.split(
138
+ torch_tools.to_numpy(output["forces"]),
139
+ indices_or_sections=batch.ptr[1:],
140
+ axis=0,
141
+ )
142
+ forces_collection.append(forces[:-1]) # drop last as its empty
143
+ end_total = time.time()
144
+
145
+ # ✅ 打印时间
146
+ if num_batches > 0 and total_cfgs > 0:
147
+ avg_time_batch = total_time / num_batches
148
+ avg_time_cfg = total_time / total_cfgs
149
+ print(f"Average inference time per batch: {avg_time_batch:.6f} s")
150
+ print(f"Average inference time per configuration: {avg_time_cfg:.6f} s")
151
+ print(f"Total inference time (only forward): {total_time:.6f} s")
152
+ print(f"Wall clock time (data+forward): {end_total - start_total:.6f} s")
153
+ else:
154
+ print("No batches were processed.")
155
+
156
+ # Process outputs
157
+ energies = np.concatenate(energies_list, axis=0)
158
+ forces_list = [
159
+ forces for forces_list in forces_collection for forces in forces_list
160
+ ]
161
+ assert len(atoms_list) == len(energies) == len(forces_list)
162
+ if args.compute_stress:
163
+ stresses = np.concatenate(stresses_list, axis=0)
164
+ assert len(atoms_list) == stresses.shape[0]
165
+
166
+ if args.return_contributions:
167
+ contributions = np.concatenate(contributions_list, axis=0)
168
+ assert len(atoms_list) == contributions.shape[0]
169
+
170
+ # Store data in atoms objects
171
+ for i, (atoms, energy, forces) in enumerate(zip(atoms_list, energies, forces_list)):
172
+ atoms.calc = None # crucial
173
+ atoms.info[args.info_prefix + "energy"] = energy
174
+ atoms.arrays[args.info_prefix + "forces"] = forces
175
+
176
+ if args.compute_stress:
177
+ atoms.info[args.info_prefix + "stress"] = stresses[i]
178
+
179
+ if args.return_contributions:
180
+ atoms.info[args.info_prefix + "BO_contributions"] = contributions[i]
181
+
182
+ # Write atoms to output path
183
+ ase.io.write(args.output, images=atoms_list, format="extxyz")
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()
scripts/fasteq/fasteq_install.sh ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # FastEq-hip 扩展安装脚本(OneScience MACE matchem 环境)
3
+ # 前置条件:已安装好 OneScience 的 matchem 环境。
4
+ # 本脚本安装 FastEq-hip 及 patch 后的 cuequivariance-torch,
5
+ # 以实现 MACE 模型在 DCU 上的训练和推理加速。
6
+ #
7
+ # 使用方法:
8
+ # cd /public/home/easyscience2024/wangrui/onescience/examples/matchem/mace/fasteq
9
+ # bash install.sh
10
+
11
+ set -e
12
+
13
+ # =============================================================================
14
+ # 1. 环境初始化
15
+ # =============================================================================
16
+ # 必须先加载 module,再激活 conda;否则 sghpcdas 会覆盖 conda 环境变量。
17
+ source ~/.bashrc
18
+
19
+ module load sghpcdas/25.6
20
+ module load sghpc-mpi-gcc/26.3
21
+
22
+ conda activate matchem_opt
23
+
24
+ # libgalaxyhip.so.5 位于 DTK 的 lib/ 和 hip/lib/ 下,但不在默认 LD_LIBRARY_PATH 中。
25
+ # 不加此路径会导致导入 torch/fasteq 时报错:
26
+ # ImportError: libgalaxyhip.so.5: cannot open shared object file
27
+ export LD_LIBRARY_PATH="/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/lib:/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/hip/lib:${LD_LIBRARY_PATH}"
28
+
29
+ # DTK 的 cmake 配置查找 amd_comgr,但 CMAKE_PREFIX_PATH 默认为空,需要显式指定。
30
+ export CMAKE_PREFIX_PATH="/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/dcc/comgr/lib64/cmake/amd_comgr:${CMAKE_PREFIX_PATH}"
31
+
32
+ # FastEq-hip 编译需要显式声明后端为 HIP,否则默认回退到 CUDA 导致找不到 nvcc。
33
+ export FASTEQ_BACKEND=hip
34
+ export ROCM_PATH=/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4
35
+ export HIP_PATH=/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4
36
+
37
+ # CMake 3.26 的 HIP 编译器 ABI 检测阶段(try_compile)不会继承主项目的 cache 变量,
38
+ # 但会读取 <Package>_DIR 环境变量。若不设置,会因找不到 amd_comgr 而报错。
39
+ export amd_comgr_DIR=/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/dcc/comgr/lib64/cmake/amd_comgr
40
+
41
+ # =============================================================================
42
+ # 2. FastEq-hip 源码路径
43
+ # =============================================================================
44
+ # 默认路径:与 onescience 项目同级目录。
45
+ # 可通过环境变量覆盖:FASTEQ_HIP_ROOT=/your/path bash install.sh
46
+ FASTEQ_HIP_ROOT="${FASTEQ_HIP_ROOT}"
47
+
48
+ if [[ ! -d "$FASTEQ_HIP_ROOT" ]]; then
49
+ echo "错误:FastEq-hip 源码未找到于 $FASTEQ_HIP_ROOT"
50
+ echo "请先克隆仓库,或通过 FASTEQ_HIP_ROOT 环境变量指定正确路径。"
51
+ exit 1
52
+ fi
53
+
54
+ echo ">>> FastEq-hip 源码路径: $FASTEQ_HIP_ROOT"
55
+
56
+ # =============================================================================
57
+ # 3. 安装 FastEq-hip
58
+ # =============================================================================
59
+ # PYTHONNOUSERSITE=1 避免使用 ~/.local 下的旧版 setuptools(59.8.0),
60
+ # 该版本会导致 editable_wheel 构建失败(KeyError: 'unix_user')。
61
+ #
62
+ # 注意:FastEq-hip 的 setup.py 中 HIP 后端的 extra_cmake_args 需补充两项:
63
+ # -DCMAKE_HIP_COMPILER=/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/llvm/bin/clang++
64
+ # -Damd_comgr_DIR=/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/dcc/comgr/lib64/cmake/amd_comgr
65
+ # 原因:CMake 3.26 不支持将 hipcc wrapper 直接作为 CMAKE_HIP_COMPILER,
66
+ # 必须使用 ROCm 的 clang++;且 try_compile 阶段不会继承 cache 变量中的 amd_comgr_DIR,
67
+ # 需要通过环境变量(已在上方导出)或 cmake 参数显式传递。
68
+ echo ">>> 正在安装 FastEq-hip(editable 模式)..."
69
+ PYTHONNOUSERSITE=1 pip install -r "$FASTEQ_HIP_ROOT/requirements.txt"
70
+ PYTHONNOUSERSITE=1 pip install -e "$FASTEQ_HIP_ROOT" --no-build-isolation
71
+
72
+ # =============================================================================
73
+ # 4. 安装 patch 后的 cuequivariance-torch
74
+ # =============================================================================
75
+ # cuequivariance==0.8.0 基础包已由 OneScience matchem 安装。
76
+ # 此处只需覆盖安装本地子模块中的 HIP patch 版本。
77
+ echo ">>> 正在安装 patched cuequivariance-torch(editable 模式)..."
78
+ PYTHONNOUSERSITE=1 pip install -e "$FASTEQ_HIP_ROOT/3rdparty/cuequivariance_torch/cuequivariance_torch" --no-build-isolation
79
+
80
+ # =============================================================================
81
+ # 5. 验证安装
82
+ # =============================================================================
83
+ echo ">>> 验证导入..."
84
+ python -c "import fasteq; print(' fasteq:', fasteq.__file__)"
85
+ python -c "import cuequivariance_torch as cuet; print(' cuequivariance_torch:', cuet.__file__)"
86
+
87
+ echo ">>> FastEq-hip 扩展安装成功!"
88
+ echo ">>> 现在可以运行 MACE 训练,并传入 --enable_cueq 启用 FastEq 加速。"
scripts/fasteq/install.sh ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # FastEq-hip 扩展安装脚本(OneScience MACE matchem 环境)
3
+ # 前置条件:已安装好 OneScience 的 matchem 环境。
4
+ # 本脚本安装 FastEq-hip 及 patch 后的 cuequivariance-torch,
5
+ # 以实现 MACE 模型在 DCU 上的训练和推理加速。
6
+ #
7
+ # 使用方法:
8
+ # cd /public/home/scnethpc2601/wr/onescience/examples/matchem/mace/demo/FastEq-hip
9
+ # bash install.sh
10
+
11
+ set -e
12
+
13
+ # =============================================================================
14
+ # 1. 环境初始化
15
+ # =============================================================================
16
+ # 必须先加载 module,再激活 conda;否则 sghpcdas 会覆盖 conda 环境变量。
17
+ module load sghpcdas/25.6
18
+ module load sghpc-mpi-gcc/26.3
19
+
20
+ source ~/.bashrc
21
+ conda activate matchem
22
+
23
+ # libgalaxyhip.so.5 位于 DTK 的 lib/ 和 hip/lib/ 下,但不在默认 LD_LIBRARY_PATH 中。
24
+ # 不加此路径会导致导入 torch/fasteq 时报错:
25
+ # ImportError: libgalaxyhip.so.5: cannot open shared object file
26
+ export LD_LIBRARY_PATH="/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/lib:/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/hip/lib:${LD_LIBRARY_PATH}"
27
+
28
+ # DTK 的 cmake 配置查找 amd_comgr,但 CMAKE_PREFIX_PATH 默认为空,需要显式指定。
29
+ export CMAKE_PREFIX_PATH="/public/software/sghpc_sdk.bak/Linux_x86_64/26.3/dtk/dtk-25.04.4/dcc/comgr/lib64/cmake/amd_comgr:${CMAKE_PREFIX_PATH}"
30
+
31
+ # =============================================================================
32
+ # 2. FastEq-hip 源码路径
33
+ # =============================================================================
34
+ # 默认路径:与 onescience 项目同级目录。
35
+ # 可通过环境变量覆盖:FASTEQ_HIP_ROOT=/your/path bash install.sh
36
+ FASTEQ_HIP_ROOT="${FASTEQ_HIP_ROOT:-/public/home/scnethpc2601/wr/project/FastEq-hip}"
37
+
38
+ if [[ ! -d "$FASTEQ_HIP_ROOT" ]]; then
39
+ echo "错误:FastEq-hip 源码未找到于 $FASTEQ_HIP_ROOT"
40
+ echo "请先克隆仓库,或通过 FASTEQ_HIP_ROOT 环境变量指定正确路径。"
41
+ exit 1
42
+ fi
43
+
44
+ echo ">>> FastEq-hip 源码路径: $FASTEQ_HIP_ROOT"
45
+
46
+ # =============================================================================
47
+ # 3. 安装 FastEq-hip
48
+ # =============================================================================
49
+ # PYTHONNOUSERSITE=1 避免使用 ~/.local 下的旧版 setuptools(59.8.0),
50
+ # 该版本会导致 editable_wheel 构建失败(KeyError: 'unix_user')。
51
+ echo ">>> 正在安装 FastEq-hip(editable 模式)..."
52
+ PYTHONNOUSERSITE=1 pip install -r "$FASTEQ_HIP_ROOT/requirements.txt"
53
+ PYTHONNOUSERSITE=1 pip install -e "$FASTEQ_HIP_ROOT" --no-build-isolation
54
+
55
+ # =============================================================================
56
+ # 4. 安装 patch 后的 cuequivariance-torch
57
+ # =============================================================================
58
+ # cuequivariance==0.8.0 基础包已由 OneScience matchem 安装。
59
+ # 此处只需覆盖安装本地子模块中的 HIP patch 版本。
60
+ echo ">>> 正在安装 patched cuequivariance-torch(editable 模式)..."
61
+ PYTHONNOUSERSITE=1 pip install -e "$FASTEQ_HIP_ROOT/3rdparty/cuequivariance_torch/cuequivariance_torch" --no-build-isolation
62
+
63
+ # =============================================================================
64
+ # 5. 验证安装
65
+ # =============================================================================
66
+ echo ">>> 验证导入..."
67
+ python -c "import fasteq; print(' fasteq:', fasteq.__file__)"
68
+ python -c "import cuequivariance_torch as cuet; print(' cuequivariance_torch:', cuet.__file__)"
69
+
70
+ echo ">>> FastEq-hip 扩展安装成功!"
71
+ echo ">>> 现在可以运行 MACE 训练,并传入 --enable_cueq 启用 FastEq 加速。"
scripts/run_md.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ase.io import read, write
2
+ from ase import units
3
+ from ase.md.langevin import Langevin
4
+ from ase.md.velocitydistribution import Stationary, ZeroRotation, MaxwellBoltzmannDistribution
5
+ import random
6
+ import os
7
+ import time
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ import argparse
11
+ from onescience.utils.mace.calculators import MACECalculator
12
+
13
+
14
+ def simpleMD(init_conf, temp, calc, fname, s, T, timestep_fs, friction, plot_file="md_plot.png"):
15
+ """
16
+ 在服务器上运行的 MD 模拟,不依赖 Jupyter,结束后保存图片。
17
+ """
18
+ init_conf.set_calculator(calc)
19
+
20
+ # 初始化温度
21
+ random.seed(701)
22
+ MaxwellBoltzmannDistribution(init_conf, temperature_K=300)
23
+ Stationary(init_conf)
24
+ ZeroRotation(init_conf)
25
+
26
+ dyn = Langevin(
27
+ init_conf,
28
+ timestep_fs * units.fs,
29
+ temperature_K=temp,
30
+ friction=friction
31
+ )
32
+
33
+ time_fs = []
34
+ temperature = []
35
+ energies = []
36
+
37
+ # 删除之前存储的同名轨迹
38
+ if os.path.exists(fname):
39
+ os.system('rm -rfv ' + fname)
40
+
41
+ # 记录函数:仅存储数据,不实时绘图
42
+ def write_frame():
43
+ dyn.atoms.write(fname, append=True)
44
+ time_fs.append(dyn.get_time() / units.fs)
45
+ temperature.append(dyn.atoms.get_temperature())
46
+ energies.append(dyn.atoms.get_potential_energy() / len(dyn.atoms))
47
+
48
+ # 可选:每隔一段时间打印进度
49
+ if len(time_fs) % 10 == 0: # 每10步打印一次
50
+ print(f"Step {len(time_fs)}, Time: {time_fs[-1]:.1f} fs, Temp: {temperature[-1]:.1f} K, E: {energies[-1]:.4f} eV/atom")
51
+
52
+ dyn.attach(write_frame, interval=s)
53
+
54
+ t0 = time.time()
55
+ dyn.run(T)
56
+ t1 = time.time()
57
+ print("MD 完成于 {0:.2f} 分钟!".format((t1 - t0) / 60))
58
+
59
+ # 模拟结束后绘制并保存图片
60
+ if time_fs and energies and temperature:
61
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(6, 6), sharex='all')
62
+ ax1.plot(time_fs, energies, 'b-')
63
+ ax1.set_ylabel('E (eV/atom)')
64
+ ax2.plot(time_fs, temperature, 'r-')
65
+ ax2.set_ylabel('T (K)')
66
+ ax2.set_xlabel('Time (fs)')
67
+ plt.tight_layout()
68
+ plt.savefig(plot_file, dpi=150)
69
+ print(f"Plot saved to {plot_file}")
70
+ plt.close() # 避免在无显示器的服务器上弹出窗口
71
+ else:
72
+ print("No data to plot.")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ parser = argparse.ArgumentParser()
77
+ parser.add_argument("--init_conf", type=str, required=True)
78
+ parser.add_argument("--model", type=str, required=True)
79
+ parser.add_argument("--output_xyz", type=str, required=True)
80
+ parser.add_argument("--plot_file", type=str, required=True)
81
+ parser.add_argument("--device", type=str, default="cuda")
82
+ parser.add_argument("--default_dtype", type=str, default="float64")
83
+ parser.add_argument("--temperature_k", type=float, required=True)
84
+ parser.add_argument("--time_step_fs", type=float, default=1.0)
85
+ parser.add_argument("--friction", type=float, default=0.1)
86
+ parser.add_argument("--save_interval", type=int, default=10)
87
+ parser.add_argument("--steps", type=int, required=True)
88
+
89
+ args = parser.parse_args()
90
+
91
+ # 读取初始构型(使用传进来的绝对路径,不再硬编码相对路径)
92
+ init_conf = read(args.init_conf, ':')[0].copy()
93
+
94
+ # 加载MACE模型
95
+ mace_calc = MACECalculator(
96
+ model_paths=[args.model],
97
+ device=args.device,
98
+ default_dtype=args.default_dtype
99
+ )
100
+
101
+ # 启动MD
102
+ simpleMD(
103
+ init_conf=init_conf,
104
+ temp=args.temperature_k,
105
+ calc=mace_calc,
106
+ fname=args.output_xyz,
107
+ s=args.save_interval,
108
+ T=args.steps,
109
+ timestep_fs=args.time_step_fs,
110
+ friction=args.friction,
111
+ plot_file=args.plot_file
112
+ )
scripts/train.py ADDED
@@ -0,0 +1,1098 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Training script for MACE
3
+
4
+ import ast
5
+ import glob
6
+ import importlib
7
+ import json
8
+ import logging
9
+ import os
10
+ import shutil
11
+ import types
12
+ from copy import deepcopy
13
+ from pathlib import Path
14
+ from typing import List, Optional
15
+
16
+ import torch.distributed
17
+ import torch.nn.functional
18
+ from e3nn.util import jit
19
+ from torch.nn.parallel import DistributedDataParallel as DDP
20
+ from torch.optim import LBFGS
21
+ from torch.utils.data import ConcatDataset
22
+ from torch_ema import ExponentialMovingAverage
23
+
24
+ import model as mace
25
+ #from onescience.utils.mace import data, tools
26
+ from onescience.utils.mace import tools
27
+ from onescience.utils.mace.calculators.foundations_models import mace_mp, mace_off
28
+ from onescience.utils.mace.cli.convert_cueq_e3nn import run as run_cueq_to_e3nn
29
+ from onescience.utils.mace.cli.convert_e3nn_cueq import run as run_e3nn_to_cueq
30
+ from onescience.utils.mace.cli.visualise_train import TrainingPlotter
31
+ #from onescience.utils.mace.data import KeySpecification, update_keyspec_from_kwargs
32
+ from onescience.datapipes.materials.pyg_stack.core.utils import KeySpecification, update_keyspec_from_kwargs
33
+ #from onescience.utils.mace.tools import torch_geometric
34
+ from onescience.datapipes.materials.tools import torch_geometric
35
+ from onescience.utils.mace.tools.model_script_utils import configure_model
36
+ import pdb
37
+ import sys
38
+ try:
39
+ import model
40
+ import onescience.utils.mace.tools
41
+ except ImportError as e:
42
+ print(f"警告:无法导入 model 模块用于别名修复: {e}")
43
+
44
+
45
+ # --- 在顶部添加这些导入 ---
46
+ from onescience.datapipes.materials.pyg_stack.core.atomic_data import AtomicData
47
+ from onescience.datapipes.materials.pyg_stack.storage.hdf5_dataset import HDF5Dataset, dataset_from_sharded_hdf5
48
+ from onescience.datapipes.materials.pyg_stack.storage.text_dataset import TextDataset
49
+ from onescience.utils.mace.tools.multihead_tools import (
50
+ HeadConfig,
51
+ assemble_mp_data,
52
+ dict_head_to_dataclass,
53
+ prepare_default_head,
54
+ prepare_pt_head,
55
+ )
56
+ from onescience.utils.mace.tools.run_train_utils import (
57
+ combine_datasets,
58
+ load_dataset_for_path,
59
+ normalize_file_paths,
60
+ )
61
+ from onescience.utils.mace.tools.scripts_utils import (
62
+ LRScheduler,
63
+ SubsetCollection,
64
+ check_path_ase_read,
65
+ convert_to_json_format,
66
+ dict_to_array,
67
+ extract_config_mace_model,
68
+ get_atomic_energies,
69
+ get_avg_num_neighbors,
70
+ get_config_type_weights,
71
+ get_dataset_from_xyz,
72
+ get_files_with_suffix,
73
+ get_loss_fn,
74
+ get_optimizer,
75
+ get_params_options,
76
+ get_swa,
77
+ print_git_commit,
78
+ remove_pt_head,
79
+ setup_wandb,
80
+ )
81
+ from onescience.utils.mace.tools.slurm_distributed import DistributedEnvironment
82
+ from onescience.utils.mace.tools.tables_utils import create_error_table
83
+
84
+ #from onescience.utils.mace.tools.utils import AtomicNumberTable
85
+ from onescience.datapipes.materials.tools.utils import AtomicNumberTable
86
+
87
+ def main() -> None:
88
+ """
89
+ This script runs the training/fine tuning for mace
90
+ """
91
+ args = tools.build_default_arg_parser().parse_args()
92
+ run(args)
93
+
94
+
95
+ def run(args) -> None:
96
+ """
97
+ This script runs the training/fine tuning for mace
98
+ """
99
+
100
+ tag = tools.get_tag(name=args.name, seed=args.seed)
101
+ args, input_log_messages = tools.check_args(args)
102
+
103
+ # default keyspec to update using heads dictionary
104
+ args.key_specification = KeySpecification()
105
+ update_keyspec_from_kwargs(args.key_specification, vars(args))
106
+
107
+ if args.device == "xpu":
108
+ try:
109
+ import intel_extension_for_pytorch as ipex
110
+ except ImportError as e:
111
+ raise ImportError(
112
+ "Error: Intel extension for PyTorch not found, but XPU device was specified"
113
+ ) from e
114
+ if args.distributed:
115
+ try:
116
+ distr_env = DistributedEnvironment()
117
+ except Exception as e: # pylint: disable=W0703
118
+ logging.error(f"Failed to initialize distributed environment: {e}")
119
+ return
120
+ world_size = distr_env.world_size
121
+ local_rank = distr_env.local_rank
122
+ rank = distr_env.rank
123
+ if rank == 0:
124
+ print(distr_env)
125
+ torch.distributed.init_process_group(backend="nccl")
126
+ else:
127
+ rank = int(0)
128
+
129
+ # Setup
130
+ tools.set_seeds(args.seed)
131
+ tools.setup_logger(level=args.log_level, tag=tag, directory=args.log_dir, rank=rank)
132
+ logging.info("===========VERIFYING SETTINGS===========")
133
+ for message, loglevel in input_log_messages:
134
+ logging.log(level=loglevel, msg=message)
135
+
136
+ if args.distributed:
137
+ torch.cuda.set_device(local_rank)
138
+ logging.info(f"!!!!!!!!!Set device {local_rank}")
139
+ logging.info(f"Process group initialized: {torch.distributed.is_initialized()}")
140
+ logging.info(f"Processes: {world_size}")
141
+
142
+ try:
143
+ logging.info(f"MACE version: {mace.__version__}")
144
+ except AttributeError:
145
+ logging.info("Cannot find MACE version, please install MACE via pip")
146
+ logging.debug(f"Configuration: {args}")
147
+
148
+ tools.set_default_dtype(args.default_dtype)
149
+ device = tools.init_device(args.device)
150
+ commit = print_git_commit()
151
+ model_foundation: Optional[torch.nn.Module] = None
152
+ foundation_model_avg_num_neighbors = 0
153
+ if args.foundation_model is not None:
154
+ if args.foundation_model in ["small", "medium", "large"]:
155
+ logging.info(
156
+ f"Using foundation model mace-mp-0 {args.foundation_model} as initial checkpoint."
157
+ )
158
+ calc = mace_mp(
159
+ model=args.foundation_model,
160
+ device=args.device,
161
+ default_dtype=args.default_dtype,
162
+ )
163
+ model_foundation = calc.models[0]
164
+ elif args.foundation_model in ["small_off", "medium_off", "large_off"]:
165
+ model_type = args.foundation_model.split("_")[0]
166
+ logging.info(
167
+ f"Using foundation model mace-off-2023 {model_type} as initial checkpoint. ASL license."
168
+ )
169
+ calc = mace_off(
170
+ model=model_type,
171
+ device=args.device,
172
+ default_dtype=args.default_dtype,
173
+ )
174
+ model_foundation = calc.models[0]
175
+ else:
176
+ # --- !! 插入这里的修复代码 !! ---
177
+ # (确保 'sys' 和 'model' 已经在文件顶部导入)
178
+ try:
179
+ logging.info(
180
+ "Creating legacy 'mace' module aliases for foundation checkpoint loading..."
181
+ )
182
+ legacy_aliases = {
183
+ "mace": "model",
184
+ "mace.tools": "onescience.utils.mace.tools",
185
+ "mace.data": "onescience.utils.mace.data",
186
+ "mace.modules.models": "model.mace",
187
+ "mace.modules.blocks": "onescience.modules.block.mace_block",
188
+ "mace.modules.radial": "onescience.modules.layer.mace_radial",
189
+ "mace.modules.loss": "onescience.modules.loss.mace_loss",
190
+ "mace.modules.utils": "onescience.modules.func_utils.mace_func_utils",
191
+ "mace.modules.irreps_tools": "onescience.modules.func_utils.mace_irreps_tools",
192
+ "mace.modules.symmetric_contraction": "onescience.modules.equivariant.mace_symmetric_contraction",
193
+ "mace.modules.wrapper_ops": "onescience.modules.equivariant.mace_wrapper_ops",
194
+ }
195
+ legacy_modules_pkg = sys.modules.setdefault(
196
+ "mace.modules", types.ModuleType("mace.modules")
197
+ )
198
+ if not hasattr(legacy_modules_pkg, "__path__"):
199
+ legacy_modules_pkg.__path__ = [] # type: ignore[attr-defined]
200
+
201
+ for legacy_name, new_name in legacy_aliases.items():
202
+ module_obj = importlib.import_module(new_name)
203
+ sys.modules[legacy_name] = module_obj
204
+ if legacy_name.startswith("mace.modules."):
205
+ setattr(
206
+ legacy_modules_pkg,
207
+ legacy_name.split(".")[-1],
208
+ module_obj,
209
+ )
210
+ logging.info("Legacy module aliases created successfully.")
211
+ except Exception as e:
212
+ logging.warning(f"Failed to create module alias: {e}")
213
+ # --- !! 修复代码结束 !! ---
214
+
215
+ # 现在这个 torch.load 应该可以工作了
216
+ model_foundation = torch.load(
217
+ args.foundation_model, map_location=args.device
218
+ )
219
+ logging.info(
220
+ f"Using foundation model {args.foundation_model} as initial checkpoint."
221
+ )
222
+ args.r_max = model_foundation.r_max.item()
223
+ foundation_model_avg_num_neighbors = model_foundation.interactions[
224
+ 0
225
+ ].avg_num_neighbors
226
+ if (
227
+ args.foundation_model not in ["small", "medium", "large"]
228
+ and args.pt_train_file is None
229
+ ):
230
+ logging.warning(
231
+ "Using multiheads finetuning with a foundation model that is not a Materials Project model, need to provied a path to a pretraining file with --pt_train_file."
232
+ )
233
+ args.multiheads_finetuning = False
234
+ if args.multiheads_finetuning:
235
+ assert (
236
+ args.E0s != "average"
237
+ ), "average atomic energies cannot be used for multiheads finetuning"
238
+ # check that the foundation model has a single head, if not, use the first head
239
+ if not args.force_mh_ft_lr:
240
+ logging.info(
241
+ "Multihead finetuning mode, setting learning rate to 0.0001 and EMA to True. To use a different learning rate, set --force_mh_ft_lr=True."
242
+ )
243
+ args.lr = 0.0001
244
+ args.ema = True
245
+ args.ema_decay = 0.99999
246
+ logging.info(
247
+ "Using multiheads finetuning mode, setting learning rate to 0.0001 and EMA to True"
248
+ )
249
+ if hasattr(model_foundation, "heads"):
250
+ if len(model_foundation.heads) > 1:
251
+ logging.warning(
252
+ "Mutlihead finetuning with models with more than one head is not supported, using the first head as foundation head."
253
+ )
254
+ model_foundation = remove_pt_head(
255
+ model_foundation, args.foundation_head
256
+ )
257
+ else:
258
+ args.multiheads_finetuning = False
259
+
260
+ if args.heads is not None:
261
+ args.heads = ast.literal_eval(args.heads)
262
+ for _, head_dict in args.heads.items():
263
+ # priority is global args < head property_key values < head info_keys+arrays_keys
264
+ head_keyspec = deepcopy(args.key_specification)
265
+ update_keyspec_from_kwargs(head_keyspec, head_dict)
266
+ head_keyspec.update(
267
+ info_keys=head_dict.get("info_keys", {}),
268
+ arrays_keys=head_dict.get("arrays_keys", {}),
269
+ )
270
+ head_dict["key_specification"] = head_keyspec
271
+ else:
272
+ args.heads = prepare_default_head(args)
273
+ if args.multiheads_finetuning:
274
+ pt_keyspec = (
275
+ args.heads["pt_head"]["key_specification"]
276
+ if "pt_head" in args.heads
277
+ else deepcopy(args.key_specification)
278
+ )
279
+ args.heads["pt_head"] = prepare_pt_head(
280
+ args, pt_keyspec, foundation_model_avg_num_neighbors
281
+ )
282
+
283
+ logging.info("===========LOADING INPUT DATA===========")
284
+ heads = list(args.heads.keys())
285
+ logging.info(f"Using heads: {heads}")
286
+ logging.info("Using the key specifications to parse data:")
287
+ for name, head_dict in args.heads.items():
288
+ head_keyspec = head_dict["key_specification"]
289
+ logging.info(f"{name}: {head_keyspec}")
290
+
291
+ head_configs: List[HeadConfig] = []
292
+ for head, head_args in args.heads.items():
293
+ logging.info(f"============= Processing head {head} ===========")
294
+ head_config = dict_head_to_dataclass(head_args, head, args)
295
+
296
+ # Handle train_file and valid_file - normalize to lists
297
+ if hasattr(head_config, "train_file") and head_config.train_file is not None:
298
+ head_config.train_file = normalize_file_paths(head_config.train_file)
299
+ if hasattr(head_config, "valid_file") and head_config.valid_file is not None:
300
+ head_config.valid_file = normalize_file_paths(head_config.valid_file)
301
+ if hasattr(head_config, "test_file") and head_config.test_file is not None:
302
+ head_config.test_file = normalize_file_paths(head_config.test_file)
303
+
304
+ if (
305
+ head_config.statistics_file is not None
306
+ and head_config.head_name != "pt_head"
307
+ ):
308
+ with open(head_config.statistics_file, "r") as f: # pylint: disable=W1514
309
+ statistics = json.load(f)
310
+ logging.info("Using statistics json file")
311
+ head_config.atomic_numbers = statistics["atomic_numbers"]
312
+ head_config.mean = statistics["mean"]
313
+ head_config.std = statistics["std"]
314
+ head_config.avg_num_neighbors = statistics["avg_num_neighbors"]
315
+ head_config.compute_avg_num_neighbors = False
316
+ if isinstance(statistics["atomic_energies"], str) and statistics[
317
+ "atomic_energies"
318
+ ].endswith(".json"):
319
+ with open(statistics["atomic_energies"], "r", encoding="utf-8") as f:
320
+ atomic_energies = json.load(f)
321
+ head_config.E0s = atomic_energies
322
+ head_config.atomic_energies_dict = ast.literal_eval(atomic_energies)
323
+ else:
324
+ head_config.E0s = statistics["atomic_energies"]
325
+ head_config.atomic_energies_dict = ast.literal_eval(
326
+ statistics["atomic_energies"]
327
+ )
328
+ if head_config.train_file == ["mp"]:
329
+ assert (
330
+ head_config.head_name == "pt_head"
331
+ ), "Only pt_head should use mp as train_file"
332
+ logging.info(
333
+ "Using the full Materials Project data for replay. You can construct a different subset using `fine_tuning_select.py` script."
334
+ )
335
+ collections = assemble_mp_data(args, head_config, tag)
336
+ head_config.collections = collections
337
+ elif any(check_path_ase_read(f) for f in head_config.train_file):
338
+ train_files_ase_list = [
339
+ f for f in head_config.train_file if check_path_ase_read(f)
340
+ ]
341
+ valid_files_ase_list = None
342
+ test_files_ase_list = None
343
+ if head_config.valid_file:
344
+ valid_files_ase_list = [
345
+ f for f in head_config.valid_file if check_path_ase_read(f)
346
+ ]
347
+ if head_config.test_file:
348
+ test_files_ase_list = [
349
+ f for f in head_config.test_file if check_path_ase_read(f)
350
+ ]
351
+ config_type_weights = get_config_type_weights(
352
+ head_config.config_type_weights
353
+ )
354
+ collections, atomic_energies_dict = get_dataset_from_xyz(
355
+ work_dir=os.path.join(args.output_dir,args.name),
356
+ train_path=train_files_ase_list,
357
+ valid_path=valid_files_ase_list,
358
+ valid_fraction=head_config.valid_fraction,
359
+ config_type_weights=config_type_weights,
360
+ test_path=test_files_ase_list,
361
+ seed=args.seed,
362
+ key_specification=head_config.key_specification,
363
+ head_name=head_config.head_name,
364
+ keep_isolated_atoms=head_config.keep_isolated_atoms,
365
+ )
366
+ head_config.collections = SubsetCollection(
367
+ train=collections.train,
368
+ valid=collections.valid,
369
+ tests=collections.tests,
370
+ )
371
+ head_config.atomic_energies_dict = atomic_energies_dict
372
+ logging.info(
373
+ f"Total number of configurations: train={len(collections.train)}, valid={len(collections.valid)}, "
374
+ f"tests=[{', '.join([name + ': ' + str(len(test_configs)) for name, test_configs in collections.tests])}],"
375
+ )
376
+ head_configs.append(head_config)
377
+
378
+ if all(
379
+ check_path_ase_read(head_config.train_file[0]) for head_config in head_configs
380
+ ):
381
+ size_collections_train = sum(
382
+ len(head_config.collections.train) for head_config in head_configs
383
+ )
384
+ size_collections_valid = sum(
385
+ len(head_config.collections.valid) for head_config in head_configs
386
+ )
387
+ if size_collections_train < args.batch_size:
388
+ logging.error(
389
+ f"Batch size ({args.batch_size}) is larger than the number of training data ({size_collections_train})"
390
+ )
391
+ if size_collections_valid < args.valid_batch_size:
392
+ logging.warning(
393
+ f"Validation batch size ({args.valid_batch_size}) is larger than the number of validation data ({size_collections_valid})"
394
+ )
395
+
396
+ if args.multiheads_finetuning:
397
+ logging.info(
398
+ "==================Using multiheads finetuning mode=================="
399
+ )
400
+ args.loss = "universal"
401
+
402
+ all_ase_readable = all(
403
+ all(check_path_ase_read(f) for f in head_config.train_file)
404
+ for head_config in head_configs
405
+ )
406
+ head_config_pt = filter(lambda x: x.head_name == "pt_head", head_configs)
407
+ head_config_pt = next(head_config_pt, None)
408
+ assert head_config_pt is not None, "Pretraining head not found"
409
+ if all_ase_readable:
410
+ ratio_pt_ft = size_collections_train / len(head_config_pt.collections.train)
411
+ if ratio_pt_ft < 0.1:
412
+ logging.warning(
413
+ f"Ratio of the number of configurations in the training set and the in the pt_train_file is {ratio_pt_ft}, "
414
+ f"increasing the number of configurations in the fine-tuning heads by {int(0.1 / ratio_pt_ft)}"
415
+ )
416
+ for head_config in head_configs:
417
+ if head_config.head_name == "pt_head":
418
+ continue
419
+ head_config.collections.train += (
420
+ head_config.collections.train * int(0.1 / ratio_pt_ft)
421
+ )
422
+ logging.info(
423
+ f"Total number of configurations in pretraining: train={len(head_config_pt.collections.train)}, valid={len(head_config_pt.collections.valid)}"
424
+ )
425
+ else:
426
+ logging.debug(
427
+ "Using LMDB/HDF5 datasets for pretraining or fine-tuning - skipping ratio check"
428
+ )
429
+
430
+ # Atomic number table
431
+ # yapf: disable
432
+ for head_config in head_configs:
433
+ if head_config.atomic_numbers is None:
434
+ assert all(check_path_ase_read(f) for f in head_config.train_file), "Must specify atomic_numbers when using .h5 or .aselmdb train_file input"
435
+ z_table_head = tools.get_atomic_number_table_from_zs(
436
+ z
437
+ for configs in (head_config.collections.train, head_config.collections.valid)
438
+ for config in configs
439
+ for z in config.atomic_numbers
440
+ )
441
+ head_config.atomic_numbers = z_table_head.zs
442
+ head_config.z_table = z_table_head
443
+ else:
444
+ if head_config.statistics_file is None:
445
+ logging.info("Using atomic numbers from command line argument")
446
+ else:
447
+ logging.info("Using atomic numbers from statistics file")
448
+ zs_list = ast.literal_eval(head_config.atomic_numbers)
449
+ assert isinstance(zs_list, list)
450
+ z_table_head = tools.AtomicNumberTable(zs_list)
451
+ head_config.atomic_numbers = zs_list
452
+ head_config.z_table = z_table_head
453
+ # yapf: enable
454
+ all_atomic_numbers = set()
455
+ for head_config in head_configs:
456
+ all_atomic_numbers.update(head_config.atomic_numbers)
457
+ z_table = AtomicNumberTable(sorted(list(all_atomic_numbers)))
458
+ if args.foundation_model_elements and model_foundation:
459
+ z_table = AtomicNumberTable(sorted(model_foundation.atomic_numbers.tolist()))
460
+ logging.info(f"Atomic Numbers used: {z_table.zs}")
461
+
462
+ # Atomic energies
463
+ atomic_energies_dict = {}
464
+ for head_config in head_configs:
465
+ if head_config.atomic_energies_dict is None or len(head_config.atomic_energies_dict) == 0:
466
+ assert head_config.E0s is not None, "Atomic energies must be provided"
467
+ if all(check_path_ase_read(f) for f in head_config.train_file) and head_config.E0s.lower() != "foundation":
468
+ atomic_energies_dict[head_config.head_name] = get_atomic_energies(
469
+ head_config.E0s, head_config.collections.train, head_config.z_table
470
+ )
471
+ elif head_config.E0s.lower() == "foundation":
472
+ assert args.foundation_model is not None
473
+ z_table_foundation = AtomicNumberTable(
474
+ [int(z) for z in model_foundation.atomic_numbers]
475
+ )
476
+ foundation_atomic_energies = model_foundation.atomic_energies_fn.atomic_energies
477
+ if foundation_atomic_energies.ndim > 1:
478
+ foundation_atomic_energies = foundation_atomic_energies.squeeze()
479
+ if foundation_atomic_energies.ndim == 2:
480
+ foundation_atomic_energies = foundation_atomic_energies[0]
481
+ logging.info("Foundation model has multiple heads, using the first head as foundation E0s.")
482
+ atomic_energies_dict[head_config.head_name] = {
483
+ z: foundation_atomic_energies[
484
+ z_table_foundation.z_to_index(z)
485
+ ].item()
486
+ for z in z_table.zs
487
+ }
488
+ else:
489
+ atomic_energies_dict[head_config.head_name] = get_atomic_energies(head_config.E0s, None, head_config.z_table)
490
+ else:
491
+ atomic_energies_dict[head_config.head_name] = head_config.atomic_energies_dict
492
+
493
+ # Atomic energies for multiheads finetuning
494
+ if args.multiheads_finetuning:
495
+ assert (
496
+ model_foundation is not None
497
+ ), "Model foundation must be provided for multiheads finetuning"
498
+ z_table_foundation = AtomicNumberTable(
499
+ [int(z) for z in model_foundation.atomic_numbers]
500
+ )
501
+ foundation_atomic_energies = model_foundation.atomic_energies_fn.atomic_energies
502
+ if foundation_atomic_energies.ndim > 1:
503
+ foundation_atomic_energies = foundation_atomic_energies.squeeze()
504
+ if foundation_atomic_energies.ndim == 2:
505
+ foundation_atomic_energies = foundation_atomic_energies[0]
506
+ logging.info("Foundation model has multiple heads, using the first head as foundation E0s.")
507
+ atomic_energies_dict["pt_head"] = {
508
+ z: foundation_atomic_energies[
509
+ z_table_foundation.z_to_index(z)
510
+ ].item()
511
+ for z in z_table.zs
512
+ }
513
+ heads = sorted(heads, key=lambda x: -1000 if x == "pt_head" else 0)
514
+ # Padding atomic energies if keeping all elements of the foundation model
515
+ if args.foundation_model_elements and model_foundation:
516
+ atomic_energies_dict_padded = {}
517
+ for head_name, head_energies in atomic_energies_dict.items():
518
+ energy_head_padded = {}
519
+ for z in z_table.zs:
520
+ energy_head_padded[z] = head_energies.get(z, 0.0)
521
+ atomic_energies_dict_padded[head_name] = energy_head_padded
522
+ atomic_energies_dict = atomic_energies_dict_padded
523
+
524
+ if args.model == "AtomicDipolesMACE":
525
+ atomic_energies = None
526
+ dipole_only = True
527
+ args.compute_dipole = True
528
+ args.compute_energy = False
529
+ args.compute_forces = False
530
+ args.compute_virials = False
531
+ args.compute_stress = False
532
+ else:
533
+ dipole_only = False
534
+ if args.model == "EnergyDipolesMACE":
535
+ args.compute_dipole = True
536
+ args.compute_energy = True
537
+ args.compute_forces = True
538
+ args.compute_virials = False
539
+ args.compute_stress = False
540
+ else:
541
+ args.compute_energy = True
542
+ args.compute_dipole = False
543
+ # atomic_energies: np.ndarray = np.array(
544
+ # [atomic_energies_dict[z] for z in z_table.zs]
545
+ # )
546
+ atomic_energies = dict_to_array(atomic_energies_dict, heads)
547
+ for head_config in head_configs:
548
+ try:
549
+ logging.info(f"Atomic Energies used (z: eV) for head {head_config.head_name}: " + "{" + ", ".join([f"{z}: {atomic_energies_dict[head_config.head_name][z]}" for z in head_config.z_table.zs]) + "}")
550
+ except KeyError as e:
551
+ raise KeyError(f"Atomic number {e} not found in atomic_energies_dict for head {head_config.head_name}, add E0s for this atomic number") from e
552
+
553
+ # Load datasets for each head, supporting multiple files per head
554
+ valid_sets = {head: [] for head in heads}
555
+ train_sets = {head: [] for head in heads}
556
+
557
+ for head_config in head_configs:
558
+ train_datasets = []
559
+
560
+ logging.info(f"Processing datasets for head '{head_config.head_name}'")
561
+ ase_files = [f for f in head_config.train_file if check_path_ase_read(f)]
562
+ non_ase_files = [f for f in head_config.train_file if not check_path_ase_read(f)]
563
+
564
+ if ase_files:
565
+ #dataset = load_dataset_for_path(
566
+ #file_path=ase_files,
567
+ #r_max=args.r_max,
568
+ #z_table=z_table,
569
+ #head_config=head_config,
570
+ #heads=heads,
571
+ #collection=head_config.collections.train,
572
+ #)
573
+ # ✨ 使用新的 TextDataset 包装内存中已划分好的数据 (collections.train)
574
+ logging.info(f"Wraping training data into TextDataset for head '{head_config.head_name}'")
575
+ dataset = TextDataset(
576
+ configurations=head_config.collections.train, # 直接传入预处理好的 Configuration 列表
577
+ r_max=args.r_max,
578
+ z_table=z_table,
579
+ head=head_config.head_name,
580
+ heads=heads
581
+ )
582
+ train_datasets.append(dataset)
583
+ logging.debug(f"Successfully loaded dataset from ASE files: {ase_files}")
584
+
585
+ for file in non_ase_files:
586
+ dataset = load_dataset_for_path(
587
+ file_path=file,
588
+ r_max=args.r_max,
589
+ z_table=z_table,
590
+ head_config=head_config,
591
+ heads=heads,
592
+ )
593
+ train_datasets.append(dataset)
594
+ logging.debug(f"Successfully loaded dataset from non-ASE file: {file}")
595
+
596
+ if not train_datasets:
597
+ raise ValueError(f"No valid training datasets found for head {head_config.head_name}")
598
+
599
+ train_sets[head_config.head_name] = combine_datasets(train_datasets, head_config.head_name)
600
+
601
+ if head_config.valid_file:
602
+ valid_datasets = []
603
+
604
+ valid_ase_files = [f for f in head_config.valid_file if check_path_ase_read(f)]
605
+ valid_non_ase_files = [f for f in head_config.valid_file if not check_path_ase_read(f)]
606
+
607
+ if valid_ase_files:
608
+ #valid_dataset = load_dataset_for_path(
609
+ # file_path=valid_ase_files,
610
+ # r_max=args.r_max,
611
+ # z_table=z_table,
612
+ # head_config=head_config,
613
+ # heads=heads,
614
+ # collection=head_config.collections.valid,
615
+ #)
616
+ #valid_datasets.append(valid_dataset)
617
+ #logging.debug(f"Successfully loaded validation dataset from ASE files: {valid_ase_files}")
618
+ # ✨ 使用新的 TextDataset 包装内存中已划分好的验证数据
619
+ logging.info(f"Wraping validation data into TextDataset for head '{head_config.head_name}'")
620
+ valid_dataset = TextDataset(
621
+ configurations=head_config.collections.valid, # 直接传入预处理好的 Configuration 列表
622
+ r_max=args.r_max,
623
+ z_table=z_table,
624
+ head=head_config.head_name,
625
+ heads=heads
626
+ )
627
+ valid_datasets.append(valid_dataset)
628
+ logging.debug(f"Successfully loaded validation dataset from ASE files: {valid_ase_files}")
629
+ for valid_file in valid_non_ase_files:
630
+ valid_dataset = load_dataset_for_path(
631
+ file_path=valid_file,
632
+ r_max=args.r_max,
633
+ z_table=z_table,
634
+ head_config=head_config,
635
+ heads=heads,
636
+ )
637
+ valid_datasets.append(valid_dataset)
638
+ logging.debug(f"Successfully loaded validation dataset from {valid_file}")
639
+
640
+ # Combine validation datasets
641
+ if valid_datasets:
642
+ valid_sets[head_config.head_name] = combine_datasets(valid_datasets, f"{head_config.head_name}_valid")
643
+ logging.info(f"Combined validation datasets for {head_config.head_name}")
644
+
645
+ # If no valid file is provided but collection exist, use the validation set from the collection
646
+ if head_config.valid_file is None and head_config.collections.valid:
647
+ valid_sets[head_config.head_name] = [
648
+ AtomicData.from_config(
649
+ config, z_table=z_table, cutoff=args.r_max, heads=heads
650
+ )
651
+ for config in head_config.collections.valid
652
+ ]
653
+ if not valid_sets[head_config.head_name]:
654
+ raise ValueError(f"No valid datasets found for head {head_config.head_name}, please provide a valid_file or a valid_fraction")
655
+
656
+ # Create data loader for this head
657
+ if isinstance(train_sets[head_config.head_name], list):
658
+ dataset_size = len(train_sets[head_config.head_name])
659
+ else:
660
+ dataset_size = len(train_sets[head_config.head_name])
661
+ logging.info(f"Head '{head_config.head_name}' training dataset size: {dataset_size}")
662
+
663
+ train_sampler_head = None
664
+ if args.distributed:
665
+ train_sampler_head = torch.utils.data.distributed.DistributedSampler(
666
+ train_sets[head_config.head_name],
667
+ num_replicas=world_size,
668
+ rank=rank,
669
+ shuffle=True,
670
+ drop_last=(not args.lbfgs),
671
+ seed=args.seed,
672
+ )
673
+ train_loader_head = torch_geometric.dataloader.DataLoader(
674
+ dataset=train_sets[head_config.head_name],
675
+ batch_size=args.batch_size,
676
+ sampler=train_sampler_head,
677
+ shuffle=(train_sampler_head is None),
678
+ drop_last=(train_sampler_head is None and not args.lbfgs),
679
+ pin_memory=args.pin_memory,
680
+ num_workers=args.num_workers,
681
+ generator=torch.Generator().manual_seed(args.seed),
682
+ )
683
+ head_config.train_loader = train_loader_head
684
+
685
+ # concatenate all the trainsets
686
+ train_set = ConcatDataset([train_sets[head] for head in heads])
687
+ train_sampler, valid_sampler = None, None
688
+ if args.distributed:
689
+ train_sampler = torch.utils.data.distributed.DistributedSampler(
690
+ train_set,
691
+ num_replicas=world_size,
692
+ rank=rank,
693
+ shuffle=True,
694
+ drop_last=(not args.lbfgs),
695
+ seed=args.seed,
696
+ )
697
+ valid_samplers = {}
698
+ for head, valid_set in valid_sets.items():
699
+ valid_sampler = torch.utils.data.distributed.DistributedSampler(
700
+ valid_set,
701
+ num_replicas=world_size,
702
+ rank=rank,
703
+ shuffle=True,
704
+ drop_last=True,
705
+ seed=args.seed,
706
+ )
707
+ valid_samplers[head] = valid_sampler
708
+ train_loader = torch_geometric.dataloader.DataLoader(
709
+ dataset=train_set,
710
+ batch_size=args.batch_size,
711
+ sampler=train_sampler,
712
+ shuffle=(train_sampler is None),
713
+ drop_last=(train_sampler is None and not args.lbfgs),
714
+ pin_memory=args.pin_memory,
715
+ num_workers=args.num_workers,
716
+ generator=torch.Generator().manual_seed(args.seed),
717
+ )
718
+ valid_loaders = {heads[i]: None for i in range(len(heads))}
719
+ if not isinstance(valid_sets, dict):
720
+ valid_sets = {"Default": valid_sets}
721
+ for head, valid_set in valid_sets.items():
722
+ valid_loaders[head] = torch_geometric.dataloader.DataLoader(
723
+ dataset=valid_set,
724
+ batch_size=args.valid_batch_size,
725
+ sampler=valid_samplers[head] if args.distributed else None,
726
+ shuffle=False,
727
+ drop_last=False,
728
+ pin_memory=args.pin_memory,
729
+ num_workers=args.num_workers,
730
+ generator=torch.Generator().manual_seed(args.seed),
731
+ )
732
+
733
+ loss_fn = get_loss_fn(args, dipole_only, args.compute_dipole)
734
+ args.avg_num_neighbors = get_avg_num_neighbors(head_configs, args, train_loader, device)
735
+
736
+ # Model
737
+ model, output_args = configure_model(args, train_loader, atomic_energies, model_foundation, heads, z_table, head_configs)
738
+ model.to(device)
739
+
740
+ logging.debug(model)
741
+ logging.info(f"Total number of parameters: {tools.count_parameters(model)}")
742
+ logging.info("")
743
+ logging.info("===========OPTIMIZER INFORMATION===========")
744
+ logging.info(f"Using {args.optimizer.upper()} as parameter optimizer")
745
+ logging.info(f"Batch size: {args.batch_size}")
746
+ if args.ema:
747
+ logging.info(f"Using Exponential Moving Average with decay: {args.ema_decay}")
748
+ logging.info(
749
+ f"Number of gradient updates: {int(args.max_num_epochs*len(train_set)/args.batch_size)}"
750
+ )
751
+ logging.info(f"Learning rate: {args.lr}, weight decay: {args.weight_decay}")
752
+ logging.info(loss_fn)
753
+
754
+ # Cueq
755
+ if args.enable_cueq:
756
+ logging.info("Converting model to CUEQ for accelerated training")
757
+ assert model.__class__.__name__ in ["MACE", "ScaleShiftMACE"]
758
+ model = run_e3nn_to_cueq(deepcopy(model), device=device)
759
+ # Optimizer
760
+ param_options = get_params_options(args, model)
761
+ optimizer: torch.optim.Optimizer
762
+ optimizer = get_optimizer(args, param_options)
763
+ if args.device == "xpu":
764
+ logging.info("Optimzing model and optimzier for XPU")
765
+ model, optimizer = ipex.optimize(model, optimizer=optimizer)
766
+ logger = tools.MetricsLogger(
767
+ directory=args.plot_dir, tag=tag + "_train"
768
+ ) # pylint: disable=E1123
769
+
770
+ lr_scheduler = LRScheduler(optimizer, args)
771
+
772
+ swa: Optional[tools.SWAContainer] = None
773
+ swas = [False]
774
+ if args.swa:
775
+ swa, swas = get_swa(args, model, optimizer, swas, dipole_only)
776
+
777
+ checkpoint_handler = tools.CheckpointHandler(
778
+ directory=args.checkpoints_dir,
779
+ tag=tag,
780
+ keep=args.keep_checkpoints,
781
+ swa_start=args.start_swa,
782
+ )
783
+
784
+ start_epoch = 0
785
+ restart_lbfgs = False
786
+ opt_start_epoch = None
787
+ if args.restart_latest:
788
+ try:
789
+ opt_start_epoch = checkpoint_handler.load_latest(
790
+ state=tools.CheckpointState(model, optimizer, lr_scheduler),
791
+ swa=True,
792
+ device=device,
793
+ )
794
+ except Exception: # pylint: disable=W0703
795
+ try:
796
+ opt_start_epoch = checkpoint_handler.load_latest(
797
+ state=tools.CheckpointState(model, optimizer, lr_scheduler),
798
+ swa=False,
799
+ device=device,
800
+ )
801
+ except Exception: # pylint: disable=W0703
802
+ restart_lbfgs = True
803
+ if opt_start_epoch is not None:
804
+ start_epoch = opt_start_epoch
805
+
806
+ ema: Optional[ExponentialMovingAverage] = None
807
+ if args.ema:
808
+ ema = ExponentialMovingAverage(model.parameters(), decay=args.ema_decay)
809
+ else:
810
+ for group in optimizer.param_groups:
811
+ group["lr"] = args.lr
812
+
813
+ if args.lbfgs:
814
+ logging.info("Switching optimizer to LBFGS")
815
+ optimizer = LBFGS(model.parameters(),
816
+ history_size=200,
817
+ max_iter=20,
818
+ line_search_fn="strong_wolfe")
819
+ if restart_lbfgs:
820
+ opt_start_epoch = checkpoint_handler.load_latest(
821
+ state=tools.CheckpointState(model, optimizer, lr_scheduler),
822
+ swa=False,
823
+ device=device,
824
+ )
825
+ if opt_start_epoch is not None:
826
+ start_epoch = opt_start_epoch
827
+
828
+ if args.wandb:
829
+ setup_wandb(args)
830
+ if args.distributed:
831
+ distributed_model = DDP(model, device_ids=[local_rank])
832
+ else:
833
+ distributed_model = None
834
+
835
+
836
+ train_valid_data_loader = {}
837
+ for head_config in head_configs:
838
+ data_loader_name = "train_" + head_config.head_name
839
+ train_valid_data_loader[data_loader_name] = head_config.train_loader
840
+ for head, valid_loader in valid_loaders.items():
841
+ data_load_name = "valid_" + head
842
+ train_valid_data_loader[data_load_name] = valid_loader
843
+
844
+ if args.plot and args.plot_frequency > 0:
845
+ try:
846
+ plotter = TrainingPlotter(
847
+ results_dir=logger.path,
848
+ heads=heads,
849
+ table_type=args.error_table,
850
+ train_valid_data=train_valid_data_loader,
851
+ test_data={},
852
+ output_args=output_args,
853
+ device=device,
854
+ plot_frequency=args.plot_frequency,
855
+ distributed=args.distributed,
856
+ swa_start=swa.start if swa else None
857
+ )
858
+ except Exception as e: # pylint: disable=W0718
859
+ logging.debug(f"Creating Plotter failed: {e}")
860
+ else:
861
+ plotter = None
862
+
863
+ if args.dry_run:
864
+ logging.info("DRY RUN mode enabled. Stopping now.")
865
+ return
866
+
867
+
868
+ tools.train(
869
+ model=model,
870
+ loss_fn=loss_fn,
871
+ train_loader=train_loader,
872
+ valid_loaders=valid_loaders,
873
+ optimizer=optimizer,
874
+ lr_scheduler=lr_scheduler,
875
+ checkpoint_handler=checkpoint_handler,
876
+ eval_interval=args.eval_interval,
877
+ start_epoch=start_epoch,
878
+ max_num_epochs=args.max_num_epochs,
879
+ logger=logger,
880
+ patience=args.patience,
881
+ save_all_checkpoints=args.save_all_checkpoints,
882
+ output_args=output_args,
883
+ device=device,
884
+ swa=swa,
885
+ ema=ema,
886
+ max_grad_norm=args.clip_grad,
887
+ log_errors=args.error_table,
888
+ log_wandb=args.wandb,
889
+ distributed=args.distributed,
890
+ distributed_model=distributed_model,
891
+ plotter=plotter,
892
+ train_sampler=train_sampler,
893
+ rank=rank,
894
+ )
895
+
896
+ logging.info("")
897
+ logging.info("===========RESULTS===========")
898
+
899
+ train_valid_data_loader = {}
900
+ for head_config in head_configs:
901
+ data_loader_name = "train_" + head_config.head_name
902
+ train_valid_data_loader[data_loader_name] = head_config.train_loader
903
+ for head, valid_loader in valid_loaders.items():
904
+ data_load_name = "valid_" + head
905
+ train_valid_data_loader[data_load_name] = valid_loader
906
+ test_sets = {}
907
+ stop_first_test = False
908
+ test_data_loader = {}
909
+ if all(
910
+ head_config.test_file == head_configs[0].test_file
911
+ for head_config in head_configs
912
+ ) and head_configs[0].test_file is not None:
913
+ stop_first_test = True
914
+ if all(
915
+ head_config.test_dir == head_configs[0].test_dir
916
+ for head_config in head_configs
917
+ ) and head_configs[0].test_dir is not None:
918
+ stop_first_test = True
919
+ for head_config in head_configs:
920
+ if all(check_path_ase_read(f) for f in head_config.train_file):
921
+ for name, subset in head_config.collections.tests:
922
+ test_sets[name] = [
923
+ AtomicData.from_config(
924
+ config, z_table=z_table, cutoff=args.r_max, heads=heads
925
+ )
926
+ for config in subset
927
+ ]
928
+ if head_config.test_dir is not None:
929
+ if not args.multi_processed_test:
930
+ test_files = get_files_with_suffix(head_config.test_dir, "_test.h5")
931
+ for test_file in test_files:
932
+ name = os.path.splitext(os.path.basename(test_file))[0]
933
+ test_sets[name] = HDF5Dataset(
934
+ test_file, r_max=args.r_max, z_table=z_table, heads=heads, head=head_config.head_name
935
+ )
936
+ else:
937
+ test_folders = glob(head_config.test_dir + "/*")
938
+ for folder in test_folders:
939
+ name = os.path.splitext(os.path.basename(test_file))[0]
940
+ test_sets[name] = dataset_from_sharded_hdf5(
941
+ folder, r_max=args.r_max, z_table=z_table, heads=heads, head=head_config.head_name
942
+ )
943
+ for test_name, test_set in test_sets.items():
944
+ test_sampler = None
945
+ if args.distributed:
946
+ test_sampler = torch.utils.data.distributed.DistributedSampler(
947
+ test_set,
948
+ num_replicas=world_size,
949
+ rank=rank,
950
+ shuffle=True,
951
+ drop_last=True,
952
+ seed=args.seed,
953
+ )
954
+ try:
955
+ drop_last = test_set.drop_last
956
+ except AttributeError as e: # pylint: disable=W0612
957
+ drop_last = False
958
+ test_loader = torch_geometric.dataloader.DataLoader(
959
+ test_set,
960
+ batch_size=args.valid_batch_size,
961
+ sampler=test_sampler,
962
+ shuffle=(test_sampler is None),
963
+ drop_last=drop_last,
964
+ num_workers=args.num_workers,
965
+ pin_memory=args.pin_memory,
966
+ )
967
+ test_data_loader[test_name] = test_loader
968
+ if stop_first_test:
969
+ break
970
+
971
+ for swa_eval in swas:
972
+ epoch = checkpoint_handler.load_latest(
973
+ state=tools.CheckpointState(model, optimizer, lr_scheduler),
974
+ swa=swa_eval,
975
+ device=device,
976
+ )
977
+ model.to(device)
978
+ if args.distributed:
979
+ distributed_model = DDP(model, device_ids=[local_rank], broadcast_buffers=False)
980
+ model_to_evaluate = model if not args.distributed else distributed_model
981
+ if swa_eval:
982
+ logging.info(f"Loaded Stage two model from epoch {epoch} for evaluation")
983
+ else:
984
+ logging.info(f"Loaded Stage one model from epoch {epoch} for evaluation")
985
+
986
+ if rank == 0:
987
+ # Save entire model
988
+ if swa_eval:
989
+ model_path = Path(args.checkpoints_dir) / (tag + "_stagetwo.model")
990
+ else:
991
+ model_path = Path(args.checkpoints_dir) / (tag + ".model")
992
+ logging.info(f"Saving model to {model_path}")
993
+ model_to_save = deepcopy(model)
994
+ if args.enable_cueq:
995
+ print("RUNING CUEQ TO E3NN")
996
+ print("swa_eval", swa_eval)
997
+ model_to_save = run_cueq_to_e3nn(deepcopy(model), device=device)
998
+ if args.save_cpu:
999
+ model_to_save = model_to_save.to("cpu")
1000
+ torch.save(model_to_save, model_path)
1001
+ extra_files = {
1002
+ "commit.txt": commit.encode("utf-8") if commit is not None else b"",
1003
+ "config.yaml": json.dumps(
1004
+ convert_to_json_format(extract_config_mace_model(model))
1005
+ ),
1006
+ }
1007
+ if swa_eval:
1008
+ torch.save(
1009
+ model_to_save, Path(args.model_dir) / (args.name + "_stagetwo.model")
1010
+ )
1011
+ try:
1012
+ path_complied = Path(args.model_dir) / (
1013
+ args.name + "_stagetwo_compiled.model"
1014
+ )
1015
+ logging.info(f"Compiling model, saving metadata {path_complied}")
1016
+ model_compiled = jit.compile(deepcopy(model_to_save))
1017
+ torch.jit.save(
1018
+ model_compiled,
1019
+ path_complied,
1020
+ _extra_files=extra_files,
1021
+ )
1022
+ except Exception as e: # pylint: disable=W0718
1023
+ pass
1024
+ else:
1025
+ torch.save(model_to_save, Path(args.model_dir) / (args.name + ".model"))
1026
+ try:
1027
+ path_complied = Path(args.model_dir) / (
1028
+ args.name + "_compiled.model"
1029
+ )
1030
+ logging.info(f"Compiling model, saving metadata to {path_complied}")
1031
+ model_compiled = jit.compile(deepcopy(model_to_save))
1032
+ torch.jit.save(
1033
+ model_compiled,
1034
+ path_complied,
1035
+ _extra_files=extra_files,
1036
+ )
1037
+ except Exception as e: # pylint: disable=W0718
1038
+ pass
1039
+
1040
+ logging.info("Computing metrics for training, validation, and test sets")
1041
+ for param in model.parameters():
1042
+ param.requires_grad = False
1043
+ skip_heads = args.skip_evaluate_heads.split(",") if args.skip_evaluate_heads else []
1044
+ if skip_heads:
1045
+ logging.info(f"Skipping evaluation for heads: {skip_heads}")
1046
+ table_train_valid = create_error_table(
1047
+ table_type=args.error_table,
1048
+ all_data_loaders=train_valid_data_loader,
1049
+ model=model_to_evaluate,
1050
+ loss_fn=loss_fn,
1051
+ output_args=output_args,
1052
+ log_wandb=args.wandb,
1053
+ device=device,
1054
+ distributed=args.distributed,
1055
+ skip_heads=skip_heads,
1056
+ )
1057
+ logging.info("Error-table on TRAIN and VALID:\n" + str(table_train_valid))
1058
+
1059
+ if test_data_loader:
1060
+ table_test = create_error_table(
1061
+ table_type=args.error_table,
1062
+ all_data_loaders=test_data_loader,
1063
+ model=model_to_evaluate,
1064
+ loss_fn=loss_fn,
1065
+ output_args=output_args,
1066
+ log_wandb=args.wandb,
1067
+ device=device,
1068
+ distributed=args.distributed,
1069
+ )
1070
+ logging.info("Error-table on TEST:\n" + str(table_test))
1071
+ if args.plot:
1072
+ try:
1073
+ plotter = TrainingPlotter(
1074
+ results_dir=logger.path,
1075
+ heads=heads,
1076
+ table_type=args.error_table,
1077
+ train_valid_data=train_valid_data_loader,
1078
+ test_data=test_data_loader,
1079
+ output_args=output_args,
1080
+ device=device,
1081
+ plot_frequency=args.plot_frequency,
1082
+ distributed=args.distributed,
1083
+ swa_start=swa.start if swa else None
1084
+ )
1085
+ plotter.plot(epoch, model_to_evaluate, rank)
1086
+ except Exception as e: # pylint: disable=W0718
1087
+ logging.debug(f"Plotting failed: {e}")
1088
+
1089
+ if args.distributed:
1090
+ torch.distributed.barrier()
1091
+
1092
+ logging.info("Done")
1093
+ if args.distributed:
1094
+ torch.distributed.destroy_process_group()
1095
+
1096
+
1097
+ if __name__ == "__main__":
1098
+ main()