Skip to content

Commit 2d80dcd

Browse files
author
Han Wang
committed
fix(tf2): apply model-level pair_exclude at nlist BUILD; align io test
Replace the bespoke test_pair_exclude_savedmodel.py with an aligned IOTest subclass (TestDeepPotPairExclude) in test_io.py: it supplies only the model dict and inherits test_data_equal / test_deep_eval, which export through every backend and cross-compare at rtol/atol 1e-12 (with the built-in all-NaN skip covering the numpy dpmodel force path). Gated on DP_TEST_TF2_ONLY, the mode in which test_deep_eval exercises the jax2tf '.savedmodel' path -- the TF v1 backend raises NotImplementedError on pair_exclude_types, so it must not run there. That aligned cross-backend eval exposed a real gap: the tf2 backend ('.savedmodeltf') silently ignored model-level pair_exclude_types (its SavedModel returned the non-excluded energy 2.847 vs the correct 2.843). tf2 was outside the original PR scope (dpmodel/pt_expt/jax) and its outer TF wrapper (deepmd/tf2/make_model.py) never folded the exclusion into the nlist. Fix it the same way as every other backend: thread pair_excl into model_call_from_call_lower and apply the canonical dpmodel apply_pair_exclusion_nlist at the nlist-BUILD seam (the tensors are already ndtensorflow arrays, so no wrap/unwrap). All five backends now agree.
1 parent d182628 commit 2d80dcd

4 files changed

Lines changed: 82 additions & 172 deletions

File tree

deepmd/tf2/make_model.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Callable,
44
)
55
from typing import (
6+
TYPE_CHECKING,
67
Any,
78
)
89

@@ -31,6 +32,11 @@
3132
normalize_coord,
3233
)
3334

35+
if TYPE_CHECKING:
36+
from deepmd.dpmodel.utils.exclude_mask import (
37+
PairExcludeMask,
38+
)
39+
3440

3541
def _unwrap_tuple(values: tuple[Array, ...]) -> tuple[tf.Tensor, ...]:
3642
return tuple(to_tf_tensor(value) for value in values)
@@ -56,9 +62,17 @@ def model_call_from_call_lower(
5662
fparam: Array | None,
5763
aparam: Array | None,
5864
do_atomic_virial: bool = False,
65+
pair_excl: "PairExcludeMask | None" = None,
5966
) -> dict[str, Array]:
6067
"""Return model prediction from lower interface.
6168
69+
``pair_excl`` is the model-level pair-type exclusion mask. Exclusion is a
70+
nlist-BUILD transform (decision #18/A4): it is folded into the neighbor
71+
list here, where the list is built, by the canonical dpmodel
72+
``apply_pair_exclusion_nlist`` (the tensors are already ``ndtensorflow``
73+
arrays), because the lower model consumes a pre-excluded nlist and never
74+
re-applies it.
75+
6276
Parameters
6377
----------
6478
coord
@@ -126,6 +140,15 @@ def no_pbc() -> tuple[Array, Array, Array]:
126140
# so it doesn't need to be distinguished here
127141
distinguish_types=False,
128142
)
143+
if pair_excl is not None:
144+
# Reuse the canonical dpmodel nlist-BUILD transform (decision #18/A4):
145+
# ``nlist``/``extended_atype`` are already ndtensorflow arrays, so no
146+
# wrap/unwrap is needed. Identity when there is nothing to exclude.
147+
from deepmd.dpmodel.utils.nlist import (
148+
apply_pair_exclusion_nlist,
149+
)
150+
151+
nlist = apply_pair_exclusion_nlist(nlist, extended_atype, pair_excl)
129152
extended_coord = xp.reshape(extended_coord, (nframes, -1, 3))
130153
model_predict_lower = wrap_value(
131154
call_lower(

deepmd/tf2/utils/serialization.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,9 @@ def call(
377377
fparam=fparam,
378378
aparam=aparam,
379379
do_atomic_virial=do_atomic_virial,
380+
# exclusion is a nlist-BUILD transform (decision #18/A4);
381+
# the traced lower consumes a pre-excluded nlist.
382+
pair_excl=getattr(model.atomic_model, "pair_excl", None),
380383
)
381384
)
382385

source/tests/consistent/io/test_io.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,59 @@ def setUp(self) -> None:
311311

312312
def tearDown(self) -> None:
313313
IOTest.tearDown(self)
314+
315+
316+
@unittest.skipIf(
317+
not DP_TEST_TF2_ONLY,
318+
"pair_exclude_types is not supported by the TF v1 backend; it is validated "
319+
"in the TF2-only job, where test_deep_eval also exercises the jax2tf "
320+
"'.savedmodel' export path (see the backend table in test_deep_eval).",
321+
)
322+
class TestDeepPotPairExclude(unittest.TestCase, IOTest):
323+
"""Model-level ``pair_exclude_types`` is a nlist-BUILD transform (decision
324+
#18/A4). Every backend folds it in where the neighbor list is built (the
325+
jax2tf ``.savedmodel`` export reuses the dpmodel
326+
``apply_pair_exclusion_nlist`` via the ``ndtensorflow`` namespace), so the
327+
exported models must still eval-agree across backends.
328+
"""
329+
330+
def setUp(self) -> None:
331+
model_def_script = {
332+
"type_map": ["O", "H"],
333+
"pair_exclude_types": [[0, 1]],
334+
"descriptor": {
335+
"type": "se_e2_a",
336+
"sel": [20, 20],
337+
"rcut_smth": 0.50,
338+
"rcut": 6.00,
339+
"neuron": [
340+
3,
341+
6,
342+
],
343+
"resnet_dt": False,
344+
"axis_neuron": 2,
345+
"precision": "float64",
346+
"type_one_side": True,
347+
"seed": 1,
348+
},
349+
"fitting_net": {
350+
"type": "ener",
351+
"neuron": [
352+
5,
353+
5,
354+
],
355+
"resnet_dt": True,
356+
"precision": "float64",
357+
"atom_ener": [],
358+
"seed": 1,
359+
},
360+
}
361+
model = get_model(copy.deepcopy(model_def_script))
362+
self.data = {
363+
"model": model.serialize(),
364+
"backend": "test",
365+
"model_def_script": model_def_script,
366+
}
367+
368+
def tearDown(self) -> None:
369+
IOTest.tearDown(self)

source/tests/consistent/io/test_pair_exclude_savedmodel.py

Lines changed: 0 additions & 172 deletions
This file was deleted.

0 commit comments

Comments
 (0)