Skip to content

Commit e069d2d

Browse files
author
Han Wang
committed
test(pt_expt): graph attention make_fx (merge gate) + model force/virial parity
- test_make_fx_graph_attn: graph forward + autograd.grad at attn_layer=2 traces under make_fx for BOTH smooth branches (the shape-static center_edge_pairs form is nonzero-free) — required since pt_expt compiled training routes eligible models through the graph lower. - model-level graph-vs-legacy lower parity now parametrized over attn_layer {0, 2} (energy/force/virial/atom_virial, 1e-12 CPU). - eligibility pins: attention+concat is graph-eligible; se_atten_v2 (tebd_input_mode='strip') correctly stays dense (strip = later PR; the plan's 'se_atten_v2 inherits for free' did not hold).
1 parent cfb0cb8 commit e069d2d

3 files changed

Lines changed: 87 additions & 4 deletions

File tree

source/tests/common/dpmodel/test_dpa1_graph_attention_parity.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,20 @@ def test_torch_block_matches_numpy(self) -> None:
208208
static_nnei=nlist.shape[2],
209209
)
210210
np.testing.assert_allclose(out.numpy(), ref, rtol=1e-12, atol=1e-12)
211+
212+
213+
class TestGraphEligibility:
214+
def test_attention_concat_is_graph_eligible(self) -> None:
215+
assert _make(2).uses_graph_lower()
216+
217+
def test_strip_mode_stays_dense(self) -> None:
218+
"""se_atten_v2 (tebd_input_mode='strip') is NOT graph-eligible yet:
219+
strip-mode graph support is a later PR; it must keep the dense route
220+
(the PR-D plan's 'se_atten_v2 inherits for free' did not hold).
221+
"""
222+
from deepmd.dpmodel.descriptor.se_atten_v2 import (
223+
DescrptSeAttenV2,
224+
)
225+
226+
dd = DescrptSeAttenV2(rcut=4.0, rcut_smth=0.5, sel=[20], ntypes=2, attn_layer=2)
227+
assert not dd.uses_graph_lower()

source/tests/pt_expt/descriptor/test_dpa1.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,65 @@ def fn(coord_ext, atype_ext, nlist, mapping):
311311
atol=atol,
312312
)
313313

314+
@pytest.mark.parametrize("smooth", [False, True]) # smooth attention branch
315+
@pytest.mark.parametrize("prec", ["float64"]) # precision
316+
def test_make_fx_graph_attn(self, prec, smooth) -> None:
317+
"""make_fx (export-readiness) of the GRAPH forward with attention.
318+
319+
MERGE BLOCKER (NeighborGraph PR-D): pt_expt compiled training routes
320+
eligible models through the graph lower by default, so graph attention
321+
(``attn_layer > 0``) must be fx-traceable — the shape-static
322+
``center_edge_pairs`` form keeps the pair enumeration ``nonzero``-free.
323+
Covers both the smooth and non-smooth attention branches.
324+
"""
325+
rng = np.random.default_rng(GLOBAL_SEED)
326+
_, _, nnei = self.nlist.shape
327+
davg = rng.normal(size=(self.nt, nnei, 4))
328+
dstd = 0.1 + np.abs(rng.normal(size=(self.nt, nnei, 4)))
329+
330+
dtype = PRECISION_DICT[prec]
331+
rtol, atol = get_tols(prec)
332+
dd0 = DescrptDPA1(
333+
self.rcut,
334+
self.rcut_smth,
335+
self.sel_mix,
336+
self.nt,
337+
attn_layer=2,
338+
attn_dotr=True,
339+
smooth_type_embedding=smooth,
340+
precision=prec,
341+
seed=GLOBAL_SEED,
342+
).to(self.device)
343+
dd0.se_atten.mean = torch.tensor(davg, dtype=dtype, device=self.device)
344+
dd0.se_atten.stddev = torch.tensor(dstd, dtype=dtype, device=self.device)
345+
dd0 = dd0.eval()
346+
coord_ext = torch.tensor(self.coord_ext, dtype=dtype, device=self.device)
347+
atype_ext = torch.tensor(self.atype_ext, dtype=int, device=self.device)
348+
nlist = torch.tensor(self.nlist, dtype=int, device=self.device)
349+
mapping = torch.tensor(self.mapping, dtype=int, device=self.device)
350+
351+
def fn(coord_ext, atype_ext, nlist, mapping):
352+
coord_ext = coord_ext.detach().requires_grad_(True)
353+
rd = dd0(coord_ext, atype_ext, nlist, mapping)[0]
354+
grad = torch.autograd.grad(rd.sum(), coord_ext, create_graph=False)[0]
355+
return rd, grad
356+
357+
rd_eager, grad_eager = fn(coord_ext, atype_ext, nlist, mapping)
358+
traced = make_fx(fn)(coord_ext, atype_ext, nlist, mapping)
359+
rd_traced, grad_traced = traced(coord_ext, atype_ext, nlist, mapping)
360+
np.testing.assert_allclose(
361+
rd_eager.detach().cpu().numpy(),
362+
rd_traced.detach().cpu().numpy(),
363+
rtol=rtol,
364+
atol=atol,
365+
)
366+
np.testing.assert_allclose(
367+
grad_eager.detach().cpu().numpy(),
368+
grad_traced.detach().cpu().numpy(),
369+
rtol=rtol,
370+
atol=atol,
371+
)
372+
314373
@pytest.mark.parametrize("shared_level", [0, 1]) # sharing level
315374
def test_share_params(self, shared_level) -> None:
316375
"""share_params level 0: share all; level 1: share type_embedding only."""

