Skip to content

feat(dpmodel): NeighborGraph support for dpa1 tebd_input_mode="strip"#5747

Open
wanghan-iapcm wants to merge 7 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-graph-strip-tebd
Open

feat(dpmodel): NeighborGraph support for dpa1 tebd_input_mode="strip"#5747
wanghan-iapcm wants to merge 7 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-graph-strip-tebd

Conversation

@wanghan-iapcm

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

Copy link
Copy Markdown
Collaborator

Summary

Adds NeighborGraph (graph-native lower) support for the dpa1 descriptor with tebd_input_mode="strip", closing the last descriptor-level gap that forced strip-mode models (and se_atten_v2, which is strip-by-construction) onto the legacy dense path.

The dense strip branch factorizes the per-neighbor feature as gg = gg_s*gg_t + gg_s — a radial-only geometric embedding times a type-pair strip embedding (optionally switch-smoothed). Because this has no neighbor-axis coupling, it maps to the graph path edge-for-edge. The change is:

  • Kernel (DescrptBlockSeAtten): a new per-edge helper _graph_edge_gg_strip (op-for-op mirror of the dense strip branch, including the center*ntypes_pad + nei nei-fastest two-side table layout and int64 gather indices), selected by a concat/strip branch in call_graph.
  • Routing (DescrptDPA1.uses_graph_lower): admits strip, while keeping compressed descriptors and exclude_types on the dense path (they have no graph kernel here). se_atten_v2 inherits this and becomes graph-eligible for free.
  • pt_expt: the two graph make_fx export tests are parametrized over tebd_input_mode to prove the strip kernel is fx-traceable.

No new op, no attention/segment_sum change, no C++/serialization change.

Scope

This PR is deliberately independent of #5733 (graph exclude_types). It does not change exclude_types eligibility — the uses_graph_lower and not exclude_types gate and the call_graph exclude_types raise are both kept. When both land, whichever merges second resolves a small (2–3 line) mechanical conflict at uses_graph_lower / the call_graph guard.

Test plan

  • Block-level graph-vs-dense strip parity at rtol=atol=1e-12 over type_one_side × smooth (attn=0) and type_one_side (attn=2, non-smooth).
  • Descriptor-level routed-call vs _call_dense parity over type_one_side × smooth × attn_layer (incl. attn=2 + smooth=True, bit-exact via the static_nnei adapter), plus a negative-contract gate test (compressed → dense, strip+exclude_types → dense).
  • se_atten_v2 eligibility + graph-vs-dense parity (replaces the obsolete "strip stays dense" test).
  • pt_expt strip make_fx export; cross-backend consistency strip cases now route pt_expt through the graph adapter.

Validated on CPU and on GPU (Tesla T4, cuda:0): pt_expt dpa1 50 passed, consistency strip 22 passed + se_atten_v2 110 passed, dpmodel strip suites 46 passed. No tolerances relaxed, no tests skipped.

Known limitations

  • Compression stays on the dense path by design (strip-only tabulation has no graph kernel); the gate excludes self.compress.
  • exclude_types stays dense (out of scope — owned by refactor: pair exclude_types as canonical NeighborGraph transform; dpa1 graph path supports exclude_types (decision #18) #5733).
  • jax graph lower remains energy-only (analytical force on the graph route is a separate follow-up).
  • The graph path's segment_sumindex_add is atomic/non-deterministic on CUDA (1–2 fp64 ULP), inherent to atomic scatter; GPU parity validated within tolerance.
  • Pre-existing (not introduced here): a softmax RuntimeWarning on the shared attention path (max over fully-masked segments), also present on the concat path.

Summary by CodeRabbit

Summary

  • New Features

    • Added graph-based execution support for the additional stripped type-embedding mode.
  • Bug Fixes

    • Updated graph routing eligibility: compressed descriptors and excluded-type configurations now reliably fall back to dense execution; graph routing is disabled when compression is enabled.
  • Tests

    • Added bit-exact parity tests between graph and dense paths for the new stripped mode (including routing eligibility checks).
    • Expanded FX graph export/trace coverage for both embedding modes.
    • Adjusted neighbor-list fallback validation with model-specific tolerance handling and added a new smooth variant.

@dosubot dosubot Bot added the enhancement label Jul 7, 2026
@github-actions github-actions Bot added the Python label Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e83bfb60-acb2-4661-ae5a-d7c561037c0a

📥 Commits

Reviewing files that changed from the base of the PR and between e935668 and b774455.

📒 Files selected for processing (1)
  • source/tests/pt_expt/utils/test_neighbor_list.py

📝 Walkthrough

Walkthrough

This PR adds tebd_input_mode="strip" support to the graph-native DPA1 path, updates graph eligibility to exclude compressed descriptors, and adds parity tests covering descriptor, block, attention, and FX-export flows.

Changes

Strip mode graph routing

Layer / File(s) Summary
Graph-lower eligibility and routing docs
deepmd/dpmodel/descriptor/dpa1.py
uses_graph_lower now disables graph routing when compress is enabled and expands eligibility to tebd_input_mode in ("concat", "strip"); related docstrings/comments in call and _call_dense updated.
call_graph strip computation
deepmd/dpmodel/descriptor/dpa1.py
call_graph validation now accepts "concat"/"strip"; a new "strip" branch delegates to _graph_edge_gg_strip, which implements the stripped factorized embedding.
Block-level strip parity tests
source/tests/common/dpmodel/test_dpa1_call_graph_block.py
New TestDpa1BlockCallGraphStrip verifies dense and graph outputs match bit-for-bit for strip mode across type_one_side, smoothing, and attention-layer settings.
Descriptor-level eligibility and parity tests
source/tests/common/dpmodel/test_dpa1_call_graph_descriptor.py
Narrows the ineligible-fallback test to exclude_types only, and adds TestDpa1StripRouting covering eligibility gating and dense/graph parity for strip mode.
SeAttenV2 strip graph parity tests
source/tests/common/dpmodel/test_dpa1_graph_attention_parity.py
Replaces the previous strip dense-only expectation with tests confirming graph eligibility and bit-exact parity for se_atten_v2 in strip mode.
FX graph export tests for strip mode
source/tests/pt_expt/descriptor/test_dpa1.py
FX-trace tests are parameterized over tebd_input_mode (concat/strip) for both plain and attention-enabled descriptors.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: new feature

Suggested reviewers: OutisLi, iProzd

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: NeighborGraph support for dpa1 strip mode.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

🧹 Nitpick comments (2)
source/tests/common/dpmodel/test_dpa1_graph_attention_parity.py (1)

231-256: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an explicit NaN guard before assert_allclose.

Same concern as the equivalent strip-parity test in test_dpa1_call_graph_descriptor.py: np.testing.assert_allclose treats NaN == NaN as equal by default (equal_nan=True), so shared NaNs between routed and dense would pass unnoticed here.

🧪 Proposed guard
         routed = dd.call(ext_coord, ext_atype, nlist, mapping=mapping)
         dense = dd._call_dense(ext_coord, ext_atype, nlist)
         assert len(routed) == len(dense)
         for r, d in zip(routed, dense, strict=True):
             if r is None:
                 assert d is None
                 continue
+            assert not np.any(np.isnan(r))
             np.testing.assert_allclose(r, d, rtol=1e-12, atol=1e-12)
🤖 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_dpa1_graph_attention_parity.py` around lines
231 - 256, The parity check in test_se_atten_v2_graph_equals_dense needs an
explicit NaN guard before np.testing.assert_allclose, because matching NaNs
could otherwise pass unnoticed. Update the loop over routed and dense to assert
that neither value contains NaNs before comparing them, using the existing
test_dpa1_graph_attention_parity test structure and the DescrptSeAttenV2
call/_call_dense outputs as the comparison points.
source/tests/common/dpmodel/test_dpa1_call_graph_descriptor.py (1)

255-286: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an explicit NaN guard before assert_allclose.

np.testing.assert_allclose defaults to equal_nan=True, so if routed and dense both produce NaN at the same position (e.g. a shared division-by-zero in the strip/attention path), this loop would pass silently instead of catching the regression. The sibling block-level test (test_dpa1_call_graph_block.py) guards against exactly this with assert not np.any(np.isnan(graph_g)) before its assert_allclose call — worth mirroring here for consistency.

🧪 Proposed guard
         routed = dd.call(ext_coord, ext_atype, nlist, mapping=mapping)
         dense = dd._call_dense(ext_coord, ext_atype, nlist)
         assert len(routed) == len(dense)
         for r, d in zip(routed, dense, strict=True):
             if r is None:
                 assert d is None
                 continue
+            assert not np.any(np.isnan(r))
             np.testing.assert_allclose(r, d, rtol=1e-12, atol=1e-12)
🤖 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_dpa1_call_graph_descriptor.py` around lines
255 - 286, The `test_call_strip_graph_equals_dense` comparison in
`test_dpa1_call_graph_descriptor.py` should explicitly reject NaNs before
calling `np.testing.assert_allclose`, since matching NaNs can otherwise hide
regressions in the routed graph path. Add a NaN check on the per-item outputs
right before the `assert_allclose` inside the `routed`/`dense` loop, mirroring
the existing guard used in `test_dpa1_call_graph_block.py`, and keep the rest of
the equality comparison unchanged.
🤖 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/common/dpmodel/test_dpa1_call_graph_descriptor.py`:
- Around line 255-286: The `test_call_strip_graph_equals_dense` comparison in
`test_dpa1_call_graph_descriptor.py` should explicitly reject NaNs before
calling `np.testing.assert_allclose`, since matching NaNs can otherwise hide
regressions in the routed graph path. Add a NaN check on the per-item outputs
right before the `assert_allclose` inside the `routed`/`dense` loop, mirroring
the existing guard used in `test_dpa1_call_graph_block.py`, and keep the rest of
the equality comparison unchanged.

In `@source/tests/common/dpmodel/test_dpa1_graph_attention_parity.py`:
- Around line 231-256: The parity check in test_se_atten_v2_graph_equals_dense
needs an explicit NaN guard before np.testing.assert_allclose, because matching
NaNs could otherwise pass unnoticed. Update the loop over routed and dense to
assert that neither value contains NaNs before comparing them, using the
existing test_dpa1_graph_attention_parity test structure and the
DescrptSeAttenV2 call/_call_dense outputs as the comparison points.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 09bc9b5d-ea2d-4462-ac04-f52ab6788c4d

📥 Commits

Reviewing files that changed from the base of the PR and between b95f21e and c4b4162.

📒 Files selected for processing (5)
  • deepmd/dpmodel/descriptor/dpa1.py
  • source/tests/common/dpmodel/test_dpa1_call_graph_block.py
  • source/tests/common/dpmodel/test_dpa1_call_graph_descriptor.py
  • source/tests/common/dpmodel/test_dpa1_graph_attention_parity.py
  • source/tests/pt_expt/descriptor/test_dpa1.py

@wanghan-iapcm wanghan-iapcm requested a review from OutisLi July 7, 2026 07:42
if self.tebd_input_mode not in ["concat", "strip"]:
raise NotImplementedError(
"graph path supports tebd_input_mode='concat' only (NeighborGraph PR-A)"
f"graph path does not support tebd_input_mode={self.tebd_input_mode!r}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if tebd_input_mode not in ["concat", "strip"] it will not pass argcheck?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — confirmed no, it will pass argcheck. tebd_input_mode is declared as a plain str Argument (no choices=) in argcheck.py, so any string value is accepted at input-validation time; the docstrings there just prose-describe 'concat'/'strip' as the supported modes without enforcing it. So this NotImplementedError guard is genuinely reachable, not dead code — it's needed because an invalid tebd_input_mode can reach call_graph at runtime. For reference, the dense call() path (dpa1.py around line 1575, if ... in ["concat"]: ... elif ... in ["strip"]: ... with no else) has no equivalent guard and would fail with an unclear UnboundLocalError on gg for the same bad input — that's a pre-existing gap outside this PR's scope, so I've left it as-is here rather than expanding scope. Tightening argcheck.py with an explicit choices=["concat", "strip"] would be the real fix for this whole class of issue; happy to file that separately if useful.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.07%. Comparing base (7b0050f) to head (c4b4162).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5747      +/-   ##
==========================================
- Coverage   80.87%   79.07%   -1.80%     
==========================================
  Files        1003     1014      +11     
  Lines      112492   115382    +2890     
  Branches     4236     4274      +38     
==========================================
+ Hits        90981    91242     +261     
- Misses      19986    22599    +2613     
- Partials     1525     1541      +16     

☔ 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 3 commits July 8, 2026 11:37
…close

np.testing.assert_allclose treats NaN == NaN as equal by default, so a
shared NaN between the graph and dense outputs would silently pass the
parity check. Applies to the two strip/attention parity tests CodeRabbit
flagged: test_call_strip_graph_equals_dense (test_dpa1_call_graph_descriptor.py)
and test_se_atten_v2_graph_equals_dense (test_dpa1_graph_attention_parity.py).
…spatch test

CI failure: test_default_fallback[se_atten_v2] failed (energy off by 1.2e-07,
rtol=1e-10). Root cause is this PR's own change: uses_graph_lower() now
admits tebd_input_mode='strip', which makes se_atten_v2 (always strip,
always smooth_type_embedding=True) graph-eligible. call_common's dispatch
(make_model.py) forces the dense route whenever an explicit neighbor_list is
passed, but pt_expt's default-flip (decision deepmodeling#17) routes neighbor_list=None
to the carry-all graph for graph-eligible mixed_types descriptors -- so
'None' and 'explicit DefaultNeighborList()' stop being the same computation
for this model. model_dpa1 already works around the same issue by pinning
smooth_type_embedding=False (with a comment explaining why); se_atten_v2
can't do that since DescrptSeAttenV2 hardcodes smooth_type_embedding=True,
so the divergence is unavoidable and documented (NeighborGraph PR-D: dense
keeps sel-padding phantom softmax terms the graph route doesn't). Exclude it
from this specific parametrize with a docstring explaining why; it is still
covered by test_pt_expt_equivalence/test_dpmodel_equivalence, which always
pass an explicit neighbor_list on both sides and so never hit this route
split.
Follow-up to 66089f1. That commit fixed the CI failure by excluding
se_atten_v2 from test_default_fallback's parametrize, but a plain exclusion
silently drops coverage rather than documenting the expected behavior.

Replace it with a positive assertion, mirroring the precedent already set by
test_block_compact_graph_smooth_clean_divergence (test_dpa1_graph_attention_parity.py):
for models where tebd_input_mode in {concat, strip} + attn_layer > 0 +
smooth_type_embedding=True, dense and the carry-all graph default
(neighbor_list=None) intentionally diverge (NeighborGraph PR-D: dense keeps
sel-padding phantom terms in the attention softmax denominator, the graph
route does not). Verified empirically this is not strip-specific: a plain
concat se_atten with smooth_type_embedding=True shows the same ~1e-7
order-of-magnitude divergence at attn_layer>0, and exactly zero divergence
at attn_layer=0 (no softmax involved there at all) -- confirming the gap
lives entirely in the shared attention code, not in anything this PR added.

Add model_dpa1_smooth (concat's counterpart to model_se_atten_v2) so both
tebd_input_mode values are covered by the same mechanism, and a
KNOWN_GRAPH_DENSE_DIVERGENT set used only by test_default_fallback to widen
its tolerance for exactly these two models (atol=3e-5, rtol=1e-3 -- measured
empirically: energy ~1e-7, force ~1e-6, virial up to ~1.3e-5, since virial
is a derivative and amplifies the softmax-denominator perturbation more than
the value itself). All other models keep the original tight tolerance.
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.

2 participants