Skip to content

Commit 71239f9

Browse files
author
Han Wang
committed
refactor(pt_expt): NativeSpinEnergyModel via make_native_spin_model; registry dispatch replaces native-spin seams
The pt_expt twin instantiates the shared factory on THIS backend's EnergyModel and registers under 'native_spin' in the PT_EXPT BaseModel registry, so the public BaseModel.deserialize returns the torch class (backend-aware, no hard-coded branches). Consequences: - The bespoke deserialize override and the __getattr__ delegation shim are deleted: the factory's deserialize closure rebuilds through the pt_expt EnergyModel, and inheritance replaces delegation. forward and forward_lower_graph_exportable (spin@index-10 ABI unchanged) now call the inherited call_common/forward_common_lower_graph_exportable on self instead of a backbone attribute. - get_native_spin_model (public, matching its sibling builders) routes the backbone through get_sezm_model for the DPA4/SeZM family (keeping its guards) or get_standard_model otherwise, gates on descriptor.supports_native_spin(), and re-classes via atomic_model_. The standard-typed get_model path now dispatches scheme='native' too, so a future native-spin descriptor works from a plain standard config. - deep_eval and deserialize_to_file drop their native-spin special cases (registry + has_spin() cover them); the eval_typeebd unwrap is isinstance-precise on the virtual-atom SpinModel only. - NEW NativeSpinModelKind marker base in the factory module: each backend's concrete class is a parallel factory product with NO subclass relation, so the old isinstance-against-dpmodel-class with-comm gate went silently dead -- the freeze then compiled a with-comm artifact for the single-rank-only spin ABI (caught by test_native_spin_graph_freeze cold-cache A/B). Seams test the shared marker instead. Validated locally: native-spin+spin batteries 25, export file 4/5 (test_dpa4_freeze_to_pt2[auto-graph] fails identically at the pre-refactor commit with a cold inductor cache -- pre-existing local inductor codegen bug, not a regression), deep_eval 93, dpmodel 19.
1 parent 24abf3c commit 71239f9

9 files changed

Lines changed: 153 additions & 177 deletions

File tree

deepmd/dpmodel/model/native_spin_model.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@
2626
)
2727

2828

29+
class NativeSpinModelKind:
30+
"""Marker base identifying classes produced by ``make_native_spin_model``.
31+
32+
Each backend instantiates the factory on its OWN standard model class,
33+
so the concrete classes (e.g. dpmodel's and pt_expt's
34+
``NativeSpinEnergyModel``) are parallel products with NO subclass
35+
relation between them -- an ``isinstance`` against one backend's
36+
concrete class is silently dead in the other. Backend seams that need a
37+
cross-backend family test (e.g. the with-comm freeze gate: native-spin
38+
lowers are single-rank only) test against this shared marker instead.
39+
"""
40+
41+
2942
def make_native_spin_model(T_Model: type) -> type:
3043
"""Make a native-spin model class from a standard model class.
3144
@@ -56,7 +69,7 @@ def make_native_spin_model(T_Model: type) -> type:
5669
The derived native-spin model class.
5770
"""
5871

59-
class NSM(T_Model):
72+
class NSM(T_Model, NativeSpinModelKind):
6073
"""Native-spin variant of ``T_Model`` (see ``make_native_spin_model``)."""
6174

6275
def __init__(self, *args: Any, spin: Spin, **kwargs: Any) -> None:

