Skip to content

fix(loss): exclude mixed_type padding atoms from the training loss#5738

Queued
wanghan-iapcm wants to merge 17 commits into
deepmodeling:masterfrom
wanghan-iapcm:fix-loss-mixed-type-padding
Queued

fix(loss): exclude mixed_type padding atoms from the training loss#5738
wanghan-iapcm wants to merge 17 commits into
deepmodeling:masterfrom
wanghan-iapcm:fix-loss-mixed-type-padding

Conversation

@wanghan-iapcm

@wanghan-iapcm wanghan-iapcm commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Problem

In the mixed_type data format, short frames are padded with type = -1 ghost atoms up to a fixed nloc, and the real atom count varies per frame within a batch. The training loss normalized by the padded scalar natoms and took unmasked or cross-frame-pooled means, so ghost atoms diluted the force/atomic denominators and mis-normalized the extensive energy/virial/property terms. As a result a padded [3-atom + 5-atom] batch did not produce the same loss/gradient as processing the 3-atom and 5-atom frames separately. Only mixed_type batches are affected; non-mixed training was already exact.

Fix

The per-atom mask (atype >= 0) now reaches the loss under the existing model_dict["mask"] convention: pt_expt already propagates it; the pt (torch.jit) backend recovers it from atype via a new TaskLoss._inject_atom_mask helper called from every pt loss forward (the exported forward drops the model's per-atom mask, so it is recovered training-side only — the exported artifact is untouched).

Every loss term is then normalized per frame so that a padded batch equals the grad-accumulation of the individual frames at their real sizes: per-atom terms (force, atom_ener, atom_pref, atomic dos/tensor, spin real-force, generalized force) use a per-frame masked mean; extensive terms (energy, virial, property) divide by the per-frame real atom count; global already-reduced terms (global dos/tensor) use a plain mean with the previous atom-count weighting dropped. Every change reduces exactly to the previous formula when the mask is all-ones, so non-mixed training is numerically identical (no-op to rounding). Covered across five shared loss types in both backends: deepmd/dpmodel/loss/{ener,ener_spin,dos,tensor,property}.py (which serve pt_expt) and deepmd/pt/loss/{ener,ener_spin,dos,tensor,property}.py.

Two additional fixes surfaced during the work: the extensive property normalization called xp.sum(mask, -1) with a positional axis, which raises TypeError under the array_api_compat torch namespace (every pt_expt extensive-property run) — now axis=-1; and ener_spin's MAE energy and real-force terms were pre-existingly inconsistent with ener.py (they summed over frames without per-atom normalization) — they are now aligned with ener.py, which changes their non-mixed MAE loss values (a deliberate bug fix, see Known Limitations).

Test

New source/tests/common/dpmodel/test_loss_padding.py and source/tests/pt/test_loss_padding.py assert, for every per-atom and extensive term of all five loss types in both backends, that a padded [3+5] batch loss equals the mean of the two frames processed separately, plus an all-ones-mask non-mixed no-op guard per term, and a torch-tensor path through the dpmodel property loss (which reproduces the positional-axis crash on the old form). An audit added invariant coverage for the generalized-force and spin magnetic-force terms and confirmed they are free of padding artifacts.

Known limitations

The tf backend loss is unchanged and retains the same mixed_type behavior (follow-up). The pt-only losses dens, population, denoise are not covered (follow-up). ener_spin's magnetic-force (force_mag) MAE term uses a sum over frames rather than a mean, so it does not satisfy the frame-average invariant — this is not a padding artifact (ghost atoms are correctly excluded via mask_mag), but a separate pre-existing MAE frame-normalization inconsistency, left for a follow-up decision. The enable_atom_ener_coeff path sums ghost atomic energies before the energy reduction (pre-existing; ghost atom_ener is ~0 by convention). Existing mixed_type trainings will not reproduce numerically — the new values are the correct ones. Ghost label forces are assumed ~0 by the dpdata convention; the mask makes the loss robust even if they are not.

Summary by CodeRabbit

  • New Features

    • Added automatic atom-mask injection so padded/multi-frame inputs with ghost atoms are handled consistently.
  • Bug Fixes

    • Reworked masked loss normalization to use per-frame masked-mean reductions for energy, forces, virials, atom/property terms, DOS/CDF, tensor L2, and spin losses.
    • Standardized masked global DOS/CDF and global tensor L2 to use unweighted mean squared error.
    • Improved masking behavior for generalized-force projection and updated RMSE/MAE reporting accordingly.
  • Tests

    • Added/extended gradient-accumulation and padding-mask invariance suites across dpmodel and pt backends, including atom-mask injection coverage.

Han Wang added 10 commits July 4, 2026 19:03
…sts can be excluded

pt losses build model_pred internally and drop the model mask; recover it from atype in the TaskLoss base and call it from every pt loss forward; pt_expt already carries the mask; enables per-frame loss normalization in later tasks; no-op for non-mixed.
…e padding

Atomic terms (ados/acdf for DOSLoss; local tensor for TensorLoss) now use per-frame masked mean (idiom 1) instead of a cross-frame pooled masked mean, ensuring each frame is normalized by its own real-atom count. Global terms (dos/cdf for DOSLoss; global tensor for TensorLoss) now use plain mean (idiom 3), dropping the prior atom-count weighting that violated the grad-accumulation invariant. Both meet the invariant: a padded [3+5]-atom batch yields the same loss as processing each frame separately and averaging. Boolean fancy indexing removed from pt atomic branches. Changes are bit-identical for non-mixed batches (all-ones mask). Applies to deepmd/dpmodel/loss/{dos,tensor}.py (serves pt_expt) and deepmd/pt/loss/{dos,tensor}.py (mirror). TDD: invariant tests confirmed RED before and GREEN after (24/24 passed).
…padding

Apply per-frame normalization to every loss term (energy, force, virial,
atom_ener, atom_pref, gen_force) in both deepmd/dpmodel/loss/ener.py and
deepmd/pt/loss/ener.py. When model_dict["mask"] is present (mixed_type
batch), ghost-padded atoms are excluded from the loss signal via Idiom 1
(masked per-atom mean) for force/atom_ener/atom_pref and Idiom 2
(extensive inv**norm_exp weighting) for energy/virial; all sub-branches
(mse/mae/huber, intensive/non-intensive) are covered. Non-mixed callers
that never inject a mask hit the unchanged else-branch and are bit-identical
to the pre-fix behavior. 40 new grad-accumulation-invariant unit tests
(20 dpmodel + 20 pt) verify the fix and the no-op property.
…ay metric

Add test_huber_grad_accum to TestDPModelEnergyLossAtomEnerGradAccum and TestPTEnergyLossAtomEnerGradAccum verifying the masked Huber atom_ener path satisfies the [3+5]==separate grad-accum invariant. Fix rmse_f in the masked force MSE branch to use the masked per-frame l2 (l2_force_masked / l2_f_masked) rather than the ghost-diluted unmasked l2_force_loss; hoist the masked MSE computation before the use_huber split so it is always in scope for the display metric in both huber and non-huber paths.
…ount

In mixed_type batches, ghost atoms (atype=-1) pad short frames to a fixed
nloc width. PropertyLoss previously divided pred/label by the scalar natoms
(the padded count), causing frames with fewer real atoms to be incorrectly
normalized. When model_dict["mask"] is present, use the per-frame real atom
count (sum of mask along the atom axis, broadcast over task_dim) instead.
The else branch retains the original /natoms behavior so mask-free callers
are unchanged. Tests: RED→GREEN for mse/mae/smooth_mae grad-accum invariant
in both dpmodel and pt; no-op and intensive guards added.
…type padding

Apply the grad-accumulation-invariant per-frame normalization to EnergySpinLoss
in both the dpmodel (deepmd/dpmodel/loss/ener_spin.py) and pt
(deepmd/pt/loss/ener_spin.py) backends:

- Energy (has_e, MSE): idiom 2 — per-frame 1/real_natoms^norm_exp normalization
  replaces the flat atom_norm**norm_exp * mean() formulation.
- Force real (has_fr, MSE): idiom 1 — per-atom masked mean (ncomp=3) replaces
  the unmasked global mean over all nloc atoms.
- Virial (has_v, MSE): idiom 2 — per-frame 1/real_natoms^norm_exp normalization
  (k=9) replaces atom_norm**norm_exp * mean().

The force_mag / mask_mag (has_fm) path is left completely untouched; mask_mag
is the spin virtual-atom mask and is a distinct concept from the padding mask.

Each masked branch is guarded by `if "mask" in model_dict/model_pred:` so
mask-less callers (non-mixed batches without a padding mask) use the original
code path unchanged — the fix is a bit-identical no-op for non-mixed batches.

Tests added to source/tests/common/dpmodel/test_loss_padding.py and
source/tests/pt/test_loss_padding.py: invariant tests for energy (norm_exp 1
and 2), force_real, and virial; no-op tests for each term; and a guard that
confirms force_mag loss is numerically unchanged when a padding mask is present.
… padding

Energy/force_real/virial MAE branches in EnergySpinLoss (dpmodel and pt) used padded-scalar natoms or unmasked global means, violating the grad-accumulation invariant for mixed_type batches. Apply the same per-frame masked pattern already used for MSE, mirroring the authoritative treatment in ener.py (Task 3). Also fix the mae=True display metric for energy to use per-frame inv weighting instead of padded atom_norm. force_mag/mask_mag and MSE branches are untouched.
The extensive property mask normalization called xp.sum(model_dict["mask"], -1) with axis positional; array_api_compat's torch namespace declares axis keyword-only, so this raised TypeError for every pt_expt extensive-property training. Use axis=-1, matching the dos/tensor/ener losses. Adds a torch-tensor test through PropertyLoss.call that reproduces the crash on the old form.
…pe padding

Apply idiom 1 (per-atom masked mean, ncomp=1) to the has_ae block in both
EnergySpinLoss backends so that ghost-padded atoms (mask=0) are excluded from
the atom_ener loss normalization. Before this fix the loss was computed as
mean(square(diff)) over a flattened [nf*nloc] vector, so ghost atoms diluted
the denominator in mixed_type batches.

The masked path uses xp.reshape(maskf, (_nf, _nloc, 1)) as a broadcastable
column mask, then idiom-1 per-frame sum / dof → mean over frames. The unmasked
(no-mask) fallback is unchanged.

Also add grad-accum invariant tests for: ener_spin.has_ae (mse/mae, RED→GREEN),
gen_force/has_gf (already GREEN — ghost forces are masked before projection so
the mean(square) over [nf, ngen] is frame-decomposable), and force_mag MSE
(GREEN — n_valid excludes ghost atoms via mask_mag=0; invariant holds when NM
is equal across frames, which is the practical spin-model case). force_mag MAE
uses xp.sum over frames instead of xp.mean (2x accumulation artifact, pre-existing,
independent of mask_mag semantics) and is reported as NEEDS_CONTEXT.
Importing deepmd.pt installs a cuda:9999999 default-device trap, so the property torch test failed only when run after the pt test file (order-dependent). Pin device=cpu on the constructed tensors to make the test hermetic. Source behavior unchanged.
@dosubot dosubot Bot added the bug label Jul 5, 2026
@github-actions github-actions Bot added the Python label Jul 5, 2026
@wanghan-iapcm wanghan-iapcm requested a review from njzjz July 5, 2026 16:13
Comment thread deepmd/dpmodel/loss/ener.py Fixed
Comment thread deepmd/dpmodel/loss/ener.py Fixed
Comment thread deepmd/dpmodel/loss/ener.py Fixed
Comment thread deepmd/dpmodel/loss/ener_spin.py Fixed
Comment thread deepmd/dpmodel/loss/ener_spin.py Fixed
Comment thread deepmd/pt/loss/ener.py Fixed
Comment thread deepmd/pt/loss/ener.py Fixed
Comment thread deepmd/pt/loss/ener_spin.py Fixed
Comment thread deepmd/pt/loss/ener_spin.py Fixed
Comment thread deepmd/pt/loss/ener_spin.py Fixed
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Loss functions in both backends now support per-frame atom masking for padded batches. Local terms use masked per-frame reductions, global terms use unweighted means with mask-derived atom counts, PT paths inject masks from atype, and extensive padding invariant tests cover the updated loss paths.

Changes

Mask-aware loss normalization

Layer / File(s) Summary
Atom mask injection helper
deepmd/pt/loss/loss.py
Adds TaskLoss._inject_atom_mask, deriving masks from atype when predictions do not include one.
dpmodel core loss reductions
deepmd/dpmodel/loss/dos.py, deepmd/dpmodel/loss/tensor.py, deepmd/dpmodel/loss/property.py
Adds per-frame masked local reductions, unweighted global means with mask-derived atom counts, and per-frame normalization for non-intensive properties.
dpmodel energy paths
deepmd/dpmodel/loss/ener.py, deepmd/dpmodel/loss/ener_spin.py
Adds mask-aware normalization across energy, force, virial, atomic-energy, prefactor-force, and generalized-force terms, including custom Huber handling.
pt core loss reductions
deepmd/pt/loss/dos.py, deepmd/pt/loss/tensor.py, deepmd/pt/loss/property.py
Uses injected masks for local reductions, global scaling, and extensive-property normalization.
pt energy paths
deepmd/pt/loss/ener.py, deepmd/pt/loss/ener_spin.py
Adds masked per-frame normalization for energy, force, virial, atomic-energy, prefactor-force, generalized-force, and spin-specific terms.
dpmodel padding tests
source/tests/common/dpmodel/test_loss_padding.py
Adds padded-batch invariants across DOS, tensor, energy, spin, property, generalized-force, and magnetic-force paths.
pt padding tests
source/tests/pt/test_loss_padding.py
Tests mask injection and padding invariants across DOS, tensor, energy, property, spin, generalized-force, and magnetic-force paths.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: njzjz, iProzd

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the core change: excluding mixed-type padding atoms from loss normalization and training.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
source/tests/pt/test_loss_padding.py (2)

1976-2064: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Known force_mag MAE bug documented but left untested.

The comment acknowledges that force_mag MAE fails the grad-accum invariant by a 2x factor due to frame-sum normalization, but only the MSE variant is exercised as a test — there's no xfail/skip test capturing the MAE case, so this documented gap in deepmd/pt/loss/ener_spin.py isn't tracked by CI and could silently regress or be forgotten.

Consider adding an explicit @pytest.mark.xfail test asserting the known MAE force_mag discrepancy, so the fix (or its continued absence) is tracked rather than only documented in a comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/tests/pt/test_loss_padding.py` around lines 1976 - 2064, The force_mag
MAE grad-accum discrepancy is only described in comments and not covered by CI.
Add an explicit pytest xfail (or skip) test near
TestPTEnerSpinLossForceMagMSEGradAccum that exercises the MAE path through
_spin_loss_fn/EnergySpinLossPT and asserts the known 2x invariant mismatch, so
the documented behavior is tracked and won’t be forgotten. Keep the existing MSE
test unchanged and use the same make_A, make_B, and make_padded helpers to
locate the relevant loss behavior.

109-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate mock model classes.

_MockModel and _EnerLossMockModel are functionally identical (same __init__/__call__). Consider consolidating into a single shared helper used across all test classes in this file.

♻️ Suggested consolidation
-class _MockModel:
-    """Callable that ignores inputs and returns a fixed model_pred dict.
-    ...
-    """
-
-    def __init__(self, pred: dict):
-        self._pred = pred
-
-    def __call__(self, **kwargs):
-        return dict(self._pred)
-
...
-class _EnerLossMockModel:
-    """Callable returning a fixed model_pred dict (mask pre-populated)."""
-
-    def __init__(self, pred: dict):
-        self._pred = pred
-
-    def __call__(self, **kwargs):
-        return dict(self._pred)
+# Reuse the single _MockModel class defined earlier in this file instead of
+# redefining an identical class for the energy-loss test section.

Also applies to: 628-636

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/tests/pt/test_loss_padding.py` around lines 109 - 122, Consolidate the
duplicate test helpers by removing the redundant `_EnerLossMockModel` and
reusing `_MockModel` across the pt loss tests. Keep the shared behavior in one
helper with the same `__init__` and `__call__` contract, and update the affected
test classes in `test_loss_padding` to instantiate that single helper instead of
maintaining two identical mock model classes. Ensure any existing expectations
around the shallow copy behavior and pre-populated `pred` mask remain unchanged.
deepmd/dpmodel/loss/dos.py (1)

133-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Optional: extract the repeated masked per-frame reduction.

This masked "per-frame sum / per-frame dof → mean" idiom is duplicated verbatim in the local CDF block (Lines 160-170) and mirrored in tensor.py and both pt backends. A small shared helper (e.g. masked_per_frame_mse(diff3d, maskf, ndof)) would reduce the maintenance surface. Behavior-preserving; safe to defer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/dpmodel/loss/dos.py` around lines 133 - 143, The masked per-frame
reduction in the DOS loss is duplicated across the local DOS and local CDF paths
and mirrored in other backends, so extract it into a shared helper to reduce
maintenance. Create a small utility such as masked_per_frame_mse(diff3d, maskf,
ndof) and have the DOS block in dos.py call it instead of inlining the
"per-frame sum / per-frame dof → mean" logic. Keep behavior identical and reuse
the helper wherever the same masked reduction pattern appears, including the
local CDF block and matching implementations in tensor.py and the pt backends.
source/tests/common/dpmodel/test_loss_padding.py (1)

204-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant acdf/cdf tests add no independent coverage.

test_acdf_grad_accum_invariant simply re-invokes test_ados_grad_accum_invariant (and test_cdf_grad_accum_invariant at Lines 282-285 re-invokes test_dos_grad_accum_invariant). The acdf/cdf paths are only exercised incidentally because _make_loss sets both *_ados/*_acdf (and *_dos/*_cdf) prefs to 1.0, so these named tests don't isolate the cumulative-distribution path. Consider either isolating the cdf term (e.g., pref only on cdf/acdf) or dropping the alias tests to avoid implying coverage that isn't independent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/tests/common/dpmodel/test_loss_padding.py` around lines 204 - 207, The
acdf/cdf grad-accum tests are just aliases of the ados/dos tests and don’t add
independent coverage. Update the relevant test methods in test_loss_padding
(test_acdf_grad_accum_invariant and test_cdf_grad_accum_invariant) so they
either isolate the cumulative-distribution path by configuring _make_loss prefs
for only acdf/cdf, or remove the redundant alias tests altogether. Use the
existing test_ados_grad_accum_invariant and test_dos_grad_accum_invariant
helpers as the reference point when deciding whether to separate coverage or
drop the wrappers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@source/tests/common/dpmodel/test_loss_padding.py`:
- Around line 2011-2019: The `force_mag` MAE path in `ener_spin.py` is still
frame-scaled because it uses a sum over per-frame averages instead of a true
frame-wise mean. Update the MAE computation in the `force_mag` loss logic to
normalize across frames consistently with the other `ener_spin` MAE branches,
using the relevant symbols around the `force_mag` reduction code path. If this
change is intentionally deferred, add a reference to the tracking issue in the
surrounding test/comment; otherwise make the loss independent of `nf` so the 2x
effect disappears.

---

Nitpick comments:
In `@deepmd/dpmodel/loss/dos.py`:
- Around line 133-143: The masked per-frame reduction in the DOS loss is
duplicated across the local DOS and local CDF paths and mirrored in other
backends, so extract it into a shared helper to reduce maintenance. Create a
small utility such as masked_per_frame_mse(diff3d, maskf, ndof) and have the DOS
block in dos.py call it instead of inlining the "per-frame sum / per-frame dof →
mean" logic. Keep behavior identical and reuse the helper wherever the same
masked reduction pattern appears, including the local CDF block and matching
implementations in tensor.py and the pt backends.

In `@source/tests/common/dpmodel/test_loss_padding.py`:
- Around line 204-207: The acdf/cdf grad-accum tests are just aliases of the
ados/dos tests and don’t add independent coverage. Update the relevant test
methods in test_loss_padding (test_acdf_grad_accum_invariant and
test_cdf_grad_accum_invariant) so they either isolate the
cumulative-distribution path by configuring _make_loss prefs for only acdf/cdf,
or remove the redundant alias tests altogether. Use the existing
test_ados_grad_accum_invariant and test_dos_grad_accum_invariant helpers as the
reference point when deciding whether to separate coverage or drop the wrappers.

In `@source/tests/pt/test_loss_padding.py`:
- Around line 1976-2064: The force_mag MAE grad-accum discrepancy is only
described in comments and not covered by CI. Add an explicit pytest xfail (or
skip) test near TestPTEnerSpinLossForceMagMSEGradAccum that exercises the MAE
path through _spin_loss_fn/EnergySpinLossPT and asserts the known 2x invariant
mismatch, so the documented behavior is tracked and won’t be forgotten. Keep the
existing MSE test unchanged and use the same make_A, make_B, and make_padded
helpers to locate the relevant loss behavior.
- Around line 109-122: Consolidate the duplicate test helpers by removing the
redundant `_EnerLossMockModel` and reusing `_MockModel` across the pt loss
tests. Keep the shared behavior in one helper with the same `__init__` and
`__call__` contract, and update the affected test classes in `test_loss_padding`
to instantiate that single helper instead of maintaining two identical mock
model classes. Ensure any existing expectations around the shallow copy behavior
and pre-populated `pred` mask remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3120253d-2b09-40b1-9819-187f97feab86

📥 Commits

Reviewing files that changed from the base of the PR and between ffe57a3 and 4990db2.

📒 Files selected for processing (13)
  • deepmd/dpmodel/loss/dos.py
  • deepmd/dpmodel/loss/ener.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/dpmodel/loss/property.py
  • deepmd/dpmodel/loss/tensor.py
  • deepmd/pt/loss/dos.py
  • deepmd/pt/loss/ener.py
  • deepmd/pt/loss/ener_spin.py
  • deepmd/pt/loss/loss.py
  • deepmd/pt/loss/property.py
  • deepmd/pt/loss/tensor.py
  • source/tests/common/dpmodel/test_loss_padding.py
  • source/tests/pt/test_loss_padding.py

Comment thread source/tests/common/dpmodel/test_loss_padding.py
@njzjz-bot

Copy link
Copy Markdown
Contributor

Thanks for the careful fix. The important invariant here should indeed be frame-wise equivalence: a padded mixed-type batch should give the same loss/gradient as processing the real frames separately and averaging. This PR consistently applies that criterion across the pt/pt_expt loss paths, and the all-ones-mask guards are a good way to protect non-mixed behavior.

A few notes before merging:

  • Please wait for the remaining Python/C++ CI shards to finish; several were still pending when I checked.
  • The documented follow-ups are real behavior gaps, especially the unchanged TF backend and the pt-only dens/population/denoise losses. I would prefer these to have explicit tracking issues or TODO references before merge, so the scope boundary is not only in the PR body.
  • The ener_spin MAE normalization change for non-mixed cases is reasonable as a bug fix, but it is user-visible for existing trainings. It should be called out in the release note/changelog if this PR is merged.
  • I agree with tracking the known force_mag MAE frame-normalization inconsistency separately; an xfail/skip test or linked issue would make that debt harder to lose.

No blocker from my side on the main masking/normalization approach once CI is green and the follow-up tracking is clear.


Authored by OpenClaw 2026.6.8 (844f405) (model: custom-chat-jinzhezeng-group/gpt-5.5)

@njzjz njzjz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve but see above non-blocking comments.

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.80105% with 85 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.62%. Comparing base (98c3ab0) to head (1fff149).

Files with missing lines Patch % Lines
deepmd/dpmodel/loss/ener.py 84.83% 32 Missing ⚠️
deepmd/pt/loss/ener.py 86.12% 29 Missing ⚠️
deepmd/pt/loss/ener_spin.py 90.90% 13 Missing ⚠️
deepmd/dpmodel/loss/ener_spin.py 90.43% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5738      +/-   ##
==========================================
- Coverage   79.70%   79.62%   -0.08%     
==========================================
  Files        1020     1020              
  Lines      116359   116847     +488     
  Branches     4305     4306       +1     
==========================================
+ Hits        92742    93045     +303     
- Misses      22076    22251     +175     
- Partials     1541     1551      +10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Han Wang added 2 commits July 9, 2026 21:27
The inv/_nf/_nloc locals are only read inside if maskf is not None
guards, so the = None fallbacks in the else branch were dead stores
(their None value is never read). CodeQL flagged all three per file as
unused. Remove them and note why the names can stay unset when maskf is
None. Pure no-op: no runtime behavior change.
Both reviewers asked for the known force_mag MAE grad-accum discrepancy
(sum over frames -> 2x factor for nf=2) to be tracked by CI rather than
only documented in a comment. Add strict xfail tests in the pt and
dpmodel loss-padding suites; strict mode self-heals (XPASS -> failure)
once force_mag MAE is switched to a frame-wise mean.
Record the deferred scope boundary of the mixed_type padding fix in the
loss-padding test modules instead of only in the PR body: the TF backend
loss (deepmodeling#5760) and the pt-only dens/population/denoise
losses (deepmodeling#5761). The force_mag MAE frame-normalization
debt stays tracked by its strict-xfail test.
@wanghan-iapcm wanghan-iapcm added the breaking change Breaking changes that should notify users. label Jul 10, 2026
@wanghan-iapcm

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Addressing the pre-merge notes:

  • Explicit tracking for the deferred follow-ups. The unchanged TF backend loss is now tracked in TF backend loss: exclude mixed_type padding atoms (per-frame normalization) #5760 and the pt-only `dens`/`population`/`denoise` losses in pt losses dens/population/denoise: not covered by mixed_type padding fix #5761, and both are referenced from the loss-padding test modules (commit b143f98) so the scope boundary is no longer only in the PR body.
  • force_mag MAE debt is tracked by the strict `xfail` tests added to both loss-padding suites (they self-heal to a failure once `force_mag` MAE switches to a frame-wise mean); the test docstrings now carry the scope note too.
  • User-visible ener_spin MAE change. The PR now carries the `breaking change` label so it surfaces in the generated release notes, and it is documented under Known Limitations in the PR body.
  • CI. The remaining Python/C++ shards are running; I will confirm them green before merge.

@wanghan-iapcm wanghan-iapcm requested a review from njzjz July 10, 2026 16:42

@njzjz-bot njzjz-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one padding-related reporting issue and left it inline. The core loss/gradient normalization otherwise looks consistent with the stated frame-wise invariant.

Validation performed on the PR head:

  • source/tests/common/dpmodel/test_loss_padding.py: 54 passed, 1 expected xfail
  • source/tests/pt/test_loss_padding.py: 57 passed, 1 expected xfail
  • The failing macOS x86_64 wheel check is an artifact-upload ENOTFOUND, not a code failure.

Coding agent: Codex
Codex version: codex-cli 0.144.0-alpha.4
Model: gpt-5.6-sol
Reasoning effort: xhigh

Comment thread deepmd/dpmodel/loss/dos.py
@njzjz njzjz enabled auto-merge July 11, 2026 12:45
@njzjz njzjz added this pull request to the merge queue Jul 11, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 11, 2026
The mixed_type padding tests for PropertyLoss created their input tensors and masks on CPU, but PropertyLoss.forward builds out_std/out_bias and the loss accumulator on env.DEVICE and combines them with the multi-dim label/pred tensors (and divides pred/label by real_natoms derived from the mask). On CPU CI env.DEVICE is cpu so this passed, but on the CUDA merge-queue runner it raised "Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu". The other loss terms only cross devices at 0-dim scalar reductions, which torch auto-promotes, so only the property tests were affected.

Move the property test inputs and their masks onto env.DEVICE.
@wanghan-iapcm wanghan-iapcm enabled auto-merge July 12, 2026 06:01
Han Wang added 2 commits July 12, 2026 15:02
force_mag MAE reduced with a sum over frames (pt: .sum(-1).mean(-1).sum(); dpmodel: xp.sum(per_frame_sum / per_frame_count)), so the loss scaled with batch size: a 2-frame batch gave twice the mean of the two single-frame losses, unlike force_mag MSE and force_real MAE which use a plain mean. Reduce force_mag MAE with a mean over frames, magnetic atoms and xyz (matching force_mag MSE, force_real MAE and the displayed mae_fm metric) so a 2-frame batch equals the mean of the two single-frame losses. Applied to both the pt and dpmodel backends (dpmodel is re-exported by pt_expt); TF and paddle have no spin loss. Drop the now-obsolete xfail(strict=True) markers on the force_mag MAE grad-accum tests, which pass after the fix.
Resolve the force_mag MAE conflict in deepmd/pt/loss/ener_spin.py. Master's deepmodeling#5734 refactored the spin force_mag loss into helpers (_masked_force_mag_tensors / _mean_within_segments), but its MAE reduction summed the per-frame means over frames (still batch-size dependent) and summed over xyz. Keep this PR's batch-size-independent global mean (mean over frames, magnetic atoms and xyz), which matches force_mag MSE, force_real MAE and the displayed mae_fm metric. Drop the now-unused _mean_within_segments helper and simplify _masked_force_mag_tensors to return (label_fm, pred_fm).
@wanghan-iapcm wanghan-iapcm added this pull request to the merge queue Jul 12, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jul 12, 2026
@wanghan-iapcm wanghan-iapcm added this pull request to the merge queue Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Breaking changes that should notify users. bug Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants