Skip to content

Commit c0c1f0c

Browse files
fix(dpmodel): remap virtual type embeddings (deepmodeling#5856)
Closes deepmodeling#5665. ## Summary - centralize remapping of negative virtual atom types to the reserved final row of padded embedding tables - apply the same pair-index convention across dpmodel, pt_expt, legacy PyTorch, and Paddle DPA1/DPA2/SE_T implementations - keep real-type-only exclusion and normalization tables on a separate clamp-to-real convention - use the shared remapping helper in SeZMTypeEmbedding - cover DPA1 virtual neighbors inside the cutoff, DPA2, both DPA3 local-mapping branches, pt_expt pair arithmetic, and legacy PyTorch descriptors ## Sentinel convention Only tables explicitly constructed with a final padding row may use ntypes as the remapped virtual type. Real-type-only tables such as davg, dstd, exclusion inputs, and spin masks must instead receive masked or clamped real-type indices. TypeEmbedNet reconstructs a literal zero padding row; SeZMTypeEmbedding reserves and initializes its stored final row to zero. ## Validation - focused virtual-type and backend regressions: 15 passed - DPA1 graph parity plus the existing DPA2 descriptor tests: 18 passed - pt_expt DPA1 export tests: 8 passed - changed-file ruff check: passed - ruff format .: passed - git diff --check: passed - Paddle-specific code was statically checked but could not be executed locally because Paddle is not installed - full ruff check . still reports five unrelated pre-existing findings in deepmd/jax/jax_md/__init__.py and deepmd/tf/entrypoints/__init__.py Coding agent: Codex Codex version: codex-cli 0.144.6 Model: gpt-5.6-sol Reasoning effort: xhigh <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added consistent padding-aware type-embedding handling across dense, graph, and accelerated descriptor calculations. - Virtual or negative atom types now map safely to the designated padding embedding. - **Bug Fixes** - Prevented incorrect embedding lookups and invalid type indexing across supported execution paths. - Ensured virtual types produce results equivalent to explicit padding types. - **Tests** - Added regression coverage across NumPy, PyTorch, descriptor modes, and embedding pathways. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: njzjz-bot <njzjz.bot@gmail.com> Co-authored-by: njzjz-bot <njzjz-bot@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 625646c commit c0c1f0c

17 files changed

Lines changed: 924 additions & 29 deletions

File tree

deepmd/dpmodel/descriptor/dpa1.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
)
5353
from deepmd.dpmodel.utils.type_embed import (
5454
TypeEmbedNet,
55+
remap_atype_to_padding,
56+
take_type_embedding,
5557
)
5658
from deepmd.dpmodel.utils.update_sel import (
5759
UpdateSel,
@@ -904,7 +906,7 @@ def _call_dense(
904906
type_embedding = self.type_embedding.call()
905907
# nf x nall x tebd_dim
906908
atype_embd_ext = xp.reshape(
907-
xp.take(type_embedding, xp.reshape(atype_ext, (-1,)), axis=0),
909+
take_type_embedding(type_embedding, xp.reshape(atype_ext, (-1,))),
908910
(nf, nall, self.tebd_dim),
909911
)
910912
# nfnl x tebd_dim
@@ -1006,7 +1008,7 @@ def call_graph(
10061008
# gradient so the tebd net never trains; type_embedding already lives
10071009
# on the model device, so the device cast was redundant anyway.
10081010
atype_local = xp.asarray(atype, device=dev)
1009-
atype_embd = xp.take(type_embedding, atype_local, axis=0) # (N, tebd_dim)
1011+
atype_embd = take_type_embedding(type_embedding, atype_local)
10101012
grrg = xp.concat([grrg, atype_embd], axis=-1)
10111013
if in_dtype != prec:
10121014
grrg = xp.astype(grrg, in_dtype)
@@ -1839,6 +1841,7 @@ def call(
18391841
# Gather neighbor types: (nf, nall) -> (nf, nloc*nnei)
18401842
nei_type = xp_take_along_axis(atype_ext, nlist_2d, axis=1)
18411843
nei_type = xp.reshape(nei_type, (-1,)) # (nf * nloc * nnei,)
1844+
nei_type = remap_atype_to_padding(nei_type, ntypes_with_padding)
18421845
# (nf x nl x nnei) x ng
18431846
nei_type_index = xp.tile(xp.reshape(nei_type, (-1, 1)), (1, ng))
18441847
if self.type_one_side:
@@ -1853,9 +1856,11 @@ def call(
18531856
# (nf x nl x nnei) x ng
18541857
gg_t = xp_take_along_axis(tt_full, nei_type_index, axis=0)
18551858
else:
1859+
center_type = remap_atype_to_padding(atype, ntypes_with_padding)
18561860
idx_i = xp.reshape(
18571861
xp.tile(
1858-
(xp.reshape(atype, (-1, 1)) * ntypes_with_padding), (1, nnei)
1862+
(xp.reshape(center_type, (-1, 1)) * ntypes_with_padding),
1863+
(1, nnei),
18591864
),
18601865
(-1,),
18611866
)
@@ -2016,22 +2021,27 @@ def call_graph(
20162021
# value so the kernel stays jit/export-traceable (no concretize of n_node).
20172022
n_total = atype.shape[0]
20182023
atype = xp.asarray(atype, device=dev)
2024+
# Padded embedding tables reserve their final row, whereas exclusion
2025+
# and normalization tables contain only real types. Keep both forms so
2026+
# each downstream lookup receives the sentinel convention it expects.
2027+
safe_real_atype = xp.where(atype >= 0, atype, xp.zeros_like(atype))
20192028
# descriptor-level pair exclusion: same canonical transform as the
20202029
# model-level ``pair_exclude_types`` (decision #18). Masked edges
20212030
# contribute zero to every segment_sum below; the dense path's
20222031
# nlist-erasure + env-mat zeroing is reproduced exactly.
20232032
# apply_pair_exclusion is a no-op when self.emask has no exclusions.
2024-
graph = apply_pair_exclusion(graph, atype, self.emask)
2033+
graph = apply_pair_exclusion(graph, safe_real_atype, self.emask)
20252034
src = graph.edge_index[0, :]
20262035
dst = graph.edge_index[1, :]
20272036
center_type = xp.take(atype, dst, axis=0) # (E,)
20282037
nei_type = xp.take(atype, src, axis=0) # (E,)
2038+
center_type_for_stats = xp.take(safe_real_atype, dst, axis=0)
20292039
# per-edge env-mat 4-vector, normalized by the center (dst) atom type.
20302040
# self.mean/self.stddev are slot-independent (ntypes, nnei, 4); slot 0 is
20312041
# the canonical per-type vector.
20322042
rr, sw_e = edge_env_mat(
20332043
graph.edge_vec,
2034-
center_type,
2044+
center_type_for_stats,
20352045
self.mean[:, 0, :],
20362046
self.stddev[:, 0, :],
20372047
self.rcut,
@@ -2061,9 +2071,9 @@ def call_graph(
20612071
# under torch and severs the type-embedding weight gradient (the tebd
20622072
# net would never train); type_embedding already lives on the device.
20632073
tebd = type_embedding
2064-
atype_embd_nlist = xp.take(tebd, nei_type, axis=0) # (E, tebd_dim)
2074+
atype_embd_nlist = take_type_embedding(tebd, nei_type)
20652075
if not self.type_one_side:
2066-
atype_embd_nnei = xp.take(tebd, center_type, axis=0) # (E, tebd_dim)
2076+
atype_embd_nnei = take_type_embedding(tebd, center_type)
20672077
ss = xp.concat([ss, atype_embd_nlist, atype_embd_nnei], axis=-1)
20682078
else:
20692079
ss = xp.concat([ss, atype_embd_nlist], axis=-1)
@@ -2147,6 +2157,8 @@ def _graph_edge_gg_strip(
21472157
xp = array_api_compat.array_namespace(ss)
21482158
nt = self.tebd_dim
21492159
ntypes_with_padding = type_embedding.shape[0]
2160+
center_type = remap_atype_to_padding(center_type, ntypes_with_padding)
2161+
nei_type = remap_atype_to_padding(nei_type, ntypes_with_padding)
21502162
# geometric net on the radial channel only (dense: gg_s = cal_g(ss_scalar))
21512163
gg_s = self.embeddings[0].call(ss) # (E, ng)
21522164
if self.type_one_side:

deepmd/dpmodel/descriptor/dpa2.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
)
4040
from deepmd.dpmodel.utils.type_embed import (
4141
TypeEmbedNet,
42+
take_type_embedding,
4243
)
4344
from deepmd.dpmodel.utils.update_sel import (
4445
UpdateSel,
@@ -1349,7 +1350,7 @@ def _call_dense(
13491350
type_embedding = self.type_embedding.call()
13501351
# repinit
13511352
g1_ext = xp.reshape(
1352-
xp.take(type_embedding, xp.reshape(atype_ext, (-1,)), axis=0),
1353+
take_type_embedding(type_embedding, xp.reshape(atype_ext, (-1,))),
13531354
(nframes, nall, self.tebd_dim),
13541355
)
13551356
g1_inp = xp_take_first_n(g1_ext, 1, nloc)

deepmd/dpmodel/descriptor/dpa3.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from deepmd.dpmodel.utils.type_embed import (
3030
TypeEmbedNet,
31+
take_type_embedding,
3132
)
3233
from deepmd.dpmodel.utils.update_sel import (
3334
UpdateSel,
@@ -740,16 +741,15 @@ def call(
740741
type_embedding = self.type_embedding.call()
741742
if self.use_loc_mapping:
742743
node_ebd_ext = xp.reshape(
743-
xp.take(
744+
take_type_embedding(
744745
type_embedding,
745746
xp.reshape(xp_take_first_n(atype_ext, 1, nloc), (-1,)),
746-
axis=0,
747747
),
748748
(nframes, nloc, self.tebd_dim),
749749
)
750750
else:
751751
node_ebd_ext = xp.reshape(
752-
xp.take(type_embedding, xp.reshape(atype_ext, (-1,)), axis=0),
752+
take_type_embedding(type_embedding, xp.reshape(atype_ext, (-1,))),
753753
(nframes, nall, self.tebd_dim),
754754
)
755755

deepmd/dpmodel/descriptor/dpa4_nn/embedding.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
from deepmd.dpmodel.utils.seed import (
4343
child_seed,
4444
)
45+
from deepmd.dpmodel.utils.type_embed import (
46+
remap_atype_to_padding,
47+
)
4548
from deepmd.utils.version import (
4649
check_version_compatibility,
4750
)
@@ -146,6 +149,8 @@ def call(self, atype: Any) -> Any:
146149
# torch.embedding gather: flatten the indices to int64, take the rows,
147150
# then restore the original index shape.
148151
index = xp.astype(xp.reshape(atype, (-1,)), xp.int64)
152+
if self.padding:
153+
index = remap_atype_to_padding(index, self.ntypes + 1)
149154
out = xp.take(weight, index, axis=0)
150155
return xp.reshape(out, (*atype.shape, self.embed_dim))
151156

deepmd/dpmodel/descriptor/se_t_tebd.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
)
3737
from deepmd.dpmodel.utils.type_embed import (
3838
TypeEmbedNet,
39+
remap_atype_to_padding,
40+
take_type_embedding,
3941
)
4042
from deepmd.dpmodel.utils.update_sel import (
4143
UpdateSel,
@@ -398,7 +400,7 @@ def call(
398400
type_embedding = self.type_embedding.call()
399401
# nf x nall x tebd_dim
400402
atype_embd_ext = xp.reshape(
401-
xp.take(type_embedding, xp.reshape(atype_ext, (-1,)), axis=0),
403+
take_type_embedding(type_embedding, xp.reshape(atype_ext, (-1,))),
402404
(nf, nall, self.tebd_dim),
403405
)
404406
# nfnl x tebd_dim
@@ -925,6 +927,7 @@ def call(
925927
nei_type = xp_take_along_axis(atype_ext, nlist_index, axis=1)
926928
# nfnl x nnei
927929
nei_type = xp.reshape(nei_type, (nf * nloc, nnei))
930+
nei_type = remap_atype_to_padding(nei_type, ntypes_with_padding)
928931

929932
# nfnl x nnei x nnei
930933
nei_type_i = xp.tile(nei_type[:, :, np.newaxis], (1, 1, nnei))

deepmd/dpmodel/utils/type_embed.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,75 @@ def _array_device_or_none(array: Array) -> Any:
3131
return None
3232

3333

34+
def remap_atype_to_padding(atype: Array, ntypes_with_padding: int) -> Array:
35+
"""Map negative placeholder types to a padded table's final row.
36+
37+
Parameters
38+
----------
39+
atype : Array
40+
Atom-type indices. Negative entries denote virtual or padding atoms.
41+
ntypes_with_padding : int
42+
Number of rows in a table that reserves its final row for padding.
43+
44+
Returns
45+
-------
46+
Array
47+
Atom-type indices with every negative entry replaced by
48+
``ntypes_with_padding - 1``.
49+
50+
Notes
51+
-----
52+
This sentinel convention is valid only for tables that explicitly include
53+
a final padding row, such as descriptor type-embedding and type-pair
54+
tables. It must not be used for real-type-only tables such as ``davg``,
55+
``dstd``, or spin masks; virtual entries must be masked or clamped to a
56+
valid real type before indexing those tables.
57+
"""
58+
xp = array_api_compat.array_namespace(atype)
59+
return xp.where(
60+
atype >= 0,
61+
atype,
62+
xp.full_like(atype, ntypes_with_padding - 1),
63+
)
64+
65+
66+
def take_type_embedding(type_embedding: Array, atype: Array) -> Array:
67+
"""Gather type embeddings, mapping virtual atom types to the padding row.
68+
69+
Parameters
70+
----------
71+
type_embedding : Array
72+
Type-embedding table whose final row is reserved for virtual or
73+
padding atoms.
74+
atype : Array
75+
Atom-type indices with arbitrary shape. Negative entries denote
76+
virtual or padding atoms.
77+
78+
Returns
79+
-------
80+
Array
81+
Gathered embeddings with shape ``(*atype.shape,
82+
type_embedding.shape[-1])``.
83+
84+
Notes
85+
-----
86+
``TypeEmbedNet`` reconstructs a literal zero padding row on every call.
87+
``SeZMTypeEmbedding`` stores its reserved row in the trainable embedding
88+
array and initializes it to zero. This helper guarantees selection of the
89+
reserved row; the table implementation remains responsible for keeping
90+
that row neutral.
91+
92+
Negative placeholder types must be remapped explicitly because negative
93+
gather indices either wrap or fail depending on the array backend.
94+
"""
95+
# The caller's atom-type array determines the active backend. Model
96+
# conversion keeps the embedding table in that same namespace while
97+
# preserving trainable tensors and their gradients.
98+
xp = array_api_compat.array_namespace(atype)
99+
safe_atype = remap_atype_to_padding(atype, type_embedding.shape[0])
100+
return xp.take(type_embedding, xp.astype(safe_atype, xp.int64), axis=0)
101+
102+
34103
class TypeEmbedNet(NativeOP):
35104
r"""Type embedding network.
36105

deepmd/pd/model/descriptor/se_atten.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,12 +502,13 @@ def forward(
502502
assert extended_atype_embd is not None
503503
nframes, nloc, nnei = nlist.shape
504504
atype = extended_atype[:, :nloc]
505+
atype_for_env = paddle.where(atype >= 0, atype, paddle.zeros_like(atype))
505506
nb = nframes
506507
nall = extended_coord.reshape([nb, -1, 3]).shape[1]
507508
dmatrix, diff, sw = prod_env_mat(
508509
extended_coord,
509510
nlist,
510-
atype,
511+
atype_for_env,
511512
self.mean,
512513
self.stddev,
513514
self.rcut,
@@ -582,6 +583,13 @@ def forward(
582583
nei_type = paddle.take_along_axis(
583584
extended_atype, indices=nlist_index, axis=1, broadcast=False
584585
)
586+
# Padded embedding tables reserve their final row for virtual
587+
# atoms; remap explicitly before pair-index arithmetic.
588+
nei_type = paddle.where(
589+
nei_type >= 0,
590+
nei_type,
591+
paddle.full_like(nei_type, ntypes_with_padding - 1),
592+
)
585593
# (nf x nl x nnei) x ng
586594
nei_type_index = nei_type.reshape([-1, 1]).expand([-1, ng]).to(paddle.int64)
587595
if self.type_one_side:
@@ -591,8 +599,13 @@ def forward(
591599
tt_full, indices=nei_type_index, axis=0, broadcast=False
592600
)
593601
else:
602+
center_type = paddle.where(
603+
atype >= 0,
604+
atype,
605+
paddle.full_like(atype, ntypes_with_padding - 1),
606+
)
594607
idx_i = paddle.tile(
595-
atype.reshape([-1, 1]) * ntypes_with_padding, [1, nnei]
608+
center_type.reshape([-1, 1]) * ntypes_with_padding, [1, nnei]
596609
).reshape([-1])
597610
idx_j = nei_type.reshape([-1])
598611
# (nf x nl x nnei) x ng

deepmd/pd/model/descriptor/se_t_tebd.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,12 +850,13 @@ def forward(
850850
assert extended_atype_embd is not None
851851
nframes, nloc, nnei = nlist.shape
852852
atype = extended_atype[:, :nloc]
853+
atype_for_env = paddle.where(atype >= 0, atype, paddle.zeros_like(atype))
853854
nb = nframes
854855
nall = extended_coord.reshape([nb, -1, 3]).shape[1]
855856
dmatrix, diff, sw = prod_env_mat(
856857
extended_coord,
857858
nlist,
858-
atype,
859+
atype_for_env,
859860
self.mean,
860861
self.stddev,
861862
self.rcut,
@@ -929,6 +930,11 @@ def forward(
929930
)
930931
# nfnl x nnei
931932
nei_type = nei_type.reshape([nfnl, nnei])
933+
nei_type = paddle.where(
934+
nei_type >= 0,
935+
nei_type,
936+
paddle.full_like(nei_type, ntypes_with_padding - 1),
937+
)
932938
# nfnl x nnei x nnei
933939
nei_type_i = nei_type.unsqueeze(2).expand([-1, -1, nnei])
934940
nei_type_j = nei_type.unsqueeze(1).expand([-1, nnei, -1])

deepmd/pt/model/descriptor/se_atten.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,12 +769,13 @@ def forward(
769769
assert extended_atype_embd is not None
770770
nframes, nloc, nnei = nlist.shape
771771
atype = extended_atype[:, :nloc]
772+
atype_for_env = atype.clamp_min(0)
772773
nb = nframes
773774
nall = extended_coord.view(nb, -1, 3).shape[1]
774775
dmatrix, diff, sw = prod_env_mat(
775776
extended_coord,
776777
nlist,
777-
atype,
778+
atype_for_env,
778779
self.mean,
779780
self.stddev,
780781
self.rcut,
@@ -897,6 +898,14 @@ def forward(
897898
nlist_index = nlist.reshape(nb, nloc * nnei)
898899
# nf x (nl x nnei)
899900
nei_type = torch.gather(extended_atype, dim=1, index=nlist_index)
901+
# Only padded type/type-pair tables use the final-row sentinel.
902+
# Remap before both one- and two-side indexing so negative virtual
903+
# types cannot wrap into an unrelated pair row.
904+
nei_type = torch.where(
905+
nei_type >= 0,
906+
nei_type,
907+
torch.full_like(nei_type, ntypes_with_padding - 1),
908+
)
900909
# Per-edge row index into the (padded) type-pair embedding table.
901910
if self.type_one_side:
902911
if self.tebd_compress:
@@ -906,8 +915,13 @@ def forward(
906915
tt_full = self.filter_layers_strip.networks[0](type_embedding)
907916
tebd_idx = nei_type.view(-1).to(torch.long)
908917
else:
918+
center_type = torch.where(
919+
atype >= 0,
920+
atype,
921+
torch.full_like(atype, ntypes_with_padding - 1),
922+
)
909923
idx_i = torch.tile(
910-
atype.reshape(-1, 1) * ntypes_with_padding, [1, nnei]
924+
center_type.reshape(-1, 1) * ntypes_with_padding, [1, nnei]
911925
).view(-1)
912926
tebd_idx = (idx_i + nei_type.view(-1)).to(torch.long)
913927
if self.tebd_compress:

0 commit comments

Comments
 (0)