Skip to content

Commit 625646c

Browse files
authored
perf: bound memory in model testing and output statistics (deepmodeling#5944)
## Summary - evaluate `dp test` in atom-bounded chunks, with lazy on-demand decoding for LMDB inputs - preserve run-level MAE/RMSE aggregation and detail output across chunks while consolidating backend-independent test logic under `deepmd.infer.model_test` - build the neighbor representation declared by each atomic model during output-bias statistics, using `NeighborGraph` for graph-native models and the existing dense neighbor list for other models ## Why Large LMDB datasets were fully decoded before frame selection, so even a small test run could require loading tens of millions of frames. Separately, graph-native atomic models were routed through a fixed-capacity dense neighbor list while calibrating output bias; because those models do not declare a finite neighbor capacity, that path could allocate tens of gigabytes and fail before fine-tuning began. This change bounds LMDB decoding and model-evaluation memory while preserving the existing evaluation and output-statistics contracts. Ordinary `DeepmdData` inputs still materialize one test system before chunked evaluation. ## Additional correctness fixes - preserve legacy TensorFlow spin-model dispatch and spin virial/stress reporting - use canonical atomic tensor labels consistently across NPY and LMDB inputs - flatten atomic parameters correctly for graph-native output statistics - keep LMDB frame selection deterministic and honor availability subgroups during iteration - keep detail files isolated across systems and emit a single header across chunks - require atomic property labels when atomic metrics are requested ## Checks - `ruff check .` - `ruff format --check .` - targeted common model-test and LMDB regression tests - targeted PyTorch streaming/output-stat regression tests - `git diff --check` The TensorFlow atomic dipole/polar integration cases could not be run locally because this checkout has no built TensorFlow backend; they are left to CI. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Expanded model evaluation for energy, spin, DOS, property, dipole, and polarizability models. * Added chunked testing, frame limits, per-frame details, and atomic-level metrics. * **Improvements** * Large LMDB test datasets now load frames lazily for improved efficiency. * Added clearer weighted aggregation of MAE and RMSE results. * Graph-capable models now use graph-based neighbor representations during inference. * **Bug Fixes** * Improved handling of periodic systems, mixed atom counts, exclusions, optional outputs, missing labels, and atomic label naming. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 6330a2f commit 625646c

16 files changed

Lines changed: 2196 additions & 1644 deletions

File tree

deepmd/dpmodel/atomic_model/base_atomic_model.py

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -805,9 +805,22 @@ def _store_out_stat(
805805
self.out_std = out_std_data
806806

807807
def _get_forward_wrapper_func(self) -> Callable[..., dict[str, np.ndarray]]:
808-
"""Get a forward wrapper of the atomic model for output bias calculation."""
808+
"""Get a forward wrapper of the atomic model for output bias calculation.
809+
810+
The wrapper starts from raw coordinates and therefore has to construct
811+
the neighbor representation itself. It builds the one this model
812+
declares through :meth:`uses_graph_lower`: a carry-all
813+
``NeighborGraph`` for graph-native models, whose neighbor count follows
814+
the geometry, or the fixed-capacity neighbor list sized by
815+
:meth:`get_sel` otherwise. Sizing a dense list from ``get_sel`` is not
816+
merely wasteful for a graph-native model -- such a model reports no
817+
finite capacity, so the allocation is unbounded.
818+
"""
809819
import array_api_compat
810820

821+
from deepmd.dpmodel.utils.neighbor_graph import (
822+
build_neighbor_graph,
823+
)
811824
from deepmd.dpmodel.utils.nlist import (
812825
extend_input_and_build_neighbor_list,
813826
)
@@ -841,31 +854,64 @@ def model_forward(
841854
if charge_spin is not None:
842855
charge_spin = xp.asarray(charge_spin, device=device)
843856

844-
(
845-
extended_coord,
846-
extended_atype,
847-
mapping,
848-
nlist,
849-
) = extend_input_and_build_neighbor_list(
850-
coord,
851-
atype,
852-
self.get_rcut(),
853-
self.get_sel(),
854-
mixed_types=self.mixed_types(),
855-
box=box,
856-
# exclusion is a nlist-BUILD transform (decision #18/A4);
857-
# forward_common_atomic consumes a pre-excluded nlist.
858-
pair_excl=self.pair_excl,
859-
)
860-
atomic_ret = self.forward_common_atomic(
861-
extended_coord,
862-
extended_atype,
863-
nlist,
864-
mapping=mapping,
865-
fparam=fparam,
866-
aparam=aparam,
867-
charge_spin=charge_spin,
868-
)
857+
if self.uses_graph_lower():
858+
nframes, nloc = atype.shape
859+
# Pair exclusion is a neighbor-BUILD transform (decision
860+
# #18/A4) on both routes; the graph builder folds it into
861+
# ``edge_mask``.
862+
graph = build_neighbor_graph(
863+
coord,
864+
atype,
865+
box,
866+
self.get_rcut(),
867+
pair_excl=self.pair_excl,
868+
)
869+
atomic_ret = self.forward_common_atomic_graph(
870+
graph,
871+
xp.reshape(atype, (-1,)),
872+
fparam=fparam,
873+
aparam=(
874+
xp.reshape(
875+
aparam,
876+
(nframes * nloc, self.get_dim_aparam()),
877+
)
878+
if aparam is not None
879+
else None
880+
),
881+
charge_spin=charge_spin,
882+
)
883+
# The graph route works on a flat node axis; restore the
884+
# per-frame layout the dense route returns.
885+
atomic_ret = {
886+
kk: xp.reshape(vv, (nframes, nloc, *vv.shape[1:]))
887+
for kk, vv in atomic_ret.items()
888+
}
889+
else:
890+
(
891+
extended_coord,
892+
extended_atype,
893+
mapping,
894+
nlist,
895+
) = extend_input_and_build_neighbor_list(
896+
coord,
897+
atype,
898+
self.get_rcut(),
899+
self.get_sel(),
900+
mixed_types=self.mixed_types(),
901+
box=box,
902+
# exclusion is a nlist-BUILD transform (decision #18/A4);
903+
# forward_common_atomic consumes a pre-excluded nlist.
904+
pair_excl=self.pair_excl,
905+
)
906+
atomic_ret = self.forward_common_atomic(
907+
extended_coord,
908+
extended_atype,
909+
nlist,
910+
mapping=mapping,
911+
fparam=fparam,
912+
aparam=aparam,
913+
charge_spin=charge_spin,
914+
)
869915
# Convert outputs back to numpy arrays
870916
return {kk: to_numpy_array(vv) for kk, vv in atomic_ret.items()}
871917

0 commit comments

Comments
 (0)