Skip to content

Commit bb3c971

Browse files
authored
Merge pull request #3 from zirenjin/master
dpa_tools merge
2 parents d5df6fa + da3f26f commit bb3c971

44 files changed

Lines changed: 10117 additions & 0 deletions

Some content is hidden

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

deepmd/dpa_tools/README.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# dpa_tools
2+
3+
Fine-tuning, descriptor extraction, cross-validation, and data utilities for
4+
DPA-3 pretrained models. Lives as a self-contained subpackage of `deepmd-kit`
5+
at `deepmd.dpa_tools`.
6+
7+
## Relationship with deepmd-kit
8+
9+
`dpa_tools` sits on top of deepmd-kit without modifying any existing module:
10+
11+
- **Model loading**: `_backend.py` is the single choke point that imports
12+
`deepmd.pt.model.model.get_model` and `deepmd.pt.train.wrapper.ModelWrapper`
13+
to load DPA-3 checkpoints and extract descriptors. No other file in
14+
`dpa_tools` touches `deepmd.pt.*` directly.
15+
- **Training**: shells out to `dp --pt train` / `dp --pt freeze` /
16+
`dp --pt test`, auto-generating `input.json` config files.
17+
- **Inference**: deepmd-kit's built-in `DeepProperty` handles neural-network
18+
models; dpa_tools adds a lightweight frozen-descriptor + sklearn-head path.
19+
- **CLI**: registered as `dp dpa` subcommand group via `deepmd/main.py`.
20+
Torch and all DPA dependencies are loaded lazily — only when a `dp dpa ...`
21+
command actually runs.
22+
- **Lazy import**: `import deepmd.dpa_tools` does **not** trigger a `torch`
23+
import. `dp dpa --help` is equally lightweight.
24+
25+
## Python API
26+
27+
```python
28+
from deepmd.dpa_tools import (
29+
DPAFineTuner, # train (frozen sklearn / finetune / linear probe)
30+
DPAPredictor, # read-only inference from frozen bundles
31+
MFTFineTuner, # multi-task fine-tuning
32+
DPATrainer, # single-task dp --pt train wrapper
33+
extract_descriptors, # standalone descriptor extraction
34+
cross_validate, # leak-proof cross-validation
35+
train_test_split, # formula-grouped data splitting
36+
# data tools
37+
convert, # structure file → deepmd/npy
38+
batch_convert, # glob-based batch conversion
39+
check_data, # data sanity checks
40+
attach_labels, # inject external label arrays
41+
load_dataset, # label-filtered data loading
42+
)
43+
```
44+
45+
### DPAFineTuner
46+
47+
Four training strategies:
48+
49+
| Strategy | Description | Best for |
50+
|----------|------------|----------|
51+
| `frozen_sklearn` | Freeze descriptor, extract once, fit sklearn head (RF/Ridge/MLP) | Small data (<1k samples), CPU inference |
52+
| `linear_probe` | Freeze backbone, train property fitting net only | Medium data, GPU |
53+
| `finetune` | Full-network fine-tuning | Larger data, GPU |
54+
| `scratch` | Train from random init (experimental) | Large-scale data only |
55+
56+
```python
57+
model = DPAFineTuner(
58+
pretrained="/path/to/DPA-3.1-3M.pt",
59+
strategy="frozen_sklearn",
60+
predictor="rf",
61+
pooling="mean",
62+
)
63+
model.fit(train_data="/data/train", target_key="homo")
64+
model.predict("/data/test")
65+
model.freeze("model.dp-sklearn.pth")
66+
```
67+
68+
### DPAPredictor
69+
70+
```python
71+
pred = DPAPredictor("model.dp-sklearn.pth")
72+
result = pred.predict("/data/test") # DotDict with .predictions
73+
metrics = pred.evaluate("/data/test") # DotDict with .mae, .rmse, .r2
74+
75+
# uncertainty: RF native, MLP via committee, Ridge raises
76+
result = pred.predict("/data/test", return_uncertainty=True)
77+
# → .predictions, .uncertainty
78+
```
79+
80+
### MFTFineTuner
81+
82+
Joint downstream property head + auxiliary force-field head (arXiv:2601.08486):
83+
84+
```python
85+
mft = MFTFineTuner(
86+
pretrained="/path/to/DPA-3.1-3M.pt",
87+
downstream_task_type="property",
88+
property_name="homo",
89+
aux_branch="MP_traj_v024_alldata_mixu",
90+
)
91+
mft.fit(train_data="/data/qm9", aux_data="/data/spice2")
92+
mft.evaluate("/data/qm9_test")
93+
```
94+
95+
### Descriptor extraction
96+
97+
```python
98+
X = extract_descriptors(
99+
"/data/systems",
100+
pretrained="/path/to/DPA-3.1-3M.pt",
101+
pooling="mean+std",
102+
)
103+
# → np.ndarray (n_frames, feat_dim * 2)
104+
```
105+
106+
### Cross-validation
107+
108+
Formula-grouped to prevent same-molecule leakage:
109+
110+
```python
111+
from deepmd.dpa_tools import cross_validate, train_test_split
112+
113+
systems = load_dataset("/data/root", label_key="energy")
114+
train, valid, test = train_test_split(systems, group_by="formula", seed=42)
115+
116+
result = cross_validate(model, systems, label_key="energy", cv=5, group_by="formula")
117+
# → {"aggregate": {"mae_mean": ..., "rmse_std": ...}, ...}
118+
```
119+
120+
### Data tools
121+
122+
```python
123+
convert("POSCAR", "output_dir", fmt="vasp/poscar", type_map=["Cu", "O"])
124+
batch_convert("calcs/**/OUTCAR", "npy_root", fmt="vasp/outcar")
125+
check_data("/data/system") # → list[Issue]
126+
attach_labels(system, head="bandgap", values=np.array([1.0, 2.0, 3.0]))
127+
```
128+
129+
## CLI
130+
131+
All commands live under `dp dpa` with two-level nesting:
132+
133+
```
134+
dp dpa
135+
extract-descriptors extract pooled DPA descriptors to .npy
136+
fit train a model (any strategy)
137+
mft multi-task fine-tuning
138+
cv cross-validate frozen_sklearn baseline
139+
predict predict with a frozen .pth bundle
140+
evaluate evaluate a frozen .pth against stored labels
141+
data
142+
convert structure file → deepmd/npy
143+
batch-convert glob-based batch conversion
144+
validate sanity-check deepmd/npy directories
145+
attach-labels inject .npy labels into a system
146+
```
147+
148+
`dp dpa --help` does not load torch. The parser is pure argparse in
149+
`deepmd/main.py`; the handler import happens lazily in
150+
`deepmd/entrypoints/main.py` only when `dp dpa ...` is invoked.
151+
152+
```bash
153+
dp dpa fit \
154+
--train-data /data/train \
155+
--pretrained /path/to/DPA-3.1-3M.pt \
156+
--strategy frozen_sklearn \
157+
--predictor rf \
158+
--target-key homo
159+
160+
dp dpa extract-descriptors \
161+
--data /data/sys1 /data/sys2 \
162+
--pretrained /path/to/DPA-3.1-3M.pt \
163+
--pooling mean+std \
164+
--output features.npy
165+
166+
dp dpa mft \
167+
--data /data/qm9 \
168+
--aux-data /data/spice2 \
169+
--pretrained /path/to/DPA-3.1-3M.pt \
170+
--property-name homo
171+
172+
dp dpa data convert --input POSCAR --output npy_dir --fmt vasp/poscar
173+
dp dpa data validate --data /data/sys1 /data/sys2
174+
```
175+
176+
## Installation
177+
178+
```bash
179+
pip install deepmd-kit[dpa-tools]
180+
```
181+
182+
The `dpa-tools` extra brings in `scikit-learn`. `torch` and `dpdata` are
183+
already provided by deepmd-kit's core dependencies.
184+
185+
## Internal architecture
186+
187+
```
188+
deepmd/dpa_tools/
189+
├── __init__.py # public API, lazy imports (no torch at import time)
190+
├── _backend.py # single choke point for deepmd.pt.* calls
191+
├── cli.py # dp dpa subcommand handlers
192+
├── finetuner.py # DPAFineTuner (training + descriptor extraction)
193+
├── predictor.py # DPAPredictor (read-only inference + uncertainty)
194+
├── mft.py # MFTFineTuner (multi-task fine-tuning)
195+
├── trainer.py # DPATrainer (dp --pt train subprocess wrapper)
196+
├── cv.py # cross-validation + data splitting
197+
├── conditions.py # scalar condition manager (T, P)
198+
├── config/
199+
│ └── manager.py # MFT input.json generation
200+
├── data/
201+
│ ├── loader.py # polymorphic data loading
202+
│ ├── dataset.py # label-filtered loading
203+
│ ├── convert.py # format conversion
204+
│ ├── validate.py # data sanity checks
205+
│ ├── desc_cache.py # two-tier descriptor cache
206+
│ ├── type_map.py # automatic type-map resolution
207+
│ └── errors.py # DPADataError
208+
└── utils/
209+
├── dotdict.py # DotDict
210+
└── sklearn_heads.py # sklearn regressor factory
211+
```
212+
213+
Key design points:
214+
- `_backend.py` is the **only** file that imports `deepmd.pt.*` — every call
215+
into deepmd internals goes through it
216+
- `_DescriptorExtraction` encapsulates the fragile chain
217+
`wrapper.model["Default"]``set_eval_descriptor_hook``forward_common`
218+
`eval_descriptor()`
219+
- `dp --pt train/test/freeze` always runs as a subprocess, keeping
220+
dpa_tools decoupled from deepmd-kit's training entry points
221+
- `dpdata.System` is the universal internal data format

