Skip to content

Commit f908c77

Browse files
committed
feat: add fail-closed DPA4 PT2 runtime support
1 parent ad9018c commit f908c77

51 files changed

Lines changed: 8149 additions & 400 deletions

Some content is hidden

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

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ confs
4949
# Generated MatMaster skill archive
5050
apex-flow.zip
5151

52-
# Large DeePMD checkpoints (fetch with scripts/fetch_models.py; keep frozen *.pb in git)
52+
# Exclude arbitrary DeePMD training/source checkpoints, but keep the one
53+
# authoritative single-task DPA4 model bundled by apex-flow.
5354
apex/skills/apex-flow/models/**/*.pt
55+
!apex/skills/apex-flow/models/DPA4-alloytongqi/model.pt
5456
apex/skills/apex-flow/models/**/*.partial

CHANGELOG-1.3.0.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Major additions include:
1515
- Finite-temperature lattice workflow improvements
1616
- Finite-temperature elasticity workflow support and example documentation
1717
- Annealing workflow support for LAMMPS
18+
- Melting-point workflow support for solid-liquid coexistence calculations
19+
- Image-resident DPA4/PT2 runtime support with a fail-closed T4 qualification profile
1820
- Shape-controlled automatic supercell generation
1921
- `req_calc`-based relaxation/property selection for joint workflows
2022
- GUI-side batched submission for large configuration sets
@@ -74,6 +76,21 @@ Major additions include:
7476
- Added LAMMPS annealing workflow support.
7577
- Improved generated annealing input scripts for heating, cooling, RDF output, and holding stages.
7678

79+
### Melting Point Workflow
80+
81+
- Added a LAMMPS solid-liquid coexistence workflow for estimating melting points across multiple temperatures and replicas.
82+
- Added restart-file staging, interface-axis controls, per-temperature restart validation, and generated skill templates.
83+
- Added input validation and regression coverage for melting-specific temperature, replica, restart, and backend constraints.
84+
85+
### DPA4/PT2 Runtime and Qualification
86+
87+
- Added image-resident DeePMD PT2 model handling so immutable runtime artifacts are not copied into task upload packages.
88+
- Added DPA4-aware LAMMPS input generation, including the required atom maps, runtime plugin auto-loading, and rejection of incompatible legacy plugin commands.
89+
- Added an audited `DPA4-alloytongqi` source checkpoint, container wrapper templates, runtime manifest template, and a reproducible CPU/GPU and phonoLAMMPS benchmark harness.
90+
- Added a single-source DPA4 runtime profile used by generation, recommendation, standalone validation, and submission. The bundled profile remains fail-closed until an immutable image `ref@digest` passes the recorded post-snapshot T4 qualification.
91+
- Restricted the qualified production contract to one MPI rank on one `c4_m15_1 * NVIDIA T4`, with exact image, wrapper, model hash, and dispatcher validation.
92+
- Added complete automatic `type_map` expansion across every resolved structure and every effective LAMMPS `overwrite_interaction`.
93+
7794
### Workflow Selection and Submission
7895

7996
- Added `req_calc`-based workflow selection for relaxation and property calculations.
@@ -94,6 +111,7 @@ Major additions include:
94111
- `.debug.log`
95112
- `.debug.stdout`
96113
- `.debug.stderr`
114+
- Added bounded retries for transient LAMMPS remote-startup failures, including header-only logs, retry evidence preservation, and explicit retry classification in task status files.
97115

98116
### Reporting
99117

@@ -126,4 +144,5 @@ Major additions include:
126144
- Added runnable examples for RSS, GammaSurface, and finite-temperature elasticity.
127145
- Added GUI developer documentation.
128146
- Added `monty` to the package dependencies and constrained supported Python versions to `<3.13`.
129-
- Added support for Phonopy v4 in terms of phonon calculation.
147+
- Added Phonopy v4-compatible setup, force-constant, and band-generation fallbacks.
148+
- Expanded the bundled `apex-flow` skill with backend-aware generation and validation for Bohrium, local debug, local cluster, VASP, LAMMPS, DPA4/PT2, melting-point, gamma, and finite-temperature workflows.