deepmd/pt_expt/infer/deep_eval.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _graph_spin_output_key(odef: "OutputVariableDef") -> str | None:
9595
"""Map a native-spin request def to its graph-spin public model key.
9696
9797
Twin of ``_GRAPH_CATEGORY_TO_KEY`` for
98-
``DPA4NativeSpinModel.forward_lower_graph_exportable``'s output dict
98+
``NativeSpinEnergyModel.forward_lower_graph_exportable``'s output dict
9999
(``_translate_spin_energy_keys``: ``atom_energy``, ``energy``,
100100
``force``, ``force_mag``, ``virial``, ``atom_virial``). Category alone
101101
is NOT enough here: ``do_derivative`` (``deepmd.dpmodel.output_def``)
@@ -338,19 +338,13 @@ def _init_from_model_json(self, model_json_str: str) -> None:
338338

339339
self._dpmodel = SpinModel.deserialize(model_data)
340340
self._is_spin = True
341-
elif model_type in ("dpa4_native_spin", "sezm_native_spin"):
342-
# Native-spin (NeighborGraph route) DPA4/SeZM: no virtual atoms,
343-
# spin rides the graph lower directly (Task 6/7 of the DPA4
344-
# native-spin plan). Mirrors the ``spin_ener`` branch above.
345-
from deepmd.pt_expt.model.dpa4_native_spin_model import (
346-
DPA4NativeSpinModel,
347-
)
348-
349-
self._dpmodel = DPA4NativeSpinModel.deserialize(model_data)
350-
self._is_spin = True
351341
else:
342+
# Registry-dispatched: wrapper classes registered in the pt_expt
343+
# BaseModel registry (e.g. the native-spin models, type
344+
# "native_spin") come back as their pt_expt torch classes and
345+
# declare spin via the base-model capability method.
352346
self._dpmodel = BaseModel.deserialize(model_data)
353-
self._is_spin = False
347+
self._is_spin = self._dpmodel.has_spin()
354348

355349
self._rcut = self._dpmodel.get_rcut()
356350
self._type_map = self._dpmodel.get_type_map()
@@ -1662,7 +1656,7 @@ def _eval_model_spin(
16621656
# extended/nlist ABI at all -- dispatch to the graph-native fast
16631657
# path (mirrors _eval_model's dispatch to _eval_model_graph for
16641658
# the non-spin case). charge_spin has no slot in this ABI (see
1665-
# DPA4NativeSpinModel.forward_lower_graph_exportable).
1659+
# NativeSpinEnergyModel.forward_lower_graph_exportable).
16661660
return self._eval_model_graph_spin(
16671661
coords, cells, atom_types, spins, fparam, aparam, request_defs
16681662
)
@@ -1851,7 +1845,7 @@ def _eval_model_graph_spin(
18511845
construction (SAME builder, SAME positional ABI up through
18521846
``source_row_ptr``), then inserts the owned-atom ``spin`` tensor
18531847
``(N, 3)`` at positional index 10 of
1854-
``DPA4NativeSpinModel.forward_lower_graph_exportable`` -- the node
1848+
``NativeSpinEnergyModel.forward_lower_graph_exportable`` -- the node
18551849
axis IS the owned-local-atom axis for single-rank eval (no ghost
18561850
nodes), so ``spin`` needs no extension/mapping, unlike the dense
18571851
spin path's ``ext_spin_t``. There is no ``charge_spin`` slot in this
@@ -2353,8 +2347,14 @@ def eval_typeebd(self) -> np.ndarray:
23532347

23542348
from deepmd.dpmodel.utils.type_embed import TypeEmbedNet as TypeEmbedNetDP
23552349

2350+
from deepmd.pt_expt.model.spin_model import (
2351+
SpinModel,
2352+
)
2353+
23562354
model = self._dpmodel
2357-
if self._is_spin_model():
2355+
if isinstance(model, SpinModel):
2356+
# Virtual-atom wrapper: type-embed nets live on the backbone.
2357+
# Native-spin models ARE the model (is-a); no unwrap.
23582358
model = model.backbone_model
23592359
out = []
23602360
for mm in model.modules():

deepmd/pt_expt/model/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from .dp_zbl_model import (
1616
DPZBLModel,
1717
)
18-
from .dpa4_native_spin_model import (
19-
DPA4NativeSpinModel,
18+
from .native_spin_model import (
19+
NativeSpinEnergyModel,
2020
)
2121
from .ener_model import (
2222
EnergyModel,
@@ -43,12 +43,12 @@
4343
__all__ = [
4444
"BaseModel",
4545
"DOSModel",
46-
"DPA4NativeSpinModel",
4746
"DPZBLModel",
4847
"DipoleModel",
4948
"EnergyModel",
5049
"FrozenModel",
5150
"LinearEnergyModel",
51+
"NativeSpinEnergyModel",
5252
"PolarModel",
5353
"PropertyModel",
5454
"SpinEnergyModel",

deepmd/pt_expt/model/get_model.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
from deepmd.pt_expt.model.dos_model import (
2727
DOSModel,
2828
)
29-
from deepmd.pt_expt.model.dpa4_native_spin_model import (
30-
DPA4NativeSpinModel,
29+
from deepmd.pt_expt.model.native_spin_model import (
30+
NativeSpinEnergyModel,
3131
)
3232
from deepmd.pt_expt.model.ener_model import (
3333
EnergyModel,
@@ -151,7 +151,7 @@ def get_sezm_model(data: dict) -> EnergyModel:
151151
"scheme are not supported in the pt_expt backend; use spin "
152152
"scheme 'native' instead."
153153
)
154-
return _get_dpa4_native_spin_model(data)
154+
return get_native_spin_model(data)
155155
if str(data.get("bridging_method", "none")).lower() != "none":
156156
raise NotImplementedError(
157157
"`bridging_method` is not supported for DPA4/SeZM in the pt_expt backend."
@@ -213,17 +213,20 @@ def get_sezm_model(data: dict) -> EnergyModel:
213213
)
214214

215215

216-
def _get_dpa4_native_spin_model(data: dict) -> DPA4NativeSpinModel:
217-
"""Build a pt_expt DPA4/SeZM native (virtual-atom-free) spin model.
216+
def get_native_spin_model(data: dict) -> NativeSpinEnergyModel:
217+
"""Build a pt_expt native (virtual-atom-free) spin model.
218218
219-
Mirrors :func:`deepmd.dpmodel.model.model.get_dpa4_native_spin_model`:
220-
no virtual atoms or doubled type map are introduced, ``use_spin`` is
219+
Mirrors :func:`deepmd.dpmodel.model.model.get_native_spin_model`: no
220+
virtual atoms or doubled type map are introduced, and ``use_spin`` is
221221
injected into the descriptor config (consumed by the descriptor's
222-
equivariant spin embedding), and the non-spin backbone is built by
223-
recursing into :func:`get_sezm_model` (now with ``"spin"`` popped),
224-
which supplies the bridging/lora/compile/preset_out_bias rejections,
225-
descriptor/fitting defaulting and ``exclude_types`` consistency check
226-
shared with the non-spin DPA4/SeZM path.
222+
equivariant spin embedding). The non-spin backbone is built by the
223+
standard builder for the config's model type -- :func:`get_sezm_model`
224+
for the DPA4/SeZM family (keeping its bridging/lora/compile/
225+
preset_out_bias rejections and ``exclude_types`` consistency check),
226+
else :func:`get_standard_model` -- then re-classed through the
227+
registered :class:`NativeSpinEnergyModel`. Eligibility is the
228+
``descriptor.supports_native_spin()`` capability, not a descriptor-type
229+
list.
227230
228231
Parameters
229232
----------
@@ -234,10 +237,6 @@ def _get_dpa4_native_spin_model(data: dict) -> DPA4NativeSpinModel:
234237
data = copy.deepcopy(data)
235238
spin_cfg = data.pop("spin")
236239
data.setdefault("descriptor", {})
237-
if data["descriptor"].get("type", "dpa4") not in ("dpa4", "DPA4", "sezm", "SeZM"):
238-
raise NotImplementedError(
239-
"spin scheme 'native' requires the DPA4/SeZM descriptor"
240-
)
241240
if data["descriptor"].get("add_chg_spin_ebd", False):
242241
raise NotImplementedError(
243242
"charge-spin FiLM combined with native spin on the graph route "
@@ -252,8 +251,28 @@ def _get_dpa4_native_spin_model(data: dict) -> DPA4NativeSpinModel:
252251
allow_missing_label=spin_cfg.get("allow_missing_label", False),
253252
)
254253
data["descriptor"]["use_spin"] = use_spin
255-
backbone_model = get_sezm_model(data)
256-
return DPA4NativeSpinModel(backbone_model=backbone_model, spin=spin)
254+
model_type = str(data.get("type", "standard")).lower()
255+
backbone_builder = (
256+
get_sezm_model if model_type in ("dpa4", "sezm") else get_standard_model
257+
)
258+
try:
259+
backbone_model = backbone_builder(data)
260+
except TypeError as err:
261+
# A descriptor without native spin support rejects the injected
262+
# ``use_spin`` keyword at construction; translate to the
263+
# capability-gate error.
264+
raise NotImplementedError(
265+
"spin scheme 'native' requires a descriptor with native spin "
266+
"support (supports_native_spin()); descriptor type "
267+
f"{data['descriptor'].get('type')!r} does not accept `use_spin`"
268+
) from err
269+
descriptor = backbone_model.atomic_model.descriptor
270+
if not descriptor.supports_native_spin():
271+
raise NotImplementedError(
272+
"spin scheme 'native' requires a descriptor declaring "
273+
"supports_native_spin()"
274+
)
275+
return NativeSpinEnergyModel(atomic_model_=backbone_model.atomic_model, spin=spin)
257276

258277

259278
def get_linear_model(model_params: dict) -> BaseModel:
@@ -355,6 +374,11 @@ def get_model(data: dict) -> BaseModel:
355374
model_type = data.get("type", "standard")
356375
if model_type == "standard":
357376
if "spin" in data:
377+
if str(data["spin"].get("scheme", "deepspin")) == "native":
378+
# Descriptor-agnostic entry: any standard-typed config whose
379+
# descriptor declares supports_native_spin() rides the
380+
# native scheme with zero model/dispatch changes.
381+
return get_native_spin_model(data)
358382
return get_spin_model(data)
359383
return get_standard_model(data)
360384
elif model_type == "linear_ener":

deepmd/pt_expt/model/dpa4_native_spin_model.py renamed to deepmd/pt_expt/model/native_spin_model.py

Lines changed: 28 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
2-
"""pt_expt DPA4/SeZM native-spin model (NeighborGraph route, autograd force_mag)."""
2+
"""pt_expt native-spin energy model (NeighborGraph route, autograd force_mag)."""
33

44
from typing import (
55
Any,
@@ -10,11 +10,14 @@
1010
make_fx,
1111
)
1212

13-
from deepmd.dpmodel.model.dpa4_native_spin_model import (
14-
DPA4NativeSpinModel as DPA4NativeSpinModelDP,
13+
from deepmd.dpmodel.model.native_spin_model import (
14+
make_native_spin_model,
1515
)
16-
from deepmd.pt_expt.common import (
17-
torch_module,
16+
from deepmd.pt_expt.model.ener_model import (
17+
EnergyModel,
18+
)
19+
from deepmd.pt_expt.model.model import (
20+
BaseModel,
1821
)
1922

2023

@@ -59,86 +62,27 @@ def _translate_spin_energy_keys(
5962
return out
6063

6164

62-
@torch_module
63-
class DPA4NativeSpinModel(DPA4NativeSpinModelDP):
64-
"""pt_expt native-spin DPA4/SeZM model.
65+
@BaseModel.register("native_spin")
66+
class NativeSpinEnergyModel(make_native_spin_model(EnergyModel)):
67+
"""pt_expt native-spin energy model.
6568
66-
Mirrors :class:`deepmd.dpmodel.model.dpa4_native_spin_model.DPA4NativeSpinModel`
67-
(construction, delegation, output defs are all inherited unchanged), but
68-
overrides two methods:
69+
``make_native_spin_model`` applied to THIS backend's
70+
:class:`~deepmd.pt_expt.model.ener_model.EnergyModel` (construction,
71+
output defs, serialization all come from the factory; the deserialize
72+
closure rebuilds through the pt_expt model class, so a registry round
73+
trip yields a real ``torch.nn.Module``), plus two torch-specific
74+
overrides:
6975
70-
- :meth:`forward`: the pt_expt backbone's ``call_common`` (Task 3 of the
71-
"DPA4 native spin on the NeighborGraph route" plan) produces REAL
72-
autograd ``energy_derv_r``/``energy_derv_r_mag``/``energy_derv_c_redu``
73-
tensors, unlike the dpmodel parent's energy-only ``call`` (which is
76+
- :meth:`forward`: the pt_expt ``call_common`` produces REAL autograd
77+
``energy_derv_r``/``energy_derv_r_mag``/``energy_derv_c_redu``
78+
tensors, unlike the dpmodel factory's energy-only ``call`` (which is
7479
restricted to ``force``/``force_mag``/``virial`` as ``None``
7580
placeholders because dpmodel has no autograd).
76-
- :meth:`deserialize`: the dpmodel parent's version hardcodes the
77-
DPMODEL (numpy) ``BaseModel`` to rebuild ``backbone_model``, which
78-
would produce a backbone missing the pt_expt/torch export machinery
79-
(see the override's docstring); this class rebuilds it through the
80-
pt_expt registry instead.
81+
- :meth:`forward_lower_graph_exportable`: the graph-spin ``.pt2``
82+
positional ABI (``spin`` at index 10) over the inherited
83+
``forward_common_lower_graph_exportable``.
8184
"""
8285