deepmd/dpa_tools/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
"""DPA tools — fine-tuning, descriptor extraction, cross-validation, and data
3+
utilities for DPA-3 pretrained models.
4+
"""
5+
6+
__version__ = "0.1.0"
7+
8+
from .conditions import ConditionManager, DPAConditionError
9+
from .cv import cross_validate, train_test_split
10+
from .data import attach_labels, batch_convert, check_data, convert, load_dataset
11+
from .finetuner import DPAFineTuner, extract_descriptors
12+
from .mft import MFTFineTuner
13+
from .predictor import DPAPredictor
14+
from .trainer import DPATrainer
15+
16+
__all__ = [
17+
"ConditionManager",
18+
"DPAConditionError",
19+
"DPAFineTuner",
20+
"DPAPredictor",
21+
"DPATrainer",
22+
"MFTFineTuner",
23+
"attach_labels",
24+
"batch_convert",
25+
"check_data",
26+
"convert",
27+
"cross_validate",
28+
"extract_descriptors",
29+
"load_dataset",
30+
"train_test_split",
31+
]

deepmd/dpa_tools/_backend.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
"""Single chokepoint for all ``deepmd`` internal API and ``torch`` calls.
3+
4+
Every import from ``deepmd.pt.*``, ``deepmd.utils.model_branch_dict``, or
5+
``torch`` that is needed by the rest of ``deepmd.dpa_tools`` must go through
6+
this module. No other file in ``dpa_tools`` may import those packages directly.
7+
8+
All functions that load ``torch`` or ``deepmd.pt`` keep the import inside the
9+
function body so that importing this module is cheap.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from typing import Any
15+
16+
# ``get_model_dict`` is backend-agnostic and lightweight — safe at module level.
17+
from deepmd.utils.model_branch_dict import get_model_dict as _get_model_dict
18+
19+
20+
# ---------------------------------------------------------------------------
21+
# torch I/O
22+
# ---------------------------------------------------------------------------
23+
24+
25+
def load_torch_file(path: str, map_location: str = "cpu") -> dict[str, Any]:
26+
"""Load a PyTorch checkpoint or frozen bundle.
27+
28+
Always uses ``weights_only=False`` because deepmd checkpoints carry
29+
``_extra_state`` (non-tensor metadata) and dpa_tools frozen bundles
30+
carry ``sklearn`` pipeline objects.
31+
"""
32+
import torch
33+
34+
return torch.load(path, map_location=map_location, weights_only=False)
35+
36+
37+
# ---------------------------------------------------------------------------
38+
# model construction
39+
# ---------------------------------------------------------------------------
40+
41+
42+
def build_model_from_config(input_param: dict[str, Any]):
43+
"""Build a (non-JIT) DPA model from an input-parameter dict.
44+
45+
Returns a ``ModelWrapper`` whose inner model is accessible as
46+
``wrapper.model["Default"]``.
47+
"""
48+
from deepmd.pt.model.model import get_model
49+
from deepmd.pt.train.wrapper import ModelWrapper
50+
51+
model = get_model(input_param)
52+
return ModelWrapper(model)
53+
54+
55+
# ---------------------------------------------------------------------------
56+
# multi-task branch helpers
57+
# ---------------------------------------------------------------------------
58+
59+
60+
def resolve_model_branch(model_dict: dict[str, Any]) -> tuple[dict[str, str], str]:
61+
"""Resolve multi-task model-branch aliases.
62+
63+
Returns ``(alias_dict, model_dict)`` — the same tuple shape as the
64+
upstream ``get_model_dict``.
65+
"""
66+
return _get_model_dict(model_dict)
67+
68+
69+
# ---------------------------------------------------------------------------
70+
# device
71+
# ---------------------------------------------------------------------------
72+
73+
74+
def get_torch_device() -> Any:
75+
"""Return ``torch.device("cuda")`` if a GPU is available, else CPU."""
76+
import torch
77+
78+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
79+
80+
81+
# ---------------------------------------------------------------------------
82+
# descriptor extraction (the fragile chain)
83+
# ---------------------------------------------------------------------------
84+
85+
86+
class _DescriptorExtraction:
87+
"""Thin wrapper around a loaded model that runs a *single* forward pass
88+
with ``eval_descriptor_hook`` enabled and returns per-atom descriptors.
89+
90+
This is the lowest-level building block. Callers (like
91+
``DPAFineTuner._extract_features``) are responsible for pooling,
92+
batching, and tensor creation.
93+
"""
94+
95+
def __init__(self, wrapper) -> None:
96+
inner = wrapper.model["Default"]
97+
self._inner_model = inner
98+
self._atomic_model = inner.atomic_model
99+
100+
def _enable_hook(self) -> None:
101+
self._atomic_model.set_eval_descriptor_hook(True)
102+
103+
def _disable_hook(self) -> None:
104+
self._atomic_model.set_eval_descriptor_hook(False)
105+
106+
def _clear_accumulator(self) -> None:
107+
self._atomic_model.eval_descriptor_list.clear()
108+
109+
def _run_forward(self, coord, atype, box):
110+
"""Run ``forward_common`` and return per-atom descriptors (detached).
111+
112+
Parameters
113+
----------
114+
coord : torch.Tensor
115+
(n_frames, n_atoms*3), float64, requires_grad.
116+
atype : torch.Tensor
117+
(n_frames, n_atoms), int64.
118+
box : torch.Tensor
119+
(n_frames, 9), float64.
120+
121+
Returns
122+
-------
123+
torch.Tensor
124+
(n_frames, n_atoms, feat_dim), detached.
125+
"""
126+
self._clear_accumulator()
127+
self._inner_model.forward_common(coord, atype, box)
128+
return self._atomic_model.eval_descriptor().detach()

0 commit comments

Comments
 (0)