apex/core/calculator/Lammps.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,24 @@ def __init__(self, inter_parameter, path_to_poscar):
149149
self.inter_type = inter_parameter["type"]
150150
self.type_map = inter_parameter["type_map"]
151151
self.in_lammps = inter_parameter.get("in_lammps", "auto")
152+
self.model_in_image = inter_parameter.get("model_in_image", False)
153+
if not isinstance(self.model_in_image, bool):
154+
raise ValueError("interaction.model_in_image must be a boolean")
155+
if self.model_in_image:
156+
model = inter_parameter.get("model")
157+
if not lammps_utils.is_deepmd_pt2(inter_parameter):
158+
raise ValueError(
159+
"interaction.model_in_image is only supported with "
160+
"type=deepmd and deepmd_runtime=dpa4_pt2"
161+
)
162+
if (
163+
not isinstance(model, str)
164+
or not os.path.isabs(model)
165+
or not model.lower().endswith(".pt2")
166+
):
167+
raise ValueError(
168+
"an image-resident DPA4 model must be an absolute .pt2 path"
169+
)
152170
if self.inter_type in MULTI_MODELS_INTER_TYPE:
153171
self.model = list(map(os.path.abspath, inter_parameter["model"]))
154172
else:
@@ -182,13 +200,17 @@ def set_inter_type_func(self):
182200
def set_model_param(self):
183201
deepmd_version = self.inter.get("deepmd_version", "2.1.1")
184202
if self.inter_type == "deepmd":
185-
model_name = os.path.basename(self.model)
203+
model_name = self.model if self.model_in_image else os.path.basename(self.model)
186204
self.model_param = {
187205
"type": self.inter_type,
188206
"model_name": [model_name],
189207
"param_type": self.type_map,
190208
"deepmd_version": deepmd_version,
191209
}
210+
if "deepmd_runtime" in self.inter:
211+
self.model_param["deepmd_runtime"] = self.inter["deepmd_runtime"]
212+
if self.model_in_image:
213+
self.model_param["model_in_image"] = True
192214
elif self.inter_type in ["meam", "snap"]:
193215
model_name = list(map(os.path.basename, self.model))
194216
self.model_param = {
@@ -231,6 +253,10 @@ def symlink_force(self, target, link_name):
231253
os.symlink(target, link_name)
232254

233255
def make_potential_files(self, output_dir):
256+
if self.model_in_image:
257+
dumpfn(self.inter, os.path.join(output_dir, "inter.json"), indent=4)
258+
return
259+
234260
parent_dir = os.path.join(output_dir, "../../")
235261
if self.inter_type in MULTI_MODELS_INTER_TYPE:
236262
model_file = map(os.path.basename, self.model)
@@ -446,6 +472,9 @@ def make_input_file(self, output_dir, task_type, task_param):
446472
):
447473
fc = _apply_gamma_fix_to_lammps_input(fc, task_param["add_fix"])
448474

475+
# This also covers user-supplied and property-specific LAMMPS inputs.
476+
fc = lammps_utils.ensure_atom_map_before_read_data(fc, self.model_param)
477+
449478
dumpfn(task_param, os.path.join(output_dir, "task.json"), indent=4)
450479

451480
in_lammps_not_link_list = ["eos", "finite_t_elastic"]
@@ -717,7 +746,11 @@ def _prepare_result_dict(self, atom_numbs, type_map_list, type_list, box, coord,
717746
return result_dict
718747

719748
def forward_files(self, property_type="relaxation", task_param=None):
720-
model_files = list(map(os.path.basename, self.model)) if self.inter_type in MULTI_MODELS_INTER_TYPE else [os.path.basename(self.model)]
749+
model_files = [] if self.model_in_image else (
750+
list(map(os.path.basename, self.model))
751+
if self.inter_type in MULTI_MODELS_INTER_TYPE
752+
else [os.path.basename(self.model)]
753+
)
721754
if property_type == "finite_t_latt":
722755
return ["in.lammps", "variable_FiniteTlatt.in"] + model_files
723756
elif property_type == "melting_point":
@@ -749,7 +782,11 @@ def forward_files(self, property_type="relaxation", task_param=None):
749782
return ["conf.lmp", "in.lammps"] + model_files
750783

751784
def forward_common_files(self, property_type="relaxation"):
752-
model_files = list(map(os.path.basename, self.model)) if self.inter_type in MULTI_MODELS_INTER_TYPE else [os.path.basename(self.model)]
785+
model_files = [] if self.model_in_image else (
786+
list(map(os.path.basename, self.model))
787+
if self.inter_type in MULTI_MODELS_INTER_TYPE
788+
else [os.path.basename(self.model)]
789+
)
753790
if property_type not in ["eos"]:
754791
if property_type == "finite_t_latt":
755792
return ["in.lammps", "variable_FiniteTlatt.in"] + model_files

apex/core/calculator/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,19 @@
1010
'mace',
1111
'nep'
1212
]
13+
14+
15+
def lammps_model_files_for_cleanup(inter_param):
16+
"""Return staged model paths that APEX may remove after retrieval.
17+
18+
Image-resident models are immutable runtime assets rather than staged task
19+
files. Never turn their absolute paths into cleanup commands.
20+
"""
21+
if inter_param.get("model_in_image") is True:
22+
return []
23+
model = inter_param.get("model")
24+
if isinstance(model, str):
25+
return [model]
26+
if isinstance(model, list):
27+
return list(model)
28+
return []

apex/core/calculator/lib/lammps_utils.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,59 @@
1414
upload_packages.append(__file__)
1515

1616

