Skip to content

Commit b534a1c

Browse files
authored
feat(lmdb): support mixed-size batches and lazy label availability (#5962)
## Summary - support `mix:N` LMDB batches containing frames with different atom counts, using padded rectangular batches for dense models and a flat ragged node axis for eligible graph models - compact phantom atoms before graph-model evaluation and make loss reductions, validation weighting, and epoch sizing use real atom counts - resolve label availability lazily after data requirements are registered, so required, optional, defaulted, and partially available fields are handled without an eager full-dataset scan - keep non-mixing and native-spin models on their existing rectangular public paths; the ragged regression coverage uses upstream DPA1 and avoids model-specific dependencies ## Behavioral changes - Masked per-atom loss terms now pool all included labels across the batch instead of averaging per-frame means. This intentionally retires the bit-identical reduction guarantee from #5738/#5783 for existing `mixed_type` NPY datasets whose frames have different real atom counts: those frames are weighted by their real label counts rather than equally. Uniform-atom-count batches are unchanged. Hessian pair terms remain normalized per frame so their quadratic component count does not make large structures dominate a batch. - Legacy LMDB files have no exact per-frame label-availability metadata. To avoid an eager O(N) startup scan, the reader uses a bounded probe and conservatively reduces per-frame `find_*` flags at collation. A missed rare signature may discard valid supervision for the affected batch, but default-filled values are never treated as real labels. Recording exact availability metadata when generating LMDB datasets is tracked in #5954. ## Testing - all pre-commit hooks passed for the changed files - 375 passed, 2 skipped, 1 deselected, and 13 subtests passed in the main targeted LMDB/PT/PT-expt/model suite - 15 passed in the isolated loss-reduction and decoder-pool regression suite - review fixes: 100 passed, 2 skipped, and 2 subtests passed in the common loss suite; 60 passed in the PT padding-loss suite; all 25 LMDB training tests passed; padded/unpadded DPA2 graph and Hessian parity tests passed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added LMDB batching for frames with different atom counts, including mixed-size and ragged layouts. - Added ragged-batch inference and training for supported energy and spin models. - Added configurable data-source policies for optional labels and parameters. - Added safer handling of padded atoms across neighbor graphs and model outputs. - **Bug Fixes** - Improved per-atom loss normalization for uneven and padded batches. - Prevented padded atoms from affecting neighbor searches, metrics, or losses. - Improved handling of missing labels and default-valued data. - **Documentation** - Documented mixed-size batching, ragged data, and per-atom normalization. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Closes #5965
1 parent 62cd093 commit b534a1c

69 files changed

Lines changed: 7533 additions & 1655 deletions

Some content is hidden

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

deepmd/dpmodel/array_api.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
Version,
1212
)
1313

14+
from deepmd.dpmodel.common import (
15+
to_numpy_array,
16+
)
17+
1418
# Type alias for array_api compatible arrays
1519
Array = np.ndarray | Any # Any to support JAX, PyTorch, etc. arrays
1620

@@ -27,22 +31,28 @@ def xp_asarray_nodetach(
2731
``torch.asarray`` detaches its input from the autograd graph, so calling
2832
``xp.asarray`` on a weight attribute that is already a backend tensor
2933
(e.g. a ``torch.nn.Parameter`` registered by the pt_expt backend)
30-
silently breaks gradient flow to that weight. This helper converts
31-
genuine non-backend data (numpy arrays, python scalars/lists) via
32-
``xp.asarray``; backend tensors are returned as-is, with an optional
33-
differentiable dtype cast via ``xp.astype``.
34-
35-
The ``device`` argument only applies to the conversion path: backend
36-
tensors are assumed to already live on the working device (they are
37-
created together with the inputs).
34+
silently breaks gradient flow to that weight. Backend tensors already in
35+
``xp`` are therefore returned as-is, with an optional differentiable dtype
36+
cast via ``xp.astype``.
37+
38+
An array from another namespace cannot retain its autograd graph. It is
39+
converted through NumPy before entering ``xp``; this also performs the
40+
required device-to-host copy when a CUDA-backed model constant is consumed
41+
by a NumPy statistics path.
42+
43+
The ``device`` argument only applies to the conversion path. Arrays already
44+
in ``xp`` are assumed to live on the working device because model buffers
45+
and inputs are moved together.
3846
"""
39-
if isinstance(obj, np.ndarray) or not array_api_compat.is_array_api_obj(obj):
40-
if dtype is None:
41-
return xp.asarray(obj, device=device)
42-
return xp.asarray(obj, dtype=dtype, device=device)
43-
if dtype is not None and obj.dtype != dtype:
44-
obj = xp.astype(obj, dtype)
45-
return obj
47+
if array_api_compat.is_array_api_obj(obj):
48+
if array_api_compat.array_namespace(obj) is xp:
49+
if dtype is not None and obj.dtype != dtype:
50+
obj = xp.astype(obj, dtype)
51+
return obj
52+
obj = to_numpy_array(obj)
53+
if dtype is None:
54+
return xp.asarray(obj, device=device)
55+
return xp.asarray(obj, dtype=dtype, device=device)
4656

4757

4858
# array api adds take_along_axis in https://github.com/data-apis/array-api/pull/816

deepmd/dpmodel/descriptor/dpa4.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2186,7 +2186,7 @@ def _canonicalize_charge_spin(
21862186
raise ValueError("`charge_spin` is required for this SeZM descriptor.")
21872187
charge_spin = xp.reshape(
21882188
xp_asarray_nodetach(
2189-
xp, np.asarray(self.default_chg_spin), dtype=dtype, device=device
2189+
xp, self.default_chg_spin, dtype=dtype, device=device
21902190
),
21912191
(1, 2),
21922192
)

deepmd/dpmodel/loss/dos.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def call(
159159
)
160160
diff3d = local_pred - local_label # [nf, natoms, numb_dos]
161161
if "mask" in model_dict:
162-
# idiom 1: per-frame masked mean, then average over frames
162+
# Idiom 1 (per-atom masked mean, ncomp=numb_dos).
163163
maskf = xp.astype(model_dict["mask"], diff3d.dtype) # [nf, natoms]
164164
l2_local_loss_dos = masked_atom_mean(
165165
xp.square(diff3d), maskf, self.numb_dos
@@ -184,7 +184,7 @@ def call(
184184
)
185185
diff3d = local_pred_cdf - local_label_cdf # [nf, natoms, numb_dos]
186186
if "mask" in model_dict:
187-
# idiom 1: per-frame masked mean, then average over frames
187+
# Idiom 1 (per-atom masked mean, ncomp=numb_dos).
188188
maskf = xp.astype(model_dict["mask"], diff3d.dtype) # [nf, natoms]
189189
l2_local_loss_cdf = masked_atom_mean(
190190
xp.square(diff3d), maskf, self.numb_dos

0 commit comments

Comments
 (0)