Skip to content

[pull] master from deepmodeling:master - #263

Merged
pull[bot] merged 3 commits into
ishandutta2007:masterfrom
deepmodeling:master
Jul 12, 2026
Merged

[pull] master from deepmodeling:master#263
pull[bot] merged 3 commits into
ishandutta2007:masterfrom
deepmodeling:master

Conversation

@pull

@pull pull Bot commented Jul 12, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

wanghan-iapcm and others added 3 commits July 12, 2026 16:34
…5738)

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Han Wang <wang_han@iapcm.ac.cn>
…ts (#5764)

## Fix #5629

### Problem

`format_nlist` pads the input nlist to width `nnei` when the input is
shorter, but the forced-sort branch (`extra_nlist_sort=True`) then
re-read the shape from the **original unpadded** `nlist` instead of the
padded `ret`. This caused:
- The sort to operate on the short tensor (width = original `n_nnei`,
not `nnei`)
- The final `ret[..., :nnei]` slice to return a tensor with the original
short width
- The `-1` padding to be lost
- The `assert ret.shape[-1] == nnei` check to fail for static shapes

This is especially reachable for linear atomic models, which always
request sorted lower neighbor lists via `need_sorted_nlist_for_lower()`.

### Fix

Use the padded `ret` tensor (instead of the original `nlist`) throughout
the forced-sort branch. Added explanatory comments.

### Test

Added `source/tests/common/dpmodel/test_format_nlist_short_padding.py`
with three test cases:
1. **Short nlist with sort**: input width 2, target `nnei=4`, verifies
output width is 4 and real neighbors are preserved.
2. **All-padded input with sort**: verifies output is all `-1` with
width `nnei`.
3. **Short nlist without sort**: sanity check that the non-sort path
also works.

### Attribution

Generated with [opencode](https://opencode.ai) using model `glm-5.2`.

### Recommended reviewers

@njzjz (maintainer of the dpmodel code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
  * Improved neighbor-list generation across supported array backends.
* Enhanced compatibility with tensor devices, data types, and
static-shape execution.
* Neighbor lists now consistently produce the requested number of
entries, using clear padding when needed.
* Improved sorting, distance filtering, truncation, and handling of
empty or fully padded neighbor lists.
* Ghost-cell coordinate generation now provides more consistent results
across execution environments.

* **Tests**
* Added coverage for sorted neighbor lists, including partially
populated and fully padded inputs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: njzjz-bot <njzjz.bot@gmail.com>
## Fix #5620

### Problem

`DP_ReadFileToChar气Char2` reported the original file size but returned a
buffer from `string_to_char`, which trims trailing whitespace before
allocating/copying. The C++ wrapper (`read_file_to_string` in
`deepmd.hpp`) then reconstructed a `std::string` with the reported
(larger) size, causing an over-read of the shorter allocation.

### Fix

Added `string_to_char_exact`, a new helper that preserves every byte
without trimming, and use it in `DP_ReadFileToChar2` and
`DP_ReadFileToChar`. Error-message paths still use the trimming
`string_to_char` since whitespace trimming is desirable there.

### Test

Added both a C++ test (`test_read_file_to_string.cc`) and a Python test
(`test_c_api_readfile.py`) that verify trailing whitespace is preserved.
The Python test uses `ctypes` to call `DP_ReadFileToChar2` directly and
the C API.

Verified: the test fails against the buggy code (over-reads garbage
bytes) and passes with the fix.

### Attribution

Generated with [opencode](https://opencode.ai) using model `glm-5.2`.

### Recommended reviewers

@njzjz (maintainer of the C API)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added per-frame `charge_spin` support to DeepPot and model-deviation C
API computations.
- Introduced “version 3” compute entry points that accept `charge_spin`
(including neighbor-list variants).
  - Added APIs to query the required charge-spin dimension.
- **Bug Fixes**
- File-reading C APIs now preserve exact bytes, including trailing
whitespace/newlines, and add safer size handling.
- **Tests**
- Added regression tests (C++ and Python) validating byte-for-byte file
content preservation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: njzjz-bot <njzjz.bot@gmail.com>
@pull pull Bot locked and limited conversation to collaborators Jul 12, 2026
@pull pull Bot added the ⤵️ pull label Jul 12, 2026
@pull
pull Bot merged commit 0601c79 into ishandutta2007:master Jul 12, 2026
33 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants