Skip to content

Commit 3d0e7ba

Browse files
author
Han Wang
committed
feat(dpa4): wire charge-spin FiLM + SFPG bridging onto the graph route
Both features already worked inside the shared _call_graph_impl trunk; only three wiring gaps kept them off the NeighborGraph lower: - DescrptDPA4.call_graph dropped charge_spin before it reached the trunk. It now accepts charge_spin, canonicalizes it (same _canonicalize_charge_spin the dense call() adapter uses, so call_graph is the single owner of that step on the graph route), recovers nf from graph.n_node.shape[0] (static shape, export-safe), and threads it through _call_graph_common. Gated end-to-end by a new DescrptDPA4.supports_charge_spin() capability method, mirrored exactly on supports_native_spin: DPAtomicModel caches self.supports_charge_spin and forward_atomic_graph only forwards the kwarg to descriptors that declare it, so dpa1/dpa2/dpa3's call_graph (which has no charge_spin parameter) never TypeErrors. - uses_graph_lower() rejected charge_spin_embedding and bridging_switch outright; both branches are removed, so only the explicit disable_graph_lower() escape hatch gates the graph route off now. Bridging itself needed no threading -- the trunk already reads self.bridging_switch directly. - make_model.call_common (dpmodel, shared by pt_expt) forced any charge_spin request onto the dense lower via "if cs is not None: graph_method = None"; removed, and charge_spin is threaded through _call_common_graph -> call_lower_graph on both the dpmodel and pt_expt overrides. has_message_passing_across_ranks() is untouched: bridging models still fail multi-rank fast (a rank cannot observe a ghost owner's full outgoing-edge set for the per-node freeze fold). Tests (source/tests/common/dpmodel/test_dpa4_call_graph.py): - test_uses_graph_lower_feature_gates flipped to the new gate set. - charge_spin sensitivity + graph-vs-dense parity at 1e-12 (nf=2, distinct per-frame conditioning -- pins the nf threading, not just presence). - bridging frozen-sphere invariance + gate-disabled ablation, mirroring pt's TestSourceFreezePropagationGate at the descriptor level. - model-level: get_model(add_chg_spin_ebd=True) actually reaches DescrptDPA4.call_graph under neighbor_graph_method="dense" (spied, not just output-changed, since the old bug was a silent dense fallback rather than an error).
1 parent dcc1a5b commit 3d0e7ba

6 files changed

Lines changed: 317 additions & 34 deletions

File tree