source/tests/pt_expt/model/test_dpa1_graph_lower.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def setup_method(self) -> None:
9191
[[0, 0, 0, 1, 1]], dtype=torch.int64, device=self.device
9292
)
9393

94-
def _make_model(self) -> EnergyModel:
94+
def _make_model(self, attn_layer: int = 0, smooth: bool = False) -> EnergyModel:
9595
ds = DescrptDPA1(
9696
self.rcut,
9797
self.rcut_smth,
@@ -100,9 +100,13 @@ def _make_model(self) -> EnergyModel:
100100
neuron=[3, 6],
101101
axis_neuron=2,
102102
attn=4,
103-
attn_layer=0, # graph lower only supports attn_layer == 0
103+
attn_layer=attn_layer,
104104
attn_dotr=True,
105105
attn_mask=False,
106+
# smooth attention keeps sel-padding in the dense softmax
107+
# denominator; the carry-all graph drops it BY DESIGN (PR-D), so
108+
# exact graph-vs-dense parity requires smooth=False here.
109+
smooth_type_embedding=smooth,
106110
activation_function="tanh",
107111
set_davg_zero=False,
108112
type_one_side=True,
@@ -165,13 +169,16 @@ def _prepare_lower_inputs(self, periodic: bool):
165169
mapping_t = torch.tensor(mapping, dtype=torch.int64, device=self.device)
166170
return ext_coord, ext_atype, nlist_t, mapping_t
167171

172+
@pytest.mark.parametrize("attn_layer", [0, 2]) # factorizable AND attention
168173
@pytest.mark.parametrize("periodic", [True, False]) # PBC vs non-PBC
169174
@pytest.mark.parametrize("do_av", [False, True]) # atom-virial off / on
170-
def test_force_virial_parity_vs_legacy(self, periodic, do_av) -> None:
175+
def test_force_virial_parity_vs_legacy(self, periodic, do_av, attn_layer) -> None:
171176
"""Graph lower energy/force/virial/atom_virial == legacy dense lower on
172177
the SAME neighbor set (regime-1 graph from from_dense_quartet).
178+
attn_layer=2 exercises graph attention through model-level autograd
179+
(smooth=False: exact carry-all parity regime, NeighborGraph PR-D).
173180
"""
174-
model = self._make_model()
181+
model = self._make_model(attn_layer=attn_layer)
175182
model.eval()
176183
tol = (
177184
{"rtol": 1e-12, "atol": 1e-12}

0 commit comments

Comments
 (0)