Skip to content

Commit d155800

Browse files
wanghan-iapcmHan Wang
andauthored
refactor(loss): extract masked per-frame reduction idioms to cut nested branching (#5783)
Resolves #5768. Follow-up to #5738. ## What this does The mixed_type padding fix (#5738) added a masked (`maskf is not None`) branch to every term of the shared losses. Combined with the existing `has_*` / `mse`|`mae` / `use_huber` conditions, that produced deeply nested, duplicated per-frame arithmetic repeated across energy, force, virial, atom_ener, atom_pref, dos, tensor, and again across `ener_spin`, in both the dpmodel and pt backends. This PR extracts the three recurring masked reduction "idioms" into a single shared `array_api_compat` module, `deepmd/dpmodel/loss/reduction.py`: - `masked_atom_mean(elem, maskf, ncomp)` — idiom 1: per-atom masked mean over `ncomp` components (force, atom_ener, atom_pref, local dos/tensor, spin real-force). - `per_frame_component_mean(err)` — idiom 2 primitive: per-frame mean over components; the caller applies the extensive `1/real_natoms**norm_exp` weighting (energy, virial). - `masked_atom_num(mask, natoms, dtype)` — idiom 3 companion: the display-only atom-count divisor for already-reduced global quantities (global dos/tensor). Both backends import the same module; the pt backend calls it with torch tensors, and `array_api_compat`'s torch-namespace dispatch preserves autograd and produces bit-identical results. ## Numerics are preserved This is a pure readability/maintainability refactor with no intended behavior change. Each helper implements only the masked branch; every call site keeps its original non-masked `else` expression verbatim, so the "bit-identical for non-mixed batches" guarantee from #5738 holds (defaulting the mask to all-ones would change the reduction order at the ULP level and is deliberately avoided). Operand order and the mask-application point are preserved in every conversion. ## Testing The safety net is the grad-accumulation-invariant and no-op-for-non-mixed tests added in #5738 plus the cross-backend consistency loss tests, all of which stay green: - `source/tests/common/dpmodel/test_loss_padding.py`, `source/tests/pt/test_loss_padding.py` - `source/tests/consistent/loss` (dpmodel <-> pt <-> tf): 244 passed, 176 skipped — unchanged, the load-bearing bit-identity guard - new direct unit tests for the three helpers (numpy + torch, autograd, bit-identity vs the torch-native inline form): `source/tests/common/dpmodel/test_loss_reduction.py` ## Known limitations - The shared module lives under `deepmd/dpmodel/loss` and is imported by `deepmd/pt/loss` — the first such cross-backend import in the loss layer. This is intentional (single source of truth for the reduction math, consistent with `deepmd/dpmodel` being the designated shared-math layer), but it does couple the pt loss backend to `array_api_compat`. - The Huber-masked sub-branches, the `f_use_norm` force variant, the generalized-force (`gf`) input masking, and the spin magnetic-force (`mask_mag`) block are intentionally NOT routed through the helpers — they are not one of the three idioms. - The TensorFlow (`deepmd/tf`) loss backend is out of scope; it does not use the mixed_type padding mask. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Improved masked loss reductions across energy, forces, virial, tensor, DOS, and spin training by switching to shared masked/per-frame reduction helpers. * Standardized masked atom normalization and per-frame aggregation for consistent RMSE/MAE reporting in local and global DOS/tensor computations. * Preserved behavior and control flow across NumPy/JAX and PyTorch execution paths. * **Tests** * Added coverage for masked mean, per-frame component mean, and atom-count normalization (including `None` masks), plus PyTorch autograd checks and no-NaN behavior on all-padding frames. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Han Wang <wang_han@iapcm.ac.cn>
1 parent 34d1fdb commit d155800

10 files changed

Lines changed: 393 additions & 222 deletions

File tree

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)