deepmd/dpmodel/atomic_model/base_atomic_model.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,10 @@ def forward_common_atomic_graph(
406406
aparam
407407
atomic parameter. N x nda
408408
charge_spin
409-
charge/spin conditioning. Unused by the dpa1 graph path; accepted so
410-
the interface stays stable for charge/spin-conditioned descriptors.
409+
frame-level charge/spin conditioning, forwarded unchanged to
410+
:meth:`forward_atomic_graph`, which only passes it on to the
411+
descriptor's ``call_graph`` for descriptors that declare
412+
``supports_charge_spin`` (currently DPA4 only).
411413
spin
412414
flat (N, 3) per-node spin, forwarded unchanged to
413415
:meth:`forward_atomic_graph` (and, from there, the descriptor's

deepmd/dpmodel/atomic_model/dp_atomic_model.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ def __init__(
137137
self.supports_native_spin: bool = getattr(
138138
self.descriptor, "supports_native_spin", lambda: False
139139
)()
140+
# Same capability-guard pattern as ``supports_native_spin`` above, for
141+
# the frame-level ``charge_spin`` FiLM kwarg: only DPA4's
142+
# ``call_graph`` declares it (see ``DescrptDPA4.supports_charge_spin``);
143+
# other descriptors' ``call_graph`` would ``TypeError`` on an
144+
# unconditional ``charge_spin=`` kwarg.
145+
self.supports_charge_spin: bool = getattr(
146+
self.descriptor, "supports_charge_spin", lambda: False
147+
)()
140148
super().init_out_stat()
141149

142150
def has_chg_spin_ebd(self) -> bool:
@@ -336,8 +344,11 @@ def forward_atomic_graph(
336344
aparam
337345
atomic parameter. N x nda
338346
charge_spin
339-
charge/spin conditioning. Unused by the dpa1 graph path; accepted so
340-
the interface stays stable for charge/spin-conditioned descriptors.
347+
frame-level charge/spin conditioning, forwarded to the
348+
descriptor's ``call_graph`` only when
349+
``self.supports_charge_spin`` (currently DPA4 only); ignored (not
350+
forwarded, never a ``TypeError``) for descriptors without that
351+
capability, keeping the interface stable for all of them.
341352
spin
342353
flat (N, 3) per-node spin, forwarded to the descriptor's
343354
``call_graph``; None for spin-less models.
@@ -363,15 +374,20 @@ def forward_atomic_graph(
363374
# Descriptor-owned: dpa1/dpa2 hand out their full tebd table; DPA4
364375
# embeds types internally from ``atype`` and returns None.
365376
type_embedding = self.descriptor.graph_type_embedding_table()
366-
# See ``self.supports_native_spin`` in ``__init__``: only forward the
367-
# ``spin`` keyword to descriptors whose ``call_graph`` declares it.
377+
# See ``self.supports_native_spin``/``self.supports_charge_spin`` in
378+
# ``__init__``: only forward the ``spin``/``charge_spin`` keyword to
379+
# descriptors whose ``call_graph`` declares it.
368380
spin_kwargs = {"spin": spin} if self.supports_native_spin else {}
381+
charge_spin_kwargs = (
382+
{"charge_spin": charge_spin} if self.supports_charge_spin else {}
383+
)
369384
gg, rot_mat = self.descriptor.call_graph(
370385
graph,
371386
atype,
372387
type_embedding=type_embedding,
373388
comm_dict=comm_dict,
374389
**spin_kwargs,
390+
**charge_spin_kwargs,
375391
)
376392
fparam_node = None
377393
if fparam is not None:

deepmd/dpmodel/descriptor/dpa4.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,6 +1676,7 @@ def call_graph(
16761676
type_embedding: Array | None = None,
16771677
comm_dict: dict[str, Array] | None = None,
16781678
spin: Array | None = None,
1679+
charge_spin: Array | None = None,
16791680
) -> tuple[Array, None]:
16801681
"""Graph-native descriptor forward on the flat node axis.
16811682
@@ -1700,6 +1701,18 @@ def call_graph(
17001701
None. Consumed by ``spin_embedding`` (l=0 magnitude into the type
17011702
embedding, l=1 into the backbone and per-edge source features).
17021703
Ghost-free graphs need only per-local-atom spin.
1704+
charge_spin
1705+
Frame-level charge/spin conditioning with shape ``(nf, 2)`` (or a
1706+
shape ``_canonicalize_charge_spin`` can broadcast to it), or
1707+
``None``. This is the SAME per-descriptor canonicalization the
1708+
dense ``call`` adapter applies (default-fill from
1709+
``default_chg_spin`` when configured, shape validation,
1710+
broadcast to ``nf``); ``call_graph`` is the one owner of that
1711+
step on the graph route. ``nf`` is recovered from
1712+
``graph.n_node.shape[0]`` (a static shape, safe under
1713+
``torch.export``); each frame's node block must therefore hold
1714+
exactly ``N // nf`` nodes, which single-rank carry-all graphs
1715+
built from a rectangular ``(nf, nloc)`` input always satisfy.
17031716
17041717
Returns
17051718
-------
@@ -1714,8 +1727,15 @@ def call_graph(
17141727
exchange (raised by the per-block leaf).
17151728
"""
17161729
n_nodes = atype.shape[0]
1730+
nf = graph.n_node.shape[0]
1731+
charge_spin = self._canonicalize_charge_spin(
1732+
charge_spin,
1733+
nf=nf,
1734+
dtype=graph.edge_vec.dtype,
1735+
device=array_api_compat.device(graph.edge_vec),
1736+
)
17171737
x_scalar, _ = self._call_graph_common(
1718-
graph, atype, spin=spin, comm_dict=comm_dict
1738+
graph, atype, nf=nf, charge_spin=charge_spin, spin=spin, comm_dict=comm_dict
17191739
)
17201740
# ``_call_graph_common`` returns the read-out with its SO(3) singleton
17211741
# axes still attached, shape (n_nodes, 1, 1, channels); flatten to the
@@ -2332,19 +2352,18 @@ def uses_graph_lower(self) -> bool:
23322352
Returns
23332353
-------
23342354
bool
2335-
False when the escape hatch has been pulled or when the model
2336-
uses conditioning inputs (charge/spin FiLM, SFPG bridging) that
2337-
still ride only the dense ``call`` signature. Native spin
2338-
(``spin_embedding``) IS supported on the graph lower: it is
2339-
threaded through ``call_graph`` like any other per-node input.
2355+
False only when the escape hatch has been pulled
2356+
(``disable_graph_lower()`` / ``_graph_lower_disabled``). Every
2357+
conditioning input DPA4 supports -- native spin
2358+
(``spin_embedding``), charge/spin FiLM (``charge_spin_embedding``),
2359+
and SFPG bridging (``bridging_switch``) -- rides the graph lower:
2360+
spin and charge_spin are threaded through ``call_graph`` like any
2361+
other per-node/per-frame input, and bridging is applied inside
2362+
the shared ``_call_graph_impl`` trunk with no extra threading (it
2363+
reads ``self.bridging_switch`` directly). Bridging models still
2364+
fail multi-rank fast via ``has_message_passing_across_ranks``.
23402365
"""
2341-
if self._graph_lower_disabled:
2342-
return False
2343-
if self.charge_spin_embedding is not None:
2344-
return False
2345-
if self.bridging_switch is not None:
2346-
return False
2347-
return True
2366+
return not self._graph_lower_disabled
23482367

23492368
def uses_compact_edge_pairs(self) -> bool:
23502369
"""DPA4 attention is a per-edge scatter softmax; no pair axis."""
@@ -2354,6 +2373,10 @@ def supports_native_spin(self) -> bool:
23542373
"""DPA4 accepts a per-node ``spin`` on ``call_graph`` (native magnetic conditioning); other descriptors do not."""
23552374
return True
23562375

2376+
def supports_charge_spin(self) -> bool:
2377+
"""DPA4 accepts a frame-level ``charge_spin`` on ``call_graph`` (FiLM conditioning); other descriptors do not."""
2378+
return True
2379+
23572380
def disable_graph_lower(self) -> None:
23582381
"""Route this descriptor through the legacy dense lower."""
23592382
self._graph_lower_disabled = True

deepmd/dpmodel/model/make_model.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,16 @@ def call_common(
310310
The coordinates correction for virial.
311311
shape: nf x (nloc x 3)
312312
313+
charge_spin
314+
Frame-level charge/spin FiLM conditioning, ``(nf, 2)`` or
315+
``None``. Both the dense (nlist) and NeighborGraph lowers
316+
consume it (currently only DPA4/SeZM); the graph route no
317+
longer forces this model onto dense (former ``cs -> dense``
318+
gate removed) -- it threads through
319+
``_call_common_graph``/``call_lower_graph`` to the
320+
descriptor's ``call_graph``, gated per-descriptor by
321+
``supports_charge_spin``.
322+
313323
spin
314324
Per-local-atom spin, ``(nf, nloc, 3)``, or ``None``. Only the
315325
NeighborGraph lower consumes it (native magnetic conditioning,
@@ -380,10 +390,6 @@ def call_common(
380390
"pass one or the other"
381391
)
382392
graph_method = None
383-
# the graph lower does not consume charge_spin yet -> keep those
384-
# models on dense (a None check, so it stays jit/export-safe)
385-
if cs is not None:
386-
graph_method = None
387393
# model-level spin rides ONLY the NeighborGraph lower
388394
if sp is not None and graph_method is None:
389395
raise NotImplementedError(
@@ -402,6 +408,7 @@ def call_common(
402408
graph_method,
403409
do_atomic_virial,
404410
spin=sp,
411+
charge_spin=cs,
405412
)
406413
else:
407414
# legacy dense-nlist path (builds the extended quartet)
@@ -467,6 +474,7 @@ def _call_common_graph(
467474
method: str,
468475
do_atomic_virial: bool = False,
469476
spin: Array | None = None,
477+
charge_spin: Array | None = None,
470478
) -> dict[str, Array]:
471479
"""Carry-all graph forward (opt-in, Option B).
472480
@@ -495,6 +503,12 @@ def _call_common_graph(
495503
Per-local-atom spin, ``(nf, nloc, 3)``, or ``None``. Flattened
496504
to the flat node axis ``(N, 3)`` and forwarded unchanged to
497505
:meth:`call_lower_graph`.
506+
charge_spin
507+
Frame-level charge/spin conditioning, ``(nf, 2)`` or ``None``.
508+
Unflattened (per-frame, not per-node) and forwarded unchanged
509+
to :meth:`call_lower_graph`, whose ``n_node`` here is always
510+
the rectangular ``full(nf, nloc)`` this method builds -- the
511+
one shape the descriptor's per-frame FiLM division requires.
498512
499513
Returns
500514
-------
@@ -549,6 +563,7 @@ def _call_common_graph(
549563
else None
550564
),
551565
spin=(xp.reshape(spin, (nf * nloc, 3)) if spin is not None else None),
566+
charge_spin=charge_spin,
552567
)
553568
# Public ABI is rectangular (nf, nloc, *); the lower is flat
554569
# (N=nf*nloc, *). Unravel per-atom keys here at the boundary.

deepmd/pt_expt/model/make_model.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,8 +584,11 @@ def forward_common_lower_graph(
584584
multi-rank graphs the ghost rows are included; their values
585585
are inert under the owned-node mask).
586586
charge_spin
587-
charge/spin conditioning. Ignored in PR-A; accepted for ABI
588-
stability with charge/spin-conditioned descriptors.
587+
Frame-level charge/spin FiLM conditioning, ``(nf, 2)`` or
588+
``None``, forwarded to the atomic model's
589+
``forward_common_atomic_graph`` (and, from there, the
590+
descriptor's ``call_graph`` for descriptors that declare
591+
``supports_charge_spin``; currently DPA4 only).
589592
spin
590593
Per-node native spin, flat ``(N, 3)``, or ``None``. When given,
591594
a SECOND autograd leaf is created next to ``edge_vec`` and
@@ -738,6 +741,7 @@ def _call_common_graph(
738741
method: str,
739742
do_atomic_virial: bool = False,
740743
spin: torch.Tensor | None = None,
744+
charge_spin: torch.Tensor | None = None,
741745
) -> dict[str, torch.Tensor]:
742746
"""Carry-all graph forward with autograd force/virial (pt_expt override).
743747
@@ -767,6 +771,10 @@ def _call_common_graph(
767771
Flattened to ``(N, 3)`` and forwarded into
768772
:meth:`forward_common_lower_graph`, completing the seam
769773
``call_common`` (dpmodel, shared) opens for the graph route.
774+
charge_spin
775+
Frame-level charge/spin FiLM conditioning, ``(nf, 2)`` or
776+
``None``. Unflattened (per-frame) and forwarded unchanged into
777+
:meth:`forward_common_lower_graph`.
770778
771779
Returns
772780
-------
@@ -818,6 +826,7 @@ def _call_common_graph(
818826
fparam=fp,
819827
aparam=ap_flat,
820828
spin=spin_flat,
829+
charge_spin=charge_spin,
821830
)
822831
# ``forward_common_lower_graph`` returns flat ``(N, *)`` per-atom
823832
# outputs (N = nf * nloc for a carry-all rectangular graph).

0 commit comments

Comments
 (0)