Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bd39787
feat(dpa4c): add compact invariant descriptor and CUDA deployment
OutisLi Jul 21, 2026
880baf0
feat(dpa4c-spin): add end-to-end native-spin support
OutisLi Jul 29, 2026
5f697a4
feat(dpa4c): add runtime charge-state conditioning and safe graph fol…
OutisLi Jul 30, 2026
99c8468
feat(dpa4c): fine-tune native spin from a spin-free pretraining
OutisLi Aug 8, 2026
dab3a6e
fix(argcheck): use backend support registry for DPA4C
OutisLi Aug 14, 2026
b0a7c03
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 14, 2026
279b740
fix(dpa4c): harden compressed inference contracts
OutisLi Aug 14, 2026
9df1e97
fix(dpa4c): align export validation with compact lower support
OutisLi Aug 14, 2026
3009cd2
fix(dpa4c): address review on metrics, compression and charge states
OutisLi Aug 16, 2026
a81184f
test(pt): compare the relative-force loss on the training device
OutisLi Aug 16, 2026
800bab1
Merge remote-tracking branch 'upstream/master' into pr/dpa4c
OutisLi Aug 17, 2026
ecad7fa
fix(charge-state): enforce the shared table domain at every host boun…
OutisLi Aug 17, 2026
cc81d7b
fix(charge-state): validate conditions from training data and malform…
OutisLi Aug 17, 2026
2fc9121
fix(pt_expt): defer the charge-state range check until the width is k…
OutisLi Aug 17, 2026
80a4d0b
fix(pt_expt): serve one charge state to every route of a conditioned …
OutisLi Aug 17, 2026
1f7d45f
fix(api_cc): honour the multi-frame contract in standalone native-spi…
OutisLi Aug 17, 2026
43daf9b
test(api_cc): pin the two-stage read of the charge-state table ranges
OutisLi Aug 17, 2026
52ce83b
fix(api_cc): divide every standalone input among the frames it belong…
OutisLi Aug 17, 2026
e3480cf
refactor(api_cc): state the charge-state domain with relational tests
OutisLi Aug 17, 2026
cd3fa56
fix(api_cc): let a native-spin call name the charge state it wants se…
OutisLi Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion deepmd/dpmodel/atomic_model/dp_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def __init__(
self.fitting_net = fitting
self.fitting_net.reinit_exclude(self.atom_exclude_types)
self.type_map = type_map
self.add_chg_spin_ebd: bool = self.descriptor.get_dim_chg_spin() > 0
self.add_chg_spin_ebd: bool = self.descriptor.has_chg_spin_ebd()
# Structural capability: only descriptors with a native spin
# conditioning mechanism (currently DPA4) accept a ``spin`` kwarg on
# ``call_graph`` at all -- unlike ``charge_spin``, which every
Expand Down Expand Up @@ -182,6 +182,10 @@ def supports_graph_export(self) -> bool:
"""Delegates to this model's own descriptor."""
return bool(self.descriptor.supports_graph_export())

def compression_needs_min_nbor_dist(self) -> bool:
"""Delegates to this model's own descriptor."""
return bool(self.descriptor.compression_needs_min_nbor_dist())

def supports_native_spin(self) -> bool:
"""Delegates to this model's own descriptor (cached at construction)."""
return self._supports_native_spin
Expand Down
9 changes: 9 additions & 0 deletions deepmd/dpmodel/atomic_model/linear_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,15 @@ def enable_compression(
check_frequency,
)

def compression_needs_min_nbor_dist(self) -> bool:
"""Required as soon as ANY child consumes it.

The statistic is measured once and handed to every child, so a single
child that tabulates from the shortest observed distance keeps the
neighbor-statistics pass for the whole composition.
"""
return any(m.compression_needs_min_nbor_dist() for m in self.models)

def uses_graph_lower(self) -> bool:
"""Graph-capable iff EVERY child supports the graph lower.

Expand Down
11 changes: 11 additions & 0 deletions deepmd/dpmodel/atomic_model/make_base_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ def enable_compression(
"""
raise NotImplementedError("This atomi model doesn't support compression!")

def compression_needs_min_nbor_dist(self) -> bool:
"""Whether :meth:`enable_compression` consumes ``min_nbor_dist``.

Returns
-------
bool
Concrete default ``True``, so a model that does not report
otherwise keeps the neighbor-statistics pass.
"""
return True

def make_atom_mask(
self,
atype: t_tensor,
Expand Down
11 changes: 11 additions & 0 deletions deepmd/dpmodel/atomic_model/pairtab_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,3 +505,14 @@ def enable_compression(
) -> None:
"""Pairtab model does not support compression."""
pass

def compression_needs_min_nbor_dist(self) -> bool:
"""Return whether compression consumes the minimum neighbor distance.

Returns
-------
bool
Always ``False``. The tabulated pair potential carries its own
domain, so compression is a no-op here.
"""
return False
4 changes: 4 additions & 0 deletions deepmd/dpmodel/descriptor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from .dpa4 import (
DescrptDPA4,
)
from .dpa4c import (
DescrptDPA4C,
)
from .hybrid import (
DescrptHybrid,
)
Expand Down Expand Up @@ -38,6 +41,7 @@
"DescrptDPA2",
"DescrptDPA3",
"DescrptDPA4",
"DescrptDPA4C",
"DescrptHybrid",
"DescrptSeA",
"DescrptSeAttenV2",
Expand Down
28 changes: 18 additions & 10 deletions deepmd/dpmodel/descriptor/dpa3.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
from deepmd.dpmodel.utils.update_sel import (
UpdateSel,
)
from deepmd.utils.charge_state import (
CHARGE_OFFSET,
CHARGE_TABLE_ROWS,
MULTIPLICITY_TABLE_ROWS,
validate_charge_state,
)
from deepmd.utils.data_system import (
DeepmdDataSystem,
)
Expand Down Expand Up @@ -468,11 +474,11 @@ def init_subclass_params(sub_data: dict | Any, sub_class: type) -> Any:

self.use_econf_tebd = use_econf_tebd
self.add_chg_spin_ebd = add_chg_spin_ebd
if default_chg_spin is not None and len(default_chg_spin) != 2:
raise ValueError(
"default_chg_spin must have exactly 2 values [charge, spin]"
)
self.default_chg_spin = default_chg_spin
self.default_chg_spin = (
None
if default_chg_spin is None
else validate_charge_state(default_chg_spin)
)
self.use_tebd_bias = use_tebd_bias
self.use_loc_mapping = use_loc_mapping
self.type_map = type_map
Expand All @@ -494,18 +500,16 @@ def init_subclass_params(sub_data: dict | Any, sub_class: type) -> Any:

if self.add_chg_spin_ebd:
self.cs_activation_fn = get_activation_fn(activation_function)
# -100 ~ 100 is a conservative bound
self.chg_embedding = TypeEmbedNet(
ntypes=200,
ntypes=CHARGE_TABLE_ROWS,
neuron=[self.tebd_dim],
padding=True,
activation_function="Linear",
precision=precision,
seed=child_seed(seed, 3),
)
# 100 is a conservative upper bound
self.spin_embedding = TypeEmbedNet(
ntypes=100,
ntypes=MULTIPLICITY_TABLE_ROWS,
neuron=[self.tebd_dim],
padding=True,
activation_function="Linear",
Expand Down Expand Up @@ -543,6 +547,10 @@ def get_dim_chg_spin(self) -> int:
"""Returns the dimension of charge_spin input."""
return 2 if self.add_chg_spin_ebd else 0

def has_chg_spin_ebd(self) -> bool:
"""Return whether a frame charge/spin condition is configured."""
return self.add_chg_spin_ebd

def get_default_chg_spin(self) -> list[float] | None:
"""Returns the default charge_spin values."""
return self.default_chg_spin
Expand Down Expand Up @@ -755,7 +763,7 @@ def call(
assert self.spin_embedding is not None
chg_tebd = self.chg_embedding.call()
spin_tebd = self.spin_embedding.call()
charge = xp.astype(charge_spin[:, 0], xp.int64) + 100
charge = xp.astype(charge_spin[:, 0], xp.int64) + CHARGE_OFFSET
spin = xp.astype(charge_spin[:, 1], xp.int64)
chg_ebd = xp.reshape(
xp.take(chg_tebd, xp.reshape(charge, (-1,)), axis=0),
Expand Down
13 changes: 10 additions & 3 deletions deepmd/dpmodel/descriptor/dpa4.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
from deepmd.dpmodel.utils.update_sel import (
UpdateSel,
)
from deepmd.utils.charge_state import (
validate_charge_state,
)
from deepmd.utils.version import (
check_version_compatibility,
)
Expand Down Expand Up @@ -784,10 +787,10 @@ def __init__(
self.edge_cartesian = bool(edge_cartesian)
self.node_cartesian = str(node_cartesian)
self.add_chg_spin_ebd = bool(add_chg_spin_ebd)
if default_chg_spin is not None and len(default_chg_spin) != 2:
raise ValueError("`default_chg_spin` must contain [charge, spin].")
self.default_chg_spin = (
None if default_chg_spin is None else [float(x) for x in default_chg_spin]
None
if default_chg_spin is None
else validate_charge_state(default_chg_spin)
)

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

def has_chg_spin_ebd(self) -> bool:
"""Return whether a frame charge/spin condition is configured."""
return self.charge_spin_embedding is not None

def get_dim_chg_spin(self) -> int:
"""Return the charge/spin condition width."""
return 2 if self.add_chg_spin_ebd else 0
Expand Down
6 changes: 6 additions & 0 deletions deepmd/dpmodel/descriptor/dpa4_nn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
merge_lora_into_base,
strip_lora_from_extra_state,
)
from .mlp import (
SwiGLUMLP,
resolve_swiglu_hidden_width,
)
from .norm import (
EquivariantRMSNorm,
ReducedEquivariantRMSNorm,
Expand Down Expand Up @@ -159,6 +163,7 @@
"SeZMTypeEmbedding",
"SpinEmbedding",
"SwiGLU",
"SwiGLUMLP",
"WignerDCalculator",
"apply_lora_to_sezm",
"build_cartesian_basis",
Expand Down Expand Up @@ -189,6 +194,7 @@
"quaternion_z_rotation",
"resolve_s2_grid_resolution",
"resolve_so3_grid",
"resolve_swiglu_hidden_width",
"safe_norm",
"segment_envelope_gated_softmax",
"so3_packed_index",
Expand Down
36 changes: 28 additions & 8 deletions deepmd/dpmodel/descriptor/dpa4_nn/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
from deepmd.dpmodel.utils.type_embed import (
remap_atype_to_padding,
)
from deepmd.utils.charge_state import (
CHARGE_OFFSET,
CHARGE_TABLE_ROWS,
MULTIPLICITY_TABLE_ROWS,
)
from deepmd.utils.version import (
check_version_compatibility,
)
Expand Down Expand Up @@ -128,26 +133,41 @@ def __init__(
# === Step 2. Register the embedding table parameter ===
self.adam_type_embedding = table

def call(self, atype: Any) -> Any:
def call(self, atype: Any | None = None) -> Any:
"""
Gather type embeddings.

Parameters
----------
atype
Atom types with shape (...,). Valid type range is [0, ntypes-1].
Atom types with shape (...). Valid type range is [0, ntypes-1].
If omitted, return the complete embedding table, including the
optional padding row. This form is used by graph-native descriptor
ABIs that precompute the table once per forward call.

Returns
-------
Array
Type embeddings with shape (..., embed_dim).
Gathered type embeddings with shape ``(..., embed_dim)`` when
``atype`` is provided. Otherwise, the complete table with shape
``(ntypes + int(padding), embed_dim)``.
"""
# === Step 1. Return the complete graph-native lookup table ===
if atype is None:
xp = array_api_compat.array_namespace(self.adam_type_embedding)
return xp_asarray_nodetach(
xp,
self.adam_type_embedding[...],
device=array_api_compat.device(self.adam_type_embedding),
)

# === Step 2. Gather rows for an explicit atom-type tensor ===
xp = array_api_compat.array_namespace(atype)
weight = xp_asarray_nodetach(
xp, self.adam_type_embedding[...], device=array_api_compat.device(atype)
)
# torch.embedding gather: flatten the indices to int64, take the rows,
# then restore the original index shape.
# Flattening provides one backend-neutral gather while preserving every
# leading batch or graph dimension on restoration.
index = xp.astype(xp.reshape(atype, (-1,)), xp.int64)
if self.padding:
index = remap_atype_to_padding(index, self.ntypes + 1)
Expand Down Expand Up @@ -869,15 +889,15 @@ def __init__(
raise ValueError("`embed_dim` must be positive")

self.charge_embedding = SeZMTypeEmbedding(
ntypes=200,
ntypes=CHARGE_TABLE_ROWS,
embed_dim=self.embed_dim,
precision=self.precision,
seed=child_seed(seed, 0),
trainable=self.trainable,
padding=False,
)
self.spin_embedding = SeZMTypeEmbedding(
ntypes=100,
ntypes=MULTIPLICITY_TABLE_ROWS,
embed_dim=self.embed_dim,
precision=self.precision,
seed=child_seed(seed, 1),
Expand Down Expand Up @@ -908,7 +928,7 @@ def call(self, charge_spin: Any) -> Any:
Mixed condition embedding with shape (nf, embed_dim).
"""
xp = array_api_compat.array_namespace(charge_spin)
charge = xp.astype(charge_spin[:, 0], xp.int64) + 100
charge = xp.astype(charge_spin[:, 0], xp.int64) + CHARGE_OFFSET
spin = xp.astype(charge_spin[:, 1], xp.int64)
charge_embed = self.charge_embedding(charge)
spin_embed = self.spin_embedding(spin)
Expand Down
Loading
Loading