83-
def __getattr__(self, name: str) -> Any:
84-
"""Get attribute from the wrapped model.
85-
86-
In torch.nn.Module, submodules (e.g. ``backbone_model``) are stored
87-
in ``_modules``, not ``__dict__``. The dpmodel parent's
88-
``__getattr__`` guards its ``backbone_model`` delegation with
89-
``"backbone_model" not in self.__dict__``, which is always true here
90-
and would incorrectly raise ``AttributeError`` for every submodule
91-
access (including ``self.backbone_model`` itself). Mirrors
92-
:class:`deepmd.pt_expt.model.spin_model.SpinModel`'s override: try
93-
``torch.nn.Module``'s own ``__getattr__`` (checks ``_parameters``,
94-
``_buffers``, ``_modules``) first, then fall back to
95-
``backbone_model`` delegation for arbitrary attributes.
96-
"""
97-
try:
98-
return torch.nn.Module.__getattr__(self, name)
99-
except AttributeError:
100-
pass
101-
# backbone_model is in _modules, access via _modules directly to
102-
# avoid re-entering __getattr__.
103-
modules = self.__dict__.get("_modules", {})
104-
backbone = modules.get("backbone_model")
105-
if backbone is not None:
106-
return getattr(backbone, name)
107-
raise AttributeError(name)
108-
109-
@classmethod
110-
def deserialize(cls, data: dict) -> "DPA4NativeSpinModel":
111-
"""Rebuild ``backbone_model`` through the pt_expt registry.
112-
113-
The dpmodel parent's ``deserialize`` (inherited otherwise) imports
114-
``deepmd.dpmodel.model.base_model.BaseModel`` at ITS OWN module
115-
level, hardcoded regardless of which subclass's ``deserialize`` is
116-
actually invoked -- so calling it on this pt_expt subclass would
117-
still rebuild a plain numpy dpmodel ``backbone_model`` (missing
118-
every pt_expt/torch export method, e.g.
119-
``forward_common_lower_graph_exportable``) and then let the
120-
``@torch_module`` auto-wrap machinery paper over it with a
121-
dynamically-generated wrapper that only covers the dpmodel object's
122-
OWN methods -- which never included the pt_expt-only export
123-
machinery to begin with. Mirrors
124-
:meth:`~deepmd.pt_expt.model.spin_model.SpinModel.deserialize`'s
125-
override for exactly the same reason.
126-
"""
127-
from deepmd.pt_expt.model.model import (
128-
BaseModel,
129-
)
130-
from deepmd.utils.spin import (
131-
Spin,
132-
)
133-
134-
data = data.copy()
135-
data.pop("@class", None)
136-
data.pop("@version", None)
137-
data.pop("type", None)
138-
spin = Spin.deserialize(data.pop("spin"))
139-
backbone_model = BaseModel.deserialize(data.pop("backbone_model"))
140-
return cls(backbone_model=backbone_model, spin=spin)
141-
14286
def forward(
14387
self,
14488
coord: torch.Tensor,
@@ -188,7 +132,7 @@ def forward(
188132
# ``spin=`` rides the NeighborGraph lower only; ``neighbor_graph_method``
189133
# is left at its default (None) so pt_expt's own default-flip resolves
190134
# it to the carry-all graph builder for this (DPA4) descriptor.
191-
model_ret = self.backbone_model.call_common(
135+
model_ret = self.call_common(
192136
coord,
193137
atype,
194138
box=box,
@@ -253,8 +197,9 @@ def forward_lower_graph_exportable(
253197
Two-layer make_fx trace, mirroring
254198
:meth:`~deepmd.pt_expt.model.ener_model.EnergyModel.forward_lower_graph_exportable`:
255199
the inner layer
256-
(:meth:`~deepmd.pt_expt.model.make_model.make_model.forward_common_lower_graph_exportable`
257-
on ``self.backbone_model``) traces ``forward_common_lower_graph``
200+
(the inherited
201+
:meth:`~deepmd.pt_expt.model.make_model.make_model.forward_common_lower_graph_exportable`)
202+
traces ``forward_common_lower_graph``
258203
with ``spin`` as a SECOND autograd leaf next to ``edge_vec`` (fixing
259204
``charge_spin=None`` -- this wrapper's ABI never exposes that slot);
260205
this outer layer re-traces with the PUBLIC positional ABI above and
@@ -312,7 +257,7 @@ def forward_lower_graph_exportable(
312257
``atom_energy``, ``energy``, ``force``, ``force_mag``,
313258
``virial``, and (when ``do_atomic_virial``) ``atom_virial``.
314259
"""
315-
traced = self.backbone_model.forward_common_lower_graph_exportable(
260+
traced = self.forward_common_lower_graph_exportable(
316261
atype,
317262
n_node,
318263
n_local,

0 commit comments

Comments
 (0)