Skip to content

Commit 96604dd

Browse files
author
Han Wang
committed
Merge upstream/master into feat-graph-dpa2
Conflict: source/api_cc/src/common.cc -- both sides fixed the same daparam-stride aparam buffer sizing (deepmodeling#5790 upstream, e95f2ba here); code identical, comment text taken from upstream so the hunk drops out of this PR's diff. deepmd/dpmodel/descriptor/dpa1.py auto-merged with master's deepmodeling#5785 (dense call is unconditionally _call_dense; verified post-merge).
2 parents 002713d + 6c3b985 commit 96604dd

23 files changed

Lines changed: 841 additions & 278 deletions

.github/workflows/test_cc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
if: matrix.enable_paddle
7777
- run: |
7878
export LD_LIBRARY_PATH=${{ github.workspace }}/dp_test/lib:$LD_LIBRARY_PATH
79-
pytest --cov=deepmd source/lmp/tests
79+
source/install/retry_on_sigterm.sh 3 pytest --cov=deepmd source/lmp/tests
8080
env:
8181
OMP_NUM_THREADS: 1
8282
TF_INTRA_OP_PARALLELISM_THREADS: 1

deepmd/dpmodel/descriptor/dpa1.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -613,18 +613,25 @@ def call(
613613
sw
614614
The smooth switch function.
615615
"""
616-
xp = array_api_compat.array_namespace(coord_ext, atype_ext, nlist)
617-
nloc = nlist.shape[1]
618-
nall = xp.reshape(coord_ext, (nlist.shape[0], -1)).shape[1] // 3
619-
# graph-eligible configs route through the graph-native adapter (decision
620-
# #14: graph = single math source, dense call = thin adapter). Ineligible
621-
# configs (compressed descriptors) and the ghost case with no mapping
622-
# fall back to the legacy dense body. The graph needs `mapping` to fold
623-
# ghosts to local owners; without it only nall == nloc is valid.
624-
if self.uses_graph_lower() and (mapping is not None or nall == nloc):
625-
return self._call_graph_adapter(coord_ext, atype_ext, nlist, mapping)
626-
else:
627-
return self._call_dense(coord_ext, atype_ext, nlist)
616+
# The dense ``call`` always runs the legacy dense body -- it is the
617+
# cross-backend consistency reference and must match the tf/pt/pd/jax
618+
# dense descriptors bit-for-bit. It previously routed graph-eligible
619+
# configs through ``_call_graph_adapter`` (decision #14), but the
620+
# adapter is bit-exact ONLY in the trivial-statistics regime
621+
# (``davg == 0``): the dense se_atten body leaks a phantom
622+
# padding-neighbor ``-davg/dstd`` residual (``EnvMat.call`` subtracts
623+
# ``davg`` AFTER the padding rows' geometry is weight-zeroed, and
624+
# neither the empty ``exclude_types`` mask nor the attention layers
625+
# re-mask it) that the graph path deliberately omits -- the graph
626+
# output is the physically correct one, but ``call`` must reproduce
627+
# the dense reference. The former gate also made ``mapping`` -- an
628+
# argument that only enables ghost folding on graph routes -- silently
629+
# change the numerics of a dense call. The graph-native route is
630+
# reached exclusively through :meth:`call_graph` (pt_expt
631+
# ``forward_atomic_graph`` and the graph ``.pt2``), never through
632+
# ``call``. ``_call_graph_adapter`` is retained as the
633+
# bit-exact-regime reference exercised by the adapter parity tests.
634+
return self._call_dense(coord_ext, atype_ext, nlist)
628635

629636
def _call_graph_adapter(
630637
self,
@@ -633,14 +640,27 @@ def _call_graph_adapter(
633640
nlist: Array,
634641
mapping: Array | None,
635642
) -> Array:
636-
"""Regime-1 dense->graph adapter (the eligible ``call`` path).
643+
"""Regime-1 dense->graph adapter.
637644
638645
Builds a NeighborGraph from the dense quartet with the SHAPE-STATIC
639646
converter (``compact=False``, so this is jit/export-traceable -- no
640647
``nonzero``), runs :meth:`call_graph`, and reconstructs the dense-shaped
641-
``sw``. Preserves the dense 5-tuple ABI exactly; masked invalid edges
642-
contribute zero in ``call_graph``'s ``segment_sum`` so the output is
643-
identical to the legacy dense body.
648+
``sw``. Preserves the dense 5-tuple ABI; masked invalid edges
649+
contribute zero in ``call_graph``'s ``segment_sum``.
650+
651+
Bit-exact vs :meth:`_call_dense` **only in the trivial-statistics
652+
regime** (``davg == 0``). For nonzero ``davg`` the dense body leaks a
653+
phantom padding-neighbor ``-davg/dstd`` residual into every padding
654+
slot (``EnvMat.call`` subtracts ``davg`` AFTER the padding geometry is
655+
weight-zeroed; with empty ``exclude_types`` nothing re-masks it, at
656+
any ``attn_layer``) that the graph path deliberately omits. The graph
657+
kernel additionally applies the slot-0 statistics ``[:, 0, :]`` to
658+
every edge -- exact for real stat-computed tables (slot-uniform by
659+
construction), not for artificially slot-varying ones. This is why
660+
the dense :meth:`call` does NOT route here: it is the cross-backend
661+
consistency reference. This method is retained as the
662+
bit-exact-regime reference exercised by the adapter parity tests; the
663+
production graph route is :meth:`call_graph`.
644664
645665
Parameters
646666
----------

deepmd/dpmodel/loss/dos.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
from deepmd.dpmodel.loss.loss import (
1212
Loss,
1313
)
14+
from deepmd.dpmodel.loss.reduction import (
15+
masked_atom_mean,
16+
masked_atom_num,
17+
)
1418
from deepmd.utils.data import (
1519
DataRequirementItem,
1620
)
@@ -134,11 +138,9 @@ def call(
134138
if "mask" in model_dict:
135139
# idiom 1: per-frame masked mean, then average over frames
136140
maskf = xp.astype(model_dict["mask"], diff3d.dtype) # [nf, natoms]
137-
nf = diff3d.shape[0]
138-
sq = xp.square(diff3d) * xp.reshape(maskf, (nf, natoms, 1))
139-
per_frame_sum = xp.sum(xp.reshape(sq, (nf, -1)), axis=-1) # [nf]
140-
per_frame_dof = xp.sum(maskf, axis=-1) * self.numb_dos # [nf]
141-
l2_local_loss_dos = xp.mean(per_frame_sum / per_frame_dof)
141+
l2_local_loss_dos = masked_atom_mean(
142+
xp.square(diff3d), maskf, self.numb_dos
143+
)
142144
else:
143145
l2_local_loss_dos = xp.mean(xp.square(diff3d))
144146
loss += pref_ados * l2_local_loss_dos
@@ -161,11 +163,9 @@ def call(
161163
if "mask" in model_dict:
162164
# idiom 1: per-frame masked mean, then average over frames
163165
maskf = xp.astype(model_dict["mask"], diff3d.dtype) # [nf, natoms]
164-
nf = diff3d.shape[0]
165-
sq = xp.square(diff3d) * xp.reshape(maskf, (nf, natoms, 1))
166-
per_frame_sum = xp.sum(xp.reshape(sq, (nf, -1)), axis=-1) # [nf]
167-
per_frame_dof = xp.sum(maskf, axis=-1) * self.numb_dos # [nf]
168-
l2_local_loss_cdf = xp.mean(per_frame_sum / per_frame_dof)
166+
l2_local_loss_cdf = masked_atom_mean(
167+
xp.square(diff3d), maskf, self.numb_dos
168+
)
169169
else:
170170
l2_local_loss_cdf = xp.mean(xp.square(diff3d))
171171
loss += pref_acdf * l2_local_loss_cdf
@@ -181,12 +181,7 @@ def call(
181181
diff = global_pred - global_label
182182
# idiom 3: global dos is already padding-invariant; plain mean suffices
183183
l2_global_loss_dos = xp.mean(xp.square(diff))
184-
if "mask" in model_dict:
185-
atom_num = xp.mean(
186-
xp.astype(xp.sum(model_dict["mask"], axis=-1), diff.dtype)
187-
)
188-
else:
189-
atom_num = natoms
184+
atom_num = masked_atom_num(model_dict.get("mask"), natoms, diff.dtype)
190185
loss += pref_dos * l2_global_loss_dos
191186
more_loss["rmse_global_dos"] = self.display_if_exist(
192187
xp.sqrt(l2_global_loss_dos) / atom_num, find_global
@@ -204,12 +199,7 @@ def call(
204199
diff = global_pred_cdf - global_label_cdf
205200
# idiom 3: global cdf is already padding-invariant; plain mean suffices
206201
l2_global_loss_cdf = xp.mean(xp.square(diff))
207-
if "mask" in model_dict:
208-
atom_num = xp.mean(
209-
xp.astype(xp.sum(model_dict["mask"], axis=-1), diff.dtype)
210-
)
211-
else:
212-
atom_num = natoms
202+
atom_num = masked_atom_num(model_dict.get("mask"), natoms, diff.dtype)
213203
loss += pref_cdf * l2_global_loss_cdf
214204
more_loss["rmse_global_cdf"] = self.display_if_exist(
215205
xp.sqrt(l2_global_loss_cdf) / atom_num, find_global

deepmd/dpmodel/loss/ener.py

Lines changed: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
from deepmd.dpmodel.loss.loss import (
1212
Loss,
1313
)
14+
from deepmd.dpmodel.loss.reduction import (
15+
masked_atom_mean,
16+
per_frame_component_mean,
17+
)
1418
from deepmd.utils.data import (
1519
DataRequirementItem,
1620
)
@@ -296,7 +300,7 @@ def call(
296300
if maskf is not None:
297301
# Idiom 2 (extensive): per-frame normalization by real-atom count.
298302
se = xp.square(energy - energy_hat) # [nf, k]
299-
per_frame = xp.mean(xp.reshape(se, (_nf, -1)), axis=-1) # [nf]
303+
per_frame = per_frame_component_mean(se) # [nf]
300304
if not self.use_huber:
301305
loss += pref_e * xp.mean(per_frame * inv**norm_exp)
302306
else:
@@ -327,9 +331,7 @@ def call(
327331
l1_ener_loss = xp.mean(xp.abs(energy - energy_hat))
328332
if maskf is not None:
329333
abs_e = xp.abs(energy - energy_hat) # [nf, k]
330-
per_frame_ae = xp.mean(
331-
xp.reshape(abs_e, (_nf, -1)), axis=-1
332-
) # [nf]
334+
per_frame_ae = per_frame_component_mean(abs_e) # [nf]
333335
l1_ener_masked = xp.mean(per_frame_ae * inv)
334336
loss += pref_e * l1_ener_masked
335337
more_loss["mae_e"] = self.display_if_exist(
@@ -346,8 +348,7 @@ def call(
346348
)
347349
if mae:
348350
if maskf is not None:
349-
abs_e = xp.abs(energy - energy_hat)
350-
per_frame_ae = xp.mean(xp.reshape(abs_e, (_nf, -1)), axis=-1)
351+
per_frame_ae = per_frame_component_mean(xp.abs(energy - energy_hat))
351352
mae_e = xp.mean(per_frame_ae * inv)
352353
else:
353354
mae_e = xp.mean(xp.abs(energy - energy_hat)) * atom_norm_ener
@@ -362,10 +363,7 @@ def call(
362363
diff_f_3d = xp.reshape(diff_f, (_nf, _nloc, 3)) # [nf, nloc, 3]
363364
maskf_col = xp.reshape(maskf, (_nf, _nloc, 1)) # [nf, nloc, 1]
364365
# Masked MSE computed for rmse_f display regardless of use_huber.
365-
sq_f = xp.square(diff_f_3d) * maskf_col # [nf, nloc, 3]
366-
_pfs = xp.sum(xp.reshape(sq_f, (_nf, -1)), axis=-1) # [nf]
367-
_pfd = xp.sum(maskf, axis=-1) * 3 # [nf]
368-
l2_force_masked = xp.mean(_pfs / _pfd)
366+
l2_force_masked = masked_atom_mean(xp.square(diff_f_3d), maskf, 3)
369367
if not self.use_huber:
370368
loss += pref_f * l2_force_masked
371369
else:
@@ -435,12 +433,8 @@ def call(
435433
elif self.loss_func == "mae":
436434
if maskf is not None:
437435
diff_f_3d = xp.reshape(diff_f, (_nf, _nloc, 3))
438-
maskf_col = xp.reshape(maskf, (_nf, _nloc, 1))
439436
if not self.f_use_norm:
440-
abs_f = xp.abs(diff_f_3d) * maskf_col # [nf, nloc, 3]
441-
per_frame_sum = xp.sum(xp.reshape(abs_f, (_nf, -1)), axis=-1)
442-
per_frame_dof = xp.sum(maskf, axis=-1) * 3
443-
l1_force_masked = xp.mean(per_frame_sum / per_frame_dof)
437+
l1_force_masked = masked_atom_mean(xp.abs(diff_f_3d), maskf, 3)
444438
else:
445439
diff_3 = xp.reshape(force_hat - force, (_nf, _nloc, 3))
446440
norm_2d = xp.reshape(
@@ -474,11 +468,7 @@ def call(
474468
if mae:
475469
if maskf is not None:
476470
diff_f_3d = xp.reshape(diff_f, (_nf, _nloc, 3))
477-
maskf_col = xp.reshape(maskf, (_nf, _nloc, 1))
478-
abs_f = xp.abs(diff_f_3d) * maskf_col
479-
per_frame_sum = xp.sum(xp.reshape(abs_f, (_nf, -1)), axis=-1)
480-
per_frame_dof = xp.sum(maskf, axis=-1) * 3
481-
mae_f = xp.mean(per_frame_sum / per_frame_dof)
471+
mae_f = masked_atom_mean(xp.abs(diff_f_3d), maskf, 3)
482472
else:
483473
mae_f = xp.mean(xp.abs(diff_f))
484474
more_loss["mae_f"] = self.display_if_exist(mae_f, find_force)
@@ -494,7 +484,7 @@ def call(
494484
v2d = xp.reshape(virial, (_nf, 9))
495485
v_hat_2d = xp.reshape(virial_hat, (_nf, 9))
496486
se_v = xp.square(v_hat_2d - v2d) # [nf, 9]
497-
per_frame_v = xp.mean(se_v, axis=-1) # [nf]
487+
per_frame_v = per_frame_component_mean(se_v) # [nf]
498488
if not self.use_huber:
499489
loss += pref_v * xp.mean(per_frame_v * inv**norm_exp)
500490
else:
@@ -526,8 +516,9 @@ def call(
526516
if maskf is not None:
527517
v2d = xp.reshape(virial, (_nf, 9))
528518
v_hat_2d = xp.reshape(virial_hat, (_nf, 9))
529-
abs_v = xp.abs(v_hat_2d - v2d) # [nf, 9]
530-
per_frame_v = xp.mean(abs_v, axis=-1) # [nf]
519+
per_frame_v = per_frame_component_mean(
520+
xp.abs(v_hat_2d - v2d)
521+
) # [nf]
531522
l1_virial_masked = xp.mean(per_frame_v * inv)
532523
loss += pref_v * l1_virial_masked
533524
more_loss["mae_v"] = self.display_if_exist(
@@ -546,8 +537,7 @@ def call(
546537
if maskf is not None:
547538
v2d = xp.reshape(virial, (_nf, 9))
548539
v_hat_2d = xp.reshape(virial_hat, (_nf, 9))
549-
abs_v = xp.abs(v_hat_2d - v2d)
550-
per_frame_v = xp.mean(abs_v, axis=-1)
540+
per_frame_v = per_frame_component_mean(xp.abs(v_hat_2d - v2d))
551541
mae_v = xp.mean(per_frame_v * inv)
552542
else:
553543
mae_v = (
@@ -565,10 +555,10 @@ def call(
565555
# Idiom 1 (per-atom masked mean, ncomp=1).
566556
ae_2d = xp.reshape(atom_ener, (_nf, _nloc))
567557
ae_hat_2d = xp.reshape(atom_ener_hat, (_nf, _nloc))
568-
sq_ae = xp.square(ae_hat_2d - ae_2d) * maskf # [nf, nloc]
569-
per_frame_sum = xp.sum(sq_ae, axis=-1) # [nf]
570558
per_frame_dof = xp.sum(maskf, axis=-1) # [nf]
571-
l2_ae_masked = xp.mean(per_frame_sum / per_frame_dof)
559+
l2_ae_masked = masked_atom_mean(
560+
xp.square(ae_hat_2d - ae_2d)[:, :, None], maskf, 1
561+
)
572562
if not self.use_huber:
573563
loss += pref_ae * l2_ae_masked
574564
else:
@@ -609,10 +599,9 @@ def call(
609599
if maskf is not None:
610600
ae_2d = xp.reshape(atom_ener, (_nf, _nloc))
611601
ae_hat_2d = xp.reshape(atom_ener_hat, (_nf, _nloc))
612-
abs_ae = xp.abs(ae_hat_2d - ae_2d) * maskf # [nf, nloc]
613-
per_frame_sum = xp.sum(abs_ae, axis=-1) # [nf]
614-
per_frame_dof = xp.sum(maskf, axis=-1) # [nf]
615-
l1_ae_masked = xp.mean(per_frame_sum / per_frame_dof)
602+
l1_ae_masked = masked_atom_mean(
603+
xp.abs(ae_hat_2d - ae_2d)[:, :, None], maskf, 1
604+
)
616605
loss += pref_ae * l1_ae_masked
617606
more_loss["mae_ae"] = self.display_if_exist(
618607
l1_ae_masked, find_atom_ener
@@ -637,13 +626,9 @@ def call(
637626
# Idiom 1 with pref weight (ncomp=3).
638627
diff_f_3d = xp.reshape(diff_f, (_nf, _nloc, 3))
639628
pf_3d = xp.reshape(atom_pref, (_nf, _nloc, 3))
640-
maskf_col = xp.reshape(maskf, (_nf, _nloc, 1))
641-
sq_pf = xp.square(diff_f_3d) * pf_3d * maskf_col # [nf, nloc, 3]
642-
per_frame_sum = xp.sum(
643-
xp.reshape(sq_pf, (_nf, -1)), axis=-1
644-
) # [nf]
645-
per_frame_dof = xp.sum(maskf, axis=-1) * 3 # [nf]
646-
l2_pf_masked = xp.mean(per_frame_sum / per_frame_dof)
629+
l2_pf_masked = masked_atom_mean(
630+
xp.square(diff_f_3d) * pf_3d, maskf, 3
631+
)
647632
loss += pref_pf * l2_pf_masked
648633
more_loss["rmse_pf"] = self.display_if_exist(
649634
xp.sqrt(l2_pf_masked), find_atom_pref
@@ -660,11 +645,7 @@ def call(
660645
if maskf is not None:
661646
diff_f_3d = xp.reshape(diff_f, (_nf, _nloc, 3))
662647
pf_3d = xp.reshape(atom_pref, (_nf, _nloc, 3))
663-
maskf_col = xp.reshape(maskf, (_nf, _nloc, 1))
664-
abs_pf = xp.abs(diff_f_3d) * pf_3d * maskf_col # [nf, nloc, 3]
665-
per_frame_sum = xp.sum(xp.reshape(abs_pf, (_nf, -1)), axis=-1)
666-
per_frame_dof = xp.sum(maskf, axis=-1) * 3
667-
l1_pf_masked = xp.mean(per_frame_sum / per_frame_dof)
648+
l1_pf_masked = masked_atom_mean(xp.abs(diff_f_3d) * pf_3d, maskf, 3)
668649
loss += pref_pf * l1_pf_masked
669650
more_loss["mae_pf"] = self.display_if_exist(
670651
l1_pf_masked, find_atom_pref

0 commit comments

Comments
 (0)