Skip to content

Commit 6a9d4f6

Browse files
committed
Merge remote-tracking branch 'upstream/master' into pr/install-skill
2 parents 0c1e377 + bdb4007 commit 6a9d4f6

150 files changed

Lines changed: 28831 additions & 1638 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deepmd/dpmodel/atomic_model/dp_atomic_model.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def __init__(
119119
self.fitting_net = fitting
120120
self.fitting_net.reinit_exclude(self.atom_exclude_types)
121121
self.type_map = type_map
122-
self.add_chg_spin_ebd: bool = self.descriptor.get_dim_chg_spin() > 0
122+
self.add_chg_spin_ebd: bool = self.descriptor.has_chg_spin_ebd()
123123
# Structural capability: only descriptors with a native spin
124124
# conditioning mechanism (currently DPA4) accept a ``spin`` kwarg on
125125
# ``call_graph`` at all -- unlike ``charge_spin``, which every
@@ -182,6 +182,10 @@ def supports_graph_export(self) -> bool:
182182
"""Delegates to this model's own descriptor."""
183183
return bool(self.descriptor.supports_graph_export())
184184

185+
def compression_needs_min_nbor_dist(self) -> bool:
186+
"""Delegates to this model's own descriptor."""
187+
return bool(self.descriptor.compression_needs_min_nbor_dist())
188+
185189
def supports_native_spin(self) -> bool:
186190
"""Delegates to this model's own descriptor (cached at construction)."""
187191
return self._supports_native_spin

deepmd/dpmodel/atomic_model/linear_atomic_model.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,15 @@ def enable_compression(
303303
check_frequency,
304304
)
305305

306+
def compression_needs_min_nbor_dist(self) -> bool:
307+
"""Required as soon as ANY child consumes it.
308+
309+
The statistic is measured once and handed to every child, so a single
310+
child that tabulates from the shortest observed distance keeps the
311+
neighbor-statistics pass for the whole composition.
312+
"""
313+
return any(m.compression_needs_min_nbor_dist() for m in self.models)
314+
306315
def uses_graph_lower(self) -> bool:
307316
"""Graph-capable iff EVERY child supports the graph lower.
308317

deepmd/dpmodel/atomic_model/make_base_atomic_model.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,17 @@ def enable_compression(
190190
"""
191191
raise NotImplementedError("This atomi model doesn't support compression!")
192192

193+
def compression_needs_min_nbor_dist(self) -> bool:
194+
"""Whether :meth:`enable_compression` consumes ``min_nbor_dist``.
195+
196+
Returns
197+
-------
198+
bool
199+
Concrete default ``True``, so a model that does not report
200+
otherwise keeps the neighbor-statistics pass.
201+
"""
202+
return True
203+
193204
def make_atom_mask(
194205
self,
195206
atype: t_tensor,

deepmd/dpmodel/atomic_model/pairtab_atomic_model.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,3 +505,14 @@ def enable_compression(
505505
) -> None:
506506
"""Pairtab model does not support compression."""
507507
pass
508+
509+
def compression_needs_min_nbor_dist(self) -> bool:
510+
"""Return whether compression consumes the minimum neighbor distance.
511+
512+
Returns
513+
-------
514+
bool
515+
Always ``False``. The tabulated pair potential carries its own
516+
domain, so compression is a no-op here.
517+
"""
518+
return False

deepmd/dpmodel/descriptor/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from .dpa4 import (
1212
DescrptDPA4,
1313
)
14+
from .dpa4c import (
15+
DescrptDPA4C,
16+
)
1417
from .hybrid import (
1518
DescrptHybrid,
1619
)
@@ -38,6 +41,7 @@
3841
"DescrptDPA2",
3942
"DescrptDPA3",
4043
"DescrptDPA4",
44+
"DescrptDPA4C",
4145
"DescrptHybrid",
4246
"DescrptSeA",
4347
"DescrptSeAttenV2",

deepmd/dpmodel/descriptor/dpa3.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
from deepmd.dpmodel.utils.update_sel import (
3434
UpdateSel,
3535
)
36+
from deepmd.utils.charge_state import (
37+
CHARGE_OFFSET,
38+
CHARGE_TABLE_ROWS,
39+
MULTIPLICITY_TABLE_ROWS,
40+
validate_charge_state,
41+
)
3642
from deepmd.utils.data_system import (
3743
DeepmdDataSystem,
3844
)
@@ -468,11 +474,11 @@ def init_subclass_params(sub_data: dict | Any, sub_class: type) -> Any:
468474

469475
self.use_econf_tebd = use_econf_tebd
470476
self.add_chg_spin_ebd = add_chg_spin_ebd
471-
if default_chg_spin is not None and len(default_chg_spin) != 2:
472-
raise ValueError(
473-
"default_chg_spin must have exactly 2 values [charge, spin]"
474-
)
475-
self.default_chg_spin = default_chg_spin
477+
self.default_chg_spin = (
478+
None
479+
if default_chg_spin is None
480+
else validate_charge_state(default_chg_spin)
481+
)
476482
self.use_tebd_bias = use_tebd_bias
477483
self.use_loc_mapping = use_loc_mapping
478484
self.type_map = type_map
@@ -494,18 +500,16 @@ def init_subclass_params(sub_data: dict | Any, sub_class: type) -> Any:
494500

495501
if self.add_chg_spin_ebd:
496502
self.cs_activation_fn = get_activation_fn(activation_function)
497-
# -100 ~ 100 is a conservative bound
498503
self.chg_embedding = TypeEmbedNet(
499-
ntypes=200,
504+
ntypes=CHARGE_TABLE_ROWS,
500505
neuron=[self.tebd_dim],
501506
padding=True,
502507
activation_function="Linear",
503508
precision=precision,
504509
seed=child_seed(seed, 3),
505510
)
506-
# 100 is a conservative upper bound
507511
self.spin_embedding = TypeEmbedNet(
508-
ntypes=100,
512+
ntypes=MULTIPLICITY_TABLE_ROWS,
509513
neuron=[self.tebd_dim],
510514
padding=True,
511515
activation_function="Linear",
@@ -543,6 +547,10 @@ def get_dim_chg_spin(self) -> int:
543547
"""Returns the dimension of charge_spin input."""
544548
return 2 if self.add_chg_spin_ebd else 0
545549

550+
def has_chg_spin_ebd(self) -> bool:
551+
"""Return whether a frame charge/spin condition is configured."""
552+
return self.add_chg_spin_ebd
553+
546554
def get_default_chg_spin(self) -> list[float] | None:
547555
"""Returns the default charge_spin values."""
548556
return self.default_chg_spin
@@ -755,7 +763,7 @@ def call(
755763
assert self.spin_embedding is not None
756764
chg_tebd = self.chg_embedding.call()
757765
spin_tebd = self.spin_embedding.call()
758-
charge = xp.astype(charge_spin[:, 0], xp.int64) + 100
766+
charge = xp.astype(charge_spin[:, 0], xp.int64) + CHARGE_OFFSET
759767
spin = xp.astype(charge_spin[:, 1], xp.int64)
760768
chg_ebd = xp.reshape(
761769
xp.take(chg_tebd, xp.reshape(charge, (-1,)), axis=0),

deepmd/dpmodel/descriptor/dpa4.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
from deepmd.dpmodel.utils.update_sel import (
7474
UpdateSel,
7575
)
76+
from deepmd.utils.charge_state import (
77+
validate_charge_state,
78+
)
7679
from deepmd.utils.version import (
7780
check_version_compatibility,
7881
)
@@ -784,10 +787,10 @@ def __init__(
784787
self.edge_cartesian = bool(edge_cartesian)
785788
self.node_cartesian = str(node_cartesian)
786789
self.add_chg_spin_ebd = bool(add_chg_spin_ebd)
787-
if default_chg_spin is not None and len(default_chg_spin) != 2:
788-
raise ValueError("`default_chg_spin` must contain [charge, spin].")
789790
self.default_chg_spin = (
790-
None if default_chg_spin is None else [float(x) for x in default_chg_spin]
791+
None
792+
if default_chg_spin is None
793+
else validate_charge_state(default_chg_spin)
791794
)
792795

793796
# === Native per-atom spin embedding ===
@@ -2282,6 +2285,10 @@ def get_ntypes(self) -> int:
22822285
def get_type_map(self) -> list[str]:
22832286
return self.type_map if self.type_map is not None else []
22842287

2288+
def has_chg_spin_ebd(self) -> bool:
2289+
"""Return whether a frame charge/spin condition is configured."""
2290+
return self.charge_spin_embedding is not None
2291+
22852292
def get_dim_chg_spin(self) -> int:
22862293
"""Return the charge/spin condition width."""
22872294
return 2 if self.add_chg_spin_ebd else 0

deepmd/dpmodel/descriptor/dpa4_nn/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@
7373
merge_lora_into_base,
7474
strip_lora_from_extra_state,
7575
)
76+
from .mlp import (
77+
SwiGLUMLP,
78+
resolve_swiglu_hidden_width,
79+
)
7680
from .norm import (
7781
EquivariantRMSNorm,
7882
ReducedEquivariantRMSNorm,
@@ -159,6 +163,7 @@
159163
"SeZMTypeEmbedding",
160164
"SpinEmbedding",
161165
"SwiGLU",
166+
"SwiGLUMLP",
162167
"WignerDCalculator",
163168
"apply_lora_to_sezm",
164169
"build_cartesian_basis",
@@ -189,6 +194,7 @@
189194
"quaternion_z_rotation",
190195
"resolve_s2_grid_resolution",
191196
"resolve_so3_grid",
197+
"resolve_swiglu_hidden_width",
192198
"safe_norm",
193199
"segment_envelope_gated_softmax",
194200
"so3_packed_index",

deepmd/dpmodel/descriptor/dpa4_nn/embedding.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
from deepmd.dpmodel.utils.type_embed import (
4646
remap_atype_to_padding,
4747
)
48+
from deepmd.utils.charge_state import (
49+
CHARGE_OFFSET,
50+
CHARGE_TABLE_ROWS,
51+
MULTIPLICITY_TABLE_ROWS,
52+
)
4853
from deepmd.utils.version import (
4954
check_version_compatibility,
5055
)
@@ -128,26 +133,41 @@ def __init__(
128133
# === Step 2. Register the embedding table parameter ===
129134
self.adam_type_embedding = table
130135

131-
def call(self, atype: Any) -> Any:
136+
def call(self, atype: Any | None = None) -> Any:
132137
"""
133138
Gather type embeddings.
134139
135140
Parameters
136141
----------
137142
atype
138-
Atom types with shape (...,). Valid type range is [0, ntypes-1].
143+
Atom types with shape (...). Valid type range is [0, ntypes-1].
144+
If omitted, return the complete embedding table, including the
145+
optional padding row. This form is used by graph-native descriptor
146+
ABIs that precompute the table once per forward call.
139147
140148
Returns
141149
-------
142150
Array
143-
Type embeddings with shape (..., embed_dim).
151+
Gathered type embeddings with shape ``(..., embed_dim)`` when
152+
``atype`` is provided. Otherwise, the complete table with shape
153+
``(ntypes + int(padding), embed_dim)``.
144154
"""
155+
# === Step 1. Return the complete graph-native lookup table ===
156+
if atype is None:
157+
xp = array_api_compat.array_namespace(self.adam_type_embedding)
158+
return xp_asarray_nodetach(
159+
xp,
160+
self.adam_type_embedding[...],
161+
device=array_api_compat.device(self.adam_type_embedding),
162+
)
163+
164+
# === Step 2. Gather rows for an explicit atom-type tensor ===
145165
xp = array_api_compat.array_namespace(atype)
146166
weight = xp_asarray_nodetach(
147167
xp, self.adam_type_embedding[...], device=array_api_compat.device(atype)
148168
)
149-
# torch.embedding gather: flatten the indices to int64, take the rows,
150-
# then restore the original index shape.
169+
# Flattening provides one backend-neutral gather while preserving every
170+
# leading batch or graph dimension on restoration.
151171
index = xp.astype(xp.reshape(atype, (-1,)), xp.int64)
152172
if self.padding:
153173
index = remap_atype_to_padding(index, self.ntypes + 1)
@@ -869,15 +889,15 @@ def __init__(
869889
raise ValueError("`embed_dim` must be positive")
870890

871891
self.charge_embedding = SeZMTypeEmbedding(
872-
ntypes=200,
892+
ntypes=CHARGE_TABLE_ROWS,
873893
embed_dim=self.embed_dim,
874894
precision=self.precision,
875895
seed=child_seed(seed, 0),
876896
trainable=self.trainable,
877897
padding=False,
878898
)
879899
self.spin_embedding = SeZMTypeEmbedding(
880-
ntypes=100,
900+
ntypes=MULTIPLICITY_TABLE_ROWS,
881901
embed_dim=self.embed_dim,
882902
precision=self.precision,
883903
seed=child_seed(seed, 1),
@@ -908,7 +928,7 @@ def call(self, charge_spin: Any) -> Any:
908928
Mixed condition embedding with shape (nf, embed_dim).
909929
"""
910930
xp = array_api_compat.array_namespace(charge_spin)
911-
charge = xp.astype(charge_spin[:, 0], xp.int64) + 100
931+
charge = xp.astype(charge_spin[:, 0], xp.int64) + CHARGE_OFFSET
912932
spin = xp.astype(charge_spin[:, 1], xp.int64)
913933
charge_embed = self.charge_embedding(charge)
914934
spin_embed = self.spin_embedding(spin)

deepmd/dpmodel/descriptor/dpa4_nn/grid_net.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,37 @@ def _build_frame_degree_index(
9999
raise ValueError("`coefficient_layout` must be either 'packed' or 'm_major'")
100100

101101

102+
def _degree_batched_matmul(xp: Any, coeff: Any, weight: Any) -> Any:
103+
"""Contract ``einsum("ndfi,dio->ndfo")`` batched over the degree axis.
104+
105+
Parameters
106+
----------
107+
xp : Any
108+
The array namespace of ``coeff``.
109+
coeff : Array
110+
Coefficients with shape ``(N, D, F, i)``.
111+
weight : Array
112+
Per-degree weights with shape ``(D, i, o)``.
113+
114+
Returns
115+
-------
116+
Array
117+
Contracted coefficients with shape ``(N, D, F, o)``.
118+
119+
Notes
120+
-----
121+
Batching over the ``(D, F)`` axes, not over ``N``: expanding ``weight``
122+
across ``F`` costs ``D*F*i*o`` elements, whereas batching over ``N``
123+
(or collapsing ``N*F``, which needs a materialized permuted copy of
124+
``coeff``) touches ``N*D*F*i`` elements — a factor ``N/o`` more. No
125+
reshape is involved, so an empty ``N`` batch (empty graph/edge set, or
126+
a distributed rank owning no nodes) flows through naturally.
127+
"""
128+
coeff_df = xp.permute_dims(coeff, (1, 2, 0, 3)) # (D, F, N, i)
129+
out = xp.matmul(coeff_df, weight[:, None, :, :]) # (D, F, N, o)
130+
return xp.permute_dims(out, (2, 0, 1, 3)) # (N, D, F, o)
131+
132+
102133
def _project_frames(coeff: Any, proj: ChannelLinear, n_frames: int) -> Any:
103134
"""
104135
Apply a channel-only linear map to each Wigner-D frame independently.
@@ -493,9 +524,8 @@ def call(self, coeff: Any) -> Any:
493524
weight = xp_asarray_nodetach(xp, self.weight[...], device=device)
494525
degree_index = xp_asarray_nodetach(xp, self.degree_index, device=device)
495526
weight = xp.take(weight, degree_index, axis=0)
496-
# einsum "ndfi,dio->ndfo" as a broadcast batched matmul:
497-
# (N, D, F, i) @ (1, D, i, o) -> (N, D, F, o)
498-
return xp.matmul(coeff, weight[None, ...])
527+
# Batched over the (D, F) axes, never over N -- see the helper's note.
528+
return _degree_batched_matmul(xp, coeff, weight)
499529

500530
def serialize(self) -> dict[str, Any]:
501531
"""Serialize the FrameContract to a dict."""
@@ -575,9 +605,8 @@ def call(self, coeff: Any) -> Any:
575605
weight = xp_asarray_nodetach(xp, self.weight[...], device=device)
576606
degree_index = xp_asarray_nodetach(xp, self.degree_index, device=device)
577607
weight = xp.take(weight, degree_index, axis=0)
578-
# einsum "ndfi,dio->ndfo" as a broadcast batched matmul:
579-
# (N, D, F, i) @ (1, D, i, o) -> (N, D, F, o)
580-
return xp.matmul(coeff, weight[None, ...])
608+
# Batched over the (D, F) axes, never over N -- see the helper's note.
609+
return _degree_batched_matmul(xp, coeff, weight)
581610

582611
def serialize(self) -> dict[str, Any]:
583612
"""Serialize the FrameExpand to a dict."""

0 commit comments

Comments
 (0)