Skip to content

refactor(loss): extract masked per-frame reduction idioms to cut nested branching#5783

Open
wanghan-iapcm wants to merge 9 commits into
deepmodeling:masterfrom
wanghan-iapcm:refactor-loss-masked-reduction
Open

refactor(loss): extract masked per-frame reduction idioms to cut nested branching#5783
wanghan-iapcm wants to merge 9 commits into
deepmodeling:masterfrom
wanghan-iapcm:refactor-loss-masked-reduction

Conversation

@wanghan-iapcm

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

Copy link
Copy Markdown
Collaborator

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.

Summary by CodeRabbit

  • Improvements

    • Improved consistency and reliability of masked loss calculations across energy, force, virial, tensor, DOS, and spin-related training workflows.
    • Standardized normalization for masked atom-based and per-frame metrics.
    • Preserved existing loss behavior across supported NumPy, JAX, and PyTorch workflows.
  • Tests

    • Added coverage validating masked reductions, normalization, cross-backend consistency, and PyTorch gradient support.

Han Wang added 9 commits July 13, 2026 13:08
Extract the three per-frame reduction idioms introduced by the mixed_type
padding fix (deepmodeling#5738) into deepmd/dpmodel/loss/reduction.py, written with
array_api_compat so both the dpmodel and pt loss backends can share them.
Callers still keep the original non-masked expression in the else branch, so
the bit-identical-for-non-mixed guarantee is preserved. No call sites adopt
the helpers yet.

Ref deepmodeling#5768
Collapse the idiom-1 (force/atom-energy/prefactor-force) and idiom-2
(energy) masked per-frame arithmetic in deepmd/dpmodel/loss/ener.py to
calls into the shared reduction helpers. Huber, f_use_norm, generalized
force and the non-masked else branches are untouched; numerics are
bit-identical.

Ref deepmodeling#5768
Complete the dpmodel EnergyLoss adoption by routing the virial masked
per-frame branches through per_frame_component_mean, matching the energy
term and EnergySpinLoss. Numerics bit-identical; else/huber/display
branches untouched.

Ref deepmodeling#5768
EnergySpinLoss now reuses the same reduction helpers as EnergyLoss for its
energy/real-force/atom-energy/virial masked branches, cutting the largest
duplication between the two files. The magnetic-force (mask_mag) block is
spin-specific and left bespoke. Numerics bit-identical.

Ref deepmodeling#5768
…loss

Local dos/cdf/tensor masked branches use the idiom-1 helper; the global
dos/cdf/tensor terms keep their plain-mean loss and only route the
display-only atom-count divisor through masked_atom_num. Numerics
bit-identical.

Ref deepmodeling#5768
…Loss

deepmd/pt/loss/ener.py now imports the array_api_compat reduction helpers
from deepmd.dpmodel.loss.reduction and calls them with torch tensors; the
torch namespace dispatch preserves autograd and is bit-identical to the
prior inlined torch code. Huber / f_use_norm / generalized-force paths and
the .to(GLOBAL_PT_FLOAT_PRECISION) casts are unchanged.

Ref deepmodeling#5768
…ng, test)

Address final-review nits on the shared reduction module: per_frame_component_mean
now derives nf from err.shape[0] internally (matching masked_atom_mean and removing
a caller footgun), add the missing Returns type line to masked_atom_num's docstring,
and add an autograd test for per_frame_component_mean. Numerics bit-identical.

Ref deepmodeling#5768
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds shared masked-reduction helpers, tests their NumPy and PyTorch behavior, and applies them across dpmodel and PyTorch DOS, energy, spin-energy, and tensor losses.

Changes

Masked loss reduction refactor

Layer / File(s) Summary
Reduction helpers and validation
deepmd/dpmodel/loss/reduction.py, source/tests/common/dpmodel/test_loss_reduction.py
Adds shared masked atom, per-frame component, and masked atom-count reductions with backend-aware implementations and coverage for correctness and autograd.
dpmodel loss integration
deepmd/dpmodel/loss/{dos,ener,ener_spin,tensor}.py
Replaces duplicated masked reshaping, summation, and normalization logic with shared reduction helpers while preserving existing loss control flow.
PyTorch loss integration
deepmd/pt/loss/{dos,ener,ener_spin,tensor}.py
Applies the same shared reductions to masked DOS, energy, spin-energy, tensor, force, virial, atomic-energy, and atom-prefactor calculations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: iprozd, njzjz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% 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 is concise and accurately summarizes the main refactor to shared masked reduction helpers.
Linked Issues check ✅ Passed The PR implements #5768 by extracting shared masked reduction helpers and reusing them across dpmodel and PyTorch loss paths.
Out of Scope Changes check ✅ Passed The changes stay within the refactor scope, with only mirrored loss updates and helper tests added.
✨ 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 (1)
source/tests/common/dpmodel/test_loss_reduction.py (1)