17+
DEEPMD_RUNTIME_DPA4_PT2 = "dpa4_pt2"
18+
19+
20+
def is_deepmd_pt2(param):
21+
"""Return whether an interaction uses the DeepMD AOTI ``.pt2`` runtime."""
22+
return (
23+
param.get("type") == "deepmd"
24+
and str(param.get("deepmd_runtime", "")).strip().lower()
25+
== DEEPMD_RUNTIME_DPA4_PT2
26+
)
27+
28+
29+
def ensure_atom_map_before_read_data(input_text, param):
30+
"""Enable a LAMMPS atom map before each PT2 box-loading command.
31+
32+
DPA4 AOTI inference consumes LAMMPS atom IDs through the DeepMD plugin.
33+
Keep legacy DeepMD inputs byte-for-byte unchanged unless the interaction
34+
explicitly opts into ``deepmd_runtime: dpa4_pt2``.
35+
"""
36+
if not is_deepmd_pt2(param):
37+
return input_text
38+
39+
lines = input_text.splitlines(keepends=True)
40+
rewritten = []
41+
previous_command = []
42+
for line in lines:
43+
command = line.split("#", 1)[0].strip().split()
44+
if (
45+
command[:2] == ["plugin", "load"]
46+
and len(command) >= 3
47+
and os.path.basename(command[2].strip("'\"")) == "libdeepmd_lmp.so"
48+
):
49+
raise ValueError(
50+
"DPA4/PT2 inputs must use LAMMPS_PLUGIN_PATH auto-loading; "
51+
"explicitly loading legacy libdeepmd_lmp.so is not supported"
52+
)
53+
if (
54+
command
55+
and command[0] in {"read_data", "read_restart"}
56+
and not (
57+
previous_command[:2] == ["atom_modify", "map"]
58+
and len(previous_command) >= 3
59+
and previous_command[2].lower() in {"yes", "array", "hash"}
60+
)
61+
):
62+
newline = "\r\n" if line.endswith("\r\n") else "\n"
63+
rewritten.append(f"atom_modify map yes{newline}")
64+
rewritten.append(line)
65+
if command:
66+
previous_command = command
67+
return "".join(rewritten)
68+
69+
1770
def cvt_lammps_conf(fin, fout, type_map, ofmt="lammps/data"):
1871
"""
1972
Format convert from fin to fout, specify the output format by ofmt
@@ -298,7 +351,7 @@ def make_lammps_eval(conf, type_map, interaction, param):
298351
ret += 'print "Final volume per atoms = ${Vpa}"\n'
299352
ret += 'print "Final Base area = ${AA}"\n'
300353
ret += 'print "Final Stress (xx yy zz xy xz yz) = ${Pxx} ${Pyy} ${Pzz} ${Pxy} ${Pxz} ${Pyz}"\n'
301-
return ret
354+
return ensure_atom_map_before_read_data(ret, param)
302355

303356

304357
def make_lammps_equi(
@@ -320,10 +373,9 @@ def make_lammps_equi(
320373
make lammps input for equilibritation
321374
"""
322375
deepmd_version = param.get("deepmd_version", None)
323-
is_new_dpmd = False
324-
if deepmd_version:
325-
split_v = deepmd_version.split('.')
326-
is_new_dpmd = bool(int(split_v[0]) >= 2 and int(split_v[1]) >= 1 and int(split_v[2]) >= 5)
376+
is_new_dpmd = bool(
377+
deepmd_version and Version(deepmd_version) >= Version("2.1.5")
378+
)
327379
prop_type = kwargs.get("prop_type", "others")
328380
dump_step = 100
329381
# detour sychronizing problem of dumping in new version of deepmd-kit >=2.1.5
@@ -385,7 +437,7 @@ def make_lammps_equi(
385437
ret += 'print "Final volume per atoms = ${Vpa}"\n'
386438
ret += 'print "Final Base area = ${AA}"\n'
387439
ret += 'print "Final Stress (xx yy zz xy xz yz) = ${Pxx} ${Pyy} ${Pzz} ${Pxy} ${Pxz} ${Pyz}"\n'
388-
return ret
440+
return ensure_atom_map_before_read_data(ret, param)
389441

390442

391443
def make_lammps_elastic(
@@ -435,7 +487,7 @@ def make_lammps_elastic(
435487
ret += 'print "Final energy per atoms = ${Epa}"\n'
436488
ret += 'print "Final volume per atoms = ${Vpa}"\n'
437489
ret += 'print "Final Stress (xx yy zz xy xz yz) = ${Pxx} ${Pyy} ${Pzz} ${Pxy} ${Pxz} ${Pyz}"\n'
438-
return ret
490+
return ensure_atom_map_before_read_data(ret, param)
439491

440492
def make_lammps_FiniteTlatt(conf, type_map, interaction, param, cal_setting=None):
441493
type_map_list = element_list(type_map)
@@ -521,7 +573,7 @@ def make_lammps_FiniteTlatt(conf, type_map, interaction, param, cal_setting=None
521573
ret += 'print "Final Base area = ${AA}"\n'
522574
ret += 'print "Final Stress (xx yy zz xy xz yz) = ${Pxx} ${Pyy} ${Pzz} ${Pxy} ${Pxz} ${Pyz}"\n'
523575
ret += 'print "Final Length (box_x box_y box_z) = ${lx} ${ly} ${lz}"\n'
524-
return ret
576+
return ensure_atom_map_before_read_data(ret, param)
525577

526578
def make_lammps_FiniteTelastic(conf, type_map, interaction, param, task_dir="."):
527579
type_map_list = element_list(type_map)
@@ -617,7 +669,7 @@ def force_field_setup():
617669
ret += "print \"Final energy per atoms = ${Epa}\"\n"
618670
ret += "print \"Final volume per atoms = ${Vpa}\"\n"
619671
ret += "print \"Final Stress (xx yy zz xy xz yz) = ${Pxx} ${Pyy} ${Pzz} ${Pxy} ${Pxz} ${Pyz}\"\n"
620-
return ret
672+
return ensure_atom_map_before_read_data(ret, param)
621673

622674
def make_lammps_press_relax(
623675
conf,
@@ -690,7 +742,7 @@ def make_lammps_press_relax(
690742
ret += 'print "Final energy per atoms = ${Epa} eV"\n'
691743
ret += 'print "Final volume per atoms = ${Vpa} A^3"\n'
692744
ret += 'print "Final Stress (xx yy zz xy xz yz) = ${Pxx} ${Pyy} ${Pzz} ${Pxy} ${Pxz} ${Pyz}"\n'
693-
return ret
745+
return ensure_atom_map_before_read_data(ret, param)
694746

695747
def make_lammps_annealing(conf, type_map, interaction, param, cal_setting):
696748
"""LAMMPS input for annealing using the same stage controls as annealing/.
@@ -896,7 +948,7 @@ def _unfix_stage_analysis(stage):
896948

897949
ret += 'print "__end_of_lmp_annealing_calculation__"\n'
898950
ret += 'label end_of_run\n'
899-
return ret
951+
return ensure_atom_map_before_read_data(ret, param)
900952

901953
"""
902954
def make_lammps_phonon(

apex/core/property/Phonon.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from apex.core.calculator.calculator import LAMMPS_INTER_TYPE
1919
from apex.core.calculator.lib import abacus_utils
2020
from apex.core.calculator.lib import vasp_utils
21+
from apex.core.calculator.lib.lammps_utils import is_deepmd_pt2
2122
from apex.core.property.Property import Property
2223
from apex.core.refine import make_refine
2324
from apex.core.reproduce import make_repro, post_repro

apex/op/RunLAMMPS.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def _cleanup_model_links(cls, task_dir):
5454
logging.warning(f"Failed to load inter.json for symlink cleanup: {exc}")
5555
return
5656

57+
if inter_param.get("model_in_image") is True:
58+
return
59+
5760
model_spec = inter_param.get("model", [])
5861
if isinstance(model_spec, str):
5962
model_list = [model_spec]

apex/op/property_ops.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from monty.serialization import dumpfn
1313
from apex.utils import recursive_search, apex_task_succeeded
1414
from apex.core.lib.utils import create_path
15-
from apex.core.calculator import LAMMPS_INTER_TYPE
15+
from apex.core.calculator import LAMMPS_INTER_TYPE, lammps_model_files_for_cleanup
1616
from apex.task_failure import (
1717
REMOTE_LAMMPS_STARTUP_FAILURE,
1818
classify_apex_task_status,
@@ -377,11 +377,7 @@ def execute(self, op_in: OPIO) -> OPIO:
377377
# remove potential files in each task
378378
if inter_type in LAMMPS_INTER_TYPE:
379379
os.chdir(abs_path_to_prop)
380-
inter_files_name = []
381-
if type(inter_param["model"]) is str:
382-
inter_files_name = [inter_param["model"]]
383-
elif type(inter_param["model"]) is list:
384-
inter_files_name.extend(inter_param["model"])
380+
inter_files_name = lammps_model_files_for_cleanup(inter_param)
385381
for file in inter_files_name:
386382
cmd = f"rm -f ../{file}"
387383
subprocess.call(cmd, shell=True)

apex/op/relaxation_ops.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Artifact,
1010
upload_packages
1111
)
12-
from apex.core.calculator import LAMMPS_INTER_TYPE
12+
from apex.core.calculator import LAMMPS_INTER_TYPE, lammps_model_files_for_cleanup
1313
from apex.utils import recursive_search, apex_task_succeeded
1414

1515
upload_packages.append(__file__)
@@ -199,23 +199,21 @@ def execute(self, op_in: OPIO) -> OPIO:
199199
# remove potential files
200200
inter_files_name = []
201201
if inter_type in LAMMPS_INTER_TYPE:
202-
if type(inter_param["model"]) is str:
203-
inter_files_name = [inter_param["model"]]
204-
elif type(inter_param["model"]) is list:
205-
inter_files_name.extend(inter_param["model"])
202+
inter_files_name = lammps_model_files_for_cleanup(inter_param)
206203
elif inter_type == 'vasp':
207204
inter_files_name = ['POTCAR']
208205

209-
for ii in conf_dirs:
210-
cmd = 'rm -f'
211-
for jj in inter_files_name:
212-
cmd += f' {jj}'
213-
os.chdir(ii)
214-
subprocess.call(cmd, shell=True)
215-
os.chdir(op_in['input_all'])
216-
os.chdir(os.path.join(ii, 'relaxation/relax_task'))
217-
subprocess.call(cmd, shell=True)
218-
os.chdir(op_in['input_all'])
206+
if inter_files_name:
207+
for ii in conf_dirs:
208+
cmd = 'rm -f'
209+
for jj in inter_files_name:
210+
cmd += f' {jj}'
211+
os.chdir(ii)
212+
subprocess.call(cmd, shell=True)
213+
os.chdir(op_in['input_all'])
214+
os.chdir(os.path.join(ii, 'relaxation/relax_task'))
215+
subprocess.call(cmd, shell=True)
216+
os.chdir(op_in['input_all'])
219217

220218
os.chdir(cwd)
221219
for ii in copy_dir_list:

0 commit comments

Comments
 (0)