fix(dpmodel): preserve virtual spin atom types - #5854
Conversation
Keep negative placeholder types and their spin partners virtual through dense and lower input expansion. Mask type-dependent spin scales and outputs for placeholders, with NumPy and Array API regression coverage. Coding-Agent: Codex Codex-Version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning-Effort: xhigh
for more information, see https://pre-commit.ci
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #5854 +/- ##
==========================================
- Coverage 79.03% 78.82% -0.22%
==========================================
Files 1055 1067 +12
Lines 122233 123813 +1580
Branches 4401 4515 +114
==========================================
+ Hits 96607 97590 +983
- Misses 24061 24613 +552
- Partials 1565 1610 +45 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Possible reviewers based on changed lines, exact file history, and exact-file review history:
No review request was made automatically. Coding agent: Codex |
wanghan-iapcm
left a comment
There was a problem hiding this comment.
The dpmodel change is correct. I checked out the base (6c3b985cb) and ran only the new test file: all three fail, with atype_updated coming back [[0, -1, 2, 3, 2, 5]] against the expected [[0, -1, 2, 3, -1, 5]] - the placeholder's partner takes type -1 + ntypes_real = 2. All three pass at the head. Genuine regression tests, and putting the placeholder mid-array rather than trailing is the right choice. The all-real path is untouched: xp.where collapses to the original expression.
Requesting changes on one point: the fix is dpmodel-only and deepmd/pt has the identical defect. deepmd/pt_expt/model/spin_model.py declares class SpinModel(SpinModelDP) and overrides none of process_spin_input, process_spin_input_lower, process_spin_output* or call_common*, so pt_expt inherits the fix - pt does not. After this lands, dpmodel and pt_expt disagree with pt on identical weights and inputs for any frame containing a placeholder. Two inline comments cover the two halves of that. One further inline comment on the lower-interface test is non-blocking.
Other notes, none blocking:
-
source/tests/consistent/model/test_spin_ener.pyhard-codes non-negativeatypeat:166and:359. That harness already compares the dpmodel, pt and pt_expt classes, so adding a-1entry is the natural gate for the divergence above - it would fail today on pt. -
Separate pre-existing bug, worth its own issue rather than this PR:
deepmd/pt/model/model/sezm_native_spin_model.py:120and:427callself.spin_mask.index_select(0, atype.reshape(-1))with unclampedatype.torch.index_selectrejects negative indices, so the DPA4 native spin model raisesIndexErroron any placeholder frame instead of corrupting silently. -
process_spin_outputandprocess_spin_output_lowerstill call_to_xpon the mask and then pass it into_lookup_type_values, which calls_to_xpagain. The other three call sites pass the raw attribute. Harmless, but the local call can go.
deepmd/pt had the same two unguarded lines the dpmodel fix covers, and pt_expt subclasses the dpmodel SpinModel without overriding them, so the same weights gave different answers per backend. _lookup_type_values now zeroes atype < 0 instead of clamping it to row 0, and the virtual-type offset preserves the -1 placeholder. Also cover call_lower end to end, the virtual_scale=False (spin_mask) branch, and the output/mask sites under array_api_strict.
📝 WalkthroughWalkthroughSpinModel now preserves negative placeholder atom types during spin expansion and safely handles their scaling and masks in DP and PT implementations. New regression tests cover dense, lower-interface, strict array-namespace, and backend-equivalence behavior. ChangesSpin placeholder handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
source/tests/pt/model/test_spin_model_virtual_types.py (1)
61-140: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd PT coverage for
process_spin_output_lowerand thevirtual_scale=Falsebranch.This file exercises
process_spin_input,process_spin_input_lower, andprocess_spin_output, but notprocess_spin_output_lower(spin_model.py Lines 273-320) or thevirtual_scale=False(spin_mask) path — both of which use the same_lookup_type_valuesfix and are equally susceptible to masking bugs. The dpmodel test file already hastest_spin_mask_branch_zeroes_virtual_placeholdersand lower-output masking coverage that could be mirrored here for backend parity.🤖 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/model/test_spin_model_virtual_types.py` around lines 61 - 140, Extend TestPtSpinModelVirtualTypes with coverage for process_spin_output_lower and the virtual_scale=False spin_mask path, mirroring the corresponding dpmodel tests. Verify virtual placeholders are masked and produce zero magnetic outputs or mask values, including lower-output handling, while preserving parity with dpmodel where applicable.
🤖 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.
Nitpick comments:
In `@source/tests/pt/model/test_spin_model_virtual_types.py`:
- Around line 61-140: Extend TestPtSpinModelVirtualTypes with coverage for
process_spin_output_lower and the virtual_scale=False spin_mask path, mirroring
the corresponding dpmodel tests. Verify virtual placeholders are masked and
produce zero magnetic outputs or mask values, including lower-output handling,
while preserving parity with dpmodel where applicable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 247001c0-f4b3-4f80-bdbf-79276d0a2f73
📒 Files selected for processing (4)
deepmd/dpmodel/model/spin_model.pydeepmd/pt/model/model/spin_model.pysource/tests/common/dpmodel/test_spin_model_virtual_types.pysource/tests/pt/model/test_spin_model_virtual_types.py
wanghan-iapcm
left a comment
There was a problem hiding this comment.
All three blocking points are resolved, verified against 777927d68 rather than the replies.
Both pt sites now build the virtual types with torch.where(atype >= 0, atype + self.ntypes_real, ...) (process_spin_input, process_spin_input_lower), so pt, dpmodel and pt_expt agree and the divergence does not land.
Masking rather than clamping in _lookup_type_values is the right resolution: keeping index_select preserves the export device-stability reason the clamp existed for, while torch.where(real_atom, gathered, 0) makes the clamped index purely an index whose value is discarded. Replacing the old docstring matters as much as the code change -- it asserted that padding atoms carry zero spin and are dropped downstream, and nothing enforced either half.
The added coverage is what I was hoping for. test_call_lower_ignores_virtual_placeholders pins the leak I measured through the real lower interface, and test_matches_dpmodel_when_the_first_real_type_is_magnetic turns the use_spin=[True, False, False] scenario into a direct cross-backend gate, including the assertion that the placeholder's mask_mag is False -- the entry ener_spin.py would otherwise have counted in the magnetic-force loss. That supersedes my suggestion of adding a -1 to test_spin_ener.py; comparing the backends directly is better than doing it through the consistency harness.
Still open, and still out of scope for this PR: sezm_native_spin_model.py L120 and L427 call self.spin_mask.index_select(0, atype.reshape(-1)) with unclamped atype, and index_select rejects negative indices. The contrast is sharper after this PR: pt's ordinary spin model and pt_expt now handle a placeholder frame, while the DPA4 native spin model would raise IndexError on the same input. I will confirm whether that path can actually receive a -1 and open an issue if so -- no action needed here.
b634aa3
Closes #5663.
Summary
Why existing tests missed this
Existing spin consistency tests use only nonnegative real atom types, including their lower-interface ghost atoms. Negative-type tests cover base atomic models and neighbor-list builders, where masking happens before type-dependent work, but not
SpinModelpreprocessing, which runs earlier. Cross-backend spin tests therefore never supplied a nonzero padded spin or asserted that both the real placeholder and its generated spin partner remained negative.Validation
pytest source/tests/common/dpmodel/test_spin_model_virtual_types.py source/tests/common/dpmodel/test_finetune_spin.py source/tests/common/dpmodel/test_spin_model_legacy_routing.py -q(12 passed)ruff format .(1664 files unchanged on final pass)ruff check .(passed)git diff --check(passed)Coding agent: Codex
Codex version: codex-cli 0.144.4
Model: gpt-5.6-sol
Reasoning effort: xhigh
Summary by CodeRabbit
Bug Fixes
Tests