19-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_ref duplicates the implementation formula rather than independently verifying it.

_ref mirrors masked_atom_mean's exact computation, so test_numpy_matches_reference mainly confirms the numpy dispatch path matches itself rather than validating correctness against an independently derived expected value. Consider adding at least one case with a manually computed expected scalar to catch a shared logic bug in both.

🤖 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_reduction.py` around lines 19 - 33,
Replace or supplement the formula-based _ref oracle in
test_numpy_matches_reference with at least one test case whose expected
reduction scalar is manually computed and asserted directly. Keep coverage for
both ncomp values as appropriate, but ensure the test can detect a shared error
in masked_atom_mean and _ref rather than comparing two identical
implementations.
🤖 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 `@deepmd/dpmodel/loss/reduction.py`:
- Around line 30-55: Update masked_atom_mean to guard zero values in
per_frame_dof before dividing per_frame_sum by it, assigning a finite neutral
per-frame result for all-padding frames while preserving the existing
calculation for positive degrees of freedom and the final frame mean.

---

Nitpick comments:
In `@source/tests/common/dpmodel/test_loss_reduction.py`:
- Around line 19-33: Replace or supplement the formula-based _ref oracle in
test_numpy_matches_reference with at least one test case whose expected
reduction scalar is manually computed and asserted directly. Keep coverage for
both ncomp values as appropriate, but ensure the test can detect a shared error
in masked_atom_mean and _ref rather than comparing two identical
implementations.
🪄 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: 611b6d93-b490-4ef1-b0e6-52b9b72acb1c

📥 Commits

Reviewing files that changed from the base of the PR and between 0601c79 and 13b74c9.

📒 Files selected for processing (10)
  • deepmd/dpmodel/loss/dos.py
  • deepmd/dpmodel/loss/ener.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/dpmodel/loss/reduction.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/tensor.py
  • source/tests/common/dpmodel/test_loss_reduction.py

Comment on lines +30 to +55
def masked_atom_mean(elem: Array, maskf: Array, ncomp: int) -> Array:
"""Idiom 1: per-atom masked mean over ``ncomp`` components, averaged over frames.

Parameters
----------
elem : Array
Non-negative per-element contribution of shape ``[nf, nloc, ncomp]``
(already squared or abs'd, and pre-multiplied by any per-atom weight
such as ``atom_pref``). NOT yet multiplied by the mask.
maskf : Array
Per-atom real/ghost mask of shape ``[nf, nloc]`` (1.0 real, 0.0 ghost).
ncomp : int
Number of components per atom (force: 3, atom energy: 1,
dos: ``numb_dos``, tensor: ``tensor_size``).

Returns
-------
Array
``mean_over_frames( sum(elem * mask) / (real_natoms * ncomp) )``.
"""
xp = array_api_compat.array_namespace(elem, maskf)
nf = elem.shape[0]
masked = elem * maskf[:, :, None]
per_frame_sum = xp.sum(xp.reshape(masked, (nf, -1)), axis=-1)
per_frame_dof = xp.sum(maskf, axis=-1) * ncomp
return xp.mean(per_frame_sum / per_frame_dof)

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n== reduction.py outline ==\n'
ast-grep outline deepmd/dpmodel/loss/reduction.py --view expanded || true

printf '\n== masked_atom_mean call sites ==\n'
rg -n "masked_atom_mean\(" deepmd -S || true

printf '\n== relevant mask logic and frame-count invariants ==\n'
rg -n "real atoms|real_natoms|maskf|atom mask|ghost|padding|nloc|nframes|all-padding|zero real atoms" deepmd -S || true

Repository: deepmodeling/deepmd-kit

Length of output: 50380


🏁 Script executed:

set -euo pipefail

printf '\n== reduction.py excerpt ==\n'
sed -n '1,140p' deepmd/dpmodel/loss/reduction.py

printf '\n== call sites with context ==\n'
rg -n -C 3 "masked_atom_mean\(" deepmd -S

printf '\n== mask generation / invariants candidates ==\n'
rg -n -C 3 "maskf|real_natoms|natoms|ghost|padding" deepmd/dpmodel deepmd/utils deepmd/pt -S

Repository: deepmodeling/deepmd-kit

Length of output: 50380


🏁 Script executed:

set -euo pipefail

printf '\n== loss/ener.py mask handling excerpt ==\n'
sed -n '220,430p' deepmd/dpmodel/loss/ener.py

printf '\n== dpmodel mask producers / consumers ==\n'
rg -n -C 2 '"mask"|maskf|mask_mag|find_atom_|real_natoms' deepmd/dpmodel deepmd/pt/model -S | sed -n '1,220p'

Repository: deepmodeling/deepmd-kit

Length of output: 26677


🏁 Script executed:

python3 - <<'PY'
import math
import numpy as np

try:
    import torch
except Exception:
    torch = None

# Numpy behavior for 0/0 in the helper shape
elem = np.zeros((1, 2, 3), dtype=np.float32)
mask = np.array([[0.0, 0.0]], dtype=np.float32)
masked = elem * mask[:, :, None]
per_frame_sum = masked.reshape(1, -1).sum(axis=-1)
per_frame_dof = mask.sum(axis=-1) * 3
print("numpy per_frame_sum:", per_frame_sum)
print("numpy per_frame_dof:", per_frame_dof)
print("numpy ratio:", per_frame_sum / per_frame_dof)

if torch is not None:
    elem_t = torch.zeros((1, 2, 3), dtype=torch.float32)
    mask_t = torch.zeros((1, 2), dtype=torch.float32)
    masked_t = elem_t * mask_t[:, :, None]
    per_frame_sum_t = masked_t.reshape(1, -1).sum(dim=-1)
    per_frame_dof_t = mask_t.sum(dim=-1) * 3
    print("torch per_frame_sum:", per_frame_sum_t)
    print("torch per_frame_dof:", per_frame_dof_t)
    print("torch ratio:", per_frame_sum_t / per_frame_dof_t)
PY

rg -n -C 2 "at least one real atom|real_natoms|sum\\(maskf|mask\\s*=|0 real atoms|zero real atoms|non-empty|empty frame" deepmd/dpmodel deepmd/pt -S | sed -n '1,220p'

Repository: deepmodeling/deepmd-kit

Length of output: 16293


Guard zero-real-atom frames in masked_atom_mean.
per_frame_dof becomes 0 for an all-padding frame, so sum / dof turns into NaN and can poison every loss that uses this helper. A zero-dof guard here would keep normal behavior unchanged.

🤖 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/reduction.py` around lines 30 - 55, Update
masked_atom_mean to guard zero values in per_frame_dof before dividing
per_frame_sum by it, assigning a finite neutral per-frame result for all-padding
frames while preserving the existing calculation for positive degrees of freedom
and the final frame mean.

@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.66667% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.61%. Comparing base (0601c79) to head (13b74c9).

Files with missing lines Patch % Lines
deepmd/dpmodel/loss/ener.py 78.57% 3 Missing ⚠️
deepmd/dpmodel/loss/ener_spin.py 75.00% 3 Missing ⚠️
deepmd/pt/loss/ener.py 80.00% 3 Missing ⚠️
deepmd/pt/loss/ener_spin.py 75.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5783      +/-   ##
==========================================
- Coverage   79.77%   79.61%   -0.17%     
==========================================
  Files        1020     1021       +1     
  Lines      116872   116763     -109     
  Branches     4308     4303       -5     
==========================================
- Hits        93239    92956     -283     
- Misses      22095    22265     +170     
- Partials     1538     1542       +4     

☔ 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.

@wanghan-iapcm wanghan-iapcm requested a review from njzjz July 13, 2026 11:31

@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.

Reviewed commit 13b74c9. I found no blocking correctness issues in the shared reduction helpers or their dpmodel and PyTorch call sites. The refactor preserves the existing shape, dtype, masking, normalization, and reduction order.

Local validation:

  • common helper and padding tests: 65 passed
  • PyTorch padding tests: 58 passed
  • cross-backend loss consistency tests: 232 passed, 188 skipped
  • Ruff check and format check: passed

Coding agent: Codex
Codex version: codex-cli 0.144.1
Model: gpt-5.6-sol
Reasoning effort: xhigh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(loss): extract masked per-frame reduction idioms to cut nested branching

3 participants