Skip to content

Commit 2b3bb05

Browse files
njzjz-botpre-commit-ci[bot]njzjz
authored
feat(tf): support training stat_file (deepmodeling#5551)
Problem - `training/stat_file` is accepted by the shared input schema but was effectively only wired for non-TF backends. - PR deepmodeling#4926 had the right direction, but it was closed and no longer applied cleanly to current `master`. Change - Port the TF `stat_file` plumbing onto current `master`: create/open `DPPath`, pass it through `DPTrainer.build()` and `Model.data_stat()`, and save/load energy statistics under the PyTorch-compatible type-map subdirectory. - Keep TensorFlow's internal `bias_atom_e` as the historical 1-D vector while storing stat files in the cross-backend `(ntypes, 1)` format. - Note the intentional TF behavior change: energy-bias initialization now uses the shared dpmodel/PyTorch per-frame regression path even when `training.stat_file` is not set. This can differ from legacy TF's per-system weighting for unequal-frame systems, but keeps freshly computed TF stats consistent with restored cross-backend stat files. - Add TF and TF/PT consistency coverage derived from deepmodeling#4926. Notes - Based on deepmodeling#4926; resolves deepmodeling#4017. - Local checks: `python3 -m py_compile` on touched files, `uvx ruff check` on touched files, and `uvx ruff format --check` on touched files passed. - Could not run unit tests in this workspace because the unbuilt checkout lacks generated package metadata (`deepmd._version` / `deepmd.__about__`). Authored by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.5) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `training.stat_file` configuration option to save training statistics during training. Statistics can be saved to an HDF5 file or directory structure. * **Tests** * Added tests to verify training statistics file creation and ensure consistency across different backends. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng <jinzhe.zeng@ustc.edu.cn>
1 parent adfa278 commit 2b3bb05

25 files changed

Lines changed: 1363 additions & 95 deletions

deepmd/tf/descriptor/se_a.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
from .se import (
7878
DescrptSe,
7979
)
80+
from .stat import (
81+
load_or_compute_se_input_stats,
82+
)
8083

8184

8285
@Descriptor.register("se_e2_a")
@@ -374,7 +377,8 @@ def compute_input_stats(
374377
**kwargs
375378
Additional keyword arguments.
376379
"""
377-
if True:
380+
381+
def compute_stats() -> dict[str, Any]:
378382
sumr = []
379383
suma = []
380384
sumn = []
@@ -398,7 +402,16 @@ def compute_input_stats(
398402
"sumr2": sumr2,
399403
"suma2": suma2,
400404
}
401-
self.merge_input_stats(stat_dict)
405+
return stat_dict
406+
407+
stat_dict = load_or_compute_se_input_stats(
408+
self,
409+
kwargs.get("stat_file_path"),
410+
last_dim=4,
411+
compute=compute_stats,
412+
mixed_types=False,
413+
)
414+
self.merge_input_stats(stat_dict)
402415

403416
def merge_input_stats(self, stat_dict: dict[str, Any]) -> None:
404417
"""Merge the statistics computed from compute_input_stats to obtain the self.davg and self.dstd.

deepmd/tf/descriptor/se_atten.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
from .se_a import (
9191
DescrptSeA,
9292
)
93+
from .stat import (
94+
load_or_compute_se_input_stats,
95+
)
9396

9497
log = logging.getLogger(__name__)
9598

@@ -373,7 +376,8 @@ def compute_input_stats(
373376
**kwargs
374377
Additional keyword arguments.
375378
"""
376-
if True:
379+
380+
def compute_stats() -> dict[str, Any]:
377381
sumr = []
378382
suma = []
379383
sumn = []
@@ -418,7 +422,16 @@ def compute_input_stats(
418422
"sumr2": sumr2,
419423
"suma2": suma2,
420424
}
421-
self.merge_input_stats(stat_dict)
425+
return stat_dict
426+
427+
stat_dict = load_or_compute_se_input_stats(
428+
self,
429+
kwargs.get("stat_file_path"),
430+
last_dim=4,
431+
compute=compute_stats,
432+
mixed_types=True,
433+
)
434+
self.merge_input_stats(stat_dict)
422435

423436
def enable_compression(
424437
self,

deepmd/tf/descriptor/se_r.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
from .se import (
5151
DescrptSe,
5252
)
53+
from .stat import (
54+
load_or_compute_se_input_stats,
55+
)
5356

5457

5558
@Descriptor.register("se_e2_r")
@@ -274,17 +277,27 @@ def compute_input_stats(
274277
**kwargs
275278
Additional keyword arguments.
276279
"""
277-
sumr = []
278-
sumn = []
279-
sumr2 = []
280-
for cc, bb, tt, nn, mm in zip(
281-
data_coord, data_box, data_atype, natoms_vec, mesh, strict=True
282-
):
283-
sysr, sysr2, sysn = self._compute_dstats_sys_se_r(cc, bb, tt, nn, mm)
284-
sumr.append(sysr)
285-
sumn.append(sysn)
286-
sumr2.append(sysr2)
287-
stat_dict = {"sumr": sumr, "sumn": sumn, "sumr2": sumr2}
280+
281+
def compute_stats() -> dict[str, Any]:
282+
sumr = []
283+
sumn = []
284+
sumr2 = []
285+
for cc, bb, tt, nn, mm in zip(
286+
data_coord, data_box, data_atype, natoms_vec, mesh, strict=True
287+
):
288+
sysr, sysr2, sysn = self._compute_dstats_sys_se_r(cc, bb, tt, nn, mm)
289+
sumr.append(sysr)
290+
sumn.append(sysn)
291+
sumr2.append(sysr2)
292+
return {"sumr": sumr, "sumn": sumn, "sumr2": sumr2}
293+
294+
stat_dict = load_or_compute_se_input_stats(
295+
self,
296+
kwargs.get("stat_file_path"),
297+
last_dim=1,
298+
compute=compute_stats,
299+
mixed_types=False,
300+
)
288301
self.merge_input_stats(stat_dict)
289302

290303
def merge_input_stats(self, stat_dict: dict[str, Any]) -> None:

deepmd/tf/descriptor/se_t.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
from .se import (
5353
DescrptSe,
5454
)
55+
from .stat import (
56+
load_or_compute_se_input_stats,
57+
)
5558

5659

5760
@Descriptor.register("se_e3")
@@ -257,7 +260,8 @@ def compute_input_stats(
257260
**kwargs
258261
Additional keyword arguments.
259262
"""
260-
if True:
263+
264+
def compute_stats() -> dict[str, Any]:
261265
sumr = []
262266
suma = []
263267
sumn = []
@@ -281,7 +285,16 @@ def compute_input_stats(
281285
"sumr2": sumr2,
282286
"suma2": suma2,
283287
}
284-
self.merge_input_stats(stat_dict)
288+
return stat_dict
289+
290+
stat_dict = load_or_compute_se_input_stats(
291+
self,
292+
kwargs.get("stat_file_path"),
293+
last_dim=4,
294+
compute=compute_stats,
295+
mixed_types=False,
296+
)
297+
self.merge_input_stats(stat_dict)
285298

286299
def merge_input_stats(self, stat_dict: dict[str, Any]) -> None:
287300
"""Merge the statistics computed from compute_input_stats to obtain the self.davg and self.dstd.

deepmd/tf/descriptor/stat.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
from collections.abc import (
3+
Callable,
4+
)
5+
from typing import (
6+
Any,
7+
)
8+
9+
import numpy as np
10+
11+
from deepmd.common import (
12+
get_hash,
13+
)
14+
from deepmd.utils.path import (
15+
DPPath,
16+
)
17+
18+
19+
def _descriptor_rcut_smth(descrpt: Any) -> float:
20+
if hasattr(descrpt, "rcut_smth"):
21+
return descrpt.rcut_smth
22+
return descrpt.rcut_r_smth
23+
24+
25+
def _descriptor_sel(descrpt: Any, last_dim: int) -> list[int]:
26+
if hasattr(descrpt, "get_sel"):
27+
sel = descrpt.get_sel()
28+
elif last_dim == 1:
29+
sel = descrpt.sel_r
30+
else:
31+
sel = descrpt.sel_a
32+
if isinstance(sel, np.ndarray):
33+
sel = sel.tolist()
34+
elif isinstance(sel, int):
35+
sel = [sel]
36+
return [int(ii) for ii in sel]
37+
38+
39+
def _descriptor_stat_path(
40+
descrpt: Any,
41+
stat_file_path: DPPath | None,
42+
last_dim: int,
43+
mixed_types: bool,
44+
) -> DPPath | None:
45+
if stat_file_path is None:
46+
return None
47+
sel = _descriptor_sel(descrpt, last_dim)
48+
stat_hash = get_hash(
49+
{
50+
"type": "se_a" if last_dim == 4 else "se_r",
51+
"ntypes": descrpt.get_ntypes(),
52+
"rcut": round(descrpt.get_rcut(), 2),
53+
"rcut_smth": round(_descriptor_rcut_smth(descrpt), 2),
54+
"nsel": sum(sel),
55+
"sel": sel,
56+
"mixed_types": mixed_types,
57+
}
58+
)
59+
return stat_file_path / stat_hash
60+
61+
62+
def _stat_keys(ntypes: int, angular: bool) -> list[str]:
63+
keys = [f"r_{ii}" for ii in range(ntypes)]
64+
if angular:
65+
keys.extend(f"a_{ii}" for ii in range(ntypes))
66+
return keys
67+
68+
69+
def _load_se_input_stats(
70+
path: DPPath | None,
71+
ntypes: int,
72+
angular: bool,
73+
) -> dict[str, list[list[float]]] | None:
74+
if path is None or not path.is_dir():
75+
return None
76+
if any(not (path / kk).is_file() for kk in _stat_keys(ntypes, angular)):
77+
return None
78+
79+
sumr = []
80+
sumn = []
81+
sumr2 = []
82+
suma = []
83+
suma2 = []
84+
for type_i in range(ntypes):
85+
r_stat = (path / f"r_{type_i}").load_numpy()
86+
sumn.append(float(r_stat[0]))
87+
sumr.append(float(r_stat[1]))
88+
sumr2.append(float(r_stat[2]))
89+
if angular:
90+
a_stat = (path / f"a_{type_i}").load_numpy()
91+
suma.append(float(a_stat[1]) / 3.0)
92+
suma2.append(float(a_stat[2]) / 3.0)
93+
94+
ret = {
95+
"sumr": [sumr],
96+
"sumn": [sumn],
97+
"sumr2": [sumr2],
98+
}
99+
if angular:
100+
ret["suma"] = [suma]
101+
ret["suma2"] = [suma2]
102+
return ret
103+
104+
105+
def _save_se_input_stats(
106+
path: DPPath | None,
107+
stat_dict: dict[str, Any],
108+
ntypes: int,
109+
angular: bool,
110+
) -> None:
111+
if path is None:
112+
return
113+
path.mkdir(parents=True, exist_ok=True)
114+
115+
sumr = np.sum(stat_dict["sumr"], axis=0)
116+
sumn = np.sum(stat_dict["sumn"], axis=0)
117+
sumr2 = np.sum(stat_dict["sumr2"], axis=0)
118+
if angular:
119+
suma = np.sum(stat_dict["suma"], axis=0)
120+
suma2 = np.sum(stat_dict["suma2"], axis=0)
121+
122+
for type_i in range(ntypes):
123+
(path / f"r_{type_i}").save_numpy(
124+
np.array([sumn[type_i], sumr[type_i], sumr2[type_i]])
125+
)
126+
if angular:
127+
(path / f"a_{type_i}").save_numpy(
128+
np.array([3.0 * sumn[type_i], 3.0 * suma[type_i], 3.0 * suma2[type_i]])
129+
)
130+
131+
132+
def load_or_compute_se_input_stats(
133+
descrpt: Any,
134+
stat_file_path: DPPath | None,
135+
last_dim: int,
136+
compute: Callable[[], dict[str, Any]],
137+
mixed_types: bool = False,
138+
) -> dict[str, Any]:
139+
"""Load or compute SE descriptor input statistics using EnvMatStatSe format."""
140+
angular = last_dim == 4
141+
stat_path = _descriptor_stat_path(descrpt, stat_file_path, last_dim, mixed_types)
142+
stat_dict = _load_se_input_stats(stat_path, descrpt.get_ntypes(), angular)
143+
if stat_dict is not None:
144+
return stat_dict
145+
146+
stat_dict = compute()
147+
_save_se_input_stats(stat_path, stat_dict, descrpt.get_ntypes(), angular)
148+
return stat_dict

deepmd/tf/entrypoints/train.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
import json
99
import logging
1010
import time
11+
from pathlib import (
12+
Path,
13+
)
1114
from typing import (
1215
Any,
1316
)
1417

18+
import h5py
1519
import numpy as np
1620

1721
from deepmd.common import (
@@ -50,6 +54,9 @@
5054
from deepmd.utils.data_system import (
5155
get_data,
5256
)
57+
from deepmd.utils.path import (
58+
DPPath,
59+
)
5360

5461
__all__ = ["train"]
5562

@@ -232,6 +239,21 @@ def _do_work(
232239
# setup data modifier
233240
modifier = get_modifier(jdata["model"].get("modifier", None))
234241

242+
# extract stat_file from training parameters
243+
stat_file_path = None
244+
if not is_compress:
245+
stat_file_raw = jdata["training"].get("stat_file", None)
246+
if stat_file_raw is not None and run_opt.is_chief:
247+
stat_file_target = Path(stat_file_raw)
248+
stat_file_target.parent.mkdir(parents=True, exist_ok=True)
249+
if not stat_file_target.exists():
250+
if stat_file_raw.endswith((".h5", ".hdf5")):
251+
with h5py.File(stat_file_raw, "w") as f:
252+
pass
253+
else:
254+
stat_file_target.mkdir(parents=True, exist_ok=True)
255+
stat_file_path = DPPath(stat_file_raw, "a")
256+
235257
# decouple the training data from the model compress process
236258
train_data = None
237259
valid_data = None
@@ -289,7 +311,12 @@ def _do_work(
289311
origin_type_map = get_data(
290312
jdata["training"]["training_data"], rcut, None, modifier
291313
).get_type_map()
292-
model.build(train_data, stop_batch, origin_type_map=origin_type_map)
314+
model.build(
315+
train_data,
316+
stop_batch,
317+
origin_type_map=origin_type_map,
318+
stat_file_path=stat_file_path,
319+
)
293320

294321
if not is_compress:
295322
# train the model with the provided systems in a cyclic way

0 commit comments

Comments
 (0)